up
Some checks failed
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Docs CI / lint-and-preview (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled

This commit is contained in:
StellaOps Bot
2025-12-01 21:16:22 +02:00
parent c11d87d252
commit 909d9b6220
208 changed files with 860954 additions and 832 deletions

View File

@@ -116,7 +116,9 @@ public sealed class ReceiptBuilder : IReceiptBuilder
}),
AmendsReceiptId = null,
IsActive = true,
SupersededReason = null
SupersedesReceiptId = null,
SupersededReason = null,
ExportHash = null
};
if (request.SigningKey is not null)
@@ -166,57 +168,24 @@ public sealed class ReceiptBuilder : IReceiptBuilder
string vector,
ImmutableList<CvssEvidenceItem> evidence)
{
using var stream = new MemoryStream();
using var writer = new Utf8JsonWriter(stream, new JsonWriterOptions
using var doc = JsonDocument.Parse(JsonSerializer.Serialize(new
{
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
Indented = false
});
vulnerabilityId = request.VulnerabilityId,
tenantId = request.TenantId,
policyId = policyRef.PolicyId,
policyVersion = policyRef.Version,
policyHash = policyRef.Hash,
vector,
baseMetrics = request.BaseMetrics,
threatMetrics = request.ThreatMetrics,
environmentalMetrics = request.EnvironmentalMetrics,
supplementalMetrics = request.SupplementalMetrics,
scores,
evidence
}, SerializerOptions));
writer.WriteStartObject();
writer.WriteString("vulnerabilityId", request.VulnerabilityId);
writer.WriteString("tenantId", request.TenantId);
writer.WriteString("policyId", policyRef.PolicyId);
writer.WriteString("policyVersion", policyRef.Version);
writer.WriteString("policyHash", policyRef.Hash);
writer.WriteString("vector", vector);
writer.WritePropertyName("baseMetrics");
WriteCanonical(JsonSerializer.SerializeToElement(request.BaseMetrics, SerializerOptions), writer);
writer.WritePropertyName("threatMetrics");
if (request.ThreatMetrics is not null)
WriteCanonical(JsonSerializer.SerializeToElement(request.ThreatMetrics, SerializerOptions), writer);
else
writer.WriteNullValue();
writer.WritePropertyName("environmentalMetrics");
if (request.EnvironmentalMetrics is not null)
WriteCanonical(JsonSerializer.SerializeToElement(request.EnvironmentalMetrics, SerializerOptions), writer);
else
writer.WriteNullValue();
writer.WritePropertyName("supplementalMetrics");
if (request.SupplementalMetrics is not null)
WriteCanonical(JsonSerializer.SerializeToElement(request.SupplementalMetrics, SerializerOptions), writer);
else
writer.WriteNullValue();
writer.WritePropertyName("scores");
WriteCanonical(JsonSerializer.SerializeToElement(scores, SerializerOptions), writer);
writer.WritePropertyName("evidence");
writer.WriteStartArray();
foreach (var ev in evidence)
{
WriteCanonical(JsonSerializer.SerializeToElement(ev, SerializerOptions), writer);
}
writer.WriteEndArray();
writer.WriteEndObject();
writer.Flush();
var hash = SHA256.HashData(stream.ToArray());
var canonicalBytes = ReceiptCanonicalizer.ToCanonicalBytes(doc.RootElement);
var hash = SHA256.HashData(canonicalBytes);
return Convert.ToHexString(hash).ToLowerInvariant();
}

View File

@@ -0,0 +1,84 @@
using System.Globalization;
using System.Text.Encodings.Web;
using System.Text.Json;
namespace StellaOps.Policy.Scoring.Receipts;
/// <summary>
/// Provides deterministic JSON canonicalization for receipt hashing.
/// Keys are sorted, numbers use invariant culture, and timestamps are formatted as ISO 8601 (O).
/// </summary>
internal static class ReceiptCanonicalizer
{
private static readonly JsonWriterOptions WriterOptions = new()
{
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
Indented = false
};
public static byte[] ToCanonicalBytes(JsonElement element)
{
using var stream = new MemoryStream();
using var writer = new Utf8JsonWriter(stream, WriterOptions);
Write(element, writer);
writer.Flush();
return stream.ToArray();
}
public static void Write(JsonElement element, Utf8JsonWriter writer)
{
switch (element.ValueKind)
{
case JsonValueKind.Object:
writer.WriteStartObject();
foreach (var prop in element.EnumerateObject().OrderBy(p => p.Name, StringComparer.Ordinal))
{
writer.WritePropertyName(prop.Name);
Write(prop.Value, writer);
}
writer.WriteEndObject();
break;
case JsonValueKind.Array:
writer.WriteStartArray();
foreach (var item in element.EnumerateArray())
{
Write(item, writer);
}
writer.WriteEndArray();
break;
case JsonValueKind.String:
// If the value looks like a timestamp, normalize to ISO 8601 round-trip
if (DateTimeOffset.TryParse(element.GetString(), CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out var dto))
{
writer.WriteStringValue(dto.ToUniversalTime().ToString("O"));
}
else
{
writer.WriteStringValue(element.GetString());
}
break;
case JsonValueKind.Number:
if (element.TryGetDouble(out var dbl))
{
writer.WriteRawValue(dbl.ToString("0.################", CultureInfo.InvariantCulture), skipInputValidation: true);
}
else
{
writer.WriteRawValue(element.GetRawText(), skipInputValidation: true);
}
break;
case JsonValueKind.True:
writer.WriteBooleanValue(true);
break;
case JsonValueKind.False:
writer.WriteBooleanValue(false);
break;
case JsonValueKind.Null:
case JsonValueKind.Undefined:
writer.WriteNullValue();
break;
default:
throw new InvalidOperationException($"Unsupported JSON value kind: {element.ValueKind}");
}
}
}