# Copyright (c) 2022 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details. # frozen_string_literal: true require 'contrast/agent/reporting/reporting_events/application_update' require 'contrast/agent/reporting/reporting_events/application_inventory' require 'contrast/components/logger' require 'contrast/extension/module' require 'contrast/framework/grape/support' require 'contrast/framework/manager_extend' require 'contrast/framework/rack/support' require 'contrast/framework/rails/support' require 'contrast/framework/sinatra/support' require 'contrast/utils/class_util' module Contrast module Framework # Allows access to framework specific information class Manager include Contrast::Components::Logger::InstanceMethods include Contrast::Framework::ManagerExtend # Order here does matter as the first framework listed will be the first one we pull information from Rack will # be a special case that may involve updating some logic to handle only applying Rack if Rails/Sinatra do not # exist SUPPORTED_FRAMEWORKS = [ Contrast::Framework::Rails::Support, Contrast::Framework::Sinatra::Support, Contrast::Framework::Grape::Support, Contrast::Framework::Rack::Support ].cs__freeze def initialize @_frameworks = SUPPORTED_FRAMEWORKS.map do |framework_klass| next unless enable_framework_support?(framework_klass.detection_class) logger.info('Framework detected. Enabling support.', framework: framework_klass.detection_class) framework_klass end @_frameworks.compact! end # Patches that have to be applied as early as possible to catch calls that happen prior to the first Request, # typically those around configuration. def before_load_patches! @_before_load_patches ||= begin SUPPORTED_FRAMEWORKS.each(&:before_load_patches!) true end end # Return all the After Load Patches for all the Frameworks we know, even if that Framework hasn't been detected. # # @return [Set] the AfterLoadPatches of each framework def find_after_load_patches patches = Set.new SUPPORTED_FRAMEWORKS.each do |framework| framework_patches = framework.after_load_patches patches.merge(framework_patches) if framework_patches && !framework_patches.empty? end patches end # @return [Array] def find_route_discovery_data routes_for_all_frameworks end def server_type first_framework_result(:server_type, 'rack') end def app_name first_framework_result(:application_name, 'root') end def app_root found = first_framework_result(:application_root, nil) found || ::Rack::Directory.new('').root end # Build a request from the provided env, based on the framework(s) we're currently supporting. # # @param env [Hash] the various variables stored by this and other Middlewares to know the state and values of # this particular Request # @return [::Rack::Request] either a rack request or subclass thereof. def retrieve_request env # If we're mounted on Rails, use Rails. if @_frameworks.include?(Contrast::Framework::Rails::Support) return Contrast::Framework::Rails::Support.retrieve_request(env) end # If we know the framework, use it. return @_frameworks[0].retrieve_request(env) if @_frameworks.length == 1 # Fall back on a regular Rack::Request ::Rack::Request.new(env) rescue StandardError => e logger.warn('Unable to retrieve_request', e) end # @param env [Hash] the various variables stored by this and other Middlewares to know the state and values of # this particular Request # @return [Boolean] true if at least one framework is streaming the response; false if none are streaming def streaming? env result = false @_frameworks.each do |framework| result = framework.streaming?(env) break if result end result end # Iterate through current frameworks and return the current request's route. This will be the first non-nil # result. # # @param request [Contrast::Agent::Request] the current request. # @return [Contrast::Api::Dtm::RouteCoverage] the current route as a Dtm. def get_route_dtm request @_frameworks.lazy.map { |framework_support| framework_support.current_route(request) }. reject(&:nil?).first # rubocop:disable Style/CollectionCompact end # Iterate through current frameworks and return the current request's route. This will be the first non-nil # result. # # @param request [Contrast::Agent::Request] the current request. # @return [Contrast::Agent::Reporting::RouteCoverage] the current route as a Dtm. def get_route_information request @_frameworks.lazy.map { |framework_support| framework_support.current_route_coverage(request) }. reject(&:nil?).first # rubocop:disable Style/CollectionCompact end # Sometimes the framework we want to instrument is loaded after our agent code. To catch that case, we'll detect # if the loaded_module is the marker class for any of our supported frameworks. If it is, and we don't already # have support enabled, we'll enable it now. We'll also need to catch up on any other startup actions that we've # missed. Most likely, this is only necessary for those applications which have applications mounted on them. # # TODO: RUBY-1356 # # @param mod [Module] the module or class that was just loaded def register_late_framework mod return unless mod module_name = mod.cs__name # Otherwise, check if the provided module_name requires us to register a new support SUPPORTED_FRAMEWORKS.each do |framework| next if @_frameworks.include?(framework) next unless module_name == framework.detection_class @_frameworks << framework report = Contrast::Agent::Reporting::ApplicationUpdate.new # This convert here is left as it'll be easier to be replaced when the Library is being changed report.libraries = Contrast::Agent::Inventory::DependencyAnalysis.instance.library_pb_list [report, Contrast::Agent::Reporting::ApplicationInventory.new].each do |e| Contrast::Agent.reporter.send_event(e) end logger.info('Framework detected after initialization. Enabling support.', framework: framework.detection_class, frameworks: @_frameworks) break end rescue StandardError => e logger.warn('Unable to register a late framework', e, module: mod.cs__name) end end end end