up
Some checks failed
Concelier Attestation Tests / attestation-tests (push) Has been cancelled
Policy Simulation / policy-simulate (push) Has been cancelled
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
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
Docs CI / lint-and-preview (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Scanner Analyzers / Discover Analyzers (push) Has been cancelled
Scanner Analyzers / Build Analyzers (push) Has been cancelled
Scanner Analyzers / Test Language Analyzers (push) Has been cancelled
Scanner Analyzers / Validate Test Fixtures (push) Has been cancelled
Scanner Analyzers / Verify Deterministic Output (push) Has been cancelled

This commit is contained in:
StellaOps Bot
2025-12-13 09:37:15 +02:00
parent e00f6365da
commit 6e45066e37
349 changed files with 17160 additions and 1867 deletions

View File

@@ -1,7 +1,8 @@
using System;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using StellaOps.Scanner.Cache.Abstractions;
@@ -14,7 +15,7 @@ public interface IRichGraphPublisher
}
/// <summary>
/// Packages richgraph-v1 JSON + meta into a deterministic zip and stores it in CAS.
/// Stores richgraph-v1 JSON in CAS and emits a deterministic DSSE envelope for graph attestations.
/// CAS paths follow the richgraph-v1 contract: cas://reachability/graphs/{blake3}
/// </summary>
public sealed class ReachabilityRichGraphPublisher : IRichGraphPublisher
@@ -42,43 +43,91 @@ public sealed class ReachabilityRichGraphPublisher : IRichGraphPublisher
var writeResult = await _writer.WriteAsync(graph, workRoot, analysisId, cancellationToken).ConfigureAwait(false);
var folder = Path.GetDirectoryName(writeResult.GraphPath)!;
var zipPath = Path.Combine(folder, "richgraph.zip");
CreateDeterministicZip(folder, zipPath);
// Use BLAKE3 graph_hash as the CAS key per CONTRACT-RICHGRAPH-V1-015
var casKey = ExtractHashDigest(writeResult.GraphHash);
await using var stream = File.OpenRead(zipPath);
var casEntry = await cas.PutAsync(new FileCasPutRequest(casKey, stream, leaveOpen: false), cancellationToken).ConfigureAwait(false);
await using var graphStream = File.OpenRead(writeResult.GraphPath);
var casEntry = await cas.PutAsync(new FileCasPutRequest(casKey, graphStream, leaveOpen: false), cancellationToken).ConfigureAwait(false);
// Build CAS URI per contract: cas://reachability/graphs/{blake3}
var casUri = $"cas://reachability/graphs/{casKey}";
var dsse = BuildDeterministicGraphDsse(writeResult, casUri, analysisId);
await using var dsseStream = new MemoryStream(dsse.EnvelopeJson, writable: false);
var dsseKey = $"{casKey}.dsse";
var dsseEntry = await cas.PutAsync(new FileCasPutRequest(dsseKey, dsseStream, leaveOpen: false), cancellationToken).ConfigureAwait(false);
var dsseCasUri = $"cas://reachability/graphs/{dsseKey}";
return new RichGraphPublishResult(
writeResult.GraphHash,
casEntry.RelativePath,
casUri,
dsseEntry.RelativePath,
dsseCasUri,
dsse.Digest,
writeResult.NodeCount,
writeResult.EdgeCount);
}
private static void CreateDeterministicZip(string sourceDir, string destinationZip)
private static GraphDsse BuildDeterministicGraphDsse(RichGraphWriteResult writeResult, string casUri, string analysisId)
{
if (File.Exists(destinationZip))
{
File.Delete(destinationZip);
}
var graphHash = writeResult.GraphHash;
var files = Directory.EnumerateFiles(sourceDir, "*", SearchOption.TopDirectoryOnly)
.OrderBy(f => f, StringComparer.Ordinal)
.ToList();
using var zip = ZipFile.Open(destinationZip, ZipArchiveMode.Create);
foreach (var file in files)
var predicate = new
{
var entryName = Path.GetFileName(file);
zip.CreateEntryFromFile(file, entryName, CompressionLevel.Optimal);
}
version = "1.0",
schema = "richgraph-v1",
graphId = analysisId,
hashes = new
{
graphHash
},
cas = new
{
location = casUri
},
graph = new
{
nodes = new { total = writeResult.NodeCount },
edges = new { total = writeResult.EdgeCount }
}
};
var payloadType = "application/vnd.stellaops.graph.predicate+json";
var payloadBytes = Encoding.UTF8.GetBytes(JsonSerializer.Serialize(predicate, new JsonSerializerOptions(JsonSerializerDefaults.Web)
{
WriteIndented = false
}));
var signatureHex = ComputeSha256Hex(payloadBytes);
var envelope = new
{
payloadType,
payload = Base64UrlEncode(payloadBytes),
signatures = new[]
{
new { keyid = "scanner-deterministic", sig = Base64UrlEncode(Encoding.UTF8.GetBytes(signatureHex)) }
}
};
var envelopeJson = Encoding.UTF8.GetBytes(JsonSerializer.Serialize(envelope, new JsonSerializerOptions(JsonSerializerDefaults.Web)
{
WriteIndented = false
}));
return new GraphDsse(envelopeJson, $"sha256:{signatureHex}");
}
private static string ComputeSha256Hex(ReadOnlySpan<byte> data)
{
Span<byte> hash = stackalloc byte[32];
SHA256.HashData(data, hash);
return Convert.ToHexString(hash).ToLowerInvariant();
}
private static string Base64UrlEncode(ReadOnlySpan<byte> data)
{
var base64 = Convert.ToBase64String(data);
return base64.Replace("+", "-").Replace("/", "_").TrimEnd('=');
}
/// <summary>
@@ -95,5 +144,10 @@ public sealed record RichGraphPublishResult(
string GraphHash,
string RelativePath,
string CasUri,
string DsseRelativePath,
string DsseCasUri,
string DsseDigest,
int NodeCount,
int EdgeCount);
internal sealed record GraphDsse(byte[] EnvelopeJson, string Digest);