# 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/platform_version' 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 def find_route_discovery_data routes_for_all_frameworks end def platform_version framework_version = first_framework_result :version, '' Contrast::Framework::PlatformVersion.from_string(framework_version) end def platform_version_string first_framework_result :version, '' 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 the registered routes of that framework now that we know we need to find them # TODO: RUBY-1438 -- remove and build ReportingEvent directly app_update_msg = Contrast::Api::Dtm::ApplicationUpdate.build if Contrast::Agent.reporter report = Contrast::Agent::Reporting::DtmMessage.dtm_to_event(app_update_msg) Contrast::Agent.reporter.send_event(report) # This is being reported separately because we extract the data from the same message inventory_report = Contrast::Agent::Reporting::ApplicationInventory.convert(app_update_msg) Contrast::Agent.reporter.send_event(inventory_report) else Contrast::Agent.messaging_queue.send_event_eventually(app_update_msg) 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