# Copyright (c) 2022 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details.
# frozen_string_literal: true

require 'rack'
require 'json'
require 'contrast/agent/reporting/reporting_utilities/dtm_message'
require 'contrast/agent/reporting/reporting_utilities/build_preflight'
require 'contrast/utils/hash_digest'
require 'contrast/utils/preflight_util'
require 'contrast/utils/string_utils'

module Contrast
  module Agent
    module Assess
      module Rule
        module Response
          # These rules check the content of the HTTP Response to determine if something was set incorrectly or
          # insecurely in it.
          class BaseRule
            DATA = 'data'.cs__freeze
            # Analyze a given application response to determine if it violates the rule
            #
            # TODO: RUBY-999999 either extract the response's body or memoize it to some degree so that it's not
            #   generated on every call of this method
            # @param response [Contrast::Agent::Response] the response of the application
            def analyze response
              return unless analyze_response?(response)

              violation = violated?(response)
              return unless violation

              finding = build_finding(violation)
              return unless finding

              if Contrast::Agent::Reporter.enabled?
                report = Contrast::Agent::Reporting::DtmMessage.dtm_to_event(finding)
                if report.is_a?(Contrast::Agent::Reporting::Finding)
                  request = Contrast::Agent::REQUEST_TRACKER.current&.request
                  preflight = Contrast::Agent::Reporting::BuildPreflight.build(report, request)
                  Contrast::Agent.reporter&.send_event(preflight)
                else
                  Contrast::Agent.reporter&.send_event(report)
                end
              else
                Contrast::Agent::REQUEST_TRACKER.current.activity.findings << finding
              end
            end

            protected

            # Rules discern which responses they can/should analyze.
            #
            # @param response [Contrast::Agent::Response] the response of the application
            def analyze_response? response
              return false unless response
              return false if disabled?
              return false unless Contrast::Agent::REQUEST_TRACKER.current&.analyze_response_assess?
              return false unless valid_response_code?(response.response_code)
              return false unless valid_content_type?(response.content_type)

              true
            end

            # Determine if the Response violates the Rule or not. If it does, return the evidence that proves it so.
            #
            # @param _response [Contrast::Agent::Response] the response of the application
            # @return [Hash, nil] the evidence required to prove the violation of the rule
            def violated? _response; end

            def evidence data = Contrast::Utils::ObjectShare::EMPTY_STRING
              data = Contrast::Utils::ObjectShare::EMPTY_STRING if data.nil?
              { DATA => data }
            end

            # Convert the given evidence into a finding. The rule will populate this evidence with each of the
            #
            # @param evidence [Hash] the properties required to build this finding.
            # @return [Contrast::Api::Dtm::Finding]
            def build_finding evidence
              finding = Contrast::Api::Dtm::Finding.new
              finding.rule_id = rule_id
              context = Contrast::Agent::REQUEST_TRACKER.current
              finding.routes << context.route if context&.route
              build_evidence(evidence, finding)
              hash = Contrast::Utils::HashDigest.generate_config_hash(finding)
              finding.hash_code = Contrast::Utils::StringUtils.force_utf8(hash)
              finding.preflight = Contrast::Utils::PreflightUtil.create_preflight(finding)
              finding
            end

            # This method allows to change the evidence we attach and the way we attach it
            # Change it accordingly the rule you work on
            #
            # @param evidence [Hash] the properties required to build this finding.
            # @param finding [Contrast::Api::Dtm::Finding] finding to attach the evidence to
            def build_evidence evidence, finding
              evidence.each_pair do |key, value|
                finding.properties[key] = if value.cs__is_a?(Hash)
                                            Contrast::Utils::StringUtils.protobuf_format(value.to_json)
                                          elsif value.cs__is_a?(Array)
                                            value.map { Contrast::Utils::StringUtils.protobuf_format(_1) }.to_s
                                          else
                                            Contrast::Utils::StringUtils.protobuf_format(value)
                                          end
              end
            end

            # A rule is disabled if assess is off or it is turned off by TeamServer or by configuration.
            #
            # @return [Boolean]
            def disabled?
              !::Contrast::ASSESS.enabled? || ::Contrast::ASSESS.rule_disabled?(rule_id)
            end

            # Rules discern which response codes to which they apply. If the response is of one that the rule should not
            # examine, it can short circuit early. If the code is unknown, it must be examined.
            #
            # @param code [Integer,nil] the response code
            # @return [Boolean]
            def valid_response_code? code
              !code || [301, 302, 307, 404, 410, 500].none?(code)
            end

            # Rules discern which Content-Type to which they apply. If the response is of one that the rule should not
            # examine, it can short circuit early. If the type is unknown, it must be examined.
            #
            # @param type [String,nil] the value of the Content-Type header
            # @return [Boolean]
            def valid_content_type? type
              !type || [/json/i, /xml/i].none? { |invalid_content| type =~ invalid_content }
            end
          end
        end
      end
    end
  end
end