Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
- Implement `SbomIngestServiceCollectionExtensionsTests` to verify the SBOM ingestion pipeline exports snapshots correctly. - Create `SbomIngestTransformerTests` to ensure the transformation produces expected nodes and edges, including deduplication of license nodes and normalization of timestamps. - Add `SbomSnapshotExporterTests` to test the export functionality for manifest, adjacency, nodes, and edges. - Introduce `VexOverlayTransformerTests` to validate the transformation of VEX nodes and edges. - Set up project file for the test project with necessary dependencies and configurations. - Include JSON fixture files for testing purposes.
386 lines
13 KiB
C#
386 lines
13 KiB
C#
using System.Collections.Immutable;
|
|
using System.Text.Json.Serialization;
|
|
|
|
namespace StellaOps.Scheduler.Models;
|
|
|
|
/// <summary>
|
|
/// Execution record for a scheduler run.
|
|
/// </summary>
|
|
public sealed record Run
|
|
{
|
|
public Run(
|
|
string id,
|
|
string tenantId,
|
|
RunTrigger trigger,
|
|
RunState state,
|
|
RunStats stats,
|
|
DateTimeOffset createdAt,
|
|
RunReason? reason = null,
|
|
string? scheduleId = null,
|
|
DateTimeOffset? startedAt = null,
|
|
DateTimeOffset? finishedAt = null,
|
|
string? error = null,
|
|
IEnumerable<DeltaSummary>? deltas = null,
|
|
string? retryOf = null,
|
|
string? schemaVersion = null)
|
|
: this(
|
|
id,
|
|
tenantId,
|
|
trigger,
|
|
state,
|
|
stats,
|
|
reason ?? RunReason.Empty,
|
|
scheduleId,
|
|
Validation.NormalizeTimestamp(createdAt),
|
|
Validation.NormalizeTimestamp(startedAt),
|
|
Validation.NormalizeTimestamp(finishedAt),
|
|
Validation.TrimToNull(error),
|
|
NormalizeDeltas(deltas),
|
|
Validation.TrimToNull(retryOf),
|
|
schemaVersion)
|
|
{
|
|
}
|
|
|
|
[JsonConstructor]
|
|
public Run(
|
|
string id,
|
|
string tenantId,
|
|
RunTrigger trigger,
|
|
RunState state,
|
|
RunStats stats,
|
|
RunReason reason,
|
|
string? scheduleId,
|
|
DateTimeOffset createdAt,
|
|
DateTimeOffset? startedAt,
|
|
DateTimeOffset? finishedAt,
|
|
string? error,
|
|
ImmutableArray<DeltaSummary> deltas,
|
|
string? retryOf,
|
|
string? schemaVersion = null)
|
|
{
|
|
Id = Validation.EnsureId(id, nameof(id));
|
|
TenantId = Validation.EnsureTenantId(tenantId, nameof(tenantId));
|
|
Trigger = trigger;
|
|
State = state;
|
|
Stats = stats ?? throw new ArgumentNullException(nameof(stats));
|
|
Reason = reason ?? RunReason.Empty;
|
|
ScheduleId = Validation.TrimToNull(scheduleId);
|
|
CreatedAt = Validation.NormalizeTimestamp(createdAt);
|
|
StartedAt = Validation.NormalizeTimestamp(startedAt);
|
|
FinishedAt = Validation.NormalizeTimestamp(finishedAt);
|
|
Error = Validation.TrimToNull(error);
|
|
Deltas = deltas.IsDefault
|
|
? ImmutableArray<DeltaSummary>.Empty
|
|
: deltas.OrderBy(static delta => delta.ImageDigest, StringComparer.Ordinal).ToImmutableArray();
|
|
RetryOf = Validation.TrimToNull(retryOf);
|
|
SchemaVersion = SchedulerSchemaVersions.EnsureRun(schemaVersion);
|
|
}
|
|
|
|
public string SchemaVersion { get; }
|
|
|
|
public string Id { get; }
|
|
|
|
public string TenantId { get; }
|
|
|
|
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
|
public string? ScheduleId { get; }
|
|
|
|
public RunTrigger Trigger { get; }
|
|
|
|
public RunState State { get; init; }
|
|
|
|
public RunStats Stats { get; init; }
|
|
|
|
public RunReason Reason { get; }
|
|
|
|
public DateTimeOffset CreatedAt { get; }
|
|
|
|
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
|
public DateTimeOffset? StartedAt { get; init; }
|
|
|
|
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
|
public DateTimeOffset? FinishedAt { get; init; }
|
|
|
|
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
|
public string? Error { get; init; }
|
|
|
|
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
|
|
public ImmutableArray<DeltaSummary> Deltas { get; } = ImmutableArray<DeltaSummary>.Empty;
|
|
|
|
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
|
public string? RetryOf { get; }
|
|
|
|
private static ImmutableArray<DeltaSummary> NormalizeDeltas(IEnumerable<DeltaSummary>? deltas)
|
|
{
|
|
if (deltas is null)
|
|
{
|
|
return ImmutableArray<DeltaSummary>.Empty;
|
|
}
|
|
|
|
return deltas
|
|
.Where(static delta => delta is not null)
|
|
.Select(static delta => delta!)
|
|
.OrderBy(static delta => delta.ImageDigest, StringComparer.Ordinal)
|
|
.ToImmutableArray();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Context describing why a run executed.
|
|
/// </summary>
|
|
public sealed record RunReason
|
|
{
|
|
public static RunReason Empty { get; } = new();
|
|
|
|
public RunReason(
|
|
string? manualReason = null,
|
|
string? conselierExportId = null,
|
|
string? excitorExportId = null,
|
|
string? cursor = null)
|
|
{
|
|
ManualReason = Validation.TrimToNull(manualReason);
|
|
ConselierExportId = Validation.TrimToNull(conselierExportId);
|
|
ExcitorExportId = Validation.TrimToNull(excitorExportId);
|
|
Cursor = Validation.TrimToNull(cursor);
|
|
}
|
|
|
|
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
|
public string? ManualReason { get; } = null;
|
|
|
|
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
|
public string? ConselierExportId { get; } = null;
|
|
|
|
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
|
public string? ExcitorExportId { get; } = null;
|
|
|
|
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
|
public string? Cursor { get; } = null;
|
|
|
|
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
|
public string? ImpactWindowFrom { get; init; }
|
|
|
|
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
|
public string? ImpactWindowTo { get; init; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Aggregated counters for a scheduler run.
|
|
/// </summary>
|
|
public sealed record RunStats
|
|
{
|
|
public static RunStats Empty { get; } = new();
|
|
|
|
public RunStats(
|
|
int candidates = 0,
|
|
int deduped = 0,
|
|
int queued = 0,
|
|
int completed = 0,
|
|
int deltas = 0,
|
|
int newCriticals = 0,
|
|
int newHigh = 0,
|
|
int newMedium = 0,
|
|
int newLow = 0)
|
|
{
|
|
Candidates = Validation.EnsureNonNegative(candidates, nameof(candidates));
|
|
Deduped = Validation.EnsureNonNegative(deduped, nameof(deduped));
|
|
Queued = Validation.EnsureNonNegative(queued, nameof(queued));
|
|
Completed = Validation.EnsureNonNegative(completed, nameof(completed));
|
|
Deltas = Validation.EnsureNonNegative(deltas, nameof(deltas));
|
|
NewCriticals = Validation.EnsureNonNegative(newCriticals, nameof(newCriticals));
|
|
NewHigh = Validation.EnsureNonNegative(newHigh, nameof(newHigh));
|
|
NewMedium = Validation.EnsureNonNegative(newMedium, nameof(newMedium));
|
|
NewLow = Validation.EnsureNonNegative(newLow, nameof(newLow));
|
|
}
|
|
|
|
public int Candidates { get; } = 0;
|
|
|
|
public int Deduped { get; } = 0;
|
|
|
|
public int Queued { get; } = 0;
|
|
|
|
public int Completed { get; } = 0;
|
|
|
|
public int Deltas { get; } = 0;
|
|
|
|
public int NewCriticals { get; } = 0;
|
|
|
|
public int NewHigh { get; } = 0;
|
|
|
|
public int NewMedium { get; } = 0;
|
|
|
|
public int NewLow { get; } = 0;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Snapshot of delta impact for an image processed in a run.
|
|
/// </summary>
|
|
public sealed record DeltaSummary
|
|
{
|
|
public DeltaSummary(
|
|
string imageDigest,
|
|
int newFindings,
|
|
int newCriticals,
|
|
int newHigh,
|
|
int newMedium,
|
|
int newLow,
|
|
IEnumerable<string>? kevHits = null,
|
|
IEnumerable<DeltaFinding>? topFindings = null,
|
|
string? reportUrl = null,
|
|
DeltaAttestation? attestation = null,
|
|
DateTimeOffset? detectedAt = null)
|
|
: this(
|
|
imageDigest,
|
|
Validation.EnsureNonNegative(newFindings, nameof(newFindings)),
|
|
Validation.EnsureNonNegative(newCriticals, nameof(newCriticals)),
|
|
Validation.EnsureNonNegative(newHigh, nameof(newHigh)),
|
|
Validation.EnsureNonNegative(newMedium, nameof(newMedium)),
|
|
Validation.EnsureNonNegative(newLow, nameof(newLow)),
|
|
NormalizeKevHits(kevHits),
|
|
NormalizeFindings(topFindings),
|
|
Validation.TrimToNull(reportUrl),
|
|
attestation,
|
|
Validation.NormalizeTimestamp(detectedAt))
|
|
{
|
|
}
|
|
|
|
[JsonConstructor]
|
|
public DeltaSummary(
|
|
string imageDigest,
|
|
int newFindings,
|
|
int newCriticals,
|
|
int newHigh,
|
|
int newMedium,
|
|
int newLow,
|
|
ImmutableArray<string> kevHits,
|
|
ImmutableArray<DeltaFinding> topFindings,
|
|
string? reportUrl,
|
|
DeltaAttestation? attestation,
|
|
DateTimeOffset? detectedAt)
|
|
{
|
|
ImageDigest = Validation.EnsureDigestFormat(imageDigest, nameof(imageDigest));
|
|
NewFindings = Validation.EnsureNonNegative(newFindings, nameof(newFindings));
|
|
NewCriticals = Validation.EnsureNonNegative(newCriticals, nameof(newCriticals));
|
|
NewHigh = Validation.EnsureNonNegative(newHigh, nameof(newHigh));
|
|
NewMedium = Validation.EnsureNonNegative(newMedium, nameof(newMedium));
|
|
NewLow = Validation.EnsureNonNegative(newLow, nameof(newLow));
|
|
KevHits = kevHits.IsDefault ? ImmutableArray<string>.Empty : kevHits;
|
|
TopFindings = topFindings.IsDefault
|
|
? ImmutableArray<DeltaFinding>.Empty
|
|
: topFindings
|
|
.OrderBy(static finding => finding.Severity, SeverityRankComparer.Instance)
|
|
.ThenBy(static finding => finding.VulnerabilityId, StringComparer.Ordinal)
|
|
.ToImmutableArray();
|
|
ReportUrl = Validation.TrimToNull(reportUrl);
|
|
Attestation = attestation;
|
|
DetectedAt = Validation.NormalizeTimestamp(detectedAt);
|
|
}
|
|
|
|
public string ImageDigest { get; }
|
|
|
|
public int NewFindings { get; }
|
|
|
|
public int NewCriticals { get; }
|
|
|
|
public int NewHigh { get; }
|
|
|
|
public int NewMedium { get; }
|
|
|
|
public int NewLow { get; }
|
|
|
|
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
|
|
public ImmutableArray<string> KevHits { get; } = ImmutableArray<string>.Empty;
|
|
|
|
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
|
|
public ImmutableArray<DeltaFinding> TopFindings { get; } = ImmutableArray<DeltaFinding>.Empty;
|
|
|
|
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
|
public string? ReportUrl { get; }
|
|
|
|
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
|
public DeltaAttestation? Attestation { get; }
|
|
|
|
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
|
public DateTimeOffset? DetectedAt { get; }
|
|
|
|
private static ImmutableArray<string> NormalizeKevHits(IEnumerable<string>? kevHits)
|
|
=> Validation.NormalizeStringSet(kevHits, nameof(kevHits));
|
|
|
|
private static ImmutableArray<DeltaFinding> NormalizeFindings(IEnumerable<DeltaFinding>? findings)
|
|
{
|
|
if (findings is null)
|
|
{
|
|
return ImmutableArray<DeltaFinding>.Empty;
|
|
}
|
|
|
|
return findings
|
|
.Where(static finding => finding is not null)
|
|
.Select(static finding => finding!)
|
|
.OrderBy(static finding => finding.Severity, SeverityRankComparer.Instance)
|
|
.ThenBy(static finding => finding.VulnerabilityId, StringComparer.Ordinal)
|
|
.ToImmutableArray();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Top finding entry included in delta summaries.
|
|
/// </summary>
|
|
public sealed record DeltaFinding
|
|
{
|
|
public DeltaFinding(string purl, string vulnerabilityId, SeverityRank severity, string? link = null)
|
|
{
|
|
Purl = Validation.EnsureSimpleIdentifier(purl, nameof(purl));
|
|
VulnerabilityId = Validation.EnsureSimpleIdentifier(vulnerabilityId, nameof(vulnerabilityId));
|
|
Severity = severity;
|
|
Link = Validation.TrimToNull(link);
|
|
}
|
|
|
|
public string Purl { get; }
|
|
|
|
public string VulnerabilityId { get; }
|
|
|
|
public SeverityRank Severity { get; }
|
|
|
|
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
|
public string? Link { get; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Rekor/attestation information surfaced with a delta summary.
|
|
/// </summary>
|
|
public sealed record DeltaAttestation
|
|
{
|
|
public DeltaAttestation(string? uuid, bool? verified = null)
|
|
{
|
|
Uuid = Validation.TrimToNull(uuid);
|
|
Verified = verified;
|
|
}
|
|
|
|
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
|
public string? Uuid { get; }
|
|
|
|
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
|
public bool? Verified { get; }
|
|
}
|
|
|
|
internal sealed class SeverityRankComparer : IComparer<SeverityRank>
|
|
{
|
|
public static SeverityRankComparer Instance { get; } = new();
|
|
|
|
private static readonly Dictionary<SeverityRank, int> Order = new()
|
|
{
|
|
[SeverityRank.Critical] = 0,
|
|
[SeverityRank.High] = 1,
|
|
[SeverityRank.Unknown] = 2,
|
|
[SeverityRank.Medium] = 3,
|
|
[SeverityRank.Low] = 4,
|
|
[SeverityRank.Info] = 5,
|
|
[SeverityRank.None] = 6,
|
|
};
|
|
|
|
public int Compare(SeverityRank x, SeverityRank y)
|
|
=> GetOrder(x).CompareTo(GetOrder(y));
|
|
|
|
private static int GetOrder(SeverityRank severity)
|
|
=> Order.TryGetValue(severity, out var value) ? value : int.MaxValue;
|
|
}
|