# Copyright (c) 2020 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details. # frozen_string_literal: true require 'contrast/framework/sinatra/support' module Contrast module Framework module Sinatra module Patch # Our patch into the Sinatra::Base Class, allowing for the inventory of the # routes called by the application module Base class << self # Use logic copied from Sinatra::Base#process_route to determine which # pattern matches the request being invoked by Sinatra::Base#call! # # @param clazz [Class] the Class that extends Sinatra::Base in # which this route is defined. # @param settings [Object] the Sinatra::Base @settings object. # @param args [Array] we rely on the @settings object in # Sinatra::Base and the env variable passed in as args[0]. def map_route clazz, settings, *args context = Contrast::Agent::REQUEST_TRACKER.current return unless context env = args[0] return unless env return unless settings convert_routes(context, clazz, settings, env) rescue StandardError # Being careful here since we're directly patching something end def instrument @_instrument ||= begin ::Sinatra::Base.class_eval do alias_method :cs__patched_sinatra_base_call!, :call! # publicly available method for Sinatra::Base things -- unfortunately, # getting the routes appear to require a lookup every time def call! *args Contrast::Framework::Sinatra::Patch::Base.map_route(cs__class, settings, *args) cs__patched_sinatra_base_call!(*args) end end true end end private # Find the route that matches this request. Since we're using # settings, we should resolve in the same precedence as Sinatra def convert_routes context, clazz, settings, env # There isn't a Request object in the Sinatra::Base yet - it's made # during the #call! method, which we're patching - so we need to # access the env method = env[::Rack::REQUEST_METHOD] # get all potential routes for this request type routes = settings.routes[method] return unless routes route = env[::Rack::PATH_INFO] route = route.to_s # Do some cleanup that matches Sinatra::Base#process_route route = '/' if route.empty? && !settings.empty_path_info? route = route[0..-2] if !settings.strict_paths? && route != '/' && route.end_with?('/') routes.each do |pattern, _, _| # Mustermann::Sinatra next unless pattern.params(route) dtm = Contrast::Api::Dtm::RouteCoverage.from_sinatra_route(clazz, method, pattern) context.append_route_coverage(dtm) break end end end end end end end end