# 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_source' require 'contrast/agent/reporting/reporting_events/finding_object' require 'contrast/agent/reporting/reporting_events/finding_stack' require 'contrast/agent/reporting/reporting_events/finding_taint_range' module Contrast module Agent module Reporting # This is the new Finding class which will include all the needed information for the new reporting system to # relay this information in the Finding/Trace messages. These findings are used by TeamServer to construct the # vulnerability information for the assess feature. They represent those parts of the application, either through # configuration, method invocation, or dataflow, which are determined to be insecure. # # @attr_reader action [String] what the event did; CREATION, A2O, A2P, A2A, A2R, O2A, O2O, O2P, O2R, P2A, P2O, # P2P, P2R, TAG, TRIGGER. # @attr_reader args [Array] the arguments passed to the method. # @attr_reader code [nil] unused. # @attr_reader event_id [Integer] the id of this event. # @attr_reader event_sources [Array] the source of taint # @attr_reader field_name [nil] unused. # @attr_reader object [Contrast::Agent::Reporting::FindingEventSource] the object this method was invoked on. # @attr_reader parent_object_ids [Array] # @attr_reader properties [Hash] # @attr_reader tags [Array] description of what's happened to the data # @attr_reader taint_ranges [Array] the tags and spans of the # source that are tracked # @attr_reader target [String] the target of the taint from the method; ^(O|R|P\d+)$ # @attr_reader thread [String] the id of the thread on which the method was invoked # @attr_reader time [Integer] the time, in ms, when the event was generated # @attr_reader type [String] the type of event; METHOD, PROPAGATION, TAG class FindingEvent attr_reader :action, :args, :code, :event_id, :event_sources, :field_name, :object, :parent_object_ids, :properties, :ret, :signature, :source, :stack, :tags, :taint_ranges, :target, :thread, :time, :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(finding, 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 display_params!(event) dataflow!(event) event_sources!(event) @signature = Contrast::Agent::Reporting::FindingSignature.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 validate { action: action, args: args, # code: code, # Unused by our agent eventId: event_id, eventSources: event_sources, # fieldName: field_name, # Unused by our agent object: object, parentObjectIds: parent_object_ids, properties: properties, ret: ret, signature: signature, source: source, stack: stack, tags: tags, taintRanges: taint_ranges, 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 = event.policy_node.node_type 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::FindingObject.convert(event.object, truncate_obj) truncate_ret = Contrast::Utils::ObjectShare::RETURN_KEY != taint_target @ret = Contrast::Agent::Reporting::FindingObject.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::FindingObject.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) event_sources << Contrast::Agent::Reporting::FindingEventSource.convert(event) 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 << 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::FindingStack.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 { |range| taint_ranges << Contrast::Agent::Reporting::FindingTaintRange.convert(range) } 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 unless taint_ranges && !taint_ranges.empty? raise(ArgumentError, "#{ self } did not have a proper taintRanges. Unable to continue.") end 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 && !tags.empty? end end end end end