save progress
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using StellaOps.Attestor.ProofChain.Json;
|
||||
|
||||
namespace StellaOps.Attestor.StandardPredicates;
|
||||
|
||||
@@ -10,6 +10,8 @@ namespace StellaOps.Attestor.StandardPredicates;
|
||||
/// </summary>
|
||||
public static class JsonCanonicalizer
|
||||
{
|
||||
private static readonly Rfc8785JsonCanonicalizer Canonicalizer = new();
|
||||
|
||||
/// <summary>
|
||||
/// Canonicalize JSON according to RFC 8785.
|
||||
/// </summary>
|
||||
@@ -17,11 +19,14 @@ public static class JsonCanonicalizer
|
||||
/// <returns>Canonical JSON (minified, lexicographically sorted keys, stable number format)</returns>
|
||||
public static string Canonicalize(string json)
|
||||
{
|
||||
var node = JsonNode.Parse(json);
|
||||
if (node == null)
|
||||
return "null";
|
||||
if (json is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(json));
|
||||
}
|
||||
|
||||
return CanonicalizeNode(node);
|
||||
var bytes = Encoding.UTF8.GetBytes(json);
|
||||
var canonical = Canonicalizer.Canonicalize(bytes);
|
||||
return Encoding.UTF8.GetString(canonical);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -32,114 +37,4 @@ public static class JsonCanonicalizer
|
||||
var json = element.GetRawText();
|
||||
return Canonicalize(json);
|
||||
}
|
||||
|
||||
private static string CanonicalizeNode(JsonNode node)
|
||||
{
|
||||
switch (node)
|
||||
{
|
||||
case JsonObject obj:
|
||||
return CanonicalizeObject(obj);
|
||||
|
||||
case JsonArray arr:
|
||||
return CanonicalizeArray(arr);
|
||||
|
||||
case JsonValue val:
|
||||
return CanonicalizeValue(val);
|
||||
|
||||
default:
|
||||
return "null";
|
||||
}
|
||||
}
|
||||
|
||||
private static string CanonicalizeObject(JsonObject obj)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.Append('{');
|
||||
|
||||
var sortedKeys = obj.Select(kvp => kvp.Key).OrderBy(k => k, StringComparer.Ordinal);
|
||||
var first = true;
|
||||
|
||||
foreach (var key in sortedKeys)
|
||||
{
|
||||
if (!first)
|
||||
sb.Append(',');
|
||||
first = false;
|
||||
|
||||
// Escape key according to JSON rules
|
||||
sb.Append(JsonSerializer.Serialize(key));
|
||||
sb.Append(':');
|
||||
|
||||
var value = obj[key];
|
||||
if (value != null)
|
||||
{
|
||||
sb.Append(CanonicalizeNode(value));
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.Append("null");
|
||||
}
|
||||
}
|
||||
|
||||
sb.Append('}');
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
private static string CanonicalizeArray(JsonArray arr)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.Append('[');
|
||||
|
||||
for (int i = 0; i < arr.Count; i++)
|
||||
{
|
||||
if (i > 0)
|
||||
sb.Append(',');
|
||||
|
||||
var item = arr[i];
|
||||
if (item != null)
|
||||
{
|
||||
sb.Append(CanonicalizeNode(item));
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.Append("null");
|
||||
}
|
||||
}
|
||||
|
||||
sb.Append(']');
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
private static string CanonicalizeValue(JsonValue val)
|
||||
{
|
||||
// Let System.Text.Json handle proper escaping and number formatting
|
||||
var jsonElement = JsonSerializer.SerializeToElement(val);
|
||||
|
||||
switch (jsonElement.ValueKind)
|
||||
{
|
||||
case JsonValueKind.String:
|
||||
return JsonSerializer.Serialize(jsonElement.GetString());
|
||||
|
||||
case JsonValueKind.Number:
|
||||
// Use ToString to get deterministic number representation
|
||||
var number = jsonElement.GetDouble();
|
||||
// Check if it's actually an integer
|
||||
if (number == Math.Floor(number) && number >= long.MinValue && number <= long.MaxValue)
|
||||
{
|
||||
return jsonElement.GetInt64().ToString();
|
||||
}
|
||||
return number.ToString("G17"); // Full precision, no trailing zeros
|
||||
|
||||
case JsonValueKind.True:
|
||||
return "true";
|
||||
|
||||
case JsonValueKind.False:
|
||||
return "false";
|
||||
|
||||
case JsonValueKind.Null:
|
||||
return "null";
|
||||
|
||||
default:
|
||||
return JsonSerializer.Serialize(jsonElement);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
@@ -173,15 +175,15 @@ public sealed class CycloneDxPredicateParser : IPredicateParser
|
||||
}
|
||||
}
|
||||
|
||||
private Dictionary<string, string> ExtractMetadata(JsonElement payload)
|
||||
private IReadOnlyDictionary<string, string> ExtractMetadata(JsonElement payload)
|
||||
{
|
||||
var metadata = new Dictionary<string, string>();
|
||||
var metadata = new SortedDictionary<string, string>(StringComparer.Ordinal);
|
||||
|
||||
if (payload.TryGetProperty("specVersion", out var specVersion))
|
||||
metadata["specVersion"] = specVersion.GetString() ?? "";
|
||||
|
||||
if (payload.TryGetProperty("version", out var version))
|
||||
metadata["version"] = version.GetInt32().ToString();
|
||||
metadata["version"] = ReadVersionValue(version);
|
||||
|
||||
if (payload.TryGetProperty("serialNumber", out var serialNumber))
|
||||
metadata["serialNumber"] = serialNumber.GetString() ?? "";
|
||||
@@ -217,4 +219,15 @@ public sealed class CycloneDxPredicateParser : IPredicateParser
|
||||
|
||||
return metadata;
|
||||
}
|
||||
|
||||
private static string ReadVersionValue(JsonElement version)
|
||||
{
|
||||
return version.ValueKind switch
|
||||
{
|
||||
JsonValueKind.Number when version.TryGetInt32(out var numeric) => numeric.ToString(CultureInfo.InvariantCulture),
|
||||
JsonValueKind.Number => version.GetDouble().ToString(CultureInfo.InvariantCulture),
|
||||
JsonValueKind.String => version.GetString() ?? "",
|
||||
_ => ""
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Text.Json;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
@@ -164,9 +166,9 @@ public sealed class SlsaProvenancePredicateParser : IPredicateParser
|
||||
}
|
||||
}
|
||||
|
||||
private Dictionary<string, string> ExtractMetadata(JsonElement payload)
|
||||
private IReadOnlyDictionary<string, string> ExtractMetadata(JsonElement payload)
|
||||
{
|
||||
var metadata = new Dictionary<string, string>();
|
||||
var metadata = new SortedDictionary<string, string>(StringComparer.Ordinal);
|
||||
|
||||
// Extract build definition metadata
|
||||
if (payload.TryGetProperty("buildDefinition", out var buildDef))
|
||||
@@ -253,7 +255,7 @@ public sealed class SlsaProvenancePredicateParser : IPredicateParser
|
||||
return element.ValueKind switch
|
||||
{
|
||||
JsonValueKind.String => element.GetString() ?? "",
|
||||
JsonValueKind.Number => element.GetDouble().ToString(),
|
||||
JsonValueKind.Number => element.GetDouble().ToString(CultureInfo.InvariantCulture),
|
||||
JsonValueKind.True => "true",
|
||||
JsonValueKind.False => "false",
|
||||
JsonValueKind.Null => "null",
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
@@ -198,9 +199,9 @@ public sealed class SpdxPredicateParser : IPredicateParser
|
||||
}
|
||||
}
|
||||
|
||||
private Dictionary<string, string> ExtractMetadata(JsonElement payload, string version)
|
||||
private IReadOnlyDictionary<string, string> ExtractMetadata(JsonElement payload, string version)
|
||||
{
|
||||
var metadata = new Dictionary<string, string>
|
||||
var metadata = new SortedDictionary<string, string>(StringComparer.Ordinal)
|
||||
{
|
||||
["spdxVersion"] = version
|
||||
};
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace StellaOps.Attestor.StandardPredicates;
|
||||
|
||||
/// <summary>
|
||||
@@ -49,7 +51,8 @@ public sealed record PredicateMetadata
|
||||
/// <summary>
|
||||
/// Additional properties extracted from the predicate.
|
||||
/// </summary>
|
||||
public Dictionary<string, string> Properties { get; init; } = new();
|
||||
public IReadOnlyDictionary<string, string> Properties { get; init; }
|
||||
= new SortedDictionary<string, string>(StringComparer.Ordinal);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -37,7 +37,19 @@ public sealed class StandardPredicateRegistry : IStandardPredicateRegistry
|
||||
/// <returns>True if parser found, false otherwise</returns>
|
||||
public bool TryGetParser(string predicateType, [NotNullWhen(true)] out IPredicateParser? parser)
|
||||
{
|
||||
return _parsers.TryGetValue(predicateType, out parser);
|
||||
if (_parsers.TryGetValue(predicateType, out parser))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var normalized = NormalizePredicateType(predicateType);
|
||||
if (normalized != null && _parsers.TryGetValue(normalized, out parser))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
parser = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -51,4 +63,24 @@ public sealed class StandardPredicateRegistry : IStandardPredicateRegistry
|
||||
.ToList()
|
||||
.AsReadOnly();
|
||||
}
|
||||
|
||||
private static string? NormalizePredicateType(string predicateType)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(predicateType))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (predicateType.StartsWith("https://cyclonedx.org/bom/", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return "https://cyclonedx.org/bom";
|
||||
}
|
||||
|
||||
if (predicateType.StartsWith("https://spdx.org/spdxdocs/spdx-v2.", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return "https://spdx.dev/Document";
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<LangVersion>preview</LangVersion>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
</PropertyGroup>
|
||||
|
||||
|
||||
@@ -7,4 +7,4 @@ Source of truth: `docs/implplan/SPRINT_20251229_049_BE_csproj_audit_maint_tests.
|
||||
| --- | --- | --- |
|
||||
| AUDIT-0064-M | DONE | Maintainability audit for StellaOps.Attestor.StandardPredicates. |
|
||||
| AUDIT-0064-T | DONE | Test coverage audit for StellaOps.Attestor.StandardPredicates. |
|
||||
| AUDIT-0064-A | TODO | Pending approval for changes. |
|
||||
| AUDIT-0064-A | DONE | Applied canonicalization, registry normalization, parser metadata fixes, tests. |
|
||||
|
||||
Reference in New Issue
Block a user