Add unit tests for SBOM ingestion and transformation
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.
This commit is contained in:
master
2025-11-04 07:49:39 +02:00
parent f72c5c513a
commit 2eb6852d34
491 changed files with 39445 additions and 3917 deletions

View File

@@ -265,13 +265,16 @@ public sealed record PolicyRunStatus
int attempts = 0,
string? traceId = null,
string? explainUri = null,
ImmutableSortedDictionary<string, string>? metadata = null,
string? schemaVersion = null)
: this(
runId,
tenantId,
policyId,
policyVersion,
ImmutableSortedDictionary<string, string>? metadata = null,
bool cancellationRequested = false,
DateTimeOffset? cancellationRequestedAt = null,
string? cancellationReason = null,
string? schemaVersion = null)
: this(
runId,
tenantId,
policyId,
policyVersion,
mode,
status,
priority,
@@ -282,16 +285,19 @@ public sealed record PolicyRunStatus
inputs ?? PolicyRunInputs.Empty,
determinismHash,
Validation.TrimToNull(errorCode),
Validation.TrimToNull(error),
attempts,
Validation.TrimToNull(traceId),
Validation.TrimToNull(explainUri),
metadata ?? ImmutableSortedDictionary<string, string>.Empty,
schemaVersion)
{
}
[JsonConstructor]
Validation.TrimToNull(error),
attempts,
Validation.TrimToNull(traceId),
Validation.TrimToNull(explainUri),
metadata ?? ImmutableSortedDictionary<string, string>.Empty,
cancellationRequested,
cancellationRequestedAt,
cancellationReason,
schemaVersion)
{
}
[JsonConstructor]
public PolicyRunStatus(
string runId,
string tenantId,
@@ -307,12 +313,15 @@ public sealed record PolicyRunStatus
PolicyRunInputs inputs,
string? determinismHash,
string? errorCode,
string? error,
int attempts,
string? traceId,
string? explainUri,
ImmutableSortedDictionary<string, string> metadata,
string? schemaVersion = null)
string? error,
int attempts,
string? traceId,
string? explainUri,
ImmutableSortedDictionary<string, string> metadata,
bool cancellationRequested,
DateTimeOffset? cancellationRequestedAt,
string? cancellationReason,
string? schemaVersion = null)
{
SchemaVersion = SchedulerSchemaVersions.EnsurePolicyRunStatus(schemaVersion);
RunId = Validation.EnsureId(runId, nameof(runId));
@@ -339,16 +348,19 @@ public sealed record PolicyRunStatus
? throw new ArgumentOutOfRangeException(nameof(attempts), attempts, "Attempts must be non-negative.")
: attempts;
TraceId = Validation.TrimToNull(traceId);
ExplainUri = Validation.TrimToNull(explainUri);
Metadata = (metadata ?? ImmutableSortedDictionary<string, string>.Empty)
.Select(static pair => new KeyValuePair<string, string>(
Validation.TrimToNull(pair.Key)?.ToLowerInvariant() ?? string.Empty,
Validation.TrimToNull(pair.Value) ?? string.Empty))
.Where(static pair => !string.IsNullOrEmpty(pair.Key) && !string.IsNullOrEmpty(pair.Value))
.DistinctBy(static pair => pair.Key, StringComparer.Ordinal)
.OrderBy(static pair => pair.Key, StringComparer.Ordinal)
.ToImmutableSortedDictionary(static pair => pair.Key, static pair => pair.Value, StringComparer.Ordinal);
}
ExplainUri = Validation.TrimToNull(explainUri);
Metadata = (metadata ?? ImmutableSortedDictionary<string, string>.Empty)
.Select(static pair => new KeyValuePair<string, string>(
Validation.TrimToNull(pair.Key)?.ToLowerInvariant() ?? string.Empty,
Validation.TrimToNull(pair.Value) ?? string.Empty))
.Where(static pair => !string.IsNullOrEmpty(pair.Key) && !string.IsNullOrEmpty(pair.Value))
.DistinctBy(static pair => pair.Key, StringComparer.Ordinal)
.OrderBy(static pair => pair.Key, StringComparer.Ordinal)
.ToImmutableSortedDictionary(static pair => pair.Key, static pair => pair.Value, StringComparer.Ordinal);
CancellationRequested = cancellationRequested;
CancellationRequestedAt = Validation.NormalizeTimestamp(cancellationRequestedAt);
CancellationReason = Validation.TrimToNull(cancellationReason);
}
public string SchemaVersion { get; }
@@ -392,13 +404,22 @@ public sealed record PolicyRunStatus
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? ExplainUri { get; init; }
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
public ImmutableSortedDictionary<string, string> Metadata { get; init; } = ImmutableSortedDictionary<string, string>.Empty;
public PolicyRunStats Stats { get; init; } = PolicyRunStats.Empty;
public PolicyRunInputs Inputs { get; init; } = PolicyRunInputs.Empty;
}
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
public ImmutableSortedDictionary<string, string> Metadata { get; init; } = ImmutableSortedDictionary<string, string>.Empty;
public PolicyRunStats Stats { get; init; } = PolicyRunStats.Empty;
public PolicyRunInputs Inputs { get; init; } = PolicyRunInputs.Empty;
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
public bool CancellationRequested { get; init; }
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public DateTimeOffset? CancellationRequestedAt { get; init; }
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? CancellationReason { get; init; }
}
/// <summary>
/// Aggregated metrics captured for a policy run.

View File

@@ -0,0 +1,62 @@
using System;
using System.Collections.Immutable;
namespace StellaOps.Scheduler.Models;
/// <summary>
/// Shared helper for translating persisted <see cref="PolicyRunJob"/> documents into
/// API-facing <see cref="PolicyRunStatus"/> projections.
/// </summary>
public static class PolicyRunStatusFactory
{
public static PolicyRunStatus Create(PolicyRunJob job, DateTimeOffset nowUtc)
{
ArgumentNullException.ThrowIfNull(job);
var status = MapExecutionStatus(job.Status);
var queuedAt = job.QueuedAt ?? job.CreatedAt;
var startedAt = job.SubmittedAt;
var finishedAt = job.CompletedAt ?? job.CancelledAt;
var metadata = job.Metadata ?? ImmutableSortedDictionary<string, string>.Empty;
var inputs = job.Inputs ?? PolicyRunInputs.Empty;
var policyVersion = job.PolicyVersion
?? throw new InvalidOperationException($"Policy run job '{job.Id}' is missing policyVersion.");
return new PolicyRunStatus(
job.RunId ?? job.Id,
job.TenantId,
job.PolicyId,
policyVersion,
job.Mode,
status,
job.Priority,
queuedAt,
job.Status == PolicyRunJobStatus.Pending ? null : startedAt,
finishedAt,
PolicyRunStats.Empty,
inputs,
determinismHash: null,
errorCode: null,
error: job.Status == PolicyRunJobStatus.Failed ? job.LastError : null,
attempts: job.AttemptCount,
traceId: null,
explainUri: null,
metadata,
cancellationRequested: job.CancellationRequested,
cancellationRequestedAt: job.CancellationRequestedAt,
cancellationReason: job.CancellationReason,
SchedulerSchemaVersions.PolicyRunStatus);
}
private static PolicyRunExecutionStatus MapExecutionStatus(PolicyRunJobStatus status)
=> status switch
{
PolicyRunJobStatus.Pending => PolicyRunExecutionStatus.Queued,
PolicyRunJobStatus.Dispatching => PolicyRunExecutionStatus.Running,
PolicyRunJobStatus.Submitted => PolicyRunExecutionStatus.Running,
PolicyRunJobStatus.Completed => PolicyRunExecutionStatus.Succeeded,
PolicyRunJobStatus.Failed => PolicyRunExecutionStatus.Failed,
PolicyRunJobStatus.Cancelled => PolicyRunExecutionStatus.Cancelled,
_ => PolicyRunExecutionStatus.Queued
};
}

View File

@@ -0,0 +1,65 @@
using System;
using System.Text.Json.Serialization;
namespace StellaOps.Scheduler.Models;
public sealed record PolicySimulationWebhookPayload(
[property: JsonPropertyName("tenantId")] string TenantId,
[property: JsonPropertyName("simulation")] PolicyRunStatus Simulation,
[property: JsonPropertyName("result")] string Result,
[property: JsonPropertyName("observedAt")] DateTimeOffset ObservedAt,
[property: JsonPropertyName("latencySeconds")] double? LatencySeconds,
[property: JsonPropertyName("reason")] string? Reason);
public static class PolicySimulationWebhookPayloadFactory
{
public static PolicySimulationWebhookPayload Create(PolicyRunStatus status, DateTimeOffset observedAt)
{
ArgumentNullException.ThrowIfNull(status);
var result = status.Status switch
{
PolicyRunExecutionStatus.Succeeded => "succeeded",
PolicyRunExecutionStatus.Failed => "failed",
PolicyRunExecutionStatus.Cancelled => "cancelled",
PolicyRunExecutionStatus.ReplayPending => "replay_pending",
PolicyRunExecutionStatus.Running => "running",
_ => "queued"
};
var latencySeconds = CalculateLatencySeconds(status, observedAt);
var reason = status.Status switch
{
PolicyRunExecutionStatus.Failed => status.Error,
PolicyRunExecutionStatus.Cancelled => status.CancellationReason,
_ => null
};
return new PolicySimulationWebhookPayload(
status.TenantId,
status,
result,
observedAt,
latencySeconds,
reason);
}
private static double? CalculateLatencySeconds(PolicyRunStatus status, DateTimeOffset observedAt)
{
var started = status.QueuedAt;
var finished = status.FinishedAt ?? observedAt;
if (started == default)
{
return null;
}
var duration = (finished - started).TotalSeconds;
if (duration < 0)
{
duration = 0;
}
return Math.Round(duration, 4);
}
}

View File

@@ -21,6 +21,7 @@ public sealed record Run
DateTimeOffset? finishedAt = null,
string? error = null,
IEnumerable<DeltaSummary>? deltas = null,
string? retryOf = null,
string? schemaVersion = null)
: this(
id,
@@ -35,6 +36,7 @@ public sealed record Run
Validation.NormalizeTimestamp(finishedAt),
Validation.TrimToNull(error),
NormalizeDeltas(deltas),
Validation.TrimToNull(retryOf),
schemaVersion)
{
}
@@ -53,6 +55,7 @@ public sealed record Run
DateTimeOffset? finishedAt,
string? error,
ImmutableArray<DeltaSummary> deltas,
string? retryOf,
string? schemaVersion = null)
{
Id = Validation.EnsureId(id, nameof(id));
@@ -69,6 +72,7 @@ public sealed record Run
Deltas = deltas.IsDefault
? ImmutableArray<DeltaSummary>.Empty
: deltas.OrderBy(static delta => delta.ImageDigest, StringComparer.Ordinal).ToImmutableArray();
RetryOf = Validation.TrimToNull(retryOf);
SchemaVersion = SchedulerSchemaVersions.EnsureRun(schemaVersion);
}
@@ -103,6 +107,9 @@ public sealed record Run
[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)