Refactor code structure for improved readability and maintainability; optimize performance in key functions.
This commit is contained in:
@@ -0,0 +1,136 @@
|
||||
using System.Collections.Immutable;
|
||||
|
||||
namespace StellaOps.Testing.Manifests.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Captures all inputs required to reproduce a scan verdict deterministically.
|
||||
/// This is the replay key that enables time-travel verification.
|
||||
/// </summary>
|
||||
public sealed record RunManifest
|
||||
{
|
||||
/// <summary>
|
||||
/// Unique identifier for this run.
|
||||
/// </summary>
|
||||
public required string RunId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Schema version for forward compatibility.
|
||||
/// </summary>
|
||||
public string SchemaVersion { get; init; } = "1.0.0";
|
||||
|
||||
/// <summary>
|
||||
/// Artifact digests being scanned (image layers, binaries, etc.).
|
||||
/// </summary>
|
||||
public required ImmutableArray<ArtifactDigest> ArtifactDigests { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// SBOM digests produced or consumed during the run.
|
||||
/// </summary>
|
||||
public ImmutableArray<SbomReference> SbomDigests { get; init; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Vulnerability feed snapshot used for matching.
|
||||
/// </summary>
|
||||
public required FeedSnapshot FeedSnapshot { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Policy version and lattice rules digest.
|
||||
/// </summary>
|
||||
public required PolicySnapshot PolicySnapshot { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Tool versions used in the scan pipeline.
|
||||
/// </summary>
|
||||
public required ToolVersions ToolVersions { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Cryptographic profile: trust roots, key IDs, algorithm set.
|
||||
/// </summary>
|
||||
public required CryptoProfile CryptoProfile { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Environment profile: postgres-only vs postgres+valkey.
|
||||
/// </summary>
|
||||
public required EnvironmentProfile EnvironmentProfile { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// PRNG seed for any randomized operations (ensures reproducibility).
|
||||
/// </summary>
|
||||
public long? PrngSeed { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Canonicalization algorithm version for stable JSON output.
|
||||
/// </summary>
|
||||
public required string CanonicalizationVersion { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// UTC timestamp when the run was initiated.
|
||||
/// </summary>
|
||||
public required DateTimeOffset InitiatedAt { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// SHA-256 hash of this manifest (excluding this field).
|
||||
/// </summary>
|
||||
public string? ManifestDigest { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Artifact digest information.
|
||||
/// </summary>
|
||||
public sealed record ArtifactDigest(
|
||||
string Algorithm,
|
||||
string Digest,
|
||||
string? MediaType,
|
||||
string? Reference);
|
||||
|
||||
/// <summary>
|
||||
/// SBOM reference information.
|
||||
/// </summary>
|
||||
public sealed record SbomReference(
|
||||
string Format,
|
||||
string Digest,
|
||||
string? Uri);
|
||||
|
||||
/// <summary>
|
||||
/// Feed snapshot reference.
|
||||
/// </summary>
|
||||
public sealed record FeedSnapshot(
|
||||
string FeedId,
|
||||
string Version,
|
||||
string Digest,
|
||||
DateTimeOffset SnapshotAt);
|
||||
|
||||
/// <summary>
|
||||
/// Policy snapshot reference.
|
||||
/// </summary>
|
||||
public sealed record PolicySnapshot(
|
||||
string PolicyVersion,
|
||||
string LatticeRulesDigest,
|
||||
ImmutableArray<string> EnabledRules);
|
||||
|
||||
/// <summary>
|
||||
/// Toolchain versions used during the scan.
|
||||
/// </summary>
|
||||
public sealed record ToolVersions(
|
||||
string ScannerVersion,
|
||||
string SbomGeneratorVersion,
|
||||
string ReachabilityEngineVersion,
|
||||
string AttestorVersion,
|
||||
ImmutableDictionary<string, string> AdditionalTools);
|
||||
|
||||
/// <summary>
|
||||
/// Cryptographic profile for the run.
|
||||
/// </summary>
|
||||
public sealed record CryptoProfile(
|
||||
string ProfileName,
|
||||
ImmutableArray<string> TrustRootIds,
|
||||
ImmutableArray<string> AllowedAlgorithms);
|
||||
|
||||
/// <summary>
|
||||
/// Environment profile for determinism.
|
||||
/// </summary>
|
||||
public sealed record EnvironmentProfile(
|
||||
string Name,
|
||||
bool ValkeyEnabled,
|
||||
string? PostgresVersion,
|
||||
string? ValkeyVersion);
|
||||
@@ -0,0 +1,120 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://stellaops.io/schemas/run-manifest/v1",
|
||||
"title": "StellaOps Run Manifest",
|
||||
"description": "Captures all inputs for deterministic scan replay",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"runId",
|
||||
"schemaVersion",
|
||||
"artifactDigests",
|
||||
"feedSnapshot",
|
||||
"policySnapshot",
|
||||
"toolVersions",
|
||||
"cryptoProfile",
|
||||
"environmentProfile",
|
||||
"canonicalizationVersion",
|
||||
"initiatedAt"
|
||||
],
|
||||
"properties": {
|
||||
"runId": { "type": "string" },
|
||||
"schemaVersion": { "type": "string", "pattern": "^\\d+\\.\\d+\\.\\d+$" },
|
||||
"artifactDigests": {
|
||||
"type": "array",
|
||||
"items": { "$ref": "#/$defs/artifactDigest" },
|
||||
"minItems": 1
|
||||
},
|
||||
"sbomDigests": {
|
||||
"type": "array",
|
||||
"items": { "$ref": "#/$defs/sbomReference" }
|
||||
},
|
||||
"feedSnapshot": { "$ref": "#/$defs/feedSnapshot" },
|
||||
"policySnapshot": { "$ref": "#/$defs/policySnapshot" },
|
||||
"toolVersions": { "$ref": "#/$defs/toolVersions" },
|
||||
"cryptoProfile": { "$ref": "#/$defs/cryptoProfile" },
|
||||
"environmentProfile": { "$ref": "#/$defs/environmentProfile" },
|
||||
"prngSeed": { "type": ["integer", "null"] },
|
||||
"canonicalizationVersion": { "type": "string" },
|
||||
"initiatedAt": { "type": "string", "format": "date-time" },
|
||||
"manifestDigest": { "type": ["string", "null"] }
|
||||
},
|
||||
"$defs": {
|
||||
"artifactDigest": {
|
||||
"type": "object",
|
||||
"required": ["algorithm", "digest"],
|
||||
"properties": {
|
||||
"algorithm": { "enum": ["sha256", "sha512"] },
|
||||
"digest": { "type": "string", "pattern": "^[a-f0-9]{64,128}$" },
|
||||
"mediaType": { "type": ["string", "null"] },
|
||||
"reference": { "type": ["string", "null"] }
|
||||
}
|
||||
},
|
||||
"sbomReference": {
|
||||
"type": "object",
|
||||
"required": ["format", "digest"],
|
||||
"properties": {
|
||||
"format": { "type": "string" },
|
||||
"digest": { "type": "string" },
|
||||
"uri": { "type": ["string", "null"] }
|
||||
}
|
||||
},
|
||||
"feedSnapshot": {
|
||||
"type": "object",
|
||||
"required": ["feedId", "version", "digest", "snapshotAt"],
|
||||
"properties": {
|
||||
"feedId": { "type": "string" },
|
||||
"version": { "type": "string" },
|
||||
"digest": { "type": "string" },
|
||||
"snapshotAt": { "type": "string", "format": "date-time" }
|
||||
}
|
||||
},
|
||||
"policySnapshot": {
|
||||
"type": "object",
|
||||
"required": ["policyVersion", "latticeRulesDigest", "enabledRules"],
|
||||
"properties": {
|
||||
"policyVersion": { "type": "string" },
|
||||
"latticeRulesDigest": { "type": "string" },
|
||||
"enabledRules": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"toolVersions": {
|
||||
"type": "object",
|
||||
"required": ["scannerVersion", "sbomGeneratorVersion", "reachabilityEngineVersion", "attestorVersion", "additionalTools"],
|
||||
"properties": {
|
||||
"scannerVersion": { "type": "string" },
|
||||
"sbomGeneratorVersion": { "type": "string" },
|
||||
"reachabilityEngineVersion": { "type": "string" },
|
||||
"attestorVersion": { "type": "string" },
|
||||
"additionalTools": { "type": "object" }
|
||||
}
|
||||
},
|
||||
"cryptoProfile": {
|
||||
"type": "object",
|
||||
"required": ["profileName", "trustRootIds", "allowedAlgorithms"],
|
||||
"properties": {
|
||||
"profileName": { "type": "string" },
|
||||
"trustRootIds": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" }
|
||||
},
|
||||
"allowedAlgorithms": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"environmentProfile": {
|
||||
"type": "object",
|
||||
"required": ["name", "valkeyEnabled"],
|
||||
"properties": {
|
||||
"name": { "type": "string" },
|
||||
"valkeyEnabled": { "type": "boolean" },
|
||||
"postgresVersion": { "type": ["string", "null"] },
|
||||
"valkeyVersion": { "type": ["string", "null"] }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Encodings.Web;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using StellaOps.Canonical.Json;
|
||||
using StellaOps.Testing.Manifests.Models;
|
||||
|
||||
namespace StellaOps.Testing.Manifests.Serialization;
|
||||
|
||||
/// <summary>
|
||||
/// Serialize and hash RunManifest in canonical form.
|
||||
/// </summary>
|
||||
public static class RunManifestSerializer
|
||||
{
|
||||
private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web)
|
||||
{
|
||||
WriteIndented = false,
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
||||
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Serializes a manifest to canonical JSON.
|
||||
/// </summary>
|
||||
public static string Serialize(RunManifest manifest)
|
||||
{
|
||||
var jsonBytes = JsonSerializer.SerializeToUtf8Bytes(manifest, JsonOptions);
|
||||
var canonicalBytes = CanonJson.CanonicalizeParsedJson(jsonBytes);
|
||||
return Encoding.UTF8.GetString(canonicalBytes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deserializes a manifest from JSON.
|
||||
/// </summary>
|
||||
public static RunManifest Deserialize(string json)
|
||||
{
|
||||
return JsonSerializer.Deserialize<RunManifest>(json, JsonOptions)
|
||||
?? throw new InvalidOperationException("Failed to deserialize manifest");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes the SHA-256 digest of a manifest (excluding ManifestDigest).
|
||||
/// </summary>
|
||||
public static string ComputeDigest(RunManifest manifest)
|
||||
{
|
||||
var withoutDigest = manifest with { ManifestDigest = null };
|
||||
var json = Serialize(withoutDigest);
|
||||
var hash = SHA256.HashData(Encoding.UTF8.GetBytes(json));
|
||||
return Convert.ToHexString(hash).ToLowerInvariant();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a manifest with the digest computed and applied.
|
||||
/// </summary>
|
||||
public static RunManifest WithDigest(RunManifest manifest)
|
||||
=> manifest with { ManifestDigest = ComputeDigest(manifest) };
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
using System.Collections.Immutable;
|
||||
using StellaOps.Testing.Manifests.Models;
|
||||
using StellaOps.Testing.Manifests.Serialization;
|
||||
|
||||
namespace StellaOps.Testing.Manifests.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Captures a RunManifest during scan execution.
|
||||
/// </summary>
|
||||
public sealed class ManifestCaptureService : IManifestCaptureService
|
||||
{
|
||||
private readonly IFeedVersionProvider _feedProvider;
|
||||
private readonly IPolicyVersionProvider _policyProvider;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
|
||||
public ManifestCaptureService(
|
||||
IFeedVersionProvider feedProvider,
|
||||
IPolicyVersionProvider policyProvider,
|
||||
TimeProvider? timeProvider = null)
|
||||
{
|
||||
_feedProvider = feedProvider;
|
||||
_policyProvider = policyProvider;
|
||||
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||
}
|
||||
|
||||
public async Task<RunManifest> CaptureAsync(
|
||||
ScanContext context,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
var feedSnapshot = await _feedProvider.GetCurrentSnapshotAsync(ct).ConfigureAwait(false);
|
||||
var policySnapshot = await _policyProvider.GetCurrentSnapshotAsync(ct).ConfigureAwait(false);
|
||||
|
||||
var manifest = new RunManifest
|
||||
{
|
||||
RunId = context.RunId,
|
||||
SchemaVersion = "1.0.0",
|
||||
ArtifactDigests = context.ArtifactDigests,
|
||||
SbomDigests = context.GeneratedSboms,
|
||||
FeedSnapshot = feedSnapshot,
|
||||
PolicySnapshot = policySnapshot,
|
||||
ToolVersions = context.ToolVersions ?? GetToolVersions(),
|
||||
CryptoProfile = context.CryptoProfile,
|
||||
EnvironmentProfile = context.EnvironmentProfile ?? GetEnvironmentProfile(),
|
||||
PrngSeed = context.PrngSeed,
|
||||
CanonicalizationVersion = "1.0.0",
|
||||
InitiatedAt = _timeProvider.GetUtcNow()
|
||||
};
|
||||
|
||||
return RunManifestSerializer.WithDigest(manifest);
|
||||
}
|
||||
|
||||
private static ToolVersions GetToolVersions() => new(
|
||||
ScannerVersion: typeof(ManifestCaptureService).Assembly.GetName().Version?.ToString() ?? "unknown",
|
||||
SbomGeneratorVersion: "unknown",
|
||||
ReachabilityEngineVersion: "unknown",
|
||||
AttestorVersion: "unknown",
|
||||
AdditionalTools: ImmutableDictionary<string, string>.Empty);
|
||||
|
||||
private static EnvironmentProfile GetEnvironmentProfile() => new(
|
||||
Name: Environment.GetEnvironmentVariable("STELLAOPS_ENV_PROFILE") ?? "postgres-only",
|
||||
ValkeyEnabled: string.Equals(Environment.GetEnvironmentVariable("STELLAOPS_VALKEY_ENABLED"), "true", StringComparison.OrdinalIgnoreCase),
|
||||
PostgresVersion: Environment.GetEnvironmentVariable("STELLAOPS_POSTGRES_VERSION"),
|
||||
ValkeyVersion: Environment.GetEnvironmentVariable("STELLAOPS_VALKEY_VERSION"));
|
||||
}
|
||||
|
||||
public interface IManifestCaptureService
|
||||
{
|
||||
Task<RunManifest> CaptureAsync(ScanContext context, CancellationToken ct = default);
|
||||
}
|
||||
|
||||
public interface IFeedVersionProvider
|
||||
{
|
||||
Task<FeedSnapshot> GetCurrentSnapshotAsync(CancellationToken ct = default);
|
||||
}
|
||||
|
||||
public interface IPolicyVersionProvider
|
||||
{
|
||||
Task<PolicySnapshot> GetCurrentSnapshotAsync(CancellationToken ct = default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Input context required to capture a RunManifest.
|
||||
/// </summary>
|
||||
public sealed record ScanContext
|
||||
{
|
||||
public required string RunId { get; init; }
|
||||
public required ImmutableArray<ArtifactDigest> ArtifactDigests { get; init; }
|
||||
public ImmutableArray<SbomReference> GeneratedSboms { get; init; } = [];
|
||||
public required CryptoProfile CryptoProfile { get; init; }
|
||||
public ToolVersions? ToolVersions { get; init; }
|
||||
public EnvironmentProfile? EnvironmentProfile { get; init; }
|
||||
public long? PrngSeed { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<LangVersion>preview</LangVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Json.Schema.Net" Version="7.2.0" />
|
||||
<PackageReference Include="System.Collections.Immutable" Version="9.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\StellaOps.Canonical.Json\StellaOps.Canonical.Json.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="Schemas\*.json" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,64 @@
|
||||
using System.Text.Json;
|
||||
using Json.Schema;
|
||||
using StellaOps.Testing.Manifests.Models;
|
||||
using StellaOps.Testing.Manifests.Serialization;
|
||||
|
||||
namespace StellaOps.Testing.Manifests.Validation;
|
||||
|
||||
/// <summary>
|
||||
/// Validates RunManifest instances against schema and invariants.
|
||||
/// </summary>
|
||||
public sealed class RunManifestValidator : IRunManifestValidator
|
||||
{
|
||||
private readonly JsonSchema _schema;
|
||||
|
||||
public RunManifestValidator()
|
||||
{
|
||||
var schemaJson = SchemaLoader.LoadSchema("run-manifest.schema.json");
|
||||
_schema = JsonSchema.FromText(schemaJson);
|
||||
}
|
||||
|
||||
public ValidationResult Validate(RunManifest manifest)
|
||||
{
|
||||
var errors = new List<ValidationError>();
|
||||
|
||||
var json = RunManifestSerializer.Serialize(manifest);
|
||||
var schemaResult = _schema.Evaluate(JsonDocument.Parse(json));
|
||||
if (!schemaResult.IsValid)
|
||||
{
|
||||
foreach (var error in schemaResult.Errors)
|
||||
{
|
||||
errors.Add(new ValidationError("Schema", error.Message));
|
||||
}
|
||||
}
|
||||
|
||||
if (manifest.ArtifactDigests.Length == 0)
|
||||
{
|
||||
errors.Add(new ValidationError("ArtifactDigests", "At least one artifact required"));
|
||||
}
|
||||
|
||||
if (manifest.FeedSnapshot.SnapshotAt > manifest.InitiatedAt)
|
||||
{
|
||||
errors.Add(new ValidationError("FeedSnapshot", "Feed snapshot cannot be after run initiation"));
|
||||
}
|
||||
|
||||
if (manifest.ManifestDigest is not null)
|
||||
{
|
||||
var computed = RunManifestSerializer.ComputeDigest(manifest);
|
||||
if (!string.Equals(computed, manifest.ManifestDigest, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
errors.Add(new ValidationError("ManifestDigest", "Digest mismatch"));
|
||||
}
|
||||
}
|
||||
|
||||
return new ValidationResult(errors.Count == 0, errors);
|
||||
}
|
||||
}
|
||||
|
||||
public interface IRunManifestValidator
|
||||
{
|
||||
ValidationResult Validate(RunManifest manifest);
|
||||
}
|
||||
|
||||
public sealed record ValidationResult(bool IsValid, IReadOnlyList<ValidationError> Errors);
|
||||
public sealed record ValidationError(string Field, string Message);
|
||||
@@ -0,0 +1,27 @@
|
||||
using System.Reflection;
|
||||
|
||||
namespace StellaOps.Testing.Manifests.Validation;
|
||||
|
||||
internal static class SchemaLoader
|
||||
{
|
||||
public static string LoadSchema(string fileName)
|
||||
{
|
||||
var assembly = Assembly.GetExecutingAssembly();
|
||||
var resourceName = assembly.GetManifestResourceNames()
|
||||
.FirstOrDefault(name => name.EndsWith(fileName, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (resourceName is null)
|
||||
{
|
||||
throw new InvalidOperationException($"Schema resource not found: {fileName}");
|
||||
}
|
||||
|
||||
using var stream = assembly.GetManifestResourceStream(resourceName);
|
||||
if (stream is null)
|
||||
{
|
||||
throw new InvalidOperationException($"Schema resource not available: {resourceName}");
|
||||
}
|
||||
|
||||
using var reader = new StreamReader(stream);
|
||||
return reader.ReadToEnd();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user