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
669 lines
28 KiB
C#
669 lines
28 KiB
C#
using System.Collections.Immutable;
|
|
using System.Diagnostics;
|
|
using System.Linq;
|
|
using System.Security.Cryptography;
|
|
using System.Text;
|
|
using System.Text.Json;
|
|
using Microsoft.Extensions.Logging;
|
|
using StellaOps.Policy.Engine.Caching;
|
|
using StellaOps.Policy.Engine.Domain;
|
|
using StellaOps.Policy.Engine.Evaluation;
|
|
using StellaOps.Policy.Engine.Telemetry;
|
|
using StellaOps.PolicyDsl;
|
|
|
|
namespace StellaOps.Policy.Engine.Services;
|
|
|
|
/// <summary>
|
|
/// Request for runtime policy evaluation over linkset/SBOM data.
|
|
/// </summary>
|
|
internal sealed record RuntimeEvaluationRequest(
|
|
string PackId,
|
|
int Version,
|
|
string TenantId,
|
|
string SubjectPurl,
|
|
string AdvisoryId,
|
|
PolicyEvaluationSeverity Severity,
|
|
PolicyEvaluationAdvisory Advisory,
|
|
PolicyEvaluationVexEvidence Vex,
|
|
PolicyEvaluationSbom Sbom,
|
|
PolicyEvaluationExceptions Exceptions,
|
|
PolicyEvaluationReachability Reachability,
|
|
string? EntropyLayerSummary,
|
|
string? EntropyReport,
|
|
bool? ProvenanceAttested,
|
|
DateTimeOffset? EvaluationTimestamp = null,
|
|
bool BypassCache = false);
|
|
|
|
/// <summary>
|
|
/// Response from runtime policy evaluation.
|
|
/// </summary>
|
|
internal sealed record RuntimeEvaluationResponse(
|
|
string PackId,
|
|
int Version,
|
|
string PolicyDigest,
|
|
string Status,
|
|
string? Severity,
|
|
string? RuleName,
|
|
int? Priority,
|
|
ImmutableDictionary<string, string> Annotations,
|
|
ImmutableArray<string> Warnings,
|
|
PolicyExceptionApplication? AppliedException,
|
|
string CorrelationId,
|
|
bool Cached,
|
|
CacheSource CacheSource,
|
|
long EvaluationDurationMs);
|
|
|
|
/// <summary>
|
|
/// Runtime evaluator executing compiled policy plans over advisory/VEX linksets and SBOM asset metadata
|
|
/// with deterministic caching (Redis) and fallback path.
|
|
/// </summary>
|
|
internal sealed class PolicyRuntimeEvaluationService
|
|
{
|
|
private readonly IPolicyPackRepository _repository;
|
|
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;
|
|
|
|
private static readonly JsonSerializerOptions ContextSerializerOptions = new()
|
|
{
|
|
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
|
WriteIndented = false,
|
|
};
|
|
|
|
public PolicyRuntimeEvaluationService(
|
|
IPolicyPackRepository repository,
|
|
IPolicyEvaluationCache cache,
|
|
PolicyEvaluator evaluator,
|
|
ReachabilityFacts.ReachabilityFactsJoiningService? reachabilityFacts,
|
|
Signals.Entropy.EntropyPenaltyCalculator entropy,
|
|
TimeProvider timeProvider,
|
|
ILogger<PolicyRuntimeEvaluationService> logger)
|
|
{
|
|
_repository = repository ?? throw new ArgumentNullException(nameof(repository));
|
|
_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));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Evaluates a policy against the provided context with deterministic caching.
|
|
/// </summary>
|
|
public async Task<RuntimeEvaluationResponse> EvaluateAsync(
|
|
RuntimeEvaluationRequest request,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(request);
|
|
|
|
var startTimestamp = _timeProvider.GetTimestamp();
|
|
var evaluationTimestamp = request.EvaluationTimestamp ?? _timeProvider.GetUtcNow();
|
|
var effectiveRequest = _reachabilityFacts is null
|
|
? request
|
|
: await EnrichReachabilityAsync(request, cancellationToken).ConfigureAwait(false);
|
|
|
|
using var activity = PolicyEngineTelemetry.StartEvaluateActivity(
|
|
effectiveRequest.TenantId, effectiveRequest.PackId, runId: null);
|
|
activity?.SetTag("policy.version", effectiveRequest.Version);
|
|
activity?.SetTag("subject.purl", effectiveRequest.SubjectPurl);
|
|
activity?.SetTag("advisory.id", effectiveRequest.AdvisoryId);
|
|
|
|
// Load the compiled policy bundle
|
|
var bundle = await _repository.GetBundleAsync(effectiveRequest.PackId, effectiveRequest.Version, cancellationToken)
|
|
.ConfigureAwait(false);
|
|
|
|
if (bundle is null)
|
|
{
|
|
PolicyEngineTelemetry.RecordError("evaluation", effectiveRequest.TenantId);
|
|
PolicyEngineTelemetry.RecordEvaluationFailure(effectiveRequest.TenantId, effectiveRequest.PackId, "bundle_not_found");
|
|
activity?.SetStatus(ActivityStatusCode.Error, "Bundle not found");
|
|
throw new InvalidOperationException(
|
|
$"Policy bundle not found for pack '{effectiveRequest.PackId}' version {effectiveRequest.Version}.");
|
|
}
|
|
|
|
// Compute deterministic cache key
|
|
var subjectDigest = ComputeSubjectDigest(effectiveRequest.TenantId, effectiveRequest.SubjectPurl, effectiveRequest.AdvisoryId);
|
|
var contextDigest = ComputeContextDigest(effectiveRequest);
|
|
var cacheKey = PolicyEvaluationCacheKey.Create(bundle.Digest, subjectDigest, contextDigest);
|
|
|
|
// Try cache lookup unless bypassed
|
|
if (!effectiveRequest.BypassCache)
|
|
{
|
|
var cacheResult = await _cache.GetAsync(cacheKey, cancellationToken).ConfigureAwait(false);
|
|
if (cacheResult.CacheHit && cacheResult.Entry is not null)
|
|
{
|
|
var duration = GetElapsedMilliseconds(startTimestamp);
|
|
var durationSeconds = duration / 1000.0;
|
|
PolicyEngineTelemetry.RecordEvaluationLatency(durationSeconds, request.TenantId, request.PackId);
|
|
PolicyEngineTelemetry.RecordEvaluation(request.TenantId, request.PackId, "cached");
|
|
activity?.SetTag("cache.hit", true);
|
|
activity?.SetTag("cache.source", cacheResult.Source.ToString());
|
|
activity?.SetStatus(ActivityStatusCode.Ok);
|
|
_logger.LogDebug(
|
|
"Cache hit for evaluation {PackId}@{Version} subject {Subject} from {Source}",
|
|
effectiveRequest.PackId, effectiveRequest.Version, effectiveRequest.SubjectPurl, cacheResult.Source);
|
|
|
|
return CreateResponseFromCache(
|
|
effectiveRequest, bundle.Digest, cacheResult.Entry, cacheResult.Source, duration);
|
|
}
|
|
}
|
|
|
|
activity?.SetTag("cache.hit", false);
|
|
|
|
// Cache miss - perform evaluation
|
|
var document = bundle.CompiledDocument;
|
|
if (document is null)
|
|
{
|
|
PolicyEngineTelemetry.RecordError("evaluation", request.TenantId);
|
|
PolicyEngineTelemetry.RecordEvaluationFailure(request.TenantId, request.PackId, "document_not_found");
|
|
activity?.SetStatus(ActivityStatusCode.Error, "Document not found");
|
|
throw new InvalidOperationException(
|
|
$"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),
|
|
effectiveRequest.Advisory,
|
|
effectiveRequest.Vex,
|
|
effectiveRequest.Sbom,
|
|
effectiveRequest.Exceptions,
|
|
effectiveRequest.Reachability,
|
|
entropy,
|
|
evaluationTimestamp);
|
|
|
|
var evalRequest = new Evaluation.PolicyEvaluationRequest(document, context);
|
|
var result = _evaluator.Evaluate(evalRequest);
|
|
|
|
var correlationId = ComputeCorrelationId(bundle.Digest, subjectDigest, contextDigest);
|
|
var expiresAt = evaluationTimestamp.AddMinutes(30);
|
|
|
|
// Store in cache
|
|
var cacheEntry = new PolicyEvaluationCacheEntry(
|
|
result.Status,
|
|
result.Severity,
|
|
result.RuleName,
|
|
result.Priority,
|
|
result.Annotations,
|
|
result.Warnings,
|
|
result.AppliedException?.ExceptionId,
|
|
correlationId,
|
|
evaluationTimestamp,
|
|
expiresAt);
|
|
|
|
await _cache.SetAsync(cacheKey, cacheEntry, cancellationToken).ConfigureAwait(false);
|
|
|
|
var evalDuration = GetElapsedMilliseconds(startTimestamp);
|
|
var evalDurationSeconds = evalDuration / 1000.0;
|
|
|
|
// Record metrics
|
|
PolicyEngineTelemetry.RecordEvaluationLatency(evalDurationSeconds, effectiveRequest.TenantId, effectiveRequest.PackId);
|
|
PolicyEngineTelemetry.RecordEvaluation(effectiveRequest.TenantId, effectiveRequest.PackId, "full");
|
|
if (!string.IsNullOrEmpty(result.RuleName))
|
|
{
|
|
PolicyEngineTelemetry.RecordRuleFired(effectiveRequest.PackId, result.RuleName);
|
|
}
|
|
|
|
if (result.AppliedException is not null)
|
|
{
|
|
PolicyEngineTelemetry.RecordExceptionApplication(effectiveRequest.TenantId, result.AppliedException.EffectType.ToString());
|
|
PolicyEngineTelemetry.RecordExceptionApplicationLatency(evalDurationSeconds, effectiveRequest.TenantId, result.AppliedException.EffectType.ToString());
|
|
_logger.LogInformation(
|
|
"Applied exception {ExceptionId} (effect {EffectType}) for tenant {TenantId} pack {PackId}@{Version} aoc {CompilationId}",
|
|
result.AppliedException.ExceptionId,
|
|
result.AppliedException.EffectType,
|
|
effectiveRequest.TenantId,
|
|
effectiveRequest.PackId,
|
|
effectiveRequest.Version,
|
|
bundle.AocMetadata?.CompilationId ?? "none");
|
|
}
|
|
|
|
activity?.SetTag("evaluation.status", result.Status);
|
|
activity?.SetTag("evaluation.rule", result.RuleName ?? "none");
|
|
activity?.SetTag("evaluation.duration_ms", evalDuration);
|
|
activity?.SetStatus(ActivityStatusCode.Ok);
|
|
|
|
_logger.LogDebug(
|
|
"Evaluated {PackId}@{Version} subject {Subject} in {Duration}ms - {Status}",
|
|
effectiveRequest.PackId, effectiveRequest.Version, effectiveRequest.SubjectPurl, evalDuration, result.Status);
|
|
|
|
return new RuntimeEvaluationResponse(
|
|
effectiveRequest.PackId,
|
|
effectiveRequest.Version,
|
|
bundle.Digest,
|
|
result.Status,
|
|
result.Severity,
|
|
result.RuleName,
|
|
result.Priority,
|
|
result.Annotations,
|
|
result.Warnings,
|
|
result.AppliedException,
|
|
correlationId,
|
|
Cached: false,
|
|
CacheSource: CacheSource.None,
|
|
EvaluationDurationMs: evalDuration);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Evaluates multiple subjects in batch with caching.
|
|
/// </summary>
|
|
public async Task<IReadOnlyList<RuntimeEvaluationResponse>> EvaluateBatchAsync(
|
|
IReadOnlyList<RuntimeEvaluationRequest> requests,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
if (requests.Count == 0)
|
|
{
|
|
return Array.Empty<RuntimeEvaluationResponse>();
|
|
}
|
|
|
|
using var activity = PolicyEngineTelemetry.ActivitySource.StartActivity("policy.evaluate_batch", ActivityKind.Internal);
|
|
activity?.SetTag("batch.size", requests.Count);
|
|
|
|
var batchStartTimestamp = _timeProvider.GetTimestamp();
|
|
var results = new List<RuntimeEvaluationResponse>(requests.Count);
|
|
var cacheHits = 0;
|
|
var cacheMisses = 0;
|
|
|
|
var hydratedRequests = _reachabilityFacts is null
|
|
? requests
|
|
: await EnrichReachabilityBatchAsync(requests, cancellationToken).ConfigureAwait(false);
|
|
|
|
// Group by pack/version for bundle loading efficiency
|
|
var groups = hydratedRequests.GroupBy(r => (r.PackId, r.Version));
|
|
|
|
foreach (var group in groups)
|
|
{
|
|
var (packId, version) = group.Key;
|
|
var bundle = await _repository.GetBundleAsync(packId, version, cancellationToken)
|
|
.ConfigureAwait(false);
|
|
|
|
if (bundle is null)
|
|
{
|
|
foreach (var request in group)
|
|
{
|
|
PolicyEngineTelemetry.RecordEvaluationFailure(request.TenantId, packId, "bundle_not_found");
|
|
_logger.LogWarning(
|
|
"Policy bundle not found for pack '{PackId}' version {Version}, skipping evaluation",
|
|
packId, version);
|
|
}
|
|
continue;
|
|
}
|
|
|
|
var document = bundle.CompiledDocument;
|
|
if (document is null)
|
|
{
|
|
PolicyEngineTelemetry.RecordEvaluationFailure("default", packId, "document_not_found");
|
|
_logger.LogWarning(
|
|
"Compiled policy document not found for pack '{PackId}' version {Version}",
|
|
packId, version);
|
|
continue;
|
|
}
|
|
|
|
// Build cache keys for batch lookup
|
|
var cacheKeys = new List<(RuntimeEvaluationRequest Request, PolicyEvaluationCacheKey Key)>();
|
|
foreach (var request in group)
|
|
{
|
|
var subjectDigest = ComputeSubjectDigest(request.TenantId, request.SubjectPurl, request.AdvisoryId);
|
|
var contextDigest = ComputeContextDigest(request);
|
|
var key = PolicyEvaluationCacheKey.Create(bundle.Digest, subjectDigest, contextDigest);
|
|
cacheKeys.Add((request, key));
|
|
}
|
|
|
|
// Batch cache lookup
|
|
var keyList = cacheKeys.Select(k => k.Key).ToList();
|
|
var cacheResults = await _cache.GetBatchAsync(keyList, cancellationToken).ConfigureAwait(false);
|
|
|
|
var toEvaluate = new List<(RuntimeEvaluationRequest Request, PolicyEvaluationCacheKey Key)>();
|
|
|
|
// Process cache hits
|
|
foreach (var (request, key) in cacheKeys)
|
|
{
|
|
if (!request.BypassCache && cacheResults.Found.TryGetValue(key, out var entry))
|
|
{
|
|
var response = CreateResponseFromCache(request, bundle.Digest, entry, CacheSource.InMemory, 0);
|
|
results.Add(response);
|
|
cacheHits++;
|
|
PolicyEngineTelemetry.RecordEvaluation(request.TenantId, packId, "cached");
|
|
}
|
|
else
|
|
{
|
|
toEvaluate.Add((request, key));
|
|
}
|
|
}
|
|
|
|
// Evaluate cache misses
|
|
var entriesToCache = new Dictionary<PolicyEvaluationCacheKey, PolicyEvaluationCacheEntry>();
|
|
|
|
foreach (var (request, key) in toEvaluate)
|
|
{
|
|
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),
|
|
request.Advisory,
|
|
request.Vex,
|
|
request.Sbom,
|
|
request.Exceptions,
|
|
request.Reachability,
|
|
entropy,
|
|
evaluationTimestamp);
|
|
|
|
var evalRequest = new Evaluation.PolicyEvaluationRequest(document, context);
|
|
var result = _evaluator.Evaluate(evalRequest);
|
|
|
|
var correlationId = ComputeCorrelationId(bundle.Digest, key.SubjectDigest, key.ContextDigest);
|
|
var expiresAt = evaluationTimestamp.AddMinutes(30);
|
|
var duration = GetElapsedMilliseconds(startTimestamp);
|
|
|
|
var cacheEntry = new PolicyEvaluationCacheEntry(
|
|
result.Status,
|
|
result.Severity,
|
|
result.RuleName,
|
|
result.Priority,
|
|
result.Annotations,
|
|
result.Warnings,
|
|
result.AppliedException?.ExceptionId,
|
|
correlationId,
|
|
evaluationTimestamp,
|
|
expiresAt);
|
|
|
|
entriesToCache[key] = cacheEntry;
|
|
cacheMisses++;
|
|
|
|
// Record metrics for each evaluation
|
|
PolicyEngineTelemetry.RecordEvaluationLatency(duration / 1000.0, request.TenantId, packId);
|
|
PolicyEngineTelemetry.RecordEvaluation(request.TenantId, packId, "full");
|
|
if (!string.IsNullOrEmpty(result.RuleName))
|
|
{
|
|
PolicyEngineTelemetry.RecordRuleFired(packId, result.RuleName);
|
|
}
|
|
|
|
if (result.AppliedException is not null)
|
|
{
|
|
PolicyEngineTelemetry.RecordExceptionApplication(request.TenantId, result.AppliedException.EffectType.ToString());
|
|
PolicyEngineTelemetry.RecordExceptionApplicationLatency(duration / 1000.0, request.TenantId, result.AppliedException.EffectType.ToString());
|
|
_logger.LogInformation(
|
|
"Applied exception {ExceptionId} (effect {EffectType}) for tenant {TenantId} pack {PackId}@{Version} aoc {CompilationId}",
|
|
result.AppliedException.ExceptionId,
|
|
result.AppliedException.EffectType,
|
|
request.TenantId,
|
|
request.PackId,
|
|
request.Version,
|
|
bundle.AocMetadata?.CompilationId ?? "none");
|
|
}
|
|
|
|
results.Add(new RuntimeEvaluationResponse(
|
|
request.PackId,
|
|
request.Version,
|
|
bundle.Digest,
|
|
result.Status,
|
|
result.Severity,
|
|
result.RuleName,
|
|
result.Priority,
|
|
result.Annotations,
|
|
result.Warnings,
|
|
result.AppliedException,
|
|
correlationId,
|
|
Cached: false,
|
|
CacheSource: CacheSource.None,
|
|
EvaluationDurationMs: duration));
|
|
}
|
|
|
|
// Batch store cache entries
|
|
if (entriesToCache.Count > 0)
|
|
{
|
|
await _cache.SetBatchAsync(entriesToCache, cancellationToken).ConfigureAwait(false);
|
|
}
|
|
}
|
|
|
|
// Record batch-level metrics
|
|
var batchDuration = GetElapsedMilliseconds(batchStartTimestamp);
|
|
activity?.SetTag("batch.cache_hits", cacheHits);
|
|
activity?.SetTag("batch.cache_misses", cacheMisses);
|
|
activity?.SetTag("batch.duration_ms", batchDuration);
|
|
activity?.SetStatus(ActivityStatusCode.Ok);
|
|
|
|
_logger.LogDebug(
|
|
"Batch evaluation completed: {Total} subjects, {CacheHits} cache hits, {CacheMisses} evaluated in {Duration}ms",
|
|
requests.Count, cacheHits, cacheMisses, batchDuration);
|
|
|
|
return results;
|
|
}
|
|
|
|
private static RuntimeEvaluationResponse CreateResponseFromCache(
|
|
RuntimeEvaluationRequest request,
|
|
string policyDigest,
|
|
PolicyEvaluationCacheEntry entry,
|
|
CacheSource source,
|
|
long durationMs)
|
|
{
|
|
PolicyExceptionApplication? appliedException = null;
|
|
if (entry.ExceptionId is not null)
|
|
{
|
|
// Reconstruct minimal exception application from cache
|
|
appliedException = new PolicyExceptionApplication(
|
|
entry.ExceptionId,
|
|
EffectId: "cached",
|
|
EffectType: PolicyExceptionEffectType.Suppress,
|
|
OriginalStatus: entry.Status,
|
|
OriginalSeverity: entry.Severity,
|
|
AppliedStatus: entry.Status,
|
|
AppliedSeverity: entry.Severity,
|
|
Metadata: ImmutableDictionary<string, string>.Empty);
|
|
}
|
|
|
|
return new RuntimeEvaluationResponse(
|
|
request.PackId,
|
|
request.Version,
|
|
policyDigest,
|
|
entry.Status,
|
|
entry.Severity,
|
|
entry.RuleName,
|
|
entry.Priority,
|
|
entry.Annotations,
|
|
entry.Warnings,
|
|
appliedException,
|
|
entry.CorrelationId,
|
|
Cached: true,
|
|
CacheSource: source,
|
|
EvaluationDurationMs: durationMs);
|
|
}
|
|
|
|
private static string ComputeSubjectDigest(string tenantId, string subjectPurl, string advisoryId)
|
|
{
|
|
var input = $"{tenantId}|{subjectPurl}|{advisoryId}";
|
|
Span<byte> hash = stackalloc byte[32];
|
|
SHA256.HashData(Encoding.UTF8.GetBytes(input), hash);
|
|
return Convert.ToHexStringLower(hash);
|
|
}
|
|
|
|
private static string ComputeContextDigest(RuntimeEvaluationRequest request)
|
|
{
|
|
// Create deterministic context representation
|
|
var contextData = new
|
|
{
|
|
severity = request.Severity.Normalized,
|
|
severityScore = request.Severity.Score,
|
|
advisorySource = request.Advisory.Source,
|
|
vexCount = request.Vex.Statements.Length,
|
|
vexStatements = request.Vex.Statements.Select(s => $"{s.Status}:{s.Justification}").OrderBy(s => s).ToArray(),
|
|
sbomTags = request.Sbom.Tags.OrderBy(t => t).ToArray(),
|
|
exceptionCount = request.Exceptions.Instances.Length,
|
|
reachability = new
|
|
{
|
|
state = request.Reachability.State,
|
|
confidence = request.Reachability.Confidence,
|
|
score = request.Reachability.Score,
|
|
hasRuntimeEvidence = request.Reachability.HasRuntimeEvidence,
|
|
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);
|
|
Span<byte> hash = stackalloc byte[32];
|
|
SHA256.HashData(Encoding.UTF8.GetBytes(json), hash);
|
|
return Convert.ToHexStringLower(hash);
|
|
}
|
|
|
|
private static string ComputeCorrelationId(string policyDigest, string subjectDigest, string contextDigest)
|
|
{
|
|
var input = $"{policyDigest}|{subjectDigest}|{contextDigest}";
|
|
Span<byte> hash = stackalloc byte[32];
|
|
SHA256.HashData(Encoding.UTF8.GetBytes(input), hash);
|
|
return Convert.ToHexString(hash);
|
|
}
|
|
|
|
private long GetElapsedMilliseconds(long startTimestamp)
|
|
{
|
|
var elapsed = _timeProvider.GetElapsedTime(startTimestamp);
|
|
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)
|
|
{
|
|
if (_reachabilityFacts is null || !request.Reachability.IsUnknown)
|
|
{
|
|
return request;
|
|
}
|
|
|
|
var fact = await _reachabilityFacts
|
|
.GetFactAsync(request.TenantId, request.SubjectPurl, request.AdvisoryId, cancellationToken)
|
|
.ConfigureAwait(false);
|
|
|
|
if (fact is null)
|
|
{
|
|
return request;
|
|
}
|
|
|
|
var reachability = new PolicyEvaluationReachability(
|
|
State: fact.State.ToString().ToLowerInvariant(),
|
|
Confidence: fact.Confidence,
|
|
Score: fact.Score,
|
|
HasRuntimeEvidence: fact.HasRuntimeEvidence,
|
|
Source: fact.Source,
|
|
Method: fact.Method.ToString().ToLowerInvariant(),
|
|
EvidenceRef: fact.EvidenceRef ?? fact.EvidenceHash);
|
|
|
|
ReachabilityFacts.ReachabilityFactsTelemetry.RecordFactApplied(reachability.State);
|
|
return request with { Reachability = reachability };
|
|
}
|
|
|
|
private async Task<IReadOnlyList<RuntimeEvaluationRequest>> EnrichReachabilityBatchAsync(
|
|
IReadOnlyList<RuntimeEvaluationRequest> requests,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
if (_reachabilityFacts is null)
|
|
{
|
|
return requests;
|
|
}
|
|
|
|
var enriched = new List<RuntimeEvaluationRequest>(requests.Count);
|
|
|
|
foreach (var tenantGroup in requests.GroupBy(r => r.TenantId, StringComparer.Ordinal))
|
|
{
|
|
var pending = tenantGroup
|
|
.Where(r => r.Reachability.IsUnknown)
|
|
.Select(r => new ReachabilityFacts.ReachabilityFactsRequest(r.SubjectPurl, r.AdvisoryId))
|
|
.Distinct()
|
|
.ToList();
|
|
|
|
ReachabilityFacts.ReachabilityFactsBatch? batch = null;
|
|
if (pending.Count > 0)
|
|
{
|
|
batch = await _reachabilityFacts
|
|
.GetFactsBatchAsync(tenantGroup.Key, pending, cancellationToken)
|
|
.ConfigureAwait(false);
|
|
}
|
|
|
|
var lookup = batch?.Found ?? new Dictionary<ReachabilityFacts.ReachabilityFactKey, ReachabilityFacts.ReachabilityFact>();
|
|
|
|
foreach (var request in tenantGroup)
|
|
{
|
|
if (!request.Reachability.IsUnknown)
|
|
{
|
|
enriched.Add(request);
|
|
continue;
|
|
}
|
|
|
|
var key = new ReachabilityFacts.ReachabilityFactKey(request.TenantId, request.SubjectPurl, request.AdvisoryId);
|
|
if (lookup.TryGetValue(key, out var fact))
|
|
{
|
|
var reachability = new PolicyEvaluationReachability(
|
|
State: fact.State.ToString().ToLowerInvariant(),
|
|
Confidence: fact.Confidence,
|
|
Score: fact.Score,
|
|
HasRuntimeEvidence: fact.HasRuntimeEvidence,
|
|
Source: fact.Source,
|
|
Method: fact.Method.ToString().ToLowerInvariant(),
|
|
EvidenceRef: fact.EvidenceRef ?? fact.EvidenceHash);
|
|
|
|
ReachabilityFacts.ReachabilityFactsTelemetry.RecordFactApplied(reachability.State);
|
|
enriched.Add(request with { Reachability = reachability });
|
|
}
|
|
else
|
|
{
|
|
enriched.Add(request);
|
|
}
|
|
}
|
|
}
|
|
|
|
return enriched;
|
|
}
|
|
}
|
|
|