# 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/application_reporting_event' require 'contrast/agent/reporting/reporting_events/finding_event' require 'contrast/agent/reporting/reporting_events/finding_request' 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. class Finding < Contrast::Agent::Reporting::ApplicationReportingEvent include Contrast::Components::Logger::InstanceMethods # @return [Integer] the time, in ms, that this object was initialized attr_reader :created # @return [String] the ID of the rule associated with this finding attr_reader :rule_id # @return [Array<Contrast::Agent::Reporting::RouteDiscovery>] the routes associated with this finding, if the # finding is request based. attr_reader :routes # @return [Array<Contrast::Agent::Reporting::FindingEvent>] the events associated with this finding, if the # finding is event (dataflow) based. attr_reader :events # @return [String] the evidence associated with this finding, if the finding is event based. deprecated in # favor of properties # attr_reader :evidence # @return [Hash<String,String>] properties that prove the violation of the rule for this finding attr_reader :properties # @return [Contrast::Agent::Reporting::FindingRequest] the request associated with this finding, if the finding # is request based attr_reader :request # @return [String] the uniquely identifying hash of this finding attr_accessor :hash_code CONFIGURATION_RULES = %w[rails-http-only-disabled secure-flag-missing session-timeout].cs__freeze HARDCODED_RULES = %w[hardcoded-key hardcoded-password].cs__freeze PROPERTIES_RULES = %w[ autocomplete-missing cache-controls-missing clickjacking-control-missing csp-header-missing csp-header-insecure hsts-header-missing parameter-pollution xcontenttype-header-missing xxssprotection-header-disabled ].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 = {} @created = Contrast::Utils::Timer.now_ms super() end def file_name 'traces' 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_messages = Contrast::Agent::Reporting::FindingEvent.from_source(source) events.concat(event_messages) if event_messages&.any? event_data = Contrast::Agent::Assess::Events::EventData.new(trigger_node, source, object, ret, args) contrast_event = Contrast::Agent::Assess::ContrastEvent.new(event_data) events << Contrast::Agent::Reporting::FindingEvent.convert(contrast_event) attach_properties return unless request @request = Contrast::Agent::Reporting::FindingRequest.convert(request) @routes << Contrast::Agent::Reporting::RouteDiscovery.convert(request.route) if request.route 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 @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&.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 begin validate rescue ArgumentError => e logger.error('Finding event validation failed with: ', e) return end hsh = { created: created, hash: hash_code.to_s, ruleId: rule_id, session_id: ::Contrast::ASSESS.session_id, version: 4 } hsh[:events] = events.map(&:to_controlled_hash) 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 return hsh unless request_based? hsh[:request] = request.to_controlled_hash hsh[:routes] = routes.map(&:to_controlled_hash) hsh end # @raise [ArgumentError] def validate raise(ArgumentError, "#{ self } did not have a proper rule. Unable to continue.") unless @rule_id unless ::Contrast::ASSESS.session_id raise(ArgumentError, "#{ self } did not have a proper session id. Unable to continue.") end if event_based? && events.empty? raise(ArgumentError, "#{ self } did not have proper events for #{ @rule_id }. Unable to continue.") end if property_based? && properties.empty? raise(ArgumentError, "#{ self } did not have proper properties for #{ @rule_id }. Unable to continue.") end return unless request_based? && request.nil? raise(ArgumentError, "#{ self } did not have a proper request for #{ @rule_id }. 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? && !config_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 config based must have a configuration 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 config_based? CONFIGURATION_RULES.include?(@rule_id) end # Rules which are hardcode based send properties 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 hardcoded? HARDCODED_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? !config_based? && !hardcoded? end end end end end