up
Some checks failed
api-governance / spectral-lint (push) Has been cancelled
Docs CI / lint-and-preview (push) Has been cancelled
oas-ci / oas-validate (push) Has been cancelled
SDK Publish & Sign / sdk-publish (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Policy Simulation / policy-simulate (push) Has been cancelled
devportal-offline / build-offline (push) Has been cancelled

This commit is contained in:
StellaOps Bot
2025-11-26 20:23:28 +02:00
parent 4831c7fcb0
commit d63af51f84
139 changed files with 8010 additions and 2795 deletions

View File

@@ -0,0 +1,333 @@
using System;
using System.Buffers;
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using System.Text.Json.Nodes;
namespace StellaOps.Policy.RiskProfile.Canonicalization;
/// <summary>
/// Provides deterministic canonicalization, digesting, and merge semantics for RiskProfile documents.
/// </summary>
public static class RiskProfileCanonicalizer
{
private static readonly JsonDocumentOptions DocOptions = new()
{
AllowTrailingCommas = true,
CommentHandling = JsonCommentHandling.Skip,
};
private static readonly JsonSerializerOptions SerializeOptions = new()
{
WriteIndented = false,
PropertyNamingPolicy = null,
};
public static byte[] CanonicalizeToUtf8(ReadOnlySpan<byte> utf8Json)
{
using var doc = JsonDocument.Parse(utf8Json, DocOptions);
var canonical = CanonicalizeElement(doc.RootElement);
return Encoding.UTF8.GetBytes(canonical);
}
public static string CanonicalizeToString(string json)
{
var utf8 = Encoding.UTF8.GetBytes(json);
return Encoding.UTF8.GetString(CanonicalizeToUtf8(utf8));
}
public static string ComputeDigest(string json)
{
var canonical = CanonicalizeToUtf8(Encoding.UTF8.GetBytes(json));
var hash = SHA256.HashData(canonical);
return Convert.ToHexString(hash).ToLowerInvariant();
}
public static string Merge(string baseProfileJson, string overlayProfileJson)
{
using var baseDoc = JsonDocument.Parse(baseProfileJson, DocOptions);
using var overlayDoc = JsonDocument.Parse(overlayProfileJson, DocOptions);
var merged = MergeObjects(baseDoc.RootElement, overlayDoc.RootElement);
var raw = merged.ToJsonString(SerializeOptions);
return CanonicalizeToString(raw);
}
private static string CanonicalizeElement(JsonElement element)
{
var node = JsonNode.Parse(element.GetRawText())!;
CanonicalizeNode(node);
return node.ToJsonString(SerializeOptions);
}
private static void CanonicalizeNode(JsonNode node, IReadOnlyList<string>? path = null)
{
path ??= Array.Empty<string>();
switch (node)
{
case JsonObject obj:
foreach (var kvp in obj.ToList())
{
if (kvp.Value is { } child)
{
CanonicalizeNode(child, Append(path, kvp.Key));
}
}
var ordered = obj.OrderBy(k => k.Key, StringComparer.Ordinal).ToList();
obj.Clear();
foreach (var kvp in ordered)
{
obj[kvp.Key] = kvp.Value;
}
break;
case JsonArray array:
var items = array.ToList();
foreach (var child in items)
{
CanonicalizeNode(child!, path);
}
if (IsSignals(path))
{
items = items.OrderBy(i => i?["name"]?.GetValue<string>(), StringComparer.Ordinal).ToList();
}
else if (IsWeights(path))
{
// weights are objects, not arrays; no-op
}
else if (IsSeverityOverrides(path))
{
items = items.OrderBy(GetWhenThenKey, StringComparer.Ordinal).ToList();
}
else if (IsDecisionOverrides(path))
{
items = items.OrderBy(GetWhenThenKey, StringComparer.Ordinal).ToList();
}
array.Clear();
foreach (var item in items)
{
array.Add(item);
}
break;
}
}
private static JsonObject MergeObjects(JsonElement baseObj, JsonElement overlayObj)
{
var result = new JsonObject();
void Copy(JsonElement source)
{
foreach (var prop in source.EnumerateObject())
{
result[prop.Name] = JsonNode.Parse(prop.Value.GetRawText());
}
}
Copy(baseObj);
Copy(overlayObj);
// Signals
var signals = MergeArrayByKey(baseObj, overlayObj, "signals", "name");
if (signals is not null)
{
result["signals"] = signals;
}
// Weights
var weights = MergeObjectProperties(baseObj, overlayObj, "weights");
if (weights is not null)
{
result["weights"] = weights;
}
// Overrides.severity
var overrides = MergeOverrides(baseObj, overlayObj);
if (overrides is not null)
{
result["overrides"] = overrides;
}
// Metadata
var metadata = MergeObjectProperties(baseObj, overlayObj, "metadata");
if (metadata is not null)
{
result["metadata"] = metadata;
}
return result;
}
private static JsonNode? MergeOverrides(JsonElement baseObj, JsonElement overlayObj)
{
JsonElement? BaseOverrides() => baseObj.TryGetProperty("overrides", out var o) ? o : (JsonElement?)null;
JsonElement? OverlayOverrides() => overlayObj.TryGetProperty("overrides", out var o) ? o : (JsonElement?)null;
var baseOverrides = BaseOverrides();
var overlayOverrides = OverlayOverrides();
if (baseOverrides is null && overlayOverrides is null)
{
return null;
}
var result = new JsonObject();
var severity = MergeArrayByPredicate(baseOverrides, overlayOverrides, "severity");
if (severity is not null)
{
result["severity"] = severity;
}
var decisions = MergeArrayByPredicate(baseOverrides, overlayOverrides, "decisions");
if (decisions is not null)
{
result["decisions"] = decisions;
}
return result;
}
private static JsonNode? MergeArrayByPredicate(JsonElement? baseObj, JsonElement? overlayObj, string propertyName)
{
var baseArray = baseObj is { } b && b.TryGetProperty(propertyName, out var ba) && ba.ValueKind == JsonValueKind.Array ? ba : (JsonElement?)null;
var overlayArray = overlayObj is { } o && o.TryGetProperty(propertyName, out var oa) && oa.ValueKind == JsonValueKind.Array ? oa : (JsonElement?)null;
if (baseArray is null && overlayArray is null)
{
return null;
}
var dict = new Dictionary<string, JsonNode>(StringComparer.Ordinal);
void Add(JsonElement? src)
{
if (src is null) return;
foreach (var item in src.Value.EnumerateArray())
{
var key = GetWhenThenKey(item);
dict[key] = JsonNode.Parse(item.GetRawText())!;
}
}
Add(baseArray);
Add(overlayArray);
var arr = new JsonArray();
foreach (var kvp in dict.OrderBy(k => k.Key, StringComparer.Ordinal))
{
arr.Add(kvp.Value);
}
return arr;
}
private static JsonNode? MergeArrayByKey(JsonElement baseObj, JsonElement overlayObj, string propertyName, string keyName)
{
JsonElement? Base() => baseObj.TryGetProperty(propertyName, out var s) && s.ValueKind == JsonValueKind.Array ? s : (JsonElement?)null;
JsonElement? Overlay() => overlayObj.TryGetProperty(propertyName, out var s) && s.ValueKind == JsonValueKind.Array ? s : (JsonElement?)null;
var baseArray = Base();
var overlayArray = Overlay();
if (baseArray is null && overlayArray is null)
{
return null;
}
var dict = new Dictionary<string, JsonNode>(StringComparer.Ordinal);
void Add(JsonElement? src)
{
if (src is null) return;
foreach (var item in src.Value.EnumerateArray())
{
if (!item.TryGetProperty(keyName, out var keyProp) || keyProp.ValueKind != JsonValueKind.String)
{
continue;
}
var key = keyProp.GetString() ?? string.Empty;
dict[key] = JsonNode.Parse(item.GetRawText())!;
}
}
Add(baseArray);
Add(overlayArray);
var arr = new JsonArray();
foreach (var kvp in dict.OrderBy(k => k.Key, StringComparer.Ordinal))
{
arr.Add(kvp.Value);
}
return arr;
}
private static JsonNode? MergeObjectProperties(JsonElement baseObj, JsonElement overlayObj, string propertyName)
{
var baseProp = baseObj.TryGetProperty(propertyName, out var bp) && bp.ValueKind == JsonValueKind.Object ? bp : (JsonElement?)null;
var overlayProp = overlayObj.TryGetProperty(propertyName, out var op) && op.ValueKind == JsonValueKind.Object ? op : (JsonElement?)null;
if (baseProp is null && overlayProp is null)
{
return null;
}
var result = new JsonObject();
void Add(JsonElement? src)
{
if (src is null) return;
foreach (var prop in src.Value.EnumerateObject())
{
result[prop.Name] = JsonNode.Parse(prop.Value.GetRawText());
}
}
Add(baseProp);
Add(overlayProp);
return result;
}
private static string GetWhenThenKey(JsonElement element)
{
var when = element.TryGetProperty("when", out var whenProp) ? whenProp.GetRawText() : string.Empty;
var then = element.TryGetProperty("set", out var setProp) ? setProp.GetRawText() : element.TryGetProperty("action", out var actionProp) ? actionProp.GetRawText() : string.Empty;
return when + "|" + then;
}
private static bool IsSignals(IReadOnlyList<string> path)
=> path.Count >= 1 && path[^1] == "signals";
private static bool IsWeights(IReadOnlyList<string> path)
=> path.Count >= 1 && path[^1] == "weights";
private static bool IsSeverityOverrides(IReadOnlyList<string> path)
=> path.Count >= 2 && path[^2] == "overrides" && path[^1] == "severity";
private static bool IsDecisionOverrides(IReadOnlyList<string> path)
=> path.Count >= 2 && path[^2] == "overrides" && path[^1] == "decisions";
private static IReadOnlyList<string> Append(IReadOnlyList<string> path, string segment)
{
if (path.Count == 0)
{
return new[] { segment };
}
var next = new string[path.Count + 1];
for (var i = 0; i < path.Count; i++)
{
next[i] = path[i];
}
next[^1] = segment;
return next;
}
}