stabilizaiton work - projects rework for maintenanceability and ui livening
This commit is contained in:
@@ -0,0 +1,77 @@
|
||||
// Licensed to StellaOps under the BUSL-1.1 license.
|
||||
using StellaOps.ReachGraph.Schema;
|
||||
using System.Collections.Immutable;
|
||||
|
||||
namespace StellaOps.ReachGraph.Serialization;
|
||||
|
||||
public sealed partial class CanonicalReachGraphSerializer
|
||||
{
|
||||
private static ReachGraphArtifactDto CreateArtifactDto(ReachGraphArtifact artifact)
|
||||
{
|
||||
var sortedEnv = artifact.Env.OrderBy(e => e, StringComparer.Ordinal).ToList();
|
||||
|
||||
return new ReachGraphArtifactDto
|
||||
{
|
||||
Name = artifact.Name,
|
||||
Digest = artifact.Digest,
|
||||
Env = sortedEnv
|
||||
};
|
||||
}
|
||||
|
||||
private static ReachGraphScopeDto CreateScopeDto(ReachGraphScope scope)
|
||||
{
|
||||
var sortedEntrypoints = scope.Entrypoints.OrderBy(e => e, StringComparer.Ordinal).ToList();
|
||||
var sortedSelectors = scope.Selectors.OrderBy(s => s, StringComparer.Ordinal).ToList();
|
||||
List<string>? sortedCves = scope.Cves is { Length: > 0 } cves
|
||||
? cves.OrderBy(c => c, StringComparer.Ordinal).ToList()
|
||||
: null;
|
||||
|
||||
return new ReachGraphScopeDto
|
||||
{
|
||||
Entrypoints = sortedEntrypoints,
|
||||
Selectors = sortedSelectors,
|
||||
Cves = sortedCves
|
||||
};
|
||||
}
|
||||
|
||||
private static ReachGraphProvenanceDto CreateProvenanceDto(ReachGraphProvenance provenance)
|
||||
{
|
||||
List<string>? sortedIntoto = provenance.Intoto is { Length: > 0 } intoto
|
||||
? intoto.OrderBy(i => i, StringComparer.Ordinal).ToList()
|
||||
: null;
|
||||
|
||||
return new ReachGraphProvenanceDto
|
||||
{
|
||||
Intoto = sortedIntoto,
|
||||
Inputs = new ReachGraphInputsDto
|
||||
{
|
||||
Sbom = provenance.Inputs.Sbom,
|
||||
Vex = provenance.Inputs.Vex,
|
||||
Callgraph = provenance.Inputs.Callgraph,
|
||||
RuntimeFacts = provenance.Inputs.RuntimeFacts,
|
||||
Policy = provenance.Inputs.Policy
|
||||
},
|
||||
ComputedAt = provenance.ComputedAt,
|
||||
Analyzer = new ReachGraphAnalyzerDto
|
||||
{
|
||||
Name = provenance.Analyzer.Name,
|
||||
Version = provenance.Analyzer.Version,
|
||||
ToolchainDigest = provenance.Analyzer.ToolchainDigest
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static List<ReachGraphSignatureDto>? CreateSignatureDtos(
|
||||
ImmutableArray<ReachGraphSignature>? signatures)
|
||||
{
|
||||
if (signatures is not { Length: > 0 } sigs)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return sigs
|
||||
.OrderBy(s => s.KeyId, StringComparer.Ordinal)
|
||||
.Select(s => new ReachGraphSignatureDto { KeyId = s.KeyId, Sig = s.Sig })
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
// Licensed to StellaOps under the BUSL-1.1 license.
|
||||
using StellaOps.ReachGraph.Schema;
|
||||
using System.Collections.Immutable;
|
||||
|
||||
namespace StellaOps.ReachGraph.Serialization;
|
||||
|
||||
public sealed partial class CanonicalReachGraphSerializer
|
||||
{
|
||||
private static List<ReachGraphNodeDto> CreateNodeDtos(ImmutableArray<ReachGraphNode> nodes)
|
||||
{
|
||||
return nodes
|
||||
.OrderBy(n => n.Id, StringComparer.Ordinal)
|
||||
.Select(n => new ReachGraphNodeDto
|
||||
{
|
||||
Id = n.Id,
|
||||
Kind = n.Kind,
|
||||
Ref = n.Ref,
|
||||
File = n.File,
|
||||
Line = n.Line,
|
||||
ModuleHash = n.ModuleHash,
|
||||
Addr = n.Addr,
|
||||
IsEntrypoint = n.IsEntrypoint,
|
||||
IsSink = n.IsSink
|
||||
})
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private static List<ReachGraphEdgeDto> CreateEdgeDtos(ImmutableArray<ReachGraphEdge> edges)
|
||||
{
|
||||
return edges
|
||||
.OrderBy(e => e.From, StringComparer.Ordinal)
|
||||
.ThenBy(e => e.To, StringComparer.Ordinal)
|
||||
.Select(e => new ReachGraphEdgeDto
|
||||
{
|
||||
From = e.From,
|
||||
To = e.To,
|
||||
Why = new EdgeExplanationDto
|
||||
{
|
||||
Type = e.Why.Type,
|
||||
Loc = e.Why.Loc,
|
||||
Guard = e.Why.Guard,
|
||||
Confidence = e.Why.Confidence,
|
||||
Metadata = e.Why.Metadata?.Count > 0
|
||||
? new SortedDictionary<string, string>(e.Why.Metadata, StringComparer.Ordinal)
|
||||
: null
|
||||
}
|
||||
})
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
// Licensed to StellaOps under the BUSL-1.1 license.
|
||||
using StellaOps.ReachGraph.Schema;
|
||||
|
||||
namespace StellaOps.ReachGraph.Serialization;
|
||||
|
||||
public sealed partial class CanonicalReachGraphSerializer
|
||||
{
|
||||
/// <summary>
|
||||
/// Create a canonical DTO with sorted arrays.
|
||||
/// </summary>
|
||||
private static ReachGraphMinimalDto Canonicalize(ReachGraphMinimal graph)
|
||||
{
|
||||
return new ReachGraphMinimalDto
|
||||
{
|
||||
SchemaVersion = graph.SchemaVersion,
|
||||
Artifact = CreateArtifactDto(graph.Artifact),
|
||||
Scope = CreateScopeDto(graph.Scope),
|
||||
Nodes = CreateNodeDtos(graph.Nodes),
|
||||
Edges = CreateEdgeDtos(graph.Edges),
|
||||
Provenance = CreateProvenanceDto(graph.Provenance),
|
||||
Signatures = CreateSignatureDtos(graph.Signatures)
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
// Licensed to StellaOps under the BUSL-1.1 license.
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace StellaOps.ReachGraph.Serialization;
|
||||
|
||||
public sealed partial class CanonicalReachGraphSerializer
|
||||
{
|
||||
private sealed class ReachGraphMinimalDto
|
||||
{
|
||||
[JsonPropertyOrder(1)]
|
||||
public required string SchemaVersion { get; init; }
|
||||
|
||||
[JsonPropertyOrder(2)]
|
||||
public required ReachGraphArtifactDto Artifact { get; init; }
|
||||
|
||||
[JsonPropertyOrder(3)]
|
||||
public required ReachGraphScopeDto Scope { get; init; }
|
||||
|
||||
[JsonPropertyOrder(4)]
|
||||
public required List<ReachGraphNodeDto> Nodes { get; init; }
|
||||
|
||||
[JsonPropertyOrder(5)]
|
||||
public required List<ReachGraphEdgeDto> Edges { get; init; }
|
||||
|
||||
[JsonPropertyOrder(6)]
|
||||
public required ReachGraphProvenanceDto Provenance { get; init; }
|
||||
|
||||
[JsonPropertyOrder(7)]
|
||||
public List<ReachGraphSignatureDto>? Signatures { get; init; }
|
||||
}
|
||||
|
||||
private sealed class ReachGraphArtifactDto
|
||||
{
|
||||
[JsonPropertyOrder(1)]
|
||||
public required string Name { get; init; }
|
||||
|
||||
[JsonPropertyOrder(2)]
|
||||
public required string Digest { get; init; }
|
||||
|
||||
[JsonPropertyOrder(3)]
|
||||
public required List<string> Env { get; init; }
|
||||
}
|
||||
|
||||
private sealed class ReachGraphScopeDto
|
||||
{
|
||||
[JsonPropertyOrder(1)]
|
||||
public required List<string> Entrypoints { get; init; }
|
||||
|
||||
[JsonPropertyOrder(2)]
|
||||
public required List<string> Selectors { get; init; }
|
||||
|
||||
[JsonPropertyOrder(3)]
|
||||
public List<string>? Cves { get; init; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
// Licensed to StellaOps under the BUSL-1.1 license.
|
||||
using StellaOps.ReachGraph.Schema;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace StellaOps.ReachGraph.Serialization;
|
||||
|
||||
public sealed partial class CanonicalReachGraphSerializer
|
||||
{
|
||||
private sealed class ReachGraphNodeDto
|
||||
{
|
||||
[JsonPropertyOrder(1)]
|
||||
public required string Id { get; init; }
|
||||
|
||||
[JsonPropertyOrder(2)]
|
||||
public required ReachGraphNodeKind Kind { get; init; }
|
||||
|
||||
[JsonPropertyOrder(3)]
|
||||
public required string Ref { get; init; }
|
||||
|
||||
[JsonPropertyOrder(4)]
|
||||
public string? File { get; init; }
|
||||
|
||||
[JsonPropertyOrder(5)]
|
||||
public int? Line { get; init; }
|
||||
|
||||
[JsonPropertyOrder(6)]
|
||||
public string? ModuleHash { get; init; }
|
||||
|
||||
[JsonPropertyOrder(7)]
|
||||
public string? Addr { get; init; }
|
||||
|
||||
[JsonPropertyOrder(8)]
|
||||
public bool? IsEntrypoint { get; init; }
|
||||
|
||||
[JsonPropertyOrder(9)]
|
||||
public bool? IsSink { get; init; }
|
||||
}
|
||||
|
||||
private sealed class ReachGraphEdgeDto
|
||||
{
|
||||
[JsonPropertyOrder(1)]
|
||||
public required string From { get; init; }
|
||||
|
||||
[JsonPropertyOrder(2)]
|
||||
public required string To { get; init; }
|
||||
|
||||
[JsonPropertyOrder(3)]
|
||||
public required EdgeExplanationDto Why { get; init; }
|
||||
}
|
||||
|
||||
private sealed class EdgeExplanationDto
|
||||
{
|
||||
[JsonPropertyOrder(1)]
|
||||
public required EdgeExplanationType Type { get; init; }
|
||||
|
||||
[JsonPropertyOrder(2)]
|
||||
public string? Loc { get; init; }
|
||||
|
||||
[JsonPropertyOrder(3)]
|
||||
public string? Guard { get; init; }
|
||||
|
||||
[JsonPropertyOrder(4)]
|
||||
public required double Confidence { get; init; }
|
||||
|
||||
[JsonPropertyOrder(5)]
|
||||
public SortedDictionary<string, string>? Metadata { get; init; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
// Licensed to StellaOps under the BUSL-1.1 license.
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace StellaOps.ReachGraph.Serialization;
|
||||
|
||||
public sealed partial class CanonicalReachGraphSerializer
|
||||
{
|
||||
private sealed class ReachGraphProvenanceDto
|
||||
{
|
||||
[JsonPropertyOrder(1)]
|
||||
public List<string>? Intoto { get; init; }
|
||||
|
||||
[JsonPropertyOrder(2)]
|
||||
public required ReachGraphInputsDto Inputs { get; init; }
|
||||
|
||||
[JsonPropertyOrder(3)]
|
||||
public required DateTimeOffset ComputedAt { get; init; }
|
||||
|
||||
[JsonPropertyOrder(4)]
|
||||
public required ReachGraphAnalyzerDto Analyzer { get; init; }
|
||||
}
|
||||
|
||||
private sealed class ReachGraphInputsDto
|
||||
{
|
||||
[JsonPropertyOrder(1)]
|
||||
public required string Sbom { get; init; }
|
||||
|
||||
[JsonPropertyOrder(2)]
|
||||
public string? Vex { get; init; }
|
||||
|
||||
[JsonPropertyOrder(3)]
|
||||
public string? Callgraph { get; init; }
|
||||
|
||||
[JsonPropertyOrder(4)]
|
||||
public string? RuntimeFacts { get; init; }
|
||||
|
||||
[JsonPropertyOrder(5)]
|
||||
public string? Policy { get; init; }
|
||||
}
|
||||
|
||||
private sealed class ReachGraphAnalyzerDto
|
||||
{
|
||||
[JsonPropertyOrder(1)]
|
||||
public required string Name { get; init; }
|
||||
|
||||
[JsonPropertyOrder(2)]
|
||||
public required string Version { get; init; }
|
||||
|
||||
[JsonPropertyOrder(3)]
|
||||
public required string ToolchainDigest { get; init; }
|
||||
}
|
||||
|
||||
private sealed class ReachGraphSignatureDto
|
||||
{
|
||||
[JsonPropertyOrder(1)]
|
||||
public required string KeyId { get; init; }
|
||||
|
||||
[JsonPropertyOrder(2)]
|
||||
public required string Sig { get; init; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
// Licensed to StellaOps under the BUSL-1.1 license.
|
||||
using StellaOps.ReachGraph.Schema;
|
||||
using System.Collections.Immutable;
|
||||
|
||||
namespace StellaOps.ReachGraph.Serialization;
|
||||
|
||||
public sealed partial class CanonicalReachGraphSerializer
|
||||
{
|
||||
private static ReachGraphArtifact CreateArtifact(ReachGraphArtifactDto dto)
|
||||
{
|
||||
return new ReachGraphArtifact(dto.Name, dto.Digest, [.. dto.Env]);
|
||||
}
|
||||
|
||||
private static ReachGraphScope CreateScope(ReachGraphScopeDto dto)
|
||||
{
|
||||
return new ReachGraphScope(
|
||||
[.. dto.Entrypoints],
|
||||
[.. dto.Selectors],
|
||||
dto.Cves is { Count: > 0 } ? [.. dto.Cves] : null);
|
||||
}
|
||||
|
||||
private static ReachGraphProvenance CreateProvenance(ReachGraphProvenanceDto dto)
|
||||
{
|
||||
return new ReachGraphProvenance
|
||||
{
|
||||
Intoto = dto.Intoto is { Count: > 0 } ? [.. dto.Intoto] : null,
|
||||
Inputs = new ReachGraphInputs
|
||||
{
|
||||
Sbom = dto.Inputs.Sbom,
|
||||
Vex = dto.Inputs.Vex,
|
||||
Callgraph = dto.Inputs.Callgraph,
|
||||
RuntimeFacts = dto.Inputs.RuntimeFacts,
|
||||
Policy = dto.Inputs.Policy
|
||||
},
|
||||
ComputedAt = dto.ComputedAt,
|
||||
Analyzer = new ReachGraphAnalyzer(
|
||||
dto.Analyzer.Name,
|
||||
dto.Analyzer.Version,
|
||||
dto.Analyzer.ToolchainDigest)
|
||||
};
|
||||
}
|
||||
|
||||
private static ImmutableArray<ReachGraphSignature>? CreateSignatures(
|
||||
List<ReachGraphSignatureDto>? signatures)
|
||||
{
|
||||
if (signatures is not { Count: > 0 })
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return [.. signatures.Select(s => new ReachGraphSignature(s.KeyId, s.Sig))];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
// Licensed to StellaOps under the BUSL-1.1 license.
|
||||
using StellaOps.ReachGraph.Schema;
|
||||
using System.Collections.Immutable;
|
||||
|
||||
namespace StellaOps.ReachGraph.Serialization;
|
||||
|
||||
public sealed partial class CanonicalReachGraphSerializer
|
||||
{
|
||||
private static ImmutableArray<ReachGraphNode> CreateNodes(List<ReachGraphNodeDto> nodes)
|
||||
{
|
||||
return [.. nodes.Select(n => new ReachGraphNode
|
||||
{
|
||||
Id = n.Id,
|
||||
Kind = n.Kind,
|
||||
Ref = n.Ref,
|
||||
File = n.File,
|
||||
Line = n.Line,
|
||||
ModuleHash = n.ModuleHash,
|
||||
Addr = n.Addr,
|
||||
IsEntrypoint = n.IsEntrypoint,
|
||||
IsSink = n.IsSink
|
||||
})];
|
||||
}
|
||||
|
||||
private static ImmutableArray<ReachGraphEdge> CreateEdges(List<ReachGraphEdgeDto> edges)
|
||||
{
|
||||
return [.. edges.Select(e => new ReachGraphEdge
|
||||
{
|
||||
From = e.From,
|
||||
To = e.To,
|
||||
Why = new EdgeExplanation
|
||||
{
|
||||
Type = e.Why.Type,
|
||||
Loc = e.Why.Loc,
|
||||
Guard = e.Why.Guard,
|
||||
Confidence = e.Why.Confidence,
|
||||
Metadata = e.Why.Metadata?.Count > 0
|
||||
? e.Why.Metadata.ToImmutableDictionary()
|
||||
: null
|
||||
}
|
||||
})];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
// Licensed to StellaOps under the BUSL-1.1 license.
|
||||
using StellaOps.ReachGraph.Schema;
|
||||
|
||||
namespace StellaOps.ReachGraph.Serialization;
|
||||
|
||||
public sealed partial class CanonicalReachGraphSerializer
|
||||
{
|
||||
/// <summary>
|
||||
/// Convert DTO back to domain record.
|
||||
/// </summary>
|
||||
private static ReachGraphMinimal FromDto(ReachGraphMinimalDto dto)
|
||||
{
|
||||
return new ReachGraphMinimal
|
||||
{
|
||||
SchemaVersion = dto.SchemaVersion,
|
||||
Artifact = CreateArtifact(dto.Artifact),
|
||||
Scope = CreateScope(dto.Scope),
|
||||
Nodes = CreateNodes(dto.Nodes),
|
||||
Edges = CreateEdges(dto.Edges),
|
||||
Provenance = CreateProvenance(dto.Provenance),
|
||||
Signatures = CreateSignatures(dto.Signatures)
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
// Licensed to StellaOps under the BUSL-1.1 license.
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace StellaOps.ReachGraph.Serialization;
|
||||
|
||||
public sealed partial class CanonicalReachGraphSerializer
|
||||
{
|
||||
private static JsonSerializerOptions CreateSerializerOptions(bool writeIndented)
|
||||
{
|
||||
return new JsonSerializerOptions
|
||||
{
|
||||
WriteIndented = writeIndented,
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
||||
Converters =
|
||||
{
|
||||
new JsonStringEnumConverter(JsonNamingPolicy.CamelCase),
|
||||
new Iso8601MillisecondConverter()
|
||||
},
|
||||
PropertyNameCaseInsensitive = true
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,6 @@
|
||||
// Licensed to StellaOps under the BUSL-1.1 license.
|
||||
|
||||
|
||||
using StellaOps.ReachGraph.Schema;
|
||||
using System.Collections.Immutable;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace StellaOps.ReachGraph.Serialization;
|
||||
|
||||
@@ -18,7 +13,7 @@ namespace StellaOps.ReachGraph.Serialization;
|
||||
/// - Null fields omitted
|
||||
/// - Minified output for storage, prettified for debugging
|
||||
/// </summary>
|
||||
public sealed class CanonicalReachGraphSerializer
|
||||
public sealed partial class CanonicalReachGraphSerializer
|
||||
{
|
||||
private readonly JsonSerializerOptions _minifiedOptions;
|
||||
private readonly JsonSerializerOptions _prettyOptions;
|
||||
@@ -74,390 +69,4 @@ public sealed class CanonicalReachGraphSerializer
|
||||
|
||||
return FromDto(dto);
|
||||
}
|
||||
|
||||
private static JsonSerializerOptions CreateSerializerOptions(bool writeIndented)
|
||||
{
|
||||
return new JsonSerializerOptions
|
||||
{
|
||||
WriteIndented = writeIndented,
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
||||
Converters =
|
||||
{
|
||||
new JsonStringEnumConverter(JsonNamingPolicy.CamelCase),
|
||||
new Iso8601MillisecondConverter()
|
||||
},
|
||||
// Ensure consistent ordering
|
||||
PropertyNameCaseInsensitive = true
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a canonical DTO with sorted arrays.
|
||||
/// </summary>
|
||||
private static ReachGraphMinimalDto Canonicalize(ReachGraphMinimal graph)
|
||||
{
|
||||
// Sort nodes by Id lexicographically
|
||||
var sortedNodes = graph.Nodes
|
||||
.OrderBy(n => n.Id, StringComparer.Ordinal)
|
||||
.Select(n => new ReachGraphNodeDto
|
||||
{
|
||||
Id = n.Id,
|
||||
Kind = n.Kind,
|
||||
Ref = n.Ref,
|
||||
File = n.File,
|
||||
Line = n.Line,
|
||||
ModuleHash = n.ModuleHash,
|
||||
Addr = n.Addr,
|
||||
IsEntrypoint = n.IsEntrypoint,
|
||||
IsSink = n.IsSink
|
||||
})
|
||||
.ToList();
|
||||
|
||||
// Sort edges by From, then To
|
||||
var sortedEdges = graph.Edges
|
||||
.OrderBy(e => e.From, StringComparer.Ordinal)
|
||||
.ThenBy(e => e.To, StringComparer.Ordinal)
|
||||
.Select(e => new ReachGraphEdgeDto
|
||||
{
|
||||
From = e.From,
|
||||
To = e.To,
|
||||
Why = new EdgeExplanationDto
|
||||
{
|
||||
Type = e.Why.Type,
|
||||
Loc = e.Why.Loc,
|
||||
Guard = e.Why.Guard,
|
||||
Confidence = e.Why.Confidence,
|
||||
Metadata = e.Why.Metadata?.Count > 0
|
||||
? new SortedDictionary<string, string>(e.Why.Metadata, StringComparer.Ordinal)
|
||||
: null
|
||||
}
|
||||
})
|
||||
.ToList();
|
||||
|
||||
// Sort signatures by KeyId if present
|
||||
List<ReachGraphSignatureDto>? sortedSignatures = null;
|
||||
if (graph.Signatures is { Length: > 0 } sigs)
|
||||
{
|
||||
sortedSignatures = sigs
|
||||
.OrderBy(s => s.KeyId, StringComparer.Ordinal)
|
||||
.Select(s => new ReachGraphSignatureDto { KeyId = s.KeyId, Sig = s.Sig })
|
||||
.ToList();
|
||||
}
|
||||
|
||||
// Sort entrypoints and selectors
|
||||
var sortedEntrypoints = graph.Scope.Entrypoints
|
||||
.OrderBy(e => e, StringComparer.Ordinal)
|
||||
.ToList();
|
||||
var sortedSelectors = graph.Scope.Selectors
|
||||
.OrderBy(s => s, StringComparer.Ordinal)
|
||||
.ToList();
|
||||
List<string>? sortedCves = graph.Scope.Cves is { Length: > 0 } cves
|
||||
? cves.OrderBy(c => c, StringComparer.Ordinal).ToList()
|
||||
: null;
|
||||
|
||||
// Sort env array
|
||||
var sortedEnv = graph.Artifact.Env
|
||||
.OrderBy(e => e, StringComparer.Ordinal)
|
||||
.ToList();
|
||||
|
||||
// Sort intoto array if present
|
||||
List<string>? sortedIntoto = graph.Provenance.Intoto is { Length: > 0 } intoto
|
||||
? intoto.OrderBy(i => i, StringComparer.Ordinal).ToList()
|
||||
: null;
|
||||
|
||||
return new ReachGraphMinimalDto
|
||||
{
|
||||
SchemaVersion = graph.SchemaVersion,
|
||||
Artifact = new ReachGraphArtifactDto
|
||||
{
|
||||
Name = graph.Artifact.Name,
|
||||
Digest = graph.Artifact.Digest,
|
||||
Env = sortedEnv
|
||||
},
|
||||
Scope = new ReachGraphScopeDto
|
||||
{
|
||||
Entrypoints = sortedEntrypoints,
|
||||
Selectors = sortedSelectors,
|
||||
Cves = sortedCves
|
||||
},
|
||||
Nodes = sortedNodes,
|
||||
Edges = sortedEdges,
|
||||
Provenance = new ReachGraphProvenanceDto
|
||||
{
|
||||
Intoto = sortedIntoto,
|
||||
Inputs = new ReachGraphInputsDto
|
||||
{
|
||||
Sbom = graph.Provenance.Inputs.Sbom,
|
||||
Vex = graph.Provenance.Inputs.Vex,
|
||||
Callgraph = graph.Provenance.Inputs.Callgraph,
|
||||
RuntimeFacts = graph.Provenance.Inputs.RuntimeFacts,
|
||||
Policy = graph.Provenance.Inputs.Policy
|
||||
},
|
||||
ComputedAt = graph.Provenance.ComputedAt,
|
||||
Analyzer = new ReachGraphAnalyzerDto
|
||||
{
|
||||
Name = graph.Provenance.Analyzer.Name,
|
||||
Version = graph.Provenance.Analyzer.Version,
|
||||
ToolchainDigest = graph.Provenance.Analyzer.ToolchainDigest
|
||||
}
|
||||
},
|
||||
Signatures = sortedSignatures
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convert DTO back to domain record.
|
||||
/// </summary>
|
||||
private static ReachGraphMinimal FromDto(ReachGraphMinimalDto dto)
|
||||
{
|
||||
return new ReachGraphMinimal
|
||||
{
|
||||
SchemaVersion = dto.SchemaVersion,
|
||||
Artifact = new ReachGraphArtifact(
|
||||
dto.Artifact.Name,
|
||||
dto.Artifact.Digest,
|
||||
[.. dto.Artifact.Env]
|
||||
),
|
||||
Scope = new ReachGraphScope(
|
||||
[.. dto.Scope.Entrypoints],
|
||||
[.. dto.Scope.Selectors],
|
||||
dto.Scope.Cves is { Count: > 0 } ? [.. dto.Scope.Cves] : null
|
||||
),
|
||||
Nodes = [.. dto.Nodes.Select(n => new ReachGraphNode
|
||||
{
|
||||
Id = n.Id,
|
||||
Kind = n.Kind,
|
||||
Ref = n.Ref,
|
||||
File = n.File,
|
||||
Line = n.Line,
|
||||
ModuleHash = n.ModuleHash,
|
||||
Addr = n.Addr,
|
||||
IsEntrypoint = n.IsEntrypoint,
|
||||
IsSink = n.IsSink
|
||||
})],
|
||||
Edges = [.. dto.Edges.Select(e => new ReachGraphEdge
|
||||
{
|
||||
From = e.From,
|
||||
To = e.To,
|
||||
Why = new EdgeExplanation
|
||||
{
|
||||
Type = e.Why.Type,
|
||||
Loc = e.Why.Loc,
|
||||
Guard = e.Why.Guard,
|
||||
Confidence = e.Why.Confidence,
|
||||
Metadata = e.Why.Metadata?.Count > 0
|
||||
? e.Why.Metadata.ToImmutableDictionary()
|
||||
: null
|
||||
}
|
||||
})],
|
||||
Provenance = new ReachGraphProvenance
|
||||
{
|
||||
Intoto = dto.Provenance.Intoto is { Count: > 0 } ? [.. dto.Provenance.Intoto] : null,
|
||||
Inputs = new ReachGraphInputs
|
||||
{
|
||||
Sbom = dto.Provenance.Inputs.Sbom,
|
||||
Vex = dto.Provenance.Inputs.Vex,
|
||||
Callgraph = dto.Provenance.Inputs.Callgraph,
|
||||
RuntimeFacts = dto.Provenance.Inputs.RuntimeFacts,
|
||||
Policy = dto.Provenance.Inputs.Policy
|
||||
},
|
||||
ComputedAt = dto.Provenance.ComputedAt,
|
||||
Analyzer = new ReachGraphAnalyzer(
|
||||
dto.Provenance.Analyzer.Name,
|
||||
dto.Provenance.Analyzer.Version,
|
||||
dto.Provenance.Analyzer.ToolchainDigest
|
||||
)
|
||||
},
|
||||
Signatures = dto.Signatures is { Count: > 0 }
|
||||
? [.. dto.Signatures.Select(s => new ReachGraphSignature(s.KeyId, s.Sig))]
|
||||
: null
|
||||
};
|
||||
}
|
||||
|
||||
#region DTOs for canonical serialization (alphabetically ordered properties)
|
||||
|
||||
private sealed class ReachGraphMinimalDto
|
||||
{
|
||||
[JsonPropertyOrder(1)]
|
||||
public required string SchemaVersion { get; init; }
|
||||
|
||||
[JsonPropertyOrder(2)]
|
||||
public required ReachGraphArtifactDto Artifact { get; init; }
|
||||
|
||||
[JsonPropertyOrder(3)]
|
||||
public required ReachGraphScopeDto Scope { get; init; }
|
||||
|
||||
[JsonPropertyOrder(4)]
|
||||
public required List<ReachGraphNodeDto> Nodes { get; init; }
|
||||
|
||||
[JsonPropertyOrder(5)]
|
||||
public required List<ReachGraphEdgeDto> Edges { get; init; }
|
||||
|
||||
[JsonPropertyOrder(6)]
|
||||
public required ReachGraphProvenanceDto Provenance { get; init; }
|
||||
|
||||
[JsonPropertyOrder(7)]
|
||||
public List<ReachGraphSignatureDto>? Signatures { get; init; }
|
||||
}
|
||||
|
||||
private sealed class ReachGraphArtifactDto
|
||||
{
|
||||
[JsonPropertyOrder(1)]
|
||||
public required string Name { get; init; }
|
||||
|
||||
[JsonPropertyOrder(2)]
|
||||
public required string Digest { get; init; }
|
||||
|
||||
[JsonPropertyOrder(3)]
|
||||
public required List<string> Env { get; init; }
|
||||
}
|
||||
|
||||
private sealed class ReachGraphScopeDto
|
||||
{
|
||||
[JsonPropertyOrder(1)]
|
||||
public required List<string> Entrypoints { get; init; }
|
||||
|
||||
[JsonPropertyOrder(2)]
|
||||
public required List<string> Selectors { get; init; }
|
||||
|
||||
[JsonPropertyOrder(3)]
|
||||
public List<string>? Cves { get; init; }
|
||||
}
|
||||
|
||||
private sealed class ReachGraphNodeDto
|
||||
{
|
||||
[JsonPropertyOrder(1)]
|
||||
public required string Id { get; init; }
|
||||
|
||||
[JsonPropertyOrder(2)]
|
||||
public required ReachGraphNodeKind Kind { get; init; }
|
||||
|
||||
[JsonPropertyOrder(3)]
|
||||
public required string Ref { get; init; }
|
||||
|
||||
[JsonPropertyOrder(4)]
|
||||
public string? File { get; init; }
|
||||
|
||||
[JsonPropertyOrder(5)]
|
||||
public int? Line { get; init; }
|
||||
|
||||
[JsonPropertyOrder(6)]
|
||||
public string? ModuleHash { get; init; }
|
||||
|
||||
[JsonPropertyOrder(7)]
|
||||
public string? Addr { get; init; }
|
||||
|
||||
[JsonPropertyOrder(8)]
|
||||
public bool? IsEntrypoint { get; init; }
|
||||
|
||||
[JsonPropertyOrder(9)]
|
||||
public bool? IsSink { get; init; }
|
||||
}
|
||||
|
||||
private sealed class ReachGraphEdgeDto
|
||||
{
|
||||
[JsonPropertyOrder(1)]
|
||||
public required string From { get; init; }
|
||||
|
||||
[JsonPropertyOrder(2)]
|
||||
public required string To { get; init; }
|
||||
|
||||
[JsonPropertyOrder(3)]
|
||||
public required EdgeExplanationDto Why { get; init; }
|
||||
}
|
||||
|
||||
private sealed class EdgeExplanationDto
|
||||
{
|
||||
[JsonPropertyOrder(1)]
|
||||
public required EdgeExplanationType Type { get; init; }
|
||||
|
||||
[JsonPropertyOrder(2)]
|
||||
public string? Loc { get; init; }
|
||||
|
||||
[JsonPropertyOrder(3)]
|
||||
public string? Guard { get; init; }
|
||||
|
||||
[JsonPropertyOrder(4)]
|
||||
public required double Confidence { get; init; }
|
||||
|
||||
[JsonPropertyOrder(5)]
|
||||
public SortedDictionary<string, string>? Metadata { get; init; }
|
||||
}
|
||||
|
||||
private sealed class ReachGraphProvenanceDto
|
||||
{
|
||||
[JsonPropertyOrder(1)]
|
||||
public List<string>? Intoto { get; init; }
|
||||
|
||||
[JsonPropertyOrder(2)]
|
||||
public required ReachGraphInputsDto Inputs { get; init; }
|
||||
|
||||
[JsonPropertyOrder(3)]
|
||||
public required DateTimeOffset ComputedAt { get; init; }
|
||||
|
||||
[JsonPropertyOrder(4)]
|
||||
public required ReachGraphAnalyzerDto Analyzer { get; init; }
|
||||
}
|
||||
|
||||
private sealed class ReachGraphInputsDto
|
||||
{
|
||||
[JsonPropertyOrder(1)]
|
||||
public required string Sbom { get; init; }
|
||||
|
||||
[JsonPropertyOrder(2)]
|
||||
public string? Vex { get; init; }
|
||||
|
||||
[JsonPropertyOrder(3)]
|
||||
public string? Callgraph { get; init; }
|
||||
|
||||
[JsonPropertyOrder(4)]
|
||||
public string? RuntimeFacts { get; init; }
|
||||
|
||||
[JsonPropertyOrder(5)]
|
||||
public string? Policy { get; init; }
|
||||
}
|
||||
|
||||
private sealed class ReachGraphAnalyzerDto
|
||||
{
|
||||
[JsonPropertyOrder(1)]
|
||||
public required string Name { get; init; }
|
||||
|
||||
[JsonPropertyOrder(2)]
|
||||
public required string Version { get; init; }
|
||||
|
||||
[JsonPropertyOrder(3)]
|
||||
public required string ToolchainDigest { get; init; }
|
||||
}
|
||||
|
||||
private sealed class ReachGraphSignatureDto
|
||||
{
|
||||
[JsonPropertyOrder(1)]
|
||||
public required string KeyId { get; init; }
|
||||
|
||||
[JsonPropertyOrder(2)]
|
||||
public required string Sig { get; init; }
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// JSON converter for ISO-8601 timestamps with millisecond precision.
|
||||
/// </summary>
|
||||
internal sealed class Iso8601MillisecondConverter : JsonConverter<DateTimeOffset>
|
||||
{
|
||||
private const string Format = "yyyy-MM-ddTHH:mm:ss.fffZ";
|
||||
|
||||
public override DateTimeOffset Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
var str = reader.GetString() ?? throw new JsonException("Expected string value for DateTimeOffset");
|
||||
return DateTimeOffset.Parse(str, System.Globalization.CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
public override void Write(Utf8JsonWriter writer, DateTimeOffset value, JsonSerializerOptions options)
|
||||
{
|
||||
// Always output in UTC with millisecond precision
|
||||
writer.WriteStringValue(value.UtcDateTime.ToString(Format, System.Globalization.CultureInfo.InvariantCulture));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
// Licensed to StellaOps under the BUSL-1.1 license.
|
||||
using System.Globalization;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace StellaOps.ReachGraph.Serialization;
|
||||
|
||||
/// <summary>
|
||||
/// JSON converter for ISO-8601 timestamps with millisecond precision.
|
||||
/// </summary>
|
||||
internal sealed class Iso8601MillisecondConverter : JsonConverter<DateTimeOffset>
|
||||
{
|
||||
private const string Format = "yyyy-MM-ddTHH:mm:ss.fffZ";
|
||||
|
||||
public override DateTimeOffset Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
var str = reader.GetString() ?? throw new JsonException("Expected string value for DateTimeOffset");
|
||||
return DateTimeOffset.Parse(str, CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
public override void Write(Utf8JsonWriter writer, DateTimeOffset value, JsonSerializerOptions options)
|
||||
{
|
||||
writer.WriteStringValue(value.UtcDateTime.ToString(Format, CultureInfo.InvariantCulture));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user