Unknowns Architecture

Scope. Architecture for the Unknowns microservice: a structured registry that tracks unresolved components, symbols, and mappings that Scanner and other analyzers cannot definitively identify. This dossier is for implementers and operators of the Unknowns service and for analyzer authors that feed it.

Source-reconciliation note (2026-05-30). Reconciled directly against src/Unknowns/ and the canonical scope catalog src/Authority/StellaOps.Authority/StellaOps.Auth.Abstractions/StellaOpsScopes.cs. The Unknown registry (/api/unknowns/*) is fully wired and persisted. The Grey Queue surface (/api/grey-queue/*) and the SLA / metrics / lifecycle background services are scaffolded in source but NOT activated at runtime — see §6 Implementation status for the exact gaps.


0) Mission & boundaries

Mission. Provide a structured registry for tracking unknown components, unresolved symbols, and incomplete mappings. Enable visibility into coverage gaps and guide future enhancement priorities.

Boundaries.

Container facts (from src/Unknowns/README.md). Container stellaops-unknowns-web; slot 46; port 8080; Router consumer group unknowns; resource tier light. Storage: PostgreSQL via ConnectionStrings:Default / ConnectionStrings:UnknownsDb (configured through the Postgres options section — see ServiceCollectionExtensions.AddUnknownsServices).

Boundary decision (Sprint 206, 2026-02-25): Unknowns retains its own schema ownership. No source consolidation into Policy and no DbContext merge. See docs-archive/implplan/2026-03-04-completed-sprints/SPRINT_20260225_206_Policy_absorb_unknowns.md (archived) for rationale.


1) Solution & project layout

Actual layout under src/Unknowns/ (verified 2026-05-30):

src/Unknowns/
 ├─ StellaOps.Unknowns.WebService/          # Standalone Minimal API host (Program.cs)
 │   ├─ Endpoints/
 │   │   ├─ UnknownsEndpoints.cs             # /api/unknowns  (registry; WIRED)
 │   │   └─ GreyQueueEndpoints.cs            # /api/grey-queue (scaffolded; NOT wired — see §6)
 │   ├─ Security/UnknownsPolicies.cs         # Unknowns.Read / Unknowns.Write policy names
 │   ├─ Translations/en-US.unknowns.json     # localized endpoint descriptions
 │   └─ ServiceCollectionExtensions.cs       # AddUnknownsServices + DatabaseHealthCheck
 ├─ StellaOps.Unknowns.Services/             # SLA / metrics / lifecycle / watchdog services
 │   │                                       #   (scaffolded; NOT registered in the host — see §6)
 │   ├─ UnknownsLifecycleService.cs
 │   ├─ UnknownsMetricsService.cs
 │   ├─ UnknownsSlaMonitorService.cs
 │   ├─ GreyQueueWatchdogService.cs
 │   ├─ SlaCalculator.cs / UnknownsSlaHealthCheck.cs
 │   └─ Abstractions.cs
 ├─ __Libraries/
 │   ├─ StellaOps.Unknowns.Core/             # Models, enums, provenance hints, analysis services
 │   │   ├─ Models/                          # Unknown, GreyQueueEntry, ProvenanceHint(+Type/Evidence), UnknownRanking, NativeUnknownContext
 │   │   ├─ Hints/                           # IProvenanceHintBuilder, ProvenanceHintBuilder
 │   │   ├─ Services/                        # NativeUnknownClassifier, RuntimeSignalIngester, UnknownProofEmitter, UnknownRanker
 │   │   ├─ Repositories/                    # IUnknownRepository, IGreyQueueRepository
 │   │   └─ Schemas/provenance-hint.schema.json
 │   ├─ StellaOps.Unknowns.Persistence/      # ACTIVE persistence
 │   │   ├─ Postgres/                         # UnknownsDataSource, PostgresUnknownRepository (runtime path used by the host)
 │   │   ├─ EfCore/                           # UnknownsDbContext, UnknownEntity, UnknownEfRepository, CompiledModels
 │   │   ├─ Extensions/UnknownsPersistenceExtensions.cs  # EF Core registration alternative (not used by host)
 │   │   └─ Migrations/                       # 001_v1_unknowns_baseline.sql (collapsed pre-1.0 chain) (+_archived/, excluded from embedding)
 │   └─ StellaOps.Unknowns.Persistence.EfCore/  # DEPRECATED scaffold — referenced by NO csproj (dead; do not use)
 │
 └─ __Tests/
     ├─ StellaOps.Unknowns.Core.Tests/
     ├─ StellaOps.Unknowns.Persistence.Tests/   # PostgresUnknownRepositoryTests
     └─ StellaOps.Unknowns.WebService.Tests/    # endpoint, persistence, startup-migration, health tests

Two persistence trees, one active. The host (ServiceCollectionExtensions.AddUnknownsServices) registers IUnknownRepository → PostgresUnknownRepository backed by UnknownsDataSource (raw Npgsql). The EF Core path (UnknownsPersistenceExtensions.AddUnknownsPersistence / …WithCompiledModel, UnknownsDbContext, UnknownEfRepository) lives in the same project but is not registered by the WebService. The separate top-level StellaOps.Unknowns.Persistence.EfCore project is a deprecated scaffold referenced by no .csproj.


2) HTTP API surface

Two endpoint groups are mapped in Program.cs (MapUnknownsEndpoints() and MapGreyQueueEndpoints()). Both route groups call .RequireTenant() and resolve the tenant exclusively from the envelope-bound stellaops:tenant claim via HttpContext.RequireTenant() (a missing tenant is rejected with 400 tenant_missing); the legacy X-Tenant-Id request header is never honoured as a tenancy source (SPRINT_20260611_001, UNKNOWNS-1). Every endpoint also requires an authorization policy. Health endpoints (/healthz, /readyz, /health) are anonymous.

2.1 Unknowns registry — /api/unknowns (WIRED)

Group default policy: Unknowns.Read (scope unknowns:read). All routes below are GET.

RouteNameDescription
GET /api/unknowns/ListUnknownsPaginated list (skip, take=50; optional asOf bitemporal, kind, severity filters)
GET /api/unknowns/{id:guid}GetUnknownByIdSingle unknown with hints; 404 if absent
GET /api/unknowns/{id:guid}/hintsGetUnknownHintsProvenance hints only (ProvenanceHintsResponse)
GET /api/unknowns/{id:guid}/historyGetUnknownHistoryBitemporal history (optional from/to; currently returns current state as a single entry)
GET /api/unknowns/triage/{band}GetUnknownsByTriageBandBy triage band (limit=50, offset=0)
GET /api/unknowns/hot-queueGetHotQueueHOT unknowns for immediate processing (limit=50)
GET /api/unknowns/high-confidenceGetHighConfidenceHintsUnknowns whose hints meet minConfidence (default 0.7, limit=50)
GET /api/unknowns/summaryGetUnknownsSummaryCounts by kind / severity / triage band + total open

The list endpoint is repository-driven: asOfAsOfAsync; else kindGetByKindAsync; else severityGetBySeverityAsync; else GetOpenUnknownsAsync + CountOpenAsync (see UnknownsEndpoints.ListUnknowns).

2.2 Grey Queue — /api/grey-queue (forward design; returns 501 until a store is wired — see §6)

Group default policy: Unknowns.Read. Mutating routes additionally require Unknowns.Write and emit audit events (.Audited(AuditModules.Unknowns, …)).

Runtime gate (BL-4, SPRINT_20260713_002). MapGreyQueueEndpoints checks at startup whether an IGreyQueueRepository is registered (IServiceProviderIsService). The live host wires none, so the full table below is not mapped; instead the entire /api/grey-queue/* surface answers 501 Not Implemented (title: grey_queue_not_implemented, application/problem+json, anonymous) — a truthful “not yet built” signal rather than the former 500 from an unresolved DI dependency. The real handlers below map automatically the moment a repository is registered (which the WebService test host does, exercising them). Routes are kept mapped so integrators see an unbuilt-but-planned surface.

RouteVerbPolicyAudit action
/api/grey-queue/GETRead
/api/grey-queue/{id:guid}GETRead
/api/grey-queue/by-unknown/{unknownId:guid}GETRead
/api/grey-queue/readyGETRead
/api/grey-queue/triggers/feed/{feedId}GETRead
/api/grey-queue/triggers/tool/{toolId}GETRead
/api/grey-queue/triggers/cve/{cveId}GETRead
/api/grey-queue/summaryGETRead
/api/grey-queue/{id:guid}/transitionsGETRead
/api/grey-queue/POSTWriteenqueue
/api/grey-queue/{id:guid}/processPOSTWritestart_processing
/api/grey-queue/{id:guid}/resultPOSTWriterecord_result
/api/grey-queue/{id:guid}/resolvePOSTWriteresolve
/api/grey-queue/{id:guid}/dismissPOSTWritedismiss
/api/grey-queue/expirePOSTWriteexpire
/api/grey-queue/{id:guid}/assignPOSTWriteassign
/api/grey-queue/{id:guid}/escalatePOSTWriteescalate
/api/grey-queue/{id:guid}/rejectPOSTWritereject
/api/grey-queue/{id:guid}/reopenPOSTWritereopen

Audit action constants are defined in StellaOps.Audit.Emission (AuditModules.Unknowns = "unknowns", AuditActions.Unknowns.*).

2.3 Authorization scopes

Defined in the canonical catalog StellaOps.Auth.Abstractions/StellaOpsScopes.cs:

ScopeConstantUsed by
unknowns:readStellaOpsScopes.UnknownsReadUnknowns.Read policy (all GET routes)
unknowns:writeStellaOpsScopes.UnknownsWriteUnknowns.Write policy (grey-queue mutations)
unknowns:adminStellaOpsScopes.UnknownsAdminDefined in the catalog; not referenced by any Unknowns endpoint policy (no Unknowns.Admin policy is registered in Program.cs)

Policies are wired via AddStellaOpsScopePolicy in Program.cs; policy name constants live in Security/UnknownsPolicies.cs (Unknowns.Read, Unknowns.Write).

2.4 Health

/healthz (liveness, Predicate=false → ignores DB), /readyz (readiness, only ready-tagged DatabaseHealthCheck), and /health (legacy) — all anonymous (Sprint 20260512_023, UNKNOWNS_HEALTH-01).


3) Contracts & data model

3.1 Unknown DTO (UnknownDto, from UnknownsEndpoints.cs)

Field names below match the serialized record exactly:

{
  "id": "0f3b…-uuid",
  "tenantId": "default",
  "subjectHash": "<sha256-hex>",
  "subjectType": "Binary",
  "subjectRef": "/usr/lib/libfoo.so",
  "kind": "UnknownBuildId",
  "severity": "High",
  "sourceScanId": "…",
  "sourceGraphId": "…",
  "sourceSbomDigest": "sha256:…",
  "validFrom": "2025-01-15T10:30:00Z",
  "validTo": null,
  "resolvedAt": null,
  "resolutionType": null,
  "resolutionRef": null,
  "compositeScore": 0.72,
  "triageBand": "Hot",
  "isOpen": true,
  "isResolved": false,
  "provenanceHints": [ /* ProvenanceHintDto[] */ ],
  "bestHypothesis": "Binary matches openssl 1.1.1k from debian",
  "combinedConfidence": 0.95,
  "primarySuggestedAction": "verify_build_id",
  "createdAt": "2025-01-15T10:30:00Z",
  "updatedAt": "2025-01-15T10:30:00Z"
}

The domain record Unknown (in Core/Models/Unknown.cs) additionally carries bitemporal sysFrom/sysTo, full scoring inputs (popularityScore, deploymentCount, exploitPotentialScore, uncertaintyScore/uncertaintyFlags, centralityScore/degreeCentrality/betweennessCentrality, stalenessScore/daysSinceAnalysis), rescan scheduling (rescanAttempts, nextScheduledRescan, lastAnalyzedAt), and evidence hashes (evidenceSetHash, graphSliceHash) — these are persisted but not all surfaced on the DTO.

3.2 UnknownKind (classification)

Core/Models/Unknown.cs defines 15 kinds. The first 10 are also declared in the unknowns.unknown_kind PostgreSQL enum (migration 001); the 5 native-binary kinds (Sprint 3500-0013-0001) exist in C# but are not in the DB enum:

KindIn DB enum?
MissingSbomyes (missing_sbom)
AmbiguousPackageyes (ambiguous_package)
MissingFeedyes (missing_feed)
UnresolvedEdgeyes (unresolved_edge)
NoVersionInfoyes (no_version_info)
UnknownEcosystemyes (unknown_ecosystem)
PartialMatchyes (partial_match)
VersionRangeUnboundedyes (version_range_unbounded)
UnsupportedFormatyes (unsupported_format)
TransitiveGapyes (transitive_gap)
MissingBuildIdno (C# only)
UnknownBuildIdno (C# only)
UnresolvedNativeLibraryno (C# only)
HeuristicDependencyno (C# only)
UnsupportedBinaryFormatno (C# only)

Other enums: UnknownSubjectType (Package, Ecosystem, Version, SbomEdge, File, Runtime, Binary — note the DB subject_type enum omits Binary), UnknownSeverity (Critical/High/Medium/Low/Info), ResolutionType (FeedUpdated, SbomProvided, ManualMapping, Superseded, FalsePositive, WontFix), TriageBand (Hot/Warm/Cold).

3.3 Triage scoring (HOT / WARM / COLD)

Composite score and band assignment are computed both in PL/pgSQL helpers (unknowns.calc_composite_score, unknowns.assign_band) and reflected in C#:

3.4 Provenance hints

ProvenanceHint (Core/Models/ProvenanceHint.cs) is an immutable record with snake_case JSON: hint_id (content-addressed, format hint:sha256:<hex24>), type, confidence (0–1), confidence_level, summary, hypothesis, evidence, suggested_actions, generated_at, source.

Hint types (ProvenanceHintType, exactly 15): BuildIdMatch, DebugLink, ImportTableFingerprint, ExportTableFingerprint, SectionLayout, StringTableSignature, CompilerSignature, PackageMetadata, DistroPattern, VersionString, SymbolPattern, PathPattern, CorpusMatch, SbomCrossReference, AdvisoryCrossReference.

Confidence levels (HintConfidence): VeryHigh (≥0.9), High (0.7–0.9), Medium (0.5–0.7), Low (0.3–0.5), VeryLow (<0.3).

Suggested action (SuggestedAction): action, priority (1 = highest), effort (low/medium/high), description, optional link. The endpoint DTO (SuggestedActionDto) exposes action, priority, description, and url (mapped from link).

Typed evidence (ProvenanceEvidence): at most one of build_id, debug_link, import_fingerprint, export_fingerprint, section_layout, compiler, distro_pattern, version_string, corpus_match, plus a raw JSON escape hatch.

JSON Schema: src/Unknowns/__Libraries/StellaOps.Unknowns.Core/Schemas/provenance-hint.schema.json.

The hint-combination confidence boost (single → two-agreeing → three-agreeing) is implemented in Core/Hints/ProvenanceHintBuilder.cs / Core/Services/UnknownRanker.cs. Exact numeric examples are not re-stated here to avoid drift from the builder; consult those classes and __Tests/.../Hints/HintCombinationTests.cs for the authoritative formula.

3.5 Grey Queue model (scaffolded)

GreyQueueEntry (Core/Models/GreyQueueEntry.cs) carries a DSSE-style evidence bundle (SBOM slice, advisory snippet, VEX evidence, diff traces, reachability evidence — each with an integrity hash), a deterministic fingerprint, trigger conditions (feeds/tools/CVEs/PURL patterns), and processing metadata (processingAttempts, maxAttempts=10, retry scheduling).

State machine (GreyQueueStateMachine): statuses Pending, Processing, Retrying, UnderReview, Escalated, Resolved, Rejected, Failed, Expired, Dismissed. Resolved/Expired are terminal; Rejected/Failed/Dismissed can return to Pending; UnderReview requires an assignee.

GreyQueueReason (10): InsufficientVex, ConflictingVex, MissingReachability, AmbiguousIdentity, FeedNotAvailable, ToolUnsupported, BinaryAnalysisInconclusive, BackportUncertain, MultipleInterpretations, AwaitingUpstreamAdvisory.

GreyQueueResolution (7): FeedUpdate, ToolUpdate, VexReceived, ManualResolution, Superseded, NotApplicable, Expired.


4) Persistence

The owning schema is unknowns(plus the unknowns_app helper schema). Startup convergence is mandatory: ServiceCollectionExtensions.AddUnknownsServices (the host path) and UnknownsPersistenceExtensions (the EF path) both call AddStartupMigrations(UnknownsDataSource.DefaultSchemaName, "Unknowns.Persistence", typeof(UnknownsDataSource).Assembly), and the persistence project embeds Migrations/**/*.sql (explicitly excluding Migrations/_archived/** per Sprint 20260512_026 to avoid leaf-name collisions in the migration runner).

Migrations (active): a single embedded file, 001_v1_unknowns_baseline.sql — the pre-1.0 chain (001_initial_schema.sql, 002_provenance_hints.sql, 003_unknowns_scoring_drift_repair.sql) was collapsed into it. Those three filenames now exist only under Migrations/_archived/pre_1.0/mig061/ and are excluded from embedding; cite the baseline, not the archived names. Folded content, in preserved source order:

There is no grey_queue table. No active migration creates unknowns.grey_queue (or any grey-queue table). The only table created is unknowns.unknown. See §6.

Tenant isolation relies on the app.tenant_id session GUC, enforced by RLS via unknowns_app.require_current_tenant().


5) Observability

Metrics and tracing are defined in StellaOps.Unknowns.Services/UnknownsMetricsService.cs (Meter and ActivitySource both named StellaOps.Unknowns, version 1.0.0):

These meters live in UnknownsMetricsService, which is not registered in the running host (see §6); they emit only if/when that background service is wired.


6) Implementation status (what is and isn’t wired at runtime)

This section is the load-bearing correction versus prior revisions of this dossier.

WIRED (functional in the running stellaops-unknowns-web):

SCAFFOLDED but NOT wired (will not function at runtime as-is):

Follow-up flag for maintainers. Option (b) — gate the grey-queue mapping so it fails honestly until backed — is done (BL-4, SPRINT_20260713_002): the surface now returns 501 Not Implemented rather than 500. The remaining forward work is option (a): register a concrete IGreyQueueRepository implementation + add a grey_queue migration + register the background services, at which point the gate lights up the real API with no further mapping changes.



Advisory Gap Status (2026-03-04 Batch)

Status: provenance-hints persistence delivered in Sprint 304.

Closure sprint (archived):