Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
3e61333
Implement normalised http routes
estringana Aug 10, 2026
65220f2
Fix pipeline errors
estringana Aug 12, 2026
da49083
test: add APPSEC_NORMALIZED_ROUTE assertions to remaining test files
estringana Aug 12, 2026
8a07b6a
Fix laminas
estringana Aug 13, 2026
b4ac068
Tiny up the PR
estringana Aug 13, 2026
a71a454
Fix pipeline
estringana Aug 13, 2026
c1d1fed
Remove non required changes
estringana Aug 13, 2026
dbea861
Fix special route
estringana Aug 17, 2026
3ba2da9
Refine slim routes
estringana Aug 17, 2026
5d777f0
Fix codeigniter
estringana Aug 17, 2026
309f833
Fix wordpress
estringana Aug 17, 2026
3c36a6e
Fix pipeline
estringana Aug 17, 2026
ec3960b
Fix pipeline
estringana Aug 17, 2026
c44b3bb
Fix wordpress
estringana Aug 17, 2026
561896b
Improve pr
estringana Aug 17, 2026
01bb9f0
Fix PR
estringana Aug 17, 2026
1d3add3
Fix laminas
estringana Aug 17, 2026
7646665
Implement a route caching system
estringana Aug 18, 2026
f6bcae6
Fix pipeline
estringana Aug 18, 2026
a14c69a
Fix pipeline
estringana Aug 18, 2026
c7270d7
Fix pipeline
estringana Aug 18, 2026
7209ed2
Add cache to wordpress and Slim
estringana Aug 19, 2026
39ba537
Make hashtable thread safety
estringana Aug 19, 2026
2cd7c31
Fix pipeline
estringana Aug 19, 2026
750ed5d
Fix pipeline
estringana Aug 19, 2026
2a85174
Fix pipeline
estringana Aug 19, 2026
e4ee771
Fix codeigniter
estringana Aug 20, 2026
f42c408
Add appsec integration tests
estringana Aug 26, 2026
ba956c4
Fix integration tests
estringana Aug 26, 2026
0273d76
Amend test file name
estringana Aug 27, 2026
e02670a
Generate normalise routes only when appsec is enabled
estringana Aug 27, 2026
cd10921
Improve route generation
estringana Aug 27, 2026
9cdee15
Improve laminas integration
estringana Aug 27, 2026
0fd44d6
Improve Symfony
estringana Aug 27, 2026
fcbd559
Fix pipeline
estringana Aug 27, 2026
6924555
Add more tests
estringana Aug 27, 2026
5ed13f3
Add more tests
estringana Aug 27, 2026
d5e909b
Fix pipeline
estringana Aug 27, 2026
be6fad4
Fix pipeline
estringana Aug 28, 2026
d30a83b
Address report comments
estringana Aug 28, 2026
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
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ class Laminas33Tests {
endpoints.size() > 0
})

assert endpoints.size() == 26
assert endpoints.size() == 32
assert endpoints.find { it.path == '/' && it.method == '*' && it.operationName == 'http.request' && it.resourceName == '* /' } != null
assert endpoints.find {
it.path == '/application[/:action]' && it.method == '*' && it.operationName == 'http.request' && it.resourceName == '* /application[/:action]'
Expand Down Expand Up @@ -124,6 +124,30 @@ class Laminas33Tests {
assert endpoints.find {
it.path == '/any-verb' && it.method == '*' && it.operationName == 'http.request' && it.resourceName == '* /any-verb'
} != null
assert endpoints.find {
it.path == '/normalized-regex/%id%.%format%' && it.method == '*' &&
it.operationName == 'http.request' && it.resourceName == '* /normalized-regex/%id%.%format%'
} != null
assert endpoints.find {
it.path == '/normalized-encoded[/:slug]' && it.method == '*' &&
it.operationName == 'http.request' && it.resourceName == '* /normalized-encoded[/:slug]'
} != null
assert endpoints.find {
it.path == '/normalized-static[/draft]' && it.method == '*' &&
it.operationName == 'http.request' && it.resourceName == '* /normalized-static[/draft]'
} != null
assert endpoints.find {
it.path == '/normalized-name/:user-id' && it.method == '*' &&
it.operationName == 'http.request' && it.resourceName == '* /normalized-name/:user-id'
} != null
assert endpoints.find {
it.path == '/normalized-wildcard/:param1' && it.method == '*' &&
it.operationName == 'http.request' && it.resourceName == '* /normalized-wildcard/:param1'
} != null
assert endpoints.find {
it.path == '/normalized-wildcard/:param1/*' && it.method == '*' &&
it.operationName == 'http.request' && it.resourceName == '* /normalized-wildcard/:param1/*'
} != null
}

@Test
Expand Down Expand Up @@ -231,6 +255,7 @@ class Laminas33Tests {
assert span.meta.'_dd.appsec.event_rules.version' != ''
assert span.meta.'appsec.blocked' == 'true'
assert span.meta.'http.route' == '/dynamic-path[/:param01]'
assert span.meta.'_dd.appsec.normalized_route' == '/dynamic-path/{param01}'
}

@Test
Expand All @@ -241,12 +266,14 @@ class Laminas33Tests {
assert resp.statusCode() == 200
}
assert nestedTrace.first().meta.'http.route' == '/resource/:resourceId/:subId'
assert nestedTrace.first().meta.'_dd.appsec.normalized_route' == '/resource/{resourceId}/{subId}'

HttpRequest chainReq = container.buildReq('/chain/abc').GET().build()
Trace chainTrace = container.traceFromRequest(chainReq, ofString()) { HttpResponse<String> resp ->
assert resp.statusCode() == 200
}
assert chainTrace.first().meta.'http.route' == '/chain/:chainId'
assert chainTrace.first().meta.'_dd.appsec.normalized_route' == '/chain/{chainId}'
}

@Test
Expand All @@ -271,26 +298,124 @@ class Laminas33Tests {
assert resp.statusCode() == 200
}
assert regexTrace.first().meta.'http.route' == '/regex-year/%year%'
assert regexTrace.first().meta.'_dd.appsec.normalized_route' == '/regex-year/{year}'

Trace schemeTrace = container.traceFromRequest(
container.buildReq('/scheme-only-page').GET().build(),
ofString()) { HttpResponse<String> resp ->
assert resp.statusCode() == 200
}
assert schemeTrace.first().meta.'http.route' == '/scheme-only-page'
assert schemeTrace.first().meta.'_dd.appsec.normalized_route' == '/scheme-only-page'

Trace placeholderTrace = container.traceFromRequest(
container.buildReq('/placeholder-literal').GET().build(),
ofString()) { HttpResponse<String> resp ->
assert resp.statusCode() == 200
}
assert placeholderTrace.first().meta.'http.route' == '/placeholder-literal'
assert placeholderTrace.first().meta.'_dd.appsec.normalized_route' == '/placeholder-literal'

Trace wildcardTrace = container.traceFromRequest(
container.buildReq('/wildcard-keys/foo/bar').GET().build(),
ofString()) { HttpResponse<String> resp ->
assert resp.statusCode() == 200
}
assert wildcardTrace.first().meta.'http.route' == '/wildcard-keys/*'
assert wildcardTrace.first().meta.'_dd.appsec.normalized_route' == '/wildcard-keys/{param1}'
}

@Test
@Order(11)
void 'optional segment absent produces correct normalized route'() {
// /application[/:action] with no action in URL — optional section dropped
// (default action=index is injected by the router but /index is not in the URL path)
Trace trace = container.traceFromRequest(
container.buildReq('/application').GET().build(),
ofString()) { HttpResponse<String> resp ->
assert resp.statusCode() == 200
}
assert trace.first().meta.'http.route' == '/application[/:action]'
assert trace.first().meta.'_dd.appsec.normalized_route' == '/application'
}

@Test
@Order(12)
void 'optional segment present produces correct normalized route'() {
// /application[/:action] with action in URL — optional section expanded
Trace trace = container.traceFromRequest(
container.buildReq('/application/hello').GET().build(),
ofString()) { HttpResponse<String> resp ->
assert resp.statusCode() == 200
}
assert trace.first().meta.'http.route' == '/application[/:action]'
assert trace.first().meta.'_dd.appsec.normalized_route' == '/application/{action}'
}

@Test
@Order(13)
void 'optional regex capture absent is omitted from normalized route'() {
Trace trace = container.traceFromRequest(
container.buildReq('/normalized-regex/article').GET().build(),
ofString()) { HttpResponse<String> resp ->
assert resp.statusCode() == 200
}

assert trace.first().meta.'http.route' == '/normalized-regex/%id%.%format%'
assert trace.first().meta.'_dd.appsec.normalized_route' == '/normalized-regex/{id}'
}

@Test
@Order(14)
void 'encoded optional value is recognized as present'() {
Trace trace = container.traceFromRequest(
container.buildReq('/normalized-encoded/a%20b').GET().build(),
ofString()) { HttpResponse<String> resp ->
assert resp.statusCode() == 200
}

assert trace.first().meta.'http.route' == '/normalized-encoded[/:slug]'
assert trace.first().meta.'_dd.appsec.normalized_route' == '/normalized-encoded/{slug}'
}

@Test
@Order(15)
void 'static-only optional segment absent is omitted'() {
Trace trace = container.traceFromRequest(
container.buildReq('/normalized-static').GET().build(),
ofString()) { HttpResponse<String> resp ->
assert resp.statusCode() == 200
}

assert trace.first().meta.'http.route' == '/normalized-static[/draft]'
assert trace.first().meta.'_dd.appsec.normalized_route' == '/normalized-static'
}

@Test
@Order(16)
void 'hyphenated segment parameter name remains intact'() {
Trace trace = container.traceFromRequest(
container.buildReq('/normalized-name/alice').GET().build(),
ofString()) { HttpResponse<String> resp ->
assert resp.statusCode() == 200
}

assert trace.first().meta.'http.route' == '/normalized-name/:user-id'
assert trace.first().meta.'_dd.appsec.normalized_route' == '/normalized-name/{user-id}'
}

@Test
@Order(17)
void 'wildcard placeholder does not collide with an existing parameter name'() {
Trace trace = container.traceFromRequest(
container.buildReq('/normalized-wildcard/value/foo/bar').GET().build(),
ofString()) { HttpResponse<String> resp ->
assert resp.statusCode() == 200
}

assert trace.first().meta.'http.route' == '/normalized-wildcard/:param1/*'
assert trace.first().meta.'_dd.appsec.normalized_route' ==
'/normalized-wildcard/{param1}/{param2}'
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,10 @@ class Laravel8xTests {
assert span.metrics."_dd.appsec.waf.duration" > 0.0d
assert span.meta."_dd.appsec.event_rules.version" != ''
assert span.meta."appsec.blocked" == "true"
// Laravel uri() returns the route without a leading slash
assert span.meta."http.route" == 'dynamic-path/{param01}'
// Normalizer adds the leading slash and keeps {param01} as-is
assert span.meta."_dd.appsec.normalized_route" == '/dynamic-path/{param01}'
}

@Test
Expand Down Expand Up @@ -209,12 +213,59 @@ class Laravel8xTests {
endpoints.size() > 0
})

assert endpoints.size() == 6
assert endpoints.size() == 8
assert endpoints.find { it.path == '/' && it.method == 'GET' && it.operationName == 'http.request' && it.resourceName == 'GET /' } != null
assert endpoints.find { it.path == 'authenticate' && it.method == 'GET' && it.operationName == 'http.request' && it.resourceName == 'GET authenticate' } != null
assert endpoints.find { it.path == 'register' && it.method == 'GET' && it.operationName == 'http.request' && it.resourceName == 'GET register' } != null
assert endpoints.find { it.path == 'dynamic-path/{param01}' && it.method == 'GET' && it.operationName == 'http.request' && it.resourceName == 'GET dynamic-path/{param01}' } != null
assert endpoints.find { it.path == 'sanctum/csrf-cookie' && it.method == 'GET' && it.operationName == 'http.request' && it.resourceName == 'GET sanctum/csrf-cookie' } != null
assert endpoints.find { it.path == 'api/user' && it.method == 'GET' && it.operationName == 'http.request' && it.resourceName == 'GET api/user' } != null
assert endpoints.find { it.path == 'normalized-optional/{value?}' && it.method == 'GET' && it.operationName == 'http.request' && it.resourceName == 'GET normalized-optional/{value?}' } != null
assert endpoints.find { it.path == 'normalized-default/{format?}' && it.method == 'GET' && it.operationName == 'http.request' && it.resourceName == 'GET normalized-default/{format?}' } != null
}

@Test
@Order(10)
void 'optional param present produces correct normalized route'() {
HttpRequest req = container.buildReq('/normalized-optional/hello').GET().build()
Trace trace = container.traceFromRequest(req, ofString()) { HttpResponse<String> re ->
assert re.statusCode() == 200
assert re.body() == 'hello'
}

Span span = trace.first()
assert span.meta.'http.route' == 'normalized-optional/{value?}'
assert span.meta.'_dd.appsec.normalized_route' == '/normalized-optional/{value}'
}

@Test
@Order(11)
void 'optional param absent produces correct normalized route'() {
HttpRequest req = container.buildReq('/normalized-optional').GET().build()
Trace trace = container.traceFromRequest(req, ofString()) { HttpResponse<String> re ->
assert re.statusCode() == 200
assert re.body() == 'absent'
}

Span span = trace.first()
assert span.meta.'http.route' == 'normalized-optional/{value?}'
assert span.meta.'_dd.appsec.normalized_route' == '/normalized-optional'
}

@Test
@Order(12)
void 'defaulted optional absent from URL produces normalized route without the param'() {
// The route uses ->defaults('format', 'html'). When the URL has no {format?} segment,
// Laravel injects 'html' into $route->parameters() — but the param is absent from the URL.
// The normalized route must not include {format} in this case.
HttpRequest req = container.buildReq('/normalized-default').GET().build()
Trace trace = container.traceFromRequest(req, ofString()) { HttpResponse<String> re ->
assert re.statusCode() == 200
assert re.body() == 'html'
}

Span span = trace.first()
assert span.meta.'http.route' == 'normalized-default/{format?}'
assert span.meta.'_dd.appsec.normalized_route' == '/normalized-default'
}
}
Loading
Loading