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

require 'contrast/utils/object_share'
require 'contrast/agent/reporting/settings/protect_rule'
require 'contrast/agent/reporting/settings/virtual_patch'

module Contrast
  module Agent
    module Reporting
      # This module will hold all the settings from the TS responce
      module Settings
        # Application level settings for the Protect featureset
        class Protect
          # modes set by NG endpoints; block at perimeter needs to be check against the blockAtEntry boolean value
          NG_PROTECT_RULES_MODE = %w[OFF MONITORING BLOCKING].cs__freeze
          ACTIVE_PROTECT_RULES_LIST = %w[
            bot-blocker cmd-injection cmd-injection-command-backdoors cmd-injection-semantic-chained-commands
            cmd-injection-semantic-dangerous-paths untrusted-deserialization nosql-injection path-traversal
            path-traversal-semantic-file-security-bypass sql-injection sql-injection-semantic-dangerous-functions
            unsafe-file-upload reflected-xss xxe
          ].cs__freeze

          # The settings for each protect rule for this application
          #
          # @return protection_rules [Array<protectRule>] protectRule: {
          #   blockAtEntry [Boolean] If in block mode, to block at perimeter or not.
          #   id           [String] The id of a rule in Contrast.
          #   mode         [String] The mode that this rule should run in. [OFF, MONITORING, BLOCKING] }
          def protection_rules
            @_protection_rules ||= []
          end

          # @return [Hash<String,Contrast::Agent::Reporting::Settings::ProtectRule>] map of rule, by id, to
          #   configuration
          def rule_settings
            @_rule_settings ||= {}
          end

          # Set the protection_rules array
          #
          # @param protection_rules [Array<protectRule>] protectRule: {
          #   blockAtEntry [Boolean] If in block mode, to block at perimeter or not.
          #   id           [String] The id of a rule in Contrast.
          #   mode         [String] The mode that this rule should run in. [OFF, MONITORING, BLOCKING] }
          # @return protection_rules [Array<protectRule>] protectRule: {
          #   blockAtEntry [Boolean] If in block mode, to block at perimeter or not.
          #   id           [String] The id of a rule in Contrast.
          #   mode         [String] The mode that this rule should run in. [OFF, MONITORING, BLOCKING] }
          def protection_rules= protection_rules
            @_protection_rules = protection_rules if protection_rules.is_a?(Array)
          end

          # The virtual patches to apply for this application
          #
          # @return [Array<Contrast::Agent::Reporting::Settings::VirtualPatch>]
          def virtual_patches
            @_virtual_patches ||= []
          end

          # Set the virtual patches array
          #
          # @param virtual_patches [Array<VirtualPatch>] Array of VirtualPatch: {
          #   name       [String] The name of the Virtual Patch
          #   headers    [Array] The headers that must be present in the request to result in the request being blocked
          #   parameters [Array] The parameters that must be present in the request
          #                       to result in the request being blocked.
          #   urls       [Array] The urls that must be present in the request to result in the request being blocked.
          #   uuids      [String] The UUID of the Virtual Patch }
          # @return virtual_patches [Array<VirtualPatch>] Array of VirtualPatch: {
          #   name       [String] The name of the Virtual Patch
          #   headers    [Array] The headers that must be present in the request to result in the request being blocked
          #   parameters [Array] The parameters that must be present in the request
          #                       to result in the request being blocked.
          #   urls       [Array] The urls that must be present in the request to result in the request being blocked.
          #   uuids      [String] The UUID of the Virtual Patch }
          def virtual_patches= virtual_patches
            @_virtual_patches = virtual_patches if virtual_patches.is_a?(Array)
          end

          # Converts settings into Agent Settings understandable hash {RULE_ID => MODE}
          #
          # @return rules [Hash<RULE_ID => MODE>, nil] Hash with rule_id as key and mode as value
          def protection_rules_to_settings_hash
            return {} if protection_rules.empty?

            modes_by_id = {}
            protection_rules.each do |rule|
              setting_mode = rule[:mode] || rule['mode']
              # BlockAtEnrtry is only available for the protection_rules Array.
              # It is used in both ng and non ng payloads. If the array is empty
              # this method will short circuit at the very first line and return
              # empty hash. this means that the #rules_settings_to_settings_hash
              # will be used next to extract the settings.
              bap = rule[:blockAtEntry] || rule['blockAtEntry']
              api_mode = assign_mode(setting_mode, block_at_entry: !!bap == bap)

              id = rule[:id] || rule['id']
              modes_by_id[id] = api_mode
            end
            modes_by_id
          end

          # Converts settings into Agent Settings understandable hash {RULE_ID => MODE}
          # Takes Hash<String, Contrast::Agent::Reporting::Settings::ProtectRule> and converts it
          # to Hash<RULE_ID => MODE>
          #
          # @return rules [Hash<RULE_ID => MODE>, nil] Hash with rule_id as key and mode as value
          def rules_settings_to_settings_hash
            return {} if rule_settings.empty?

            modes_by_id = {}
            rule_settings.each do |rule_id, rule_mode|
              next unless active_defend_rules.include?(rule_id.to_s)

              modes_by_id[rule_id.to_s] = assign_mode(rule_mode.mode)
            end
            modes_by_id
          end

          # Returns list of actively used protection rules to be updated, or default list.
          # This will be used to query the received settings for the ones used by the Agent.
          def active_defend_rules
            return ACTIVE_PROTECT_RULES_LIST unless defined?(Contrast::PROTECT)

            current_rules = Contrast::PROTECT.defend_rules.keys
            return current_rules unless current_rules.empty?

            ACTIVE_PROTECT_RULES_LIST
          end

          private

          # Assigns the received settings mode to be used as actual config.
          # @param setting_mode []
          def assign_mode setting_mode, block_at_entry: false
            # modes set by newer settings endpoints are of [OFF MONITOR BLOCK BLOCK_AT_PERIMETER] and
            # can just be cast to symbols
            return setting_mode.to_sym unless NG_PROTECT_RULES_MODE.include?(setting_mode)

            case setting_mode
            when NG_PROTECT_RULES_MODE[1]
              :MONITOR
            when NG_PROTECT_RULES_MODE[2]
              block_at_entry ? :BLOCK_AT_PERIMETER : :BLOCK
            else
              :NO_ACTION
            end
          end
        end
      end
    end
  end
end