201 lines
5.9 KiB
C#
201 lines
5.9 KiB
C#
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<InlinePolicyEvaluationService> _logger;
|
|
|
|
public InlinePolicyEvaluationService(ILogger<InlinePolicyEvaluationService> logger)
|
|
{
|
|
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
|
}
|
|
|
|
public Task<PolicyEvaluationResult> 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;
|
|
}
|
|
}
|