# 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