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