save audit remarks applications progress

This commit is contained in:
StellaOps Bot
2026-01-04 22:49:53 +02:00
parent 8862e112c4
commit eca4e964d3
48 changed files with 1850 additions and 112 deletions

View File

@@ -4,6 +4,7 @@ using System.Text.RegularExpressions;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using StellaOps.AirGap.Importer.Telemetry;
using StellaOps.Determinism;
namespace StellaOps.AirGap.Importer.Quarantine;
@@ -17,15 +18,18 @@ public sealed class FileSystemQuarantineService : IQuarantineService
private readonly QuarantineOptions _options;
private readonly ILogger<FileSystemQuarantineService> _logger;
private readonly TimeProvider _timeProvider;
private readonly IGuidProvider _guidProvider;
public FileSystemQuarantineService(
IOptions<QuarantineOptions> options,
ILogger<FileSystemQuarantineService> logger,
TimeProvider timeProvider)
TimeProvider timeProvider,
IGuidProvider? guidProvider = null)
{
_options = options?.Value ?? throw new ArgumentNullException(nameof(options));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider));
_guidProvider = guidProvider ?? SystemGuidProvider.Instance;
}
public async Task<QuarantineResult> QuarantineAsync(
@@ -74,7 +78,7 @@ public sealed class FileSystemQuarantineService : IQuarantineService
var now = _timeProvider.GetUtcNow();
var timestamp = now.ToString("yyyyMMdd-HHmmss", CultureInfo.InvariantCulture);
var sanitizedReason = SanitizeForPathSegment(request.ReasonCode);
var quarantineId = $"{timestamp}-{sanitizedReason}-{Guid.NewGuid():N}";
var quarantineId = $"{timestamp}-{sanitizedReason}-{_guidProvider.NewGuid():N}";
var quarantinePath = Path.Combine(tenantRoot, quarantineId);
@@ -250,7 +254,7 @@ public sealed class FileSystemQuarantineService : IQuarantineService
var removedPath = Path.Combine(removedRoot, quarantineId);
if (Directory.Exists(removedPath))
{
removedPath = Path.Combine(removedRoot, $"{quarantineId}-{Guid.NewGuid():N}");
removedPath = Path.Combine(removedRoot, $"{quarantineId}-{_guidProvider.NewGuid():N}");
}
Directory.Move(entryPath, removedPath);

View File

@@ -18,5 +18,6 @@
<ProjectReference Include="..\\..\\Attestor\\StellaOps.Attestor.Envelope\\StellaOps.Attestor.Envelope.csproj" />
<ProjectReference Include="..\\..\\__Libraries\\StellaOps.Cryptography\\StellaOps.Cryptography.csproj" />
<ProjectReference Include="..\\..\\__Libraries\\StellaOps.Cryptography.Plugin.OfflineVerification\\StellaOps.Cryptography.Plugin.OfflineVerification.csproj" />
<ProjectReference Include="..\\..\\__Libraries\\StellaOps.Determinism.Abstractions\\StellaOps.Determinism.Abstractions.csproj" />
</ItemGroup>
</Project>

View File

@@ -23,15 +23,18 @@ public sealed class RuleBundleValidator
private readonly DsseVerifier _dsseVerifier;
private readonly IVersionMonotonicityChecker _monotonicityChecker;
private readonly ILogger<RuleBundleValidator> _logger;
private readonly TimeProvider _timeProvider;
public RuleBundleValidator(
DsseVerifier dsseVerifier,
IVersionMonotonicityChecker monotonicityChecker,
ILogger<RuleBundleValidator> logger)
ILogger<RuleBundleValidator> logger,
TimeProvider? timeProvider = null)
{
_dsseVerifier = dsseVerifier ?? throw new ArgumentNullException(nameof(dsseVerifier));
_monotonicityChecker = monotonicityChecker ?? throw new ArgumentNullException(nameof(monotonicityChecker));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_timeProvider = timeProvider ?? TimeProvider.System;
}
/// <summary>
@@ -157,7 +160,7 @@ public sealed class RuleBundleValidator
BundleVersion incomingVersion;
try
{
incomingVersion = BundleVersion.Parse(request.Version, request.CreatedAt ?? DateTimeOffset.UtcNow);
incomingVersion = BundleVersion.Parse(request.Version, request.CreatedAt ?? _timeProvider.GetUtcNow());
}
catch (Exception ex)
{

View File

@@ -12,6 +12,7 @@ using System.Text.Json;
using StellaOps.AirGap.Bundle.Models;
using StellaOps.Concelier.Core.Raw;
using StellaOps.Concelier.RawModels;
using StellaOps.Determinism;
namespace StellaOps.AirGap.Bundle.Services;
@@ -134,12 +135,22 @@ public sealed class InMemoryAdvisoryRawRepository : IAdvisoryRawRepository
{
private readonly Dictionary<string, AdvisoryRawRecord> _records = new();
private readonly object _lock = new();
private readonly TimeProvider _timeProvider;
private readonly IGuidProvider _guidProvider;
public InMemoryAdvisoryRawRepository(
TimeProvider? timeProvider = null,
IGuidProvider? guidProvider = null)
{
_timeProvider = timeProvider ?? TimeProvider.System;
_guidProvider = guidProvider ?? SystemGuidProvider.Instance;
}
public Task<AdvisoryRawUpsertResult> UpsertAsync(AdvisoryRawDocument document, CancellationToken cancellationToken)
{
var contentHash = ComputeHash(document);
var key = $"{document.Tenant}:{contentHash}";
var now = DateTimeOffset.UtcNow;
var now = _timeProvider.GetUtcNow();
lock (_lock)
{
@@ -149,7 +160,7 @@ public sealed class InMemoryAdvisoryRawRepository : IAdvisoryRawRepository
}
var record = new AdvisoryRawRecord(
Id: Guid.NewGuid().ToString(),
Id: _guidProvider.NewGuid().ToString(),
Document: document,
IngestedAt: now,
CreatedAt: now);

View File

@@ -10,6 +10,7 @@ using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using StellaOps.AirGap.Bundle.Models;
using StellaOps.Determinism;
using StellaOps.Excititor.Core;
using StellaOps.Excititor.Core.Storage;
@@ -161,10 +162,14 @@ public sealed class InMemoryVexRawDocumentSink : IVexRawDocumentSink, IVexRawSto
private readonly Dictionary<string, VexRawRecord> _records = new();
private readonly string _tenant;
private readonly object _lock = new();
private readonly TimeProvider _timeProvider;
public InMemoryVexRawDocumentSink(string tenant = "default")
public InMemoryVexRawDocumentSink(
string tenant = "default",
TimeProvider? timeProvider = null)
{
_tenant = tenant;
_timeProvider = timeProvider ?? TimeProvider.System;
}
public ValueTask StoreAsync(VexRawDocument document, CancellationToken cancellationToken)
@@ -183,7 +188,7 @@ public sealed class InMemoryVexRawDocumentSink : IVexRawDocumentSink, IVexRawSto
Metadata: document.Metadata,
Content: document.Content,
InlineContent: true,
RecordedAt: DateTimeOffset.UtcNow);
RecordedAt: _timeProvider.GetUtcNow());
}
}

View File

@@ -9,6 +9,7 @@ using System.IO.Compression;
using System.Formats.Tar;
using System.Text.Json;
using StellaOps.AirGap.Bundle.Models;
using StellaOps.Determinism;
namespace StellaOps.AirGap.Bundle.Services;
@@ -25,15 +26,21 @@ public sealed class KnowledgeSnapshotImporter : IKnowledgeSnapshotImporter
private readonly IAdvisoryImportTarget? _advisoryTarget;
private readonly IVexImportTarget? _vexTarget;
private readonly IPolicyImportTarget? _policyTarget;
private readonly TimeProvider _timeProvider;
private readonly IGuidProvider _guidProvider;
public KnowledgeSnapshotImporter(
IAdvisoryImportTarget? advisoryTarget = null,
IVexImportTarget? vexTarget = null,
IPolicyImportTarget? policyTarget = null)
IPolicyImportTarget? policyTarget = null,
TimeProvider? timeProvider = null,
IGuidProvider? guidProvider = null)
{
_advisoryTarget = advisoryTarget;
_vexTarget = vexTarget;
_policyTarget = policyTarget;
_timeProvider = timeProvider ?? TimeProvider.System;
_guidProvider = guidProvider ?? SystemGuidProvider.Instance;
}
/// <summary>
@@ -48,10 +55,10 @@ public sealed class KnowledgeSnapshotImporter : IKnowledgeSnapshotImporter
if (!File.Exists(request.BundlePath))
{
return SnapshotImportResult.Failed("Bundle file not found");
return SnapshotImportResult.Failed("Bundle file not found", _timeProvider);
}
var tempDir = Path.Combine(Path.GetTempPath(), $"import-{Guid.NewGuid():N}");
var tempDir = Path.Combine(Path.GetTempPath(), $"import-{_guidProvider.NewGuid():N}");
Directory.CreateDirectory(tempDir);
try
@@ -63,21 +70,21 @@ public sealed class KnowledgeSnapshotImporter : IKnowledgeSnapshotImporter
var manifestPath = Path.Combine(tempDir, "manifest.json");
if (!File.Exists(manifestPath))
{
return SnapshotImportResult.Failed("Manifest not found in bundle");
return SnapshotImportResult.Failed("Manifest not found in bundle", _timeProvider);
}
var manifestBytes = await File.ReadAllBytesAsync(manifestPath, cancellationToken);
var manifest = JsonSerializer.Deserialize<KnowledgeSnapshotManifest>(manifestBytes, JsonOptions);
if (manifest is null)
{
return SnapshotImportResult.Failed("Failed to parse manifest");
return SnapshotImportResult.Failed("Failed to parse manifest", _timeProvider);
}
var result = new SnapshotImportResult
{
Success = true,
BundleId = manifest.BundleId,
StartedAt = DateTimeOffset.UtcNow
StartedAt = _timeProvider.GetUtcNow()
};
var errors = new List<string>();
@@ -148,7 +155,7 @@ public sealed class KnowledgeSnapshotImporter : IKnowledgeSnapshotImporter
result = result with
{
CompletedAt = DateTimeOffset.UtcNow,
CompletedAt = _timeProvider.GetUtcNow(),
Statistics = stats,
Errors = errors.Count > 0 ? [.. errors] : null,
Success = errors.Count == 0 || !request.FailOnAnyError
@@ -158,7 +165,7 @@ public sealed class KnowledgeSnapshotImporter : IKnowledgeSnapshotImporter
}
catch (Exception ex)
{
return SnapshotImportResult.Failed($"Import failed: {ex.Message}");
return SnapshotImportResult.Failed($"Import failed: {ex.Message}", _timeProvider);
}
finally
{
@@ -422,13 +429,17 @@ public sealed record SnapshotImportResult
public IReadOnlyList<string>? Errors { get; init; }
public string? Error { get; init; }
public static SnapshotImportResult Failed(string error) => new()
public static SnapshotImportResult Failed(string error, TimeProvider? timeProvider = null)
{
Success = false,
Error = error,
StartedAt = DateTimeOffset.UtcNow,
CompletedAt = DateTimeOffset.UtcNow
};
var now = (timeProvider ?? TimeProvider.System).GetUtcNow();
return new()
{
Success = false,
Error = error,
StartedAt = now,
CompletedAt = now
};
}
}
public sealed record ImportStatistics

View File

@@ -9,6 +9,7 @@ using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using StellaOps.AirGap.Bundle.Models;
using StellaOps.Determinism;
namespace StellaOps.AirGap.Bundle.Services;
@@ -26,13 +27,16 @@ public sealed class PolicyRegistryImportTarget : IPolicyImportTarget
private readonly IPolicyPackImportStore _store;
private readonly string _tenantId;
private readonly TimeProvider _timeProvider;
public PolicyRegistryImportTarget(
IPolicyPackImportStore store,
string tenantId = "default")
string tenantId = "default",
TimeProvider? timeProvider = null)
{
_store = store ?? throw new ArgumentNullException(nameof(store));
_tenantId = tenantId;
_timeProvider = timeProvider ?? TimeProvider.System;
}
/// <inheritdoc />
@@ -83,7 +87,7 @@ public sealed class PolicyRegistryImportTarget : IPolicyImportTarget
Version: data.Version ?? "1.0.0",
Content: data.Content,
Metadata: bundle.Metadata,
ImportedAt: DateTimeOffset.UtcNow);
ImportedAt: _timeProvider.GetUtcNow());
await _store.SaveAsync(pack, cancellationToken);
created++;

View File

@@ -9,6 +9,7 @@
<ItemGroup>
<ProjectReference Include="..\..\..\__Libraries\StellaOps.Canonical.Json\StellaOps.Canonical.Json.csproj" />
<ProjectReference Include="..\..\..\__Libraries\StellaOps.Determinism.Abstractions\StellaOps.Determinism.Abstractions.csproj" />
<ProjectReference Include="..\..\..\Concelier\__Libraries\StellaOps.Concelier.Core\StellaOps.Concelier.Core.csproj" />
<ProjectReference Include="..\..\..\Concelier\__Libraries\StellaOps.Concelier.RawModels\StellaOps.Concelier.RawModels.csproj" />
<ProjectReference Include="..\..\..\Excititor\__Libraries\StellaOps.Excititor.Core\StellaOps.Excititor.Core.csproj" />

View File

@@ -67,6 +67,7 @@ public static class VerdictEndpoints
private static async Task<IResult> StoreVerdictAsync(
[FromBody] StoreVerdictRequest request,
[FromServices] IVerdictRepository repository,
[FromServices] TimeProvider timeProvider,
[FromServices] ILogger<VerdictEndpointsLogger> logger,
CancellationToken cancellationToken)
{
@@ -105,7 +106,7 @@ public static class VerdictEndpoints
PredicateDigest = request.PredicateDigest,
DeterminismHash = request.DeterminismHash,
RekorLogIndex = request.RekorLogIndex,
CreatedAt = DateTimeOffset.UtcNow
CreatedAt = timeProvider.GetUtcNow()
};
// Store in repository
@@ -253,6 +254,7 @@ public static class VerdictEndpoints
private static async Task<IResult> VerifyVerdictAsync(
string verdictId,
[FromServices] IVerdictRepository repository,
[FromServices] TimeProvider timeProvider,
[FromServices] ILogger<VerdictEndpointsLogger> logger,
CancellationToken cancellationToken)
{
@@ -270,11 +272,12 @@ public static class VerdictEndpoints
// TODO: Implement actual signature verification
// For now, return a placeholder response
var now = timeProvider.GetUtcNow();
var response = new VerifyVerdictResponse
{
VerdictId = verdictId,
SignatureValid = true, // TODO: Implement verification
VerifiedAt = DateTimeOffset.UtcNow,
VerifiedAt = now,
Verifications = new[]
{
new SignatureVerification
@@ -289,7 +292,7 @@ public static class VerdictEndpoints
{
LogIndex = record.RekorLogIndex.Value,
InclusionProofValid = true, // TODO: Implement verification
VerifiedAt = DateTimeOffset.UtcNow
VerifiedAt = now
}
: null
};

View File

@@ -19,6 +19,7 @@ using StellaOps.EvidenceLocker.Core.Signing;
using StellaOps.EvidenceLocker.Core.Incident;
using StellaOps.EvidenceLocker.Core.Timeline;
using StellaOps.EvidenceLocker.Core.Storage;
using StellaOps.Determinism;
namespace StellaOps.EvidenceLocker.Infrastructure.Services;
@@ -37,6 +38,7 @@ public sealed class EvidenceSnapshotService
private readonly IIncidentModeState _incidentMode;
private readonly IEvidenceObjectStore _objectStore;
private readonly TimeProvider _timeProvider;
private readonly IGuidProvider _guidProvider;
private readonly ILogger<EvidenceSnapshotService> _logger;
private readonly QuotaOptions _quotas;
@@ -49,7 +51,8 @@ public sealed class EvidenceSnapshotService
IEvidenceObjectStore objectStore,
TimeProvider timeProvider,
IOptions<EvidenceLockerOptions> options,
ILogger<EvidenceSnapshotService> logger)
ILogger<EvidenceSnapshotService> logger,
IGuidProvider? guidProvider = null)
{
_repository = repository ?? throw new ArgumentNullException(nameof(repository));
_bundleBuilder = bundleBuilder ?? throw new ArgumentNullException(nameof(bundleBuilder));
@@ -61,6 +64,7 @@ public sealed class EvidenceSnapshotService
ArgumentNullException.ThrowIfNull(options);
_quotas = options.Value.Quotas ?? throw new InvalidOperationException("Quota options are required.");
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_guidProvider = guidProvider ?? SystemGuidProvider.Instance;
}
public async Task<EvidenceSnapshotResult> CreateSnapshotAsync(
@@ -76,7 +80,7 @@ public sealed class EvidenceSnapshotService
ArgumentNullException.ThrowIfNull(request);
ValidateRequest(request);
var bundleId = EvidenceBundleId.FromGuid(Guid.NewGuid());
var bundleId = EvidenceBundleId.FromGuid(_guidProvider.NewGuid());
var createdAt = _timeProvider.GetUtcNow();
var storageKey = $"tenants/{tenantId.Value:N}/bundles/{bundleId.Value:N}/bundle.tgz";
var incidentSnapshot = _incidentMode.Current;
@@ -245,7 +249,7 @@ public sealed class EvidenceSnapshotService
}
}
var holdId = EvidenceHoldId.FromGuid(Guid.NewGuid());
var holdId = EvidenceHoldId.FromGuid(_guidProvider.NewGuid());
var createdAt = _timeProvider.GetUtcNow();
var hold = new EvidenceHold(
holdId,

View File

@@ -14,6 +14,7 @@
<ProjectReference Include="..\..\..\__Libraries\StellaOps.Cryptography\StellaOps.Cryptography.csproj" />
<ProjectReference Include="..\..\..\__Libraries\StellaOps.Cryptography.DependencyInjection\StellaOps.Cryptography.DependencyInjection.csproj" />
<ProjectReference Include="..\..\..\__Libraries\StellaOps.Cryptography.Plugin.BouncyCastle\StellaOps.Cryptography.Plugin.BouncyCastle.csproj" />
<ProjectReference Include="..\..\..\__Libraries\StellaOps.Determinism.Abstractions\StellaOps.Determinism.Abstractions.csproj" />
</ItemGroup>
<ItemGroup>

View File

@@ -3,6 +3,7 @@ using Microsoft.Extensions.Logging;
using StellaOps.EvidenceLocker.Core.Configuration;
using StellaOps.EvidenceLocker.Core.Domain;
using StellaOps.EvidenceLocker.Core.Storage;
using StellaOps.Determinism;
namespace StellaOps.EvidenceLocker.Infrastructure.Storage;
@@ -11,11 +12,15 @@ internal sealed class FileSystemEvidenceObjectStore : IEvidenceObjectStore
private readonly string _rootPath;
private readonly bool _enforceWriteOnce;
private readonly ILogger<FileSystemEvidenceObjectStore> _logger;
private readonly TimeProvider _timeProvider;
private readonly IGuidProvider _guidProvider;
public FileSystemEvidenceObjectStore(
FileSystemStoreOptions options,
bool enforceWriteOnce,
ILogger<FileSystemEvidenceObjectStore> logger)
ILogger<FileSystemEvidenceObjectStore> logger,
TimeProvider? timeProvider = null,
IGuidProvider? guidProvider = null)
{
ArgumentNullException.ThrowIfNull(options);
ArgumentException.ThrowIfNullOrWhiteSpace(options.RootPath);
@@ -23,6 +28,8 @@ internal sealed class FileSystemEvidenceObjectStore : IEvidenceObjectStore
_rootPath = Path.GetFullPath(options.RootPath);
_enforceWriteOnce = enforceWriteOnce;
_logger = logger;
_timeProvider = timeProvider ?? TimeProvider.System;
_guidProvider = guidProvider ?? SystemGuidProvider.Instance;
Directory.CreateDirectory(_rootPath);
}
@@ -33,8 +40,8 @@ internal sealed class FileSystemEvidenceObjectStore : IEvidenceObjectStore
ArgumentNullException.ThrowIfNull(options);
var writeOnce = _enforceWriteOnce || options.EnforceWriteOnce;
var utcNow = DateTimeOffset.UtcNow;
var tempFilePath = Path.Combine(_rootPath, ".tmp", Guid.NewGuid().ToString("N"));
var utcNow = _timeProvider.GetUtcNow();
var tempFilePath = Path.Combine(_rootPath, ".tmp", _guidProvider.NewGuid().ToString("N"));
Directory.CreateDirectory(Path.GetDirectoryName(tempFilePath)!);

View File

@@ -6,6 +6,7 @@ using Microsoft.Extensions.Logging;
using StellaOps.EvidenceLocker.Core.Configuration;
using StellaOps.EvidenceLocker.Core.Domain;
using StellaOps.EvidenceLocker.Core.Storage;
using StellaOps.Determinism;
namespace StellaOps.EvidenceLocker.Infrastructure.Storage;
@@ -15,17 +16,23 @@ internal sealed class S3EvidenceObjectStore : IEvidenceObjectStore, IDisposable
private readonly AmazonS3StoreOptions _options;
private readonly bool _enforceWriteOnce;
private readonly ILogger<S3EvidenceObjectStore> _logger;
private readonly TimeProvider _timeProvider;
private readonly IGuidProvider _guidProvider;
public S3EvidenceObjectStore(
IAmazonS3 s3,
AmazonS3StoreOptions options,
bool enforceWriteOnce,
ILogger<S3EvidenceObjectStore> logger)
ILogger<S3EvidenceObjectStore> logger,
TimeProvider? timeProvider = null,
IGuidProvider? guidProvider = null)
{
_s3 = s3 ?? throw new ArgumentNullException(nameof(s3));
_options = options ?? throw new ArgumentNullException(nameof(options));
_enforceWriteOnce = enforceWriteOnce;
_logger = logger;
_timeProvider = timeProvider ?? TimeProvider.System;
_guidProvider = guidProvider ?? SystemGuidProvider.Instance;
}
public async Task<EvidenceObjectMetadata> StoreAsync(
@@ -37,7 +44,7 @@ internal sealed class S3EvidenceObjectStore : IEvidenceObjectStore, IDisposable
ArgumentNullException.ThrowIfNull(options);
var writeOnce = _enforceWriteOnce || options.EnforceWriteOnce;
var tempFilePath = Path.Combine(Path.GetTempPath(), $"evidence-{Guid.NewGuid():N}.tmp");
var tempFilePath = Path.Combine(Path.GetTempPath(), $"evidence-{_guidProvider.NewGuid():N}.tmp");
using var sha = SHA256.Create();
long totalBytes = 0;
@@ -83,7 +90,7 @@ internal sealed class S3EvidenceObjectStore : IEvidenceObjectStore, IDisposable
SizeBytes: totalBytes,
Sha256: sha256,
ETag: eTag,
CreatedAt: DateTimeOffset.UtcNow);
CreatedAt: _timeProvider.GetUtcNow());
}
public async Task<Stream> OpenReadAsync(string storageKey, CancellationToken cancellationToken)

View File

@@ -15,6 +15,7 @@ using StellaOps.EvidenceLocker.Core.Configuration;
using StellaOps.EvidenceLocker.Core.Domain;
using StellaOps.EvidenceLocker.Core.Incident;
using StellaOps.EvidenceLocker.Core.Timeline;
using StellaOps.Determinism;
namespace StellaOps.EvidenceLocker.Infrastructure.Timeline;
@@ -29,12 +30,14 @@ internal sealed class TimelineIndexerEvidenceTimelinePublisher : IEvidenceTimeli
private readonly TimelineOptions _options;
private readonly ILogger<TimelineIndexerEvidenceTimelinePublisher> _logger;
private readonly Uri _endpoint;
private readonly IGuidProvider _guidProvider;
public TimelineIndexerEvidenceTimelinePublisher(
HttpClient httpClient,
IOptions<EvidenceLockerOptions> options,
TimeProvider timeProvider,
ILogger<TimelineIndexerEvidenceTimelinePublisher> logger)
ILogger<TimelineIndexerEvidenceTimelinePublisher> logger,
IGuidProvider? guidProvider = null)
{
_httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
ArgumentNullException.ThrowIfNull(options);
@@ -53,6 +56,7 @@ internal sealed class TimelineIndexerEvidenceTimelinePublisher : IEvidenceTimeli
_endpoint = new Uri(_options.Endpoint, UriKind.Absolute);
ArgumentNullException.ThrowIfNull(timeProvider);
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_guidProvider = guidProvider ?? SystemGuidProvider.Instance;
}
public async Task PublishBundleSealedAsync(
@@ -85,7 +89,7 @@ internal sealed class TimelineIndexerEvidenceTimelinePublisher : IEvidenceTimeli
private TimelineEventEnvelope BuildBundleEvent(EvidenceBundleSignature signature, EvidenceBundleManifest manifest, string rootHash)
{
var eventId = Guid.NewGuid();
var eventId = _guidProvider.NewGuid();
var occurredAt = signature.TimestampedAt ?? signature.SignedAt;
var attributes = new SortedDictionary<string, string>(StringComparer.Ordinal)
{
@@ -139,7 +143,7 @@ internal sealed class TimelineIndexerEvidenceTimelinePublisher : IEvidenceTimeli
private TimelineEventEnvelope BuildHoldEvent(EvidenceHold hold)
{
var eventId = Guid.NewGuid();
var eventId = _guidProvider.NewGuid();
var occurredAt = hold.CreatedAt;
var attributes = new SortedDictionary<string, string>(StringComparer.Ordinal)
{
@@ -175,7 +179,7 @@ internal sealed class TimelineIndexerEvidenceTimelinePublisher : IEvidenceTimeli
private TimelineEventEnvelope BuildIncidentEvent(IncidentModeChange change)
{
var eventId = Guid.NewGuid();
var eventId = _guidProvider.NewGuid();
var attributes = new SortedDictionary<string, string>(StringComparer.Ordinal)
{
["state"] = change.IsActive ? "enabled" : "disabled",

View File

@@ -63,7 +63,7 @@ public sealed record VerdictAttestationRecord
public required string PredicateDigest { get; init; }
public string? DeterminismHash { get; init; }
public long? RekorLogIndex { get; init; }
public DateTimeOffset CreatedAt { get; init; } = DateTimeOffset.UtcNow;
public required DateTimeOffset CreatedAt { get; init; }
}
/// <summary>

View File

@@ -1,5 +1,6 @@
using System.Security.Cryptography;
using Microsoft.Extensions.Logging;
using StellaOps.Determinism;
using StellaOps.IssuerDirectory.Core.Abstractions;
using StellaOps.IssuerDirectory.Core.Domain;
using StellaOps.IssuerDirectory.Core.Observability;
@@ -16,6 +17,7 @@ public sealed class IssuerKeyService
private readonly IIssuerKeyRepository _keyRepository;
private readonly IIssuerAuditSink _auditSink;
private readonly TimeProvider _timeProvider;
private readonly IGuidProvider _guidProvider;
private readonly ILogger<IssuerKeyService> _logger;
public IssuerKeyService(
@@ -23,13 +25,15 @@ public sealed class IssuerKeyService
IIssuerKeyRepository keyRepository,
IIssuerAuditSink auditSink,
TimeProvider timeProvider,
ILogger<IssuerKeyService> logger)
ILogger<IssuerKeyService> logger,
IGuidProvider? guidProvider = null)
{
_issuerRepository = issuerRepository ?? throw new ArgumentNullException(nameof(issuerRepository));
_keyRepository = keyRepository ?? throw new ArgumentNullException(nameof(keyRepository));
_auditSink = auditSink ?? throw new ArgumentNullException(nameof(auditSink));
_timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_guidProvider = guidProvider ?? SystemGuidProvider.Instance;
}
public async Task<IReadOnlyCollection<IssuerKeyRecord>> ListAsync(
@@ -101,7 +105,7 @@ public sealed class IssuerKeyService
var now = _timeProvider.GetUtcNow();
var record = IssuerKeyRecord.Create(
Guid.NewGuid().ToString("n"),
_guidProvider.NewGuid().ToString("n"),
issuerId,
tenantId,
type,
@@ -205,7 +209,7 @@ public sealed class IssuerKeyService
.ConfigureAwait(false);
var replacement = IssuerKeyRecord.Create(
Guid.NewGuid().ToString("n"),
_guidProvider.NewGuid().ToString("n"),
issuerId,
tenantId,
newType,

View File

@@ -9,4 +9,7 @@
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\__Libraries\StellaOps.Determinism.Abstractions\StellaOps.Determinism.Abstractions.csproj" />
</ItemGroup>
</Project>

View File

@@ -11,6 +11,13 @@ namespace StellaOps.Scanner.Analyzers.Native.Hardening;
/// </summary>
public sealed class ElfHardeningExtractor : IHardeningExtractor
{
private readonly TimeProvider _timeProvider;
public ElfHardeningExtractor(TimeProvider? timeProvider = null)
{
_timeProvider = timeProvider ?? TimeProvider.System;
}
// ELF magic bytes
private static readonly byte[] ElfMagic = [0x7F, 0x45, 0x4C, 0x46]; // \x7FELF
@@ -596,7 +603,7 @@ public sealed class ElfHardeningExtractor : IHardeningExtractor
#endregion
private static BinaryHardeningFlags CreateResult(
private BinaryHardeningFlags CreateResult(
string path,
string digest,
List<HardeningFlag> flags,
@@ -623,7 +630,7 @@ public sealed class ElfHardeningExtractor : IHardeningExtractor
Flags: [.. flags],
HardeningScore: Math.Round(score, 2),
MissingFlags: [.. missing],
ExtractedAt: DateTimeOffset.UtcNow);
ExtractedAt: _timeProvider.GetUtcNow());
}
private static ushort ReadUInt16(ReadOnlySpan<byte> span, bool littleEndian)

View File

@@ -17,6 +17,13 @@ namespace StellaOps.Scanner.Analyzers.Native.Hardening;
/// </summary>
public sealed class MachoHardeningExtractor : IHardeningExtractor
{
private readonly TimeProvider _timeProvider;
public MachoHardeningExtractor(TimeProvider? timeProvider = null)
{
_timeProvider = timeProvider ?? TimeProvider.System;
}
// Mach-O magic numbers
private const uint MH_MAGIC = 0xFEEDFACE; // 32-bit
private const uint MH_CIGAM = 0xCEFAEDFE; // 32-bit (reversed)
@@ -257,7 +264,7 @@ public sealed class MachoHardeningExtractor : IHardeningExtractor
: BinaryPrimitives.ReadUInt32BigEndian(data.AsSpan(offset, 4));
}
private static BinaryHardeningFlags CreateResult(
private BinaryHardeningFlags CreateResult(
string path,
string digest,
List<HardeningFlag> flags,
@@ -283,6 +290,6 @@ public sealed class MachoHardeningExtractor : IHardeningExtractor
Flags: [.. flags],
HardeningScore: Math.Round(score, 2),
MissingFlags: [.. missing],
ExtractedAt: DateTimeOffset.UtcNow);
ExtractedAt: _timeProvider.GetUtcNow());
}
}

View File

@@ -19,6 +19,13 @@ namespace StellaOps.Scanner.Analyzers.Native.Hardening;
/// </summary>
public sealed class PeHardeningExtractor : IHardeningExtractor
{
private readonly TimeProvider _timeProvider;
public PeHardeningExtractor(TimeProvider? timeProvider = null)
{
_timeProvider = timeProvider ?? TimeProvider.System;
}
// PE magic bytes: MZ (DOS header)
private const ushort DOS_MAGIC = 0x5A4D; // "MZ"
private const uint PE_SIGNATURE = 0x00004550; // "PE\0\0"
@@ -233,7 +240,7 @@ public sealed class PeHardeningExtractor : IHardeningExtractor
}
}
private static BinaryHardeningFlags CreateResult(
private BinaryHardeningFlags CreateResult(
string path,
string digest,
List<HardeningFlag> flags,
@@ -259,6 +266,6 @@ public sealed class PeHardeningExtractor : IHardeningExtractor
Flags: [.. flags],
HardeningScore: Math.Round(score, 2),
MissingFlags: [.. missing],
ExtractedAt: DateTimeOffset.UtcNow);
ExtractedAt: _timeProvider.GetUtcNow());
}
}

View File

@@ -17,6 +17,7 @@ public sealed class OfflineBuildIdIndex : IBuildIdIndex
private readonly BuildIdIndexOptions _options;
private readonly ILogger<OfflineBuildIdIndex> _logger;
private readonly IDsseSigningService? _dsseSigningService;
private readonly TimeProvider _timeProvider;
private FrozenDictionary<string, BuildIdLookupResult> _index = FrozenDictionary<string, BuildIdLookupResult>.Empty;
private bool _isLoaded;
@@ -31,7 +32,8 @@ public sealed class OfflineBuildIdIndex : IBuildIdIndex
public OfflineBuildIdIndex(
IOptions<BuildIdIndexOptions> options,
ILogger<OfflineBuildIdIndex> logger,
IDsseSigningService? dsseSigningService = null)
IDsseSigningService? dsseSigningService = null,
TimeProvider? timeProvider = null)
{
ArgumentNullException.ThrowIfNull(options);
ArgumentNullException.ThrowIfNull(logger);
@@ -39,6 +41,7 @@ public sealed class OfflineBuildIdIndex : IBuildIdIndex
_options = options.Value;
_logger = logger;
_dsseSigningService = dsseSigningService;
_timeProvider = timeProvider ?? TimeProvider.System;
}
/// <inheritdoc />
@@ -176,7 +179,7 @@ public sealed class OfflineBuildIdIndex : IBuildIdIndex
// Check index freshness
if (_options.MaxIndexAge > TimeSpan.Zero)
{
var oldestAllowed = DateTimeOffset.UtcNow - _options.MaxIndexAge;
var oldestAllowed = _timeProvider.GetUtcNow() - _options.MaxIndexAge;
var latestEntry = entries.Values.MaxBy(e => e.IndexedAt);
if (latestEntry is not null && latestEntry.IndexedAt < oldestAllowed)
{

View File

@@ -4,6 +4,7 @@ using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using System.Text;
using System.Text.RegularExpressions;
using StellaOps.Determinism;
namespace StellaOps.Scanner.Analyzers.Native.RuntimeCapture;
@@ -22,6 +23,8 @@ namespace StellaOps.Scanner.Analyzers.Native.RuntimeCapture;
[SupportedOSPlatform("linux")]
public sealed class LinuxEbpfCaptureAdapter : IRuntimeCaptureAdapter
{
private readonly TimeProvider _timeProvider;
private readonly IGuidProvider _guidProvider;
private readonly ConcurrentBag<RuntimeLoadEvent> _events = [];
private readonly object _stateLock = new();
private CaptureState _state = CaptureState.Idle;
@@ -33,6 +36,17 @@ public sealed class LinuxEbpfCaptureAdapter : IRuntimeCaptureAdapter
private long _droppedEvents;
private int _redactedPaths;
/// <summary>
/// Creates a new Linux eBPF capture adapter.
/// </summary>
/// <param name="timeProvider">Optional time provider for deterministic timestamps.</param>
/// <param name="guidProvider">Optional GUID provider for deterministic session IDs.</param>
public LinuxEbpfCaptureAdapter(TimeProvider? timeProvider = null, IGuidProvider? guidProvider = null)
{
_timeProvider = timeProvider ?? TimeProvider.System;
_guidProvider = guidProvider ?? SystemGuidProvider.Instance;
}
/// <inheritdoc />
public string AdapterId => "linux-ebpf-dlopen";
@@ -152,8 +166,8 @@ public sealed class LinuxEbpfCaptureAdapter : IRuntimeCaptureAdapter
_events.Clear();
_droppedEvents = 0;
_redactedPaths = 0;
SessionId = Guid.NewGuid().ToString("N");
_startTime = DateTime.UtcNow;
SessionId = _guidProvider.NewGuid().ToString("N");
_startTime = _timeProvider.GetUtcNow().UtcDateTime;
_captureCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
try
@@ -243,7 +257,7 @@ public sealed class LinuxEbpfCaptureAdapter : IRuntimeCaptureAdapter
var session = new RuntimeCaptureSession(
SessionId: SessionId ?? "unknown",
StartTime: _startTime,
EndTime: DateTime.UtcNow,
EndTime: _timeProvider.GetUtcNow().UtcDateTime,
Platform: Platform,
CaptureMethod: CaptureMethod,
TargetProcessId: _options?.TargetProcessId,
@@ -405,7 +419,7 @@ public sealed class LinuxEbpfCaptureAdapter : IRuntimeCaptureAdapter
if (parts[0] == "DLOPEN" && parts.Length >= 5)
{
return new RuntimeLoadEvent(
Timestamp: DateTime.UtcNow,
Timestamp: _timeProvider.GetUtcNow().UtcDateTime,
ProcessId: int.Parse(parts[1]),
ThreadId: int.Parse(parts[2]),
LoadType: RuntimeLoadType.Dlopen,

View File

@@ -3,6 +3,7 @@ using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using System.Text.RegularExpressions;
using StellaOps.Determinism;
namespace StellaOps.Scanner.Analyzers.Native.RuntimeCapture;
@@ -23,6 +24,8 @@ namespace StellaOps.Scanner.Analyzers.Native.RuntimeCapture;
[SupportedOSPlatform("macos")]
public sealed class MacOsDyldCaptureAdapter : IRuntimeCaptureAdapter
{
private readonly TimeProvider _timeProvider;
private readonly IGuidProvider _guidProvider;
private readonly ConcurrentBag<RuntimeLoadEvent> _events = [];
private readonly object _stateLock = new();
private CaptureState _state = CaptureState.Idle;
@@ -34,6 +37,17 @@ public sealed class MacOsDyldCaptureAdapter : IRuntimeCaptureAdapter
private long _droppedEvents;
private int _redactedPaths;
/// <summary>
/// Creates a new macOS dyld capture adapter.
/// </summary>
/// <param name="timeProvider">Optional time provider for deterministic timestamps.</param>
/// <param name="guidProvider">Optional GUID provider for deterministic session IDs.</param>
public MacOsDyldCaptureAdapter(TimeProvider? timeProvider = null, IGuidProvider? guidProvider = null)
{
_timeProvider = timeProvider ?? TimeProvider.System;
_guidProvider = guidProvider ?? SystemGuidProvider.Instance;
}
/// <inheritdoc />
public string AdapterId => "macos-dyld-interpose";
@@ -156,8 +170,8 @@ public sealed class MacOsDyldCaptureAdapter : IRuntimeCaptureAdapter
_events.Clear();
_droppedEvents = 0;
_redactedPaths = 0;
SessionId = Guid.NewGuid().ToString("N");
_startTime = DateTime.UtcNow;
SessionId = _guidProvider.NewGuid().ToString("N");
_startTime = _timeProvider.GetUtcNow().UtcDateTime;
_captureCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
try
@@ -247,7 +261,7 @@ public sealed class MacOsDyldCaptureAdapter : IRuntimeCaptureAdapter
var session = new RuntimeCaptureSession(
SessionId: SessionId ?? "unknown",
StartTime: _startTime,
EndTime: DateTime.UtcNow,
EndTime: _timeProvider.GetUtcNow().UtcDateTime,
Platform: Platform,
CaptureMethod: CaptureMethod,
TargetProcessId: _options?.TargetProcessId,
@@ -417,7 +431,7 @@ public sealed class MacOsDyldCaptureAdapter : IRuntimeCaptureAdapter
: RuntimeLoadType.MacOsDlopen;
return new RuntimeLoadEvent(
Timestamp: DateTime.UtcNow,
Timestamp: _timeProvider.GetUtcNow().UtcDateTime,
ProcessId: int.Parse(parts[1]),
ThreadId: int.Parse(parts[2]),
LoadType: loadType,

View File

@@ -48,11 +48,13 @@ public static class RuntimeEvidenceAggregator
/// <param name="runtimeEvidence">Runtime capture evidence.</param>
/// <param name="staticEdges">Static analysis dependency edges.</param>
/// <param name="heuristicEdges">Heuristic analysis edges.</param>
/// <param name="timeProvider">Optional time provider for deterministic timestamps.</param>
/// <returns>Merged evidence document.</returns>
public static MergedEvidence MergeWithStaticAnalysis(
RuntimeEvidence runtimeEvidence,
IEnumerable<Observations.NativeObservationDeclaredEdge> staticEdges,
IEnumerable<Observations.NativeObservationHeuristicEdge> heuristicEdges)
IEnumerable<Observations.NativeObservationHeuristicEdge> heuristicEdges,
TimeProvider? timeProvider = null)
{
var staticList = staticEdges.ToList();
var heuristicList = heuristicEdges.ToList();
@@ -140,6 +142,7 @@ public static class RuntimeEvidenceAggregator
}
}
var tp = timeProvider ?? TimeProvider.System;
return new MergedEvidence(
ConfirmedEdges: confirmedEdges,
StaticOnlyEdges: staticOnlyEdges,
@@ -148,7 +151,7 @@ public static class RuntimeEvidenceAggregator
TotalRuntimeEvents: runtimeEvidence.Sessions.Sum(s => s.Events.Count),
TotalDroppedEvents: runtimeEvidence.Sessions.Sum(s => s.TotalEventsDropped),
CaptureStartTime: runtimeEvidence.Sessions.Min(s => s.StartTime),
CaptureEndTime: runtimeEvidence.Sessions.Max(s => s.EndTime ?? DateTime.UtcNow));
CaptureEndTime: runtimeEvidence.Sessions.Max(s => s.EndTime ?? tp.GetUtcNow().UtcDateTime));
}
/// <summary>

View File

@@ -273,7 +273,9 @@ public sealed record CollapsedStack
/// Parses a collapsed stack line.
/// Format: "container@digest;buildid=xxx;func;... count"
/// </summary>
public static CollapsedStack? Parse(string line)
/// <param name="line">The collapsed stack line to parse.</param>
/// <param name="timeProvider">Optional time provider for deterministic timestamps.</param>
public static CollapsedStack? Parse(string line, TimeProvider? timeProvider = null)
{
if (string.IsNullOrWhiteSpace(line))
return null;
@@ -305,7 +307,8 @@ public sealed record CollapsedStack
}
}
var now = DateTime.UtcNow;
var tp = timeProvider ?? TimeProvider.System;
var now = tp.GetUtcNow().UtcDateTime;
return new CollapsedStack
{
ContainerIdentifier = container,

View File

@@ -4,6 +4,7 @@ using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using System.Security.Principal;
using System.Text.RegularExpressions;
using StellaOps.Determinism;
namespace StellaOps.Scanner.Analyzers.Native.RuntimeCapture;
@@ -21,6 +22,8 @@ namespace StellaOps.Scanner.Analyzers.Native.RuntimeCapture;
[SupportedOSPlatform("windows")]
public sealed class WindowsEtwCaptureAdapter : IRuntimeCaptureAdapter
{
private readonly TimeProvider _timeProvider;
private readonly IGuidProvider _guidProvider;
private readonly ConcurrentBag<RuntimeLoadEvent> _events = [];
private readonly object _stateLock = new();
private CaptureState _state = CaptureState.Idle;
@@ -34,6 +37,17 @@ public sealed class WindowsEtwCaptureAdapter : IRuntimeCaptureAdapter
private long _droppedEvents;
private int _redactedPaths;
/// <summary>
/// Creates a new Windows ETW capture adapter.
/// </summary>
/// <param name="timeProvider">Optional time provider for deterministic timestamps.</param>
/// <param name="guidProvider">Optional GUID provider for deterministic session IDs.</param>
public WindowsEtwCaptureAdapter(TimeProvider? timeProvider = null, IGuidProvider? guidProvider = null)
{
_timeProvider = timeProvider ?? TimeProvider.System;
_guidProvider = guidProvider ?? SystemGuidProvider.Instance;
}
/// <inheritdoc />
public string AdapterId => "windows-etw-imageload";
@@ -146,8 +160,8 @@ public sealed class WindowsEtwCaptureAdapter : IRuntimeCaptureAdapter
_events.Clear();
_droppedEvents = 0;
_redactedPaths = 0;
SessionId = Guid.NewGuid().ToString("N");
_startTime = DateTime.UtcNow;
SessionId = _guidProvider.NewGuid().ToString("N");
_startTime = _timeProvider.GetUtcNow().UtcDateTime;
_captureCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
try
@@ -240,7 +254,7 @@ public sealed class WindowsEtwCaptureAdapter : IRuntimeCaptureAdapter
var session = new RuntimeCaptureSession(
SessionId: SessionId ?? "unknown",
StartTime: _startTime,
EndTime: DateTime.UtcNow,
EndTime: _timeProvider.GetUtcNow().UtcDateTime,
Platform: Platform,
CaptureMethod: CaptureMethod,
TargetProcessId: _options?.TargetProcessId,
@@ -480,7 +494,7 @@ public sealed class WindowsEtwCaptureAdapter : IRuntimeCaptureAdapter
: RuntimeLoadType.LoadLibrary;
var evt = new RuntimeLoadEvent(
Timestamp: DateTime.UtcNow,
Timestamp: _timeProvider.GetUtcNow().UtcDateTime,
ProcessId: processId,
ThreadId: 0,
LoadType: loadType,

View File

@@ -15,6 +15,7 @@
<ItemGroup>
<ProjectReference Include="..\\__Libraries\\StellaOps.Scanner.ProofSpine\\StellaOps.Scanner.ProofSpine.csproj" />
<ProjectReference Include="..\\..\\__Libraries\\StellaOps.Determinism.Abstractions\\StellaOps.Determinism.Abstractions.csproj" />
</ItemGroup>
<ItemGroup>

View File

@@ -151,6 +151,7 @@ public static class EpssEndpoints
private static async Task<IResult> GetHistory(
[FromRoute] string cveId,
[FromServices] IEpssProvider epssProvider,
[FromServices] TimeProvider timeProvider,
[FromQuery] string? startDate = null,
[FromQuery] string? endDate = null,
[FromQuery] int days = 30,
@@ -183,7 +184,7 @@ public static class EpssEndpoints
else
{
// Default to last N days
end = DateOnly.FromDateTime(DateTime.UtcNow);
end = DateOnly.FromDateTime(timeProvider.GetUtcNow().UtcDateTime);
start = end.AddDays(-days);
}
@@ -213,6 +214,7 @@ public static class EpssEndpoints
/// </summary>
private static async Task<IResult> GetStatus(
[FromServices] IEpssProvider epssProvider,
[FromServices] TimeProvider timeProvider,
CancellationToken cancellationToken)
{
var isAvailable = await epssProvider.IsAvailableAsync(cancellationToken);
@@ -222,7 +224,7 @@ public static class EpssEndpoints
{
Available = isAvailable,
LatestModelDate = modelDate?.ToString("yyyy-MM-dd"),
LastCheckedUtc = DateTimeOffset.UtcNow
LastCheckedUtc = timeProvider.GetUtcNow()
});
}
}

View File

@@ -60,6 +60,7 @@ internal static class EvidenceEndpoints
string scanId,
string findingId,
IEvidenceCompositionService evidenceService,
TimeProvider timeProvider,
HttpContext context,
CancellationToken cancellationToken)
{
@@ -108,7 +109,7 @@ internal static class EvidenceEndpoints
}
else if (evidence.Freshness.ExpiresAt.HasValue)
{
var timeUntilExpiry = evidence.Freshness.ExpiresAt.Value - DateTimeOffset.UtcNow;
var timeUntilExpiry = evidence.Freshness.ExpiresAt.Value - timeProvider.GetUtcNow();
if (timeUntilExpiry <= TimeSpan.FromDays(1))
{
context.Response.Headers["X-Evidence-Warning"] = "near-expiry";

View File

@@ -270,6 +270,7 @@ internal static class SmartDiffEndpoints
string candidateId,
ReviewRequest request,
IVexCandidateStore store,
TimeProvider timeProvider,
HttpContext httpContext,
CancellationToken ct = default)
{
@@ -282,7 +283,7 @@ internal static class SmartDiffEndpoints
var review = new VexCandidateReview(
Action: action,
Reviewer: reviewer,
ReviewedAt: DateTimeOffset.UtcNow,
ReviewedAt: timeProvider.GetUtcNow(),
Comment: request.Comment);
var success = await store.ReviewCandidateAsync(candidateId, review, ct);

View File

@@ -41,6 +41,7 @@ internal static class ProofBundleEndpoints
private static async Task<IResult> HandleGenerateProofBundleAsync(
[FromBody] ProofBundleRequest request,
[FromServices] IProofBundleGenerator bundleGenerator,
[FromServices] TimeProvider timeProvider,
CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(bundleGenerator);
@@ -67,7 +68,7 @@ internal static class ProofBundleEndpoints
{
PathId = request.PathId,
Bundle = bundle,
GeneratedAt = DateTimeOffset.UtcNow
GeneratedAt = timeProvider.GetUtcNow()
};
return Results.Ok(response);

View File

@@ -50,6 +50,7 @@ internal static class TriageInboxEndpoints
[FromQuery] string? filter,
[FromServices] IExploitPathGroupingService groupingService,
[FromServices] IFindingQueryService findingService,
[FromServices] TimeProvider timeProvider,
CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(groupingService);
@@ -77,7 +78,7 @@ internal static class TriageInboxEndpoints
FilteredPaths = filteredPaths.Count,
Filter = filter,
Paths = filteredPaths,
GeneratedAt = DateTimeOffset.UtcNow
GeneratedAt = timeProvider.GetUtcNow()
};
return Results.Ok(response);

View File

@@ -55,6 +55,7 @@ internal static class UnknownsEndpoints
[FromQuery] int? limit,
IUnknownRepository repository,
IUnknownRanker ranker,
TimeProvider timeProvider,
CancellationToken cancellationToken)
{
// Validate and default pagination
@@ -95,9 +96,10 @@ internal static class UnknownsEndpoints
PageSize: pageSize);
var result = await repository.ListUnknownsAsync(query, cancellationToken);
var now = timeProvider.GetUtcNow();
return Results.Ok(new UnknownsListResponse(
Items: result.Items.Select(UnknownItemResponse.FromUnknownItem).ToList(),
Items: result.Items.Select(item => UnknownItemResponse.FromUnknownItem(item, now)).ToList(),
TotalCount: result.TotalCount,
Page: pageNum,
PageSize: pageSize,
@@ -195,7 +197,7 @@ public sealed record UnknownItemResponse(
ContainmentResponse? Containment,
DateTimeOffset CreatedAt)
{
public static UnknownItemResponse FromUnknownItem(UnknownItem item) => new(
public static UnknownItemResponse FromUnknownItem(UnknownItem item, DateTimeOffset now) => new(
Id: Guid.TryParse(item.Id, out var id) ? id : Guid.Empty,
SubjectRef: item.ArtifactPurl ?? item.ArtifactDigest,
Kind: string.Join(",", item.Reasons),
@@ -209,7 +211,7 @@ public sealed record UnknownItemResponse(
Containment: item.Containment != null
? new ContainmentResponse(item.Containment.Seccomp, item.Containment.Fs)
: null,
CreatedAt: DateTimeOffset.UtcNow); // Would come from Unknown.SysFrom
CreatedAt: now); // Would come from Unknown.SysFrom
}
/// <summary>

View File

@@ -120,6 +120,7 @@ internal static class WitnessEndpoints
private static async Task<IResult> HandleVerifyWitnessAsync(
Guid witnessId,
IWitnessRepository repository,
TimeProvider timeProvider,
CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(repository);
@@ -161,10 +162,11 @@ internal static class WitnessEndpoints
}
// Record verification attempt
var now = timeProvider.GetUtcNow();
await repository.RecordVerificationAsync(new WitnessVerificationRecord
{
WitnessId = witnessId,
VerifiedAt = DateTimeOffset.UtcNow,
VerifiedAt = now,
VerifiedBy = "api",
VerificationStatus = verificationStatus,
VerificationError = verificationError
@@ -176,7 +178,7 @@ internal static class WitnessEndpoints
WitnessHash = witness.WitnessHash,
Status = verificationStatus,
Error = verificationError,
VerifiedAt = DateTimeOffset.UtcNow,
VerifiedAt = now,
IsSigned = !string.IsNullOrEmpty(witness.DsseEnvelope)
});
}

View File

@@ -306,7 +306,7 @@ public sealed class EvidenceBundleExporter : IEvidenceBundleExporter
return sb.ToString();
}
private static async Task PrepareEvidenceFilesAsync(
private async Task PrepareEvidenceFilesAsync(
UnifiedEvidenceResponseDto evidence,
List<(string path, MemoryStream stream, string contentType)> streams,
List<ArchiveFileEntry> entries,
@@ -621,7 +621,7 @@ public sealed class EvidenceBundleExporter : IEvidenceBundleExporter
}
}
private static async Task CreateTarGzArchiveAsync(
private async Task CreateTarGzArchiveAsync(
string findingId,
List<(string path, MemoryStream stream, string contentType)> files,
Stream outputStream,
@@ -660,7 +660,7 @@ public sealed class EvidenceBundleExporter : IEvidenceBundleExporter
await gzipStream.WriteAsync(endBlocks, ct).ConfigureAwait(false);
}
private static byte[] CreateTarHeader(string name, long size)
private byte[] CreateTarHeader(string name, long size)
{
var header = new byte[512];

View File

@@ -230,7 +230,7 @@ public sealed class GatingReasonService : IGatingReasonService
/// <summary>
/// Computes a composite trust score for a VEX record.
/// </summary>
private static double ComputeVexTrustScore(TriageEffectiveVex vex)
private double ComputeVexTrustScore(TriageEffectiveVex vex)
{
// Weighted combination of trust factors
const double IssuerWeight = 0.4;

View File

@@ -29,6 +29,7 @@ public sealed class ReportSigner : IReportSigner
private readonly ILogger<ReportSigner> logger;
private readonly ICryptoProviderRegistry cryptoRegistry;
private readonly ICryptoHmac cryptoHmac;
private readonly TimeProvider timeProvider;
private readonly ICryptoProvider? provider;
private readonly CryptoKeyReference? keyReference;
private readonly CryptoSignerResolution? signerResolution;
@@ -38,11 +39,13 @@ public sealed class ReportSigner : IReportSigner
IOptions<ScannerWebServiceOptions> options,
ICryptoProviderRegistry cryptoRegistry,
ICryptoHmac cryptoHmac,
TimeProvider timeProvider,
ILogger<ReportSigner> logger)
{
ArgumentNullException.ThrowIfNull(options);
this.cryptoRegistry = cryptoRegistry ?? throw new ArgumentNullException(nameof(cryptoRegistry));
this.cryptoHmac = cryptoHmac ?? throw new ArgumentNullException(nameof(cryptoHmac));
this.timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider));
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
var value = options.Value ?? new ScannerWebServiceOptions();
@@ -79,7 +82,7 @@ public sealed class ReportSigner : IReportSigner
reference,
canonicalAlgorithm,
privateKey,
createdAt: DateTimeOffset.UtcNow);
createdAt: timeProvider.GetUtcNow());
provider.UpsertSigningKey(signingKeyDescriptor);

View File

@@ -23,6 +23,7 @@ public sealed class ScoreReplayService : IScoreReplayService
private readonly IProofBundleWriter _bundleWriter;
private readonly IScanManifestSigner _manifestSigner;
private readonly IScoringService _scoringService;
private readonly TimeProvider _timeProvider;
private readonly ILogger<ScoreReplayService> _logger;
public ScoreReplayService(
@@ -31,6 +32,7 @@ public sealed class ScoreReplayService : IScoreReplayService
IProofBundleWriter bundleWriter,
IScanManifestSigner manifestSigner,
IScoringService scoringService,
TimeProvider timeProvider,
ILogger<ScoreReplayService> logger)
{
_manifestRepository = manifestRepository ?? throw new ArgumentNullException(nameof(manifestRepository));
@@ -38,6 +40,7 @@ public sealed class ScoreReplayService : IScoreReplayService
_bundleWriter = bundleWriter ?? throw new ArgumentNullException(nameof(bundleWriter));
_manifestSigner = manifestSigner ?? throw new ArgumentNullException(nameof(manifestSigner));
_scoringService = scoringService ?? throw new ArgumentNullException(nameof(scoringService));
_timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
@@ -99,7 +102,7 @@ public sealed class ScoreReplayService : IScoreReplayService
RootHash: bundle.RootHash,
BundleUri: bundle.BundleUri,
ManifestHash: manifest.ComputeHash(),
ReplayedAt: DateTimeOffset.UtcNow,
ReplayedAt: _timeProvider.GetUtcNow(),
Deterministic: manifest.Deterministic);
}
finally
@@ -164,7 +167,7 @@ public sealed class ScoreReplayService : IScoreReplayService
ComputedRootHash: computedRootHash,
ManifestValid: manifestVerify.IsValid,
LedgerValid: ledgerValid,
VerifiedAt: DateTimeOffset.UtcNow,
VerifiedAt: _timeProvider.GetUtcNow(),
ErrorMessage: string.Join("; ", errors));
}

View File

@@ -40,6 +40,7 @@ public sealed class SliceQueryService : ISliceQueryService
private readonly SliceHasher _hasher;
private readonly IFileContentAddressableStore _cas;
private readonly IScanMetadataRepository _scanRepo;
private readonly TimeProvider _timeProvider;
private readonly SliceQueryServiceOptions _options;
private readonly ILogger<SliceQueryService> _logger;
@@ -51,6 +52,7 @@ public sealed class SliceQueryService : ISliceQueryService
SliceHasher hasher,
IFileContentAddressableStore cas,
IScanMetadataRepository scanRepo,
TimeProvider timeProvider,
IOptions<SliceQueryServiceOptions> options,
ILogger<SliceQueryService> logger)
{
@@ -61,6 +63,7 @@ public sealed class SliceQueryService : ISliceQueryService
_hasher = hasher ?? throw new ArgumentNullException(nameof(hasher));
_cas = cas ?? throw new ArgumentNullException(nameof(cas));
_scanRepo = scanRepo ?? throw new ArgumentNullException(nameof(scanRepo));
_timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider));
_options = options?.Value ?? new SliceQueryServiceOptions();
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
@@ -121,7 +124,7 @@ public sealed class SliceQueryService : ISliceQueryService
PathWitnesses = slice.Verdict.PathWitnesses.IsDefaultOrEmpty
? Array.Empty<string>()
: slice.Verdict.PathWitnesses.ToList(),
CachedAt = DateTimeOffset.UtcNow
CachedAt = _timeProvider.GetUtcNow()
};
await _cache.SetAsync(cacheKey, cacheEntry, TimeSpan.FromHours(1), cancellationToken).ConfigureAwait(false);
}

View File

@@ -64,6 +64,12 @@ public sealed class TestProofBundleRepository : StellaOps.Scanner.Storage.Reposi
{
private readonly ConcurrentDictionary<string, ProofBundleRow> _bundlesByRootHash = new(StringComparer.OrdinalIgnoreCase);
private readonly ConcurrentDictionary<Guid, List<ProofBundleRow>> _bundlesByScanId = new();
private readonly TimeProvider _timeProvider;
public TestProofBundleRepository(TimeProvider timeProvider)
{
_timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider));
}
public Task<ProofBundleRow?> GetByRootHashAsync(string rootHash, CancellationToken cancellationToken = default)
{
@@ -112,8 +118,8 @@ public sealed class TestProofBundleRepository : StellaOps.Scanner.Storage.Reposi
public Task<int> DeleteExpiredAsync(CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
var now = DateTimeOffset.UtcNow;
var now = _timeProvider.GetUtcNow();
var expired = _bundlesByRootHash.Values
.Where(b => b.ExpiresAt.HasValue && b.ExpiresAt.Value < now)
.ToList();

View File

@@ -22,6 +22,7 @@ public sealed class UnifiedEvidenceService : IUnifiedEvidenceService
private readonly TriageDbContext _dbContext;
private readonly IGatingReasonService _gatingService;
private readonly IReplayCommandService _replayService;
private readonly TimeProvider _timeProvider;
private readonly ILogger<UnifiedEvidenceService> _logger;
private const double DefaultPolicyTrustThreshold = 0.7;
@@ -30,11 +31,13 @@ public sealed class UnifiedEvidenceService : IUnifiedEvidenceService
TriageDbContext dbContext,
IGatingReasonService gatingService,
IReplayCommandService replayService,
TimeProvider timeProvider,
ILogger<UnifiedEvidenceService> logger)
{
_dbContext = dbContext ?? throw new ArgumentNullException(nameof(dbContext));
_gatingService = gatingService ?? throw new ArgumentNullException(nameof(gatingService));
_replayService = replayService ?? throw new ArgumentNullException(nameof(replayService));
_timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
@@ -106,7 +109,7 @@ public sealed class UnifiedEvidenceService : IUnifiedEvidenceService
ReplayCommand = replayResponse?.FullCommand?.Command,
ShortReplayCommand = replayResponse?.ShortCommand?.Command,
EvidenceBundleUrl = replayResponse?.Bundle?.DownloadUri,
GeneratedAt = DateTimeOffset.UtcNow,
GeneratedAt = _timeProvider.GetUtcNow(),
CacheKey = cacheKey
};
}
@@ -277,11 +280,11 @@ public sealed class UnifiedEvidenceService : IUnifiedEvidenceService
AttestationsVerified = hasAttestations,
EvidenceComplete = hasVex && hasReachability,
Issues = issues.Count > 0 ? issues : null,
VerifiedAt = DateTimeOffset.UtcNow
VerifiedAt = _timeProvider.GetUtcNow()
};
}
private static double ComputeVexTrustScore(TriageEffectiveVex vex)
private double ComputeVexTrustScore(TriageEffectiveVex vex)
{
const double IssuerWeight = 0.4;
const double RecencyWeight = 0.2;
@@ -289,7 +292,7 @@ public sealed class UnifiedEvidenceService : IUnifiedEvidenceService
const double EvidenceWeight = 0.2;
var issuerTrust = GetIssuerTrust(vex.Issuer);
var recencyTrust = GetRecencyTrust((DateTimeOffset?)vex.ValidFrom);
var recencyTrust = GetRecencyTrust((DateTimeOffset?)vex.ValidFrom, _timeProvider.GetUtcNow());
var justificationTrust = GetJustificationTrust(vex.PrunedSourcesJson);
var evidenceTrust = !string.IsNullOrEmpty(vex.DsseEnvelopeHash) ? 0.8 : 0.3;
@@ -309,10 +312,10 @@ public sealed class UnifiedEvidenceService : IUnifiedEvidenceService
_ => 0.5
};
private static double GetRecencyTrust(DateTimeOffset? timestamp)
private static double GetRecencyTrust(DateTimeOffset? timestamp, DateTimeOffset now)
{
if (timestamp is null) return 0.3;
var age = DateTimeOffset.UtcNow - timestamp.Value;
var age = now - timestamp.Value;
return age.TotalDays switch { <= 7 => 1.0, <= 30 => 0.9, <= 90 => 0.7, <= 365 => 0.5, _ => 0.3 };
}

View File

@@ -267,7 +267,7 @@ public sealed record SourceRunResponse
Status = run.Status,
StartedAt = run.StartedAt,
CompletedAt = run.CompletedAt,
DurationMs = run.DurationMs,
DurationMs = run.GetDurationMs(),
ItemsDiscovered = run.ItemsDiscovered,
ItemsScanned = run.ItemsScanned,
ItemsSucceeded = run.ItemsSucceeded,

View File

@@ -84,8 +84,9 @@ public sealed class SourceTriggerDispatcher : ISourceTriggerDispatcher
source.TenantId,
context.Trigger,
context.CorrelationId,
_timeProvider,
context.TriggerDetails);
failedRun.Fail(canTrigger.Error!);
failedRun.Fail(canTrigger.Error!, _timeProvider);
await _runRepository.CreateAsync(failedRun, ct);
return new TriggerDispatchResult
@@ -102,6 +103,7 @@ public sealed class SourceTriggerDispatcher : ISourceTriggerDispatcher
source.TenantId,
context.Trigger,
context.CorrelationId,
_timeProvider,
context.TriggerDetails);
await _runRepository.CreateAsync(run, ct);
@@ -112,7 +114,7 @@ public sealed class SourceTriggerDispatcher : ISourceTriggerDispatcher
var handler = GetHandler(source.SourceType);
if (handler == null)
{
run.Fail($"No handler registered for source type {source.SourceType}");
run.Fail($"No handler registered for source type {source.SourceType}", _timeProvider);
await _runRepository.UpdateAsync(run, ct);
return new TriggerDispatchResult
{
@@ -133,9 +135,9 @@ public sealed class SourceTriggerDispatcher : ISourceTriggerDispatcher
if (targets.Count == 0)
{
run.Complete();
run.Complete(_timeProvider);
await _runRepository.UpdateAsync(run, ct);
source.RecordSuccessfulRun(_timeProvider.GetUtcNow());
source.RecordSuccessfulRun(_timeProvider.GetUtcNow(), _timeProvider);
await _sourceRepository.UpdateAsync(source, ct);
return new TriggerDispatchResult
@@ -176,13 +178,13 @@ public sealed class SourceTriggerDispatcher : ISourceTriggerDispatcher
// 7. Complete or fail based on results
if (run.ItemsFailed == run.ItemsDiscovered)
{
run.Fail("All targets failed to queue");
source.RecordFailedRun(_timeProvider.GetUtcNow(), run.ErrorMessage!);
run.Fail("All targets failed to queue", _timeProvider);
source.RecordFailedRun(_timeProvider.GetUtcNow(), run.ErrorMessage!, _timeProvider);
}
else
{
run.Complete();
source.RecordSuccessfulRun(_timeProvider.GetUtcNow());
run.Complete(_timeProvider);
source.RecordSuccessfulRun(_timeProvider.GetUtcNow(), _timeProvider);
}
await _runRepository.UpdateAsync(run, ct);
@@ -199,10 +201,10 @@ public sealed class SourceTriggerDispatcher : ISourceTriggerDispatcher
{
_logger.LogError(ex, "Dispatch failed for source {SourceId}", sourceId);
run.Fail(ex.Message);
run.Fail(ex.Message, _timeProvider);
await _runRepository.UpdateAsync(run, ct);
source.RecordFailedRun(_timeProvider.GetUtcNow(), ex.Message);
source.RecordFailedRun(_timeProvider.GetUtcNow(), ex.Message, _timeProvider);
await _sourceRepository.UpdateAsync(source, ct);
return new TriggerDispatchResult
@@ -266,7 +268,7 @@ public sealed class SourceTriggerDispatcher : ISourceTriggerDispatcher
return _handlers.FirstOrDefault(h => h.SourceType == sourceType);
}
private static (bool Success, string? Error) CanTrigger(SbomSource source, TriggerContext context)
private (bool Success, string? Error) CanTrigger(SbomSource source, TriggerContext context)
{
if (source.Status == SbomSourceStatus.Disabled)
{
@@ -292,7 +294,7 @@ public sealed class SourceTriggerDispatcher : ISourceTriggerDispatcher
}
}
if (source.IsRateLimited())
if (source.IsRateLimited(_timeProvider))
{
return (false, "Source is rate limited");
}