prep docs and service updates
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
This commit is contained in:
@@ -13,6 +13,8 @@ Provide language-agnostic collection, normalization, and scoring of reachability
|
||||
## Required Reading
|
||||
- `docs/modules/zastava/architecture.md`
|
||||
- `docs/modules/platform/architecture-overview.md`
|
||||
- `docs/signals/unknowns-registry.md`
|
||||
- `docs/reachability/DELIVERY_GUIDE.md` (unknowns + runtime ingestion sections)
|
||||
|
||||
## Working Agreement
|
||||
- 1. Update task status to `DOING`/`DONE` in both correspoding sprint file `/docs/implplan/SPRINT_*.md` and the local `TASKS.md` when you start or finish work.
|
||||
|
||||
@@ -32,13 +32,21 @@ public sealed class CallgraphDocument
|
||||
[BsonElement("nodes")]
|
||||
public List<CallgraphNode> Nodes { get; set; } = new();
|
||||
|
||||
[BsonElement("edges")]
|
||||
public List<CallgraphEdge> Edges { get; set; } = new();
|
||||
|
||||
[BsonElement("edges")]
|
||||
public List<CallgraphEdge> Edges { get; set; } = new();
|
||||
|
||||
[BsonElement("metadata")]
|
||||
[BsonIgnoreIfNull]
|
||||
public Dictionary<string, string?>? Metadata { get; set; }
|
||||
|
||||
[BsonElement("graphHash")]
|
||||
public string GraphHash { get; set; } = string.Empty;
|
||||
|
||||
[BsonElement("roots")]
|
||||
[BsonIgnoreIfNull]
|
||||
public List<CallgraphRoot>? Roots { get; set; }
|
||||
|
||||
[BsonElement("schemaVersion")]
|
||||
[BsonIgnoreIfNull]
|
||||
public string? SchemaVersion { get; set; }
|
||||
}
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
namespace StellaOps.Signals.Models;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace StellaOps.Signals.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Normalized callgraph edge.
|
||||
/// </summary>
|
||||
public sealed record CallgraphEdge(
|
||||
string SourceId,
|
||||
string TargetId,
|
||||
string Type);
|
||||
public sealed record CallgraphEdge(
|
||||
string SourceId,
|
||||
string TargetId,
|
||||
string Type,
|
||||
string? Purl = null,
|
||||
string? SymbolDigest = null,
|
||||
IReadOnlyList<string>? Candidates = null,
|
||||
double? Confidence = null,
|
||||
IReadOnlyList<string>? Evidence = null);
|
||||
|
||||
@@ -6,11 +6,13 @@ namespace StellaOps.Signals.Models;
|
||||
/// <summary>
|
||||
/// API request payload for callgraph ingestion.
|
||||
/// </summary>
|
||||
public sealed record CallgraphIngestRequest(
|
||||
[property: Required] string Language,
|
||||
[property: Required] string Component,
|
||||
[property: Required] string Version,
|
||||
[property: Required] string ArtifactContentType,
|
||||
[property: Required] string ArtifactFileName,
|
||||
[property: Required] string ArtifactContentBase64,
|
||||
IReadOnlyDictionary<string, string?>? Metadata);
|
||||
public sealed record CallgraphIngestRequest(
|
||||
[property: Required] string Language,
|
||||
[property: Required] string Component,
|
||||
[property: Required] string Version,
|
||||
[property: Required] string ArtifactContentType,
|
||||
[property: Required] string ArtifactFileName,
|
||||
[property: Required] string ArtifactContentBase64,
|
||||
IReadOnlyDictionary<string, string?>? Metadata,
|
||||
string? SchemaVersion = null,
|
||||
IReadOnlyDictionary<string, string?>? Analyzer = null);
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
namespace StellaOps.Signals.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Response returned after callgraph ingestion.
|
||||
/// </summary>
|
||||
/// <summary>
|
||||
/// Response returned after callgraph ingestion.
|
||||
/// </summary>
|
||||
public sealed record CallgraphIngestResponse(
|
||||
string CallgraphId,
|
||||
string ArtifactPath,
|
||||
string ArtifactHash,
|
||||
string CasUri,
|
||||
string GraphHash,
|
||||
string ManifestCasUri);
|
||||
string ManifestCasUri,
|
||||
string SchemaVersion,
|
||||
int NodeCount,
|
||||
int EdgeCount,
|
||||
int RootCount);
|
||||
|
||||
@@ -17,6 +17,9 @@ public sealed class CallgraphManifest
|
||||
[JsonPropertyName("graphHash")]
|
||||
public string GraphHash { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("schemaVersion")]
|
||||
public string SchemaVersion { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("artifactHash")]
|
||||
public string ArtifactHash { get; set; } = string.Empty;
|
||||
|
||||
@@ -26,6 +29,9 @@ public sealed class CallgraphManifest
|
||||
[JsonPropertyName("edgeCount")]
|
||||
public int EdgeCount { get; set; }
|
||||
|
||||
[JsonPropertyName("rootCount")]
|
||||
public int RootCount { get; set; }
|
||||
|
||||
[JsonPropertyName("createdAt")]
|
||||
public DateTimeOffset CreatedAt { get; set; }
|
||||
}
|
||||
|
||||
@@ -1,12 +1,21 @@
|
||||
namespace StellaOps.Signals.Models;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace StellaOps.Signals.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Normalized callgraph node.
|
||||
/// </summary>
|
||||
public sealed record CallgraphNode(
|
||||
string Id,
|
||||
string Name,
|
||||
string Kind,
|
||||
string? Namespace,
|
||||
string? File,
|
||||
int? Line);
|
||||
public sealed record CallgraphNode(
|
||||
string Id,
|
||||
string Name,
|
||||
string Kind,
|
||||
string? Namespace,
|
||||
string? File,
|
||||
int? Line,
|
||||
string? Purl = null,
|
||||
string? SymbolDigest = null,
|
||||
string? BuildId = null,
|
||||
string? Language = null,
|
||||
IReadOnlyList<string>? Evidence = null,
|
||||
IReadOnlyDictionary<string, string?>? Analyzer = null,
|
||||
string? CodeId = null);
|
||||
|
||||
9
src/Signals/StellaOps.Signals/Models/CallgraphRoot.cs
Normal file
9
src/Signals/StellaOps.Signals/Models/CallgraphRoot.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
namespace StellaOps.Signals.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Synthetic or declared graph root (e.g., main, init_array constructor).
|
||||
/// </summary>
|
||||
public sealed record CallgraphRoot(
|
||||
string Id,
|
||||
string Phase,
|
||||
string? Source = null);
|
||||
@@ -110,6 +110,18 @@ public sealed class RuntimeFactDocument
|
||||
[BsonIgnoreIfNull]
|
||||
public string? CodeId { get; set; }
|
||||
|
||||
[BsonElement("symbolDigest")]
|
||||
[BsonIgnoreIfNull]
|
||||
public string? SymbolDigest { get; set; }
|
||||
|
||||
[BsonElement("purl")]
|
||||
[BsonIgnoreIfNull]
|
||||
public string? Purl { get; set; }
|
||||
|
||||
[BsonElement("buildId")]
|
||||
[BsonIgnoreIfNull]
|
||||
public string? BuildId { get; set; }
|
||||
|
||||
[BsonElement("loaderBase")]
|
||||
[BsonIgnoreIfNull]
|
||||
public string? LoaderBase { get; set; }
|
||||
@@ -137,6 +149,10 @@ public sealed class RuntimeFactDocument
|
||||
[BsonElement("hitCount")]
|
||||
public int HitCount { get; set; }
|
||||
|
||||
[BsonElement("observedAt")]
|
||||
[BsonIgnoreIfNull]
|
||||
public DateTimeOffset? ObservedAt { get; set; }
|
||||
|
||||
[BsonElement("metadata")]
|
||||
[BsonIgnoreIfNull]
|
||||
public Dictionary<string, string?>? Metadata { get; set; }
|
||||
|
||||
@@ -24,6 +24,12 @@ public sealed class RuntimeFactEvent
|
||||
|
||||
public string? CodeId { get; set; }
|
||||
|
||||
public string? SymbolDigest { get; set; }
|
||||
|
||||
public string? Purl { get; set; }
|
||||
|
||||
public string? BuildId { get; set; }
|
||||
|
||||
public string? LoaderBase { get; set; }
|
||||
|
||||
public int? ProcessId { get; set; }
|
||||
@@ -38,6 +44,8 @@ public sealed class RuntimeFactEvent
|
||||
|
||||
public int HitCount { get; set; } = 1;
|
||||
|
||||
public DateTimeOffset? ObservedAt { get; set; }
|
||||
|
||||
public Dictionary<string, string?>? Metadata { get; set; }
|
||||
}
|
||||
|
||||
|
||||
@@ -19,11 +19,15 @@ public sealed class RuntimeFactsStreamMetadata
|
||||
[FromQuery(Name = "version")]
|
||||
public string? Version { get; set; }
|
||||
|
||||
[FromQuery(Name = "purl")]
|
||||
public string? Purl { get; set; }
|
||||
|
||||
public ReachabilitySubject ToSubject() => new()
|
||||
{
|
||||
ScanId = ScanId,
|
||||
ImageDigest = ImageDigest,
|
||||
Component = Component,
|
||||
Version = Version
|
||||
Version = Version,
|
||||
// purl is kept at runtime-fact level; subject stays coarse.
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
namespace StellaOps.Signals.Options;
|
||||
|
||||
/// <summary>
|
||||
/// OpenAPI exposure options.
|
||||
/// </summary>
|
||||
public sealed class SignalsOpenApiOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether to expose OpenAPI description.
|
||||
/// </summary>
|
||||
public bool Enabled { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// UI path if enabled (e.g., /signals/swagger).
|
||||
/// </summary>
|
||||
public string UiPath { get; set; } = "/signals/swagger";
|
||||
|
||||
/// <summary>
|
||||
/// JSON path if enabled (e.g., /signals/openapi.json).
|
||||
/// </summary>
|
||||
public string JsonPath { get; set; } = "/signals/openapi.json";
|
||||
|
||||
public void Validate()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(UiPath)) UiPath = "/signals/swagger";
|
||||
if (string.IsNullOrWhiteSpace(JsonPath)) JsonPath = "/signals/openapi.json";
|
||||
}
|
||||
}
|
||||
@@ -39,6 +39,11 @@ public sealed class SignalsOptions
|
||||
/// Cache configuration.
|
||||
/// </summary>
|
||||
public SignalsCacheOptions Cache { get; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// OpenAPI exposure (if enabled).
|
||||
/// </summary>
|
||||
public SignalsOpenApiOptions OpenApi { get; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Validates configured options.
|
||||
@@ -51,5 +56,6 @@ public sealed class SignalsOptions
|
||||
AirGap.Validate();
|
||||
Scoring.Validate();
|
||||
Cache.Validate();
|
||||
OpenApi.Validate();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,10 +3,13 @@ using StellaOps.Signals.Models;
|
||||
|
||||
namespace StellaOps.Signals.Parsing;
|
||||
|
||||
/// <summary>
|
||||
/// Result produced by a callgraph parser.
|
||||
/// </summary>
|
||||
public sealed record CallgraphParseResult(
|
||||
IReadOnlyList<CallgraphNode> Nodes,
|
||||
IReadOnlyList<CallgraphEdge> Edges,
|
||||
string FormatVersion);
|
||||
/// <summary>
|
||||
/// Result produced by a callgraph parser.
|
||||
/// </summary>
|
||||
public sealed record CallgraphParseResult(
|
||||
IReadOnlyList<CallgraphNode> Nodes,
|
||||
IReadOnlyList<CallgraphEdge> Edges,
|
||||
IReadOnlyList<CallgraphRoot> Roots,
|
||||
string FormatVersion,
|
||||
string SchemaVersion,
|
||||
IReadOnlyDictionary<string, string?>? Analyzer = null);
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using StellaOps.Signals.Models;
|
||||
|
||||
namespace StellaOps.Signals.Parsing;
|
||||
@@ -74,7 +74,14 @@ public sealed class SimpleJsonCallgraphParser : ICallgraphParser
|
||||
Kind: nodeElement.TryGetProperty("kind", out var kindEl) ? kindEl.GetString() ?? "function" : "function",
|
||||
Namespace: nodeElement.TryGetProperty("namespace", out var nsEl) ? nsEl.GetString() : null,
|
||||
File: nodeElement.TryGetProperty("file", out var fileEl) ? fileEl.GetString() : null,
|
||||
Line: nodeElement.TryGetProperty("line", out var lineEl) && lineEl.ValueKind == JsonValueKind.Number ? lineEl.GetInt32() : null));
|
||||
Line: nodeElement.TryGetProperty("line", out var lineEl) && lineEl.ValueKind == JsonValueKind.Number ? lineEl.GetInt32() : null,
|
||||
Purl: GetString(nodeElement, "purl"),
|
||||
SymbolDigest: GetString(nodeElement, "symbol_digest", "symbolDigest"),
|
||||
BuildId: GetString(nodeElement, "build_id", "buildId"),
|
||||
Language: GetString(nodeElement, "language"),
|
||||
Evidence: GetStringArray(nodeElement, "evidence"),
|
||||
Analyzer: GetStringDictionary(nodeElement, "analyzer"),
|
||||
CodeId: GetString(nodeElement, "code_id", "codeId")));
|
||||
}
|
||||
|
||||
var edges = new List<CallgraphEdge>();
|
||||
@@ -90,7 +97,15 @@ public sealed class SimpleJsonCallgraphParser : ICallgraphParser
|
||||
}
|
||||
|
||||
var type = edgeElement.TryGetProperty("type", out var typeEl) ? typeEl.GetString() ?? "call" : "call";
|
||||
edges.Add(new CallgraphEdge(source.Trim(), target.Trim(), type));
|
||||
edges.Add(new CallgraphEdge(
|
||||
source.Trim(),
|
||||
target.Trim(),
|
||||
type,
|
||||
Purl: GetString(edgeElement, "purl"),
|
||||
SymbolDigest: GetString(edgeElement, "symbol_digest", "symbolDigest"),
|
||||
Candidates: GetStringArray(edgeElement, "candidates"),
|
||||
Confidence: GetNullableDouble(edgeElement, "confidence"),
|
||||
Evidence: GetStringArray(edgeElement, "evidence")));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,7 +113,19 @@ public sealed class SimpleJsonCallgraphParser : ICallgraphParser
|
||||
? versionEl.GetString()
|
||||
: null;
|
||||
|
||||
result = new CallgraphParseResult(nodes, edges, string.IsNullOrWhiteSpace(formatVersion) ? "1.0" : formatVersion!.Trim());
|
||||
var schemaVersion = root.TryGetProperty("schema_version", out var schemaEl)
|
||||
? schemaEl.GetString()
|
||||
: formatVersion;
|
||||
|
||||
var analyzer = GetStringDictionary(root, "analyzer") ?? GetStringDictionary(root, "toolchain");
|
||||
|
||||
result = new CallgraphParseResult(
|
||||
nodes,
|
||||
edges,
|
||||
Array.Empty<CallgraphRoot>(),
|
||||
string.IsNullOrWhiteSpace(formatVersion) ? "1.0" : formatVersion!.Trim(),
|
||||
string.IsNullOrWhiteSpace(schemaVersion) ? "1.0" : schemaVersion!.Trim(),
|
||||
analyzer);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -128,7 +155,14 @@ public sealed class SimpleJsonCallgraphParser : ICallgraphParser
|
||||
Kind: nodeElement.TryGetProperty("kind", out var kindEl) ? kindEl.GetString() ?? "function" : "function",
|
||||
Namespace: nodeElement.TryGetProperty("namespace", out var nsEl) ? nsEl.GetString() : null,
|
||||
File: nodeElement.TryGetProperty("file", out var fileEl) ? fileEl.GetString() : null,
|
||||
Line: nodeElement.TryGetProperty("line", out var lineEl) && lineEl.ValueKind == JsonValueKind.Number ? lineEl.GetInt32() : null));
|
||||
Line: nodeElement.TryGetProperty("line", out var lineEl) && lineEl.ValueKind == JsonValueKind.Number ? lineEl.GetInt32() : null,
|
||||
Purl: GetString(nodeElement, "purl"),
|
||||
SymbolDigest: GetString(nodeElement, "symbol_digest", "symbolDigest"),
|
||||
BuildId: GetString(nodeElement, "build_id", "buildId"),
|
||||
Language: GetString(nodeElement, "language"),
|
||||
Evidence: GetStringArray(nodeElement, "evidence"),
|
||||
Analyzer: GetStringDictionary(nodeElement, "analyzer"),
|
||||
CodeId: GetString(nodeElement, "code_id", "codeId")));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -155,7 +189,15 @@ public sealed class SimpleJsonCallgraphParser : ICallgraphParser
|
||||
? typeEl.GetString() ?? "call"
|
||||
: "call";
|
||||
|
||||
edges.Add(new CallgraphEdge(from.Trim(), to.Trim(), kind));
|
||||
edges.Add(new CallgraphEdge(
|
||||
from.Trim(),
|
||||
to.Trim(),
|
||||
kind,
|
||||
Purl: GetString(edgeElement, "purl"),
|
||||
SymbolDigest: GetString(edgeElement, "symbol_digest", "symbolDigest"),
|
||||
Candidates: GetStringArray(edgeElement, "candidates"),
|
||||
Confidence: GetNullableDouble(edgeElement, "confidence"),
|
||||
Evidence: GetStringArray(edgeElement, "evidence")));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -171,7 +213,7 @@ public sealed class SimpleJsonCallgraphParser : ICallgraphParser
|
||||
|
||||
foreach (var nodeId in uniqueNodeIds)
|
||||
{
|
||||
nodes.Add(new CallgraphNode(nodeId, nodeId, "function", null, null, null));
|
||||
nodes.Add(new CallgraphNode(nodeId, nodeId, "function", null, null, null, null, null, null, null, null, null, null));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -179,8 +221,105 @@ public sealed class SimpleJsonCallgraphParser : ICallgraphParser
|
||||
? schemaEl.GetString()
|
||||
: "1.0";
|
||||
|
||||
result = new CallgraphParseResult(nodes, edges, string.IsNullOrWhiteSpace(schemaVersion) ? "1.0" : schemaVersion!.Trim());
|
||||
var roots = ParseRoots(root);
|
||||
|
||||
var analyzer = GetStringDictionary(root, "analyzer") ?? GetStringDictionary(root, "toolchain");
|
||||
|
||||
result = new CallgraphParseResult(
|
||||
nodes,
|
||||
edges,
|
||||
roots,
|
||||
string.IsNullOrWhiteSpace(schemaVersion) ? "1.0" : schemaVersion!.Trim(),
|
||||
string.IsNullOrWhiteSpace(schemaVersion) ? "1.0" : schemaVersion!.Trim(),
|
||||
analyzer);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static IReadOnlyList<CallgraphRoot> ParseRoots(JsonElement root)
|
||||
{
|
||||
if (!root.TryGetProperty("roots", out var rootsEl) || rootsEl.ValueKind != JsonValueKind.Array)
|
||||
{
|
||||
return Array.Empty<CallgraphRoot>();
|
||||
}
|
||||
|
||||
var roots = new List<CallgraphRoot>(rootsEl.GetArrayLength());
|
||||
foreach (var r in rootsEl.EnumerateArray())
|
||||
{
|
||||
var id = GetString(r, "id");
|
||||
if (string.IsNullOrWhiteSpace(id))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
var phase = GetString(r, "phase") ?? "runtime";
|
||||
var source = GetString(r, "source");
|
||||
roots.Add(new CallgraphRoot(id.Trim(), phase.Trim(), source));
|
||||
}
|
||||
|
||||
return roots;
|
||||
}
|
||||
|
||||
private static string? GetString(JsonElement element, string name1, string? name2 = null)
|
||||
{
|
||||
if (element.TryGetProperty(name1, out var v1) && v1.ValueKind == JsonValueKind.String)
|
||||
{
|
||||
return v1.GetString();
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(name2) && element.TryGetProperty(name2!, out var v2) && v2.ValueKind == JsonValueKind.String)
|
||||
{
|
||||
return v2.GetString();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static IReadOnlyList<string>? GetStringArray(JsonElement element, string name)
|
||||
{
|
||||
if (!element.TryGetProperty(name, out var arr) || arr.ValueKind != JsonValueKind.Array)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var list = new List<string>(arr.GetArrayLength());
|
||||
foreach (var item in arr.EnumerateArray())
|
||||
{
|
||||
if (item.ValueKind == JsonValueKind.String)
|
||||
{
|
||||
list.Add(item.GetString()!);
|
||||
}
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
private static IReadOnlyDictionary<string, string?>? GetStringDictionary(JsonElement element, string name)
|
||||
{
|
||||
if (!element.TryGetProperty(name, out var obj) || obj.ValueKind != JsonValueKind.Object)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var dict = new Dictionary<string, string?>(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var prop in obj.EnumerateObject())
|
||||
{
|
||||
dict[prop.Name] = prop.Value.ValueKind == JsonValueKind.String ? prop.Value.GetString() : prop.Value.ToString();
|
||||
}
|
||||
|
||||
return dict;
|
||||
}
|
||||
|
||||
private static double? GetNullableDouble(JsonElement element, string name)
|
||||
{
|
||||
if (!element.TryGetProperty(name, out var val))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return val.ValueKind switch
|
||||
{
|
||||
JsonValueKind.Number when val.TryGetDouble(out var d) => d,
|
||||
_ => null
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -59,6 +59,10 @@ internal sealed class CallgraphIngestionService : ICallgraphIngestionService
|
||||
var artifactBytes = Convert.FromBase64String(request.ArtifactContentBase64);
|
||||
await using var parseStream = new MemoryStream(artifactBytes, writable: false);
|
||||
var parseResult = await parser.ParseAsync(parseStream, cancellationToken).ConfigureAwait(false);
|
||||
var schemaVersion = !string.IsNullOrWhiteSpace(request.SchemaVersion)
|
||||
? request.SchemaVersion!
|
||||
: parseResult.SchemaVersion;
|
||||
var analyzerMeta = request.Analyzer ?? parseResult.Analyzer;
|
||||
|
||||
parseStream.Position = 0;
|
||||
var artifactHash = ComputeSha256(artifactBytes);
|
||||
@@ -71,8 +75,10 @@ internal sealed class CallgraphIngestionService : ICallgraphIngestionService
|
||||
Version = request.Version,
|
||||
ArtifactHash = artifactHash,
|
||||
GraphHash = graphHash,
|
||||
SchemaVersion = schemaVersion,
|
||||
NodeCount = parseResult.Nodes.Count,
|
||||
EdgeCount = parseResult.Edges.Count,
|
||||
RootCount = parseResult.Roots.Count,
|
||||
CreatedAt = timeProvider.GetUtcNow()
|
||||
};
|
||||
|
||||
@@ -95,14 +101,15 @@ internal sealed class CallgraphIngestionService : ICallgraphIngestionService
|
||||
|
||||
var document = new CallgraphDocument
|
||||
{
|
||||
Language = parser.Language,
|
||||
Component = request.Component,
|
||||
Version = request.Version,
|
||||
Nodes = new List<CallgraphNode>(parseResult.Nodes),
|
||||
Edges = new List<CallgraphEdge>(parseResult.Edges),
|
||||
Metadata = request.Metadata is null
|
||||
? null
|
||||
: new Dictionary<string, string?>(request.Metadata, StringComparer.OrdinalIgnoreCase),
|
||||
Language = parser.Language,
|
||||
Component = request.Component,
|
||||
Version = request.Version,
|
||||
Nodes = new List<CallgraphNode>(parseResult.Nodes),
|
||||
Edges = new List<CallgraphEdge>(parseResult.Edges),
|
||||
Roots = new List<CallgraphRoot>(parseResult.Roots),
|
||||
Metadata = request.Metadata is null
|
||||
? null
|
||||
: new Dictionary<string, string?>(request.Metadata, StringComparer.OrdinalIgnoreCase),
|
||||
Artifact = new CallgraphArtifactMetadata
|
||||
{
|
||||
Path = artifactMetadata.Path,
|
||||
@@ -119,7 +126,16 @@ internal sealed class CallgraphIngestionService : ICallgraphIngestionService
|
||||
|
||||
document.Metadata ??= new Dictionary<string, string?>(StringComparer.OrdinalIgnoreCase);
|
||||
document.Metadata["formatVersion"] = parseResult.FormatVersion;
|
||||
document.Metadata["schemaVersion"] = schemaVersion;
|
||||
if (analyzerMeta is not null)
|
||||
{
|
||||
foreach (var kv in analyzerMeta)
|
||||
{
|
||||
document.Metadata[$"analyzer.{kv.Key}"] = kv.Value;
|
||||
}
|
||||
}
|
||||
document.GraphHash = graphHash;
|
||||
document.SchemaVersion = schemaVersion;
|
||||
|
||||
document = await repository.UpsertAsync(document, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
@@ -138,7 +154,11 @@ internal sealed class CallgraphIngestionService : ICallgraphIngestionService
|
||||
document.Artifact.Hash,
|
||||
document.Artifact.CasUri,
|
||||
graphHash,
|
||||
document.Artifact.ManifestCasUri);
|
||||
document.Artifact.ManifestCasUri,
|
||||
schemaVersion,
|
||||
document.Nodes.Count,
|
||||
document.Edges.Count,
|
||||
document.Roots?.Count ?? 0);
|
||||
}
|
||||
|
||||
private static void ValidateRequest(CallgraphIngestRequest request)
|
||||
@@ -186,18 +206,73 @@ internal sealed class CallgraphIngestionService : ICallgraphIngestionService
|
||||
private static string ComputeGraphHash(CallgraphParseResult result)
|
||||
{
|
||||
var builder = new StringBuilder();
|
||||
|
||||
builder.Append("schema|").Append(result.SchemaVersion).AppendLine();
|
||||
|
||||
foreach (var node in result.Nodes.OrderBy(n => n.Id, StringComparer.Ordinal))
|
||||
{
|
||||
builder.Append(node.Id).Append('|').Append(node.Name).AppendLine();
|
||||
builder
|
||||
.Append(node.Id).Append('|')
|
||||
.Append(node.Name).Append('|')
|
||||
.Append(node.Kind).Append('|')
|
||||
.Append(node.Namespace).Append('|')
|
||||
.Append(node.File).Append('|')
|
||||
.Append(node.Line?.ToString() ?? string.Empty).Append('|')
|
||||
.Append(node.Purl).Append('|')
|
||||
.Append(node.SymbolDigest).Append('|')
|
||||
.Append(node.BuildId).Append('|')
|
||||
.Append(node.CodeId).Append('|')
|
||||
.Append(node.Language).Append('|')
|
||||
.Append(Join(node.Evidence)).Append('|')
|
||||
.Append(JoinDict(node.Analyzer))
|
||||
.AppendLine();
|
||||
}
|
||||
|
||||
foreach (var edge in result.Edges.OrderBy(e => e.SourceId, StringComparer.Ordinal).ThenBy(e => e.TargetId, StringComparer.Ordinal))
|
||||
{
|
||||
builder.Append(edge.SourceId).Append("->").Append(edge.TargetId).AppendLine();
|
||||
builder
|
||||
.Append(edge.SourceId).Append("->").Append(edge.TargetId).Append('|')
|
||||
.Append(edge.Type).Append('|')
|
||||
.Append(edge.Purl).Append('|')
|
||||
.Append(edge.SymbolDigest).Append('|')
|
||||
.Append(edge.Confidence?.ToString() ?? string.Empty).Append('|')
|
||||
.Append(Join(edge.Candidates)).Append('|')
|
||||
.Append(Join(edge.Evidence))
|
||||
.AppendLine();
|
||||
}
|
||||
|
||||
foreach (var root in result.Roots.OrderBy(r => r.Id, StringComparer.Ordinal))
|
||||
{
|
||||
builder.Append("root|").Append(root.Id).Append('|').Append(root.Phase).Append('|').Append(root.Source).AppendLine();
|
||||
}
|
||||
|
||||
return ComputeSha256(Encoding.UTF8.GetBytes(builder.ToString()));
|
||||
}
|
||||
|
||||
private static string Join(IEnumerable<string>? values)
|
||||
{
|
||||
if (values is null)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
return string.Join(',', values.OrderBy(v => v, StringComparer.Ordinal));
|
||||
}
|
||||
|
||||
private static string JoinDict(IReadOnlyDictionary<string, string?>? values)
|
||||
{
|
||||
if (values is null)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
var ordered = new StringBuilder();
|
||||
foreach (var kv in values.OrderBy(k => k.Key, StringComparer.Ordinal))
|
||||
{
|
||||
ordered.Append(kv.Key).Append('=').Append(kv.Value).Append(';');
|
||||
}
|
||||
return ordered.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -125,7 +125,13 @@ public sealed class RuntimeFactsIngestionService : IRuntimeFactsIngestionService
|
||||
continue;
|
||||
}
|
||||
|
||||
var key = new RuntimeFactKey(evt.SymbolId.Trim(), evt.CodeId?.Trim(), evt.LoaderBase?.Trim());
|
||||
var key = new RuntimeFactKey(
|
||||
evt.SymbolId.Trim(),
|
||||
evt.CodeId?.Trim(),
|
||||
evt.LoaderBase?.Trim(),
|
||||
evt.Purl?.Trim(),
|
||||
evt.SymbolDigest?.Trim(),
|
||||
evt.BuildId?.Trim());
|
||||
if (!map.TryGetValue(key, out var document))
|
||||
{
|
||||
document = new RuntimeFactDocument
|
||||
@@ -133,11 +139,15 @@ public sealed class RuntimeFactsIngestionService : IRuntimeFactsIngestionService
|
||||
SymbolId = key.SymbolId,
|
||||
CodeId = key.CodeId,
|
||||
LoaderBase = key.LoaderBase,
|
||||
Purl = key.Purl,
|
||||
SymbolDigest = key.SymbolDigest,
|
||||
BuildId = key.BuildId,
|
||||
ProcessId = evt.ProcessId,
|
||||
ProcessName = Normalize(evt.ProcessName),
|
||||
SocketAddress = Normalize(evt.SocketAddress),
|
||||
ContainerId = Normalize(evt.ContainerId),
|
||||
EvidenceUri = Normalize(evt.EvidenceUri),
|
||||
ObservedAt = evt.ObservedAt,
|
||||
Metadata = evt.Metadata != null
|
||||
? new Dictionary<string, string?>(evt.Metadata, StringComparer.Ordinal)
|
||||
: null
|
||||
@@ -155,6 +165,10 @@ public sealed class RuntimeFactsIngestionService : IRuntimeFactsIngestionService
|
||||
}
|
||||
|
||||
document.HitCount = Math.Clamp(document.HitCount + Math.Max(evt.HitCount, 1), 1, int.MaxValue);
|
||||
document.Purl ??= Normalize(evt.Purl);
|
||||
document.SymbolDigest ??= Normalize(evt.SymbolDigest);
|
||||
document.BuildId ??= Normalize(evt.BuildId);
|
||||
document.ObservedAt ??= evt.ObservedAt;
|
||||
}
|
||||
|
||||
return map.Values.ToList();
|
||||
@@ -194,18 +208,22 @@ public sealed class RuntimeFactsIngestionService : IRuntimeFactsIngestionService
|
||||
{
|
||||
foreach (var fact in existing)
|
||||
{
|
||||
var key = new RuntimeFactKey(fact.SymbolId, fact.CodeId, fact.LoaderBase);
|
||||
var key = new RuntimeFactKey(fact.SymbolId, fact.CodeId, fact.LoaderBase, fact.Purl, fact.SymbolDigest, fact.BuildId);
|
||||
map[key] = new RuntimeFactDocument
|
||||
{
|
||||
SymbolId = fact.SymbolId,
|
||||
CodeId = fact.CodeId,
|
||||
LoaderBase = fact.LoaderBase,
|
||||
Purl = fact.Purl,
|
||||
SymbolDigest = fact.SymbolDigest,
|
||||
BuildId = fact.BuildId,
|
||||
ProcessId = fact.ProcessId,
|
||||
ProcessName = fact.ProcessName,
|
||||
SocketAddress = fact.SocketAddress,
|
||||
ContainerId = fact.ContainerId,
|
||||
EvidenceUri = fact.EvidenceUri,
|
||||
HitCount = fact.HitCount,
|
||||
ObservedAt = fact.ObservedAt,
|
||||
Metadata = fact.Metadata is null
|
||||
? null
|
||||
: new Dictionary<string, string?>(fact.Metadata, StringComparer.Ordinal)
|
||||
@@ -217,7 +235,7 @@ public sealed class RuntimeFactsIngestionService : IRuntimeFactsIngestionService
|
||||
{
|
||||
foreach (var fact in incoming)
|
||||
{
|
||||
var key = new RuntimeFactKey(fact.SymbolId, fact.CodeId, fact.LoaderBase);
|
||||
var key = new RuntimeFactKey(fact.SymbolId, fact.CodeId, fact.LoaderBase, fact.Purl, fact.SymbolDigest, fact.BuildId);
|
||||
if (!map.TryGetValue(key, out var existingFact))
|
||||
{
|
||||
map[key] = new RuntimeFactDocument
|
||||
@@ -225,12 +243,16 @@ public sealed class RuntimeFactsIngestionService : IRuntimeFactsIngestionService
|
||||
SymbolId = fact.SymbolId,
|
||||
CodeId = fact.CodeId,
|
||||
LoaderBase = fact.LoaderBase,
|
||||
Purl = fact.Purl,
|
||||
SymbolDigest = fact.SymbolDigest,
|
||||
BuildId = fact.BuildId,
|
||||
ProcessId = fact.ProcessId,
|
||||
ProcessName = fact.ProcessName,
|
||||
SocketAddress = fact.SocketAddress,
|
||||
ContainerId = fact.ContainerId,
|
||||
EvidenceUri = fact.EvidenceUri,
|
||||
HitCount = fact.HitCount,
|
||||
ObservedAt = fact.ObservedAt,
|
||||
Metadata = fact.Metadata is null
|
||||
? null
|
||||
: new Dictionary<string, string?>(fact.Metadata, StringComparer.Ordinal)
|
||||
@@ -244,6 +266,10 @@ public sealed class RuntimeFactsIngestionService : IRuntimeFactsIngestionService
|
||||
existingFact.SocketAddress ??= fact.SocketAddress;
|
||||
existingFact.ContainerId ??= fact.ContainerId;
|
||||
existingFact.EvidenceUri ??= fact.EvidenceUri;
|
||||
existingFact.Purl ??= fact.Purl;
|
||||
existingFact.SymbolDigest ??= fact.SymbolDigest;
|
||||
existingFact.BuildId ??= fact.BuildId;
|
||||
existingFact.ObservedAt ??= fact.ObservedAt;
|
||||
if (fact.Metadata != null && fact.Metadata.Count > 0)
|
||||
{
|
||||
existingFact.Metadata ??= new Dictionary<string, string?>(StringComparer.Ordinal);
|
||||
@@ -326,7 +352,7 @@ public sealed class RuntimeFactsIngestionService : IRuntimeFactsIngestionService
|
||||
private static string? Normalize(string? value) =>
|
||||
string.IsNullOrWhiteSpace(value) ? null : value.Trim();
|
||||
|
||||
private readonly record struct RuntimeFactKey(string SymbolId, string? CodeId, string? LoaderBase);
|
||||
private readonly record struct RuntimeFactKey(string SymbolId, string? CodeId, string? LoaderBase, string? Purl, string? SymbolDigest, string? BuildId);
|
||||
|
||||
private sealed class RuntimeFactKeyComparer : IEqualityComparer<RuntimeFactKey>
|
||||
{
|
||||
@@ -335,7 +361,10 @@ public sealed class RuntimeFactsIngestionService : IRuntimeFactsIngestionService
|
||||
public bool Equals(RuntimeFactKey x, RuntimeFactKey y) =>
|
||||
string.Equals(x.SymbolId, y.SymbolId, StringComparison.Ordinal) &&
|
||||
string.Equals(x.CodeId, y.CodeId, StringComparison.Ordinal) &&
|
||||
string.Equals(x.LoaderBase, y.LoaderBase, StringComparison.Ordinal);
|
||||
string.Equals(x.LoaderBase, y.LoaderBase, StringComparison.Ordinal) &&
|
||||
string.Equals(x.Purl, y.Purl, StringComparison.Ordinal) &&
|
||||
string.Equals(x.SymbolDigest, y.SymbolDigest, StringComparison.Ordinal) &&
|
||||
string.Equals(x.BuildId, y.BuildId, StringComparison.Ordinal);
|
||||
|
||||
public int GetHashCode(RuntimeFactKey obj)
|
||||
{
|
||||
@@ -351,6 +380,21 @@ public sealed class RuntimeFactsIngestionService : IRuntimeFactsIngestionService
|
||||
hash.Add(obj.LoaderBase, StringComparer.Ordinal);
|
||||
}
|
||||
|
||||
if (obj.Purl is not null)
|
||||
{
|
||||
hash.Add(obj.Purl, StringComparer.Ordinal);
|
||||
}
|
||||
|
||||
if (obj.SymbolDigest is not null)
|
||||
{
|
||||
hash.Add(obj.SymbolDigest, StringComparer.Ordinal);
|
||||
}
|
||||
|
||||
if (obj.BuildId is not null)
|
||||
{
|
||||
hash.Add(obj.BuildId, StringComparer.Ordinal);
|
||||
}
|
||||
|
||||
return hash.ToHashCode();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user