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

require 'contrast/agent/protect/rule/path_traversal'
require 'contrast/agent/protect/policy/rule_applicator'
require 'contrast/utils/object_share'

module Contrast
  module Agent
    module Protect
      module Policy
        # This Module is how we apply the Path Traversal rule. It is called from
        # our patches of the targeted methods in which File or IO access occur.
        # It is responsible for deciding if the infilter methods of the rule
        # should be invoked.
        module AppliesPathTraversalRule
          extend Contrast::Agent::Protect::Policy::RuleApplicator

          class << self
            def invoke method, _exception, properties, object, args
              return unless args&.any?

              path = args[0]
              return unless path.is_a?(String)
              return if skip_analysis?

              action = properties['action']
              write_marker = write?(action, *args)
              possible_write = write_marker && possible_write?(write_marker)
              path_traversal_rule(path, possible_write, object, method)

              # If the action was copy, we need to handle the write half of it.
              # We handled read in line above.
              return unless action == COPY
              return unless args.length > 1

              dst = args[1]
              return unless dst.is_a?(String)

              path_traversal_rule(dst, true, object, method)
            end

            protected

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

            private

            def possible_write? input
              input.cs__respond_to?(:to_s) && input.to_s.include?(Contrast::Utils::ObjectShare::WRITE_FLAG)
            end

            READ = 'read'
            WRITE = 'write'
            COPY = 'copy'
            def write? action, *args
              return false if action == READ
              return false if action == COPY
              return true if action == WRITE

              write_marker = args.length > 1 ? args[1] : nil
              write_marker && possible_write?(write_marker)
            end

            def path_traversal_rule path, possible_write, object, method
              return unless applies_to?(path, possible_write)

              rule.infilter(Contrast::Agent::REQUEST_TRACKER.current, method, path)
            rescue Contrast::SecurityException => e
              raise e
            rescue StandardError => e
              logger.error('Error applying path traversal', e, module: object.cs__class.cs__name, method: method)
            end

            CS__SAFER_REL_PATHS = %w[public app log tmp].cs__freeze
            def safer_abs_paths
              @_safer_abs_paths ||= begin
                pwd = ENV['PWD']
                if pwd
                  tmp = CS__SAFER_REL_PATHS.map { |r| "#{ pwd }/#{ r }" }
                  gems = ENV['GEM_PATH']
                  tmp += gems.split(Contrast::Utils::ObjectShare::COLON) if gems
                  tmp.map!(&:downcase)
                  tmp
                else
                  []
                end
              end
            end

            def applies_to? path, possible_write = false
              # any possible write is a potential risk
              return true if possible_write

              # any path that moves 'up' is a potential risk
              return true if path.index(Contrast::Utils::ObjectShare::PARENT_PATH)

              path = path.downcase
              if path.start_with?(Contrast::Utils::ObjectShare::SLASH)
                safer_abs_paths.each do |prefix|
                  return false if path.start_with?(prefix)
                end
              else
                CS__SAFER_REL_PATHS.each do |prefix|
                  return false if path.start_with?(prefix)
                end
              end
              true
            end
          end
        end
      end
    end
  end
end