using System.Text.Json.Nodes; using StellaOps.Findings.Ledger.Domain; namespace StellaOps.Findings.Ledger.Hashing; public static class ProjectionHashing { private const string TenantIdProperty = nameof(FindingProjection.TenantId); private const string FindingIdProperty = nameof(FindingProjection.FindingId); private const string PolicyVersionProperty = nameof(FindingProjection.PolicyVersion); private const string StatusProperty = nameof(FindingProjection.Status); private const string SeverityProperty = nameof(FindingProjection.Severity); private const string LabelsProperty = nameof(FindingProjection.Labels); private const string CurrentEventIdProperty = nameof(FindingProjection.CurrentEventId); private const string ExplainRefProperty = nameof(FindingProjection.ExplainRef); private const string PolicyRationaleProperty = nameof(FindingProjection.PolicyRationale); private const string UpdatedAtProperty = nameof(FindingProjection.UpdatedAt); public static string ComputeCycleHash(FindingProjection projection) { ArgumentNullException.ThrowIfNull(projection); var envelope = new JsonObject { [TenantIdProperty] = projection.TenantId, [FindingIdProperty] = projection.FindingId, [PolicyVersionProperty] = projection.PolicyVersion, [StatusProperty] = projection.Status, [SeverityProperty] = projection.Severity, [LabelsProperty] = projection.Labels.DeepClone(), [CurrentEventIdProperty] = projection.CurrentEventId.ToString(), [ExplainRefProperty] = projection.ExplainRef, [PolicyRationaleProperty] = CloneArray(projection.PolicyRationale), [UpdatedAtProperty] = FormatTimestamp(projection.UpdatedAt) }; var canonical = LedgerCanonicalJsonSerializer.Canonicalize(envelope); var canonicalJson = LedgerCanonicalJsonSerializer.Serialize(canonical); return HashUtilities.ComputeSha256Hex(canonicalJson); } private static string FormatTimestamp(DateTimeOffset value) { var utc = value.ToUniversalTime(); Span buffer = stackalloc char[24]; WriteFourDigits(buffer, 0, utc.Year); buffer[4] = '-'; WriteTwoDigits(buffer, 5, utc.Month); buffer[7] = '-'; WriteTwoDigits(buffer, 8, utc.Day); buffer[10] = 'T'; WriteTwoDigits(buffer, 11, utc.Hour); buffer[13] = ':'; WriteTwoDigits(buffer, 14, utc.Minute); buffer[16] = ':'; WriteTwoDigits(buffer, 17, utc.Second); buffer[19] = '.'; WriteThreeDigits(buffer, 20, utc.Millisecond); buffer[23] = 'Z'; return new string(buffer); } private static void WriteFourDigits(Span buffer, int offset, int value) { buffer[offset] = (char)('0' + (value / 1000) % 10); buffer[offset + 1] = (char)('0' + (value / 100) % 10); buffer[offset + 2] = (char)('0' + (value / 10) % 10); buffer[offset + 3] = (char)('0' + value % 10); } private static void WriteTwoDigits(Span buffer, int offset, int value) { buffer[offset] = (char)('0' + (value / 10) % 10); buffer[offset + 1] = (char)('0' + value % 10); } private static void WriteThreeDigits(Span buffer, int offset, int value) { buffer[offset] = (char)('0' + (value / 100) % 10); buffer[offset + 1] = (char)('0' + (value / 10) % 10); buffer[offset + 2] = (char)('0' + value % 10); } private static JsonArray CloneArray(JsonArray array) { ArgumentNullException.ThrowIfNull(array); var clone = new JsonArray(); foreach (var item in array) { clone.Add(item?.DeepClone()); } return clone; } }