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

require 'contrast/agent/assess/rule/response/header_rule'
require 'contrast/utils/string_utils'

module Contrast
  module Agent
    module Assess
      module Rule
        module Response
          # These rules check that the HTTP Headers include CSP header types
          class CspHeaderInsecure < HeaderRule
            HEADER_KEYS = %w[Content-Security-Policy X-Content-Security-Policy X-Webkit-CSP].cs__freeze
            DEFAULT_SAFE = false
            SETTINGS = %w[
              base-uri child-src default-src connect-src frame-src media-src object-src script-src
              style-src form-action frame-ancestors plugin-types reflected-xss referer
            ].cs__freeze
            UNSAFE_VALUE_REGEXP = /^unsafe-(?:inline|eval)$/.cs__freeze
            ASTERISK_REGEXP = /[*]/.cs__freeze
            SAFE_REFLECTED_XSS = /1/.cs__freeze

            def rule_id
              'csp-header-insecure'
            end

            protected

            # 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 [Contrast::Utils::ObjectShare::EMPTY_STRING, nil] if CSP Header is not found
            def violated? response
              settings = {}
              csp_hash = get_header_value(response)
              return if csp_hash.nil?

              SETTINGS.each do |setting_attr|
                # default src has to be checked all other keys may be missing
                next unless csp_hash.key?(setting_attr) || setting_attr == 'default-src'

                value = csp_hash[setting_attr]
                key = convert_key(setting_attr)
                settings["#{ key }Secure"] = !value.nil? && value_secure?(value) && value_safe?(value)
                settings["#{ key }Value"] = value.nil? ? Contrast::Utils::ObjectShare::EMPTY_STRING : value
              end
              evidence(settings) if settings.value?(false)
            end

            # Get the CSP values from and transforms them to key value hash
            #
            # ex default-src 'self' *.test.com; img-src * becomes:
            # { default-src: "'self' *.test.com", img-src: "*" }
            #
            # @param headers [Hash] the response of the application
            # @return [Array, nil] array of CSP header values
            def get_header_value response
              csp_hash = {}
              headers = response.headers
              HEADER_KEYS.each do |header_key|
                next unless headers[header_key]&.length&.positive?

                values = headers[header_key].split(Contrast::Utils::ObjectShare::SEMICOLON)
                values.each do |value|
                  normalized = value.downcase.strip
                  kv = normalized.split(Contrast::Utils::ObjectShare::SPACE, 2)
                  csp_hash[kv[0]] = kv[1]
                end
              end
              csp_hash
            end

            def value_secure? value
              ASTERISK_REGEXP.match(value).nil?
            end

            def value_safe? value
              UNSAFE_VALUE_REGEXP.match(value).nil? || !SAFE_REFLECTED_XSS.match(value).nil?
            end

            # Converts the CSP key to camelcase to be used as key for evidence object
            #
            #  base-uri -> baseUri
            #
            # @param key [String] key as found in header
            # @return [String] camelcase key
            def convert_key key
              return key unless key.include?('-')

              str = key.split('-')
              "#{ str[0] }#{ str[1].capitalize }"
            end
          end
        end
      end
    end
  end
end