# Copyright (c) 2023 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details. # frozen_string_literal: true require 'json' 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 # @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] the routes associated with this finding, if the # finding is request based. attr_reader :routes # @return [Array] 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] 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_accessor :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 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] 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::Reporting::FindingEvent.new(event_data) events << contrast_event return unless request @request = Contrast::Agent::Reporting::FindingRequest.convert(request) @routes << request.discovered_route if request.discovered_route 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 = base_hash hsh[:events] = events.map(&:to_controlled_hash) if events.any? hsh[:properties] = properties if properties.any? 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 # @return [Hash] the base of every finding, regardless of type def base_hash { created: created, hash: hash_code.to_s, ruleId: rule_id, session_id: ::Contrast::ASSESS.session_id, version: 4 } 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 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 # TeamServer and have had time to build proper child structure. # # @return [Boolean] def event_based? !property_based? && !config_based? && !hardcoded? 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 TeamServer 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 TeamServer 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 TeamServer 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