using System; using System.Buffers; using System.Collections.Immutable; using System.Linq; using System.Security.Cryptography; using System.Text.Json; namespace StellaOps.Policy; public static class PolicyDigest { public static string Compute(PolicyDocument document) { if (document is null) { throw new ArgumentNullException(nameof(document)); } var buffer = new ArrayBufferWriter(); using (var writer = new Utf8JsonWriter(buffer, new JsonWriterOptions { SkipValidation = true, })) { WriteDocument(writer, document); } var hash = SHA256.HashData(buffer.WrittenSpan); return Convert.ToHexString(hash).ToLowerInvariant(); } private static void WriteDocument(Utf8JsonWriter writer, PolicyDocument document) { writer.WriteStartObject(); writer.WriteString("version", document.Version); if (!document.Metadata.IsEmpty) { writer.WritePropertyName("metadata"); writer.WriteStartObject(); foreach (var pair in document.Metadata.OrderBy(static kvp => kvp.Key, StringComparer.Ordinal)) { writer.WriteString(pair.Key, pair.Value); } writer.WriteEndObject(); } writer.WritePropertyName("rules"); writer.WriteStartArray(); foreach (var rule in document.Rules) { WriteRule(writer, rule); } writer.WriteEndArray(); if (!document.Exceptions.Effects.IsDefaultOrEmpty || !document.Exceptions.RoutingTemplates.IsDefaultOrEmpty) { writer.WritePropertyName("exceptions"); writer.WriteStartObject(); if (!document.Exceptions.Effects.IsDefaultOrEmpty) { writer.WritePropertyName("effects"); writer.WriteStartArray(); foreach (var effect in document.Exceptions.Effects .OrderBy(static e => e.Id, StringComparer.Ordinal)) { WriteExceptionEffect(writer, effect); } writer.WriteEndArray(); } if (!document.Exceptions.RoutingTemplates.IsDefaultOrEmpty) { writer.WritePropertyName("routingTemplates"); writer.WriteStartArray(); foreach (var template in document.Exceptions.RoutingTemplates .OrderBy(static t => t.Id, StringComparer.Ordinal)) { WriteExceptionRoutingTemplate(writer, template); } writer.WriteEndArray(); } writer.WriteEndObject(); } writer.WriteEndObject(); writer.Flush(); } private static void WriteRule(Utf8JsonWriter writer, PolicyRule rule) { writer.WriteStartObject(); writer.WriteString("name", rule.Name); if (!string.IsNullOrWhiteSpace(rule.Identifier)) { writer.WriteString("id", rule.Identifier); } if (!string.IsNullOrWhiteSpace(rule.Description)) { writer.WriteString("description", rule.Description); } WriteMetadata(writer, rule.Metadata); WriteSeverities(writer, rule.Severities); WriteStringArray(writer, "environments", rule.Environments); WriteStringArray(writer, "sources", rule.Sources); WriteStringArray(writer, "vendors", rule.Vendors); WriteStringArray(writer, "licenses", rule.Licenses); WriteStringArray(writer, "tags", rule.Tags); if (!rule.Match.IsEmpty) { writer.WritePropertyName("match"); writer.WriteStartObject(); WriteStringArray(writer, "images", rule.Match.Images); WriteStringArray(writer, "repositories", rule.Match.Repositories); WriteStringArray(writer, "packages", rule.Match.Packages); WriteStringArray(writer, "purls", rule.Match.Purls); WriteStringArray(writer, "cves", rule.Match.Cves); WriteStringArray(writer, "paths", rule.Match.Paths); WriteStringArray(writer, "layerDigests", rule.Match.LayerDigests); WriteStringArray(writer, "usedByEntrypoint", rule.Match.UsedByEntrypoint); writer.WriteEndObject(); } WriteAction(writer, rule.Action); if (rule.Expires is DateTimeOffset expires) { writer.WriteString("expires", expires.ToUniversalTime().ToString("O")); } if (!string.IsNullOrWhiteSpace(rule.Justification)) { writer.WriteString("justification", rule.Justification); } writer.WriteEndObject(); } private static void WriteAction(Utf8JsonWriter writer, PolicyAction action) { writer.WritePropertyName("action"); writer.WriteStartObject(); writer.WriteString("type", action.Type.ToString().ToLowerInvariant()); if (action.Quiet) { writer.WriteBoolean("quiet", true); } if (action.Ignore is { } ignore) { if (ignore.Until is DateTimeOffset until) { writer.WriteString("until", until.ToUniversalTime().ToString("O")); } if (!string.IsNullOrWhiteSpace(ignore.Justification)) { writer.WriteString("justification", ignore.Justification); } } if (action.Escalate is { } escalate) { if (escalate.MinimumSeverity is { } severity) { writer.WriteString("severity", severity.ToString()); } if (escalate.RequireKev) { writer.WriteBoolean("kev", true); } if (escalate.MinimumEpss is double epss) { writer.WriteNumber("epss", epss); } } if (action.RequireVex is { } requireVex) { WriteStringArray(writer, "vendors", requireVex.Vendors); WriteStringArray(writer, "justifications", requireVex.Justifications); } writer.WriteEndObject(); } private static void WriteMetadata(Utf8JsonWriter writer, ImmutableDictionary metadata) { if (metadata.IsEmpty) { return; } writer.WritePropertyName("metadata"); writer.WriteStartObject(); foreach (var pair in metadata.OrderBy(static kvp => kvp.Key, StringComparer.Ordinal)) { writer.WriteString(pair.Key, pair.Value); } writer.WriteEndObject(); } private static void WriteSeverities(Utf8JsonWriter writer, ImmutableArray severities) { if (severities.IsDefaultOrEmpty) { return; } writer.WritePropertyName("severity"); writer.WriteStartArray(); foreach (var severity in severities) { writer.WriteStringValue(severity.ToString()); } writer.WriteEndArray(); } private static void WriteStringArray(Utf8JsonWriter writer, string propertyName, ImmutableArray values) { if (values.IsDefaultOrEmpty) { return; } writer.WritePropertyName(propertyName); writer.WriteStartArray(); foreach (var value in values) { writer.WriteStringValue(value); } writer.WriteEndArray(); } private static void WriteExceptionEffect(Utf8JsonWriter writer, PolicyExceptionEffect effect) { writer.WriteStartObject(); writer.WriteString("id", effect.Id); if (!string.IsNullOrWhiteSpace(effect.Name)) { writer.WriteString("name", effect.Name); } writer.WriteString("effect", effect.Effect.ToString().ToLowerInvariant()); if (effect.DowngradeSeverity is { } downgradeSeverity) { writer.WriteString("downgradeSeverity", downgradeSeverity.ToString()); } if (!string.IsNullOrWhiteSpace(effect.RequiredControlId)) { writer.WriteString("requiredControlId", effect.RequiredControlId); } if (!string.IsNullOrWhiteSpace(effect.RoutingTemplate)) { writer.WriteString("routingTemplate", effect.RoutingTemplate); } if (effect.MaxDurationDays is int maxDurationDays) { writer.WriteNumber("maxDurationDays", maxDurationDays); } if (!string.IsNullOrWhiteSpace(effect.Description)) { writer.WriteString("description", effect.Description); } writer.WriteEndObject(); } private static void WriteExceptionRoutingTemplate(Utf8JsonWriter writer, PolicyExceptionRoutingTemplate template) { writer.WriteStartObject(); writer.WriteString("id", template.Id); writer.WriteString("authorityRouteId", template.AuthorityRouteId); if (template.RequireMfa) { writer.WriteBoolean("requireMfa", true); } if (!string.IsNullOrWhiteSpace(template.Description)) { writer.WriteString("description", template.Description); } writer.WriteEndObject(); } }