tests fixes and some product advisories tunes ups
This commit is contained in:
@@ -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; }
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
|
||||
@@ -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))
|
||||
{
|
||||
|
||||
@@ -159,6 +159,147 @@ public sealed class OpaGateAdapterTests
|
||||
Assert.Equal("decision-abc-123", result.Details["opaDecisionId"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EvaluateAsync_WithSupplyChainEvidence_IncludesEvidenceInInput()
|
||||
{
|
||||
object? capturedInput = null;
|
||||
var mockClient = new CapturingMockOpaClient(
|
||||
new OpaTypedResult<object>
|
||||
{
|
||||
Success = true,
|
||||
DecisionId = "evidence-test",
|
||||
Result = new { allow = true }
|
||||
},
|
||||
input => capturedInput = input);
|
||||
|
||||
var options = Options.Create(new OpaGateOptions
|
||||
{
|
||||
GateName = "TestOpaGate",
|
||||
PolicyPath = "stella/test/allow"
|
||||
});
|
||||
|
||||
var evidence = new OpaSupplyChainEvidence
|
||||
{
|
||||
Artifact = new OpaArtifactDescriptor
|
||||
{
|
||||
Digest = "sha256:abc123def456",
|
||||
MediaType = "application/vnd.oci.image.manifest.v1+json",
|
||||
Reference = "registry.example.com/app:v1.0.0"
|
||||
},
|
||||
Sbom = new OpaSbomReference
|
||||
{
|
||||
Digest = "sha256:sbom123",
|
||||
Format = "cyclonedx-1.7",
|
||||
ComponentCount = 42
|
||||
},
|
||||
Attestations = new[]
|
||||
{
|
||||
new OpaAttestationReference
|
||||
{
|
||||
Digest = "sha256:att123",
|
||||
PredicateType = "https://slsa.dev/provenance/v1",
|
||||
SignatureVerified = true,
|
||||
RekorLogIndex = 12345
|
||||
}
|
||||
},
|
||||
Transparency = new OpaTransparencyEvidence
|
||||
{
|
||||
Rekor = new[]
|
||||
{
|
||||
new OpaRekorReceipt
|
||||
{
|
||||
LogId = "rekor.sigstore.dev",
|
||||
Uuid = "abc123",
|
||||
LogIndex = 12345,
|
||||
IntegratedTime = 1700000000,
|
||||
Verified = true
|
||||
}
|
||||
}
|
||||
},
|
||||
Vex = new OpaVexEvidence
|
||||
{
|
||||
MergeDecision = new OpaVexMergeDecision
|
||||
{
|
||||
Algorithm = "trust-weighted-lattice-v1",
|
||||
Inputs = new[]
|
||||
{
|
||||
new OpaVexMergeInput { Source = "vendor", Digest = "sha256:vex1", TrustTier = "authoritative" }
|
||||
},
|
||||
Decisions = new[]
|
||||
{
|
||||
new OpaVexDecision { Vuln = "CVE-2024-1234", Status = "not_affected", Justification = "component_not_present" }
|
||||
},
|
||||
HadConflicts = false
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var context = new PolicyGateContext
|
||||
{
|
||||
Environment = "production",
|
||||
HasReachabilityProof = true,
|
||||
SupplyChainEvidence = evidence
|
||||
};
|
||||
|
||||
var gate = new OpaGateAdapter(mockClient, options, NullLogger<OpaGateAdapter>.Instance);
|
||||
await gate.EvaluateAsync(CreateMergeResult(), context);
|
||||
|
||||
Assert.NotNull(capturedInput);
|
||||
|
||||
// Serialize to JSON and verify structure
|
||||
var json = JsonSerializer.Serialize(capturedInput, new JsonSerializerOptions { WriteIndented = true });
|
||||
|
||||
Assert.Contains("artifact", json);
|
||||
Assert.Contains("sha256:abc123def456", json);
|
||||
Assert.Contains("sbom", json);
|
||||
Assert.Contains("cyclonedx-1.7", json);
|
||||
Assert.Contains("attestations", json);
|
||||
Assert.Contains("https://slsa.dev/provenance/v1", json);
|
||||
Assert.Contains("transparency", json);
|
||||
Assert.Contains("rekor", json);
|
||||
Assert.Contains("vex", json);
|
||||
Assert.Contains("trust-weighted-lattice-v1", json);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EvaluateAsync_WithoutSupplyChainEvidence_DoesNotIncludeEvidenceFields()
|
||||
{
|
||||
object? capturedInput = null;
|
||||
var mockClient = new CapturingMockOpaClient(
|
||||
new OpaTypedResult<object>
|
||||
{
|
||||
Success = true,
|
||||
DecisionId = "no-evidence-test",
|
||||
Result = new { allow = true }
|
||||
},
|
||||
input => capturedInput = input);
|
||||
|
||||
var options = Options.Create(new OpaGateOptions
|
||||
{
|
||||
GateName = "TestOpaGate",
|
||||
PolicyPath = "stella/test/allow"
|
||||
});
|
||||
|
||||
var gate = new OpaGateAdapter(mockClient, options, NullLogger<OpaGateAdapter>.Instance);
|
||||
await gate.EvaluateAsync(CreateMergeResult(), CreateContext());
|
||||
|
||||
Assert.NotNull(capturedInput);
|
||||
|
||||
var json = JsonSerializer.Serialize(capturedInput);
|
||||
|
||||
// Should not contain evidence fields when not provided
|
||||
Assert.DoesNotContain("\"artifact\"", json);
|
||||
Assert.DoesNotContain("\"sbom\"", json);
|
||||
Assert.DoesNotContain("\"attestations\"", json);
|
||||
Assert.DoesNotContain("\"transparency\"", json);
|
||||
Assert.DoesNotContain("\"vex\"", json);
|
||||
|
||||
// Should still contain standard fields
|
||||
Assert.Contains("mergeResult", json);
|
||||
Assert.Contains("context", json);
|
||||
Assert.Contains("policy", json);
|
||||
}
|
||||
|
||||
private sealed class MockOpaClient : IOpaClient
|
||||
{
|
||||
private readonly OpaTypedResult<object> _result;
|
||||
@@ -207,4 +348,56 @@ public sealed class OpaGateAdapterTests
|
||||
public Task DeletePolicyAsync(string policyId, CancellationToken cancellationToken = default)
|
||||
=> Task.CompletedTask;
|
||||
}
|
||||
|
||||
private sealed class CapturingMockOpaClient : IOpaClient
|
||||
{
|
||||
private readonly OpaTypedResult<object> _result;
|
||||
private readonly Action<object> _captureInput;
|
||||
|
||||
public CapturingMockOpaClient(OpaTypedResult<object> result, Action<object> captureInput)
|
||||
{
|
||||
_result = result;
|
||||
_captureInput = captureInput;
|
||||
}
|
||||
|
||||
public Task<OpaEvaluationResult> EvaluateAsync(string policyPath, object input, CancellationToken cancellationToken = default)
|
||||
{
|
||||
_captureInput(input);
|
||||
return Task.FromResult(new OpaEvaluationResult
|
||||
{
|
||||
Success = _result.Success,
|
||||
DecisionId = _result.DecisionId,
|
||||
Result = _result.Result,
|
||||
Error = _result.Error
|
||||
});
|
||||
}
|
||||
|
||||
public Task<OpaTypedResult<TResult>> EvaluateAsync<TResult>(string policyPath, object input, CancellationToken cancellationToken = default)
|
||||
{
|
||||
_captureInput(input);
|
||||
TResult? typedResult = default;
|
||||
if (_result.Result is not null)
|
||||
{
|
||||
var json = JsonSerializer.Serialize(_result.Result, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase });
|
||||
typedResult = JsonSerializer.Deserialize<TResult>(json, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase });
|
||||
}
|
||||
|
||||
return Task.FromResult(new OpaTypedResult<TResult>
|
||||
{
|
||||
Success = _result.Success,
|
||||
DecisionId = _result.DecisionId,
|
||||
Result = typedResult,
|
||||
Error = _result.Error
|
||||
});
|
||||
}
|
||||
|
||||
public Task<bool> HealthCheckAsync(CancellationToken cancellationToken = default)
|
||||
=> Task.FromResult(true);
|
||||
|
||||
public Task UploadPolicyAsync(string policyId, string regoContent, CancellationToken cancellationToken = default)
|
||||
=> Task.CompletedTask;
|
||||
|
||||
public Task DeletePolicyAsync(string policyId, CancellationToken cancellationToken = default)
|
||||
=> Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user