using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Text.Json.Serialization; namespace StellaOps.Scanner.WebService.Contracts; internal static class OrchestratorEventKinds { public const string ScannerReportReady = "scanner.event.report.ready"; public const string ScannerScanCompleted = "scanner.event.scan.completed"; public const string ScannerScanStarted = "scanner.event.scan.started"; public const string ScannerScanFailed = "scanner.event.scan.failed"; public const string ScannerSbomGenerated = "scanner.event.sbom.generated"; public const string ScannerVulnerabilityDetected = "scanner.event.vulnerability.detected"; } internal sealed record OrchestratorEvent { [JsonPropertyName("eventId")] [JsonPropertyOrder(0)] public Guid EventId { get; init; } [JsonPropertyName("kind")] [JsonPropertyOrder(1)] public string Kind { get; init; } = string.Empty; [JsonPropertyName("version")] [JsonPropertyOrder(2)] public int Version { get; init; } = 1; [JsonPropertyName("tenant")] [JsonPropertyOrder(3)] public string Tenant { get; init; } = string.Empty; [JsonPropertyName("occurredAt")] [JsonPropertyOrder(4)] public DateTimeOffset OccurredAt { get; init; } [JsonPropertyName("recordedAt")] [JsonPropertyOrder(5)] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public DateTimeOffset? RecordedAt { get; init; } [JsonPropertyName("source")] [JsonPropertyOrder(6)] public string Source { get; init; } = string.Empty; [JsonPropertyName("idempotencyKey")] [JsonPropertyOrder(7)] public string IdempotencyKey { get; init; } = string.Empty; [JsonPropertyName("correlationId")] [JsonPropertyOrder(8)] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? CorrelationId { get; init; } [JsonPropertyName("traceId")] [JsonPropertyOrder(9)] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? TraceId { get; init; } [JsonPropertyName("spanId")] [JsonPropertyOrder(10)] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? SpanId { get; init; } [JsonPropertyName("scope")] [JsonPropertyOrder(11)] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public OrchestratorEventScope? Scope { get; init; } [JsonPropertyName("payload")] [JsonPropertyOrder(12)] public OrchestratorEventPayload Payload { get; init; } = default!; [JsonPropertyName("attributes")] [JsonPropertyOrder(13)] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public ImmutableSortedDictionary? Attributes { get; init; } [JsonPropertyName("notifier")] [JsonPropertyOrder(14)] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public NotifierIngestionMetadata? Notifier { get; init; } } /// /// Metadata for Notifier service ingestion per orchestrator-envelope.schema.json. /// internal sealed record NotifierIngestionMetadata { [JsonPropertyName("severityThresholdMet")] [JsonPropertyOrder(0)] public bool SeverityThresholdMet { get; init; } [JsonPropertyName("notificationChannels")] [JsonPropertyOrder(1)] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public IReadOnlyList? NotificationChannels { get; init; } [JsonPropertyName("digestEligible")] [JsonPropertyOrder(2)] public bool DigestEligible { get; init; } = true; [JsonPropertyName("immediateDispatch")] [JsonPropertyOrder(3)] public bool ImmediateDispatch { get; init; } [JsonPropertyName("priority")] [JsonPropertyOrder(4)] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? Priority { get; init; } } internal sealed record OrchestratorEventScope { [JsonPropertyName("namespace")] [JsonPropertyOrder(0)] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? Namespace { get; init; } [JsonPropertyName("repo")] [JsonPropertyOrder(1)] public string Repo { get; init; } = string.Empty; [JsonPropertyName("digest")] [JsonPropertyOrder(2)] public string Digest { get; init; } = string.Empty; [JsonPropertyName("component")] [JsonPropertyOrder(3)] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? Component { get; init; } [JsonPropertyName("image")] [JsonPropertyOrder(4)] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? Image { get; init; } } internal abstract record OrchestratorEventPayload; internal sealed record ReportReadyEventPayload : OrchestratorEventPayload { [JsonPropertyName("reportId")] [JsonPropertyOrder(0)] public string ReportId { get; init; } = string.Empty; [JsonPropertyName("scanId")] [JsonPropertyOrder(1)] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? ScanId { get; init; } [JsonPropertyName("imageDigest")] [JsonPropertyOrder(2)] public string ImageDigest { get; init; } = string.Empty; [JsonPropertyName("generatedAt")] [JsonPropertyOrder(3)] public DateTimeOffset GeneratedAt { get; init; } [JsonPropertyName("verdict")] [JsonPropertyOrder(4)] public string Verdict { get; init; } = string.Empty; [JsonPropertyName("summary")] [JsonPropertyOrder(5)] public ReportSummaryDto Summary { get; init; } = new(); [JsonPropertyName("delta")] [JsonPropertyOrder(6)] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public ReportDeltaPayload? Delta { get; init; } [JsonPropertyName("quietedFindingCount")] [JsonPropertyOrder(7)] public int QuietedFindingCount { get; init; } [JsonPropertyName("policy")] [JsonPropertyOrder(8)] public ReportPolicyDto Policy { get; init; } = new(); [JsonPropertyName("links")] [JsonPropertyOrder(9)] public ReportLinksPayload Links { get; init; } = new(); [JsonPropertyName("dsse")] [JsonPropertyOrder(10)] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public DsseEnvelopeDto? Dsse { get; init; } [JsonPropertyName("report")] [JsonPropertyOrder(11)] public ReportDocumentDto Report { get; init; } = new(); } internal sealed record ScanCompletedEventPayload : OrchestratorEventPayload { [JsonPropertyName("reportId")] [JsonPropertyOrder(0)] public string ReportId { get; init; } = string.Empty; [JsonPropertyName("scanId")] [JsonPropertyOrder(1)] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? ScanId { get; init; } [JsonPropertyName("imageDigest")] [JsonPropertyOrder(2)] public string ImageDigest { get; init; } = string.Empty; [JsonPropertyName("verdict")] [JsonPropertyOrder(3)] public string Verdict { get; init; } = string.Empty; [JsonPropertyName("summary")] [JsonPropertyOrder(4)] public ReportSummaryDto Summary { get; init; } = new(); [JsonPropertyName("delta")] [JsonPropertyOrder(5)] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public ReportDeltaPayload? Delta { get; init; } [JsonPropertyName("policy")] [JsonPropertyOrder(6)] public ReportPolicyDto Policy { get; init; } = new(); [JsonPropertyName("findings")] [JsonPropertyOrder(7)] public IReadOnlyList Findings { get; init; } = Array.Empty(); [JsonPropertyName("links")] [JsonPropertyOrder(8)] public ReportLinksPayload Links { get; init; } = new(); [JsonPropertyName("dsse")] [JsonPropertyOrder(9)] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public DsseEnvelopeDto? Dsse { get; init; } [JsonPropertyName("report")] [JsonPropertyOrder(10)] public ReportDocumentDto Report { get; init; } = new(); } internal sealed record ReportDeltaPayload { [JsonPropertyName("newCritical")] [JsonPropertyOrder(0)] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public int? NewCritical { get; init; } [JsonPropertyName("newHigh")] [JsonPropertyOrder(1)] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public int? NewHigh { get; init; } [JsonPropertyName("kev")] [JsonPropertyOrder(2)] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public IReadOnlyList? Kev { get; init; } } internal sealed record ReportLinksPayload { [JsonPropertyName("report")] [JsonPropertyOrder(0)] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public LinkTarget? Report { get; init; } [JsonPropertyName("policy")] [JsonPropertyOrder(1)] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public LinkTarget? Policy { get; init; } [JsonPropertyName("attestation")] [JsonPropertyOrder(2)] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public LinkTarget? Attestation { get; init; } } internal sealed record LinkTarget( [property: JsonPropertyName("ui"), JsonPropertyOrder(0), JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] string? Ui, [property: JsonPropertyName("api"), JsonPropertyOrder(1), JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] string? Api) { public static LinkTarget? Create(string? ui, string? api) { if (string.IsNullOrWhiteSpace(ui) && string.IsNullOrWhiteSpace(api)) { return null; } return new LinkTarget( string.IsNullOrWhiteSpace(ui) ? null : ui, string.IsNullOrWhiteSpace(api) ? null : api); } } internal sealed record FindingSummaryPayload { [JsonPropertyName("id")] [JsonPropertyOrder(0)] public string Id { get; init; } = string.Empty; [JsonPropertyName("severity")] [JsonPropertyOrder(1)] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? Severity { get; init; } [JsonPropertyName("cve")] [JsonPropertyOrder(2)] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? Cve { get; init; } [JsonPropertyName("purl")] [JsonPropertyOrder(3)] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? Purl { get; init; } [JsonPropertyName("reachability")] [JsonPropertyOrder(4)] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? Reachability { get; init; } } /// /// Payload for scanner.event.scan.started events. /// internal sealed record ScanStartedEventPayload : OrchestratorEventPayload { [JsonPropertyName("scanId")] [JsonPropertyOrder(0)] public string ScanId { get; init; } = string.Empty; [JsonPropertyName("jobId")] [JsonPropertyOrder(1)] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? JobId { get; init; } [JsonPropertyName("target")] [JsonPropertyOrder(2)] public ScanTargetPayload Target { get; init; } = new(); [JsonPropertyName("startedAt")] [JsonPropertyOrder(3)] public DateTimeOffset StartedAt { get; init; } [JsonPropertyName("status")] [JsonPropertyOrder(4)] public string Status { get; init; } = "started"; } /// /// Payload for scanner.event.scan.failed events. /// internal sealed record ScanFailedEventPayload : OrchestratorEventPayload { [JsonPropertyName("scanId")] [JsonPropertyOrder(0)] public string ScanId { get; init; } = string.Empty; [JsonPropertyName("jobId")] [JsonPropertyOrder(1)] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? JobId { get; init; } [JsonPropertyName("target")] [JsonPropertyOrder(2)] public ScanTargetPayload Target { get; init; } = new(); [JsonPropertyName("startedAt")] [JsonPropertyOrder(3)] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public DateTimeOffset? StartedAt { get; init; } [JsonPropertyName("failedAt")] [JsonPropertyOrder(4)] public DateTimeOffset FailedAt { get; init; } [JsonPropertyName("durationMs")] [JsonPropertyOrder(5)] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public long? DurationMs { get; init; } [JsonPropertyName("status")] [JsonPropertyOrder(6)] public string Status { get; init; } = "failed"; [JsonPropertyName("error")] [JsonPropertyOrder(7)] public ScanErrorPayload Error { get; init; } = new(); } /// /// Payload for scanner.event.sbom.generated events. /// internal sealed record SbomGeneratedEventPayload : OrchestratorEventPayload { [JsonPropertyName("scanId")] [JsonPropertyOrder(0)] public string ScanId { get; init; } = string.Empty; [JsonPropertyName("sbomId")] [JsonPropertyOrder(1)] public string SbomId { get; init; } = string.Empty; [JsonPropertyName("target")] [JsonPropertyOrder(2)] public ScanTargetPayload Target { get; init; } = new(); [JsonPropertyName("generatedAt")] [JsonPropertyOrder(3)] public DateTimeOffset GeneratedAt { get; init; } [JsonPropertyName("format")] [JsonPropertyOrder(4)] public string Format { get; init; } = "cyclonedx"; [JsonPropertyName("specVersion")] [JsonPropertyOrder(5)] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? SpecVersion { get; init; } [JsonPropertyName("componentCount")] [JsonPropertyOrder(6)] public int ComponentCount { get; init; } [JsonPropertyName("sbomRef")] [JsonPropertyOrder(7)] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? SbomRef { get; init; } [JsonPropertyName("digest")] [JsonPropertyOrder(8)] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? Digest { get; init; } } /// /// Payload for scanner.event.vulnerability.detected events. /// internal sealed record VulnerabilityDetectedEventPayload : OrchestratorEventPayload { [JsonPropertyName("scanId")] [JsonPropertyOrder(0)] public string ScanId { get; init; } = string.Empty; [JsonPropertyName("vulnerability")] [JsonPropertyOrder(1)] public VulnerabilityInfoPayload Vulnerability { get; init; } = new(); [JsonPropertyName("affectedComponent")] [JsonPropertyOrder(2)] public ComponentInfoPayload AffectedComponent { get; init; } = new(); [JsonPropertyName("reachability")] [JsonPropertyOrder(3)] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? Reachability { get; init; } [JsonPropertyName("detectedAt")] [JsonPropertyOrder(4)] public DateTimeOffset DetectedAt { get; init; } } /// /// Target being scanned. /// internal sealed record ScanTargetPayload { [JsonPropertyName("type")] [JsonPropertyOrder(0)] public string Type { get; init; } = "container_image"; [JsonPropertyName("identifier")] [JsonPropertyOrder(1)] public string Identifier { get; init; } = string.Empty; [JsonPropertyName("digest")] [JsonPropertyOrder(2)] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? Digest { get; init; } [JsonPropertyName("tag")] [JsonPropertyOrder(3)] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? Tag { get; init; } [JsonPropertyName("platform")] [JsonPropertyOrder(4)] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? Platform { get; init; } } /// /// Error information for failed scans. /// internal sealed record ScanErrorPayload { [JsonPropertyName("code")] [JsonPropertyOrder(0)] public string Code { get; init; } = "SCAN_FAILED"; [JsonPropertyName("message")] [JsonPropertyOrder(1)] public string Message { get; init; } = string.Empty; [JsonPropertyName("details")] [JsonPropertyOrder(2)] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public ImmutableDictionary? Details { get; init; } [JsonPropertyName("recoverable")] [JsonPropertyOrder(3)] public bool Recoverable { get; init; } } /// /// Vulnerability information. /// internal sealed record VulnerabilityInfoPayload { [JsonPropertyName("id")] [JsonPropertyOrder(0)] public string Id { get; init; } = string.Empty; [JsonPropertyName("severity")] [JsonPropertyOrder(1)] public string Severity { get; init; } = "unknown"; [JsonPropertyName("cvssScore")] [JsonPropertyOrder(2)] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public double? CvssScore { get; init; } [JsonPropertyName("cvssVector")] [JsonPropertyOrder(3)] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? CvssVector { get; init; } [JsonPropertyName("title")] [JsonPropertyOrder(4)] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? Title { get; init; } [JsonPropertyName("fixAvailable")] [JsonPropertyOrder(5)] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public bool? FixAvailable { get; init; } [JsonPropertyName("fixedVersion")] [JsonPropertyOrder(6)] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? FixedVersion { get; init; } [JsonPropertyName("kevListed")] [JsonPropertyOrder(7)] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public bool? KevListed { get; init; } [JsonPropertyName("epssScore")] [JsonPropertyOrder(8)] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public double? EpssScore { get; init; } } /// /// Component information. /// internal sealed record ComponentInfoPayload { [JsonPropertyName("purl")] [JsonPropertyOrder(0)] public string Purl { get; init; } = string.Empty; [JsonPropertyName("name")] [JsonPropertyOrder(1)] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? Name { get; init; } [JsonPropertyName("version")] [JsonPropertyOrder(2)] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? Version { get; init; } [JsonPropertyName("ecosystem")] [JsonPropertyOrder(3)] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? Ecosystem { get; init; } [JsonPropertyName("location")] [JsonPropertyOrder(4)] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? Location { get; init; } }