# Copyright (c) 2022 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details. # frozen_string_literal: true require 'contrast/agent/reporting/reporting_events/finding_event_object' require 'contrast/agent/reporting/reporting_events/finding_event_parent_object' require 'contrast/agent/reporting/reporting_events/finding_event_property' require 'contrast/agent/reporting/reporting_events/finding_event_signature' require 'contrast/agent/reporting/reporting_events/finding_event_source' require 'contrast/agent/reporting/reporting_events/finding_event_stack' require 'contrast/agent/reporting/reporting_events/finding_event_taint_range' module Contrast module Agent module Reporting # This is the new FindingEvent class which will include all the needed information for the new reporting system # to relay this information in the Finding/Trace messages. These FindingEvents are used by TeamServer to # construct the vulnerability information for the assess feature. They represent the operation the application # underwent that transformed data during the dataflow. class FindingEvent # @return [Symbol] what the event did; CREATION, A2O, A2P, A2A, A2R, O2A, O2O, O2P, O2R, P2A, P2O, P2P, P2R, # TAG, TRIGGER. attr_reader :action # @return [Array] the arguments passed to the method. attr_reader :args # @return [nil] unused. attr_reader :code # @return [Integer] the id of this event. attr_reader :event_id # @return [Array] the source of taint attr_reader :event_sources # @return [nil] unused. attr_reader :field_name # @return [Contrast::Agent::Reporting::FindingEventObject] the object this method was invoked on. attr_reader :object # @@return [Array] the ids of all the events directly # preceding this attr_reader :parent_object_ids # @return [Array] attr_reader :properties # @return [Contrast::Agent::Reporting::FindingEventObject] the return of the method. attr_reader :ret # @return [Contrast::Agent::Reporting::FindingEventSignature] the signature of the method. attr_reader :signature # @return [String] the source of the taint from the method; ^(O|R|P\d+)$ attr_reader :source # @return [Array] attr_reader :stack # @return [String] comma separated list of descriptions of what's happened to the data attr_reader :tags # @return [Array] the tags and spans of the source that are # tracked attr_reader :taint_ranges # @return [String] the target of the taint from the method; ^(O|R|P\d+)$ attr_reader :target # @return [String] the id of the thread on which the method was invoked attr_reader :thread # @return [Integer] the time, in ms, when the event was generated attr_reader :time # @return [String] the type of event; METHOD, PROPAGATION, TAG attr_reader :type class << self # Find all the events leading up to the given source and return an array of FindingEvents # # @param source [Object] something that may have been tracked # @return [Array] def from_source source return unless source && (props = Contrast::Agent::Assess::Tracker.properties(source)) build_events([], props.event) if props.event end # @param event [Contrast::Agent::Assess::ContrastEvent] # @return [Contrast::Agent::Reporting::FindingEvent] def convert event report = new report.attach_data(event) report end private # Pull the parent events from the given event, generating an array of FindingEvents, passing back the given # array of events after populating it. # # @param events [Array] # @param event [Contrast::Agent::Assess::ContrastEvent] # @return [Array] def build_events events, event return unless event event.parent_events&.each do |parent_event| build_events(events, parent_event) end events << convert(event) events end end # Parse the data from a Contrast::Agent::Assess::ContrastEvent to attach what is required for reporting to # TeamServer to this Contrast::Agent::Reporting::FindingEvent # # @param event [Contrast::Agent::Assess::ContrastEvent] def attach_data event @event_id = event.event_id @time = event.time.to_i @thread = event.thread.to_s display_params!(event) dataflow!(event) event_sources!(event) @signature = Contrast::Agent::Reporting::FindingEventSignature.convert(event) stack!(event) parent_ids!(event) properties!(event) end # Convert the instance variables on the class, and other information, into the identifiers required for # TeamServer to process the JSON form of this message. # # @return [Hash] # @raise [ArgumentError] def to_controlled_hash # rubocop:disable Metrics/AbcSize validate { action: action, args: args.map(&:to_controlled_hash), # code: code, # Unused by our agent objectId: event_id, eventSources: event_sources.map(&:to_controlled_hash), # fieldName: field_name, # Unused by our agent object: object.to_controlled_hash, parentObjectIds: parent_object_ids.map(&:to_controlled_hash), properties: properties.map(&:to_controlled_hash), ret: ret&.to_controlled_hash, signature: signature.to_controlled_hash, source: source || '', stack: stack.map(&:to_controlled_hash), tags: tags.join(','), taintRanges: taint_ranges.map(&:to_controlled_hash), target: target || '', thread: thread, time: time, type: type } end # @raise [ArgumentError] def validate validate_base validate_dataflow end private # Find the action and type of this event, used by TeamServer to display type of the event and the # transformation if made. # # @param event [Contrast::Agent::Assess::ContrastEvent] def display_params! event @action = event.policy_node.build_action @type = case event.policy_node.node_type when :TYPE_TAG 'TAG' when :TYPE_PROPAGATION 'PROPAGATION' else # :TYPE_METHOD 'METHOD' end end # Build the dataflow components of this FindingEvent. # # @param event [Contrast::Agent::Assess::ContrastEvent] def dataflow! event taint_target = taint_target!(event) truncate_obj = Contrast::Utils::ObjectShare::OBJECT_KEY != taint_target @object = Contrast::Agent::Reporting::FindingEventObject.convert(event.object, truncate_obj) truncate_ret = Contrast::Utils::ObjectShare::RETURN_KEY != taint_target @ret = Contrast::Agent::Reporting::FindingEventObject.convert(event.ret, truncate_ret) event_args!(event, taint_target) taint_ranges!(event) end # Convert the arguments of the given ContrastEvent to the reportable form for this FindingEvent. We'll # truncate any argument that isn't the taint target of this event. # # @param event [Contrast::Agent::Assess::ContrastEvent] # @param taint_target [String,nil] the target of the taint in this event. def event_args! event, taint_target @args = [] idx = 0 while idx < event.args.length @args << Contrast::Agent::Reporting::FindingEventObject.convert(event.args[idx], taint_target != idx) idx += 1 end end # Convert the sources of the event to sources here # # @param event [Contrast::Agent::Assess::ContrastEvent] def event_sources! event @event_sources = [] return unless event.cs__is_a?(Contrast::Agent::Assess::Events::SourceEvent) source = Contrast::Agent::Reporting::FindingEventSource.convert(event) event_sources << source if source end # Convert the parent id's of the given ContrastEvent to the reportable form for this FindingEvent. # # @param event [Contrast::Agent::Assess::ContrastEvent] def parent_ids! event @parent_object_ids = [] event.parent_events&.each do |parent_event| parent_object_ids << Contrast::Agent::Reporting::FindingEventParentObject.new(parent_event.event_id.to_i) end end # Convert the properties of the given ContrastEvent to the reportable form for this FindingEvent. # TODO: RUBY-99999 How do properties get to events? Do they? I thought Stored-XSS needed it. # # @param _event [Contrast::Agent::Assess::ContrastEvent] def properties! _event @properties = [] end # Convert the stack of the given ContrastEvent to the reportable form for this FindingEvent. # # @param event [Contrast::Agent::Assess::ContrastEvent] def stack! event @stack = [] event.stack_trace.each do |stack_event| if (report = Contrast::Agent::Reporting::FindingEventStack.convert(stack_event)) stack << report end end end # Convert the taint ranges of the given ContrastEvent to the reportable form for this FindingEvent. # # @param event [Contrast::Agent::Assess::ContrastEvent] def taint_ranges! event @tags = [] @taint_ranges = [] event&.tags&.each_pair do |tag_key, tag_ranges| tags << tag_key tag_ranges.each do |range| taint_ranges << Contrast::Agent::Reporting::FindingEventTaintRange.convert(range) end end end # Find the source and target for this FindingEvent based on the provided event. # # @param event [Contrast::Agent::Assess::ContrastEvent] # @return [String,nil] the target of the taint in this event. def taint_target! event return unless (node = event.policy_node) @source = node.source_string if node.source_string @target = node.target_string if node.target_string return node.targets[0] if node.targets&.any? node.sources[0] if node.sources&.any? end # @raise [ArgumentError] def validate_base raise(ArgumentError, "#{ self } did not have a proper action. Unable to continue.") unless action raise(ArgumentError, "#{ self } did not have a proper eventId. Unable to continue.") unless event_id raise(ArgumentError, "#{ self } did not have a proper thread. Unable to continue.") unless thread raise(ArgumentError, "#{ self } did not have a proper time. Unable to continue.") unless time raise(ArgumentError, "#{ self } did not have a proper type. Unable to continue.") unless type end # @raise [ArgumentError] def validate_dataflow unless parent_object_ids raise(ArgumentError, "#{ self } did not have a proper parentObjectIds. Unable to continue.") end raise(ArgumentError, "#{ self } did not have a proper taintRanges. Unable to continue.") unless taint_ranges raise(ArgumentError, "#{ self } did not have a proper args. Unable to continue.") unless args raise(ArgumentError, "#{ self } did not have a proper object. Unable to continue.") unless object raise(ArgumentError, "#{ self } did not have a proper signature. Unable to continue.") unless signature raise(ArgumentError, "#{ self } did not have a proper stack. Unable to continue.") unless stack raise(ArgumentError, "#{ self } did not have a proper tags. Unable to continue.") unless tags end end end end end