up
This commit is contained in:
@@ -58,6 +58,7 @@ internal sealed class PolicyRuntimeEvaluationService
|
||||
private readonly IPolicyPackRepository _repository;
|
||||
private readonly IPolicyEvaluationCache _cache;
|
||||
private readonly PolicyEvaluator _evaluator;
|
||||
private readonly ReachabilityFacts.ReachabilityFactsJoiningService? _reachabilityFacts;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
private readonly ILogger<PolicyRuntimeEvaluationService> _logger;
|
||||
|
||||
@@ -71,12 +72,14 @@ internal sealed class PolicyRuntimeEvaluationService
|
||||
IPolicyPackRepository repository,
|
||||
IPolicyEvaluationCache cache,
|
||||
PolicyEvaluator evaluator,
|
||||
ReachabilityFacts.ReachabilityFactsJoiningService? reachabilityFacts,
|
||||
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;
|
||||
_timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
@@ -90,35 +93,38 @@ internal sealed class PolicyRuntimeEvaluationService
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(request);
|
||||
|
||||
using var activity = PolicyEngineTelemetry.StartEvaluateActivity(
|
||||
request.TenantId, request.PackId, runId: null);
|
||||
activity?.SetTag("policy.version", request.Version);
|
||||
activity?.SetTag("subject.purl", request.SubjectPurl);
|
||||
activity?.SetTag("advisory.id", request.AdvisoryId);
|
||||
|
||||
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(request.PackId, request.Version, cancellationToken)
|
||||
var bundle = await _repository.GetBundleAsync(effectiveRequest.PackId, effectiveRequest.Version, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (bundle is null)
|
||||
{
|
||||
PolicyEngineTelemetry.RecordError("evaluation", request.TenantId);
|
||||
PolicyEngineTelemetry.RecordEvaluationFailure(request.TenantId, request.PackId, "bundle_not_found");
|
||||
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 '{request.PackId}' version {request.Version}.");
|
||||
$"Policy bundle not found for pack '{effectiveRequest.PackId}' version {effectiveRequest.Version}.");
|
||||
}
|
||||
|
||||
// Compute deterministic cache key
|
||||
var subjectDigest = ComputeSubjectDigest(request.TenantId, request.SubjectPurl, request.AdvisoryId);
|
||||
var contextDigest = ComputeContextDigest(request);
|
||||
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 (!request.BypassCache)
|
||||
if (!effectiveRequest.BypassCache)
|
||||
{
|
||||
var cacheResult = await _cache.GetAsync(cacheKey, cancellationToken).ConfigureAwait(false);
|
||||
if (cacheResult.CacheHit && cacheResult.Entry is not null)
|
||||
@@ -132,10 +138,10 @@ internal sealed class PolicyRuntimeEvaluationService
|
||||
activity?.SetStatus(ActivityStatusCode.Ok);
|
||||
_logger.LogDebug(
|
||||
"Cache hit for evaluation {PackId}@{Version} subject {Subject} from {Source}",
|
||||
request.PackId, request.Version, request.SubjectPurl, cacheResult.Source);
|
||||
effectiveRequest.PackId, effectiveRequest.Version, effectiveRequest.SubjectPurl, cacheResult.Source);
|
||||
|
||||
return CreateResponseFromCache(
|
||||
request, bundle.Digest, cacheResult.Entry, cacheResult.Source, duration);
|
||||
effectiveRequest, bundle.Digest, cacheResult.Entry, cacheResult.Source, duration);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -153,13 +159,13 @@ internal sealed class PolicyRuntimeEvaluationService
|
||||
}
|
||||
|
||||
var context = new PolicyEvaluationContext(
|
||||
request.Severity,
|
||||
effectiveRequest.Severity,
|
||||
new PolicyEvaluationEnvironment(ImmutableDictionary<string, string>.Empty),
|
||||
request.Advisory,
|
||||
request.Vex,
|
||||
request.Sbom,
|
||||
request.Exceptions,
|
||||
request.Reachability,
|
||||
effectiveRequest.Advisory,
|
||||
effectiveRequest.Vex,
|
||||
effectiveRequest.Sbom,
|
||||
effectiveRequest.Exceptions,
|
||||
effectiveRequest.Reachability,
|
||||
evaluationTimestamp);
|
||||
|
||||
var evalRequest = new Evaluation.PolicyEvaluationRequest(document, context);
|
||||
@@ -187,11 +193,25 @@ internal sealed class PolicyRuntimeEvaluationService
|
||||
var evalDurationSeconds = evalDuration / 1000.0;
|
||||
|
||||
// Record metrics
|
||||
PolicyEngineTelemetry.RecordEvaluationLatency(evalDurationSeconds, request.TenantId, request.PackId);
|
||||
PolicyEngineTelemetry.RecordEvaluation(request.TenantId, request.PackId, "full");
|
||||
PolicyEngineTelemetry.RecordEvaluationLatency(evalDurationSeconds, effectiveRequest.TenantId, effectiveRequest.PackId);
|
||||
PolicyEngineTelemetry.RecordEvaluation(effectiveRequest.TenantId, effectiveRequest.PackId, "full");
|
||||
if (!string.IsNullOrEmpty(result.RuleName))
|
||||
{
|
||||
PolicyEngineTelemetry.RecordRuleFired(request.PackId, 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);
|
||||
@@ -201,11 +221,11 @@ internal sealed class PolicyRuntimeEvaluationService
|
||||
|
||||
_logger.LogDebug(
|
||||
"Evaluated {PackId}@{Version} subject {Subject} in {Duration}ms - {Status}",
|
||||
request.PackId, request.Version, request.SubjectPurl, evalDuration, result.Status);
|
||||
effectiveRequest.PackId, effectiveRequest.Version, effectiveRequest.SubjectPurl, evalDuration, result.Status);
|
||||
|
||||
return new RuntimeEvaluationResponse(
|
||||
request.PackId,
|
||||
request.Version,
|
||||
effectiveRequest.PackId,
|
||||
effectiveRequest.Version,
|
||||
bundle.Digest,
|
||||
result.Status,
|
||||
result.Severity,
|
||||
@@ -240,8 +260,12 @@ internal sealed class PolicyRuntimeEvaluationService
|
||||
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 = requests.GroupBy(r => (r.PackId, r.Version));
|
||||
var groups = hydratedRequests.GroupBy(r => (r.PackId, r.Version));
|
||||
|
||||
foreach (var group in groups)
|
||||
{
|
||||
@@ -351,6 +375,20 @@ internal sealed class PolicyRuntimeEvaluationService
|
||||
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,
|
||||
@@ -448,7 +486,15 @@ internal sealed class PolicyRuntimeEvaluationService
|
||||
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 = request.Reachability.State,
|
||||
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
|
||||
},
|
||||
};
|
||||
|
||||
var json = JsonSerializer.Serialize(contextData, ContextSerializerOptions);
|
||||
@@ -470,5 +516,98 @@ internal sealed class PolicyRuntimeEvaluationService
|
||||
var elapsed = _timeProvider.GetElapsedTime(startTimestamp);
|
||||
return (long)elapsed.TotalMilliseconds;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user