Fix build and code structure improvements. New but essential UI functionality. CI improvements. Documentation improvements. AI module improvements.
This commit is contained in:
@@ -0,0 +1,272 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// ReplayHashService.cs
|
||||
// Sprint: SPRINT_20251228_007_BE_sbom_lineage_graph_ii (LIN-BE-023)
|
||||
// Task: Compute replay hash per lineage node
|
||||
// Description: Implements deterministic replay hash computation per spec:
|
||||
// Hash = SHA256(sbom_digest + feeds_snapshot_digest +
|
||||
// policy_version + vex_verdicts_digest + timestamp)
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using System.Diagnostics;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace StellaOps.SbomService.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Implementation of <see cref="IReplayHashService"/>.
|
||||
/// Computes deterministic replay hashes for lineage nodes.
|
||||
/// </summary>
|
||||
internal sealed class ReplayHashService : IReplayHashService
|
||||
{
|
||||
private readonly IFeedsSnapshotService? _feedsService;
|
||||
private readonly IPolicyVersionService? _policyService;
|
||||
private readonly IVexVerdictsDigestService? _vexService;
|
||||
private readonly ILogger<ReplayHashService> _logger;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
private readonly ActivitySource _activitySource = new("StellaOps.SbomService.ReplayHash");
|
||||
|
||||
// Delimiter used between hash components for determinism
|
||||
private const char Delimiter = '|';
|
||||
|
||||
// Timestamp format for reproducibility (minute precision)
|
||||
private const string TimestampFormat = "yyyy-MM-ddTHH:mm";
|
||||
|
||||
public ReplayHashService(
|
||||
ILogger<ReplayHashService> logger,
|
||||
IFeedsSnapshotService? feedsService = null,
|
||||
IPolicyVersionService? policyService = null,
|
||||
IVexVerdictsDigestService? vexService = null,
|
||||
TimeProvider? timeProvider = null)
|
||||
{
|
||||
_logger = logger;
|
||||
_feedsService = feedsService;
|
||||
_policyService = policyService;
|
||||
_vexService = vexService;
|
||||
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string ComputeHash(ReplayHashInputs inputs)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(inputs);
|
||||
|
||||
using var activity = _activitySource.StartActivity("ComputeReplayHash");
|
||||
activity?.SetTag("sbom_digest", inputs.SbomDigest);
|
||||
|
||||
// Build canonical string representation
|
||||
// Order: sbom_digest | feeds_snapshot_digest | policy_version | vex_verdicts_digest | timestamp
|
||||
var canonicalString = BuildCanonicalString(inputs);
|
||||
|
||||
// Compute SHA256 hash
|
||||
var bytes = Encoding.UTF8.GetBytes(canonicalString);
|
||||
var hash = SHA256.HashData(bytes);
|
||||
var hexHash = Convert.ToHexStringLower(hash);
|
||||
|
||||
activity?.SetTag("replay_hash", hexHash);
|
||||
|
||||
_logger.LogDebug(
|
||||
"Computed replay hash {Hash} for SBOM {SbomDigest}",
|
||||
hexHash,
|
||||
inputs.SbomDigest);
|
||||
|
||||
return hexHash;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<ReplayHashResult> ComputeHashAsync(
|
||||
string sbomDigest,
|
||||
string tenantId,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(sbomDigest);
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(tenantId);
|
||||
|
||||
using var activity = _activitySource.StartActivity("ComputeReplayHashAsync");
|
||||
activity?.SetTag("sbom_digest", sbomDigest);
|
||||
activity?.SetTag("tenant_id", tenantId);
|
||||
|
||||
var now = _timeProvider.GetUtcNow();
|
||||
|
||||
// Gather inputs from services
|
||||
var feedsDigest = await GetFeedsSnapshotDigestAsync(tenantId, ct);
|
||||
var policyVersion = await GetPolicyVersionAsync(tenantId, ct);
|
||||
var vexDigest = await GetVexVerdictsDigestAsync(sbomDigest, tenantId, ct);
|
||||
|
||||
var inputs = new ReplayHashInputs
|
||||
{
|
||||
SbomDigest = sbomDigest,
|
||||
FeedsSnapshotDigest = feedsDigest,
|
||||
PolicyVersion = policyVersion,
|
||||
VexVerdictsDigest = vexDigest,
|
||||
Timestamp = TruncateToMinute(now)
|
||||
};
|
||||
|
||||
var hash = ComputeHash(inputs);
|
||||
|
||||
return new ReplayHashResult
|
||||
{
|
||||
Hash = hash,
|
||||
Inputs = inputs,
|
||||
ComputedAt = now
|
||||
};
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool VerifyHash(string expectedHash, ReplayHashInputs inputs)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(expectedHash);
|
||||
ArgumentNullException.ThrowIfNull(inputs);
|
||||
|
||||
var computedHash = ComputeHash(inputs);
|
||||
var matches = string.Equals(expectedHash, computedHash, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
if (!matches)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"Replay hash verification failed. Expected={Expected}, Computed={Computed}",
|
||||
expectedHash,
|
||||
computedHash);
|
||||
}
|
||||
|
||||
return matches;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds the canonical string representation for hashing.
|
||||
/// Components are sorted and concatenated with delimiters for determinism.
|
||||
/// </summary>
|
||||
private static string BuildCanonicalString(ReplayHashInputs inputs)
|
||||
{
|
||||
var sb = new StringBuilder(512);
|
||||
|
||||
// Component 1: SBOM digest (normalized to lowercase)
|
||||
sb.Append(NormalizeDigest(inputs.SbomDigest));
|
||||
sb.Append(Delimiter);
|
||||
|
||||
// Component 2: Feeds snapshot digest
|
||||
sb.Append(NormalizeDigest(inputs.FeedsSnapshotDigest));
|
||||
sb.Append(Delimiter);
|
||||
|
||||
// Component 3: Policy version (trimmed, lowercase for consistency)
|
||||
sb.Append(inputs.PolicyVersion.Trim().ToLowerInvariant());
|
||||
sb.Append(Delimiter);
|
||||
|
||||
// Component 4: VEX verdicts digest
|
||||
sb.Append(NormalizeDigest(inputs.VexVerdictsDigest));
|
||||
sb.Append(Delimiter);
|
||||
|
||||
// Component 5: Timestamp (minute precision, UTC)
|
||||
sb.Append(inputs.Timestamp.ToUniversalTime().ToString(TimestampFormat));
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Normalizes a digest string for consistent hashing.
|
||||
/// </summary>
|
||||
private static string NormalizeDigest(string digest)
|
||||
{
|
||||
// Handle digest formats: sha256:xxxx, xxxx (bare)
|
||||
var normalized = digest.Trim().ToLowerInvariant();
|
||||
|
||||
// If it doesn't have algorithm prefix, assume sha256
|
||||
if (!normalized.Contains(':'))
|
||||
{
|
||||
normalized = $"sha256:{normalized}";
|
||||
}
|
||||
|
||||
return normalized;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Truncates a timestamp to minute precision for reproducibility.
|
||||
/// </summary>
|
||||
private static DateTimeOffset TruncateToMinute(DateTimeOffset timestamp)
|
||||
{
|
||||
return new DateTimeOffset(
|
||||
timestamp.Year,
|
||||
timestamp.Month,
|
||||
timestamp.Day,
|
||||
timestamp.Hour,
|
||||
timestamp.Minute,
|
||||
0,
|
||||
TimeSpan.Zero);
|
||||
}
|
||||
|
||||
private async Task<string> GetFeedsSnapshotDigestAsync(string tenantId, CancellationToken ct)
|
||||
{
|
||||
if (_feedsService is not null)
|
||||
{
|
||||
return await _feedsService.GetCurrentSnapshotDigestAsync(tenantId, ct);
|
||||
}
|
||||
|
||||
// Fallback: return a placeholder indicating feeds service not available
|
||||
_logger.LogDebug("FeedsSnapshotService not available, using placeholder digest");
|
||||
return "sha256:feeds-snapshot-unavailable";
|
||||
}
|
||||
|
||||
private async Task<string> GetPolicyVersionAsync(string tenantId, CancellationToken ct)
|
||||
{
|
||||
if (_policyService is not null)
|
||||
{
|
||||
return await _policyService.GetCurrentVersionAsync(tenantId, ct);
|
||||
}
|
||||
|
||||
// Fallback: return default policy version
|
||||
_logger.LogDebug("PolicyVersionService not available, using default version");
|
||||
return "v1.0.0-default";
|
||||
}
|
||||
|
||||
private async Task<string> GetVexVerdictsDigestAsync(
|
||||
string sbomDigest,
|
||||
string tenantId,
|
||||
CancellationToken ct)
|
||||
{
|
||||
if (_vexService is not null)
|
||||
{
|
||||
return await _vexService.ComputeVerdictsDigestAsync(sbomDigest, tenantId, ct);
|
||||
}
|
||||
|
||||
// Fallback: return a placeholder
|
||||
_logger.LogDebug("VexVerdictsDigestService not available, using placeholder digest");
|
||||
return "sha256:vex-verdicts-unavailable";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Service for getting the current feeds snapshot digest.
|
||||
/// </summary>
|
||||
internal interface IFeedsSnapshotService
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the content digest of the current vulnerability feeds snapshot.
|
||||
/// </summary>
|
||||
Task<string> GetCurrentSnapshotDigestAsync(string tenantId, CancellationToken ct = default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Service for getting the current policy version.
|
||||
/// </summary>
|
||||
internal interface IPolicyVersionService
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the current policy bundle version for a tenant.
|
||||
/// </summary>
|
||||
Task<string> GetCurrentVersionAsync(string tenantId, CancellationToken ct = default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Service for computing VEX verdicts digest.
|
||||
/// </summary>
|
||||
internal interface IVexVerdictsDigestService
|
||||
{
|
||||
/// <summary>
|
||||
/// Computes a content digest of all VEX verdicts applicable to an SBOM.
|
||||
/// </summary>
|
||||
Task<string> ComputeVerdictsDigestAsync(
|
||||
string sbomDigest,
|
||||
string tenantId,
|
||||
CancellationToken ct = default);
|
||||
}
|
||||
Reference in New Issue
Block a user