Files
git.stella-ops.org/src/Scanner/__Libraries/StellaOps.Scanner.Reachability/RichGraphWriter.cs
StellaOps Bot efaf3cb789
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
up
2025-12-12 09:35:37 +02:00

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