Restructure solution layout by module
This commit is contained in:
378
src/Scheduler/__Libraries/StellaOps.Scheduler.Models/Run.cs
Normal file
378
src/Scheduler/__Libraries/StellaOps.Scheduler.Models/Run.cs
Normal file
@@ -0,0 +1,378 @@
|
||||
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? 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),
|
||||
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? 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();
|
||||
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;
|
||||
|
||||
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? feedserExportId = null,
|
||||
string? vexerExportId = null,
|
||||
string? cursor = null)
|
||||
{
|
||||
ManualReason = Validation.TrimToNull(manualReason);
|
||||
FeedserExportId = Validation.TrimToNull(feedserExportId);
|
||||
VexerExportId = Validation.TrimToNull(vexerExportId);
|
||||
Cursor = Validation.TrimToNull(cursor);
|
||||
}
|
||||
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public string? ManualReason { get; } = null;
|
||||
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public string? FeedserExportId { get; } = null;
|
||||
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public string? VexerExportId { 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;
|
||||
}
|
||||
Reference in New Issue
Block a user