# Copyright (c) 2022 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
          end
        end
      end
    end
  end
end