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

require 'contrast/agent/protect/rule/base_service'
require 'contrast/components/interface'
require 'contrast/utils/stack_trace_utils'

module Contrast
  module Agent
    module Protect
      module Rule
        # This class handles our implementation of the Path Traversal
        # Protect rule.
        class PathTraversal < Contrast::Agent::Protect::Rule::BaseService
          include Contrast::Components::Interface
          access_component :agent, :analysis

          NAME = 'path-traversal'
          SYSTEM_PATHS = %w[
            /proc/self
            etc/passwd
            etc/shadow
            etc/hosts
            etc/groups
            etc/gshadow
            ntuser.dat
            /Windows/win.ini
            /windows/system32/
            /windows/repair/
          ].cs__freeze

          def name
            NAME
          end

          def infilter context, method, path
            return unless infilter?(context)

            result = find_attacker(context, path)
            return unless result

            append_to_activity(context, result)
            return unless blocked?

            raise Contrast::SecurityException.new(
                self,
                "Path Traversal rule triggered. Call to File.#{ method } blocked.")
          end

          protected

          def find_attacker context, path
            attack_result = nil
            attack_result = super(context, path) if infilter?(context)
            check_rep_features(context, path, attack_result)
          end

          # Build a subclass of the RaspRuleSample using the query string and the
          # evaluation
          def build_sample context, input_analysis_result, path, **_kwargs
            sample = build_base_sample(context, input_analysis_result)
            sample.path_traversal = Contrast::Api::Dtm::PathTraversalDetails.new
            path ||= input_analysis_result.value
            sample.path_traversal.path = Contrast::Utils::StringUtils.protobuf_safe_string(path)
            sample
          end

          private

          # Build a subclass of the RaspRuleSample if the sample matches
          def build_rep_sample context, path
            sample = build_base_sample(context, nil)
            sample.path_traversal_semantic = Contrast::Api::Dtm::PathTraversalSemanticAnalysisDetails.new
            path = Contrast::Utils::StringUtils.protobuf_safe_string(path)
            sample.path_traversal_semantic.path = path

            if custom_code_access_sysfile_enabled? && custom_code_accessing_system_file?(path)
              sample.path_traversal_semantic.findings << :CUSTOM_CODE_ACCESSING_SYSTEM_FILES
              return sample
            end

            if common_file_exploits_enabled? && contains_known_attack_signatures?(path)
              sample.path_traversal_semantic.findings << :COMMON_FILE_EXPLOITS
              return sample
            end

            nil
          end

          def check_rep_features context, path, attack_result
            rep_sample = build_rep_sample(context, path)
            if rep_sample
              attack_result = build_attack_result(context) if attack_result.nil?
              build_attack_with_match(context, nil, attack_result, path)
            end
            attack_result
          end

          def custom_code_access_sysfile_enabled?
            PROTECT.report_custom_code_sysfile_access?
          end

          def custom_code_accessing_system_file? input
            system_file?(input) && Contrast::Utils::StackTraceUtils.custom_code_context?
          end

          def system_file? path
            return false unless path

            SYSTEM_PATHS.any? { |sys_path| sys_path.include?(path) }
          end

          def common_file_exploits_enabled?
            false
          end

          # TODO: RUBY-318
          # KNOWN_SECURITY_BYPASS_MARKERS = ['::$DATA', '::$Index', '', '\x00'].cs__freeze
          def contains_known_attack_signatures? input
            utf8 = Contrast::Utils::StringUtils.force_utf8(input)
            _ = CGI.unescape(utf8)
            # TODO: RUBY-318 implement REP for known attack signatures
            #   try:
            #     realpath = os.path.realpath(unescaped).lower().rstrip('/')
            #   except ValueError as e:
            #     return 'embedded null byte' == str(e)
            #   except TypeError as e:
            #     return 'NUL' in str(e) or 'null byte' in str(e) or (PY34 and 'embedded NUL character' == str(e))
            #   except Exception as e:
            #     return 'null byte' in str(e).lower()
            #   return return any([bypass_markers.lower().rstrip('/') in realpath for bypass_markers in PathTraversalREPMixin.KNOWN_SECURITY_BYPASS_MARKERS])
            false
          end
        end
      end
    end
  end
end