save progress

This commit is contained in:
StellaOps Bot
2026-01-06 09:42:02 +02:00
parent 94d68bee8b
commit 37e11918e0
443 changed files with 85863 additions and 897 deletions

View File

@@ -0,0 +1,229 @@
// <copyright file="FacetQuotaGate.cs" company="StellaOps">
// Copyright (c) StellaOps. Licensed under AGPL-3.0-or-later.
// </copyright>
using System.Collections.Immutable;
using Microsoft.Extensions.Logging;
using StellaOps.Facet;
using StellaOps.Policy.TrustLattice;
namespace StellaOps.Policy.Gates;
/// <summary>
/// Configuration options for <see cref="FacetQuotaGate"/>.
/// </summary>
public sealed record FacetQuotaGateOptions
{
/// <summary>
/// Gets or sets a value indicating whether the gate is enabled.
/// </summary>
public bool Enabled { get; init; } = true;
/// <summary>
/// Gets or sets the action to take when no facet seal is available for comparison.
/// </summary>
public NoSealAction NoSealAction { get; init; } = NoSealAction.Pass;
/// <summary>
/// Gets or sets the default quota to apply when no facet-specific quota is configured.
/// </summary>
public FacetQuota DefaultQuota { get; init; } = FacetQuota.Default;
/// <summary>
/// Gets or sets per-facet quota overrides.
/// </summary>
public ImmutableDictionary<string, FacetQuota> FacetQuotas { get; init; } =
ImmutableDictionary<string, FacetQuota>.Empty;
}
/// <summary>
/// Specifies the action when no baseline seal is available.
/// </summary>
public enum NoSealAction
{
/// <summary>
/// Pass the gate when no seal is available (first scan).
/// </summary>
Pass,
/// <summary>
/// Warn when no seal is available.
/// </summary>
Warn,
/// <summary>
/// Block when no seal is available.
/// </summary>
Block
}
/// <summary>
/// Policy gate that enforces per-facet drift quotas.
/// This gate evaluates facet drift reports and enforces quotas configured per facet.
/// </summary>
/// <remarks>
/// The FacetQuotaGate operates on pre-computed <see cref="FacetDriftReport"/> instances,
/// which should be attached to the <see cref="PolicyGateContext"/> before evaluation.
/// If no drift report is available, the gate behavior is determined by <see cref="FacetQuotaGateOptions.NoSealAction"/>.
/// </remarks>
public sealed class FacetQuotaGate : IPolicyGate
{
private readonly FacetQuotaGateOptions _options;
private readonly IFacetDriftDetector _driftDetector;
private readonly ILogger<FacetQuotaGate> _logger;
/// <summary>
/// Initializes a new instance of the <see cref="FacetQuotaGate"/> class.
/// </summary>
/// <param name="options">Gate configuration options.</param>
/// <param name="driftDetector">The facet drift detector.</param>
/// <param name="logger">Logger instance.</param>
public FacetQuotaGate(
FacetQuotaGateOptions? options = null,
IFacetDriftDetector? driftDetector = null,
ILogger<FacetQuotaGate>? logger = null)
{
_options = options ?? new FacetQuotaGateOptions();
_driftDetector = driftDetector ?? throw new ArgumentNullException(nameof(driftDetector));
_logger = logger ?? Microsoft.Extensions.Logging.Abstractions.NullLogger<FacetQuotaGate>.Instance;
}
/// <inheritdoc/>
public Task<GateResult> EvaluateAsync(
MergeResult mergeResult,
PolicyGateContext context,
CancellationToken ct = default)
{
ArgumentNullException.ThrowIfNull(mergeResult);
ArgumentNullException.ThrowIfNull(context);
// Check if gate is enabled
if (!_options.Enabled)
{
return Task.FromResult(Pass("Gate disabled"));
}
// Check for drift report in metadata
var driftReport = GetDriftReportFromContext(context);
if (driftReport is null)
{
return Task.FromResult(HandleNoSeal());
}
// Evaluate drift report against quotas
var result = EvaluateDriftReport(driftReport);
return Task.FromResult(result);
}
private static FacetDriftReport? GetDriftReportFromContext(PolicyGateContext context)
{
// Drift report is expected to be in metadata under a well-known key
if (context.Metadata?.TryGetValue("FacetDriftReport", out var value) == true &&
value is string json)
{
// In a real implementation, deserialize from JSON
// For now, return null to trigger the no-seal path
return null;
}
return null;
}
private GateResult HandleNoSeal()
{
return _options.NoSealAction switch
{
NoSealAction.Pass => Pass("No baseline seal available - first scan"),
NoSealAction.Warn => new GateResult
{
GateName = nameof(FacetQuotaGate),
Passed = true,
Reason = "no_baseline_seal",
Details = ImmutableDictionary<string, object>.Empty
.Add("action", "warn")
.Add("message", "No baseline seal available for comparison")
},
NoSealAction.Block => new GateResult
{
GateName = nameof(FacetQuotaGate),
Passed = false,
Reason = "no_baseline_seal",
Details = ImmutableDictionary<string, object>.Empty
.Add("action", "block")
.Add("message", "Baseline seal required but not available")
},
_ => Pass("Unknown NoSealAction - defaulting to pass")
};
}
private GateResult EvaluateDriftReport(FacetDriftReport report)
{
// Find worst verdict across all facets
var worstVerdict = report.OverallVerdict;
var breachedFacets = report.FacetDrifts
.Where(d => d.QuotaVerdict != QuotaVerdict.Ok)
.ToList();
if (breachedFacets.Count == 0)
{
_logger.LogDebug("All facets within quota limits");
return Pass("All facets within quota limits");
}
// Build details
var details = ImmutableDictionary<string, object>.Empty
.Add("overallVerdict", worstVerdict.ToString())
.Add("breachedFacets", breachedFacets.Select(f => f.FacetId).ToArray())
.Add("totalChangedFiles", report.TotalChangedFiles)
.Add("imageDigest", report.ImageDigest);
foreach (var facet in breachedFacets)
{
details = details.Add(
$"facet:{facet.FacetId}",
new Dictionary<string, object>
{
["verdict"] = facet.QuotaVerdict.ToString(),
["churnPercent"] = facet.ChurnPercent,
["added"] = facet.Added.Length,
["removed"] = facet.Removed.Length,
["modified"] = facet.Modified.Length
});
}
return worstVerdict switch
{
QuotaVerdict.Ok => Pass("All quotas satisfied"),
QuotaVerdict.Warning => new GateResult
{
GateName = nameof(FacetQuotaGate),
Passed = true,
Reason = "quota_warning",
Details = details
},
QuotaVerdict.Blocked => new GateResult
{
GateName = nameof(FacetQuotaGate),
Passed = false,
Reason = "quota_exceeded",
Details = details
},
QuotaVerdict.RequiresVex => new GateResult
{
GateName = nameof(FacetQuotaGate),
Passed = false,
Reason = "requires_vex_authorization",
Details = details.Add("vexRequired", true)
},
_ => Pass("Unknown verdict - defaulting to pass")
};
}
private static GateResult Pass(string reason) => new()
{
GateName = nameof(FacetQuotaGate),
Passed = true,
Reason = reason,
Details = ImmutableDictionary<string, object>.Empty
};
}

View File

@@ -0,0 +1,73 @@
// <copyright file="FacetQuotaGateServiceCollectionExtensions.cs" company="StellaOps">
// Copyright (c) StellaOps. Licensed under AGPL-3.0-or-later.
// </copyright>
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using StellaOps.Facet;
namespace StellaOps.Policy.Gates;
/// <summary>
/// Extension methods for registering <see cref="FacetQuotaGate"/> with dependency injection.
/// </summary>
public static class FacetQuotaGateServiceCollectionExtensions
{
/// <summary>
/// Adds the <see cref="FacetQuotaGate"/> to the service collection with default options.
/// </summary>
/// <param name="services">The service collection.</param>
/// <returns>The service collection for chaining.</returns>
public static IServiceCollection AddFacetQuotaGate(this IServiceCollection services)
{
ArgumentNullException.ThrowIfNull(services);
return services.AddFacetQuotaGate(_ => { });
}
/// <summary>
/// Adds the <see cref="FacetQuotaGate"/> to the service collection with custom configuration.
/// </summary>
/// <param name="services">The service collection.</param>
/// <param name="configure">Action to configure <see cref="FacetQuotaGateOptions"/>.</param>
/// <returns>The service collection for chaining.</returns>
public static IServiceCollection AddFacetQuotaGate(
this IServiceCollection services,
Action<FacetQuotaGateOptions> configure)
{
ArgumentNullException.ThrowIfNull(services);
ArgumentNullException.ThrowIfNull(configure);
var options = new FacetQuotaGateOptions();
configure(options);
// Ensure facet drift detector is registered
services.TryAddSingleton<IFacetDriftDetector>(sp =>
{
var timeProvider = sp.GetService<TimeProvider>() ?? TimeProvider.System;
return new FacetDriftDetector(timeProvider);
});
// Register the gate options
services.AddSingleton(options);
// Register the gate
services.TryAddSingleton<FacetQuotaGate>();
return services;
}
/// <summary>
/// Registers the <see cref="FacetQuotaGate"/> with a <see cref="IPolicyGateRegistry"/>.
/// </summary>
/// <param name="registry">The policy gate registry.</param>
/// <param name="gateName">Optional custom gate name. Defaults to "facet-quota".</param>
/// <returns>The registry for chaining.</returns>
public static IPolicyGateRegistry RegisterFacetQuotaGate(
this IPolicyGateRegistry registry,
string gateName = "facet-quota")
{
ArgumentNullException.ThrowIfNull(registry);
registry.Register<FacetQuotaGate>(gateName);
return registry;
}
}