# Copyright (c) 2021 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details. # frozen_string_literal: true module Contrast module Agent module Assess module Rule module Provider # Determine if there are any cryptographic keys hardcoded into the sourcecode # of the application. A constant is a cryptographic key if: # 1) the name contains a KEY_FIELD_NAME value # 2) the value is a non-empty array of only Fixnums class HardcodedKey include Contrast::Agent::Assess::Rule::Provider::HardcodedValueRule NAME = 'hardcoded-key' def rule_id NAME end # These are names, determined by the security team (Matt & Ar), that # indicate a field is likely to be a password or secret token of some # sort. KEY_FIELD_NAMES = %w[KEY AES DES IV SECRET].cs__freeze # These are markers whose presence indicates that a field is more # likely to be a descriptor or requirement than an actual key. # We should ignore fields that contain them. NON_KEY_PARTIAL_NAMES = %w[CONTENT_CODES RESPONSE_CODES ERROR_CODES].cs__freeze def name_passes? constant_string KEY_FIELD_NAMES.any? { |name| constant_string.index(name) } && NON_KEY_PARTIAL_NAMES.none? { |name| constant_string.index(name) } end BYTE_HOLDERS = %i[ARRAY LIST].cs__freeze # Determine if the given value node violates the hardcode key rule # @param value_node [RubyVM::AbstractSyntaxTree::Node] the node to # evaluate # @return [Boolean] def value_node_passes? value_node # If it's a freeze call, then evaluate the entity being frozen value_node = value_node.children[0] if freeze_call?(value_node) # If it's a String being turned into bytes, then it matches key # expectations return true if bytes_call?(value_node) type = value_node.type return false unless BYTE_HOLDERS.include?(type) return false unless value_node.children.any? # Unless this is an array of literal numerics, we don't match. # That array seems to always end in a nil value, so we allow # those as well. value_node.children.each do |child| next unless child unless child.cs__is_a?(RubyVM::AbstractSyntaxTree::Node) && child.type == :LIT && child.children[0]&.cs__is_a?(Integer) return false end end true end REDACTED_MARKER = ' = [**REDACTED**]' def redacted_marker REDACTED_MARKER end # A node is a bytes_call if it's the Node for String#bytes. We care # about this specifically as it's likely to be a common way to # generate a key constant, rather than directly declaring an # integer array. # # @param value_node [RubyVM::AbstractSyntaxTree::Node] the node to # evaluate # @return [Boolean] is this a node for String#bytes or not def bytes_call? value_node return false unless value_node.type == :CALL children = value_node.children return false unless children return false unless children.length >= 2 potential_string_node = children[0] unless potential_string_node.cs__is_a?(RubyVM::AbstractSyntaxTree::Node) && potential_string_node.type == :STR return false end children[1] == :bytes end # TODO: RUBY-1014 remove `#value_type_passes?` and `#value_passes?` # If the value is a byte array, or at least an array of numbers, it # passes for this rule def value_type_passes? value return false unless value.is_a?(Array) && value.any? value.each do |byte| return false unless byte.is_a?(Integer) end true end # There isn't a filter for the byte value. The check is not evaluated # for this rule def value_passes? _value true end end end end end end end