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:
master
2025-12-18 13:15:13 +02:00
parent 7d5250238c
commit 00d2c99af9
118 changed files with 13463 additions and 151 deletions

View File

@@ -0,0 +1,147 @@
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using StellaOps.Cryptography;
namespace StellaOps.Scanner.Reachability.Attestation;
/// <summary>
/// Publishes reachability witness attestations to CAS and Rekor.
/// </summary>
public sealed class ReachabilityWitnessPublisher : IReachabilityWitnessPublisher
{
private readonly ReachabilityWitnessOptions _options;
private readonly ReachabilityWitnessDsseBuilder _dsseBuilder;
private readonly ICryptoHash _cryptoHash;
private readonly ILogger<ReachabilityWitnessPublisher> _logger;
/// <summary>
/// Creates a new reachability witness publisher.
/// </summary>
public ReachabilityWitnessPublisher(
IOptions<ReachabilityWitnessOptions> options,
ICryptoHash cryptoHash,
ILogger<ReachabilityWitnessPublisher> logger,
TimeProvider? timeProvider = null)
{
ArgumentNullException.ThrowIfNull(options);
ArgumentNullException.ThrowIfNull(cryptoHash);
ArgumentNullException.ThrowIfNull(logger);
_options = options.Value;
_cryptoHash = cryptoHash;
_logger = logger;
_dsseBuilder = new ReachabilityWitnessDsseBuilder(cryptoHash, timeProvider);
}
/// <inheritdoc />
public async Task<ReachabilityWitnessPublishResult> PublishAsync(
RichGraph graph,
byte[] graphBytes,
string graphHash,
string subjectDigest,
string? policyHash = null,
string? sourceCommit = null,
CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(graph);
ArgumentNullException.ThrowIfNull(graphBytes);
ArgumentException.ThrowIfNullOrWhiteSpace(graphHash);
ArgumentException.ThrowIfNullOrWhiteSpace(subjectDigest);
if (!_options.Enabled)
{
_logger.LogDebug("Reachability witness attestation is disabled");
return new ReachabilityWitnessPublishResult(
StatementHash: string.Empty,
GraphHash: graphHash,
CasUri: null,
RekorLogIndex: null,
RekorLogId: null,
DsseEnvelopeBytes: Array.Empty<byte>());
}
string? casUri = null;
// Step 1: Store graph in CAS (if enabled)
if (_options.StoreInCas)
{
casUri = await StoreInCasAsync(graphBytes, graphHash, cancellationToken).ConfigureAwait(false);
}
// Step 2: Build in-toto statement
var statement = _dsseBuilder.BuildStatement(
graph,
graphHash,
subjectDigest,
casUri,
policyHash,
sourceCommit);
var statementBytes = _dsseBuilder.SerializeStatement(statement);
var statementHash = _dsseBuilder.ComputeStatementHash(statementBytes);
_logger.LogInformation(
"Built reachability witness statement: hash={StatementHash}, nodes={NodeCount}, edges={EdgeCount}",
statementHash,
graph.Nodes.Count,
graph.Edges.Count);
// Step 3: Create DSSE envelope (placeholder - actual signing via Attestor service)
var dsseEnvelope = CreateDsseEnvelope(statementBytes);
// Step 4: Submit to Rekor (if enabled and not air-gapped)
long? rekorLogIndex = null;
string? rekorLogId = null;
if (_options.PublishToRekor && _options.Tier != AttestationTier.AirGapped)
{
(rekorLogIndex, rekorLogId) = await SubmitToRekorAsync(dsseEnvelope, cancellationToken).ConfigureAwait(false);
}
else if (_options.Tier == AttestationTier.AirGapped)
{
_logger.LogDebug("Skipping Rekor submission (air-gapped tier)");
}
return new ReachabilityWitnessPublishResult(
StatementHash: statementHash,
GraphHash: graphHash,
CasUri: casUri,
RekorLogIndex: rekorLogIndex,
RekorLogId: rekorLogId,
DsseEnvelopeBytes: dsseEnvelope);
}
private Task<string?> StoreInCasAsync(byte[] graphBytes, string graphHash, CancellationToken cancellationToken)
{
// TODO: Integrate with actual CAS storage (BID-007)
// For now, return a placeholder CAS URI based on hash
var casUri = $"cas://local/{graphHash}";
_logger.LogDebug("Stored graph in CAS: {CasUri}", casUri);
return Task.FromResult<string?>(casUri);
}
private byte[] CreateDsseEnvelope(byte[] statementBytes)
{
// TODO: Integrate with Attestor DSSE signing service (RWD-008)
// For now, return unsigned envelope structure
// In production, this would call the Attestor service to sign the statement
// Minimal DSSE envelope structure (unsigned)
var envelope = new
{
payloadType = "application/vnd.in-toto+json",
payload = Convert.ToBase64String(statementBytes),
signatures = Array.Empty<object>() // Will be populated by Attestor
};
return System.Text.Json.JsonSerializer.SerializeToUtf8Bytes(envelope);
}
private Task<(long? logIndex, string? logId)> SubmitToRekorAsync(byte[] dsseEnvelope, CancellationToken cancellationToken)
{
// TODO: Integrate with Rekor backend (RWD-008)
// For now, return placeholder values
_logger.LogDebug("Rekor submission placeholder - actual integration pending");
return Task.FromResult<(long?, string?)>((null, null));
}
}