up
Some checks failed
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
Manifest Integrity / Validate Schema Integrity (push) Has been cancelled
Manifest Integrity / Validate Contract Documents (push) Has been cancelled
Manifest Integrity / Validate Pack Fixtures (push) Has been cancelled
Manifest Integrity / Audit SHA256SUMS Files (push) Has been cancelled
Manifest Integrity / Verify Merkle Roots (push) Has been cancelled
Docs CI / lint-and-preview (push) Has been cancelled

This commit is contained in:
StellaOps Bot
2025-12-12 09:35:37 +02:00
parent ce5ec9c158
commit efaf3cb789
238 changed files with 146274 additions and 5767 deletions

View File

@@ -1,7 +1,6 @@
using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
using MongoDB.Bson.Serialization.Attributes;
namespace StellaOps.Signals.Models;
@@ -13,52 +12,37 @@ public sealed class ProvenanceFeed
{
public const int CurrentSchemaVersion = 1;
[BsonElement("schemaVersion")]
[JsonPropertyName("schemaVersion")]
public int SchemaVersion { get; init; } = CurrentSchemaVersion;
[BsonElement("feedId")]
[JsonPropertyName("feedId")]
public string FeedId { get; init; } = Guid.NewGuid().ToString("D");
[BsonElement("feedType")]
[JsonPropertyName("feedType")]
public ProvenanceFeedType FeedType { get; init; } = ProvenanceFeedType.RuntimeFacts;
[BsonElement("generatedAt")]
[JsonPropertyName("generatedAt")]
public DateTimeOffset GeneratedAt { get; init; } = DateTimeOffset.UtcNow;
[BsonElement("sourceService")]
[BsonIgnoreIfNull]
[JsonPropertyName("sourceService")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? SourceService { get; init; }
[BsonElement("tenantId")]
[BsonIgnoreIfNull]
[JsonPropertyName("tenantId")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? TenantId { get; init; }
[BsonElement("correlationId")]
[BsonIgnoreIfNull]
[JsonPropertyName("correlationId")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? CorrelationId { get; init; }
[BsonElement("records")]
[JsonPropertyName("records")]
public List<ProvenanceRecord> Records { get; init; } = new();
[BsonElement("metadata")]
[BsonIgnoreIfNull]
[JsonPropertyName("metadata")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public Dictionary<string, string?>? Metadata { get; init; }
[BsonElement("attestation")]
[BsonIgnoreIfNull]
[JsonPropertyName("attestation")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public FeedAttestation? Attestation { get; init; }
@@ -88,42 +72,30 @@ public enum ProvenanceFeedType
/// </summary>
public sealed class ProvenanceRecord
{
[BsonElement("recordId")]
[JsonPropertyName("recordId")]
public string RecordId { get; init; } = Guid.NewGuid().ToString("D");
[BsonElement("recordType")]
[JsonPropertyName("recordType")]
public string RecordType { get; init; } = string.Empty;
[BsonElement("subject")]
[JsonPropertyName("subject")]
public ProvenanceSubject Subject { get; init; } = new();
[BsonElement("occurredAt")]
[JsonPropertyName("occurredAt")]
public DateTimeOffset OccurredAt { get; init; }
[BsonElement("observedBy")]
[BsonIgnoreIfNull]
[JsonPropertyName("observedBy")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? ObservedBy { get; init; }
[BsonElement("confidence")]
[BsonIgnoreIfNull]
[JsonPropertyName("confidence")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public double? Confidence { get; init; }
[BsonElement("facts")]
[BsonIgnoreIfNull]
[JsonPropertyName("facts")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public RuntimeProvenanceFacts? Facts { get; init; }
[BsonElement("evidence")]
[BsonIgnoreIfNull]
[JsonPropertyName("evidence")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public RecordEvidence? Evidence { get; init; }
@@ -134,22 +106,16 @@ public sealed class ProvenanceRecord
/// </summary>
public sealed class ProvenanceSubject
{
[BsonElement("type")]
[JsonPropertyName("type")]
public ProvenanceSubjectType Type { get; init; } = ProvenanceSubjectType.Package;
[BsonElement("identifier")]
[JsonPropertyName("identifier")]
public string Identifier { get; init; } = string.Empty;
[BsonElement("digest")]
[BsonIgnoreIfNull]
[JsonPropertyName("digest")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? Digest { get; init; }
[BsonElement("namespace")]
[BsonIgnoreIfNull]
[JsonPropertyName("namespace")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? Namespace { get; init; }
@@ -182,66 +148,45 @@ public enum ProvenanceSubjectType
/// </summary>
public sealed class RuntimeProvenanceFacts
{
[BsonElement("symbolId")]
[BsonIgnoreIfNull]
[JsonPropertyName("symbolId")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? SymbolId { get; init; }
[BsonElement("processName")]
[BsonIgnoreIfNull]
[JsonPropertyName("processName")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? ProcessName { get; init; }
[BsonElement("processId")]
[BsonIgnoreIfNull]
[JsonPropertyName("processId")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public int? ProcessId { get; init; }
[BsonElement("socketAddress")]
[BsonIgnoreIfNull]
[JsonPropertyName("socketAddress")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? SocketAddress { get; init; }
[BsonElement("containerId")]
[BsonIgnoreIfNull]
[JsonPropertyName("containerId")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? ContainerId { get; init; }
[BsonElement("hitCount")]
[JsonPropertyName("hitCount")]
public int HitCount { get; init; }
[BsonElement("purl")]
[BsonIgnoreIfNull]
[JsonPropertyName("purl")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? Purl { get; init; }
[BsonElement("codeId")]
[BsonIgnoreIfNull]
[JsonPropertyName("codeId")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? CodeId { get; init; }
[BsonElement("buildId")]
[BsonIgnoreIfNull]
[JsonPropertyName("buildId")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? BuildId { get; init; }
[BsonElement("loaderBase")]
[BsonIgnoreIfNull]
[JsonPropertyName("loaderBase")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? LoaderBase { get; init; }
[BsonElement("metadata")]
[BsonIgnoreIfNull]
[JsonPropertyName("metadata")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public Dictionary<string, string?>? Metadata { get; init; }
@@ -252,20 +197,14 @@ public sealed class RuntimeProvenanceFacts
/// </summary>
public sealed class RecordEvidence
{
[BsonElement("sourceDigest")]
[BsonIgnoreIfNull]
[JsonPropertyName("sourceDigest")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? SourceDigest { get; init; }
[BsonElement("captureMethod")]
[BsonIgnoreIfNull]
[JsonPropertyName("captureMethod")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public EvidenceCaptureMethod? CaptureMethod { get; init; }
[BsonElement("rawDataRef")]
[BsonIgnoreIfNull]
[JsonPropertyName("rawDataRef")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? RawDataRef { get; init; }
@@ -295,28 +234,20 @@ public enum EvidenceCaptureMethod
/// </summary>
public sealed class FeedAttestation
{
[BsonElement("predicateType")]
[JsonPropertyName("predicateType")]
public string PredicateType { get; init; } = "https://stella.ops/attestation/provenance-feed/v1";
[BsonElement("signedAt")]
[JsonPropertyName("signedAt")]
public DateTimeOffset SignedAt { get; init; }
[BsonElement("keyId")]
[BsonIgnoreIfNull]
[JsonPropertyName("keyId")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? KeyId { get; init; }
[BsonElement("envelopeDigest")]
[BsonIgnoreIfNull]
[JsonPropertyName("envelopeDigest")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? EnvelopeDigest { get; init; }
[BsonElement("transparencyLog")]
[BsonIgnoreIfNull]
[JsonPropertyName("transparencyLog")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? TransparencyLog { get; init; }
@@ -327,17 +258,13 @@ public sealed class FeedAttestation
/// </summary>
public sealed class ContextFacts
{
[BsonElement("provenance")]
[BsonIgnoreIfNull]
[JsonPropertyName("provenance")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public ProvenanceFeed? Provenance { get; set; }
[BsonElement("lastUpdatedAt")]
[JsonPropertyName("lastUpdatedAt")]
public DateTimeOffset LastUpdatedAt { get; set; }
[BsonElement("recordCount")]
[JsonPropertyName("recordCount")]
public int RecordCount { get; set; }
}

View File

@@ -1,33 +1,23 @@
using MongoDB.Bson.Serialization.Attributes;
namespace StellaOps.Signals.Models;
/// <summary>
/// Metadata describing the stored raw callgraph artifact.
/// </summary>
public sealed class CallgraphArtifactMetadata
{
[BsonElement("path")]
namespace StellaOps.Signals.Models;
/// <summary>
/// Metadata describing the stored raw callgraph artifact.
/// </summary>
public sealed class CallgraphArtifactMetadata
{
public string Path { get; set; } = string.Empty;
[BsonElement("hash")]
public string Hash { get; set; } = string.Empty;
[BsonElement("casUri")]
public string CasUri { get; set; } = string.Empty;
public string? CasUri { get; set; }
[BsonElement("manifestPath")]
public string ManifestPath { get; set; } = string.Empty;
[BsonElement("manifestCasUri")]
public string ManifestCasUri { get; set; } = string.Empty;
public string? ManifestCasUri { get; set; }
[BsonElement("graphHash")]
public string GraphHash { get; set; } = string.Empty;
[BsonElement("contentType")]
public string ContentType { get; set; } = string.Empty;
[BsonElement("length")]
public long Length { get; set; }
}
public long Length { get; set; }
}

View File

@@ -1,52 +1,34 @@
using System;
using System.Collections.Generic;
using MongoDB.Bson;
using MongoDB.Bson.Serialization.Attributes;
namespace StellaOps.Signals.Models;
/// <summary>
/// MongoDB document representing an ingested callgraph.
/// </summary>
public sealed class CallgraphDocument
{
[BsonId]
[BsonRepresentation(BsonType.ObjectId)]
public string Id { get; set; } = ObjectId.GenerateNewId().ToString();
[BsonElement("language")]
public string Language { get; set; } = string.Empty;
[BsonElement("component")]
public string Component { get; set; } = string.Empty;
[BsonElement("version")]
public string Version { get; set; } = string.Empty;
[BsonElement("ingestedAt")]
public DateTimeOffset IngestedAt { get; set; }
[BsonElement("artifact")]
public CallgraphArtifactMetadata Artifact { get; set; } = new();
[BsonElement("nodes")]
public List<CallgraphNode> Nodes { get; set; } = new();
[BsonElement("edges")]
using System;
using System.Collections.Generic;
namespace StellaOps.Signals.Models;
/// <summary>
/// Document representing an ingested callgraph.
/// </summary>
public sealed class CallgraphDocument
{
public string Id { get; set; } = Guid.NewGuid().ToString("N");
public string Language { get; set; } = string.Empty;
public string Component { get; set; } = string.Empty;
public string Version { get; set; } = string.Empty;
public DateTimeOffset IngestedAt { get; set; }
public CallgraphArtifactMetadata Artifact { get; set; } = new();
public List<CallgraphNode> Nodes { get; set; } = new();
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; }
}

View File

@@ -1,110 +1,71 @@
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();
public string Id { get; set; } = Guid.NewGuid().ToString("N");
[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("runtimeFacts")]
[BsonIgnoreIfNull]
public List<RuntimeFactDocument>? RuntimeFacts { get; set; }
[BsonElement("metadata")]
[BsonIgnoreIfNull]
public Dictionary<string, string?>? Metadata { get; set; }
[BsonElement("contextFacts")]
[BsonIgnoreIfNull]
public ContextFacts? ContextFacts { get; set; }
[BsonElement("score")]
public double Score { get; set; }
[BsonElement("unknownsCount")]
public int UnknownsCount { get; set; }
[BsonElement("unknownsPressure")]
public double UnknownsPressure { 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("bucket")]
public string Bucket { get; set; } = "unknown";
[BsonElement("weight")]
public double Weight { get; set; }
[BsonElement("score")]
public double Score { 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()
@@ -125,57 +86,31 @@ public sealed class ReachabilitySubject
public sealed class RuntimeFactDocument
{
[BsonElement("symbolId")]
public string SymbolId { get; set; } = string.Empty;
[BsonElement("codeId")]
[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; }
[BsonElement("processId")]
[BsonIgnoreIfNull]
public int? ProcessId { get; set; }
[BsonElement("processName")]
[BsonIgnoreIfNull]
public string? ProcessName { get; set; }
[BsonElement("socketAddress")]
[BsonIgnoreIfNull]
public string? SocketAddress { get; set; }
[BsonElement("containerId")]
[BsonIgnoreIfNull]
public string? ContainerId { get; set; }
[BsonElement("evidenceUri")]
[BsonIgnoreIfNull]
public string? EvidenceUri { get; set; }
[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; }
}

View File

@@ -0,0 +1,29 @@
using System.Collections.Generic;
namespace StellaOps.Signals.Models;
/// <summary>
/// Request to generate synthetic runtime facts for a callgraph to unblock probe testing.
/// </summary>
public sealed class SyntheticRuntimeProbeRequest
{
/// <summary>
/// Target callgraph id to sample nodes from.
/// </summary>
public string CallgraphId { get; set; } = string.Empty;
/// <summary>
/// Subject to associate with generated runtime facts.
/// </summary>
public ReachabilitySubject? Subject { get; set; }
/// <summary>
/// Optional metadata attached to the runtime fact ingestion.
/// </summary>
public Dictionary<string, string?>? Metadata { get; set; }
/// <summary>
/// How many events to emit (defaults to 5, capped by node count).
/// </summary>
public int EventCount { get; set; } = 5;
}

View File

@@ -1,47 +1,26 @@
using System;
using MongoDB.Bson;
using MongoDB.Bson.Serialization.Attributes;
namespace StellaOps.Signals.Models;
public sealed class UnknownSymbolDocument
{
[BsonId]
[BsonRepresentation(BsonType.ObjectId)]
public string Id { get; set; } = ObjectId.GenerateNewId().ToString();
public string Id { get; set; } = Guid.NewGuid().ToString("N");
[BsonElement("subjectKey")]
[BsonRequired]
public string SubjectKey { get; set; } = string.Empty;
[BsonElement("callgraphId")]
[BsonIgnoreIfNull]
public string? CallgraphId { get; set; }
[BsonElement("symbolId")]
[BsonIgnoreIfNull]
public string? SymbolId { get; set; }
[BsonElement("codeId")]
[BsonIgnoreIfNull]
public string? CodeId { get; set; }
[BsonElement("purl")]
[BsonIgnoreIfNull]
public string? Purl { get; set; }
[BsonElement("edgeFrom")]
[BsonIgnoreIfNull]
public string? EdgeFrom { get; set; }
[BsonElement("edgeTo")]
[BsonIgnoreIfNull]
public string? EdgeTo { get; set; }
[BsonElement("reason")]
[BsonIgnoreIfNull]
public string? Reason { get; set; }
[BsonElement("createdAt")]
public DateTimeOffset CreatedAt { get; set; }
}

View File

@@ -1,65 +0,0 @@
using System;
namespace StellaOps.Signals.Options;
/// <summary>
/// MongoDB configuration for Signals.
/// </summary>
public sealed class SignalsMongoOptions
{
/// <summary>
/// MongoDB connection string.
/// </summary>
public string ConnectionString { get; set; } = string.Empty;
/// <summary>
/// Database name to use when the connection string omits one.
/// </summary>
public string Database { get; set; } = "signals";
/// <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>
/// Collection name storing unresolved symbols/edges (Unknowns Registry).
/// </summary>
public string UnknownsCollection { get; set; } = "unknowns";
/// <summary>
/// Validates the configured values.
/// </summary>
public void Validate()
{
if (string.IsNullOrWhiteSpace(ConnectionString))
{
throw new InvalidOperationException("Signals Mongo connection string must be configured.");
}
if (string.IsNullOrWhiteSpace(Database))
{
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(ReachabilityFactsCollection))
{
throw new InvalidOperationException("Signals reachability fact collection name must be configured.");
}
if (string.IsNullOrWhiteSpace(UnknownsCollection))
{
throw new InvalidOperationException("Signals unknowns collection name must be configured.");
}
}
}

View File

@@ -15,11 +15,6 @@ public sealed class SignalsOptions
/// </summary>
public SignalsAuthorityOptions Authority { get; } = new();
/// <summary>
/// MongoDB configuration.
/// </summary>
public SignalsMongoOptions Mongo { get; } = new();
/// <summary>
/// Artifact storage configuration.
/// </summary>
@@ -56,7 +51,6 @@ public sealed class SignalsOptions
public void Validate()
{
Authority.Validate();
Mongo.Validate();
Storage.Validate();
AirGap.Validate();
Scoring.Validate();

View File

@@ -0,0 +1,87 @@
using System.Collections.Concurrent;
using System.Threading;
using System.Threading.Tasks;
using StellaOps.Signals.Models;
namespace StellaOps.Signals.Persistence;
internal sealed class InMemoryCallgraphRepository : ICallgraphRepository
{
private readonly ConcurrentDictionary<string, CallgraphDocument> _store = new(StringComparer.OrdinalIgnoreCase);
public Task<CallgraphDocument> UpsertAsync(CallgraphDocument document, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(document);
if (string.IsNullOrWhiteSpace(document.Id))
{
document.Id = Guid.NewGuid().ToString("N");
}
_store[document.Id] = Clone(document);
return Task.FromResult(Clone(document));
}
public Task<CallgraphDocument?> GetByIdAsync(string id, CancellationToken cancellationToken)
{
if (string.IsNullOrWhiteSpace(id))
{
return Task.FromResult<CallgraphDocument?>(null);
}
return Task.FromResult(_store.TryGetValue(id, out var doc) ? Clone(doc) : null);
}
private static CallgraphDocument Clone(CallgraphDocument source) => new()
{
Id = source.Id,
Language = source.Language,
Component = source.Component,
Version = source.Version,
IngestedAt = source.IngestedAt,
Artifact = CloneArtifact(source.Artifact),
Nodes = source.Nodes.Select(CloneNode).ToList(),
Edges = source.Edges.Select(CloneEdge).ToList(),
Metadata = source.Metadata is null ? null : new Dictionary<string, string?>(source.Metadata, StringComparer.OrdinalIgnoreCase),
GraphHash = source.GraphHash,
Roots = source.Roots?.Select(r => new CallgraphRoot(r.Id, r.Phase, r.Source)).ToList(),
SchemaVersion = source.SchemaVersion
};
private static CallgraphArtifactMetadata CloneArtifact(CallgraphArtifactMetadata source) => new()
{
Path = source.Path,
Hash = source.Hash,
CasUri = source.CasUri,
ManifestPath = source.ManifestPath,
ManifestCasUri = source.ManifestCasUri,
GraphHash = source.GraphHash,
ContentType = source.ContentType,
Length = source.Length
};
private static CallgraphNode CloneNode(CallgraphNode source) => new(
source.Id,
source.Name,
source.Kind,
source.Namespace,
source.File,
source.Line,
source.Purl,
source.SymbolDigest,
source.BuildId,
source.Language,
source.Evidence?.ToList(),
source.Analyzer is null ? null : new Dictionary<string, string?>(source.Analyzer, StringComparer.OrdinalIgnoreCase),
source.CodeId);
private static CallgraphEdge CloneEdge(CallgraphEdge source) => new(
source.SourceId,
source.TargetId,
source.Type,
source.Purl,
source.SymbolDigest,
source.Candidates?.ToList(),
source.Confidence,
source.Evidence?.ToList());
}

View File

@@ -0,0 +1,84 @@
using System.Collections.Concurrent;
using System.Threading;
using System.Threading.Tasks;
using StellaOps.Signals.Models;
namespace StellaOps.Signals.Persistence;
internal sealed class InMemoryReachabilityFactRepository : IReachabilityFactRepository
{
private readonly ConcurrentDictionary<string, ReachabilityFactDocument> _store = new(StringComparer.OrdinalIgnoreCase);
public Task<ReachabilityFactDocument> UpsertAsync(ReachabilityFactDocument document, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(document);
if (string.IsNullOrWhiteSpace(document.SubjectKey))
{
throw new ArgumentException("Subject key is required.", nameof(document));
}
_store[document.SubjectKey] = Clone(document);
return Task.FromResult(Clone(document));
}
public Task<ReachabilityFactDocument?> GetBySubjectAsync(string subjectKey, CancellationToken cancellationToken)
{
if (string.IsNullOrWhiteSpace(subjectKey))
{
throw new ArgumentException("Subject key is required.", nameof(subjectKey));
}
return Task.FromResult(_store.TryGetValue(subjectKey, out var doc) ? Clone(doc) : null);
}
private static ReachabilityFactDocument Clone(ReachabilityFactDocument source) => new()
{
Id = source.Id,
CallgraphId = source.CallgraphId,
Subject = source.Subject,
EntryPoints = source.EntryPoints.ToList(),
States = source.States.Select(CloneState).ToList(),
RuntimeFacts = source.RuntimeFacts?.Select(CloneRuntime).ToList(),
Metadata = source.Metadata is null ? null : new Dictionary<string, string?>(source.Metadata, StringComparer.OrdinalIgnoreCase),
ContextFacts = source.ContextFacts,
Score = source.Score,
UnknownsCount = source.UnknownsCount,
UnknownsPressure = source.UnknownsPressure,
ComputedAt = source.ComputedAt,
SubjectKey = source.SubjectKey
};
private static ReachabilityStateDocument CloneState(ReachabilityStateDocument source) => new()
{
Target = source.Target,
Reachable = source.Reachable,
Confidence = source.Confidence,
Bucket = source.Bucket,
Weight = source.Weight,
Score = source.Score,
Path = source.Path.ToList(),
Evidence = new ReachabilityEvidenceDocument
{
RuntimeHits = source.Evidence.RuntimeHits.ToList(),
BlockedEdges = source.Evidence.BlockedEdges?.ToList()
}
};
private static RuntimeFactDocument CloneRuntime(RuntimeFactDocument source) => new()
{
SymbolId = source.SymbolId,
CodeId = source.CodeId,
SymbolDigest = source.SymbolDigest,
Purl = source.Purl,
BuildId = source.BuildId,
LoaderBase = source.LoaderBase,
ProcessId = source.ProcessId,
ProcessName = source.ProcessName,
SocketAddress = source.SocketAddress,
ContainerId = source.ContainerId,
EvidenceUri = source.EvidenceUri,
HitCount = source.HitCount,
ObservedAt = source.ObservedAt,
Metadata = source.Metadata is null ? null : new Dictionary<string, string?>(source.Metadata, StringComparer.OrdinalIgnoreCase)
};
}

View File

@@ -0,0 +1,53 @@
using System.Collections.Concurrent;
using System.Threading;
using System.Threading.Tasks;
using StellaOps.Signals.Models;
namespace StellaOps.Signals.Persistence;
public sealed class InMemoryUnknownsRepository : IUnknownsRepository
{
private readonly ConcurrentDictionary<string, List<UnknownSymbolDocument>> _store = new(StringComparer.OrdinalIgnoreCase);
public Task UpsertAsync(string subjectKey, IEnumerable<UnknownSymbolDocument> items, CancellationToken cancellationToken)
{
ArgumentException.ThrowIfNullOrWhiteSpace(subjectKey);
ArgumentNullException.ThrowIfNull(items);
_store[subjectKey] = items.Select(Clone).ToList();
return Task.CompletedTask;
}
public Task<IReadOnlyList<UnknownSymbolDocument>> GetBySubjectAsync(string subjectKey, CancellationToken cancellationToken)
{
ArgumentException.ThrowIfNullOrWhiteSpace(subjectKey);
if (_store.TryGetValue(subjectKey, out var items))
{
return Task.FromResult<IReadOnlyList<UnknownSymbolDocument>>(items.Select(Clone).ToList());
}
return Task.FromResult<IReadOnlyList<UnknownSymbolDocument>>(Array.Empty<UnknownSymbolDocument>());
}
public Task<int> CountBySubjectAsync(string subjectKey, CancellationToken cancellationToken)
{
ArgumentException.ThrowIfNullOrWhiteSpace(subjectKey);
return Task.FromResult(_store.TryGetValue(subjectKey, out var items) ? items.Count : 0);
}
private static UnknownSymbolDocument Clone(UnknownSymbolDocument source) => new()
{
Id = source.Id,
SubjectKey = source.SubjectKey,
CallgraphId = source.CallgraphId,
SymbolId = source.SymbolId,
CodeId = source.CodeId,
Purl = source.Purl,
EdgeFrom = source.EdgeFrom,
EdgeTo = source.EdgeTo,
Reason = source.Reason,
CreatedAt = source.CreatedAt
};
}

View File

@@ -1,59 +0,0 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using MongoDB.Bson;
using MongoDB.Driver;
using StellaOps.Signals.Models;
namespace StellaOps.Signals.Persistence;
internal sealed class MongoCallgraphRepository : ICallgraphRepository
{
private readonly IMongoCollection<CallgraphDocument> collection;
private readonly ILogger<MongoCallgraphRepository> logger;
public MongoCallgraphRepository(IMongoCollection<CallgraphDocument> collection, ILogger<MongoCallgraphRepository> logger)
{
this.collection = collection ?? throw new ArgumentNullException(nameof(collection));
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)
& Builders<CallgraphDocument>.Filter.Eq(d => d.Version, document.Version)
& Builders<CallgraphDocument>.Filter.Eq(d => d.Language, document.Language);
if (string.IsNullOrWhiteSpace(document.Id))
{
document.Id = ObjectId.GenerateNewId().ToString();
}
document.IngestedAt = DateTimeOffset.UtcNow;
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 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);
}
}

View File

@@ -1,53 +0,0 @@
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);
}
}

View File

@@ -1,53 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using MongoDB.Bson;
using MongoDB.Driver;
using StellaOps.Signals.Models;
namespace StellaOps.Signals.Persistence;
public sealed class MongoUnknownsRepository : IUnknownsRepository
{
private readonly IMongoCollection<UnknownSymbolDocument> collection;
public MongoUnknownsRepository(IMongoCollection<UnknownSymbolDocument> collection)
{
this.collection = collection ?? throw new ArgumentNullException(nameof(collection));
}
public async Task UpsertAsync(string subjectKey, IEnumerable<UnknownSymbolDocument> items, CancellationToken cancellationToken)
{
ArgumentException.ThrowIfNullOrWhiteSpace(subjectKey);
ArgumentNullException.ThrowIfNull(items);
// deterministic replace per subject to keep the registry stable
await collection.DeleteManyAsync(doc => doc.SubjectKey == subjectKey, cancellationToken).ConfigureAwait(false);
var batch = items.ToList();
if (batch.Count == 0)
{
return;
}
await collection.InsertManyAsync(batch, cancellationToken: cancellationToken).ConfigureAwait(false);
}
public async Task<IReadOnlyList<UnknownSymbolDocument>> GetBySubjectAsync(string subjectKey, CancellationToken cancellationToken)
{
ArgumentException.ThrowIfNullOrWhiteSpace(subjectKey);
var cursor = await collection.FindAsync(doc => doc.SubjectKey == subjectKey, cancellationToken: cancellationToken).ConfigureAwait(false);
return await cursor.ToListAsync(cancellationToken).ConfigureAwait(false);
}
public async Task<int> CountBySubjectAsync(string subjectKey, CancellationToken cancellationToken)
{
ArgumentException.ThrowIfNullOrWhiteSpace(subjectKey);
var count = await collection.CountDocumentsAsync(doc => doc.SubjectKey == subjectKey, cancellationToken: cancellationToken).ConfigureAwait(false);
return (int)count;
}
}

View File

@@ -4,7 +4,6 @@ using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
using MongoDB.Driver;
using NetEscapades.Configuration.Yaml;
using StellaOps.Auth.Abstractions;
using StellaOps.Auth.ServerIntegration;
@@ -82,49 +81,7 @@ builder.Services.AddProblemDetails();
builder.Services.AddHealthChecks();
builder.Services.AddRouting(options => options.LowercaseUrls = true);
builder.Services.AddSingleton<IMongoClient>(sp =>
{
var opts = sp.GetRequiredService<IOptions<SignalsOptions>>().Value;
return new MongoClient(opts.Mongo.ConnectionString);
});
builder.Services.AddSingleton<IMongoDatabase>(sp =>
{
var opts = sp.GetRequiredService<IOptions<SignalsOptions>>().Value;
var mongoClient = sp.GetRequiredService<IMongoClient>();
var mongoUrl = MongoUrl.Create(opts.Mongo.ConnectionString);
var databaseName = string.IsNullOrWhiteSpace(mongoUrl.DatabaseName) ? opts.Mongo.Database : mongoUrl.DatabaseName;
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<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<IMongoCollection<UnknownSymbolDocument>>(sp =>
{
var opts = sp.GetRequiredService<IOptions<SignalsOptions>>().Value;
var database = sp.GetRequiredService<IMongoDatabase>();
var collection = database.GetCollection<UnknownSymbolDocument>(opts.Mongo.UnknownsCollection);
EnsureUnknownsIndexes(collection);
return collection;
});
builder.Services.AddSingleton<ICallgraphRepository, MongoCallgraphRepository>();
builder.Services.AddSingleton<ICallgraphRepository, InMemoryCallgraphRepository>();
builder.Services.AddSingleton<ICallgraphNormalizationService, CallgraphNormalizationService>();
// Configure callgraph artifact storage based on driver
@@ -160,7 +117,6 @@ builder.Services.AddSingleton<ICallgraphParser>(new SimpleJsonCallgraphParser("p
builder.Services.AddSingleton<ICallgraphParser>(new SimpleJsonCallgraphParser("go"));
builder.Services.AddSingleton<ICallgraphParserResolver, CallgraphParserResolver>();
builder.Services.AddSingleton<ICallgraphIngestionService, CallgraphIngestionService>();
builder.Services.AddSingleton<MongoReachabilityFactRepository>();
builder.Services.AddSingleton<IReachabilityCache>(sp =>
{
var options = sp.GetRequiredService<IOptions<SignalsOptions>>().Value;
@@ -168,6 +124,14 @@ builder.Services.AddSingleton<IReachabilityCache>(sp =>
});
builder.Services.AddSingleton<IRedisConnectionFactory, RedisConnectionFactory>();
builder.Services.AddSingleton<ReachabilityFactEventBuilder>();
builder.Services.AddSingleton<InMemoryReachabilityFactRepository>();
builder.Services.AddSingleton<IReachabilityFactRepository>(sp =>
{
var inner = sp.GetRequiredService<InMemoryReachabilityFactRepository>();
var cache = sp.GetRequiredService<IReachabilityCache>();
return new ReachabilityFactCacheDecorator(inner, cache);
});
builder.Services.AddSingleton<IUnknownsRepository, InMemoryUnknownsRepository>();
builder.Services.AddHttpClient<RouterEventsPublisher>((sp, client) =>
{
var opts = sp.GetRequiredService<SignalsOptions>().Events.Router;
@@ -221,18 +185,12 @@ builder.Services.AddSingleton<IEventsPublisher>(sp =>
sp.GetRequiredService<ILogger<InMemoryEventsPublisher>>(),
eventBuilder);
});
builder.Services.AddSingleton<IReachabilityFactRepository>(sp =>
{
var inner = sp.GetRequiredService<MongoReachabilityFactRepository>();
var cache = sp.GetRequiredService<IReachabilityCache>();
return new ReachabilityFactCacheDecorator(inner, cache);
});
builder.Services.AddSingleton<IReachabilityScoringService, ReachabilityScoringService>();
builder.Services.AddSingleton<IRuntimeFactsProvenanceNormalizer, RuntimeFactsProvenanceNormalizer>();
builder.Services.AddSingleton<IRuntimeFactsIngestionService, RuntimeFactsIngestionService>();
builder.Services.AddSingleton<IReachabilityUnionIngestionService, ReachabilityUnionIngestionService>();
builder.Services.AddSingleton<IUnknownsRepository, MongoUnknownsRepository>();
builder.Services.AddSingleton<IUnknownsIngestionService, UnknownsIngestionService>();
builder.Services.AddSingleton<SyntheticRuntimeProbeBuilder>();
if (bootstrap.Authority.Enabled)
{
@@ -488,6 +446,56 @@ signalsGroup.MapPost("/runtime-facts", async Task<IResult> (
}
}).WithName("SignalsRuntimeIngest");
signalsGroup.MapPost("/runtime-facts/synthetic", async Task<IResult> (
HttpContext context,
SignalsOptions options,
SyntheticRuntimeProbeRequest request,
ICallgraphRepository callgraphRepository,
IRuntimeFactsIngestionService ingestionService,
SyntheticRuntimeProbeBuilder probeBuilder,
SignalsSealedModeMonitor sealedModeMonitor,
CancellationToken cancellationToken) =>
{
if (!Program.TryAuthorize(context, SignalsPolicies.Write, options.Authority.AllowAnonymousFallback, out var authFailure))
{
return authFailure ?? Results.Unauthorized();
}
if (!Program.TryEnsureSealedMode(sealedModeMonitor, out var sealedFailure))
{
return sealedFailure ?? Results.StatusCode(StatusCodes.Status503ServiceUnavailable);
}
if (string.IsNullOrWhiteSpace(request.CallgraphId))
{
return Results.BadRequest(new { error = "callgraphId is required." });
}
var callgraph = await callgraphRepository.GetByIdAsync(request.CallgraphId.Trim(), cancellationToken).ConfigureAwait(false);
if (callgraph is null)
{
return Results.NotFound(new { error = "callgraph not found." });
}
var subject = request.Subject ?? new ReachabilitySubject { ScanId = $"synthetic-{callgraph.Id}" };
var events = probeBuilder.BuildEvents(callgraph, request.EventCount);
var metadata = request.Metadata is null
? new Dictionary<string, string?>(StringComparer.Ordinal)
: new Dictionary<string, string?>(request.Metadata, StringComparer.Ordinal);
metadata.TryAdd("source", "synthetic-probe");
var ingestRequest = new RuntimeFactsIngestRequest
{
CallgraphId = callgraph.Id,
Subject = subject,
Events = events,
Metadata = metadata
};
var response = await ingestionService.IngestAsync(ingestRequest, cancellationToken).ConfigureAwait(false);
return Results.Accepted($"/signals/runtime-facts/{response.SubjectKey}", response);
}).WithName("SignalsRuntimeIngestSynthetic");
signalsGroup.MapPost("/reachability/union", async Task<IResult> (
HttpContext context,
SignalsOptions options,
@@ -808,55 +816,6 @@ public partial class Program
return false;
}
internal static void EnsureCallgraphIndexes(IMongoCollection<CallgraphDocument> collection)
{
ArgumentNullException.ThrowIfNull(collection);
try
{
var indexKeys = Builders<CallgraphDocument>.IndexKeys
.Ascending(document => document.Component)
.Ascending(document => document.Version)
.Ascending(document => document.Language);
var model = new CreateIndexModel<CallgraphDocument>(indexKeys, new CreateIndexOptions
{
Name = "callgraphs_component_version_language_unique",
Unique = true
});
collection.Indexes.CreateOne(model);
}
catch (MongoCommandException ex) when (string.Equals(ex.CodeName, "IndexOptionsConflict", StringComparison.Ordinal))
{
// 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.
}
}
internal static bool TryEnsureSealedMode(SignalsSealedModeMonitor monitor, out IResult? failure)
{
if (!monitor.EnforcementEnabled)
@@ -876,31 +835,4 @@ public partial class Program
statusCode: StatusCodes.Status503ServiceUnavailable);
return false;
}
internal static void EnsureUnknownsIndexes(IMongoCollection<UnknownSymbolDocument> collection)
{
ArgumentNullException.ThrowIfNull(collection);
try
{
var subjectIndex = new CreateIndexModel<UnknownSymbolDocument>(
Builders<UnknownSymbolDocument>.IndexKeys.Ascending(doc => doc.SubjectKey),
new CreateIndexOptions { Name = "unknowns_subject_lookup" });
var dedupeIndex = new CreateIndexModel<UnknownSymbolDocument>(
Builders<UnknownSymbolDocument>.IndexKeys
.Ascending(doc => doc.SubjectKey)
.Ascending(doc => doc.SymbolId)
.Ascending(doc => doc.Purl)
.Ascending(doc => doc.EdgeFrom)
.Ascending(doc => doc.EdgeTo),
new CreateIndexOptions { Name = "unknowns_subject_symbol_edge_unique", Unique = true });
collection.Indexes.CreateMany(new[] { subjectIndex, dedupeIndex });
}
catch (MongoCommandException ex) when (string.Equals(ex.CodeName, "IndexOptionsConflict", StringComparison.Ordinal))
{
// Ignore to keep startup idempotent when index options differ.
}
}
}

View File

@@ -0,0 +1,48 @@
using System;
using System.Collections.Generic;
using System.Linq;
using StellaOps.Signals.Models;
namespace StellaOps.Signals.Services;
/// <summary>
/// Builds deterministic synthetic runtime events from an existing callgraph.
/// </summary>
public sealed class SyntheticRuntimeProbeBuilder
{
private readonly TimeProvider timeProvider;
public SyntheticRuntimeProbeBuilder(TimeProvider timeProvider)
{
this.timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider));
}
public List<RuntimeFactEvent> BuildEvents(CallgraphDocument callgraph, int requestedCount)
{
ArgumentNullException.ThrowIfNull(callgraph);
var now = timeProvider.GetUtcNow();
var count = Math.Max(1, Math.Min(requestedCount <= 0 ? 5 : requestedCount, callgraph.Nodes.Count));
return callgraph.Nodes
.OrderBy(n => n.Id, StringComparer.Ordinal)
.Take(count)
.Select((node, index) => new RuntimeFactEvent
{
SymbolId = node.Id,
CodeId = node.CodeId,
Purl = node.Purl,
SymbolDigest = node.SymbolDigest,
BuildId = node.BuildId,
ObservedAt = now,
ProcessId = 1000 + index,
ProcessName = "synthetic-probe",
Metadata = new Dictionary<string, string?>(StringComparer.Ordinal)
{
["probe"] = "synthetic",
["probe.version"] = "v1",
["probe.seed"] = callgraph.Id,
}
})
.ToList();
}
}

View File

@@ -10,7 +10,6 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="MongoDB.Driver" Version="2.24.0" />
<PackageReference Include="StackExchange.Redis" Version="2.7.33" />
</ItemGroup>