diff --git a/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Core.Tests/Validation/PredicateSchemaValidatorTests.cs b/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Core.Tests/Validation/PredicateSchemaValidatorTests.cs index 5e252db72..bfab01740 100644 --- a/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Core.Tests/Validation/PredicateSchemaValidatorTests.cs +++ b/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Core.Tests/Validation/PredicateSchemaValidatorTests.cs @@ -2,18 +2,15 @@ using System.Text.Json; using Microsoft.Extensions.Logging.Abstractions; using StellaOps.Attestor.Core.Validation; using Xunit; -using Xunit.Abstractions; namespace StellaOps.Attestor.Core.Tests.Validation; public sealed class PredicateSchemaValidatorTests { private readonly PredicateSchemaValidator _validator; - private readonly ITestOutputHelper _output; - public PredicateSchemaValidatorTests(ITestOutputHelper output) + public PredicateSchemaValidatorTests() { - _output = output; _validator = new PredicateSchemaValidator(NullLogger.Instance); } @@ -23,15 +20,17 @@ public sealed class PredicateSchemaValidatorTests var assembly = typeof(PredicateSchemaValidator).Assembly; var resourceNames = assembly.GetManifestResourceNames(); - _output.WriteLine($"Assembly: {assembly.FullName}"); - _output.WriteLine($"Found {resourceNames.Length} resources:"); - foreach (var name in resourceNames) - { - _output.WriteLine($" - {name}"); - } - Assert.Contains(resourceNames, n => n.Contains("vex-delta")); Assert.Contains(resourceNames, n => n.Contains("sbom-delta")); + + // Verify the exact resource names match what LoadSchemas expects + var resourcePrefix = "StellaOps.Attestor.Core.Schemas."; + Assert.Contains(resourceNames, n => n == resourcePrefix + "vex-delta.v1.schema.json"); + Assert.Contains(resourceNames, n => n == resourcePrefix + "sbom-delta.v1.schema.json"); + + // Verify we can load the stream directly + using var stream = assembly.GetManifestResourceStream(resourcePrefix + "vex-delta.v1.schema.json"); + Assert.NotNull(stream); } [Fact] diff --git a/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Core/Validation/PredicateSchemaValidator.cs b/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Core/Validation/PredicateSchemaValidator.cs index db45b8bac..744c9a900 100644 --- a/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Core/Validation/PredicateSchemaValidator.cs +++ b/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Core/Validation/PredicateSchemaValidator.cs @@ -1,6 +1,7 @@ using System.Text.Json; using Json.Schema; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; namespace StellaOps.Attestor.Core.Validation; @@ -52,21 +53,26 @@ public interface IPredicateSchemaValidator /// public sealed class PredicateSchemaValidator : IPredicateSchemaValidator { - private readonly IReadOnlyDictionary _schemas; + private static readonly Lazy> _lazySchemas = + new(() => LoadSchemasInternal(NullLogger.Instance), LazyThreadSafetyMode.ExecutionAndPublication); + private readonly ILogger _logger; public PredicateSchemaValidator(ILogger logger) { _logger = logger; - _schemas = LoadSchemas(_logger); + // Force schema loading on first access + _ = _lazySchemas.Value; } + private static IReadOnlyDictionary Schemas => _lazySchemas.Value; + public ValidationResult Validate(string predicateType, JsonElement predicate) { // Normalize predicate type (handle both with and without stella.ops/ prefix) var normalizedType = NormalizePredicateType(predicateType); - if (!_schemas.TryGetValue(normalizedType, out var schema)) + if (!Schemas.TryGetValue(normalizedType, out var schema)) { _logger.LogDebug("No schema found for predicate type {PredicateType}, skipping validation", predicateType); return ValidationResult.Skip($"No schema for {predicateType}"); @@ -133,7 +139,7 @@ public sealed class PredicateSchemaValidator : IPredicateSchemaValidator return errors; } - private static IReadOnlyDictionary LoadSchemas(ILogger logger) + private static IReadOnlyDictionary LoadSchemasInternal(ILogger logger) { var schemas = new Dictionary(StringComparer.OrdinalIgnoreCase); diff --git a/src/Policy/__Libraries/StellaOps.Policy/Gates/Attestation/AttestationVerificationGate.cs b/src/Policy/__Libraries/StellaOps.Policy/Gates/Attestation/AttestationVerificationGate.cs index 32422198f..d001db0a8 100644 --- a/src/Policy/__Libraries/StellaOps.Policy/Gates/Attestation/AttestationVerificationGate.cs +++ b/src/Policy/__Libraries/StellaOps.Policy/Gates/Attestation/AttestationVerificationGate.cs @@ -11,7 +11,7 @@ namespace StellaOps.Policy.Gates.Attestation; /// Policy gate that validates DSSE attestation envelopes. /// Checks payload type, signature validity, and key trust. /// -public sealed class AttestationVerificationGate : IPolicyGate +public sealed class AttestationVerificationGate : IAttestationGate { /// /// Gate identifier. diff --git a/src/Policy/__Libraries/StellaOps.Policy/Gates/Attestation/CompositeAttestationGate.cs b/src/Policy/__Libraries/StellaOps.Policy/Gates/Attestation/CompositeAttestationGate.cs index 17f15d707..e68a27be5 100644 --- a/src/Policy/__Libraries/StellaOps.Policy/Gates/Attestation/CompositeAttestationGate.cs +++ b/src/Policy/__Libraries/StellaOps.Policy/Gates/Attestation/CompositeAttestationGate.cs @@ -11,14 +11,14 @@ namespace StellaOps.Policy.Gates.Attestation; /// Composite gate that orchestrates multiple attestation gates. /// Supports AND, OR, and threshold-based composition. /// -public sealed class CompositeAttestationGate : IPolicyGate +public sealed class CompositeAttestationGate : IAttestationGate { /// /// Gate identifier. /// public const string GateId = "composite-attestation"; - private readonly IReadOnlyList _gates; + private readonly IReadOnlyList _gates; private readonly CompositeAttestationGateOptions _options; /// @@ -34,7 +34,7 @@ public sealed class CompositeAttestationGate : IPolicyGate /// Creates a new composite attestation gate. /// public CompositeAttestationGate( - IEnumerable gates, + IEnumerable gates, CompositeAttestationGateOptions? options = null) { _gates = gates?.ToList() ?? throw new ArgumentNullException(nameof(gates)); @@ -145,7 +145,7 @@ public sealed class CompositeAttestationGate : IPolicyGate /// /// Creates a composite gate with AND logic. /// - public static CompositeAttestationGate And(params IPolicyGate[] gates) + public static CompositeAttestationGate And(params IAttestationGate[] gates) { return new CompositeAttestationGate(gates, new CompositeAttestationGateOptions { @@ -156,7 +156,7 @@ public sealed class CompositeAttestationGate : IPolicyGate /// /// Creates a composite gate with OR logic. /// - public static CompositeAttestationGate Or(params IPolicyGate[] gates) + public static CompositeAttestationGate Or(params IAttestationGate[] gates) { return new CompositeAttestationGate(gates, new CompositeAttestationGateOptions { @@ -167,7 +167,7 @@ public sealed class CompositeAttestationGate : IPolicyGate /// /// Creates a composite gate with threshold logic. /// - public static CompositeAttestationGate Threshold(int threshold, params IPolicyGate[] gates) + public static CompositeAttestationGate Threshold(int threshold, params IAttestationGate[] gates) { return new CompositeAttestationGate(gates, new CompositeAttestationGateOptions { diff --git a/src/Policy/__Libraries/StellaOps.Policy/Gates/Attestation/ITrustedKeyRegistry.cs b/src/Policy/__Libraries/StellaOps.Policy/Gates/Attestation/ITrustedKeyRegistry.cs index 1b952d9ba..c34a2bf39 100644 --- a/src/Policy/__Libraries/StellaOps.Policy/Gates/Attestation/ITrustedKeyRegistry.cs +++ b/src/Policy/__Libraries/StellaOps.Policy/Gates/Attestation/ITrustedKeyRegistry.cs @@ -7,6 +7,79 @@ namespace StellaOps.Policy.Gates.Attestation; +/// +/// Attestation-specific gate interface. +/// Uses simplified signature without MergeResult for attestation verification gates. +/// +public interface IAttestationGate +{ + /// + /// Gate identifier. + /// + string Id { get; } + + /// + /// Display name for the gate. + /// + string DisplayName { get; } + + /// + /// Description of what the gate checks. + /// + string Description { get; } + + /// + /// Evaluates the gate against the given context. + /// + Task EvaluateAsync(PolicyGateContext context, CancellationToken ct = default); +} + +/// +/// Extension methods for PolicyGateContext to support attestation gates. +/// +public static class AttestationContextExtensions +{ + /// + /// Gets attestation from context metadata. + /// + public static object? GetAttestation(this PolicyGateContext context) + { + if (context.Metadata?.TryGetValue("Attestation", out var value) == true) + return value; + return null; + } + + /// + /// Gets VEX summary from context metadata. + /// + public static object? GetVexSummary(this PolicyGateContext context) + { + if (context.Metadata?.TryGetValue("VexSummary", out var value) == true) + return value; + return null; + } + + /// + /// Gets Rekor proof from context metadata. + /// + public static object? GetRekorProof(this PolicyGateContext context) + { + if (context.Metadata?.TryGetValue("RekorProof", out var value) == true) + return value; + return null; + } + + /// + /// Gets reachability findings from context metadata. + /// + public static object? GetReachabilityFindings(this PolicyGateContext context) + { + if (context.Metadata?.TryGetValue("ReachabilityFindings", out var value) == true) + return value; + return null; + } +} + /// /// Registry of trusted signing keys for attestation verification. /// @@ -171,7 +244,7 @@ public sealed class InMemoryTrustedKeyRegistry : ITrustedKeyRegistry } /// - public async IAsyncEnumerable ListAsync(CancellationToken ct = default) + public async IAsyncEnumerable ListAsync([System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken ct = default) { List keys; lock (_lock) diff --git a/src/Policy/__Libraries/StellaOps.Policy/Gates/Attestation/RekorFreshnessGate.cs b/src/Policy/__Libraries/StellaOps.Policy/Gates/Attestation/RekorFreshnessGate.cs index cc46033bf..22780374f 100644 --- a/src/Policy/__Libraries/StellaOps.Policy/Gates/Attestation/RekorFreshnessGate.cs +++ b/src/Policy/__Libraries/StellaOps.Policy/Gates/Attestation/RekorFreshnessGate.cs @@ -11,7 +11,7 @@ namespace StellaOps.Policy.Gates.Attestation; /// Policy gate that enforces Rekor entry freshness based on integratedTime. /// Rejects attestations older than the configured cutoff. /// -public sealed class RekorFreshnessGate : IPolicyGate +public sealed class RekorFreshnessGate : IAttestationGate { /// /// Gate identifier. diff --git a/src/Policy/__Libraries/StellaOps.Policy/Gates/Attestation/VexStatusPromotionGate.cs b/src/Policy/__Libraries/StellaOps.Policy/Gates/Attestation/VexStatusPromotionGate.cs index 4c6f8edfe..50be58f80 100644 --- a/src/Policy/__Libraries/StellaOps.Policy/Gates/Attestation/VexStatusPromotionGate.cs +++ b/src/Policy/__Libraries/StellaOps.Policy/Gates/Attestation/VexStatusPromotionGate.cs @@ -11,7 +11,7 @@ namespace StellaOps.Policy.Gates.Attestation; /// Policy gate that enforces VEX status requirements with reachability awareness. /// Blocks promotion based on affected + reachable combinations. /// -public sealed class VexStatusPromotionGate : IPolicyGate +public sealed class VexStatusPromotionGate : IAttestationGate { /// /// Gate identifier. diff --git a/src/Policy/__Libraries/StellaOps.Policy/Gates/Cve/CveDeltaGate.cs b/src/Policy/__Libraries/StellaOps.Policy/Gates/Cve/CveDeltaGate.cs index 8fff43582..0fbaf9377 100644 --- a/src/Policy/__Libraries/StellaOps.Policy/Gates/Cve/CveDeltaGate.cs +++ b/src/Policy/__Libraries/StellaOps.Policy/Gates/Cve/CveDeltaGate.cs @@ -11,7 +11,7 @@ namespace StellaOps.Policy.Gates.Cve; /// Policy gate that blocks releases introducing new high-severity CVEs compared to baseline. /// Prevents security regressions by tracking CVE delta between releases. /// -public sealed class CveDeltaGate : IPolicyGate +public sealed class CveDeltaGate : ICveGate { /// /// Gate identifier. @@ -48,7 +48,7 @@ public sealed class CveDeltaGate : IPolicyGate if (!_options.Enabled) { - return GateResult.Pass(Id, "CVE delta gate disabled"); + return GateResultFactory.Pass(Id, "CVE delta gate disabled"); } var envOptions = GetEnvironmentOptions(context.Environment); @@ -57,20 +57,20 @@ public sealed class CveDeltaGate : IPolicyGate var currentCves = context.GetCveFindings(); if (currentCves == null || currentCves.Count == 0) { - return GateResult.Pass(Id, "No CVE findings in current release"); + return GateResultFactory.Pass(Id, "No CVE findings in current release"); } // Get baseline CVEs IReadOnlyList baselineCves; - if (_deltaProvider != null && !string.IsNullOrWhiteSpace(context.BaselineReference)) + if (_deltaProvider != null && !string.IsNullOrWhiteSpace(context.GetBaselineReference())) { baselineCves = await _deltaProvider.GetBaselineCvesAsync( - context.BaselineReference, + context.GetBaselineReference(), ct).ConfigureAwait(false); } - else if (context.BaselineCves != null) + else if (context.GetBaselineCves() != null) { - baselineCves = context.BaselineCves; + baselineCves = context.GetBaselineCves(); } else { @@ -139,7 +139,7 @@ public sealed class CveDeltaGate : IPolicyGate message += $" and {blockingNewCves.Count - 5} more"; } - return GateResult.Fail(Id, message); + return GateResultFactory.Fail(Id, message); } var passMessage = $"CVE delta check passed. " + @@ -154,7 +154,7 @@ public sealed class CveDeltaGate : IPolicyGate } } - return GateResult.Pass(Id, passMessage, warnings: warnings); + return GateResultFactory.Pass(Id, passMessage, warnings: warnings); } private GateResult EvaluateWithoutBaseline( @@ -168,15 +168,15 @@ public sealed class CveDeltaGate : IPolicyGate if (highSeverity > 0) { - return GateResult.Pass(Id, message, + return GateResultFactory.Pass(Id, message, warnings: new[] { $"First release contains {highSeverity} high+ severity CVE(s)" }); } - return GateResult.Pass(Id, message); + return GateResultFactory.Pass(Id, message); } // Require baseline - return GateResult.Fail(Id, "CVE delta gate requires baseline reference but none provided"); + return GateResultFactory.Fail(Id, "CVE delta gate requires baseline reference but none provided"); } private static List CheckRemediationSla( @@ -193,7 +193,7 @@ public sealed class CveDeltaGate : IPolicyGate continue; // Get first seen date from context metadata - if (context.CveFirstSeenDates?.TryGetValue(cve.CveId, out var firstSeen) == true) + if (context.GetCveFirstSeenDates()?.TryGetValue(cve.CveId, out var firstSeen) == true) { var daysSinceFirstSeen = (DateTimeOffset.UtcNow - firstSeen).TotalDays; if (daysSinceFirstSeen > slaDays) diff --git a/src/Policy/__Libraries/StellaOps.Policy/Gates/Cve/CveGateHelpers.cs b/src/Policy/__Libraries/StellaOps.Policy/Gates/Cve/CveGateHelpers.cs index 905e4708e..f382d3f55 100644 --- a/src/Policy/__Libraries/StellaOps.Policy/Gates/Cve/CveGateHelpers.cs +++ b/src/Policy/__Libraries/StellaOps.Policy/Gates/Cve/CveGateHelpers.cs @@ -13,7 +13,7 @@ namespace StellaOps.Policy.Gates.Cve; /// Static helper methods for creating GateResult instances. /// Simplifies gate implementation with consistent result creation. /// -public static class GateResult +public static class GateResultFactory { /// /// Creates a passing gate result. @@ -170,10 +170,10 @@ public sealed record ExtendedPolicyGateContext : PolicyGateContext } /// -/// IPolicyGate interface for CVE gates. +/// ICveGate interface for CVE gates. /// Simplified interface without MergeResult for CVE-specific gates. /// -public interface IPolicyGate +public interface ICveGate { /// /// Gate identifier. diff --git a/src/Policy/__Libraries/StellaOps.Policy/Gates/Cve/CveGatesServiceCollectionExtensions.cs b/src/Policy/__Libraries/StellaOps.Policy/Gates/Cve/CveGatesServiceCollectionExtensions.cs index a43ac7515..7f48ccdfe 100644 --- a/src/Policy/__Libraries/StellaOps.Policy/Gates/Cve/CveGatesServiceCollectionExtensions.cs +++ b/src/Policy/__Libraries/StellaOps.Policy/Gates/Cve/CveGatesServiceCollectionExtensions.cs @@ -67,7 +67,7 @@ public static class CveGatesServiceCollectionExtensions options); }); - services.AddSingleton(sp => sp.GetRequiredService()); + services.AddSingleton(sp => sp.GetRequiredService()); return services; } @@ -96,7 +96,7 @@ public static class CveGatesServiceCollectionExtensions options); }); - services.AddSingleton(sp => sp.GetRequiredService()); + services.AddSingleton(sp => sp.GetRequiredService()); return services; } @@ -122,7 +122,7 @@ public static class CveGatesServiceCollectionExtensions return new ReachableCveGate(options); }); - services.AddSingleton(sp => sp.GetRequiredService()); + services.AddSingleton(sp => sp.GetRequiredService()); return services; } @@ -149,7 +149,7 @@ public static class CveGatesServiceCollectionExtensions return new CveDeltaGate(options, deltaProvider); }); - services.AddSingleton(sp => sp.GetRequiredService()); + services.AddSingleton(sp => sp.GetRequiredService()); return services; } @@ -175,7 +175,7 @@ public static class CveGatesServiceCollectionExtensions return new ReleaseAggregateCveGate(options); }); - services.AddSingleton(sp => sp.GetRequiredService()); + services.AddSingleton(sp => sp.GetRequiredService()); return services; } diff --git a/src/Policy/__Libraries/StellaOps.Policy/Gates/Cve/EpssThresholdGate.cs b/src/Policy/__Libraries/StellaOps.Policy/Gates/Cve/EpssThresholdGate.cs index a0de7539f..66a765241 100644 --- a/src/Policy/__Libraries/StellaOps.Policy/Gates/Cve/EpssThresholdGate.cs +++ b/src/Policy/__Libraries/StellaOps.Policy/Gates/Cve/EpssThresholdGate.cs @@ -11,7 +11,7 @@ namespace StellaOps.Policy.Gates.Cve; /// Policy gate that blocks releases based on EPSS exploitation probability. /// EPSS + reachability enables accurate risk-based gating. /// -public sealed class EpssThresholdGate : IPolicyGate +public sealed class EpssThresholdGate : ICveGate { /// /// Gate identifier. @@ -48,7 +48,7 @@ public sealed class EpssThresholdGate : IPolicyGate if (!_options.Enabled) { - return GateResult.Pass(Id, "EPSS threshold gate disabled"); + return GateResultFactory.Pass(Id, "EPSS threshold gate disabled"); } var envOptions = GetEnvironmentOptions(context.Environment); @@ -56,7 +56,7 @@ public sealed class EpssThresholdGate : IPolicyGate if (cves == null || cves.Count == 0) { - return GateResult.Pass(Id, "No CVE findings to evaluate"); + return GateResultFactory.Pass(Id, "No CVE findings to evaluate"); } // Batch fetch EPSS scores @@ -122,7 +122,7 @@ public sealed class EpssThresholdGate : IPolicyGate message += $" and {violations.Count - 5} more"; } - return GateResult.Fail(Id, message); + return GateResultFactory.Fail(Id, message); } var passMessage = $"EPSS check passed for {cves.Count} CVE(s)"; @@ -131,7 +131,7 @@ public sealed class EpssThresholdGate : IPolicyGate passMessage += $" ({warnings.Count} warnings)"; } - return GateResult.Pass(Id, passMessage, warnings: warnings); + return GateResultFactory.Pass(Id, passMessage, warnings: warnings); } private void HandleMissingEpss( diff --git a/src/Policy/__Libraries/StellaOps.Policy/Gates/Cve/KevBlockerGate.cs b/src/Policy/__Libraries/StellaOps.Policy/Gates/Cve/KevBlockerGate.cs index 93fd348dc..d19f64c60 100644 --- a/src/Policy/__Libraries/StellaOps.Policy/Gates/Cve/KevBlockerGate.cs +++ b/src/Policy/__Libraries/StellaOps.Policy/Gates/Cve/KevBlockerGate.cs @@ -11,7 +11,7 @@ namespace StellaOps.Policy.Gates.Cve; /// Policy gate that blocks releases containing CVEs in the CISA Known Exploited /// Vulnerabilities (KEV) catalog. KEV entries are actively exploited in the wild. /// -public sealed class KevBlockerGate : IPolicyGate +public sealed class KevBlockerGate : ICveGate { /// /// Gate identifier. @@ -48,7 +48,7 @@ public sealed class KevBlockerGate : IPolicyGate if (!_options.Enabled) { - return GateResult.Pass(Id, "KEV blocker gate disabled"); + return GateResultFactory.Pass(Id, "KEV blocker gate disabled"); } var envOptions = GetEnvironmentOptions(context.Environment); @@ -56,7 +56,7 @@ public sealed class KevBlockerGate : IPolicyGate if (cves == null || cves.Count == 0) { - return GateResult.Pass(Id, "No CVE findings to evaluate"); + return GateResultFactory.Pass(Id, "No CVE findings to evaluate"); } // Batch check KEV membership @@ -121,10 +121,10 @@ public sealed class KevBlockerGate : IPolicyGate message += $" and {violations.Count - 5} more"; } - return GateResult.Fail(Id, message); + return GateResultFactory.Fail(Id, message); } - return GateResult.Pass(Id, $"No KEV entries found among {cves.Count} CVE(s)"); + return GateResultFactory.Pass(Id, $"No KEV entries found among {cves.Count} CVE(s)"); } private KevBlockerGateOptions GetEnvironmentOptions(string? environment) diff --git a/src/Policy/__Libraries/StellaOps.Policy/Gates/Cve/ReachableCveGate.cs b/src/Policy/__Libraries/StellaOps.Policy/Gates/Cve/ReachableCveGate.cs index 66f51a922..f5adc8543 100644 --- a/src/Policy/__Libraries/StellaOps.Policy/Gates/Cve/ReachableCveGate.cs +++ b/src/Policy/__Libraries/StellaOps.Policy/Gates/Cve/ReachableCveGate.cs @@ -11,7 +11,7 @@ namespace StellaOps.Policy.Gates.Cve; /// Policy gate that only blocks CVEs that are confirmed reachable in the application. /// Reduces false positives by ignoring unreachable vulnerable code. /// -public sealed class ReachableCveGate : IPolicyGate +public sealed class ReachableCveGate : ICveGate { /// /// Gate identifier. @@ -44,7 +44,7 @@ public sealed class ReachableCveGate : IPolicyGate if (!_options.Enabled) { - return Task.FromResult(GateResult.Pass(Id, "Reachable CVE gate disabled")); + return Task.FromResult(GateResultFactory.Pass(Id, "Reachable CVE gate disabled")); } var envOptions = GetEnvironmentOptions(context.Environment); @@ -52,7 +52,7 @@ public sealed class ReachableCveGate : IPolicyGate if (cves == null || cves.Count == 0) { - return Task.FromResult(GateResult.Pass(Id, "No CVE findings to evaluate")); + return Task.FromResult(GateResultFactory.Pass(Id, "No CVE findings to evaluate")); } var reachableCves = new List(); @@ -109,7 +109,7 @@ public sealed class ReachableCveGate : IPolicyGate message += $" and {blocking.Count - 5} more"; } - return Task.FromResult(GateResult.Fail(Id, message)); + return Task.FromResult(GateResultFactory.Fail(Id, message)); } var passMessage = $"No blocking reachable CVEs. " + @@ -117,7 +117,7 @@ public sealed class ReachableCveGate : IPolicyGate $"Unreachable: {unreachableCves.Count}, " + $"Unknown: {unknownReachability.Count}"; - return Task.FromResult(GateResult.Pass(Id, passMessage, warnings: warnings)); + return Task.FromResult(GateResultFactory.Pass(Id, passMessage, warnings: warnings)); } private ReachableCveGateOptions GetEnvironmentOptions(string? environment) diff --git a/src/Policy/__Libraries/StellaOps.Policy/Gates/Cve/ReleaseAggregateCveGate.cs b/src/Policy/__Libraries/StellaOps.Policy/Gates/Cve/ReleaseAggregateCveGate.cs index b464a4071..2c760d75d 100644 --- a/src/Policy/__Libraries/StellaOps.Policy/Gates/Cve/ReleaseAggregateCveGate.cs +++ b/src/Policy/__Libraries/StellaOps.Policy/Gates/Cve/ReleaseAggregateCveGate.cs @@ -11,7 +11,7 @@ namespace StellaOps.Policy.Gates.Cve; /// Policy gate that enforces aggregate CVE limits per release. /// Unlike CvssThresholdGate which operates per-finding, this operates per-release. /// -public sealed class ReleaseAggregateCveGate : IPolicyGate +public sealed class ReleaseAggregateCveGate : ICveGate { /// /// Gate identifier. @@ -44,7 +44,7 @@ public sealed class ReleaseAggregateCveGate : IPolicyGate if (!_options.Enabled) { - return Task.FromResult(GateResult.Pass(Id, "Release aggregate CVE gate disabled")); + return Task.FromResult(GateResultFactory.Pass(Id, "Release aggregate CVE gate disabled")); } var envOptions = GetEnvironmentOptions(context.Environment); @@ -52,7 +52,7 @@ public sealed class ReleaseAggregateCveGate : IPolicyGate if (cves == null || cves.Count == 0) { - return Task.FromResult(GateResult.Pass(Id, "No CVE findings in release")); + return Task.FromResult(GateResultFactory.Pass(Id, "No CVE findings in release")); } // Filter CVEs based on options @@ -74,13 +74,13 @@ public sealed class ReleaseAggregateCveGate : IPolicyGate string.Join(", ", violations.Select(v => $"{v.Severity}: {v.Count}/{v.Limit}")); - return Task.FromResult(GateResult.Fail(Id, message)); + return Task.FromResult(GateResultFactory.Fail(Id, message)); } var passMessage = $"Release CVE counts within limits. " + $"Critical: {counts.Critical}, High: {counts.High}, Medium: {counts.Medium}, Low: {counts.Low}"; - return Task.FromResult(GateResult.Pass(Id, passMessage, warnings: warnings)); + return Task.FromResult(GateResultFactory.Pass(Id, passMessage, warnings: warnings)); } private IReadOnlyList FilterCves( diff --git a/src/Policy/__Libraries/StellaOps.Policy/Gates/PolicyGateAbstractions.cs b/src/Policy/__Libraries/StellaOps.Policy/Gates/PolicyGateAbstractions.cs index 38d62bb71..ce0975734 100644 --- a/src/Policy/__Libraries/StellaOps.Policy/Gates/PolicyGateAbstractions.cs +++ b/src/Policy/__Libraries/StellaOps.Policy/Gates/PolicyGateAbstractions.cs @@ -3,7 +3,7 @@ using StellaOps.Policy.TrustLattice; namespace StellaOps.Policy.Gates; -public sealed record PolicyGateContext +public record PolicyGateContext { public string Environment { get; init; } = "production"; public int UnknownCount { get; init; } @@ -42,6 +42,77 @@ public sealed record GateResult public required bool Passed { get; init; } public required string? Reason { get; init; } public required ImmutableDictionary Details { get; init; } + + /// + /// Creates a passing gate result. + /// + public static GateResult Pass(string gateName, string reason, IEnumerable? warnings = null) + { + var details = ImmutableDictionary.Empty; + if (warnings != null) + { + var warningList = warnings.ToList(); + if (warningList.Count > 0) + { + details = details.Add("warnings", warningList); + } + } + return new GateResult + { + GateName = gateName, + Passed = true, + Reason = reason, + Details = details + }; + } + + /// + /// Creates a passing gate result with child gate results. + /// + public static GateResult Pass(string gateName, string reason, IReadOnlyList? childResults) + { + var details = childResults != null && childResults.Count > 0 + ? ImmutableDictionary.Empty.Add("childResults", childResults) + : ImmutableDictionary.Empty; + return new GateResult + { + GateName = gateName, + Passed = true, + Reason = reason, + Details = details + }; + } + + /// + /// Creates a failing gate result. + /// + public static GateResult Fail(string gateName, string reason, ImmutableDictionary? details = null) + { + return new GateResult + { + GateName = gateName, + Passed = false, + Reason = reason, + Details = details ?? ImmutableDictionary.Empty + }; + } + + /// + /// Creates a failing gate result with child gate results. + /// + public static GateResult Fail(string gateName, string reason, IReadOnlyList? childResults) + { + var details = childResults != null && childResults.Count > 0 + ? ImmutableDictionary.Empty.Add("childResults", childResults) + : ImmutableDictionary.Empty; + return new GateResult + { + GateName = gateName, + Passed = false, + Reason = reason, + Details = details + }; + } } public sealed record GateEvaluationResult @@ -51,6 +122,9 @@ public sealed record GateEvaluationResult public GateResult? FirstFailure => Results.FirstOrDefault(r => !r.Passed); } +/// +/// Policy gate interface for gates that require MergeResult. +/// public interface IPolicyGate { Task EvaluateAsync( @@ -59,6 +133,33 @@ public interface IPolicyGate CancellationToken ct = default); } +/// +/// Simplified policy gate interface for context-only evaluation. +/// Used by attestation, runtime witness, and CVE gates that don't require MergeResult. +/// +public interface IContextPolicyGate +{ + /// + /// Gate identifier. + /// + string Id { get; } + + /// + /// Display name for the gate. + /// + string DisplayName { get; } + + /// + /// Description of what the gate checks. + /// + string Description { get; } + + /// + /// Evaluates the gate against the given context. + /// + Task EvaluateAsync(PolicyGateContext context, CancellationToken ct = default); +} + public sealed record PolicyGateRegistryOptions { public bool StopOnFirstFailure { get; init; } = true; diff --git a/src/Policy/__Libraries/StellaOps.Policy/Gates/RuntimeWitness/RuntimeWitnessGate.cs b/src/Policy/__Libraries/StellaOps.Policy/Gates/RuntimeWitness/RuntimeWitnessGate.cs index fabbc499d..05ef5b585 100644 --- a/src/Policy/__Libraries/StellaOps.Policy/Gates/RuntimeWitness/RuntimeWitnessGate.cs +++ b/src/Policy/__Libraries/StellaOps.Policy/Gates/RuntimeWitness/RuntimeWitnessGate.cs @@ -11,7 +11,7 @@ namespace StellaOps.Policy.Gates.RuntimeWitness; /// Policy gate that requires runtime witness confirmation for reachability claims. /// Follows VexProofGate anchor-aware pattern. /// -public sealed class RuntimeWitnessGate : IPolicyGate +public sealed class RuntimeWitnessGate : IContextPolicyGate { /// /// Gate identifier. diff --git a/src/Policy/__Libraries/StellaOps.Policy/Gates/UnknownsGateChecker.cs b/src/Policy/__Libraries/StellaOps.Policy/Gates/UnknownsGateChecker.cs index f9afd4e03..6008440c8 100644 --- a/src/Policy/__Libraries/StellaOps.Policy/Gates/UnknownsGateChecker.cs +++ b/src/Policy/__Libraries/StellaOps.Policy/Gates/UnknownsGateChecker.cs @@ -117,7 +117,7 @@ public interface IUnknownsGateChecker /// /// Requests an exception to bypass the gate. /// - Task RequestExceptionAsync( + Task RequestExceptionAsync( string bomRef, IEnumerable unknownIds, string justification, @@ -128,7 +128,7 @@ public interface IUnknownsGateChecker /// /// Exception request result. /// -public sealed record ExceptionResult +public sealed record ExceptionCheckResult { /// Whether exception was granted. public bool Granted { get; init; } @@ -338,7 +338,7 @@ public sealed class UnknownsGateChecker : IUnknownsGateChecker } } - public async Task RequestExceptionAsync( + public async Task RequestExceptionAsync( string bomRef, IEnumerable unknownIds, string justification, @@ -352,7 +352,7 @@ public sealed class UnknownsGateChecker : IUnknownsGateChecker // In production, this would create an exception record await Task.Delay(10, ct); - return new ExceptionResult + return new ExceptionCheckResult { Granted = false, DenialReason = "Automatic exceptions not enabled - requires manual approval", diff --git a/src/Policy/__Libraries/StellaOps.Policy/StellaOps.Policy.csproj b/src/Policy/__Libraries/StellaOps.Policy/StellaOps.Policy.csproj index 3a0a369b0..4b52917a3 100644 --- a/src/Policy/__Libraries/StellaOps.Policy/StellaOps.Policy.csproj +++ b/src/Policy/__Libraries/StellaOps.Policy/StellaOps.Policy.csproj @@ -35,4 +35,17 @@ + + + + + + + + + + + + + diff --git a/src/Policy/__Libraries/StellaOps.Policy/TrustLattice/PolicyBundle.cs b/src/Policy/__Libraries/StellaOps.Policy/TrustLattice/PolicyBundle.cs index f52fef367..d6c9ffa9f 100644 --- a/src/Policy/__Libraries/StellaOps.Policy/TrustLattice/PolicyBundle.cs +++ b/src/Policy/__Libraries/StellaOps.Policy/TrustLattice/PolicyBundle.cs @@ -334,7 +334,7 @@ public sealed record PolicyBundle { Name = r.Name, Priority = r.Priority, - Condition = r.Condition, + Condition = r.ConditionDescription, Disposition = r.Disposition.ToString() }) .ToList(), diff --git a/src/Policy/__Libraries/StellaOps.Policy/TrustLattice/TrustLabel.cs b/src/Policy/__Libraries/StellaOps.Policy/TrustLattice/TrustLabel.cs index b8b7fde64..f135e0283 100644 --- a/src/Policy/__Libraries/StellaOps.Policy/TrustLattice/TrustLabel.cs +++ b/src/Policy/__Libraries/StellaOps.Policy/TrustLattice/TrustLabel.cs @@ -169,6 +169,16 @@ public enum PrincipalRole /// public sealed record AuthorityScope { + /// + /// Scope type for canonical serialization. + /// + public string Type { get; init; } = "default"; + + /// + /// Constraint expression for the scope. + /// + public string? Constraint { get; init; } + /// /// Product namespace patterns (e.g., "vendor.example/*"). /// Principal is authoritative for these products. @@ -335,6 +345,11 @@ public sealed record Principal /// public required string Id { get; init; } + /// + /// Principal type for canonical serialization. + /// + public string Type { get; init; } = "identity"; + /// /// Key identifiers for verification. ///