420 lines
14 KiB
C#
420 lines
14 KiB
C#
|
|
using System.Buffers;
|
|
using System.Collections.Immutable;
|
|
using System.Text.Json;
|
|
|
|
namespace StellaOps.PolicyDsl;
|
|
|
|
/// <summary>
|
|
/// Serializes policy IR documents to a canonical JSON representation for hashing.
|
|
/// </summary>
|
|
public static class PolicyIrSerializer
|
|
{
|
|
public static ImmutableArray<byte> Serialize(PolicyIrDocument document)
|
|
{
|
|
var buffer = new ArrayBufferWriter<byte>();
|
|
using var writer = new Utf8JsonWriter(buffer, new JsonWriterOptions
|
|
{
|
|
Indented = false,
|
|
SkipValidation = false
|
|
});
|
|
|
|
WriteDocument(writer, document);
|
|
writer.Flush();
|
|
|
|
return buffer.WrittenSpan.ToArray().ToImmutableArray();
|
|
}
|
|
|
|
private static void WriteDocument(Utf8JsonWriter writer, PolicyIrDocument document)
|
|
{
|
|
writer.WriteStartObject();
|
|
writer.WriteString("name", document.Name);
|
|
writer.WriteString("syntax", document.Syntax);
|
|
|
|
writer.WritePropertyName("metadata");
|
|
WriteLiteralDictionary(writer, document.Metadata);
|
|
|
|
writer.WritePropertyName("profiles");
|
|
writer.WriteStartArray();
|
|
foreach (var profile in document.Profiles)
|
|
{
|
|
WriteProfile(writer, profile);
|
|
}
|
|
|
|
writer.WriteEndArray();
|
|
|
|
writer.WritePropertyName("settings");
|
|
WriteLiteralDictionary(writer, document.Settings);
|
|
|
|
writer.WritePropertyName("rules");
|
|
writer.WriteStartArray();
|
|
foreach (var rule in document.Rules)
|
|
{
|
|
WriteRule(writer, rule);
|
|
}
|
|
|
|
writer.WriteEndArray();
|
|
writer.WriteEndObject();
|
|
}
|
|
|
|
private static void WriteProfile(Utf8JsonWriter writer, PolicyIrProfile profile)
|
|
{
|
|
writer.WriteStartObject();
|
|
writer.WriteString("name", profile.Name);
|
|
|
|
writer.WritePropertyName("maps");
|
|
writer.WriteStartArray();
|
|
foreach (var map in profile.Maps)
|
|
{
|
|
writer.WriteStartObject();
|
|
writer.WriteString("name", map.Name);
|
|
writer.WritePropertyName("entries");
|
|
writer.WriteStartArray();
|
|
foreach (var entry in map.Entries)
|
|
{
|
|
writer.WriteStartObject();
|
|
writer.WriteString("source", entry.Source);
|
|
writer.WriteNumber("weight", entry.Weight);
|
|
writer.WriteEndObject();
|
|
}
|
|
|
|
writer.WriteEndArray();
|
|
writer.WriteEndObject();
|
|
}
|
|
|
|
writer.WriteEndArray();
|
|
|
|
writer.WritePropertyName("env");
|
|
writer.WriteStartArray();
|
|
foreach (var env in profile.Environments)
|
|
{
|
|
writer.WriteStartObject();
|
|
writer.WriteString("name", env.Name);
|
|
writer.WritePropertyName("entries");
|
|
writer.WriteStartArray();
|
|
foreach (var entry in env.Entries)
|
|
{
|
|
writer.WriteStartObject();
|
|
writer.WritePropertyName("condition");
|
|
WriteExpression(writer, entry.Condition);
|
|
writer.WriteNumber("weight", entry.Weight);
|
|
writer.WriteEndObject();
|
|
}
|
|
|
|
writer.WriteEndArray();
|
|
writer.WriteEndObject();
|
|
}
|
|
|
|
writer.WriteEndArray();
|
|
|
|
writer.WritePropertyName("scalars");
|
|
writer.WriteStartArray();
|
|
foreach (var scalar in profile.Scalars)
|
|
{
|
|
writer.WriteStartObject();
|
|
writer.WriteString("name", scalar.Name);
|
|
writer.WritePropertyName("value");
|
|
WriteLiteral(writer, scalar.Value);
|
|
writer.WriteEndObject();
|
|
}
|
|
|
|
writer.WriteEndArray();
|
|
writer.WriteEndObject();
|
|
}
|
|
|
|
private static void WriteRule(Utf8JsonWriter writer, PolicyIrRule rule)
|
|
{
|
|
writer.WriteStartObject();
|
|
writer.WriteString("name", rule.Name);
|
|
writer.WriteNumber("priority", rule.Priority);
|
|
writer.WritePropertyName("when");
|
|
WriteExpression(writer, rule.When);
|
|
|
|
writer.WritePropertyName("then");
|
|
WriteActions(writer, rule.ThenActions);
|
|
|
|
writer.WritePropertyName("else");
|
|
WriteActions(writer, rule.ElseActions);
|
|
|
|
writer.WriteString("because", rule.Because);
|
|
writer.WriteEndObject();
|
|
}
|
|
|
|
private static void WriteActions(Utf8JsonWriter writer, ImmutableArray<PolicyIrAction> actions)
|
|
{
|
|
writer.WriteStartArray();
|
|
foreach (var action in actions)
|
|
{
|
|
WriteAction(writer, action);
|
|
}
|
|
|
|
writer.WriteEndArray();
|
|
}
|
|
|
|
private static void WriteAction(Utf8JsonWriter writer, PolicyIrAction action)
|
|
{
|
|
switch (action)
|
|
{
|
|
case PolicyIrAssignmentAction assign:
|
|
writer.WriteStartObject();
|
|
writer.WriteString("type", "assign");
|
|
WriteReference(writer, assign.Target);
|
|
writer.WritePropertyName("value");
|
|
WriteExpression(writer, assign.Value);
|
|
writer.WriteEndObject();
|
|
break;
|
|
case PolicyIrAnnotateAction annotate:
|
|
writer.WriteStartObject();
|
|
writer.WriteString("type", "annotate");
|
|
WriteReference(writer, annotate.Target);
|
|
writer.WritePropertyName("value");
|
|
WriteExpression(writer, annotate.Value);
|
|
writer.WriteEndObject();
|
|
break;
|
|
case PolicyIrIgnoreAction ignore:
|
|
writer.WriteStartObject();
|
|
writer.WriteString("type", "ignore");
|
|
writer.WritePropertyName("until");
|
|
WriteOptionalExpression(writer, ignore.Until);
|
|
writer.WriteString("because", ignore.Because ?? string.Empty);
|
|
writer.WriteEndObject();
|
|
break;
|
|
case PolicyIrEscalateAction escalate:
|
|
writer.WriteStartObject();
|
|
writer.WriteString("type", "escalate");
|
|
writer.WritePropertyName("to");
|
|
WriteOptionalExpression(writer, escalate.To);
|
|
writer.WritePropertyName("when");
|
|
WriteOptionalExpression(writer, escalate.When);
|
|
writer.WriteEndObject();
|
|
break;
|
|
case PolicyIrRequireVexAction require:
|
|
writer.WriteStartObject();
|
|
writer.WriteString("type", "requireVex");
|
|
writer.WritePropertyName("conditions");
|
|
writer.WriteStartObject();
|
|
foreach (var kvp in require.Conditions)
|
|
{
|
|
writer.WritePropertyName(kvp.Key);
|
|
WriteExpression(writer, kvp.Value);
|
|
}
|
|
|
|
writer.WriteEndObject();
|
|
writer.WriteEndObject();
|
|
break;
|
|
case PolicyIrWarnAction warn:
|
|
writer.WriteStartObject();
|
|
writer.WriteString("type", "warn");
|
|
writer.WritePropertyName("message");
|
|
WriteOptionalExpression(writer, warn.Message);
|
|
writer.WriteEndObject();
|
|
break;
|
|
case PolicyIrDeferAction defer:
|
|
writer.WriteStartObject();
|
|
writer.WriteString("type", "defer");
|
|
writer.WritePropertyName("until");
|
|
WriteOptionalExpression(writer, defer.Until);
|
|
writer.WriteEndObject();
|
|
break;
|
|
}
|
|
}
|
|
|
|
private static void WriteReference(Utf8JsonWriter writer, ImmutableArray<string> segments)
|
|
{
|
|
writer.WritePropertyName("target");
|
|
writer.WriteStartArray();
|
|
foreach (var segment in segments)
|
|
{
|
|
writer.WriteStringValue(segment);
|
|
}
|
|
|
|
writer.WriteEndArray();
|
|
}
|
|
|
|
private static void WriteOptionalExpression(Utf8JsonWriter writer, PolicyExpression? expression)
|
|
{
|
|
if (expression is null)
|
|
{
|
|
writer.WriteNullValue();
|
|
return;
|
|
}
|
|
|
|
WriteExpression(writer, expression);
|
|
}
|
|
|
|
private static void WriteExpression(Utf8JsonWriter writer, PolicyExpression expression)
|
|
{
|
|
switch (expression)
|
|
{
|
|
case PolicyLiteralExpression literal:
|
|
writer.WriteStartObject();
|
|
writer.WriteString("type", "literal");
|
|
writer.WritePropertyName("value");
|
|
WriteLiteralValue(writer, literal.Value);
|
|
writer.WriteEndObject();
|
|
break;
|
|
case PolicyListExpression list:
|
|
writer.WriteStartObject();
|
|
writer.WriteString("type", "list");
|
|
writer.WritePropertyName("items");
|
|
writer.WriteStartArray();
|
|
foreach (var item in list.Items)
|
|
{
|
|
WriteExpression(writer, item);
|
|
}
|
|
|
|
writer.WriteEndArray();
|
|
writer.WriteEndObject();
|
|
break;
|
|
case PolicyIdentifierExpression identifier:
|
|
writer.WriteStartObject();
|
|
writer.WriteString("type", "identifier");
|
|
writer.WriteString("name", identifier.Name);
|
|
writer.WriteEndObject();
|
|
break;
|
|
case PolicyMemberAccessExpression member:
|
|
writer.WriteStartObject();
|
|
writer.WriteString("type", "member");
|
|
writer.WritePropertyName("target");
|
|
WriteExpression(writer, member.Target);
|
|
writer.WriteString("member", member.Member);
|
|
writer.WriteEndObject();
|
|
break;
|
|
case PolicyInvocationExpression invocation:
|
|
writer.WriteStartObject();
|
|
writer.WriteString("type", "call");
|
|
writer.WritePropertyName("target");
|
|
WriteExpression(writer, invocation.Target);
|
|
writer.WritePropertyName("args");
|
|
writer.WriteStartArray();
|
|
foreach (var arg in invocation.Arguments)
|
|
{
|
|
WriteExpression(writer, arg);
|
|
}
|
|
|
|
writer.WriteEndArray();
|
|
writer.WriteEndObject();
|
|
break;
|
|
case PolicyIndexerExpression indexer:
|
|
writer.WriteStartObject();
|
|
writer.WriteString("type", "indexer");
|
|
writer.WritePropertyName("target");
|
|
WriteExpression(writer, indexer.Target);
|
|
writer.WritePropertyName("index");
|
|
WriteExpression(writer, indexer.Index);
|
|
writer.WriteEndObject();
|
|
break;
|
|
case PolicyUnaryExpression unary:
|
|
writer.WriteStartObject();
|
|
writer.WriteString("type", "unary");
|
|
writer.WriteString("op", unary.Operator switch
|
|
{
|
|
PolicyUnaryOperator.Not => "not",
|
|
_ => unary.Operator.ToString().ToLowerInvariant(),
|
|
});
|
|
writer.WritePropertyName("operand");
|
|
WriteExpression(writer, unary.Operand);
|
|
writer.WriteEndObject();
|
|
break;
|
|
case PolicyBinaryExpression binary:
|
|
writer.WriteStartObject();
|
|
writer.WriteString("type", "binary");
|
|
writer.WriteString("op", GetBinaryOperator(binary.Operator));
|
|
writer.WritePropertyName("left");
|
|
WriteExpression(writer, binary.Left);
|
|
writer.WritePropertyName("right");
|
|
WriteExpression(writer, binary.Right);
|
|
writer.WriteEndObject();
|
|
break;
|
|
default:
|
|
writer.WriteStartObject();
|
|
writer.WriteString("type", "unknown");
|
|
writer.WriteEndObject();
|
|
break;
|
|
}
|
|
}
|
|
|
|
private static string GetBinaryOperator(PolicyBinaryOperator op) => op switch
|
|
{
|
|
PolicyBinaryOperator.And => "and",
|
|
PolicyBinaryOperator.Or => "or",
|
|
PolicyBinaryOperator.Equal => "eq",
|
|
PolicyBinaryOperator.NotEqual => "neq",
|
|
PolicyBinaryOperator.LessThan => "lt",
|
|
PolicyBinaryOperator.LessThanOrEqual => "lte",
|
|
PolicyBinaryOperator.GreaterThan => "gt",
|
|
PolicyBinaryOperator.GreaterThanOrEqual => "gte",
|
|
PolicyBinaryOperator.In => "in",
|
|
PolicyBinaryOperator.NotIn => "not_in",
|
|
_ => op.ToString().ToLowerInvariant(),
|
|
};
|
|
|
|
private static void WriteLiteralDictionary(Utf8JsonWriter writer, ImmutableSortedDictionary<string, PolicyIrLiteral> dictionary)
|
|
{
|
|
writer.WriteStartObject();
|
|
foreach (var kvp in dictionary)
|
|
{
|
|
writer.WritePropertyName(kvp.Key);
|
|
WriteLiteral(writer, kvp.Value);
|
|
}
|
|
|
|
writer.WriteEndObject();
|
|
}
|
|
|
|
private static void WriteLiteral(Utf8JsonWriter writer, PolicyIrLiteral literal)
|
|
{
|
|
switch (literal)
|
|
{
|
|
case PolicyIrStringLiteral s:
|
|
writer.WriteStringValue(s.Value);
|
|
break;
|
|
case PolicyIrNumberLiteral n:
|
|
writer.WriteNumberValue(n.Value);
|
|
break;
|
|
case PolicyIrBooleanLiteral b:
|
|
writer.WriteBooleanValue(b.Value);
|
|
break;
|
|
case PolicyIrListLiteral list:
|
|
writer.WriteStartArray();
|
|
foreach (var item in list.Items)
|
|
{
|
|
WriteLiteral(writer, item);
|
|
}
|
|
|
|
writer.WriteEndArray();
|
|
break;
|
|
default:
|
|
writer.WriteNullValue();
|
|
break;
|
|
}
|
|
}
|
|
|
|
private static void WriteLiteralValue(Utf8JsonWriter writer, object? value)
|
|
{
|
|
switch (value)
|
|
{
|
|
case null:
|
|
writer.WriteNullValue();
|
|
break;
|
|
case string s:
|
|
writer.WriteStringValue(s);
|
|
break;
|
|
case bool b:
|
|
writer.WriteBooleanValue(b);
|
|
break;
|
|
case decimal dec:
|
|
writer.WriteNumberValue(dec);
|
|
break;
|
|
case double dbl:
|
|
writer.WriteNumberValue(dbl);
|
|
break;
|
|
case int i:
|
|
writer.WriteNumberValue(i);
|
|
break;
|
|
default:
|
|
writer.WriteStringValue(value.ToString());
|
|
break;
|
|
}
|
|
}
|
|
}
|