feat: Add PathViewer and RiskDriftCard components with templates and styles
- Implemented PathViewerComponent for visualizing reachability call paths. - Added RiskDriftCardComponent to display reachability drift results. - Created corresponding HTML templates and SCSS styles for both components. - Introduced test fixtures for reachability analysis in JSON format. - Enhanced user interaction with collapsible and expandable features in PathViewer. - Included risk trend visualization and summary metrics in RiskDriftCard.
This commit is contained in:
@@ -0,0 +1,304 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// AttestingRichGraphWriterTests.cs
|
||||
// Sprint: SPRINT_3620_0001_0001_reachability_witness_dsse
|
||||
// Description: Tests for AttestingRichGraphWriter integration.
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Cryptography;
|
||||
using StellaOps.Scanner.Reachability.Attestation;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Scanner.Reachability.Tests;
|
||||
|
||||
public class AttestingRichGraphWriterTests : IAsyncLifetime
|
||||
{
|
||||
private DirectoryInfo _tempDir = null!;
|
||||
|
||||
public Task InitializeAsync()
|
||||
{
|
||||
_tempDir = Directory.CreateTempSubdirectory("attesting-writer-test-");
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task DisposeAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (_tempDir.Exists)
|
||||
{
|
||||
_tempDir.Delete(recursive: true);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore cleanup errors
|
||||
}
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task WriteWithAttestationAsync_WhenEnabled_ProducesAttestationFile()
|
||||
{
|
||||
// Arrange
|
||||
var cryptoHash = new TestCryptoHash();
|
||||
var graphWriter = new RichGraphWriter(cryptoHash);
|
||||
var witnessOptions = Options.Create(new ReachabilityWitnessOptions
|
||||
{
|
||||
Enabled = true,
|
||||
StoreInCas = false,
|
||||
PublishToRekor = false
|
||||
});
|
||||
var witnessPublisher = new ReachabilityWitnessPublisher(
|
||||
witnessOptions,
|
||||
cryptoHash,
|
||||
NullLogger<ReachabilityWitnessPublisher>.Instance);
|
||||
|
||||
var writer = new AttestingRichGraphWriter(
|
||||
graphWriter,
|
||||
witnessPublisher,
|
||||
witnessOptions,
|
||||
NullLogger<AttestingRichGraphWriter>.Instance);
|
||||
|
||||
var graph = CreateTestGraph();
|
||||
|
||||
// Act
|
||||
var result = await writer.WriteWithAttestationAsync(
|
||||
graph,
|
||||
_tempDir.FullName,
|
||||
"test-analysis",
|
||||
"sha256:abc123");
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.True(File.Exists(result.GraphPath));
|
||||
Assert.True(File.Exists(result.MetaPath));
|
||||
Assert.NotNull(result.AttestationPath);
|
||||
Assert.True(File.Exists(result.AttestationPath));
|
||||
Assert.NotNull(result.WitnessResult);
|
||||
Assert.NotEmpty(result.WitnessResult.StatementHash);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task WriteWithAttestationAsync_WhenDisabled_NoAttestationFile()
|
||||
{
|
||||
// Arrange
|
||||
var cryptoHash = new TestCryptoHash();
|
||||
var graphWriter = new RichGraphWriter(cryptoHash);
|
||||
var witnessOptions = Options.Create(new ReachabilityWitnessOptions
|
||||
{
|
||||
Enabled = false
|
||||
});
|
||||
var witnessPublisher = new ReachabilityWitnessPublisher(
|
||||
witnessOptions,
|
||||
cryptoHash,
|
||||
NullLogger<ReachabilityWitnessPublisher>.Instance);
|
||||
|
||||
var writer = new AttestingRichGraphWriter(
|
||||
graphWriter,
|
||||
witnessPublisher,
|
||||
witnessOptions,
|
||||
NullLogger<AttestingRichGraphWriter>.Instance);
|
||||
|
||||
var graph = CreateTestGraph();
|
||||
|
||||
// Act
|
||||
var result = await writer.WriteWithAttestationAsync(
|
||||
graph,
|
||||
_tempDir.FullName,
|
||||
"test-analysis",
|
||||
"sha256:abc123");
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.True(File.Exists(result.GraphPath));
|
||||
Assert.True(File.Exists(result.MetaPath));
|
||||
Assert.Null(result.AttestationPath);
|
||||
Assert.Null(result.WitnessResult);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task WriteWithAttestationAsync_AttestationContainsValidDsse()
|
||||
{
|
||||
// Arrange
|
||||
var cryptoHash = new TestCryptoHash();
|
||||
var graphWriter = new RichGraphWriter(cryptoHash);
|
||||
var witnessOptions = Options.Create(new ReachabilityWitnessOptions
|
||||
{
|
||||
Enabled = true,
|
||||
StoreInCas = false,
|
||||
PublishToRekor = false
|
||||
});
|
||||
var witnessPublisher = new ReachabilityWitnessPublisher(
|
||||
witnessOptions,
|
||||
cryptoHash,
|
||||
NullLogger<ReachabilityWitnessPublisher>.Instance);
|
||||
|
||||
var writer = new AttestingRichGraphWriter(
|
||||
graphWriter,
|
||||
witnessPublisher,
|
||||
witnessOptions,
|
||||
NullLogger<AttestingRichGraphWriter>.Instance);
|
||||
|
||||
var graph = CreateTestGraph();
|
||||
|
||||
// Act
|
||||
var result = await writer.WriteWithAttestationAsync(
|
||||
graph,
|
||||
_tempDir.FullName,
|
||||
"test-analysis",
|
||||
"sha256:abc123");
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result.AttestationPath);
|
||||
var dsseJson = await File.ReadAllTextAsync(result.AttestationPath);
|
||||
Assert.Contains("payloadType", dsseJson);
|
||||
// Note: + may be encoded as \u002B in JSON
|
||||
Assert.True(dsseJson.Contains("application/vnd.in-toto+json") || dsseJson.Contains("application/vnd.in-toto\\u002Bjson"));
|
||||
Assert.Contains("payload", dsseJson);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task WriteWithAttestationAsync_GraphHashIsDeterministic()
|
||||
{
|
||||
// Arrange
|
||||
var cryptoHash = new TestCryptoHash();
|
||||
var graphWriter = new RichGraphWriter(cryptoHash);
|
||||
var witnessOptions = Options.Create(new ReachabilityWitnessOptions
|
||||
{
|
||||
Enabled = true,
|
||||
StoreInCas = false,
|
||||
PublishToRekor = false
|
||||
});
|
||||
var witnessPublisher = new ReachabilityWitnessPublisher(
|
||||
witnessOptions,
|
||||
cryptoHash,
|
||||
NullLogger<ReachabilityWitnessPublisher>.Instance);
|
||||
|
||||
var writer = new AttestingRichGraphWriter(
|
||||
graphWriter,
|
||||
witnessPublisher,
|
||||
witnessOptions,
|
||||
NullLogger<AttestingRichGraphWriter>.Instance);
|
||||
|
||||
var graph = CreateTestGraph();
|
||||
|
||||
// Act - write twice with same input
|
||||
var result1 = await writer.WriteWithAttestationAsync(
|
||||
graph,
|
||||
_tempDir.FullName,
|
||||
"analysis-1",
|
||||
"sha256:abc123");
|
||||
|
||||
var result2 = await writer.WriteWithAttestationAsync(
|
||||
graph,
|
||||
_tempDir.FullName,
|
||||
"analysis-2",
|
||||
"sha256:abc123");
|
||||
|
||||
// Assert - same graph should produce same hash
|
||||
Assert.Equal(result1.GraphHash, result2.GraphHash);
|
||||
}
|
||||
|
||||
private static RichGraph CreateTestGraph()
|
||||
{
|
||||
return new RichGraph(
|
||||
Nodes: new[]
|
||||
{
|
||||
new RichGraphNode(
|
||||
Id: "entry-1",
|
||||
SymbolId: "Handler.handle",
|
||||
CodeId: null,
|
||||
Purl: "pkg:maven/com.example/handler@1.0.0",
|
||||
Lang: "java",
|
||||
Kind: "http_handler",
|
||||
Display: "GET /api/users",
|
||||
BuildId: null,
|
||||
Evidence: null,
|
||||
Attributes: null,
|
||||
SymbolDigest: "sha256:entry1digest"),
|
||||
new RichGraphNode(
|
||||
Id: "sink-1",
|
||||
SymbolId: "DB.executeQuery",
|
||||
CodeId: null,
|
||||
Purl: "pkg:maven/org.database/driver@2.0.0",
|
||||
Lang: "java",
|
||||
Kind: "sql_sink",
|
||||
Display: "executeQuery(String)",
|
||||
BuildId: null,
|
||||
Evidence: null,
|
||||
Attributes: new Dictionary<string, string> { ["is_sink"] = "true" },
|
||||
SymbolDigest: "sha256:sink1digest")
|
||||
},
|
||||
Edges: new[]
|
||||
{
|
||||
new RichGraphEdge(
|
||||
From: "entry-1",
|
||||
To: "sink-1",
|
||||
Kind: "call",
|
||||
Purl: null,
|
||||
SymbolDigest: null,
|
||||
Evidence: null,
|
||||
Confidence: 1.0,
|
||||
Candidates: null)
|
||||
},
|
||||
Roots: new[]
|
||||
{
|
||||
new RichGraphRoot("entry-1", "runtime", null)
|
||||
},
|
||||
Analyzer: new RichGraphAnalyzer("stellaops.scanner.reachability", "1.0.0", null),
|
||||
Schema: "richgraph-v1"
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test crypto hash implementation.
|
||||
/// </summary>
|
||||
private sealed class TestCryptoHash : ICryptoHash
|
||||
{
|
||||
public byte[] ComputeHash(ReadOnlySpan<byte> data, string? algorithmId = null)
|
||||
=> System.Security.Cryptography.SHA256.HashData(data);
|
||||
|
||||
public string ComputeHashHex(ReadOnlySpan<byte> data, string? algorithmId = null)
|
||||
=> Convert.ToHexString(ComputeHash(data, algorithmId)).ToLowerInvariant();
|
||||
|
||||
public string ComputeHashBase64(ReadOnlySpan<byte> data, string? algorithmId = null)
|
||||
=> Convert.ToBase64String(ComputeHash(data, algorithmId));
|
||||
|
||||
public async ValueTask<byte[]> ComputeHashAsync(Stream stream, string? algorithmId = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
using var buffer = new MemoryStream();
|
||||
await stream.CopyToAsync(buffer, cancellationToken).ConfigureAwait(false);
|
||||
return System.Security.Cryptography.SHA256.HashData(buffer.ToArray());
|
||||
}
|
||||
|
||||
public async ValueTask<string> ComputeHashHexAsync(Stream stream, string? algorithmId = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var hash = await ComputeHashAsync(stream, algorithmId, cancellationToken).ConfigureAwait(false);
|
||||
return Convert.ToHexString(hash).ToLowerInvariant();
|
||||
}
|
||||
|
||||
public byte[] ComputeHashForPurpose(ReadOnlySpan<byte> data, string purpose)
|
||||
=> ComputeHash(data);
|
||||
|
||||
public string ComputeHashHexForPurpose(ReadOnlySpan<byte> data, string purpose)
|
||||
=> ComputeHashHex(data);
|
||||
|
||||
public string ComputeHashBase64ForPurpose(ReadOnlySpan<byte> data, string purpose)
|
||||
=> ComputeHashBase64(data);
|
||||
|
||||
public async ValueTask<byte[]> ComputeHashForPurposeAsync(Stream stream, string purpose, CancellationToken cancellationToken = default)
|
||||
=> await ComputeHashAsync(stream, null, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
public async ValueTask<string> ComputeHashHexForPurposeAsync(Stream stream, string purpose, CancellationToken cancellationToken = default)
|
||||
=> await ComputeHashHexAsync(stream, null, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
public string GetAlgorithmForPurpose(string purpose) => "blake3";
|
||||
|
||||
public string GetHashPrefix(string purpose) => "blake3:";
|
||||
|
||||
public string ComputePrefixedHashForPurpose(ReadOnlySpan<byte> data, string purpose)
|
||||
=> $"blake3:{ComputeHashHex(data)}";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"_type": "https://in-toto.io/Statement/v1",
|
||||
"subject": [
|
||||
{
|
||||
"name": "pkg:oci/test-image@sha256:abc123",
|
||||
"digest": {
|
||||
"sha256": "abc123def456789012345678901234567890123456789012345678901234"
|
||||
}
|
||||
}
|
||||
],
|
||||
"predicateType": "https://stellaops.io/attestation/reachabilityWitness/v1",
|
||||
"predicate": {
|
||||
"version": "1.0.0",
|
||||
"analysisTimestamp": "2025-01-01T00:00:00.0000000Z",
|
||||
"analyzer": {
|
||||
"name": "stellaops.scanner.reachability",
|
||||
"version": "1.0.0"
|
||||
},
|
||||
"graph": {
|
||||
"schema": "richgraph-v1",
|
||||
"hash": "blake3:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
|
||||
"nodeCount": 3,
|
||||
"edgeCount": 2
|
||||
},
|
||||
"summary": {
|
||||
"sinkCount": 1,
|
||||
"entrypointCount": 1,
|
||||
"pathCount": 1,
|
||||
"gateCoverage": 0.0
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"_type": "https://in-toto.io/Statement/v1",
|
||||
"subject": [
|
||||
{
|
||||
"name": "pkg:oci/production-app@sha256:xyz789",
|
||||
"digest": {
|
||||
"sha256": "xyz789abc123def456789012345678901234567890123456789012345678"
|
||||
}
|
||||
}
|
||||
],
|
||||
"predicateType": "https://stellaops.io/attestation/reachabilityWitness/v1",
|
||||
"predicate": {
|
||||
"version": "1.0.0",
|
||||
"analysisTimestamp": "2025-01-15T12:30:00.0000000Z",
|
||||
"analyzer": {
|
||||
"name": "stellaops.scanner.reachability",
|
||||
"version": "1.0.0"
|
||||
},
|
||||
"graph": {
|
||||
"schema": "richgraph-v1",
|
||||
"hash": "blake3:fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210",
|
||||
"nodeCount": 150,
|
||||
"edgeCount": 340,
|
||||
"casUri": "cas://reachability/graphs/fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210"
|
||||
},
|
||||
"summary": {
|
||||
"sinkCount": 12,
|
||||
"entrypointCount": 8,
|
||||
"pathCount": 45,
|
||||
"gateCoverage": 0.67
|
||||
},
|
||||
"policy": {
|
||||
"hash": "sha256:policy123456789012345678901234567890123456789012345678901234"
|
||||
},
|
||||
"source": {
|
||||
"commit": "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2"
|
||||
},
|
||||
"runtime": {
|
||||
"observedAt": "2025-01-15T12:25:00.0000000Z",
|
||||
"traceCount": 1250,
|
||||
"coveredPaths": 38,
|
||||
"runtimeConfidence": 0.84
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -206,15 +206,8 @@ public class PathExplanationServiceTests
|
||||
|
||||
private static RichGraph CreateSimpleGraph()
|
||||
{
|
||||
return new RichGraph
|
||||
{
|
||||
Schema = "stellaops.richgraph.v1",
|
||||
Meta = new RichGraphMeta { Hash = "test-hash" },
|
||||
Roots = new[]
|
||||
{
|
||||
new RichGraphRoot("entry-1", "runtime", null)
|
||||
},
|
||||
Nodes = new[]
|
||||
return new RichGraph(
|
||||
Nodes: new[]
|
||||
{
|
||||
new RichGraphNode(
|
||||
Id: "entry-1",
|
||||
@@ -241,21 +234,23 @@ public class PathExplanationServiceTests
|
||||
Attributes: new Dictionary<string, string> { ["is_sink"] = "true" },
|
||||
SymbolDigest: null)
|
||||
},
|
||||
Edges = new[]
|
||||
Edges: new[]
|
||||
{
|
||||
new RichGraphEdge("entry-1", "sink-1", "call", null)
|
||||
}
|
||||
};
|
||||
new RichGraphEdge("entry-1", "sink-1", "call", null, null, null, 1.0, null)
|
||||
},
|
||||
Roots: new[]
|
||||
{
|
||||
new RichGraphRoot("entry-1", "runtime", null)
|
||||
},
|
||||
Analyzer: new RichGraphAnalyzer("test", "1.0", null),
|
||||
Schema: "stellaops.richgraph.v1"
|
||||
);
|
||||
}
|
||||
|
||||
private static RichGraph CreateGraphWithMultipleSinks()
|
||||
{
|
||||
return new RichGraph
|
||||
{
|
||||
Schema = "stellaops.richgraph.v1",
|
||||
Meta = new RichGraphMeta { Hash = "test-hash" },
|
||||
Roots = new[] { new RichGraphRoot("entry-1", "runtime", null) },
|
||||
Nodes = new[]
|
||||
return new RichGraph(
|
||||
Nodes: new[]
|
||||
{
|
||||
new RichGraphNode("entry-1", "Handler", null, null, "java", "handler", null, null, null, null, null),
|
||||
new RichGraphNode("sink-1", "Sink1", null, null, "java", "sink", null, null, null,
|
||||
@@ -263,12 +258,15 @@ public class PathExplanationServiceTests
|
||||
new RichGraphNode("sink-2", "Sink2", null, null, "java", "sink", null, null, null,
|
||||
new Dictionary<string, string> { ["is_sink"] = "true" }, null)
|
||||
},
|
||||
Edges = new[]
|
||||
Edges: new[]
|
||||
{
|
||||
new RichGraphEdge("entry-1", "sink-1", "call", null),
|
||||
new RichGraphEdge("entry-1", "sink-2", "call", null)
|
||||
}
|
||||
};
|
||||
new RichGraphEdge("entry-1", "sink-1", "call", null, null, null, 1.0, null),
|
||||
new RichGraphEdge("entry-1", "sink-2", "call", null, null, null, 1.0, null)
|
||||
},
|
||||
Roots: new[] { new RichGraphRoot("entry-1", "runtime", null) },
|
||||
Analyzer: new RichGraphAnalyzer("test", "1.0", null),
|
||||
Schema: "stellaops.richgraph.v1"
|
||||
);
|
||||
}
|
||||
|
||||
private static RichGraph CreateGraphWithGates()
|
||||
@@ -285,22 +283,21 @@ public class PathExplanationServiceTests
|
||||
}
|
||||
};
|
||||
|
||||
return new RichGraph
|
||||
{
|
||||
Schema = "stellaops.richgraph.v1",
|
||||
Meta = new RichGraphMeta { Hash = "test-hash" },
|
||||
Roots = new[] { new RichGraphRoot("entry-1", "runtime", null) },
|
||||
Nodes = new[]
|
||||
return new RichGraph(
|
||||
Nodes: new[]
|
||||
{
|
||||
new RichGraphNode("entry-1", "Handler", null, null, "java", "handler", null, null, null, null, null),
|
||||
new RichGraphNode("sink-1", "Sink", null, null, "java", "sink", null, null, null,
|
||||
new Dictionary<string, string> { ["is_sink"] = "true" }, null)
|
||||
},
|
||||
Edges = new[]
|
||||
Edges: new[]
|
||||
{
|
||||
new RichGraphEdge("entry-1", "sink-1", "call", gates)
|
||||
}
|
||||
};
|
||||
new RichGraphEdge("entry-1", "sink-1", "call", null, null, null, 1.0, null, gates)
|
||||
},
|
||||
Roots: new[] { new RichGraphRoot("entry-1", "runtime", null) },
|
||||
Analyzer: new RichGraphAnalyzer("test", "1.0", null),
|
||||
Schema: "stellaops.richgraph.v1"
|
||||
);
|
||||
}
|
||||
|
||||
private static RichGraph CreateDeepGraph(int depth)
|
||||
@@ -317,18 +314,17 @@ public class PathExplanationServiceTests
|
||||
|
||||
if (i > 0)
|
||||
{
|
||||
edges.Add(new RichGraphEdge($"node-{i - 1}", $"node-{i}", "call", null));
|
||||
edges.Add(new RichGraphEdge($"node-{i - 1}", $"node-{i}", "call", null, null, null, 1.0, null));
|
||||
}
|
||||
}
|
||||
|
||||
return new RichGraph
|
||||
{
|
||||
Schema = "stellaops.richgraph.v1",
|
||||
Meta = new RichGraphMeta { Hash = "test-hash" },
|
||||
Roots = new[] { new RichGraphRoot("node-0", "runtime", null) },
|
||||
Nodes = nodes,
|
||||
Edges = edges
|
||||
};
|
||||
return new RichGraph(
|
||||
Nodes: nodes,
|
||||
Edges: edges,
|
||||
Roots: new[] { new RichGraphRoot("node-0", "runtime", null) },
|
||||
Analyzer: new RichGraphAnalyzer("test", "1.0", null),
|
||||
Schema: "stellaops.richgraph.v1"
|
||||
);
|
||||
}
|
||||
|
||||
private static RichGraph CreateGraphWithMultiplePaths(int pathCount)
|
||||
@@ -344,17 +340,16 @@ public class PathExplanationServiceTests
|
||||
{
|
||||
nodes.Add(new RichGraphNode($"sink-{i}", $"Sink{i}", null, null, "java", "sink", null, null, null,
|
||||
new Dictionary<string, string> { ["is_sink"] = "true" }, null));
|
||||
edges.Add(new RichGraphEdge("entry-1", $"sink-{i}", "call", null));
|
||||
edges.Add(new RichGraphEdge("entry-1", $"sink-{i}", "call", null, null, null, 1.0, null));
|
||||
}
|
||||
|
||||
return new RichGraph
|
||||
{
|
||||
Schema = "stellaops.richgraph.v1",
|
||||
Meta = new RichGraphMeta { Hash = "test-hash" },
|
||||
Roots = new[] { new RichGraphRoot("entry-1", "runtime", null) },
|
||||
Nodes = nodes,
|
||||
Edges = edges
|
||||
};
|
||||
return new RichGraph(
|
||||
Nodes: nodes,
|
||||
Edges: edges,
|
||||
Roots: new[] { new RichGraphRoot("entry-1", "runtime", null) },
|
||||
Analyzer: new RichGraphAnalyzer("test", "1.0", null),
|
||||
Schema: "stellaops.richgraph.v1"
|
||||
);
|
||||
}
|
||||
|
||||
private static ExplainedPath CreateTestPath()
|
||||
@@ -364,7 +359,7 @@ public class PathExplanationServiceTests
|
||||
PathId = "entry:sink:0",
|
||||
SinkId = "sink-1",
|
||||
SinkSymbol = "DB.query",
|
||||
SinkCategory = SinkCategory.SqlRaw,
|
||||
SinkCategory = Explanation.SinkCategory.SqlRaw,
|
||||
EntrypointId = "entry-1",
|
||||
EntrypointSymbol = "Handler.handle",
|
||||
EntrypointType = EntrypointType.HttpEndpoint,
|
||||
@@ -402,7 +397,7 @@ public class PathExplanationServiceTests
|
||||
PathId = "entry:sink:0",
|
||||
SinkId = "sink-1",
|
||||
SinkSymbol = "DB.query",
|
||||
SinkCategory = SinkCategory.SqlRaw,
|
||||
SinkCategory = Explanation.SinkCategory.SqlRaw,
|
||||
EntrypointId = "entry-1",
|
||||
EntrypointSymbol = "Handler.handle",
|
||||
EntrypointType = EntrypointType.HttpEndpoint,
|
||||
|
||||
@@ -132,6 +132,6 @@ public class RichGraphWriterTests
|
||||
|
||||
// Verify meta.json also contains the blake3-prefixed hash
|
||||
var metaJson = await File.ReadAllTextAsync(result.MetaPath);
|
||||
Assert.Contains("\"graph_hash\":\"blake3:", metaJson);
|
||||
Assert.Contains("\"graph_hash\": \"blake3:", metaJson);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user