Some checks failed
Signals CI & Image / signals-ci (push) Has been cancelled
Signals Reachability Scoring & Events / reachability-smoke (push) Has been cancelled
Signals Reachability Scoring & Events / sign-and-upload (push) Has been cancelled
Manifest Integrity / Validate Schema Integrity (push) Has been cancelled
Manifest Integrity / Validate Contract Documents (push) Has been cancelled
Manifest Integrity / Validate Pack Fixtures (push) Has been cancelled
Manifest Integrity / Audit SHA256SUMS Files (push) Has been cancelled
Manifest Integrity / Verify Merkle Roots (push) Has been cancelled
Docs CI / lint-and-preview (push) Has been cancelled
223 lines
8.1 KiB
C#
223 lines
8.1 KiB
C#
using System;
|
|
using System.IO;
|
|
using System.Text.Json;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using StellaOps.Cryptography;
|
|
|
|
namespace StellaOps.Scanner.Reachability;
|
|
|
|
/// <summary>
|
|
/// Writes richgraph-v1 documents to disk with canonical ordering and compliance-profile-aware hashing.
|
|
/// Uses <see cref="HashPurpose.Graph"/> for content addressing, which resolves to:
|
|
/// - BLAKE3-256 for "world" profile
|
|
/// - SHA-256 for "fips" profile
|
|
/// - GOST3411-2012-256 for "gost" profile
|
|
/// - SM3 for "sm" profile
|
|
/// </summary>
|
|
public sealed class RichGraphWriter
|
|
{
|
|
private readonly ICryptoHash _cryptoHash;
|
|
|
|
private static readonly JsonWriterOptions JsonOptions = new()
|
|
{
|
|
Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
|
|
Indented = false,
|
|
SkipValidation = false
|
|
};
|
|
|
|
/// <summary>
|
|
/// Creates a new RichGraphWriter with the specified crypto hash service.
|
|
/// </summary>
|
|
/// <param name="cryptoHash">Crypto hash service for compliance-aware hashing.</param>
|
|
public RichGraphWriter(ICryptoHash cryptoHash)
|
|
{
|
|
_cryptoHash = cryptoHash ?? throw new ArgumentNullException(nameof(cryptoHash));
|
|
}
|
|
|
|
public async Task<RichGraphWriteResult> WriteAsync(
|
|
RichGraph graph,
|
|
string outputRoot,
|
|
string analysisId,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(graph);
|
|
ArgumentException.ThrowIfNullOrWhiteSpace(outputRoot);
|
|
ArgumentException.ThrowIfNullOrWhiteSpace(analysisId);
|
|
|
|
var trimmed = graph.Trimmed();
|
|
var root = Path.Combine(outputRoot, "reachability_graphs", analysisId);
|
|
Directory.CreateDirectory(root);
|
|
|
|
var graphPath = Path.Combine(root, "richgraph-v1.json");
|
|
|
|
await using (var stream = File.Create(graphPath))
|
|
await using (var writer = new Utf8JsonWriter(stream, JsonOptions))
|
|
{
|
|
WriteGraph(writer, trimmed);
|
|
await writer.FlushAsync(cancellationToken).ConfigureAwait(false);
|
|
}
|
|
|
|
var bytes = await File.ReadAllBytesAsync(graphPath, cancellationToken).ConfigureAwait(false);
|
|
var graphHash = _cryptoHash.ComputePrefixedHashForPurpose(bytes, HashPurpose.Graph);
|
|
|
|
var metaPath = Path.Combine(root, "meta.json");
|
|
await using (var stream = File.Create(metaPath))
|
|
await using (var writer = new Utf8JsonWriter(stream, new JsonWriterOptions { Indented = true }))
|
|
{
|
|
writer.WriteStartObject();
|
|
writer.WriteString("schema", trimmed.Schema);
|
|
writer.WriteString("graph_hash", graphHash);
|
|
writer.WritePropertyName("files");
|
|
writer.WriteStartArray();
|
|
writer.WriteStartObject();
|
|
writer.WriteString("path", graphPath);
|
|
writer.WriteString("hash", graphHash);
|
|
writer.WriteEndObject();
|
|
writer.WriteEndArray();
|
|
writer.WriteEndObject();
|
|
await writer.FlushAsync(cancellationToken).ConfigureAwait(false);
|
|
}
|
|
|
|
return new RichGraphWriteResult(graphPath, metaPath, graphHash, trimmed.Nodes.Count, trimmed.Edges.Count);
|
|
}
|
|
|
|
private static void WriteGraph(Utf8JsonWriter writer, RichGraph graph)
|
|
{
|
|
writer.WriteStartObject();
|
|
writer.WriteString("schema", graph.Schema);
|
|
|
|
writer.WritePropertyName("analyzer");
|
|
writer.WriteStartObject();
|
|
writer.WriteString("name", graph.Analyzer.Name);
|
|
writer.WriteString("version", graph.Analyzer.Version);
|
|
if (!string.IsNullOrWhiteSpace(graph.Analyzer.ToolchainDigest))
|
|
{
|
|
writer.WriteString("toolchain_digest", graph.Analyzer.ToolchainDigest);
|
|
}
|
|
writer.WriteEndObject();
|
|
|
|
writer.WritePropertyName("nodes");
|
|
writer.WriteStartArray();
|
|
foreach (var node in graph.Nodes)
|
|
{
|
|
writer.WriteStartObject();
|
|
writer.WriteString("id", node.Id);
|
|
writer.WriteString("symbol_id", node.SymbolId);
|
|
writer.WriteString("lang", node.Lang);
|
|
writer.WriteString("kind", node.Kind);
|
|
if (!string.IsNullOrWhiteSpace(node.Display)) writer.WriteString("display", node.Display);
|
|
if (!string.IsNullOrWhiteSpace(node.CodeId)) writer.WriteString("code_id", node.CodeId);
|
|
if (!string.IsNullOrWhiteSpace(node.Purl)) writer.WriteString("purl", node.Purl);
|
|
if (!string.IsNullOrWhiteSpace(node.BuildId)) writer.WriteString("build_id", node.BuildId);
|
|
if (!string.IsNullOrWhiteSpace(node.CodeBlockHash)) writer.WriteString("code_block_hash", node.CodeBlockHash);
|
|
if (!string.IsNullOrWhiteSpace(node.SymbolDigest)) writer.WriteString("symbol_digest", node.SymbolDigest);
|
|
if (node.Symbol is not null)
|
|
{
|
|
writer.WritePropertyName("symbol");
|
|
WriteSymbol(writer, node.Symbol);
|
|
}
|
|
|
|
if (node.Evidence is { Count: > 0 })
|
|
{
|
|
writer.WritePropertyName("evidence");
|
|
writer.WriteStartArray();
|
|
foreach (var e in node.Evidence) writer.WriteStringValue(e);
|
|
writer.WriteEndArray();
|
|
}
|
|
|
|
if (node.Attributes is { Count: > 0 })
|
|
{
|
|
writer.WritePropertyName("attributes");
|
|
writer.WriteStartObject();
|
|
foreach (var kv in node.Attributes)
|
|
{
|
|
writer.WriteString(kv.Key, kv.Value);
|
|
}
|
|
writer.WriteEndObject();
|
|
}
|
|
|
|
writer.WriteEndObject();
|
|
}
|
|
writer.WriteEndArray();
|
|
|
|
writer.WritePropertyName("edges");
|
|
writer.WriteStartArray();
|
|
foreach (var edge in graph.Edges)
|
|
{
|
|
writer.WriteStartObject();
|
|
writer.WriteString("from", edge.From);
|
|
writer.WriteString("to", edge.To);
|
|
writer.WriteString("kind", edge.Kind);
|
|
if (!string.IsNullOrWhiteSpace(edge.Purl)) writer.WriteString("purl", edge.Purl);
|
|
if (!string.IsNullOrWhiteSpace(edge.SymbolDigest)) writer.WriteString("symbol_digest", edge.SymbolDigest);
|
|
writer.WriteNumber("confidence", edge.Confidence);
|
|
|
|
if (edge.Evidence is { Count: > 0 })
|
|
{
|
|
writer.WritePropertyName("evidence");
|
|
writer.WriteStartArray();
|
|
foreach (var e in edge.Evidence) writer.WriteStringValue(e);
|
|
writer.WriteEndArray();
|
|
}
|
|
|
|
if (edge.Candidates is { Count: > 0 })
|
|
{
|
|
writer.WritePropertyName("candidates");
|
|
writer.WriteStartArray();
|
|
foreach (var c in edge.Candidates) writer.WriteStringValue(c);
|
|
writer.WriteEndArray();
|
|
}
|
|
|
|
writer.WriteEndObject();
|
|
}
|
|
writer.WriteEndArray();
|
|
|
|
writer.WritePropertyName("roots");
|
|
writer.WriteStartArray();
|
|
foreach (var root in graph.Roots)
|
|
{
|
|
writer.WriteStartObject();
|
|
writer.WriteString("id", root.Id);
|
|
writer.WriteString("phase", root.Phase);
|
|
if (!string.IsNullOrWhiteSpace(root.Source)) writer.WriteString("source", root.Source);
|
|
writer.WriteEndObject();
|
|
}
|
|
writer.WriteEndArray();
|
|
|
|
writer.WriteEndObject();
|
|
}
|
|
|
|
private static void WriteSymbol(Utf8JsonWriter writer, ReachabilitySymbol symbol)
|
|
{
|
|
writer.WriteStartObject();
|
|
if (!string.IsNullOrWhiteSpace(symbol.Mangled))
|
|
{
|
|
writer.WriteString("mangled", symbol.Mangled);
|
|
}
|
|
|
|
if (!string.IsNullOrWhiteSpace(symbol.Demangled))
|
|
{
|
|
writer.WriteString("demangled", symbol.Demangled);
|
|
}
|
|
|
|
if (!string.IsNullOrWhiteSpace(symbol.Source))
|
|
{
|
|
writer.WriteString("source", symbol.Source);
|
|
}
|
|
|
|
if (symbol.Confidence is not null)
|
|
{
|
|
writer.WriteNumber("confidence", symbol.Confidence.Value);
|
|
}
|
|
writer.WriteEndObject();
|
|
}
|
|
}
|
|
|
|
public sealed record RichGraphWriteResult(
|
|
string GraphPath,
|
|
string MetaPath,
|
|
string GraphHash,
|
|
int NodeCount,
|
|
int EdgeCount);
|