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
16 changes: 16 additions & 0 deletions changelog.d/unreleased/3442.security.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
category: security
issues:
- 3442
affected:
- tools/CodeIndex.TestTelemetry/Program.cs
- tests/CodeIndex.Tests/TestTelemetryTests.cs
---

## English

- **Test telemetry TRX processing is now bounded (#3442)** — the telemetry helper now caps `--top`, TRX directory and entry traversal, TRX file discovery, TRX file size, XML parser work, and retained result collections so unusually large test result trees cannot force unbounded processing. It also skips non-regular `.trx` filesystem entries and discards malformed TRX files atomically instead of contributing partial pass/fail counts.

## 日本語

- **Test telemetry の TRX 処理に上限を設けました (#3442)** — telemetry helper は `--top`、TRX ディレクトリ / エントリ走査、TRX ファイル探索、TRX ファイルサイズ、XML parser の処理量、保持する結果コレクションを制限し、過大なテスト結果ツリーで無制限の処理が発生しないようにしました。通常ファイルではない `.trx` filesystem entry をスキップし、壊れた TRX ファイルは pass/fail 件数へ部分的に反映せずファイル単位で破棄します。
16 changes: 16 additions & 0 deletions changelog.d/unreleased/3443.security.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
category: security
issues:
- 3443
affected:
- tools/CodeIndex.TestTelemetry/Program.cs
- tests/CodeIndex.Tests/TestTelemetryTests.cs
---

## English

- **Test telemetry TRX warnings now avoid leaking local paths and XML parser details (#3443)** — parse and inspection warnings now use relative TRX paths when possible and stable reason codes instead of raw exception messages.

## 日本語

- **Test telemetry の TRX warning がローカルパスや XML parser 詳細を漏らさないようになりました (#3443)** — parse / inspection warning は可能な限り相対 TRX パスと安定した reason code を使い、生の例外メッセージを出力しないようにしました。
224 changes: 224 additions & 0 deletions tests/CodeIndex.Tests/TestTelemetryTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -75,4 +75,228 @@ public void Load_MissingDirectoryReturnsWarningInsteadOfFailingCiSummary()
Assert.Single(summary.Warnings);
Assert.Contains("Results directory not found", summary.Warnings[0], StringComparison.Ordinal);
}

[Fact]
public void Load_RejectsTopValuesAboveTelemetryCap()
{
var exception = Assert.Throws<TelemetryException>(() =>
TrxTelemetry.Load(Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N")), TrxTelemetry.MaxTop + 1));

Assert.Contains($"between 1 and {TrxTelemetry.MaxTop}", exception.Message, StringComparison.Ordinal);
}

[Fact]
public void Load_CapsTrxDiscovery()
{
var projectRoot = TestProjectHelper.CreateTempProject("cdidx_trx_telemetry_cap");
try
{
var resultsDirectory = Path.Combine(projectRoot, "TestResults");
Directory.CreateDirectory(resultsDirectory);

for (var i = 0; i < TrxTelemetry.MaxTrxFiles + 1; i++)
{
File.WriteAllText(Path.Combine(resultsDirectory, $"results-{i:D4}.trx"), MinimalTrx($"Test{i:D4}"));
}

var summary = TrxTelemetry.Load(resultsDirectory, top: 1);

Assert.Equal(TrxTelemetry.MaxTrxFiles, summary.TrxFileCount);
Assert.Equal(TrxTelemetry.MaxTrxFiles, summary.Total);
Assert.Contains(summary.Warnings, warning =>
warning.Contains("TRX file cap reached", StringComparison.Ordinal));
}
finally
{
TestProjectHelper.DeleteDirectory(projectRoot);
}
}

[Fact]
public void Load_CapsTrxDirectoryTraversal()
{
var projectRoot = TestProjectHelper.CreateTempProject("cdidx_trx_telemetry_directory_cap");
try
{
var resultsDirectory = Path.Combine(projectRoot, "TestResults");
Directory.CreateDirectory(resultsDirectory);

for (var i = 0; i < TrxTelemetry.MaxTraversalDirectories + 1; i++)
{
Directory.CreateDirectory(Path.Combine(resultsDirectory, $"dir-{i:D4}"));
}

var summary = TrxTelemetry.Load(resultsDirectory, top: 1);

Assert.Equal(0, summary.TrxFileCount);
Assert.Equal(0, summary.Total);
Assert.Contains(summary.Warnings, warning =>
warning.Contains("directory traversal cap", StringComparison.Ordinal));
}
finally
{
TestProjectHelper.DeleteDirectory(projectRoot);
}
}

[Fact]
public void Load_SkipsTrxFilesAboveSizeCap()
{
var projectRoot = TestProjectHelper.CreateTempProject("cdidx_trx_telemetry_size");
try
{
var resultsDirectory = Path.Combine(projectRoot, "TestResults");
var nestedDirectory = Path.Combine(resultsDirectory, "nested");
Directory.CreateDirectory(nestedDirectory);
var largeTrx = Path.Combine(nestedDirectory, "too-large.trx");
using (var stream = File.Create(largeTrx))
{
stream.SetLength(TrxTelemetry.MaxTrxFileBytes + 1);
}

var summary = TrxTelemetry.Load(resultsDirectory, top: 1);

Assert.Equal(1, summary.TrxFileCount);
Assert.Equal(0, summary.Total);
var warning = Assert.Single(summary.Warnings);
Assert.Contains("byte cap", warning, StringComparison.Ordinal);
Assert.Contains("nested/too-large.trx", warning, StringComparison.Ordinal);
Assert.DoesNotContain(projectRoot, warning, StringComparison.Ordinal);
}
finally
{
TestProjectHelper.DeleteDirectory(projectRoot);
}
}

[Fact]
public void Load_SkipsUnixFifoTrxEntries()
{
if (OperatingSystem.IsWindows())
return;

var projectRoot = TestProjectHelper.CreateTempProject("cdidx_trx_telemetry_fifo");
try
{
var resultsDirectory = Path.Combine(projectRoot, "TestResults");
Directory.CreateDirectory(resultsDirectory);
var fifoPath = Path.Combine(resultsDirectory, "pipe.trx");
if (Mkfifo(fifoPath, Convert.ToUInt32("600", 8)) != 0)
throw new IOException($"mkfifo failed with errno {System.Runtime.InteropServices.Marshal.GetLastWin32Error()}.");

var summary = TrxTelemetry.Load(resultsDirectory, top: 1);

Assert.Equal(0, summary.TrxFileCount);
Assert.Equal(0, summary.Total);
}
finally
{
TestProjectHelper.DeleteDirectory(projectRoot);
}
}

[Fact]
public void Load_RejectsTrxDtds()
{
var projectRoot = TestProjectHelper.CreateTempProject("cdidx_trx_telemetry_dtd");
try
{
var resultsDirectory = Path.Combine(projectRoot, "TestResults");
Directory.CreateDirectory(resultsDirectory);
File.WriteAllText(Path.Combine(resultsDirectory, "with-dtd.trx"), """
<!DOCTYPE TestRun [
<!ELEMENT TestRun ANY>
]>
<TestRun>
<Results>
<UnitTestResult testName="Unsafe" outcome="Passed" />
</Results>
</TestRun>
""");

var summary = TrxTelemetry.Load(resultsDirectory, top: 1);

Assert.Equal(0, summary.Total);
Assert.Contains(summary.Warnings, warning =>
string.Equals(warning, "Could not parse with-dtd.trx: invalid_xml", StringComparison.Ordinal));
}
finally
{
TestProjectHelper.DeleteDirectory(projectRoot);
}
}

[Fact]
public void Load_DiscardsPartialResultsFromMalformedTrx()
{
var projectRoot = TestProjectHelper.CreateTempProject("cdidx_trx_telemetry_partial_xml");
try
{
var resultsDirectory = Path.Combine(projectRoot, "TestResults");
Directory.CreateDirectory(resultsDirectory);
File.WriteAllText(Path.Combine(resultsDirectory, "partial.trx"), """
<TestRun>
<Results>
<UnitTestResult testName="ShouldNotCount" outcome="Passed" duration="00:00:09.0000000" />
<UnitTestResult testName="Broken" outcome="Failed">
</Results>
</TestRun>
""");

var summary = TrxTelemetry.Load(resultsDirectory, top: 1);

Assert.Equal(0, summary.Total);
Assert.Equal(0, summary.Passed);
Assert.Empty(summary.Slowest);
Assert.Contains(summary.Warnings, warning =>
string.Equals(warning, "Could not parse partial.trx: invalid_xml", StringComparison.Ordinal));
}
finally
{
TestProjectHelper.DeleteDirectory(projectRoot);
}
}

[Fact]
public void Load_SanitizesInvalidXmlWarnings()
{
var projectRoot = TestProjectHelper.CreateTempProject("cdidx_trx_telemetry_xml_warning");
try
{
var resultsDirectory = Path.Combine(projectRoot, "TestResults");
var nestedDirectory = Path.Combine(resultsDirectory, "nested");
Directory.CreateDirectory(nestedDirectory);
File.WriteAllText(Path.Combine(nestedDirectory, "broken.trx"), """
<TestRun>
<Results>
<UnitTestResult testName="Broken" outcome="Passed">
</Results>
</TestRun>
""");

var summary = TrxTelemetry.Load(resultsDirectory, top: 1);

var warning = Assert.Single(summary.Warnings);
Assert.Equal("Could not parse nested/broken.trx: invalid_xml", warning);
Assert.DoesNotContain(projectRoot, warning, StringComparison.Ordinal);
Assert.DoesNotContain("Name cannot begin", warning, StringComparison.OrdinalIgnoreCase);
Assert.DoesNotContain("position", warning, StringComparison.OrdinalIgnoreCase);
}
finally
{
TestProjectHelper.DeleteDirectory(projectRoot);
}
}

private static string MinimalTrx(string testName) => $$"""
<?xml version="1.0" encoding="utf-8"?>
<TestRun xmlns="http://microsoft.com/schemas/VisualStudio/TeamTest/2010">
<Results>
<UnitTestResult testName="{{testName}}" outcome="Passed" duration="00:00:00.1000000" />
</Results>
</TestRun>
""";

[System.Runtime.InteropServices.DllImport("libc", EntryPoint = "mkfifo", SetLastError = true)]
private static extern int Mkfifo(string path, uint mode);
}
Loading
Loading