Skip to content

Commit 5f7f1f4

Browse files
committed
Revert "Merge pull request #4944 from DataDog/revert-endpoint-collection"
This reverts commit 49a5c68, reversing changes made to eae7fc6.
1 parent 49a5c68 commit 5f7f1f4

File tree

10 files changed

+463
-0
lines changed

10 files changed

+463
-0
lines changed
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# frozen_string_literal: true
2+
3+
module Datadog
4+
module AppSec
5+
module APISecurity
6+
module EndpointCollection
7+
# This module serializes Rails Journey Router routes.
8+
class RailsRoutesSerializer
9+
FORMAT_SUFFIX = "(.:format)"
10+
11+
def initialize(routes)
12+
@routes = routes
13+
end
14+
15+
def to_enum
16+
Enumerator.new do |yielder|
17+
@routes.each do |route|
18+
next unless route.dispatcher?
19+
20+
yielder.yield serialize_route(route)
21+
end
22+
end
23+
end
24+
25+
private
26+
27+
def serialize_route(route)
28+
method = route.verb.empty? ? "*" : route.verb
29+
path = route.path.spec.to_s.delete_suffix(FORMAT_SUFFIX)
30+
31+
{
32+
type: "REST",
33+
resource_name: "#{method} #{path}",
34+
operation_name: "http.request",
35+
method: method,
36+
path: path
37+
}
38+
end
39+
end
40+
end
41+
end
42+
end
43+
end

lib/datadog/appsec/configuration/settings.rb

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -352,6 +352,15 @@ def self.add_settings!(base)
352352
o.default true
353353
end
354354

355+
settings :endpoint_collection do
356+
# Enables reporting of application routes at application start via telemetry
357+
option :enabled do |o|
358+
o.type :bool, nilable: true
359+
o.env 'DD_API_SECURITY_ENDPOINT_COLLECTION_ENABLED'
360+
o.default false
361+
end
362+
end
363+
355364
# NOTE: Unfortunately, we have to go with Float due to other libs
356365
# setup, even tho we don't plan to support sub-second delays.
357366
#

lib/datadog/appsec/contrib/rails/patcher.rb

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
require_relative 'gateway/request'
1111
require_relative 'patches/render_to_body_patch'
1212
require_relative 'patches/process_action_patch'
13+
require_relative '../../api_security/endpoint_collection/rails_routes_serializer'
1314

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

@@ -20,6 +21,7 @@ module Rails
2021
# Patcher for AppSec on Rails
2122
module Patcher
2223
GUARD_ACTION_CONTROLLER_ONCE_PER_APP = Hash.new { |h, key| h[key] = Datadog::Core::Utils::OnlyOnce.new }
24+
GUARD_ROUTES_REPORTING_ONCE_PER_APP = Hash.new { |h, key| h[key] = Datadog::Core::Utils::OnlyOnce.new }
2325
BEFORE_INITIALIZE_ONLY_ONCE_PER_APP = Hash.new { |h, key| h[key] = Datadog::Core::Utils::OnlyOnce.new }
2426
AFTER_INITIALIZE_ONLY_ONCE_PER_APP = Hash.new { |h, key| h[key] = Datadog::Core::Utils::OnlyOnce.new }
2527

@@ -38,6 +40,7 @@ def patch
3840
patch_before_initialize
3941
patch_after_initialize
4042
patch_action_controller
43+
subscribe_to_routes_loaded
4144

4245
Patcher.instance_variable_set(:@patched, true)
4346
end
@@ -128,6 +131,31 @@ def patch_action_controller
128131
GUARD_ACTION_CONTROLLER_ONCE_PER_APP[self].run do
129132
::ActionController::Base.prepend(Patches::RenderToBodyPatch)
130133
end
134+
135+
# Rails 7.1 adds `after_routes_loaded` hook
136+
if Datadog::AppSec::Contrib::Rails::Patcher.target_version < Gem::Version.new('7.1')
137+
Datadog::AppSec::Contrib::Rails::Patcher.report_routes_via_telemetry(::Rails.application.routes.routes)
138+
end
139+
end
140+
end
141+
142+
def subscribe_to_routes_loaded
143+
::ActiveSupport.on_load(:after_routes_loaded) do |app|
144+
Datadog::AppSec::Contrib::Rails::Patcher.report_routes_via_telemetry(app.routes.routes)
145+
end
146+
end
147+
148+
def report_routes_via_telemetry(routes)
149+
# We do not support Rails 4.x for Endpoint Collection,
150+
# mainly because the Route#verb was a Regexp before Rails 5.0
151+
return if target_version < Gem::Version.new('5.0')
152+
153+
return unless Datadog.configuration.appsec.api_security.endpoint_collection.enabled
154+
155+
GUARD_ROUTES_REPORTING_ONCE_PER_APP[self].run do
156+
AppSec.telemetry.app_endpoints_loaded(
157+
APISecurity::EndpointCollection::RailsRoutesSerializer.new(routes).to_enum
158+
)
131159
end
132160
end
133161

lib/datadog/core/configuration/supported_configurations.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ module Configuration
1010
{"DD_AGENT_HOST" => {version: ["A"]},
1111
"DD_API_KEY" => {version: ["A"]},
1212
"DD_API_SECURITY_ENABLED" => {version: ["A"]},
13+
"DD_API_SECURITY_ENDPOINT_COLLECTION_ENABLED" => {version: ["A"]},
1314
"DD_API_SECURITY_REQUEST_SAMPLE_RATE" => {version: ["A"]},
1415
"DD_API_SECURITY_SAMPLE_DELAY" => {version: ["A"]},
1516
"DD_APM_TRACING_ENABLED" => {version: ["A"]},
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
module Datadog
2+
module AppSec
3+
module APISecurity
4+
module EndpointCollection
5+
interface _Route
6+
def dispatcher?: () -> bool
7+
def verb: () -> String
8+
def path: () -> _RoutePath
9+
end
10+
11+
interface _RoutePath
12+
def spec: () -> _RouteSpec
13+
end
14+
15+
interface _RouteSpec
16+
def to_s: () -> String
17+
end
18+
19+
class RailsRoutesSerializer
20+
FORMAT_SUFFIX: String
21+
22+
@routes: Array[_Route]
23+
24+
def initialize: (Array[_Route] routes) -> void
25+
26+
def to_enum: () -> Enumerator[Core::Telemetry::Event::AppEndpointsLoaded::endpoint]
27+
28+
def serialize_route: (_Route route) -> Core::Telemetry::Event::AppEndpointsLoaded::endpoint
29+
end
30+
end
31+
end
32+
end
33+
end

sig/datadog/appsec/contrib/rails/patcher.rbs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@ module Datadog
2727

2828
def self?.after_initialize: (untyped app) -> untyped
2929

30+
def self.subscribe_to_routes_loaded: () -> void
31+
32+
def self.report_routes_via_telemetry: () -> void
33+
3034
def self?.setup_security: () -> untyped
3135
end
3236
end
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
# frozen_string_literal: true
2+
3+
require 'spec_helper'
4+
require 'datadog/appsec/api_security/endpoint_collection/rails_routes_serializer'
5+
6+
RSpec.describe Datadog::AppSec::APISecurity::EndpointCollection::RailsRoutesSerializer do
7+
describe '#to_enum' do
8+
it 'returns an Enumerator' do
9+
expect(described_class.new([]).to_enum).to be_a(Enumerator)
10+
end
11+
12+
it 'correctly serializes routes' do
13+
routes = described_class.new([
14+
build_route_double(method: 'GET', path: '/events')
15+
]).to_enum
16+
17+
expect(routes.count).to eq(1)
18+
19+
aggregate_failures 'serialized attributes' do
20+
expect(routes.first.fetch(:type)).to eq('REST')
21+
expect(routes.first.fetch(:resource_name)).to eq('GET /events')
22+
expect(routes.first.fetch(:operation_name)).to eq('http.request')
23+
expect(routes.first.fetch(:method)).to eq('GET')
24+
expect(routes.first.fetch(:path)).to eq('/events')
25+
end
26+
end
27+
28+
it 'removes rails format suffix from the path' do
29+
routes = described_class.new([
30+
build_route_double(method: 'GET', path: '/events(.:format)')
31+
]).to_enum
32+
33+
aggregate_failures 'path attributes' do
34+
expect(routes.first.fetch(:resource_name)).to eq('GET /events')
35+
expect(routes.first.fetch(:path)).to eq('/events')
36+
end
37+
end
38+
39+
it 'sets method to * for wildcard routes' do
40+
routes = described_class.new([
41+
build_route_double(method: '*', path: '/')
42+
]).to_enum
43+
44+
aggregate_failures 'path attributes' do
45+
expect(routes.first.fetch(:resource_name)).to eq('* /')
46+
expect(routes.first.fetch(:method)).to eq('*')
47+
end
48+
end
49+
50+
it 'skips non-dispatcher routes for now' do
51+
routes = described_class.new([
52+
build_route_double(method: nil, path: 'admin', is_dispatcher: false)
53+
]).to_enum
54+
55+
expect(routes.to_a).to be_empty
56+
end
57+
end
58+
59+
def build_route_double(method:, path:, is_dispatcher: true)
60+
instance_double(
61+
'ActionDispatch::Journey::Route',
62+
dispatcher?: is_dispatcher,
63+
verb: method,
64+
path: instance_double(
65+
'ActionDispatch::Journey::Path::Pattern',
66+
spec: path
67+
)
68+
)
69+
end
70+
end

spec/datadog/appsec/configuration/settings_spec.rb

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1015,6 +1015,44 @@ def patcher
10151015
end
10161016
end
10171017
end
1018+
1019+
describe 'endpoint_collection' do
1020+
describe '#enabled' do
1021+
context 'when DD_API_SECURITY_ENDPOINT_COLLECTION_ENABLED is undefined' do
1022+
around do |example|
1023+
ClimateControl.modify('DD_API_SECURITY_ENDPOINT_COLLECTION_ENABLED' => nil) { example.run }
1024+
end
1025+
1026+
it { expect(settings.appsec.api_security.endpoint_collection.enabled).to eq(false) }
1027+
end
1028+
1029+
context 'when DD_API_SECURITY_ENDPOINT_COLLECTION_ENABLED is set to true' do
1030+
around do |example|
1031+
ClimateControl.modify('DD_API_SECURITY_ENDPOINT_COLLECTION_ENABLED' => 'true') { example.run }
1032+
end
1033+
1034+
it { expect(settings.appsec.api_security.endpoint_collection.enabled).to eq(true) }
1035+
end
1036+
1037+
context 'when DD_API_SECURITY_ENDPOINT_COLLECTION_ENABLED is set to false' do
1038+
around do |example|
1039+
ClimateControl.modify('DD_API_SECURITY_ENDPOINT_COLLECTION_ENABLED' => 'false') { example.run }
1040+
end
1041+
1042+
it { expect(settings.appsec.api_security.endpoint_collection.enabled).to eq(false) }
1043+
end
1044+
end
1045+
1046+
describe '#enabled=' do
1047+
[true, false].each do |value|
1048+
context "when given #{value}" do
1049+
before { settings.appsec.api_security.endpoint_collection.enabled = value }
1050+
1051+
it { expect(settings.appsec.api_security.endpoint_collection.enabled).to eq(value) }
1052+
end
1053+
end
1054+
end
1055+
end
10181056
end
10191057

10201058
describe 'sca' do

0 commit comments

Comments
 (0)