# 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) return if attack_result&.empty? 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] # @return [Contrast::Agent::Reporting::ApplicationDefendAttackerActivity, nil] 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] return unless new_violation sample_activity = Contrast::Agent::Reporting::ApplicationDefendAttackSampleActivity if (previously_violated = existing_attacker_activity.protection_rules[rule]) if (new_blocked_samples = new_violation.blocked&.samples)&.any? previously_violated.blocked ||= sample_activity.new previously_violated.blocked.samples.concat(new_blocked_samples) end if (new_exploited_samples = new_violation.exploited&.samples)&.any? previously_violated.exploited ||= sample_activity.new previously_violated.exploited.samples.concat(new_exploited_samples) end if (new_ineffective_samples = new_violation.ineffective&.samples)&.any? previously_violated.ineffective ||= sample_activity.new previously_violated.ineffective.samples.concat(new_ineffective_samples) end if (new_suspicious_samples = new_violation.suspicious&.samples)&.any? previously_violated.suspicious ||= sample_activity.new previously_violated.suspicious.samples.concat(new_suspicious_samples) end else existing_attacker_activity.protection_rules[rule] = new_violation end end end end end end