using System.Text.Json; using System.Text.Json.Nodes; using Microsoft.Extensions.Logging; using StellaOps.Findings.Ledger.Domain; namespace StellaOps.Findings.Ledger.Infrastructure.Policy; public sealed class InlinePolicyEvaluationService : IPolicyEvaluationService { private readonly ILogger _logger; public InlinePolicyEvaluationService(ILogger logger) { _logger = logger ?? throw new ArgumentNullException(nameof(logger)); } public Task EvaluateAsync( LedgerEventRecord record, FindingProjection? existingProjection, CancellationToken cancellationToken) { if (record is null) { throw new ArgumentNullException(nameof(record)); } var eventObject = record.EventBody["event"]?.AsObject(); if (eventObject is null) { _logger.LogWarning("Ledger event {EventId} missing canonical event payload; falling back to existing projection.", record.EventId); return Task.FromResult(CreateFallback(existingProjection)); } var payload = eventObject["payload"] as JsonObject; var status = ExtractString(payload, "status"); var severity = ExtractDecimal(payload, "severity"); var explainRef = ExtractString(payload, "explainRef") ?? ExtractString(payload, "explain_ref"); var labels = ExtractLabels(payload, existingProjection); var rationale = ExtractRationale(payload, explainRef); var result = new PolicyEvaluationResult( status, severity, null, null, null, null, existingProjection?.RiskEventSequence, labels, explainRef, rationale); return Task.FromResult(result); } private static PolicyEvaluationResult CreateFallback(FindingProjection? existingProjection) { var labels = existingProjection?.Labels is not null ? (JsonObject)existingProjection.Labels.DeepClone() : new JsonObject(); var rationale = existingProjection?.PolicyRationale is not null ? CloneArray(existingProjection.PolicyRationale) : new JsonArray(); return new PolicyEvaluationResult( existingProjection?.Status, existingProjection?.Severity, existingProjection?.RiskScore, existingProjection?.RiskSeverity, existingProjection?.RiskProfileVersion, existingProjection?.RiskExplanationId, existingProjection?.RiskEventSequence, labels, existingProjection?.ExplainRef, rationale); } private static JsonObject ExtractLabels(JsonObject? payload, FindingProjection? existingProjection) { var labels = existingProjection?.Labels is not null ? (JsonObject)existingProjection.Labels.DeepClone() : new JsonObject(); if (payload is null) { return labels; } if (payload.TryGetPropertyValue("labels", out var labelsNode) && labelsNode is JsonObject labelUpdates) { foreach (var property in labelUpdates) { if (property.Value is null || property.Value.GetValueKind() == JsonValueKind.Null) { labels.Remove(property.Key); } else { labels[property.Key] = property.Value.DeepClone(); } } } if (payload.TryGetPropertyValue("labelsRemove", out var removeNode) && removeNode is JsonArray removeArray) { foreach (var item in removeArray) { if (item is JsonValue value && value.TryGetValue(out string? key) && !string.IsNullOrWhiteSpace(key)) { labels.Remove(key); } } } return labels; } private static JsonArray ExtractRationale(JsonObject? payload, string? explainRef) { if (payload?.TryGetPropertyValue("rationaleRefs", out var rationaleNode) == true && rationaleNode is JsonArray rationaleRefs) { return CloneArray(rationaleRefs); } var rationale = new JsonArray(); if (!string.IsNullOrWhiteSpace(explainRef)) { rationale.Add(explainRef); } return rationale; } private static string? ExtractString(JsonObject? obj, string propertyName) { if (obj is null) { return null; } if (!obj.TryGetPropertyValue(propertyName, out var value) || value is null) { return null; } if (value is JsonValue jsonValue && jsonValue.TryGetValue(out string? text)) { return string.IsNullOrWhiteSpace(text) ? null : text; } return value.ToString(); } private static decimal? ExtractDecimal(JsonObject? obj, string propertyName) { if (obj is null) { return null; } if (!obj.TryGetPropertyValue(propertyName, out var value) || value is null) { return null; } if (value is JsonValue jsonValue) { if (jsonValue.TryGetValue(out decimal decimalValue)) { return decimalValue; } if (jsonValue.TryGetValue(out double doubleValue)) { return Convert.ToDecimal(doubleValue); } } if (decimal.TryParse(value.ToString(), out var parsed)) { return parsed; } return null; } private static JsonArray CloneArray(JsonArray array) { var clone = new JsonArray(); foreach (var item in array) { clone.Add(item?.DeepClone()); } return clone; } }