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)}";
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user