# Copyright (c) 2023 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details. # frozen_string_literal: true require 'contrast/agent/protect/rule/base' require 'contrast/components/logger' module Contrast module Agent module Protect module Rule # Encapsulate common code for protect rules that do their input analysis on agent-lib rather in ruby code class BaseService < Contrast::Agent::Protect::Rule::Base include Contrast::Components::Logger::InstanceMethods def rule_name 'base-service' end def block_message 'Contrast Security Protect Rule Triggered. Response blocked.' end def prefilter context return unless prefilter?(context) ia_results = gather_ia_results(context) ia_results.each do |ia_result| result = build_attack_result(context) build_attack_without_match(context, ia_result, result) append_to_activity(context, result) cef_logging(result, :successful_attack) raise(Contrast::SecurityException.new(self, block_message)) if blocked? end end # @param context [Contrast::Agent::RequestContext] # @return [Boolean] def infilter? context return false unless enabled? return false unless (results = gather_ia_results(context)) && results.any? return false if protect_excluded_by_url?(rule_name, context.request.path) return false if protect_excluded_by_input?(results, context.request.path) true end # Base method for prefilter check. Extend if rule needs more # specific conditioning. # # @param context [Contrast::Agent::RequestContext] # @return [Boolean] def prefilter? context return false unless context return false unless enabled? return false unless (results = gather_ia_results(context)) && results.any? return false if protect_excluded_by_url?(rule_name, context.request.path) return false if protect_excluded_by_input?(results, context.request.path) true end # Override for rules that need the response # Currently postfilter can be applied to streamed responses, if any logic within postfilter changes to modify # the response streamed responses will break # @param context [Contrast::Agent::RequestContext] # @raise [Contrast::SecurityException] def postfilter context return unless enabled? && POSTFILTER_MODES.include?(mode) return false if protect_excluded_by_url?(rule_name, context.request.path) return if protect_excluded_by_input?(gather_ia_results(context), context.request.path) return if mode == :NO_ACTION || mode == :PERMIT result = find_postfilter_attacker(context, nil) return unless result&.samples&.any? cef_logging(result) append_to_activity(context, result) return unless result.response == :BLOCKED raise(Contrast::SecurityException.new(self, "#{ rule_name } triggered in postfilter. Response blocked.")) end protected # Used to build and report semantic rules. # # @param context [Contrast::Agent::RequestContext] current request contest # @param potential_attack_string [String] def build_violation context, potential_attack_string result = build_attack_result(context) update_successful_attack_response(context, nil, result, potential_attack_string) return unless result append_sample(context, nil, result, potential_attack_string) cef_logging(result, :successful_attack) result end # Check to if result is blocked. Used for raise check. # # @param result [Contrast::Agent::Reporting::AttackResult] def blocked_violation? result return false unless result result.response == Contrast::Agent::Reporting::ResponseType::BLOCKED end # @param context [Contrast::Agent::RequestContext] # @return [Array] def gather_ia_results context return Contrast::Utils::ObjectShare::EMPTY_ARRAY unless context&.agent_input_analysis&.results context.agent_input_analysis.results.select do |ia_result| ia_result.rule_id == rule_name && ia_result.score_level != Contrast::Agent::Reporting::ScoreLevel::IGNORE end end def build_attack_result _context result = Contrast::Agent::Reporting::AttackResult.new result.rule_id = rule_name result end # @param context [Contrast::Agent::RequestContext] # @param potential_attack_string [String, nil] # @param **kwargs # @return [Contrast::Agent::Reporting] def find_attacker context, potential_attack_string, **kwargs ia_results = gather_ia_results(context) find_attacker_with_results(context, potential_attack_string, ia_results, **kwargs) end # Allows for the InputAnalysis from Agent Library to be extracted early # @param context [Contrast::Agent::RequestContext] # @param potential_attack_string [String, nil] # @param ia_results [Array] # @param **kwargs # @return [Contrast::Agent::Reporting, nil] def find_attacker_with_results context, potential_attack_string, ia_results, **kwargs logger.trace('Checking vectors for attacks', rule: rule_name, input: potential_attack_string) result = nil ia_results.each do |ia_result| if potential_attack_string idx = potential_attack_string.index(ia_result.value) next unless idx result = build_attack_with_match(context, ia_result, result, potential_attack_string, **kwargs) else result = build_attack_without_match(context, ia_result, result, **kwargs) end end result end private # @param context [Contrast::Agent::RequestContext] # @param potential_attack_string [String, nil] # @return [Contrast::Agent::Reporting, nil] def find_postfilter_attacker context, potential_attack_string, **kwargs ia_results = gather_ia_results(context) ia_results.select! do |ia_result| ia_result.score_level == Contrast::Agent::Reporting::ScoreLevel::DEFINITEATTACK end find_attacker_with_results(context, potential_attack_string, ia_results, **kwargs) end end end end end end