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
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:
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user