Files
git.stella-ops.org/src/Scanner/__Tests/StellaOps.Scanner.Reachability.Tests/PathWitnessBuilderTests.cs
master 00d2c99af9 feat: add Attestation Chain and Triage Evidence API clients and models
- Implemented Attestation Chain API client with methods for verifying, fetching, and managing attestation chains.
- Created models for Attestation Chain, including DSSE envelope structures and verification results.
- Developed Triage Evidence API client for fetching finding evidence, including methods for evidence retrieval by CVE and component.
- Added models for Triage Evidence, encapsulating evidence responses, entry points, boundary proofs, and VEX evidence.
- Introduced mock implementations for both API clients to facilitate testing and development.
2025-12-18 13:15:13 +02:00

388 lines
13 KiB
C#

using StellaOps.Cryptography;
using StellaOps.Scanner.Reachability.Gates;
using StellaOps.Scanner.Reachability.Witnesses;
using Xunit;
namespace StellaOps.Scanner.Reachability.Tests;
public class PathWitnessBuilderTests
{
private readonly ICryptoHash _cryptoHash;
private readonly TimeProvider _timeProvider;
public PathWitnessBuilderTests()
{
_cryptoHash = DefaultCryptoHash.CreateForTests();
_timeProvider = TimeProvider.System;
}
[Fact]
public async Task BuildAsync_ReturnsNull_WhenNoPathExists()
{
// Arrange
var graph = CreateSimpleGraph();
var builder = new PathWitnessBuilder(_cryptoHash, _timeProvider);
var request = new PathWitnessRequest
{
SbomDigest = "sha256:abc123",
ComponentPurl = "pkg:nuget/Newtonsoft.Json@12.0.3",
VulnId = "CVE-2024-12345",
VulnSource = "NVD",
AffectedRange = "<=12.0.3",
EntrypointSymbolId = "sym:entry1",
EntrypointKind = "http",
EntrypointName = "GET /api/test",
SinkSymbolId = "sym:unreachable", // Not in graph
SinkType = "deserialization",
CallGraph = graph,
CallgraphDigest = "blake3:abc123"
};
// Act
var result = await builder.BuildAsync(request);
// Assert
Assert.Null(result);
}
[Fact]
public async Task BuildAsync_ReturnsWitness_WhenPathExists()
{
// Arrange
var graph = CreateSimpleGraph();
var builder = new PathWitnessBuilder(_cryptoHash, _timeProvider);
var request = new PathWitnessRequest
{
SbomDigest = "sha256:abc123",
ComponentPurl = "pkg:nuget/Newtonsoft.Json@12.0.3",
VulnId = "CVE-2024-12345",
VulnSource = "NVD",
AffectedRange = "<=12.0.3",
EntrypointSymbolId = "sym:entry1",
EntrypointKind = "http",
EntrypointName = "GET /api/test",
SinkSymbolId = "sym:sink1",
SinkType = "deserialization",
CallGraph = graph,
CallgraphDigest = "blake3:abc123"
};
// Act
var result = await builder.BuildAsync(request);
// Assert
Assert.NotNull(result);
Assert.Equal(WitnessSchema.Version, result.WitnessSchema);
Assert.StartsWith(WitnessSchema.WitnessIdPrefix, result.WitnessId);
Assert.Equal("CVE-2024-12345", result.Vuln.Id);
Assert.Equal("sym:entry1", result.Entrypoint.SymbolId);
Assert.Equal("sym:sink1", result.Sink.SymbolId);
Assert.NotEmpty(result.Path);
}
[Fact]
public async Task BuildAsync_GeneratesContentAddressedWitnessId()
{
// Arrange
var graph = CreateSimpleGraph();
var builder = new PathWitnessBuilder(_cryptoHash, _timeProvider);
var request = new PathWitnessRequest
{
SbomDigest = "sha256:abc123",
ComponentPurl = "pkg:nuget/Newtonsoft.Json@12.0.3",
VulnId = "CVE-2024-12345",
VulnSource = "NVD",
AffectedRange = "<=12.0.3",
EntrypointSymbolId = "sym:entry1",
EntrypointKind = "http",
EntrypointName = "GET /api/test",
SinkSymbolId = "sym:sink1",
SinkType = "deserialization",
CallGraph = graph,
CallgraphDigest = "blake3:abc123"
};
// Act
var result1 = await builder.BuildAsync(request);
var result2 = await builder.BuildAsync(request);
// Assert
Assert.NotNull(result1);
Assert.NotNull(result2);
// The witness ID should be deterministic (same input = same hash)
// Note: ObservedAt differs, but witness ID is computed without it
Assert.Equal(result1.WitnessId, result2.WitnessId);
}
[Fact]
public async Task BuildAsync_PopulatesArtifactInfo()
{
// Arrange
var graph = CreateSimpleGraph();
var builder = new PathWitnessBuilder(_cryptoHash, _timeProvider);
var request = new PathWitnessRequest
{
SbomDigest = "sha256:sbom123",
ComponentPurl = "pkg:npm/lodash@4.17.21",
VulnId = "CVE-2024-99999",
VulnSource = "GHSA",
AffectedRange = "<4.17.21",
EntrypointSymbolId = "sym:entry1",
EntrypointKind = "grpc",
EntrypointName = "UserService.GetUser",
SinkSymbolId = "sym:sink1",
SinkType = "prototype_pollution",
CallGraph = graph,
CallgraphDigest = "blake3:graph456"
};
// Act
var result = await builder.BuildAsync(request);
// Assert
Assert.NotNull(result);
Assert.Equal("sha256:sbom123", result.Artifact.SbomDigest);
Assert.Equal("pkg:npm/lodash@4.17.21", result.Artifact.ComponentPurl);
}
[Fact]
public async Task BuildAsync_PopulatesEvidenceInfo()
{
// Arrange
var graph = CreateSimpleGraph();
var builder = new PathWitnessBuilder(_cryptoHash, _timeProvider);
var request = new PathWitnessRequest
{
SbomDigest = "sha256:abc123",
ComponentPurl = "pkg:nuget/Test@1.0.0",
VulnId = "CVE-2024-12345",
VulnSource = "NVD",
AffectedRange = "<=1.0.0",
EntrypointSymbolId = "sym:entry1",
EntrypointKind = "http",
EntrypointName = "TestController.Get",
SinkSymbolId = "sym:sink1",
SinkType = "sql_injection",
CallGraph = graph,
CallgraphDigest = "blake3:callgraph789",
SurfaceDigest = "sha256:surface123",
AnalysisConfigDigest = "sha256:config456",
BuildId = "build:xyz789"
};
// Act
var result = await builder.BuildAsync(request);
// Assert
Assert.NotNull(result);
Assert.Equal("blake3:callgraph789", result.Evidence.CallgraphDigest);
Assert.Equal("sha256:surface123", result.Evidence.SurfaceDigest);
Assert.Equal("sha256:config456", result.Evidence.AnalysisConfigDigest);
Assert.Equal("build:xyz789", result.Evidence.BuildId);
}
[Fact]
public async Task BuildAsync_FindsShortestPath()
{
// Arrange - graph with multiple paths
var graph = CreateGraphWithMultiplePaths();
var builder = new PathWitnessBuilder(_cryptoHash, _timeProvider);
var request = new PathWitnessRequest
{
SbomDigest = "sha256:abc123",
ComponentPurl = "pkg:nuget/Test@1.0.0",
VulnId = "CVE-2024-12345",
VulnSource = "NVD",
AffectedRange = "<=1.0.0",
EntrypointSymbolId = "sym:start",
EntrypointKind = "http",
EntrypointName = "Start",
SinkSymbolId = "sym:end",
SinkType = "deserialization",
CallGraph = graph,
CallgraphDigest = "blake3:abc123"
};
// Act
var result = await builder.BuildAsync(request);
// Assert
Assert.NotNull(result);
// Short path: start -> direct -> end (3 steps)
// Long path: start -> long1 -> long2 -> long3 -> end (5 steps)
Assert.Equal(3, result.Path.Count);
Assert.Equal("sym:start", result.Path[0].SymbolId);
Assert.Equal("sym:direct", result.Path[1].SymbolId);
Assert.Equal("sym:end", result.Path[2].SymbolId);
}
[Fact]
public async Task BuildAllAsync_YieldsMultipleWitnesses_WhenMultipleRootsReachSink()
{
// Arrange
var graph = CreateGraphWithMultipleRoots();
var builder = new PathWitnessBuilder(_cryptoHash, _timeProvider);
var request = new BatchWitnessRequest
{
SbomDigest = "sha256:abc123",
ComponentPurl = "pkg:nuget/Test@1.0.0",
VulnId = "CVE-2024-12345",
VulnSource = "NVD",
AffectedRange = "<=1.0.0",
SinkSymbolId = "sym:sink",
SinkType = "deserialization",
CallGraph = graph,
CallgraphDigest = "blake3:abc123",
MaxWitnesses = 10
};
// Act
var witnesses = new List<PathWitness>();
await foreach (var witness in builder.BuildAllAsync(request))
{
witnesses.Add(witness);
}
// Assert
Assert.Equal(2, witnesses.Count);
Assert.Contains(witnesses, w => w.Entrypoint.SymbolId == "sym:root1");
Assert.Contains(witnesses, w => w.Entrypoint.SymbolId == "sym:root2");
}
[Fact]
public async Task BuildAllAsync_RespectsMaxWitnesses()
{
// Arrange
var graph = CreateGraphWithMultipleRoots();
var builder = new PathWitnessBuilder(_cryptoHash, _timeProvider);
var request = new BatchWitnessRequest
{
SbomDigest = "sha256:abc123",
ComponentPurl = "pkg:nuget/Test@1.0.0",
VulnId = "CVE-2024-12345",
VulnSource = "NVD",
AffectedRange = "<=1.0.0",
SinkSymbolId = "sym:sink",
SinkType = "deserialization",
CallGraph = graph,
CallgraphDigest = "blake3:abc123",
MaxWitnesses = 1 // Limit to 1
};
// Act
var witnesses = new List<PathWitness>();
await foreach (var witness in builder.BuildAllAsync(request))
{
witnesses.Add(witness);
}
// Assert
Assert.Single(witnesses);
}
#region Test Helpers
private static RichGraph CreateSimpleGraph()
{
var nodes = new List<RichGraphNode>
{
new("n1", "sym:entry1", null, null, "dotnet", "method", "Entry1", null, null, null, null),
new("n2", "sym:middle1", null, null, "dotnet", "method", "Middle1", null, null, null, null),
new("n3", "sym:sink1", null, null, "dotnet", "method", "Sink1", null, null, null, null)
};
var edges = new List<RichGraphEdge>
{
new("n1", "n2", "call", null, null, null, 1.0, null),
new("n2", "n3", "call", null, null, null, 1.0, null)
};
var roots = new List<RichGraphRoot>
{
new("n1", "http", "/api/test")
};
return new RichGraph(
nodes,
edges,
roots,
new RichGraphAnalyzer("test", "1.0.0", null));
}
private static RichGraph CreateGraphWithMultiplePaths()
{
var nodes = new List<RichGraphNode>
{
new("n0", "sym:start", null, null, "dotnet", "method", "Start", null, null, null, null),
new("n1", "sym:direct", null, null, "dotnet", "method", "Direct", null, null, null, null),
new("n2", "sym:long1", null, null, "dotnet", "method", "Long1", null, null, null, null),
new("n3", "sym:long2", null, null, "dotnet", "method", "Long2", null, null, null, null),
new("n4", "sym:long3", null, null, "dotnet", "method", "Long3", null, null, null, null),
new("n5", "sym:end", null, null, "dotnet", "method", "End", null, null, null, null)
};
var edges = new List<RichGraphEdge>
{
// Short path: start -> direct -> end
new("n0", "n1", "call", null, null, null, 1.0, null),
new("n1", "n5", "call", null, null, null, 1.0, null),
// Long path: start -> long1 -> long2 -> long3 -> end
new("n0", "n2", "call", null, null, null, 1.0, null),
new("n2", "n3", "call", null, null, null, 1.0, null),
new("n3", "n4", "call", null, null, null, 1.0, null),
new("n4", "n5", "call", null, null, null, 1.0, null)
};
var roots = new List<RichGraphRoot>
{
new("n0", "http", "/api/start")
};
return new RichGraph(
nodes,
edges,
roots,
new RichGraphAnalyzer("test", "1.0.0", null));
}
private static RichGraph CreateGraphWithMultipleRoots()
{
var nodes = new List<RichGraphNode>
{
new("n1", "sym:root1", null, null, "dotnet", "method", "Root1", null, null, null, null),
new("n2", "sym:root2", null, null, "dotnet", "method", "Root2", null, null, null, null),
new("n3", "sym:middle", null, null, "dotnet", "method", "Middle", null, null, null, null),
new("n4", "sym:sink", null, null, "dotnet", "method", "Sink", null, null, null, null)
};
var edges = new List<RichGraphEdge>
{
new("n1", "n3", "call", null, null, null, 1.0, null),
new("n2", "n3", "call", null, null, null, 1.0, null),
new("n3", "n4", "call", null, null, null, 1.0, null)
};
var roots = new List<RichGraphRoot>
{
new("n1", "http", "/api/root1"),
new("n2", "http", "/api/root2")
};
return new RichGraph(
nodes,
edges,
roots,
new RichGraphAnalyzer("test", "1.0.0", null));
}
#endregion
}