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

This commit is contained in:
StellaOps Bot
2025-12-13 00:20:26 +02:00
parent e1f1bef4c1
commit 564df71bfb
2376 changed files with 334389 additions and 328032 deletions

View File

@@ -1,12 +1,12 @@
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
namespace StellaOps.Policy;
public interface IPolicyAuditRepository
{
Task AddAsync(PolicyAuditEntry entry, CancellationToken cancellationToken = default);
Task<IReadOnlyList<PolicyAuditEntry>> ListAsync(int limit, CancellationToken cancellationToken = default);
}
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
namespace StellaOps.Policy;
public interface IPolicyAuditRepository
{
Task AddAsync(PolicyAuditEntry entry, CancellationToken cancellationToken = default);
Task<IReadOnlyList<PolicyAuditEntry>> ListAsync(int limit, CancellationToken cancellationToken = default);
}

View File

@@ -1,52 +1,52 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace StellaOps.Policy;
public sealed class InMemoryPolicyAuditRepository : IPolicyAuditRepository
{
private readonly List<PolicyAuditEntry> _entries = new();
private readonly SemaphoreSlim _mutex = new(1, 1);
public async Task AddAsync(PolicyAuditEntry entry, CancellationToken cancellationToken = default)
{
if (entry is null)
{
throw new ArgumentNullException(nameof(entry));
}
await _mutex.WaitAsync(cancellationToken).ConfigureAwait(false);
try
{
_entries.Add(entry);
_entries.Sort(static (left, right) => left.CreatedAt.CompareTo(right.CreatedAt));
}
finally
{
_mutex.Release();
}
}
public async Task<IReadOnlyList<PolicyAuditEntry>> ListAsync(int limit, CancellationToken cancellationToken = default)
{
await _mutex.WaitAsync(cancellationToken).ConfigureAwait(false);
try
{
IEnumerable<PolicyAuditEntry> query = _entries;
if (limit > 0)
{
query = query.TakeLast(limit);
}
return query.ToImmutableArray();
}
finally
{
_mutex.Release();
}
}
}
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace StellaOps.Policy;
public sealed class InMemoryPolicyAuditRepository : IPolicyAuditRepository
{
private readonly List<PolicyAuditEntry> _entries = new();
private readonly SemaphoreSlim _mutex = new(1, 1);
public async Task AddAsync(PolicyAuditEntry entry, CancellationToken cancellationToken = default)
{
if (entry is null)
{
throw new ArgumentNullException(nameof(entry));
}
await _mutex.WaitAsync(cancellationToken).ConfigureAwait(false);
try
{
_entries.Add(entry);
_entries.Sort(static (left, right) => left.CreatedAt.CompareTo(right.CreatedAt));
}
finally
{
_mutex.Release();
}
}
public async Task<IReadOnlyList<PolicyAuditEntry>> ListAsync(int limit, CancellationToken cancellationToken = default)
{
await _mutex.WaitAsync(cancellationToken).ConfigureAwait(false);
try
{
IEnumerable<PolicyAuditEntry> query = _entries;
if (limit > 0)
{
query = query.TakeLast(limit);
}
return query.ToImmutableArray();
}
finally
{
_mutex.Release();
}
}
}

View File

@@ -1,12 +1,12 @@
using System;
namespace StellaOps.Policy;
public sealed record PolicyAuditEntry(
Guid Id,
DateTimeOffset CreatedAt,
string Action,
string RevisionId,
string Digest,
string? Actor,
string Message);
using System;
namespace StellaOps.Policy;
public sealed record PolicyAuditEntry(
Guid Id,
DateTimeOffset CreatedAt,
string Action,
string RevisionId,
string Digest,
string? Actor,
string Message);

File diff suppressed because it is too large Load Diff

View File

@@ -1,77 +1,77 @@
using System;
using System.Collections.Immutable;
using System.Linq;
namespace StellaOps.Policy;
public sealed record PolicyDiagnosticsReport(
string Version,
int RuleCount,
int ErrorCount,
int WarningCount,
DateTimeOffset GeneratedAt,
ImmutableArray<PolicyIssue> Issues,
ImmutableArray<string> Recommendations);
public static class PolicyDiagnostics
{
public static PolicyDiagnosticsReport Create(PolicyBindingResult bindingResult, TimeProvider? timeProvider = null)
{
if (bindingResult is null)
{
throw new ArgumentNullException(nameof(bindingResult));
}
var time = (timeProvider ?? TimeProvider.System).GetUtcNow();
var errorCount = bindingResult.Issues.Count(static issue => issue.Severity == PolicyIssueSeverity.Error);
var warningCount = bindingResult.Issues.Count(static issue => issue.Severity == PolicyIssueSeverity.Warning);
var recommendations = BuildRecommendations(bindingResult.Document, errorCount, warningCount);
return new PolicyDiagnosticsReport(
bindingResult.Document.Version,
bindingResult.Document.Rules.Length,
errorCount,
warningCount,
time,
bindingResult.Issues,
recommendations);
}
private static ImmutableArray<string> BuildRecommendations(PolicyDocument document, int errorCount, int warningCount)
{
var messages = ImmutableArray.CreateBuilder<string>();
if (errorCount > 0)
{
messages.Add("Resolve policy errors before promoting the revision; fallback rules may be applied while errors remain.");
}
if (warningCount > 0)
{
messages.Add("Review policy warnings and ensure intentional overrides are documented.");
}
if (document.Rules.Length == 0)
{
messages.Add("Add at least one policy rule to enforce gating logic.");
}
var quietRules = document.Rules
.Where(static rule => rule.Action.Quiet)
.Select(static rule => rule.Name)
.ToArray();
if (quietRules.Length > 0)
{
messages.Add($"Quiet rules detected ({string.Join(", ", quietRules)}); verify scoring behaviour aligns with expectations.");
}
if (messages.Count == 0)
{
messages.Add("Policy validated successfully; no additional action required.");
}
return messages.ToImmutable();
}
}
using System;
using System.Collections.Immutable;
using System.Linq;
namespace StellaOps.Policy;
public sealed record PolicyDiagnosticsReport(
string Version,
int RuleCount,
int ErrorCount,
int WarningCount,
DateTimeOffset GeneratedAt,
ImmutableArray<PolicyIssue> Issues,
ImmutableArray<string> Recommendations);
public static class PolicyDiagnostics
{
public static PolicyDiagnosticsReport Create(PolicyBindingResult bindingResult, TimeProvider? timeProvider = null)
{
if (bindingResult is null)
{
throw new ArgumentNullException(nameof(bindingResult));
}
var time = (timeProvider ?? TimeProvider.System).GetUtcNow();
var errorCount = bindingResult.Issues.Count(static issue => issue.Severity == PolicyIssueSeverity.Error);
var warningCount = bindingResult.Issues.Count(static issue => issue.Severity == PolicyIssueSeverity.Warning);
var recommendations = BuildRecommendations(bindingResult.Document, errorCount, warningCount);
return new PolicyDiagnosticsReport(
bindingResult.Document.Version,
bindingResult.Document.Rules.Length,
errorCount,
warningCount,
time,
bindingResult.Issues,
recommendations);
}
private static ImmutableArray<string> BuildRecommendations(PolicyDocument document, int errorCount, int warningCount)
{
var messages = ImmutableArray.CreateBuilder<string>();
if (errorCount > 0)
{
messages.Add("Resolve policy errors before promoting the revision; fallback rules may be applied while errors remain.");
}
if (warningCount > 0)
{
messages.Add("Review policy warnings and ensure intentional overrides are documented.");
}
if (document.Rules.Length == 0)
{
messages.Add("Add at least one policy rule to enforce gating logic.");
}
var quietRules = document.Rules
.Where(static rule => rule.Action.Quiet)
.Select(static rule => rule.Name)
.ToArray();
if (quietRules.Length > 0)
{
messages.Add($"Quiet rules detected ({string.Join(", ", quietRules)}); verify scoring behaviour aligns with expectations.");
}
if (messages.Count == 0)
{
messages.Add("Policy validated successfully; no additional action required.");
}
return messages.ToImmutable();
}
}

View File

@@ -1,51 +1,51 @@
using System;
using System.Buffers;
using System.Collections.Immutable;
using System.Linq;
using System.Security.Cryptography;
using System.Text.Json;
namespace StellaOps.Policy;
public static class PolicyDigest
{
public static string Compute(PolicyDocument document)
{
if (document is null)
{
throw new ArgumentNullException(nameof(document));
}
var buffer = new ArrayBufferWriter<byte>();
using (var writer = new Utf8JsonWriter(buffer, new JsonWriterOptions
{
SkipValidation = true,
}))
{
WriteDocument(writer, document);
}
var hash = SHA256.HashData(buffer.WrittenSpan);
return Convert.ToHexString(hash).ToLowerInvariant();
}
private static void WriteDocument(Utf8JsonWriter writer, PolicyDocument document)
{
writer.WriteStartObject();
writer.WriteString("version", document.Version);
if (!document.Metadata.IsEmpty)
{
writer.WritePropertyName("metadata");
writer.WriteStartObject();
foreach (var pair in document.Metadata.OrderBy(static kvp => kvp.Key, StringComparer.Ordinal))
{
writer.WriteString(pair.Key, pair.Value);
}
writer.WriteEndObject();
}
writer.WritePropertyName("rules");
using System;
using System.Buffers;
using System.Collections.Immutable;
using System.Linq;
using System.Security.Cryptography;
using System.Text.Json;
namespace StellaOps.Policy;
public static class PolicyDigest
{
public static string Compute(PolicyDocument document)
{
if (document is null)
{
throw new ArgumentNullException(nameof(document));
}
var buffer = new ArrayBufferWriter<byte>();
using (var writer = new Utf8JsonWriter(buffer, new JsonWriterOptions
{
SkipValidation = true,
}))
{
WriteDocument(writer, document);
}
var hash = SHA256.HashData(buffer.WrittenSpan);
return Convert.ToHexString(hash).ToLowerInvariant();
}
private static void WriteDocument(Utf8JsonWriter writer, PolicyDocument document)
{
writer.WriteStartObject();
writer.WriteString("version", document.Version);
if (!document.Metadata.IsEmpty)
{
writer.WritePropertyName("metadata");
writer.WriteStartObject();
foreach (var pair in document.Metadata.OrderBy(static kvp => kvp.Key, StringComparer.Ordinal))
{
writer.WriteString(pair.Key, pair.Value);
}
writer.WriteEndObject();
}
writer.WritePropertyName("rules");
writer.WriteStartArray();
foreach (var rule in document.Rules)
{
@@ -90,143 +90,143 @@ public static class PolicyDigest
writer.WriteEndObject();
writer.Flush();
}
private static void WriteRule(Utf8JsonWriter writer, PolicyRule rule)
{
writer.WriteStartObject();
writer.WriteString("name", rule.Name);
if (!string.IsNullOrWhiteSpace(rule.Identifier))
{
writer.WriteString("id", rule.Identifier);
}
if (!string.IsNullOrWhiteSpace(rule.Description))
{
writer.WriteString("description", rule.Description);
}
WriteMetadata(writer, rule.Metadata);
WriteSeverities(writer, rule.Severities);
WriteStringArray(writer, "environments", rule.Environments);
WriteStringArray(writer, "sources", rule.Sources);
WriteStringArray(writer, "vendors", rule.Vendors);
WriteStringArray(writer, "licenses", rule.Licenses);
WriteStringArray(writer, "tags", rule.Tags);
if (!rule.Match.IsEmpty)
{
writer.WritePropertyName("match");
writer.WriteStartObject();
WriteStringArray(writer, "images", rule.Match.Images);
WriteStringArray(writer, "repositories", rule.Match.Repositories);
WriteStringArray(writer, "packages", rule.Match.Packages);
WriteStringArray(writer, "purls", rule.Match.Purls);
WriteStringArray(writer, "cves", rule.Match.Cves);
WriteStringArray(writer, "paths", rule.Match.Paths);
WriteStringArray(writer, "layerDigests", rule.Match.LayerDigests);
WriteStringArray(writer, "usedByEntrypoint", rule.Match.UsedByEntrypoint);
writer.WriteEndObject();
}
WriteAction(writer, rule.Action);
if (rule.Expires is DateTimeOffset expires)
{
writer.WriteString("expires", expires.ToUniversalTime().ToString("O"));
}
if (!string.IsNullOrWhiteSpace(rule.Justification))
{
writer.WriteString("justification", rule.Justification);
}
writer.WriteEndObject();
}
private static void WriteAction(Utf8JsonWriter writer, PolicyAction action)
{
writer.WritePropertyName("action");
writer.WriteStartObject();
writer.WriteString("type", action.Type.ToString().ToLowerInvariant());
if (action.Quiet)
{
writer.WriteBoolean("quiet", true);
}
if (action.Ignore is { } ignore)
{
if (ignore.Until is DateTimeOffset until)
{
writer.WriteString("until", until.ToUniversalTime().ToString("O"));
}
if (!string.IsNullOrWhiteSpace(ignore.Justification))
{
writer.WriteString("justification", ignore.Justification);
}
}
if (action.Escalate is { } escalate)
{
if (escalate.MinimumSeverity is { } severity)
{
writer.WriteString("severity", severity.ToString());
}
if (escalate.RequireKev)
{
writer.WriteBoolean("kev", true);
}
if (escalate.MinimumEpss is double epss)
{
writer.WriteNumber("epss", epss);
}
}
if (action.RequireVex is { } requireVex)
{
WriteStringArray(writer, "vendors", requireVex.Vendors);
WriteStringArray(writer, "justifications", requireVex.Justifications);
}
writer.WriteEndObject();
}
private static void WriteMetadata(Utf8JsonWriter writer, ImmutableDictionary<string, string> metadata)
{
if (metadata.IsEmpty)
{
return;
}
writer.WritePropertyName("metadata");
writer.WriteStartObject();
foreach (var pair in metadata.OrderBy(static kvp => kvp.Key, StringComparer.Ordinal))
{
writer.WriteString(pair.Key, pair.Value);
}
writer.WriteEndObject();
}
private static void WriteSeverities(Utf8JsonWriter writer, ImmutableArray<PolicySeverity> severities)
{
if (severities.IsDefaultOrEmpty)
{
return;
}
writer.WritePropertyName("severity");
writer.WriteStartArray();
foreach (var severity in severities)
{
writer.WriteStringValue(severity.ToString());
}
writer.WriteEndArray();
}
private static void WriteRule(Utf8JsonWriter writer, PolicyRule rule)
{
writer.WriteStartObject();
writer.WriteString("name", rule.Name);
if (!string.IsNullOrWhiteSpace(rule.Identifier))
{
writer.WriteString("id", rule.Identifier);
}
if (!string.IsNullOrWhiteSpace(rule.Description))
{
writer.WriteString("description", rule.Description);
}
WriteMetadata(writer, rule.Metadata);
WriteSeverities(writer, rule.Severities);
WriteStringArray(writer, "environments", rule.Environments);
WriteStringArray(writer, "sources", rule.Sources);
WriteStringArray(writer, "vendors", rule.Vendors);
WriteStringArray(writer, "licenses", rule.Licenses);
WriteStringArray(writer, "tags", rule.Tags);
if (!rule.Match.IsEmpty)
{
writer.WritePropertyName("match");
writer.WriteStartObject();
WriteStringArray(writer, "images", rule.Match.Images);
WriteStringArray(writer, "repositories", rule.Match.Repositories);
WriteStringArray(writer, "packages", rule.Match.Packages);
WriteStringArray(writer, "purls", rule.Match.Purls);
WriteStringArray(writer, "cves", rule.Match.Cves);
WriteStringArray(writer, "paths", rule.Match.Paths);
WriteStringArray(writer, "layerDigests", rule.Match.LayerDigests);
WriteStringArray(writer, "usedByEntrypoint", rule.Match.UsedByEntrypoint);
writer.WriteEndObject();
}
WriteAction(writer, rule.Action);
if (rule.Expires is DateTimeOffset expires)
{
writer.WriteString("expires", expires.ToUniversalTime().ToString("O"));
}
if (!string.IsNullOrWhiteSpace(rule.Justification))
{
writer.WriteString("justification", rule.Justification);
}
writer.WriteEndObject();
}
private static void WriteAction(Utf8JsonWriter writer, PolicyAction action)
{
writer.WritePropertyName("action");
writer.WriteStartObject();
writer.WriteString("type", action.Type.ToString().ToLowerInvariant());
if (action.Quiet)
{
writer.WriteBoolean("quiet", true);
}
if (action.Ignore is { } ignore)
{
if (ignore.Until is DateTimeOffset until)
{
writer.WriteString("until", until.ToUniversalTime().ToString("O"));
}
if (!string.IsNullOrWhiteSpace(ignore.Justification))
{
writer.WriteString("justification", ignore.Justification);
}
}
if (action.Escalate is { } escalate)
{
if (escalate.MinimumSeverity is { } severity)
{
writer.WriteString("severity", severity.ToString());
}
if (escalate.RequireKev)
{
writer.WriteBoolean("kev", true);
}
if (escalate.MinimumEpss is double epss)
{
writer.WriteNumber("epss", epss);
}
}
if (action.RequireVex is { } requireVex)
{
WriteStringArray(writer, "vendors", requireVex.Vendors);
WriteStringArray(writer, "justifications", requireVex.Justifications);
}
writer.WriteEndObject();
}
private static void WriteMetadata(Utf8JsonWriter writer, ImmutableDictionary<string, string> metadata)
{
if (metadata.IsEmpty)
{
return;
}
writer.WritePropertyName("metadata");
writer.WriteStartObject();
foreach (var pair in metadata.OrderBy(static kvp => kvp.Key, StringComparer.Ordinal))
{
writer.WriteString(pair.Key, pair.Value);
}
writer.WriteEndObject();
}
private static void WriteSeverities(Utf8JsonWriter writer, ImmutableArray<PolicySeverity> severities)
{
if (severities.IsDefaultOrEmpty)
{
return;
}
writer.WritePropertyName("severity");
writer.WriteStartArray();
foreach (var severity in severities)
{
writer.WriteStringValue(severity.ToString());
}
writer.WriteEndArray();
}
private static void WriteStringArray(Utf8JsonWriter writer, string propertyName, ImmutableArray<string> values)
{
if (values.IsDefaultOrEmpty)

View File

@@ -23,164 +23,164 @@ public sealed record PolicyDocument(
public static class PolicySchema
{
public const string SchemaId = "https://schemas.stella-ops.org/policy/policy-schema@1.json";
public const string CurrentVersion = "1.0";
public static PolicyDocumentFormat DetectFormat(string fileName)
{
if (fileName is null)
{
throw new ArgumentNullException(nameof(fileName));
}
var lower = fileName.Trim().ToLowerInvariant();
public const string CurrentVersion = "1.0";
public static PolicyDocumentFormat DetectFormat(string fileName)
{
if (fileName is null)
{
throw new ArgumentNullException(nameof(fileName));
}
var lower = fileName.Trim().ToLowerInvariant();
if (lower.EndsWith(".yaml", StringComparison.Ordinal)
|| lower.EndsWith(".yml", StringComparison.Ordinal)
|| lower.EndsWith(".stella", StringComparison.Ordinal))
{
return PolicyDocumentFormat.Yaml;
}
return PolicyDocumentFormat.Json;
}
}
public sealed record PolicyRule(
string Name,
string? Identifier,
string? Description,
PolicyAction Action,
ImmutableArray<PolicySeverity> Severities,
ImmutableArray<string> Environments,
ImmutableArray<string> Sources,
ImmutableArray<string> Vendors,
ImmutableArray<string> Licenses,
ImmutableArray<string> Tags,
PolicyRuleMatchCriteria Match,
DateTimeOffset? Expires,
string? Justification,
ImmutableDictionary<string, string> Metadata)
{
public static PolicyRuleMatchCriteria EmptyMatch { get; } = new(
ImmutableArray<string>.Empty,
ImmutableArray<string>.Empty,
ImmutableArray<string>.Empty,
ImmutableArray<string>.Empty,
ImmutableArray<string>.Empty,
ImmutableArray<string>.Empty,
ImmutableArray<string>.Empty,
ImmutableArray<string>.Empty);
public static PolicyRule Create(
string name,
PolicyAction action,
ImmutableArray<PolicySeverity> severities,
ImmutableArray<string> environments,
ImmutableArray<string> sources,
ImmutableArray<string> vendors,
ImmutableArray<string> licenses,
ImmutableArray<string> tags,
PolicyRuleMatchCriteria match,
DateTimeOffset? expires,
string? justification,
string? identifier = null,
string? description = null,
ImmutableDictionary<string, string>? metadata = null)
{
metadata ??= ImmutableDictionary<string, string>.Empty;
return new PolicyRule(
name,
identifier,
description,
action,
severities,
environments,
sources,
vendors,
licenses,
tags,
match,
expires,
justification,
metadata);
}
public bool MatchesAnyEnvironment => Environments.IsDefaultOrEmpty;
}
public sealed record PolicyRuleMatchCriteria(
ImmutableArray<string> Images,
ImmutableArray<string> Repositories,
ImmutableArray<string> Packages,
ImmutableArray<string> Purls,
ImmutableArray<string> Cves,
ImmutableArray<string> Paths,
ImmutableArray<string> LayerDigests,
ImmutableArray<string> UsedByEntrypoint)
{
public static PolicyRuleMatchCriteria Create(
ImmutableArray<string> images,
ImmutableArray<string> repositories,
ImmutableArray<string> packages,
ImmutableArray<string> purls,
ImmutableArray<string> cves,
ImmutableArray<string> paths,
ImmutableArray<string> layerDigests,
ImmutableArray<string> usedByEntrypoint)
=> new(
images,
repositories,
packages,
purls,
cves,
paths,
layerDigests,
usedByEntrypoint);
public static PolicyRuleMatchCriteria Empty { get; } = new(
ImmutableArray<string>.Empty,
ImmutableArray<string>.Empty,
ImmutableArray<string>.Empty,
ImmutableArray<string>.Empty,
ImmutableArray<string>.Empty,
ImmutableArray<string>.Empty,
ImmutableArray<string>.Empty,
ImmutableArray<string>.Empty);
public bool IsEmpty =>
Images.IsDefaultOrEmpty &&
Repositories.IsDefaultOrEmpty &&
Packages.IsDefaultOrEmpty &&
Purls.IsDefaultOrEmpty &&
Cves.IsDefaultOrEmpty &&
Paths.IsDefaultOrEmpty &&
LayerDigests.IsDefaultOrEmpty &&
UsedByEntrypoint.IsDefaultOrEmpty;
}
return PolicyDocumentFormat.Json;
}
}
public sealed record PolicyRule(
string Name,
string? Identifier,
string? Description,
PolicyAction Action,
ImmutableArray<PolicySeverity> Severities,
ImmutableArray<string> Environments,
ImmutableArray<string> Sources,
ImmutableArray<string> Vendors,
ImmutableArray<string> Licenses,
ImmutableArray<string> Tags,
PolicyRuleMatchCriteria Match,
DateTimeOffset? Expires,
string? Justification,
ImmutableDictionary<string, string> Metadata)
{
public static PolicyRuleMatchCriteria EmptyMatch { get; } = new(
ImmutableArray<string>.Empty,
ImmutableArray<string>.Empty,
ImmutableArray<string>.Empty,
ImmutableArray<string>.Empty,
ImmutableArray<string>.Empty,
ImmutableArray<string>.Empty,
ImmutableArray<string>.Empty,
ImmutableArray<string>.Empty);
public static PolicyRule Create(
string name,
PolicyAction action,
ImmutableArray<PolicySeverity> severities,
ImmutableArray<string> environments,
ImmutableArray<string> sources,
ImmutableArray<string> vendors,
ImmutableArray<string> licenses,
ImmutableArray<string> tags,
PolicyRuleMatchCriteria match,
DateTimeOffset? expires,
string? justification,
string? identifier = null,
string? description = null,
ImmutableDictionary<string, string>? metadata = null)
{
metadata ??= ImmutableDictionary<string, string>.Empty;
return new PolicyRule(
name,
identifier,
description,
action,
severities,
environments,
sources,
vendors,
licenses,
tags,
match,
expires,
justification,
metadata);
}
public bool MatchesAnyEnvironment => Environments.IsDefaultOrEmpty;
}
public sealed record PolicyRuleMatchCriteria(
ImmutableArray<string> Images,
ImmutableArray<string> Repositories,
ImmutableArray<string> Packages,
ImmutableArray<string> Purls,
ImmutableArray<string> Cves,
ImmutableArray<string> Paths,
ImmutableArray<string> LayerDigests,
ImmutableArray<string> UsedByEntrypoint)
{
public static PolicyRuleMatchCriteria Create(
ImmutableArray<string> images,
ImmutableArray<string> repositories,
ImmutableArray<string> packages,
ImmutableArray<string> purls,
ImmutableArray<string> cves,
ImmutableArray<string> paths,
ImmutableArray<string> layerDigests,
ImmutableArray<string> usedByEntrypoint)
=> new(
images,
repositories,
packages,
purls,
cves,
paths,
layerDigests,
usedByEntrypoint);
public static PolicyRuleMatchCriteria Empty { get; } = new(
ImmutableArray<string>.Empty,
ImmutableArray<string>.Empty,
ImmutableArray<string>.Empty,
ImmutableArray<string>.Empty,
ImmutableArray<string>.Empty,
ImmutableArray<string>.Empty,
ImmutableArray<string>.Empty,
ImmutableArray<string>.Empty);
public bool IsEmpty =>
Images.IsDefaultOrEmpty &&
Repositories.IsDefaultOrEmpty &&
Packages.IsDefaultOrEmpty &&
Purls.IsDefaultOrEmpty &&
Cves.IsDefaultOrEmpty &&
Paths.IsDefaultOrEmpty &&
LayerDigests.IsDefaultOrEmpty &&
UsedByEntrypoint.IsDefaultOrEmpty;
}
public sealed record PolicyAction(
PolicyActionType Type,
PolicyIgnoreOptions? Ignore,
PolicyEscalateOptions? Escalate,
PolicyRequireVexOptions? RequireVex,
bool Quiet);
public enum PolicyActionType
{
Block,
Ignore,
Warn,
Defer,
Escalate,
RequireVex,
}
public sealed record PolicyIgnoreOptions(DateTimeOffset? Until, string? Justification);
public sealed record PolicyEscalateOptions(
PolicySeverity? MinimumSeverity,
bool RequireKev,
double? MinimumEpss);
public enum PolicyActionType
{
Block,
Ignore,
Warn,
Defer,
Escalate,
RequireVex,
}
public sealed record PolicyIgnoreOptions(DateTimeOffset? Until, string? Justification);
public sealed record PolicyEscalateOptions(
PolicySeverity? MinimumSeverity,
bool RequireKev,
double? MinimumEpss);
public sealed record PolicyRequireVexOptions(
ImmutableArray<string> Vendors,
ImmutableArray<string> Justifications);

File diff suppressed because it is too large Load Diff

View File

@@ -1,51 +1,51 @@
using System.Collections.Immutable;
namespace StellaOps.Policy;
public sealed record PolicyFinding(
string FindingId,
PolicySeverity Severity,
string? Environment,
string? Source,
string? Vendor,
string? License,
string? Image,
string? Repository,
string? Package,
string? Purl,
string? Cve,
string? Path,
string? LayerDigest,
ImmutableArray<string> Tags)
{
public static PolicyFinding Create(
string findingId,
PolicySeverity severity,
string? environment = null,
string? source = null,
string? vendor = null,
string? license = null,
string? image = null,
string? repository = null,
string? package = null,
string? purl = null,
string? cve = null,
string? path = null,
string? layerDigest = null,
ImmutableArray<string>? tags = null)
=> new(
findingId,
severity,
environment,
source,
vendor,
license,
image,
repository,
package,
purl,
cve,
path,
layerDigest,
tags ?? ImmutableArray<string>.Empty);
}
using System.Collections.Immutable;
namespace StellaOps.Policy;
public sealed record PolicyFinding(
string FindingId,
PolicySeverity Severity,
string? Environment,
string? Source,
string? Vendor,
string? License,
string? Image,
string? Repository,
string? Package,
string? Purl,
string? Cve,
string? Path,
string? LayerDigest,
ImmutableArray<string> Tags)
{
public static PolicyFinding Create(
string findingId,
PolicySeverity severity,
string? environment = null,
string? source = null,
string? vendor = null,
string? license = null,
string? image = null,
string? repository = null,
string? package = null,
string? purl = null,
string? cve = null,
string? path = null,
string? layerDigest = null,
ImmutableArray<string>? tags = null)
=> new(
findingId,
severity,
environment,
source,
vendor,
license,
image,
repository,
package,
purl,
cve,
path,
layerDigest,
tags ?? ImmutableArray<string>.Empty);
}

View File

@@ -1,28 +1,28 @@
using System;
namespace StellaOps.Policy;
/// <summary>
/// Represents a validation or normalization issue discovered while processing a policy document.
/// </summary>
public sealed record PolicyIssue(string Code, string Message, PolicyIssueSeverity Severity, string Path)
{
public static PolicyIssue Error(string code, string message, string path)
=> new(code, message, PolicyIssueSeverity.Error, path);
public static PolicyIssue Warning(string code, string message, string path)
=> new(code, message, PolicyIssueSeverity.Warning, path);
public static PolicyIssue Info(string code, string message, string path)
=> new(code, message, PolicyIssueSeverity.Info, path);
public PolicyIssue EnsurePath(string fallbackPath)
=> string.IsNullOrWhiteSpace(Path) ? this with { Path = fallbackPath } : this;
}
public enum PolicyIssueSeverity
{
Error,
Warning,
Info,
}
using System;
namespace StellaOps.Policy;
/// <summary>
/// Represents a validation or normalization issue discovered while processing a policy document.
/// </summary>
public sealed record PolicyIssue(string Code, string Message, PolicyIssueSeverity Severity, string Path)
{
public static PolicyIssue Error(string code, string message, string path)
=> new(code, message, PolicyIssueSeverity.Error, path);
public static PolicyIssue Warning(string code, string message, string path)
=> new(code, message, PolicyIssueSeverity.Warning, path);
public static PolicyIssue Info(string code, string message, string path)
=> new(code, message, PolicyIssueSeverity.Info, path);
public PolicyIssue EnsurePath(string fallbackPath)
=> string.IsNullOrWhiteSpace(Path) ? this with { Path = fallbackPath } : this;
}
public enum PolicyIssueSeverity
{
Error,
Warning,
Info,
}

View File

@@ -1,18 +1,18 @@
using System.Collections.Immutable;
namespace StellaOps.Policy;
public sealed record PolicyPreviewRequest(
string ImageDigest,
ImmutableArray<PolicyFinding> Findings,
ImmutableArray<PolicyVerdict> BaselineVerdicts,
PolicySnapshot? SnapshotOverride = null,
PolicySnapshotContent? ProposedPolicy = null);
public sealed record PolicyPreviewResponse(
bool Success,
string PolicyDigest,
string? RevisionId,
ImmutableArray<PolicyIssue> Issues,
ImmutableArray<PolicyVerdictDiff> Diffs,
int ChangedCount);
using System.Collections.Immutable;
namespace StellaOps.Policy;
public sealed record PolicyPreviewRequest(
string ImageDigest,
ImmutableArray<PolicyFinding> Findings,
ImmutableArray<PolicyVerdict> BaselineVerdicts,
PolicySnapshot? SnapshotOverride = null,
PolicySnapshotContent? ProposedPolicy = null);
public sealed record PolicyPreviewResponse(
bool Success,
string PolicyDigest,
string? RevisionId,
ImmutableArray<PolicyIssue> Issues,
ImmutableArray<PolicyVerdictDiff> Diffs,
int ChangedCount);

View File

@@ -1,142 +1,142 @@
using System;
using System.Collections.Generic;
using System;
using System.Collections.Immutable;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
namespace StellaOps.Policy;
public sealed class PolicyPreviewService
{
private readonly PolicySnapshotStore _snapshotStore;
private readonly ILogger<PolicyPreviewService> _logger;
public PolicyPreviewService(PolicySnapshotStore snapshotStore, ILogger<PolicyPreviewService> logger)
{
_snapshotStore = snapshotStore ?? throw new ArgumentNullException(nameof(snapshotStore));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
public async Task<PolicyPreviewResponse> PreviewAsync(PolicyPreviewRequest request, CancellationToken cancellationToken = default)
{
if (request is null)
{
throw new ArgumentNullException(nameof(request));
}
var (snapshot, bindingIssues) = await ResolveSnapshotAsync(request, cancellationToken).ConfigureAwait(false);
if (snapshot is null)
{
_logger.LogWarning("Policy preview failed: snapshot unavailable or validation errors. Issues={Count}", bindingIssues.Length);
return new PolicyPreviewResponse(false, string.Empty, null, bindingIssues, ImmutableArray<PolicyVerdictDiff>.Empty, 0);
}
var projected = Evaluate(snapshot.Document, snapshot.ScoringConfig, request.Findings);
var baseline = BuildBaseline(request.BaselineVerdicts, projected, snapshot.ScoringConfig);
var diffs = BuildDiffs(baseline, projected);
var changed = diffs.Count(static diff => diff.Changed);
_logger.LogDebug("Policy preview computed for {ImageDigest}. Changed={Changed}", request.ImageDigest, changed);
return new PolicyPreviewResponse(true, snapshot.Digest, snapshot.RevisionId, bindingIssues, diffs, changed);
}
private async Task<(PolicySnapshot? Snapshot, ImmutableArray<PolicyIssue> Issues)> ResolveSnapshotAsync(PolicyPreviewRequest request, CancellationToken cancellationToken)
{
if (request.ProposedPolicy is not null)
{
var binding = PolicyBinder.Bind(request.ProposedPolicy.Content, request.ProposedPolicy.Format);
if (!binding.Success)
{
return (null, binding.Issues);
}
var digest = PolicyDigest.Compute(binding.Document);
var snapshot = new PolicySnapshot(
request.SnapshotOverride?.RevisionNumber + 1 ?? 0,
request.SnapshotOverride?.RevisionId ?? "preview",
digest,
DateTimeOffset.UtcNow,
request.ProposedPolicy.Actor,
request.ProposedPolicy.Format,
binding.Document,
binding.Issues,
PolicyScoringConfig.Default);
return (snapshot, binding.Issues);
}
if (request.SnapshotOverride is not null)
{
return (request.SnapshotOverride, ImmutableArray<PolicyIssue>.Empty);
}
var latest = await _snapshotStore.GetLatestAsync(cancellationToken).ConfigureAwait(false);
if (latest is not null)
{
return (latest, ImmutableArray<PolicyIssue>.Empty);
}
return (null, ImmutableArray.Create(PolicyIssue.Error("policy.preview.snapshot_missing", "No policy snapshot is available for preview.", "$")));
}
private static ImmutableArray<PolicyVerdict> Evaluate(PolicyDocument document, PolicyScoringConfig scoringConfig, ImmutableArray<PolicyFinding> findings)
{
if (findings.IsDefaultOrEmpty)
{
return ImmutableArray<PolicyVerdict>.Empty;
}
var results = ImmutableArray.CreateBuilder<PolicyVerdict>(findings.Length);
foreach (var finding in findings)
{
using System;
using System.Collections.Generic;
using System;
using System.Collections.Immutable;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
namespace StellaOps.Policy;
public sealed class PolicyPreviewService
{
private readonly PolicySnapshotStore _snapshotStore;
private readonly ILogger<PolicyPreviewService> _logger;
public PolicyPreviewService(PolicySnapshotStore snapshotStore, ILogger<PolicyPreviewService> logger)
{
_snapshotStore = snapshotStore ?? throw new ArgumentNullException(nameof(snapshotStore));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
public async Task<PolicyPreviewResponse> PreviewAsync(PolicyPreviewRequest request, CancellationToken cancellationToken = default)
{
if (request is null)
{
throw new ArgumentNullException(nameof(request));
}
var (snapshot, bindingIssues) = await ResolveSnapshotAsync(request, cancellationToken).ConfigureAwait(false);
if (snapshot is null)
{
_logger.LogWarning("Policy preview failed: snapshot unavailable or validation errors. Issues={Count}", bindingIssues.Length);
return new PolicyPreviewResponse(false, string.Empty, null, bindingIssues, ImmutableArray<PolicyVerdictDiff>.Empty, 0);
}
var projected = Evaluate(snapshot.Document, snapshot.ScoringConfig, request.Findings);
var baseline = BuildBaseline(request.BaselineVerdicts, projected, snapshot.ScoringConfig);
var diffs = BuildDiffs(baseline, projected);
var changed = diffs.Count(static diff => diff.Changed);
_logger.LogDebug("Policy preview computed for {ImageDigest}. Changed={Changed}", request.ImageDigest, changed);
return new PolicyPreviewResponse(true, snapshot.Digest, snapshot.RevisionId, bindingIssues, diffs, changed);
}
private async Task<(PolicySnapshot? Snapshot, ImmutableArray<PolicyIssue> Issues)> ResolveSnapshotAsync(PolicyPreviewRequest request, CancellationToken cancellationToken)
{
if (request.ProposedPolicy is not null)
{
var binding = PolicyBinder.Bind(request.ProposedPolicy.Content, request.ProposedPolicy.Format);
if (!binding.Success)
{
return (null, binding.Issues);
}
var digest = PolicyDigest.Compute(binding.Document);
var snapshot = new PolicySnapshot(
request.SnapshotOverride?.RevisionNumber + 1 ?? 0,
request.SnapshotOverride?.RevisionId ?? "preview",
digest,
DateTimeOffset.UtcNow,
request.ProposedPolicy.Actor,
request.ProposedPolicy.Format,
binding.Document,
binding.Issues,
PolicyScoringConfig.Default);
return (snapshot, binding.Issues);
}
if (request.SnapshotOverride is not null)
{
return (request.SnapshotOverride, ImmutableArray<PolicyIssue>.Empty);
}
var latest = await _snapshotStore.GetLatestAsync(cancellationToken).ConfigureAwait(false);
if (latest is not null)
{
return (latest, ImmutableArray<PolicyIssue>.Empty);
}
return (null, ImmutableArray.Create(PolicyIssue.Error("policy.preview.snapshot_missing", "No policy snapshot is available for preview.", "$")));
}
private static ImmutableArray<PolicyVerdict> Evaluate(PolicyDocument document, PolicyScoringConfig scoringConfig, ImmutableArray<PolicyFinding> findings)
{
if (findings.IsDefaultOrEmpty)
{
return ImmutableArray<PolicyVerdict>.Empty;
}
var results = ImmutableArray.CreateBuilder<PolicyVerdict>(findings.Length);
foreach (var finding in findings)
{
var verdict = PolicyEvaluation.EvaluateFinding(document, scoringConfig, finding, out _);
results.Add(verdict);
}
return results.ToImmutable();
}
private static ImmutableDictionary<string, PolicyVerdict> BuildBaseline(ImmutableArray<PolicyVerdict> baseline, ImmutableArray<PolicyVerdict> projected, PolicyScoringConfig scoringConfig)
{
var builder = ImmutableDictionary.CreateBuilder<string, PolicyVerdict>(StringComparer.Ordinal);
if (!baseline.IsDefaultOrEmpty)
{
foreach (var verdict in baseline)
{
if (!string.IsNullOrEmpty(verdict.FindingId) && !builder.ContainsKey(verdict.FindingId))
{
builder.Add(verdict.FindingId, verdict);
}
}
}
foreach (var verdict in projected)
{
if (!builder.ContainsKey(verdict.FindingId))
{
builder.Add(verdict.FindingId, PolicyVerdict.CreateBaseline(verdict.FindingId, scoringConfig));
}
}
return builder.ToImmutable();
}
private static ImmutableArray<PolicyVerdictDiff> BuildDiffs(ImmutableDictionary<string, PolicyVerdict> baseline, ImmutableArray<PolicyVerdict> projected)
{
var diffs = ImmutableArray.CreateBuilder<PolicyVerdictDiff>(projected.Length);
foreach (var verdict in projected.OrderBy(static v => v.FindingId, StringComparer.Ordinal))
{
var baseVerdict = baseline.TryGetValue(verdict.FindingId, out var existing)
? existing
: new PolicyVerdict(verdict.FindingId, PolicyVerdictStatus.Pass);
diffs.Add(new PolicyVerdictDiff(baseVerdict, verdict));
}
return diffs.ToImmutable();
}
}
results.Add(verdict);
}
return results.ToImmutable();
}
private static ImmutableDictionary<string, PolicyVerdict> BuildBaseline(ImmutableArray<PolicyVerdict> baseline, ImmutableArray<PolicyVerdict> projected, PolicyScoringConfig scoringConfig)
{
var builder = ImmutableDictionary.CreateBuilder<string, PolicyVerdict>(StringComparer.Ordinal);
if (!baseline.IsDefaultOrEmpty)
{
foreach (var verdict in baseline)
{
if (!string.IsNullOrEmpty(verdict.FindingId) && !builder.ContainsKey(verdict.FindingId))
{
builder.Add(verdict.FindingId, verdict);
}
}
}
foreach (var verdict in projected)
{
if (!builder.ContainsKey(verdict.FindingId))
{
builder.Add(verdict.FindingId, PolicyVerdict.CreateBaseline(verdict.FindingId, scoringConfig));
}
}
return builder.ToImmutable();
}
private static ImmutableArray<PolicyVerdictDiff> BuildDiffs(ImmutableDictionary<string, PolicyVerdict> baseline, ImmutableArray<PolicyVerdict> projected)
{
var diffs = ImmutableArray.CreateBuilder<PolicyVerdictDiff>(projected.Length);
foreach (var verdict in projected.OrderBy(static v => v.FindingId, StringComparer.Ordinal))
{
var baseVerdict = baseline.TryGetValue(verdict.FindingId, out var existing)
? existing
: new PolicyVerdict(verdict.FindingId, PolicyVerdictStatus.Pass);
diffs.Add(new PolicyVerdictDiff(baseVerdict, verdict));
}
return diffs.ToImmutable();
}
}

View File

@@ -1,30 +1,30 @@
using System;
using System.IO;
using System.Reflection;
using System.Text;
namespace StellaOps.Policy;
public static class PolicySchemaResource
{
private const string SchemaResourceName = "StellaOps.Policy.Schemas.policy-schema@1.json";
public static Stream OpenSchemaStream()
{
var assembly = Assembly.GetExecutingAssembly();
var stream = assembly.GetManifestResourceStream(SchemaResourceName);
if (stream is null)
{
throw new InvalidOperationException($"Unable to locate embedded schema resource '{SchemaResourceName}'.");
}
return stream;
}
public static string ReadSchemaJson()
{
using var stream = OpenSchemaStream();
using var reader = new StreamReader(stream, Encoding.UTF8, detectEncodingFromByteOrderMarks: true);
return reader.ReadToEnd();
}
}
using System;
using System.IO;
using System.Reflection;
using System.Text;
namespace StellaOps.Policy;
public static class PolicySchemaResource
{
private const string SchemaResourceName = "StellaOps.Policy.Schemas.policy-schema@1.json";
public static Stream OpenSchemaStream()
{
var assembly = Assembly.GetExecutingAssembly();
var stream = assembly.GetManifestResourceStream(SchemaResourceName);
if (stream is null)
{
throw new InvalidOperationException($"Unable to locate embedded schema resource '{SchemaResourceName}'.");
}
return stream;
}
public static string ReadSchemaJson()
{
using var stream = OpenSchemaStream();
using var reader = new StreamReader(stream, Encoding.UTF8, detectEncodingFromByteOrderMarks: true);
return reader.ReadToEnd();
}
}

View File

@@ -1,18 +1,18 @@
using System.Collections.Immutable;
namespace StellaOps.Policy;
public sealed record PolicyScoringConfig(
string Version,
ImmutableDictionary<PolicySeverity, double> SeverityWeights,
double QuietPenalty,
double WarnPenalty,
double IgnorePenalty,
ImmutableDictionary<string, double> TrustOverrides,
ImmutableDictionary<string, double> ReachabilityBuckets,
PolicyUnknownConfidenceConfig UnknownConfidence)
{
public static string BaselineVersion => "1.0";
public static PolicyScoringConfig Default { get; } = PolicyScoringConfigBinder.LoadDefault();
}
using System.Collections.Immutable;
namespace StellaOps.Policy;
public sealed record PolicyScoringConfig(
string Version,
ImmutableDictionary<PolicySeverity, double> SeverityWeights,
double QuietPenalty,
double WarnPenalty,
double IgnorePenalty,
ImmutableDictionary<string, double> TrustOverrides,
ImmutableDictionary<string, double> ReachabilityBuckets,
PolicyUnknownConfidenceConfig UnknownConfidence)
{
public static string BaselineVersion => "1.0";
public static PolicyScoringConfig Default { get; } = PolicyScoringConfigBinder.LoadDefault();
}

View File

@@ -1,100 +1,100 @@
using System;
using System.Buffers;
using System.Collections.Immutable;
using System.Linq;
using System.Security.Cryptography;
using System.Text.Json;
namespace StellaOps.Policy;
public static class PolicyScoringConfigDigest
{
public static string Compute(PolicyScoringConfig config)
{
ArgumentNullException.ThrowIfNull(config);
var buffer = new ArrayBufferWriter<byte>();
using (var writer = new Utf8JsonWriter(buffer, new JsonWriterOptions
{
SkipValidation = true,
}))
{
WriteConfig(writer, config);
}
var hash = SHA256.HashData(buffer.WrittenSpan);
return Convert.ToHexString(hash).ToLowerInvariant();
}
private static void WriteConfig(Utf8JsonWriter writer, PolicyScoringConfig config)
{
writer.WriteStartObject();
writer.WriteString("version", config.Version);
writer.WritePropertyName("severityWeights");
writer.WriteStartObject();
foreach (var severity in Enum.GetValues<PolicySeverity>())
{
var key = severity.ToString();
var value = config.SeverityWeights.TryGetValue(severity, out var weight) ? weight : 0;
writer.WriteNumber(key, value);
}
writer.WriteEndObject();
writer.WriteNumber("quietPenalty", config.QuietPenalty);
writer.WriteNumber("warnPenalty", config.WarnPenalty);
writer.WriteNumber("ignorePenalty", config.IgnorePenalty);
if (!config.TrustOverrides.IsEmpty)
{
writer.WritePropertyName("trustOverrides");
writer.WriteStartObject();
foreach (var pair in config.TrustOverrides.OrderBy(static kvp => kvp.Key, StringComparer.OrdinalIgnoreCase))
{
writer.WriteNumber(pair.Key, pair.Value);
}
writer.WriteEndObject();
}
if (!config.ReachabilityBuckets.IsEmpty)
{
writer.WritePropertyName("reachabilityBuckets");
writer.WriteStartObject();
foreach (var pair in config.ReachabilityBuckets.OrderBy(static kvp => kvp.Key, StringComparer.OrdinalIgnoreCase))
{
writer.WriteNumber(pair.Key, pair.Value);
}
writer.WriteEndObject();
}
writer.WritePropertyName("unknownConfidence");
writer.WriteStartObject();
writer.WriteNumber("initial", config.UnknownConfidence.Initial);
writer.WriteNumber("decayPerDay", config.UnknownConfidence.DecayPerDay);
writer.WriteNumber("floor", config.UnknownConfidence.Floor);
if (!config.UnknownConfidence.Bands.IsDefaultOrEmpty)
{
writer.WritePropertyName("bands");
writer.WriteStartArray();
foreach (var band in config.UnknownConfidence.Bands
.OrderByDescending(static b => b.Min)
.ThenBy(static b => b.Name, StringComparer.OrdinalIgnoreCase))
{
writer.WriteStartObject();
writer.WriteString("name", band.Name);
writer.WriteNumber("min", band.Min);
if (!string.IsNullOrWhiteSpace(band.Description))
{
writer.WriteString("description", band.Description);
}
writer.WriteEndObject();
}
writer.WriteEndArray();
}
writer.WriteEndObject();
writer.WriteEndObject();
writer.Flush();
}
}
using System;
using System.Buffers;
using System.Collections.Immutable;
using System.Linq;
using System.Security.Cryptography;
using System.Text.Json;
namespace StellaOps.Policy;
public static class PolicyScoringConfigDigest
{
public static string Compute(PolicyScoringConfig config)
{
ArgumentNullException.ThrowIfNull(config);
var buffer = new ArrayBufferWriter<byte>();
using (var writer = new Utf8JsonWriter(buffer, new JsonWriterOptions
{
SkipValidation = true,
}))
{
WriteConfig(writer, config);
}
var hash = SHA256.HashData(buffer.WrittenSpan);
return Convert.ToHexString(hash).ToLowerInvariant();
}
private static void WriteConfig(Utf8JsonWriter writer, PolicyScoringConfig config)
{
writer.WriteStartObject();
writer.WriteString("version", config.Version);
writer.WritePropertyName("severityWeights");
writer.WriteStartObject();
foreach (var severity in Enum.GetValues<PolicySeverity>())
{
var key = severity.ToString();
var value = config.SeverityWeights.TryGetValue(severity, out var weight) ? weight : 0;
writer.WriteNumber(key, value);
}
writer.WriteEndObject();
writer.WriteNumber("quietPenalty", config.QuietPenalty);
writer.WriteNumber("warnPenalty", config.WarnPenalty);
writer.WriteNumber("ignorePenalty", config.IgnorePenalty);
if (!config.TrustOverrides.IsEmpty)
{
writer.WritePropertyName("trustOverrides");
writer.WriteStartObject();
foreach (var pair in config.TrustOverrides.OrderBy(static kvp => kvp.Key, StringComparer.OrdinalIgnoreCase))
{
writer.WriteNumber(pair.Key, pair.Value);
}
writer.WriteEndObject();
}
if (!config.ReachabilityBuckets.IsEmpty)
{
writer.WritePropertyName("reachabilityBuckets");
writer.WriteStartObject();
foreach (var pair in config.ReachabilityBuckets.OrderBy(static kvp => kvp.Key, StringComparer.OrdinalIgnoreCase))
{
writer.WriteNumber(pair.Key, pair.Value);
}
writer.WriteEndObject();
}
writer.WritePropertyName("unknownConfidence");
writer.WriteStartObject();
writer.WriteNumber("initial", config.UnknownConfidence.Initial);
writer.WriteNumber("decayPerDay", config.UnknownConfidence.DecayPerDay);
writer.WriteNumber("floor", config.UnknownConfidence.Floor);
if (!config.UnknownConfidence.Bands.IsDefaultOrEmpty)
{
writer.WritePropertyName("bands");
writer.WriteStartArray();
foreach (var band in config.UnknownConfidence.Bands
.OrderByDescending(static b => b.Min)
.ThenBy(static b => b.Name, StringComparer.OrdinalIgnoreCase))
{
writer.WriteStartObject();
writer.WriteString("name", band.Name);
writer.WriteNumber("min", band.Min);
if (!string.IsNullOrWhiteSpace(band.Description))
{
writer.WriteString("description", band.Description);
}
writer.WriteEndObject();
}
writer.WriteEndArray();
}
writer.WriteEndObject();
writer.WriteEndObject();
writer.Flush();
}
}

View File

@@ -1,27 +1,27 @@
using System;
using System.IO;
using System.Reflection;
using System.Text;
using System.Threading;
using Json.Schema;
namespace StellaOps.Policy;
public static class PolicyScoringSchema
{
private const string SchemaResourceName = "StellaOps.Policy.Schemas.policy-scoring-schema@1.json";
private static readonly Lazy<JsonSchema> CachedSchema = new(LoadSchema, LazyThreadSafetyMode.ExecutionAndPublication);
public static JsonSchema Schema => CachedSchema.Value;
private static JsonSchema LoadSchema()
{
var assembly = Assembly.GetExecutingAssembly();
using var stream = assembly.GetManifestResourceStream(SchemaResourceName)
?? throw new InvalidOperationException($"Embedded resource '{SchemaResourceName}' was not found.");
using var reader = new StreamReader(stream, Encoding.UTF8, detectEncodingFromByteOrderMarks: true);
var schemaJson = reader.ReadToEnd();
return JsonSchema.FromText(schemaJson);
}
}
using System;
using System.IO;
using System.Reflection;
using System.Text;
using System.Threading;
using Json.Schema;
namespace StellaOps.Policy;
public static class PolicyScoringSchema
{
private const string SchemaResourceName = "StellaOps.Policy.Schemas.policy-scoring-schema@1.json";
private static readonly Lazy<JsonSchema> CachedSchema = new(LoadSchema, LazyThreadSafetyMode.ExecutionAndPublication);
public static JsonSchema Schema => CachedSchema.Value;
private static JsonSchema LoadSchema()
{
var assembly = Assembly.GetExecutingAssembly();
using var stream = assembly.GetManifestResourceStream(SchemaResourceName)
?? throw new InvalidOperationException($"Embedded resource '{SchemaResourceName}' was not found.");
using var reader = new StreamReader(stream, Encoding.UTF8, detectEncodingFromByteOrderMarks: true);
var schemaJson = reader.ReadToEnd();
return JsonSchema.FromText(schemaJson);
}
}

View File

@@ -1,29 +1,29 @@
using System;
using System.Collections.Immutable;
namespace StellaOps.Policy;
public sealed record PolicySnapshot(
long RevisionNumber,
string RevisionId,
string Digest,
DateTimeOffset CreatedAt,
string? CreatedBy,
PolicyDocumentFormat Format,
PolicyDocument Document,
ImmutableArray<PolicyIssue> Issues,
PolicyScoringConfig ScoringConfig);
public sealed record PolicySnapshotContent(
string Content,
PolicyDocumentFormat Format,
string? Actor,
string? Source,
string? Description);
public sealed record PolicySnapshotSaveResult(
bool Success,
bool Created,
string Digest,
PolicySnapshot? Snapshot,
PolicyBindingResult BindingResult);
using System;
using System.Collections.Immutable;
namespace StellaOps.Policy;
public sealed record PolicySnapshot(
long RevisionNumber,
string RevisionId,
string Digest,
DateTimeOffset CreatedAt,
string? CreatedBy,
PolicyDocumentFormat Format,
PolicyDocument Document,
ImmutableArray<PolicyIssue> Issues,
PolicyScoringConfig ScoringConfig);
public sealed record PolicySnapshotContent(
string Content,
PolicyDocumentFormat Format,
string? Actor,
string? Source,
string? Description);
public sealed record PolicySnapshotSaveResult(
bool Success,
bool Created,
string Digest,
PolicySnapshot? Snapshot,
PolicyBindingResult BindingResult);

View File

@@ -1,101 +1,101 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
namespace StellaOps.Policy;
public sealed class PolicySnapshotStore
{
private readonly IPolicySnapshotRepository _snapshotRepository;
private readonly IPolicyAuditRepository _auditRepository;
private readonly TimeProvider _timeProvider;
private readonly ILogger<PolicySnapshotStore> _logger;
private readonly SemaphoreSlim _mutex = new(1, 1);
public PolicySnapshotStore(
IPolicySnapshotRepository snapshotRepository,
IPolicyAuditRepository auditRepository,
TimeProvider? timeProvider,
ILogger<PolicySnapshotStore> logger)
{
_snapshotRepository = snapshotRepository ?? throw new ArgumentNullException(nameof(snapshotRepository));
_auditRepository = auditRepository ?? throw new ArgumentNullException(nameof(auditRepository));
_timeProvider = timeProvider ?? TimeProvider.System;
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
public async Task<PolicySnapshotSaveResult> SaveAsync(PolicySnapshotContent content, CancellationToken cancellationToken = default)
{
if (content is null)
{
throw new ArgumentNullException(nameof(content));
}
var bindingResult = PolicyBinder.Bind(content.Content, content.Format);
if (!bindingResult.Success)
{
_logger.LogWarning("Policy snapshot rejected due to validation errors (Format: {Format})", content.Format);
return new PolicySnapshotSaveResult(false, false, string.Empty, null, bindingResult);
}
var digest = PolicyDigest.Compute(bindingResult.Document);
await _mutex.WaitAsync(cancellationToken).ConfigureAwait(false);
try
{
var latest = await _snapshotRepository.GetLatestAsync(cancellationToken).ConfigureAwait(false);
if (latest is not null && string.Equals(latest.Digest, digest, StringComparison.Ordinal))
{
_logger.LogInformation("Policy snapshot unchanged; digest {Digest} matches revision {RevisionId}", digest, latest.RevisionId);
return new PolicySnapshotSaveResult(true, false, digest, latest, bindingResult);
}
var revisionNumber = (latest?.RevisionNumber ?? 0) + 1;
var revisionId = $"rev-{revisionNumber}";
var createdAt = _timeProvider.GetUtcNow();
var scoringConfig = PolicyScoringConfig.Default;
var snapshot = new PolicySnapshot(
revisionNumber,
revisionId,
digest,
createdAt,
content.Actor,
content.Format,
bindingResult.Document,
bindingResult.Issues,
scoringConfig);
await _snapshotRepository.AddAsync(snapshot, cancellationToken).ConfigureAwait(false);
var auditMessage = content.Description ?? "Policy snapshot created";
var auditEntry = new PolicyAuditEntry(
Guid.NewGuid(),
createdAt,
"snapshot.created",
revisionId,
digest,
content.Actor,
auditMessage);
await _auditRepository.AddAsync(auditEntry, cancellationToken).ConfigureAwait(false);
_logger.LogInformation(
"Policy snapshot saved. Revision {RevisionId}, digest {Digest}, issues {IssueCount}",
revisionId,
digest,
bindingResult.Issues.Length);
return new PolicySnapshotSaveResult(true, true, digest, snapshot, bindingResult);
}
finally
{
_mutex.Release();
}
}
public Task<PolicySnapshot?> GetLatestAsync(CancellationToken cancellationToken = default)
=> _snapshotRepository.GetLatestAsync(cancellationToken);
}
using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
namespace StellaOps.Policy;
public sealed class PolicySnapshotStore
{
private readonly IPolicySnapshotRepository _snapshotRepository;
private readonly IPolicyAuditRepository _auditRepository;
private readonly TimeProvider _timeProvider;
private readonly ILogger<PolicySnapshotStore> _logger;
private readonly SemaphoreSlim _mutex = new(1, 1);
public PolicySnapshotStore(
IPolicySnapshotRepository snapshotRepository,
IPolicyAuditRepository auditRepository,
TimeProvider? timeProvider,
ILogger<PolicySnapshotStore> logger)
{
_snapshotRepository = snapshotRepository ?? throw new ArgumentNullException(nameof(snapshotRepository));
_auditRepository = auditRepository ?? throw new ArgumentNullException(nameof(auditRepository));
_timeProvider = timeProvider ?? TimeProvider.System;
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
public async Task<PolicySnapshotSaveResult> SaveAsync(PolicySnapshotContent content, CancellationToken cancellationToken = default)
{
if (content is null)
{
throw new ArgumentNullException(nameof(content));
}
var bindingResult = PolicyBinder.Bind(content.Content, content.Format);
if (!bindingResult.Success)
{
_logger.LogWarning("Policy snapshot rejected due to validation errors (Format: {Format})", content.Format);
return new PolicySnapshotSaveResult(false, false, string.Empty, null, bindingResult);
}
var digest = PolicyDigest.Compute(bindingResult.Document);
await _mutex.WaitAsync(cancellationToken).ConfigureAwait(false);
try
{
var latest = await _snapshotRepository.GetLatestAsync(cancellationToken).ConfigureAwait(false);
if (latest is not null && string.Equals(latest.Digest, digest, StringComparison.Ordinal))
{
_logger.LogInformation("Policy snapshot unchanged; digest {Digest} matches revision {RevisionId}", digest, latest.RevisionId);
return new PolicySnapshotSaveResult(true, false, digest, latest, bindingResult);
}
var revisionNumber = (latest?.RevisionNumber ?? 0) + 1;
var revisionId = $"rev-{revisionNumber}";
var createdAt = _timeProvider.GetUtcNow();
var scoringConfig = PolicyScoringConfig.Default;
var snapshot = new PolicySnapshot(
revisionNumber,
revisionId,
digest,
createdAt,
content.Actor,
content.Format,
bindingResult.Document,
bindingResult.Issues,
scoringConfig);
await _snapshotRepository.AddAsync(snapshot, cancellationToken).ConfigureAwait(false);
var auditMessage = content.Description ?? "Policy snapshot created";
var auditEntry = new PolicyAuditEntry(
Guid.NewGuid(),
createdAt,
"snapshot.created",
revisionId,
digest,
content.Actor,
auditMessage);
await _auditRepository.AddAsync(auditEntry, cancellationToken).ConfigureAwait(false);
_logger.LogInformation(
"Policy snapshot saved. Revision {RevisionId}, digest {Digest}, issues {IssueCount}",
revisionId,
digest,
bindingResult.Issues.Length);
return new PolicySnapshotSaveResult(true, true, digest, snapshot, bindingResult);
}
finally
{
_mutex.Release();
}
}
public Task<PolicySnapshot?> GetLatestAsync(CancellationToken cancellationToken = default)
=> _snapshotRepository.GetLatestAsync(cancellationToken);
}

View File

@@ -1,37 +1,37 @@
using System;
using System.Collections.Immutable;
namespace StellaOps.Policy;
public sealed record PolicyUnknownConfidenceConfig(
double Initial,
double DecayPerDay,
double Floor,
ImmutableArray<PolicyUnknownConfidenceBand> Bands)
{
public double Clamp(double value)
=> Math.Clamp(value, Floor, 1.0);
public PolicyUnknownConfidenceBand ResolveBand(double value)
{
if (Bands.IsDefaultOrEmpty)
{
return PolicyUnknownConfidenceBand.Default;
}
foreach (var band in Bands)
{
if (value >= band.Min)
{
return band;
}
}
return Bands[Bands.Length - 1];
}
}
public sealed record PolicyUnknownConfidenceBand(string Name, double Min, string? Description = null)
{
public static PolicyUnknownConfidenceBand Default { get; } = new("unspecified", 0, null);
}
using System;
using System.Collections.Immutable;
namespace StellaOps.Policy;
public sealed record PolicyUnknownConfidenceConfig(
double Initial,
double DecayPerDay,
double Floor,
ImmutableArray<PolicyUnknownConfidenceBand> Bands)
{
public double Clamp(double value)
=> Math.Clamp(value, Floor, 1.0);
public PolicyUnknownConfidenceBand ResolveBand(double value)
{
if (Bands.IsDefaultOrEmpty)
{
return PolicyUnknownConfidenceBand.Default;
}
foreach (var band in Bands)
{
if (value >= band.Min)
{
return band;
}
}
return Bands[Bands.Length - 1];
}
}
public sealed record PolicyUnknownConfidenceBand(string Name, double Min, string? Description = null)
{
public static PolicyUnknownConfidenceBand Default { get; } = new("unspecified", 0, null);
}

View File

@@ -1,76 +1,76 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.IO;
using System.Linq;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
namespace StellaOps.Policy;
public sealed record PolicyValidationCliOptions
{
public IReadOnlyList<string> Inputs { get; init; } = Array.Empty<string>();
/// <summary>
/// Writes machine-readable JSON instead of human-formatted text.
/// </summary>
public bool OutputJson { get; init; }
/// <summary>
/// When enabled, warnings cause a non-zero exit code.
/// </summary>
public bool Strict { get; init; }
}
public sealed record PolicyValidationFileResult(
string Path,
PolicyBindingResult BindingResult,
PolicyDiagnosticsReport Diagnostics);
public sealed class PolicyValidationCli
{
private readonly TextWriter _output;
private readonly TextWriter _error;
public PolicyValidationCli(TextWriter? output = null, TextWriter? error = null)
{
_output = output ?? Console.Out;
_error = error ?? Console.Error;
}
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.IO;
using System.Linq;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
namespace StellaOps.Policy;
public sealed record PolicyValidationCliOptions
{
public IReadOnlyList<string> Inputs { get; init; } = Array.Empty<string>();
/// <summary>
/// Writes machine-readable JSON instead of human-formatted text.
/// </summary>
public bool OutputJson { get; init; }
/// <summary>
/// When enabled, warnings cause a non-zero exit code.
/// </summary>
public bool Strict { get; init; }
}
public sealed record PolicyValidationFileResult(
string Path,
PolicyBindingResult BindingResult,
PolicyDiagnosticsReport Diagnostics);
public sealed class PolicyValidationCli
{
private readonly TextWriter _output;
private readonly TextWriter _error;
public PolicyValidationCli(TextWriter? output = null, TextWriter? error = null)
{
_output = output ?? Console.Out;
_error = error ?? Console.Error;
}
public async Task<int> RunAsync(PolicyValidationCliOptions options, CancellationToken cancellationToken = default)
{
if (options is null)
{
throw new ArgumentNullException(nameof(options));
}
if (options.Inputs.Count == 0)
{
await _error.WriteLineAsync("No input files provided. Supply one or more policy file paths.");
return 64; // EX_USAGE
}
var results = new List<PolicyValidationFileResult>();
foreach (var input in options.Inputs)
{
cancellationToken.ThrowIfCancellationRequested();
var resolvedPaths = ResolveInput(input);
if (resolvedPaths.Count == 0)
{
await _error.WriteLineAsync($"No files matched '{input}'.");
continue;
}
foreach (var path in resolvedPaths)
{
cancellationToken.ThrowIfCancellationRequested();
var format = PolicySchema.DetectFormat(path);
var content = await File.ReadAllTextAsync(path, cancellationToken);
if (options is null)
{
throw new ArgumentNullException(nameof(options));
}
if (options.Inputs.Count == 0)
{
await _error.WriteLineAsync("No input files provided. Supply one or more policy file paths.");
return 64; // EX_USAGE
}
var results = new List<PolicyValidationFileResult>();
foreach (var input in options.Inputs)
{
cancellationToken.ThrowIfCancellationRequested();
var resolvedPaths = ResolveInput(input);
if (resolvedPaths.Count == 0)
{
await _error.WriteLineAsync($"No files matched '{input}'.");
continue;
}
foreach (var path in resolvedPaths)
{
cancellationToken.ThrowIfCancellationRequested();
var format = PolicySchema.DetectFormat(path);
var content = await File.ReadAllTextAsync(path, cancellationToken);
var bindingResult = PolicyBinder.Bind(content, format);
var diagnostics = PolicyDiagnostics.Create(bindingResult);
@@ -83,170 +83,170 @@ public sealed class PolicyValidationCli
Recommendations = diagnostics.Recommendations.Add($"canonical.spl.digest:{splHash}"),
};
}
results.Add(new PolicyValidationFileResult(path, bindingResult, diagnostics));
}
}
if (results.Count == 0)
{
await _error.WriteLineAsync("No files were processed.");
return 65; // EX_DATAERR
}
if (options.OutputJson)
{
WriteJson(results);
}
else
{
await WriteTextAsync(results, cancellationToken);
}
var hasErrors = results.Any(static result => !result.BindingResult.Success);
var hasWarnings = results.Any(static result => result.BindingResult.Issues.Any(static issue => issue.Severity == PolicyIssueSeverity.Warning));
if (hasErrors)
{
return 1;
}
if (options.Strict && hasWarnings)
{
return 2;
}
return 0;
}
private async Task WriteTextAsync(IReadOnlyList<PolicyValidationFileResult> results, CancellationToken cancellationToken)
{
foreach (var result in results)
{
cancellationToken.ThrowIfCancellationRequested();
var relativePath = MakeRelative(result.Path);
await _output.WriteLineAsync($"{relativePath} [{result.BindingResult.Format}]");
if (result.BindingResult.Issues.Length == 0)
{
await _output.WriteLineAsync(" OK");
continue;
}
foreach (var issue in result.BindingResult.Issues)
{
var severity = issue.Severity.ToString().ToUpperInvariant().PadRight(7);
await _output.WriteLineAsync($" {severity} {issue.Path} :: {issue.Message} ({issue.Code})");
}
}
}
private void WriteJson(IReadOnlyList<PolicyValidationFileResult> results)
{
var payload = results.Select(static result => new
{
path = result.Path,
format = result.BindingResult.Format.ToString().ToLowerInvariant(),
success = result.BindingResult.Success,
issues = result.BindingResult.Issues.Select(static issue => new
{
code = issue.Code,
message = issue.Message,
severity = issue.Severity.ToString().ToLowerInvariant(),
path = issue.Path,
}),
diagnostics = new
{
version = result.Diagnostics.Version,
ruleCount = result.Diagnostics.RuleCount,
errorCount = result.Diagnostics.ErrorCount,
warningCount = result.Diagnostics.WarningCount,
generatedAt = result.Diagnostics.GeneratedAt,
recommendations = result.Diagnostics.Recommendations,
},
})
.ToArray();
var json = JsonSerializer.Serialize(payload, new JsonSerializerOptions
{
WriteIndented = true,
});
_output.WriteLine(json);
}
private static IReadOnlyList<string> ResolveInput(string input)
{
if (string.IsNullOrWhiteSpace(input))
{
return Array.Empty<string>();
}
var expanded = Environment.ExpandEnvironmentVariables(input.Trim());
if (File.Exists(expanded))
{
return new[] { Path.GetFullPath(expanded) };
}
if (Directory.Exists(expanded))
{
return Directory.EnumerateFiles(expanded, "*.*", SearchOption.TopDirectoryOnly)
.Where(static path => MatchesPolicyExtension(path))
.OrderBy(static path => path, StringComparer.OrdinalIgnoreCase)
.Select(Path.GetFullPath)
.ToArray();
}
var directory = Path.GetDirectoryName(expanded);
var searchPattern = Path.GetFileName(expanded);
if (string.IsNullOrEmpty(searchPattern))
{
return Array.Empty<string>();
}
if (string.IsNullOrEmpty(directory))
{
directory = ".";
}
if (!Directory.Exists(directory))
{
return Array.Empty<string>();
}
return Directory.EnumerateFiles(directory, searchPattern, SearchOption.TopDirectoryOnly)
.Where(static path => MatchesPolicyExtension(path))
.OrderBy(static path => path, StringComparer.OrdinalIgnoreCase)
.Select(Path.GetFullPath)
.ToArray();
}
private static bool MatchesPolicyExtension(string path)
{
var extension = Path.GetExtension(path);
results.Add(new PolicyValidationFileResult(path, bindingResult, diagnostics));
}
}
if (results.Count == 0)
{
await _error.WriteLineAsync("No files were processed.");
return 65; // EX_DATAERR
}
if (options.OutputJson)
{
WriteJson(results);
}
else
{
await WriteTextAsync(results, cancellationToken);
}
var hasErrors = results.Any(static result => !result.BindingResult.Success);
var hasWarnings = results.Any(static result => result.BindingResult.Issues.Any(static issue => issue.Severity == PolicyIssueSeverity.Warning));
if (hasErrors)
{
return 1;
}
if (options.Strict && hasWarnings)
{
return 2;
}
return 0;
}
private async Task WriteTextAsync(IReadOnlyList<PolicyValidationFileResult> results, CancellationToken cancellationToken)
{
foreach (var result in results)
{
cancellationToken.ThrowIfCancellationRequested();
var relativePath = MakeRelative(result.Path);
await _output.WriteLineAsync($"{relativePath} [{result.BindingResult.Format}]");
if (result.BindingResult.Issues.Length == 0)
{
await _output.WriteLineAsync(" OK");
continue;
}
foreach (var issue in result.BindingResult.Issues)
{
var severity = issue.Severity.ToString().ToUpperInvariant().PadRight(7);
await _output.WriteLineAsync($" {severity} {issue.Path} :: {issue.Message} ({issue.Code})");
}
}
}
private void WriteJson(IReadOnlyList<PolicyValidationFileResult> results)
{
var payload = results.Select(static result => new
{
path = result.Path,
format = result.BindingResult.Format.ToString().ToLowerInvariant(),
success = result.BindingResult.Success,
issues = result.BindingResult.Issues.Select(static issue => new
{
code = issue.Code,
message = issue.Message,
severity = issue.Severity.ToString().ToLowerInvariant(),
path = issue.Path,
}),
diagnostics = new
{
version = result.Diagnostics.Version,
ruleCount = result.Diagnostics.RuleCount,
errorCount = result.Diagnostics.ErrorCount,
warningCount = result.Diagnostics.WarningCount,
generatedAt = result.Diagnostics.GeneratedAt,
recommendations = result.Diagnostics.Recommendations,
},
})
.ToArray();
var json = JsonSerializer.Serialize(payload, new JsonSerializerOptions
{
WriteIndented = true,
});
_output.WriteLine(json);
}
private static IReadOnlyList<string> ResolveInput(string input)
{
if (string.IsNullOrWhiteSpace(input))
{
return Array.Empty<string>();
}
var expanded = Environment.ExpandEnvironmentVariables(input.Trim());
if (File.Exists(expanded))
{
return new[] { Path.GetFullPath(expanded) };
}
if (Directory.Exists(expanded))
{
return Directory.EnumerateFiles(expanded, "*.*", SearchOption.TopDirectoryOnly)
.Where(static path => MatchesPolicyExtension(path))
.OrderBy(static path => path, StringComparer.OrdinalIgnoreCase)
.Select(Path.GetFullPath)
.ToArray();
}
var directory = Path.GetDirectoryName(expanded);
var searchPattern = Path.GetFileName(expanded);
if (string.IsNullOrEmpty(searchPattern))
{
return Array.Empty<string>();
}
if (string.IsNullOrEmpty(directory))
{
directory = ".";
}
if (!Directory.Exists(directory))
{
return Array.Empty<string>();
}
return Directory.EnumerateFiles(directory, searchPattern, SearchOption.TopDirectoryOnly)
.Where(static path => MatchesPolicyExtension(path))
.OrderBy(static path => path, StringComparer.OrdinalIgnoreCase)
.Select(Path.GetFullPath)
.ToArray();
}
private static bool MatchesPolicyExtension(string path)
{
var extension = Path.GetExtension(path);
return extension.Equals(".yaml", StringComparison.OrdinalIgnoreCase)
|| extension.Equals(".yml", StringComparison.OrdinalIgnoreCase)
|| extension.Equals(".json", StringComparison.OrdinalIgnoreCase)
|| extension.Equals(".stella", StringComparison.OrdinalIgnoreCase);
}
private static string MakeRelative(string path)
{
try
{
var fullPath = Path.GetFullPath(path);
var current = Directory.GetCurrentDirectory();
if (fullPath.StartsWith(current, StringComparison.OrdinalIgnoreCase))
{
return fullPath[current.Length..].TrimStart(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
}
return fullPath;
}
catch
{
return path;
}
}
}
}
private static string MakeRelative(string path)
{
try
{
var fullPath = Path.GetFullPath(path);
var current = Directory.GetCurrentDirectory();
if (fullPath.StartsWith(current, StringComparison.OrdinalIgnoreCase))
{
return fullPath[current.Length..].TrimStart(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
}
return fullPath;
}
catch
{
return path;
}
}
}

View File

@@ -1,112 +1,112 @@
using System;
using System.Collections.Immutable;
namespace StellaOps.Policy;
public enum PolicyVerdictStatus
{
Pass,
Blocked,
Ignored,
Warned,
Deferred,
Escalated,
RequiresVex,
}
public sealed record PolicyVerdict(
string FindingId,
PolicyVerdictStatus Status,
string? RuleName = null,
string? RuleAction = null,
string? Notes = null,
double Score = 0,
string ConfigVersion = "1.0",
ImmutableDictionary<string, double>? Inputs = null,
string? QuietedBy = null,
bool Quiet = false,
double? UnknownConfidence = null,
string? ConfidenceBand = null,
double? UnknownAgeDays = null,
string? SourceTrust = null,
string? Reachability = null)
{
public static PolicyVerdict CreateBaseline(string findingId, PolicyScoringConfig scoringConfig)
{
var inputs = ImmutableDictionary<string, double>.Empty;
return new PolicyVerdict(
findingId,
PolicyVerdictStatus.Pass,
RuleName: null,
RuleAction: null,
Notes: null,
Score: 0,
ConfigVersion: scoringConfig.Version,
Inputs: inputs,
QuietedBy: null,
Quiet: false,
UnknownConfidence: null,
ConfidenceBand: null,
UnknownAgeDays: null,
SourceTrust: null,
Reachability: null);
}
public ImmutableDictionary<string, double> GetInputs()
=> Inputs ?? ImmutableDictionary<string, double>.Empty;
}
public sealed record PolicyVerdictDiff(
PolicyVerdict Baseline,
PolicyVerdict Projected)
{
public bool Changed
{
get
{
if (Baseline.Status != Projected.Status)
{
return true;
}
if (!string.Equals(Baseline.RuleName, Projected.RuleName, StringComparison.Ordinal))
{
return true;
}
if (Math.Abs(Baseline.Score - Projected.Score) > 0.0001)
{
return true;
}
if (!string.Equals(Baseline.QuietedBy, Projected.QuietedBy, StringComparison.Ordinal))
{
return true;
}
var baselineConfidence = Baseline.UnknownConfidence ?? 0;
var projectedConfidence = Projected.UnknownConfidence ?? 0;
if (Math.Abs(baselineConfidence - projectedConfidence) > 0.0001)
{
return true;
}
if (!string.Equals(Baseline.ConfidenceBand, Projected.ConfidenceBand, StringComparison.Ordinal))
{
return true;
}
if (!string.Equals(Baseline.SourceTrust, Projected.SourceTrust, StringComparison.Ordinal))
{
return true;
}
if (!string.Equals(Baseline.Reachability, Projected.Reachability, StringComparison.Ordinal))
{
return true;
}
return false;
}
}
}
using System;
using System.Collections.Immutable;
namespace StellaOps.Policy;
public enum PolicyVerdictStatus
{
Pass,
Blocked,
Ignored,
Warned,
Deferred,
Escalated,
RequiresVex,
}
public sealed record PolicyVerdict(
string FindingId,
PolicyVerdictStatus Status,
string? RuleName = null,
string? RuleAction = null,
string? Notes = null,
double Score = 0,
string ConfigVersion = "1.0",
ImmutableDictionary<string, double>? Inputs = null,
string? QuietedBy = null,
bool Quiet = false,
double? UnknownConfidence = null,
string? ConfidenceBand = null,
double? UnknownAgeDays = null,
string? SourceTrust = null,
string? Reachability = null)
{
public static PolicyVerdict CreateBaseline(string findingId, PolicyScoringConfig scoringConfig)
{
var inputs = ImmutableDictionary<string, double>.Empty;
return new PolicyVerdict(
findingId,
PolicyVerdictStatus.Pass,
RuleName: null,
RuleAction: null,
Notes: null,
Score: 0,
ConfigVersion: scoringConfig.Version,
Inputs: inputs,
QuietedBy: null,
Quiet: false,
UnknownConfidence: null,
ConfidenceBand: null,
UnknownAgeDays: null,
SourceTrust: null,
Reachability: null);
}
public ImmutableDictionary<string, double> GetInputs()
=> Inputs ?? ImmutableDictionary<string, double>.Empty;
}
public sealed record PolicyVerdictDiff(
PolicyVerdict Baseline,
PolicyVerdict Projected)
{
public bool Changed
{
get
{
if (Baseline.Status != Projected.Status)
{
return true;
}
if (!string.Equals(Baseline.RuleName, Projected.RuleName, StringComparison.Ordinal))
{
return true;
}
if (Math.Abs(Baseline.Score - Projected.Score) > 0.0001)
{
return true;
}
if (!string.Equals(Baseline.QuietedBy, Projected.QuietedBy, StringComparison.Ordinal))
{
return true;
}
var baselineConfidence = Baseline.UnknownConfidence ?? 0;
var projectedConfidence = Projected.UnknownConfidence ?? 0;
if (Math.Abs(baselineConfidence - projectedConfidence) > 0.0001)
{
return true;
}
if (!string.Equals(Baseline.ConfidenceBand, Projected.ConfidenceBand, StringComparison.Ordinal))
{
return true;
}
if (!string.Equals(Baseline.SourceTrust, Projected.SourceTrust, StringComparison.Ordinal))
{
return true;
}
if (!string.Equals(Baseline.Reachability, Projected.Reachability, StringComparison.Ordinal))
{
return true;
}
return false;
}
}
}

View File

@@ -1,14 +1,14 @@
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
namespace StellaOps.Policy;
public interface IPolicySnapshotRepository
{
Task<PolicySnapshot?> GetLatestAsync(CancellationToken cancellationToken = default);
Task<IReadOnlyList<PolicySnapshot>> ListAsync(int limit, CancellationToken cancellationToken = default);
Task AddAsync(PolicySnapshot snapshot, CancellationToken cancellationToken = default);
}
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
namespace StellaOps.Policy;
public interface IPolicySnapshotRepository
{
Task<PolicySnapshot?> GetLatestAsync(CancellationToken cancellationToken = default);
Task<IReadOnlyList<PolicySnapshot>> ListAsync(int limit, CancellationToken cancellationToken = default);
Task AddAsync(PolicySnapshot snapshot, CancellationToken cancellationToken = default);
}

View File

@@ -1,65 +1,65 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace StellaOps.Policy;
public sealed class InMemoryPolicySnapshotRepository : IPolicySnapshotRepository
{
private readonly List<PolicySnapshot> _snapshots = new();
private readonly SemaphoreSlim _mutex = new(1, 1);
public async Task AddAsync(PolicySnapshot snapshot, CancellationToken cancellationToken = default)
{
if (snapshot is null)
{
throw new ArgumentNullException(nameof(snapshot));
}
await _mutex.WaitAsync(cancellationToken).ConfigureAwait(false);
try
{
_snapshots.Add(snapshot);
_snapshots.Sort(static (left, right) => left.RevisionNumber.CompareTo(right.RevisionNumber));
}
finally
{
_mutex.Release();
}
}
public async Task<PolicySnapshot?> GetLatestAsync(CancellationToken cancellationToken = default)
{
await _mutex.WaitAsync(cancellationToken).ConfigureAwait(false);
try
{
return _snapshots.Count == 0 ? null : _snapshots[^1];
}
finally
{
_mutex.Release();
}
}
public async Task<IReadOnlyList<PolicySnapshot>> ListAsync(int limit, CancellationToken cancellationToken = default)
{
await _mutex.WaitAsync(cancellationToken).ConfigureAwait(false);
try
{
IEnumerable<PolicySnapshot> query = _snapshots;
if (limit > 0)
{
query = query.TakeLast(limit);
}
return query.ToImmutableArray();
}
finally
{
_mutex.Release();
}
}
}
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace StellaOps.Policy;
public sealed class InMemoryPolicySnapshotRepository : IPolicySnapshotRepository
{
private readonly List<PolicySnapshot> _snapshots = new();
private readonly SemaphoreSlim _mutex = new(1, 1);
public async Task AddAsync(PolicySnapshot snapshot, CancellationToken cancellationToken = default)
{
if (snapshot is null)
{
throw new ArgumentNullException(nameof(snapshot));
}
await _mutex.WaitAsync(cancellationToken).ConfigureAwait(false);
try
{
_snapshots.Add(snapshot);
_snapshots.Sort(static (left, right) => left.RevisionNumber.CompareTo(right.RevisionNumber));
}
finally
{
_mutex.Release();
}
}
public async Task<PolicySnapshot?> GetLatestAsync(CancellationToken cancellationToken = default)
{
await _mutex.WaitAsync(cancellationToken).ConfigureAwait(false);
try
{
return _snapshots.Count == 0 ? null : _snapshots[^1];
}
finally
{
_mutex.Release();
}
}
public async Task<IReadOnlyList<PolicySnapshot>> ListAsync(int limit, CancellationToken cancellationToken = default)
{
await _mutex.WaitAsync(cancellationToken).ConfigureAwait(false);
try
{
IEnumerable<PolicySnapshot> query = _snapshots;
if (limit > 0)
{
query = query.TakeLast(limit);
}
return query.ToImmutableArray();
}
finally
{
_mutex.Release();
}
}
}