# Copyright (c) 2023 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details. # frozen_string_literal: true require 'contrast/framework/base_support' require 'contrast/components/logger' module Contrast module Framework module Grape # Used when Grape is present to define framework specific behaviour class Support extend Contrast::Framework::BaseSupport class << self include Contrast::Components::Logger::InstanceMethods def detection_class 'Grape::API' end def version ::Grape::VERSION end def application_name app_class&.cs__name end def application_root app_instance&.root end def server_type 'grape' end # Given an object, determine if it is a Grape controller. # Which could include cases of ::Grape::API subclass or actual class # # @param app [Object] suspected Grape app. # @return [Boolean] def grape_controller? app # Grape is loaded? return false unless grape_defined? # App is a subclass of or actually is ::Grape::API. return false unless app.cs__respond_to?(:<=) && app <= ::Grape::API true end # Find all classes that subclass ::Grape::API, Gather their routes # # @return [Array] def collect_routes return Contrast::Utils::ObjectShare::EMPTY_ARRAY unless grape_defined? # Each Grape controller has endpoints and each endpoints has routes # and that's why we need to go through each one and create separate RouteCoverage object routes = [] grape_controllers.each do |c| c&.endpoints&.each do |endpoint| endpoint&.routes&.map do |r| pattern = r.pattern.pattern routes << Contrast::Agent::Reporting::DiscoveredRoute.from_grape_controller(c, r.request_method, pattern, r.path) end end end routes end # Given the current request - return a RouteCoverage object # @param request [Contrast::Agent::Request] a contrast tracked request. # @param controller [::Grape::API] optionally use this controller instead of global ::Grape::API. # @return [Contrast::Agent::Reporting::RouteCoverage, nil] the route coverage object or nil if no route def current_route_coverage request, controller = ::Grape::API, full_route = nil return unless grape_controller?(controller) method = request.env[::Rack::REQUEST_METHOD] # GET, PUT, POST, etc... # Find final controller - actually we gotta match the route to the scanned application # Initially Grape compiles all routes on startup, so we can use the url from the request # and create the observed route # Class < Grape::API, Grape::Router::Route final_controller, route_pattern = _route_recurse(method, _cleaned_route(request), grape_controllers) return unless final_controller full_route ||= request.env[::Rack::PATH_INFO] new_route_coverage = Contrast::Agent::Reporting::RouteCoverage.new new_route_coverage.attach_rack_based_data(final_controller, method, route_pattern, full_route) new_route_coverage end # Search object space for grape controllers--any class that subclasses ::Grape::API. # # @return [Array<::Grape::API>] grape controllers def grape_controllers ObjectSpace.each_object(Class).select { |klass| klass < ::Grape::API } end # Grape Request inherits the same as the Sinatra, so we can easily call it as it's called in Sinatra def retrieve_request env ::Grape::Request.new(env) end private # Determine if, at the time of our Framework Support determination, Grape has been defined. # # @return [Boolean] def grape_defined? @_grape_defined = !!(defined?(::Grape) && defined?(::Grape::API)) if @_grape_defined.nil? @_grape_defined end # @param method [::Rack::REQUEST_METHOD] GET, POST, PUT, etc... # @param route [String] the relative route passed from Rack. # @param controllers [Array<::Grape::API>] All Grape controllers found # @return [Array[::Grape::API. Grape::Router::Route], nil] Either the controller that will handle the route # along with the route pattern or nil if no match. def _route_recurse method, route, controllers = grape_controllers # return if there aren't any controllers return unless controllers&.any? # Here we can go through the all detected controllers # and find the one that's routes include the current one # Grape controller actually has endpoints and each endpoint # has routes and that's why we need to do it that way controller = controllers.pop return _route_recurse(method, route, controllers) unless controller contr_routes = controller.endpoints&.map(&:routes)&.flatten || [] route_pattern = contr_routes&.find do |r| r.pattern.to_regexp.match(route) # Grape::Router::Route match end return controller, route_pattern unless route_pattern.nil? _route_recurse(method, route, controllers) end # Get route and do some cleaning # # @param request [Contrast::Agent::Request] a contrast tracked request. # @return [String] the extracted and cleaned relative route. def _cleaned_route request route = request.env[::Rack::PATH_INFO] return '/' if route.empty? route.end_with?('/') ? route[0..-2] : route end def app_class return unless grape_defined? app_instance.cs__class end # Search the object space for the controller handling this request which will be # the class inheriting from ::Grape::API with @app=nil # # @return [::Grape::API] the current controller as routed by Rack. def app_instance return unless grape_defined? @_app_instance ||= begin grape_layers = ObjectSpace.each_object(::Grape::API).to_a grape_layers.find { |layer| layer.app.nil? } end end end end end end end