# 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/base'
require 'contrast/agent/request/request_context'
require 'contrast/utils/object_share'
require 'contrast/agent/protect/rule/cmdi/cmdi_base_rule'
require 'contrast/agent/protect/rule/cmdi/cmd_injection'

module Contrast
  module Agent
    module Protect
      module Rule
        # The Ruby implementation of the Protect Command Injection Command
        # Backdoors sub-rule.
        class CmdiBackdoors < Contrast::Agent::Protect::Rule::CmdiBaseRule
          NAME = 'cmd-injection-command-backdoors'
          MATCHES = %w[/bin/bash-c /bin/sh-c sh-c bash-c].cs__freeze

          def rule_name
            NAME
          end

          def sub_rules
            Contrast::Utils::ObjectShare::EMPTY_ARRAY
          end

          # CMDI Backdoors infilter:
          # This rule does not have input classification.
          # If a value matches the CMDI applicable input types and it's length is > 2
          # we can check if it's used as command backdoors.
          #
          # @param context [Contrast::Agent::RequestContext] current request contest
          # @param classname [String] Name of the class
          # @param method [String] name of the method triggering the rule
          # @param command [String] potential dangerous command executed.
          # @raise [Contrast::SecurityException] if the rule mode ise set
          # to BLOCK and valid cdmi is detected.
          def infilter context, classname, method, command
            return if protect_excluded_by_url?(rule_name, context.request.path)
            return unless backdoors_match?(command)
            return unless (result = build_attack_with_match(context, nil, nil, command,
                                                            **{ classname: classname, method: method }))

            append_to_activity(context, result)
            cef_logging(result, :successful_attack)
            return unless blocked?

            raise_error(classname, method)
          end

          private

          # Used to customize the raised error message.
          #
          # @param classname [String] Name of the class
          # @param method [String] name of the method triggering the rule
          # @raise [Contrast::SecurityException]
          def raise_error classname, method
            raise(Contrast::SecurityException.new(self,
                                                  'Command Injection Command Backdoor rule triggered. ' \
                                                  "Call to #{ classname }.#{ method } blocked."))
          end

          # Check to see if value is backdoor match
          #
          # @param potential_attack_string [String]
          def backdoors_match? potential_attack_string
            return false unless potential_attack_string && potential_attack_string.length > 2
            return false unless matches_shell_parameter?(potential_attack_string)

            true
          end

          # Checks to see if input is used as parameter for a shell cmd.
          #
          # @param cmd [String]
          # @return [Boolean]
          def matches_shell_parameter? cmd
            normalize_cmd = cmd.delete(Contrast::Utils::ObjectShare::SPACE)
            MATCHES.each do |match|
              return true if normalize_cmd.include?(match)
            end
            false
          end
        end
      end
    end
  end
end