Add Canonical JSON serialization library with tests and documentation

- Implemented CanonJson class for deterministic JSON serialization and hashing.
- Added unit tests for CanonJson functionality, covering various scenarios including key sorting, handling of nested objects, arrays, and special characters.
- Created project files for the Canonical JSON library and its tests, including necessary package references.
- Added README.md for library usage and API reference.
- Introduced RabbitMqIntegrationFactAttribute for conditional RabbitMQ integration tests.
This commit is contained in:
master
2025-12-19 15:35:00 +02:00
parent 43882078a4
commit 951a38d561
192 changed files with 27550 additions and 2611 deletions

View File

@@ -0,0 +1,221 @@
// -----------------------------------------------------------------------------
// ReachabilityDriftPredicate.cs
// Sprint: SPRINT_3600_0004_0001_ui_evidence_chain
// Task: UI-014
// Description: DSSE predicate for reachability drift attestation.
// -----------------------------------------------------------------------------
using System.Collections.Immutable;
using System.Text.Json.Serialization;
namespace StellaOps.Attestor.ProofChain.Predicates;
/// <summary>
/// DSSE predicate for reachability drift attestation.
/// predicateType: stellaops.dev/predicates/reachability-drift@v1
/// </summary>
public sealed record ReachabilityDriftPredicate
{
/// <summary>
/// The predicate type URI for reachability drift attestations.
/// </summary>
public const string PredicateType = "stellaops.dev/predicates/reachability-drift@v1";
/// <summary>
/// Reference to the base (previous) image being compared.
/// </summary>
[JsonPropertyName("baseImage")]
public required DriftImageReference BaseImage { get; init; }
/// <summary>
/// Reference to the target (current) image being compared.
/// </summary>
[JsonPropertyName("targetImage")]
public required DriftImageReference TargetImage { get; init; }
/// <summary>
/// Scan ID of the baseline scan.
/// </summary>
[JsonPropertyName("baseScanId")]
public required string BaseScanId { get; init; }
/// <summary>
/// Scan ID of the head (current) scan.
/// </summary>
[JsonPropertyName("headScanId")]
public required string HeadScanId { get; init; }
/// <summary>
/// Summary of detected drift.
/// </summary>
[JsonPropertyName("drift")]
public required DriftPredicateSummary Drift { get; init; }
/// <summary>
/// Metadata about the analysis performed.
/// </summary>
[JsonPropertyName("analysis")]
public required DriftAnalysisMetadata Analysis { get; init; }
}
/// <summary>
/// Reference to a container image in drift analysis.
/// </summary>
public sealed record DriftImageReference
{
/// <summary>
/// Image name (repository/image).
/// </summary>
[JsonPropertyName("name")]
public required string Name { get; init; }
/// <summary>
/// Image digest (sha256:...).
/// </summary>
[JsonPropertyName("digest")]
public required string Digest { get; init; }
/// <summary>
/// Optional tag at time of analysis.
/// </summary>
[JsonPropertyName("tag")]
public string? Tag { get; init; }
}
/// <summary>
/// Summary of drift detection results for the predicate.
/// </summary>
public sealed record DriftPredicateSummary
{
/// <summary>
/// Number of sinks that became reachable.
/// </summary>
[JsonPropertyName("newlyReachableCount")]
public required int NewlyReachableCount { get; init; }
/// <summary>
/// Number of sinks that became unreachable.
/// </summary>
[JsonPropertyName("newlyUnreachableCount")]
public required int NewlyUnreachableCount { get; init; }
/// <summary>
/// Details of newly reachable sinks.
/// </summary>
[JsonPropertyName("newlyReachable")]
public required ImmutableArray<DriftedSinkPredicateSummary> NewlyReachable { get; init; }
/// <summary>
/// Details of newly unreachable (mitigated) sinks.
/// </summary>
[JsonPropertyName("newlyUnreachable")]
public required ImmutableArray<DriftedSinkPredicateSummary> NewlyUnreachable { get; init; }
}
/// <summary>
/// Summary of a single drifted sink for inclusion in the predicate.
/// </summary>
public sealed record DriftedSinkPredicateSummary
{
/// <summary>
/// Unique identifier for the sink node.
/// </summary>
[JsonPropertyName("sinkNodeId")]
public required string SinkNodeId { get; init; }
/// <summary>
/// Fully qualified symbol name of the sink.
/// </summary>
[JsonPropertyName("symbol")]
public required string Symbol { get; init; }
/// <summary>
/// Category of the sink (sql_injection, command_execution, etc.).
/// </summary>
[JsonPropertyName("sinkCategory")]
public required string SinkCategory { get; init; }
/// <summary>
/// Kind of drift cause (guard_removed, new_route, dependency_change, etc.).
/// </summary>
[JsonPropertyName("causeKind")]
public required string CauseKind { get; init; }
/// <summary>
/// Human-readable description of the cause.
/// </summary>
[JsonPropertyName("causeDescription")]
public required string CauseDescription { get; init; }
/// <summary>
/// CVE IDs associated with this sink.
/// </summary>
[JsonPropertyName("associatedCves")]
public ImmutableArray<string> AssociatedCves { get; init; } = [];
/// <summary>
/// Hash of the compressed path for verification.
/// </summary>
[JsonPropertyName("pathHash")]
public string? PathHash { get; init; }
}
/// <summary>
/// Metadata about the drift analysis.
/// </summary>
public sealed record DriftAnalysisMetadata
{
/// <summary>
/// When the analysis was performed.
/// </summary>
[JsonPropertyName("analyzedAt")]
public required DateTimeOffset AnalyzedAt { get; init; }
/// <summary>
/// Information about the scanner that performed the analysis.
/// </summary>
[JsonPropertyName("scanner")]
public required DriftScannerInfo Scanner { get; init; }
/// <summary>
/// Content-addressed digest of the baseline call graph.
/// </summary>
[JsonPropertyName("baseGraphDigest")]
public required string BaseGraphDigest { get; init; }
/// <summary>
/// Content-addressed digest of the head call graph.
/// </summary>
[JsonPropertyName("headGraphDigest")]
public required string HeadGraphDigest { get; init; }
/// <summary>
/// Optional: digest of the code change facts used.
/// </summary>
[JsonPropertyName("codeChangesDigest")]
public string? CodeChangesDigest { get; init; }
}
/// <summary>
/// Information about the scanner that performed drift analysis.
/// </summary>
public sealed record DriftScannerInfo
{
/// <summary>
/// Name of the scanner.
/// </summary>
[JsonPropertyName("name")]
public required string Name { get; init; }
/// <summary>
/// Version of the scanner.
/// </summary>
[JsonPropertyName("version")]
public required string Version { get; init; }
/// <summary>
/// Optional ruleset used for sink detection.
/// </summary>
[JsonPropertyName("ruleset")]
public string? Ruleset { get; init; }
}

View File

@@ -0,0 +1,257 @@
// -----------------------------------------------------------------------------
// ReachabilityDriftStatement.cs
// Sprint: SPRINT_3600_0004_0001_ui_evidence_chain
// Description: DSSE predicate for reachability drift attestation.
// -----------------------------------------------------------------------------
using System;
using System.Collections.Immutable;
using System.Text.Json.Serialization;
namespace StellaOps.Attestor.ProofChain.Statements;
/// <summary>
/// In-toto statement for reachability drift between scans.
/// Predicate type: stellaops.dev/predicates/reachability-drift@v1
/// </summary>
public sealed record ReachabilityDriftStatement : InTotoStatement
{
/// <inheritdoc />
[JsonPropertyName("predicateType")]
public override string PredicateType => "stellaops.dev/predicates/reachability-drift@v1";
/// <summary>
/// The drift payload.
/// </summary>
[JsonPropertyName("predicate")]
public required ReachabilityDriftPayload Predicate { get; init; }
}
/// <summary>
/// Payload for reachability drift statements.
/// </summary>
public sealed record ReachabilityDriftPayload
{
/// <summary>
/// Base image reference (before).
/// </summary>
[JsonPropertyName("baseImage")]
public required ImageReference BaseImage { get; init; }
/// <summary>
/// Target image reference (after).
/// </summary>
[JsonPropertyName("targetImage")]
public required ImageReference TargetImage { get; init; }
/// <summary>
/// Scan ID of the base scan.
/// </summary>
[JsonPropertyName("baseScanId")]
public required string BaseScanId { get; init; }
/// <summary>
/// Scan ID of the head scan.
/// </summary>
[JsonPropertyName("headScanId")]
public required string HeadScanId { get; init; }
/// <summary>
/// Drift summary.
/// </summary>
[JsonPropertyName("drift")]
public required DriftSummary Drift { get; init; }
/// <summary>
/// Analysis metadata.
/// </summary>
[JsonPropertyName("analysis")]
public required DriftAnalysisMetadata Analysis { get; init; }
}
/// <summary>
/// Image reference for drift comparison.
/// </summary>
public sealed record ImageReference
{
/// <summary>
/// Image name (e.g., "myregistry.io/app").
/// </summary>
[JsonPropertyName("name")]
public required string Name { get; init; }
/// <summary>
/// Image digest (e.g., "sha256:...").
/// </summary>
[JsonPropertyName("digest")]
public required string Digest { get; init; }
}
/// <summary>
/// Summary of reachability drift.
/// </summary>
public sealed record DriftSummary
{
/// <summary>
/// Count of newly reachable paths (NEW RISK).
/// </summary>
[JsonPropertyName("newlyReachableCount")]
public required int NewlyReachableCount { get; init; }
/// <summary>
/// Count of newly unreachable paths (MITIGATED).
/// </summary>
[JsonPropertyName("newlyUnreachableCount")]
public required int NewlyUnreachableCount { get; init; }
/// <summary>
/// Details of newly reachable sinks.
/// </summary>
[JsonPropertyName("newlyReachable")]
public ImmutableArray<DriftedSinkSummary> NewlyReachable { get; init; } = [];
/// <summary>
/// Details of newly unreachable sinks.
/// </summary>
[JsonPropertyName("newlyUnreachable")]
public ImmutableArray<DriftedSinkSummary> NewlyUnreachable { get; init; } = [];
/// <summary>
/// Net change in reachable vulnerability paths.
/// Positive = more risk, negative = less risk.
/// </summary>
[JsonPropertyName("netChange")]
public int NetChange => NewlyReachableCount - NewlyUnreachableCount;
/// <summary>
/// Whether this drift should block a PR.
/// </summary>
[JsonPropertyName("shouldBlock")]
public bool ShouldBlock => NewlyReachableCount > 0;
}
/// <summary>
/// Summary of a drifted sink.
/// </summary>
public sealed record DriftedSinkSummary
{
/// <summary>
/// Sink node identifier.
/// </summary>
[JsonPropertyName("sinkNodeId")]
public required string SinkNodeId { get; init; }
/// <summary>
/// Symbol name of the sink.
/// </summary>
[JsonPropertyName("symbol")]
public required string Symbol { get; init; }
/// <summary>
/// Category of the sink (e.g., "deserialization", "sql_injection").
/// </summary>
[JsonPropertyName("sinkCategory")]
public required string SinkCategory { get; init; }
/// <summary>
/// Kind of change that caused the drift.
/// </summary>
[JsonPropertyName("causeKind")]
public required string CauseKind { get; init; }
/// <summary>
/// Human-readable description of the cause.
/// </summary>
[JsonPropertyName("causeDescription")]
public required string CauseDescription { get; init; }
/// <summary>
/// File where the change occurred.
/// </summary>
[JsonPropertyName("changedFile")]
public string? ChangedFile { get; init; }
/// <summary>
/// Line where the change occurred.
/// </summary>
[JsonPropertyName("changedLine")]
public int? ChangedLine { get; init; }
/// <summary>
/// Associated CVE IDs.
/// </summary>
[JsonPropertyName("associatedCves")]
public ImmutableArray<string> AssociatedCves { get; init; } = [];
/// <summary>
/// Entry point method key.
/// </summary>
[JsonPropertyName("entryMethodKey")]
public string? EntryMethodKey { get; init; }
/// <summary>
/// Path length from entry to sink.
/// </summary>
[JsonPropertyName("pathLength")]
public int? PathLength { get; init; }
}
/// <summary>
/// Metadata about the drift analysis.
/// </summary>
public sealed record DriftAnalysisMetadata
{
/// <summary>
/// When the analysis was performed.
/// </summary>
[JsonPropertyName("analyzedAt")]
public required DateTimeOffset AnalyzedAt { get; init; }
/// <summary>
/// Scanner information.
/// </summary>
[JsonPropertyName("scanner")]
public required DriftScannerInfo Scanner { get; init; }
/// <summary>
/// Digest of the base call graph.
/// </summary>
[JsonPropertyName("baseGraphDigest")]
public required string BaseGraphDigest { get; init; }
/// <summary>
/// Digest of the head call graph.
/// </summary>
[JsonPropertyName("headGraphDigest")]
public required string HeadGraphDigest { get; init; }
/// <summary>
/// Algorithm used for graph hashing.
/// </summary>
[JsonPropertyName("hashAlgorithm")]
public string HashAlgorithm { get; init; } = "blake3";
}
/// <summary>
/// Scanner information for drift analysis.
/// </summary>
public sealed record DriftScannerInfo
{
/// <summary>
/// Scanner name.
/// </summary>
[JsonPropertyName("name")]
public required string Name { get; init; }
/// <summary>
/// Scanner version.
/// </summary>
[JsonPropertyName("version")]
public required string Version { get; init; }
/// <summary>
/// Ruleset used for analysis.
/// </summary>
[JsonPropertyName("ruleset")]
public string? Ruleset { get; init; }
}

View File

@@ -0,0 +1,316 @@
// -----------------------------------------------------------------------------
// ReachabilityWitnessStatement.cs
// Sprint: SPRINT_3600_0004_0001_ui_evidence_chain
// Description: DSSE predicate for individual reachability witness attestation.
// -----------------------------------------------------------------------------
using System;
using System.Collections.Immutable;
using System.Text.Json.Serialization;
namespace StellaOps.Attestor.ProofChain.Statements;
/// <summary>
/// In-toto statement for reachability witness attestation.
/// Predicate type: stellaops.dev/predicates/reachability-witness@v1
/// </summary>
public sealed record ReachabilityWitnessStatement : InTotoStatement
{
/// <inheritdoc />
[JsonPropertyName("predicateType")]
public override string PredicateType => "stellaops.dev/predicates/reachability-witness@v1";
/// <summary>
/// The witness payload.
/// </summary>
[JsonPropertyName("predicate")]
public required ReachabilityWitnessPayload Predicate { get; init; }
}
/// <summary>
/// Payload for reachability witness statements.
/// </summary>
public sealed record ReachabilityWitnessPayload
{
/// <summary>
/// Unique witness identifier.
/// </summary>
[JsonPropertyName("witnessId")]
public required string WitnessId { get; init; }
/// <summary>
/// Scan ID that produced this witness.
/// </summary>
[JsonPropertyName("scanId")]
public required string ScanId { get; init; }
/// <summary>
/// Vulnerability identifier (internal).
/// </summary>
[JsonPropertyName("vulnId")]
public required string VulnId { get; init; }
/// <summary>
/// CVE identifier if applicable.
/// </summary>
[JsonPropertyName("cveId")]
public string? CveId { get; init; }
/// <summary>
/// Package name.
/// </summary>
[JsonPropertyName("packageName")]
public required string PackageName { get; init; }
/// <summary>
/// Package version.
/// </summary>
[JsonPropertyName("packageVersion")]
public string? PackageVersion { get; init; }
/// <summary>
/// Package URL (purl).
/// </summary>
[JsonPropertyName("purl")]
public string? Purl { get; init; }
/// <summary>
/// Confidence tier for reachability assessment.
/// </summary>
[JsonPropertyName("confidenceTier")]
public required string ConfidenceTier { get; init; }
/// <summary>
/// Confidence score (0.0-1.0).
/// </summary>
[JsonPropertyName("confidenceScore")]
public required double ConfidenceScore { get; init; }
/// <summary>
/// Whether the vulnerable code is reachable.
/// </summary>
[JsonPropertyName("isReachable")]
public required bool IsReachable { get; init; }
/// <summary>
/// Call path from entry point to sink.
/// </summary>
[JsonPropertyName("callPath")]
public ImmutableArray<WitnessCallPathNode> CallPath { get; init; } = [];
/// <summary>
/// Entry point information.
/// </summary>
[JsonPropertyName("entrypoint")]
public WitnessPathNode? Entrypoint { get; init; }
/// <summary>
/// Sink (vulnerable method) information.
/// </summary>
[JsonPropertyName("sink")]
public WitnessPathNode? Sink { get; init; }
/// <summary>
/// Security gates encountered along the path.
/// </summary>
[JsonPropertyName("gates")]
public ImmutableArray<WitnessGateInfo> Gates { get; init; } = [];
/// <summary>
/// Evidence metadata.
/// </summary>
[JsonPropertyName("evidence")]
public required WitnessEvidenceMetadata Evidence { get; init; }
/// <summary>
/// When the witness was observed.
/// </summary>
[JsonPropertyName("observedAt")]
public required DateTimeOffset ObservedAt { get; init; }
/// <summary>
/// VEX recommendation based on reachability.
/// </summary>
[JsonPropertyName("vexRecommendation")]
public string? VexRecommendation { get; init; }
}
/// <summary>
/// Node in the witness call path.
/// </summary>
public sealed record WitnessCallPathNode
{
/// <summary>
/// Node identifier.
/// </summary>
[JsonPropertyName("nodeId")]
public required string NodeId { get; init; }
/// <summary>
/// Symbol name.
/// </summary>
[JsonPropertyName("symbol")]
public required string Symbol { get; init; }
/// <summary>
/// Source file path.
/// </summary>
[JsonPropertyName("file")]
public string? File { get; init; }
/// <summary>
/// Line number.
/// </summary>
[JsonPropertyName("line")]
public int? Line { get; init; }
/// <summary>
/// Package name if external.
/// </summary>
[JsonPropertyName("package")]
public string? Package { get; init; }
/// <summary>
/// Whether this node was changed (for drift).
/// </summary>
[JsonPropertyName("isChanged")]
public bool IsChanged { get; init; }
/// <summary>
/// Kind of change if changed.
/// </summary>
[JsonPropertyName("changeKind")]
public string? ChangeKind { get; init; }
}
/// <summary>
/// Detailed path node for entry/sink.
/// </summary>
public sealed record WitnessPathNode
{
/// <summary>
/// Node identifier.
/// </summary>
[JsonPropertyName("nodeId")]
public required string NodeId { get; init; }
/// <summary>
/// Symbol name.
/// </summary>
[JsonPropertyName("symbol")]
public required string Symbol { get; init; }
/// <summary>
/// Source file path.
/// </summary>
[JsonPropertyName("file")]
public string? File { get; init; }
/// <summary>
/// Line number.
/// </summary>
[JsonPropertyName("line")]
public int? Line { get; init; }
/// <summary>
/// Package name.
/// </summary>
[JsonPropertyName("package")]
public string? Package { get; init; }
/// <summary>
/// Method name.
/// </summary>
[JsonPropertyName("method")]
public string? Method { get; init; }
/// <summary>
/// HTTP route if entry point.
/// </summary>
[JsonPropertyName("httpRoute")]
public string? HttpRoute { get; init; }
/// <summary>
/// HTTP method if entry point.
/// </summary>
[JsonPropertyName("httpMethod")]
public string? HttpMethod { get; init; }
}
/// <summary>
/// Security gate information in witness.
/// </summary>
public sealed record WitnessGateInfo
{
/// <summary>
/// Type of gate.
/// </summary>
[JsonPropertyName("gateType")]
public required string GateType { get; init; }
/// <summary>
/// Symbol name.
/// </summary>
[JsonPropertyName("symbol")]
public required string Symbol { get; init; }
/// <summary>
/// Confidence in gate detection.
/// </summary>
[JsonPropertyName("confidence")]
public required double Confidence { get; init; }
/// <summary>
/// Description of the gate.
/// </summary>
[JsonPropertyName("description")]
public string? Description { get; init; }
/// <summary>
/// File where gate is located.
/// </summary>
[JsonPropertyName("file")]
public string? File { get; init; }
/// <summary>
/// Line number.
/// </summary>
[JsonPropertyName("line")]
public int? Line { get; init; }
}
/// <summary>
/// Evidence metadata for witness.
/// </summary>
public sealed record WitnessEvidenceMetadata
{
/// <summary>
/// Call graph hash.
/// </summary>
[JsonPropertyName("callGraphHash")]
public string? CallGraphHash { get; init; }
/// <summary>
/// Surface hash.
/// </summary>
[JsonPropertyName("surfaceHash")]
public string? SurfaceHash { get; init; }
/// <summary>
/// Analysis method used.
/// </summary>
[JsonPropertyName("analysisMethod")]
public required string AnalysisMethod { get; init; }
/// <summary>
/// Tool version.
/// </summary>
[JsonPropertyName("toolVersion")]
public string? ToolVersion { get; init; }
/// <summary>
/// Hash algorithm used.
/// </summary>
[JsonPropertyName("hashAlgorithm")]
public string HashAlgorithm { get; init; } = "blake3";
}