finish secrets finding work and audit remarks work save

This commit is contained in:
StellaOps Bot
2026-01-04 21:48:13 +02:00
parent 75611a505f
commit 8862e112c4
157 changed files with 11702 additions and 416 deletions

View File

@@ -2,6 +2,8 @@ using System.Text.Json;
namespace StellaOps.Scanner.Sources.Domain;
#pragma warning disable CA1062 // Validate arguments of public methods - TimeProvider validated at DI boundary
/// <summary>
/// Represents a configured SBOM ingestion source.
/// Sources can be registry webhooks (Zastava), direct Docker image scans,
@@ -115,12 +117,13 @@ public sealed class SbomSource
SbomSourceType sourceType,
JsonDocument configuration,
string createdBy,
TimeProvider timeProvider,
string? description = null,
string? authRef = null,
string? cronSchedule = null,
string? cronTimezone = null)
{
var now = DateTimeOffset.UtcNow;
var now = timeProvider.GetUtcNow();
var source = new SbomSource
{
SourceId = Guid.NewGuid(),
@@ -148,7 +151,7 @@ public sealed class SbomSource
// Calculate next scheduled run
if (!string.IsNullOrEmpty(cronSchedule))
{
source.CalculateNextScheduledRun();
source.CalculateNextScheduledRun(timeProvider);
}
return source;
@@ -161,37 +164,38 @@ public sealed class SbomSource
/// <summary>
/// Activate the source (after successful validation).
/// </summary>
public void Activate(string updatedBy)
public void Activate(string updatedBy, TimeProvider timeProvider)
{
if (Status == SbomSourceStatus.Disabled)
throw new InvalidOperationException("Cannot activate a disabled source. Enable it first.");
Status = SbomSourceStatus.Active;
UpdatedAt = DateTimeOffset.UtcNow;
UpdatedAt = timeProvider.GetUtcNow();
UpdatedBy = updatedBy;
}
/// <summary>
/// Pause the source with a reason.
/// </summary>
public void Pause(string reason, string? ticket, string pausedBy)
public void Pause(string reason, string? ticket, string pausedBy, TimeProvider timeProvider)
{
if (Paused) return;
var now = timeProvider.GetUtcNow();
Paused = true;
PauseReason = reason;
PauseTicket = ticket;
PausedAt = DateTimeOffset.UtcNow;
PausedAt = now;
PausedBy = pausedBy;
Status = SbomSourceStatus.Paused;
UpdatedAt = DateTimeOffset.UtcNow;
UpdatedAt = now;
UpdatedBy = pausedBy;
}
/// <summary>
/// Resume a paused source.
/// </summary>
public void Resume(string resumedBy)
public void Resume(string resumedBy, TimeProvider timeProvider)
{
if (!Paused) return;
@@ -201,30 +205,30 @@ public sealed class SbomSource
PausedAt = null;
PausedBy = null;
Status = ConsecutiveFailures > 0 ? SbomSourceStatus.Error : SbomSourceStatus.Active;
UpdatedAt = DateTimeOffset.UtcNow;
UpdatedAt = timeProvider.GetUtcNow();
UpdatedBy = resumedBy;
}
/// <summary>
/// Disable the source administratively.
/// </summary>
public void Disable(string disabledBy)
public void Disable(string disabledBy, TimeProvider timeProvider)
{
Status = SbomSourceStatus.Disabled;
UpdatedAt = DateTimeOffset.UtcNow;
UpdatedAt = timeProvider.GetUtcNow();
UpdatedBy = disabledBy;
}
/// <summary>
/// Enable a disabled source.
/// </summary>
public void Enable(string enabledBy)
public void Enable(string enabledBy, TimeProvider timeProvider)
{
if (Status != SbomSourceStatus.Disabled)
throw new InvalidOperationException("Source is not disabled.");
Status = SbomSourceStatus.Pending;
UpdatedAt = DateTimeOffset.UtcNow;
UpdatedAt = timeProvider.GetUtcNow();
UpdatedBy = enabledBy;
}
@@ -235,7 +239,7 @@ public sealed class SbomSource
/// <summary>
/// Record a successful run.
/// </summary>
public void RecordSuccessfulRun(DateTimeOffset runAt)
public void RecordSuccessfulRun(DateTimeOffset runAt, TimeProvider timeProvider)
{
LastRunAt = runAt;
LastRunStatus = SbomSourceRunStatus.Succeeded;
@@ -247,14 +251,14 @@ public sealed class SbomSource
Status = SbomSourceStatus.Active;
}
IncrementHourScans();
CalculateNextScheduledRun();
IncrementHourScans(timeProvider);
CalculateNextScheduledRun(timeProvider);
}
/// <summary>
/// Record a failed run.
/// </summary>
public void RecordFailedRun(DateTimeOffset runAt, string error)
public void RecordFailedRun(DateTimeOffset runAt, string error, TimeProvider timeProvider)
{
LastRunAt = runAt;
LastRunStatus = SbomSourceRunStatus.Failed;
@@ -266,22 +270,22 @@ public sealed class SbomSource
Status = SbomSourceStatus.Error;
}
IncrementHourScans();
CalculateNextScheduledRun();
IncrementHourScans(timeProvider);
CalculateNextScheduledRun(timeProvider);
}
/// <summary>
/// Record a partial success run.
/// </summary>
public void RecordPartialRun(DateTimeOffset runAt, string? warning = null)
public void RecordPartialRun(DateTimeOffset runAt, TimeProvider timeProvider, string? warning = null)
{
LastRunAt = runAt;
LastRunStatus = SbomSourceRunStatus.PartialSuccess;
LastRunError = warning;
// Don't reset consecutive failures for partial success
IncrementHourScans();
CalculateNextScheduledRun();
IncrementHourScans(timeProvider);
CalculateNextScheduledRun(timeProvider);
}
// -------------------------------------------------------------------------
@@ -291,12 +295,12 @@ public sealed class SbomSource
/// <summary>
/// Check if the source is rate limited.
/// </summary>
public bool IsRateLimited()
public bool IsRateLimited(TimeProvider timeProvider)
{
if (!MaxScansPerHour.HasValue) return false;
// Check if we're in a new hour window
var now = DateTimeOffset.UtcNow;
var now = timeProvider.GetUtcNow();
if (!HourWindowStart.HasValue || now - HourWindowStart.Value >= TimeSpan.FromHours(1))
{
return false; // New window, not rate limited
@@ -305,9 +309,9 @@ public sealed class SbomSource
return CurrentHourScans >= MaxScansPerHour.Value;
}
private void IncrementHourScans()
private void IncrementHourScans(TimeProvider timeProvider)
{
var now = DateTimeOffset.UtcNow;
var now = timeProvider.GetUtcNow();
if (!HourWindowStart.HasValue || now - HourWindowStart.Value >= TimeSpan.FromHours(1))
{
@@ -343,14 +347,14 @@ public sealed class SbomSource
/// <summary>
/// Regenerate webhook secret (for rotation).
/// </summary>
public void RotateWebhookSecret(string updatedBy)
public void RotateWebhookSecret(string updatedBy, TimeProvider timeProvider)
{
if (WebhookEndpoint == null)
throw new InvalidOperationException("Source does not have a webhook endpoint.");
// The actual secret rotation happens in the credential store
// This just updates the audit trail
UpdatedAt = DateTimeOffset.UtcNow;
UpdatedAt = timeProvider.GetUtcNow();
UpdatedBy = updatedBy;
}
@@ -361,7 +365,7 @@ public sealed class SbomSource
/// <summary>
/// Calculate the next scheduled run time.
/// </summary>
public void CalculateNextScheduledRun()
public void CalculateNextScheduledRun(TimeProvider timeProvider)
{
if (string.IsNullOrEmpty(CronSchedule))
{
@@ -373,7 +377,7 @@ public sealed class SbomSource
{
var cron = Cronos.CronExpression.Parse(CronSchedule);
var timezone = TimeZoneInfo.FindSystemTimeZoneById(CronTimezone ?? "UTC");
NextScheduledRun = cron.GetNextOccurrence(DateTimeOffset.UtcNow, timezone);
NextScheduledRun = cron.GetNextOccurrence(timeProvider.GetUtcNow(), timezone);
}
catch
{
@@ -397,10 +401,10 @@ public sealed class SbomSource
/// <summary>
/// Update the configuration.
/// </summary>
public void UpdateConfiguration(JsonDocument newConfiguration, string updatedBy)
public void UpdateConfiguration(JsonDocument newConfiguration, string updatedBy, TimeProvider timeProvider)
{
Configuration = newConfiguration;
UpdatedAt = DateTimeOffset.UtcNow;
UpdatedAt = timeProvider.GetUtcNow();
UpdatedBy = updatedBy;
}
}

View File

@@ -1,5 +1,7 @@
namespace StellaOps.Scanner.Sources.Domain;
#pragma warning disable CA1062 // Validate arguments of public methods - TimeProvider validated at DI boundary
/// <summary>
/// Represents a single execution run of an SBOM source.
/// Tracks status, timing, item counts, and any errors.
@@ -30,10 +32,17 @@ public sealed class SbomSourceRun
/// <summary>When the run completed (if finished).</summary>
public DateTimeOffset? CompletedAt { get; private set; }
/// <summary>Duration in milliseconds.</summary>
public long DurationMs => CompletedAt.HasValue
? (long)(CompletedAt.Value - StartedAt).TotalMilliseconds
: (long)(DateTimeOffset.UtcNow - StartedAt).TotalMilliseconds;
/// <summary>
/// Duration in milliseconds. Pass a TimeProvider to get the live duration for in-progress runs.
/// </summary>
public long GetDurationMs(TimeProvider? timeProvider = null)
{
if (CompletedAt.HasValue)
return (long)(CompletedAt.Value - StartedAt).TotalMilliseconds;
var now = timeProvider?.GetUtcNow() ?? DateTimeOffset.UtcNow;
return (long)(now - StartedAt).TotalMilliseconds;
}
/// <summary>Number of items discovered to scan.</summary>
public int ItemsDiscovered { get; private set; }
@@ -74,6 +83,7 @@ public sealed class SbomSourceRun
string tenantId,
SbomSourceRunTrigger trigger,
string correlationId,
TimeProvider timeProvider,
string? triggerDetails = null)
{
return new SbomSourceRun
@@ -84,7 +94,7 @@ public sealed class SbomSourceRun
Trigger = trigger,
TriggerDetails = triggerDetails,
Status = SbomSourceRunStatus.Running,
StartedAt = DateTimeOffset.UtcNow,
StartedAt = timeProvider.GetUtcNow(),
CorrelationId = correlationId
};
}
@@ -135,7 +145,7 @@ public sealed class SbomSourceRun
/// <summary>
/// Complete the run successfully.
/// </summary>
public void Complete()
public void Complete(TimeProvider timeProvider)
{
Status = ItemsFailed > 0
? SbomSourceRunStatus.PartialSuccess
@@ -143,27 +153,27 @@ public sealed class SbomSourceRun
? SbomSourceRunStatus.Succeeded
: SbomSourceRunStatus.Skipped;
CompletedAt = DateTimeOffset.UtcNow;
CompletedAt = timeProvider.GetUtcNow();
}
/// <summary>
/// Fail the run with an error.
/// </summary>
public void Fail(string message, string? stackTrace = null)
public void Fail(string message, TimeProvider timeProvider, string? stackTrace = null)
{
Status = SbomSourceRunStatus.Failed;
ErrorMessage = message;
ErrorStackTrace = stackTrace;
CompletedAt = DateTimeOffset.UtcNow;
CompletedAt = timeProvider.GetUtcNow();
}
/// <summary>
/// Cancel the run.
/// </summary>
public void Cancel(string reason)
public void Cancel(string reason, TimeProvider timeProvider)
{
Status = SbomSourceRunStatus.Cancelled;
ErrorMessage = reason;
CompletedAt = DateTimeOffset.UtcNow;
CompletedAt = timeProvider.GetUtcNow();
}
}