# 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