up
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Concelier Attestation Tests / attestation-tests (push) Has been cancelled
Export Center CI / export-ci (push) Has been cancelled
Notify Smoke Test / Notify Unit Tests (push) Has been cancelled
Notify Smoke Test / Notifier Service Tests (push) Has been cancelled
Notify Smoke Test / Notification Smoke Test (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Scanner Analyzers / Discover Analyzers (push) Has been cancelled
Scanner Analyzers / Build Analyzers (push) Has been cancelled
Scanner Analyzers / Test Language Analyzers (push) Has been cancelled
Scanner Analyzers / Validate Test Fixtures (push) Has been cancelled
Scanner Analyzers / Verify Deterministic Output (push) Has been cancelled
Signals CI & Image / signals-ci (push) Has been cancelled
Signals Reachability Scoring & Events / reachability-smoke (push) Has been cancelled
Signals Reachability Scoring & Events / sign-and-upload (push) Has been cancelled
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Concelier Attestation Tests / attestation-tests (push) Has been cancelled
Export Center CI / export-ci (push) Has been cancelled
Notify Smoke Test / Notify Unit Tests (push) Has been cancelled
Notify Smoke Test / Notifier Service Tests (push) Has been cancelled
Notify Smoke Test / Notification Smoke Test (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Scanner Analyzers / Discover Analyzers (push) Has been cancelled
Scanner Analyzers / Build Analyzers (push) Has been cancelled
Scanner Analyzers / Test Language Analyzers (push) Has been cancelled
Scanner Analyzers / Validate Test Fixtures (push) Has been cancelled
Scanner Analyzers / Verify Deterministic Output (push) Has been cancelled
Signals CI & Image / signals-ci (push) Has been cancelled
Signals Reachability Scoring & Events / reachability-smoke (push) Has been cancelled
Signals Reachability Scoring & Events / sign-and-upload (push) Has been cancelled
This commit is contained in:
@@ -1,161 +1,161 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Globalization;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Excititor.Core;
|
||||
|
||||
namespace StellaOps.Excititor.Policy;
|
||||
|
||||
public interface IVexPolicyProvider
|
||||
{
|
||||
VexPolicySnapshot GetSnapshot();
|
||||
}
|
||||
|
||||
public interface IVexPolicyEvaluator
|
||||
{
|
||||
string Version { get; }
|
||||
|
||||
VexPolicySnapshot Snapshot { get; }
|
||||
|
||||
double GetProviderWeight(VexProvider provider);
|
||||
|
||||
bool IsClaimEligible(VexClaim claim, VexProvider provider, out string? rejectionReason);
|
||||
}
|
||||
|
||||
public sealed record VexPolicySnapshot(
|
||||
string Version,
|
||||
VexConsensusPolicyOptions ConsensusOptions,
|
||||
IVexConsensusPolicy ConsensusPolicy,
|
||||
ImmutableArray<VexPolicyIssue> Issues,
|
||||
string RevisionId,
|
||||
string Digest)
|
||||
{
|
||||
public static readonly VexPolicySnapshot Default = new(
|
||||
VexConsensusPolicyOptions.BaselineVersion,
|
||||
new VexConsensusPolicyOptions(),
|
||||
new BaselineVexConsensusPolicy(),
|
||||
ImmutableArray<VexPolicyIssue>.Empty,
|
||||
"rev-0",
|
||||
string.Empty);
|
||||
}
|
||||
|
||||
public sealed record VexPolicyIssue(
|
||||
string Code,
|
||||
string Message,
|
||||
VexPolicyIssueSeverity Severity);
|
||||
|
||||
public enum VexPolicyIssueSeverity
|
||||
{
|
||||
Warning,
|
||||
Error,
|
||||
}
|
||||
|
||||
public sealed class VexPolicyProvider : IVexPolicyProvider
|
||||
{
|
||||
private readonly IOptionsMonitor<VexPolicyOptions> _options;
|
||||
private readonly ILogger<VexPolicyProvider> _logger;
|
||||
private readonly object _sync = new();
|
||||
private long _revisionCounter;
|
||||
private string? _currentRevisionId;
|
||||
private string? _currentDigest;
|
||||
|
||||
public VexPolicyProvider(
|
||||
IOptionsMonitor<VexPolicyOptions> options,
|
||||
ILogger<VexPolicyProvider> logger)
|
||||
{
|
||||
_options = options ?? throw new ArgumentNullException(nameof(options));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
public VexPolicySnapshot GetSnapshot()
|
||||
{
|
||||
var options = _options.CurrentValue ?? new VexPolicyOptions();
|
||||
return BuildSnapshot(options);
|
||||
}
|
||||
|
||||
private VexPolicySnapshot BuildSnapshot(VexPolicyOptions options)
|
||||
{
|
||||
var normalization = VexPolicyProcessing.Normalize(options);
|
||||
var digest = VexPolicyDigest.Compute(normalization.ConsensusOptions);
|
||||
string revisionId;
|
||||
bool isNewRevision;
|
||||
|
||||
lock (_sync)
|
||||
{
|
||||
if (!string.Equals(_currentDigest, digest, StringComparison.Ordinal))
|
||||
{
|
||||
_revisionCounter++;
|
||||
revisionId = $"rev-{_revisionCounter}";
|
||||
_currentDigest = digest;
|
||||
_currentRevisionId = revisionId;
|
||||
isNewRevision = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
revisionId = _currentRevisionId ?? "rev-0";
|
||||
isNewRevision = false;
|
||||
}
|
||||
}
|
||||
|
||||
var policy = new BaselineVexConsensusPolicy(normalization.ConsensusOptions);
|
||||
var snapshot = new VexPolicySnapshot(
|
||||
normalization.ConsensusOptions.Version,
|
||||
normalization.ConsensusOptions,
|
||||
policy,
|
||||
normalization.Issues,
|
||||
revisionId,
|
||||
digest);
|
||||
|
||||
if (isNewRevision)
|
||||
{
|
||||
_logger.LogInformation(
|
||||
"Policy snapshot updated: revision {RevisionId}, version {Version}, digest {Digest}, issues {IssueCount}",
|
||||
snapshot.RevisionId,
|
||||
snapshot.Version,
|
||||
snapshot.Digest,
|
||||
snapshot.Issues.Length);
|
||||
VexPolicyTelemetry.RecordReload(snapshot.RevisionId, snapshot.Version, snapshot.Issues.Length);
|
||||
}
|
||||
else if (snapshot.Issues.Length > 0)
|
||||
{
|
||||
foreach (var issue in snapshot.Issues)
|
||||
{
|
||||
_logger.LogWarning("Policy issue {Code}: {Message}", issue.Code, issue.Message);
|
||||
}
|
||||
}
|
||||
|
||||
return snapshot;
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class VexPolicyEvaluator : IVexPolicyEvaluator
|
||||
{
|
||||
private readonly IVexPolicyProvider _provider;
|
||||
|
||||
public VexPolicyEvaluator(IVexPolicyProvider provider)
|
||||
{
|
||||
_provider = provider ?? throw new ArgumentNullException(nameof(provider));
|
||||
}
|
||||
|
||||
public string Version => Snapshot.Version;
|
||||
|
||||
public VexPolicySnapshot Snapshot => _provider.GetSnapshot();
|
||||
|
||||
public double GetProviderWeight(VexProvider provider)
|
||||
=> Snapshot.ConsensusPolicy.GetProviderWeight(provider);
|
||||
|
||||
public bool IsClaimEligible(VexClaim claim, VexProvider provider, out string? rejectionReason)
|
||||
=> Snapshot.ConsensusPolicy.IsClaimEligible(claim, provider, out rejectionReason);
|
||||
}
|
||||
|
||||
public static class VexPolicyServiceCollectionExtensions
|
||||
{
|
||||
public static IServiceCollection AddVexPolicy(this IServiceCollection services)
|
||||
{
|
||||
services.AddSingleton<IVexPolicyProvider, VexPolicyProvider>();
|
||||
services.AddSingleton<IVexPolicyEvaluator, VexPolicyEvaluator>();
|
||||
return services;
|
||||
}
|
||||
}
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Globalization;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Excititor.Core;
|
||||
|
||||
namespace StellaOps.Excititor.Policy;
|
||||
|
||||
public interface IVexPolicyProvider
|
||||
{
|
||||
VexPolicySnapshot GetSnapshot();
|
||||
}
|
||||
|
||||
public interface IVexPolicyEvaluator
|
||||
{
|
||||
string Version { get; }
|
||||
|
||||
VexPolicySnapshot Snapshot { get; }
|
||||
|
||||
double GetProviderWeight(VexProvider provider);
|
||||
|
||||
bool IsClaimEligible(VexClaim claim, VexProvider provider, out string? rejectionReason);
|
||||
}
|
||||
|
||||
public sealed record VexPolicySnapshot(
|
||||
string Version,
|
||||
VexConsensusPolicyOptions ConsensusOptions,
|
||||
IVexConsensusPolicy ConsensusPolicy,
|
||||
ImmutableArray<VexPolicyIssue> Issues,
|
||||
string RevisionId,
|
||||
string Digest)
|
||||
{
|
||||
public static readonly VexPolicySnapshot Default = new(
|
||||
VexConsensusPolicyOptions.BaselineVersion,
|
||||
new VexConsensusPolicyOptions(),
|
||||
new BaselineVexConsensusPolicy(),
|
||||
ImmutableArray<VexPolicyIssue>.Empty,
|
||||
"rev-0",
|
||||
string.Empty);
|
||||
}
|
||||
|
||||
public sealed record VexPolicyIssue(
|
||||
string Code,
|
||||
string Message,
|
||||
VexPolicyIssueSeverity Severity);
|
||||
|
||||
public enum VexPolicyIssueSeverity
|
||||
{
|
||||
Warning,
|
||||
Error,
|
||||
}
|
||||
|
||||
public sealed class VexPolicyProvider : IVexPolicyProvider
|
||||
{
|
||||
private readonly IOptionsMonitor<VexPolicyOptions> _options;
|
||||
private readonly ILogger<VexPolicyProvider> _logger;
|
||||
private readonly object _sync = new();
|
||||
private long _revisionCounter;
|
||||
private string? _currentRevisionId;
|
||||
private string? _currentDigest;
|
||||
|
||||
public VexPolicyProvider(
|
||||
IOptionsMonitor<VexPolicyOptions> options,
|
||||
ILogger<VexPolicyProvider> logger)
|
||||
{
|
||||
_options = options ?? throw new ArgumentNullException(nameof(options));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
public VexPolicySnapshot GetSnapshot()
|
||||
{
|
||||
var options = _options.CurrentValue ?? new VexPolicyOptions();
|
||||
return BuildSnapshot(options);
|
||||
}
|
||||
|
||||
private VexPolicySnapshot BuildSnapshot(VexPolicyOptions options)
|
||||
{
|
||||
var normalization = VexPolicyProcessing.Normalize(options);
|
||||
var digest = VexPolicyDigest.Compute(normalization.ConsensusOptions);
|
||||
string revisionId;
|
||||
bool isNewRevision;
|
||||
|
||||
lock (_sync)
|
||||
{
|
||||
if (!string.Equals(_currentDigest, digest, StringComparison.Ordinal))
|
||||
{
|
||||
_revisionCounter++;
|
||||
revisionId = $"rev-{_revisionCounter}";
|
||||
_currentDigest = digest;
|
||||
_currentRevisionId = revisionId;
|
||||
isNewRevision = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
revisionId = _currentRevisionId ?? "rev-0";
|
||||
isNewRevision = false;
|
||||
}
|
||||
}
|
||||
|
||||
var policy = new BaselineVexConsensusPolicy(normalization.ConsensusOptions);
|
||||
var snapshot = new VexPolicySnapshot(
|
||||
normalization.ConsensusOptions.Version,
|
||||
normalization.ConsensusOptions,
|
||||
policy,
|
||||
normalization.Issues,
|
||||
revisionId,
|
||||
digest);
|
||||
|
||||
if (isNewRevision)
|
||||
{
|
||||
_logger.LogInformation(
|
||||
"Policy snapshot updated: revision {RevisionId}, version {Version}, digest {Digest}, issues {IssueCount}",
|
||||
snapshot.RevisionId,
|
||||
snapshot.Version,
|
||||
snapshot.Digest,
|
||||
snapshot.Issues.Length);
|
||||
VexPolicyTelemetry.RecordReload(snapshot.RevisionId, snapshot.Version, snapshot.Issues.Length);
|
||||
}
|
||||
else if (snapshot.Issues.Length > 0)
|
||||
{
|
||||
foreach (var issue in snapshot.Issues)
|
||||
{
|
||||
_logger.LogWarning("Policy issue {Code}: {Message}", issue.Code, issue.Message);
|
||||
}
|
||||
}
|
||||
|
||||
return snapshot;
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class VexPolicyEvaluator : IVexPolicyEvaluator
|
||||
{
|
||||
private readonly IVexPolicyProvider _provider;
|
||||
|
||||
public VexPolicyEvaluator(IVexPolicyProvider provider)
|
||||
{
|
||||
_provider = provider ?? throw new ArgumentNullException(nameof(provider));
|
||||
}
|
||||
|
||||
public string Version => Snapshot.Version;
|
||||
|
||||
public VexPolicySnapshot Snapshot => _provider.GetSnapshot();
|
||||
|
||||
public double GetProviderWeight(VexProvider provider)
|
||||
=> Snapshot.ConsensusPolicy.GetProviderWeight(provider);
|
||||
|
||||
public bool IsClaimEligible(VexClaim claim, VexProvider provider, out string? rejectionReason)
|
||||
=> Snapshot.ConsensusPolicy.IsClaimEligible(claim, provider, out rejectionReason);
|
||||
}
|
||||
|
||||
public static class VexPolicyServiceCollectionExtensions
|
||||
{
|
||||
public static IServiceCollection AddVexPolicy(this IServiceCollection services)
|
||||
{
|
||||
services.AddSingleton<IVexPolicyProvider, VexPolicyProvider>();
|
||||
services.AddSingleton<IVexPolicyEvaluator, VexPolicyEvaluator>();
|
||||
return services;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,94 +1,94 @@
|
||||
using System.Collections.Immutable;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using StellaOps.Excititor.Core;
|
||||
using YamlDotNet.Serialization;
|
||||
using YamlDotNet.Serialization.NamingConventions;
|
||||
|
||||
namespace StellaOps.Excititor.Policy;
|
||||
|
||||
public enum VexPolicyDocumentFormat
|
||||
{
|
||||
Json,
|
||||
Yaml,
|
||||
}
|
||||
|
||||
public sealed record VexPolicyBindingResult(
|
||||
bool Success,
|
||||
VexPolicyOptions? Options,
|
||||
VexConsensusPolicyOptions? NormalizedOptions,
|
||||
ImmutableArray<VexPolicyIssue> Issues);
|
||||
|
||||
public static class VexPolicyBinder
|
||||
{
|
||||
public static VexPolicyBindingResult Bind(string content, VexPolicyDocumentFormat format)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(content))
|
||||
{
|
||||
return Failure("policy.empty", "Policy document is empty.");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var options = Parse(content, format);
|
||||
return Normalize(options);
|
||||
}
|
||||
catch (JsonException ex)
|
||||
{
|
||||
return Failure("policy.parse.json", $"Failed to parse JSON policy document: {ex.Message}");
|
||||
}
|
||||
catch (YamlDotNet.Core.YamlException ex)
|
||||
{
|
||||
return Failure("policy.parse.yaml", $"Failed to parse YAML policy document: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
public static VexPolicyBindingResult Bind(Stream stream, VexPolicyDocumentFormat format, Encoding? encoding = null)
|
||||
{
|
||||
if (stream is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(stream));
|
||||
}
|
||||
|
||||
encoding ??= Encoding.UTF8;
|
||||
using var reader = new StreamReader(stream, encoding, detectEncodingFromByteOrderMarks: true, leaveOpen: true);
|
||||
var content = reader.ReadToEnd();
|
||||
return Bind(content, format);
|
||||
}
|
||||
|
||||
private static VexPolicyBindingResult Normalize(VexPolicyOptions options)
|
||||
{
|
||||
var normalization = VexPolicyProcessing.Normalize(options);
|
||||
var hasErrors = normalization.Issues.Any(static issue => issue.Severity == VexPolicyIssueSeverity.Error);
|
||||
return new VexPolicyBindingResult(!hasErrors, options, normalization.ConsensusOptions, normalization.Issues);
|
||||
}
|
||||
|
||||
private static VexPolicyBindingResult Failure(string code, string message)
|
||||
{
|
||||
var issue = new VexPolicyIssue(code, message, VexPolicyIssueSeverity.Error);
|
||||
return new VexPolicyBindingResult(false, null, null, ImmutableArray.Create(issue));
|
||||
}
|
||||
|
||||
private static VexPolicyOptions Parse(string content, VexPolicyDocumentFormat format)
|
||||
{
|
||||
return format switch
|
||||
{
|
||||
VexPolicyDocumentFormat.Json => JsonSerializer.Deserialize<VexPolicyOptions>(content, new JsonSerializerOptions
|
||||
{
|
||||
PropertyNameCaseInsensitive = true,
|
||||
ReadCommentHandling = JsonCommentHandling.Skip,
|
||||
AllowTrailingCommas = true,
|
||||
}) ?? new VexPolicyOptions(),
|
||||
VexPolicyDocumentFormat.Yaml => BuildYamlDeserializer().Deserialize<VexPolicyOptions>(content) ?? new VexPolicyOptions(),
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(format), format, "Unsupported policy document format."),
|
||||
};
|
||||
}
|
||||
|
||||
private static IDeserializer BuildYamlDeserializer()
|
||||
=> new DeserializerBuilder()
|
||||
.WithNamingConvention(CamelCaseNamingConvention.Instance)
|
||||
.IgnoreUnmatchedProperties()
|
||||
.Build();
|
||||
}
|
||||
using System.Collections.Immutable;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using StellaOps.Excititor.Core;
|
||||
using YamlDotNet.Serialization;
|
||||
using YamlDotNet.Serialization.NamingConventions;
|
||||
|
||||
namespace StellaOps.Excititor.Policy;
|
||||
|
||||
public enum VexPolicyDocumentFormat
|
||||
{
|
||||
Json,
|
||||
Yaml,
|
||||
}
|
||||
|
||||
public sealed record VexPolicyBindingResult(
|
||||
bool Success,
|
||||
VexPolicyOptions? Options,
|
||||
VexConsensusPolicyOptions? NormalizedOptions,
|
||||
ImmutableArray<VexPolicyIssue> Issues);
|
||||
|
||||
public static class VexPolicyBinder
|
||||
{
|
||||
public static VexPolicyBindingResult Bind(string content, VexPolicyDocumentFormat format)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(content))
|
||||
{
|
||||
return Failure("policy.empty", "Policy document is empty.");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var options = Parse(content, format);
|
||||
return Normalize(options);
|
||||
}
|
||||
catch (JsonException ex)
|
||||
{
|
||||
return Failure("policy.parse.json", $"Failed to parse JSON policy document: {ex.Message}");
|
||||
}
|
||||
catch (YamlDotNet.Core.YamlException ex)
|
||||
{
|
||||
return Failure("policy.parse.yaml", $"Failed to parse YAML policy document: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
public static VexPolicyBindingResult Bind(Stream stream, VexPolicyDocumentFormat format, Encoding? encoding = null)
|
||||
{
|
||||
if (stream is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(stream));
|
||||
}
|
||||
|
||||
encoding ??= Encoding.UTF8;
|
||||
using var reader = new StreamReader(stream, encoding, detectEncodingFromByteOrderMarks: true, leaveOpen: true);
|
||||
var content = reader.ReadToEnd();
|
||||
return Bind(content, format);
|
||||
}
|
||||
|
||||
private static VexPolicyBindingResult Normalize(VexPolicyOptions options)
|
||||
{
|
||||
var normalization = VexPolicyProcessing.Normalize(options);
|
||||
var hasErrors = normalization.Issues.Any(static issue => issue.Severity == VexPolicyIssueSeverity.Error);
|
||||
return new VexPolicyBindingResult(!hasErrors, options, normalization.ConsensusOptions, normalization.Issues);
|
||||
}
|
||||
|
||||
private static VexPolicyBindingResult Failure(string code, string message)
|
||||
{
|
||||
var issue = new VexPolicyIssue(code, message, VexPolicyIssueSeverity.Error);
|
||||
return new VexPolicyBindingResult(false, null, null, ImmutableArray.Create(issue));
|
||||
}
|
||||
|
||||
private static VexPolicyOptions Parse(string content, VexPolicyDocumentFormat format)
|
||||
{
|
||||
return format switch
|
||||
{
|
||||
VexPolicyDocumentFormat.Json => JsonSerializer.Deserialize<VexPolicyOptions>(content, new JsonSerializerOptions
|
||||
{
|
||||
PropertyNameCaseInsensitive = true,
|
||||
ReadCommentHandling = JsonCommentHandling.Skip,
|
||||
AllowTrailingCommas = true,
|
||||
}) ?? new VexPolicyOptions(),
|
||||
VexPolicyDocumentFormat.Yaml => BuildYamlDeserializer().Deserialize<VexPolicyOptions>(content) ?? new VexPolicyOptions(),
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(format), format, "Unsupported policy document format."),
|
||||
};
|
||||
}
|
||||
|
||||
private static IDeserializer BuildYamlDeserializer()
|
||||
=> new DeserializerBuilder()
|
||||
.WithNamingConvention(CamelCaseNamingConvention.Instance)
|
||||
.IgnoreUnmatchedProperties()
|
||||
.Build();
|
||||
}
|
||||
|
||||
@@ -1,87 +1,87 @@
|
||||
using System;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
|
||||
namespace StellaOps.Excititor.Policy;
|
||||
|
||||
public interface IVexPolicyDiagnostics
|
||||
{
|
||||
VexPolicyDiagnosticsReport GetDiagnostics();
|
||||
}
|
||||
|
||||
public sealed record VexPolicyDiagnosticsReport(
|
||||
string Version,
|
||||
string RevisionId,
|
||||
string Digest,
|
||||
int ErrorCount,
|
||||
int WarningCount,
|
||||
DateTimeOffset GeneratedAt,
|
||||
ImmutableArray<VexPolicyIssue> Issues,
|
||||
ImmutableArray<string> Recommendations,
|
||||
ImmutableDictionary<string, double> ActiveOverrides);
|
||||
|
||||
public sealed class VexPolicyDiagnostics : IVexPolicyDiagnostics
|
||||
{
|
||||
private readonly IVexPolicyProvider _policyProvider;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
|
||||
public VexPolicyDiagnostics(
|
||||
IVexPolicyProvider policyProvider,
|
||||
TimeProvider? timeProvider = null)
|
||||
{
|
||||
_policyProvider = policyProvider ?? throw new ArgumentNullException(nameof(policyProvider));
|
||||
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||
}
|
||||
|
||||
public VexPolicyDiagnosticsReport GetDiagnostics()
|
||||
{
|
||||
var snapshot = _policyProvider.GetSnapshot();
|
||||
var issues = snapshot.Issues;
|
||||
|
||||
var errorCount = issues.Count(static issue => issue.Severity == VexPolicyIssueSeverity.Error);
|
||||
var warningCount = issues.Count(static issue => issue.Severity == VexPolicyIssueSeverity.Warning);
|
||||
var overrides = snapshot.ConsensusOptions.ProviderOverrides
|
||||
.OrderBy(static pair => pair.Key, StringComparer.Ordinal)
|
||||
.ToImmutableDictionary();
|
||||
|
||||
var recommendations = BuildRecommendations(errorCount, warningCount, overrides);
|
||||
|
||||
return new VexPolicyDiagnosticsReport(
|
||||
snapshot.Version,
|
||||
snapshot.RevisionId,
|
||||
snapshot.Digest,
|
||||
errorCount,
|
||||
warningCount,
|
||||
_timeProvider.GetUtcNow(),
|
||||
issues,
|
||||
recommendations,
|
||||
overrides);
|
||||
}
|
||||
|
||||
private static ImmutableArray<string> BuildRecommendations(
|
||||
int errorCount,
|
||||
int warningCount,
|
||||
ImmutableDictionary<string, double> overrides)
|
||||
{
|
||||
var messages = ImmutableArray.CreateBuilder<string>();
|
||||
|
||||
if (errorCount > 0)
|
||||
{
|
||||
messages.Add("Resolve policy errors before running consensus; defaults are used while errors persist.");
|
||||
}
|
||||
|
||||
if (warningCount > 0)
|
||||
{
|
||||
messages.Add("Review policy warnings via CLI/Web diagnostics and adjust configuration as needed.");
|
||||
}
|
||||
|
||||
if (overrides.Count > 0)
|
||||
{
|
||||
messages.Add($"Provider overrides active for: {string.Join(", ", overrides.Keys)}.");
|
||||
}
|
||||
|
||||
messages.Add("Refer to docs/modules/excititor/architecture.md for policy upgrade and diagnostics guidance.");
|
||||
|
||||
return messages.ToImmutable();
|
||||
}
|
||||
}
|
||||
using System;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
|
||||
namespace StellaOps.Excititor.Policy;
|
||||
|
||||
public interface IVexPolicyDiagnostics
|
||||
{
|
||||
VexPolicyDiagnosticsReport GetDiagnostics();
|
||||
}
|
||||
|
||||
public sealed record VexPolicyDiagnosticsReport(
|
||||
string Version,
|
||||
string RevisionId,
|
||||
string Digest,
|
||||
int ErrorCount,
|
||||
int WarningCount,
|
||||
DateTimeOffset GeneratedAt,
|
||||
ImmutableArray<VexPolicyIssue> Issues,
|
||||
ImmutableArray<string> Recommendations,
|
||||
ImmutableDictionary<string, double> ActiveOverrides);
|
||||
|
||||
public sealed class VexPolicyDiagnostics : IVexPolicyDiagnostics
|
||||
{
|
||||
private readonly IVexPolicyProvider _policyProvider;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
|
||||
public VexPolicyDiagnostics(
|
||||
IVexPolicyProvider policyProvider,
|
||||
TimeProvider? timeProvider = null)
|
||||
{
|
||||
_policyProvider = policyProvider ?? throw new ArgumentNullException(nameof(policyProvider));
|
||||
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||
}
|
||||
|
||||
public VexPolicyDiagnosticsReport GetDiagnostics()
|
||||
{
|
||||
var snapshot = _policyProvider.GetSnapshot();
|
||||
var issues = snapshot.Issues;
|
||||
|
||||
var errorCount = issues.Count(static issue => issue.Severity == VexPolicyIssueSeverity.Error);
|
||||
var warningCount = issues.Count(static issue => issue.Severity == VexPolicyIssueSeverity.Warning);
|
||||
var overrides = snapshot.ConsensusOptions.ProviderOverrides
|
||||
.OrderBy(static pair => pair.Key, StringComparer.Ordinal)
|
||||
.ToImmutableDictionary();
|
||||
|
||||
var recommendations = BuildRecommendations(errorCount, warningCount, overrides);
|
||||
|
||||
return new VexPolicyDiagnosticsReport(
|
||||
snapshot.Version,
|
||||
snapshot.RevisionId,
|
||||
snapshot.Digest,
|
||||
errorCount,
|
||||
warningCount,
|
||||
_timeProvider.GetUtcNow(),
|
||||
issues,
|
||||
recommendations,
|
||||
overrides);
|
||||
}
|
||||
|
||||
private static ImmutableArray<string> BuildRecommendations(
|
||||
int errorCount,
|
||||
int warningCount,
|
||||
ImmutableDictionary<string, double> overrides)
|
||||
{
|
||||
var messages = ImmutableArray.CreateBuilder<string>();
|
||||
|
||||
if (errorCount > 0)
|
||||
{
|
||||
messages.Add("Resolve policy errors before running consensus; defaults are used while errors persist.");
|
||||
}
|
||||
|
||||
if (warningCount > 0)
|
||||
{
|
||||
messages.Add("Review policy warnings via CLI/Web diagnostics and adjust configuration as needed.");
|
||||
}
|
||||
|
||||
if (overrides.Count > 0)
|
||||
{
|
||||
messages.Add($"Provider overrides active for: {string.Join(", ", overrides.Keys)}.");
|
||||
}
|
||||
|
||||
messages.Add("Refer to docs/modules/excititor/architecture.md for policy upgrade and diagnostics guidance.");
|
||||
|
||||
return messages.ToImmutable();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,38 +1,38 @@
|
||||
using System.Globalization;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using StellaOps.Excititor.Core;
|
||||
|
||||
namespace StellaOps.Excititor.Policy;
|
||||
|
||||
internal static class VexPolicyDigest
|
||||
{
|
||||
public static string Compute(VexConsensusPolicyOptions options)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(options);
|
||||
|
||||
var builder = new StringBuilder();
|
||||
builder.Append(options.Version).Append('|')
|
||||
.Append(options.VendorWeight.ToString("F6", CultureInfo.InvariantCulture)).Append('|')
|
||||
.Append(options.DistroWeight.ToString("F6", CultureInfo.InvariantCulture)).Append('|')
|
||||
.Append(options.PlatformWeight.ToString("F6", CultureInfo.InvariantCulture)).Append('|')
|
||||
.Append(options.HubWeight.ToString("F6", CultureInfo.InvariantCulture)).Append('|')
|
||||
.Append(options.AttestationWeight.ToString("F6", CultureInfo.InvariantCulture)).Append('|')
|
||||
.Append(options.WeightCeiling.ToString("F6", CultureInfo.InvariantCulture)).Append('|')
|
||||
.Append(options.Alpha.ToString("F6", CultureInfo.InvariantCulture)).Append('|')
|
||||
.Append(options.Beta.ToString("F6", CultureInfo.InvariantCulture));
|
||||
|
||||
foreach (var kvp in options.ProviderOverrides
|
||||
.OrderBy(static pair => pair.Key, StringComparer.Ordinal))
|
||||
{
|
||||
builder.Append('|')
|
||||
.Append(kvp.Key)
|
||||
.Append('=')
|
||||
.Append(kvp.Value.ToString("F6", CultureInfo.InvariantCulture));
|
||||
}
|
||||
|
||||
var input = builder.ToString();
|
||||
var hash = SHA256.HashData(Encoding.UTF8.GetBytes(input));
|
||||
return Convert.ToHexString(hash);
|
||||
}
|
||||
}
|
||||
using System.Globalization;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using StellaOps.Excititor.Core;
|
||||
|
||||
namespace StellaOps.Excititor.Policy;
|
||||
|
||||
internal static class VexPolicyDigest
|
||||
{
|
||||
public static string Compute(VexConsensusPolicyOptions options)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(options);
|
||||
|
||||
var builder = new StringBuilder();
|
||||
builder.Append(options.Version).Append('|')
|
||||
.Append(options.VendorWeight.ToString("F6", CultureInfo.InvariantCulture)).Append('|')
|
||||
.Append(options.DistroWeight.ToString("F6", CultureInfo.InvariantCulture)).Append('|')
|
||||
.Append(options.PlatformWeight.ToString("F6", CultureInfo.InvariantCulture)).Append('|')
|
||||
.Append(options.HubWeight.ToString("F6", CultureInfo.InvariantCulture)).Append('|')
|
||||
.Append(options.AttestationWeight.ToString("F6", CultureInfo.InvariantCulture)).Append('|')
|
||||
.Append(options.WeightCeiling.ToString("F6", CultureInfo.InvariantCulture)).Append('|')
|
||||
.Append(options.Alpha.ToString("F6", CultureInfo.InvariantCulture)).Append('|')
|
||||
.Append(options.Beta.ToString("F6", CultureInfo.InvariantCulture));
|
||||
|
||||
foreach (var kvp in options.ProviderOverrides
|
||||
.OrderBy(static pair => pair.Key, StringComparer.Ordinal))
|
||||
{
|
||||
builder.Append('|')
|
||||
.Append(kvp.Key)
|
||||
.Append('=')
|
||||
.Append(kvp.Value.ToString("F6", CultureInfo.InvariantCulture));
|
||||
}
|
||||
|
||||
var input = builder.ToString();
|
||||
var hash = SHA256.HashData(Encoding.UTF8.GetBytes(input));
|
||||
return Convert.ToHexString(hash);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,36 +1,36 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace StellaOps.Excititor.Policy;
|
||||
|
||||
public sealed class VexPolicyOptions
|
||||
{
|
||||
public string? Version { get; set; }
|
||||
|
||||
public VexPolicyWeightOptions Weights { get; set; } = new();
|
||||
|
||||
public VexPolicyScoringOptions Scoring { get; set; } = new();
|
||||
|
||||
public IDictionary<string, double>? ProviderOverrides { get; set; }
|
||||
}
|
||||
|
||||
public sealed class VexPolicyWeightOptions
|
||||
{
|
||||
public double? Vendor { get; set; }
|
||||
|
||||
public double? Distro { get; set; }
|
||||
|
||||
public double? Platform { get; set; }
|
||||
|
||||
public double? Hub { get; set; }
|
||||
|
||||
public double? Attestation { get; set; }
|
||||
|
||||
public double? Ceiling { get; set; }
|
||||
}
|
||||
|
||||
public sealed class VexPolicyScoringOptions
|
||||
{
|
||||
public double? Alpha { get; set; }
|
||||
|
||||
public double? Beta { get; set; }
|
||||
}
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace StellaOps.Excititor.Policy;
|
||||
|
||||
public sealed class VexPolicyOptions
|
||||
{
|
||||
public string? Version { get; set; }
|
||||
|
||||
public VexPolicyWeightOptions Weights { get; set; } = new();
|
||||
|
||||
public VexPolicyScoringOptions Scoring { get; set; } = new();
|
||||
|
||||
public IDictionary<string, double>? ProviderOverrides { get; set; }
|
||||
}
|
||||
|
||||
public sealed class VexPolicyWeightOptions
|
||||
{
|
||||
public double? Vendor { get; set; }
|
||||
|
||||
public double? Distro { get; set; }
|
||||
|
||||
public double? Platform { get; set; }
|
||||
|
||||
public double? Hub { get; set; }
|
||||
|
||||
public double? Attestation { get; set; }
|
||||
|
||||
public double? Ceiling { get; set; }
|
||||
}
|
||||
|
||||
public sealed class VexPolicyScoringOptions
|
||||
{
|
||||
public double? Alpha { get; set; }
|
||||
|
||||
public double? Beta { get; set; }
|
||||
}
|
||||
|
||||
@@ -1,282 +1,282 @@
|
||||
using System.Collections.Immutable;
|
||||
using System.Globalization;
|
||||
using StellaOps.Excititor.Core;
|
||||
|
||||
namespace StellaOps.Excititor.Policy;
|
||||
|
||||
internal static class VexPolicyProcessing
|
||||
{
|
||||
private const double DefaultVendorWeight = 1.0;
|
||||
private const double DefaultDistroWeight = 0.9;
|
||||
private const double DefaultPlatformWeight = 0.7;
|
||||
private const double DefaultHubWeight = 0.5;
|
||||
private const double DefaultAttestationWeight = 0.6;
|
||||
|
||||
public static VexPolicyNormalizationResult Normalize(VexPolicyOptions? options)
|
||||
{
|
||||
var issues = ImmutableArray.CreateBuilder<VexPolicyIssue>();
|
||||
|
||||
var policyOptions = options ?? new VexPolicyOptions();
|
||||
|
||||
var normalizedWeights = NormalizeWeights(policyOptions.Weights, issues);
|
||||
var overrides = NormalizeOverrides(policyOptions.ProviderOverrides, normalizedWeights.Ceiling, issues);
|
||||
var scoring = NormalizeScoring(policyOptions.Scoring, issues);
|
||||
|
||||
var consensusOptions = new VexConsensusPolicyOptions(
|
||||
policyOptions.Version ?? VexConsensusPolicyOptions.BaselineVersion,
|
||||
normalizedWeights.Vendor,
|
||||
normalizedWeights.Distro,
|
||||
normalizedWeights.Platform,
|
||||
normalizedWeights.Hub,
|
||||
normalizedWeights.Attestation,
|
||||
overrides,
|
||||
normalizedWeights.Ceiling,
|
||||
scoring.Alpha,
|
||||
scoring.Beta);
|
||||
|
||||
var orderedIssues = issues.ToImmutable().Sort(IssueComparer);
|
||||
|
||||
return new VexPolicyNormalizationResult(consensusOptions, orderedIssues);
|
||||
}
|
||||
|
||||
public static ImmutableArray<VexPolicyIssue> SortIssues(IEnumerable<VexPolicyIssue> issues)
|
||||
=> issues.ToImmutableArray().Sort(IssueComparer);
|
||||
|
||||
private static WeightNormalizationResult NormalizeWeights(
|
||||
VexPolicyWeightOptions? options,
|
||||
ImmutableArray<VexPolicyIssue>.Builder issues)
|
||||
{
|
||||
var ceiling = NormalizeWeightCeiling(options?.Ceiling, issues);
|
||||
|
||||
var vendor = NormalizeWeightValue(
|
||||
options?.Vendor,
|
||||
"vendor",
|
||||
DefaultVendorWeight,
|
||||
ceiling,
|
||||
issues);
|
||||
var distro = NormalizeWeightValue(
|
||||
options?.Distro,
|
||||
"distro",
|
||||
DefaultDistroWeight,
|
||||
ceiling,
|
||||
issues);
|
||||
var platform = NormalizeWeightValue(
|
||||
options?.Platform,
|
||||
"platform",
|
||||
DefaultPlatformWeight,
|
||||
ceiling,
|
||||
issues);
|
||||
var hub = NormalizeWeightValue(
|
||||
options?.Hub,
|
||||
"hub",
|
||||
DefaultHubWeight,
|
||||
ceiling,
|
||||
issues);
|
||||
var attestation = NormalizeWeightValue(
|
||||
options?.Attestation,
|
||||
"attestation",
|
||||
DefaultAttestationWeight,
|
||||
ceiling,
|
||||
issues);
|
||||
|
||||
return new WeightNormalizationResult(vendor, distro, platform, hub, attestation, ceiling);
|
||||
}
|
||||
|
||||
private static double NormalizeWeightValue(
|
||||
double? value,
|
||||
string fieldName,
|
||||
double defaultValue,
|
||||
double ceiling,
|
||||
ImmutableArray<VexPolicyIssue>.Builder issues)
|
||||
{
|
||||
if (value is null)
|
||||
{
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
if (double.IsNaN(value.Value) || double.IsInfinity(value.Value))
|
||||
{
|
||||
issues.Add(new VexPolicyIssue(
|
||||
$"weights.{fieldName}.invalid",
|
||||
$"{fieldName} must be a finite number; default {defaultValue.ToString(CultureInfo.InvariantCulture)} applied.",
|
||||
VexPolicyIssueSeverity.Warning));
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
if (value.Value < 0 || value.Value > ceiling)
|
||||
{
|
||||
issues.Add(new VexPolicyIssue(
|
||||
$"weights.{fieldName}.range",
|
||||
$"{fieldName} must be between 0 and {ceiling.ToString(CultureInfo.InvariantCulture)}; value {value.Value.ToString(CultureInfo.InvariantCulture)} was clamped.",
|
||||
VexPolicyIssueSeverity.Warning));
|
||||
return Math.Clamp(value.Value, 0, ceiling);
|
||||
}
|
||||
|
||||
return value.Value;
|
||||
}
|
||||
|
||||
private static double NormalizeWeightCeiling(double? ceiling, ImmutableArray<VexPolicyIssue>.Builder issues)
|
||||
{
|
||||
if (ceiling is null)
|
||||
{
|
||||
return VexConsensusPolicyOptions.DefaultWeightCeiling;
|
||||
}
|
||||
|
||||
if (double.IsNaN(ceiling.Value) || double.IsInfinity(ceiling.Value) || ceiling.Value <= 0)
|
||||
{
|
||||
issues.Add(new VexPolicyIssue(
|
||||
"weights.ceiling.invalid",
|
||||
"weights.ceiling must be a positive, finite number; default ceiling applied.",
|
||||
VexPolicyIssueSeverity.Warning));
|
||||
return VexConsensusPolicyOptions.DefaultWeightCeiling;
|
||||
}
|
||||
|
||||
if (ceiling.Value < 1)
|
||||
{
|
||||
issues.Add(new VexPolicyIssue(
|
||||
"weights.ceiling.minimum",
|
||||
"weights.ceiling below 1 falls back to 1 to preserve baseline behaviour.",
|
||||
VexPolicyIssueSeverity.Warning));
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (ceiling.Value > VexConsensusPolicyOptions.MaxSupportedCeiling)
|
||||
{
|
||||
issues.Add(new VexPolicyIssue(
|
||||
"weights.ceiling.maximum",
|
||||
$"weights.ceiling exceeded supported range; value {ceiling.Value.ToString(CultureInfo.InvariantCulture)} was clamped to {VexConsensusPolicyOptions.MaxSupportedCeiling.ToString(CultureInfo.InvariantCulture)}.",
|
||||
VexPolicyIssueSeverity.Warning));
|
||||
return VexConsensusPolicyOptions.MaxSupportedCeiling;
|
||||
}
|
||||
|
||||
return ceiling.Value;
|
||||
}
|
||||
|
||||
private static ImmutableDictionary<string, double> NormalizeOverrides(
|
||||
IDictionary<string, double>? overrides,
|
||||
double ceiling,
|
||||
ImmutableArray<VexPolicyIssue>.Builder issues)
|
||||
{
|
||||
if (overrides is null || overrides.Count == 0)
|
||||
{
|
||||
return ImmutableDictionary<string, double>.Empty;
|
||||
}
|
||||
|
||||
var builder = ImmutableDictionary.CreateBuilder<string, double>(StringComparer.Ordinal);
|
||||
foreach (var kvp in overrides)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(kvp.Key))
|
||||
{
|
||||
issues.Add(new VexPolicyIssue(
|
||||
"overrides.key.missing",
|
||||
"Encountered provider override with empty key; ignoring entry.",
|
||||
VexPolicyIssueSeverity.Warning));
|
||||
continue;
|
||||
}
|
||||
|
||||
var key = kvp.Key.Trim();
|
||||
var weight = NormalizeWeightValue(
|
||||
kvp.Value,
|
||||
$"overrides.{key}",
|
||||
DefaultVendorWeight,
|
||||
ceiling,
|
||||
issues);
|
||||
builder[key] = weight;
|
||||
}
|
||||
|
||||
return builder.ToImmutable();
|
||||
}
|
||||
|
||||
private static ScoringNormalizationResult NormalizeScoring(
|
||||
VexPolicyScoringOptions? options,
|
||||
ImmutableArray<VexPolicyIssue>.Builder issues)
|
||||
{
|
||||
var alpha = NormalizeCoefficient(
|
||||
options?.Alpha,
|
||||
"alpha",
|
||||
VexConsensusPolicyOptions.DefaultAlpha,
|
||||
issues);
|
||||
var beta = NormalizeCoefficient(
|
||||
options?.Beta,
|
||||
"beta",
|
||||
VexConsensusPolicyOptions.DefaultBeta,
|
||||
issues);
|
||||
return new ScoringNormalizationResult(alpha, beta);
|
||||
}
|
||||
|
||||
private static double NormalizeCoefficient(
|
||||
double? value,
|
||||
string fieldName,
|
||||
double defaultValue,
|
||||
ImmutableArray<VexPolicyIssue>.Builder issues)
|
||||
{
|
||||
if (value is null)
|
||||
{
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
if (double.IsNaN(value.Value) || double.IsInfinity(value.Value))
|
||||
{
|
||||
issues.Add(new VexPolicyIssue(
|
||||
$"scoring.{fieldName}.invalid",
|
||||
$"{fieldName} coefficient must be a finite number; default {defaultValue.ToString(CultureInfo.InvariantCulture)} applied.",
|
||||
VexPolicyIssueSeverity.Warning));
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
if (value.Value < 0)
|
||||
{
|
||||
issues.Add(new VexPolicyIssue(
|
||||
$"scoring.{fieldName}.range",
|
||||
$"{fieldName} cannot be negative; default {defaultValue.ToString(CultureInfo.InvariantCulture)} applied.",
|
||||
VexPolicyIssueSeverity.Warning));
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
if (value.Value > VexConsensusPolicyOptions.MaxSupportedCoefficient)
|
||||
{
|
||||
issues.Add(new VexPolicyIssue(
|
||||
$"scoring.{fieldName}.maximum",
|
||||
$"{fieldName} exceeded supported range; value {value.Value.ToString(CultureInfo.InvariantCulture)} was clamped to {VexConsensusPolicyOptions.MaxSupportedCoefficient.ToString(CultureInfo.InvariantCulture)}.",
|
||||
VexPolicyIssueSeverity.Warning));
|
||||
return VexConsensusPolicyOptions.MaxSupportedCoefficient;
|
||||
}
|
||||
|
||||
return value.Value;
|
||||
}
|
||||
|
||||
private static int CompareIssues(VexPolicyIssue left, VexPolicyIssue right)
|
||||
{
|
||||
var severityCompare = GetSeverityRank(left.Severity).CompareTo(GetSeverityRank(right.Severity));
|
||||
if (severityCompare != 0)
|
||||
{
|
||||
return severityCompare;
|
||||
}
|
||||
|
||||
return string.Compare(left.Code, right.Code, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
private static int GetSeverityRank(VexPolicyIssueSeverity severity)
|
||||
=> severity switch
|
||||
{
|
||||
VexPolicyIssueSeverity.Error => 0,
|
||||
VexPolicyIssueSeverity.Warning => 1,
|
||||
_ => 2,
|
||||
};
|
||||
|
||||
private static readonly Comparer<VexPolicyIssue> IssueComparer = Comparer<VexPolicyIssue>.Create(CompareIssues);
|
||||
|
||||
internal sealed record VexPolicyNormalizationResult(
|
||||
VexConsensusPolicyOptions ConsensusOptions,
|
||||
ImmutableArray<VexPolicyIssue> Issues);
|
||||
|
||||
private sealed record WeightNormalizationResult(
|
||||
double Vendor,
|
||||
double Distro,
|
||||
double Platform,
|
||||
double Hub,
|
||||
double Attestation,
|
||||
double Ceiling);
|
||||
|
||||
private sealed record ScoringNormalizationResult(double Alpha, double Beta);
|
||||
}
|
||||
using System.Collections.Immutable;
|
||||
using System.Globalization;
|
||||
using StellaOps.Excititor.Core;
|
||||
|
||||
namespace StellaOps.Excititor.Policy;
|
||||
|
||||
internal static class VexPolicyProcessing
|
||||
{
|
||||
private const double DefaultVendorWeight = 1.0;
|
||||
private const double DefaultDistroWeight = 0.9;
|
||||
private const double DefaultPlatformWeight = 0.7;
|
||||
private const double DefaultHubWeight = 0.5;
|
||||
private const double DefaultAttestationWeight = 0.6;
|
||||
|
||||
public static VexPolicyNormalizationResult Normalize(VexPolicyOptions? options)
|
||||
{
|
||||
var issues = ImmutableArray.CreateBuilder<VexPolicyIssue>();
|
||||
|
||||
var policyOptions = options ?? new VexPolicyOptions();
|
||||
|
||||
var normalizedWeights = NormalizeWeights(policyOptions.Weights, issues);
|
||||
var overrides = NormalizeOverrides(policyOptions.ProviderOverrides, normalizedWeights.Ceiling, issues);
|
||||
var scoring = NormalizeScoring(policyOptions.Scoring, issues);
|
||||
|
||||
var consensusOptions = new VexConsensusPolicyOptions(
|
||||
policyOptions.Version ?? VexConsensusPolicyOptions.BaselineVersion,
|
||||
normalizedWeights.Vendor,
|
||||
normalizedWeights.Distro,
|
||||
normalizedWeights.Platform,
|
||||
normalizedWeights.Hub,
|
||||
normalizedWeights.Attestation,
|
||||
overrides,
|
||||
normalizedWeights.Ceiling,
|
||||
scoring.Alpha,
|
||||
scoring.Beta);
|
||||
|
||||
var orderedIssues = issues.ToImmutable().Sort(IssueComparer);
|
||||
|
||||
return new VexPolicyNormalizationResult(consensusOptions, orderedIssues);
|
||||
}
|
||||
|
||||
public static ImmutableArray<VexPolicyIssue> SortIssues(IEnumerable<VexPolicyIssue> issues)
|
||||
=> issues.ToImmutableArray().Sort(IssueComparer);
|
||||
|
||||
private static WeightNormalizationResult NormalizeWeights(
|
||||
VexPolicyWeightOptions? options,
|
||||
ImmutableArray<VexPolicyIssue>.Builder issues)
|
||||
{
|
||||
var ceiling = NormalizeWeightCeiling(options?.Ceiling, issues);
|
||||
|
||||
var vendor = NormalizeWeightValue(
|
||||
options?.Vendor,
|
||||
"vendor",
|
||||
DefaultVendorWeight,
|
||||
ceiling,
|
||||
issues);
|
||||
var distro = NormalizeWeightValue(
|
||||
options?.Distro,
|
||||
"distro",
|
||||
DefaultDistroWeight,
|
||||
ceiling,
|
||||
issues);
|
||||
var platform = NormalizeWeightValue(
|
||||
options?.Platform,
|
||||
"platform",
|
||||
DefaultPlatformWeight,
|
||||
ceiling,
|
||||
issues);
|
||||
var hub = NormalizeWeightValue(
|
||||
options?.Hub,
|
||||
"hub",
|
||||
DefaultHubWeight,
|
||||
ceiling,
|
||||
issues);
|
||||
var attestation = NormalizeWeightValue(
|
||||
options?.Attestation,
|
||||
"attestation",
|
||||
DefaultAttestationWeight,
|
||||
ceiling,
|
||||
issues);
|
||||
|
||||
return new WeightNormalizationResult(vendor, distro, platform, hub, attestation, ceiling);
|
||||
}
|
||||
|
||||
private static double NormalizeWeightValue(
|
||||
double? value,
|
||||
string fieldName,
|
||||
double defaultValue,
|
||||
double ceiling,
|
||||
ImmutableArray<VexPolicyIssue>.Builder issues)
|
||||
{
|
||||
if (value is null)
|
||||
{
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
if (double.IsNaN(value.Value) || double.IsInfinity(value.Value))
|
||||
{
|
||||
issues.Add(new VexPolicyIssue(
|
||||
$"weights.{fieldName}.invalid",
|
||||
$"{fieldName} must be a finite number; default {defaultValue.ToString(CultureInfo.InvariantCulture)} applied.",
|
||||
VexPolicyIssueSeverity.Warning));
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
if (value.Value < 0 || value.Value > ceiling)
|
||||
{
|
||||
issues.Add(new VexPolicyIssue(
|
||||
$"weights.{fieldName}.range",
|
||||
$"{fieldName} must be between 0 and {ceiling.ToString(CultureInfo.InvariantCulture)}; value {value.Value.ToString(CultureInfo.InvariantCulture)} was clamped.",
|
||||
VexPolicyIssueSeverity.Warning));
|
||||
return Math.Clamp(value.Value, 0, ceiling);
|
||||
}
|
||||
|
||||
return value.Value;
|
||||
}
|
||||
|
||||
private static double NormalizeWeightCeiling(double? ceiling, ImmutableArray<VexPolicyIssue>.Builder issues)
|
||||
{
|
||||
if (ceiling is null)
|
||||
{
|
||||
return VexConsensusPolicyOptions.DefaultWeightCeiling;
|
||||
}
|
||||
|
||||
if (double.IsNaN(ceiling.Value) || double.IsInfinity(ceiling.Value) || ceiling.Value <= 0)
|
||||
{
|
||||
issues.Add(new VexPolicyIssue(
|
||||
"weights.ceiling.invalid",
|
||||
"weights.ceiling must be a positive, finite number; default ceiling applied.",
|
||||
VexPolicyIssueSeverity.Warning));
|
||||
return VexConsensusPolicyOptions.DefaultWeightCeiling;
|
||||
}
|
||||
|
||||
if (ceiling.Value < 1)
|
||||
{
|
||||
issues.Add(new VexPolicyIssue(
|
||||
"weights.ceiling.minimum",
|
||||
"weights.ceiling below 1 falls back to 1 to preserve baseline behaviour.",
|
||||
VexPolicyIssueSeverity.Warning));
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (ceiling.Value > VexConsensusPolicyOptions.MaxSupportedCeiling)
|
||||
{
|
||||
issues.Add(new VexPolicyIssue(
|
||||
"weights.ceiling.maximum",
|
||||
$"weights.ceiling exceeded supported range; value {ceiling.Value.ToString(CultureInfo.InvariantCulture)} was clamped to {VexConsensusPolicyOptions.MaxSupportedCeiling.ToString(CultureInfo.InvariantCulture)}.",
|
||||
VexPolicyIssueSeverity.Warning));
|
||||
return VexConsensusPolicyOptions.MaxSupportedCeiling;
|
||||
}
|
||||
|
||||
return ceiling.Value;
|
||||
}
|
||||
|
||||
private static ImmutableDictionary<string, double> NormalizeOverrides(
|
||||
IDictionary<string, double>? overrides,
|
||||
double ceiling,
|
||||
ImmutableArray<VexPolicyIssue>.Builder issues)
|
||||
{
|
||||
if (overrides is null || overrides.Count == 0)
|
||||
{
|
||||
return ImmutableDictionary<string, double>.Empty;
|
||||
}
|
||||
|
||||
var builder = ImmutableDictionary.CreateBuilder<string, double>(StringComparer.Ordinal);
|
||||
foreach (var kvp in overrides)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(kvp.Key))
|
||||
{
|
||||
issues.Add(new VexPolicyIssue(
|
||||
"overrides.key.missing",
|
||||
"Encountered provider override with empty key; ignoring entry.",
|
||||
VexPolicyIssueSeverity.Warning));
|
||||
continue;
|
||||
}
|
||||
|
||||
var key = kvp.Key.Trim();
|
||||
var weight = NormalizeWeightValue(
|
||||
kvp.Value,
|
||||
$"overrides.{key}",
|
||||
DefaultVendorWeight,
|
||||
ceiling,
|
||||
issues);
|
||||
builder[key] = weight;
|
||||
}
|
||||
|
||||
return builder.ToImmutable();
|
||||
}
|
||||
|
||||
private static ScoringNormalizationResult NormalizeScoring(
|
||||
VexPolicyScoringOptions? options,
|
||||
ImmutableArray<VexPolicyIssue>.Builder issues)
|
||||
{
|
||||
var alpha = NormalizeCoefficient(
|
||||
options?.Alpha,
|
||||
"alpha",
|
||||
VexConsensusPolicyOptions.DefaultAlpha,
|
||||
issues);
|
||||
var beta = NormalizeCoefficient(
|
||||
options?.Beta,
|
||||
"beta",
|
||||
VexConsensusPolicyOptions.DefaultBeta,
|
||||
issues);
|
||||
return new ScoringNormalizationResult(alpha, beta);
|
||||
}
|
||||
|
||||
private static double NormalizeCoefficient(
|
||||
double? value,
|
||||
string fieldName,
|
||||
double defaultValue,
|
||||
ImmutableArray<VexPolicyIssue>.Builder issues)
|
||||
{
|
||||
if (value is null)
|
||||
{
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
if (double.IsNaN(value.Value) || double.IsInfinity(value.Value))
|
||||
{
|
||||
issues.Add(new VexPolicyIssue(
|
||||
$"scoring.{fieldName}.invalid",
|
||||
$"{fieldName} coefficient must be a finite number; default {defaultValue.ToString(CultureInfo.InvariantCulture)} applied.",
|
||||
VexPolicyIssueSeverity.Warning));
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
if (value.Value < 0)
|
||||
{
|
||||
issues.Add(new VexPolicyIssue(
|
||||
$"scoring.{fieldName}.range",
|
||||
$"{fieldName} cannot be negative; default {defaultValue.ToString(CultureInfo.InvariantCulture)} applied.",
|
||||
VexPolicyIssueSeverity.Warning));
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
if (value.Value > VexConsensusPolicyOptions.MaxSupportedCoefficient)
|
||||
{
|
||||
issues.Add(new VexPolicyIssue(
|
||||
$"scoring.{fieldName}.maximum",
|
||||
$"{fieldName} exceeded supported range; value {value.Value.ToString(CultureInfo.InvariantCulture)} was clamped to {VexConsensusPolicyOptions.MaxSupportedCoefficient.ToString(CultureInfo.InvariantCulture)}.",
|
||||
VexPolicyIssueSeverity.Warning));
|
||||
return VexConsensusPolicyOptions.MaxSupportedCoefficient;
|
||||
}
|
||||
|
||||
return value.Value;
|
||||
}
|
||||
|
||||
private static int CompareIssues(VexPolicyIssue left, VexPolicyIssue right)
|
||||
{
|
||||
var severityCompare = GetSeverityRank(left.Severity).CompareTo(GetSeverityRank(right.Severity));
|
||||
if (severityCompare != 0)
|
||||
{
|
||||
return severityCompare;
|
||||
}
|
||||
|
||||
return string.Compare(left.Code, right.Code, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
private static int GetSeverityRank(VexPolicyIssueSeverity severity)
|
||||
=> severity switch
|
||||
{
|
||||
VexPolicyIssueSeverity.Error => 0,
|
||||
VexPolicyIssueSeverity.Warning => 1,
|
||||
_ => 2,
|
||||
};
|
||||
|
||||
private static readonly Comparer<VexPolicyIssue> IssueComparer = Comparer<VexPolicyIssue>.Create(CompareIssues);
|
||||
|
||||
internal sealed record VexPolicyNormalizationResult(
|
||||
VexConsensusPolicyOptions ConsensusOptions,
|
||||
ImmutableArray<VexPolicyIssue> Issues);
|
||||
|
||||
private sealed record WeightNormalizationResult(
|
||||
double Vendor,
|
||||
double Distro,
|
||||
double Platform,
|
||||
double Hub,
|
||||
double Attestation,
|
||||
double Ceiling);
|
||||
|
||||
private sealed record ScoringNormalizationResult(double Alpha, double Beta);
|
||||
}
|
||||
|
||||
@@ -1,24 +1,24 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.Metrics;
|
||||
|
||||
namespace StellaOps.Excititor.Policy;
|
||||
|
||||
internal static class VexPolicyTelemetry
|
||||
{
|
||||
private const string MeterName = "StellaOps.Excititor.Policy";
|
||||
private const string MeterVersion = "1.0.0";
|
||||
|
||||
private static readonly Meter Meter = new(MeterName, MeterVersion);
|
||||
private static readonly Counter<long> PolicyReloads = Meter.CreateCounter<long>("vex.policy.reloads", unit: "events");
|
||||
|
||||
public static void RecordReload(string revisionId, string version, int issueCount)
|
||||
{
|
||||
var tags = new KeyValuePair<string, object?>[]
|
||||
{
|
||||
new("revision", revisionId),
|
||||
new("version", version),
|
||||
new("issues", issueCount),
|
||||
};
|
||||
PolicyReloads.Add(1, tags);
|
||||
}
|
||||
}
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.Metrics;
|
||||
|
||||
namespace StellaOps.Excititor.Policy;
|
||||
|
||||
internal static class VexPolicyTelemetry
|
||||
{
|
||||
private const string MeterName = "StellaOps.Excititor.Policy";
|
||||
private const string MeterVersion = "1.0.0";
|
||||
|
||||
private static readonly Meter Meter = new(MeterName, MeterVersion);
|
||||
private static readonly Counter<long> PolicyReloads = Meter.CreateCounter<long>("vex.policy.reloads", unit: "events");
|
||||
|
||||
public static void RecordReload(string revisionId, string version, int issueCount)
|
||||
{
|
||||
var tags = new KeyValuePair<string, object?>[]
|
||||
{
|
||||
new("revision", revisionId),
|
||||
new("version", version),
|
||||
new("issues", issueCount),
|
||||
};
|
||||
PolicyReloads.Add(1, tags);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user