feat: Add in-memory implementations for issuer audit, key, repository, and trust management
Some checks failed
devportal-offline / build-offline (push) Has been cancelled
Mirror Thin Bundle Sign & Verify / mirror-sign (push) Has been cancelled
Docs CI / lint-and-preview (push) Has been cancelled
api-governance / spectral-lint (push) Has been cancelled
oas-ci / oas-validate (push) Has been cancelled
Some checks failed
devportal-offline / build-offline (push) Has been cancelled
Mirror Thin Bundle Sign & Verify / mirror-sign (push) Has been cancelled
Docs CI / lint-and-preview (push) Has been cancelled
api-governance / spectral-lint (push) Has been cancelled
oas-ci / oas-validate (push) Has been cancelled
- Introduced InMemoryIssuerAuditSink to retain audit entries for testing. - Implemented InMemoryIssuerKeyRepository for deterministic key storage. - Created InMemoryIssuerRepository to manage issuer records in memory. - Added InMemoryIssuerTrustRepository for managing issuer trust overrides. - Each repository utilizes concurrent collections for thread-safe operations. - Enhanced deprecation tracking with a comprehensive YAML schema for API governance.
This commit is contained in:
@@ -44,7 +44,9 @@ public sealed class ReachabilityGraphBuilder
|
||||
string? display = null,
|
||||
string? sourceFile = null,
|
||||
int? sourceLine = null,
|
||||
IReadOnlyDictionary<string, string>? attributes = null)
|
||||
IReadOnlyDictionary<string, string>? attributes = null,
|
||||
string? purl = null,
|
||||
string? symbolDigest = null)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(symbolId))
|
||||
{
|
||||
@@ -59,7 +61,9 @@ public sealed class ReachabilityGraphBuilder
|
||||
display?.Trim(),
|
||||
sourceFile?.Trim(),
|
||||
sourceLine,
|
||||
attributes?.ToImmutableSortedDictionary(StringComparer.Ordinal) ?? ImmutableSortedDictionary<string, string>.Empty);
|
||||
attributes?.ToImmutableSortedDictionary(StringComparer.Ordinal) ?? ImmutableSortedDictionary<string, string>.Empty,
|
||||
purl?.Trim(),
|
||||
symbolDigest?.Trim());
|
||||
|
||||
_richNodes[id] = node;
|
||||
nodes.Add(id);
|
||||
@@ -93,6 +97,9 @@ public sealed class ReachabilityGraphBuilder
|
||||
/// <param name="origin">Origin: static or runtime.</param>
|
||||
/// <param name="provenance">Provenance hint: jvm-bytecode, il, ts-ast, ssa, ebpf, etw, jfr, hook.</param>
|
||||
/// <param name="evidence">Evidence locator (e.g., "file:path:line").</param>
|
||||
/// <param name="purl">PURL of the component that defines the callee.</param>
|
||||
/// <param name="symbolDigest">Stable hash of the normalized callee signature.</param>
|
||||
/// <param name="candidates">Ranked candidate purls when resolution is ambiguous.</param>
|
||||
public ReachabilityGraphBuilder AddEdge(
|
||||
string from,
|
||||
string to,
|
||||
@@ -100,7 +107,10 @@ public sealed class ReachabilityGraphBuilder
|
||||
EdgeConfidence confidence,
|
||||
string origin = "static",
|
||||
string? provenance = null,
|
||||
string? evidence = null)
|
||||
string? evidence = null,
|
||||
string? purl = null,
|
||||
string? symbolDigest = null,
|
||||
IReadOnlyList<(string Purl, string? SymbolDigest, double? Score)>? candidates = null)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(from) || string.IsNullOrWhiteSpace(to))
|
||||
{
|
||||
@@ -118,7 +128,10 @@ public sealed class ReachabilityGraphBuilder
|
||||
confidence,
|
||||
origin?.Trim() ?? "static",
|
||||
provenance?.Trim(),
|
||||
evidence?.Trim());
|
||||
evidence?.Trim(),
|
||||
purl?.Trim(),
|
||||
symbolDigest?.Trim(),
|
||||
candidates);
|
||||
|
||||
_richEdges.Add(richEdge);
|
||||
nodes.Add(fromId);
|
||||
@@ -172,7 +185,9 @@ public sealed class ReachabilityGraphBuilder
|
||||
rich.Kind,
|
||||
rich.Display,
|
||||
source,
|
||||
rich.Attributes.Count > 0 ? rich.Attributes : null));
|
||||
rich.Attributes.Count > 0 ? rich.Attributes : null,
|
||||
rich.Purl,
|
||||
rich.SymbolDigest));
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -199,12 +214,17 @@ public sealed class ReachabilityGraphBuilder
|
||||
rich.Provenance,
|
||||
rich.Evidence);
|
||||
|
||||
var candidates = rich.Candidates?.Select(c => new ReachabilityEdgeCandidate(c.Purl, c.SymbolDigest, c.Score)).ToList();
|
||||
|
||||
edgeList.Add(new ReachabilityUnionEdge(
|
||||
rich.From,
|
||||
rich.To,
|
||||
rich.EdgeType,
|
||||
ConfidenceToString(rich.Confidence),
|
||||
source));
|
||||
source,
|
||||
rich.Purl,
|
||||
rich.SymbolDigest,
|
||||
candidates));
|
||||
}
|
||||
|
||||
// Add any legacy edges not already covered
|
||||
@@ -315,7 +335,9 @@ public sealed class ReachabilityGraphBuilder
|
||||
string? Display,
|
||||
string? SourceFile,
|
||||
int? SourceLine,
|
||||
ImmutableSortedDictionary<string, string> Attributes);
|
||||
ImmutableSortedDictionary<string, string> Attributes,
|
||||
string? Purl = null,
|
||||
string? SymbolDigest = null);
|
||||
|
||||
private sealed record RichEdge(
|
||||
string From,
|
||||
@@ -324,7 +346,10 @@ public sealed class ReachabilityGraphBuilder
|
||||
EdgeConfidence Confidence,
|
||||
string Origin,
|
||||
string? Provenance,
|
||||
string? Evidence);
|
||||
string? Evidence,
|
||||
string? Purl = null,
|
||||
string? SymbolDigest = null,
|
||||
IReadOnlyList<(string Purl, string? SymbolDigest, double? Score)>? Candidates = null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -15,6 +15,7 @@ public interface IRichGraphPublisher
|
||||
|
||||
/// <summary>
|
||||
/// Packages richgraph-v1 JSON + meta into a deterministic zip and stores it in CAS.
|
||||
/// CAS paths follow the richgraph-v1 contract: cas://reachability/graphs/{blake3}
|
||||
/// </summary>
|
||||
public sealed class ReachabilityRichGraphPublisher : IRichGraphPublisher
|
||||
{
|
||||
@@ -45,11 +46,20 @@ public sealed class ReachabilityRichGraphPublisher : IRichGraphPublisher
|
||||
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 sha = ComputeSha256(zipPath);
|
||||
var casEntry = await cas.PutAsync(new FileCasPutRequest(sha, stream, leaveOpen: false), cancellationToken).ConfigureAwait(false);
|
||||
var casEntry = await cas.PutAsync(new FileCasPutRequest(casKey, stream, leaveOpen: false), cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return new RichGraphPublishResult(writeResult.GraphHash, casEntry.RelativePath, writeResult.NodeCount, writeResult.EdgeCount);
|
||||
// Build CAS URI per contract: cas://reachability/graphs/{blake3}
|
||||
var casUri = $"cas://reachability/graphs/{casKey}";
|
||||
|
||||
return new RichGraphPublishResult(
|
||||
writeResult.GraphHash,
|
||||
casEntry.RelativePath,
|
||||
casUri,
|
||||
writeResult.NodeCount,
|
||||
writeResult.EdgeCount);
|
||||
}
|
||||
|
||||
private static void CreateDeterministicZip(string sourceDir, string destinationZip)
|
||||
@@ -71,16 +81,19 @@ public sealed class ReachabilityRichGraphPublisher : IRichGraphPublisher
|
||||
}
|
||||
}
|
||||
|
||||
private static string ComputeSha256(string path)
|
||||
/// <summary>
|
||||
/// Extracts the hex digest from a prefixed hash (e.g., "blake3:abc123" → "abc123").
|
||||
/// </summary>
|
||||
private static string ExtractHashDigest(string prefixedHash)
|
||||
{
|
||||
using var sha = System.Security.Cryptography.SHA256.Create();
|
||||
using var stream = File.OpenRead(path);
|
||||
return Convert.ToHexString(sha.ComputeHash(stream)).ToLowerInvariant();
|
||||
var colonIndex = prefixedHash.IndexOf(':');
|
||||
return colonIndex >= 0 ? prefixedHash[(colonIndex + 1)..] : prefixedHash;
|
||||
}
|
||||
}
|
||||
|
||||
public sealed record RichGraphPublishResult(
|
||||
string GraphHash,
|
||||
string RelativePath,
|
||||
string CasUri,
|
||||
int NodeCount,
|
||||
int EdgeCount);
|
||||
|
||||
@@ -75,7 +75,9 @@ public sealed class ReachabilityUnionWriter
|
||||
Source = n.Source?.Trimmed(),
|
||||
Attributes = (n.Attributes ?? ImmutableDictionary<string, string>.Empty)
|
||||
.Where(kv => !string.IsNullOrWhiteSpace(kv.Key) && kv.Value is not null)
|
||||
.ToImmutableSortedDictionary(kv => kv.Key.Trim(), kv => kv.Value!.Trim())
|
||||
.ToImmutableSortedDictionary(kv => kv.Key.Trim(), kv => kv.Value!.Trim()),
|
||||
Purl = Trim(n.Purl),
|
||||
SymbolDigest = Trim(n.SymbolDigest)
|
||||
})
|
||||
.OrderBy(n => n.SymbolId, StringComparer.Ordinal)
|
||||
.ThenBy(n => n.Kind, StringComparer.Ordinal)
|
||||
@@ -89,7 +91,10 @@ public sealed class ReachabilityUnionWriter
|
||||
To = Trim(e.To)!,
|
||||
EdgeType = Trim(e.EdgeType) ?? "call",
|
||||
Confidence = Trim(e.Confidence) ?? "certain",
|
||||
Source = e.Source?.Trimmed()
|
||||
Source = e.Source?.Trimmed(),
|
||||
Purl = Trim(e.Purl),
|
||||
SymbolDigest = Trim(e.SymbolDigest),
|
||||
Candidates = NormalizeCandidates(e.Candidates)
|
||||
})
|
||||
.OrderBy(e => e.From, StringComparer.Ordinal)
|
||||
.ThenBy(e => e.To, StringComparer.Ordinal)
|
||||
@@ -110,6 +115,24 @@ public sealed class ReachabilityUnionWriter
|
||||
return new NormalizedGraph(nodes, edges, facts);
|
||||
}
|
||||
|
||||
private static IReadOnlyList<ReachabilityEdgeCandidate>? NormalizeCandidates(IReadOnlyList<ReachabilityEdgeCandidate>? candidates)
|
||||
{
|
||||
if (candidates is null || candidates.Count == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return candidates
|
||||
.Where(c => !string.IsNullOrWhiteSpace(c.Purl))
|
||||
.Select(c => new ReachabilityEdgeCandidate(
|
||||
c.Purl.Trim(),
|
||||
Trim(c.SymbolDigest),
|
||||
c.Score))
|
||||
.OrderByDescending(c => c.Score ?? 0)
|
||||
.ThenBy(c => c.Purl, StringComparer.Ordinal)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private static async Task<FileHashInfo> WriteNdjsonAsync<T>(
|
||||
string path,
|
||||
IReadOnlyCollection<T> items,
|
||||
@@ -145,6 +168,16 @@ public sealed class ReachabilityUnionWriter
|
||||
jw.WriteString("display", node.Display);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(node.Purl))
|
||||
{
|
||||
jw.WriteString("purl", node.Purl);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(node.SymbolDigest))
|
||||
{
|
||||
jw.WriteString("symbol_digest", node.SymbolDigest);
|
||||
}
|
||||
|
||||
if (node.Source is not null)
|
||||
{
|
||||
jw.WritePropertyName("source");
|
||||
@@ -180,6 +213,37 @@ public sealed class ReachabilityUnionWriter
|
||||
jw.WriteString("edge_type", edge.EdgeType);
|
||||
jw.WriteString("confidence", edge.Confidence);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(edge.Purl))
|
||||
{
|
||||
jw.WriteString("purl", edge.Purl);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(edge.SymbolDigest))
|
||||
{
|
||||
jw.WriteString("symbol_digest", edge.SymbolDigest);
|
||||
}
|
||||
|
||||
if (edge.Candidates is { Count: > 0 })
|
||||
{
|
||||
jw.WritePropertyName("candidates");
|
||||
jw.WriteStartArray();
|
||||
foreach (var candidate in edge.Candidates)
|
||||
{
|
||||
jw.WriteStartObject();
|
||||
jw.WriteString("purl", candidate.Purl);
|
||||
if (!string.IsNullOrWhiteSpace(candidate.SymbolDigest))
|
||||
{
|
||||
jw.WriteString("symbol_digest", candidate.SymbolDigest);
|
||||
}
|
||||
if (candidate.Score.HasValue)
|
||||
{
|
||||
jw.WriteNumber("score", candidate.Score.Value);
|
||||
}
|
||||
jw.WriteEndObject();
|
||||
}
|
||||
jw.WriteEndArray();
|
||||
}
|
||||
|
||||
if (edge.Source is not null)
|
||||
{
|
||||
jw.WritePropertyName("source");
|
||||
@@ -327,14 +391,27 @@ public sealed record ReachabilityUnionNode(
|
||||
string Kind,
|
||||
string? Display = null,
|
||||
ReachabilitySource? Source = null,
|
||||
IReadOnlyDictionary<string, string>? Attributes = null);
|
||||
IReadOnlyDictionary<string, string>? Attributes = null,
|
||||
string? Purl = null,
|
||||
string? SymbolDigest = null);
|
||||
|
||||
public sealed record ReachabilityUnionEdge(
|
||||
string From,
|
||||
string To,
|
||||
string EdgeType,
|
||||
string? Confidence = "certain",
|
||||
ReachabilitySource? Source = null);
|
||||
ReachabilitySource? Source = null,
|
||||
string? Purl = null,
|
||||
string? SymbolDigest = null,
|
||||
IReadOnlyList<ReachabilityEdgeCandidate>? Candidates = null);
|
||||
|
||||
/// <summary>
|
||||
/// Represents a candidate purl+digest when callee resolution is ambiguous.
|
||||
/// </summary>
|
||||
public sealed record ReachabilityEdgeCandidate(
|
||||
string Purl,
|
||||
string? SymbolDigest = null,
|
||||
double? Score = null);
|
||||
|
||||
public sealed record ReachabilityRuntimeFact(
|
||||
string SymbolId,
|
||||
|
||||
Reference in New Issue
Block a user