Restructure solution layout by module
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled

This commit is contained in:
root
2025-10-28 15:10:40 +02:00
parent 4e3e575db5
commit 68da90a11a
4103 changed files with 192899 additions and 187024 deletions

View File

@@ -0,0 +1,277 @@
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";
}
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<string, string>? Attributes { 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<FindingSummaryPayload> Findings { get; init; } = Array.Empty<FindingSummaryPayload>();
[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<string>? Kev { get; init; }
}
internal sealed record ReportLinksPayload
{
[JsonPropertyName("ui")]
[JsonPropertyOrder(0)]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? Ui { get; init; }
[JsonPropertyName("report")]
[JsonPropertyOrder(1)]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? Report { get; init; }
[JsonPropertyName("policy")]
[JsonPropertyOrder(2)]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? Policy { get; init; }
[JsonPropertyName("attestation")]
[JsonPropertyOrder(3)]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? Attestation { get; init; }
}
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; }
}

View File

@@ -0,0 +1,38 @@
using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace StellaOps.Scanner.WebService.Contracts;
public sealed record PolicyDiagnosticsRequestDto
{
[JsonPropertyName("policy")]
public PolicyPreviewPolicyDto? Policy { get; init; }
}
public sealed record PolicyDiagnosticsResponseDto
{
[JsonPropertyName("success")]
public bool Success { get; init; }
[JsonPropertyName("version")]
public string Version { get; init; } = string.Empty;
[JsonPropertyName("ruleCount")]
public int RuleCount { get; init; }
[JsonPropertyName("errorCount")]
public int ErrorCount { get; init; }
[JsonPropertyName("warningCount")]
public int WarningCount { get; init; }
[JsonPropertyName("generatedAt")]
public DateTimeOffset GeneratedAt { get; init; }
[JsonPropertyName("issues")]
public IReadOnlyList<PolicyPreviewIssueDto> Issues { get; init; } = Array.Empty<PolicyPreviewIssueDto>();
[JsonPropertyName("recommendations")]
public IReadOnlyList<string> Recommendations { get; init; } = Array.Empty<string>();
}

View File

@@ -0,0 +1,180 @@
using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace StellaOps.Scanner.WebService.Contracts;
public sealed record PolicyPreviewRequestDto
{
[JsonPropertyName("imageDigest")]
public string? ImageDigest { get; init; }
[JsonPropertyName("findings")]
public IReadOnlyList<PolicyPreviewFindingDto>? Findings { get; init; }
[JsonPropertyName("baseline")]
public IReadOnlyList<PolicyPreviewVerdictDto>? Baseline { get; init; }
[JsonPropertyName("policy")]
public PolicyPreviewPolicyDto? Policy { get; init; }
}
public sealed record PolicyPreviewFindingDto
{
[JsonPropertyName("id")]
public string? Id { get; init; }
[JsonPropertyName("severity")]
public string? Severity { get; init; }
[JsonPropertyName("environment")]
public string? Environment { get; init; }
[JsonPropertyName("source")]
public string? Source { get; init; }
[JsonPropertyName("vendor")]
public string? Vendor { get; init; }
[JsonPropertyName("license")]
public string? License { get; init; }
[JsonPropertyName("image")]
public string? Image { get; init; }
[JsonPropertyName("repository")]
public string? Repository { get; init; }
[JsonPropertyName("package")]
public string? Package { get; init; }
[JsonPropertyName("purl")]
public string? Purl { get; init; }
[JsonPropertyName("cve")]
public string? Cve { get; init; }
[JsonPropertyName("path")]
public string? Path { get; init; }
[JsonPropertyName("layerDigest")]
public string? LayerDigest { get; init; }
[JsonPropertyName("tags")]
public IReadOnlyList<string>? Tags { get; init; }
}
public sealed record PolicyPreviewVerdictDto
{
[JsonPropertyName("findingId")]
public string? FindingId { get; init; }
[JsonPropertyName("status")]
public string? Status { get; init; }
[JsonPropertyName("ruleName")]
public string? RuleName { get; init; }
[JsonPropertyName("ruleAction")]
public string? RuleAction { get; init; }
[JsonPropertyName("notes")]
public string? Notes { get; init; }
[JsonPropertyName("score")]
public double? Score { get; init; }
[JsonPropertyName("configVersion")]
public string? ConfigVersion { get; init; }
[JsonPropertyName("inputs")]
public IReadOnlyDictionary<string, double>? Inputs { get; init; }
[JsonPropertyName("quietedBy")]
public string? QuietedBy { get; init; }
[JsonPropertyName("quiet")]
public bool? Quiet { get; init; }
[JsonPropertyName("unknownConfidence")]
public double? UnknownConfidence { get; init; }
[JsonPropertyName("confidenceBand")]
public string? ConfidenceBand { get; init; }
[JsonPropertyName("unknownAgeDays")]
public double? UnknownAgeDays { get; init; }
[JsonPropertyName("sourceTrust")]
public string? SourceTrust { get; init; }
[JsonPropertyName("reachability")]
public string? Reachability { get; init; }
}
public sealed record PolicyPreviewPolicyDto
{
[JsonPropertyName("content")]
public string? Content { get; init; }
[JsonPropertyName("format")]
public string? Format { get; init; }
[JsonPropertyName("actor")]
public string? Actor { get; init; }
[JsonPropertyName("description")]
public string? Description { get; init; }
}
public sealed record PolicyPreviewResponseDto
{
[JsonPropertyName("success")]
public bool Success { get; init; }
[JsonPropertyName("policyDigest")]
public string? PolicyDigest { get; init; }
[JsonPropertyName("revisionId")]
public string? RevisionId { get; init; }
[JsonPropertyName("changed")]
public int Changed { get; init; }
[JsonPropertyName("diffs")]
public IReadOnlyList<PolicyPreviewDiffDto> Diffs { get; init; } = Array.Empty<PolicyPreviewDiffDto>();
[JsonPropertyName("issues")]
public IReadOnlyList<PolicyPreviewIssueDto> Issues { get; init; } = Array.Empty<PolicyPreviewIssueDto>();
}
public sealed record PolicyPreviewDiffDto
{
[JsonPropertyName("findingId")]
public string? FindingId { get; init; }
[JsonPropertyName("baseline")]
public PolicyPreviewVerdictDto? Baseline { get; init; }
[JsonPropertyName("projected")]
public PolicyPreviewVerdictDto? Projected { get; init; }
[JsonPropertyName("changed")]
public bool Changed { get; init; }
}
public sealed record PolicyPreviewIssueDto
{
[JsonPropertyName("code")]
public string Code { get; init; } = string.Empty;
[JsonPropertyName("message")]
public string Message { get; init; } = string.Empty;
[JsonPropertyName("severity")]
public string Severity { get; init; } = string.Empty;
[JsonPropertyName("path")]
public string Path { get; init; } = string.Empty;
}

View File

@@ -0,0 +1,122 @@
using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace StellaOps.Scanner.WebService.Contracts;
public sealed record ReportRequestDto
{
[JsonPropertyName("imageDigest")]
public string? ImageDigest { get; init; }
[JsonPropertyName("findings")]
public IReadOnlyList<PolicyPreviewFindingDto>? Findings { get; init; }
[JsonPropertyName("baseline")]
public IReadOnlyList<PolicyPreviewVerdictDto>? Baseline { get; init; }
}
public sealed record ReportResponseDto
{
[JsonPropertyName("report")]
public ReportDocumentDto Report { get; init; } = new();
[JsonPropertyName("dsse")]
public DsseEnvelopeDto? Dsse { get; init; }
}
public sealed record ReportDocumentDto
{
[JsonPropertyName("reportId")]
[JsonPropertyOrder(0)]
public string ReportId { get; init; } = string.Empty;
[JsonPropertyName("imageDigest")]
[JsonPropertyOrder(1)]
public string ImageDigest { get; init; } = string.Empty;
[JsonPropertyName("generatedAt")]
[JsonPropertyOrder(2)]
public DateTimeOffset GeneratedAt { get; init; }
[JsonPropertyName("verdict")]
[JsonPropertyOrder(3)]
public string Verdict { get; init; } = string.Empty;
[JsonPropertyName("policy")]
[JsonPropertyOrder(4)]
public ReportPolicyDto Policy { get; init; } = new();
[JsonPropertyName("summary")]
[JsonPropertyOrder(5)]
public ReportSummaryDto Summary { get; init; } = new();
[JsonPropertyName("verdicts")]
[JsonPropertyOrder(6)]
public IReadOnlyList<PolicyPreviewVerdictDto> Verdicts { get; init; } = Array.Empty<PolicyPreviewVerdictDto>();
[JsonPropertyName("issues")]
[JsonPropertyOrder(7)]
public IReadOnlyList<PolicyPreviewIssueDto> Issues { get; init; } = Array.Empty<PolicyPreviewIssueDto>();
}
public sealed record ReportPolicyDto
{
[JsonPropertyName("revisionId")]
[JsonPropertyOrder(0)]
public string? RevisionId { get; init; }
[JsonPropertyName("digest")]
[JsonPropertyOrder(1)]
public string? Digest { get; init; }
}
public sealed record ReportSummaryDto
{
[JsonPropertyName("total")]
[JsonPropertyOrder(0)]
public int Total { get; init; }
[JsonPropertyName("blocked")]
[JsonPropertyOrder(1)]
public int Blocked { get; init; }
[JsonPropertyName("warned")]
[JsonPropertyOrder(2)]
public int Warned { get; init; }
[JsonPropertyName("ignored")]
[JsonPropertyOrder(3)]
public int Ignored { get; init; }
[JsonPropertyName("quieted")]
[JsonPropertyOrder(4)]
public int Quieted { get; init; }
}
public sealed record DsseEnvelopeDto
{
[JsonPropertyName("payloadType")]
[JsonPropertyOrder(0)]
public string PayloadType { get; init; } = string.Empty;
[JsonPropertyName("payload")]
[JsonPropertyOrder(1)]
public string Payload { get; init; } = string.Empty;
[JsonPropertyName("signatures")]
[JsonPropertyOrder(2)]
public IReadOnlyList<DsseSignatureDto> Signatures { get; init; } = Array.Empty<DsseSignatureDto>();
}
public sealed record DsseSignatureDto
{
[JsonPropertyName("keyId")]
public string KeyId { get; init; } = string.Empty;
[JsonPropertyName("algorithm")]
public string Algorithm { get; init; } = string.Empty;
[JsonPropertyName("signature")]
public string Signature { get; init; } = string.Empty;
}

View File

@@ -0,0 +1,22 @@
using System.Text.Json.Serialization;
using StellaOps.Zastava.Core.Contracts;
namespace StellaOps.Scanner.WebService.Contracts;
public sealed record RuntimeEventsIngestRequestDto
{
[JsonPropertyName("batchId")]
public string? BatchId { get; init; }
[JsonPropertyName("events")]
public IReadOnlyList<RuntimeEventEnvelope> Events { get; init; } = Array.Empty<RuntimeEventEnvelope>();
}
public sealed record RuntimeEventsIngestResponseDto
{
[JsonPropertyName("accepted")]
public int Accepted { get; init; }
[JsonPropertyName("duplicates")]
public int Duplicates { get; init; }
}

View File

@@ -0,0 +1,91 @@
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace StellaOps.Scanner.WebService.Contracts;
public sealed record RuntimePolicyRequestDto
{
[JsonPropertyName("namespace")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? Namespace { get; init; }
[JsonPropertyName("labels")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public IDictionary<string, string>? Labels { get; init; }
[JsonPropertyName("images")]
public IReadOnlyList<string> Images { get; init; } = Array.Empty<string>();
}
public sealed record RuntimePolicyResponseDto
{
[JsonPropertyName("ttlSeconds")]
public int TtlSeconds { get; init; }
[JsonPropertyName("expiresAtUtc")]
public DateTimeOffset ExpiresAtUtc { get; init; }
[JsonPropertyName("policyRevision")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? PolicyRevision { get; init; }
[JsonPropertyName("results")]
public IReadOnlyDictionary<string, RuntimePolicyImageResponseDto> Results { get; init; } = new Dictionary<string, RuntimePolicyImageResponseDto>(StringComparer.Ordinal);
}
public sealed record RuntimePolicyImageResponseDto
{
[JsonPropertyName("policyVerdict")]
public string PolicyVerdict { get; init; } = "unknown";
[JsonPropertyName("signed")]
public bool Signed { get; init; }
[JsonPropertyName("hasSbomReferrers")]
public bool HasSbomReferrers { get; init; }
[JsonPropertyName("hasSbom")]
public bool HasSbomLegacy { get; init; }
[JsonPropertyName("reasons")]
public IReadOnlyList<string> Reasons { get; init; } = Array.Empty<string>();
[JsonPropertyName("rekor")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public RuntimePolicyRekorDto? Rekor { get; init; }
[JsonPropertyName("confidence")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public double? Confidence { get; init; }
[JsonPropertyName("quieted")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public bool? Quieted { get; init; }
[JsonPropertyName("quietedBy")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? QuietedBy { get; init; }
[JsonPropertyName("metadata")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? Metadata { get; init; }
[JsonPropertyName("buildIds")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public IReadOnlyList<string>? BuildIds { get; init; }
}
public sealed record RuntimePolicyRekorDto
{
[JsonPropertyName("uuid")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? Uuid { get; init; }
[JsonPropertyName("url")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? Url { get; init; }
[JsonPropertyName("verified")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public bool? Verified { get; init; }
}

View File

@@ -0,0 +1,13 @@
namespace StellaOps.Scanner.WebService.Contracts;
public sealed record ScanStatusResponse(
string ScanId,
string Status,
ScanStatusTarget Image,
DateTimeOffset CreatedAt,
DateTimeOffset UpdatedAt,
string? FailureReason);
public sealed record ScanStatusTarget(
string? Reference,
string? Digest);

View File

@@ -0,0 +1,21 @@
using System.Collections.Generic;
namespace StellaOps.Scanner.WebService.Contracts;
public sealed record ScanSubmitRequest
{
public required ScanImageDescriptor Image { get; init; } = new();
public bool Force { get; init; }
public string? ClientRequestId { get; init; }
public IDictionary<string, string> Metadata { get; init; } = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
}
public sealed record ScanImageDescriptor
{
public string? Reference { get; init; }
public string? Digest { get; init; }
}

View File

@@ -0,0 +1,7 @@
namespace StellaOps.Scanner.WebService.Contracts;
public sealed record ScanSubmitResponse(
string ScanId,
string Status,
string? Location,
bool Created);