Signals Module Architecture
Overview
The Signals service (StellaOps.Signals) is the reachability and runtime-evidence plane. It ingests and serves the raw evidence signals that downstream scoring and policy consume: static call graphs, reachability facts, runtime facts (observed symbol execution from the runtime agent / deployment-observation agent), unknown symbols, execution evidence, and verification beacons. It owns the signals PostgreSQL schema, exposes an HTTP API under /signals (plus SCM/CI webhooks and a compatibility /api/v1/signals surface), and publishes signals.fact.updated.v1 events when reachability facts change.
The service also owns the Evidence-Weighted Score (EWS) and Unified Score runtime. Score/Endpoints/ScoreEndpoints.cs exposes the tenant-scoped /api/v1/score API, backed by ScoreEvaluationService and durable signals.score_history; the older UnifiedScore/ facade remains an in-process library rather than the endpoint handler. The EWS algorithm is documented under Evidence-Weighted Score (scoring library) below.
Audience: engineers producing or consuming reachability/runtime evidence, integrators of the EWS scoring library, and operators configuring durable storage and the SCM/CI webhook surface. For the scoring model in isolation see unified-score.md; for the wire contract see api-reference.md.
Module Purpose
- Ingest & serve reachability evidence: Call graphs, reachability facts, runtime facts, unknown symbols, execution evidence, beacons — keyed by callgraph/scan/subject identifiers.
- Own durable evidence storage: The
signalsPostgreSQL schema (auto-migrated on startup) plus a content-addressed artifact store (filesystem or RustFS) for callgraph and runtime-facts blobs. - Score deterministically: Combine normalized evidence dimensions into a single 0-100 score via the EWS / Unified Score calculators; same inputs + policy = same score, always.
- Provide transparency: Decomposable scores with per-dimension contributions, applied guardrails, and explanations.
- Fail closed: Outside explicit local-harness mode, require durable PostgreSQL, an explicit cache endpoint, Authority authentication, and (in production) the RustFS artifact store. See Production Runtime Boundary.
Location
src/Signals/
├── StellaOps.Signals/ # Web service (minimal-API host)
│ ├── Program.cs # Host + all /signals endpoints
│ ├── CompatibilityApiV1Endpoints.cs # /api/v1/signals (static demo records)
│ ├── Scm/ScmWebhookEndpoints.cs # /webhooks/{github,gitlab,gitea}
│ ├── Hosting/ # SignalsRuntimeConfigurationValidator,
│ │ # SignalsSealedModeMonitor, SignalsStartupState
│ ├── Models/ # Callgraph*, ReachabilityFact*, RuntimeFacts*,
│ │ # Unknown*, Beacon*, ExecutionEvidence* DTOs
│ ├── Persistence/ # Repo interfaces + in-memory implementations
│ ├── Services/ # Ingestion, scoring, events, retention, decay
│ ├── Storage/ # FileSystem/RustFs callgraph + runtime-facts stores
│ ├── EvidenceWeightedScore/ # EWS scoring library used by Score runtime
│ │ ├── EvidenceWeightedScoreCalculator.cs # also defines EvidenceWeightedScoreResult,
│ │ │ # AppliedGuardrails, DimensionContribution
│ │ ├── EvidenceWeightedScoreInput.cs
│ │ ├── EvidenceWeightPolicy.cs # weights, guardrails, BucketThresholds
│ │ ├── EvidenceWeightPolicyOptions.cs / IEvidenceWeightPolicyProvider.cs
│ │ ├── WeightManifest.cs / FileBasedWeightManifestLoader.cs / IWeightManifestLoader.cs
│ │ ├── ReachabilityInput.cs / RuntimeInput.cs / BackportInput.cs /
│ │ │ ExploitInput.cs / SourceTrustInput.cs / MitigationInput.cs / AnchorMetadata.cs
│ │ └── Normalizers/
│ │ ├── BackportEvidenceNormalizer.cs ExploitLikelihoodNormalizer.cs
│ │ ├── MitigationNormalizer.cs ReachabilityNormalizer.cs
│ │ ├── RuntimeSignalNormalizer.cs SourceTrustNormalizer.cs
│ │ ├── IEvidenceNormalizer.cs NormalizerAggregator.cs / INormalizerAggregator.cs
│ │ └── EvidenceNormalizersServiceCollectionExtensions.cs
│ ├── Score/ # Live /api/v1/score endpoints, contracts, services
│ └── UnifiedScore/ # Older in-process facade over EWS
│ ├── UnifiedScoreService.cs / IUnifiedScoreService.cs / UnifiedScoreModels.cs
│ ├── UnknownsBandMapper.cs ServiceCollectionExtensions.cs
│ └── Replay/ # Deterministic replay verifier
├── StellaOps.Signals.RuntimeAgent/ # In-process runtime evidence agent (CLR EventPipe, eBPF)
├── StellaOps.Signals.Scheduler/ # Scheduler queue job client for rescans
├── __Libraries/
│ ├── StellaOps.Signals.Persistence/ # PostgreSQL repos + Migrations/*.sql (schema: signals)
│ └── StellaOps.Signals.Ebpf/ # eBPF micro-witness support
└── __Tests/
└── StellaOps.Signals.Tests/ # Unit, property, integration, snapshot tests
Note: The doc previously listed
EvidenceWeightedScoreResult.cs,Guardrails/ScoreGuardrails.cs, andPolicy/EvidenceWeightPolicyProvider.csas separate files. Those do not exist. The result record (EvidenceWeightedScoreResult) and guardrail types live insideEvidenceWeightedScoreCalculator.cs; the policy provider interface isIEvidenceWeightPolicyProvider.cs.
Evidence-Weighted Score (scoring library)
Status: The EWS calculator remains a reusable in-process library, and the Unified Score facade is also registered by
AddScoreEvaluationServices()and exposed through the live/api/v1/score/*Signals API. The evidence dimensions below are populated from plain input DTOs (ReachabilityInput,RuntimeInput,BackportInput,ExploitInput,SourceTrustInput,MitigationInput) supplied by the caller — Signals does not hold direct project references to Policy, Concelier, Scanner, or Excititor (see Integration Points).
Evidence Dimensions
| Dimension | Symbol | Description | Input DTO |
|---|---|---|---|
| Reachability | RCH | Code path reachability to vulnerable sink | ReachabilityInput |
| Runtime | RTS | Live observation strength (eBPF/dyld/ETW) | RuntimeInput |
| Backport | BKP | Patch evidence from distro/changelog/binary | BackportInput |
| Exploit | XPL | Exploit probability (EPSS + KEV) | ExploitInput |
| Source Trust | SRC | VEX source trustworthiness | SourceTrustInput |
| Mitigations | MIT | Active protective controls | MitigationInput |
Scoring Formula
The calculator (EvidenceWeightedScoreCalculator) supports several formula modes selected by policy/input: the standard weighted-sum, an advisory formula (raw = 0.25*cvss + 0.30*epss + 0.20*reachability + 0.10*exploit_maturity - 0.15*patch_proof), an attested-reduction mode, and a VEX override (authoritative not_affected/fixed forces score 0). The standard weighted-sum is:
Score = clamp01(W_rch*RCH + W_rts*RTS + W_bkp*BKP + W_xpl*XPL + W_src*SRC - W_mit*MIT) * 100
Note: MIT is subtractive — mitigations reduce risk. The result is an EvidenceWeightedScoreResult carrying the score, bucket, per-dimension DimensionContribution values, applied AppliedGuardrails, badge flags, a canonical digest, and CalculatedAt.
Guardrails
Hard caps and floors based on evidence conditions:
- Not-Affected Cap: If vendor says not-affected (BKP=1) and no runtime contradiction (RTS<0.6), cap at 15
- Runtime Floor: If strong live signal (RTS>=0.8), floor at 60
- Speculative Cap: If no reachability and no runtime (RCH=0, RTS=0), cap at 45
Score Buckets
| Bucket | Range | Meaning |
|---|---|---|
| ActNow | 90-100 | Strong evidence of exploitable risk; immediate action |
| ScheduleNext | 70-89 | Likely real; schedule for next sprint |
| Investigate | 40-69 | Moderate evidence; investigate when touching component |
| Watchlist | 0-39 | Low/insufficient evidence; monitor |
Architecture
Component Diagram
┌─────────────────────────────────────────────────────────────────┐
│ Signals Module │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────┐ ┌──────────────────────────────────┐ │
│ │ NormalizerAggr. │───▶│ EvidenceWeightedScoreCalculator │ │
│ └────────┬────────┘ └──────────────┬───────────────────┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌─────────────────┐ ┌──────────────────────────────────┐ │
│ │ Normalizers │ │ Guardrails (AppliedGuardrails, │ │
│ │ ┌─────┐ ┌─────┐ │ │ in EvidenceWeightedScoreCalc.) │ │
│ │ │ BKP │ │ XPL │ │ └──────────────────────────────────┘ │
│ │ └─────┘ └─────┘ │ │
│ │ ┌─────┐ ┌─────┐ │ ┌──────────────────────────────────┐ │
│ │ │ MIT │ │ RCH │ │ │ IEvidenceWeightPolicyProvider │ │
│ │ └─────┘ └─────┘ │ │
│ │ ┌─────┐ ┌─────┐ │ │
│ │ │ RTS │ │ SRC │ │ │
│ │ └─────┘ └─────┘ │ │
│ └─────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
│ │
▼ ▼
┌─────────────────────┐ ┌───────────────────────────────────────┐
│ Caller-supplied │ │ Consumers │
│ input DTOs │ │ │
│ │ │ • In-process callers / tests │
│ • ReachabilityInput │ │ • Unified Score facade │
│ • RuntimeInput │ │ (UnifiedScore/UnifiedScoreService) │
│ • BackportInput │ │ │
│ • ExploitInput │ │ • Signals /api/v1/score API │
│ • SourceTrustInput │ │ │
│ • MitigationInput │ │ │
└─────────────────────┘ └───────────────────────────────────────┘
The diagram above describes the EWS scoring library only. The Signals service runtime is described under HTTP API Surface and Persistence.
NormalizerAggregatorand the calculator are registered as part of the score-evaluation service. They still take caller-supplied DTOs; this wiring does not create direct project references to evidence producers.
Data Flow (EWS library)
- Caller assembles an
EvidenceWeightedScoreInputfrom the per-dimension input DTOs. - Normalizers (
ReachabilityNormalizer,RuntimeSignalNormalizer, …) convert raw evidence to 0-1 values;NormalizerAggregatorcomposes them. - EvidenceWeightedScoreCalculator selects a formula mode and applies it against the active
EvidenceWeightPolicy. - Guardrails (caps/floors, encoded in the calculator +
AppliedGuardrails) adjust the score. - An
EvidenceWeightedScoreResultis returned with score, bucket, per-dimension contributions, applied guardrails, badge flags, and a canonical digest.
HTTP API Surface
Signals is a minimal-API host (Program.cs). Endpoints under /signals are authorized per-request via Program.TryAuthorize against the scopes below (Authority bearer token, or X-Scopes header when Signals:Authority:AllowAnonymousFallback=true in local/test harnesses). All /signals endpoints also run the sealed-mode gate (SignalsSealedModeMonitor) and return 503 when sealed-mode evidence is invalid. Several write endpoints emit audit events via .Audited(AuditModules.Signals, …).
Health / discovery (anonymous)
| Method | Path | Notes |
|---|---|---|
| GET | /healthz | Liveness. |
| GET | /readyz | Readiness; 503 sealed-mode-blocked when sealed-mode non-compliant, else startup-state gated. |
| GET | /openapi/v1.json | OpenAPI 3.1 document, exposed anonymously for the gateway aggregator. |
| GET | /api/v1/buildinfo | Image provenance via MapBuildInfoEndpoint (verify aggregator). |
Core /signals API
| Method | Path | Scope | Purpose |
|---|---|---|---|
| GET | /signals/ping | signals:read | Liveness probe (204). |
| GET | /signals/status | signals:read | Service version + sealed-mode status. |
| POST | /signals/callgraphs | signals:write | Ingest a call graph (CallgraphIngestRequest); returns 202 + callgraphId. Audited. |
| GET | /signals/callgraphs/{callgraphId} | signals:read | Fetch a stored callgraph document. |
| GET | /signals/callgraphs/{callgraphId}/manifest | signals:read | Stream the callgraph manifest blob. |
| POST | /signals/runtime-facts | signals:write | Ingest runtime facts (RuntimeFactsIngestRequest); returns 202 + subjectKey. Audited. |
| POST | /signals/runtime-facts/ndjson | signals:write | Stream runtime facts as NDJSON (gzip-aware); metadata via query/headers. Audited. |
| POST | /signals/runtime-facts/synthetic | signals:write | Local-harness only — deterministic synthetic probe fixtures. Not mapped in production. Audited. |
| GET | /signals/facts/{subjectKey} | signals:read | Fetch the reachability fact for a subject. |
| POST | /signals/reachability/union | signals:write | Ingest a reachability union graph (application/zip; X-Analysis-Id header). Audited. |
| GET | /signals/reachability/union/{analysisId}/meta | signals:read | Stream union meta.json. |
| GET | /signals/reachability/union/{analysisId}/files/{fileName} | signals:read | Stream a union graph file (json / ndjson). |
| POST | /signals/reachability/recompute | signals:admin | Recompute a reachability fact (ReachabilityRecomputeRequest). Audited. |
| POST | /signals/unknowns | signals:write | Ingest unknown symbols (UnknownsIngestRequest); returns 202 + subjectKey. Audited. |
| GET | /signals/unknowns | signals:read | Query unknowns (band, limit≤1000, offset); paged. |
| GET | /signals/unknowns/{subjectKey} | signals:read | Unknowns for a subject. |
| GET | /signals/unknowns/{id}/explain | signals:read | Score/band/normalization-trace explanation for one unknown. |
| POST | /signals/execution-evidence | signals:write | Build execution evidence (ExecutionEvidenceRequest); 202 / rate_limited / 422. Audited. |
| POST | /signals/beacons | signals:write | Ingest verification beacon events (BeaconIngestRequest). Audited. |
| GET | /signals/beacons/rate/{artifactId}/{environmentId} | signals:read | Beacon verification rate for an artifact/environment pair. |
Runtime observation ingest (agent)
| Method | Path | Auth | Purpose |
|---|---|---|---|
| POST | /api/v1/runtime/observations | Anonymous (network-edge trust) | Unified runtime-observation dual-write from the deployment-observation agent. Idempotently upserts signals.runtime_facts tagged with release_id. Validates tenantId/agentId are GUIDs and imageDigest is sha256:…. Returns 503 when durable PostgreSQL is not configured. Sprint 20260512_020 RO-RUN-003. |
Unlike the
/signals/*endpoints, this endpoint does not applyProgram.TryAuthorize— the deployment-observation agent authenticates at the network edge (mTLS / backend networking), not via the Authority OAuth flow, and the tenant id is carried in the request body.
SCM/CI webhooks (anonymous; signature-validated)
See SCM/CI Integration (Webhooks). Endpoints /webhooks/{github,gitlab,gitea} are mapped via MapScmWebhookEndpoints and run the sealed-mode gate; authorization is by provider signature, not Authority scopes.
Compatibility API (/api/v1/signals)
| Method | Path | Scope | Purpose |
|---|---|---|---|
| GET | /api/v1/signals | signals:read or orch:read | List signal records (type/status/provider filters, cursor paging). |
| GET | /api/v1/signals/stats | signals:read or orch:read | Aggregate counts by type/status/provider, success rate, avg latency. |
Truthfulness flag:
CompatibilityApiV1Endpointscurrently serves a hard-coded array of five static demo records (sig-001…sig-005), not data sourced from thesignalsschema. It exists so the gateway/orchestrator compatibility surface andorch:readconsumers resolve, but it is not a live query of stored signals. Treat this as a placeholder until backed by real persistence.
Unified score API (/api/v1/score)
MapScoreEndpoints() exposes evaluation, persisted score lookup/history, weight manifests, explanation, replay-envelope retrieval, and deterministic replay verification. The group is tenant-required. All routes require signals.score.read (score.read); POST /evaluate also requires signals.score.evaluate (score.evaluate). See Unified Score and the generated API reference.
Authorization scopes
Defined in StellaOps.Signals.Routing.SignalsPolicies:
| Scope | Constant | Used by |
|---|---|---|
signals:read | SignalsPolicies.Read | All GET / read endpoints. |
signals:write | SignalsPolicies.Write | Ingest endpoints (callgraphs, runtime-facts, unknowns, beacons, execution-evidence, reachability union). |
signals:admin | SignalsPolicies.Admin | POST /signals/reachability/recompute. |
score.read | ScorePolicies.ScoreRead (signals.score.read) | All /api/v1/score/* routes. |
score.evaluate | ScorePolicies.ScoreEvaluate (signals.score.evaluate) | Additional requirement for POST /api/v1/score/evaluate. |
Persistence (signals PostgreSQL schema)
Signals owns the signalsPostgreSQL schema (SignalsDataSource.DefaultSchemaName). Per repo policy [§2.7], it auto-migrates on startup: AddSignalsPersistence calls AddStartupMigrations("signals", "Signals.Persistence", typeof(SignalsDataSource).Assembly), and the migration SQL files are embedded resources (Migrations\**\*.sql, excluding Migrations\_archived\**). The connection string is resolved from Signals:Postgres:ConnectionString, Postgres:Signals:ConnectionString, or the equivalent Signals__… / Postgres__… environment variables (see ResolveSignalsPostgresConnectionString). When no connection string is present, durable repos are replaced with InMemory* implementations only in local-harness mode (otherwise startup throws).
Migrations
| File | Adds |
|---|---|
001_v1_signals_baseline.sql | Collapsed v1 baseline for callgraphs, reachability, runtime evidence/agents, deployment references, metrics, and unknowns. The pre-v1 source migrations remain archived history. |
002_v1_score_history.sql | Tenant-scoped score_history persistence and lookup indexes for the unified score API. |
Tables (by responsibility)
| Domain | Tables |
|---|---|
| Scan / artifact intake | scans, artifacts |
| Static call graph (relational) | cg_nodes, cg_edges, entrypoints, symbol_component_map, func_nodes, call_edges, cve_func_hits |
| Canonical callgraph documents | callgraphs |
| Reachability | reachability_components, reachability_findings, reachability_facts |
| Runtime evidence | runtime_samples, runtime_agents, runtime_facts, agent_heartbeats, agent_commands |
| Deployment / graph metrics | deploy_refs, graph_metrics |
| Unknown symbols | unknowns |
| Unified score | score_history |
Repository interfaces live under StellaOps.Signals/Persistence/ (with InMemory* doubles) and PostgreSQL implementations under StellaOps.Signals.Persistence/Postgres/Repositories/ (PostgresCallgraphRepository, PostgresReachabilityFactRepository, PostgresUnknownsRepository, PostgresReachabilityStoreRepository, PostgresDeploymentRefsRepository, PostgresGraphMetricsRepository, PostgresCallGraphQueryRepository, PostgresCallGraphProjectionRepository, PostgresRuntimeObservationRepository).
Artifact storage (blobs)
Callgraph and runtime-facts blobs are stored separately from the relational schema via a content-addressed artifact store selected by Signals:Storage:Driver:
filesystem(default) →FileSystemCallgraphArtifactStore/FileSystemRuntimeFactsArtifactStore(development-only; container filesystem is ephemeral).rustfs→RustFsCallgraphArtifactStore/RustFsRuntimeFactsArtifactStore(required in production — see Runtime-facts production storage).
Events
When a reachability fact is created/updated, Signals publishes an event built by ReachabilityFactEventBuilder (signals.fact.updated@v1 payload). The publisher is selected by Signals:Events:Driver:
redis→RedisEventsPublisherrouter→RouterEventsPublisher(HTTP to the Stella Router)inmemory→InMemoryEventsPublisher(local-harness only)- disabled (
Signals:Events:Enabled=false) →NullEventsPublisher
The default topic is signals.fact.updated.v1 (overridable via Signals:AirGap:EventTopic).
Background workers & scheduler
NightlyDecayWorker(hosted service) runs the unknowns decay pass (UnknownsDecayService, configured viaUnknownsDecayOptions).UnknownsRescanWorker/SchedulerRescanOrchestratorroute rescans of stale unknowns through the scheduler (StellaOps.Signals.Scheduler→SchedulerQueueJobClient); aNullSchedulerJobClientis the no-op fallback.
Production Runtime Boundary
Signals is fail-closed outside explicit local harness mode:
Signals:Postgres:ConnectionStringorPostgres:Signals:ConnectionStringis required. Non-durable repositories are available only when the host runs asTestingLocalHarness, or whenSignals:LocalHarness:Enabled=trueis set in Development/Testing.Signals:Authority:Enabled=trueis required, andSignals:Authority:AllowAnonymousFallback=falseis required. Header/anonymous fallback is only for explicit local and test harnesses. Production startup throws (viaStellaOps.Signals.Hosting.SignalsRuntimeConfigurationValidator, which implements the sharedIRuntimeConfigurationValidatorcontract introduced in Sprint 20260513_018 URCV-002) if either constraint is violated; the validator short-circuits only forDevelopment,Testing,TestingLocalHarness, or the explicitSignals:LocalHarness:Enabled=trueopt-in. Operators must declareSignals__Authority__Enabled=trueandSignals__Authority__AllowAnonymousFallback=falseexplicitly in production compose profiles.Signals:Cache:ConnectionStringmust be explicitly configured. Production startup must not silently use a local Redis/Valkey default such aslocalhost:6379.Signals:Events:Driver=inmemoryis local-harness-only. Production can useredis,router, or explicitly disable publishing withSignals:Events:Enabled=false, which binds the null publisher intentionally./signals/runtime-facts/syntheticis local-harness-only and is not mapped on production hosts. Synthetic probes are deterministic test fixtures, not verified runtime observations, and must not influence release policy decisions.- Runtime-facts batch ingestion is evidence-bearing. It requires a configured
ICryptoHashprovider and a runtime-facts artifact store before any subject is processed. Signals must fail closed instead of returning timestamp-derived hashes orcas://references when the raw batch was not actually persisted. The filesystem driver bindsFileSystemRuntimeFactsArtifactStore; the RustFS driver bindsRustFsRuntimeFactsArtifactStoreand persists runtime-facts artifacts underruntime-facts/{first2}/{blake3}/runtime-facts.ndjson[.gz]. RustFS save, read, existence, and delete operations verify the BLAKE3-256 digest instead of trusting object metadata.
Runtime-facts production storage (Sprint 20260502_013 G9-RFS)
Production hosts MUST use the RustFS-compatible object-store driver for runtime-facts artifacts. The local filesystem driver is a development-only escape hatch (the container filesystem is ephemeral, non-shared across replicas, and not durable across redeploys). SignalsRuntimeConfigurationValidator rejects the filesystem driver (options.Storage.IsFileSystemDriver(), i.e. Signals:Storage:Driver=filesystem) outside of Development, Testing, TestingLocalHarness, or the explicit Signals:LocalHarness:Enabled=true opt-in.
Truthfulness note: the validator’s error message string reads
"Signals artifact storage Provider=Filesystem is not permitted in Production. Set Signals:ArtifactStorage:Provider=RustFs and configure RustFs:Endpoint.". Those config keys (Signals:ArtifactStorage:Provider,RustFs:Endpoint) are stale in the message text — the options class actually bound isSignalsArtifactStorageOptions, whose keys areSignals:Storage:Driver(filesystem/rustfs) andSignals:Storage:RustFs:BaseUrl. Use the keys in the table below; the validator message is misleading and should be corrected in code.
The RustFS driver is implemented in RustFsRuntimeFactsArtifactStore. Required configuration keys:
| Key | Purpose |
|---|---|
Signals:Storage:Driver=rustfs | Selects the RustFS-backed binding. |
Signals:Storage:BucketName | Bucket containing the runtime-facts/... prefix (default signals-data). |
Signals:Storage:RootPrefix | Object-key prefix under the bucket for callgraph artifacts (default callgraphs). |
Signals:Storage:RuntimeFactsRootPrefix | Object-key prefix under the bucket for runtime-facts artifacts (default runtime-facts). |
Signals:Storage:RustFs:BaseUrl | Absolute HTTP(S) endpoint for the RustFS API (e.g. http://rustfs:9000/api/v1/). |
Signals:Storage:RustFs:ApiKey + :ApiKeyHeader | Authentication credentials applied to every request (default header X-API-Key). |
Signals:Storage:RustFs:Timeout | Per-request timeout (default 60s; bounded so 5xx propagates rather than hanging). |
Signals:Retention:RuntimeFactsTtlHours | Drives the per-object retention header (X-RustFS-Retain-Seconds). Default 720h. The store also tags every upload X-RustFS-Immutable=true and X-RustFS-Retain-Seconds=<TTL>. |
Operational guarantees:
- Digest-verified round-trip.
SaveAsyncre-reads the object after PUT and re-hashes the bytes; mismatched digests throwInvalidDataExceptionrather than returning a fake CAS URI. - Bounded failure propagation. RustFS 4xx/5xx responses surface as
InvalidOperationExceptioncarrying status code + reason; there is no silent fallback to filesystem. - Retention emits structured telemetry.
RuntimeFactsRetentionServicedeletes the underlying artifact by digest viaIRuntimeFactsArtifactStoreand on transient RustFS failures (e.g. 503) logs a warning carryingStoreType=rustfs,Operation=delete, andErrorKind=<exception>instead of swallowing the error.
Coverage lives in RustFsRuntimeFactsIntegrationTests (production-shape round-trip, fail-closed on misconfigured endpoint, fail-closed on 5xx) and RuntimeFactsRetentionRustFsTests (delete-by-digest + structured telemetry on 503). The shared object-store CAS pattern is mirrored in BinaryIndex Fingerprints; see docs/modules/binary-index/architecture.md.
EWS weight manifests
EWS weight policies are versioned JSON manifests loaded by FileBasedWeightManifestLoader from etc/weights/*.json (keyed by version, e.g. ews.v1). Versioning supports reproducibility — a computed score carries the policy digest. The default weights/guardrails (below) match EvidenceWeights.Default and BucketThresholds.Default / guardrail defaults in EvidenceWeightPolicy:
{
"version": "ews.v1",
"weights": { "rch": 0.30, "rts": 0.25, "bkp": 0.15, "xpl": 0.15, "src": 0.10, "mit": 0.10 },
"guardrails": {
"not_affected_cap": { "enabled": true, "max_score": 15 },
"runtime_floor": { "enabled": true, "min_score": 60 },
"speculative_cap": { "enabled": true, "max_score": 45 }
}
}
Do not confuse these EWS weight manifests with the
Signals:Scoringconfig section inetc/signals.yaml. The latter configures the reachability confidence scorer (ReachableConfidence,UnreachableConfidence,RuntimeBonus,MaxConfidence,MinConfidence) used byReachabilityScoringService, not the EWS dimension weights.
Service configuration (etc/signals.yaml)
Top-level sections under Signals: (see SignalsOptions and etc/signals.yaml.sample):
| Section | Purpose |
|---|---|
Authority | OIDC resource-server config (Enabled, Issuer, Audiences, RequiredScopes, AllowAnonymousFallback, BypassNetworks, …). |
Postgres | Durable persistence connection string (resolved alongside Postgres:Signals:ConnectionString). |
Storage | Artifact-store driver (filesystem/rustfs), RootPath, BucketName, RuntimeFactsRootPrefix, RustFs:*. |
Scoring | Reachability confidence parameters (see above). |
Cache | Redis/Valkey reachability-fact cache (ConnectionString, DefaultTtlSeconds). |
Events | Event publishing (Enabled, Driver = redis/router/inmemory, Router:*). |
AirGap | Air-gap event topic override. |
OpenApi, Retention, Scm | OpenAPI doc, runtime-facts retention TTL, SCM webhook options. |
Integration Points
Truthfulness note:
StellaOps.Signals.csprojholds no project references to Policy, Concelier, Scanner, or Excititor. The previous “Direct type reference” inbound table was inaccurate — the EWS calculator consumes plain caller-supplied input DTOs, and the service ingests evidence over HTTP. The integration model below reflects the actual build graph and wire surface.
Inbound (how evidence reaches Signals)
| Producer | Data | Mechanism |
|---|---|---|
| Scanner / reachability tooling | Call graphs, reachability union graphs | POST /signals/callgraphs, POST /signals/reachability/union |
Runtime agent (StellaOps.Signals.RuntimeAgent) | Observed symbol executions | POST /signals/runtime-facts / …/ndjson |
| Deployment-observation agent | Runtime observations (per release) | POST /api/v1/runtime/observations (anonymous, network-edge auth) |
| Unknown-symbol producers | Unknown symbols | POST /signals/unknowns |
| CI/CD evidence | Execution evidence, verification beacons | POST /signals/execution-evidence, POST /signals/beacons |
| SCM/CI providers | Push/PR/release/pipeline events | POST /webhooks/{github,gitlab,gitea} |
| EWS library callers (in-process) | Per-dimension *Input DTOs | IEvidenceWeightedScoreCalculator.Calculate(...) |
Direct project references: StellaOps.Auth.* (Authority), StellaOps.Messaging / StellaOps.Router.Transport.Messaging (events/router), StellaOps.Infrastructure.Postgres (migrations), StellaOps.Audit.Emission, StellaOps.Cryptography, StellaOps.Canonical.Json, and the local StellaOps.Signals.Persistence / StellaOps.Signals.RuntimeAgent libraries.
Outbound (Consumers)
| Consumer | Usage |
|---|---|
| Stella Router / event subscribers | signals.fact.updated.v1 reachability-fact-change events (redis or router driver) |
Scheduler (StellaOps.Signals.Scheduler) | Unknowns rescan jobs via SchedulerQueueJobClient |
| Gateway / orchestrator compatibility | /api/v1/signals + /api/v1/signals/stats (currently static demo data) |
| Backend Router / score clients | Tenant-scoped /api/v1/score/* evaluation, lookup, history, explain, weights, replay, and verify. |
| Audit pipeline | Audit events on ingest/recompute via StellaOps.Audit.Emission |
Determinism
Signals maintains strict determinism in its scoring and evidence digests:
- Same inputs + same policy = same score: No randomness, no time-dependent factors in the EWS formula.
AddDeterminismDefaults()is wired inProgram.cs, and time-dependent values are explicitly annotated (// Determinism:Allowed). - Policy versioning: Each weight manifest is versioned; the
EvidenceWeightedScoreResultcarries a canonical digest (WithComputedDigest()). - Reproducible replay:
UnifiedScore/Replay/IReplayVerifierre-derives scores for verification. - Canonical fact digests:
ReachabilityFactDigestCalculatorproduces stable digests over reachability facts; runtime-facts artifacts are content-addressed (BLAKE3-256).
The performance targets that previously appeared here (single calc <10ms, batch <1s, cache hit >80%, etc.) were aspirational and not codified in a benchmark in this module; they have been removed pending a real perf harness.
Testing Strategy
Tests live in the single project __Tests/StellaOps.Signals.Tests/, grouped by feature subdirectory (not by Properties/Integration/Snapshots folders as earlier drafts claimed):
| Focus | Location (examples) |
|---|---|
| EWS calculator, normalizers, determinism, property tests | EvidenceWeightedScore/ (EvidenceWeightedScoreCalculatorTests, …PropertyTests, …DeterminismTests, Normalizers/) |
| Unified score + replay | UnifiedScore/ |
| Callgraph / reachability ingestion & scoring | CallgraphIngestionServiceTests, ReachabilityScoringServiceTests, ReachabilityUnionIngestionServiceTests, ReachabilityLatticeTests |
| Runtime facts (incl. batch, provenance, stack frames) | RuntimeFactsIngestionServiceTests, RuntimeFactsBatchIngestionTests, RuntimeFactsProvenanceNormalizerTests, RustFsRuntimeFactsArtifactStoreTests |
| Unknowns scoring & decay | UnknownsScoringServiceTests, UnknownsDecayServiceTests, UnknownsScoringIntegrationTests |
| Host / config / durability | SignalsDurableHostTests, SignalsRuntimeConfigurationValidationTests, ProgramCompatibilityTests |
| SCM webhooks | Scm/ |
Related Documentation
- Companion:
unified-score.md,api-reference.md,README.md - eBPF determinism contract:
contracts/ebpf-micro-witness-determinism-profile.md - Runtime configuration validation:
../../architecture/runtime-configuration-validation.md - Forward-only migration policy: ADR-004
- BinaryIndex CAS pattern (shared object-store shape):
../binary-index/architecture.md
Earlier drafts linked a
24-Dec-2025 Evidence-Weighted Score Modeladvisory,SPRINT_8200_0012_*plans, and per-module pages (policy/confidence-scoring.md,concelier/backport-detection.md,scanner/epss-enrichment.md,excititor/trust-vector.md). Those links were not verified to exist at the stated paths and the cross-module “direct integration” they implied is not present in the build graph; they have been dropped in favour of in-module companions above.
SCM/CI Integration (Webhooks)
The Signals module also handles webhook ingestion from SCM (Source Code Management) and CI (Continuous Integration) providers. This enables:
- Triggering scans on push/PR/release events
- SBOM uploads from CI pipelines
- Image push detection and automated scanning
Location
src/Signals/StellaOps.Signals/Scm/
├── Models/
│ ├── NormalizedScmEvent.cs # Provider-agnostic event payload
│ ├── ScmEventType.cs # Event type enumeration
│ └── ScmProvider.cs # Provider enumeration
├── Webhooks/
│ ├── IWebhookSignatureValidator.cs
│ ├── GitHubWebhookValidator.cs # HMAC-SHA256 validation
│ ├── GitLabWebhookValidator.cs # Token-based validation
│ ├── GiteaWebhookValidator.cs # HMAC-SHA256 validation
│ ├── IScmEventMapper.cs
│ ├── GitHubEventMapper.cs # GitHub -> NormalizedScmEvent
│ ├── GitLabEventMapper.cs # GitLab -> NormalizedScmEvent
│ └── GiteaEventMapper.cs # Gitea -> NormalizedScmEvent
├── Services/
│ ├── IScmWebhookService.cs
│ ├── ScmWebhookService.cs # Orchestrates validation + mapping
│ ├── IScmTriggerService.cs
│ └── ScmTriggerService.cs # Routes events to Scanner/Orchestrator
└── ScmWebhookEndpoints.cs # Minimal API webhook endpoints
Supported Providers
| Provider | Webhook Endpoint | Signature Header | Validation |
|---|---|---|---|
| GitHub | /webhooks/github | X-Hub-Signature-256 | HMAC-SHA256 |
| GitLab | /webhooks/gitlab | X-Gitlab-Token | Token match |
| Gitea | /webhooks/gitea | X-Gitea-Signature | HMAC-SHA256 |
Event Types
| Event Type | Description | Triggers |
|---|---|---|
Push | Code push to branch | Scan (main/release branches) |
PullRequestOpened | PR opened | — |
PullRequestMerged | PR merged | Scan |
ReleasePublished | Release created | Scan |
ImagePushed | Container image pushed | Scan |
PipelineSucceeded | CI pipeline completed | Scan |
SbomUploaded | SBOM artifact uploaded | SBOM ingestion |
Webhook Payload Normalization
All provider-specific payloads are normalized to NormalizedScmEvent:
public sealed record NormalizedScmEvent
{
public required string EventId { get; init; }
public ScmProvider Provider { get; init; }
public ScmEventType EventType { get; init; }
public DateTimeOffset Timestamp { get; init; }
public required ScmRepository Repository { get; init; }
public ScmActor? Actor { get; init; }
public string? Ref { get; init; }
public string? CommitSha { get; init; }
public ScmPullRequest? PullRequest { get; init; }
public ScmRelease? Release { get; init; }
public ScmPipeline? Pipeline { get; init; }
public ScmArtifact? Artifact { get; init; }
public IReadOnlyDictionary<string, object?>? RawMetadata { get; init; }
public string? TenantId { get; init; }
public string? IntegrationId { get; init; }
}
The full
ScmEventTypeenum is broader than the trigger table above — it also includesPullRequest,PullRequestClosed,TagCreated,RefCreated,RefDeleted,PipelineStarted,PipelineCompleted,PipelineFailed, andArtifactPublished. Only the subset listed in the table currently triggers downstream work.
Trigger Routing
ScmTriggerService.ShouldTriggerScan triggers a scan for: Push to a main/release branch (main, release/*, release-*), PullRequestMerged, ReleasePublished, ImagePushed, and PipelineSucceeded. SBOM ingestion is triggered for SbomUploaded (and artifact events carrying SBOM content).
Security
- Signature verification: All webhooks validate the provider signature before processing (GitHub/Gitea HMAC-SHA256, GitLab token match); missing secret or invalid signature is rejected with a warning log.
- Sealed-mode gate: Webhook handlers run the sealed-mode check and return 503 when sealed-mode evidence is invalid.
- Delivery logging: Each delivery is logged with provider, event type, and delivery ID (structured
ILoggerscope), and the result is returned to the caller.
Webhook secrets are resolved from Signals:Scm options — IntegrationSecrets[integrationId], then ProviderSecrets[provider], then DefaultSecret. When no secret is configured, the request is rejected unless Signals:Scm:AllowUnsignedWebhooks=true.
Truthfulness note: the previous draft claimed AuthRef-managed secrets and built-in rate limiting for webhooks. Neither is implemented in
src/Signals/StellaOps.Signals/Scm/as of 2026-05-29 — secrets come fromSignals:Scmconfig (above), and there is no rate limiter in the SCM webhook path. These claims have been removed pending implementation.
