Gaps fill up, fixes, ui restructuring
This commit is contained in:
@@ -15,6 +15,7 @@ using StellaOps.Policy.Engine.Events;
|
||||
using StellaOps.Policy.Engine.ExceptionCache;
|
||||
using StellaOps.Policy.Engine.Gates;
|
||||
using StellaOps.Policy.Engine.Options;
|
||||
using StellaOps.Policy.Gates;
|
||||
using StellaOps.Policy.Engine.ReachabilityFacts;
|
||||
using StellaOps.Policy.Engine.Scoring.EvidenceWeightedScore;
|
||||
using StellaOps.Policy.Engine.Services;
|
||||
@@ -232,6 +233,28 @@ public static class PolicyEngineServiceCollectionExtensions
|
||||
return services.AddPolicyDecisionAttestation();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds the execution evidence policy gate.
|
||||
/// Enforces that artifacts have signed execution evidence before promotion.
|
||||
/// Sprint: SPRINT_20260219_013 (SEE-03)
|
||||
/// </summary>
|
||||
public static IServiceCollection AddExecutionEvidenceGate(this IServiceCollection services)
|
||||
{
|
||||
services.TryAddSingleton<IContextPolicyGate, ExecutionEvidenceGate>();
|
||||
return services;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds the beacon rate policy gate.
|
||||
/// Enforces minimum beacon verification rate for runtime canary coverage.
|
||||
/// Sprint: SPRINT_20260219_014 (BEA-03)
|
||||
/// </summary>
|
||||
public static IServiceCollection AddBeaconRateGate(this IServiceCollection services)
|
||||
{
|
||||
services.TryAddSingleton<IContextPolicyGate, BeaconRateGate>();
|
||||
return services;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds build gate evaluators for exception recheck policies.
|
||||
/// </summary>
|
||||
@@ -328,6 +351,11 @@ public static class PolicyEngineServiceCollectionExtensions
|
||||
// Always registered; activation controlled by PolicyEvidenceWeightedScoreOptions.Enabled
|
||||
services.AddEvidenceWeightedScore();
|
||||
|
||||
// Execution evidence and beacon rate gates (Sprint: SPRINT_20260219_013/014)
|
||||
// Always registered; activation controlled by PolicyGateOptions.ExecutionEvidence.Enabled / BeaconRate.Enabled
|
||||
services.AddExecutionEvidenceGate();
|
||||
services.AddBeaconRateGate();
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
|
||||
115
src/Policy/StellaOps.Policy.Engine/Gates/BeaconRateGate.cs
Normal file
115
src/Policy/StellaOps.Policy.Engine/Gates/BeaconRateGate.cs
Normal file
@@ -0,0 +1,115 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Policy.Gates;
|
||||
using System.Collections.Immutable;
|
||||
using System.Globalization;
|
||||
|
||||
namespace StellaOps.Policy.Engine.Gates;
|
||||
|
||||
/// <summary>
|
||||
/// Policy gate that enforces beacon verification rate thresholds.
|
||||
/// When enabled, blocks or warns for releases where beacon coverage is insufficient.
|
||||
/// Sprint: SPRINT_20260219_014 (BEA-03)
|
||||
/// </summary>
|
||||
public sealed class BeaconRateGate : IContextPolicyGate
|
||||
{
|
||||
private readonly IOptionsMonitor<PolicyGateOptions> _options;
|
||||
private readonly ILogger<BeaconRateGate> _logger;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
|
||||
public string Id => "beacon-rate";
|
||||
public string DisplayName => "Beacon Verification Rate";
|
||||
public string Description => "Enforces minimum beacon verification rate for runtime canary coverage.";
|
||||
|
||||
public BeaconRateGate(
|
||||
IOptionsMonitor<PolicyGateOptions> options,
|
||||
ILogger<BeaconRateGate> logger,
|
||||
TimeProvider? timeProvider = null)
|
||||
{
|
||||
_options = options ?? throw new ArgumentNullException(nameof(options));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||
}
|
||||
|
||||
public Task<GateResult> EvaluateAsync(PolicyGateContext context, CancellationToken ct = default)
|
||||
{
|
||||
var gateOpts = _options.CurrentValue.BeaconRate;
|
||||
|
||||
if (!gateOpts.Enabled)
|
||||
{
|
||||
return Task.FromResult(GateResult.Pass(Id, "Beacon rate gate is disabled"));
|
||||
}
|
||||
|
||||
var environment = context.Environment ?? "unknown";
|
||||
|
||||
// Check if environment requires beacon coverage.
|
||||
if (!gateOpts.RequiredEnvironments.Contains(environment, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
return Task.FromResult(GateResult.Pass(Id, $"Beacon rate not required for environment '{environment}'"));
|
||||
}
|
||||
|
||||
// Read beacon data from context metadata.
|
||||
double verificationRate = 0;
|
||||
var hasBeaconData = context.Metadata?.TryGetValue("beacon_verification_rate", out var rateStr) == true
|
||||
&& double.TryParse(rateStr, CultureInfo.InvariantCulture, out verificationRate);
|
||||
|
||||
if (!hasBeaconData)
|
||||
{
|
||||
_logger.LogInformation(
|
||||
"BeaconRateGate: no beacon data for environment {Environment}, subject {Subject}",
|
||||
environment, context.SubjectKey);
|
||||
|
||||
return gateOpts.MissingBeaconAction == PolicyGateDecisionType.Block
|
||||
? Task.FromResult(GateResult.Fail(Id, "No beacon telemetry data available for this artifact",
|
||||
ImmutableDictionary<string, object>.Empty
|
||||
.Add("environment", environment)
|
||||
.Add("suggestion", "Ensure beacon instrumentation is active in the target environment")))
|
||||
: Task.FromResult(GateResult.Pass(Id, "No beacon data available (warn mode)",
|
||||
new[] { "No beacon telemetry found; ensure runtime canary beacons are deployed" }));
|
||||
}
|
||||
|
||||
// Check minimum beacon count before enforcing rate.
|
||||
int beaconCount = 0;
|
||||
var hasBeaconCount = context.Metadata?.TryGetValue("beacon_verified_count", out var countStr) == true
|
||||
&& int.TryParse(countStr, out beaconCount);
|
||||
|
||||
if (hasBeaconCount && beaconCount < gateOpts.MinBeaconCount)
|
||||
{
|
||||
_logger.LogDebug(
|
||||
"BeaconRateGate: insufficient beacon count ({Count} < {Min}) for {Environment}; skipping rate check",
|
||||
beaconCount, gateOpts.MinBeaconCount, environment);
|
||||
|
||||
return Task.FromResult(GateResult.Pass(Id,
|
||||
$"Beacon count ({beaconCount}) below minimum ({gateOpts.MinBeaconCount}); rate enforcement deferred",
|
||||
new[] { $"Only {beaconCount} beacons observed; need {gateOpts.MinBeaconCount} before rate enforcement applies" }));
|
||||
}
|
||||
|
||||
// Evaluate rate against threshold.
|
||||
if (verificationRate < gateOpts.MinVerificationRate)
|
||||
{
|
||||
_logger.LogInformation(
|
||||
"BeaconRateGate: rate {Rate:F4} below threshold {Threshold:F4} for {Environment}, subject {Subject}",
|
||||
verificationRate, gateOpts.MinVerificationRate, environment, context.SubjectKey);
|
||||
|
||||
var details = ImmutableDictionary<string, object>.Empty
|
||||
.Add("verification_rate", verificationRate)
|
||||
.Add("threshold", gateOpts.MinVerificationRate)
|
||||
.Add("environment", environment);
|
||||
|
||||
return gateOpts.BelowThresholdAction == PolicyGateDecisionType.Block
|
||||
? Task.FromResult(GateResult.Fail(Id,
|
||||
$"Beacon verification rate ({verificationRate:P1}) is below threshold ({gateOpts.MinVerificationRate:P1})",
|
||||
details.Add("suggestion", "Investigate beacon gaps; possible deployment drift or instrumentation failure")))
|
||||
: Task.FromResult(GateResult.Pass(Id,
|
||||
$"Beacon verification rate ({verificationRate:P1}) is below threshold (warn mode)",
|
||||
new[] { $"Beacon rate {verificationRate:P1} < {gateOpts.MinVerificationRate:P1}; investigate potential gaps" }));
|
||||
}
|
||||
|
||||
_logger.LogDebug(
|
||||
"BeaconRateGate: passed for {Environment}, rate={Rate:F4}, subject {Subject}",
|
||||
environment, verificationRate, context.SubjectKey);
|
||||
|
||||
return Task.FromResult(GateResult.Pass(Id,
|
||||
$"Beacon verification rate ({verificationRate:P1}) meets threshold ({gateOpts.MinVerificationRate:P1})"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Policy.Gates;
|
||||
using System.Collections.Immutable;
|
||||
|
||||
namespace StellaOps.Policy.Engine.Gates;
|
||||
|
||||
/// <summary>
|
||||
/// Policy gate that enforces execution evidence requirements.
|
||||
/// When enabled, blocks or warns for releases without signed execution evidence.
|
||||
/// Sprint: SPRINT_20260219_013 (SEE-03)
|
||||
/// </summary>
|
||||
public sealed class ExecutionEvidenceGate : IContextPolicyGate
|
||||
{
|
||||
private readonly IOptionsMonitor<PolicyGateOptions> _options;
|
||||
private readonly ILogger<ExecutionEvidenceGate> _logger;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
|
||||
public string Id => "execution-evidence";
|
||||
public string DisplayName => "Execution Evidence";
|
||||
public string Description => "Requires signed execution evidence (runtime trace attestation) for production releases.";
|
||||
|
||||
public ExecutionEvidenceGate(
|
||||
IOptionsMonitor<PolicyGateOptions> options,
|
||||
ILogger<ExecutionEvidenceGate> logger,
|
||||
TimeProvider? timeProvider = null)
|
||||
{
|
||||
_options = options ?? throw new ArgumentNullException(nameof(options));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||
}
|
||||
|
||||
public Task<GateResult> EvaluateAsync(PolicyGateContext context, CancellationToken ct = default)
|
||||
{
|
||||
var gateOpts = _options.CurrentValue.ExecutionEvidence;
|
||||
|
||||
if (!gateOpts.Enabled)
|
||||
{
|
||||
return Task.FromResult(GateResult.Pass(Id, "Execution evidence gate is disabled"));
|
||||
}
|
||||
|
||||
var environment = context.Environment ?? "unknown";
|
||||
|
||||
// Check if environment requires execution evidence.
|
||||
if (!gateOpts.RequiredEnvironments.Contains(environment, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
return Task.FromResult(GateResult.Pass(Id, $"Execution evidence not required for environment '{environment}'"));
|
||||
}
|
||||
|
||||
// Check metadata for execution evidence fields.
|
||||
var hasEvidence = context.Metadata?.TryGetValue("has_execution_evidence", out var evidenceStr) == true
|
||||
&& string.Equals(evidenceStr, "true", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
if (!hasEvidence)
|
||||
{
|
||||
_logger.LogInformation(
|
||||
"ExecutionEvidenceGate: missing execution evidence for environment {Environment}, subject {Subject}",
|
||||
environment, context.SubjectKey);
|
||||
|
||||
return gateOpts.MissingEvidenceAction == PolicyGateDecisionType.Block
|
||||
? Task.FromResult(GateResult.Fail(Id, "Signed execution evidence is required for production releases",
|
||||
ImmutableDictionary<string, object>.Empty
|
||||
.Add("environment", environment)
|
||||
.Add("suggestion", "Run the execution evidence pipeline before promoting to production")))
|
||||
: Task.FromResult(GateResult.Pass(Id, "Execution evidence missing (warn mode)",
|
||||
new[] { "No signed execution evidence found; consider running the trace pipeline" }));
|
||||
}
|
||||
|
||||
// Validate evidence quality if hot symbol count is provided.
|
||||
if (context.Metadata?.TryGetValue("execution_evidence_hot_symbol_count", out var hotStr) == true
|
||||
&& int.TryParse(hotStr, out var hotCount)
|
||||
&& hotCount < gateOpts.MinHotSymbolCount)
|
||||
{
|
||||
_logger.LogInformation(
|
||||
"ExecutionEvidenceGate: insufficient hot symbols ({Count} < {Min}) for environment {Environment}",
|
||||
hotCount, gateOpts.MinHotSymbolCount, environment);
|
||||
|
||||
return Task.FromResult(GateResult.Pass(Id,
|
||||
$"Execution evidence has insufficient coverage ({hotCount} hot symbols < {gateOpts.MinHotSymbolCount} minimum)",
|
||||
new[] { $"Execution evidence trace has only {hotCount} hot symbols; minimum is {gateOpts.MinHotSymbolCount}" }));
|
||||
}
|
||||
|
||||
// Validate unique call paths if provided.
|
||||
if (context.Metadata?.TryGetValue("execution_evidence_unique_call_paths", out var pathStr) == true
|
||||
&& int.TryParse(pathStr, out var pathCount)
|
||||
&& pathCount < gateOpts.MinUniqueCallPaths)
|
||||
{
|
||||
return Task.FromResult(GateResult.Pass(Id,
|
||||
$"Execution evidence has insufficient call path coverage ({pathCount} < {gateOpts.MinUniqueCallPaths})",
|
||||
new[] { $"Execution evidence covers only {pathCount} unique call paths; minimum is {gateOpts.MinUniqueCallPaths}" }));
|
||||
}
|
||||
|
||||
_logger.LogDebug(
|
||||
"ExecutionEvidenceGate: passed for environment {Environment}, subject {Subject}",
|
||||
environment, context.SubjectKey);
|
||||
|
||||
return Task.FromResult(GateResult.Pass(Id, "Signed execution evidence is present and meets quality thresholds"));
|
||||
}
|
||||
}
|
||||
@@ -151,6 +151,34 @@ public sealed record PolicyGateEvidence
|
||||
/// </summary>
|
||||
[JsonPropertyName("pathLength")]
|
||||
public int? PathLength { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether signed execution evidence exists for this artifact.
|
||||
/// Sprint: SPRINT_20260219_013 (SEE-03)
|
||||
/// </summary>
|
||||
[JsonPropertyName("hasExecutionEvidence")]
|
||||
public bool HasExecutionEvidence { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Execution evidence predicate digest (sha256:...), if available.
|
||||
/// Sprint: SPRINT_20260219_013 (SEE-03)
|
||||
/// </summary>
|
||||
[JsonPropertyName("executionEvidenceDigest")]
|
||||
public string? ExecutionEvidenceDigest { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Beacon verification rate (0.0 - 1.0), if beacons are observed.
|
||||
/// Sprint: SPRINT_20260219_014 (BEA-03)
|
||||
/// </summary>
|
||||
[JsonPropertyName("beaconVerificationRate")]
|
||||
public double? BeaconVerificationRate { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Total beacons verified in the lookback window.
|
||||
/// Sprint: SPRINT_20260219_014 (BEA-03)
|
||||
/// </summary>
|
||||
[JsonPropertyName("beaconCount")]
|
||||
public int? BeaconCount { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -366,4 +394,45 @@ public sealed record PolicyGateRequest
|
||||
/// Signature verification method (dsse, cosign, pgp, x509).
|
||||
/// </summary>
|
||||
public string? VexSignatureMethod { get; init; }
|
||||
|
||||
// Execution evidence fields (added for ExecutionEvidenceGate integration)
|
||||
// Sprint: SPRINT_20260219_013 (SEE-03)
|
||||
|
||||
/// <summary>
|
||||
/// Whether signed execution evidence exists for this artifact.
|
||||
/// </summary>
|
||||
public bool HasExecutionEvidence { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Execution evidence predicate digest (sha256:...).
|
||||
/// </summary>
|
||||
public string? ExecutionEvidenceDigest { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Number of hot symbols in the execution evidence trace summary.
|
||||
/// </summary>
|
||||
public int? ExecutionEvidenceHotSymbolCount { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Number of unique call paths in the execution evidence.
|
||||
/// </summary>
|
||||
public int? ExecutionEvidenceUniqueCallPaths { get; init; }
|
||||
|
||||
// Beacon attestation fields (added for BeaconRateGate integration)
|
||||
// Sprint: SPRINT_20260219_014 (BEA-03)
|
||||
|
||||
/// <summary>
|
||||
/// Beacon verification rate (0.0 - 1.0) for this artifact/environment.
|
||||
/// </summary>
|
||||
public double? BeaconVerificationRate { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Total beacons verified in the lookback window.
|
||||
/// </summary>
|
||||
public int? BeaconVerifiedCount { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Total beacons expected in the lookback window.
|
||||
/// </summary>
|
||||
public int? BeaconExpectedCount { get; init; }
|
||||
}
|
||||
|
||||
@@ -43,6 +43,18 @@ public sealed class PolicyGateOptions
|
||||
/// </summary>
|
||||
public FacetQuotaGateOptions FacetQuota { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Execution evidence gate options.
|
||||
/// Sprint: SPRINT_20260219_013 (SEE-03)
|
||||
/// </summary>
|
||||
public ExecutionEvidenceGateOptions ExecutionEvidence { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Beacon verification rate gate options.
|
||||
/// Sprint: SPRINT_20260219_014 (BEA-03)
|
||||
/// </summary>
|
||||
public BeaconRateGateOptions BeaconRate { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Whether gates are enabled.
|
||||
/// </summary>
|
||||
@@ -216,3 +228,80 @@ public sealed class FacetQuotaOverride
|
||||
/// </summary>
|
||||
public List<string> AllowlistGlobs { get; set; } = new();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Configuration options for the execution evidence gate.
|
||||
/// When enabled, requires signed execution evidence for production releases.
|
||||
/// Sprint: SPRINT_20260219_013 (SEE-03)
|
||||
/// </summary>
|
||||
public sealed class ExecutionEvidenceGateOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether execution evidence enforcement is enabled.
|
||||
/// </summary>
|
||||
public bool Enabled { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Action when execution evidence is missing.
|
||||
/// </summary>
|
||||
public PolicyGateDecisionType MissingEvidenceAction { get; set; } = PolicyGateDecisionType.Warn;
|
||||
|
||||
/// <summary>
|
||||
/// Environments where execution evidence is required (case-insensitive).
|
||||
/// Default: production only.
|
||||
/// </summary>
|
||||
public List<string> RequiredEnvironments { get; set; } = new() { "production" };
|
||||
|
||||
/// <summary>
|
||||
/// Minimum number of hot symbols to consider evidence meaningful.
|
||||
/// Prevents trivial evidence from satisfying the gate.
|
||||
/// </summary>
|
||||
public int MinHotSymbolCount { get; set; } = 3;
|
||||
|
||||
/// <summary>
|
||||
/// Minimum number of unique call paths to consider evidence meaningful.
|
||||
/// </summary>
|
||||
public int MinUniqueCallPaths { get; set; } = 1;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Configuration options for the beacon verification rate gate.
|
||||
/// When enabled, enforces minimum beacon coverage thresholds.
|
||||
/// Sprint: SPRINT_20260219_014 (BEA-03)
|
||||
/// </summary>
|
||||
public sealed class BeaconRateGateOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether beacon rate enforcement is enabled.
|
||||
/// </summary>
|
||||
public bool Enabled { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Action when beacon rate is below threshold.
|
||||
/// </summary>
|
||||
public PolicyGateDecisionType BelowThresholdAction { get; set; } = PolicyGateDecisionType.Warn;
|
||||
|
||||
/// <summary>
|
||||
/// Action when no beacon data exists for the artifact.
|
||||
/// </summary>
|
||||
public PolicyGateDecisionType MissingBeaconAction { get; set; } = PolicyGateDecisionType.Warn;
|
||||
|
||||
/// <summary>
|
||||
/// Minimum beacon verification rate (0.0 - 1.0).
|
||||
/// Beacon rate below this triggers the BelowThresholdAction.
|
||||
/// Default: 0.8 (80% of expected beacons must be observed).
|
||||
/// </summary>
|
||||
public double MinVerificationRate { get; set; } = 0.8;
|
||||
|
||||
/// <summary>
|
||||
/// Environments where beacon rate is enforced (case-insensitive).
|
||||
/// Default: production only.
|
||||
/// </summary>
|
||||
public List<string> RequiredEnvironments { get; set; } = new() { "production" };
|
||||
|
||||
/// <summary>
|
||||
/// Minimum number of beacons observed before rate enforcement applies.
|
||||
/// Prevents premature gating on insufficient data.
|
||||
/// </summary>
|
||||
public int MinBeaconCount { get; set; } = 10;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user