# 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/input_classification/utils' require 'contrast/agent/protect/rule/input_classification/match_rates' require 'contrast/agent/telemetry/input_analysis_cache_event' require 'contrast/agent/reporting/input_analysis/score_level' require 'contrast/agent/reporting/input_analysis/input_type' module Contrast module Agent module Protect module Rule module InputClassification # This class will hold match information for each rule when input classification is being saved in LRU cache. class Statistics include Contrast::Agent::Protect::Rule::InputClassification::Utils include Contrast::Components::Logger::InstanceMethods attr_reader :data # Protect rules will always be fixed number, on other hand the number of inputs will grow, # we need to limit the number of inputs to be cached. CAPACITY = 30 def initialize @data = {} end # This method will handle the statistics for the input match # # @param rule_id [String] the Protect rule name. # @param cached [Contrast::Agent::Protect::InputClassification::CachedResult] # @param request [Contrast::Agent::Request] the current request. def match! rule_id, cached, request return unless Contrast::Agent::Telemetry::Base.enabled? push(rule_id, cached) fetch(rule_id, cached.result.input_type)&.increase_match_for_input return unless cached.request_id == request.__id__ fetch(rule_id, cached.result.input_type)&.increase_match_for_request end # This method will handle the statistics for the input mismatch. # Skip if this is the called with empty cache since it's not fair. # # @param rule_id [String] the Protect rule name. # @param input_type [Symbol] Type of the input def mismatch! rule_id, input_type return unless Contrast::Agent::Telemetry::Base.enabled? return if Contrast::Agent::Protect::InputAnalyzer.lru_cache.empty? fetch(rule_id, input_type)&.increase_mismatch_for_input end # @return [Array<Contrast::Agent::Telemetry::InputAnalysisCacheEvent>] the events to be sent. def to_events events = [] data.each do |_rule_id, match_rates| match_rates.each do |match_rate| event = Contrast::Agent::Telemetry::InputAnalysisCacheEvent.new(match_rate.rule_id, match_rate) next if event.empty? events << event end end events rescue StandardError => e logger.error("[IA_LRU_Cache] Error while creating events: #{ e }", stacktrace: e.backtrace) [] end # Creates new statisctics for protect rule. # # @param rule_id [String] the Protect rule name. # @param cached [Contrast::Agent::Protect::InputClassification::CachedResult] def push rule_id, cached new_entry = Contrast::Agent::Protect::Rule::InputClassification::MatchRates. new(rule_id, cached.result.input_type, cached.result.score_level) @data[rule_id] = [] if Contrast::Utils::DuckUtils.empty_duck?(@data[rule_id]) @data[rule_id].shift if @data[rule_id].length >= CAPACITY @data[rule_id] << new_entry unless saved?(rule_id, cached) end # Get the statistics for the protect rule. # # @param rule_id [String] the Protect rule name. # @param input_type [Symbol] Type of the input def fetch rule_id, input_type safe_extract(@data[rule_id]&.select { |e| e.input_type == input_type }) end private # Checks if the rate is saved for the input. # # @param rule_id [String] the Protect rule name. # @param new_entry [Contrast::Agent::Protect::InputClassification::CachedResult] def saved? rule_id, new_entry !fetch(rule_id, new_entry&.result&.input_type).nil? end # Call this method from the LRU Cache to get the statistics for a given rule_id, so it could be # thread safe. def clear @data.clear end end end end end end end