release orchestrator v1 draft and build fixes
This commit is contained in:
@@ -0,0 +1,290 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// ChangeTraceBundleBuilder.cs
|
||||
// Sprint: SPRINT_20260112_200_005_ATTEST_predicate
|
||||
// Description: Builds change trace evidence bundles for export.
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using System.Globalization;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using StellaOps.Attestor.ProofChain.ChangeTrace;
|
||||
using StellaOps.Attestor.ProofChain.Signing;
|
||||
using StellaOps.Scanner.ChangeTrace.CycloneDx;
|
||||
|
||||
using ChangeTraceModel = StellaOps.Scanner.ChangeTrace.Models.ChangeTrace;
|
||||
|
||||
namespace StellaOps.ExportCenter.Core.ChangeTrace;
|
||||
|
||||
/// <summary>
|
||||
/// Builds change trace evidence bundles for export.
|
||||
/// </summary>
|
||||
public sealed class ChangeTraceBundleBuilder : IChangeTraceBundleBuilder
|
||||
{
|
||||
private const string ManifestVersion = "change-trace-bundle/v1";
|
||||
private const string RawTraceFileName = "change-trace.json";
|
||||
private const string AttestationFileName = "attestation.dsse.json";
|
||||
private const string CycloneDxFileName = "evidence.cdx.json";
|
||||
private const string ManifestFileName = "manifest.json";
|
||||
private const string ChecksumsFileName = "checksums.sha256";
|
||||
private const string VerifyScriptFileName = "verify.sh";
|
||||
|
||||
private static readonly JsonSerializerOptions SerializerOptions = new(JsonSerializerDefaults.Web)
|
||||
{
|
||||
WriteIndented = true,
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
|
||||
};
|
||||
|
||||
private readonly IChangeTraceAttestationService? _attestationService;
|
||||
private readonly IChangeTraceEvidenceExtension _evidenceExtension;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
|
||||
/// <summary>
|
||||
/// Create a new change trace bundle builder.
|
||||
/// </summary>
|
||||
/// <param name="attestationService">Optional attestation service for DSSE envelopes.</param>
|
||||
/// <param name="evidenceExtension">CycloneDX evidence extension service.</param>
|
||||
/// <param name="timeProvider">Time provider for timestamps.</param>
|
||||
public ChangeTraceBundleBuilder(
|
||||
IChangeTraceAttestationService? attestationService,
|
||||
IChangeTraceEvidenceExtension evidenceExtension,
|
||||
TimeProvider timeProvider)
|
||||
{
|
||||
_attestationService = attestationService;
|
||||
_evidenceExtension = evidenceExtension ?? throw new ArgumentNullException(nameof(evidenceExtension));
|
||||
_timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a new change trace bundle builder with default dependencies.
|
||||
/// </summary>
|
||||
public ChangeTraceBundleBuilder()
|
||||
: this(null, new ChangeTraceEvidenceExtension(), TimeProvider.System)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<ChangeTraceBundle> BuildAsync(
|
||||
ChangeTraceModel trace,
|
||||
ChangeTraceBundleOptions? options = null,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(trace);
|
||||
options ??= ChangeTraceBundleOptions.Default;
|
||||
|
||||
var bundleId = GenerateBundleId(trace);
|
||||
var createdAt = _timeProvider.GetUtcNow();
|
||||
var files = new Dictionary<string, byte[]>(StringComparer.Ordinal);
|
||||
var entries = new List<ChangeTraceBundleEntry>();
|
||||
|
||||
// 1. Raw trace JSON
|
||||
if (options.IncludeRawTrace)
|
||||
{
|
||||
var traceJson = JsonSerializer.Serialize(trace, SerializerOptions);
|
||||
var traceBytes = Encoding.UTF8.GetBytes(traceJson);
|
||||
files[RawTraceFileName] = traceBytes;
|
||||
entries.Add(CreateEntry("change-traces", RawTraceFileName, traceBytes, "application/json"));
|
||||
}
|
||||
|
||||
// 2. DSSE Attestation
|
||||
if (options.IncludeAttestation && _attestationService is not null)
|
||||
{
|
||||
var attestationOptions = new ChangeTraceAttestationOptions
|
||||
{
|
||||
TenantId = options.TenantId,
|
||||
MaxDeltas = options.MaxDeltas
|
||||
};
|
||||
|
||||
var envelope = await _attestationService.GenerateAttestationAsync(
|
||||
trace, attestationOptions, ct).ConfigureAwait(false);
|
||||
|
||||
var envelopeJson = JsonSerializer.Serialize(envelope, SerializerOptions);
|
||||
var envelopeBytes = Encoding.UTF8.GetBytes(envelopeJson);
|
||||
files[AttestationFileName] = envelopeBytes;
|
||||
entries.Add(CreateEntry("attestations", AttestationFileName, envelopeBytes, "application/vnd.in-toto+json"));
|
||||
}
|
||||
|
||||
// 3. CycloneDX Evidence
|
||||
if (options.IncludeCycloneDxEvidence)
|
||||
{
|
||||
var evidenceOptions = new ChangeTraceEvidenceOptions
|
||||
{
|
||||
MaxDeltas = options.MaxDeltas,
|
||||
IncludeProofSteps = true,
|
||||
IncludeSymbolDeltas = true
|
||||
};
|
||||
|
||||
using var cycloneDxDoc = _evidenceExtension.ExportAsStandalone(trace, evidenceOptions);
|
||||
var cdxJson = JsonSerializer.Serialize(cycloneDxDoc, SerializerOptions);
|
||||
var cdxBytes = Encoding.UTF8.GetBytes(cdxJson);
|
||||
files[CycloneDxFileName] = cdxBytes;
|
||||
entries.Add(CreateEntry("evidence", CycloneDxFileName, cdxBytes, "application/vnd.cyclonedx+json"));
|
||||
}
|
||||
|
||||
// 4. Verification script
|
||||
if (options.IncludeVerifyScript)
|
||||
{
|
||||
var verifyScript = BuildVerificationScript(bundleId);
|
||||
var scriptBytes = Encoding.UTF8.GetBytes(verifyScript);
|
||||
files[VerifyScriptFileName] = scriptBytes;
|
||||
entries.Add(CreateEntry("scripts", VerifyScriptFileName, scriptBytes, "application/x-sh"));
|
||||
}
|
||||
|
||||
// 5. Checksums file
|
||||
var checksums = BuildChecksums(entries);
|
||||
var checksumsBytes = Encoding.UTF8.GetBytes(checksums);
|
||||
files[ChecksumsFileName] = checksumsBytes;
|
||||
entries.Add(CreateEntry("metadata", ChecksumsFileName, checksumsBytes, "text/plain"));
|
||||
|
||||
// 6. Manifest
|
||||
var manifestHash = ComputeManifestHash(entries);
|
||||
var manifest = new ChangeTraceBundleManifest
|
||||
{
|
||||
Version = ManifestVersion,
|
||||
BundleId = bundleId,
|
||||
CreatedAt = createdAt,
|
||||
Entries = entries,
|
||||
ManifestHash = manifestHash,
|
||||
SubjectDigest = trace.Subject.Digest,
|
||||
TenantId = options.TenantId
|
||||
};
|
||||
|
||||
var manifestJson = JsonSerializer.Serialize(manifest, SerializerOptions);
|
||||
var manifestBytes = Encoding.UTF8.GetBytes(manifestJson);
|
||||
files[ManifestFileName] = manifestBytes;
|
||||
|
||||
return new ChangeTraceBundle
|
||||
{
|
||||
BundleId = bundleId,
|
||||
CreatedAt = createdAt,
|
||||
Manifest = manifest,
|
||||
Files = files
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generate a deterministic bundle ID from the trace.
|
||||
/// </summary>
|
||||
private static string GenerateBundleId(ChangeTraceModel trace)
|
||||
{
|
||||
var input = $"{trace.Subject.Digest}:{trace.Basis.ScanId}:{trace.Commitment?.Sha256 ?? "none"}";
|
||||
var hash = SHA256.HashData(Encoding.UTF8.GetBytes(input));
|
||||
return $"ctb-{Convert.ToHexStringLower(hash)[..16]}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a bundle entry from content.
|
||||
/// </summary>
|
||||
private static ChangeTraceBundleEntry CreateEntry(
|
||||
string category,
|
||||
string path,
|
||||
byte[] content,
|
||||
string contentType)
|
||||
{
|
||||
var hash = SHA256.HashData(content);
|
||||
return new ChangeTraceBundleEntry
|
||||
{
|
||||
Category = category,
|
||||
Path = path,
|
||||
Sha256 = Convert.ToHexStringLower(hash),
|
||||
SizeBytes = content.Length,
|
||||
ContentType = contentType
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Build checksums file content.
|
||||
/// </summary>
|
||||
private static string BuildChecksums(List<ChangeTraceBundleEntry> entries)
|
||||
{
|
||||
var builder = new StringBuilder();
|
||||
builder.AppendLine("# Change trace bundle checksums (SHA-256)");
|
||||
builder.AppendLine("# Generated by StellaOps ChangeTraceBundleBuilder");
|
||||
builder.AppendLine();
|
||||
|
||||
foreach (var entry in entries.OrderBy(e => e.Path, StringComparer.Ordinal))
|
||||
{
|
||||
builder.Append(entry.Sha256);
|
||||
builder.Append(" ");
|
||||
builder.AppendLine(entry.Path);
|
||||
}
|
||||
|
||||
return builder.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compute manifest hash from all entries.
|
||||
/// </summary>
|
||||
private static string ComputeManifestHash(List<ChangeTraceBundleEntry> entries)
|
||||
{
|
||||
var builder = new StringBuilder();
|
||||
foreach (var entry in entries.OrderBy(e => e.Path, StringComparer.Ordinal))
|
||||
{
|
||||
builder.Append(entry.Sha256);
|
||||
builder.Append(':');
|
||||
builder.Append(entry.Path);
|
||||
builder.Append('\n');
|
||||
}
|
||||
|
||||
var hash = SHA256.HashData(Encoding.UTF8.GetBytes(builder.ToString()));
|
||||
return Convert.ToHexStringLower(hash);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Build verification script for the bundle.
|
||||
/// </summary>
|
||||
private static string BuildVerificationScript(string bundleId)
|
||||
{
|
||||
var builder = new StringBuilder();
|
||||
builder.AppendLine("#!/usr/bin/env sh");
|
||||
builder.AppendLine("# Change Trace Bundle Verification Script");
|
||||
builder.AppendLine("# No network access required");
|
||||
builder.AppendLine();
|
||||
builder.AppendLine("set -eu");
|
||||
builder.AppendLine();
|
||||
builder.Append("BUNDLE_ID=\"").Append(bundleId).AppendLine("\"");
|
||||
builder.AppendLine();
|
||||
builder.AppendLine("echo \"Verifying change trace bundle: $BUNDLE_ID\"");
|
||||
builder.AppendLine("echo \"\"");
|
||||
builder.AppendLine();
|
||||
builder.AppendLine("# Step 1: Verify checksums");
|
||||
builder.AppendLine("echo \"Verifying file checksums...\"");
|
||||
builder.AppendLine("if command -v sha256sum >/dev/null 2>&1; then");
|
||||
builder.AppendLine(" sha256sum --check checksums.sha256 --ignore-missing");
|
||||
builder.AppendLine("elif command -v shasum >/dev/null 2>&1; then");
|
||||
builder.AppendLine(" shasum -a 256 --check checksums.sha256 --ignore-missing");
|
||||
builder.AppendLine("else");
|
||||
builder.AppendLine(" echo \"Error: sha256sum or shasum required\" >&2");
|
||||
builder.AppendLine(" exit 1");
|
||||
builder.AppendLine("fi");
|
||||
builder.AppendLine("echo \"Checksums verified.\"");
|
||||
builder.AppendLine("echo \"\"");
|
||||
builder.AppendLine();
|
||||
builder.AppendLine("# Step 2: Verify DSSE attestation (if present)");
|
||||
builder.AppendLine("if [ -f \"attestation.dsse.json\" ]; then");
|
||||
builder.AppendLine(" if command -v stella >/dev/null 2>&1; then");
|
||||
builder.AppendLine(" echo \"Verifying DSSE attestation with stella CLI...\"");
|
||||
builder.AppendLine(" stella attest verify --envelope attestation.dsse.json");
|
||||
builder.AppendLine(" else");
|
||||
builder.AppendLine(" echo \"Note: stella CLI not found. Manual DSSE verification recommended.\"");
|
||||
builder.AppendLine(" fi");
|
||||
builder.AppendLine("fi");
|
||||
builder.AppendLine("echo \"\"");
|
||||
builder.AppendLine();
|
||||
builder.AppendLine("# Step 3: Display summary");
|
||||
builder.AppendLine("echo \"Bundle verification complete.\"");
|
||||
builder.AppendLine("if [ -f \"change-trace.json\" ]; then");
|
||||
builder.AppendLine(" echo \"\"");
|
||||
builder.AppendLine(" echo \"Change trace summary:\"");
|
||||
builder.AppendLine(" if command -v jq >/dev/null 2>&1; then");
|
||||
builder.AppendLine(" jq '.summary' change-trace.json");
|
||||
builder.AppendLine(" else");
|
||||
builder.AppendLine(" cat change-trace.json | grep -A5 '\"summary\"'");
|
||||
builder.AppendLine(" fi");
|
||||
builder.AppendLine("fi");
|
||||
|
||||
return builder.ToString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// IChangeTraceBundleBuilder.cs
|
||||
// Sprint: SPRINT_20260112_200_005_ATTEST_predicate
|
||||
// Description: Interface for building change trace evidence bundles.
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using StellaOps.Scanner.ChangeTrace.Models;
|
||||
|
||||
namespace StellaOps.ExportCenter.Core.ChangeTrace;
|
||||
|
||||
/// <summary>
|
||||
/// Builds change trace evidence bundles for export.
|
||||
/// </summary>
|
||||
public interface IChangeTraceBundleBuilder
|
||||
{
|
||||
/// <summary>
|
||||
/// Build a complete change trace evidence bundle.
|
||||
/// </summary>
|
||||
/// <param name="trace">The change trace to bundle.</param>
|
||||
/// <param name="options">Optional bundle options.</param>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
/// <returns>The built change trace bundle.</returns>
|
||||
Task<ChangeTraceBundle> BuildAsync(
|
||||
Scanner.ChangeTrace.Models.ChangeTrace trace,
|
||||
ChangeTraceBundleOptions? options = null,
|
||||
CancellationToken ct = default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Options for change trace bundle building.
|
||||
/// </summary>
|
||||
public sealed record ChangeTraceBundleOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Default bundle options.
|
||||
/// </summary>
|
||||
public static readonly ChangeTraceBundleOptions Default = new();
|
||||
|
||||
/// <summary>
|
||||
/// Include DSSE attestation envelope.
|
||||
/// </summary>
|
||||
public bool IncludeAttestation { get; init; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Include CycloneDX evidence file (standalone mode).
|
||||
/// </summary>
|
||||
public bool IncludeCycloneDxEvidence { get; init; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Include raw trace JSON.
|
||||
/// </summary>
|
||||
public bool IncludeRawTrace { get; init; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Include verification script.
|
||||
/// </summary>
|
||||
public bool IncludeVerifyScript { get; init; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Tenant ID for multi-tenant isolation.
|
||||
/// </summary>
|
||||
public string TenantId { get; init; } = "default";
|
||||
|
||||
/// <summary>
|
||||
/// Maximum deltas to include in exports.
|
||||
/// </summary>
|
||||
public int MaxDeltas { get; init; } = 1000;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A bundled change trace export package.
|
||||
/// </summary>
|
||||
public sealed record ChangeTraceBundle
|
||||
{
|
||||
/// <summary>
|
||||
/// Unique identifier for this bundle.
|
||||
/// </summary>
|
||||
public required string BundleId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// When the bundle was created.
|
||||
/// </summary>
|
||||
public required DateTimeOffset CreatedAt { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Bundle manifest with file entries and checksums.
|
||||
/// </summary>
|
||||
public required ChangeTraceBundleManifest Manifest { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Bundle files keyed by path.
|
||||
/// </summary>
|
||||
public required IReadOnlyDictionary<string, byte[]> Files { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Manifest describing the contents of a change trace bundle.
|
||||
/// </summary>
|
||||
public sealed record ChangeTraceBundleManifest
|
||||
{
|
||||
/// <summary>
|
||||
/// Manifest format version.
|
||||
/// </summary>
|
||||
public required string Version { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Bundle identifier.
|
||||
/// </summary>
|
||||
public required string BundleId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// When the bundle was created.
|
||||
/// </summary>
|
||||
public required DateTimeOffset CreatedAt { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Entries in this bundle.
|
||||
/// </summary>
|
||||
public required IReadOnlyList<ChangeTraceBundleEntry> Entries { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Root hash of all entries for integrity verification.
|
||||
/// </summary>
|
||||
public required string ManifestHash { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Subject artifact digest.
|
||||
/// </summary>
|
||||
public string? SubjectDigest { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Tenant ID.
|
||||
/// </summary>
|
||||
public string? TenantId { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A single entry in a change trace bundle.
|
||||
/// </summary>
|
||||
public sealed record ChangeTraceBundleEntry
|
||||
{
|
||||
/// <summary>
|
||||
/// Category of the entry (change-traces, attestations, evidence).
|
||||
/// </summary>
|
||||
public required string Category { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Path within the bundle.
|
||||
/// </summary>
|
||||
public required string Path { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// SHA-256 hash of the entry content.
|
||||
/// </summary>
|
||||
public required string Sha256 { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Size in bytes.
|
||||
/// </summary>
|
||||
public required long SizeBytes { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Content type (MIME type).
|
||||
/// </summary>
|
||||
public string? ContentType { get; init; }
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.Determinism;
|
||||
using StellaOps.ExportCenter.Core.Domain;
|
||||
|
||||
namespace StellaOps.ExportCenter.Core.Distribution;
|
||||
@@ -10,16 +11,19 @@ public sealed class DistributionLifecycleService : IDistributionLifecycleService
|
||||
{
|
||||
private readonly IDistributionRepository _repository;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
private readonly IGuidProvider _guidProvider;
|
||||
private readonly ILogger<DistributionLifecycleService> _logger;
|
||||
|
||||
public DistributionLifecycleService(
|
||||
IDistributionRepository repository,
|
||||
TimeProvider timeProvider,
|
||||
ILogger<DistributionLifecycleService> logger)
|
||||
ILogger<DistributionLifecycleService> logger,
|
||||
IGuidProvider? guidProvider = null)
|
||||
{
|
||||
_repository = repository ?? throw new ArgumentNullException(nameof(repository));
|
||||
_timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
_guidProvider = guidProvider ?? SystemGuidProvider.Instance;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -58,7 +62,7 @@ public sealed class DistributionLifecycleService : IDistributionLifecycleService
|
||||
|
||||
var distribution = new ExportDistribution
|
||||
{
|
||||
DistributionId = Guid.NewGuid(),
|
||||
DistributionId = _guidProvider.NewGuid(),
|
||||
RunId = request.RunId,
|
||||
TenantId = request.TenantId,
|
||||
Kind = request.Kind,
|
||||
|
||||
@@ -3,6 +3,7 @@ using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using Cronos;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.Determinism;
|
||||
using StellaOps.ExportCenter.Core.Domain;
|
||||
|
||||
namespace StellaOps.ExportCenter.Core.Scheduling;
|
||||
@@ -14,6 +15,7 @@ public sealed class ExportSchedulerService : IExportSchedulerService
|
||||
{
|
||||
private readonly IExportScheduleStore _scheduleStore;
|
||||
private readonly ILogger<ExportSchedulerService> _logger;
|
||||
private readonly IGuidProvider _guidProvider;
|
||||
private readonly ConcurrentDictionary<string, CronExpression> _cronCache = new();
|
||||
|
||||
// Pause profiles after this many consecutive failures
|
||||
@@ -21,10 +23,12 @@ public sealed class ExportSchedulerService : IExportSchedulerService
|
||||
|
||||
public ExportSchedulerService(
|
||||
IExportScheduleStore scheduleStore,
|
||||
ILogger<ExportSchedulerService> logger)
|
||||
ILogger<ExportSchedulerService> logger,
|
||||
IGuidProvider? guidProvider = null)
|
||||
{
|
||||
_scheduleStore = scheduleStore ?? throw new ArgumentNullException(nameof(scheduleStore));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
_guidProvider = guidProvider ?? SystemGuidProvider.Instance;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -89,7 +93,7 @@ public sealed class ExportSchedulerService : IExportSchedulerService
|
||||
}
|
||||
|
||||
// Create new run
|
||||
var runId = Guid.NewGuid();
|
||||
var runId = _guidProvider.NewGuid();
|
||||
await _scheduleStore.RecordTriggerAsync(
|
||||
request.ProfileId,
|
||||
runId,
|
||||
|
||||
@@ -34,10 +34,14 @@ public sealed class EvidencePackSigningService : IEvidencePackSigningService
|
||||
};
|
||||
|
||||
private readonly ILogger<EvidencePackSigningService> _logger;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
|
||||
public EvidencePackSigningService(ILogger<EvidencePackSigningService> logger)
|
||||
public EvidencePackSigningService(
|
||||
ILogger<EvidencePackSigningService> logger,
|
||||
TimeProvider? timeProvider = null)
|
||||
{
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -92,7 +96,7 @@ public sealed class EvidencePackSigningService : IEvidencePackSigningService
|
||||
var envelopeBase64 = Convert.ToBase64String(Encoding.UTF8.GetBytes(envelopeJson));
|
||||
var envelopeDigest = ComputeDigest(envelopeJson);
|
||||
|
||||
var signedAt = DateTimeOffset.UtcNow;
|
||||
var signedAt = _timeProvider.GetUtcNow();
|
||||
|
||||
// Build signature record
|
||||
var signature = new EvidencePackSignature
|
||||
@@ -318,11 +322,11 @@ public sealed class EvidencePackSigningService : IEvidencePackSigningService
|
||||
};
|
||||
}
|
||||
|
||||
private static Task<long?> UploadToRekorAsync(DsseEnvelope envelope, CancellationToken ct)
|
||||
private Task<long?> UploadToRekorAsync(DsseEnvelope envelope, CancellationToken ct)
|
||||
{
|
||||
// In real implementation, would upload to Rekor transparency log
|
||||
// For now, return placeholder index
|
||||
return Task.FromResult<long?>(DateTimeOffset.UtcNow.ToUnixTimeMilliseconds());
|
||||
return Task.FromResult<long?>(_timeProvider.GetUtcNow().ToUnixTimeMilliseconds());
|
||||
}
|
||||
|
||||
private static string ComputeMerkleRoot(ImmutableArray<ManifestEntry> entries)
|
||||
|
||||
@@ -21,5 +21,8 @@
|
||||
<ProjectReference Include="..\..\..\__Libraries\StellaOps.Cryptography\StellaOps.Cryptography.csproj" />
|
||||
<ProjectReference Include="..\..\..\Policy\__Libraries\StellaOps.Policy\StellaOps.Policy.csproj" />
|
||||
<ProjectReference Include="..\..\..\__Libraries\StellaOps.Provcache\StellaOps.Provcache.csproj" />
|
||||
<ProjectReference Include="..\..\..\__Libraries\StellaOps.Determinism.Abstractions\StellaOps.Determinism.Abstractions.csproj" />
|
||||
<ProjectReference Include="..\..\..\Scanner\__Libraries\StellaOps.Scanner.ChangeTrace\StellaOps.Scanner.ChangeTrace.csproj" />
|
||||
<ProjectReference Include="..\..\..\Attestor\__Libraries\StellaOps.Attestor.ProofChain\StellaOps.Attestor.ProofChain.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -6,6 +6,7 @@ using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Auth.Abstractions;
|
||||
using StellaOps.Auth.ServerIntegration;
|
||||
using StellaOps.Determinism;
|
||||
using StellaOps.ExportCenter.Core.Domain;
|
||||
using StellaOps.ExportCenter.Core.Planner;
|
||||
using StellaOps.ExportCenter.WebService.Telemetry;
|
||||
@@ -202,6 +203,8 @@ public static class ExportApiEndpoints
|
||||
ClaimsPrincipal user,
|
||||
IExportProfileRepository profileRepo,
|
||||
IExportAuditService auditService,
|
||||
TimeProvider timeProvider,
|
||||
IGuidProvider guidProvider,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var tenantId = GetTenantId(user);
|
||||
@@ -212,10 +215,10 @@ public static class ExportApiEndpoints
|
||||
if (!await profileRepo.IsNameUniqueAsync(tenantId, request.Name, cancellationToken: cancellationToken))
|
||||
return TypedResults.Conflict($"Profile name '{request.Name}' already exists");
|
||||
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
var now = timeProvider.GetUtcNow();
|
||||
var profile = new ExportProfile
|
||||
{
|
||||
ProfileId = Guid.NewGuid(),
|
||||
ProfileId = guidProvider.NewGuid(),
|
||||
TenantId = tenantId,
|
||||
Name = request.Name,
|
||||
Description = request.Description,
|
||||
@@ -255,6 +258,7 @@ public static class ExportApiEndpoints
|
||||
ClaimsPrincipal user,
|
||||
IExportProfileRepository profileRepo,
|
||||
IExportAuditService auditService,
|
||||
TimeProvider timeProvider,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var tenantId = GetTenantId(user);
|
||||
@@ -291,7 +295,7 @@ public static class ExportApiEndpoints
|
||||
? JsonSerializer.Serialize(request.Signing)
|
||||
: existing.SigningJson,
|
||||
Schedule = request.Schedule ?? existing.Schedule,
|
||||
UpdatedAt = DateTimeOffset.UtcNow
|
||||
UpdatedAt = timeProvider.GetUtcNow()
|
||||
};
|
||||
|
||||
await profileRepo.UpdateAsync(updated, cancellationToken);
|
||||
@@ -347,6 +351,8 @@ public static class ExportApiEndpoints
|
||||
IExportRunRepository runRepo,
|
||||
IExportAuditService auditService,
|
||||
IOptions<ExportConcurrencyOptions> concurrencyOptions,
|
||||
TimeProvider timeProvider,
|
||||
IGuidProvider guidProvider,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var tenantId = GetTenantId(user);
|
||||
@@ -396,18 +402,18 @@ public static class ExportApiEndpoints
|
||||
return TypedResults.StatusCode(429);
|
||||
}
|
||||
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
var now = timeProvider.GetUtcNow();
|
||||
var shouldQueue = activeRunsCount >= options.MaxConcurrentRunsPerTenant ||
|
||||
profileActiveRuns >= options.MaxConcurrentRunsPerProfile;
|
||||
|
||||
var run = new ExportRun
|
||||
{
|
||||
RunId = Guid.NewGuid(),
|
||||
RunId = guidProvider.NewGuid(),
|
||||
ProfileId = profileId,
|
||||
TenantId = tenantId,
|
||||
Status = shouldQueue ? ExportRunStatus.Queued : ExportRunStatus.Running,
|
||||
Trigger = ExportRunTrigger.Api,
|
||||
CorrelationId = request.CorrelationId ?? Guid.NewGuid().ToString(),
|
||||
CorrelationId = request.CorrelationId ?? guidProvider.NewGuid().ToString(),
|
||||
InitiatedBy = GetUserId(user),
|
||||
TotalItems = 0,
|
||||
ProcessedItems = 0,
|
||||
@@ -632,6 +638,7 @@ public static class ExportApiEndpoints
|
||||
ClaimsPrincipal user,
|
||||
HttpContext httpContext,
|
||||
IExportRunRepository runRepo,
|
||||
TimeProvider timeProvider,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var tenantId = GetTenantId(user);
|
||||
@@ -661,7 +668,7 @@ public static class ExportApiEndpoints
|
||||
{
|
||||
EventType = "connected",
|
||||
RunId = runId,
|
||||
Timestamp = DateTimeOffset.UtcNow,
|
||||
Timestamp = timeProvider.GetUtcNow(),
|
||||
Data = MapToRunResponse(run, null)
|
||||
}, cancellationToken);
|
||||
|
||||
@@ -685,7 +692,7 @@ public static class ExportApiEndpoints
|
||||
{
|
||||
EventType = ExportRunSseEventTypes.RunProgress,
|
||||
RunId = runId,
|
||||
Timestamp = DateTimeOffset.UtcNow,
|
||||
Timestamp = timeProvider.GetUtcNow(),
|
||||
Data = new ExportRunProgress
|
||||
{
|
||||
TotalItems = run.TotalItems,
|
||||
@@ -714,7 +721,7 @@ public static class ExportApiEndpoints
|
||||
{
|
||||
EventType = eventType,
|
||||
RunId = runId,
|
||||
Timestamp = DateTimeOffset.UtcNow,
|
||||
Timestamp = timeProvider.GetUtcNow(),
|
||||
Data = MapToRunResponse(run, null)
|
||||
}, cancellationToken);
|
||||
|
||||
@@ -729,7 +736,7 @@ public static class ExportApiEndpoints
|
||||
{
|
||||
EventType = "disconnected",
|
||||
RunId = runId,
|
||||
Timestamp = DateTimeOffset.UtcNow,
|
||||
Timestamp = timeProvider.GetUtcNow(),
|
||||
Data = MapToRunResponse(run, null)
|
||||
}, cancellationToken);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user