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
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:
@@ -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; }
|
||||
}
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
@@ -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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
@@ -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)
|
||||
};
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,6 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="MongoDB.Driver" Version="2.24.0" />
|
||||
<PackageReference Include="StackExchange.Redis" Version="2.7.33" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user