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

require 'contrast/agent/protect/rule/deserialization/deserialization'
require 'contrast/agent/protect/policy/rule_applicator'

module Contrast
  module Agent
    module Protect
      module Policy
        # This Module is how we apply the Deserialization rule. It is called from
        # our patches of the targeted methods in which deserialization occurs.
        # It is responsible for deciding if the infilter methods of the rule
        # should be invoked.
        module AppliesDeserializationRule
          extend Contrast::Agent::Protect::Policy::RuleApplicator

          class << self
            # Calls the actual rule for this applicator, if required. Most rules
            # invoke this from within their apply_rule method after doing
            # whatever transformations they need to get into this common format.
            #
            # @param _method [Symbol] the name of the method for which this rule
            #   is invoked
            # @param _exception [Exception] any exception raised; used for rules
            #   like Padding Oracle Attack (now defunct), which determine if the
            #   number and type of exceptions are an attack
            # @param _properties [Hash] set of extra information provided by the
            #   applicator in an attempt to build a better story for the user
            # @param _object [Object] the thing on which the triggering method
            #   was invoked
            # @param args [Array<Object>] the arguments passed to the triggering
            #   method at invocation
            def invoke _method, _exception, _properties, _object, args
              return unless valid_input?(args)
              return if skip_analysis?

              rule.infilter(Contrast::Agent::REQUEST_TRACKER.current, args[0])
              # add rescue here
            end

            # Calls the actual rule for this applicator, if required, when the
            # triggering method is called from Marshal.load when it has been
            # prepended.
            #
            # @param arg [Object] the argument passed to the triggering method
            #   at invocation
            def prepended_invoke arg
              return unless arg&.cs__is_a?(String)
              return if skip_analysis?

              rule.infilter(Contrast::Agent::REQUEST_TRACKER.current, arg)
              # add rescue here
            end

            # Allow the rule to check if the given input is an attempt to
            # deserialize something in a way that will result in a command
            # execution
            #
            # @param command [String] user input that potentially contains a
            #   Gadget, a Module that results in code execution on
            #   deserialization, and some form of command.
            def apply_deserialization_command_check command
              return unless command
              return if skip_analysis?

              rule.check_command_scope(command)
              # add rescue here
            end

            protected

            def rule_name
              Contrast::Agent::Protect::Rule::Deserialization::NAME
            end

            private

            # Determine if the rule would care about the arguments passed in
            # this method invocation. Required b/c Ruby doesn't do type
            # checking on its method signatures.
            #
            # @param args [Array<Object>] the arguments provided to the
            #   instrumented method
            # @return [Boolean]
            def valid_input? args
              return false unless args&.any?

              input = args[0]
              input.cs__is_a?(String)
            end
          end
        end
      end
    end
  end
end