# Copyright (c) 2022 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details. # frozen_string_literal: true require 'contrast/components/logger' 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 # Create a matcher around an exclusion sent from TeamServer. # # @param excl [Contrast::Api::Settings::Exclusion] # @return [Contrast::Agent::ExclusionMatcher] def initialize excl @exclusion = excl @protect = @exclusion.protect @assess = @exclusion.assess handle_wildcard_input handle_wildcard_url handle_wildcard_code 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.input_name @wildcard_input = @exclusion.input_name == '.*' || @exclusion.input_name == Contrast::Utils::ObjectShare::ASTERISK 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 # According to the docs for exclusions, code applies to the entire stacktrace # of the caller, and can act as a regexp. Per our user instructions in the # Contrast UI, these comparisons must be done at the end of the input. # https://docs.contrastsecurity.com/admin-policymgmt.html#exclude def handle_wildcard_code return unless @exclusion.denylist&.any? @wildcard_exclusions = [] @exclusion.denylist.each do |code| class_name, method_name = code.split(Contrast::Utils::ObjectShare::COLON) class_pattern = build_regexp(class_name, start_anchor: false, end_anchor: true) method_pattern = build_regexp(method_name) next unless class_pattern && method_pattern @wildcard_exclusions << [class_pattern, method_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 code? @exclusion.type == Contrast::Api::Settings::Exclusion::ExclusionType::CODE end def match_all? @exclusion.urls.nil? || @exclusion.urls.empty? end # Determine if the given rule is excluded by this exclusion. # In this case, the `protection_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.protection_rules.empty? || @exclusion.protection_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.assessment_rules.empty? || @exclusion.assessment_rules.include?(rule)) end def match_code? stack_trace return false unless code? return false if @wildcard_exclusions&.empty? @wildcard_exclusions.each do |code| class_name = code[0] method_name = code[1] stack_trace.each do |location| next unless location.base_label.match?(method_name) next unless location.path.match?(class_name) return true end end false end end end end