# 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'

module Contrast
  module Framework
    module Rack
      module Patch
        # Our patch into the Rack::Session::Cookie Class, allowing for the
        # runtime detection of insecure configurations on individual cookies
        # within the application
        class SessionCookie
          extend Contrast::Components::Logger::InstanceMethods
          extend Contrast::Components::Scope::InstanceMethods

          CS__SECURE_RULE_NAME = 'secure-flag-missing'
          CS__HTTPONLY_NAME = 'rails-http-only-disabled'
          CS__SESSION_TIMEOUT_NAME = 'session-timeout'
          SAFE_SESSION_TIMEOUT = (30 * 60 * 60)
          class << self
            include Contrast::Utils::InvalidConfigurationUtil

            def instrument
              @_instrument ||= begin
                ::Rack::Session::Cookie.class_eval do
                  alias_method(:cs__patched_initialize, :initialize)
                  def initialize app, options = {} # rubocop:disable Style/OptionHash
                    Contrast::Framework::Rack::Patch::SessionCookie.analyze(options)
                    cs__patched_initialize(app, options)
                  end
                end
                true
              end
            end

            def analyze options
              return unless ::Contrast::AGENT.enabled?
              return if ::Contrast::ASSESS.forcibly_disabled?

              apply_session_timeout(options)
              apply_httponly(options)
              apply_secure_session(options)
            end

            private

            def vulnerable_setting?(setting_key,
                                    safe_settings_value,
                                    options, safe_default: true,
                                    comparison_type: nil)
              # In most cases, Rack is pretty nice and the default value is safe
              return !safe_default unless options&.key?(setting_key)

              value = options[setting_key]

              return value.to_i > safe_settings_value.to_i if comparison_type&.to_sym == :greater_than

              value != safe_settings_value
            end

            def apply_secure_session options
              return unless vulnerable_setting?(:secure, true, options, safe_default: false)

              cs__report_finding(CS__SECURE_RULE_NAME, options, caller_locations(10, 9)[0])
            rescue StandardError => e
              begin
                logger.error('Unable to track call to secure session', e)
              rescue StandardError
                nil
              end
            end

            def apply_session_timeout options
              return unless vulnerable_setting?(:expire_after,
                                                SAFE_SESSION_TIMEOUT,
                                                options,
                                                safe_default: false,
                                                comparison_type: :greater_than)

              cs__report_finding(CS__SESSION_TIMEOUT_NAME, options, caller_locations(10, 9)[0])
            rescue StandardError => e
              begin
                logger.error('Unable to track call to set session timeout', e)
              rescue StandardError
                nil
              end
            end

            def apply_httponly options
              return unless vulnerable_setting?(:httponly, true, options)

              cs__report_finding(CS__HTTPONLY_NAME, options, caller_locations(10, 9)[0])
            rescue StandardError => e
              begin
                logger.error('Unable to track call to httponly', e)
              rescue StandardError
                nil
              end
            end
          end
        end
      end
    end
  end
end