# Copyright (c) 2022 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details.
# frozen_string_literal: true

require 'contrast/components/logger'
require 'contrast/components/scope'
require 'contrast/agent/assess/events/event_data'

module Contrast
  module Extension
    module Assess
      # This is our patch of the Marshal class
      # Disclaimer: there may be a better way, but we're in a 'get it work' state.
      # Hopefully, we'll be in a 'get it right' state soon.
      # This module is used for our Marshal.load patches
      class MarshalPropagator
        extend Contrast::Components::Scope::InstanceMethods

        class << self
          extend Contrast::Components::Logger::InstanceMethods
          include Contrast::Components::Logger::InstanceMethods

          def cs__load_protect arg
            return if in_contrast_scope?

            with_contrast_scope do
              Contrast::Agent::Protect::Policy::AppliesDeserializationRule.prepended_invoke(arg)
            end
            nil
          end

          def cs__load_assess source, ret
            with_contrast_scope do
              return unless ::Contrast::ASSESS.non_request_tracking? || Contrast::Agent::REQUEST_TRACKER.current

              args = [source]
              # source might not be all the args passed in, but it is the one we care
              # about. we could pass in all the args in the last param here if it
              # becomes an issue in rendering on TS
              finding = Contrast::Agent::Assess::Policy::TriggerMethod.build_finding(trigger_node('Marshal', :load),
                                                                                     source, self, ret, *args)
              Contrast::Agent::Assess::Policy::TriggerMethod.report_finding(finding) if finding
              return unless (properties = Contrast::Agent::Assess::Tracker.properties!(ret))

              properties.copy_from(source, ret)

              node = Contrast::Agent::Assess::Policy::Policy.instance.find_propagator_node('Marshal', :load, false)
              event_data = Contrast::Agent::Assess::Events::EventData.new(node, ret, self, ret, args)
              properties.build_event(event_data)
            rescue StandardError => e
              logger.error('Unable to run Assess for Marshal.load', e)
            end
          end

          def trigger_node clazz, method
            triggers = Contrast::Agent::Assess::Policy::Policy.instance.triggers
            return unless triggers

            triggers.find { |node| node.class_name == clazz && node.method_name == method }
          end
        end
      end

      # Used for aliasing
      module ContrastMarshal
        def cs__marshal_load source
          # Do the protect
          Contrast::Extension::Assess::MarshalPropagator.cs__load_protect(source) if source
          # call the original
          result = Marshal.load(source) # rubocop:disable Security/MarshalLoad
          # Do the assess
          tracked = Contrast::Agent::Assess::Tracker::PROPERTIES_HASH.tracked?(source) if source
          skip = Contrast::Agent::Patching::Policy::Patch.skip_assess_analysis? if tracked
          Contrast::Extension::Assess::MarshalPropagator.cs__load_assess(source, result) if skip
          # return original
          result
        end
      end
    end
  end
end