up
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
api-governance / spectral-lint (push) Has been cancelled
oas-ci / oas-validate (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Policy Simulation / policy-simulate (push) Has been cancelled
SDK Publish & Sign / sdk-publish (push) Has been cancelled

This commit is contained in:
master
2025-11-27 15:05:48 +02:00
parent 4831c7fcb0
commit e950474a77
278 changed files with 81498 additions and 672 deletions

View File

@@ -0,0 +1,213 @@
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using StellaOps.Policy.RiskProfile.Models;
namespace StellaOps.Policy.RiskProfile.Hashing;
/// <summary>
/// Service for computing deterministic hashes of risk profiles.
/// </summary>
public sealed class RiskProfileHasher
{
private static readonly JsonSerializerOptions CanonicalJsonOptions = new()
{
WriteIndented = false,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
Converters =
{
new JsonStringEnumConverter(JsonNamingPolicy.CamelCase),
},
};
/// <summary>
/// Computes a deterministic SHA-256 hash of the risk profile.
/// </summary>
/// <param name="profile">The profile to hash.</param>
/// <returns>Lowercase hex-encoded SHA-256 hash.</returns>
public string ComputeHash(RiskProfileModel profile)
{
ArgumentNullException.ThrowIfNull(profile);
var canonical = CreateCanonicalForm(profile);
var json = JsonSerializer.Serialize(canonical, CanonicalJsonOptions);
var hash = SHA256.HashData(Encoding.UTF8.GetBytes(json));
return Convert.ToHexStringLower(hash);
}
/// <summary>
/// Computes a deterministic content hash that ignores identity fields (id, version).
/// Useful for detecting semantic changes regardless of versioning.
/// </summary>
/// <param name="profile">The profile to hash.</param>
/// <returns>Lowercase hex-encoded SHA-256 hash.</returns>
public string ComputeContentHash(RiskProfileModel profile)
{
ArgumentNullException.ThrowIfNull(profile);
var canonical = CreateCanonicalContentForm(profile);
var json = JsonSerializer.Serialize(canonical, CanonicalJsonOptions);
var hash = SHA256.HashData(Encoding.UTF8.GetBytes(json));
return Convert.ToHexStringLower(hash);
}
/// <summary>
/// Verifies that two profiles have the same semantic content (ignoring identity fields).
/// </summary>
public bool AreEquivalent(RiskProfileModel profile1, RiskProfileModel profile2)
{
ArgumentNullException.ThrowIfNull(profile1);
ArgumentNullException.ThrowIfNull(profile2);
return ComputeContentHash(profile1) == ComputeContentHash(profile2);
}
private static CanonicalRiskProfile CreateCanonicalForm(RiskProfileModel profile)
{
return new CanonicalRiskProfile
{
Id = profile.Id,
Version = profile.Version,
Description = profile.Description,
Extends = profile.Extends,
Signals = CreateCanonicalSignals(profile.Signals),
Weights = CreateCanonicalWeights(profile.Weights),
Overrides = CreateCanonicalOverrides(profile.Overrides),
Metadata = CreateCanonicalMetadata(profile.Metadata),
};
}
private static CanonicalRiskProfileContent CreateCanonicalContentForm(RiskProfileModel profile)
{
return new CanonicalRiskProfileContent
{
Signals = CreateCanonicalSignals(profile.Signals),
Weights = CreateCanonicalWeights(profile.Weights),
Overrides = CreateCanonicalOverrides(profile.Overrides),
};
}
private static List<CanonicalSignal> CreateCanonicalSignals(List<RiskSignal> signals)
{
return signals
.OrderBy(s => s.Name, StringComparer.Ordinal)
.Select(s => new CanonicalSignal
{
Name = s.Name,
Source = s.Source,
Type = s.Type.ToString().ToLowerInvariant(),
Path = s.Path,
Transform = s.Transform,
Unit = s.Unit,
})
.ToList();
}
private static SortedDictionary<string, double> CreateCanonicalWeights(Dictionary<string, double> weights)
{
return new SortedDictionary<string, double>(weights, StringComparer.Ordinal);
}
private static CanonicalOverrides CreateCanonicalOverrides(RiskOverrides overrides)
{
return new CanonicalOverrides
{
Severity = overrides.Severity
.Select(CreateCanonicalSeverityOverride)
.ToList(),
Decisions = overrides.Decisions
.Select(CreateCanonicalDecisionOverride)
.ToList(),
};
}
private static CanonicalSeverityOverride CreateCanonicalSeverityOverride(SeverityOverride rule)
{
return new CanonicalSeverityOverride
{
When = CreateCanonicalWhen(rule.When),
Set = rule.Set.ToString().ToLowerInvariant(),
};
}
private static CanonicalDecisionOverride CreateCanonicalDecisionOverride(DecisionOverride rule)
{
return new CanonicalDecisionOverride
{
When = CreateCanonicalWhen(rule.When),
Action = rule.Action.ToString().ToLowerInvariant(),
Reason = rule.Reason,
};
}
private static SortedDictionary<string, object> CreateCanonicalWhen(Dictionary<string, object> when)
{
return new SortedDictionary<string, object>(when, StringComparer.Ordinal);
}
private static SortedDictionary<string, object?>? CreateCanonicalMetadata(Dictionary<string, object?>? metadata)
{
if (metadata == null || metadata.Count == 0)
{
return null;
}
return new SortedDictionary<string, object?>(metadata, StringComparer.Ordinal);
}
#region Canonical Form Types
private sealed class CanonicalRiskProfile
{
public required string Id { get; init; }
public required string Version { get; init; }
public string? Description { get; init; }
public string? Extends { get; init; }
public required List<CanonicalSignal> Signals { get; init; }
public required SortedDictionary<string, double> Weights { get; init; }
public required CanonicalOverrides Overrides { get; init; }
public SortedDictionary<string, object?>? Metadata { get; init; }
}
private sealed class CanonicalRiskProfileContent
{
public required List<CanonicalSignal> Signals { get; init; }
public required SortedDictionary<string, double> Weights { get; init; }
public required CanonicalOverrides Overrides { get; init; }
}
private sealed class CanonicalSignal
{
public required string Name { get; init; }
public required string Source { get; init; }
public required string Type { get; init; }
public string? Path { get; init; }
public string? Transform { get; init; }
public string? Unit { get; init; }
}
private sealed class CanonicalOverrides
{
public required List<CanonicalSeverityOverride> Severity { get; init; }
public required List<CanonicalDecisionOverride> Decisions { get; init; }
}
private sealed class CanonicalSeverityOverride
{
public required SortedDictionary<string, object> When { get; init; }
public required string Set { get; init; }
}
private sealed class CanonicalDecisionOverride
{
public required SortedDictionary<string, object> When { get; init; }
public required string Action { get; init; }
public string? Reason { get; init; }
}
#endregion
}