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

@@ -26,7 +26,10 @@ internal sealed record BatchEvaluationItemDto(
SbomDto Sbom,
ExceptionsDto Exceptions,
ReachabilityDto Reachability,
DateTimeOffset? EvaluationTimestamp,
string? EntropyLayerSummary = null,
string? EntropyReport = null,
bool? ProvenanceAttested = null,
DateTimeOffset? EvaluationTimestamp = null,
bool BypassCache = false);
internal sealed record EvaluationSeverityDto(string Normalized, decimal? Score = null);
@@ -207,6 +210,9 @@ internal static class BatchEvaluationMapper
Sbom: sbom,
Exceptions: exceptions,
Reachability: reachability,
EntropyLayerSummary: item.EntropyLayerSummary,
EntropyReport: item.EntropyReport,
ProvenanceAttested: item.ProvenanceAttested,
EvaluationTimestamp: item.EvaluationTimestamp,
BypassCache: item.BypassCache);
}

View File

@@ -0,0 +1,61 @@
using System.Collections.Immutable;
using System.Text.Json.Serialization;
namespace StellaOps.Policy.Engine.ConsoleSurface;
internal sealed record ConsoleSimulationDiffRequest(
[property: JsonPropertyName("baselinePolicyVersion")] string BaselinePolicyVersion,
[property: JsonPropertyName("candidatePolicyVersion")] string CandidatePolicyVersion,
[property: JsonPropertyName("artifactScope")] IReadOnlyList<ConsoleArtifactScope>? ArtifactScope,
[property: JsonPropertyName("filters")] ConsoleSimulationFilters? Filters,
[property: JsonPropertyName("budget")] ConsoleSimulationBudget? Budget,
[property: JsonPropertyName("evaluationTimestamp")] DateTimeOffset EvaluationTimestamp);
internal sealed record ConsoleArtifactScope(
[property: JsonPropertyName("artifactDigest")] string ArtifactDigest,
[property: JsonPropertyName("purl")] string? Purl = null,
[property: JsonPropertyName("advisoryId")] string? AdvisoryId = null);
internal sealed record ConsoleSimulationFilters(
[property: JsonPropertyName("severityBand")] IReadOnlyList<string>? SeverityBand = null,
[property: JsonPropertyName("ruleId")] IReadOnlyList<string>? RuleId = null);
internal sealed record ConsoleSimulationBudget(
[property: JsonPropertyName("maxFindings")] int? MaxFindings = null,
[property: JsonPropertyName("maxExplainSamples")] int? MaxExplainSamples = null);
internal sealed record ConsoleSimulationDiffResponse(
[property: JsonPropertyName("schemaVersion")] string SchemaVersion,
[property: JsonPropertyName("summary")] ConsoleDiffSummary Summary,
[property: JsonPropertyName("ruleImpact")] IReadOnlyList<ConsoleRuleImpact> RuleImpact,
[property: JsonPropertyName("samples")] ConsoleDiffSamples Samples,
[property: JsonPropertyName("provenance")] ConsoleDiffProvenance Provenance);
internal sealed record ConsoleDiffSummary(
[property: JsonPropertyName("before")] ConsoleSeverityBreakdown Before,
[property: JsonPropertyName("after")] ConsoleSeverityBreakdown After,
[property: JsonPropertyName("delta")] ConsoleDiffDelta Delta);
internal sealed record ConsoleSeverityBreakdown(
[property: JsonPropertyName("total")] int Total,
[property: JsonPropertyName("severity")] ImmutableDictionary<string, int> Severity);
internal sealed record ConsoleDiffDelta(
[property: JsonPropertyName("added")] int Added,
[property: JsonPropertyName("removed")] int Removed,
[property: JsonPropertyName("regressed")] int Regressed);
internal sealed record ConsoleRuleImpact(
[property: JsonPropertyName("ruleId")] string RuleId,
[property: JsonPropertyName("added")] int Added,
[property: JsonPropertyName("removed")] int Removed,
[property: JsonPropertyName("severityShift")] ImmutableDictionary<string, int> SeverityShift);
internal sealed record ConsoleDiffSamples(
[property: JsonPropertyName("explain")] ImmutableArray<string> Explain,
[property: JsonPropertyName("findings")] ImmutableArray<string> Findings);
internal sealed record ConsoleDiffProvenance(
[property: JsonPropertyName("baselinePolicyVersion")] string BaselinePolicyVersion,
[property: JsonPropertyName("candidatePolicyVersion")] string CandidatePolicyVersion,
[property: JsonPropertyName("evaluationTimestamp")] DateTimeOffset EvaluationTimestamp);

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);
}
}

View File

@@ -0,0 +1,49 @@
using Microsoft.AspNetCore.Mvc;
using StellaOps.Policy.Engine.ConsoleSurface;
namespace StellaOps.Policy.Engine.Endpoints;
internal static class ConsoleSimulationEndpoint
{
public static IEndpointRouteBuilder MapConsoleSimulationDiff(this IEndpointRouteBuilder routes)
{
routes.MapPost("/policy/console/simulations/diff", HandleAsync)
.WithName("PolicyEngine.ConsoleSimulationDiff")
.Produces<ConsoleSimulationDiffResponse>(StatusCodes.Status200OK)
.ProducesValidationProblem();
return routes;
}
private static IResult HandleAsync(
[FromBody] ConsoleSimulationDiffRequest request,
ConsoleSimulationDiffService service)
{
if (request is null)
{
return Results.ValidationProblem(new Dictionary<string, string[]>
{
["request"] = new[] { "Request body is required." }
});
}
if (request.EvaluationTimestamp == default)
{
return Results.ValidationProblem(new Dictionary<string, string[]>
{
["evaluationTimestamp"] = new[] { "evaluationTimestamp is required." }
});
}
if (string.IsNullOrWhiteSpace(request.BaselinePolicyVersion) || string.IsNullOrWhiteSpace(request.CandidatePolicyVersion))
{
return Results.ValidationProblem(new Dictionary<string, string[]>
{
["policyVersion"] = new[] { "baselinePolicyVersion and candidatePolicyVersion are required." }
});
}
var response = service.Compute(request);
return Results.Json(response);
}
}

View File

@@ -11,15 +11,16 @@ internal sealed record PolicyEvaluationRequest(
PolicyIrDocument Document,
PolicyEvaluationContext Context);
internal sealed record PolicyEvaluationContext(
PolicyEvaluationSeverity Severity,
PolicyEvaluationEnvironment Environment,
PolicyEvaluationAdvisory Advisory,
PolicyEvaluationVexEvidence Vex,
PolicyEvaluationSbom Sbom,
PolicyEvaluationExceptions Exceptions,
PolicyEvaluationReachability Reachability,
DateTimeOffset? EvaluationTimestamp = null)
internal sealed record PolicyEvaluationContext(
PolicyEvaluationSeverity Severity,
PolicyEvaluationEnvironment Environment,
PolicyEvaluationAdvisory Advisory,
PolicyEvaluationVexEvidence Vex,
PolicyEvaluationSbom Sbom,
PolicyEvaluationExceptions Exceptions,
PolicyEvaluationReachability Reachability,
PolicyEvaluationEntropy Entropy,
DateTimeOffset? EvaluationTimestamp = null)
{
/// <summary>
/// Gets the evaluation timestamp for deterministic time-based operations.
@@ -36,12 +37,12 @@ internal sealed record PolicyEvaluationContext(
PolicyEvaluationEnvironment environment,
PolicyEvaluationAdvisory advisory,
PolicyEvaluationVexEvidence vex,
PolicyEvaluationSbom sbom,
PolicyEvaluationExceptions exceptions,
DateTimeOffset? evaluationTimestamp = null)
: this(severity, environment, advisory, vex, sbom, exceptions, PolicyEvaluationReachability.Unknown, evaluationTimestamp)
{
}
PolicyEvaluationSbom sbom,
PolicyEvaluationExceptions exceptions,
DateTimeOffset? evaluationTimestamp = null)
: this(severity, environment, advisory, vex, sbom, exceptions, PolicyEvaluationReachability.Unknown, PolicyEvaluationEntropy.Unknown, evaluationTimestamp)
{
}
}
internal sealed record PolicyEvaluationSeverity(string Normalized, decimal? Score = null);
@@ -187,15 +188,15 @@ internal sealed record PolicyExceptionApplication(
/// <summary>
/// Reachability evidence for policy evaluation.
/// </summary>
internal sealed record PolicyEvaluationReachability(
string State,
decimal Confidence,
decimal Score,
bool HasRuntimeEvidence,
string? Source,
string? Method,
string? EvidenceRef)
{
internal sealed record PolicyEvaluationReachability(
string State,
decimal Confidence,
decimal Score,
bool HasRuntimeEvidence,
string? Source,
string? Method,
string? EvidenceRef)
{
/// <summary>
/// Default unknown reachability state.
/// </summary>
@@ -275,4 +276,26 @@ internal sealed record PolicyEvaluationReachability(
/// Whether this reachability data has low confidence (&lt; 0.5).
/// </summary>
public bool IsLowConfidence => Confidence < 0.5m;
}
}
/// <summary>
/// Entropy evidence for policy evaluation.
/// </summary>
internal sealed record PolicyEvaluationEntropy(
decimal Penalty,
decimal ImageOpaqueRatio,
bool Blocked,
bool Warned,
bool Capped,
decimal? TopFileOpaqueRatio)
{
public static PolicyEvaluationEntropy Unknown { get; } = new(
Penalty: 0m,
ImageOpaqueRatio: 0m,
Blocked: false,
Warned: false,
Capped: false,
TopFileOpaqueRatio: null);
public bool HasData => Penalty != 0m || ImageOpaqueRatio != 0m || Warned || Blocked;
}

View File

@@ -63,12 +63,13 @@ internal sealed class PolicyExpressionEvaluator
"vex" => new EvaluationValue(new VexScope(this, context.Vex)),
"advisory" => new EvaluationValue(new AdvisoryScope(context.Advisory)),
"sbom" => new EvaluationValue(new SbomScope(context.Sbom)),
"reachability" => new EvaluationValue(new ReachabilityScope(context.Reachability)),
"now" => new EvaluationValue(context.Now),
"true" => EvaluationValue.True,
"false" => EvaluationValue.False,
_ => EvaluationValue.Null,
};
"reachability" => new EvaluationValue(new ReachabilityScope(context.Reachability)),
"entropy" => new EvaluationValue(new EntropyScope(context.Entropy)),
"now" => new EvaluationValue(context.Now),
"true" => EvaluationValue.True,
"false" => EvaluationValue.False,
_ => EvaluationValue.Null,
};
}
private EvaluationValue EvaluateMember(PolicyMemberAccessExpression member, EvaluationScope scope)
@@ -100,15 +101,20 @@ internal sealed class PolicyExpressionEvaluator
return sbom.Get(member.Member);
}
if (raw is ReachabilityScope reachability)
{
return reachability.Get(member.Member);
}
if (raw is ComponentScope componentScope)
{
return componentScope.Get(member.Member);
}
if (raw is ReachabilityScope reachability)
{
return reachability.Get(member.Member);
}
if (raw is EntropyScope entropy)
{
return entropy.Get(member.Member);
}
if (raw is ComponentScope componentScope)
{
return componentScope.Get(member.Member);
}
if (raw is RubyComponentScope rubyScope)
{
@@ -856,12 +862,12 @@ internal sealed class PolicyExpressionEvaluator
/// - reachability.method == "static"
/// </example>
private sealed class ReachabilityScope
{
private readonly PolicyEvaluationReachability reachability;
public ReachabilityScope(PolicyEvaluationReachability reachability)
{
this.reachability = reachability;
{
private readonly PolicyEvaluationReachability reachability;
public ReachabilityScope(PolicyEvaluationReachability reachability)
{
this.reachability = reachability;
}
public EvaluationValue Get(string member) => member.ToLowerInvariant() switch
@@ -879,10 +885,35 @@ internal sealed class PolicyExpressionEvaluator
"is_under_investigation" or "isunderinvestigation" => new EvaluationValue(reachability.IsUnderInvestigation),
"is_high_confidence" or "ishighconfidence" => new EvaluationValue(reachability.IsHighConfidence),
"is_medium_confidence" or "ismediumconfidence" => new EvaluationValue(reachability.IsMediumConfidence),
"is_low_confidence" or "islowconfidence" => new EvaluationValue(reachability.IsLowConfidence),
_ => EvaluationValue.Null,
};
}
"is_low_confidence" or "islowconfidence" => new EvaluationValue(reachability.IsLowConfidence),
_ => EvaluationValue.Null,
};
}
/// <summary>
/// SPL scope for entropy predicates.
/// </summary>
private sealed class EntropyScope
{
private readonly PolicyEvaluationEntropy entropy;
public EntropyScope(PolicyEvaluationEntropy entropy)
{
this.entropy = entropy;
}
public EvaluationValue Get(string member) => member.ToLowerInvariant() switch
{
"penalty" => new EvaluationValue(entropy.Penalty),
"image_opaque_ratio" or "imageopaqueratio" => new EvaluationValue(entropy.ImageOpaqueRatio),
"blocked" => new EvaluationValue(entropy.Blocked),
"warned" => new EvaluationValue(entropy.Warned),
"capped" => new EvaluationValue(entropy.Capped),
"top_file_opaque_ratio" or "topfileopaqueratio" => new EvaluationValue(entropy.TopFileOpaqueRatio),
"has_data" or "hasdata" => new EvaluationValue(entropy.HasData),
_ => EvaluationValue.Null,
};
}
/// <summary>
/// SPL scope for macOS component predicates.

View File

@@ -16,6 +16,7 @@ using StellaOps.Policy.Engine.Services;
using StellaOps.Policy.Engine.Workers;
using StellaOps.Policy.Engine.Streaming;
using StellaOps.Policy.Engine.Telemetry;
using StellaOps.Policy.Engine.ConsoleSurface;
using StellaOps.AirGap.Policy;
using StellaOps.Policy.Engine.Orchestration;
using StellaOps.Policy.Engine.ReachabilityFacts;
@@ -138,6 +139,8 @@ builder.Services.AddSingleton<ExceptionLifecycleService>();
builder.Services.AddHostedService<ExceptionLifecycleWorker>();
builder.Services.AddHostedService<IncidentModeExpirationWorker>();
builder.Services.AddHostedService<PolicyEngineBootstrapWorker>();
builder.Services.AddSingleton<StellaOps.Policy.Engine.Simulation.SimulationAnalyticsService>();
builder.Services.AddSingleton<ConsoleSimulationDiffService>();
builder.Services.AddSingleton<StellaOps.PolicyDsl.PolicyCompiler>();
builder.Services.AddSingleton<PolicyCompilationService>();
builder.Services.AddSingleton<StellaOps.Policy.Engine.Services.PathScopeMetrics>();
@@ -223,9 +226,10 @@ app.MapPathScopeSimulation();
app.MapOverlaySimulation();
app.MapEvidenceSummaries();
app.MapBatchEvaluation();
app.MapConsoleSimulationDiff();
app.MapTrustWeighting();
app.MapAdvisoryAiKnobs();
app.MapBatchContext();
app.MapBatchContext();
app.MapOrchestratorJobs();
app.MapPolicyWorker();
app.MapLedgerExport();

View File

@@ -1,5 +1,6 @@
using System.Collections.Immutable;
using System.Diagnostics;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
@@ -27,6 +28,9 @@ internal sealed record RuntimeEvaluationRequest(
PolicyEvaluationSbom Sbom,
PolicyEvaluationExceptions Exceptions,
PolicyEvaluationReachability Reachability,
string? EntropyLayerSummary,
string? EntropyReport,
bool? ProvenanceAttested,
DateTimeOffset? EvaluationTimestamp = null,
bool BypassCache = false);
@@ -59,6 +63,7 @@ internal sealed class PolicyRuntimeEvaluationService
private readonly IPolicyEvaluationCache _cache;
private readonly PolicyEvaluator _evaluator;
private readonly ReachabilityFacts.ReachabilityFactsJoiningService? _reachabilityFacts;
private readonly Signals.Entropy.EntropyPenaltyCalculator _entropy;
private readonly TimeProvider _timeProvider;
private readonly ILogger<PolicyRuntimeEvaluationService> _logger;
@@ -73,6 +78,7 @@ internal sealed class PolicyRuntimeEvaluationService
IPolicyEvaluationCache cache,
PolicyEvaluator evaluator,
ReachabilityFacts.ReachabilityFactsJoiningService? reachabilityFacts,
Signals.Entropy.EntropyPenaltyCalculator entropy,
TimeProvider timeProvider,
ILogger<PolicyRuntimeEvaluationService> logger)
{
@@ -80,6 +86,7 @@ internal sealed class PolicyRuntimeEvaluationService
_cache = cache ?? throw new ArgumentNullException(nameof(cache));
_evaluator = evaluator ?? throw new ArgumentNullException(nameof(evaluator));
_reachabilityFacts = reachabilityFacts;
_entropy = entropy ?? throw new ArgumentNullException(nameof(entropy));
_timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
@@ -158,6 +165,8 @@ internal sealed class PolicyRuntimeEvaluationService
$"Compiled policy document not found for pack '{request.PackId}' version {request.Version}.");
}
var entropy = ComputeEntropy(effectiveRequest);
var context = new PolicyEvaluationContext(
effectiveRequest.Severity,
new PolicyEvaluationEnvironment(ImmutableDictionary<string, string>.Empty),
@@ -166,6 +175,7 @@ internal sealed class PolicyRuntimeEvaluationService
effectiveRequest.Sbom,
effectiveRequest.Exceptions,
effectiveRequest.Reachability,
entropy,
evaluationTimestamp);
var evalRequest = new Evaluation.PolicyEvaluationRequest(document, context);
@@ -335,6 +345,8 @@ internal sealed class PolicyRuntimeEvaluationService
var startTimestamp = _timeProvider.GetTimestamp();
var evaluationTimestamp = request.EvaluationTimestamp ?? _timeProvider.GetUtcNow();
var entropy = ComputeEntropy(request);
var context = new PolicyEvaluationContext(
request.Severity,
new PolicyEvaluationEnvironment(ImmutableDictionary<string, string>.Empty),
@@ -343,6 +355,7 @@ internal sealed class PolicyRuntimeEvaluationService
request.Sbom,
request.Exceptions,
request.Reachability,
entropy,
evaluationTimestamp);
var evalRequest = new Evaluation.PolicyEvaluationRequest(document, context);
@@ -495,6 +508,12 @@ internal sealed class PolicyRuntimeEvaluationService
source = request.Reachability.Source,
method = request.Reachability.Method
},
entropy = new
{
layerSummary = request.EntropyLayerSummary is null ? null : StableHash(request.EntropyLayerSummary),
entropyReport = request.EntropyReport is null ? null : StableHash(request.EntropyReport),
provenanceAttested = request.ProvenanceAttested ?? false
}
};
var json = JsonSerializer.Serialize(contextData, ContextSerializerOptions);
@@ -517,6 +536,42 @@ internal sealed class PolicyRuntimeEvaluationService
return (long)elapsed.TotalMilliseconds;
}
private PolicyEvaluationEntropy ComputeEntropy(RuntimeEvaluationRequest request)
{
if (string.IsNullOrWhiteSpace(request.EntropyLayerSummary))
{
return PolicyEvaluationEntropy.Unknown;
}
try
{
var result = _entropy.ComputeFromJson(
request.EntropyLayerSummary!,
request.EntropyReport,
request.ProvenanceAttested ?? false);
return new PolicyEvaluationEntropy(
Penalty: result.Penalty,
ImageOpaqueRatio: result.ImageOpaqueRatio,
Blocked: result.Blocked,
Warned: result.Warned,
Capped: result.Capped,
TopFileOpaqueRatio: result.TopFiles.FirstOrDefault()?.OpaqueRatio);
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to compute entropy penalty; defaulting to zero.");
return PolicyEvaluationEntropy.Unknown;
}
}
private static string StableHash(string input)
{
Span<byte> hash = stackalloc byte[32];
SHA256.HashData(Encoding.UTF8.GetBytes(input), hash);
return Convert.ToHexStringLower(hash);
}
private async Task<RuntimeEvaluationRequest> EnrichReachabilityAsync(
RuntimeEvaluationRequest request,
CancellationToken cancellationToken)