Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions appraisal/ruby-3.3.rb
Original file line number Diff line number Diff line change
Expand Up @@ -239,11 +239,13 @@
appraise 'rails-app' do
gem 'devise', '~> 4.9'
gem 'faraday', '~> 2.0'
gem 'grape' # for endpoint collection tests
gem 'excon', '~> 1.2'
gem 'rest-client'
gem 'rack', '~> 2'
gem 'rack-contrib', '~> 2'
gem 'rack-test' # Dev dependencies for testing rack-based code
gem 'rails', '~> 7.0'
gem 'sinatra' # for endpoint collection tests
gem 'sqlite3', '>= 1.4.2', platform: :ruby
end
2 changes: 2 additions & 0 deletions gemfiles/ruby_3.3_rails_app.gemfile

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

39 changes: 39 additions & 0 deletions gemfiles/ruby_3.3_rails_app.gemfile.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 10 additions & 0 deletions lib/datadog/appsec/api_security/endpoint_collection.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# frozen_string_literal: true

module Datadog
module AppSec
module APISecurity
module EndpointCollection
end
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# frozen_string_literal: true

module Datadog
module AppSec
module APISecurity
module EndpointCollection
# This module serializes Grape routes.
module GrapeRouteSerializer
module_function

def serialize(route, path_prefix: '')
path = path_prefix + route.pattern.origin

{
type: "REST",
resource_name: "#{route.request_method} #{path}",
operation_name: "http.request",
method: route.request_method,
path: path
}
end
end
end
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# frozen_string_literal: true

require_relative 'rails_route_serializer'
require_relative 'grape_route_serializer'
require_relative 'sinatra_route_serializer'

module Datadog
module AppSec
module APISecurity
module EndpointCollection
# This class works with a collection of rails routes
# and produces an Enumerator that yields serialized endpoints.
class RailsCollector
def initialize(routes)
@routes = routes
end

def to_enum
Enumerator.new do |yielder|
@routes.each do |route|
if route.dispatcher?
yielder.yield RailsRouteSerializer.serialize(route)
elsif mounted_grape_app?(route.app.rack_app)
route.app.rack_app.routes.each do |grape_route|
yielder.yield GrapeRouteSerializer.serialize(grape_route, path_prefix: route.path.spec.to_s)
end
elsif mounted_sinatra_app?(route.app.rack_app)
route.app.rack_app.routes.each do |method, sinatra_routes|
next if method == 'HEAD'

sinatra_routes.each do |sinatra_route, _, _|
yielder.yield SinatraRouteSerializer.serialize(
sinatra_route, method: method, path_prefix: route.path.spec.to_s
)
end
end
end
end
end
end

private

def mounted_grape_app?(rack_app)
return false unless defined?(::Grape::API)

rack_app.is_a?(Class) && rack_app < ::Grape::API
end

def mounted_sinatra_app?(rack_app)
return false unless defined?(::Sinatra::Base)

rack_app.is_a?(Class) && rack_app < ::Sinatra::Base
end
end
end
end
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -5,26 +5,12 @@ module AppSec
module APISecurity
module EndpointCollection
# This module serializes Rails Journey Router routes.
class RailsRoutesSerializer
module RailsRouteSerializer
FORMAT_SUFFIX = "(.:format)"

def initialize(routes)
@routes = routes
end

def to_enum
Enumerator.new do |yielder|
@routes.each do |route|
next unless route.dispatcher?

yielder.yield serialize_route(route)
end
end
end

private
module_function

def serialize_route(route)
def serialize(route)
method = route.verb.empty? ? "*" : route.verb
path = route.path.spec.to_s.delete_suffix(FORMAT_SUFFIX)

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# frozen_string_literal: true

module Datadog
module AppSec
module APISecurity
module EndpointCollection
# This module serializes Sinatra routes.
module SinatraRouteSerializer
module_function

def serialize(route, method:, path_prefix: '')
path = path_prefix + route.safe_string

{
type: "REST",
resource_name: "#{method} #{path}",
operation_name: "http.request",
method: method,
path: path
}
end
end
end
end
end
end
4 changes: 2 additions & 2 deletions lib/datadog/appsec/contrib/rails/patcher.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
require_relative 'gateway/request'
require_relative 'patches/render_to_body_patch'
require_relative 'patches/process_action_patch'
require_relative '../../api_security/endpoint_collection/rails_routes_serializer'
require_relative '../../api_security/endpoint_collection/rails_collector'

require_relative '../../../tracing/contrib/rack/middlewares'

Expand Down Expand Up @@ -154,7 +154,7 @@ def report_routes_via_telemetry(routes)

GUARD_ROUTES_REPORTING_ONCE_PER_APP[::Rails.application].run do
AppSec.telemetry.app_endpoints_loaded(
APISecurity::EndpointCollection::RailsRoutesSerializer.new(routes).to_enum
APISecurity::EndpointCollection::RailsCollector.new(routes).to_enum
)
end
rescue => e
Expand Down
45 changes: 45 additions & 0 deletions sig/datadog/appsec/api_security/endpoint_collection.rbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
module Datadog
module AppSec
module APISecurity
module EndpointCollection
interface _RailsRoute
def dispatcher?: () -> bool
def verb: () -> ::String
def path: () -> _RailsRoutePath
def app: () -> _RailsRouteApp
end

interface _RailsRoutePath
def spec: () -> _RailsRouteSpec
end

interface _RailsRouteSpec
def to_s: () -> ::String
end

interface _RailsRouteApp
def rack_app: () -> _RackApp
end

interface _RackApp
def is_a?: (Class) -> bool

def routes: () -> untyped
end

interface _GrapeRoute
def request_method: () -> String
def pattern: () -> _GrapeRoutePattern
end

interface _GrapeRoutePattern
def origin: () -> String
end

interface _SinatraRoute
def safe_string: () -> String
end
end
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
module Datadog
module AppSec
module APISecurity
module EndpointCollection
module GrapeRouteSerializer
def self?.serialize: (_GrapeRoute route, ?path_prefix: ::String) -> Core::Telemetry::Event::AppEndpointsLoaded::endpoint
end
end
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
module Datadog
module AppSec
module APISecurity
module EndpointCollection
class RailsCollector
@routes: Array[_RailsRoute]

def initialize: (Array[_RailsRoute] routes) -> void

def to_enum: () -> Enumerator[Core::Telemetry::Event::AppEndpointsLoaded::endpoint]

private

def mounted_grape_app?: (_RackApp rack_app) -> bool

def mounted_sinatra_app?: (_RackApp rack_app) -> bool
end
end
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
module Datadog
module AppSec
module APISecurity
module EndpointCollection
module RailsRouteSerializer
FORMAT_SUFFIX: ::String

def self?.serialize: (_RailsRoute route) -> Core::Telemetry::Event::AppEndpointsLoaded::endpoint
end
end
end
end
end
Loading