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
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:
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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 (< 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;
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user