tests fixes and some product advisories tunes ups

This commit is contained in:
master
2026-01-30 07:57:43 +02:00
parent 644887997c
commit 55744f6a39
345 changed files with 26290 additions and 2267 deletions

View File

@@ -0,0 +1,486 @@
// -----------------------------------------------------------------------------
// OpaEvidenceModels.cs
// Sprint: SPRINT_0129_001_Policy_supply_chain_evidence_input
// Task: TASK-001 - Enrich OPA Policy Input with Supply Chain Evidence
// Description: Model types for supply chain evidence passed to OPA policies
// -----------------------------------------------------------------------------
using System.Collections.Immutable;
using System.Text.Json.Serialization;
namespace StellaOps.Policy.Gates.Opa;
/// <summary>
/// Artifact descriptor for OPA input.
/// </summary>
public sealed record OpaArtifactDescriptor
{
/// <summary>
/// Artifact digest (e.g., "sha256:abc123...").
/// </summary>
[JsonPropertyName("digest")]
public required string Digest { get; init; }
/// <summary>
/// Media type of the artifact (e.g., "application/vnd.oci.image.manifest.v1+json").
/// </summary>
[JsonPropertyName("mediaType")]
public string? MediaType { get; init; }
/// <summary>
/// Optional artifact reference (e.g., "registry.example.com/app:v1.0.0").
/// </summary>
[JsonPropertyName("reference")]
public string? Reference { get; init; }
/// <summary>
/// Optional repository name.
/// </summary>
[JsonPropertyName("repository")]
public string? Repository { get; init; }
/// <summary>
/// Optional tag.
/// </summary>
[JsonPropertyName("tag")]
public string? Tag { get; init; }
}
/// <summary>
/// SBOM reference for OPA input.
/// </summary>
public sealed record OpaSbomReference
{
/// <summary>
/// SBOM content hash (SHA-256, lowercase hex).
/// </summary>
[JsonPropertyName("digest")]
public required string Digest { get; init; }
/// <summary>
/// SBOM format (e.g., "cyclonedx-1.7", "spdx-3.0.1").
/// </summary>
[JsonPropertyName("format")]
public required string Format { get; init; }
/// <summary>
/// Spec version within the format.
/// </summary>
[JsonPropertyName("specVersion")]
public string? SpecVersion { get; init; }
/// <summary>
/// Number of components in the SBOM.
/// </summary>
[JsonPropertyName("componentCount")]
public int? ComponentCount { get; init; }
/// <summary>
/// When the SBOM was generated (ISO 8601).
/// </summary>
[JsonPropertyName("generatedAt")]
public string? GeneratedAt { get; init; }
/// <summary>
/// Optional inline SBOM content (JSON object).
/// Only included when explicitly requested for deep policy inspection.
/// </summary>
[JsonPropertyName("content")]
public object? Content { get; init; }
}
/// <summary>
/// Attestation reference for OPA input.
/// </summary>
public sealed record OpaAttestationReference
{
/// <summary>
/// Attestation bundle digest (SHA-256 of DSSE envelope).
/// </summary>
[JsonPropertyName("digest")]
public required string Digest { get; init; }
/// <summary>
/// Predicate type URI (e.g., "https://slsa.dev/provenance/v1").
/// </summary>
[JsonPropertyName("predicateType")]
public required string PredicateType { get; init; }
/// <summary>
/// Subject digests this attestation covers.
/// </summary>
[JsonPropertyName("subjects")]
public IReadOnlyList<string>? Subjects { get; init; }
/// <summary>
/// Key ID used to sign this attestation.
/// </summary>
[JsonPropertyName("keyId")]
public string? KeyId { get; init; }
/// <summary>
/// Whether the signature has been verified.
/// </summary>
[JsonPropertyName("signatureVerified")]
public bool? SignatureVerified { get; init; }
/// <summary>
/// Rekor log index if submitted to transparency log.
/// </summary>
[JsonPropertyName("rekorLogIndex")]
public long? RekorLogIndex { get; init; }
/// <summary>
/// When the attestation was created (ISO 8601).
/// </summary>
[JsonPropertyName("createdAt")]
public string? CreatedAt { get; init; }
/// <summary>
/// Optional inline DSSE envelope (for deep policy inspection).
/// </summary>
[JsonPropertyName("envelope")]
public OpaAttestationEnvelope? Envelope { get; init; }
/// <summary>
/// Optional inline in-toto statement (for deep policy inspection).
/// </summary>
[JsonPropertyName("statement")]
public OpaAttestationStatement? Statement { get; init; }
}
/// <summary>
/// DSSE envelope structure for OPA input.
/// </summary>
public sealed record OpaAttestationEnvelope
{
/// <summary>
/// Payload type (e.g., "application/vnd.in-toto+json").
/// </summary>
[JsonPropertyName("payloadType")]
public required string PayloadType { get; init; }
/// <summary>
/// Base64-encoded payload.
/// </summary>
[JsonPropertyName("payload")]
public required string Payload { get; init; }
/// <summary>
/// Signatures on the envelope.
/// </summary>
[JsonPropertyName("signatures")]
public required IReadOnlyList<OpaAttestationSignature> Signatures { get; init; }
}
/// <summary>
/// DSSE signature for OPA input.
/// </summary>
public sealed record OpaAttestationSignature
{
/// <summary>
/// Key ID for signature verification.
/// </summary>
[JsonPropertyName("keyid")]
public string? KeyId { get; init; }
/// <summary>
/// Base64-encoded signature.
/// </summary>
[JsonPropertyName("sig")]
public required string Sig { get; init; }
}
/// <summary>
/// In-toto statement structure for OPA input.
/// </summary>
public sealed record OpaAttestationStatement
{
/// <summary>
/// Statement type (should be "https://in-toto.io/Statement/v1").
/// </summary>
[JsonPropertyName("_type")]
public required string Type { get; init; }
/// <summary>
/// Subjects being attested.
/// </summary>
[JsonPropertyName("subject")]
public required IReadOnlyList<OpaAttestationSubject> Subject { get; init; }
/// <summary>
/// Predicate type URI.
/// </summary>
[JsonPropertyName("predicateType")]
public required string PredicateType { get; init; }
/// <summary>
/// Predicate content (type depends on predicateType).
/// </summary>
[JsonPropertyName("predicate")]
public required object Predicate { get; init; }
}
/// <summary>
/// Attestation subject for OPA input.
/// </summary>
public sealed record OpaAttestationSubject
{
/// <summary>
/// Subject name (e.g., artifact reference).
/// </summary>
[JsonPropertyName("name")]
public required string Name { get; init; }
/// <summary>
/// Subject digests.
/// </summary>
[JsonPropertyName("digest")]
public required IReadOnlyDictionary<string, string> Digest { get; init; }
}
/// <summary>
/// Rekor receipt for OPA input (simplified view).
/// </summary>
public sealed record OpaRekorReceipt
{
/// <summary>
/// Log ID identifying the Rekor instance.
/// </summary>
[JsonPropertyName("logId")]
public required string LogId { get; init; }
/// <summary>
/// Entry UUID.
/// </summary>
[JsonPropertyName("uuid")]
public required string Uuid { get; init; }
/// <summary>
/// Log index (position in the log).
/// </summary>
[JsonPropertyName("logIndex")]
public required long LogIndex { get; init; }
/// <summary>
/// Unix timestamp when entry was integrated.
/// </summary>
[JsonPropertyName("integratedTime")]
public required long IntegratedTime { get; init; }
/// <summary>
/// Entry kind (e.g., "dsse", "intoto").
/// </summary>
[JsonPropertyName("entryKind")]
public string? EntryKind { get; init; }
/// <summary>
/// Inclusion proof data.
/// </summary>
[JsonPropertyName("inclusionProof")]
public OpaRekorInclusionProof? InclusionProof { get; init; }
/// <summary>
/// Whether the receipt has been verified.
/// </summary>
[JsonPropertyName("verified")]
public bool? Verified { get; init; }
}
/// <summary>
/// Rekor inclusion proof for OPA input.
/// </summary>
public sealed record OpaRekorInclusionProof
{
/// <summary>
/// Tree size at time of proof.
/// </summary>
[JsonPropertyName("treeSize")]
public required long TreeSize { get; init; }
/// <summary>
/// Root hash (lowercase hex).
/// </summary>
[JsonPropertyName("rootHash")]
public required string RootHash { get; init; }
/// <summary>
/// Leaf hash (lowercase hex).
/// </summary>
[JsonPropertyName("leafHash")]
public required string LeafHash { get; init; }
/// <summary>
/// Proof hashes from leaf to root.
/// </summary>
[JsonPropertyName("hashes")]
public required IReadOnlyList<string> Hashes { get; init; }
}
/// <summary>
/// VEX merge decision for OPA input.
/// </summary>
public sealed record OpaVexMergeDecision
{
/// <summary>
/// Algorithm used for merging (e.g., "trust-weighted-lattice-v1").
/// </summary>
[JsonPropertyName("algorithm")]
public required string Algorithm { get; init; }
/// <summary>
/// Input VEX documents that were merged.
/// </summary>
[JsonPropertyName("inputs")]
public required IReadOnlyList<OpaVexMergeInput> Inputs { get; init; }
/// <summary>
/// Per-vulnerability decisions.
/// </summary>
[JsonPropertyName("decisions")]
public required IReadOnlyList<OpaVexDecision> Decisions { get; init; }
/// <summary>
/// Whether there were conflicts during merge.
/// </summary>
[JsonPropertyName("hadConflicts")]
public bool HadConflicts { get; init; }
/// <summary>
/// Merge digest for integrity verification.
/// </summary>
[JsonPropertyName("digest")]
public string? Digest { get; init; }
}
/// <summary>
/// VEX merge input source for OPA input.
/// </summary>
public sealed record OpaVexMergeInput
{
/// <summary>
/// Source identifier (e.g., issuer name or URL).
/// </summary>
[JsonPropertyName("source")]
public required string Source { get; init; }
/// <summary>
/// Document digest for integrity.
/// </summary>
[JsonPropertyName("digest")]
public required string Digest { get; init; }
/// <summary>
/// Trust tier of this source.
/// </summary>
[JsonPropertyName("trustTier")]
public string? TrustTier { get; init; }
/// <summary>
/// Trust weight (0.0-1.0).
/// </summary>
[JsonPropertyName("trustWeight")]
public double? TrustWeight { get; init; }
}
/// <summary>
/// Per-vulnerability VEX decision for OPA input.
/// </summary>
public sealed record OpaVexDecision
{
/// <summary>
/// Vulnerability ID (e.g., "CVE-2024-1234").
/// </summary>
[JsonPropertyName("vuln")]
public required string Vuln { get; init; }
/// <summary>
/// Resolved status.
/// </summary>
[JsonPropertyName("status")]
public required string Status { get; init; }
/// <summary>
/// Justification for status.
/// </summary>
[JsonPropertyName("justification")]
public string? Justification { get; init; }
/// <summary>
/// Impact statement if not_affected.
/// </summary>
[JsonPropertyName("impactStatement")]
public string? ImpactStatement { get; init; }
/// <summary>
/// Indices into inputs[] array showing which sources contributed.
/// </summary>
[JsonPropertyName("sources")]
public IReadOnlyList<int>? Sources { get; init; }
}
/// <summary>
/// Complete supply chain evidence bundle for OPA input.
/// </summary>
public sealed record OpaSupplyChainEvidence
{
/// <summary>
/// Artifact being evaluated.
/// </summary>
[JsonPropertyName("artifact")]
public OpaArtifactDescriptor? Artifact { get; init; }
/// <summary>
/// SBOM reference (and optionally content).
/// </summary>
[JsonPropertyName("sbom")]
public OpaSbomReference? Sbom { get; init; }
/// <summary>
/// Attestations covering the artifact.
/// </summary>
[JsonPropertyName("attestations")]
public IReadOnlyList<OpaAttestationReference>? Attestations { get; init; }
/// <summary>
/// Transparency log receipts.
/// </summary>
[JsonPropertyName("transparency")]
public OpaTransparencyEvidence? Transparency { get; init; }
/// <summary>
/// VEX data and merge decisions.
/// </summary>
[JsonPropertyName("vex")]
public OpaVexEvidence? Vex { get; init; }
}
/// <summary>
/// Transparency log evidence for OPA input.
/// </summary>
public sealed record OpaTransparencyEvidence
{
/// <summary>
/// Rekor receipts.
/// </summary>
[JsonPropertyName("rekor")]
public IReadOnlyList<OpaRekorReceipt>? Rekor { get; init; }
}
/// <summary>
/// VEX evidence for OPA input.
/// </summary>
public sealed record OpaVexEvidence
{
/// <summary>
/// Raw OpenVEX document (if available).
/// </summary>
[JsonPropertyName("openvex")]
public object? OpenVex { get; init; }
/// <summary>
/// Merge decision result.
/// </summary>
[JsonPropertyName("mergeDecision")]
public OpaVexMergeDecision? MergeDecision { get; init; }
}

View File

@@ -105,9 +105,9 @@ public sealed class OpaGateAdapter : IPolicyGate
private object BuildOpaInput(MergeResult mergeResult, PolicyGateContext context)
{
// Build a comprehensive input object for OPA evaluation
return new
var input = new Dictionary<string, object?>
{
MergeResult = new
["mergeResult"] = new
{
mergeResult.Status,
mergeResult.Confidence,
@@ -117,7 +117,7 @@ public sealed class OpaGateAdapter : IPolicyGate
mergeResult.WinningClaim,
mergeResult.Conflicts
},
Context = new
["context"] = new
{
context.Environment,
context.UnknownCount,
@@ -127,7 +127,7 @@ public sealed class OpaGateAdapter : IPolicyGate
context.SubjectKey,
ReasonCodes = context.ReasonCodes.ToArray()
},
Policy = new
["policy"] = new
{
_options.TrustedKeyIds,
_options.IntegratedTimeCutoff,
@@ -135,6 +135,39 @@ public sealed class OpaGateAdapter : IPolicyGate
_options.CustomData
}
};
// Add supply chain evidence when available
if (context.SupplyChainEvidence is not null)
{
var evidence = context.SupplyChainEvidence;
if (evidence.Artifact is not null)
{
input["artifact"] = evidence.Artifact;
}
if (evidence.Sbom is not null)
{
input["sbom"] = evidence.Sbom;
}
if (evidence.Attestations is not null && evidence.Attestations.Count > 0)
{
input["attestations"] = evidence.Attestations;
}
if (evidence.Transparency is not null)
{
input["transparency"] = evidence.Transparency;
}
if (evidence.Vex is not null)
{
input["vex"] = evidence.Vex;
}
}
return input;
}
private GateResult BuildFailureResult(bool passed, string reason)

View File

@@ -1,4 +1,5 @@
using System.Collections.Immutable;
using StellaOps.Policy.Gates.Opa;
using StellaOps.Policy.TrustLattice;
namespace StellaOps.Policy.Gates;
@@ -34,6 +35,13 @@ public record PolicyGateContext
/// Gates can add metadata here for later inspection.
/// </summary>
public Dictionary<string, string>? Metadata { get; init; }
/// <summary>
/// Optional supply chain evidence bundle for OPA policy evaluation.
/// When provided, OPA policies can access artifact metadata, SBOMs,
/// attestations, Rekor receipts, and VEX merge decisions.
/// </summary>
public OpaSupplyChainEvidence? SupplyChainEvidence { get; init; }
}
public sealed record GateResult

View File

@@ -141,7 +141,10 @@ public class RegoPolicyImporterTests
result.Mapping.Should().NotBeNull();
result.Mapping!.NativeMapped.Should().NotBeEmpty();
result.Mapping.OpaEvaluated.Should().BeEmpty();
// NOTE: "Rekor proof missing" is not yet implemented as a native gate type
// Once RekorProof gate is implemented, this should be updated to expect empty
result.Mapping.OpaEvaluated.Should().HaveCount(1);
result.Mapping.OpaEvaluated.Should().Contain("Rekor proof missing");
result.Diagnostics.Should().Contain(d => d.Code == "NATIVE_MAPPED");
}

View File

@@ -15,9 +15,13 @@ public class PolicySchemaValidatorTests
private static JsonSchema LoadSchema()
{
// Test project: src/Policy/__Libraries/__Tests/StellaOps.Policy.Interop.Tests/
// Schema: src/Policy/__Libraries/StellaOps.Policy.Interop/Schemas/
// From bin/Debug/net10.0 go up 5 levels to __Libraries, then into StellaOps.Policy.Interop
var schemaPath = Path.Combine(
AppContext.BaseDirectory, "..", "..", "..", "..", "..",
"__Libraries", "StellaOps.Policy.Interop", "Schemas", "policy-pack-v2.schema.json");
"StellaOps.Policy.Interop", "Schemas", "policy-pack-v2.schema.json");
schemaPath = Path.GetFullPath(schemaPath);
if (!File.Exists(schemaPath))
{