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.
This commit is contained in:
@@ -0,0 +1,387 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user