# 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 passwords hardcoded into the sourcecode # of the application. A constant is a password if: # 1) the name contains a PASSWORD_FIELD_NAMES value # 2) the name does not contain a NON_PASSWORD_PARTIAL_NAMES value # 3) the value is a String # 4) the value is not solely alphanumeric and '.' or '_' * note that # mixing the characters counts as a violation of this rule class HardcodedPassword include Contrast::Agent::Assess::Rule::Provider::HardcodedValueRule NAME = 'hardcoded-password' 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. PASSWORD_FIELD_NAMES = %w[PASSWORD PASSKEY PASSPHRASE SECRET].cs__freeze # These are markers whose presence indicates that a field is more # likely to be a descriptor or requirement than an actual password. # We should ignore fields that contain them. NON_PASSWORD_PARTIAL_NAMES = %w[ DATE FORGOT FORM ENCODE PATTERN PREFIX PROP SUFFIX URL BASE FILE URI ].cs__freeze # If the constant looks like a password and it doesn't look like a # password descriptor, it passes for this rule def name_passes? constant_string PASSWORD_FIELD_NAMES.any? { |name| constant_string.index(name) } && NON_PASSWORD_PARTIAL_NAMES.none? { |name| constant_string.index(name) } end # 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) return false unless value_node.type == :STR # https://www.rubydoc.info/gems/ruby-internal/Node/STR string = value_node.children[0] !probably_property_name?(string) end # If a field name matches an expected password field, we'll check it's # value to see if it looks like a placeholder. For our purposes, # placeholders will be any non-empty String conforming to the patterns # below. We do combine the patterns with [\._] as in Ruby these two # characters are probably more likely to appear together in a # default placeholder than in a password. Note this is opposite of # the behavior in Java PROPERTY_NAME_PATTERN = /^[a-z]+[._][._a-z]*[a-z]+$/.cs__freeze def probably_property_name? value value.match?(PROPERTY_NAME_PATTERN) end REDACTED_MARKER = ' = "**REDACTED**"' def redacted_marker REDACTED_MARKER end # TODO: RUBY-1014 remove `#value_type_passes?` and `#value_passes?` # If the value is a string, it passes for this rule def value_type_passes? value value.is_a?(String) end # If the value probably isn't a property name, it passes for this # rule def value_passes? value !probably_property_name?(value) end end end end end end end