Add SBOM, symbols, traces, and VEX files for CVE-2022-21661 SQLi case
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
- Created CycloneDX and SPDX SBOM files for both reachable and unreachable images. - Added symbols.json detailing function entry and sink points in the WordPress code. - Included runtime traces for function calls in both reachable and unreachable scenarios. - Developed OpenVEX files indicating vulnerability status and justification for both cases. - Updated README for evaluator harness to guide integration with scanner output.
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using MongoDB.Bson;
|
||||
using MongoDB.Bson.Serialization.Attributes;
|
||||
|
||||
namespace StellaOps.Signals.Models;
|
||||
|
||||
public sealed class ReachabilityFactDocument
|
||||
{
|
||||
[BsonId]
|
||||
[BsonRepresentation(BsonType.ObjectId)]
|
||||
public string Id { get; set; } = ObjectId.GenerateNewId().ToString();
|
||||
|
||||
[BsonElement("callgraphId")]
|
||||
public string CallgraphId { get; set; } = string.Empty;
|
||||
|
||||
[BsonElement("subject")]
|
||||
public ReachabilitySubject Subject { get; set; } = new();
|
||||
|
||||
[BsonElement("entryPoints")]
|
||||
public List<string> EntryPoints { get; set; } = new();
|
||||
|
||||
[BsonElement("states")]
|
||||
public List<ReachabilityStateDocument> States { get; set; } = new();
|
||||
|
||||
[BsonElement("metadata")]
|
||||
[BsonIgnoreIfNull]
|
||||
public Dictionary<string, string?>? Metadata { get; set; }
|
||||
|
||||
[BsonElement("computedAt")]
|
||||
public DateTimeOffset ComputedAt { get; set; }
|
||||
|
||||
[BsonElement("subjectKey")]
|
||||
[BsonRequired]
|
||||
public string SubjectKey { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public sealed class ReachabilityStateDocument
|
||||
{
|
||||
[BsonElement("target")]
|
||||
public string Target { get; set; } = string.Empty;
|
||||
|
||||
[BsonElement("reachable")]
|
||||
public bool Reachable { get; set; }
|
||||
|
||||
[BsonElement("confidence")]
|
||||
public double Confidence { get; set; }
|
||||
|
||||
[BsonElement("path")]
|
||||
public List<string> Path { get; set; } = new();
|
||||
|
||||
[BsonElement("evidence")]
|
||||
public ReachabilityEvidenceDocument Evidence { get; set; } = new();
|
||||
}
|
||||
|
||||
public sealed class ReachabilityEvidenceDocument
|
||||
{
|
||||
[BsonElement("runtimeHits")]
|
||||
public List<string> RuntimeHits { get; set; } = new();
|
||||
|
||||
[BsonElement("blockedEdges")]
|
||||
[BsonIgnoreIfNull]
|
||||
public List<string>? BlockedEdges { get; set; }
|
||||
}
|
||||
|
||||
public sealed class ReachabilitySubject
|
||||
{
|
||||
[BsonElement("imageDigest")]
|
||||
[BsonIgnoreIfNull]
|
||||
public string? ImageDigest { get; set; }
|
||||
|
||||
[BsonElement("component")]
|
||||
[BsonIgnoreIfNull]
|
||||
public string? Component { get; set; }
|
||||
|
||||
[BsonElement("version")]
|
||||
[BsonIgnoreIfNull]
|
||||
public string? Version { get; set; }
|
||||
|
||||
[BsonElement("scanId")]
|
||||
[BsonIgnoreIfNull]
|
||||
public string? ScanId { get; set; }
|
||||
|
||||
public string ToSubjectKey()
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(ScanId))
|
||||
{
|
||||
return ScanId!;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(ImageDigest))
|
||||
{
|
||||
return ImageDigest!;
|
||||
}
|
||||
|
||||
return string.Join('|', Component ?? string.Empty, Version ?? string.Empty).Trim('|');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace StellaOps.Signals.Models;
|
||||
|
||||
public sealed class ReachabilityRecomputeRequest
|
||||
{
|
||||
public string CallgraphId { get; set; } = string.Empty;
|
||||
|
||||
public ReachabilitySubject Subject { get; set; } = new();
|
||||
|
||||
public List<string> EntryPoints { get; set; } = new();
|
||||
|
||||
public List<string> Targets { get; set; } = new();
|
||||
|
||||
public List<string>? RuntimeHits { get; set; }
|
||||
|
||||
public List<ReachabilityBlockedEdge>? BlockedEdges { get; set; }
|
||||
|
||||
public Dictionary<string, string?>? Metadata { get; set; }
|
||||
}
|
||||
|
||||
public sealed class ReachabilityBlockedEdge
|
||||
{
|
||||
public string From { get; set; } = string.Empty;
|
||||
public string To { get; set; } = string.Empty;
|
||||
}
|
||||
@@ -17,10 +17,15 @@ public sealed class SignalsMongoOptions
|
||||
/// </summary>
|
||||
public string Database { get; set; } = "signals";
|
||||
|
||||
/// <summary>
|
||||
/// Collection name storing normalized callgraphs.
|
||||
/// </summary>
|
||||
public string CallgraphsCollection { get; set; } = "callgraphs";
|
||||
/// <summary>
|
||||
/// Collection name storing normalized callgraphs.
|
||||
/// </summary>
|
||||
public string CallgraphsCollection { get; set; } = "callgraphs";
|
||||
|
||||
/// <summary>
|
||||
/// Collection name storing reachability facts.
|
||||
/// </summary>
|
||||
public string ReachabilityFactsCollection { get; set; } = "reachability_facts";
|
||||
|
||||
/// <summary>
|
||||
/// Validates the configured values.
|
||||
@@ -37,9 +42,14 @@ public sealed class SignalsMongoOptions
|
||||
throw new InvalidOperationException("Signals Mongo database name must be configured.");
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(CallgraphsCollection))
|
||||
{
|
||||
throw new InvalidOperationException("Signals callgraph collection name must be configured.");
|
||||
}
|
||||
}
|
||||
}
|
||||
if (string.IsNullOrWhiteSpace(CallgraphsCollection))
|
||||
{
|
||||
throw new InvalidOperationException("Signals callgraph collection name must be configured.");
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(ReachabilityFactsCollection))
|
||||
{
|
||||
throw new InvalidOperationException("Signals reachability fact collection name must be configured.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,9 +9,9 @@ using StellaOps.Signals.Models;
|
||||
namespace StellaOps.Signals.Parsing;
|
||||
|
||||
/// <summary>
|
||||
/// Simple JSON-based callgraph parser used for initial language coverage.
|
||||
/// </summary>
|
||||
internal sealed class SimpleJsonCallgraphParser : ICallgraphParser
|
||||
/// Simple JSON-based callgraph parser used for initial language coverage.
|
||||
/// </summary>
|
||||
public sealed class SimpleJsonCallgraphParser : ICallgraphParser
|
||||
{
|
||||
private readonly JsonSerializerOptions serializerOptions;
|
||||
|
||||
@@ -27,93 +27,160 @@ internal sealed class SimpleJsonCallgraphParser : ICallgraphParser
|
||||
|
||||
public string Language { get; }
|
||||
|
||||
public async Task<CallgraphParseResult> ParseAsync(Stream artifactStream, CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(artifactStream);
|
||||
public async Task<CallgraphParseResult> ParseAsync(Stream artifactStream, CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(artifactStream);
|
||||
|
||||
using var document = await JsonDocument.ParseAsync(artifactStream, cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
var root = document.RootElement;
|
||||
|
||||
if (TryParseLegacy(root, out var legacyResult))
|
||||
{
|
||||
return legacyResult;
|
||||
}
|
||||
|
||||
if (TryParseSchemaV1(root, out var schemaResult))
|
||||
{
|
||||
return schemaResult;
|
||||
}
|
||||
|
||||
throw new CallgraphParserValidationException("Callgraph artifact payload is empty or missing required fields.");
|
||||
}
|
||||
|
||||
private static bool TryParseLegacy(JsonElement root, out CallgraphParseResult result)
|
||||
{
|
||||
result = default!;
|
||||
|
||||
if (!root.TryGetProperty("graph", out var graphElement))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var nodesElement = graphElement.GetProperty("nodes");
|
||||
var edgesElement = graphElement.TryGetProperty("edges", out var edgesValue) ? edgesValue : default;
|
||||
|
||||
var nodes = new List<CallgraphNode>(nodesElement.GetArrayLength());
|
||||
foreach (var nodeElement in nodesElement.EnumerateArray())
|
||||
{
|
||||
var id = nodeElement.GetProperty("id").GetString();
|
||||
if (string.IsNullOrWhiteSpace(id))
|
||||
{
|
||||
throw new CallgraphParserValidationException("Callgraph node is missing an id.");
|
||||
}
|
||||
|
||||
nodes.Add(new CallgraphNode(
|
||||
Id: id.Trim(),
|
||||
Name: nodeElement.TryGetProperty("name", out var nameEl) ? nameEl.GetString() ?? id.Trim() : id.Trim(),
|
||||
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));
|
||||
}
|
||||
|
||||
var edges = new List<CallgraphEdge>();
|
||||
if (edgesElement.ValueKind == JsonValueKind.Array)
|
||||
{
|
||||
foreach (var edgeElement in edgesElement.EnumerateArray())
|
||||
{
|
||||
var source = edgeElement.GetProperty("source").GetString();
|
||||
var target = edgeElement.GetProperty("target").GetString();
|
||||
if (string.IsNullOrWhiteSpace(source) || string.IsNullOrWhiteSpace(target))
|
||||
{
|
||||
throw new CallgraphParserValidationException("Callgraph edge requires both source and target.");
|
||||
}
|
||||
|
||||
var type = edgeElement.TryGetProperty("type", out var typeEl) ? typeEl.GetString() ?? "call" : "call";
|
||||
edges.Add(new CallgraphEdge(source.Trim(), target.Trim(), type));
|
||||
}
|
||||
}
|
||||
|
||||
var formatVersion = root.TryGetProperty("formatVersion", out var versionEl)
|
||||
? versionEl.GetString()
|
||||
: null;
|
||||
|
||||
result = new CallgraphParseResult(nodes, edges, string.IsNullOrWhiteSpace(formatVersion) ? "1.0" : formatVersion!.Trim());
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryParseSchemaV1(JsonElement root, out CallgraphParseResult result)
|
||||
{
|
||||
result = default!;
|
||||
|
||||
if (!root.TryGetProperty("nodes", out var nodesElement) && !root.TryGetProperty("edges", out _))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var nodes = new List<CallgraphNode>();
|
||||
if (nodesElement.ValueKind == JsonValueKind.Array)
|
||||
{
|
||||
foreach (var nodeElement in nodesElement.EnumerateArray())
|
||||
{
|
||||
var id = nodeElement.TryGetProperty("sid", out var sidEl) ? sidEl.GetString() : nodeElement.GetProperty("id").GetString();
|
||||
if (string.IsNullOrWhiteSpace(id))
|
||||
{
|
||||
throw new CallgraphParserValidationException("Callgraph node is missing an id.");
|
||||
}
|
||||
|
||||
nodes.Add(new CallgraphNode(
|
||||
Id: id.Trim(),
|
||||
Name: nodeElement.TryGetProperty("name", out var nameEl) ? nameEl.GetString() ?? id.Trim() : id.Trim(),
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
if (!root.TryGetProperty("edges", out var edgesElement) || edgesElement.ValueKind != JsonValueKind.Array)
|
||||
{
|
||||
edgesElement = default;
|
||||
}
|
||||
|
||||
var edges = new List<CallgraphEdge>();
|
||||
if (edgesElement.ValueKind == JsonValueKind.Array)
|
||||
{
|
||||
foreach (var edgeElement in edgesElement.EnumerateArray())
|
||||
{
|
||||
var from = edgeElement.TryGetProperty("from", out var fromEl) ? fromEl.GetString() : edgeElement.GetProperty("source").GetString();
|
||||
var to = edgeElement.TryGetProperty("to", out var toEl) ? toEl.GetString() : edgeElement.GetProperty("target").GetString();
|
||||
if (string.IsNullOrWhiteSpace(from) || string.IsNullOrWhiteSpace(to))
|
||||
{
|
||||
throw new CallgraphParserValidationException("Callgraph edge requires both source and target.");
|
||||
}
|
||||
|
||||
var kind = edgeElement.TryGetProperty("kind", out var kindEl)
|
||||
? kindEl.GetString() ?? "call"
|
||||
: edgeElement.TryGetProperty("type", out var typeEl)
|
||||
? typeEl.GetString() ?? "call"
|
||||
: "call";
|
||||
|
||||
edges.Add(new CallgraphEdge(from.Trim(), to.Trim(), kind));
|
||||
}
|
||||
}
|
||||
|
||||
if (nodes.Count == 0)
|
||||
{
|
||||
// When nodes are omitted (framework overlay), derive them from the referenced edges.
|
||||
var uniqueNodeIds = new HashSet<string>(StringComparer.Ordinal);
|
||||
foreach (var edge in edges)
|
||||
{
|
||||
uniqueNodeIds.Add(edge.SourceId);
|
||||
uniqueNodeIds.Add(edge.TargetId);
|
||||
}
|
||||
|
||||
foreach (var nodeId in uniqueNodeIds)
|
||||
{
|
||||
nodes.Add(new CallgraphNode(nodeId, nodeId, "function", null, null, null));
|
||||
}
|
||||
}
|
||||
|
||||
var schemaVersion = root.TryGetProperty("schema_version", out var schemaEl)
|
||||
? schemaEl.GetString()
|
||||
: "1.0";
|
||||
|
||||
result = new CallgraphParseResult(nodes, edges, string.IsNullOrWhiteSpace(schemaVersion) ? "1.0" : schemaVersion!.Trim());
|
||||
return true;
|
||||
}
|
||||
|
||||
var payload = await JsonSerializer.DeserializeAsync<RawCallgraphPayload>(
|
||||
artifactStream,
|
||||
serializerOptions,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (payload is null)
|
||||
{
|
||||
throw new CallgraphParserValidationException("Callgraph artifact payload is empty.");
|
||||
}
|
||||
|
||||
if (payload.Graph is null)
|
||||
{
|
||||
throw new CallgraphParserValidationException("Callgraph artifact is missing 'graph' section.");
|
||||
}
|
||||
|
||||
if (payload.Graph.Nodes is null || payload.Graph.Nodes.Count == 0)
|
||||
{
|
||||
throw new CallgraphParserValidationException("Callgraph artifact must include at least one node.");
|
||||
}
|
||||
|
||||
if (payload.Graph.Edges is null)
|
||||
{
|
||||
payload.Graph.Edges = new List<RawCallgraphEdge>();
|
||||
}
|
||||
|
||||
var nodes = new List<CallgraphNode>(payload.Graph.Nodes.Count);
|
||||
foreach (var node in payload.Graph.Nodes)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(node.Id))
|
||||
{
|
||||
throw new CallgraphParserValidationException("Callgraph node is missing an id.");
|
||||
}
|
||||
|
||||
nodes.Add(new CallgraphNode(
|
||||
Id: node.Id.Trim(),
|
||||
Name: node.Name ?? node.Id.Trim(),
|
||||
Kind: node.Kind ?? "function",
|
||||
Namespace: node.Namespace,
|
||||
File: node.File,
|
||||
Line: node.Line));
|
||||
}
|
||||
|
||||
var edges = new List<CallgraphEdge>(payload.Graph.Edges.Count);
|
||||
foreach (var edge in payload.Graph.Edges)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(edge.Source) || string.IsNullOrWhiteSpace(edge.Target))
|
||||
{
|
||||
throw new CallgraphParserValidationException("Callgraph edge requires both source and target.");
|
||||
}
|
||||
|
||||
edges.Add(new CallgraphEdge(edge.Source.Trim(), edge.Target.Trim(), edge.Type ?? "call"));
|
||||
}
|
||||
|
||||
var formatVersion = string.IsNullOrWhiteSpace(payload.FormatVersion) ? "1.0" : payload.FormatVersion.Trim();
|
||||
return new CallgraphParseResult(nodes, edges, formatVersion);
|
||||
}
|
||||
|
||||
private sealed class RawCallgraphPayload
|
||||
{
|
||||
public string? FormatVersion { get; set; }
|
||||
public RawCallgraphGraph? Graph { get; set; }
|
||||
}
|
||||
|
||||
private sealed class RawCallgraphGraph
|
||||
{
|
||||
public List<RawCallgraphNode>? Nodes { get; set; }
|
||||
public List<RawCallgraphEdge>? Edges { get; set; }
|
||||
}
|
||||
|
||||
private sealed class RawCallgraphNode
|
||||
{
|
||||
public string? Id { get; set; }
|
||||
public string? Name { get; set; }
|
||||
public string? Kind { get; set; }
|
||||
public string? Namespace { get; set; }
|
||||
public string? File { get; set; }
|
||||
public int? Line { get; set; }
|
||||
}
|
||||
|
||||
private sealed class RawCallgraphEdge
|
||||
{
|
||||
public string? Source { get; set; }
|
||||
public string? Target { get; set; }
|
||||
public string? Type { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,9 @@ namespace StellaOps.Signals.Persistence;
|
||||
/// <summary>
|
||||
/// Persists normalized callgraphs.
|
||||
/// </summary>
|
||||
public interface ICallgraphRepository
|
||||
{
|
||||
Task<CallgraphDocument> UpsertAsync(CallgraphDocument document, CancellationToken cancellationToken);
|
||||
}
|
||||
public interface ICallgraphRepository
|
||||
{
|
||||
Task<CallgraphDocument> UpsertAsync(CallgraphDocument document, CancellationToken cancellationToken);
|
||||
|
||||
Task<CallgraphDocument?> GetByIdAsync(string id, CancellationToken cancellationToken);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using StellaOps.Signals.Models;
|
||||
|
||||
namespace StellaOps.Signals.Persistence;
|
||||
|
||||
public interface IReachabilityFactRepository
|
||||
{
|
||||
Task<ReachabilityFactDocument> UpsertAsync(ReachabilityFactDocument document, CancellationToken cancellationToken);
|
||||
|
||||
Task<ReachabilityFactDocument?> GetBySubjectAsync(string subjectKey, CancellationToken cancellationToken);
|
||||
}
|
||||
@@ -19,11 +19,11 @@ internal sealed class MongoCallgraphRepository : ICallgraphRepository
|
||||
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
public async Task<CallgraphDocument> UpsertAsync(CallgraphDocument document, CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(document);
|
||||
|
||||
var filter = Builders<CallgraphDocument>.Filter.Eq(d => d.Component, document.Component)
|
||||
public async Task<CallgraphDocument> UpsertAsync(CallgraphDocument document, CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(document);
|
||||
|
||||
var filter = Builders<CallgraphDocument>.Filter.Eq(d => d.Component, document.Component)
|
||||
& Builders<CallgraphDocument>.Filter.Eq(d => d.Version, document.Version)
|
||||
& Builders<CallgraphDocument>.Filter.Eq(d => d.Language, document.Language);
|
||||
|
||||
@@ -42,7 +42,18 @@ internal sealed class MongoCallgraphRepository : ICallgraphRepository
|
||||
document.Id = result.UpsertedId.AsObjectId.ToString();
|
||||
}
|
||||
|
||||
logger.LogInformation("Upserted callgraph {Language}:{Component}:{Version} (id={Id}).", document.Language, document.Component, document.Version, document.Id);
|
||||
return document;
|
||||
}
|
||||
}
|
||||
logger.LogInformation("Upserted callgraph {Language}:{Component}:{Version} (id={Id}).", document.Language, document.Component, document.Version, document.Id);
|
||||
return document;
|
||||
}
|
||||
|
||||
public async Task<CallgraphDocument?> GetByIdAsync(string id, CancellationToken cancellationToken)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(id))
|
||||
{
|
||||
throw new ArgumentException("Callgraph id is required.", nameof(id));
|
||||
}
|
||||
|
||||
var filter = Builders<CallgraphDocument>.Filter.Eq(d => d.Id, id);
|
||||
return await collection.Find(filter).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using MongoDB.Driver;
|
||||
using StellaOps.Signals.Models;
|
||||
|
||||
namespace StellaOps.Signals.Persistence;
|
||||
|
||||
internal sealed class MongoReachabilityFactRepository : IReachabilityFactRepository
|
||||
{
|
||||
private readonly IMongoCollection<ReachabilityFactDocument> collection;
|
||||
private readonly ILogger<MongoReachabilityFactRepository> logger;
|
||||
|
||||
public MongoReachabilityFactRepository(
|
||||
IMongoCollection<ReachabilityFactDocument> collection,
|
||||
ILogger<MongoReachabilityFactRepository> logger)
|
||||
{
|
||||
this.collection = collection ?? throw new ArgumentNullException(nameof(collection));
|
||||
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
public async Task<ReachabilityFactDocument> UpsertAsync(ReachabilityFactDocument document, CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(document);
|
||||
if (string.IsNullOrWhiteSpace(document.SubjectKey))
|
||||
{
|
||||
throw new ArgumentException("Subject key is required.", nameof(document));
|
||||
}
|
||||
|
||||
var filter = Builders<ReachabilityFactDocument>.Filter.Eq(d => d.SubjectKey, document.SubjectKey);
|
||||
var options = new ReplaceOptions { IsUpsert = true };
|
||||
var result = await collection.ReplaceOneAsync(filter, document, options, cancellationToken).ConfigureAwait(false);
|
||||
if (result.UpsertedId != null)
|
||||
{
|
||||
document.Id = result.UpsertedId.AsObjectId.ToString();
|
||||
}
|
||||
|
||||
logger.LogInformation("Upserted reachability fact for subject {SubjectKey} (callgraph={CallgraphId}).", document.SubjectKey, document.CallgraphId);
|
||||
return document;
|
||||
}
|
||||
|
||||
public async Task<ReachabilityFactDocument?> GetBySubjectAsync(string subjectKey, CancellationToken cancellationToken)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(subjectKey))
|
||||
{
|
||||
throw new ArgumentException("Subject key is required.", nameof(subjectKey));
|
||||
}
|
||||
|
||||
var filter = Builders<ReachabilityFactDocument>.Filter.Eq(d => d.SubjectKey, subjectKey);
|
||||
return await collection.Find(filter).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.IO;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.Extensions.Options;
|
||||
using MongoDB.Driver;
|
||||
@@ -93,23 +94,34 @@ builder.Services.AddSingleton<IMongoDatabase>(sp =>
|
||||
return mongoClient.GetDatabase(databaseName);
|
||||
});
|
||||
|
||||
builder.Services.AddSingleton<IMongoCollection<CallgraphDocument>>(sp =>
|
||||
{
|
||||
var opts = sp.GetRequiredService<IOptions<SignalsOptions>>().Value;
|
||||
var database = sp.GetRequiredService<IMongoDatabase>();
|
||||
var collection = database.GetCollection<CallgraphDocument>(opts.Mongo.CallgraphsCollection);
|
||||
EnsureCallgraphIndexes(collection);
|
||||
return collection;
|
||||
});
|
||||
|
||||
builder.Services.AddSingleton<ICallgraphRepository, MongoCallgraphRepository>();
|
||||
builder.Services.AddSingleton<ICallgraphArtifactStore, FileSystemCallgraphArtifactStore>();
|
||||
builder.Services.AddSingleton<ICallgraphParser>(new SimpleJsonCallgraphParser("java"));
|
||||
builder.Services.AddSingleton<ICallgraphParser>(new SimpleJsonCallgraphParser("nodejs"));
|
||||
builder.Services.AddSingleton<ICallgraphParser>(new SimpleJsonCallgraphParser("python"));
|
||||
builder.Services.AddSingleton<ICallgraphParser>(new SimpleJsonCallgraphParser("go"));
|
||||
builder.Services.AddSingleton<ICallgraphParserResolver, CallgraphParserResolver>();
|
||||
builder.Services.AddSingleton<ICallgraphIngestionService, CallgraphIngestionService>();
|
||||
builder.Services.AddSingleton<IMongoCollection<CallgraphDocument>>(sp =>
|
||||
{
|
||||
var opts = sp.GetRequiredService<IOptions<SignalsOptions>>().Value;
|
||||
var database = sp.GetRequiredService<IMongoDatabase>();
|
||||
var collection = database.GetCollection<CallgraphDocument>(opts.Mongo.CallgraphsCollection);
|
||||
EnsureCallgraphIndexes(collection);
|
||||
return collection;
|
||||
});
|
||||
|
||||
builder.Services.AddSingleton<IMongoCollection<ReachabilityFactDocument>>(sp =>
|
||||
{
|
||||
var opts = sp.GetRequiredService<IOptions<SignalsOptions>>().Value;
|
||||
var database = sp.GetRequiredService<IMongoDatabase>();
|
||||
var collection = database.GetCollection<ReachabilityFactDocument>(opts.Mongo.ReachabilityFactsCollection);
|
||||
EnsureReachabilityFactIndexes(collection);
|
||||
return collection;
|
||||
});
|
||||
|
||||
builder.Services.AddSingleton<ICallgraphRepository, MongoCallgraphRepository>();
|
||||
builder.Services.AddSingleton<ICallgraphArtifactStore, FileSystemCallgraphArtifactStore>();
|
||||
builder.Services.AddSingleton<ICallgraphParser>(new SimpleJsonCallgraphParser("java"));
|
||||
builder.Services.AddSingleton<ICallgraphParser>(new SimpleJsonCallgraphParser("nodejs"));
|
||||
builder.Services.AddSingleton<ICallgraphParser>(new SimpleJsonCallgraphParser("python"));
|
||||
builder.Services.AddSingleton<ICallgraphParser>(new SimpleJsonCallgraphParser("go"));
|
||||
builder.Services.AddSingleton<ICallgraphParserResolver, CallgraphParserResolver>();
|
||||
builder.Services.AddSingleton<ICallgraphIngestionService, CallgraphIngestionService>();
|
||||
builder.Services.AddSingleton<IReachabilityFactRepository, MongoReachabilityFactRepository>();
|
||||
builder.Services.AddSingleton<IReachabilityScoringService, ReachabilityScoringService>();
|
||||
|
||||
if (bootstrap.Authority.Enabled)
|
||||
{
|
||||
@@ -239,10 +251,40 @@ signalsGroup.MapPost("/runtime-facts", (HttpContext context, SignalsOptions opti
|
||||
? Results.StatusCode(StatusCodes.Status501NotImplemented)
|
||||
: failure ?? Results.Unauthorized()).WithName("SignalsRuntimeIngest");
|
||||
|
||||
signalsGroup.MapPost("/reachability/recompute", (HttpContext context, SignalsOptions options) =>
|
||||
Program.TryAuthorize(context, SignalsPolicies.Admin, options.Authority.AllowAnonymousFallback, out var failure)
|
||||
? Results.StatusCode(StatusCodes.Status501NotImplemented)
|
||||
: failure ?? Results.Unauthorized()).WithName("SignalsReachabilityRecompute");
|
||||
signalsGroup.MapPost("/reachability/recompute", async Task<IResult> (
|
||||
HttpContext context,
|
||||
SignalsOptions options,
|
||||
ReachabilityRecomputeRequest request,
|
||||
IReachabilityScoringService scoringService,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
if (!Program.TryAuthorize(context, SignalsPolicies.Admin, options.Authority.AllowAnonymousFallback, out var failure))
|
||||
{
|
||||
return failure ?? Results.Unauthorized();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var fact = await scoringService.RecomputeAsync(request, cancellationToken).ConfigureAwait(false);
|
||||
return Results.Ok(new
|
||||
{
|
||||
fact.Id,
|
||||
fact.CallgraphId,
|
||||
subject = fact.Subject,
|
||||
fact.EntryPoints,
|
||||
fact.States,
|
||||
fact.ComputedAt
|
||||
});
|
||||
}
|
||||
catch (ReachabilityScoringValidationException ex)
|
||||
{
|
||||
return Results.BadRequest(new { error = ex.Message });
|
||||
}
|
||||
catch (ReachabilityCallgraphNotFoundException ex)
|
||||
{
|
||||
return Results.NotFound(new { error = ex.Message });
|
||||
}
|
||||
}).WithName("SignalsReachabilityRecompute");
|
||||
|
||||
app.Run();
|
||||
|
||||
@@ -286,11 +328,11 @@ public partial class Program
|
||||
return false;
|
||||
}
|
||||
|
||||
internal static void EnsureCallgraphIndexes(IMongoCollection<CallgraphDocument> collection)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(collection);
|
||||
|
||||
try
|
||||
internal static void EnsureCallgraphIndexes(IMongoCollection<CallgraphDocument> collection)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(collection);
|
||||
|
||||
try
|
||||
{
|
||||
var indexKeys = Builders<CallgraphDocument>.IndexKeys
|
||||
.Ascending(document => document.Component)
|
||||
@@ -307,7 +349,31 @@ public partial class Program
|
||||
}
|
||||
catch (MongoCommandException ex) when (string.Equals(ex.CodeName, "IndexOptionsConflict", StringComparison.Ordinal))
|
||||
{
|
||||
// Index already exists with different options – ignore to keep startup idempotent.
|
||||
}
|
||||
}
|
||||
}
|
||||
// Index already exists with different options – ignore to keep startup idempotent.
|
||||
}
|
||||
}
|
||||
|
||||
internal static void EnsureReachabilityFactIndexes(IMongoCollection<ReachabilityFactDocument> collection)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(collection);
|
||||
|
||||
try
|
||||
{
|
||||
var subjectIndex = new CreateIndexModel<ReachabilityFactDocument>(
|
||||
Builders<ReachabilityFactDocument>.IndexKeys.Ascending(doc => doc.SubjectKey),
|
||||
new CreateIndexOptions { Name = "reachability_subject_key_unique", Unique = true });
|
||||
|
||||
collection.Indexes.CreateOne(subjectIndex);
|
||||
|
||||
var callgraphIndex = new CreateIndexModel<ReachabilityFactDocument>(
|
||||
Builders<ReachabilityFactDocument>.IndexKeys.Ascending(doc => doc.CallgraphId),
|
||||
new CreateIndexOptions { Name = "reachability_callgraph_lookup" });
|
||||
|
||||
collection.Indexes.CreateOne(callgraphIndex);
|
||||
}
|
||||
catch (MongoCommandException ex) when (string.Equals(ex.CodeName, "IndexOptionsConflict", StringComparison.Ordinal))
|
||||
{
|
||||
// Ignore when indexes already exist with different options to keep startup idempotent.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
4
src/Signals/StellaOps.Signals/Properties/AssemblyInfo.cs
Normal file
4
src/Signals/StellaOps.Signals/Properties/AssemblyInfo.cs
Normal file
@@ -0,0 +1,4 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
[assembly: InternalsVisibleTo("StellaOps.Signals.Reachability.Tests")]
|
||||
[assembly: InternalsVisibleTo("StellaOps.ScannerSignals.IntegrationTests")]
|
||||
@@ -0,0 +1,10 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using StellaOps.Signals.Models;
|
||||
|
||||
namespace StellaOps.Signals.Services;
|
||||
|
||||
public interface IReachabilityScoringService
|
||||
{
|
||||
Task<ReachabilityFactDocument> RecomputeAsync(ReachabilityRecomputeRequest request, CancellationToken cancellationToken);
|
||||
}
|
||||
@@ -0,0 +1,284 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.Signals.Models;
|
||||
using StellaOps.Signals.Persistence;
|
||||
|
||||
namespace StellaOps.Signals.Services;
|
||||
|
||||
public sealed class ReachabilityScoringService : IReachabilityScoringService
|
||||
{
|
||||
private const double ReachableConfidence = 0.75;
|
||||
private const double UnreachableConfidence = 0.25;
|
||||
private const double RuntimeBonus = 0.15;
|
||||
private const double MaxConfidence = 0.99;
|
||||
private const double MinConfidence = 0.05;
|
||||
|
||||
private readonly ICallgraphRepository callgraphRepository;
|
||||
private readonly IReachabilityFactRepository factRepository;
|
||||
private readonly TimeProvider timeProvider;
|
||||
private readonly ILogger<ReachabilityScoringService> logger;
|
||||
|
||||
public ReachabilityScoringService(
|
||||
ICallgraphRepository callgraphRepository,
|
||||
IReachabilityFactRepository factRepository,
|
||||
TimeProvider timeProvider,
|
||||
ILogger<ReachabilityScoringService> logger)
|
||||
{
|
||||
this.callgraphRepository = callgraphRepository ?? throw new ArgumentNullException(nameof(callgraphRepository));
|
||||
this.factRepository = factRepository ?? throw new ArgumentNullException(nameof(factRepository));
|
||||
this.timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider));
|
||||
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
public async Task<ReachabilityFactDocument> RecomputeAsync(ReachabilityRecomputeRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(request);
|
||||
|
||||
ValidateRequest(request);
|
||||
|
||||
var callgraph = await callgraphRepository.GetByIdAsync(request.CallgraphId, cancellationToken).ConfigureAwait(false);
|
||||
if (callgraph is null)
|
||||
{
|
||||
throw new ReachabilityCallgraphNotFoundException(request.CallgraphId);
|
||||
}
|
||||
|
||||
IEnumerable<ReachabilityBlockedEdge> blockedEdges = request.BlockedEdges is { Count: > 0 } list
|
||||
? list
|
||||
: Array.Empty<ReachabilityBlockedEdge>();
|
||||
var graph = BuildGraph(callgraph, blockedEdges);
|
||||
var entryPoints = NormalizeEntryPoints(request.EntryPoints, graph.Nodes, graph.Inbound);
|
||||
var targets = request.Targets.Where(t => !string.IsNullOrWhiteSpace(t)).Select(t => t.Trim()).Distinct(StringComparer.Ordinal).ToList();
|
||||
if (targets.Count == 0)
|
||||
{
|
||||
throw new ReachabilityScoringValidationException("At least one target symbol is required.");
|
||||
}
|
||||
|
||||
var runtimeHits = request.RuntimeHits?.Where(hit => !string.IsNullOrWhiteSpace(hit))
|
||||
.Select(hit => hit.Trim())
|
||||
.Distinct(StringComparer.Ordinal)
|
||||
.ToList() ?? new List<string>();
|
||||
|
||||
var states = new List<ReachabilityStateDocument>(targets.Count);
|
||||
foreach (var target in targets)
|
||||
{
|
||||
var path = FindPath(entryPoints, target, graph.Adjacency);
|
||||
var reachable = path is not null;
|
||||
var confidence = reachable ? ReachableConfidence : UnreachableConfidence;
|
||||
|
||||
var runtimeEvidence = runtimeHits.Where(hit => path?.Contains(hit, StringComparer.Ordinal) == true)
|
||||
.ToList();
|
||||
if (runtimeEvidence.Count > 0)
|
||||
{
|
||||
confidence = Math.Min(MaxConfidence, confidence + RuntimeBonus);
|
||||
}
|
||||
|
||||
confidence = Math.Clamp(confidence, MinConfidence, MaxConfidence);
|
||||
|
||||
states.Add(new ReachabilityStateDocument
|
||||
{
|
||||
Target = target,
|
||||
Reachable = reachable,
|
||||
Confidence = confidence,
|
||||
Path = path ?? new List<string>(),
|
||||
Evidence = new ReachabilityEvidenceDocument
|
||||
{
|
||||
RuntimeHits = runtimeEvidence,
|
||||
BlockedEdges = request.BlockedEdges?.Select(edge => $"{edge.From} -> {edge.To}").ToList()
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
var document = new ReachabilityFactDocument
|
||||
{
|
||||
CallgraphId = request.CallgraphId,
|
||||
Subject = request.Subject,
|
||||
EntryPoints = entryPoints,
|
||||
States = states,
|
||||
Metadata = request.Metadata,
|
||||
ComputedAt = timeProvider.GetUtcNow(),
|
||||
SubjectKey = request.Subject.ToSubjectKey()
|
||||
};
|
||||
|
||||
logger.LogInformation("Computed reachability fact for subject {SubjectKey} with {StateCount} targets.", document.SubjectKey, states.Count);
|
||||
return await factRepository.UpsertAsync(document, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private static void ValidateRequest(ReachabilityRecomputeRequest request)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(request.CallgraphId))
|
||||
{
|
||||
throw new ReachabilityScoringValidationException("Callgraph id is required.");
|
||||
}
|
||||
|
||||
if (request.Subject is null)
|
||||
{
|
||||
throw new ReachabilityScoringValidationException("Subject is required.");
|
||||
}
|
||||
}
|
||||
|
||||
private static ReachabilityGraph BuildGraph(CallgraphDocument document, IEnumerable<ReachabilityBlockedEdge> blockedEdges)
|
||||
{
|
||||
var adjacency = new Dictionary<string, HashSet<string>>(StringComparer.Ordinal);
|
||||
var inbound = new Dictionary<string, HashSet<string>>(StringComparer.Ordinal);
|
||||
var blocked = new HashSet<(string From, string To)>(new ReachabilityBlockedEdgeComparer());
|
||||
foreach (var blockedEdge in blockedEdges)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(blockedEdge.From) && !string.IsNullOrWhiteSpace(blockedEdge.To))
|
||||
{
|
||||
blocked.Add((blockedEdge.From.Trim(), blockedEdge.To.Trim()));
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var edge in document.Edges)
|
||||
{
|
||||
if (blocked.Contains((edge.SourceId, edge.TargetId)))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!adjacency.TryGetValue(edge.SourceId, out var targets))
|
||||
{
|
||||
targets = new HashSet<string>(StringComparer.Ordinal);
|
||||
adjacency[edge.SourceId] = targets;
|
||||
}
|
||||
|
||||
if (targets.Add(edge.TargetId) && !inbound.TryGetValue(edge.TargetId, out var sources))
|
||||
{
|
||||
sources = new HashSet<string>(StringComparer.Ordinal);
|
||||
inbound[edge.TargetId] = sources;
|
||||
}
|
||||
|
||||
inbound[edge.TargetId].Add(edge.SourceId);
|
||||
}
|
||||
|
||||
var nodes = new HashSet<string>(document.Nodes?.Select(n => n.Id) ?? Array.Empty<string>(), StringComparer.Ordinal);
|
||||
foreach (var pair in adjacency)
|
||||
{
|
||||
nodes.Add(pair.Key);
|
||||
foreach (var neighbor in pair.Value)
|
||||
{
|
||||
nodes.Add(neighbor);
|
||||
}
|
||||
}
|
||||
|
||||
return new ReachabilityGraph(nodes, adjacency, inbound);
|
||||
}
|
||||
|
||||
private static List<string> NormalizeEntryPoints(IEnumerable<string> requestedEntries, HashSet<string> nodes, Dictionary<string, HashSet<string>> inbound)
|
||||
{
|
||||
var entries = requestedEntries?
|
||||
.Where(entry => !string.IsNullOrWhiteSpace(entry))
|
||||
.Select(entry => entry.Trim())
|
||||
.Distinct(StringComparer.Ordinal)
|
||||
.Where(nodes.Contains)
|
||||
.ToList() ?? new List<string>();
|
||||
|
||||
if (entries.Count > 0)
|
||||
{
|
||||
return entries;
|
||||
}
|
||||
|
||||
var inferred = nodes.Where(node => !inbound.ContainsKey(node)).ToList();
|
||||
if (inferred.Count == 0)
|
||||
{
|
||||
inferred.AddRange(nodes);
|
||||
}
|
||||
|
||||
return inferred;
|
||||
}
|
||||
|
||||
private static List<string>? FindPath(IEnumerable<string> entryPoints, string target, Dictionary<string, HashSet<string>> adjacency)
|
||||
{
|
||||
var queue = new Queue<string>();
|
||||
var parents = new Dictionary<string, string>(StringComparer.Ordinal);
|
||||
var visited = new HashSet<string>(StringComparer.Ordinal);
|
||||
|
||||
foreach (var entry in entryPoints)
|
||||
{
|
||||
if (visited.Add(entry))
|
||||
{
|
||||
queue.Enqueue(entry);
|
||||
}
|
||||
}
|
||||
|
||||
while (queue.Count > 0)
|
||||
{
|
||||
var current = queue.Dequeue();
|
||||
if (string.Equals(current, target, StringComparison.Ordinal))
|
||||
{
|
||||
return BuildPath(current, parents);
|
||||
}
|
||||
|
||||
if (!adjacency.TryGetValue(current, out var neighbors))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (var neighbor in neighbors)
|
||||
{
|
||||
if (visited.Add(neighbor))
|
||||
{
|
||||
parents[neighbor] = current;
|
||||
queue.Enqueue(neighbor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static List<string> BuildPath(string target, Dictionary<string, string> parents)
|
||||
{
|
||||
var path = new List<string>();
|
||||
var current = target;
|
||||
path.Add(current);
|
||||
|
||||
while (parents.TryGetValue(current, out var prev))
|
||||
{
|
||||
path.Add(prev);
|
||||
current = prev;
|
||||
}
|
||||
|
||||
path.Reverse();
|
||||
return path;
|
||||
}
|
||||
|
||||
private sealed record ReachabilityGraph(
|
||||
HashSet<string> Nodes,
|
||||
Dictionary<string, HashSet<string>> Adjacency,
|
||||
Dictionary<string, HashSet<string>> Inbound);
|
||||
|
||||
private sealed class ReachabilityBlockedEdgeComparer : IEqualityComparer<(string From, string To)>
|
||||
{
|
||||
public bool Equals((string From, string To) x, (string From, string To) y)
|
||||
=> string.Equals(x.From, y.From, StringComparison.Ordinal)
|
||||
&& string.Equals(x.To, y.To, StringComparison.Ordinal);
|
||||
|
||||
public int GetHashCode((string From, string To) obj)
|
||||
{
|
||||
unchecked
|
||||
{
|
||||
return (StringComparer.Ordinal.GetHashCode(obj.From) * 397)
|
||||
^ StringComparer.Ordinal.GetHashCode(obj.To);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class ReachabilityScoringValidationException : Exception
|
||||
{
|
||||
public ReachabilityScoringValidationException(string message) : base(message)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class ReachabilityCallgraphNotFoundException : Exception
|
||||
{
|
||||
public ReachabilityCallgraphNotFoundException(string callgraphId) : base($"Callgraph '{callgraphId}' was not found.")
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,10 @@
|
||||
# Signals Service Task Board — Reachability v1
|
||||
| ID | Status | Owner(s) | Depends on | Description | Exit Criteria |
|
||||
|----|--------|----------|------------|-------------|---------------|
|
||||
| SIGNALS-24-001 | DOING (2025-11-07) | Signals Guild, Authority Guild | AUTH-SIG-26-001 | Stand up Signals API skeleton with RBAC + health checks, sealed-mode config, and DPoP/mTLS plumbing; seed config/tests for `/facts` ingestion. | Host scaffold deployed; `/healthz` and `/facts` endpoints respond with tenant-enforced RBAC; integration tests cover token binding; docs outline bootstrap steps. |
|
||||
> 2025-11-07: DPoP nonce store wired to Authority preview tenants; `/healthz` + `/facts` smoke tests passing in CI with sealed-mode env.
|
||||
| SIGNALS-24-002 | DOING (2025-11-07) | Signals Guild | SIGNALS-24-001 | Implement callgraph ingestion/normalisation pipeline (Java/Node/Python/Go), persist artifacts to CAS, and expose retrieval APIs. | Parser fixtures recorded; storage writes deterministic; retries/backoff documented; integration tests cover dedupe and failure paths. |
|
||||
> 2025-11-07: Java/Node ingestion harness writing CAS blobs locally; Python/Go parsers next along with Mongo upserts.
|
||||
> 2025-10-29: Skeleton live with scope policies, stub endpoints, integration tests. Sample config added under `etc/signals.yaml.sample`.
|
||||
> 2025-10-29: JSON parsers for java/nodejs/python/go implemented; artifacts stored on filesystem with SHA-256, callgraphs upserted into Mongo with unique index; integration tests cover success + malformed requests.
|
||||
| SIGNALS-24-003 | BLOCKED (2025-10-27) | Signals Guild, Runtime Guild | SIGNALS-24-001 | Implement runtime facts ingestion endpoint and normalizer (process, sockets, container metadata) populating `context_facts` with AOC provenance. | Endpoint ingests fixture batches; duplicates deduped; schema enforced; tests cover privacy filters. |
|
||||
@@ -9,3 +13,5 @@
|
||||
> 2025-10-27: Upstream ingestion pipelines (SIGNALS-24-002/003) blocked; scoring engine cannot proceed.
|
||||
| SIGNALS-24-005 | BLOCKED (2025-10-27) | Signals Guild, Platform Events Guild | SIGNALS-24-004 | Implement Redis caches (`reachability_cache:*`), invalidation on new facts, and publish `signals.fact.updated` events. | Cache hit rate tracked; invalidations working; events delivered with idempotent ids; integration tests pass. |
|
||||
> 2025-10-27: Awaiting scoring engine and ingestion layers before wiring cache/events.
|
||||
| SIGNALS-REACH-201-003 | DOING (2025-11-08) | Signals Guild | SIGNALS-24-002 | Normalize multi-language callgraphs + runtime facts into `reachability_graphs` CAS layout, expose `/graphs/{scanId}` APIs, and document schema validations. | Parser fixtures for JVM/.NET/Go/Node/Rust/Swift pass; CAS manifests stored; API integration tests cover RBAC/tenancy. |
|
||||
| SIGNALS-REACH-201-004 | DOING (2025-11-08) | Signals Guild, Policy Guild | SIGNALS-24-004 | Build reachability scoring + cache pipeline (state/score/confidence), emit `signals.fact.updated` events, and provide policy-ready projections with reachability weights. | Engine produces deterministic outputs; Redis cache hit metrics tracked; Policy integration tests consume signals successfully. |
|
||||
|
||||
Reference in New Issue
Block a user