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

@@ -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");
}