# Copyright (c) 2022 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details. # frozen_string_literal: true require 'json' require 'contrast/components/logger' require 'contrast/agent/reporting/reporting_events/finding_event' require 'contrast/agent/reporting/reporting_events/finding_request' require 'contrast/agent/reporting/reporting_events/reporting_event' require 'contrast/agent/assess/events/event_data' require 'contrast/utils/timer' 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_accessor events [Array<Contrast::Agent::Assess::ContrastEvent>] if a dataflow based finding, the # representation of those method calls which constitute a dangerous code path. # @attr_accessor properties [Hash<String,String>] a set of values that TeamServer can use to provide more context # to the user when rendering the finding. For some findings, a specific set of keys and values are required. # @attr_accessor request [Contrast::Agent::Request, nil] the request, if any, in which this finding occurred # @attr_accessor hash_code [String] the unique identifier of this finding. # @attr_reader rule_id [String] the name of the rule violated; must match those in TeamServer. class Finding < Contrast::Agent::Reporting::ReportingEvent attr_reader :rule_id, :routes, :events, :properties, :request attr_accessor :hash_code PROPERTIES_RULES = %w[ autocomplete-missing cache-controls-missing clickjacking-control-missing xcontenttype-header-missing hsts-header-missing xxssprotection-header-disabled csp-header-missing csp-header-insecure parameter-pollution ].cs__freeze class << self # @param finding_dtm [Contrast::Api::Dtm::Finding] # @return [Contrast::Agent::Reporting::Finding] def convert finding_dtm report = new(finding_dtm.rule_id) report.attach_property_data(finding_dtm) report end end def initialize rule_id @event_method = :PUT @event_endpoint = "#{ Contrast::API.api_url }/api/ng/traces" @events = [] @routes = [] @rule_id = Contrast::Utils::StringUtils.truncate(rule_id) @properties = {} super() end # Some reports require specific additional headers to be used. To that end, we'll attach them here, letting # each handle their own. # # @param request [Net::HTTPRequest] def attach_headers request request['Report-Hash'] = hash_code end # @param trigger_node [Contrast::Agent::Assess::Policy::TriggerNode] the node to direct applying this # trigger event # @param source [Object] the source of the Trigger Event # @param object [Object] the Object on which the method was invoked # @param ret [Object] the Return of the invoked method # @param request [Contrast::Agent::Request] # @param args [Array<Object>] the Arguments with which the method was invoked def attach_data trigger_node, source, object, ret, request, *args event_data = Contrast::Agent::Assess::Events::EventData.new(trigger_node, source, object, ret, args) event_msgs = Contrast::Agent::Reporting::FindingEvent.from_source(source) events.concat(event_msgs) if event_msgs if request @request = Contrast::Agent::Reporting::FindingRequest.convert(request) @routes << Contrast::Agent::Reporting::RouteDiscovery.convert(request&.route) end events << Contrast::Agent::Assess::ContrastEvent.new(event_data) attach_properties end # Attach the data from a Contrast::Api::Dtm::Finding required for property based findings generated during # response analysis. # # @param finding_dtm [Contrast::Api::Dtm::Finding] def attach_property_data finding_dtm @created = Contrast::Utils::Timer.now_ms @hash_code = finding_dtm.hash_code @rule_id = finding_dtm.rule_id finding_dtm.properties.each_pair do |key, value| @properties[key] = value end finding_dtm.routes.each do |route| @routes << Contrast::Agent::Reporting::RouteDiscovery.convert(route) end request = Contrast::Agent::REQUEST_TRACKER.current&.activity&.http_request @request = Contrast::Agent::Reporting::FindingRequest.convert(request) if request 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 hsh = { created: @created, hash: hash_code, ruleId: @rule_id, session_id: @agent_session_id_value, version: 4 } hsh[:events] = events if event_based? hsh[:evidence] = @evidence unless event_based? || property_based? hsh[:properties] = properties if property_based? hsh[:tags] = Contrast::ASSESS.tags if Contrast::ASSESS.tags if request_based? hsh[:request] = request hsh[:routes] = routes end hsh end # @raise [ArgumentError] def validate raise(ArgumentError, "#{ self } did not have a proper rule. Unable to continue.") unless @rule_id if event_based? && events.empty? raise(ArgumentError, "#{ self } did not have proper events. Unable to continue.") end if property_based? && properties.empty? raise(ArgumentError, "#{ self } did not have proper properties. Unable to continue.") end return unless request_based? && request.nil? raise(ArgumentError, "#{ self } did not have a proper request. Unable to continue.") end private # Our events have properties on them. To report them to TeamServer, we need to pull them from our object up to # the Contrast::Agent::Reporting::Finding level. # # TODO: RUBY-99999 put properties on events, not just on DTM def attach_properties; end def build_events events, event return unless event event.parent_events&.each do |parent_event| build_events(events, parent_event) end events << event end # Rules which are event based must have an event to be sent to TeamServer. They include the Trigger, Regexp, # and Data flow type Rules, meaning all those which are not Properties based. Eventually, we may have # validation for each of those types; however, that's a refactor for after we've translated all rules from the # Service and have had time to build proper child structure. # # @return [Boolean] def event_based? !property_based? end # Rules which are property based must have a property to be sent to TeamServer. Eventually, each rule may own # its own validation, as the properties each needs are different; however, that's a refactor for after we've # translated all rules from the Service and have had time to build proper child structure. # # @return [Boolean] def property_based? PROPERTIES_RULES.include?(@rule_id) end # Rules which are request based must have a request to be sent to TeamServer. Most rules fit this category, so # we'll default to true for now. Eventually, this will be split out for those rules, like Hardcoded, which do # not need requests. # # @return [Boolean] def request_based? true end end end end end