Skip to content

Commit 7bdf71e

Browse files
dromanolclaude
andauthored
[IAST] Add Class field to vulnerability Location and repurpose Path to source file (#8930)
## Summary of changes Resemantizes the IAST vulnerability `Location` so the declaring type and the source file are reported in distinct fields: - **New `class` field** always carries the declaring type (`DeclaringType.FullName`) — this is what `path` used to hold. - **`method`** continues to carry the method name (unchanged). - **`path`** is repurposed to hold the **source file name** (basename of `StackFrame.GetFileName()`), present **only when debug info (PDBs)** is available. - **`line`** remains present only with debug info (as before). ## Reason for change Previously `path` conflated two concepts: it held the *type name*, never the actual source file, and left file/line information (which depends on PDBs) unused. This split makes `class` a stable, always-present identifier and lets `path` carry genuinely useful source-file information when PDBs are present. ## Implementation details - `Iast/Location.cs`: added `Class` property; runtime ctor now sets `Class = method?.DeclaringType?.FullName` and `Path = Path.GetFileName(stackFrame?.GetFileName())` (filename only, matching `StackReporter` and avoiding leaking build-machine paths). String and test ctors updated accordingly. - `Location.GetHashCode()` now hashes `Class`+`Method` (previously `Path`+`Method`, where `Path` was the type). **Deduplication is unchanged** — same inputs, same hash — and is now independent of PDB availability. - `AppSec/Rasp/MetaStructHelper.cs`: emits `class` in the MessagePack meta-struct. The `_dd.iast.json` span tag emits `class` automatically via the reflection-based camelCase serializer (`NullValueHandling.Ignore`). - `vulnerability_schema.json`: added `class`; `path` re-documented as "source file name (only available with debug info)". ## Test coverage - Updated `LocationTests` and `VulnerabilityBatchTests` unit tests. - Regenerated the IAST integration snapshots (144 files): most are a deterministic `path`→`class` rename (the normal integration environment does not expose PDBs for sample code). - The **3 snapshots with debug info** (Razor/WebForms compiled types) were regenerated by **running the IIS integration tests locally in Release (net48)** to capture the real runtime source-file `path`: - `Iast.ReflectedXss.AspNetMvc5.IastEnabled` → `path: "ReflectedXss.cshtml"` - `Security.AspNetWebForms.Classic/Integrated…TestQueryParameterNameVulnerability` → `path: "print.aspx.cs"` - All 3 verified green locally. ## Other details ⚠️ **Backend coordination required**: please confirm the wire/backend accepts the new `class` field and the resemantized `path` before merging. --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 2302d41 commit 7bdf71e

152 files changed

Lines changed: 311 additions & 278 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

tracer/src/Datadog.Trace/AppSec/Rasp/MetaStructHelper.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,11 @@ public static Dictionary<string, object> VulnerabilityBatchToDictionary(Vulnerab
114114
locationDict["path"] = location.Path;
115115
}
116116

117+
if (location.Class is { Length: > 0 })
118+
{
119+
locationDict["class"] = location.Class;
120+
}
121+
117122
if (location.Method is { Length: > 0 })
118123
{
119124
locationDict["method"] = location.Method;

tracer/src/Datadog.Trace/Iast/Location.cs

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,17 @@ namespace Datadog.Trace.Iast;
1616

1717
internal readonly struct Location
1818
{
19+
// Both Windows ('\') and Unix ('/') separators, because PDBs produced on one OS may be read on another.
20+
private static readonly char[] PathSeparators = ['/', '\\'];
21+
1922
internal readonly StackTrace? _stack = null;
2023

2124
public Location(string method)
2225
{
2326
var index = method.LastIndexOf("::", StringComparison.Ordinal);
2427
if (index >= 0)
2528
{
26-
Path = method.Substring(0, length: index);
29+
Class = method.Substring(0, length: index);
2730
var bracketIndex = method.IndexOf("(", startIndex: index + 2, StringComparison.Ordinal);
2831
Method = bracketIndex > 0
2932
? method.Substring(index + 2, length: bracketIndex - index - 2)
@@ -38,10 +41,11 @@ public Location(string method)
3841
public Location(StackFrame? stackFrame, StackTrace? stack, string? stackId, ulong? spanId)
3942
{
4043
var method = stackFrame?.GetMethod();
41-
Path = method?.DeclaringType?.FullName;
44+
Class = method?.DeclaringType?.FullName;
4245
Method = method?.Name;
4346
var line = stackFrame?.GetFileLineNumber();
4447
Line = line > 0 ? line : null;
48+
Path = GetFileName(stackFrame?.GetFileName());
4549

4650
SpanId = spanId == 0 ? null : spanId;
4751

@@ -51,7 +55,7 @@ public Location(StackFrame? stackFrame, StackTrace? stack, string? stackId, ulon
5155

5256
internal Location(string? typeName, string? methodName, int? line, ulong? spanId) // For testing purposes only
5357
{
54-
this.Path = typeName;
58+
this.Class = typeName;
5559
this.Method = methodName;
5660
Line = line > 0 ? line : null;
5761

@@ -62,6 +66,8 @@ internal Location(string? typeName, string? methodName, int? line, ulong? spanId
6266

6367
public string? Path { get; }
6468

69+
public string? Class { get; }
70+
6571
public string? Method { get; }
6672

6773
public int? Line { get; }
@@ -71,7 +77,21 @@ internal Location(string? typeName, string? methodName, int? line, ulong? spanId
7177
public override int GetHashCode()
7278
{
7379
// We do not calculate the hash including the spanId nor the line
74-
return IastUtils.GetHashCode(Path, Method);
80+
return IastUtils.GetHashCode(Class, Method);
81+
}
82+
83+
// Extracts the file name from a path, handling both Windows ('\') and Unix ('/') separators.
84+
// We can't rely on System.IO.Path.GetFileName here because it only recognizes the current OS
85+
// separator, so a Windows path read on Unix (from a Windows-built PDB) would leak the full path.
86+
private static string? GetFileName(string? filePath)
87+
{
88+
if (string.IsNullOrEmpty(filePath))
89+
{
90+
return null;
91+
}
92+
93+
var separatorIndex = filePath!.LastIndexOfAny(PathSeparators);
94+
return separatorIndex >= 0 ? filePath.Substring(separatorIndex + 1) : filePath;
7595
}
7696

7797
internal void ReportStack(Span? span)

tracer/test/Datadog.Trace.Security.IntegrationTests/IAST/AspNetCore5IastTests.cs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -375,7 +375,7 @@ public async Task TestVulnerabilityStack(string name, string url)
375375
var settings = VerifyHelper.GetSpanVerifierSettings();
376376
settings.AddIastScrubbing();
377377
var hashRegex = (new Regex(@"""hash"": -?\d+"), @"""hash"": XXX");
378-
var pathRegex = (new Regex(@"""path"": ""AspNetCore.*\."), @"""path"": ""AspNetCore.");
378+
var pathRegex = (new Regex(@"""class"": ""AspNetCore.*\."), @"""class"": ""AspNetCore.");
379379

380380
settings.AddRegexScrubber(hashRegex);
381381
settings.AddRegexScrubber(pathRegex);
@@ -658,8 +658,8 @@ public async Task TestSessionTimeoutVulnerability(int timeoutMinutes)
658658
(Regex RegexPattern, string Replacement) sessionIdleTimeoutRegex = (new Regex(@"Session idle timeout is configured with: options.IdleTimeout, with a value of \d+ minutes"), "Session idle timeout is configured with: options.IdleTimeout, with a value of XXX minutes");
659659
(Regex RegexPattern, string Replacement) hashRegex = (new Regex(@"""hash"": -?\d+"), @"""hash"": XXX");
660660

661-
// Only for net5.0: path and method are different
662-
(Regex RegexPattern, string Replacement) pathRegex = (new Regex(@"""path"": ""Samples.Security.AspNetCore5.Program"""), @"""path"": ""Samples.Security.AspNetCore5.Startup+<>c__DisplayClass4_0""");
661+
// Only for net5.0: class and method are different
662+
(Regex RegexPattern, string Replacement) pathRegex = (new Regex(@"""class"": ""Samples.Security.AspNetCore5.Program"""), @"""class"": ""Samples.Security.AspNetCore5.Startup+<>c__DisplayClass4_0""");
663663
(Regex RegexPattern, string Replacement) methodRegex = (new Regex(@"""method"": ""Main"""), @"""method"": ""<ConfigureServices>b__0""");
664664

665665
var settings = VerifyHelper.GetSpanVerifierSettings();
@@ -895,7 +895,7 @@ public async Task TestIastLdapRequest()
895895

896896
var settings = VerifyHelper.GetSpanVerifierSettings();
897897
settings.AddIastScrubbing()
898-
.AddRegexScrubber((new Regex("\"path\": \"Samples.Security.AspNetCore5.Controllers.IastController\\+.*"), "\"path\": \"Samples.Security.AspNetCore5.Controllers.IastController+\""))
898+
.AddRegexScrubber((new Regex("\"class\": \"Samples.Security.AspNetCore5.Controllers.IastController\\+.*"), "\"class\": \"Samples.Security.AspNetCore5.Controllers.IastController+\""))
899899
.AddRegexScrubber((new Regex("\"hash\": .*"), "\"hash\": 9515978"))
900900
.AddRegexScrubber((new Regex("\"method\": \"<Ldap>g__PerformLdapQuery\\|.\""), "\"method\": \"<Ldap>g__PerformLdapQuery|0\""));
901901
await VerifySpans(spansFiltered, settings, fileNameOverride: filename);
@@ -1208,7 +1208,7 @@ public async Task TestNHibernateSqlInjection()
12081208
public abstract class AspNetCore5IastTests : AspNetBase, IClassFixture<AspNetCoreTestFixture>
12091209
{
12101210
#pragma warning disable SA1311 // Static readonly fields should begin with upper-case letter
1211-
protected static readonly (Regex RegexPattern, string Replacement) aspNetCorePathScrubber = (new Regex("\"path\": \"AspNetCore[^\\.]+\\."), "\"path\": \"AspNetCore.");
1211+
protected static readonly (Regex RegexPattern, string Replacement) aspNetCorePathScrubber = (new Regex("\"class\": \"AspNetCore[^\\.]+\\."), "\"class\": \"AspNetCore.");
12121212
protected static readonly (Regex RegexPattern, string Replacement) hashScrubber = (new Regex("\"hash\": .+,"), "\"hash\": XXX,");
12131213
#pragma warning restore SA1311 // Static readonly fields should begin with upper-case letter
12141214

tracer/test/Datadog.Trace.Security.IntegrationTests/IAST/Grpc/GrpcDotNetTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ public async Task SubmitsTraces()
6060
var settings = VerifyHelper.GetSpanVerifierSettings();
6161

6262
// Add scrub for the location data, as using APM sample, we won't disable symbols on their sample
63-
(Regex RegexPattern, string Replacement) locationMsgRegex = (new Regex(@"(\S)*""location"": {(\r|\n){1,2}(.*(\r|\n){1,2}){0,4}(\s)*},"), string.Empty);
63+
(Regex RegexPattern, string Replacement) locationMsgRegex = (new Regex(@"(\S)*""location"": {(\r|\n){1,2}(.*(\r|\n){1,2}){0,6}(\s)*},"), string.Empty);
6464
settings.AddRegexScrubber(locationMsgRegex);
6565

6666
settings.AddIastScrubbing();

tracer/test/Datadog.Trace.Security.Unit.Tests/IAST/LocationTests.cs

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ public void GivenALocation_WhenCreatedFromMethod_MethodIsStored()
1818
var method = "GivenAProcess_WhenStartTaintedProcess_ThenIsVulnerable";
1919
var typeName = "Samples.InstrumentedTests.Iast.Vulnerabilities.CommandInjectionTests";
2020
var location = new Location(typeName, method, 23, 4);
21-
location.Path.Should().Be(typeName);
21+
location.Class.Should().Be(typeName);
2222
location.Method.Should().Be(method);
2323
}
2424

@@ -27,7 +27,7 @@ public void GivenALocation_WhenCreatedFromMethod_MethodIsStored2()
2727
{
2828
var method = "GivenAProcess_WhenStartTaintedProcess_ThenIsVulnerable";
2929
var location = new Location(null, method, null, 4);
30-
location.Path.Should().BeNull();
30+
location.Class.Should().BeNull();
3131
location.Method.Should().Be("GivenAProcess_WhenStartTaintedProcess_ThenIsVulnerable");
3232
}
3333

@@ -36,25 +36,26 @@ public void GivenALocation_WhenCreatedFromMethod_MethodIsStored3()
3636
{
3737
var method = "Samples.InstrumentedTests.Iast.Vulnerabilities.CommandInjectionTests.GivenAProcess_WhenStartTaintedProcess_ThenIsVulnerable";
3838
var location = new Location(null, method, 23, 4);
39-
location.Path.Should().BeNull();
39+
location.Class.Should().BeNull();
4040
location.Method.Should().Be("Samples.InstrumentedTests.Iast.Vulnerabilities.CommandInjectionTests.GivenAProcess_WhenStartTaintedProcess_ThenIsVulnerable");
4141
}
4242

4343
[Fact]
4444
public void GivenALocation_WhenCreatedFromNull_NothingIsStored()
4545
{
4646
var location = new Location(null, null, 23, 4);
47-
location.Path.Should().BeNull();
47+
location.Class.Should().BeNull();
4848
location.Method.Should().BeNull();
4949
}
5050

5151
[Fact]
5252
public void GivenALocation_WhenCreatedFromStackFrame_ValueIsExpected()
5353
{
54-
var stack = new StackTrace();
54+
var stack = new StackTrace(fNeedFileInfo: true);
5555
var frame = stack.GetFrame(0);
5656
var location = new Location(frame, stack, null, null);
57-
location.Path.Should().Be("Datadog.Trace.Security.Unit.Tests.IAST.LocationTests");
57+
location.Class.Should().Be("Datadog.Trace.Security.Unit.Tests.IAST.LocationTests");
5858
location.Method.Should().Be("GivenALocation_WhenCreatedFromStackFrame_ValueIsExpected");
59+
location.Path.Should().Be("LocationTests.cs");
5960
}
6061
}

tracer/test/Datadog.Trace.Security.Unit.Tests/IAST/Tainted/VulnerabilityBatchTests.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -513,7 +513,7 @@ public void GivenOneLongEvidenceValueVuln_WhenSerializing_JsonIsTruncated()
513513
"spanId": 123456,
514514
"line": 1,
515515
"method": "fooMethod",
516-
"path": "foo"
516+
"class": "foo"
517517
}
518518
}
519519
]
@@ -548,7 +548,7 @@ public void GivenTwoLongEvidenceValueVulns_WhenSerializing_JsonIsTruncated()
548548
"spanId": 123456,
549549
"line": 1,
550550
"method": "fooMethod",
551-
"path": "foo"
551+
"class": "foo"
552552
},
553553
"type": "WEAK_HASH"
554554
},
@@ -561,7 +561,7 @@ public void GivenTwoLongEvidenceValueVulns_WhenSerializing_JsonIsTruncated()
561561
"spanId": 123456,
562562
"line": 1,
563563
"method": "fooMethod",
564-
"path": "foo"
564+
"class": "foo"
565565
},
566566
"type": "WEAK_HASH"
567567
}

tracer/test/Datadog.Trace.Security.Unit.Tests/IAST/Tainted/vulnerability_schema.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,11 @@
8686
"type": "integer"
8787
},
8888
"path": {
89-
"description": "The name of the file containing the vulnerability or class name",
89+
"description": "The name of the source file containing the vulnerability (only available with debug info)",
90+
"type": "string"
91+
},
92+
"class": {
93+
"description": "The name of the class (declaring type) where this location points to",
9094
"type": "string"
9195
},
9296
"line": {

tracer/test/snapshots/Iast.CommandInjection.AspNetCore2.IastEnabled.RedactionEnabled.verified.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
"hash": -430498668,
2929
"location": {
3030
"spanId": XXX,
31-
"path": "Samples.Security.AspNetCore5.Controllers.IastController",
31+
"class": "Samples.Security.AspNetCore5.Controllers.IastController",
3232
"method": "ExecuteCommandInternal"
3333
},
3434
"evidence": {

tracer/test/snapshots/Iast.CommandInjection.AspNetCore2.IastEnabled.verified.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
"hash": -430498668,
2929
"location": {
3030
"spanId": XXX,
31-
"path": "Samples.Security.AspNetCore5.Controllers.IastController",
31+
"class": "Samples.Security.AspNetCore5.Controllers.IastController",
3232
"method": "ExecuteCommandInternal"
3333
},
3434
"evidence": {

tracer/test/snapshots/Iast.CommandInjection.AspNetCore5.IastEnabled.RedactionEnabled.verified.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
"hash": -430498668,
3434
"location": {
3535
"spanId": XXX,
36-
"path": "Samples.Security.AspNetCore5.Controllers.IastController",
36+
"class": "Samples.Security.AspNetCore5.Controllers.IastController",
3737
"method": "ExecuteCommandInternal"
3838
},
3939
"evidence": {

0 commit comments

Comments
 (0)