finish secrets finding work and audit remarks work save
This commit is contained in:
@@ -15,6 +15,7 @@ namespace StellaOps.Scanner.Sources.ConnectionTesters;
|
||||
public sealed class CliConnectionTester : ISourceTypeConnectionTester
|
||||
{
|
||||
private readonly ICredentialResolver _credentialResolver;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
private readonly ILogger<CliConnectionTester> _logger;
|
||||
|
||||
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||
@@ -27,10 +28,12 @@ public sealed class CliConnectionTester : ISourceTypeConnectionTester
|
||||
|
||||
public CliConnectionTester(
|
||||
ICredentialResolver credentialResolver,
|
||||
ILogger<CliConnectionTester> logger)
|
||||
ILogger<CliConnectionTester> logger,
|
||||
TimeProvider? timeProvider = null)
|
||||
{
|
||||
_credentialResolver = credentialResolver;
|
||||
_logger = logger;
|
||||
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||
}
|
||||
|
||||
public async Task<ConnectionTestResult> TestAsync(
|
||||
@@ -45,7 +48,7 @@ public sealed class CliConnectionTester : ISourceTypeConnectionTester
|
||||
{
|
||||
Success = false,
|
||||
Message = "Invalid configuration format",
|
||||
TestedAt = DateTimeOffset.UtcNow
|
||||
TestedAt = _timeProvider.GetUtcNow()
|
||||
};
|
||||
}
|
||||
|
||||
@@ -103,7 +106,7 @@ public sealed class CliConnectionTester : ISourceTypeConnectionTester
|
||||
{
|
||||
Success = false,
|
||||
Message = $"Configuration issues: {string.Join("; ", validationIssues)}",
|
||||
TestedAt = DateTimeOffset.UtcNow,
|
||||
TestedAt = _timeProvider.GetUtcNow(),
|
||||
Details = details
|
||||
};
|
||||
}
|
||||
@@ -112,7 +115,7 @@ public sealed class CliConnectionTester : ISourceTypeConnectionTester
|
||||
{
|
||||
Success = true,
|
||||
Message = "CLI source configuration is valid - ready to receive SBOMs",
|
||||
TestedAt = DateTimeOffset.UtcNow,
|
||||
TestedAt = _timeProvider.GetUtcNow(),
|
||||
Details = details
|
||||
};
|
||||
}
|
||||
|
||||
@@ -298,23 +298,23 @@ public sealed record ConnectionTestResult
|
||||
public required bool Success { get; init; }
|
||||
public string? Message { get; init; }
|
||||
public string? ErrorCode { get; init; }
|
||||
public DateTimeOffset TestedAt { get; init; } = DateTimeOffset.UtcNow;
|
||||
public required DateTimeOffset TestedAt { get; init; }
|
||||
public List<ConnectionTestCheck> Checks { get; init; } = [];
|
||||
public Dictionary<string, object>? Details { get; init; }
|
||||
|
||||
public static ConnectionTestResult Succeeded(string? message = null) => new()
|
||||
public static ConnectionTestResult Succeeded(TimeProvider timeProvider, string? message = null) => new()
|
||||
{
|
||||
Success = true,
|
||||
Message = message ?? "Connection successful",
|
||||
TestedAt = DateTimeOffset.UtcNow
|
||||
TestedAt = timeProvider.GetUtcNow()
|
||||
};
|
||||
|
||||
public static ConnectionTestResult Failed(string message, string? errorCode = null) => new()
|
||||
public static ConnectionTestResult Failed(TimeProvider timeProvider, string message, string? errorCode = null) => new()
|
||||
{
|
||||
Success = false,
|
||||
Message = message,
|
||||
ErrorCode = errorCode,
|
||||
TestedAt = DateTimeOffset.UtcNow
|
||||
TestedAt = timeProvider.GetUtcNow()
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,7 +106,7 @@ public sealed record WebhookPayloadInfo
|
||||
public string? Actor { get; init; }
|
||||
|
||||
/// <summary>Timestamp of the event.</summary>
|
||||
public DateTimeOffset Timestamp { get; init; } = DateTimeOffset.UtcNow;
|
||||
public required DateTimeOffset Timestamp { get; init; }
|
||||
|
||||
/// <summary>Additional metadata from the payload.</summary>
|
||||
public Dictionary<string, string> Metadata { get; init; } = [];
|
||||
|
||||
@@ -101,6 +101,7 @@ public sealed class SbomSourceService : ISbomSourceService
|
||||
request.SourceType,
|
||||
request.Configuration,
|
||||
createdBy,
|
||||
_timeProvider,
|
||||
request.Description,
|
||||
request.AuthRef,
|
||||
request.CronSchedule,
|
||||
@@ -158,7 +159,7 @@ public sealed class SbomSourceService : ISbomSourceService
|
||||
throw new ArgumentException($"Invalid configuration: {string.Join(", ", validationResult.Errors)}");
|
||||
}
|
||||
|
||||
source.UpdateConfiguration(request.Configuration, updatedBy);
|
||||
source.UpdateConfiguration(request.Configuration, updatedBy, _timeProvider);
|
||||
}
|
||||
|
||||
// Validate cron schedule if provided
|
||||
@@ -177,7 +178,7 @@ public sealed class SbomSourceService : ISbomSourceService
|
||||
}
|
||||
|
||||
source.CronSchedule = request.CronSchedule;
|
||||
source.CalculateNextScheduledRun();
|
||||
source.CalculateNextScheduledRun(_timeProvider);
|
||||
}
|
||||
|
||||
// Update simple fields via reflection (maintaining encapsulation)
|
||||
@@ -199,7 +200,7 @@ public sealed class SbomSourceService : ISbomSourceService
|
||||
if (request.CronTimezone != null)
|
||||
{
|
||||
source.CronTimezone = request.CronTimezone;
|
||||
source.CalculateNextScheduledRun();
|
||||
source.CalculateNextScheduledRun(_timeProvider);
|
||||
}
|
||||
|
||||
if (request.MaxScansPerHour.HasValue)
|
||||
@@ -265,6 +266,7 @@ public sealed class SbomSourceService : ISbomSourceService
|
||||
request.SourceType,
|
||||
request.Configuration,
|
||||
"__test__",
|
||||
_timeProvider,
|
||||
authRef: request.AuthRef);
|
||||
|
||||
return await _connectionTester.TestAsync(tempSource, request.TestCredentials, ct);
|
||||
@@ -280,7 +282,7 @@ public sealed class SbomSourceService : ISbomSourceService
|
||||
var source = await _sourceRepository.GetByIdAsync(tenantId, sourceId, ct)
|
||||
?? throw new KeyNotFoundException($"Source {sourceId} not found");
|
||||
|
||||
source.Pause(request.Reason, request.Ticket, pausedBy);
|
||||
source.Pause(request.Reason, request.Ticket, pausedBy, _timeProvider);
|
||||
await _sourceRepository.UpdateAsync(source, ct);
|
||||
|
||||
_logger.LogInformation(
|
||||
@@ -299,7 +301,7 @@ public sealed class SbomSourceService : ISbomSourceService
|
||||
var source = await _sourceRepository.GetByIdAsync(tenantId, sourceId, ct)
|
||||
?? throw new KeyNotFoundException($"Source {sourceId} not found");
|
||||
|
||||
source.Resume(resumedBy);
|
||||
source.Resume(resumedBy, _timeProvider);
|
||||
await _sourceRepository.UpdateAsync(source, ct);
|
||||
|
||||
_logger.LogInformation(
|
||||
@@ -330,7 +332,7 @@ public sealed class SbomSourceService : ISbomSourceService
|
||||
throw new InvalidOperationException($"Source is paused: {source.PauseReason}");
|
||||
}
|
||||
|
||||
if (source.IsRateLimited() && request?.Force != true)
|
||||
if (source.IsRateLimited(_timeProvider) && request?.Force != true)
|
||||
{
|
||||
throw new InvalidOperationException("Source is rate limited. Use force=true to override.");
|
||||
}
|
||||
@@ -341,6 +343,7 @@ public sealed class SbomSourceService : ISbomSourceService
|
||||
tenantId,
|
||||
SbomSourceRunTrigger.Manual,
|
||||
Guid.NewGuid().ToString("N"),
|
||||
_timeProvider,
|
||||
$"Triggered by {triggeredBy}");
|
||||
|
||||
await _runRepository.CreateAsync(run, ct);
|
||||
@@ -407,7 +410,7 @@ public sealed class SbomSourceService : ISbomSourceService
|
||||
var source = await _sourceRepository.GetByIdAsync(tenantId, sourceId, ct)
|
||||
?? throw new KeyNotFoundException($"Source {sourceId} not found");
|
||||
|
||||
source.Activate(activatedBy);
|
||||
source.Activate(activatedBy, _timeProvider);
|
||||
await _sourceRepository.UpdateAsync(source, ct);
|
||||
|
||||
_logger.LogInformation(
|
||||
|
||||
Reference in New Issue
Block a user