Fix build and code structure improvements. New but essential UI functionality. CI improvements. Documentation improvements. AI module improvements.
This commit is contained in:
@@ -0,0 +1,406 @@
|
||||
using System.Text.Json;
|
||||
|
||||
namespace StellaOps.Scanner.Sources.Domain;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a configured SBOM ingestion source.
|
||||
/// Sources can be registry webhooks (Zastava), direct Docker image scans,
|
||||
/// CLI submissions, or Git repository scans.
|
||||
/// </summary>
|
||||
public sealed class SbomSource
|
||||
{
|
||||
/// <summary>Unique source identifier.</summary>
|
||||
public Guid SourceId { get; init; }
|
||||
|
||||
/// <summary>Tenant owning this source.</summary>
|
||||
public string TenantId { get; init; } = null!;
|
||||
|
||||
/// <summary>Human-readable source name.</summary>
|
||||
public string Name { get; init; } = null!;
|
||||
|
||||
/// <summary>Optional description.</summary>
|
||||
public string? Description { get; set; }
|
||||
|
||||
/// <summary>Type of source (Zastava, Docker, CLI, Git).</summary>
|
||||
public SbomSourceType SourceType { get; init; }
|
||||
|
||||
/// <summary>Current status of the source.</summary>
|
||||
public SbomSourceStatus Status { get; private set; } = SbomSourceStatus.Pending;
|
||||
|
||||
/// <summary>Type-specific configuration (JSON).</summary>
|
||||
public JsonDocument Configuration { get; set; } = null!;
|
||||
|
||||
/// <summary>Reference to credentials in vault (never the actual secret).</summary>
|
||||
public string? AuthRef { get; set; }
|
||||
|
||||
/// <summary>Generated webhook endpoint for webhook-based sources.</summary>
|
||||
public string? WebhookEndpoint { get; private set; }
|
||||
|
||||
/// <summary>Reference to webhook secret in vault.</summary>
|
||||
public string? WebhookSecretRef { get; private set; }
|
||||
|
||||
/// <summary>Cron schedule expression for scheduled sources.</summary>
|
||||
public string? CronSchedule { get; set; }
|
||||
|
||||
/// <summary>Timezone for cron schedule (default: UTC).</summary>
|
||||
public string? CronTimezone { get; set; }
|
||||
|
||||
/// <summary>Next scheduled run time.</summary>
|
||||
public DateTimeOffset? NextScheduledRun { get; private set; }
|
||||
|
||||
/// <summary>When the source last ran.</summary>
|
||||
public DateTimeOffset? LastRunAt { get; private set; }
|
||||
|
||||
/// <summary>Status of the last run.</summary>
|
||||
public SbomSourceRunStatus? LastRunStatus { get; private set; }
|
||||
|
||||
/// <summary>Error message from last run (if failed).</summary>
|
||||
public string? LastRunError { get; private set; }
|
||||
|
||||
/// <summary>Number of consecutive failures.</summary>
|
||||
public int ConsecutiveFailures { get; private set; }
|
||||
|
||||
/// <summary>Whether the source is paused.</summary>
|
||||
public bool Paused { get; private set; }
|
||||
|
||||
/// <summary>Reason for pause (operator-provided).</summary>
|
||||
public string? PauseReason { get; private set; }
|
||||
|
||||
/// <summary>Ticket reference for pause audit.</summary>
|
||||
public string? PauseTicket { get; private set; }
|
||||
|
||||
/// <summary>When the source was paused.</summary>
|
||||
public DateTimeOffset? PausedAt { get; private set; }
|
||||
|
||||
/// <summary>Who paused the source.</summary>
|
||||
public string? PausedBy { get; private set; }
|
||||
|
||||
/// <summary>Maximum scans per hour (rate limiting).</summary>
|
||||
public int? MaxScansPerHour { get; set; }
|
||||
|
||||
/// <summary>Current scans in the hour window.</summary>
|
||||
public int CurrentHourScans { get; private set; }
|
||||
|
||||
/// <summary>Start of the current hour window.</summary>
|
||||
public DateTimeOffset? HourWindowStart { get; private set; }
|
||||
|
||||
/// <summary>When the source was created.</summary>
|
||||
public DateTimeOffset CreatedAt { get; init; }
|
||||
|
||||
/// <summary>Who created the source.</summary>
|
||||
public string CreatedBy { get; init; } = null!;
|
||||
|
||||
/// <summary>When the source was last updated.</summary>
|
||||
public DateTimeOffset UpdatedAt { get; private set; }
|
||||
|
||||
/// <summary>Who last updated the source.</summary>
|
||||
public string UpdatedBy { get; private set; } = null!;
|
||||
|
||||
/// <summary>Tags for organization.</summary>
|
||||
public List<string> Tags { get; set; } = [];
|
||||
|
||||
/// <summary>Custom metadata key-value pairs.</summary>
|
||||
public Dictionary<string, string> Metadata { get; set; } = [];
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Factory Methods
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// Create a new SBOM source.
|
||||
/// </summary>
|
||||
public static SbomSource Create(
|
||||
string tenantId,
|
||||
string name,
|
||||
SbomSourceType sourceType,
|
||||
JsonDocument configuration,
|
||||
string createdBy,
|
||||
string? description = null,
|
||||
string? authRef = null,
|
||||
string? cronSchedule = null,
|
||||
string? cronTimezone = null)
|
||||
{
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
var source = new SbomSource
|
||||
{
|
||||
SourceId = Guid.NewGuid(),
|
||||
TenantId = tenantId,
|
||||
Name = name,
|
||||
Description = description,
|
||||
SourceType = sourceType,
|
||||
Status = SbomSourceStatus.Pending,
|
||||
Configuration = configuration,
|
||||
AuthRef = authRef,
|
||||
CronSchedule = cronSchedule,
|
||||
CronTimezone = cronTimezone ?? "UTC",
|
||||
CreatedAt = now,
|
||||
CreatedBy = createdBy,
|
||||
UpdatedAt = now,
|
||||
UpdatedBy = createdBy
|
||||
};
|
||||
|
||||
// Generate webhook endpoint for webhook-based sources
|
||||
if (sourceType == SbomSourceType.Zastava || sourceType == SbomSourceType.Git)
|
||||
{
|
||||
source.GenerateWebhookEndpoint();
|
||||
}
|
||||
|
||||
// Calculate next scheduled run
|
||||
if (!string.IsNullOrEmpty(cronSchedule))
|
||||
{
|
||||
source.CalculateNextScheduledRun();
|
||||
}
|
||||
|
||||
return source;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// State Transitions
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// Activate the source (after successful validation).
|
||||
/// </summary>
|
||||
public void Activate(string updatedBy)
|
||||
{
|
||||
if (Status == SbomSourceStatus.Disabled)
|
||||
throw new InvalidOperationException("Cannot activate a disabled source. Enable it first.");
|
||||
|
||||
Status = SbomSourceStatus.Active;
|
||||
UpdatedAt = DateTimeOffset.UtcNow;
|
||||
UpdatedBy = updatedBy;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Pause the source with a reason.
|
||||
/// </summary>
|
||||
public void Pause(string reason, string? ticket, string pausedBy)
|
||||
{
|
||||
if (Paused) return;
|
||||
|
||||
Paused = true;
|
||||
PauseReason = reason;
|
||||
PauseTicket = ticket;
|
||||
PausedAt = DateTimeOffset.UtcNow;
|
||||
PausedBy = pausedBy;
|
||||
Status = SbomSourceStatus.Paused;
|
||||
UpdatedAt = DateTimeOffset.UtcNow;
|
||||
UpdatedBy = pausedBy;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resume a paused source.
|
||||
/// </summary>
|
||||
public void Resume(string resumedBy)
|
||||
{
|
||||
if (!Paused) return;
|
||||
|
||||
Paused = false;
|
||||
PauseReason = null;
|
||||
PauseTicket = null;
|
||||
PausedAt = null;
|
||||
PausedBy = null;
|
||||
Status = ConsecutiveFailures > 0 ? SbomSourceStatus.Error : SbomSourceStatus.Active;
|
||||
UpdatedAt = DateTimeOffset.UtcNow;
|
||||
UpdatedBy = resumedBy;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Disable the source administratively.
|
||||
/// </summary>
|
||||
public void Disable(string disabledBy)
|
||||
{
|
||||
Status = SbomSourceStatus.Disabled;
|
||||
UpdatedAt = DateTimeOffset.UtcNow;
|
||||
UpdatedBy = disabledBy;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enable a disabled source.
|
||||
/// </summary>
|
||||
public void Enable(string enabledBy)
|
||||
{
|
||||
if (Status != SbomSourceStatus.Disabled)
|
||||
throw new InvalidOperationException("Source is not disabled.");
|
||||
|
||||
Status = SbomSourceStatus.Pending;
|
||||
UpdatedAt = DateTimeOffset.UtcNow;
|
||||
UpdatedBy = enabledBy;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Run Tracking
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// Record a successful run.
|
||||
/// </summary>
|
||||
public void RecordSuccessfulRun(DateTimeOffset runAt)
|
||||
{
|
||||
LastRunAt = runAt;
|
||||
LastRunStatus = SbomSourceRunStatus.Succeeded;
|
||||
LastRunError = null;
|
||||
ConsecutiveFailures = 0;
|
||||
|
||||
if (Status == SbomSourceStatus.Error)
|
||||
{
|
||||
Status = SbomSourceStatus.Active;
|
||||
}
|
||||
|
||||
IncrementHourScans();
|
||||
CalculateNextScheduledRun();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Record a failed run.
|
||||
/// </summary>
|
||||
public void RecordFailedRun(DateTimeOffset runAt, string error)
|
||||
{
|
||||
LastRunAt = runAt;
|
||||
LastRunStatus = SbomSourceRunStatus.Failed;
|
||||
LastRunError = error;
|
||||
ConsecutiveFailures++;
|
||||
|
||||
if (!Paused)
|
||||
{
|
||||
Status = SbomSourceStatus.Error;
|
||||
}
|
||||
|
||||
IncrementHourScans();
|
||||
CalculateNextScheduledRun();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Record a partial success run.
|
||||
/// </summary>
|
||||
public void RecordPartialRun(DateTimeOffset runAt, string? warning = null)
|
||||
{
|
||||
LastRunAt = runAt;
|
||||
LastRunStatus = SbomSourceRunStatus.PartialSuccess;
|
||||
LastRunError = warning;
|
||||
// Don't reset consecutive failures for partial success
|
||||
|
||||
IncrementHourScans();
|
||||
CalculateNextScheduledRun();
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Rate Limiting
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// Check if the source is rate limited.
|
||||
/// </summary>
|
||||
public bool IsRateLimited()
|
||||
{
|
||||
if (!MaxScansPerHour.HasValue) return false;
|
||||
|
||||
// Check if we're in a new hour window
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
if (!HourWindowStart.HasValue || now - HourWindowStart.Value >= TimeSpan.FromHours(1))
|
||||
{
|
||||
return false; // New window, not rate limited
|
||||
}
|
||||
|
||||
return CurrentHourScans >= MaxScansPerHour.Value;
|
||||
}
|
||||
|
||||
private void IncrementHourScans()
|
||||
{
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
|
||||
if (!HourWindowStart.HasValue || now - HourWindowStart.Value >= TimeSpan.FromHours(1))
|
||||
{
|
||||
HourWindowStart = now;
|
||||
CurrentHourScans = 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
CurrentHourScans++;
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Webhook Management
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// Generate a new webhook endpoint.
|
||||
/// </summary>
|
||||
public void GenerateWebhookEndpoint()
|
||||
{
|
||||
var typePrefix = SourceType switch
|
||||
{
|
||||
SbomSourceType.Zastava => "zastava",
|
||||
SbomSourceType.Git => "git",
|
||||
_ => throw new InvalidOperationException($"Source type {SourceType} does not support webhooks")
|
||||
};
|
||||
|
||||
WebhookEndpoint = $"/api/v1/webhooks/{typePrefix}/{SourceId}";
|
||||
WebhookSecretRef = $"webhook.{SourceId}.secret";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Regenerate webhook secret (for rotation).
|
||||
/// </summary>
|
||||
public void RotateWebhookSecret(string updatedBy)
|
||||
{
|
||||
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;
|
||||
UpdatedBy = updatedBy;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Scheduling
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// Calculate the next scheduled run time.
|
||||
/// </summary>
|
||||
public void CalculateNextScheduledRun()
|
||||
{
|
||||
if (string.IsNullOrEmpty(CronSchedule))
|
||||
{
|
||||
NextScheduledRun = null;
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var cron = Cronos.CronExpression.Parse(CronSchedule);
|
||||
var timezone = TimeZoneInfo.FindSystemTimeZoneById(CronTimezone ?? "UTC");
|
||||
NextScheduledRun = cron.GetNextOccurrence(DateTimeOffset.UtcNow, timezone);
|
||||
}
|
||||
catch
|
||||
{
|
||||
NextScheduledRun = null;
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Configuration Access
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// Get the typed configuration.
|
||||
/// </summary>
|
||||
public T GetConfiguration<T>() where T : class
|
||||
{
|
||||
return Configuration.Deserialize<T>()
|
||||
?? throw new InvalidOperationException($"Failed to deserialize configuration as {typeof(T).Name}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update the configuration.
|
||||
/// </summary>
|
||||
public void UpdateConfiguration(JsonDocument newConfiguration, string updatedBy)
|
||||
{
|
||||
Configuration = newConfiguration;
|
||||
UpdatedAt = DateTimeOffset.UtcNow;
|
||||
UpdatedBy = updatedBy;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
namespace StellaOps.Scanner.Sources.Domain;
|
||||
|
||||
/// <summary>
|
||||
/// Type of SBOM ingestion source.
|
||||
/// </summary>
|
||||
public enum SbomSourceType
|
||||
{
|
||||
/// <summary>Registry webhook source (receives push events from container registries).</summary>
|
||||
Zastava = 0,
|
||||
|
||||
/// <summary>Direct Docker image scanning (scheduled or on-demand).</summary>
|
||||
Docker = 1,
|
||||
|
||||
/// <summary>External CLI submissions (receives SBOMs from CI/CD pipelines).</summary>
|
||||
Cli = 2,
|
||||
|
||||
/// <summary>Git repository source scanning.</summary>
|
||||
Git = 3
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Status of an SBOM source.
|
||||
/// </summary>
|
||||
public enum SbomSourceStatus
|
||||
{
|
||||
/// <summary>Source is pending initial validation/test.</summary>
|
||||
Pending = 0,
|
||||
|
||||
/// <summary>Source is active and processing events.</summary>
|
||||
Active = 1,
|
||||
|
||||
/// <summary>Source is manually paused by operator.</summary>
|
||||
Paused = 2,
|
||||
|
||||
/// <summary>Source encountered an error (last run failed).</summary>
|
||||
Error = 3,
|
||||
|
||||
/// <summary>Source is administratively disabled.</summary>
|
||||
Disabled = 4
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Status of an individual source run.
|
||||
/// </summary>
|
||||
public enum SbomSourceRunStatus
|
||||
{
|
||||
/// <summary>Run is in progress.</summary>
|
||||
Running = 0,
|
||||
|
||||
/// <summary>Run completed successfully.</summary>
|
||||
Succeeded = 1,
|
||||
|
||||
/// <summary>Run failed.</summary>
|
||||
Failed = 2,
|
||||
|
||||
/// <summary>Run partially succeeded (some items failed).</summary>
|
||||
PartialSuccess = 3,
|
||||
|
||||
/// <summary>Run was skipped (no matching items).</summary>
|
||||
Skipped = 4,
|
||||
|
||||
/// <summary>Run was cancelled.</summary>
|
||||
Cancelled = 5
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Trigger type for a source run.
|
||||
/// </summary>
|
||||
public enum SbomSourceRunTrigger
|
||||
{
|
||||
/// <summary>Scheduled trigger (cron-based).</summary>
|
||||
Scheduled = 0,
|
||||
|
||||
/// <summary>Webhook trigger (registry push, git push).</summary>
|
||||
Webhook = 1,
|
||||
|
||||
/// <summary>Manual trigger (user-initiated).</summary>
|
||||
Manual = 2,
|
||||
|
||||
/// <summary>Backfill trigger (historical scan).</summary>
|
||||
Backfill = 3,
|
||||
|
||||
/// <summary>Retry trigger (retry of failed run).</summary>
|
||||
Retry = 4
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
namespace StellaOps.Scanner.Sources.Domain;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a single execution run of an SBOM source.
|
||||
/// Tracks status, timing, item counts, and any errors.
|
||||
/// </summary>
|
||||
public sealed class SbomSourceRun
|
||||
{
|
||||
/// <summary>Unique run identifier.</summary>
|
||||
public Guid RunId { get; init; }
|
||||
|
||||
/// <summary>Source that was run.</summary>
|
||||
public Guid SourceId { get; init; }
|
||||
|
||||
/// <summary>Tenant owning the source.</summary>
|
||||
public string TenantId { get; init; } = null!;
|
||||
|
||||
/// <summary>What triggered this run.</summary>
|
||||
public SbomSourceRunTrigger Trigger { get; init; }
|
||||
|
||||
/// <summary>Additional trigger details (webhook payload digest, cron expression, etc.).</summary>
|
||||
public string? TriggerDetails { get; init; }
|
||||
|
||||
/// <summary>Current status of the run.</summary>
|
||||
public SbomSourceRunStatus Status { get; private set; } = SbomSourceRunStatus.Running;
|
||||
|
||||
/// <summary>When the run started.</summary>
|
||||
public DateTimeOffset StartedAt { get; init; }
|
||||
|
||||
/// <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>Number of items discovered to scan.</summary>
|
||||
public int ItemsDiscovered { get; private set; }
|
||||
|
||||
/// <summary>Number of items that were scanned.</summary>
|
||||
public int ItemsScanned { get; private set; }
|
||||
|
||||
/// <summary>Number of items that succeeded.</summary>
|
||||
public int ItemsSucceeded { get; private set; }
|
||||
|
||||
/// <summary>Number of items that failed.</summary>
|
||||
public int ItemsFailed { get; private set; }
|
||||
|
||||
/// <summary>Number of items that were skipped.</summary>
|
||||
public int ItemsSkipped { get; private set; }
|
||||
|
||||
/// <summary>IDs of scan jobs created by this run.</summary>
|
||||
public List<Guid> ScanJobIds { get; init; } = [];
|
||||
|
||||
/// <summary>Error message if failed.</summary>
|
||||
public string? ErrorMessage { get; private set; }
|
||||
|
||||
/// <summary>Error stack trace if failed.</summary>
|
||||
public string? ErrorStackTrace { get; private set; }
|
||||
|
||||
/// <summary>Correlation ID for distributed tracing.</summary>
|
||||
public string CorrelationId { get; init; } = null!;
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Factory Methods
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// Create a new source run.
|
||||
/// </summary>
|
||||
public static SbomSourceRun Create(
|
||||
Guid sourceId,
|
||||
string tenantId,
|
||||
SbomSourceRunTrigger trigger,
|
||||
string correlationId,
|
||||
string? triggerDetails = null)
|
||||
{
|
||||
return new SbomSourceRun
|
||||
{
|
||||
RunId = Guid.NewGuid(),
|
||||
SourceId = sourceId,
|
||||
TenantId = tenantId,
|
||||
Trigger = trigger,
|
||||
TriggerDetails = triggerDetails,
|
||||
Status = SbomSourceRunStatus.Running,
|
||||
StartedAt = DateTimeOffset.UtcNow,
|
||||
CorrelationId = correlationId
|
||||
};
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Progress Updates
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// Set the number of discovered items.
|
||||
/// </summary>
|
||||
public void SetDiscoveredItems(int count)
|
||||
{
|
||||
ItemsDiscovered = count;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Record a successfully scanned item.
|
||||
/// </summary>
|
||||
public void RecordItemSuccess(Guid scanJobId)
|
||||
{
|
||||
ItemsScanned++;
|
||||
ItemsSucceeded++;
|
||||
ScanJobIds.Add(scanJobId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Record a failed item.
|
||||
/// </summary>
|
||||
public void RecordItemFailure()
|
||||
{
|
||||
ItemsScanned++;
|
||||
ItemsFailed++;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Record a skipped item.
|
||||
/// </summary>
|
||||
public void RecordItemSkipped()
|
||||
{
|
||||
ItemsSkipped++;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Completion
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// Complete the run successfully.
|
||||
/// </summary>
|
||||
public void Complete()
|
||||
{
|
||||
Status = ItemsFailed > 0
|
||||
? SbomSourceRunStatus.PartialSuccess
|
||||
: ItemsSucceeded > 0
|
||||
? SbomSourceRunStatus.Succeeded
|
||||
: SbomSourceRunStatus.Skipped;
|
||||
|
||||
CompletedAt = DateTimeOffset.UtcNow;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fail the run with an error.
|
||||
/// </summary>
|
||||
public void Fail(string message, string? stackTrace = null)
|
||||
{
|
||||
Status = SbomSourceRunStatus.Failed;
|
||||
ErrorMessage = message;
|
||||
ErrorStackTrace = stackTrace;
|
||||
CompletedAt = DateTimeOffset.UtcNow;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cancel the run.
|
||||
/// </summary>
|
||||
public void Cancel(string reason)
|
||||
{
|
||||
Status = SbomSourceRunStatus.Cancelled;
|
||||
ErrorMessage = reason;
|
||||
CompletedAt = DateTimeOffset.UtcNow;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user