up
Some checks failed
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
Docs CI / lint-and-preview (push) Has been cancelled
Export Center CI / export-ci (push) Has been cancelled
devportal-offline / build-offline (push) Has been cancelled

This commit is contained in:
StellaOps Bot
2025-12-03 00:10:19 +02:00
parent ea1d58a89b
commit 37cba83708
158 changed files with 147438 additions and 867 deletions

View File

@@ -0,0 +1,237 @@
using System.Collections.Immutable;
using System.Security.Cryptography;
using System.Text;
using StellaOps.Policy.Engine.Simulation;
namespace StellaOps.Policy.Engine.ConsoleSurface;
/// <summary>
/// Deterministic simulation diff metadata for Console surfaces (POLICY-CONSOLE-23-002).
/// Generates stable before/after counts, rule impact, and samples without relying on
/// wall-clock or external data. Intended as a contract-aligned shim until the
/// Console surface is wired to live evaluation outputs.
/// </summary>
internal sealed class ConsoleSimulationDiffService
{
private static readonly string[] SeverityOrder = { "critical", "high", "medium", "low", "unknown" };
private static readonly string[] Outcomes = { "deny", "block", "warn", "allow" };
private static readonly string SchemaVersion = "console-policy-23-001";
private readonly SimulationAnalyticsService _analytics;
public ConsoleSimulationDiffService(SimulationAnalyticsService analytics)
{
_analytics = analytics ?? throw new ArgumentNullException(nameof(analytics));
}
public ConsoleSimulationDiffResponse Compute(ConsoleSimulationDiffRequest request)
{
ArgumentNullException.ThrowIfNull(request);
var artifacts = (request.ArtifactScope?.Count ?? 0) == 0
? new[] { new ConsoleArtifactScope("sha256:default") }
: request.ArtifactScope!;
// Respect budget caps if provided
var maxFindings = Math.Clamp(request.Budget?.MaxFindings ?? 5, 1, 50_000);
var maxSamples = Math.Clamp(request.Budget?.MaxExplainSamples ?? 20, 0, 200);
var baselineFindings = BuildFindings(request.BaselinePolicyVersion, artifacts, maxFindings, seed: 1);
var candidateFindings = BuildFindings(request.CandidatePolicyVersion, artifacts, maxFindings, seed: 2);
// Delta summary for regressions/added/removed
var delta = _analytics.ComputeDeltaSummary(
request.BaselinePolicyVersion,
request.CandidatePolicyVersion,
baselineFindings,
candidateFindings);
var beforeBreakdown = BuildSeverityBreakdown(baselineFindings);
var afterBreakdown = BuildSeverityBreakdown(candidateFindings);
var added = candidateFindings.Count(c => baselineFindings.All(b => b.FindingId != c.FindingId));
var removed = baselineFindings.Count(b => candidateFindings.All(c => c.FindingId != b.FindingId));
var regressed = delta.SeverityChanges.Escalated;
var ruleImpact = BuildRuleImpact(baselineFindings, candidateFindings);
var samples = BuildSamples(candidateFindings, maxSamples);
var response = new ConsoleSimulationDiffResponse(
SchemaVersion,
new ConsoleDiffSummary(
Before: beforeBreakdown,
After: afterBreakdown,
Delta: new ConsoleDiffDelta(added, removed, regressed)),
ruleImpact,
samples,
new ConsoleDiffProvenance(
request.BaselinePolicyVersion,
request.CandidatePolicyVersion,
request.EvaluationTimestamp));
return response;
}
private static IReadOnlyList<SimulationFindingResult> BuildFindings(
string policyVersion,
IReadOnlyList<ConsoleArtifactScope> artifacts,
int maxFindings,
int seed)
{
var results = new List<SimulationFindingResult>();
foreach (var artifact in artifacts.OrderBy(a => a.ArtifactDigest, StringComparer.Ordinal))
{
var baseSeed = HashToBytes($"{policyVersion}:{artifact.ArtifactDigest}:{seed}");
var include = baseSeed[0] % 7 != 0; // occasionally drop to simulate removal
if (!include)
{
continue;
}
var findingId = CreateDeterministicId("fid", policyVersion, artifact.ArtifactDigest, seed.ToString());
var severity = SeverityOrder[baseSeed[1] % SeverityOrder.Length];
var outcome = Outcomes[baseSeed[2] % Outcomes.Length];
var ruleId = $"RULE-{(baseSeed[3] % 9000) + 1000}";
results.Add(new SimulationFindingResult(
FindingId: findingId,
ComponentPurl: artifact.Purl ?? "pkg:generic/unknown@0.0.0",
AdvisoryId: artifact.AdvisoryId ?? "unknown",
Outcome: outcome,
Severity: severity,
FiredRules: new[] { ruleId }));
// Add a secondary finding for variability if budget allows
if (results.Count < maxFindings && baseSeed[4] % 5 == 0)
{
var secondaryId = CreateDeterministicId("fid", policyVersion, artifact.ArtifactDigest, seed + "-b");
var secondaryRule = $"RULE-{(baseSeed[5] % 9000) + 1000}";
var secondarySeverity = SeverityOrder[(baseSeed[6] + seed) % SeverityOrder.Length];
results.Add(new SimulationFindingResult(
FindingId: secondaryId,
ComponentPurl: artifact.Purl ?? "pkg:generic/unknown@0.0.0",
AdvisoryId: artifact.AdvisoryId ?? "unknown",
Outcome: Outcomes[(baseSeed[7] + seed) % Outcomes.Length],
Severity: secondarySeverity,
FiredRules: new[] { secondaryRule }));
}
if (results.Count >= maxFindings)
{
break;
}
}
return results;
}
private static ConsoleSeverityBreakdown BuildSeverityBreakdown(IReadOnlyList<SimulationFindingResult> findings)
{
var counts = SeverityOrder.ToDictionary(s => s, _ => 0, StringComparer.OrdinalIgnoreCase);
foreach (var finding in findings)
{
var severity = finding.Severity ?? "unknown";
counts.TryGetValue(severity, out var current);
counts[severity] = current + 1;
}
return new ConsoleSeverityBreakdown(
Total: findings.Count,
Severity: counts.ToImmutableDictionary(StringComparer.OrdinalIgnoreCase));
}
private static IReadOnlyList<ConsoleRuleImpact> BuildRuleImpact(
IReadOnlyList<SimulationFindingResult> baseline,
IReadOnlyList<SimulationFindingResult> candidate)
{
var ruleImpact = new Dictionary<string, (int added, int removed, Dictionary<string, int> shifts)>(StringComparer.Ordinal);
var baseMap = baseline.ToDictionary(f => f.FindingId, f => f, StringComparer.Ordinal);
foreach (var result in candidate)
{
var ruleId = result.FiredRules?.FirstOrDefault() ?? "RULE-0000";
if (!ruleImpact.TryGetValue(ruleId, out var entry))
{
entry = (0, 0, new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase));
}
if (!baseMap.TryGetValue(result.FindingId, out var baseResult))
{
entry.added += 1;
}
else if (!string.Equals(baseResult.Severity, result.Severity, StringComparison.OrdinalIgnoreCase))
{
var key = $"{baseResult.Severity}->{result.Severity}";
entry.shifts.TryGetValue(key, out var count);
entry.shifts[key] = count + 1;
}
ruleImpact[ruleId] = entry;
}
foreach (var result in baseline)
{
var ruleId = result.FiredRules?.FirstOrDefault() ?? "RULE-0000";
if (!candidate.Any(c => c.FindingId == result.FindingId))
{
if (!ruleImpact.TryGetValue(ruleId, out var entry))
{
entry = (0, 0, new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase));
}
entry.removed += 1;
ruleImpact[ruleId] = entry;
}
}
return ruleImpact
.OrderBy(kvp => kvp.Key, StringComparer.Ordinal)
.Select(kvp => new ConsoleRuleImpact(
kvp.Key,
kvp.Value.added,
kvp.Value.removed,
kvp.Value.shifts.ToImmutableDictionary(StringComparer.OrdinalIgnoreCase)))
.ToList();
}
private static ConsoleDiffSamples BuildSamples(IReadOnlyList<SimulationFindingResult> candidate, int maxSamples)
{
var ordered = candidate
.OrderBy(f => f.FindingId, StringComparer.Ordinal)
.Take(maxSamples)
.ToList();
var explain = ordered
.Select(f => CreateDeterministicId("trace", f.FindingId))
.ToImmutableArray();
var findings = ordered
.Select(f => f.FindingId)
.ToImmutableArray();
return new ConsoleDiffSamples(explain, findings);
}
private static string CreateDeterministicId(params string[] parts)
{
var input = string.Join("|", parts);
var hash = HashToBytes(input);
var sb = new StringBuilder("ulid-");
for (var i = 0; i < 8; i++)
{
sb.Append(hash[i].ToString("x2"));
}
return sb.ToString();
}
private static byte[] HashToBytes(string input)
{
var bytes = Encoding.UTF8.GetBytes(input);
return SHA256.HashData(bytes);
}
}