Fix build and code structure improvements. New but essential UI functionality. CI improvements. Documentation improvements. AI module improvements.

This commit is contained in:
StellaOps Bot
2025-12-26 21:54:17 +02:00
parent 335ff7da16
commit c2b9cd8d1f
3717 changed files with 264714 additions and 48202 deletions

View File

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

View File

@@ -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
}

View File

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