# 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/reporting_events/application_defend_attacker_activity' require 'contrast/utils/duck_utils' require 'contrast/agent/reporting/reporting_events/reporting_event' module Contrast module Agent module Reporting # This is the new ApplicationDefendActivity class which includes information about the defense of the application # which was discovered during exercise of the application during this activity period. class ApplicationDefendActivity < Contrast::Agent::Reporting::ReportingEvent include Contrast::Components::Logger::InstanceMethods # @return [Array] attr_reader :attackers def initialize ia_request: nil @attackers = [] @event_type = :application_defend_activity @request = ia_request super() end def to_controlled_hash validate { attackers: attackers.map(&:to_controlled_hash) } end def validate return unless Contrast::Utils::DuckUtils.empty_duck?(attackers) raise(ArgumentError, 'Attackers data must be populated') end # @param attack_result [Contrast::Agent::Reporting::AttackResult] def attach_data attack_result return unless attack_result&.cs__is_a?(Contrast::Agent::Reporting::AttackResult) attacker_activity = Contrast::Agent::Reporting::ApplicationDefendAttackerActivity.new(ia_request: @request) attacker_activity.attach_data(attack_result) @existing_attacker_activity = true if (existing_attacker_activity = find_existing_attacker_activity(attacker_activity)) attach_existing(existing_attacker_activity, attacker_activity, attack_result.rule_id) else attackers << attacker_activity end end # Find an existing attacker if it matches on source details # @param new_attacker_activity [Contrast::Agent::Reporting::ApplicationDefendAttackerActivity] def find_existing_attacker_activity new_attacker_activity attackers.find do |existing| existing.source_forwarded_for == new_attacker_activity.source_forwarded_for && existing.source_ip == new_attacker_activity.source_ip end end # @param existing_attacker_activity [Contrast::Agent::Reporting::ApplicationDefendAttackerActivity] # @param attacker_activity [Contrast::Agent::Reporting::ApplicationDefendAttackerActivity] # @param rule [String] def attach_existing existing_attacker_activity, attacker_activity, rule new_violation = attacker_activity.protection_rules[rule] sample_activity = Contrast::Agent::Reporting::ApplicationDefendAttackSampleActivity if (previously_violated = existing_attacker_activity.protection_rules[rule]) if (new_blocked = new_violation.blocked) previously_violated.blocked ||= sample_activity.new previously_violated.blocked.samples.concat(new_blocked.samples) if new_blocked.samples previously_violated.blocked.merge_time_maps(new_blocked.time_map) end if (new_exploited = new_violation.exploited) previously_violated.exploited ||= sample_activity.new previously_violated.exploited.samples.concat(new_exploited.samples) if new_exploited.samples previously_violated.exploited.merge_time_maps(new_exploited.time_map) end if (new_ineffective = new_violation.ineffective) previously_violated.ineffective ||= sample_activity.new previously_violated.ineffective.samples.concat(new_ineffective.samples) if new_ineffective.samples previously_violated.ineffective.merge_time_maps(new_ineffective.time_map) end if (new_suspicious = new_violation.suspicious) previously_violated.suspicious ||= sample_activity.new previously_violated.suspicious.samples.concat(new_suspicious.samples) if new_suspicious.samples previously_violated.suspicious.merge_time_maps(new_suspicious.time_map) end else existing_attacker_activity.protection_rules[rule] = new_violation end end end end end end