[AppSec] APPSEC-65483 Collect Datadog security-testing headers on entry spans#8682
Conversation
APPSEC-65483 Tag `x-datadog-endpoint-scan` and `x-datadog-security-test` HTTP request headers as `http.request.headers.<name>` on every HTTP server entry span (and on the inferred-proxy span when one is created), unconditionally — independent of `DD_TRACE_HEADER_TAGS` and AppSec enablement. These markers let the API endpoint reducer distinguish Datadog scan/test traffic from real user traffic and keep it out of the API inventory. Wired into: ASP.NET MVC, ASP.NET Web API 2, OWIN/IIS classic, ASP.NET Core, WCF over HTTP. Markers are not propagated downstream. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This comment has been minimized.
This comment has been minimized.
System.ValueTuple<> is not in the net461 BCL, so the `(string Header, string Tag)[]` precomputed-tags array broke the Windows build. Switch to a small private struct matching the existing `Key` struct pattern in the same file. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…/appsec-65483-security-testing-headers
andrewlock
left a comment
There was a problem hiding this comment.
LGTM, I wonder if we could/should get rid of all the looping given it's only 2 headers, but not required
Per @andrewlock's review on #8682: inline both header lookups directly in AddSecurityTestingHeadersAsTags instead of iterating a precomputed (name, tag) array. Header / tag names become compile-time string constants, the SecurityTestingHeader struct and the static array are removed. Each call site uses a single `is IList<string> { Count: > 0 }` pattern match — covers every in-tree server carrier (string[], boxed StringValues, BCL HttpHeaders.TryGetValues materialization) without per-request enumerator allocation. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Replying to @andrewlock's two open threads here so they're easy to find: Inlining suggestion (
|
BenchmarksBenchmark execution time: 2026-05-26 19:17:47 Comparing candidate commit 53bda94 in PR branch Found 0 performance improvements and 1 performance regressions! Performance is the same for 71 metrics, 0 unstable metrics, 59 known flaky benchmarks, 67 flaky benchmarks without significant changes.
|
Execution-Time Benchmarks Report ⏱️Execution-time results for samples comparing This PR (8682) and master. ✅ No regressions detected - check the details below Full Metrics ComparisonFakeDbCommand
HttpMessageHandler
Comparison explanationExecution-time benchmarks measure the whole time it takes to execute a program, and are intended to measure the one-off costs. Cases where the execution time results for the PR are worse than latest master results are highlighted in **red**. The following thresholds were used for comparing the execution times:
Note that these results are based on a single point-in-time result for each branch. For full results, see the dashboard. Graphs show the p99 interval based on the mean and StdDev of the test run, as well as the mean value of the run (shown as a diamond below the graph). Duration chartsFakeDbCommand (.NET Framework 4.8)gantt
title Execution time (ms) FakeDbCommand (.NET Framework 4.8)
dateFormat x
axisFormat %Q
todayMarker off
section Baseline
This PR (8682) - mean (73ms) : 71, 75
master - mean (74ms) : 70, 78
section Bailout
This PR (8682) - mean (80ms) : 76, 83
master - mean (77ms) : 75, 79
section CallTarget+Inlining+NGEN
This PR (8682) - mean (1,115ms) : 1058, 1173
master - mean (1,108ms) : 1060, 1157
FakeDbCommand (.NET Core 3.1)gantt
title Execution time (ms) FakeDbCommand (.NET Core 3.1)
dateFormat x
axisFormat %Q
todayMarker off
section Baseline
This PR (8682) - mean (118ms) : 112, 124
master - mean (115ms) : 110, 120
section Bailout
This PR (8682) - mean (116ms) : 112, 121
master - mean (115ms) : 112, 117
section CallTarget+Inlining+NGEN
This PR (8682) - mean (798ms) : 773, 823
master - mean (799ms) : 776, 822
FakeDbCommand (.NET 6)gantt
title Execution time (ms) FakeDbCommand (.NET 6)
dateFormat x
axisFormat %Q
todayMarker off
section Baseline
This PR (8682) - mean (105ms) : 98, 111
master - mean (101ms) : 97, 105
section Bailout
This PR (8682) - mean (103ms) : 99, 106
master - mean (102ms) : 99, 105
section CallTarget+Inlining+NGEN
This PR (8682) - mean (953ms) : 917, 989
master - mean (950ms) : 915, 984
FakeDbCommand (.NET 8)gantt
title Execution time (ms) FakeDbCommand (.NET 8)
dateFormat x
axisFormat %Q
todayMarker off
section Baseline
This PR (8682) - mean (102ms) : 97, 108
master - mean (102ms) : 96, 109
section Bailout
This PR (8682) - mean (105ms) : 100, 110
master - mean (101ms) : 97, 105
section CallTarget+Inlining+NGEN
This PR (8682) - mean (829ms) : 781, 878
master - mean (827ms) : 781, 873
HttpMessageHandler (.NET Framework 4.8)gantt
title Execution time (ms) HttpMessageHandler (.NET Framework 4.8)
dateFormat x
axisFormat %Q
todayMarker off
section Baseline
This PR (8682) - mean (199ms) : 194, 203
master - mean (198ms) : 191, 204
section Bailout
This PR (8682) - mean (202ms) : 198, 206
master - mean (200ms) : 195, 204
section CallTarget+Inlining+NGEN
This PR (8682) - mean (1,204ms) : 1165, 1244
master - mean (1,195ms) : 1160, 1231
HttpMessageHandler (.NET Core 3.1)gantt
title Execution time (ms) HttpMessageHandler (.NET Core 3.1)
dateFormat x
axisFormat %Q
todayMarker off
section Baseline
This PR (8682) - mean (286ms) : 274, 297
master - mean (284ms) : 276, 291
section Bailout
This PR (8682) - mean (303ms) : crit, 282, 324
master - mean (286ms) : 281, 291
section CallTarget+Inlining+NGEN
This PR (8682) - mean (992ms) : 922, 1061
master - mean (963ms) : 945, 981
HttpMessageHandler (.NET 6)gantt
title Execution time (ms) HttpMessageHandler (.NET 6)
dateFormat x
axisFormat %Q
todayMarker off
section Baseline
This PR (8682) - mean (277ms) : 269, 285
master - mean (277ms) : 272, 282
section Bailout
This PR (8682) - mean (276ms) : 267, 286
master - mean (277ms) : 272, 282
section CallTarget+Inlining+NGEN
This PR (8682) - mean (1,159ms) : 1121, 1198
master - mean (1,160ms) : 1118, 1202
HttpMessageHandler (.NET 8)gantt
title Execution time (ms) HttpMessageHandler (.NET 8)
dateFormat x
axisFormat %Q
todayMarker off
section Baseline
This PR (8682) - mean (279ms) : 273, 284
master - mean (276ms) : 271, 282
section Bailout
This PR (8682) - mean (278ms) : 273, 283
master - mean (276ms) : 271, 281
section CallTarget+Inlining+NGEN
This PR (8682) - mean (1,042ms) : 995, 1089
master - mean (1,035ms) : 991, 1079
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
…/appsec-65483-security-testing-headers
The DoesNotTagWhenHeadersAreAbsent theory added a "content-type" header to a WebHeaderCollection to populate an unrelated header. On .NET Framework, WebHeaderCollection.Add throws on well-known request headers including Content-Type. .NET Core / .NET 5+ relaxed this, so the test passed locally on net8.0 but failed in CI on net48. Swap "content-type" for a custom "x-other-header" name. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
e-n-0
left a comment
There was a problem hiding this comment.
Thanks!
I would add also an integration test with a snapshot file to validate that the tags are correctly set. Please look at the sample app
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Per @e-n-0's review: add a snapshot-based integration test that sends a request to Samples.Security.AspNetCore5 with the scan/test marker headers and verifies they appear as http.request.headers.* tags on the entry span. Lives in AspNetCore5TestsWithoutExternalRulesFile so it runs under both SecurityDisabled and SecurityEnabled fixtures — proves the unconditional-tagging contract regardless of AppSec. Snapshots will be generated on first CI run and committed in a follow-up after review. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Generated from CI build 202229. Both snapshots show the entry span carrying http.request.headers.x-datadog-endpoint-scan and http.request.headers.x-datadog-security-test. The SecurityEnabled snapshot additionally includes _dd.appsec.* tags, proving the markers don't interfere with AppSec being on. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Summary of changes
Tag
x-datadog-endpoint-scanandx-datadog-security-testHTTP request headers ashttp.request.headers.<name>on every HTTP server entry span (and the inferred-proxy span when one is created), unconditionally — independent ofDD_TRACE_HEADER_TAGSand AppSec enablement. Markers are not propagated downstream.Reason for change
APPSEC-65483 — RFC "Security Testing: Trace Attribution for Inventory Enrichment and Pollution Prevention". These two markers let the API endpoint reducer distinguish Datadog scan/test traffic from real user traffic and keep it out of the API inventory.
Sibling-tracer implementations already merged:
dd-trace-py#18049,dd-trace-js#8463,dd-trace-java#11418.Implementation details
SpanContextPropagator.AddSecurityTestingHeadersAsTags<THeaders>reads both markers from anyIHeadersCollectionand tags them on the supplied span. Tag names are precomputed;string[]fast-path avoids enumerator allocation on the legacyNameValueCollection/WebHeaderCollectioncarriers; presence-based (empty values still tagged).AspNetMvcIntegration(System.Web MVC) — entry span + inferred-proxy span (proxy tagged at creation site)AspNetWebApi2Integration(System.Web Web API 2) — entry span (no proxy support on this path)TracingHttpModule(OWIN/IIS classic) — entry span + inferred-proxy spanAspNetCoreHttpRequestHandler(ASP.NET Core, including Azure Functions isolated worker HTTP-proxying mode) — entry span + inferred-proxy spanWcfCommon(WCF over HTTP) — entry spanTest coverage
SpanContextPropagatorTests_AddSecurityTestingHeadersAsTags(new — 13 cases): both markers + unrelated header, absent headers, noHeaderTagsconfig, only one marker present, empty-string value still tagged, case-insensitive lookup, ASP.NET CoreHeadersCollectionAdapterwith mixed-case lookup.SpanContextPropagatorTests*continue to pass.Other details