- 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.
388 lines
13 KiB
C#
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
|
|
}
|