# Copyright (c) 2023 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details. # frozen_string_literal: true require 'json' require 'contrast/agent/reporting/reporting_events/reporting_event' require 'contrast/utils/object_share' require 'contrast/utils/duck_utils' module Contrast module Agent module Reporting # This is the new Discovered Route class which will include all the needed information for the new reporting # system to relay this information in the Route Observation messages. These observations are used by TeamServer # to construct the route coverage information for the assess feature. They represent those methods which map to # externally accessible endpoints within the application, as registered to the application framework. This also # includes the literal URL and HTTP Verb used to invoke them, as they must have been called at this point to be # recorded. class DiscoveredRoute < Contrast::Agent::Reporting::ObservedRoute class << self # @param obj [Regexp, Object] # @return [String] def source_or_string obj if obj.cs__is_a?(Regexp) obj.source elsif obj.cs__respond_to?(:safe_string) obj.safe_string else obj.to_s end end # Convert ActionDispatch::Journey::Route to Contrast::Agent::Reporting::DiscoveredRoute # # @param journey_obj [ActionDispatch::Journey::Route] a rails route # @param url [String, nil] use url from string instead of journey object. # @return [Contrast::Agent::Reporting::DiscoveredRoute] def from_action_dispatch_journey journey_obj, url = nil msg = new msg.signature = "#{ journey_obj.defaults[:controller] }##{ journey_obj.defaults[:action] }" verb = source_or_string(journey_obj.verb) msg.verb = Contrast::Utils::StringUtils.force_utf8(verb) url ||= source_or_string(journey_obj.path.spec) msg.url = Contrast::Utils::StringUtils.force_utf8(url) msg end # Convert Grape route data to discovered route. # # @param controller [::Grape::API] the route's final controller. # @param method [String] GET, PUT, POST, etc... # @param url [String, nil] use url from string instead matched pattern. # @param pattern [String, Grape::Router::Route] the pattern that was matched in routing. # @return [Contrast::Agent::Reporting::DiscoveredRoute] def from_grape_controller controller, method, pattern, url = nil if pattern.cs__is_a?(Grape::Router::Route) safe_pattern = pattern.pattern&.path&.to_s safe_url = source_or_string(url || safe_pattern) else safe_pattern = source_or_string(pattern) safe_url = source_or_string(url || pattern) end msg = new msg.signature = "#{ controller }##{ method } #{ safe_pattern }" msg.verb = Contrast::Utils::StringUtils.force_utf8(method) msg.url = Contrast::Utils::StringUtils.force_utf8(safe_url) msg end # Convert Sinatra route data to discovered route. # # @param controller [::Sinatra::Base] the route's final controller. # @param method [String] GET, PUT, POST, etc... # @param pattern [::Mustermann::Sinatra] the pattern that was matched in routing. # @param url [String, nil] use url from string instead matched pattern. # @return [Contrast::Agent::Reporting::DiscoveredRoute] def from_sinatra_route controller, method, pattern, url = nil safe_pattern = source_or_string(pattern) safe_url = source_or_string(url || pattern) msg = new msg.signature = "#{ controller }##{ method } #{ safe_pattern }" msg.verb = Contrast::Utils::StringUtils.force_utf8(method) msg.url = Contrast::Utils::StringUtils.force_utf8(safe_url) msg end end # @return [String] the controller, method, and pattern of this route attr_accessor :signature # @return [String] the url (or url pattern) used to access this route attr_accessor :url # @return [String, nil] the HTTP verb used to access this route or nil for any verb attr_accessor :verb def initialize @event_type = :discovered_route @verb = Contrast::Utils::ObjectShare::EMPTY_STRING super() end def to_controlled_hash validate { session_id: ::Contrast::ASSESS.session_id, signature: @signature, verb: @verb, url: @url }.compact end def validate if Contrast::Utils::DuckUtils.empty_duck?(signature) raise(ArgumentError, "#{ self } did not have a proper signature. Unable to continue.") end if Contrast::Utils::DuckUtils.empty_duck?(url) raise(ArgumentError, "#{ self } did not have a proper url. Unable to continue.") end nil end end end end end