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
|
||||
}
|
||||
@@ -0,0 +1,320 @@
|
||||
using System.Text.Json;
|
||||
using StellaOps.Cryptography;
|
||||
using StellaOps.Scanner.Reachability.Attestation;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Scanner.Reachability.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for <see cref="ReachabilityWitnessDsseBuilder"/>.
|
||||
/// Sprint: SPRINT_3620_0001_0001
|
||||
/// Task: RWD-011
|
||||
/// </summary>
|
||||
public sealed class ReachabilityWitnessDsseBuilderTests
|
||||
{
|
||||
private readonly ReachabilityWitnessDsseBuilder _builder;
|
||||
private readonly FakeTimeProvider _timeProvider;
|
||||
|
||||
public ReachabilityWitnessDsseBuilderTests()
|
||||
{
|
||||
_timeProvider = new FakeTimeProvider(new DateTimeOffset(2025, 12, 18, 10, 0, 0, TimeSpan.Zero));
|
||||
_builder = new ReachabilityWitnessDsseBuilder(
|
||||
CryptoHashFactory.CreateDefault(),
|
||||
_timeProvider);
|
||||
}
|
||||
|
||||
#region BuildStatement Tests
|
||||
|
||||
[Fact]
|
||||
public void BuildStatement_CreatesValidStatement()
|
||||
{
|
||||
var graph = CreateTestGraph();
|
||||
|
||||
var statement = _builder.BuildStatement(
|
||||
graph,
|
||||
graphHash: "blake3:abc123",
|
||||
subjectDigest: "sha256:def456");
|
||||
|
||||
Assert.NotNull(statement);
|
||||
Assert.Equal("https://in-toto.io/Statement/v1", statement.Type);
|
||||
Assert.Equal("https://stella.ops/reachabilityWitness/v1", statement.PredicateType);
|
||||
Assert.Single(statement.Subject);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildStatement_SetsSubjectCorrectly()
|
||||
{
|
||||
var graph = CreateTestGraph();
|
||||
|
||||
var statement = _builder.BuildStatement(
|
||||
graph,
|
||||
graphHash: "blake3:abc123",
|
||||
subjectDigest: "sha256:imageabc123");
|
||||
|
||||
var subject = statement.Subject[0];
|
||||
Assert.Equal("sha256:imageabc123", subject.Name);
|
||||
Assert.Equal("imageabc123", subject.Digest["sha256"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildStatement_ExtractsPredicateCorrectly()
|
||||
{
|
||||
var graph = CreateTestGraph();
|
||||
|
||||
var statement = _builder.BuildStatement(
|
||||
graph,
|
||||
graphHash: "blake3:abc123",
|
||||
subjectDigest: "sha256:def456",
|
||||
graphCasUri: "cas://local/blake3:abc123",
|
||||
policyHash: "sha256:policy123",
|
||||
sourceCommit: "abc123def456");
|
||||
|
||||
var predicate = statement.Predicate as ReachabilityWitnessStatement;
|
||||
Assert.NotNull(predicate);
|
||||
Assert.Equal("stella.ops/reachabilityWitness@v1", predicate.Schema);
|
||||
Assert.Equal("blake3:abc123", predicate.GraphHash);
|
||||
Assert.Equal("cas://local/blake3:abc123", predicate.GraphCasUri);
|
||||
Assert.Equal("sha256:def456", predicate.SubjectDigest);
|
||||
Assert.Equal("sha256:policy123", predicate.PolicyHash);
|
||||
Assert.Equal("abc123def456", predicate.SourceCommit);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildStatement_CountsNodesAndEdges()
|
||||
{
|
||||
var graph = CreateTestGraph();
|
||||
|
||||
var statement = _builder.BuildStatement(
|
||||
graph,
|
||||
graphHash: "blake3:abc123",
|
||||
subjectDigest: "sha256:def456");
|
||||
|
||||
var predicate = statement.Predicate as ReachabilityWitnessStatement;
|
||||
Assert.NotNull(predicate);
|
||||
Assert.Equal(3, predicate.NodeCount);
|
||||
Assert.Equal(2, predicate.EdgeCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildStatement_CountsEntrypoints()
|
||||
{
|
||||
var graph = CreateTestGraphWithRoots();
|
||||
|
||||
var statement = _builder.BuildStatement(
|
||||
graph,
|
||||
graphHash: "blake3:abc123",
|
||||
subjectDigest: "sha256:def456");
|
||||
|
||||
var predicate = statement.Predicate as ReachabilityWitnessStatement;
|
||||
Assert.NotNull(predicate);
|
||||
Assert.Equal(2, predicate.EntrypointCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildStatement_UsesProvidedTimestamp()
|
||||
{
|
||||
var graph = CreateTestGraph();
|
||||
|
||||
var statement = _builder.BuildStatement(
|
||||
graph,
|
||||
graphHash: "blake3:abc123",
|
||||
subjectDigest: "sha256:def456");
|
||||
|
||||
var predicate = statement.Predicate as ReachabilityWitnessStatement;
|
||||
Assert.NotNull(predicate);
|
||||
Assert.Equal(_timeProvider.GetUtcNow(), predicate.GeneratedAt);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildStatement_ExtractsAnalyzerVersion()
|
||||
{
|
||||
var graph = CreateTestGraph();
|
||||
|
||||
var statement = _builder.BuildStatement(
|
||||
graph,
|
||||
graphHash: "blake3:abc123",
|
||||
subjectDigest: "sha256:def456");
|
||||
|
||||
var predicate = statement.Predicate as ReachabilityWitnessStatement;
|
||||
Assert.NotNull(predicate);
|
||||
Assert.Equal("1.0.0", predicate.AnalyzerVersion);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region SerializeStatement Tests
|
||||
|
||||
[Fact]
|
||||
public void SerializeStatement_ProducesValidJson()
|
||||
{
|
||||
var graph = CreateTestGraph();
|
||||
var statement = _builder.BuildStatement(
|
||||
graph,
|
||||
graphHash: "blake3:abc123",
|
||||
subjectDigest: "sha256:def456");
|
||||
|
||||
var bytes = _builder.SerializeStatement(statement);
|
||||
|
||||
Assert.NotEmpty(bytes);
|
||||
var json = System.Text.Encoding.UTF8.GetString(bytes);
|
||||
Assert.Contains("\"_type\":\"https://in-toto.io/Statement/v1\"", json);
|
||||
Assert.Contains("\"predicateType\":\"https://stella.ops/reachabilityWitness/v1\"", json);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SerializeStatement_IsDeterministic()
|
||||
{
|
||||
var graph = CreateTestGraph();
|
||||
var statement = _builder.BuildStatement(
|
||||
graph,
|
||||
graphHash: "blake3:abc123",
|
||||
subjectDigest: "sha256:def456");
|
||||
|
||||
var bytes1 = _builder.SerializeStatement(statement);
|
||||
var bytes2 = _builder.SerializeStatement(statement);
|
||||
|
||||
Assert.Equal(bytes1, bytes2);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region ComputeStatementHash Tests
|
||||
|
||||
[Fact]
|
||||
public void ComputeStatementHash_ReturnsBlake3Hash()
|
||||
{
|
||||
var graph = CreateTestGraph();
|
||||
var statement = _builder.BuildStatement(
|
||||
graph,
|
||||
graphHash: "blake3:abc123",
|
||||
subjectDigest: "sha256:def456");
|
||||
var bytes = _builder.SerializeStatement(statement);
|
||||
|
||||
var hash = _builder.ComputeStatementHash(bytes);
|
||||
|
||||
Assert.StartsWith("blake3:", hash);
|
||||
Assert.Equal(64 + 7, hash.Length); // "blake3:" + 64 hex chars
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ComputeStatementHash_IsDeterministic()
|
||||
{
|
||||
var graph = CreateTestGraph();
|
||||
var statement = _builder.BuildStatement(
|
||||
graph,
|
||||
graphHash: "blake3:abc123",
|
||||
subjectDigest: "sha256:def456");
|
||||
var bytes = _builder.SerializeStatement(statement);
|
||||
|
||||
var hash1 = _builder.ComputeStatementHash(bytes);
|
||||
var hash2 = _builder.ComputeStatementHash(bytes);
|
||||
|
||||
Assert.Equal(hash1, hash2);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Edge Cases
|
||||
|
||||
[Fact]
|
||||
public void BuildStatement_ThrowsForNullGraph()
|
||||
{
|
||||
Assert.Throws<ArgumentNullException>(() =>
|
||||
_builder.BuildStatement(null!, "blake3:abc", "sha256:def"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildStatement_ThrowsForEmptyGraphHash()
|
||||
{
|
||||
var graph = CreateTestGraph();
|
||||
Assert.Throws<ArgumentException>(() =>
|
||||
_builder.BuildStatement(graph, "", "sha256:def"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildStatement_ThrowsForEmptySubjectDigest()
|
||||
{
|
||||
var graph = CreateTestGraph();
|
||||
Assert.Throws<ArgumentException>(() =>
|
||||
_builder.BuildStatement(graph, "blake3:abc", ""));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildStatement_HandlesEmptyGraph()
|
||||
{
|
||||
var graph = new RichGraph(
|
||||
Schema: "richgraph-v1",
|
||||
Analyzer: new RichGraphAnalyzer("test", "1.0.0", null),
|
||||
Nodes: Array.Empty<RichGraphNode>(),
|
||||
Edges: Array.Empty<RichGraphEdge>(),
|
||||
Roots: null);
|
||||
|
||||
var statement = _builder.BuildStatement(
|
||||
graph,
|
||||
graphHash: "blake3:abc123",
|
||||
subjectDigest: "sha256:def456");
|
||||
|
||||
var predicate = statement.Predicate as ReachabilityWitnessStatement;
|
||||
Assert.NotNull(predicate);
|
||||
Assert.Equal(0, predicate.NodeCount);
|
||||
Assert.Equal(0, predicate.EdgeCount);
|
||||
Assert.Equal("unknown", predicate.Language);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Test Helpers
|
||||
|
||||
private static RichGraph CreateTestGraph()
|
||||
{
|
||||
return new RichGraph(
|
||||
Schema: "richgraph-v1",
|
||||
Analyzer: new RichGraphAnalyzer("test-analyzer", "1.0.0", null),
|
||||
Nodes: new[]
|
||||
{
|
||||
new RichGraphNode("n1", "sym:dotnet:A", null, null, "dotnet", "method", "A", null, null, null, null),
|
||||
new RichGraphNode("n2", "sym:dotnet:B", null, null, "dotnet", "method", "B", null, null, null, null),
|
||||
new RichGraphNode("n3", "sym:dotnet:C", null, null, "dotnet", "sink", "C", null, null, null, null)
|
||||
},
|
||||
Edges: new[]
|
||||
{
|
||||
new RichGraphEdge("n1", "n2", "call", null, null, null, 0.9, null),
|
||||
new RichGraphEdge("n2", "n3", "call", null, null, null, 0.9, null)
|
||||
},
|
||||
Roots: null);
|
||||
}
|
||||
|
||||
private static RichGraph CreateTestGraphWithRoots()
|
||||
{
|
||||
return new RichGraph(
|
||||
Schema: "richgraph-v1",
|
||||
Analyzer: new RichGraphAnalyzer("test-analyzer", "1.0.0", null),
|
||||
Nodes: new[]
|
||||
{
|
||||
new RichGraphNode("n1", "sym:dotnet:A", null, null, "dotnet", "method", "A", null, null, null, null),
|
||||
new RichGraphNode("n2", "sym:dotnet:B", null, null, "dotnet", "method", "B", null, null, null, null),
|
||||
new RichGraphNode("n3", "sym:dotnet:C", null, null, "dotnet", "sink", "C", null, null, null, null)
|
||||
},
|
||||
Edges: new[]
|
||||
{
|
||||
new RichGraphEdge("n1", "n2", "call", null, null, null, 0.9, null),
|
||||
new RichGraphEdge("n2", "n3", "call", null, null, null, 0.9, null)
|
||||
},
|
||||
Roots: new[]
|
||||
{
|
||||
new RichGraphRoot("n1", "http", "GET /api"),
|
||||
new RichGraphRoot("n2", "grpc", "Service.Method")
|
||||
});
|
||||
}
|
||||
|
||||
private sealed class FakeTimeProvider : TimeProvider
|
||||
{
|
||||
private readonly DateTimeOffset _fixedTime;
|
||||
|
||||
public FakeTimeProvider(DateTimeOffset fixedTime) => _fixedTime = fixedTime;
|
||||
|
||||
public override DateTimeOffset GetUtcNow() => _fixedTime;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -108,4 +108,30 @@ public class RichGraphWriterTests
|
||||
Assert.Contains("\"type\":\"authRequired\"", json);
|
||||
Assert.Contains("\"guard_symbol\":\"sym:dotnet:B\"", json);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UsesBlake3HashForDefaultProfile()
|
||||
{
|
||||
// WIT-013: Verify BLAKE3 is used for graph hashing
|
||||
var writer = new RichGraphWriter(CryptoHashFactory.CreateDefault());
|
||||
using var temp = new TempDir();
|
||||
|
||||
var union = new ReachabilityUnionGraph(
|
||||
Nodes: new[]
|
||||
{
|
||||
new ReachabilityUnionNode("sym:dotnet:A", "dotnet", "method", "A")
|
||||
},
|
||||
Edges: Array.Empty<ReachabilityUnionEdge>());
|
||||
|
||||
var rich = RichGraphBuilder.FromUnion(union, "test-analyzer", "1.0.0");
|
||||
var result = await writer.WriteAsync(rich, temp.Path, "analysis-blake3");
|
||||
|
||||
// Default profile (world) uses BLAKE3
|
||||
Assert.StartsWith("blake3:", result.GraphHash);
|
||||
Assert.Equal(64 + 7, result.GraphHash.Length); // "blake3:" (7) + 64 hex chars
|
||||
|
||||
// Verify meta.json also contains the blake3-prefixed hash
|
||||
var metaJson = await File.ReadAllTextAsync(result.MetaPath);
|
||||
Assert.Contains("\"graph_hash\":\"blake3:", metaJson);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user