doctor enhancements, setup, enhancements, ui functionality and design consolidation and , test projects fixes , product advisory attestation/rekor and delta verfications enhancements
This commit is contained in:
@@ -0,0 +1,234 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// BundleFormatV2.cs
|
||||
// Sprint: SPRINT_20260118_018_AirGap_router_integration
|
||||
// Task: TASK-018-001 - Complete Air-Gap Bundle Format
|
||||
// Description: Air-gap bundle format v2.0.0 matching advisory specification
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using System.Collections.Immutable;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace StellaOps.AirGap.Bundle.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Air-gap bundle manifest v2.0.0 per advisory specification.
|
||||
/// </summary>
|
||||
public sealed record BundleManifestV2
|
||||
{
|
||||
/// <summary>Schema version.</summary>
|
||||
[JsonPropertyName("schemaVersion")]
|
||||
public string SchemaVersion { get; init; } = "2.0.0";
|
||||
|
||||
/// <summary>Bundle information.</summary>
|
||||
[JsonPropertyName("bundle")]
|
||||
public required BundleInfoV2 Bundle { get; init; }
|
||||
|
||||
/// <summary>Verification configuration.</summary>
|
||||
[JsonPropertyName("verify")]
|
||||
public BundleVerifySection? Verify { get; init; }
|
||||
|
||||
/// <summary>Bundle metadata.</summary>
|
||||
[JsonPropertyName("metadata")]
|
||||
public BundleMetadata? Metadata { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bundle information.
|
||||
/// </summary>
|
||||
public sealed record BundleInfoV2
|
||||
{
|
||||
/// <summary>Primary image reference.</summary>
|
||||
[JsonPropertyName("image")]
|
||||
public required string Image { get; init; }
|
||||
|
||||
/// <summary>Image digest.</summary>
|
||||
[JsonPropertyName("digest")]
|
||||
public string? Digest { get; init; }
|
||||
|
||||
/// <summary>Bundle artifacts.</summary>
|
||||
[JsonPropertyName("artifacts")]
|
||||
public required ImmutableArray<BundleArtifact> Artifacts { get; init; }
|
||||
|
||||
/// <summary>OCI referrer manifest.</summary>
|
||||
[JsonPropertyName("referrers")]
|
||||
public OciReferrerIndex? Referrers { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bundle artifact entry.
|
||||
/// </summary>
|
||||
public sealed record BundleArtifact
|
||||
{
|
||||
/// <summary>Path within bundle.</summary>
|
||||
[JsonPropertyName("path")]
|
||||
public required string Path { get; init; }
|
||||
|
||||
/// <summary>Artifact type.</summary>
|
||||
[JsonPropertyName("type")]
|
||||
public BundleArtifactType Type { get; init; }
|
||||
|
||||
/// <summary>Content digest (sha256).</summary>
|
||||
[JsonPropertyName("digest")]
|
||||
public string? Digest { get; init; }
|
||||
|
||||
/// <summary>Media type.</summary>
|
||||
[JsonPropertyName("mediaType")]
|
||||
public string? MediaType { get; init; }
|
||||
|
||||
/// <summary>Size in bytes.</summary>
|
||||
[JsonPropertyName("size")]
|
||||
public long Size { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bundle artifact type.
|
||||
/// </summary>
|
||||
[JsonConverter(typeof(JsonStringEnumConverter))]
|
||||
public enum BundleArtifactType
|
||||
{
|
||||
/// <summary>SBOM document.</summary>
|
||||
[JsonPropertyName("sbom")]
|
||||
Sbom,
|
||||
|
||||
/// <summary>DSSE-signed SBOM statement.</summary>
|
||||
[JsonPropertyName("sbom.dsse")]
|
||||
SbomDsse,
|
||||
|
||||
/// <summary>VEX document.</summary>
|
||||
[JsonPropertyName("vex")]
|
||||
Vex,
|
||||
|
||||
/// <summary>DSSE-signed VEX statement.</summary>
|
||||
[JsonPropertyName("vex.dsse")]
|
||||
VexDsse,
|
||||
|
||||
/// <summary>Rekor inclusion proof.</summary>
|
||||
[JsonPropertyName("rekor.proof")]
|
||||
RekorProof,
|
||||
|
||||
/// <summary>OCI referrers index.</summary>
|
||||
[JsonPropertyName("oci.referrers")]
|
||||
OciReferrers,
|
||||
|
||||
/// <summary>Policy snapshot.</summary>
|
||||
[JsonPropertyName("policy")]
|
||||
Policy,
|
||||
|
||||
/// <summary>Feed snapshot.</summary>
|
||||
[JsonPropertyName("feed")]
|
||||
Feed,
|
||||
|
||||
/// <summary>Rekor checkpoint.</summary>
|
||||
[JsonPropertyName("rekor.checkpoint")]
|
||||
RekorCheckpoint,
|
||||
|
||||
/// <summary>Other/generic artifact.</summary>
|
||||
[JsonPropertyName("other")]
|
||||
Other
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bundle verification section.
|
||||
/// </summary>
|
||||
public sealed record BundleVerifySection
|
||||
{
|
||||
/// <summary>Trusted signing keys.</summary>
|
||||
[JsonPropertyName("keys")]
|
||||
public ImmutableArray<string> Keys { get; init; } = [];
|
||||
|
||||
/// <summary>Verification expectations.</summary>
|
||||
[JsonPropertyName("expectations")]
|
||||
public VerifyExpectations? Expectations { get; init; }
|
||||
|
||||
/// <summary>Certificate roots for verification.</summary>
|
||||
[JsonPropertyName("certificateRoots")]
|
||||
public ImmutableArray<string> CertificateRoots { get; init; } = [];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verification expectations.
|
||||
/// </summary>
|
||||
public sealed record VerifyExpectations
|
||||
{
|
||||
/// <summary>Expected payload types.</summary>
|
||||
[JsonPropertyName("payloadTypes")]
|
||||
public ImmutableArray<string> PayloadTypes { get; init; } = [];
|
||||
|
||||
/// <summary>Whether Rekor inclusion is required.</summary>
|
||||
[JsonPropertyName("rekorRequired")]
|
||||
public bool RekorRequired { get; init; }
|
||||
|
||||
/// <summary>Expected issuers.</summary>
|
||||
[JsonPropertyName("issuers")]
|
||||
public ImmutableArray<string> Issuers { get; init; } = [];
|
||||
|
||||
/// <summary>Minimum signature count.</summary>
|
||||
[JsonPropertyName("minSignatures")]
|
||||
public int MinSignatures { get; init; } = 1;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// OCI referrer index.
|
||||
/// </summary>
|
||||
public sealed record OciReferrerIndex
|
||||
{
|
||||
/// <summary>Referrer descriptors.</summary>
|
||||
[JsonPropertyName("manifests")]
|
||||
public ImmutableArray<OciReferrerDescriptor> Manifests { get; init; } = [];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// OCI referrer descriptor.
|
||||
/// </summary>
|
||||
public sealed record OciReferrerDescriptor
|
||||
{
|
||||
/// <summary>Media type.</summary>
|
||||
[JsonPropertyName("mediaType")]
|
||||
public required string MediaType { get; init; }
|
||||
|
||||
/// <summary>Digest.</summary>
|
||||
[JsonPropertyName("digest")]
|
||||
public required string Digest { get; init; }
|
||||
|
||||
/// <summary>Artifact type.</summary>
|
||||
[JsonPropertyName("artifactType")]
|
||||
public string? ArtifactType { get; init; }
|
||||
|
||||
/// <summary>Size.</summary>
|
||||
[JsonPropertyName("size")]
|
||||
public long Size { get; init; }
|
||||
|
||||
/// <summary>Annotations.</summary>
|
||||
[JsonPropertyName("annotations")]
|
||||
public IReadOnlyDictionary<string, string>? Annotations { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bundle metadata.
|
||||
/// </summary>
|
||||
public sealed record BundleMetadata
|
||||
{
|
||||
/// <summary>When bundle was created.</summary>
|
||||
[JsonPropertyName("createdAt")]
|
||||
public DateTimeOffset CreatedAt { get; init; }
|
||||
|
||||
/// <summary>Bundle creator.</summary>
|
||||
[JsonPropertyName("createdBy")]
|
||||
public string? CreatedBy { get; init; }
|
||||
|
||||
/// <summary>Bundle description.</summary>
|
||||
[JsonPropertyName("description")]
|
||||
public string? Description { get; init; }
|
||||
|
||||
/// <summary>Source environment.</summary>
|
||||
[JsonPropertyName("sourceEnvironment")]
|
||||
public string? SourceEnvironment { get; init; }
|
||||
|
||||
/// <summary>Target environment.</summary>
|
||||
[JsonPropertyName("targetEnvironment")]
|
||||
public string? TargetEnvironment { get; init; }
|
||||
|
||||
/// <summary>Additional labels.</summary>
|
||||
[JsonPropertyName("labels")]
|
||||
public IReadOnlyDictionary<string, string>? Labels { get; init; }
|
||||
}
|
||||
@@ -5,11 +5,12 @@ namespace StellaOps.AirGap.Bundle.Models;
|
||||
/// <summary>
|
||||
/// Manifest for an offline bundle, inventorying all components with content digests.
|
||||
/// Used for integrity verification and completeness checking in air-gapped environments.
|
||||
/// Sprint: SPRINT_20260118_018 (TASK-018-001) - Updated to v2.0.0
|
||||
/// </summary>
|
||||
public sealed record BundleManifest
|
||||
{
|
||||
public required string BundleId { get; init; }
|
||||
public string SchemaVersion { get; init; } = "1.0.0";
|
||||
public string SchemaVersion { get; init; } = "2.0.0";
|
||||
public required string Name { get; init; }
|
||||
public required string Version { get; init; }
|
||||
public required DateTimeOffset CreatedAt { get; init; }
|
||||
@@ -23,6 +24,103 @@ public sealed record BundleManifest
|
||||
public ImmutableArray<RuleBundleComponent> RuleBundles { get; init; } = [];
|
||||
public long TotalSizeBytes { get; init; }
|
||||
public string? BundleDigest { get; init; }
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// v2.0.0 Additions - Sprint: SPRINT_20260118_018 (TASK-018-001)
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// Image reference this bundle is for (advisory-specified format).
|
||||
/// Example: "registry.example.com/app@sha256:..."
|
||||
/// </summary>
|
||||
public string? Image { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// List of artifacts in the bundle with path and type information.
|
||||
/// </summary>
|
||||
public ImmutableArray<BundleArtifact> Artifacts { get; init; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Verification section with keys and expectations.
|
||||
/// </summary>
|
||||
public BundleVerifySection? Verify { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Artifact entry in a bundle (v2.0.0).
|
||||
/// Sprint: SPRINT_20260118_018 (TASK-018-001)
|
||||
/// </summary>
|
||||
public sealed record BundleArtifact(
|
||||
/// <summary>Relative path within the bundle.</summary>
|
||||
string Path,
|
||||
/// <summary>Artifact type: sbom, vex, dsse, rekor-proof, oci-referrers, etc.</summary>
|
||||
string Type,
|
||||
/// <summary>Content type (MIME).</summary>
|
||||
string? ContentType,
|
||||
/// <summary>SHA-256 digest of the artifact.</summary>
|
||||
string? Digest,
|
||||
/// <summary>Size in bytes.</summary>
|
||||
long? SizeBytes);
|
||||
|
||||
/// <summary>
|
||||
/// Verification section for bundle validation (v2.0.0).
|
||||
/// Sprint: SPRINT_20260118_018 (TASK-018-001)
|
||||
/// </summary>
|
||||
public sealed record BundleVerifySection
|
||||
{
|
||||
/// <summary>
|
||||
/// Trusted signing keys for verification.
|
||||
/// Formats: kms://..., file://..., sigstore://...
|
||||
/// </summary>
|
||||
public ImmutableArray<string> Keys { get; init; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Verification expectations.
|
||||
/// </summary>
|
||||
public BundleVerifyExpectations? Expectations { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Optional: path to trust root certificate.
|
||||
/// </summary>
|
||||
public string? TrustRoot { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Optional: Rekor checkpoint for offline proof verification.
|
||||
/// </summary>
|
||||
public string? RekorCheckpointPath { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verification expectations (v2.0.0).
|
||||
/// Sprint: SPRINT_20260118_018 (TASK-018-001)
|
||||
/// </summary>
|
||||
public sealed record BundleVerifyExpectations
|
||||
{
|
||||
/// <summary>
|
||||
/// Expected payload types in DSSE envelopes.
|
||||
/// Example: ["application/vnd.cyclonedx+json;version=1.6", "application/vnd.openvex+json"]
|
||||
/// </summary>
|
||||
public ImmutableArray<string> PayloadTypes { get; init; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Whether Rekor proof is required for verification.
|
||||
/// </summary>
|
||||
public bool RekorRequired { get; init; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Minimum number of signatures required.
|
||||
/// </summary>
|
||||
public int MinSignatures { get; init; } = 1;
|
||||
|
||||
/// <summary>
|
||||
/// Required artifact types that must be present.
|
||||
/// </summary>
|
||||
public ImmutableArray<string> RequiredArtifacts { get; init; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Whether all artifacts must pass checksum verification.
|
||||
/// </summary>
|
||||
public bool VerifyChecksums { get; init; } = true;
|
||||
}
|
||||
|
||||
public sealed record FeedComponent(
|
||||
|
||||
@@ -96,4 +96,160 @@ public class BundleManifestTests
|
||||
TotalSizeBytes = 30
|
||||
};
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// v2.0.0 Tests - Sprint: SPRINT_20260118_018 (TASK-018-001)
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ManifestV2_DefaultSchemaVersion_Is200()
|
||||
{
|
||||
var manifest = new BundleManifest
|
||||
{
|
||||
BundleId = "test",
|
||||
Name = "test",
|
||||
Version = "1.0.0",
|
||||
CreatedAt = DateTimeOffset.UtcNow,
|
||||
Feeds = [],
|
||||
Policies = [],
|
||||
CryptoMaterials = []
|
||||
};
|
||||
|
||||
manifest.SchemaVersion.Should().Be("2.0.0");
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ManifestV2_WithImage_SetsImageReference()
|
||||
{
|
||||
var manifest = new BundleManifest
|
||||
{
|
||||
BundleId = "test",
|
||||
Name = "test",
|
||||
Version = "1.0.0",
|
||||
CreatedAt = DateTimeOffset.UtcNow,
|
||||
Feeds = [],
|
||||
Policies = [],
|
||||
CryptoMaterials = [],
|
||||
Image = "registry.example.com/app@sha256:abc123"
|
||||
};
|
||||
|
||||
manifest.Image.Should().Be("registry.example.com/app@sha256:abc123");
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ManifestV2_WithArtifacts_ContainsExpectedEntries()
|
||||
{
|
||||
var manifest = new BundleManifest
|
||||
{
|
||||
BundleId = "test",
|
||||
Name = "test",
|
||||
Version = "1.0.0",
|
||||
CreatedAt = DateTimeOffset.UtcNow,
|
||||
Feeds = [],
|
||||
Policies = [],
|
||||
CryptoMaterials = [],
|
||||
Image = "registry.example.com/app@sha256:abc123",
|
||||
Artifacts =
|
||||
[
|
||||
new BundleArtifact("sbom.cdx.json", "sbom", "application/vnd.cyclonedx+json", "sha256:def", 1024),
|
||||
new BundleArtifact("sbom.statement.dsse.json", "dsse", "application/vnd.dsse+json", "sha256:ghi", 512),
|
||||
new BundleArtifact("vex.statement.dsse.json", "dsse", "application/vnd.dsse+json", "sha256:jkl", 256),
|
||||
new BundleArtifact("rekor.proof.json", "rekor-proof", "application/json", "sha256:mno", 128),
|
||||
new BundleArtifact("oci.referrers.json", "oci-referrers", "application/vnd.oci.image.index.v1+json", "sha256:pqr", 64)
|
||||
]
|
||||
};
|
||||
|
||||
manifest.Artifacts.Should().HaveCount(5);
|
||||
manifest.Artifacts.Should().Contain(a => a.Path == "sbom.cdx.json");
|
||||
manifest.Artifacts.Should().Contain(a => a.Type == "dsse");
|
||||
manifest.Artifacts.Should().Contain(a => a.Type == "rekor-proof");
|
||||
manifest.Artifacts.Should().Contain(a => a.Type == "oci-referrers");
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ManifestV2_WithVerifySection_ContainsKeysAndExpectations()
|
||||
{
|
||||
var manifest = new BundleManifest
|
||||
{
|
||||
BundleId = "test",
|
||||
Name = "test",
|
||||
Version = "1.0.0",
|
||||
CreatedAt = DateTimeOffset.UtcNow,
|
||||
Feeds = [],
|
||||
Policies = [],
|
||||
CryptoMaterials = [],
|
||||
Image = "registry.example.com/app@sha256:abc123",
|
||||
Verify = new BundleVerifySection
|
||||
{
|
||||
Keys = ["kms://projects/test/locations/global/keyRings/ring/cryptoKeys/key"],
|
||||
TrustRoot = "trust-root.pem",
|
||||
RekorCheckpointPath = "rekor-checkpoint.json",
|
||||
Expectations = new BundleVerifyExpectations
|
||||
{
|
||||
PayloadTypes = ["application/vnd.cyclonedx+json;version=1.6", "application/vnd.openvex+json"],
|
||||
RekorRequired = true,
|
||||
MinSignatures = 1,
|
||||
RequiredArtifacts = ["sbom.cdx.json", "sbom.statement.dsse.json"],
|
||||
VerifyChecksums = true
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
manifest.Verify.Should().NotBeNull();
|
||||
manifest.Verify!.Keys.Should().HaveCount(1);
|
||||
manifest.Verify.Keys[0].Should().StartWith("kms://");
|
||||
manifest.Verify.Expectations.Should().NotBeNull();
|
||||
manifest.Verify.Expectations!.PayloadTypes.Should().HaveCount(2);
|
||||
manifest.Verify.Expectations.RekorRequired.Should().BeTrue();
|
||||
manifest.Verify.Expectations.RequiredArtifacts.Should().HaveCount(2);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ManifestV2_Serialization_RoundTrip()
|
||||
{
|
||||
var manifest = CreateV2Manifest();
|
||||
var json = BundleManifestSerializer.Serialize(manifest);
|
||||
var deserialized = BundleManifestSerializer.Deserialize(json);
|
||||
|
||||
deserialized.SchemaVersion.Should().Be("2.0.0");
|
||||
deserialized.Image.Should().Be(manifest.Image);
|
||||
deserialized.Artifacts.Should().HaveCount(manifest.Artifacts.Length);
|
||||
deserialized.Verify.Should().NotBeNull();
|
||||
deserialized.Verify!.Keys.Should().BeEquivalentTo(manifest.Verify!.Keys);
|
||||
}
|
||||
|
||||
private static BundleManifest CreateV2Manifest()
|
||||
{
|
||||
return new BundleManifest
|
||||
{
|
||||
BundleId = Guid.NewGuid().ToString(),
|
||||
SchemaVersion = "2.0.0",
|
||||
Name = "offline-bundle-v2",
|
||||
Version = "1.0.0",
|
||||
CreatedAt = DateTimeOffset.UtcNow,
|
||||
Feeds = [],
|
||||
Policies = [],
|
||||
CryptoMaterials = [],
|
||||
Image = "registry.example.com/app@sha256:abc123def456",
|
||||
Artifacts =
|
||||
[
|
||||
new BundleArtifact("sbom.cdx.json", "sbom", "application/vnd.cyclonedx+json", "sha256:aaa", 1024),
|
||||
new BundleArtifact("sbom.statement.dsse.json", "dsse", "application/vnd.dsse+json", "sha256:bbb", 512)
|
||||
],
|
||||
Verify = new BundleVerifySection
|
||||
{
|
||||
Keys = ["kms://example/key"],
|
||||
Expectations = new BundleVerifyExpectations
|
||||
{
|
||||
PayloadTypes = ["application/vnd.cyclonedx+json;version=1.6"],
|
||||
RekorRequired = true
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user