save progress

This commit is contained in:
StellaOps Bot
2026-01-02 21:06:27 +02:00
parent f46bde5575
commit 3f197814c5
441 changed files with 21545 additions and 4306 deletions

View File

@@ -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);
}
}
}

View File

@@ -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() ?? "",
_ => ""
};
}
}

View File

@@ -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",

View File

@@ -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
};

View File

@@ -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>

View File

@@ -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;
}
}

View File

@@ -5,7 +5,7 @@
<LangVersion>preview</LangVersion>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>

View File

@@ -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. |