feat: Add UI benchmark driver and scenarios for graph interactions
Some checks failed
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Docs CI / lint-and-preview (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
devportal-offline / build-offline (push) Has been cancelled

- Introduced `ui_bench_driver.mjs` to read scenarios and fixture manifest, generating a deterministic run plan.
- Created `ui_bench_plan.md` outlining the purpose, scope, and next steps for the benchmark.
- Added `ui_bench_scenarios.json` containing various scenarios for graph UI interactions.
- Implemented tests for CLI commands, ensuring bundle verification and telemetry defaults.
- Developed schemas for orchestrator components, including replay manifests and event envelopes.
- Added mock API for risk management, including listing and statistics functionalities.
- Implemented models for risk profiles and query options to support the new API.
This commit is contained in:
StellaOps Bot
2025-12-02 01:28:17 +02:00
parent 909d9b6220
commit 44171930ff
94 changed files with 3606 additions and 271 deletions

View File

@@ -24,11 +24,11 @@ public sealed class CycloneDxComposer
private const string InventoryMediaTypeProtobuf = "application/vnd.cyclonedx+protobuf; version=1.6";
private const string UsageMediaTypeProtobuf = "application/vnd.cyclonedx+protobuf; version=1.6; view=usage";
public SbomCompositionResult Compose(SbomCompositionRequest request)
{
ArgumentNullException.ThrowIfNull(request);
if (request.LayerFragments.IsDefaultOrEmpty)
{
public SbomCompositionResult Compose(SbomCompositionRequest request)
{
ArgumentNullException.ThrowIfNull(request);
if (request.LayerFragments.IsDefaultOrEmpty)
{
throw new ArgumentException("At least one layer fragment is required.", nameof(request));
}
@@ -48,9 +48,9 @@ public sealed class CycloneDxComposer
.Where(static component => component.Usage.UsedByEntrypoint)
.ToImmutableArray();
CycloneDxArtifact? usageArtifact = null;
if (!usageComponents.IsEmpty)
{
CycloneDxArtifact? usageArtifact = null;
if (!usageComponents.IsEmpty)
{
usageArtifact = BuildArtifact(
request,
graph,
@@ -59,15 +59,36 @@ public sealed class CycloneDxComposer
generatedAt,
UsageMediaTypeJson,
UsageMediaTypeProtobuf);
}
return new SbomCompositionResult
{
Inventory = inventoryArtifact,
Usage = usageArtifact,
Graph = graph,
};
}
}
var compositionRecipeJson = BuildCompositionRecipeJson(graph, generatedAt);
var compositionRecipeSha = ComputeSha256(compositionRecipeJson);
var compositionRecipeUri = $"cas://sbom/composition/{compositionRecipeSha}.json";
inventoryArtifact = inventoryArtifact with
{
MerkleRoot = compositionRecipeSha,
CompositionRecipeUri = compositionRecipeUri,
};
if (usageArtifact is not null)
{
usageArtifact = usageArtifact with
{
MerkleRoot = compositionRecipeSha,
CompositionRecipeUri = compositionRecipeUri,
};
}
return new SbomCompositionResult
{
Inventory = inventoryArtifact,
Usage = usageArtifact,
Graph = graph,
CompositionRecipeJson = compositionRecipeJson,
CompositionRecipeSha256 = compositionRecipeSha,
};
}
private CycloneDxArtifact BuildArtifact(
SbomCompositionRequest request,
@@ -92,6 +113,7 @@ public sealed class CycloneDxComposer
: null;
request.AdditionalProperties?.TryGetValue("stellaops:composition.manifest", out var compositionUri);
request.AdditionalProperties?.TryGetValue("stellaops:composition.recipe", out var compositionRecipeUri);
return new CycloneDxArtifact
{
@@ -104,12 +126,38 @@ public sealed class CycloneDxComposer
ContentHash = jsonHash,
MerkleRoot = merkleRoot,
CompositionUri = compositionUri,
CompositionRecipeUri = compositionRecipeUri,
JsonMediaType = jsonMediaType,
ProtobufBytes = protobufBytes,
ProtobufSha256 = protobufHash,
ProtobufMediaType = protobufMediaType,
};
}
private static byte[] BuildCompositionRecipeJson(ComponentGraph graph, DateTimeOffset generatedAt)
{
var recipe = new
{
schema = "stellaops.composition.recipe@1",
generatedAt = ScannerTimestamps.ToIso8601(generatedAt),
layers = graph.Layers.Select(layer => new
{
layer.LayerDigest,
components = layer.Components
.Select(component => component.Identity.Key)
.OrderBy(key => key, StringComparer.Ordinal)
.ToArray(),
}).OrderBy(entry => entry.LayerDigest, StringComparer.Ordinal).ToArray(),
};
var json = JsonSerializer.Serialize(recipe, new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
WriteIndented = false,
});
return Encoding.UTF8.GetBytes(json);
}
private Bom BuildBom(
SbomCompositionRequest request,

View File

@@ -33,6 +33,11 @@ public sealed record CycloneDxArtifact
/// </summary>
public string? CompositionUri { get; init; }
/// <summary>
/// CAS URI of the layer composition recipe (_composition.json) if emitted.
/// </summary>
public string? CompositionRecipeUri { get; init; }
public required string JsonMediaType { get; init; }
public required byte[] ProtobufBytes { get; init; }
@@ -42,11 +47,21 @@ public sealed record CycloneDxArtifact
public required string ProtobufMediaType { get; init; }
}
public sealed record SbomCompositionResult
{
public required CycloneDxArtifact Inventory { get; init; }
public CycloneDxArtifact? Usage { get; init; }
public required ComponentGraph Graph { get; init; }
}
public sealed record SbomCompositionResult
{
public required CycloneDxArtifact Inventory { get; init; }
public CycloneDxArtifact? Usage { get; init; }
public required ComponentGraph Graph { get; init; }
/// <summary>
/// Composition recipe JSON bytes (canonical) capturing fragment ordering and hashes.
/// </summary>
public required byte[] CompositionRecipeJson { get; init; }
/// <summary>
/// SHA256 hex of the composition recipe JSON.
/// </summary>
public required string CompositionRecipeSha256 { get; init; }
}

View File

@@ -88,18 +88,26 @@ public sealed class ScannerArtifactPackageBuilder
descriptors.Add(CreateDescriptor(ArtifactDocumentType.ImageBom, ArtifactDocumentFormat.CycloneDxProtobuf, composition.Usage.ProtobufMediaType, composition.Usage.ProtobufBytes, composition.Usage.ProtobufSha256, SbomView.Usage));
}
descriptors.Add(CreateDescriptor(ArtifactDocumentType.Index, ArtifactDocumentFormat.BomIndex, "application/vnd.stellaops.bom-index.v1+binary", bomIndex.Bytes, bomIndex.Sha256, null));
descriptors.Add(CreateDescriptor(ArtifactDocumentType.Index, ArtifactDocumentFormat.BomIndex, "application/vnd.stellaops.bom-index.v1+binary", bomIndex.Bytes, bomIndex.Sha256, null));
descriptors.Add(CreateDescriptor(
ArtifactDocumentType.CompositionRecipe,
ArtifactDocumentFormat.CompositionRecipeJson,
"application/vnd.stellaops.composition.recipe+json",
composition.CompositionRecipeJson,
composition.CompositionRecipeSha256,
null));
var manifest = new ScannerArtifactManifest
{
ImageDigest = imageDigest.Trim(),
GeneratedAt = generatedAt,
Artifacts = descriptors
.Select(ToManifestEntry)
.OrderBy(entry => entry.Kind, StringComparer.Ordinal)
.ThenBy(entry => entry.Format)
.ToImmutableArray(),
};
var manifest = new ScannerArtifactManifest
{
ImageDigest = imageDigest.Trim(),
GeneratedAt = generatedAt,
Artifacts = descriptors
.Select(ToManifestEntry)
.OrderBy(entry => entry.Kind, StringComparer.Ordinal)
.ThenBy(entry => entry.Format)
.ToImmutableArray(),
};
return new ScannerArtifactPackage
{
@@ -136,9 +144,10 @@ public sealed class ScannerArtifactPackageBuilder
ArtifactDocumentType.ImageBom => "sbom-inventory",
ArtifactDocumentType.LayerBom => "layer-sbom",
ArtifactDocumentType.Diff => "diff",
ArtifactDocumentType.Attestation => "attestation",
_ => descriptor.Type.ToString().ToLowerInvariant(),
};
ArtifactDocumentType.Attestation => "attestation",
ArtifactDocumentType.CompositionRecipe => "composition-recipe",
_ => descriptor.Type.ToString().ToLowerInvariant(),
};
return new ScannerArtifactManifestEntry
{

View File

@@ -12,7 +12,8 @@ public enum ArtifactDocumentType
SurfaceManifest,
SurfaceEntryTrace,
SurfaceLayerFragment,
SurfaceObservation
SurfaceObservation,
CompositionRecipe
}
public enum ArtifactDocumentFormat
@@ -26,7 +27,8 @@ public enum ArtifactDocumentFormat
EntryTraceNdjson,
EntryTraceGraphJson,
ComponentFragmentJson,
ObservationJson
ObservationJson,
CompositionRecipeJson
}
[BsonIgnoreExtraElements]