# Copyright (c) 2023 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details. # frozen_string_literal: true require 'contrast/components/logger' require 'contrast/agent/reporting/settings/exclusion_base' require 'contrast/agent/reporting/settings/input_exclusion' require 'contrast/agent/reporting/settings/url_exclusion' module Contrast module Agent # Exclusions are ways for the User to tell the Agent to ignore sections of # the Application. If a request or an event matches one of these, the # functions of the Agent are suppressed for that request or event. class ExclusionMatcher include Contrast::Components::Logger::InstanceMethods extend Forwardable attr_reader :protect, :assess, :exclusion_type, :wildcard_url, :wildcard_input def_delegators :@exclusion, :protect_rules, :assess_rules, :type, :name, :input_name, :urls, :match_strategy MATCH_ALL = 'ALL'.cs__freeze # Create a matcher around an exclusion sent from TeamServer. # # @param excl [Contrast::Agent::Reporting::Settings::ExclusionBase] # @return [Contrast::Agent::ExclusionMatcher] def initialize excl @exclusion = excl @protect = @exclusion.protect @assess = @exclusion.assess case excl when Contrast::Agent::Reporting::Settings::InputExclusion handle_wildcard_input @exclusion_type = :INPUT when Contrast::Agent::Reporting::Settings::UrlExclusion handle_wildcard_url @exclusion_type = :URL end end # According to the docs for exclusions, user input applies to all inputs if # the name supplied is an '*' or '.*'. The name matcher does NOT support # regexp beyond this. # https://docs.contrastsecurity.com/admin-policymgmt.html#exclude def handle_wildcard_input return unless @exclusion.name # rubocop:disable Security/Module/Name @wildcard_input = @exclusion.name == '.*' || @exclusion.name == Contrast::Utils::ObjectShare::ASTERISK # rubocop:disable Security/Module/Name end # According to the docs for exclusions, urls apply to all urls if the url # supplied is '/.*' or if the URL mode is all. Otherwise, the URL supplied is # to be treated as a regular expression that must match the entire URL # against which it is tested. # https://docs.contrastsecurity.com/admin-policymgmt.html#exclude def handle_wildcard_url @wildcard_url = match_all? return if @wildcard_url return unless @exclusion.urls&.any? @wildcard_url ||= @exclusion.urls.any?('/.*') return if @wildcard_url @urls = [] @exclusion.urls.each do |url| url_pattern = build_regexp(url, start_anchor: true, end_anchor: true) @urls << url_pattern if url_pattern end end def build_regexp pattern, start_anchor: false, end_anchor: false pattern = Contrast::Utils::ObjectShare::CARROT + pattern if start_anchor pattern += Contrast::Utils::ObjectShare::DOLLAR_SIGN if end_anchor Regexp.compile(pattern) rescue RegexpError => e logger.error('Unable to generate a pattern for exclusion matching.', e, pattern: pattern) end def protect? @protect end def assess? @assess end def match_all? (@exclusion.urls.nil? || @exclusion.urls.empty?) && @exclusion.match_strategy == MATCH_ALL end # Determine if the given rule is excluded by this exclusion. # In this case, the `protect_rules` being empty means apply to all rules, # not no rules # # @param rule - the id of the rule which we're checking for exclusion def protection_rule? rule protect? && (@exclusion.protect_rules.empty? || @exclusion.protect_rules.include?(rule)) end # Determine if the given rule is excluded by this exclusion. # In this case, the `assessment_rules` being empty means apply to all rules, # not no rules # # @param rule - the id of the rule which we're checking for exclusion def assess_rule? rule assess? && (@exclusion.assess_rules.empty? || @exclusion.assess_rules.include?(rule)) end end end end