save progress
This commit is contained in:
@@ -28,6 +28,7 @@ public sealed class AttestationBundler : IAttestationBundler
|
||||
private readonly IMerkleTreeBuilder _merkleBuilder;
|
||||
private readonly ILogger<AttestationBundler> _logger;
|
||||
private readonly BundlingOptions _options;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
|
||||
/// <summary>
|
||||
/// Create a new attestation bundler.
|
||||
@@ -38,7 +39,8 @@ public sealed class AttestationBundler : IAttestationBundler
|
||||
IMerkleTreeBuilder merkleBuilder,
|
||||
ILogger<AttestationBundler> logger,
|
||||
IOptions<BundlingOptions> options,
|
||||
IOrgKeySigner? orgSigner = null)
|
||||
IOrgKeySigner? orgSigner = null,
|
||||
TimeProvider? timeProvider = null)
|
||||
{
|
||||
_aggregator = aggregator ?? throw new ArgumentNullException(nameof(aggregator));
|
||||
_store = store ?? throw new ArgumentNullException(nameof(store));
|
||||
@@ -46,6 +48,7 @@ public sealed class AttestationBundler : IAttestationBundler
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
_options = options?.Value ?? new BundlingOptions();
|
||||
_orgSigner = orgSigner;
|
||||
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -60,13 +63,42 @@ public sealed class AttestationBundler : IAttestationBundler
|
||||
request.PeriodStart,
|
||||
request.PeriodEnd);
|
||||
|
||||
// Collect attestations in deterministic order
|
||||
var attestations = await CollectAttestationsAsync(request, cancellationToken);
|
||||
|
||||
if (attestations.Count == 0)
|
||||
if (request.PeriodStart > request.PeriodEnd)
|
||||
{
|
||||
_logger.LogWarning("No attestations found for the specified period");
|
||||
throw new InvalidOperationException("No attestations found for the specified period.");
|
||||
throw new ArgumentException(
|
||||
"PeriodStart must be less than or equal to PeriodEnd.",
|
||||
nameof(request));
|
||||
}
|
||||
|
||||
var effectivePeriodStart = request.PeriodStart;
|
||||
var lookbackDays = _options.Aggregation.LookbackDays;
|
||||
if (lookbackDays > 0)
|
||||
{
|
||||
var lookbackStart = request.PeriodEnd.AddDays(-lookbackDays);
|
||||
if (effectivePeriodStart < lookbackStart)
|
||||
{
|
||||
_logger.LogDebug(
|
||||
"Clamping period start from {RequestedStart} to {EffectiveStart} to honor lookback window.",
|
||||
request.PeriodStart,
|
||||
lookbackStart);
|
||||
effectivePeriodStart = lookbackStart;
|
||||
}
|
||||
}
|
||||
|
||||
// Collect attestations in deterministic order
|
||||
var attestations = await CollectAttestationsAsync(
|
||||
request with { PeriodStart = effectivePeriodStart },
|
||||
cancellationToken);
|
||||
|
||||
var minimumAttestations = Math.Max(1, _options.Aggregation.MinAttestationsForBundle);
|
||||
if (attestations.Count < minimumAttestations)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"Insufficient attestations for bundling. Required {Required}, found {Found}.",
|
||||
minimumAttestations,
|
||||
attestations.Count);
|
||||
throw new InvalidOperationException(
|
||||
$"Insufficient attestations for bundling. Required {minimumAttestations}, found {attestations.Count}.");
|
||||
}
|
||||
|
||||
_logger.LogInformation("Collected {Count} attestations for bundling", attestations.Count);
|
||||
@@ -83,8 +115,8 @@ public sealed class AttestationBundler : IAttestationBundler
|
||||
{
|
||||
BundleId = bundleId,
|
||||
Version = "1.0",
|
||||
CreatedAt = DateTimeOffset.UtcNow,
|
||||
PeriodStart = request.PeriodStart,
|
||||
CreatedAt = _timeProvider.GetUtcNow(),
|
||||
PeriodStart = effectivePeriodStart,
|
||||
PeriodEnd = request.PeriodEnd,
|
||||
AttestationCount = attestations.Count,
|
||||
TenantId = request.TenantId
|
||||
@@ -104,6 +136,11 @@ public sealed class AttestationBundler : IAttestationBundler
|
||||
};
|
||||
|
||||
// Sign with organization key if requested
|
||||
if (request.SignWithOrgKey && _orgSigner == null)
|
||||
{
|
||||
throw new InvalidOperationException("Organization signer is not configured.");
|
||||
}
|
||||
|
||||
if (request.SignWithOrgKey && _orgSigner != null)
|
||||
{
|
||||
bundle = await SignBundleAsync(bundle, request.OrgKeyId, cancellationToken);
|
||||
@@ -146,14 +183,22 @@ public sealed class AttestationBundler : IAttestationBundler
|
||||
ArgumentNullException.ThrowIfNull(bundle);
|
||||
|
||||
var issues = new List<BundleVerificationIssue>();
|
||||
var verifiedAt = DateTimeOffset.UtcNow;
|
||||
var verifiedAt = _timeProvider.GetUtcNow();
|
||||
|
||||
// Verify Merkle root
|
||||
var merkleValid = VerifyMerkleRoot(bundle, issues);
|
||||
|
||||
// Verify org signature if present
|
||||
bool? orgSigValid = null;
|
||||
if (bundle.OrgSignature != null && _orgSigner != null)
|
||||
if (bundle.OrgSignature != null && _orgSigner == null)
|
||||
{
|
||||
issues.Add(new BundleVerificationIssue(
|
||||
VerificationIssueSeverity.Critical,
|
||||
"ORG_SIG_VERIFIER_UNAVAILABLE",
|
||||
"Organization signature present but no signer is configured for verification."));
|
||||
orgSigValid = false;
|
||||
}
|
||||
else if (bundle.OrgSignature != null && _orgSigner != null)
|
||||
{
|
||||
orgSigValid = await VerifyOrgSignatureAsync(bundle, issues, cancellationToken);
|
||||
}
|
||||
@@ -236,11 +281,19 @@ public sealed class AttestationBundler : IAttestationBundler
|
||||
keyId);
|
||||
|
||||
// Return bundle with signature and updated metadata
|
||||
var fingerprint = await GetKeyFingerprintAsync(keyId, cancellationToken);
|
||||
if (fingerprint == null)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"Organization key fingerprint not found for key {KeyId}; leaving fingerprint unset.",
|
||||
keyId);
|
||||
}
|
||||
|
||||
return bundle with
|
||||
{
|
||||
Metadata = bundle.Metadata with
|
||||
{
|
||||
OrgKeyFingerprint = $"sha256:{ComputeKeyFingerprint(keyId)}"
|
||||
OrgKeyFingerprint = fingerprint
|
||||
},
|
||||
OrgSignature = signature
|
||||
};
|
||||
@@ -328,10 +381,17 @@ public sealed class AttestationBundler : IAttestationBundler
|
||||
return SHA256.HashData(Encoding.UTF8.GetBytes(sb.ToString()));
|
||||
}
|
||||
|
||||
private static string ComputeKeyFingerprint(string keyId)
|
||||
private async Task<string?> GetKeyFingerprintAsync(
|
||||
string keyId,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// Simple fingerprint - in production this would use the actual public key
|
||||
var hash = SHA256.HashData(Encoding.UTF8.GetBytes(keyId));
|
||||
return Convert.ToHexString(hash[..16]).ToLowerInvariant();
|
||||
if (_orgSigner == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var keys = await _orgSigner.ListKeysAsync(cancellationToken) ?? Array.Empty<OrgKeyInfo>();
|
||||
var match = keys.FirstOrDefault(key => string.Equals(key.KeyId, keyId, StringComparison.Ordinal));
|
||||
return match?.Fingerprint;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -120,15 +120,18 @@ public sealed class OfflineKitBundleProvider : IOfflineKitBundleProvider
|
||||
private readonly IBundleStore _bundleStore;
|
||||
private readonly BundlingOptions _options;
|
||||
private readonly ILogger<OfflineKitBundleProvider> _logger;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
|
||||
public OfflineKitBundleProvider(
|
||||
IBundleStore bundleStore,
|
||||
IOptions<BundlingOptions> options,
|
||||
ILogger<OfflineKitBundleProvider> logger)
|
||||
ILogger<OfflineKitBundleProvider> logger,
|
||||
TimeProvider? timeProvider = null)
|
||||
{
|
||||
_bundleStore = bundleStore ?? throw new ArgumentNullException(nameof(bundleStore));
|
||||
_options = options?.Value ?? new BundlingOptions();
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
@@ -137,7 +140,7 @@ public sealed class OfflineKitBundleProvider : IOfflineKitBundleProvider
|
||||
OfflineKitExportOptions? options = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
options ??= new OfflineKitExportOptions();
|
||||
options = ResolveExportOptions(options);
|
||||
|
||||
if (!_options.Export.IncludeInOfflineKit)
|
||||
{
|
||||
@@ -147,7 +150,7 @@ public sealed class OfflineKitBundleProvider : IOfflineKitBundleProvider
|
||||
Bundles = [],
|
||||
TotalAttestations = 0,
|
||||
TotalSizeBytes = 0,
|
||||
ExportedAt = DateTimeOffset.UtcNow
|
||||
ExportedAt = _timeProvider.GetUtcNow()
|
||||
};
|
||||
}
|
||||
|
||||
@@ -203,7 +206,7 @@ public sealed class OfflineKitBundleProvider : IOfflineKitBundleProvider
|
||||
Bundles = exportedBundles,
|
||||
TotalAttestations = totalAttestations,
|
||||
TotalSizeBytes = totalSize,
|
||||
ExportedAt = DateTimeOffset.UtcNow
|
||||
ExportedAt = _timeProvider.GetUtcNow()
|
||||
};
|
||||
}
|
||||
|
||||
@@ -212,9 +215,9 @@ public sealed class OfflineKitBundleProvider : IOfflineKitBundleProvider
|
||||
OfflineKitExportOptions? options = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
options ??= new OfflineKitExportOptions();
|
||||
options = ResolveExportOptions(options);
|
||||
|
||||
var cutoffDate = DateTimeOffset.UtcNow.AddMonths(-options.MaxAgeMonths);
|
||||
var cutoffDate = _timeProvider.GetUtcNow().AddMonths(-options.MaxAgeMonths);
|
||||
var result = new List<BundleListItem>();
|
||||
string? cursor = null;
|
||||
|
||||
@@ -303,4 +306,58 @@ public sealed class OfflineKitBundleProvider : IOfflineKitBundleProvider
|
||||
|
||||
return $"bundle-{hash}{extension}{compression}";
|
||||
}
|
||||
|
||||
private OfflineKitExportOptions ResolveExportOptions(OfflineKitExportOptions? options)
|
||||
{
|
||||
if (options != null)
|
||||
{
|
||||
return options;
|
||||
}
|
||||
|
||||
return new OfflineKitExportOptions
|
||||
{
|
||||
MaxAgeMonths = _options.Export.MaxAgeMonths,
|
||||
Format = ParseFormat(_options.Export.SupportedFormats ?? new List<string>()),
|
||||
Compression = ParseCompression(_options.Export.Compression),
|
||||
RequireOrgSignature = false,
|
||||
TenantId = null
|
||||
};
|
||||
}
|
||||
|
||||
private static BundleFormat ParseFormat(IList<string> supportedFormats)
|
||||
{
|
||||
if (supportedFormats.Count == 0)
|
||||
{
|
||||
return BundleFormat.Json;
|
||||
}
|
||||
|
||||
var format = supportedFormats
|
||||
.FirstOrDefault(value => value.Equals("json", StringComparison.OrdinalIgnoreCase))
|
||||
?? supportedFormats.FirstOrDefault()
|
||||
?? "json";
|
||||
|
||||
return format.Equals("cbor", StringComparison.OrdinalIgnoreCase)
|
||||
? BundleFormat.Cbor
|
||||
: BundleFormat.Json;
|
||||
}
|
||||
|
||||
private static BundleCompression ParseCompression(string? compression)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(compression))
|
||||
{
|
||||
return BundleCompression.None;
|
||||
}
|
||||
|
||||
if (compression.Equals("gzip", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return BundleCompression.Gzip;
|
||||
}
|
||||
|
||||
if (compression.Equals("zstd", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return BundleCompression.Zstd;
|
||||
}
|
||||
|
||||
return BundleCompression.None;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -164,25 +164,28 @@ public sealed class RetentionPolicyEnforcer : IRetentionPolicyEnforcer
|
||||
private readonly IBundleExpiryNotifier? _notifier;
|
||||
private readonly BundleRetentionOptions _options;
|
||||
private readonly ILogger<RetentionPolicyEnforcer> _logger;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
|
||||
public RetentionPolicyEnforcer(
|
||||
IBundleStore bundleStore,
|
||||
IOptions<BundlingOptions> options,
|
||||
ILogger<RetentionPolicyEnforcer> logger,
|
||||
IBundleArchiver? archiver = null,
|
||||
IBundleExpiryNotifier? notifier = null)
|
||||
IBundleExpiryNotifier? notifier = null,
|
||||
TimeProvider? timeProvider = null)
|
||||
{
|
||||
_bundleStore = bundleStore ?? throw new ArgumentNullException(nameof(bundleStore));
|
||||
_options = options?.Value?.Retention ?? new BundleRetentionOptions();
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
_archiver = archiver;
|
||||
_notifier = notifier;
|
||||
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<RetentionEnforcementResult> EnforceAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
var startedAt = DateTimeOffset.UtcNow;
|
||||
var startedAt = _timeProvider.GetUtcNow();
|
||||
var failures = new List<BundleEnforcementFailure>();
|
||||
int evaluated = 0;
|
||||
int deleted = 0;
|
||||
@@ -196,7 +199,7 @@ public sealed class RetentionPolicyEnforcer : IRetentionPolicyEnforcer
|
||||
return new RetentionEnforcementResult
|
||||
{
|
||||
StartedAt = startedAt,
|
||||
CompletedAt = DateTimeOffset.UtcNow,
|
||||
CompletedAt = _timeProvider.GetUtcNow(),
|
||||
BundlesEvaluated = 0,
|
||||
BundlesDeleted = 0,
|
||||
BundlesArchived = 0,
|
||||
@@ -213,10 +216,11 @@ public sealed class RetentionPolicyEnforcer : IRetentionPolicyEnforcer
|
||||
|
||||
// Process bundles in batches
|
||||
string? cursor = null;
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
var now = _timeProvider.GetUtcNow();
|
||||
var notificationCutoff = now.AddDays(_options.NotifyDaysBeforeExpiry);
|
||||
var gracePeriodCutoff = now.AddDays(-_options.GracePeriodDays);
|
||||
var expiredNotifications = new List<BundleExpiryNotification>();
|
||||
var applyOverrides = _options.TenantOverrides.Count > 0 || _options.PredicateTypeOverrides.Count > 0;
|
||||
|
||||
do
|
||||
{
|
||||
@@ -227,7 +231,29 @@ public sealed class RetentionPolicyEnforcer : IRetentionPolicyEnforcer
|
||||
foreach (var bundle in listResult.Bundles)
|
||||
{
|
||||
evaluated++;
|
||||
var expiryDate = CalculateExpiryDate(bundle);
|
||||
string? tenantId = null;
|
||||
IReadOnlyList<string>? predicateTypes = null;
|
||||
|
||||
if (applyOverrides)
|
||||
{
|
||||
var fullBundle = await _bundleStore.GetBundleAsync(bundle.BundleId, cancellationToken);
|
||||
if (fullBundle == null)
|
||||
{
|
||||
failures.Add(new BundleEnforcementFailure(
|
||||
bundle.BundleId,
|
||||
"Bundle not found",
|
||||
"Failed to load bundle metadata for retention overrides."));
|
||||
continue;
|
||||
}
|
||||
|
||||
tenantId = fullBundle.Metadata.TenantId;
|
||||
predicateTypes = fullBundle.Attestations
|
||||
.Select(attestation => attestation.PredicateType)
|
||||
.Distinct(StringComparer.Ordinal)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
var expiryDate = CalculateExpiryDate(tenantId, predicateTypes, bundle.CreatedAt);
|
||||
|
||||
// Check if bundle has expired
|
||||
if (expiryDate <= now)
|
||||
@@ -300,7 +326,7 @@ public sealed class RetentionPolicyEnforcer : IRetentionPolicyEnforcer
|
||||
}
|
||||
}
|
||||
|
||||
var completedAt = DateTimeOffset.UtcNow;
|
||||
var completedAt = _timeProvider.GetUtcNow();
|
||||
_logger.LogInformation(
|
||||
"Retention enforcement completed. Evaluated={Evaluated}, Deleted={Deleted}, Archived={Archived}, Marked={Marked}, Approaching={Approaching}, Failed={Failed}",
|
||||
evaluated, deleted, archived, markedExpired, approachingExpiry, failures.Count);
|
||||
@@ -324,9 +350,10 @@ public sealed class RetentionPolicyEnforcer : IRetentionPolicyEnforcer
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var notifications = new List<BundleExpiryNotification>();
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
var now = _timeProvider.GetUtcNow();
|
||||
var cutoff = now.AddDays(daysBeforeExpiry);
|
||||
string? cursor = null;
|
||||
var applyOverrides = _options.TenantOverrides.Count > 0 || _options.PredicateTypeOverrides.Count > 0;
|
||||
|
||||
do
|
||||
{
|
||||
@@ -336,7 +363,25 @@ public sealed class RetentionPolicyEnforcer : IRetentionPolicyEnforcer
|
||||
|
||||
foreach (var bundle in listResult.Bundles)
|
||||
{
|
||||
var expiryDate = CalculateExpiryDate(bundle);
|
||||
string? tenantId = null;
|
||||
IReadOnlyList<string>? predicateTypes = null;
|
||||
|
||||
if (applyOverrides)
|
||||
{
|
||||
var fullBundle = await _bundleStore.GetBundleAsync(bundle.BundleId, cancellationToken);
|
||||
if (fullBundle == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
tenantId = fullBundle.Metadata.TenantId;
|
||||
predicateTypes = fullBundle.Attestations
|
||||
.Select(attestation => attestation.PredicateType)
|
||||
.Distinct(StringComparer.Ordinal)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
var expiryDate = CalculateExpiryDate(tenantId, predicateTypes, bundle.CreatedAt);
|
||||
if (expiryDate > now && expiryDate <= cutoff)
|
||||
{
|
||||
notifications.Add(new BundleExpiryNotification(
|
||||
@@ -364,17 +409,51 @@ public sealed class RetentionPolicyEnforcer : IRetentionPolicyEnforcer
|
||||
/// <inheritdoc/>
|
||||
public DateTimeOffset CalculateExpiryDate(string? tenantId, DateTimeOffset createdAt)
|
||||
{
|
||||
int retentionMonths = _options.DefaultMonths;
|
||||
var retentionMonths = ResolveRetentionMonths(tenantId, null);
|
||||
|
||||
return createdAt.AddMonths(retentionMonths);
|
||||
}
|
||||
|
||||
private DateTimeOffset CalculateExpiryDate(
|
||||
string? tenantId,
|
||||
IReadOnlyList<string>? predicateTypes,
|
||||
DateTimeOffset createdAt)
|
||||
{
|
||||
var retentionMonths = ResolveRetentionMonths(tenantId, predicateTypes);
|
||||
return createdAt.AddMonths(retentionMonths);
|
||||
}
|
||||
|
||||
private int ResolveRetentionMonths(
|
||||
string? tenantId,
|
||||
IReadOnlyList<string>? predicateTypes)
|
||||
{
|
||||
var retentionMonths = ClampRetentionMonths(_options.DefaultMonths);
|
||||
|
||||
// Check for tenant-specific override
|
||||
if (!string.IsNullOrEmpty(tenantId) &&
|
||||
_options.TenantOverrides.TryGetValue(tenantId, out var tenantMonths))
|
||||
{
|
||||
retentionMonths = Math.Max(tenantMonths, _options.MinimumMonths);
|
||||
retentionMonths = Math.Min(retentionMonths, _options.MaximumMonths);
|
||||
retentionMonths = ClampRetentionMonths(tenantMonths);
|
||||
}
|
||||
|
||||
return createdAt.AddMonths(retentionMonths);
|
||||
if (predicateTypes != null && _options.PredicateTypeOverrides.Count > 0)
|
||||
{
|
||||
foreach (var predicateType in predicateTypes)
|
||||
{
|
||||
if (_options.PredicateTypeOverrides.TryGetValue(predicateType, out var predicateMonths))
|
||||
{
|
||||
retentionMonths = Math.Max(retentionMonths, ClampRetentionMonths(predicateMonths));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return retentionMonths;
|
||||
}
|
||||
|
||||
private int ClampRetentionMonths(int months)
|
||||
{
|
||||
var clamped = Math.Max(months, _options.MinimumMonths);
|
||||
return Math.Min(clamped, _options.MaximumMonths);
|
||||
}
|
||||
|
||||
private async Task<(bool Success, BundleEnforcementFailure? Failure)> HandleExpiredBundleAsync(
|
||||
|
||||
@@ -6,10 +6,10 @@
|
||||
<Nullable>enable</Nullable>
|
||||
<RootNamespace>StellaOps.Attestor.Bundling</RootNamespace>
|
||||
<Description>Attestation bundle aggregation and rotation for long-term verification in air-gapped environments.</Description>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="BouncyCastle.Cryptography" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" />
|
||||
<PackageReference Include="Microsoft.Extensions.Options" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<RootNamespace>StellaOps.Attestor.Bundling</RootNamespace>
|
||||
<Description>Attestation bundle aggregation and rotation for long-term verification in air-gapped environments.</Description>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="BouncyCastle.Cryptography" Version="2.6.2" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Options" Version="10.0.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\__Libraries\StellaOps.Canonical.Json\StellaOps.Canonical.Json.csproj" />
|
||||
<ProjectReference Include="..\StellaOps.Attestor.ProofChain\StellaOps.Attestor.ProofChain.csproj" />
|
||||
<ProjectReference Include="..\..\StellaOps.Attestor.Envelope\StellaOps.Attestor.Envelope.csproj" />
|
||||
<ProjectReference Include="..\StellaOps.Attestor.Bundle\StellaOps.Attestor.Bundle.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -7,4 +7,4 @@ Source of truth: `docs/implplan/SPRINT_20251229_049_BE_csproj_audit_maint_tests.
|
||||
| --- | --- | --- |
|
||||
| AUDIT-0047-M | DONE | Maintainability audit for StellaOps.Attestor.Bundling. |
|
||||
| AUDIT-0047-T | DONE | Test coverage audit for StellaOps.Attestor.Bundling. |
|
||||
| AUDIT-0047-A | TODO | Pending approval for changes. |
|
||||
| AUDIT-0047-A | DONE | Applied bundling validation, defaults, and test coverage updates. |
|
||||
|
||||
Reference in New Issue
Block a user