up
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
Build Test Deploy / build-test (push) Has been cancelled
Build Test Deploy / authority-container (push) Has been cancelled
Build Test Deploy / docs (push) Has been cancelled
Build Test Deploy / deploy (push) Has been cancelled
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
Build Test Deploy / build-test (push) Has been cancelled
Build Test Deploy / authority-container (push) Has been cancelled
Build Test Deploy / docs (push) Has been cancelled
Build Test Deploy / deploy (push) Has been cancelled
This commit is contained in:
173
src/StellaOps.Scanner.Core/Contracts/ScanJob.cs
Normal file
173
src/StellaOps.Scanner.Core/Contracts/ScanJob.cs
Normal file
@@ -0,0 +1,173 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Globalization;
|
||||
using System.Text.Json.Serialization;
|
||||
using StellaOps.Scanner.Core.Utility;
|
||||
|
||||
namespace StellaOps.Scanner.Core.Contracts;
|
||||
|
||||
[JsonConverter(typeof(ScanJobIdJsonConverter))]
|
||||
public readonly record struct ScanJobId(Guid Value)
|
||||
{
|
||||
public static readonly ScanJobId Empty = new(Guid.Empty);
|
||||
|
||||
public override string ToString()
|
||||
=> Value.ToString("n", CultureInfo.InvariantCulture);
|
||||
|
||||
public static ScanJobId From(Guid value)
|
||||
=> new(value);
|
||||
|
||||
public static bool TryParse(string? text, out ScanJobId id)
|
||||
{
|
||||
if (Guid.TryParse(text, out var guid))
|
||||
{
|
||||
id = new ScanJobId(guid);
|
||||
return true;
|
||||
}
|
||||
|
||||
id = Empty;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
[JsonConverter(typeof(JsonStringEnumConverter<ScanJobStatus>))]
|
||||
public enum ScanJobStatus
|
||||
{
|
||||
Unknown = 0,
|
||||
Pending,
|
||||
Queued,
|
||||
Running,
|
||||
Succeeded,
|
||||
Failed,
|
||||
Cancelled
|
||||
}
|
||||
|
||||
public sealed class ScanJob
|
||||
{
|
||||
private static readonly IReadOnlyDictionary<string, string> EmptyMetadata =
|
||||
new ReadOnlyDictionary<string, string>(new Dictionary<string, string>(0, StringComparer.Ordinal));
|
||||
|
||||
[JsonConstructor]
|
||||
public ScanJob(
|
||||
ScanJobId id,
|
||||
ScanJobStatus status,
|
||||
string imageReference,
|
||||
string? imageDigest,
|
||||
DateTimeOffset createdAt,
|
||||
DateTimeOffset? updatedAt,
|
||||
string correlationId,
|
||||
string? tenantId,
|
||||
IReadOnlyDictionary<string, string>? metadata = null,
|
||||
ScannerError? failure = null)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(imageReference))
|
||||
{
|
||||
throw new ArgumentException("Image reference cannot be null or whitespace.", nameof(imageReference));
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(correlationId))
|
||||
{
|
||||
throw new ArgumentException("Correlation identifier cannot be null or whitespace.", nameof(correlationId));
|
||||
}
|
||||
|
||||
Id = id;
|
||||
Status = status;
|
||||
ImageReference = imageReference.Trim();
|
||||
ImageDigest = NormalizeDigest(imageDigest);
|
||||
CreatedAt = ScannerTimestamps.Normalize(createdAt);
|
||||
UpdatedAt = updatedAt is null ? null : ScannerTimestamps.Normalize(updatedAt.Value);
|
||||
CorrelationId = correlationId;
|
||||
TenantId = string.IsNullOrWhiteSpace(tenantId) ? null : tenantId.Trim();
|
||||
Metadata = metadata is null or { Count: 0 }
|
||||
? EmptyMetadata
|
||||
: new ReadOnlyDictionary<string, string>(new Dictionary<string, string>(metadata, StringComparer.Ordinal));
|
||||
Failure = failure;
|
||||
}
|
||||
|
||||
[JsonPropertyName("id")]
|
||||
[JsonPropertyOrder(0)]
|
||||
public ScanJobId Id { get; }
|
||||
|
||||
[JsonPropertyName("status")]
|
||||
[JsonPropertyOrder(1)]
|
||||
public ScanJobStatus Status { get; init; }
|
||||
|
||||
[JsonPropertyName("imageReference")]
|
||||
[JsonPropertyOrder(2)]
|
||||
public string ImageReference { get; }
|
||||
|
||||
[JsonPropertyName("imageDigest")]
|
||||
[JsonPropertyOrder(3)]
|
||||
public string? ImageDigest { get; }
|
||||
|
||||
[JsonPropertyName("createdAt")]
|
||||
[JsonPropertyOrder(4)]
|
||||
public DateTimeOffset CreatedAt { get; }
|
||||
|
||||
[JsonPropertyName("updatedAt")]
|
||||
[JsonPropertyOrder(5)]
|
||||
public DateTimeOffset? UpdatedAt { get; init; }
|
||||
|
||||
[JsonPropertyName("correlationId")]
|
||||
[JsonPropertyOrder(6)]
|
||||
public string CorrelationId { get; }
|
||||
|
||||
[JsonPropertyName("tenantId")]
|
||||
[JsonPropertyOrder(7)]
|
||||
public string? TenantId { get; }
|
||||
|
||||
[JsonPropertyName("metadata")]
|
||||
[JsonPropertyOrder(8)]
|
||||
public IReadOnlyDictionary<string, string> Metadata { get; }
|
||||
|
||||
[JsonPropertyName("failure")]
|
||||
[JsonPropertyOrder(9)]
|
||||
public ScannerError? Failure { get; init; }
|
||||
|
||||
public ScanJob WithStatus(ScanJobStatus status, DateTimeOffset? updatedAt = null)
|
||||
=> new(
|
||||
Id,
|
||||
status,
|
||||
ImageReference,
|
||||
ImageDigest,
|
||||
CreatedAt,
|
||||
updatedAt ?? UpdatedAt ?? CreatedAt,
|
||||
CorrelationId,
|
||||
TenantId,
|
||||
Metadata,
|
||||
Failure);
|
||||
|
||||
public ScanJob WithFailure(ScannerError failure, DateTimeOffset? updatedAt = null, TimeProvider? timeProvider = null)
|
||||
=> new(
|
||||
Id,
|
||||
ScanJobStatus.Failed,
|
||||
ImageReference,
|
||||
ImageDigest,
|
||||
CreatedAt,
|
||||
updatedAt ?? ScannerTimestamps.UtcNow(timeProvider),
|
||||
CorrelationId,
|
||||
TenantId,
|
||||
Metadata,
|
||||
failure);
|
||||
|
||||
private static string? NormalizeDigest(string? digest)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(digest))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var trimmed = digest.Trim();
|
||||
if (!trimmed.StartsWith("sha", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
var parts = trimmed.Split(':', 2, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||
if (parts.Length != 2)
|
||||
{
|
||||
return trimmed.ToLowerInvariant();
|
||||
}
|
||||
|
||||
return $"{parts[0].ToLowerInvariant()}:{parts[1].ToLowerInvariant()}";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace StellaOps.Scanner.Core.Contracts;
|
||||
|
||||
internal sealed class ScanJobIdJsonConverter : JsonConverter<ScanJobId>
|
||||
{
|
||||
public override ScanJobId Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
if (reader.TokenType != JsonTokenType.String)
|
||||
{
|
||||
throw new JsonException("Expected scan job identifier to be a string.");
|
||||
}
|
||||
|
||||
var value = reader.GetString();
|
||||
if (!ScanJobId.TryParse(value, out var id))
|
||||
{
|
||||
throw new JsonException("Invalid scan job identifier.");
|
||||
}
|
||||
|
||||
return id;
|
||||
}
|
||||
|
||||
public override void Write(Utf8JsonWriter writer, ScanJobId value, JsonSerializerOptions options)
|
||||
=> writer.WriteStringValue(value.ToString());
|
||||
}
|
||||
121
src/StellaOps.Scanner.Core/Contracts/ScanProgressEvent.cs
Normal file
121
src/StellaOps.Scanner.Core/Contracts/ScanProgressEvent.cs
Normal file
@@ -0,0 +1,121 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Text.Json.Serialization;
|
||||
using StellaOps.Scanner.Core.Utility;
|
||||
|
||||
namespace StellaOps.Scanner.Core.Contracts;
|
||||
|
||||
[JsonConverter(typeof(JsonStringEnumConverter<ScanStage>))]
|
||||
public enum ScanStage
|
||||
{
|
||||
Unknown = 0,
|
||||
ResolveImage,
|
||||
FetchLayers,
|
||||
MountLayers,
|
||||
AnalyzeOperatingSystem,
|
||||
AnalyzeLanguageEcosystems,
|
||||
AnalyzeNativeArtifacts,
|
||||
ComposeSbom,
|
||||
BuildDiffs,
|
||||
EmitArtifacts,
|
||||
SignArtifacts,
|
||||
Complete
|
||||
}
|
||||
|
||||
[JsonConverter(typeof(JsonStringEnumConverter<ScanProgressEventKind>))]
|
||||
public enum ScanProgressEventKind
|
||||
{
|
||||
Progress = 0,
|
||||
StageStarted,
|
||||
StageCompleted,
|
||||
Warning,
|
||||
Error
|
||||
}
|
||||
|
||||
public sealed class ScanProgressEvent
|
||||
{
|
||||
private static readonly IReadOnlyDictionary<string, string> EmptyAttributes =
|
||||
new ReadOnlyDictionary<string, string>(new Dictionary<string, string>(0, StringComparer.Ordinal));
|
||||
|
||||
[JsonConstructor]
|
||||
public ScanProgressEvent(
|
||||
ScanJobId jobId,
|
||||
ScanStage stage,
|
||||
ScanProgressEventKind kind,
|
||||
int sequence,
|
||||
DateTimeOffset timestamp,
|
||||
double? percentComplete = null,
|
||||
string? message = null,
|
||||
IReadOnlyDictionary<string, string>? attributes = null,
|
||||
ScannerError? error = null)
|
||||
{
|
||||
if (sequence < 0)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(sequence), sequence, "Sequence cannot be negative.");
|
||||
}
|
||||
|
||||
JobId = jobId;
|
||||
Stage = stage;
|
||||
Kind = kind;
|
||||
Sequence = sequence;
|
||||
Timestamp = ScannerTimestamps.Normalize(timestamp);
|
||||
PercentComplete = percentComplete is < 0 or > 100 ? null : percentComplete;
|
||||
Message = message is { Length: > 0 } ? message.Trim() : null;
|
||||
Attributes = attributes is null or { Count: 0 }
|
||||
? EmptyAttributes
|
||||
: new ReadOnlyDictionary<string, string>(new Dictionary<string, string>(attributes, StringComparer.Ordinal));
|
||||
Error = error;
|
||||
}
|
||||
|
||||
[JsonPropertyName("jobId")]
|
||||
[JsonPropertyOrder(0)]
|
||||
public ScanJobId JobId { get; }
|
||||
|
||||
[JsonPropertyName("stage")]
|
||||
[JsonPropertyOrder(1)]
|
||||
public ScanStage Stage { get; }
|
||||
|
||||
[JsonPropertyName("kind")]
|
||||
[JsonPropertyOrder(2)]
|
||||
public ScanProgressEventKind Kind { get; }
|
||||
|
||||
[JsonPropertyName("sequence")]
|
||||
[JsonPropertyOrder(3)]
|
||||
public int Sequence { get; }
|
||||
|
||||
[JsonPropertyName("timestamp")]
|
||||
[JsonPropertyOrder(4)]
|
||||
public DateTimeOffset Timestamp { get; }
|
||||
|
||||
[JsonPropertyName("percentComplete")]
|
||||
[JsonPropertyOrder(5)]
|
||||
public double? PercentComplete { get; }
|
||||
|
||||
[JsonPropertyName("message")]
|
||||
[JsonPropertyOrder(6)]
|
||||
public string? Message { get; }
|
||||
|
||||
[JsonPropertyName("attributes")]
|
||||
[JsonPropertyOrder(7)]
|
||||
public IReadOnlyDictionary<string, string> Attributes { get; }
|
||||
|
||||
[JsonPropertyName("error")]
|
||||
[JsonPropertyOrder(8)]
|
||||
public ScannerError? Error { get; }
|
||||
|
||||
public ScanProgressEvent With(
|
||||
ScanProgressEventKind? kind = null,
|
||||
double? percentComplete = null,
|
||||
string? message = null,
|
||||
IReadOnlyDictionary<string, string>? attributes = null,
|
||||
ScannerError? error = null)
|
||||
=> new(
|
||||
JobId,
|
||||
Stage,
|
||||
kind ?? Kind,
|
||||
Sequence,
|
||||
Timestamp,
|
||||
percentComplete ?? PercentComplete,
|
||||
message ?? Message,
|
||||
attributes ?? Attributes,
|
||||
error ?? Error);
|
||||
}
|
||||
110
src/StellaOps.Scanner.Core/Contracts/ScannerError.cs
Normal file
110
src/StellaOps.Scanner.Core/Contracts/ScannerError.cs
Normal file
@@ -0,0 +1,110 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Text.Json.Serialization;
|
||||
using StellaOps.Scanner.Core.Utility;
|
||||
|
||||
namespace StellaOps.Scanner.Core.Contracts;
|
||||
|
||||
[JsonConverter(typeof(JsonStringEnumConverter<ScannerErrorCode>))]
|
||||
public enum ScannerErrorCode
|
||||
{
|
||||
Unknown = 0,
|
||||
InvalidImageReference,
|
||||
ImageNotFound,
|
||||
AuthorizationFailed,
|
||||
QueueUnavailable,
|
||||
StorageUnavailable,
|
||||
AnalyzerFailure,
|
||||
ExportFailure,
|
||||
SigningFailure,
|
||||
RuntimeFailure,
|
||||
Timeout,
|
||||
Cancelled,
|
||||
PluginViolation
|
||||
}
|
||||
|
||||
[JsonConverter(typeof(JsonStringEnumConverter<ScannerErrorSeverity>))]
|
||||
public enum ScannerErrorSeverity
|
||||
{
|
||||
Warning = 0,
|
||||
Error,
|
||||
Fatal
|
||||
}
|
||||
|
||||
public sealed class ScannerError
|
||||
{
|
||||
private static readonly IReadOnlyDictionary<string, string> EmptyDetails =
|
||||
new ReadOnlyDictionary<string, string>(new Dictionary<string, string>(0, StringComparer.Ordinal));
|
||||
|
||||
[JsonConstructor]
|
||||
public ScannerError(
|
||||
ScannerErrorCode code,
|
||||
ScannerErrorSeverity severity,
|
||||
string message,
|
||||
DateTimeOffset timestamp,
|
||||
bool retryable,
|
||||
IReadOnlyDictionary<string, string>? details = null,
|
||||
string? stage = null,
|
||||
string? component = null)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(message))
|
||||
{
|
||||
throw new ArgumentException("Error message cannot be null or whitespace.", nameof(message));
|
||||
}
|
||||
|
||||
Code = code;
|
||||
Severity = severity;
|
||||
Message = message.Trim();
|
||||
Timestamp = ScannerTimestamps.Normalize(timestamp);
|
||||
Retryable = retryable;
|
||||
Stage = stage;
|
||||
Component = component;
|
||||
Details = details is null or { Count: 0 }
|
||||
? EmptyDetails
|
||||
: new ReadOnlyDictionary<string, string>(new Dictionary<string, string>(details, StringComparer.Ordinal));
|
||||
}
|
||||
|
||||
[JsonPropertyName("code")]
|
||||
[JsonPropertyOrder(0)]
|
||||
public ScannerErrorCode Code { get; }
|
||||
|
||||
[JsonPropertyName("severity")]
|
||||
[JsonPropertyOrder(1)]
|
||||
public ScannerErrorSeverity Severity { get; }
|
||||
|
||||
[JsonPropertyName("message")]
|
||||
[JsonPropertyOrder(2)]
|
||||
public string Message { get; }
|
||||
|
||||
[JsonPropertyName("timestamp")]
|
||||
[JsonPropertyOrder(3)]
|
||||
public DateTimeOffset Timestamp { get; }
|
||||
|
||||
[JsonPropertyName("retryable")]
|
||||
[JsonPropertyOrder(4)]
|
||||
public bool Retryable { get; }
|
||||
|
||||
[JsonPropertyName("stage")]
|
||||
[JsonPropertyOrder(5)]
|
||||
public string? Stage { get; }
|
||||
|
||||
[JsonPropertyName("component")]
|
||||
[JsonPropertyOrder(6)]
|
||||
public string? Component { get; }
|
||||
|
||||
[JsonPropertyName("details")]
|
||||
[JsonPropertyOrder(7)]
|
||||
public IReadOnlyDictionary<string, string> Details { get; }
|
||||
|
||||
public ScannerError WithDetail(string key, string value)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(key);
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(value);
|
||||
|
||||
var mutable = new Dictionary<string, string>(Details, StringComparer.Ordinal)
|
||||
{
|
||||
[key] = value
|
||||
};
|
||||
|
||||
return new ScannerError(Code, Severity, Message, Timestamp, Retryable, mutable, Stage, Component);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user