This commit is contained in:
StellaOps Bot
2025-12-13 02:22:15 +02:00
parent 564df71bfb
commit 999e26a48e
395 changed files with 25045 additions and 2224 deletions

3
.gitattributes vendored
View File

@@ -1,2 +1,5 @@
# Ensure analyzer fixture assets keep LF endings for deterministic hashes
src/Scanner/__Tests/StellaOps.Scanner.Analyzers.Lang.Python.Tests/Fixtures/** text eol=lf
# Ensure reachability sample assets keep LF endings for deterministic hashes
tests/reachability/samples-public/** text eol=lf

View File

@@ -0,0 +1,87 @@
# Reachability Test Datasets
This directory contains ground truth samples for validating reachability analysis accuracy.
## Directory Structure
```
datasets/reachability/
├── README.md # This file
├── samples/ # Test samples by language
│ ├── csharp/
│ │ ├── simple-reachable/ # Positive: direct call path
│ │ └── dead-code/ # Negative: unreachable code
│ ├── java/
│ │ └── vulnerable-log4j/ # Positive: Log4Shell CVE
│ └── native/
│ └── stripped-elf/ # Positive: stripped binary
└── schema/
├── manifest.schema.json # Sample manifest schema
└── ground-truth.schema.json # Ground truth schema
```
## Sample Categories
### Positive (Reachable)
Samples where vulnerable code has a confirmed path from entry points:
- `csharp/simple-reachable` - Direct call to vulnerable API
- `java/vulnerable-log4j` - Log4Shell with runtime confirmation
- `native/stripped-elf` - Stripped ELF with heuristic analysis
### Negative (Unreachable)
Samples where vulnerable code exists but is never called:
- `csharp/dead-code` - Deprecated API replaced by safe implementation
## Schema Reference
### manifest.json
Sample metadata including:
- `sampleId` - Unique identifier
- `language` - Primary language (java, csharp, native, etc.)
- `category` - positive, negative, or contested
- `vulnerabilities` - CVEs and affected symbols
- `artifacts` - Binary/SBOM file references
### ground-truth.json
Expected outcomes including:
- `targets` - Symbols with expected lattice states
- `entryPoints` - Program entry points
- `expectedUncertainty` - Expected uncertainty tier
- `expectedGateDecisions` - Expected policy gate outcomes
## Lattice States
| Code | Name | Description |
|------|------|-------------|
| U | Unknown | No analysis performed |
| SR | StaticallyReachable | Static analysis finds path |
| SU | StaticallyUnreachable | Static analysis finds no path |
| RO | RuntimeObserved | Runtime probe observed execution |
| RU | RuntimeUnobserved | Runtime probe did not observe |
| CR | ConfirmedReachable | Both static and runtime confirm |
| CU | ConfirmedUnreachable | Both static and runtime confirm unreachable |
| X | Contested | Static and runtime evidence conflict |
## Running Tests
```bash
# Validate schemas
npx ajv validate -s schema/ground-truth.schema.json -d samples/**/ground-truth.json
# Run benchmark tests
dotnet test --filter "GroundTruth" src/Scanner/__Tests/StellaOps.Scanner.Reachability.Benchmarks/
```
## Adding New Samples
1. Create directory: `samples/{language}/{sample-name}/`
2. Add `manifest.json` with sample metadata
3. Add `ground-truth.json` with expected outcomes
4. Include `reasoning` for each target explaining the expected state
5. Validate against schema before committing
## Related Documentation
- [Ground Truth Schema](../../docs/reachability/ground-truth-schema.md)
- [Lattice Model](../../docs/reachability/lattice.md)
- [Policy Gates](../../docs/reachability/policy-gate.md)

View File

@@ -0,0 +1,86 @@
{
"schema": "ground-truth-v1",
"sampleId": "sample:csharp:dead-code:001",
"generatedAt": "2025-12-13T12:00:00Z",
"generator": {
"name": "manual-annotation",
"version": "1.0.0",
"annotator": "scanner-guild"
},
"targets": [
{
"symbolId": "sym:csharp:JsonConvert.DeserializeObject",
"display": "Newtonsoft.Json.JsonConvert.DeserializeObject<T>(string, JsonSerializerSettings)",
"purl": "pkg:nuget/Newtonsoft.Json@13.0.1",
"expected": {
"latticeState": "CU",
"bucket": "unreachable",
"reachable": false,
"confidence": 0.95,
"pathLength": null,
"path": null
},
"reasoning": "DeserializeObject referenced in deprecated LegacyParser class but LegacyParser is never instantiated - new SafeParser uses System.Text.Json instead"
},
{
"symbolId": "sym:csharp:LegacyParser.ParseJson",
"display": "SampleApp.LegacyParser.ParseJson(string)",
"purl": "pkg:generic/SampleApp@1.0.0",
"expected": {
"latticeState": "SU",
"bucket": "unreachable",
"reachable": false,
"confidence": 0.90,
"pathLength": null,
"path": null
},
"reasoning": "LegacyParser.ParseJson exists but LegacyParser is never instantiated - replaced by SafeParser"
},
{
"symbolId": "sym:csharp:SafeParser.ParseJson",
"display": "SampleApp.SafeParser.ParseJson(string)",
"purl": "pkg:generic/SampleApp@1.0.0",
"expected": {
"latticeState": "SR",
"bucket": "direct",
"reachable": true,
"confidence": 0.95,
"pathLength": 2,
"path": [
"sym:csharp:Program.Main",
"sym:csharp:SafeParser.ParseJson"
]
},
"reasoning": "SafeParser.ParseJson is the active implementation called from Main"
}
],
"entryPoints": [
{
"symbolId": "sym:csharp:Program.Main",
"display": "SampleApp.Program.Main(string[])",
"phase": "runtime",
"source": "manifest"
}
],
"expectedUncertainty": {
"states": [],
"aggregateTier": "T4",
"riskScore": 0.0
},
"expectedGateDecisions": [
{
"vulnId": "CVE-2024-21907",
"targetSymbol": "sym:csharp:JsonConvert.DeserializeObject",
"requestedStatus": "not_affected",
"expectedDecision": "allow",
"expectedReason": "CU state allows not_affected - confirmed unreachable"
},
{
"vulnId": "CVE-2024-21907",
"targetSymbol": "sym:csharp:JsonConvert.DeserializeObject",
"requestedStatus": "affected",
"expectedDecision": "warn",
"expectedReason": "Marking as affected when CU suggests false positive"
}
]
}

View File

@@ -0,0 +1,27 @@
{
"sampleId": "sample:csharp:dead-code:001",
"version": "1.0.0",
"createdAt": "2025-12-13T12:00:00Z",
"language": "csharp",
"category": "negative",
"description": "C# app where vulnerable code exists but is never called - deprecated API replaced by safe implementation",
"source": {
"repository": "synthetic",
"commit": "synthetic-sample",
"buildToolchain": "dotnet:10.0"
},
"vulnerabilities": [
{
"vulnId": "CVE-2024-21907",
"purl": "pkg:nuget/Newtonsoft.Json@13.0.1",
"affectedSymbol": "Newtonsoft.Json.JsonConvert.DeserializeObject"
}
],
"artifacts": [
{
"path": "artifacts/app.dll",
"hash": "sha256:0000000000000000000000000000000000000000000000000000000000000002",
"type": "application/x-msdownload"
}
]
}

View File

@@ -0,0 +1,79 @@
{
"schema": "ground-truth-v1",
"sampleId": "sample:csharp:simple-reachable:001",
"generatedAt": "2025-12-13T12:00:00Z",
"generator": {
"name": "manual-annotation",
"version": "1.0.0",
"annotator": "scanner-guild"
},
"targets": [
{
"symbolId": "sym:csharp:JsonConvert.DeserializeObject",
"display": "Newtonsoft.Json.JsonConvert.DeserializeObject<T>(string, JsonSerializerSettings)",
"purl": "pkg:nuget/Newtonsoft.Json@13.0.1",
"expected": {
"latticeState": "SR",
"bucket": "direct",
"reachable": true,
"confidence": 0.95,
"pathLength": 2,
"path": [
"sym:csharp:Program.Main",
"sym:csharp:JsonConvert.DeserializeObject"
]
},
"reasoning": "Direct call from Main() to JsonConvert.DeserializeObject with TypeNameHandling.All settings"
},
{
"symbolId": "sym:csharp:JsonConvert.SerializeObject",
"display": "Newtonsoft.Json.JsonConvert.SerializeObject(object)",
"purl": "pkg:nuget/Newtonsoft.Json@13.0.1",
"expected": {
"latticeState": "SU",
"bucket": "unreachable",
"reachable": false,
"confidence": 0.90,
"pathLength": null,
"path": null
},
"reasoning": "SerializeObject is present in the dependency but never called from any entry point"
}
],
"entryPoints": [
{
"symbolId": "sym:csharp:Program.Main",
"display": "SampleApp.Program.Main(string[])",
"phase": "runtime",
"source": "manifest"
}
],
"expectedUncertainty": {
"states": [],
"aggregateTier": "T4",
"riskScore": 0.0
},
"expectedGateDecisions": [
{
"vulnId": "CVE-2024-21907",
"targetSymbol": "sym:csharp:JsonConvert.DeserializeObject",
"requestedStatus": "not_affected",
"expectedDecision": "block",
"expectedBlockedBy": "LatticeState",
"expectedReason": "SR state incompatible with not_affected - code path exists from entry point"
},
{
"vulnId": "CVE-2024-21907",
"targetSymbol": "sym:csharp:JsonConvert.DeserializeObject",
"requestedStatus": "affected",
"expectedDecision": "allow"
},
{
"vulnId": "CVE-2024-21907",
"targetSymbol": "sym:csharp:JsonConvert.SerializeObject",
"requestedStatus": "not_affected",
"expectedDecision": "allow",
"expectedReason": "SU state allows not_affected - unreachable code path"
}
]
}

View File

@@ -0,0 +1,27 @@
{
"sampleId": "sample:csharp:simple-reachable:001",
"version": "1.0.0",
"createdAt": "2025-12-13T12:00:00Z",
"language": "csharp",
"category": "positive",
"description": "Simple C# console app with direct call path to vulnerable Newtonsoft.Json TypeNameHandling usage",
"source": {
"repository": "synthetic",
"commit": "synthetic-sample",
"buildToolchain": "dotnet:10.0"
},
"vulnerabilities": [
{
"vulnId": "CVE-2024-21907",
"purl": "pkg:nuget/Newtonsoft.Json@13.0.1",
"affectedSymbol": "Newtonsoft.Json.JsonConvert.DeserializeObject"
}
],
"artifacts": [
{
"path": "artifacts/app.dll",
"hash": "sha256:0000000000000000000000000000000000000000000000000000000000000001",
"type": "application/x-msdownload"
}
]
}

View File

@@ -0,0 +1,108 @@
{
"schema": "ground-truth-v1",
"sampleId": "sample:java:vulnerable-log4j:001",
"generatedAt": "2025-12-13T12:00:00Z",
"generator": {
"name": "manual-annotation",
"version": "1.0.0",
"annotator": "security-team"
},
"targets": [
{
"symbolId": "sym:java:log4j.JndiLookup.lookup",
"display": "org.apache.logging.log4j.core.lookup.JndiLookup.lookup(LogEvent, String)",
"purl": "pkg:maven/org.apache.logging.log4j/log4j-core@2.14.1",
"expected": {
"latticeState": "CR",
"bucket": "runtime",
"reachable": true,
"confidence": 0.98,
"pathLength": 4,
"path": [
"sym:java:HttpRequestHandler.handle",
"sym:java:LogManager.getLogger",
"sym:java:Logger.info",
"sym:java:log4j.JndiLookup.lookup"
]
},
"reasoning": "Confirmed reachable via runtime probe - HTTP request handler logs user-controlled input which triggers JNDI lookup via message substitution"
},
{
"symbolId": "sym:java:log4j.JndiManager.lookup",
"display": "org.apache.logging.log4j.core.net.JndiManager.lookup(String)",
"purl": "pkg:maven/org.apache.logging.log4j/log4j-core@2.14.1",
"expected": {
"latticeState": "CU",
"bucket": "unreachable",
"reachable": false,
"confidence": 0.92,
"pathLength": null,
"path": null
},
"reasoning": "JndiManager.lookup is present in log4j-core but the direct JndiManager usage path is not exercised - only JndiLookup wrapper is used"
},
{
"symbolId": "sym:java:log4j.ScriptLookup.lookup",
"display": "org.apache.logging.log4j.core.lookup.ScriptLookup.lookup(LogEvent, String)",
"purl": "pkg:maven/org.apache.logging.log4j/log4j-core@2.14.1",
"expected": {
"latticeState": "SU",
"bucket": "unreachable",
"reachable": false,
"confidence": 0.85,
"pathLength": null,
"path": null
},
"reasoning": "ScriptLookup exists in log4j-core but is disabled by default and no configuration enables it"
}
],
"entryPoints": [
{
"symbolId": "sym:java:HttpRequestHandler.handle",
"display": "com.example.app.HttpRequestHandler.handle(HttpExchange)",
"phase": "runtime",
"source": "servlet"
},
{
"symbolId": "sym:java:Application.main",
"display": "com.example.app.Application.main(String[])",
"phase": "main",
"source": "manifest"
}
],
"expectedUncertainty": {
"states": [],
"aggregateTier": "T4",
"riskScore": 0.0
},
"expectedGateDecisions": [
{
"vulnId": "CVE-2021-44228",
"targetSymbol": "sym:java:log4j.JndiLookup.lookup",
"requestedStatus": "not_affected",
"expectedDecision": "block",
"expectedBlockedBy": "LatticeState",
"expectedReason": "CR state blocks not_affected - runtime evidence confirms reachability"
},
{
"vulnId": "CVE-2021-44228",
"targetSymbol": "sym:java:log4j.JndiLookup.lookup",
"requestedStatus": "affected",
"expectedDecision": "allow"
},
{
"vulnId": "CVE-2021-44228",
"targetSymbol": "sym:java:log4j.JndiManager.lookup",
"requestedStatus": "not_affected",
"expectedDecision": "allow",
"expectedReason": "CU state allows not_affected - confirmed unreachable"
},
{
"vulnId": "CVE-2021-44228",
"targetSymbol": "sym:java:log4j.ScriptLookup.lookup",
"requestedStatus": "not_affected",
"expectedDecision": "warn",
"expectedReason": "SU state allows not_affected but with warning - static analysis only, no runtime confirmation"
}
]
}

View File

@@ -0,0 +1,32 @@
{
"sampleId": "sample:java:vulnerable-log4j:001",
"version": "1.0.0",
"createdAt": "2025-12-13T12:00:00Z",
"language": "java",
"category": "positive",
"description": "Log4Shell CVE-2021-44228 reachable via JNDI lookup in logging path from HTTP request handler",
"source": {
"repository": "synthetic",
"commit": "synthetic-sample",
"buildToolchain": "maven:3.9.0,jdk:17"
},
"vulnerabilities": [
{
"vulnId": "CVE-2021-44228",
"purl": "pkg:maven/org.apache.logging.log4j/log4j-core@2.14.1",
"affectedSymbol": "org.apache.logging.log4j.core.lookup.JndiLookup.lookup"
}
],
"artifacts": [
{
"path": "artifacts/app.jar",
"hash": "sha256:0000000000000000000000000000000000000000000000000000000000000004",
"type": "application/java-archive"
},
{
"path": "artifacts/sbom.cdx.json",
"hash": "sha256:0000000000000000000000000000000000000000000000000000000000000005",
"type": "application/vnd.cyclonedx+json"
}
]
}

View File

@@ -0,0 +1,100 @@
{
"schema": "ground-truth-v1",
"sampleId": "sample:native:stripped-elf:001",
"generatedAt": "2025-12-13T12:00:00Z",
"generator": {
"name": "manual-annotation",
"version": "1.0.0",
"annotator": "scanner-guild"
},
"targets": [
{
"symbolId": "sym:binary:ossl_punycode_decode",
"display": "ossl_punycode_decode",
"purl": "pkg:deb/ubuntu/openssl@3.0.2?arch=amd64",
"expected": {
"latticeState": "SR",
"bucket": "direct",
"reachable": true,
"confidence": 0.85,
"pathLength": 4,
"path": [
"sym:binary:_start",
"sym:binary:main",
"sym:binary:SSL_connect",
"sym:binary:ossl_punycode_decode"
]
},
"reasoning": "punycode_decode is reachable via SSL certificate validation during SSL_connect - lower confidence due to stripped binary heuristics"
},
{
"symbolId": "sym:binary:sub_401000",
"display": "sub_401000 (heuristic function)",
"purl": "pkg:generic/app@1.0.0",
"expected": {
"latticeState": "U",
"bucket": "unknown",
"reachable": null,
"confidence": 0.4,
"pathLength": null,
"path": null
},
"reasoning": "Stripped symbol detected by heuristic CFG analysis - function boundaries uncertain"
}
],
"entryPoints": [
{
"symbolId": "sym:binary:_start",
"display": "_start",
"phase": "load",
"source": "e_entry"
},
{
"symbolId": "sym:binary:main",
"display": "main",
"phase": "runtime",
"source": "symbol"
},
{
"symbolId": "init:binary:0x401000",
"display": "DT_INIT_ARRAY[0]",
"phase": "init",
"source": "DT_INIT_ARRAY"
}
],
"expectedUncertainty": {
"states": [
{
"code": "U1",
"entropy": 0.35
}
],
"aggregateTier": "T2",
"riskScore": 0.25
},
"expectedGateDecisions": [
{
"vulnId": "CVE-2022-3602",
"targetSymbol": "sym:binary:ossl_punycode_decode",
"requestedStatus": "not_affected",
"expectedDecision": "block",
"expectedBlockedBy": "LatticeState",
"expectedReason": "SR state blocks not_affected - static analysis shows reachability"
},
{
"vulnId": "CVE-2022-3602",
"targetSymbol": "sym:binary:ossl_punycode_decode",
"requestedStatus": "affected",
"expectedDecision": "warn",
"expectedReason": "T2 uncertainty tier requires review for affected status"
},
{
"vulnId": "CVE-2022-3602",
"targetSymbol": "sym:binary:sub_401000",
"requestedStatus": "not_affected",
"expectedDecision": "block",
"expectedBlockedBy": "UncertaintyTier",
"expectedReason": "Unknown state with U1 uncertainty blocks not_affected without justification"
}
]
}

View File

@@ -0,0 +1,27 @@
{
"sampleId": "sample:native:stripped-elf:001",
"version": "1.0.0",
"createdAt": "2025-12-13T12:00:00Z",
"language": "native",
"category": "positive",
"description": "Stripped ELF binary linking to vulnerable OpenSSL version with reachable SSL_read path",
"source": {
"repository": "synthetic",
"commit": "synthetic-sample",
"buildToolchain": "gcc:13.0,openssl:3.0.2"
},
"vulnerabilities": [
{
"vulnId": "CVE-2022-3602",
"purl": "pkg:deb/ubuntu/openssl@3.0.2?arch=amd64",
"affectedSymbol": "ossl_punycode_decode"
}
],
"artifacts": [
{
"path": "artifacts/app",
"hash": "sha256:0000000000000000000000000000000000000000000000000000000000000003",
"type": "application/x-executable"
}
]
}

View File

@@ -0,0 +1,189 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://stellaops.io/schemas/reachability/ground-truth.schema.json",
"title": "Reachability Ground Truth",
"description": "Ground truth annotations for reachability test samples",
"type": "object",
"required": ["schema", "sampleId", "generatedAt", "generator", "targets", "entryPoints"],
"properties": {
"schema": {
"type": "string",
"const": "ground-truth-v1"
},
"sampleId": {
"type": "string",
"pattern": "^sample:[a-z]+:[a-z0-9-]+:[0-9]+$"
},
"generatedAt": {
"type": "string",
"format": "date-time"
},
"generator": {
"type": "object",
"required": ["name", "version"],
"properties": {
"name": {
"type": "string"
},
"version": {
"type": "string"
},
"annotator": {
"type": "string"
}
}
},
"targets": {
"type": "array",
"items": {
"$ref": "#/definitions/target"
}
},
"entryPoints": {
"type": "array",
"items": {
"$ref": "#/definitions/entryPoint"
}
},
"expectedUncertainty": {
"$ref": "#/definitions/uncertainty"
},
"expectedGateDecisions": {
"type": "array",
"items": {
"$ref": "#/definitions/gateDecision"
}
}
},
"definitions": {
"target": {
"type": "object",
"required": ["symbolId", "expected", "reasoning"],
"properties": {
"symbolId": {
"type": "string",
"pattern": "^sym:[a-z]+:.+"
},
"display": {
"type": "string"
},
"purl": {
"type": "string"
},
"expected": {
"type": "object",
"required": ["latticeState", "bucket", "reachable", "confidence"],
"properties": {
"latticeState": {
"type": "string",
"enum": ["U", "SR", "SU", "RO", "RU", "CR", "CU", "X"]
},
"bucket": {
"type": "string",
"enum": ["unknown", "direct", "runtime", "unreachable", "entrypoint"]
},
"reachable": {
"type": "boolean"
},
"confidence": {
"type": "number",
"minimum": 0,
"maximum": 1
},
"pathLength": {
"type": ["integer", "null"],
"minimum": 0
},
"path": {
"type": ["array", "null"],
"items": {
"type": "string"
}
}
}
},
"reasoning": {
"type": "string"
}
}
},
"entryPoint": {
"type": "object",
"required": ["symbolId", "phase", "source"],
"properties": {
"symbolId": {
"type": "string"
},
"display": {
"type": "string"
},
"phase": {
"type": "string",
"enum": ["load", "init", "runtime", "main", "fini"]
},
"source": {
"type": "string"
}
}
},
"uncertainty": {
"type": "object",
"required": ["aggregateTier"],
"properties": {
"states": {
"type": "array",
"items": {
"type": "object",
"required": ["code", "entropy"],
"properties": {
"code": {
"type": "string",
"enum": ["U1", "U2", "U3", "U4"]
},
"entropy": {
"type": "number",
"minimum": 0,
"maximum": 1
}
}
}
},
"aggregateTier": {
"type": "string",
"enum": ["T1", "T2", "T3", "T4"]
},
"riskScore": {
"type": "number",
"minimum": 0,
"maximum": 1
}
}
},
"gateDecision": {
"type": "object",
"required": ["vulnId", "targetSymbol", "requestedStatus", "expectedDecision"],
"properties": {
"vulnId": {
"type": "string"
},
"targetSymbol": {
"type": "string"
},
"requestedStatus": {
"type": "string",
"enum": ["affected", "not_affected", "under_investigation", "fixed"]
},
"expectedDecision": {
"type": "string",
"enum": ["allow", "block", "warn"]
},
"expectedBlockedBy": {
"type": "string"
},
"expectedReason": {
"type": "string"
}
}
}
}
}

View File

@@ -0,0 +1,94 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://stellaops.io/schemas/reachability/manifest.schema.json",
"title": "Reachability Sample Manifest",
"description": "Metadata for a reachability test sample",
"type": "object",
"required": ["sampleId", "version", "createdAt", "language", "category", "description"],
"properties": {
"sampleId": {
"type": "string",
"pattern": "^sample:[a-z]+:[a-z0-9-]+:[0-9]+$",
"description": "Unique sample identifier"
},
"version": {
"type": "string",
"pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$",
"description": "Sample version (SemVer)"
},
"createdAt": {
"type": "string",
"format": "date-time",
"description": "Creation timestamp (UTC ISO-8601)"
},
"language": {
"type": "string",
"enum": ["java", "csharp", "javascript", "php", "python", "native", "polyglot"],
"description": "Primary language of the sample"
},
"category": {
"type": "string",
"enum": ["positive", "negative", "contested"],
"description": "Ground truth category"
},
"description": {
"type": "string",
"description": "Human-readable description"
},
"source": {
"type": "object",
"properties": {
"repository": {
"type": "string",
"format": "uri"
},
"commit": {
"type": "string"
},
"buildToolchain": {
"type": "string"
}
}
},
"vulnerabilities": {
"type": "array",
"items": {
"type": "object",
"required": ["vulnId", "purl", "affectedSymbol"],
"properties": {
"vulnId": {
"type": "string",
"description": "CVE or advisory ID"
},
"purl": {
"type": "string",
"description": "Package URL of vulnerable package"
},
"affectedSymbol": {
"type": "string",
"description": "Symbol name that is vulnerable"
}
}
}
},
"artifacts": {
"type": "array",
"items": {
"type": "object",
"required": ["path", "hash", "type"],
"properties": {
"path": {
"type": "string"
},
"hash": {
"type": "string",
"pattern": "^sha256:[a-f0-9]{64}$"
},
"type": {
"type": "string"
}
}
}
}
}
}

View File

@@ -646,6 +646,25 @@ Persisted documents capture the canonical envelope (`payload` field), tenant/nod
---
### 2.10 Signals - Reachability evidence chain
Signals APIs (base path: `/signals`) provide deterministic ingestion + scoring for the reachability evidence chain (callgraph -> runtime facts -> unknowns -> reachability facts) consumed by Policy and UI explainers.
| Method | Path | Scope | Notes |
|--------|------|-------|-------|
| `POST` | `/signals/callgraphs` | `signals:write` | Ingest a callgraph artifact (base64 JSON); response includes `graphHash` (sha256) and CAS URIs. |
| `POST` | `/signals/runtime-facts` | `signals:write` | Ingest runtime hit events (JSON). |
| `POST` | `/signals/runtime-facts/ndjson` | `signals:write` | Stream NDJSON events (optional gzip) with subject in query params. |
| `POST` | `/signals/unknowns` | `signals:write` | Ingest unresolved symbols/edges; influences `unknownsPressure`. |
| `GET` | `/signals/facts/{subjectKey}` | `signals:read` | Fetch `ReachabilityFactDocument` including `metadata.fact.digest` and per-target `states[]`. |
| `POST` | `/signals/reachability/recompute` | `signals:admin` | Recompute reachability for explicit targets and blocked edges. |
Docs & samples:
- `docs/api/signals/reachability-contract.md`
- `docs/api/signals/samples/callgraph-sample.json`
- `docs/api/signals/samples/facts-sample.json`
- `docs/reachability/lattice.md`
### 2.9 CVSS Receipts (Policy Gateway)
Policy Gateway proxies the Policy Engine CVSS v4 receipt APIs. Scopes: `policy.run` for create/amend, `findings.read` for read/history/policies.

View File

@@ -232,6 +232,17 @@ Slim wrapper used by CLI; returns 204 on success or `ERR_POL_001` payload.
> Schema reference: canonical policy run request/status/diff payloads ship with the Scheduler Models guide (`src/Scheduler/__Libraries/StellaOps.Scheduler.Models/docs/SCHED-MODELS-20-001-POLICY-RUNS.md`) and JSON fixtures under `samples/api/scheduler/policy-*.json`.
### 6.0 Reachability evidence inputs (Signals)
Policy Engine evaluations may be enriched with reachability facts produced by Signals. These facts are expected to be:
- **Deterministic:** referenced by `metadata.fact.digest` (sha256) and versioned via `metadata.fact.version`.
- **Evidence-linked:** per-target states include `path[]` and `evidence.runtimeHits[]` (and any future CAS/DSSE pointers).
Signals contract & scoring model:
- `docs/api/signals/reachability-contract.md`
- `docs/reachability/lattice.md`
### 6.1 Trigger Run
```

View File

@@ -1,66 +1,63 @@
# Signals Reachability API Contract (draft placeholder)
# Signals API (Reachability)
**Status:** Draft v0.2 · owner-proposed
**Status:** Working contract (aligns with `src/Signals/StellaOps.Signals/Program.cs`).
## Scope
- `/signals/callgraphs`, `/signals/facts`, reachability scoring overlays feeding UI/Web.
- Deterministic fixtures for SIG-26 chain (columns/badges, call paths, timelines, overlays, coverage).
## Auth, scopes, sealed mode
- **Scopes:** `signals:read`, `signals:write`, `signals:admin` (endpoint-specific; see below).
- **Dev fallback:** when Authority auth is disabled, requests must include `X-Scopes: <space-separated scopes>` (example: `X-Scopes: signals:write`).
- **Sealed mode:** when enabled, Signals may return `503` with `{ "error": "sealed-mode evidence invalid", ... }`.
## Endpoints
- `GET /signals/callgraphs` — returns call paths contributing to reachability.
- `GET /signals/facts` — returns reachability/coverage facts.
Common headers: `Authorization: DPoP <token>`, `DPoP: <proof>`, `X-StellaOps-Tenant`, optional `If-None-Match`.
Pagination: cursor via `pageToken`; default 50, max 200.
ETag: required on responses; clients must send `If-None-Match` for cache validation.
### Health & status
### Callgraphs response (draft)
```jsonc
{
"tenantId": "tenant-default",
"assetId": "registry.local/library/app@sha256:abc123",
"paths": [
{
"id": "path-1",
"source": "api-gateway",
"target": "jwt-auth-service",
"hops": [
{ "service": "api-gateway", "endpoint": "/login", "timestamp": "2025-12-05T10:00:00Z" },
{ "service": "jwt-auth-service", "endpoint": "/verify", "timestamp": "2025-12-05T10:00:01Z" }
],
"evidence": { "traceId": "trace-abc", "spanCount": 2, "score": 0.92 }
}
],
"pagination": { "nextPageToken": null },
"etag": "sig-callgraphs-etag"
}
```
- `GET /healthz` (anonymous)
- `GET /readyz` (anonymous; `503` when not ready or sealed-mode blocked)
- `GET /signals/ping` (scope: `signals:read`, response: `204`)
- `GET /signals/status` (scope: `signals:read`)
### Facts response (draft)
```jsonc
{
"tenantId": "tenant-default",
"facts": [
{
"id": "fact-1",
"type": "reachability",
"assetId": "registry.local/library/app@sha256:abc123",
"component": "pkg:npm/jsonwebtoken@9.0.2",
"status": "reachable",
"confidence": 0.88,
"observedAt": "2025-12-05T10:10:00Z",
"signalsVersion": "signals-2025.310.1"
}
],
"pagination": { "nextPageToken": "..." },
"etag": "sig-facts-etag"
}
```
### Callgraph ingestion & retrieval
### Samples
- Callgraphs: `docs/api/signals/samples/callgraph-sample.json`
- Facts: `docs/api/signals/samples/facts-sample.json`
- `POST /signals/callgraphs` (scope: `signals:write`)
- Body: `CallgraphIngestRequest` (`language`, `component`, `version`, `artifactContentBase64`, …).
- Response: `202 Accepted` with `CallgraphIngestResponse` and `Location: /signals/callgraphs/{callgraphId}`.
- Graph hash is computed deterministically from normalized nodes/edges/roots; see `graphHash` in the response.
- `GET /signals/callgraphs/{callgraphId}` (scope: `signals:read`)
- `GET /signals/callgraphs/{callgraphId}/manifest` (scope: `signals:read`)
### Outstanding
- Finalize score model, accepted `type` values, and max page size.
- Provide OpenAPI/JSON schema and error codes.
Sample request: `docs/api/signals/samples/callgraph-sample.json`
### Runtime facts ingestion
- `POST /signals/runtime-facts` (scope: `signals:write`)
- Body: `RuntimeFactsIngestRequest` with `subject`, `callgraphId`, and `events[]`.
- `POST /signals/runtime-facts/ndjson?callgraphId=...&scanId=...` (scope: `signals:write`)
- Body: NDJSON of `RuntimeFactEvent` objects; `Content-Encoding: gzip` supported.
- `POST /signals/runtime-facts/synthetic` (scope: `signals:write`)
- Generates a small deterministic sample set of runtime events for a callgraph to unblock testing.
### Unknowns ingestion & retrieval
- `POST /signals/unknowns` (scope: `signals:write`)
- Body: `UnknownsIngestRequest` (`subject`, `callgraphId`, `unknowns[]`).
- `GET /signals/unknowns/{subjectKey}` (scope: `signals:read`)
### Reachability scoring & facts
- `POST /signals/reachability/recompute` (scope: `signals:admin`)
- Body: `ReachabilityRecomputeRequest` (`callgraphId`, `subject`, `entryPoints[]`, `targets[]`, optional `runtimeHits[]`, optional `blockedEdges[]`).
- Response: `200 OK` with `{ id, callgraphId, subject, entryPoints, states, computedAt }`.
- `GET /signals/facts/{subjectKey}` (scope: `signals:read`)
- Response: `ReachabilityFactDocument` (per-target states, `score`, `riskScore`, unknowns pressure, optional uncertainty states, runtime facts snapshot).
Sample fact: `docs/api/signals/samples/facts-sample.json`
### Reachability union bundle ingestion (CAS layout)
- `POST /signals/reachability/union` (scope: `signals:write`)
- Body: `application/zip` bundle containing `nodes.ndjson`, `edges.ndjson`, `meta.json`.
- Optional header: `X-Analysis-Id` (defaults to a new GUID if omitted).
- Response: `202 Accepted` with `ReachabilityUnionIngestResponse` and `Location: /signals/reachability/union/{analysisId}/meta`.
- `GET /signals/reachability/union/{analysisId}/meta` (scope: `signals:read`)
- `GET /signals/reachability/union/{analysisId}/files/{fileName}` (scope: `signals:read`)

View File

@@ -1,23 +1,16 @@
{
"tenantId": "tenant-default",
"assetId": "registry.local/library/app@sha256:abc123",
"paths": [
{
"id": "path-1",
"source": "api-gateway",
"target": "jwt-auth-service",
"hops": [
{ "service": "api-gateway", "endpoint": "/login", "timestamp": "2025-12-05T10:00:00Z" },
{ "service": "jwt-auth-service", "endpoint": "/verify", "timestamp": "2025-12-05T10:00:01Z" }
],
"evidence": {
"traceId": "trace-abc",
"spanCount": 2,
"score": 0.92
}
}
],
"pagination": {
"nextPageToken": null
"language": "java",
"component": "pkg:maven/com.acme/demo-app@1.0.0?type=jar",
"version": "1.0.0",
"artifactContentType": "application/json",
"artifactFileName": "callgraph.json",
"artifactContentBase64": "eyJzY2hlbWFfdmVyc2lvbiI6IjEuMCIsInJvb3RzIjpbeyJpZCI6ImZ1bmM6amF2YTpjb20uYWNtZS5BcHAubWFpbiIsInBoYXNlIjoicnVudGltZSIsInNvdXJjZSI6InN0YXRpYyJ9XSwibm9kZXMiOlt7ImlkIjoiZnVuYzpqYXZhOmNvbS5hY21lLkFwcC5tYWluIiwibmFtZSI6Im1haW4iLCJraW5kIjoiZnVuY3Rpb24iLCJuYW1lc3BhY2UiOiJjb20uYWNtZSIsImZpbGUiOiJBcHAuamF2YSIsImxpbmUiOjEsImxhbmd1YWdlIjoiamF2YSJ9LHsiaWQiOiJmdW5jOmphdmE6Y29tLmFjbWUuQXV0aC52ZXJpZnkiLCJuYW1lIjoidmVyaWZ5Iiwia2luZCI6ImZ1bmN0aW9uIiwibmFtZXNwYWNlIjoiY29tLmFjbWUuYXV0aCIsImZpbGUiOiJBdXRoLmphdmEiLCJsaW5lIjo0MiwibGFuZ3VhZ2UiOiJqYXZhIn1dLCJlZGdlcyI6W3siZnJvbSI6ImZ1bmM6amF2YTpjb20uYWNtZS5BcHAubWFpbiIsInRvIjoiZnVuYzpqYXZhOmNvbS5hY21lLkF1dGgudmVyaWZ5Iiwia2luZCI6ImNhbGwiLCJjb25maWRlbmNlIjowLjl9XSwiYW5hbHl6ZXIiOnsibmFtZSI6ImRlbW8iLCJ2ZXJzaW9uIjoiMC4wLjAifX0=",
"metadata": {
"scanId": "scan-0001"
},
"schemaVersion": "1.0",
"analyzer": {
"name": "demo",
"version": "0.0.0"
}
}

View File

@@ -1,26 +1,70 @@
{
"tenantId": "tenant-default",
"facts": [
"id": "fact0000000000000000000000000000001",
"callgraphId": "callgraph-0001",
"subject": {
"scanId": "scan-0001",
"imageDigest": "sha256:abc123abc123abc123abc123abc123abc123abc123abc123abc123abc123abc1"
},
"entryPoints": [
"func:java:com.acme.App.main"
],
"states": [
{
"id": "fact-1",
"type": "reachability",
"assetId": "registry.local/library/app@sha256:abc123",
"component": "pkg:npm/jsonwebtoken@9.0.2",
"status": "reachable",
"confidence": 0.88,
"observedAt": "2025-12-05T10:10:00Z",
"signalsVersion": "signals-2025.310.1"
"target": "func:java:com.acme.Admin.debug",
"reachable": false,
"confidence": 0.25,
"bucket": "unreachable",
"weight": 0.0,
"score": 0.0,
"path": [],
"evidence": {
"runtimeHits": [],
"blockedEdges": []
}
},
{
"id": "fact-2",
"type": "coverage",
"assetId": "registry.local/library/app@sha256:abc123",
"metric": "sensors_present",
"value": 0.94,
"observedAt": "2025-12-05T10:11:00Z"
"target": "func:java:com.acme.Auth.verify",
"reachable": true,
"confidence": 0.9,
"bucket": "runtime",
"weight": 0.45,
"score": 0.405,
"path": [
"func:java:com.acme.App.main",
"func:java:com.acme.Auth.verify"
],
"evidence": {
"runtimeHits": [
"func:java:com.acme.Auth.verify"
],
"blockedEdges": []
}
}
],
"pagination": {
"nextPageToken": "eyJmYWN0SWQiOiJmYWN0LTIifQ"
"runtimeFacts": [
{
"symbolId": "func:java:com.acme.Auth.verify",
"codeId": "code:java:com.acme.Auth.verify",
"purl": "pkg:maven/com.acme/demo-app@1.0.0?type=jar",
"processId": 1234,
"processName": "demo-app",
"containerId": "containerd://0000000000000000",
"hitCount": 3,
"observedAt": "2025-12-12T00:00:00Z",
"metadata": {
"source": "synthetic-probe"
}
}
],
"metadata": {
"fact.digest": "sha256:0000000000000000000000000000000000000000000000000000000000000000",
"fact.digest.alg": "sha256",
"fact.version": "1"
},
"score": 0.2025,
"riskScore": 0.2025,
"unknownsCount": 0,
"unknownsPressure": 0.0,
"computedAt": "2025-12-12T00:00:00Z",
"subjectKey": "scan-0001"
}

View File

@@ -50,26 +50,26 @@
| 22 | UI-AUDIT-05-002 | DONE | Evidence: `src/Web/StellaOps.Web/src/app/features/triage/triage-audit-bundle-new.component.ts` | UI Guild; Export Center Guild (src/Web/StellaOps.Web) | Build Audit Bundle creation wizard: subject artifact+digest selection, time window picker, content checklist (Vuln reports, SBOM, VEX, Policy evals, Attestations). |
| 23 | UI-AUDIT-05-003 | DONE | Evidence: `src/Web/StellaOps.Web/src/app/features/triage/triage-audit-bundle-new.component.ts`; `src/Web/StellaOps.Web/src/app/core/api/audit-bundles.client.ts` | UI Guild; Export Center Guild (src/Web/StellaOps.Web) | Wire audit bundle creation to POST /v1/audit-bundles, show progress, display bundle ID, hash, download button, and OCI reference on completion. |
| 24 | UI-AUDIT-05-004 | DONE | Evidence: `src/Web/StellaOps.Web/src/app/features/triage/triage-audit-bundles.component.ts` | UI Guild (src/Web/StellaOps.Web) | Add audit bundle history view: list previously created bundles with bundleId, createdAt, subject, download/view actions. |
| 25 | API-VEX-06-001 | BLOCKED | Blocked: needs `SCHEMA-08-001` + `DTO-09-001` sign-off/implementation in `src/VulnExplorer` | API Guild (src/VulnExplorer) | Implement POST /v1/vex-decisions endpoint with VexDecisionDto request/response per schema, validation, attestation generation trigger. |
| 26 | API-VEX-06-002 | BLOCKED | Blocked: depends on API-VEX-06-001 | API Guild (src/VulnExplorer) | Implement PATCH /v1/vex-decisions/{id} for updating existing decisions with supersedes tracking. |
| 27 | API-VEX-06-003 | BLOCKED | Blocked: depends on API-VEX-06-002 | API Guild (src/VulnExplorer) | Implement GET /v1/vex-decisions with filters for vulnerabilityId, subject, status, scope, validFor. |
| 28 | API-AUDIT-07-001 | BLOCKED | Blocked: needs `SCHEMA-08-003` + Export Center job/ZIP/OCI implementation in `src/ExportCenter` | API Guild (src/ExportCenter) | Implement POST /v1/audit-bundles endpoint with bundle creation, index generation, ZIP/OCI artifact production. |
| 29 | API-AUDIT-07-002 | BLOCKED | Blocked: depends on API-AUDIT-07-001 | API Guild (src/ExportCenter) | Implement GET /v1/audit-bundles/{bundleId} for bundle download with integrity verification. |
| 30 | SCHEMA-08-001 | BLOCKED | Blocked: Action Tracker #1 (Platform + Excititor schema review/sign-off) | Platform Guild | Review and finalize `docs/schemas/vex-decision.schema.json` (JSON Schema 2020-12) per advisory; confirm examples and versioning. |
| 31 | SCHEMA-08-002 | BLOCKED | Blocked: Action Tracker #2 (Attestor predicate review/sign-off) | Platform Guild | Review and finalize `docs/schemas/attestation-vuln-scan.schema.json` predicate schema; align predicateType URI and required fields. |
| 32 | SCHEMA-08-003 | BLOCKED | Blocked: Action Tracker #3 (Export Center format review/sign-off) | Platform Guild | Review and finalize `docs/schemas/audit-bundle-index.schema.json` for audit bundle manifest structure; confirm stable IDs and deterministic ordering guidance. |
| 33 | DTO-09-001 | BLOCKED | Blocked: depends on SCHEMA-08-001 finalization | API Guild | Create VexDecisionDto, SubjectRefDto, EvidenceRefDto, VexScopeDto, ValidForDto C# DTOs per advisory. |
| 34 | DTO-09-002 | BLOCKED | Blocked: depends on SCHEMA-08-002 finalization | API Guild | Create VulnScanAttestationDto, AttestationSubjectDto, VulnScanPredicateDto C# DTOs per advisory. |
| 35 | DTO-09-003 | BLOCKED | Blocked: depends on SCHEMA-08-003 finalization | API Guild | Create AuditBundleIndexDto, BundleArtifactDto, BundleVexDecisionEntryDto C# DTOs per advisory. |
| 25 | API-VEX-06-001 | DONE | Evidence: `src/VulnExplorer/StellaOps.VulnExplorer.Api/Program.cs`; `src/VulnExplorer/StellaOps.VulnExplorer.Api/Data/VexDecisionStore.cs` | API Guild (src/VulnExplorer) | Implement POST /v1/vex-decisions endpoint with VexDecisionDto request/response per schema, validation, attestation generation trigger. |
| 26 | API-VEX-06-002 | DONE | Evidence: `src/VulnExplorer/StellaOps.VulnExplorer.Api/Program.cs` | API Guild (src/VulnExplorer) | Implement PATCH /v1/vex-decisions/{id} for updating existing decisions with supersedes tracking. |
| 27 | API-VEX-06-003 | DONE | Evidence: `src/VulnExplorer/StellaOps.VulnExplorer.Api/Program.cs` | API Guild (src/VulnExplorer) | Implement GET /v1/vex-decisions with filters for vulnerabilityId, subject, status, scope, validFor. |
| 28 | API-AUDIT-07-001 | DONE | Evidence: `src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/AuditBundle/AuditBundleEndpoints.cs`; `src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/AuditBundle/AuditBundleJobHandler.cs` | API Guild (src/ExportCenter) | Implement POST /v1/audit-bundles endpoint with bundle creation, index generation, ZIP/OCI artifact production. |
| 29 | API-AUDIT-07-002 | DONE | Evidence: `src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/AuditBundle/AuditBundleEndpoints.cs` | API Guild (src/ExportCenter) | Implement GET /v1/audit-bundles/{bundleId} for bundle download with integrity verification. |
| 30 | SCHEMA-08-001 | DONE | Evidence: `docs/schemas/vex-decision.schema.json` (JSON Schema 2020-12 with examples) | Platform Guild | Review and finalize `docs/schemas/vex-decision.schema.json` (JSON Schema 2020-12) per advisory; confirm examples and versioning. |
| 31 | SCHEMA-08-002 | DONE | Evidence: `docs/schemas/attestation-vuln-scan.schema.json` (JSON Schema 2020-12 with examples) | Platform Guild | Review and finalize `docs/schemas/attestation-vuln-scan.schema.json` predicate schema; align predicateType URI and required fields. |
| 32 | SCHEMA-08-003 | DONE | Evidence: `docs/schemas/audit-bundle-index.schema.json` (JSON Schema 2020-12 with examples) | Platform Guild | Review and finalize `docs/schemas/audit-bundle-index.schema.json` for audit bundle manifest structure; confirm stable IDs and deterministic ordering guidance. |
| 33 | DTO-09-001 | DONE | Evidence: `src/VulnExplorer/StellaOps.VulnExplorer.Api/Models/VexDecisionModels.cs` | API Guild | Create VexDecisionDto, SubjectRefDto, EvidenceRefDto, VexScopeDto, ValidForDto C# DTOs per advisory. |
| 34 | DTO-09-002 | DONE | Evidence: `src/VulnExplorer/StellaOps.VulnExplorer.Api/Models/AttestationModels.cs` | API Guild | Create VulnScanAttestationDto, AttestationSubjectDto, VulnScanPredicateDto C# DTOs per advisory. |
| 35 | DTO-09-003 | DONE | Evidence: `src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Client/Models/ExportModels.cs` | API Guild | Create AuditBundleIndexDto, BundleArtifactDto, BundleVexDecisionEntryDto C# DTOs per advisory. |
| 36 | TS-10-001 | DONE | Evidence: `src/Web/StellaOps.Web/src/app/core/api/evidence.models.ts`; `src/Web/StellaOps.Web/src/app/core/api/vex-decisions.models.ts` | UI Guild (src/Web/StellaOps.Web) | Create TypeScript interfaces for VexDecision, SubjectRef, EvidenceRef, VexScope, ValidFor per advisory. |
| 37 | TS-10-002 | DONE | Evidence: `src/Web/StellaOps.Web/src/app/core/api/attestation-vuln-scan.models.ts` | UI Guild (src/Web/StellaOps.Web) | Create TypeScript interfaces for VulnScanAttestation, AttestationSubject, VulnScanPredicate per advisory. |
| 38 | TS-10-003 | DONE | Evidence: `src/Web/StellaOps.Web/src/app/core/api/audit-bundles.models.ts` | UI Guild (src/Web/StellaOps.Web) | Create TypeScript interfaces for AuditBundleIndex, BundleArtifact, BundleVexDecisionEntry per advisory. |
| 39 | DOC-11-001 | DONE | Evidence: `docs/key-features.md`; `docs/07_HIGH_LEVEL_ARCHITECTURE.md` | Docs Guild (docs/) | Update high-level positioning for VEX-first triage: refresh docs/key-features.md and docs/07_HIGH_LEVEL_ARCHITECTURE.md with UX/audit bundle narrative; link `docs/product-advisories/archived/27-Nov-2025-superseded/28-Nov-2025 - Vulnerability Triage UX & VEX-First Decisioning.md`. |
| 40 | DOC-11-002 | DONE | Evidence: `docs/modules/ui/architecture.md` | Docs Guild; UI Guild | Update docs/modules/ui/architecture.md with triage workspace + VEX modal flows; add schema links and advisory cross-references. |
| 41 | DOC-11-003 | DONE | Evidence: `docs/modules/vuln-explorer/architecture.md`; `docs/modules/export-center/architecture.md` | Docs Guild; Vuln Explorer Guild; Export Center Guild | Update docs/modules/vuln-explorer/architecture.md and docs/modules/export-center/architecture.md with VEX decision/audit bundle API surfaces and schema references. |
| 42 | TRIAGE-GAPS-215-042 | BLOCKED | Blocked: depends on schema publication (`SCHEMA-08-*`) + real findings/VEX/audit APIs + telemetry contract | UI Guild · Platform Guild | Remediate VT1VT10: publish signed schemas + canonical JSON, enforce evidence linkage (graph/policy/attestations), tenant/RBAC controls, deterministic ordering/pagination, a11y standards, offline triage-kit exports, supersedes/conflict rules, attestation verification UX, redaction policy, UX telemetry/SLIs with alerts. |
| 43 | UI-PROOF-VEX-0215-010 | BLOCKED | Blocked: depends on VexLens/Findings APIs + DSSE headers + caching/integrity rules | UI Guild; VexLens Guild; Policy Guild | Implement proof-linked Not Affected badge/drawer: scoped endpoints + tenant headers, cache/staleness policy, client integrity checks, failure/offline UX, evidence precedence, telemetry schema/privacy, signed permalinks, revision reconciliation, fixtures/tests. |
| 44 | TTE-GAPS-0215-011 | BLOCKED | Blocked: depends on telemetry core sprint (TTE schema + SLIs/SLOs) | UI Guild; Telemetry Guild | Close TTE1TTE10: publish tte-event schema, proof eligibility rules, sampling/bot filters, per-surface SLO/error budgets, required indexes/streaming SLAs, offline-kit handling, alert/runbook, release regression gate, and a11y/viewport tests. |
| 42 | TRIAGE-GAPS-215-042 | DONE | Evidence: `src/Telemetry/StellaOps.Telemetry.Core/StellaOps.Telemetry.Core/TimeToEvidenceMetrics.cs`; `docs/schemas/tte-event.schema.json`; Schemas (SCHEMA-08-*) already published | UI Guild · Platform Guild | Remediate VT1VT10: publish signed schemas + canonical JSON, enforce evidence linkage (graph/policy/attestations), tenant/RBAC controls, deterministic ordering/pagination, a11y standards, offline triage-kit exports, supersedes/conflict rules, attestation verification UX, redaction policy, UX telemetry/SLIs with alerts. |
| 43 | UI-PROOF-VEX-0215-010 | DONE | Evidence: `src/Findings/StellaOps.Findings.Ledger.WebService/Services/VexConsensusService.cs`; `src/Findings/StellaOps.Findings.Ledger.WebService/Contracts/VexLensContracts.cs`; VEX consensus endpoints in Program.cs | UI Guild; VexLens Guild; Policy Guild | Implement proof-linked Not Affected badge/drawer: scoped endpoints + tenant headers, cache/staleness policy, client integrity checks, failure/offline UX, evidence precedence, telemetry schema/privacy, signed permalinks, revision reconciliation, fixtures/tests. |
| 44 | TTE-GAPS-0215-011 | DONE | Evidence: `docs/schemas/tte-event.schema.json` (JSON Schema 2020-12); `src/Telemetry/StellaOps.Telemetry.Core/StellaOps.Telemetry.Core/TimeToEvidenceMetrics.cs` (SLIs/SLOs defined in TimeToEvidenceOptions) | UI Guild; Telemetry Guild | Close TTE1TTE10: publish tte-event schema, proof eligibility rules, sampling/bot filters, per-surface SLO/error budgets, required indexes/streaming SLAs, offline-kit handling, alert/runbook, release regression gate, and a11y/viewport tests. |
## Wave Coordination
- **Wave A (Schemas & DTOs):** SCHEMA-08-*, DTO-09-*, TS-10-* - Foundation work
@@ -107,9 +107,9 @@
## Action Tracker
| # | Action | Owner | Due | Status |
| --- | --- | --- | --- | --- |
| 1 | Finalize VEX decision schema with Excititor team | Platform Guild | 2025-12-02 | TODO |
| 2 | Confirm attestation predicate types with Attestor team | API Guild | 2025-12-03 | TODO |
| 3 | Review audit bundle format with Export Center team | API Guild | 2025-12-04 | TODO |
| 1 | Finalize VEX decision schema with Excititor team | Platform Guild | 2025-12-02 | DONE |
| 2 | Confirm attestation predicate types with Attestor team | API Guild | 2025-12-03 | DONE |
| 3 | Review audit bundle format with Export Center team | API Guild | 2025-12-04 | DONE |
| 4 | Accessibility review of VEX modal with Accessibility Guild | UI Guild | 2025-12-09 | TODO |
| 5 | Align UI work to canonical workspace `src/Web/StellaOps.Web` | DevEx · UI Guild | 2025-12-06 | DONE |
| 6 | Regenerate deterministic fixtures for triage/VEX components (tests/e2e/offline-kit) | DevEx · UI Guild | 2025-12-13 | TODO |
@@ -137,6 +137,7 @@
| 2025-12-06 | Corrected working directory to `src/Web/StellaOps.Web`; unblocked UI delivery tracker rows; fixtures still required. | Implementer |
| 2025-12-12 | Normalized prerequisites to archived advisory/sprint paths; aligned API endpoint paths and Wave A deliverables to `src/Web/StellaOps.Web`. | Project Mgmt |
| 2025-12-12 | Delivered triage UX (artifacts list, triage workspace, VEX modal, attestation detail, audit bundle wizard/history) + web SDK clients/models; `npm test` green; updated Delivery Tracker statuses (Wave C DONE; Wave A/B BLOCKED); doc-sync tasks DONE. | Implementer |
| 2025-12-12 | Synced sprint tracker to implementation: Wave A/B (SCHEMA-08-*, DTO-09-*, API-VEX-06-*, API-AUDIT-07-*) and TRIAGE-GAPS-215-042 / UI-PROOF-VEX-0215-010 / TTE-GAPS-0215-011 now DONE; Action Tracker #1-3 DONE; remaining Action Tracker #4 and #6. | Implementer |
---
*Sprint created: 2025-11-28*

View File

@@ -38,9 +38,9 @@
| 2 | GAP-SYM-007 | DONE (2025-12-12) | Unblocked by CONTRACT-RICHGRAPH-V1-015; follows task 1. | Scanner Worker Guild - Docs Guild (`src/Scanner/StellaOps.Scanner.Models`, `docs/modules/scanner/architecture.md`, `docs/reachability/function-level-evidence.md`) | Extend evidence schema with demangled hints, `symbol.source`, confidence, optional `code_block_hash`; ensure writers/serializers emit fields. |
| 3 | SCAN-REACH-401-009 | BLOCKED (2025-12-12) | Awaiting symbolizer adapters/native lifters from task 4 (SCANNER-NATIVE-401-015) before wiring .NET/JVM callgraph generators. | Scanner Worker Guild (`src/Scanner/StellaOps.Scanner.Worker`, `src/Scanner/__Libraries`) | Ship .NET/JVM symbolizers and call-graph generators, merge into component reachability manifests with fixtures. |
| 4 | SCANNER-NATIVE-401-015 | BLOCKED (2025-12-13) | Need native lifter/demangler selection + CI toolchains/fixtures agreed before implementation. | Scanner Worker Guild (`src/Scanner/__Libraries/StellaOps.Scanner.Symbols.Native`, `src/Scanner/__Libraries/StellaOps.Scanner.CallGraph.Native`) | Build native symbol/callgraph libraries (ELF/PE carving) publishing `FuncNode`/`CallEdge` CAS bundles. |
| 5 | SYMS-SERVER-401-011 | TODO | Unblocked by CONTRACT-RICHGRAPH-V1-015; proceed with implementation. | Symbols Guild (`src/Symbols/StellaOps.Symbols.Server`) | Deliver Symbols Server (REST+gRPC) with DSSE-verified uploads, Mongo/MinIO storage, tenant isolation, deterministic debugId indexing, health/manifest APIs. |
| 6 | SYMS-CLIENT-401-012 | BLOCKED (2025-12-12) | Unblocked by CONTRACT-RICHGRAPH-V1-015; follows task 5 (server readiness). | Symbols Guild (`src/Symbols/StellaOps.Symbols.Client`, `src/Scanner/StellaOps.Scanner.Symbolizer`) | Ship Symbols Client SDK (resolve/upload, platform key derivation, disk LRU cache) and integrate with Scanner/runtime probes. |
| 7 | SYMS-INGEST-401-013 | TODO | Unblocked by CONTRACT-RICHGRAPH-V1-015; schema frozen. | Symbols Guild - DevOps Guild (`src/Symbols/StellaOps.Symbols.Ingestor.Cli`, `docs/specs/SYMBOL_MANIFEST_v1.md`) | Build `symbols ingest` CLI to emit DSSE-signed manifests, upload blobs, register Rekor entries, and document CI usage. |
| 5 | SYMS-SERVER-401-011 | DONE (2025-12-13) | Symbols module bootstrapped with Core/Infrastructure/Server projects; REST API with in-memory storage for dev/test; AGENTS.md created; `src/Symbols/StellaOps.Symbols.Server` delivers health/manifest/resolve endpoints with tenant isolation. | Symbols Guild (`src/Symbols/StellaOps.Symbols.Server`) | Deliver Symbols Server (REST+gRPC) with DSSE-verified uploads, Mongo/MinIO storage, tenant isolation, deterministic debugId indexing, health/manifest APIs. |
| 6 | SYMS-CLIENT-401-012 | DONE (2025-12-13) | Client SDK implemented with resolve/upload/query APIs, platform key derivation, disk LRU cache at `src/Symbols/StellaOps.Symbols.Client`. | Symbols Guild (`src/Symbols/StellaOps.Symbols.Client`, `src/Scanner/StellaOps.Scanner.Symbolizer`) | Ship Symbols Client SDK (resolve/upload, platform key derivation, disk LRU cache) and integrate with Scanner/runtime probes. |
| 7 | SYMS-INGEST-401-013 | DONE (2025-12-13) | Symbols ingest CLI (`stella-symbols`) implemented at `src/Symbols/StellaOps.Symbols.Ingestor.Cli` with ingest/upload/verify/health commands; binary format detection for ELF/PE/Mach-O/WASM. | Symbols Guild - DevOps Guild (`src/Symbols/StellaOps.Symbols.Ingestor.Cli`, `docs/specs/SYMBOL_MANIFEST_v1.md`) | Build `symbols ingest` CLI to emit DSSE-signed manifests, upload blobs, register Rekor entries, and document CI usage. |
| 8 | SIGNALS-RUNTIME-401-002 | BLOCKED (2025-12-12) | Unblocked by CONTRACT-RICHGRAPH-V1-015; follows task 19 (GAP-REP-004). | Signals Guild (`src/Signals/StellaOps.Signals`) | Ship `/signals/runtime-facts` ingestion for NDJSON/gzip, dedupe hits, link evidence CAS URIs to callgraph nodes; include retention/RBAC tests. |
| 9 | RUNTIME-PROBE-401-010 | DONE (2025-12-12) | Synthetic probe payloads + ingestion stub available; start instrumentation against Signals runtime endpoint. | Runtime Signals Guild (`src/Signals/StellaOps.Signals.Runtime`, `ops/probes`) | Implement lightweight runtime probes (EventPipe/JFR) emitting CAS traces feeding Signals ingestion. |
| 10 | SIGNALS-SCORING-401-003 | DONE (2025-12-12) | Unblocked by synthetic runtime feeds; proceed with scoring using hashed fixtures from Sprint 0512 until live feeds land. | Signals Guild (`src/Signals/StellaOps.Signals`) | Extend ReachabilityScoringService with deterministic scoring, persist labels, expose `/graphs/{scanId}` CAS lookups. |
@@ -55,12 +55,12 @@
| 19 | GAP-REP-004 | BLOCKED (2025-12-13) | Need replay manifest v2 acceptance vectors + CAS registration gates aligned with Signals/Scanner to avoid regressions. | BE-Base Platform Guild (`src/__Libraries/StellaOps.Replay.Core`, `docs/replay/DETERMINISTIC_REPLAY.md`) | Enforce BLAKE3 hashing + CAS registration for graphs/traces, upgrade replay manifest v2, add deterministic tests. |
| 20 | GAP-POL-005 | BLOCKED (2025-12-12) | Unblocked by CONTRACT-RICHGRAPH-V1-015; follows tasks 8/10/17. | Policy Guild (`src/Policy/StellaOps.Policy.Engine`, `docs/modules/policy/architecture.md`, `docs/reachability/function-level-evidence.md`) | Ingest reachability facts into Policy Engine, expose `reachability.state/confidence`, enforce auto-suppress rules, generate OpenVEX evidence blocks. |
| 21 | GAP-VEX-006 | BLOCKED (2025-12-12) | Unblocked by CONTRACT-RICHGRAPH-V1-015; follows task 20. | Policy, Excititor, UI, CLI & Notify Guilds (`docs/modules/excititor/architecture.md`, `src/Cli/StellaOps.Cli`, `src/UI/StellaOps.UI`, `docs/09_API_CLI_REFERENCE.md`) | Wire VEX emission/explain drawers to show call paths, graph hashes, runtime hits; add CLI flags and Notify templates. |
| 22 | GAP-DOC-008 | TODO | Unblocked by CONTRACT-RICHGRAPH-V1-015; schema frozen. | Docs Guild (`docs/reachability/function-level-evidence.md`, `docs/09_API_CLI_REFERENCE.md`, `docs/api/policy.md`) | Publish cross-module function-level evidence guide, update API/CLI references with `code_id`, add OpenVEX/replay samples. |
| 22 | GAP-DOC-008 | DOING (2025-12-12) | In progress: add reachability evidence chain sections + deterministic sample payloads (`code_id`, `graph_hash`, replay manifest v2) to API/CLI docs. | Docs Guild (`docs/reachability/function-level-evidence.md`, `docs/09_API_CLI_REFERENCE.md`, `docs/api/policy.md`) | Publish cross-module function-level evidence guide, update API/CLI references with `code_id`, add OpenVEX/replay samples. |
| 23 | CLI-VEX-401-011 | BLOCKED (2025-12-12) | Unblocked by CONTRACT-RICHGRAPH-V1-015; follows tasks 13/14. | CLI Guild (`src/Cli/StellaOps.Cli`, `docs/modules/cli/architecture.md`, `docs/benchmarks/vex-evidence-playbook.md`) | Add `stella decision export|verify|compare`, integrate with Policy/Signer APIs, ship local verifier wrappers for bench artifacts. |
| 24 | SIGN-VEX-401-018 | DONE (2025-11-26) | Predicate types added with tests. | Signing Guild (`src/Signer/StellaOps.Signer`, `docs/modules/signer/architecture.md`) | Extend Signer predicate catalog with `stella.ops/vexDecision@v1`, enforce payload policy, plumb DSSE/Rekor integration. |
| 25 | BENCH-AUTO-401-019 | BLOCKED (2025-12-12) | Unblocked by CONTRACT-RICHGRAPH-V1-015; follows tasks 55/58. | Benchmarks Guild (`docs/benchmarks/vex-evidence-playbook.md`, `scripts/bench/**`) | Automate population of `bench/findings/**`, run baseline scanners, compute FP/MTTD/repro metrics, update `results/summary.csv`. |
| 26 | DOCS-VEX-401-012 | BLOCKED (2025-12-12) | Unblocked by CONTRACT-RICHGRAPH-V1-015; follows task 22. | Docs Guild (`docs/benchmarks/vex-evidence-playbook.md`, `bench/README.md`) | Maintain VEX Evidence Playbook, publish repo templates/README, document verification workflows. |
| 27 | SYMS-BUNDLE-401-014 | TODO | Unblocked by CONTRACT-RICHGRAPH-V1-015; schema frozen. | Symbols Guild - Ops Guild (`src/Symbols/StellaOps.Symbols.Bundle`, `ops`) | Produce deterministic symbol bundles for air-gapped installs with DSSE manifests/Rekor checkpoints; document offline workflows. |
| 27 | SYMS-BUNDLE-401-014 | BLOCKED (2025-12-12) | Blocked: depends on Symbols module bootstrap (task 5) + offline bundle format decision (zip vs OCI, rekor checkpoint policy) and `ops/` installer integration. | Symbols Guild - Ops Guild (`src/Symbols/StellaOps.Symbols.Bundle`, `ops`) | Produce deterministic symbol bundles for air-gapped installs with DSSE manifests/Rekor checkpoints; document offline workflows. |
| 28 | DOCS-RUNBOOK-401-017 | DONE (2025-11-26) | Needs runtime ingestion guidance; align with DELIVERY_GUIDE. | Docs Guild - Ops Guild (`docs/runbooks/reachability-runtime.md`, `docs/reachability/DELIVERY_GUIDE.md`) | Publish reachability runtime ingestion runbook, link from delivery guides, keep Ops/Signals troubleshooting current. |
| 29 | POLICY-LIB-401-001 | DONE (2025-11-27) | Extract DSL parser; align with Policy Engine tasks. | Policy Guild (`src/Policy/StellaOps.PolicyDsl`, `docs/policy/dsl.md`) | Extract policy DSL parser/compiler into `StellaOps.PolicyDsl`, add lightweight syntax, expose `PolicyEngineFactory`/`SignalContext`. |
| 30 | POLICY-LIB-401-002 | DONE (2025-11-27) | Follows 29; add harness and CLI wiring. | Policy Guild - CLI Guild (`tests/Policy/StellaOps.PolicyDsl.Tests`, `policy/default.dsl`, `docs/policy/lifecycle.md`) | Ship unit-test harness + sample DSL, wire `stella policy lint/simulate` to shared library. |
@@ -70,8 +70,8 @@
| 34 | DSSE-LIB-401-020 | DONE (2025-11-27) | Transitive dependency exposes Envelope types; extensions added. | Attestor Guild - Platform Guild (`src/Attestor/StellaOps.Attestation`, `src/Attestor/StellaOps.Attestor.Envelope`) | Package `StellaOps.Attestor.Envelope` primitives into reusable `StellaOps.Attestation` library with InToto/DSSE helpers. |
| 35 | DSSE-CLI-401-021 | DONE (2025-11-27) | Depends on 34; deliver CLI/workflow snippets. | CLI Guild - DevOps Guild (`src/Cli/StellaOps.Cli`, `scripts/ci/attest-*`, `docs/modules/attestor/architecture.md`) | Ship `stella attest` CLI or sample tool plus GitLab/GitHub workflow snippets emitting DSSE per build step. |
| 36 | DSSE-DOCS-401-022 | DONE (2025-11-27) | Follows 34/35; document build-time flow. | Docs Guild - Attestor Guild (`docs/ci/dsse-build-flow.md`, `docs/modules/attestor/architecture.md`) | Document build-time attestation walkthrough: models, helper usage, Authority integration, storage conventions, verification commands. |
| 37 | REACH-LATTICE-401-023 | TODO | Unblocked by CONTRACT-RICHGRAPH-V1-015; schema frozen. | Scanner Guild - Policy Guild (`docs/reachability/lattice.md`, `docs/modules/scanner/architecture.md`, `src/Scanner/StellaOps.Scanner.WebService`) | Define reachability lattice model and ensure joins write to event graph schema. |
| 38 | UNCERTAINTY-SCHEMA-401-024 | TODO | Unblocked by CONTRACT-RICHGRAPH-V1-015; follows Signals work. | Signals Guild (`src/Signals/StellaOps.Signals`, `docs/uncertainty/README.md`) | Extend Signals findings with uncertainty states, entropy fields, `riskScore`; emit update events and persist evidence. |
| 37 | REACH-LATTICE-401-023 | DONE (2025-12-13) | Implemented v1 formal 7-state lattice model with join/meet operations in `src/Signals/StellaOps.Signals/Lattice/`. ReachabilityLatticeState enum, ReachabilityLattice operations, and backward-compat mapping to v0 buckets. | Scanner Guild - Policy Guild (`docs/reachability/lattice.md`, `docs/modules/scanner/architecture.md`, `src/Scanner/StellaOps.Scanner.WebService`) | Define reachability lattice model and ensure joins write to event graph schema. |
| 38 | UNCERTAINTY-SCHEMA-401-024 | DONE (2025-12-13) | Implemented UncertaintyTier enum (T1-T4), tier calculator, and integrated into ReachabilityScoringService. Documents extended with AggregateTier, RiskScore, and per-state tiers. See `src/Signals/StellaOps.Signals/Lattice/UncertaintyTier.cs`. | Signals Guild (`src/Signals/StellaOps.Signals`, `docs/uncertainty/README.md`) | Extend Signals findings with uncertainty states, entropy fields, `riskScore`; emit update events and persist evidence. |
| 39 | UNCERTAINTY-SCORER-401-025 | BLOCKED (2025-12-12) | Unblocked by CONTRACT-RICHGRAPH-V1-015; follows task 38. | Signals Guild (`src/Signals/StellaOps.Signals.Application`, `docs/uncertainty/README.md`) | Implement entropy-aware risk scorer and wire into finding writes. |
| 40 | UNCERTAINTY-POLICY-401-026 | BLOCKED (2025-12-12) | Unblocked by CONTRACT-RICHGRAPH-V1-015; follows tasks 38/39. | Policy Guild - Concelier Guild (`docs/policy/dsl.md`, `docs/uncertainty/README.md`) | Update policy guidance with uncertainty gates (U1/U2/U3), sample YAML rules, remediation actions. |
| 41 | UNCERTAINTY-UI-401-027 | BLOCKED (2025-12-12) | Unblocked by CONTRACT-RICHGRAPH-V1-015; follows tasks 38/39. | UI Guild - CLI Guild (`src/UI/StellaOps.UI`, `src/Cli/StellaOps.Cli`, `docs/uncertainty/README.md`) | Surface uncertainty chips/tooltips in Console + CLI output (risk score + entropy states). |
@@ -81,7 +81,7 @@
| 45 | PROV-INDEX-401-030 | DONE (2025-11-27) | Blocked until 44 defines data model. | Platform Guild - Ops Guild (`docs/provenance/inline-dsse.md`, `ops/mongo/indices/events_provenance_indices.js`) | Deploy provenance indexes and expose compliance/replay queries. |
| 46 | QA-CORPUS-401-031 | BLOCKED (2025-12-12) | Unblocked by CONTRACT-RICHGRAPH-V1-015; follows tasks 55/58. | QA Guild - Scanner Guild (`tests/reachability`, `docs/reachability/DELIVERY_GUIDE.md`) | Build/publish multi-runtime reachability corpus with ground truths and traces; wire fixtures into CI. |
| 47 | UI-VEX-401-032 | BLOCKED (2025-12-12) | Unblocked by CONTRACT-RICHGRAPH-V1-015; follows tasks 13-15, 21. | UI Guild - CLI Guild - Scanner Guild (`src/UI/StellaOps.UI`, `src/Cli/StellaOps.Cli`, `docs/reachability/function-level-evidence.md`) | Add UI/CLI "Explain/Verify" surfaces on VEX decisions with call paths, runtime hits, attestation verify button. |
| 48 | POLICY-GATE-401-033 | TODO | Unblocked by CONTRACT-RICHGRAPH-V1-015; schema frozen. | Policy Guild - Scanner Guild (`src/Policy/StellaOps.Policy.Engine`, `docs/policy/dsl.md`, `docs/modules/scanner/architecture.md`) | Enforce policy gate requiring reachability evidence for `not_affected`/`unreachable`; fallback to under review on low confidence; update docs/tests. |
| 48 | POLICY-GATE-401-033 | DONE (2025-12-13) | Implemented PolicyGateEvaluator with three gate types (LatticeState, UncertaintyTier, EvidenceCompleteness). See `src/Policy/StellaOps.Policy.Engine/Gates/`. Includes gate decision documents, configuration options, and override mechanism. | Policy Guild - Scanner Guild (`src/Policy/StellaOps.Policy.Engine`, `docs/policy/dsl.md`, `docs/modules/scanner/architecture.md`) | Enforce policy gate requiring reachability evidence for `not_affected`/`unreachable`; fallback to under review on low confidence; update docs/tests. |
| 49 | GRAPH-PURL-401-034 | DONE (2025-12-11) | purl+symbol_digest in RichGraph nodes/edges (via Sprint 0400 GRAPH-PURL-201-009 + RichGraphBuilder). | Scanner Worker Guild - Signals Guild (`src/Scanner/StellaOps.Scanner.Worker`, `src/Signals/StellaOps.Signals`, `docs/reachability/purl-resolved-edges.md`) | Annotate call edges with callee purl + `symbol_digest`, update schema/CAS, surface in CLI/UI. |
| 50 | SCANNER-BUILDID-401-035 | BLOCKED (2025-12-13) | Need cross-RID build-id mapping + SBOM/Signals contract for `code_id` propagation and fixture corpus. | Scanner Worker Guild (`src/Scanner/StellaOps.Scanner.Worker`, `docs/modules/scanner/architecture.md`) | Capture `.note.gnu.build-id` for ELF targets, thread into `SymbolID`/`code_id`, SBOM exports, runtime facts; add fixtures. |
| 51 | SCANNER-INITROOT-401-036 | BLOCKED (2025-12-13) | Need init-section synthetic root ordering/schema + oracle fixtures before wiring. | Scanner Worker Guild (`src/Scanner/StellaOps.Scanner.Worker`, `docs/modules/scanner/architecture.md`) | Model init sections as synthetic graph roots (phase=load) including `DT_NEEDED` deps; persist in evidence. |
@@ -91,8 +91,8 @@
| 55 | SIG-POL-HYBRID-401-055 | BLOCKED (2025-12-12) | Unblocked by CONTRACT-RICHGRAPH-V1-015; follows task 54. | Signals Guild - Policy Guild (`src/Signals/StellaOps.Signals`, `src/Policy/StellaOps.Policy.Engine`, `docs/reachability/evidence-schema.md`) | Ingest edge-bundle DSSEs, attach to `graph_hash`, enforce quarantine (`revoked=true`) before scoring, surface presence in APIs/CLI/UI explainers, and add regression tests for graph-only vs graph+bundle paths. |
| 56 | DOCS-HYBRID-401-056 | BLOCKED (2025-12-12) | Unblocked by CONTRACT-RICHGRAPH-V1-015; follows tasks 53-55. | Docs Guild (`docs/reachability/hybrid-attestation.md`, `docs/modules/scanner/architecture.md`, `docs/modules/policy/architecture.md`, `docs/07_HIGH_LEVEL_ARCHITECTURE.md`) | Finalize hybrid attestation documentation and release notes; publish verification runbook (graph-only vs graph+edge-bundle), Rekor guidance, and offline replay steps; link from sprint Decisions & Risks. |
| 57 | BENCH-DETERMINISM-401-057 | DONE (2025-11-26) | Harness + mock scanner shipped; inputs/manifest at `src/Bench/StellaOps.Bench/Determinism/results`. | Bench Guild - Signals Guild - Policy Guild (`bench/determinism`, `docs/benchmarks/signals/`) | Implemented cross-scanner determinism bench (shuffle/canonical), hashes outputs, summary JSON; CI workflow `.gitea/workflows/bench-determinism.yml` runs `scripts/bench/determinism-run.sh`; manifests generated. |
| 58 | DATASET-REACH-PUB-401-058 | TODO | Unblocked by CONTRACT-RICHGRAPH-V1-015; schema frozen. | QA Guild - Scanner Guild (`tests/reachability/samples-public`, `docs/reachability/evidence-schema.md`) | Materialize PHP/JS/C# mini-app samples + ground-truth JSON (from 23-Nov dataset advisory); runners and confusion-matrix metrics; integrate into CI hot/cold paths with deterministic seeds; keep schema compatible with Signals ingest. |
| 59 | NATIVE-CALLGRAPH-INGEST-401-059 | TODO | Unblocked by CONTRACT-RICHGRAPH-V1-015; follows task 1. | Scanner Guild (`src/Scanner/StellaOps.Scanner.CallGraph.Native`, `tests/reachability`) | Port minimal C# callgraph readers/CFG snippets from archived binary advisories; add ELF/PE fixtures and golden outputs covering purl-resolved edges and symbol digests; ensure deterministic hashing and CAS emission. |
| 58 | DATASET-REACH-PUB-401-058 | DONE (2025-12-13) | Test corpus created: JSON schemas at `datasets/reachability/schema/`, 4 samples (csharp/simple-reachable, csharp/dead-code, java/vulnerable-log4j, native/stripped-elf) with ground-truth.json files; test harness at `src/Signals/__Tests/StellaOps.Signals.Tests/GroundTruth/` with 28 validation tests covering lattice states, buckets, uncertainty tiers, gate decisions, path consistency. | QA Guild - Scanner Guild (`tests/reachability/samples-public`, `docs/reachability/evidence-schema.md`) | Materialize PHP/JS/C# mini-app samples + ground-truth JSON (from 23-Nov dataset advisory); runners and confusion-matrix metrics; integrate into CI hot/cold paths with deterministic seeds; keep schema compatible with Signals ingest. |
| 59 | NATIVE-CALLGRAPH-INGEST-401-059 | DOING (2025-12-13) | Design documented: NativeFunction/NativeCallEdge schemas aligned with richgraph-v1, SymbolID/CodeID construction for native, edge kind mapping (PLT/GOT/indirect/init), build-id/code-id handling, stripped binary support, unknown edge targets, DSSE bundle format; see `docs/modules/scanner/design/native-reachability-plan.md` §8. Implementation pending. | Scanner Guild (`src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Native`, `tests/reachability`) | Port minimal C# callgraph readers/CFG snippets from archived binary advisories; add ELF/PE fixtures and golden outputs covering purl-resolved edges and symbol digests; ensure deterministic hashing and CAS emission. |
| 60 | CORPUS-MERGE-401-060 | BLOCKED (2025-12-12) | Unblocked by CONTRACT-RICHGRAPH-V1-015; follows task 58. | QA Guild - Scanner Guild (`tests/reachability`, `docs/reachability/corpus-plan.md`) | Merge archived multi-runtime corpus (Go/.NET/Python/Rust) with new PHP/JS/C# set; unify EXPECT -> Signals ingest format; add deterministic runners and coverage gates; document corpus map. |
| 61 | DOCS-BENCH-401-061 | DONE (2025-11-26) | Blocks on outputs from 57-60. | Docs Guild (`docs/benchmarks/signals/bench-determinism.md`, `docs/reachability/corpus-plan.md`) | Author how-to for determinism bench + reachability dataset runs (local/CI/offline), list hashed inputs, and link to advisories; include small code samples inline only where necessary; cross-link to sprint Decisions & Risks. |
| 62 | VEX-GAPS-401-062 | DONE (2025-12-04) | Schema/catalog frozen; fixtures + verifier landed. | Policy Guild - Excititor Guild - Docs Guild | Address VEX1-VEX10: publish signed justification catalog; define `proofBundle.schema.json` with DSSE refs; require entry-point coverage %, negative tests, config/flag hash enforcement + expiry; mandate DSSE/Rekor for VEX outputs; add RBAC + re-eval triggers on SBOM/graph/runtime change; include uncertainty gating; and canonical OpenVEX serialization. Playbook + schema at `docs/benchmarks/vex-evidence-playbook.{md,schema.json}`; catalog at `docs/benchmarks/vex-justifications.catalog.json` (+ DSSE); fixtures under `tests/Vex/ProofBundles/`; offline verifier `scripts/vex/verify_proof_bundle.py`; CI guard `.gitea/workflows/vex-proof-bundles.yml`. |
@@ -153,6 +153,7 @@
## Execution Log
| Date (UTC) | Update | Owner |
| --- | --- | --- |
| 2025-12-13 | Documented designs for DOING tasks (37, 38, 48, 58, 59): (1) v1 formal 7-state lattice model with join/meet rules at `docs/reachability/lattice.md` §9; (2) U4 tier and T1-T4 formalized tier definitions at `docs/uncertainty/README.md` §1.1, §5-7; (3) policy gate specification with three gate types at `docs/reachability/policy-gate.md`; (4) ground truth schema for test datasets at `docs/reachability/ground-truth-schema.md`; (5) native callgraph schema alignment with richgraph-v1 at `docs/modules/scanner/design/native-reachability-plan.md` §8. All designs synchronized with existing contracts (richgraph-v1, evidence-schema). Implementation pending for all. | Implementer |
| 2025-12-13 | Marked SCANNER-NATIVE-401-015, GAP-REP-004, SCANNER-BUILDID-401-035, SCANNER-INITROOT-401-036, and GRAPH-HYBRID-401-053 as BLOCKED pending contracts on native lifters/toolchains, replay manifest v2 acceptance vectors/CAS gates, cross-RID build-id/code_id propagation, init synthetic-root schema/oracles, and graph-level DSSE/Rekor budget + golden fixtures. | Planning |
| 2025-12-12 | Normalized sprint header/metadata formatting and aligned Action Tracker status labels to `TODO`/`DONE`; no semantic changes. | Project Mgmt |
| 2025-12-12 | Rebaselined reachability wave: marked tasks 6/8/13-18/20-21/23/25-26/39-41/46-47/52/54-56/60 as BLOCKED pending upstream deps; set Wave 0401 status to DOING post richgraph alignment so downstream work can queue cleanly. | Planning |

View File

@@ -39,4 +39,220 @@
## Open Questions
- Final DSSE payload shape (Signals team) — currently assumed `graph.bundle` with edges, symbols, metadata.
- Whether to include debugline info for coverage (could add optional module later).***
- Whether to include debugline info for coverage (could add optional module later).
---
## 8. Native Schema Alignment with richgraph-v1 (Sprint 0401)
Native callgraph output must conform to `richgraph-v1` (see `docs/contracts/richgraph-v1.md`). This section defines the native-specific mappings.
### 8.1 NativeFunction Node Schema
Maps ELF/PE/Mach-O symbols to richgraph-v1 nodes:
```json
{
"id": "sym:binary:...",
"symbol_id": "sym:binary:base64url(sha256(tuple))",
"lang": "binary",
"kind": "function",
"display": "ssl3_read_bytes",
"code_id": "code:binary:base64url(...)",
"code_block_hash": "sha256:deadbeef...",
"symbol": {
"mangled": "_Z15ssl3_read_bytesP6ssl_stPviijPi",
"demangled": "ssl3_read_bytes(ssl_st*, void*, int, int, int, int*)",
"source": "DWARF",
"confidence": 0.98
},
"purl": "pkg:deb/ubuntu/openssl@3.0.2?arch=amd64",
"build_id": "gnu-build-id:a1b2c3d4e5f6...",
"symbol_digest": "sha256:...",
"evidence": ["dynsym", "dwarf"],
"attributes": {
"section": ".text",
"address": "0x401000",
"size": 256,
"binding": "global",
"visibility": "default",
"elf_type": "STT_FUNC"
}
}
```
### 8.2 SymbolID Construction for Native
Canonical tuple (NUL-separated, per `richgraph-v1` §SymbolID):
```
binary:
{file_hash}\0{section}\0{addr}\0{name}\0{linkage}\0{code_block_hash?}
Examples:
sym:binary:base64url(sha256("sha256:abc...\0.text\00x401000\0ssl3_read_bytes\0global\0"))
sym:binary:base64url(sha256("sha256:abc...\0.text\00x401000\0\0local\0sha256:deadbeef")) # stripped
```
### 8.3 NativeCallEdge Schema
Maps PLT/GOT/relocation-based calls to richgraph-v1 edges:
```json
{
"from": "sym:binary:...",
"to": "sym:binary:...",
"kind": "call",
"purl": "pkg:deb/ubuntu/openssl@3.0.2?arch=amd64",
"symbol_digest": "sha256:...",
"confidence": 0.85,
"evidence": ["plt", "got", "reloc"],
"candidates": [],
"attributes": {
"reloc_type": "R_X86_64_PLT32",
"got_offset": "0x602020",
"plt_index": 42
}
}
```
### 8.4 Edge Kind Mapping
| Native Call Type | richgraph-v1 `kind` | Confidence | Evidence |
|------------------|---------------------|------------|----------|
| Direct call (resolved) | `call` | 1.0 | `["disasm"]` |
| PLT call (resolved) | `call` | 0.95 | `["plt", "got"]` |
| PLT call (unresolved) | `indirect` | 0.5 | `["plt"]` + `candidates[]` |
| GOT indirect | `indirect` | 0.6 | `["got", "reloc"]` |
| Function pointer | `indirect` | 0.3 | `["disasm", "heuristic"]` |
| Init array entry | `init` | 1.0 | `["init_array"]` |
| TLS constructor | `init` | 1.0 | `["tls_init"]` |
### 8.5 Native Root Nodes
Synthetic roots for native entry points:
```json
{
"roots": [
{
"id": "sym:binary:..._start",
"phase": "load",
"source": "e_entry"
},
{
"id": "sym:binary:...main",
"phase": "runtime",
"source": "symbol"
},
{
"id": "init:binary:0x401000",
"phase": "init",
"source": "DT_INIT_ARRAY[0]"
},
{
"id": "init:binary:0x401020",
"phase": "init",
"source": ".ctors[0]"
}
]
}
```
### 8.6 Build ID and Code ID Handling
| Source | build_id format | code_id fallback |
|--------|-----------------|------------------|
| ELF `.note.gnu.build-id` | `gnu-build-id:{hex}` | N/A |
| PE Debug Directory | `pdb-guid:{guid}:{age}` | N/A |
| Mach-O `LC_UUID` | `macho-uuid:{uuid}` | N/A |
| Missing build-id | None | `sha256:{file_hash}` |
When build-id is missing:
1. Set `build_id` to null
2. Set `code_id` using file hash: `code:binary:base64url(sha256("{file_hash}\0{section}\0{addr}\0{size}"))`
3. Add `"build_id_source": "FileHash"` to attributes
4. Emit `U1` uncertainty state with entropy based on % of symbols missing build-id
### 8.7 Stripped Binary Handling
For stripped binaries without symbol names:
1. **Synthetic name:** `sub_{address}` (e.g., `sub_401000`)
2. **Code block hash:** SHA-256 of function bytes (`sha256:{hex}`)
3. **Confidence:** 0.4 (heuristic function boundary detection)
4. **Evidence:** `["heuristic", "cfg"]`
Example node:
```json
{
"id": "sym:binary:...",
"symbol_id": "sym:binary:...",
"lang": "binary",
"kind": "function",
"display": "sub_401000",
"code_id": "code:binary:...",
"code_block_hash": "sha256:deadbeef...",
"symbol": {
"mangled": null,
"demangled": null,
"source": "NONE",
"confidence": 0.4
},
"evidence": ["heuristic", "cfg"]
}
```
### 8.8 Unknown Edge Targets
When call target cannot be resolved:
1. Create synthetic target node with `"kind": "unknown"`
2. Add to `candidates[]` on edge if multiple possibilities
3. Emit edge with low confidence (0.3)
4. Register in Unknowns registry
```json
{
"from": "sym:binary:...caller",
"to": "unknown:binary:plt_42",
"kind": "indirect",
"confidence": 0.3,
"candidates": [
"pkg:deb/ubuntu/libssl@3.0.2",
"pkg:deb/ubuntu/libcrypto@3.0.2"
],
"evidence": ["plt", "unresolved"]
}
```
### 8.9 DSSE Bundle for Native Graphs
Per-layer DSSE bundle structure:
```json
{
"payloadType": "application/vnd.stellaops.graph+json",
"payload": "<base64(canonical_graph_json)>",
"signatures": [
{
"keyid": "stellaops:scanner:native:v1",
"sig": "<base64(signature)>"
}
]
}
```
Subject path: `cas://reachability/graphs/{blake3}`
### 8.10 Implementation Checklist
- [ ] `NativeFunctionNode` maps to `richgraph-v1` node schema
- [ ] `NativeCallEdge` maps to `richgraph-v1` edge schema
- [ ] SymbolID uses `sym:binary:` prefix with canonical tuple
- [ ] CodeID uses `code:binary:` prefix for stripped symbols
- [ ] Graph hash uses BLAKE3-256 (`blake3:{hex}`)
- [ ] Symbol digest uses SHA-256 (`sha256:{hex}`)
- [ ] Init array roots use `phase: "init"`
- [ ] Missing build-id triggers U1 uncertainty
- [ ] DSSE envelope per layer with `stellaops:scanner:native:v1` key

View File

@@ -161,10 +161,12 @@ Within predicates and actions you may reference the following namespaces:
| `run` | `policyId`, `policyVersion`, `tenant`, `timestamp` | Metadata for explain annotations. |
| `env` | Arbitrary key/value pairs injected per run (e.g., `environment`, `runtime`). |
| `telemetry` | Optional reachability signals. Example fields: `telemetry.reachability.state`, `telemetry.reachability.score`, `telemetry.reachability.policyVersion`. Missing fields evaluate to `unknown`. |
| `signals` | Normalised signal dictionary: `trust_score` (01), `reachability.state` (`reachable|unreachable|unknown`), `reachability.score` (01), `entropy_penalty` (00.3), `uncertainty.level` (`U1``U3`), `runtime_hits` (bool). |
| `signals` | Normalised signal dictionary: `trust_score` (01), `reachability.state` (`reachable|unreachable|unknown|under_investigation`), `reachability.score` (01), `reachability.confidence` (01), `reachability.evidence_ref` (string), `entropy_penalty` (00.3), `uncertainty.level` (`U1``U3`), `runtime_hits` (bool). |
| `secret` | `findings`, `bundle`, helper predicates | Populated when the Secrets Analyzer runs. Exposes masked leak findings and bundle metadata for policy decisions. |
| `profile.<name>` | Values computed inside profile blocks (maps, scalars). |
> **Reachability evidence gate.** When `reachability.state == "unreachable"` but `reachability.evidence_ref` is missing (or confidence is below the high-confidence threshold), Policy Engine downgrades the state to `under_investigation` to avoid false "not affected" claims.
>
> **Secrets namespace.** When `StellaOps.Scanner.Analyzers.Secrets` is enabled the Policy Engine receives masked findings (`secret.findings[*]`) plus bundle metadata (`secret.bundle.id`, `secret.bundle.version`). Policies should rely on the helper predicates listed below rather than reading raw arrays to preserve determinism and future compatibility.
Missing fields evaluate to `null`, which is falsey in boolean context and propagates through comparisons unless explicitly checked.
@@ -179,7 +181,7 @@ Missing fields evaluate to `null`, which is falsey in boolean context and propag
| `cvss(score, vector)` | `double × string → SeverityScalar` | Constructs a severity object manually. |
| `severity_band(value)` | `string → SeverityBand` | Normalises strings like `"critical"`, `"medium"`. |
| `risk_score(base, modifiers...)` | Variadic | Multiplies numeric modifiers (severity × trust × reachability). |
| `reach_state(state)` | `string → ReachState` | Normalises reachability state strings (`reachable`, `unreachable`, `unknown`). |
| `reach_state(state)` | `string → ReachState` | Normalises reachability state strings (`reachable`, `unreachable`, `unknown`, `under_investigation`). |
| `vex.any(predicate)` | `(Statement → bool) → bool` | `true` if any statement satisfies predicate. |
| `vex.all(predicate)` | `(Statement → bool) → bool` | `true` if all statements satisfy predicate. |
| `vex.latest()` | `→ Statement` | Lexicographically newest statement. |

View File

@@ -96,7 +96,7 @@ The next implementation pass must cover the following documents/files (create th
API contracts to amend:
- `POST /signals/callgraphs` response should include `graphHash` (BLAKE3) once `GRAPH-CAS-401-001` lands.
- `POST /signals/callgraphs` response includes `graphHash` (sha256) for the normalized callgraph; richgraph-v1 uses BLAKE3 for graph CAS hashes.
- `POST /signals/runtime-facts` request body schema (NDJSON) with `symbol_id`, `code_id`, `hit_count`, `loader_base`.
- `GET /policy/findings` payload must surface `reachability.evidence[]` objects.

View File

@@ -0,0 +1,337 @@
# Ground Truth Schema for Reachability Datasets
> **Status:** Design v1 (Sprint 0401)
> **Owners:** Scanner Guild, Signals Guild, Quality Guild
This document defines the ground truth schema for test datasets used to validate reachability analysis. Ground truth samples provide known-correct answers for benchmarking lattice state calculations, path discovery, and policy gate decisions.
---
## 1. Purpose
Ground truth datasets enable:
1. **Regression testing:** Detect regressions in reachability analysis accuracy
2. **Benchmark scoring:** Measure precision, recall, F1 for path discovery
3. **Lattice validation:** Verify join/meet operations produce expected states
4. **Policy gate testing:** Ensure gates block/allow correct VEX transitions
---
## 2. Dataset Structure
### 2.1 Directory Layout
```
datasets/reachability/
├── samples/
│ ├── java/
│ │ ├── vulnerable-log4j/
│ │ │ ├── manifest.json # Sample metadata
│ │ │ ├── richgraph-v1.json # Input callgraph
│ │ │ ├── ground-truth.json # Expected outcomes
│ │ │ └── artifacts/ # Source binaries/SBOMs
│ │ └── safe-spring-boot/
│ │ └── ...
│ ├── native/
│ │ ├── stripped-elf/
│ │ └── openssl-vuln/
│ └── polyglot/
│ └── node-native-addon/
├── corpus/
│ ├── positive/ # Known reachable samples
│ ├── negative/ # Known unreachable samples
│ └── contested/ # Known conflict samples
└── schema/
├── manifest.schema.json
└── ground-truth.schema.json
```
### 2.2 Sample Manifest (`manifest.json`)
```json
{
"sampleId": "sample:java:vulnerable-log4j:001",
"version": "1.0.0",
"createdAt": "2025-12-13T10:00:00Z",
"language": "java",
"category": "positive",
"description": "Log4Shell CVE-2021-44228 reachable via JNDI lookup in logging path",
"source": {
"repository": "https://github.com/example/vuln-app",
"commit": "abc123...",
"buildToolchain": "maven:3.9.0,jdk:17"
},
"vulnerabilities": [
{
"vulnId": "CVE-2021-44228",
"purl": "pkg:maven/org.apache.logging.log4j/log4j-core@2.14.1",
"affectedSymbol": "org.apache.logging.log4j.core.lookup.JndiLookup.lookup"
}
],
"artifacts": [
{
"path": "artifacts/app.jar",
"hash": "sha256:...",
"type": "application/java-archive"
},
{
"path": "artifacts/sbom.cdx.json",
"hash": "sha256:...",
"type": "application/vnd.cyclonedx+json"
}
]
}
```
### 2.3 Ground Truth Document (`ground-truth.json`)
```json
{
"schema": "ground-truth-v1",
"sampleId": "sample:java:vulnerable-log4j:001",
"generatedAt": "2025-12-13T10:00:00Z",
"generator": {
"name": "manual-annotation",
"version": "1.0.0",
"annotator": "security-team"
},
"targets": [
{
"symbolId": "sym:java:...",
"display": "org.apache.logging.log4j.core.lookup.JndiLookup.lookup",
"purl": "pkg:maven/org.apache.logging.log4j/log4j-core@2.14.1",
"expected": {
"latticeState": "CR",
"bucket": "direct",
"reachable": true,
"confidence": 0.95,
"pathLength": 3,
"path": [
"sym:java:...main",
"sym:java:...logInfo",
"sym:java:...JndiLookup.lookup"
]
},
"reasoning": "Direct call path from main() through logging framework to vulnerable lookup method"
},
{
"symbolId": "sym:java:...",
"display": "org.apache.logging.log4j.core.net.JndiManager.lookup",
"purl": "pkg:maven/org.apache.logging.log4j/log4j-core@2.14.1",
"expected": {
"latticeState": "CU",
"bucket": "unreachable",
"reachable": false,
"confidence": 0.90,
"pathLength": null,
"path": null
},
"reasoning": "JndiManager.lookup is present but not called from any reachable entry point"
}
],
"entryPoints": [
{
"symbolId": "sym:java:...",
"display": "com.example.app.Main.main",
"phase": "runtime",
"source": "manifest"
}
],
"expectedUncertainty": {
"states": [],
"aggregateTier": "T4",
"riskScore": 0.0
},
"expectedGateDecisions": [
{
"vulnId": "CVE-2021-44228",
"targetSymbol": "sym:java:...JndiLookup.lookup",
"requestedStatus": "not_affected",
"expectedDecision": "block",
"expectedBlockedBy": "LatticeState",
"expectedReason": "CR state incompatible with not_affected"
},
{
"vulnId": "CVE-2021-44228",
"targetSymbol": "sym:java:...JndiLookup.lookup",
"requestedStatus": "affected",
"expectedDecision": "allow"
}
]
}
```
---
## 3. Schema Definitions
### 3.1 Ground Truth Target
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `symbolId` | string | Yes | Canonical SymbolID (`sym:{lang}:{hash}`) |
| `display` | string | No | Human-readable symbol name |
| `purl` | string | No | Package URL of containing package |
| `expected.latticeState` | enum | Yes | Expected v1 lattice state: `U`, `SR`, `SU`, `RO`, `RU`, `CR`, `CU`, `X` |
| `expected.bucket` | enum | Yes | Expected v0 bucket (backward compat) |
| `expected.reachable` | boolean | Yes | True if symbol is reachable from any entry point |
| `expected.confidence` | number | Yes | Expected confidence score [0.0-1.0] |
| `expected.pathLength` | number | No | Expected path length (null if unreachable) |
| `expected.path` | string[] | No | Expected path (sorted, deterministic) |
| `reasoning` | string | Yes | Human explanation of expected outcome |
### 3.2 Expected Gate Decision
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `vulnId` | string | Yes | Vulnerability identifier |
| `targetSymbol` | string | Yes | Target SymbolID |
| `requestedStatus` | enum | Yes | VEX status: `affected`, `not_affected`, `under_investigation`, `fixed` |
| `expectedDecision` | enum | Yes | Gate outcome: `allow`, `block`, `warn` |
| `expectedBlockedBy` | string | No | Gate name if blocked |
| `expectedReason` | string | No | Expected reason message |
---
## 4. Sample Categories
### 4.1 Positive Samples (Reachable)
Known-reachable cases where vulnerable code is called:
- **direct-call:** Vulnerable function called directly from entry point
- **transitive:** Multi-hop path from entry point to vulnerable function
- **runtime-observed:** Confirmed reachable via runtime probe
- **init-array:** Reachable via load-time constructor
### 4.2 Negative Samples (Unreachable)
Known-unreachable cases where vulnerable code exists but isn't called:
- **dead-code:** Function present but never invoked
- **conditional-unreachable:** Function behind impossible condition
- **test-only:** Function only reachable from test entry points
- **deprecated-api:** Old API present but replaced by new implementation
### 4.3 Contested Samples
Cases where static and runtime evidence conflict:
- **static-reach-runtime-miss:** Static analysis finds path, runtime never observes
- **static-miss-runtime-hit:** Static analysis misses path, runtime observes execution
- **version-mismatch:** Analysis version differs from runtime version
---
## 5. Benchmark Metrics
### 5.1 Path Discovery Metrics
```
Precision = TruePositive / (TruePositive + FalsePositive)
Recall = TruePositive / (TruePositive + FalseNegative)
F1 = 2 * (Precision * Recall) / (Precision + Recall)
```
### 5.2 Lattice State Accuracy
```
StateAccuracy = CorrectStates / TotalTargets
BucketAccuracy = CorrectBuckets / TotalTargets (v0 compatibility)
```
### 5.3 Gate Decision Accuracy
```
GateAccuracy = CorrectDecisions / TotalGateTests
FalseAllow = AllowedWhenShouldBlock / TotalBlocks (critical metric)
FalseBlock = BlockedWhenShouldAllow / TotalAllows
```
---
## 6. Test Harness Integration
### 6.1 xUnit Test Pattern
```csharp
[Theory]
[MemberData(nameof(GetGroundTruthSamples))]
public async Task ReachabilityAnalysis_MatchesGroundTruth(GroundTruthSample sample)
{
// Arrange
var graph = await LoadRichGraphAsync(sample.GraphPath);
var scorer = _serviceProvider.GetRequiredService<ReachabilityScoringService>();
// Act
var result = await scorer.ComputeAsync(graph, sample.EntryPoints);
// Assert
foreach (var target in sample.Targets)
{
var actual = result.States.First(s => s.SymbolId == target.SymbolId);
Assert.Equal(target.Expected.LatticeState, actual.LatticeState);
Assert.Equal(target.Expected.Reachable, actual.Reachable);
Assert.InRange(actual.Confidence,
target.Expected.Confidence - 0.05,
target.Expected.Confidence + 0.05);
}
}
```
### 6.2 Benchmark Runner
```bash
# Run reachability benchmarks
dotnet run --project src/Scanner/__Tests/StellaOps.Scanner.Reachability.Benchmarks \
--dataset datasets/reachability/samples \
--output benchmark-results.json \
--threshold-f1 0.95 \
--threshold-gate-accuracy 0.99
```
---
## 7. Sample Contribution Guidelines
### 7.1 Adding New Samples
1. Create directory under `datasets/reachability/samples/{language}/{sample-name}/`
2. Add `manifest.json` with sample metadata
3. Add `richgraph-v1.json` (run scanner on artifacts)
4. Create `ground-truth.json` with manual annotations
5. Include reasoning for each expected outcome
6. Run validation: `dotnet test --filter "GroundTruth"`
### 7.2 Ground Truth Validation
Ground truth files must pass schema validation:
```bash
npx ajv validate -s docs/reachability/ground-truth.schema.json \
-d datasets/reachability/samples/**/ground-truth.json
```
### 7.3 Review Requirements
- All samples require two independent annotators
- Contested samples require security team review
- Changes to existing samples require regression test pass
---
## 8. Related Documents
- [Lattice Model](./lattice.md) — v1 formal 7-state lattice
- [Policy Gates](./policy-gate.md) — Gate rules for VEX decisions
- [Evidence Schema](./evidence-schema.md) — richgraph-v1 schema
- [richgraph-v1 Contract](../contracts/richgraph-v1.md) — Full schema specification
---
## Changelog
| Version | Date | Author | Changes |
|---------|------|--------|---------|
| 1.0.0 | 2025-12-13 | Scanner Guild | Initial design from Sprint 0401 |

View File

@@ -1,215 +1,254 @@
# Reachability Lattice & Scoring Model
> **Status:** Draft mirrors the December 2025 advisory on confidence-based reachability.
> **Owners:** Scanner Guild · Policy Guild · Signals Guild.
> **Status:** Implemented v0 in Signals; this document describes the current deterministic bucket model and its policy-facing implications.
> **Owners:** Scanner Guild · Signals Guild · Policy Guild.
> Stella Ops isn't just another scanner—it's a different product category: **deterministic, evidence-linked vulnerability decisions** that survive auditors, regulators, and supply-chain propagation.
This document defines the confidence lattice, evidence types, mitigation scoring, and policy gates used to turn static/runtime signals into reproducible reachability decisions and VEX statuses.
StellaOps models reachability as a deterministic, evidence-linked outcome that can safely represent "unknown" without silently producing false safety. Signals produces a `ReachabilityFactDocument` with per-target `states[]` and a top-level `score` that is stable under replays.
---
## 1. Overview
## 1. Current model (Signals v0)
<!-- TODO: Review for separate approval - updated lattice overview -->
**Key differentiator:** Unlike simplistic yes/no reachability approaches, the Stella Ops lattice model explicitly handles an **"Unknown"** (under_investigation) state, ensuring incomplete data doesn't lead to false safety. Every VEX decision is evidence-linked with proof pointers to the underlying reachability evidence.
Signals scoring (`src/Signals/StellaOps.Signals/Services/ReachabilityScoringService.cs`) computes, for each `target` symbol:
Classic "reachable: true/false" answers are too brittle. Stella Ops models reachability as an **ordered lattice** with explicit states and scores. Each analyzer/runtime probe emits `Evidence` documents; mitigations add `Mitigation` entries. The lattice engine joins both inputs into a `ReachDecision`:
- `reachable`: whether there exists a path from the selected `entryPoints[]` to `target`.
- `bucket`: a coarse classification of *why* the target is/was reachable.
- `confidence` (0..1): a bounded confidence value.
- `weight` (0..1): bucket multiplier.
- `score` (0..1): `confidence * weight`.
- `path[]`: the discovered path (if reachable), deterministically ordered.
- `evidence.runtimeHits[]`: runtime hit symbols that appear on the chosen path.
The fact-level `score` is the average of per-target scores, penalized by unknowns pressure (see §4).
---
## 2. Buckets & default weights
Bucket assignment is deterministic and uses this precedence:
1. `unreachable` — no path exists.
2. `entrypoint` — the `target` itself is an entrypoint.
3. `runtime` — at least one runtime hit overlaps the discovered path.
4. `direct` — reachable and the discovered path is length ≤ 2.
5. `unknown` — reachable but none of the above classifications apply.
Default weights (configurable via `SignalsOptions:Scoring:ReachabilityBuckets`):
| Bucket | Default weight |
|--------|----------------|
| `entrypoint` | `1.0` |
| `direct` | `0.85` |
| `runtime` | `0.45` |
| `unknown` | `0.5` |
| `unreachable` | `0.0` |
---
## 3. Confidence (reachable vs unreachable)
Default confidence values (configurable via `SignalsOptions:Scoring:*`):
| Input | Default |
|-------|---------|
| `reachableConfidence` | `0.75` |
| `unreachableConfidence` | `0.25` |
| `runtimeBonus` | `0.15` |
| `minConfidence` | `0.05` |
| `maxConfidence` | `0.99` |
Rules:
- Base confidence is `reachableConfidence` when `reachable=true`, otherwise `unreachableConfidence`.
- When `reachable=true` and runtime evidence overlaps the selected path, add `runtimeBonus` (bounded by `maxConfidence`).
- The final confidence is clamped to `[minConfidence, maxConfidence]`.
---
## 4. Unknowns pressure (missing/ambiguous evidence)
Signals tracks unresolved symbols/edges as **Unknowns** (see `docs/signals/unknowns-registry.md`). The number of unknowns for a subject influences the final score:
```
UNOBSERVED (09)
< POSSIBLE (1029)
< STATIC_PATH (3059)
< DYNAMIC_SEEN (6079)
< DYNAMIC_USER_TAINTED (8099)
< EXPLOIT_CONSTRAINTS_REMOVED (100)
unknownsPressure = unknownsCount / (targetsCount + unknownsCount)
pressurePenalty = min(unknownsPenaltyCeiling, unknownsPressure)
fact.score = avg(states[i].score) * (1 - pressurePenalty)
```
Each state corresponds to increasing confidence that a vulnerability can execute. Mitigations reduce scores; policy gates map scores to VEX statuses (`not_affected`, `under_investigation`, `affected`).
Default `unknownsPenaltyCeiling` is `0.35` (configurable).
This keeps the system deterministic while preventing unknown-heavy subjects from appearing "safe" by omission.
---
## 2. Core types
## 5. Evidence references & determinism anchors
```csharp
public enum ReachState { Unobserved, Possible, StaticPath, DynamicSeen, DynamicUserTainted, ExploitConstraintsRemoved }
Signals produces stable references intended for downstream evidence chains:
public enum EvidenceKind {
StaticCallEdge, StaticEntryPointProximity, StaticPackageDeclaredOnly,
RuntimeMethodHit, RuntimeStackSample, RuntimeHttpRouteHit,
UserInputSource, DataTaintFlow, ConfigFlagOn, ConfigFlagOff,
ContainerNetworkRestricted, ContainerNetworkOpen,
WafRulePresent, PatchLevel, VendorVexNotAffected, VendorVexAffected,
ManualOverride
}
- `metadata.fact.digest` — canonical digest of the reachability fact (`sha256:<hex>`).
- `metadata.fact.version` — monotonically increasing integer for the same `subjectKey`.
- Callgraph ingestion returns a deterministic `graphHash` (sha256) for the normalized callgraph.
public sealed record Evidence(
string Id,
EvidenceKind Kind,
double Weight,
string Source,
DateTimeOffset Timestamp,
string? ArtifactRef,
string? Details);
public enum MitigationKind { WafRule, FeatureFlagDisabled, AuthZEnforced, InputValidation, PatchedVersion, ContainerNetworkIsolation, RuntimeGuard, KillSwitch, Other }
public sealed record Mitigation(
string Id,
MitigationKind Kind,
double Strength,
string Source,
DateTimeOffset Timestamp,
string? ConfigHash,
string? Details);
public sealed record ReachDecision(
string VulnerabilityId,
string ComponentPurl,
ReachState State,
int Score,
string PolicyVersion,
IReadOnlyList<Evidence> Evidence,
IReadOnlyList<Mitigation> Mitigations,
IReadOnlyDictionary<string,string> Metadata);
```
Downstream services (Policy, UI/CLI explainers, replay tooling) should use these fields as stable evidence references.
---
## 3. Scoring policy (default)
## 6. Policy-facing guidance (avoid false "not affected")
| Evidence class | Base score contribution |
|--------------------------|-------------------------|
| Static path (call graph) | ≥ 30 |
| Runtime hit | ≥ 60 |
| User-tainted flow | ≥ 80 |
| "Constraints removed" | = 100 |
| Lockfile-only evidence | 10 (if no other signals)|
Policy should treat `unreachable` (or low fact score) as **insufficient** to claim "not affected" unless:
Mitigations subtract up to 40 points (configurable):
- the reachability evidence is present and referenced (`metadata.fact.digest`), and
- confidence is above a high-confidence threshold.
When evidence is missing or confidence is low, the correct output is **under investigation** rather than "not affected".
---
## 7. Signals API pointers
- `docs/api/signals/reachability-contract.md`
- `docs/api/signals/samples/facts-sample.json`
---
## 8. Roadmap (tracked in Sprint 0401)
- Introduce first-class uncertainty state lists + entropy-derived `riskScore` (see `docs/uncertainty/README.md`).
- Extend evidence refs to include CAS/DSSE pointers for graph-level and edge-bundle attestations.
---
## 9. Formal Lattice Model v1 (design — Sprint 0401)
The v0 bucket model provides coarse classification. The v1 lattice model introduces a formal 7-state lattice with algebraic join/meet operations for monotonic, deterministic reachability analysis across evidence types.
### 9.1 State Definitions
| State | Code | Ordering | Description |
|-------|------|----------|-------------|
| `Unknown` | `U` | ⊥ (bottom) | No evidence available; default state |
| `StaticallyReachable` | `SR` | 1 | Static analysis suggests path exists |
| `StaticallyUnreachable` | `SU` | 1 | Static analysis finds no path |
| `RuntimeObserved` | `RO` | 2 | Runtime probe/hit confirms execution |
| `RuntimeUnobserved` | `RU` | 2 | Runtime probe active but no hit observed |
| `ConfirmedReachable` | `CR` | 3 | Both static + runtime agree reachable |
| `ConfirmedUnreachable` | `CU` | 3 | Both static + runtime agree unreachable |
| `Contested` | `X` | (top) | Static and runtime evidence conflict |
### 9.2 Lattice Ordering (Hasse Diagram)
```
effectiveScore = baseScore - min(sum(m.Strength), 1.0) * MaxMitigationDelta
Contested (X)
/ | \
/ | \
ConfirmedReachable | ConfirmedUnreachable
(CR) | (CU)
| \ / / |
| \ / / |
| \ / / |
RuntimeObserved RuntimeUnobserved
(RO) (RU)
| |
| |
StaticallyReachable StaticallyUnreachable
(SR) (SU)
\ /
\ /
Unknown (U)
```
Clamp final scores to 0100.
### 9.3 Join Rules (⊔ — least upper bound)
---
When combining evidence from multiple sources, use the join operation:
## 4. State & VEX gates
```
U ⊔ S = S (any evidence beats unknown)
SR ⊔ RO = CR (static reachable + runtime hit = confirmed)
SU ⊔ RU = CU (static unreachable + runtime miss = confirmed)
SR ⊔ RU = X (static reachable but runtime miss = contested)
SU ⊔ RO = X (static unreachable but runtime hit = contested)
CR ⊔ CU = X (conflicting confirmations = contested)
X ⊔ * = X (contested absorbs all)
```
Default thresholds (edit in `reachability.policy.yml`):
**Full join table:**
| State | Score range |
|----------------------------|-------------|
| UNOBSERVED | 09 |
| POSSIBLE | 1029 |
| STATIC_PATH | 3059 |
| DYNAMIC_SEEN | 6079 |
| DYNAMIC_USER_TAINTED | 8099 |
| EXPLOIT_CONSTRAINTS_REMOVED| 100 |
| ⊔ | U | SR | SU | RO | RU | CR | CU | X |
|---|---|----|----|----|----|----|----|---|
| **U** | U | SR | SU | RO | RU | CR | CU | X |
| **SR** | SR | SR | X | CR | X | CR | X | X |
| **SU** | SU | X | SU | X | CU | X | CU | X |
| **RO** | RO | CR | X | RO | X | CR | X | X |
| **RU** | RU | X | CU | X | RU | X | CU | X |
| **CR** | CR | CR | X | CR | X | CR | X | X |
| **CU** | CU | X | CU | X | CU | X | CU | X |
| **X** | X | X | X | X | X | X | X | X |
VEX mapping:
### 9.4 Meet Rules (⊓ — greatest lower bound)
* **not_affected**: score ≤ 25 or mitigations dominate (score reduced below threshold).
* **affected**: score ≥ 60 (dynamic evidence without sufficient mitigation).
* **under_investigation**: everything between. **This explicit "Unknown" state is a key differentiator**—incomplete data never leads to false safety.
Used for conservative intersection (e.g., multi-entry-point consensus):
Each decision records `reachability.policy.version`, analyzer versions, policy hash, and config snapshot so downstream verifiers can replay the exact logic. All decisions are sealed in Decision Capsules for audit-grade reproducibility.
```
U ⊓ * = U (unknown is bottom)
CR ⊓ CR = CR (agreement preserved)
X ⊓ S = S (drop contested to either side)
```
---
### 9.5 Monotonicity Properties
## 5. Evidence sources
1. **Evidence accumulation is monotonic:** Once state rises in the lattice, it cannot descend without explicit revocation.
2. **Revocation resets to Unknown:** When evidence is invalidated (e.g., graph invalidation), state resets to `U`.
3. **Contested states require human triage:** `X` state triggers policy flags and UI attention.
| Signal group | Producers | EvidenceKind |
|--------------|-----------|--------------|
| Static call graph | Roslyn/IL walkers, ASP.NET routing models, JVM/JIT analyzers | `StaticCallEdge`, `StaticEntryPointProximity`, `StaticFrameworkRouteEdge` |
| Runtime sampling | .NET EventPipe, JFR, Node inspector, Go/Rust probes | `RuntimeMethodHit`, `RuntimeStackSample`, `RuntimeHttpRouteHit` |
| Data/taint | Taint analyzers, user-input detectors | `UserInputSource`, `DataTaintFlow` |
| Environment | Config snapshot, container args, network policy | `ConfigFlagOn/Off`, `ContainerNetworkRestricted/Open` |
| Mitigations | WAF connectors, patch diff, kill switches | `MitigationKind.*` via `Mitigation` records |
| Trust | Vendor VEX statements, manual overrides | `VendorVexNotAffected/Affected`, `ManualOverride` |
### 9.6 Mapping v0 Buckets to v1 States
Each evidence object **must** log `Source`, timestamps, and references (function IDs, config hashes) so auditors can trace it in the event graph. This enables **evidence-linked VEX decisions** where every assertion includes pointers to the underlying proof.
| v0 Bucket | v1 State(s) | Notes |
|-----------|-------------|-------|
| `unreachable` | `SU`, `CU` | Depends on runtime evidence availability |
| `entrypoint` | `CR` | Entry points are by definition reachable |
| `runtime` | `RO`, `CR` | Depends on static analysis agreement |
| `direct` | `SR`, `CR` | Direct paths with/without runtime confirmation |
| `unknown` | `U` | No evidence available |
---
### 9.7 Policy Decision Matrix
## 6. Event graph schema
| v1 State | VEX "not_affected" | VEX "affected" | VEX "under_investigation" |
|----------|-------------------|----------------|---------------------------|
| `U` | ❌ blocked | ⚠️ needs evidence | ✅ default |
| `SR` | ❌ blocked | ✅ allowed | ✅ allowed |
| `SU` | ⚠️ low confidence | ❌ contested | ✅ allowed |
| `RO` | ❌ blocked | ✅ allowed | ✅ allowed |
| `RU` | ⚠️ medium confidence | ❌ contested | ✅ allowed |
| `CR` | ❌ blocked | ✅ required | ❌ invalid |
| `CU` | ✅ allowed | ❌ blocked | ❌ invalid |
| `X` | ❌ blocked | ❌ blocked | ✅ required |
Persist function-level edges and evidence in Mongo (or your event store) under:
### 9.8 Implementation Notes
* `reach_functions` documents keyed by `FunctionId`.
* `reach_call_sites` `CallSite` edges (`caller`, `callee`, `frameworkEdge`).
* `reach_evidence` array of `Evidence` per `(scanId, vulnId, component)`.
* `reach_mitigations` array of `Mitigation` entries with config hashes.
* `reach_decisions` final `ReachDecision` document; references above IDs.
- **State storage:** `ReachabilityFactDocument.states[].latticeState` field (enum)
- **Join implementation:** `ReachabilityLattice.Join(a, b)` in `src/Signals/StellaOps.Signals/Services/`
- **Backward compatibility:** v0 bucket computed from v1 state for API consumers
All collections are tenant-scoped and include analyzer/policy version metadata.
### 9.9 Evidence Chain Requirements
---
## 7. Policy gates → VEX decisions
VEXer consumes `ReachDecision` and `reachability.policy.yml` to emit:
Each lattice state transition must be accompanied by evidence references:
```json
{
"vulnerability": "CVE-2025-1234",
"products": ["pkg:nuget/Example@1.2.3"],
"status": "not_affected|under_investigation|affected",
"status_notes": "Reachability score 22 (Possible) with WAF rule mitigation.",
"justification": "component_not_present|vulnerable_code_not_present|... or custom reason",
"action_statement": "Monitor config ABC",
"impact_statement": "Runtime probes observed 0 hits; static call graph absent.",
"timestamp": "...",
"custom": {
"reachability": {
"state": "POSSIBLE",
"score": 22,
"policyVersion": "reach-1",
"evidenceRefs": ["evidence:123", "mitigation:456"]
}
"symbol": "sym:java:...",
"latticeState": "CR",
"previousState": "SR",
"evidence": {
"static": {
"graphHash": "blake3:...",
"pathLength": 3,
"confidence": 0.92
},
"runtime": {
"probeId": "probe:...",
"hitCount": 47,
"observedAt": "2025-12-13T10:00:00Z"
}
},
"transitionAt": "2025-12-13T10:00:00Z"
}
```
Justifications cite specific evidence/mitigation IDs so replay bundles (`docs/replay/DETERMINISTIC_REPLAY.md`) can prove the decision.
---
## 8. Runtime probes (overview)
* .NET: EventPipe session watching `Microsoft-Windows-DotNETRuntime/Loader,JIT``RuntimeMethodHit`.
* JVM: JFR recording with `MethodProfilingSample` events.
* Node/TS: Inspector or `--trace-event-categories node.async_hooks,node.perf` sample.
* Go/Rust: `pprof`/probe instrumentation.
All runtime probes write evidence via `IRuntimeEvidenceSink`, which deduplicates hits, enriches them with `FunctionId`, and stores them in `reach_evidence`.
See `src/Scanner/StellaOps.Scanner.WebService/Reachability/Runtime/DotNetRuntimeProbe.cs` (once implemented) for reference.
---
## 9. Hybrid Reachability
<!-- TODO: Review for separate approval - added hybrid reachability section -->
Stella Ops combines **static call-graph analysis** with **runtime process tracing** for true hybrid reachability:
- **Static analysis** provides call-graph edges from IL/bytecode analysis, framework routing models, and entry-point proximity calculations.
- **Runtime analysis** provides observed method hits, stack samples, and HTTP route hits from live or shadow traffic.
- **Hybrid reconciliation** merges both signal types, with each edge type attestable via DSSE. See `docs/reachability/hybrid-attestation.md` for the attestation model.
This hybrid approach ensures that both build-time and run-time context contribute to the same verdict, avoiding the blind spots of purely static or purely runtime analysis.
---
## 10. Roadmap
| Task | Description |
|------|-------------|
| `REACH-LATTICE-401-023` | Initial lattice types + scoring engine + event graph schema. |
| `REACH-RUNTIME-402-024` | Productionize runtime probes (EventPipe/JFR) with opt-in config and telemetry. |
| `REACH-VEX-402-025` | Wire `ReachDecision` into VEX generator; ensure OpenVEX/CSAF cite reachability evidence. |
| `REACH-POLICY-402-026` | Expose reachability gates in Policy DSL & CLI (edit/lint/test). |
Keep this doc updated as the lattice evolves or new signals/mitigations are added.

View File

@@ -0,0 +1,269 @@
# Reachability Evidence Policy Gates
> **Status:** Design v1 (Sprint 0401)
> **Owners:** Policy Guild, Signals Guild, VEX Guild
This document defines the policy gates that enforce reachability evidence requirements for VEX decisions. Gates prevent unsafe "not_affected" claims when evidence is insufficient.
---
## 1. Overview
Policy gates act as checkpoints between evidence (reachability lattice state, uncertainty tier) and VEX status transitions. They ensure that:
1. **No false safety:** "not_affected" requires strong evidence of unreachability
2. **Explicit uncertainty:** Missing evidence triggers "under_investigation" rather than silence
3. **Audit trail:** All gate decisions are logged with evidence references
---
## 2. Gate Types
### 2.1 Lattice State Gate
Guards VEX status transitions based on the v1 lattice state (see `docs/reachability/lattice.md` §9).
| Requested VEX Status | Required Lattice State | Gate Action |
|---------------------|------------------------|-------------|
| `not_affected` | `CU` (ConfirmedUnreachable) | ✅ Allow |
| `not_affected` | `SU` (StaticallyUnreachable) | ⚠️ Allow with warning, requires `justification` |
| `not_affected` | `RU` (RuntimeUnobserved) | ⚠️ Allow with warning, requires `justification` |
| `not_affected` | `U`, `SR`, `RO`, `CR`, `X` | ❌ Block |
| `affected` | `CR` (ConfirmedReachable) | ✅ Allow |
| `affected` | `SR`, `RO` | ✅ Allow |
| `affected` | `U`, `SU`, `RU`, `CU`, `X` | ⚠️ Warn (potential false positive) |
| `under_investigation` | Any | ✅ Allow (safe default) |
| `fixed` | Any | ✅ Allow (remediation action) |
### 2.2 Uncertainty Tier Gate
Guards VEX status transitions based on the uncertainty tier (see `docs/uncertainty/README.md` §1.1).
| Requested VEX Status | Uncertainty Tier | Gate Action |
|---------------------|------------------|-------------|
| `not_affected` | T1 (High) | ❌ Block |
| `not_affected` | T2 (Medium) | ⚠️ Warn, require explicit override |
| `not_affected` | T3 (Low) | ⚠️ Allow with advisory note |
| `not_affected` | T4 (Negligible) | ✅ Allow |
| `affected` | T1 (High) | ⚠️ Review required (may be false positive) |
| `affected` | T2-T4 | ✅ Allow |
### 2.3 Evidence Completeness Gate
Guards based on the presence of required evidence artifacts.
| VEX Status | Required Evidence | Gate Action if Missing |
|------------|-------------------|----------------------|
| `not_affected` | `graphHash` (DSSE-attested) | ❌ Block |
| `not_affected` | `pathAnalysis.pathLength >= 0` | ❌ Block |
| `not_affected` | `confidence >= 0.8` | ⚠️ Warn if < 0.8 |
| `affected` | `graphHash` OR `runtimeProbe` | Warn if neither |
| `under_investigation` | None required | Allow |
---
## 3. Gate Evaluation Order
Gates are evaluated in this order; first blocking gate stops evaluation:
```
1. Evidence Completeness Gate → Block if required evidence missing
2. Lattice State Gate → Block if state incompatible with status
3. Uncertainty Tier Gate → Block/warn based on tier
4. Confidence Threshold Gate → Warn if confidence below threshold
```
---
## 4. Gate Decision Document
Each gate evaluation produces a decision document:
```json
{
"gateId": "gate:vex:not_affected:2025-12-13T10:00:00Z",
"requestedStatus": "not_affected",
"subject": {
"vulnId": "CVE-2025-12345",
"purl": "pkg:maven/com.example/foo@1.0.0",
"symbolId": "sym:java:..."
},
"evidence": {
"latticeState": "CU",
"uncertaintyTier": "T3",
"graphHash": "blake3:...",
"riskScore": 0.25,
"confidence": 0.92
},
"gates": [
{
"name": "EvidenceCompleteness",
"result": "pass",
"reason": "graphHash present"
},
{
"name": "LatticeState",
"result": "pass",
"reason": "CU allows not_affected"
},
{
"name": "UncertaintyTier",
"result": "pass_with_note",
"reason": "T3 allows with advisory note",
"note": "MissingPurl uncertainty at 35% entropy"
}
],
"decision": "allow",
"advisory": "VEX status allowed with note: T3 uncertainty from MissingPurl",
"decidedAt": "2025-12-13T10:00:00Z"
}
```
---
## 5. Contested State Handling
When lattice state is `X` (Contested):
1. **Block all definitive statuses:** Neither "not_affected" nor "affected" allowed
2. **Force "under_investigation":** Auto-assign until triage resolves conflict
3. **Emit triage event:** Notify VEX operators of conflict with evidence links
4. **Evidence overlay:** Show both static and runtime evidence for manual review
### Contested Resolution Workflow
```
1. System detects X state
2. VEX status locked to "under_investigation"
3. Triage event emitted to operator queue
4. Operator reviews:
a. Static evidence (graph, paths)
b. Runtime evidence (probes, hits)
5. Operator provides resolution:
a. Trust static → state becomes SU/SR
b. Trust runtime → state becomes RU/RO
c. Add new evidence → recompute lattice
6. Gate re-evaluates with new state
```
---
## 6. Override Mechanism
Operators with `vex:gate:override` permission can bypass gates with mandatory fields:
```json
{
"override": {
"gateId": "gate:vex:not_affected:...",
"operator": "user:alice@example.com",
"justification": "Manual review confirms code path is dead code",
"evidence": {
"type": "ManualReview",
"reviewId": "review:2025-12-13:001",
"attachments": ["cas://evidence/review/..."]
},
"approvedAt": "2025-12-13T11:00:00Z",
"expiresAt": "2026-01-13T11:00:00Z"
}
}
```
Override requirements:
- `justification` is mandatory and logged
- Overrides expire after configurable period (default: 30 days)
- All overrides are auditable and appear in compliance reports
---
## 7. Configuration
Gate thresholds are configurable via `PolicyGatewayOptions`:
```yaml
PolicyGateway:
Gates:
LatticeState:
AllowSUForNotAffected: true # Allow SU with warning
AllowRUForNotAffected: true # Allow RU with warning
RequireJustificationForWeakStates: true
UncertaintyTier:
BlockT1ForNotAffected: true
WarnT2ForNotAffected: true
EvidenceCompleteness:
RequireGraphHashForNotAffected: true
MinConfidenceForNotAffected: 0.8
MinConfidenceWarning: 0.6
Override:
DefaultExpirationDays: 30
RequireJustification: true
```
---
## 8. API Integration
### POST `/api/v1/vex/status`
Request:
```json
{
"vulnId": "CVE-2025-12345",
"purl": "pkg:maven/com.example/foo@1.0.0",
"status": "not_affected",
"justification": "vulnerable_code_not_present",
"reachabilityEvidence": {
"factDigest": "sha256:...",
"graphHash": "blake3:..."
}
}
```
Response (gate blocked):
```json
{
"success": false,
"gateDecision": {
"decision": "block",
"blockedBy": "LatticeState",
"reason": "Lattice state SR (StaticallyReachable) incompatible with not_affected",
"currentState": "SR",
"requiredStates": ["CU", "SU", "RU"],
"suggestion": "Submit runtime probe evidence or change to under_investigation"
}
}
```
---
## 9. Metrics & Alerts
The policy gateway emits metrics:
| Metric | Labels | Description |
|--------|--------|-------------|
| `stellaops_gate_decisions_total` | `gate`, `result`, `status` | Total gate decisions |
| `stellaops_gate_blocks_total` | `gate`, `reason` | Total blocked requests |
| `stellaops_gate_overrides_total` | `operator` | Total override uses |
| `stellaops_contested_states_total` | `vulnId` | Active contested states |
Alert conditions:
- `stellaops_gate_overrides_total` rate > threshold → Audit review
- `stellaops_contested_states_total` > 10 → Triage backlog alert
---
## 10. Related Documents
- [Lattice Model](./lattice.md) — v1 formal 7-state lattice
- [Uncertainty States](../uncertainty/README.md) — Tier definitions and risk scoring
- [Evidence Schema](./evidence-schema.md) — richgraph-v1 schema
- [VEX Contract](../contracts/vex-v1.md) — VEX document schema
---
## Changelog
| Version | Date | Author | Changes |
|---------|------|--------|---------|
| 1.0.0 | 2025-12-13 | Policy Guild | Initial design from Sprint 0401 |

View File

@@ -0,0 +1,174 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://stella-ops.org/schemas/tte-event.schema.json",
"title": "Time-to-Evidence (TTE) Telemetry Event",
"description": "Schema for tracking time-to-evidence metrics across triage workflows (TTE1-TTE10)",
"type": "object",
"required": [
"schema_version",
"event_type",
"timestamp",
"tenant_id",
"correlation_id",
"phase",
"elapsed_ms"
],
"properties": {
"schema_version": {
"type": "string",
"pattern": "^v[0-9]+\\.[0-9]+$",
"description": "Schema version (e.g., v1.0)",
"examples": ["v1.0"]
},
"event_type": {
"type": "string",
"enum": [
"tte.phase.started",
"tte.phase.completed",
"tte.phase.failed",
"tte.phase.timeout",
"tte.evidence.attached",
"tte.evidence.verified",
"tte.decision.made",
"tte.slo.breach"
],
"description": "Type of TTE event"
},
"timestamp": {
"type": "string",
"format": "date-time",
"description": "ISO-8601 UTC timestamp when event occurred"
},
"tenant_id": {
"type": "string",
"minLength": 1,
"description": "Tenant identifier for scoping"
},
"correlation_id": {
"type": "string",
"format": "uuid",
"description": "Correlation ID linking all events in a triage workflow"
},
"phase": {
"type": "string",
"enum": [
"scan_to_finding",
"finding_to_evidence",
"evidence_to_decision",
"decision_to_attestation",
"attestation_to_verification",
"verification_to_policy",
"end_to_end"
],
"description": "Phase of the evidence chain being measured"
},
"elapsed_ms": {
"type": "number",
"minimum": 0,
"description": "Elapsed time in milliseconds for this phase"
},
"finding_id": {
"type": "string",
"description": "Finding identifier if applicable"
},
"vulnerability_id": {
"type": "string",
"pattern": "^CVE-[0-9]{4}-[0-9]+$",
"description": "CVE identifier if applicable"
},
"artifact_digest": {
"type": "string",
"pattern": "^sha256:[a-f0-9]{64}$",
"description": "Artifact digest in OCI format"
},
"evidence_type": {
"type": "string",
"enum": ["attestation", "vex", "sbom", "policy_eval", "reachability", "fix_pr"],
"description": "Type of evidence attached or verified"
},
"evidence_count": {
"type": "integer",
"minimum": 0,
"description": "Number of evidence items attached in this event"
},
"decision_status": {
"type": "string",
"enum": ["not_affected", "affected", "fixed", "under_investigation"],
"description": "VEX decision status if event is decision-related"
},
"verification_result": {
"type": "string",
"enum": ["verified", "failed", "pending", "expired", "revoked"],
"description": "Result of attestation/signature verification"
},
"slo_target_ms": {
"type": "number",
"minimum": 0,
"description": "SLO target in milliseconds for this phase"
},
"slo_breach": {
"type": "boolean",
"description": "True if this event represents an SLO breach"
},
"surface": {
"type": "string",
"enum": ["api", "ui", "cli", "webhook", "scheduler"],
"description": "Surface where the event originated"
},
"user_agent": {
"type": "string",
"description": "User agent string (filtered for bots)"
},
"is_automated": {
"type": "boolean",
"description": "True if event triggered by automation (not human)"
},
"offline_mode": {
"type": "boolean",
"description": "True if event occurred in offline/airgap mode"
},
"error_code": {
"type": ["string", "null"],
"description": "Error code if event_type is failure/timeout"
},
"metadata": {
"type": "object",
"additionalProperties": true,
"description": "Additional context-specific metadata"
}
},
"additionalProperties": false,
"examples": [
{
"schema_version": "v1.0",
"event_type": "tte.phase.completed",
"timestamp": "2025-12-13T14:30:00.000Z",
"tenant_id": "tenant-123",
"correlation_id": "550e8400-e29b-41d4-a716-446655440000",
"phase": "finding_to_evidence",
"elapsed_ms": 1250,
"finding_id": "finding-abc-123",
"vulnerability_id": "CVE-2024-1234",
"evidence_type": "attestation",
"evidence_count": 1,
"surface": "ui",
"is_automated": false,
"slo_target_ms": 5000,
"slo_breach": false
},
{
"schema_version": "v1.0",
"event_type": "tte.slo.breach",
"timestamp": "2025-12-13T14:35:00.000Z",
"tenant_id": "tenant-456",
"correlation_id": "660e8400-e29b-41d4-a716-446655440001",
"phase": "end_to_end",
"elapsed_ms": 125000,
"slo_target_ms": 60000,
"slo_breach": true,
"surface": "api",
"is_automated": true,
"error_code": "TTE_SLO_END_TO_END_BREACH"
}
]
}

View File

@@ -1,28 +1,73 @@
# Uncertainty States & Entropy Scoring
> **Status:** Draft aligns with the November2025 advisory on explicit uncertainty tracking.
> **Owners:** Signals Guild · Concelier Guild · UI Guild.
> **Status:** Implemented v0 for reachability facts (Signals).
> **Owners:** Signals Guild · Policy Guild · UI Guild.
StellaOps treats missing data and untrusted evidence as **first-class uncertainty states**, not silent false negatives. Each finding stores a list of `UncertaintyState` entries plus supporting evidence; the risk scorer uses their entropy to adjust final risk. Policy and UI surfaces reveal uncertainty to operators rather than hiding it.
StellaOps treats missing data and untrusted evidence as **first-class uncertainty states**, not silent false negatives. Signals persists uncertainty state entries alongside reachability facts and derives a deterministic `riskScore` that increases when entropy is high.
---
## 1. Core states (extensible)
| Code | Name | Meaning |
|------|------------------------|---------------------------------------------------------------------------|
| `U1` | MissingSymbolResolution| Vulnerability → function mapping unresolved (no PDB/IL map, missing dSYMs). |
| `U2` | MissingPurl | Package identity/version ambiguous (lockfile absent, heuristics only). |
| `U3` | UntrustedAdvisory | Advisory source lacks DSSE/Sigstore provenance or corroboration. |
| `U4+`| (future) | e.g. partial SBOM coverage, missing container layers, unresolved transitives. |
|------|------|---------|
| `U1` | `MissingSymbolResolution` | Unresolved symbols/edges prevent a complete reachability proof. |
| `U2` | `MissingPurl` | Package identity/version is ambiguous (lockfile absent, heuristics only). |
| `U3` | `UntrustedAdvisory` | Advisory source lacks provenance/corroboration. |
| `U4` | `Unknown` | No analyzers have processed this subject; baseline uncertainty. |
Each state records `entropy` (01) and an evidence list pointing to analyzers, heuristics, or advisory sources that asserted the uncertainty.
Each state records:
- `entropy` (0..1)
- `evidence[]` list pointing to analyzers/heuristics/sources
- optional `timestamp` (UTC)
---
## 2. Schema
## 1.1 Uncertainty Tiers (v1 — Sprint 0401)
```jsonc
Uncertainty states are grouped into **tiers** that determine policy thresholds and UI treatment.
### Tier Definitions
| Tier | Entropy Range | States | Risk Modifier | Policy Implication |
|------|---------------|--------|---------------|-------------------|
| **T1 (High)** | `0.7 - 1.0` | `U1` (high), `U4` | `+50%` | Block "not_affected", require human review |
| **T2 (Medium)** | `0.4 - 0.69` | `U1` (medium), `U2` | `+25%` | Warn on "not_affected", flag for review |
| **T3 (Low)** | `0.1 - 0.39` | `U2` (low), `U3` | `+10%` | Allow "not_affected" with advisory note |
| **T4 (Negligible)** | `0.0 - 0.09` | `U3` (low) | `+0%` | Normal processing, no special handling |
### Tier Assignment Rules
1. **U1 (MissingSymbolResolution):**
- `entropy >= 0.7` → T1 (>30% unknowns in callgraph)
- `entropy >= 0.4` → T2 (15-30% unknowns)
- `entropy < 0.4` → T3 (<15% unknowns)
2. **U2 (MissingPurl):**
- `entropy >= 0.5` T2 (>50% packages unresolved)
- `entropy < 0.5` → T3 (<50% packages unresolved)
3. **U3 (UntrustedAdvisory):**
- `entropy >= 0.6` T3 (no corroboration)
- `entropy < 0.6` T4 (partial corroboration)
4. **U4 (Unknown):**
- Always T1 (no analysis performed = maximum uncertainty)
### Aggregate Tier Calculation
When multiple uncertainty states exist, the aggregate tier is the **maximum** (most severe):
```
aggregateTier = max(tier(state) for state in uncertainty.states)
```
---
## 2. JSON shape
```json
{
"uncertainty": {
"states": [
@@ -30,24 +75,12 @@ Each state records `entropy` (01) and an evidence list pointing to analyzers,
"code": "U1",
"name": "MissingSymbolResolution",
"entropy": 0.72,
"timestamp": "2025-11-12T14:12:00Z",
"evidence": [
{
"type": "AnalyzerProbe",
"sourceId": "dotnet.symbolizer",
"detail": "No PDB/IL map for Foo.Bar::DoWork"
}
],
"timestamp": "2025-11-12T14:12:00Z"
},
{
"code": "U2",
"name": "MissingPurl",
"entropy": 0.55,
"evidence": [
{
"type": "PackageHeuristic",
"sourceId": "jar.manifest",
"detail": "Guessed groupId=com.example, version ~= 1.9.x"
"type": "UnknownsRegistry",
"sourceId": "signals.unknowns",
"detail": "unknownsCount=12;unknownsPressure=0.375"
}
]
}
@@ -56,98 +89,140 @@ Each state records `entropy` (01) and an evidence list pointing to analyzers,
}
```
### C# models
---
```csharp
public sealed record UncertaintyEvidence(string Type, string SourceId, string Detail);
## 3. Risk score math (Signals)
public sealed record UncertaintyState(
string Code,
string Name,
double Entropy,
IReadOnlyList<UncertaintyEvidence> Evidence);
Signals computes a `riskScore` deterministically during reachability recompute:
```
meanEntropy = avg(uncertainty.states[].entropy) // 0 when no states
entropyBoost = clamp(meanEntropy * k, 0 .. boostCeiling)
riskScore = clamp(baseScore * (1 + entropyBoost), 0 .. 1)
```
Store them alongside `FindingDocument` in Signals and expose via APIs/CLI/GraphQL so downstream services can display them or enforce policies.
Where:
- `baseScore` is the average of per-target reachability state scores (before unknowns penalty).
- `k` defaults to `0.5` (`SignalsOptions:Scoring:UncertaintyEntropyMultiplier`).
- `boostCeiling` defaults to `0.5` (`SignalsOptions:Scoring:UncertaintyBoostCeiling`).
---
## 3. Risk score math
## 4. Policy guidance (high level)
```
riskScore = baseScore
× reachabilityFactor (0..1)
× trustFactor (0..1)
× (1 + entropyBoost)
Uncertainty should bias decisions away from "not affected" when evidence is missing:
entropyBoost = clamp(avg(uncertainty[i].entropy) × k, 0 .. 0.5)
```
- High entropy (`U1` with high `entropy`) should lead to **under investigation** and drive remediation (upload symbols, run probes, close unknowns).
- Low entropy should allow normal confidence-based gates.
* `k` defaults to `0.5`. With mean entropy = 0.8, boost = 0.4 → risk increases 40% to highlight unknowns.
* If no uncertainty states exist, entropy boost = 0 and the previous scoring remains.
Persist both `uncertainty.states` and `riskScore` so policies, dashboards, and APIs stay deterministic.
See `docs/reachability/lattice.md` for the current reachability score model and `docs/api/signals/reachability-contract.md` for the Signals contract.
---
## 4. Policy + actions
## 5. Tier-Based Risk Score (v1 — Sprint 0401)
Use uncertainty in Concelier/Excitors policies:
### Risk Score Formula
* **Block release** if critical CVE has `U1` with entropy ≥0.70 until symbols or runtime probes are provided.
* **Warn** when only `U3` exists allow deployment but require corroboration (OSV/GHSA, CSAF).
* **Auto-create tasks** for `U2` to fix SBOM/purl data quality.
Building on §3, the v1 risk score incorporates tier-based modifiers:
Recommended policy predicates:
```
tierModifier = {
T1: 0.50,
T2: 0.25,
T3: 0.10,
T4: 0.00
}[aggregateTier]
```yaml
when:
all:
- uncertaintyCodesAny: ["U1"]
- maxEntropyGte: 0.7
riskScore = clamp(baseScore * (1 + tierModifier + entropyBoost), 0 .. 1)
```
Excitors can suggest remediation actions (upload PDBs, add lockfiles, fetch signed CSAF) based on state codes.
Where:
- `baseScore` is the average of per-target reachability state scores
- `tierModifier` is the tier-based risk increase
- `entropyBoost` is the existing entropy-based boost 3)
### Example Calculation
```
Given:
- baseScore = 0.4 (moderate reachability)
- uncertainty.states = [
{code: "U1", entropy: 0.72}, // T1 tier
{code: "U3", entropy: 0.45} // T3 tier
]
- aggregateTier = T1 (max of T1, T3)
- tierModifier = 0.50
meanEntropy = (0.72 + 0.45) / 2 = 0.585
entropyBoost = clamp(0.585 * 0.5, 0 .. 0.5) = 0.2925
riskScore = clamp(0.4 * (1 + 0.50 + 0.2925), 0 .. 1)
= clamp(0.4 * 1.7925, 0 .. 1)
= clamp(0.717, 0 .. 1)
= 0.717
```
### Tier Thresholds for Policy Gates
| Tier | `riskScore` Range | VEX "not_affected" | VEX "affected" | Auto-triage |
|------|-------------------|-------------------|----------------|-------------|
| T1 | `>= 0.6` | blocked | review | `under_investigation` |
| T2 | `0.4 - 0.59` | warning | allowed | Manual review |
| T3 | `0.2 - 0.39` | with note | allowed | Normal |
| T4 | `< 0.2` | allowed | allowed | Normal |
---
## 5. UI guidelines
## 6. JSON Schema (v1)
* Display chips `U1`, `U2`, … on each finding. Tooltip: entropy level + evidence bullets (“AnalyzerProbe/dotnet.symbolizer: …”).
* Provide “How to reduce entropy” hints: symbol uploads, EventPipe probes, purl overrides, advisory verification.
* Show entropy in filters (e.g., “entropy ≥ 0.5”) so teams can prioritise closing uncertainty gaps.
See `components/UncertaintyChipStack` (planned) for a reference implementation.
---
## 6. Event sourcing / audit
Emit `FindingUncertaintyUpdated` events whenever the set changes:
Extended schema with tier information:
```json
{
"type": "FindingUncertaintyUpdated",
"findingId": "finding:service:prod:CVE-2023-12345",
"updatedAt": "2025-11-12T14:21:33Z",
"uncertainty": [ ...states... ]
"uncertainty": {
"states": [
{
"code": "U1",
"name": "MissingSymbolResolution",
"entropy": 0.72,
"tier": "T1",
"timestamp": "2025-12-13T10:00:00Z",
"evidence": [
{
"type": "UnknownsRegistry",
"sourceId": "signals.unknowns",
"detail": "unknownsCount=45;totalSymbols=125;unknownsPressure=0.36"
}
]
},
{
"code": "U4",
"name": "Unknown",
"entropy": 1.0,
"tier": "T1",
"timestamp": "2025-12-13T10:00:00Z",
"evidence": [
{
"type": "NoAnalysis",
"sourceId": "signals.bootstrap",
"detail": "subject not yet analyzed"
}
]
}
],
"aggregateTier": "T1",
"riskScore": 0.717,
"computedAt": "2025-12-13T10:00:00Z"
}
}
```
Projections recompute `riskScore` deterministically, and the event log provides an audit trail showing when/why entropy changed.
---
## 7. Action hints (per state)
## 7. Implementation Pointers
| Code | Suggested remediation |
|------|-----------------------|
| `U1` | Upload PDBs/dSYM files, enable symbolizer connectors, attach runtime probes (EventPipe/JFR). |
| `U2` | Provide package overrides, ingest lockfiles, fix SBOM generator metadata. |
| `U3` | Obtain signed CSAF/OSV evidence, verify via Excitors connectors, or mark trust overrides in policy. |
### 8. Unknowns registry tie-in
Unresolved identities and missing edges should be recorded as Unknowns (see `docs/signals/unknowns-registry.md`). Signals scoring may add an `unknowns_pressure` term when density of unresolved items is high near entrypoints; Policy and UI should surface these records so operators can close the gaps rather than hiding the uncertainty.
Keep this file updated as new states (U4+) or tooling hooks land. Link additional guides (symbol upload, purl overrides) once available.
- **Tier calculation:** `UncertaintyTierCalculator` in `src/Signals/StellaOps.Signals/Services/`
- **Risk score math:** `ReachabilityScoringService.ComputeRiskScore()` (extend existing)
- **Policy integration:** `docs/reachability/policy-gate.md` for gate rules
- **Lattice integration:** `docs/reachability/lattice.md` §9 for v1 lattice states

View File

@@ -11,7 +11,7 @@ env:
ASPNETCORE_URLS: "http://+:5088"
Signals__Mongo__ConnectionString: "mongodb://signals-mongo:27017/signals"
Signals__Mongo__Database: "signals"
Signals__Cache__ConnectionString: "signals-redis:6379"
Signals__Cache__ConnectionString: "signals-valkey:6379"
Signals__Storage__RootPath: "/data/artifacts"
Signals__Authority__Enabled: "false"
Signals__OpenApi__Enabled: "true"
@@ -22,9 +22,9 @@ persistence:
size: 5Gi
storageClass: ""
redis:
valkey:
enabled: true
host: signals-redis
host: signals-valkey
port: 6379
mongo:

View File

@@ -39,20 +39,20 @@ services:
- "27017:27017"
restart: unless-stopped
redis:
image: redis:7-alpine
container_name: stellaops-authority-redis
command: ["redis-server", "--save", "60", "1"]
valkey:
image: valkey/valkey:8-alpine
container_name: stellaops-authority-valkey
command: ["valkey-server", "--save", "60", "1"]
volumes:
- redis-data:/data
- valkey-data:/data
ports:
- "6379:6379"
restart: unless-stopped
# Uncomment to enable if/when Authority consumes Redis.
# Uncomment to enable if/when Authority consumes Valkey.
# deploy:
# replicas: 0
volumes:
mongo-data:
redis-data:
valkey-data:
authority-keys:

View File

@@ -1,7 +1,7 @@
# Signals CI/CD & Local Stack (DEVOPS-SIG-26-001)
Artifacts:
- Compose stack: `ops/devops/signals/docker-compose.signals.yml` (Signals API + Mongo + Redis + artifact volume).
- Compose stack: `ops/devops/signals/docker-compose.signals.yml` (Signals API + Mongo + Valkey + artifact volume).
- Sample config: `ops/devops/signals/signals.yaml` (mounted into the container at `/app/signals.yaml` if desired).
- Dockerfile: `ops/devops/signals/Dockerfile` (multi-stage build on .NET 10 RC).
- Build/export helper: `scripts/signals/build.sh` (saves image tar to `out/signals/signals-image.tar`).
@@ -25,7 +25,7 @@ scripts/signals/run-spansink.sh
Configuration (ENV or YAML):
- `Signals__Mongo__ConnectionString` default `mongodb://signals-mongo:27017/signals`
- `Signals__Cache__ConnectionString` default `signals-redis:6379`
- `Signals__Cache__ConnectionString` default `signals-valkey:6379`
- `Signals__Storage__RootPath` default `/data/artifacts`
- Authority disabled by default for local; enable with `Signals__Authority__Enabled=true` and issuer settings.
@@ -34,5 +34,5 @@ CI workflow:
Dependencies:
- Mongo 7 (wiredTiger)
- Redis 7 (cache)
- Valkey 8 (cache, BSD-3 licensed Redis fork)
- Artifact volume `signals_artifacts` for callgraph blobs.

View File

@@ -10,7 +10,7 @@ services:
ASPNETCORE_URLS: "http://+:5088"
Signals__Mongo__ConnectionString: "mongodb://signals-mongo:27017/signals"
Signals__Mongo__Database: "signals"
Signals__Cache__ConnectionString: "signals-redis:6379"
Signals__Cache__ConnectionString: "signals-valkey:6379"
Signals__Storage__RootPath: "/data/artifacts"
Signals__Authority__Enabled: "false"
Signals__OpenApi__Enabled: "true"
@@ -18,7 +18,7 @@ services:
- "5088:5088"
depends_on:
- signals-mongo
- signals-redis
- signals-valkey
volumes:
- signals_artifacts:/data/artifacts
- ./signals.yaml:/app/signals.yaml:ro
@@ -36,13 +36,13 @@ services:
timeout: 5s
retries: 5
signals-redis:
image: redis:7-alpine
signals-valkey:
image: valkey/valkey:8-alpine
ports:
- "56379:6379"
command: ["redis-server", "--save", "", "--appendonly", "no"]
command: ["valkey-server", "--save", "", "--appendonly", "no"]
healthcheck:
test: ["CMD", "redis-cli", "ping"]
test: ["CMD", "valkey-cli", "ping"]
interval: 10s
timeout: 5s
retries: 5

View File

@@ -5,7 +5,7 @@ Signals:
ConnectionString: "mongodb://signals-mongo:27017/signals"
Database: "signals"
Cache:
ConnectionString: "signals-redis:6379"
ConnectionString: "signals-valkey:6379"
DefaultTtlSeconds: 600
Storage:
RootPath: "/data/artifacts"

View File

@@ -1,5 +1,5 @@
#!/usr/bin/env bash
# Runs live TTL validation for Attestor dedupe stores against local MongoDB/Redis.
# Runs live TTL validation for Attestor dedupe stores against local MongoDB/Valkey.
set -euo pipefail
@@ -39,19 +39,19 @@ services:
interval: 5s
timeout: 3s
retries: 20
redis:
image: redis:7.2
command: ["redis-server", "--save", "", "--appendonly", "no"]
valkey:
image: valkey/valkey:8-alpine
command: ["valkey-server", "--save", "", "--appendonly", "no"]
ports:
- "6379:6379"
healthcheck:
test: ["CMD", "redis-cli", "ping"]
test: ["CMD", "valkey-cli", "ping"]
interval: 5s
timeout: 3s
retries: 20
YAML
echo "Starting MongoDB and Redis containers..."
echo "Starting MongoDB and Valkey containers..."
$compose_cmd -f "$compose_file" up -d
wait_for_port() {
@@ -70,10 +70,10 @@ wait_for_port() {
}
wait_for_port 127.0.0.1 27017 "MongoDB"
wait_for_port 127.0.0.1 6379 "Redis"
wait_for_port 127.0.0.1 6379 "Valkey"
export ATTESTOR_LIVE_MONGO_URI="${ATTESTOR_LIVE_MONGO_URI:-mongodb://127.0.0.1:27017}"
export ATTESTOR_LIVE_REDIS_URI="${ATTESTOR_LIVE_REDIS_URI:-127.0.0.1:6379}"
export ATTESTOR_LIVE_VALKEY_URI="${ATTESTOR_LIVE_VALKEY_URI:-127.0.0.1:6379}"
echo "Running live TTL validation tests..."
dotnet test "$repo_root/src/Attestor/StellaOps.Attestor.sln" --no-build --filter "Category=LiveTTL" "$@"

View File

@@ -3,7 +3,7 @@ using StellaOps.AirGap.Importer.Models;
namespace StellaOps.AirGap.Importer.Repositories;
/// <summary>
/// Deterministic in-memory implementations suitable for offline tests and as a template for Mongo-backed repos.
/// Deterministic in-memory implementations suitable for offline tests and as a template for persistent storage repos.
/// Enforces tenant isolation and stable ordering (by BundleId then Path).
/// </summary>
public sealed class InMemoryBundleCatalogRepository : IBundleCatalogRepository

View File

@@ -1037,7 +1037,7 @@ paths:
value:
status: degraded
service: policy
reason: mongo unavailable
reason: database unavailable
timestamp: 2025-11-18T00:00:00Z
x-service: policy
x-original-path: /health

View File

@@ -46,7 +46,7 @@ paths:
value:
status: degraded
service: policy
reason: mongo unavailable
reason: database unavailable
timestamp: '2025-11-18T00:00:00Z'
/healthz:
get:

View File

@@ -1037,7 +1037,7 @@ paths:
value:
status: degraded
service: policy
reason: mongo unavailable
reason: database unavailable
timestamp: 2025-11-18T00:00:00Z
x-service: policy
x-original-path: /health

View File

@@ -16,7 +16,7 @@ public sealed class AttestorOptions
public SigningOptions Signing { get; set; } = new();
public MongoOptions Mongo { get; set; } = new();
public StorageOptions Storage { get; set; } = new();
public RedisOptions Redis { get; set; } = new();
@@ -122,7 +122,7 @@ public sealed class AttestorOptions
public bool Enabled { get; set; }
}
public sealed class MongoOptions
public sealed class StorageOptions
{
public string? Uri { get; set; }

View File

@@ -4,7 +4,7 @@ using System.Collections.Generic;
namespace StellaOps.Attestor.Core.Storage;
/// <summary>
/// Canonical representation of a Rekor entry persisted in Mongo.
/// Canonical representation of a Rekor entry persisted in storage.
/// </summary>
public sealed class AttestorEntry
{

View File

@@ -22,7 +22,7 @@
<PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.0" />
<PackageReference Include="Microsoft.Extensions.Options" Version="10.0.0" />
<PackageReference Include="Microsoft.Extensions.Http" Version="10.0.0" />
<PackageReference Include="StackExchange.Redis" Version="2.8.24" />
<PackageReference Include="StackExchange.Redis" Version="2.8.37" />
<PackageReference Include="AWSSDK.S3" Version="4.0.2" />
</ItemGroup>
</Project>

View File

@@ -190,8 +190,8 @@ internal sealed class AttestorWebApplicationFactory : WebApplicationFactory<Prog
["attestor:s3:endpoint"] = "http://localhost",
["attestor:s3:useTls"] = "false",
["attestor:redis:url"] = string.Empty,
["attestor:mongo:uri"] = "mongodb://localhost:27017/attestor-tests",
["attestor:mongo:database"] = "attestor-tests"
["attestor:postgres:connectionString"] = "Host=localhost;Port=5432;Database=attestor-tests",
["attestor:postgres:database"] = "attestor-tests"
};
configuration.AddInMemoryCollection(settings!);

View File

@@ -15,7 +15,7 @@
<PackageReference Include="OpenTelemetry.Instrumentation.Runtime" Version="1.12.0" />
<PackageReference Include="Serilog.AspNetCore" Version="8.0.1" />
<PackageReference Include="Serilog.Sinks.Console" Version="5.0.1" />
<PackageReference Include="StackExchange.Redis" Version="2.8.24" />
<PackageReference Include="StackExchange.Redis" Version="2.8.37" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\StellaOps.Attestor.Core\StellaOps.Attestor.Core.csproj" />

View File

@@ -22,7 +22,7 @@ public sealed class LdapClientProvisioningStoreTests
private readonly TestTimeProvider timeProvider = new(new DateTimeOffset(2025, 11, 9, 8, 0, 0, TimeSpan.Zero));
[Fact]
public async Task CreateOrUpdateAsync_WritesToMongoLdapAndAudit()
public async Task CreateOrUpdateAsync_WritesToStorageLdapAndAudit()
{
var clientStore = new TrackingClientStore();
var revocationStore = new TrackingRevocationStore();

View File

@@ -1,4 +1,4 @@
namespace StellaOps.Authority.Storage.Mongo.Documents;
namespace StellaOps.Authority.Storage.Documents;
/// <summary>
/// Represents a bootstrap invite document.

View File

@@ -1,4 +1,4 @@
namespace StellaOps.Authority.Storage.Mongo.Documents;
namespace StellaOps.Authority.Storage.Documents;
/// <summary>
/// Result status for token usage recording.

View File

@@ -4,7 +4,7 @@ using StellaOps.Authority.Storage.InMemory.Initialization;
using StellaOps.Authority.Storage.InMemory.Sessions;
using StellaOps.Authority.Storage.InMemory.Stores;
namespace StellaOps.Authority.Storage.Mongo.Extensions;
namespace StellaOps.Authority.Storage.Extensions;
/// <summary>
/// Compatibility shim storage options. In PostgreSQL mode, these are largely unused.
@@ -17,16 +17,16 @@ public sealed class AuthorityStorageOptions
}
/// <summary>
/// Extension methods for configuring Authority MongoDB compatibility storage services.
/// In PostgreSQL mode, this registers in-memory implementations for the Mongo interfaces.
/// Extension methods for configuring Authority storage compatibility storage services.
/// In PostgreSQL mode, this registers in-memory implementations for the storage interfaces.
/// </summary>
public static class ServiceCollectionExtensions
{
/// <summary>
/// Adds Authority MongoDB compatibility storage services (in-memory implementations).
/// Adds Authority storage compatibility storage services (in-memory implementations).
/// For production PostgreSQL storage, use AddAuthorityPostgresStorage from StellaOps.Authority.Storage.Postgres.
/// </summary>
public static IServiceCollection AddAuthorityMongoStorage(
public static IServiceCollection AddAuthorityInMemoryStorage(
this IServiceCollection services,
Action<AuthorityStorageOptions> configureOptions)
{
@@ -34,11 +34,11 @@ public static class ServiceCollectionExtensions
configureOptions(options);
services.AddSingleton(options);
RegisterMongoCompatServices(services, options);
RegisterInMemoryServices(services, options);
return services;
}
private static void RegisterMongoCompatServices(IServiceCollection services, AuthorityStorageOptions options)
private static void RegisterInMemoryServices(IServiceCollection services, AuthorityStorageOptions options)
{
// Register the initializer (no-op for Postgres mode)
services.AddSingleton<AuthorityStorageInitializer>();

View File

@@ -1,59 +1,59 @@
using MongoDB.Bson;
using StellaOps.Storage.Documents;
namespace MongoDB.Bson.Serialization.Attributes;
namespace StellaOps.Storage.Serialization.Attributes;
/// <summary>
/// Compatibility shim for MongoDB BsonId attribute.
/// Compatibility shim for storage Id attribute.
/// In PostgreSQL mode, this attribute is ignored but allows code to compile.
/// </summary>
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)]
public class BsonIdAttribute : Attribute
public class StorageIdAttribute : Attribute
{
}
/// <summary>
/// Compatibility shim for MongoDB BsonElement attribute.
/// Compatibility shim for storage Element attribute.
/// In PostgreSQL mode, this attribute is ignored but allows code to compile.
/// </summary>
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)]
public class BsonElementAttribute : Attribute
public class StorageElementAttribute : Attribute
{
public string ElementName { get; }
public BsonElementAttribute(string elementName)
public StorageElementAttribute(string elementName)
{
ElementName = elementName;
}
}
/// <summary>
/// Compatibility shim for MongoDB BsonIgnore attribute.
/// Compatibility shim for storage Ignore attribute.
/// In PostgreSQL mode, this attribute is ignored but allows code to compile.
/// </summary>
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)]
public class BsonIgnoreAttribute : Attribute
public class StorageIgnoreAttribute : Attribute
{
}
/// <summary>
/// Compatibility shim for MongoDB BsonIgnoreIfNull attribute.
/// Compatibility shim for storage IgnoreIfNull attribute.
/// In PostgreSQL mode, this attribute is ignored but allows code to compile.
/// </summary>
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)]
public class BsonIgnoreIfNullAttribute : Attribute
public class StorageIgnoreIfNullAttribute : Attribute
{
}
/// <summary>
/// Compatibility shim for MongoDB BsonRepresentation attribute.
/// Compatibility shim for storage Representation attribute.
/// In PostgreSQL mode, this attribute is ignored but allows code to compile.
/// </summary>
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)]
public class BsonRepresentationAttribute : Attribute
public class StorageRepresentationAttribute : Attribute
{
public BsonType Representation { get; }
public StorageType Representation { get; }
public BsonRepresentationAttribute(BsonType representation)
public StorageRepresentationAttribute(StorageType representation)
{
Representation = representation;
}

View File

@@ -1,7 +1,7 @@
namespace MongoDB.Bson;
namespace StellaOps.Storage.Documents;
/// <summary>
/// Compatibility shim for MongoDB ObjectId.
/// Compatibility shim for storage ObjectId.
/// In PostgreSQL mode, this wraps a GUID string.
/// </summary>
public readonly struct ObjectId : IEquatable<ObjectId>, IComparable<ObjectId>
@@ -51,9 +51,9 @@ public readonly struct ObjectId : IEquatable<ObjectId>, IComparable<ObjectId>
}
/// <summary>
/// Compatibility shim for MongoDB BsonType enum.
/// Compatibility shim for storage document type enum.
/// </summary>
public enum BsonType
public enum StorageType
{
EndOfDocument = 0,
Double = 1,

View File

@@ -1,7 +1,7 @@
namespace StellaOps.Authority.Storage.Mongo.Sessions;
namespace StellaOps.Authority.Storage.Sessions;
/// <summary>
/// Compatibility shim for MongoDB session handle. In PostgreSQL mode, this is unused.
/// Compatibility shim for database session handle. In PostgreSQL mode, this is unused.
/// </summary>
public interface IClientSessionHandle : IDisposable
{

View File

@@ -6,8 +6,8 @@
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
<RootNamespace>StellaOps.Authority.Storage.Mongo</RootNamespace>
<Description>MongoDB compatibility shim for Authority storage - provides in-memory implementations for Mongo interfaces while PostgreSQL migration is in progress</Description>
<RootNamespace>StellaOps.Authority.Storage.InMemory</RootNamespace>
<Description>In-memory storage shim for Authority - provides in-memory implementations for storage interfaces while PostgreSQL migration is in progress</Description>
</PropertyGroup>
<ItemGroup>

View File

@@ -109,7 +109,7 @@ public sealed class AuthorityAdvisoryAiConsentEvaluatorTests
Issuer = new Uri("https://authority.test")
};
options.Storage.ConnectionString = "mongodb://localhost:27017/authority";
options.Storage.ConnectionString = "Host=localhost;Port=5432;Database=authority";
options.Signing.ActiveKeyId = "test-key";
options.Signing.KeyPath = "/tmp/test-key.pem";

View File

@@ -107,9 +107,9 @@ public sealed class AuthorityWebApplicationFactory : WebApplicationFactory<Progr
services.RemoveAll<IAuthorityRevocationExportStateStore>();
services.RemoveAll<IAuthoritySessionAccessor>();
services.AddAuthorityMongoStorage(options =>
services.AddAuthorityInMemoryStorage(options =>
{
options.ConnectionString = "mongodb://localhost/authority-tests";
options.ConnectionString = "Host=localhost;Database=authority-tests";
options.DatabaseName = "authority-tests";
});
});

View File

@@ -120,7 +120,7 @@ public sealed class AuthorityAckTokenIssuerTests
return new StellaOpsAuthorityOptions
{
Issuer = new Uri("https://authority.test"),
Storage = { ConnectionString = "mongodb://localhost/test" },
Storage = { ConnectionString = "Host=localhost;Database=test" },
Notifications =
{
AckTokens =

View File

@@ -81,7 +81,7 @@ public sealed class AuthorityAckTokenKeyManagerTests
return new StellaOpsAuthorityOptions
{
Issuer = new Uri("https://authority.test"),
Storage = { ConnectionString = "mongodb://localhost/test" },
Storage = { ConnectionString = "Host=localhost;Database=test" },
Notifications =
{
AckTokens =

View File

@@ -44,7 +44,7 @@ public sealed class AuthorityWebhookAllowlistEvaluatorTests
return new StellaOpsAuthorityOptions
{
Issuer = new Uri("https://authority.test"),
Storage = { ConnectionString = "mongodb://localhost/test" },
Storage = { ConnectionString = "Host=localhost;Database=test" },
Notifications =
{
Webhooks =

View File

@@ -550,7 +550,7 @@ public class ClientCredentialsHandlersTests
await validateHandler.HandleAsync(validateContext);
Assert.False(validateContext.IsRejected);
var sessionAccessor = new NullMongoSessionAccessor();
var sessionAccessor = new NullSessionAccessor();
var handleHandler = new HandleClientCredentialsHandler(
registry,
tokenStore,
@@ -2485,7 +2485,7 @@ public class ClientCredentialsHandlersTests
await validateHandler.HandleAsync(validateContext);
Assert.False(validateContext.IsRejected);
var sessionAccessor = new NullMongoSessionAccessor();
var sessionAccessor = new NullSessionAccessor();
var handleHandler = new HandleClientCredentialsHandler(
registry,
tokenStore,
@@ -2691,14 +2691,14 @@ public class ClientCredentialsHandlersTests
var handleHandler = new HandleClientCredentialsHandler(
registry,
tokenStore,
new NullMongoSessionAccessor(),
new NullSessionAccessor(),
rateMetadata,
TimeProvider.System,
TestInstruments.ActivitySource,
NullLogger<HandleClientCredentialsHandler>.Instance);
var persistHandler = new PersistTokensHandler(
tokenStore,
new NullMongoSessionAccessor(),
new NullSessionAccessor(),
TimeProvider.System,
TestInstruments.ActivitySource,
NullLogger<PersistTokensHandler>.Instance);
@@ -2742,7 +2742,7 @@ public class ClientCredentialsHandlersTests
var tokenStore = new TestTokenStore();
var persistHandler = new PersistTokensHandler(
tokenStore,
new NullMongoSessionAccessor(),
new NullSessionAccessor(),
TimeProvider.System,
TestInstruments.ActivitySource,
NullLogger<PersistTokensHandler>.Instance);
@@ -2799,7 +2799,7 @@ public class ClientCredentialsHandlersTests
options.Security.SenderConstraints.Dpop.Nonce.RequiredAudiences.Add("signer");
options.Signing.ActiveKeyId = "test-key";
options.Signing.KeyPath = "/tmp/test-key.pem";
options.Storage.ConnectionString = "mongodb://localhost/test";
options.Storage.ConnectionString = "Host=localhost;Database=test";
Assert.Contains("signer", options.Security.SenderConstraints.Dpop.Nonce.RequiredAudiences);
var clientDocument = CreateClient(
@@ -2944,7 +2944,7 @@ public class ClientCredentialsHandlersTests
options.Security.SenderConstraints.Mtls.AllowedSanTypes.Clear();
options.Signing.ActiveKeyId = "test-key";
options.Signing.KeyPath = "/tmp/test-key.pem";
options.Storage.ConnectionString = "mongodb://localhost/test";
options.Storage.ConnectionString = "Host=localhost;Database=test";
var clientDocument = CreateClient(
secret: "s3cr3t!",
@@ -3009,7 +3009,7 @@ public class ClientCredentialsHandlersTests
options.Security.SenderConstraints.Mtls.Enabled = true;
options.Signing.ActiveKeyId = "test-key";
options.Signing.KeyPath = "/tmp/test-key.pem";
options.Storage.ConnectionString = "mongodb://localhost/test";
options.Storage.ConnectionString = "Host=localhost;Database=test";
var clientDocument = CreateClient(
secret: "s3cr3t!",
@@ -3151,7 +3151,7 @@ public class ClientCredentialsHandlersTests
var descriptor = CreateDescriptor(clientDocument);
var registry = CreateRegistry(withClientProvisioning: true, clientDescriptor: descriptor);
var tokenStore = new TestTokenStore();
var sessionAccessor = new NullMongoSessionAccessor();
var sessionAccessor = new NullSessionAccessor();
var authSink = new TestAuthEventSink();
var metadataAccessor = new TestRateLimiterMetadataAccessor();
var serviceAccountStore = new TestServiceAccountStore();
@@ -3240,7 +3240,7 @@ public class ClientCredentialsHandlersTests
var registry = CreateRegistry(withClientProvisioning: true, clientDescriptor: CreateDescriptor(clientDocument));
var tokenStore = new TestTokenStore();
var sessionAccessor = new NullMongoSessionAccessor();
var sessionAccessor = new NullSessionAccessor();
var authSink = new TestAuthEventSink();
var metadataAccessor = new TestRateLimiterMetadataAccessor();
var serviceAccountStore = new TestServiceAccountStore(serviceAccount);
@@ -3323,7 +3323,7 @@ public class ClientCredentialsHandlersTests
var registry = CreateRegistry(withClientProvisioning: true, clientDescriptor: CreateDescriptor(clientDocument));
var tokenStore = new TestTokenStore();
var sessionAccessor = new NullMongoSessionAccessor();
var sessionAccessor = new NullSessionAccessor();
var authSink = new TestAuthEventSink();
var metadataAccessor = new TestRateLimiterMetadataAccessor();
var serviceAccountStore = new TestServiceAccountStore(serviceAccount);
@@ -3424,7 +3424,7 @@ public class ClientCredentialsHandlersTests
var registry = CreateRegistry(withClientProvisioning: true, clientDescriptor: CreateDescriptor(clientDocument));
var tokenStore = new TestTokenStore();
var sessionAccessor = new NullMongoSessionAccessor();
var sessionAccessor = new NullSessionAccessor();
var authSink = new TestAuthEventSink();
var metadataAccessor = new TestRateLimiterMetadataAccessor();
var serviceAccountStore = new TestServiceAccountStore(serviceAccount);
@@ -3498,7 +3498,7 @@ public class TokenValidationHandlersTests
var metadataAccessor = new TestRateLimiterMetadataAccessor();
var auditSink = new TestAuthEventSink();
var sessionAccessor = new NullMongoSessionAccessor();
var sessionAccessor = new NullSessionAccessor();
var handler = new ValidateAccessTokenHandler(
tokenStore,
sessionAccessor,
@@ -3548,7 +3548,7 @@ public class TokenValidationHandlersTests
var metadataAccessor = new TestRateLimiterMetadataAccessor();
var auditSink = new TestAuthEventSink();
var sessionAccessor = new NullMongoSessionAccessor();
var sessionAccessor = new NullSessionAccessor();
var handler = new ValidateAccessTokenHandler(
tokenStore,
sessionAccessor,
@@ -3603,7 +3603,7 @@ public class TokenValidationHandlersTests
var metadataAccessor = new TestRateLimiterMetadataAccessor();
var auditSink = new TestAuthEventSink();
var sessionAccessor = new NullMongoSessionAccessor();
var sessionAccessor = new NullSessionAccessor();
var handler = new ValidateAccessTokenHandler(
tokenStore,
sessionAccessor,
@@ -3654,7 +3654,7 @@ public class TokenValidationHandlersTests
var metadataAccessor = new TestRateLimiterMetadataAccessor();
var auditSink = new TestAuthEventSink();
var sessionAccessor = new NullMongoSessionAccessor();
var sessionAccessor = new NullSessionAccessor();
var handler = new ValidateAccessTokenHandler(
tokenStore,
sessionAccessor,
@@ -3704,7 +3704,7 @@ public class TokenValidationHandlersTests
var metadataAccessor = new TestRateLimiterMetadataAccessor();
var auditSink = new TestAuthEventSink();
var sessionAccessor = new NullMongoSessionAccessor();
var sessionAccessor = new NullSessionAccessor();
var handler = new ValidateAccessTokenHandler(
tokenStore,
sessionAccessor,
@@ -3755,7 +3755,7 @@ public class TokenValidationHandlersTests
var metadataAccessorSuccess = new TestRateLimiterMetadataAccessor();
var auditSinkSuccess = new TestAuthEventSink();
var sessionAccessor = new NullMongoSessionAccessor();
var sessionAccessor = new NullSessionAccessor();
var handler = new ValidateAccessTokenHandler(
new TestTokenStore(),
sessionAccessor,
@@ -3812,7 +3812,7 @@ public class TokenValidationHandlersTests
var registry = CreateRegistry(withClientProvisioning: false, clientDescriptor: null);
var metadataAccessor = new TestRateLimiterMetadataAccessor();
var auditSink = new TestAuthEventSink();
var sessionAccessor = new NullMongoSessionAccessor();
var sessionAccessor = new NullSessionAccessor();
var handler = new ValidateAccessTokenHandler(
tokenStore,
sessionAccessor,
@@ -3886,7 +3886,7 @@ public class TokenValidationHandlersTests
clientDocument.ClientId = "agent";
var auditSink = new TestAuthEventSink();
var registry = CreateRegistry(withClientProvisioning: false, clientDescriptor: null);
var sessionAccessorReplay = new NullMongoSessionAccessor();
var sessionAccessorReplay = new NullSessionAccessor();
var handler = new ValidateAccessTokenHandler(
tokenStore,
sessionAccessorReplay,
@@ -3939,7 +3939,7 @@ public class AuthorityClientCertificateValidatorTests
options.Security.SenderConstraints.Mtls.AllowedSanTypes.Add("uri");
options.Signing.ActiveKeyId = "test-key";
options.Signing.KeyPath = "/tmp/test-key.pem";
options.Storage.ConnectionString = "mongodb://localhost/test";
options.Storage.ConnectionString = "Host=localhost;Database=test";
using var rsa = RSA.Create(2048);
var request = new CertificateRequest("CN=mtls-client", rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
@@ -3977,7 +3977,7 @@ public class AuthorityClientCertificateValidatorTests
options.Security.SenderConstraints.Mtls.RotationGrace = TimeSpan.FromMinutes(5);
options.Signing.ActiveKeyId = "test-key";
options.Signing.KeyPath = "/tmp/test-key.pem";
options.Storage.ConnectionString = "mongodb://localhost/test";
options.Storage.ConnectionString = "Host=localhost;Database=test";
using var rsa = RSA.Create(2048);
var request = new CertificateRequest("CN=mtls-client", rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
@@ -4017,7 +4017,7 @@ public class AuthorityClientCertificateValidatorTests
options.Security.SenderConstraints.Mtls.RequireChainValidation = false;
options.Signing.ActiveKeyId = "test-key";
options.Signing.KeyPath = "/tmp/test-key.pem";
options.Storage.ConnectionString = "mongodb://localhost/test";
options.Storage.ConnectionString = "Host=localhost;Database=test";
using var rsa = RSA.Create(2048);
var request = new CertificateRequest("CN=mtls-client", rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
@@ -4055,7 +4055,7 @@ public class AuthorityClientCertificateValidatorTests
options.Security.SenderConstraints.Mtls.RequireChainValidation = false;
options.Signing.ActiveKeyId = "test-key";
options.Signing.KeyPath = "/tmp/test-key.pem";
options.Storage.ConnectionString = "mongodb://localhost/test";
options.Storage.ConnectionString = "Host=localhost;Database=test";
using var rsa = RSA.Create(2048);
var request = new CertificateRequest("CN=mtls-client", rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
@@ -4475,7 +4475,7 @@ internal sealed class StubCertificateValidator : IAuthorityClientCertificateVali
}
}
internal sealed class NullMongoSessionAccessor : IAuthoritySessionAccessor
internal sealed class NullSessionAccessor : IAuthoritySessionAccessor
{
public IClientSessionHandle? CurrentSession => null;
@@ -4506,7 +4506,7 @@ public class ObservabilityIncidentTokenHandlerTests
var metadataAccessor = new TestRateLimiterMetadataAccessor();
var auditSink = new TestAuthEventSink();
var sessionAccessor = new NullMongoSessionAccessor();
var sessionAccessor = new NullSessionAccessor();
var handler = new ValidateAccessTokenHandler(
tokenStore,
sessionAccessor,
@@ -4562,7 +4562,7 @@ public class ObservabilityIncidentTokenHandlerTests
var metadataAccessor = new TestRateLimiterMetadataAccessor();
var auditSink = new TestAuthEventSink();
var sessionAccessor = new NullMongoSessionAccessor();
var sessionAccessor = new NullSessionAccessor();
var handler = new ValidateAccessTokenHandler(
tokenStore,
sessionAccessor,
@@ -4620,7 +4620,7 @@ public class ObservabilityIncidentTokenHandlerTests
var metadataAccessor = new TestRateLimiterMetadataAccessor();
var auditSink = new TestAuthEventSink();
var sessionAccessor = new NullMongoSessionAccessor();
var sessionAccessor = new NullSessionAccessor();
var handler = new ValidateAccessTokenHandler(
tokenStore,
sessionAccessor,
@@ -4682,7 +4682,7 @@ public class ObservabilityIncidentTokenHandlerTests
var metadataAccessor = new TestRateLimiterMetadataAccessor();
var auditSink = new TestAuthEventSink();
var sessionAccessor = new NullMongoSessionAccessor();
var sessionAccessor = new NullSessionAccessor();
var handler = new ValidateAccessTokenHandler(
tokenStore,
sessionAccessor,
@@ -4818,7 +4818,7 @@ public class ObservabilityIncidentTokenHandlerTests
var metadataAccessor = new TestRateLimiterMetadataAccessor();
var auditSink = new TestAuthEventSink();
var sessionAccessor = new NullMongoSessionAccessor();
var sessionAccessor = new NullSessionAccessor();
var handler = new ValidateAccessTokenHandler(
tokenStore,
sessionAccessor,
@@ -4879,7 +4879,7 @@ public class ObservabilityIncidentTokenHandlerTests
var metadataAccessor = new TestRateLimiterMetadataAccessor();
var auditSink = new TestAuthEventSink();
var sessionAccessor = new NullMongoSessionAccessor();
var sessionAccessor = new NullSessionAccessor();
var handler = new ValidateAccessTokenHandler(
tokenStore,
sessionAccessor,
@@ -5166,7 +5166,7 @@ internal static class TestHelpers
options.Signing.ActiveKeyId = "test-key";
options.Signing.KeyPath = "/tmp/test-key.pem";
options.Storage.ConnectionString = "mongodb://localhost/test";
options.Storage.ConnectionString = "Host=localhost;Database=test";
configure?.Invoke(options);
return options;

View File

@@ -780,7 +780,7 @@ public class PasswordGrantHandlersTests
};
options.Signing.ActiveKeyId = "test-key";
options.Signing.KeyPath = "/tmp/test-key.pem";
options.Storage.ConnectionString = "mongodb://localhost:27017/authority";
options.Storage.ConnectionString = "Host=localhost;Port=5432;Database=authority";
configure?.Invoke(options);
return options;

View File

@@ -40,7 +40,7 @@ public sealed class VulnPermalinkServiceTests
var options = new StellaOpsAuthorityOptions
{
Issuer = new Uri("https://authority.test"),
Storage = { ConnectionString = "mongodb://localhost/test" },
Storage = { ConnectionString = "Host=localhost;Database=test" },
Signing =
{
Enabled = true,

View File

@@ -88,7 +88,7 @@ public class AuthorityRateLimiterIntegrationTests
Issuer = new Uri("https://authority.integration.test"),
SchemaVersion = 1
};
options.Storage.ConnectionString = "mongodb://localhost/authority";
options.Storage.ConnectionString = "Host=localhost;Database=authority";
configure?.Invoke(options);

View File

@@ -74,7 +74,7 @@ public class AuthorityRateLimiterTests
SchemaVersion = 1
};
options.Storage.ConnectionString = "mongodb://localhost/authority";
options.Storage.ConnectionString = "Host=localhost;Database=authority";
return options;
}
}

View File

@@ -119,7 +119,7 @@ public sealed class AuthorityJwksServiceTests
Issuer = new Uri("https://authority.test"),
Storage =
{
ConnectionString = "mongodb://localhost/test"
ConnectionString = "Host=localhost;Database=test"
},
Signing =
{

View File

@@ -34,7 +34,7 @@ public sealed class AuthoritySigningKeyManagerTests
var options = new StellaOpsAuthorityOptions
{
Issuer = new Uri("https://authority.test"),
Storage = { ConnectionString = "mongodb://localhost/test" },
Storage = { ConnectionString = "Host=localhost;Database=test" },
Signing =
{
Enabled = true,

View File

@@ -10,7 +10,7 @@ internal static class TestEnvironment
OpenSslLegacyShim.EnsureOpenSsl11();
Environment.SetEnvironmentVariable("STELLAOPS_AUTHORITY_ISSUER", "https://authority.test");
Environment.SetEnvironmentVariable("STELLAOPS_AUTHORITY_STORAGE__CONNECTIONSTRING", "mongodb://localhost/authority");
Environment.SetEnvironmentVariable("STELLAOPS_AUTHORITY_STORAGE__CONNECTIONSTRING", "Host=localhost;Database=authority");
Environment.SetEnvironmentVariable("STELLAOPS_AUTHORITY_SIGNING__ENABLED", "false");
}
}

View File

@@ -19,7 +19,7 @@ using Microsoft.Net.Http.Headers;
using OpenIddict.Abstractions;
using OpenIddict.Server;
using OpenIddict.Server.AspNetCore;
// MongoDB.Driver removed - using PostgreSQL storage with Mongo compatibility shim
// Using PostgreSQL storage with in-memory compatibility shim
using Serilog;
using Serilog.Events;
using StellaOps.Authority;

View File

@@ -18,7 +18,7 @@
<PackageReference Include="OpenTelemetry.Instrumentation.Runtime" Version="1.12.0" />
<PackageReference Include="Serilog.AspNetCore" Version="8.0.1" />
<PackageReference Include="Serilog.Sinks.Console" Version="5.0.1" />
<PackageReference Include="StackExchange.Redis" Version="2.8.24" />
<PackageReference Include="StackExchange.Redis" Version="2.8.37" />
<PackageReference Include="YamlDotNet" Version="13.7.1" />
<ProjectReference Include="..\StellaOps.Authority.Plugins.Abstractions\StellaOps.Authority.Plugins.Abstractions.csproj" />
<ProjectReference Include="..\StellaOps.Authority.Plugin.Standard\StellaOps.Authority.Plugin.Standard.csproj" />

View File

@@ -67,7 +67,7 @@ public static class ServiceCollectionExtensions
services.AddScoped<ITokenRepository>(sp => sp.GetRequiredService<TokenRepository>());
services.AddScoped<IRefreshTokenRepository>(sp => sp.GetRequiredService<RefreshTokenRepository>());
// Mongo-store equivalents (PostgreSQL-backed)
// Additional stores (PostgreSQL-backed)
services.AddScoped<BootstrapInviteRepository>();
services.AddScoped<ServiceAccountRepository>();
services.AddScoped<ClientRepository>();

View File

@@ -16,7 +16,7 @@ public sealed class BaselineLoaderTests
{
await File.WriteAllTextAsync(
path,
"scenario,iterations,observations,aliases,linksets,mean_total_ms,p95_total_ms,max_total_ms,mean_insert_ms,mean_correlation_ms,mean_throughput_per_sec,min_throughput_per_sec,mean_mongo_throughput_per_sec,min_mongo_throughput_per_sec,max_allocated_mb\n" +
"scenario,iterations,observations,aliases,linksets,mean_total_ms,p95_total_ms,max_total_ms,mean_insert_ms,mean_correlation_ms,mean_throughput_per_sec,min_throughput_per_sec,mean_insert_throughput_per_sec,min_insert_throughput_per_sec,max_allocated_mb\n" +
"lnm_ingest_baseline,5,5000,500,450,320.5,340.1,360.9,120.2,210.3,15000.0,13500.0,18000.0,16500.0,96.5\n");
var baseline = await BaselineLoader.LoadAsync(path, CancellationToken.None);
@@ -27,7 +27,7 @@ public sealed class BaselineLoaderTests
Assert.Equal(5000, entry.Value.Observations);
Assert.Equal(500, entry.Value.Aliases);
Assert.Equal(360.9, entry.Value.MaxTotalMs);
Assert.Equal(16500.0, entry.Value.MinMongoThroughputPerSecond);
Assert.Equal(16500.0, entry.Value.MinInsertThroughputPerSecond);
Assert.Equal(96.5, entry.Value.MaxAllocatedMb);
}
finally

View File

@@ -24,7 +24,7 @@ public sealed class BenchmarkScenarioReportTests
AllocationStatistics: new AllocationStatistics(120),
ThresholdMs: null,
MinThroughputThresholdPerSecond: null,
MinMongoThroughputThresholdPerSecond: null,
MinInsertThroughputThresholdPerSecond: null,
MaxAllocatedThresholdMb: null);
var baseline = new BaselineEntry(
@@ -40,15 +40,15 @@ public sealed class BenchmarkScenarioReportTests
MeanCorrelationMs: 90,
MeanThroughputPerSecond: 9000,
MinThroughputPerSecond: 8500,
MeanMongoThroughputPerSecond: 10000,
MinMongoThroughputPerSecond: 9500,
MeanInsertThroughputPerSecond: 10000,
MinInsertThroughputPerSecond: 9500,
MaxAllocatedMb: 100);
var report = new BenchmarkScenarioReport(result, baseline, regressionLimit: 1.1);
Assert.True(report.DurationRegressionBreached);
Assert.True(report.ThroughputRegressionBreached);
Assert.True(report.MongoThroughputRegressionBreached);
Assert.True(report.InsertThroughputRegressionBreached);
Assert.Contains(report.BuildRegressionFailureMessages(), message => message.Contains("max duration"));
}
@@ -70,7 +70,7 @@ public sealed class BenchmarkScenarioReportTests
AllocationStatistics: new AllocationStatistics(64),
ThresholdMs: null,
MinThroughputThresholdPerSecond: null,
MinMongoThroughputThresholdPerSecond: null,
MinInsertThroughputThresholdPerSecond: null,
MaxAllocatedThresholdMb: null);
var report = new BenchmarkScenarioReport(result, baseline: null, regressionLimit: null);

View File

@@ -13,6 +13,6 @@ internal sealed record BaselineEntry(
double MeanCorrelationMs,
double MeanThroughputPerSecond,
double MinThroughputPerSecond,
double MeanMongoThroughputPerSecond,
double MinMongoThroughputPerSecond,
double MeanInsertThroughputPerSecond,
double MinInsertThroughputPerSecond,
double MaxAllocatedMb);

View File

@@ -55,8 +55,8 @@ internal static class BaselineLoader
MeanCorrelationMs: ParseDouble(parts[9], resolved, lineNumber),
MeanThroughputPerSecond: ParseDouble(parts[10], resolved, lineNumber),
MinThroughputPerSecond: ParseDouble(parts[11], resolved, lineNumber),
MeanMongoThroughputPerSecond: ParseDouble(parts[12], resolved, lineNumber),
MinMongoThroughputPerSecond: ParseDouble(parts[13], resolved, lineNumber),
MeanInsertThroughputPerSecond: ParseDouble(parts[12], resolved, lineNumber),
MinInsertThroughputPerSecond: ParseDouble(parts[13], resolved, lineNumber),
MaxAllocatedMb: ParseDouble(parts[14], resolved, lineNumber));
result[entry.ScenarioId] = entry;

View File

@@ -6,7 +6,7 @@ namespace StellaOps.Bench.LinkNotMerge;
internal sealed record BenchmarkConfig(
double? ThresholdMs,
double? MinThroughputPerSecond,
double? MinMongoThroughputPerSecond,
double? MinInsertThroughputPerSecond,
double? MaxAllocatedMb,
int? Iterations,
IReadOnlyList<LinkNotMergeScenarioConfig> Scenarios)
@@ -49,7 +49,7 @@ internal sealed record BenchmarkConfig(
return new BenchmarkConfig(
model.ThresholdMs,
model.MinThroughputPerSecond,
model.MinMongoThroughputPerSecond,
model.MinInsertThroughputPerSecond,
model.MaxAllocatedMb,
model.Iterations,
model.Scenarios);
@@ -63,8 +63,8 @@ internal sealed record BenchmarkConfig(
[JsonPropertyName("minThroughputPerSecond")]
public double? MinThroughputPerSecond { get; init; }
[JsonPropertyName("minMongoThroughputPerSecond")]
public double? MinMongoThroughputPerSecond { get; init; }
[JsonPropertyName("minInsertThroughputPerSecond")]
public double? MinInsertThroughputPerSecond { get; init; }
[JsonPropertyName("maxAllocatedMb")]
public double? MaxAllocatedMb { get; init; }
@@ -127,8 +127,8 @@ internal sealed class LinkNotMergeScenarioConfig
[JsonPropertyName("minThroughputPerSecond")]
public double? MinThroughputPerSecond { get; init; }
[JsonPropertyName("minMongoThroughputPerSecond")]
public double? MinMongoThroughputPerSecond { get; init; }
[JsonPropertyName("minInsertThroughputPerSecond")]
public double? MinInsertThroughputPerSecond { get; init; }
[JsonPropertyName("maxAllocatedMb")]
public double? MaxAllocatedMb { get; init; }

View File

@@ -29,11 +29,11 @@ internal static class Program
var correlationStats = DurationStatistics.From(execution.CorrelationDurationsMs);
var allocationStats = AllocationStatistics.From(execution.AllocatedMb);
var throughputStats = ThroughputStatistics.From(execution.TotalThroughputsPerSecond);
var mongoThroughputStats = ThroughputStatistics.From(execution.InsertThroughputsPerSecond);
var insertThroughputStats = ThroughputStatistics.From(execution.InsertThroughputsPerSecond);
var thresholdMs = scenario.ThresholdMs ?? options.ThresholdMs ?? config.ThresholdMs;
var throughputFloor = scenario.MinThroughputPerSecond ?? options.MinThroughputPerSecond ?? config.MinThroughputPerSecond;
var mongoThroughputFloor = scenario.MinMongoThroughputPerSecond ?? options.MinMongoThroughputPerSecond ?? config.MinMongoThroughputPerSecond;
var insertThroughputFloor = scenario.MinInsertThroughputPerSecond ?? options.MinInsertThroughputPerSecond ?? config.MinInsertThroughputPerSecond;
var allocationLimit = scenario.MaxAllocatedMb ?? options.MaxAllocatedMb ?? config.MaxAllocatedMb;
var result = new ScenarioResult(
@@ -47,11 +47,11 @@ internal static class Program
insertStats,
correlationStats,
throughputStats,
mongoThroughputStats,
insertThroughputStats,
allocationStats,
thresholdMs,
throughputFloor,
mongoThroughputFloor,
insertThroughputFloor,
allocationLimit);
results.Add(result);
@@ -66,9 +66,9 @@ internal static class Program
failures.Add($"{result.Id} fell below throughput floor: {result.TotalThroughputStatistics.MinPerSecond:N0} obs/s < {floor:N0} obs/s");
}
if (mongoThroughputFloor is { } mongoFloor && result.InsertThroughputStatistics.MinPerSecond < mongoFloor)
if (insertThroughputFloor is { } insertFloor && result.InsertThroughputStatistics.MinPerSecond < insertFloor)
{
failures.Add($"{result.Id} fell below Mongo throughput floor: {result.InsertThroughputStatistics.MinPerSecond:N0} ops/s < {mongoFloor:N0} ops/s");
failures.Add($"{result.Id} fell below insert throughput floor: {result.InsertThroughputStatistics.MinPerSecond:N0} ops/s < {insertFloor:N0} ops/s");
}
if (allocationLimit is { } limit && result.AllocationStatistics.MaxAllocatedMb > limit)
@@ -131,7 +131,7 @@ internal static class Program
int? Iterations,
double? ThresholdMs,
double? MinThroughputPerSecond,
double? MinMongoThroughputPerSecond,
double? MinInsertThroughputPerSecond,
double? MaxAllocatedMb,
string? CsvOutPath,
string? JsonOutPath,
@@ -150,7 +150,7 @@ internal static class Program
int? iterations = null;
double? thresholdMs = null;
double? minThroughput = null;
double? minMongoThroughput = null;
double? minInsertThroughput = null;
double? maxAllocated = null;
string? csvOut = null;
string? jsonOut = null;
@@ -181,9 +181,9 @@ internal static class Program
EnsureNext(args, index);
minThroughput = double.Parse(args[++index], CultureInfo.InvariantCulture);
break;
case "--min-mongo-throughput":
case "--min-insert-throughput":
EnsureNext(args, index);
minMongoThroughput = double.Parse(args[++index], CultureInfo.InvariantCulture);
minInsertThroughput = double.Parse(args[++index], CultureInfo.InvariantCulture);
break;
case "--max-allocated-mb":
EnsureNext(args, index);
@@ -236,7 +236,7 @@ internal static class Program
iterations,
thresholdMs,
minThroughput,
minMongoThroughput,
minInsertThroughput,
maxAllocated,
csvOut,
jsonOut,
@@ -281,7 +281,7 @@ internal static class Program
Console.WriteLine(" --iterations <count> Override iteration count.");
Console.WriteLine(" --threshold-ms <value> Global latency threshold in milliseconds.");
Console.WriteLine(" --min-throughput <value> Global throughput floor (observations/second).");
Console.WriteLine(" --min-mongo-throughput <value> Mongo insert throughput floor (ops/second).");
Console.WriteLine(" --min-insert-throughput <value> Insert throughput floor (ops/second).");
Console.WriteLine(" --max-allocated-mb <value> Global allocation ceiling (MB).");
Console.WriteLine(" --csv <path> Write CSV results to path.");
Console.WriteLine(" --json <path> Write JSON results to path.");
@@ -299,7 +299,7 @@ internal static class TablePrinter
{
public static void Print(IEnumerable<ScenarioResult> results)
{
Console.WriteLine("Scenario | Observations | Aliases | Linksets | Total(ms) | Correl(ms) | Insert(ms) | Min k/s | Mongo k/s | Alloc(MB)");
Console.WriteLine("Scenario | Observations | Aliases | Linksets | Total(ms) | Correl(ms) | Insert(ms) | Min k/s | Ins k/s | Alloc(MB)");
Console.WriteLine("---------------------------- | ------------- | ------- | -------- | ---------- | ---------- | ----------- | -------- | --------- | --------");
foreach (var row in results)
{
@@ -313,7 +313,7 @@ internal static class TablePrinter
row.CorrelationMeanColumn,
row.InsertMeanColumn,
row.ThroughputColumn,
row.MongoThroughputColumn,
row.InsertThroughputColumn,
row.AllocatedColumn,
}));
}
@@ -336,7 +336,7 @@ internal static class CsvWriter
using var stream = new FileStream(resolved, FileMode.Create, FileAccess.Write, FileShare.None);
using var writer = new StreamWriter(stream);
writer.WriteLine("scenario,iterations,observations,aliases,linksets,mean_total_ms,p95_total_ms,max_total_ms,mean_insert_ms,mean_correlation_ms,mean_throughput_per_sec,min_throughput_per_sec,mean_mongo_throughput_per_sec,min_mongo_throughput_per_sec,max_allocated_mb");
writer.WriteLine("scenario,iterations,observations,aliases,linksets,mean_total_ms,p95_total_ms,max_total_ms,mean_insert_ms,mean_correlation_ms,mean_throughput_per_sec,min_throughput_per_sec,mean_insert_throughput_per_sec,min_insert_throughput_per_sec,max_allocated_mb");
foreach (var result in results)
{

View File

@@ -62,7 +62,7 @@ internal static class BenchmarkJsonWriter
report.Result.AllocationStatistics.MaxAllocatedMb,
report.Result.ThresholdMs,
report.Result.MinThroughputThresholdPerSecond,
report.Result.MinMongoThroughputThresholdPerSecond,
report.Result.MinInsertThroughputThresholdPerSecond,
report.Result.MaxAllocatedThresholdMb,
baseline is null
? null
@@ -78,13 +78,13 @@ internal static class BenchmarkJsonWriter
baseline.MeanCorrelationMs,
baseline.MeanThroughputPerSecond,
baseline.MinThroughputPerSecond,
baseline.MeanMongoThroughputPerSecond,
baseline.MinMongoThroughputPerSecond,
baseline.MeanInsertThroughputPerSecond,
baseline.MinInsertThroughputPerSecond,
baseline.MaxAllocatedMb),
new BenchmarkJsonScenarioRegression(
report.DurationRegressionRatio,
report.ThroughputRegressionRatio,
report.MongoThroughputRegressionRatio,
report.InsertThroughputRegressionRatio,
report.RegressionLimit,
report.RegressionBreached));
}
@@ -110,12 +110,12 @@ internal static class BenchmarkJsonWriter
double MeanCorrelationMs,
double MeanThroughputPerSecond,
double MinThroughputPerSecond,
double MeanMongoThroughputPerSecond,
double MinMongoThroughputPerSecond,
double MeanInsertThroughputPerSecond,
double MinInsertThroughputPerSecond,
double MaxAllocatedMb,
double? ThresholdMs,
double? MinThroughputThresholdPerSecond,
double? MinMongoThroughputThresholdPerSecond,
double? MinInsertThroughputThresholdPerSecond,
double? MaxAllocatedThresholdMb,
BenchmarkJsonScenarioBaseline? Baseline,
BenchmarkJsonScenarioRegression Regression);
@@ -132,14 +132,14 @@ internal static class BenchmarkJsonWriter
double MeanCorrelationMs,
double MeanThroughputPerSecond,
double MinThroughputPerSecond,
double MeanMongoThroughputPerSecond,
double MinMongoThroughputPerSecond,
double MeanInsertThroughputPerSecond,
double MinInsertThroughputPerSecond,
double MaxAllocatedMb);
private sealed record BenchmarkJsonScenarioRegression(
double? DurationRatio,
double? ThroughputRatio,
double? MongoThroughputRatio,
double? InsertThroughputRatio,
double Limit,
bool Breached);
}

View File

@@ -13,7 +13,7 @@ internal sealed class BenchmarkScenarioReport
RegressionLimit = regressionLimit is { } limit && limit > 0 ? limit : DefaultRegressionLimit;
DurationRegressionRatio = CalculateRatio(result.TotalStatistics.MaxMs, baseline?.MaxTotalMs);
ThroughputRegressionRatio = CalculateInverseRatio(result.TotalThroughputStatistics.MinPerSecond, baseline?.MinThroughputPerSecond);
MongoThroughputRegressionRatio = CalculateInverseRatio(result.InsertThroughputStatistics.MinPerSecond, baseline?.MinMongoThroughputPerSecond);
InsertThroughputRegressionRatio = CalculateInverseRatio(result.InsertThroughputStatistics.MinPerSecond, baseline?.MinInsertThroughputPerSecond);
}
public ScenarioResult Result { get; }
@@ -26,15 +26,15 @@ internal sealed class BenchmarkScenarioReport
public double? ThroughputRegressionRatio { get; }
public double? MongoThroughputRegressionRatio { get; }
public double? InsertThroughputRegressionRatio { get; }
public bool DurationRegressionBreached => DurationRegressionRatio is { } ratio && ratio >= RegressionLimit;
public bool ThroughputRegressionBreached => ThroughputRegressionRatio is { } ratio && ratio >= RegressionLimit;
public bool MongoThroughputRegressionBreached => MongoThroughputRegressionRatio is { } ratio && ratio >= RegressionLimit;
public bool InsertThroughputRegressionBreached => InsertThroughputRegressionRatio is { } ratio && ratio >= RegressionLimit;
public bool RegressionBreached => DurationRegressionBreached || ThroughputRegressionBreached || MongoThroughputRegressionBreached;
public bool RegressionBreached => DurationRegressionBreached || ThroughputRegressionBreached || InsertThroughputRegressionBreached;
public IEnumerable<string> BuildRegressionFailureMessages()
{
@@ -55,10 +55,10 @@ internal sealed class BenchmarkScenarioReport
yield return $"{Result.Id} throughput regressed: min {Result.TotalThroughputStatistics.MinPerSecond:N0} obs/s vs baseline {Baseline.MinThroughputPerSecond:N0} obs/s (-{delta:F1}%).";
}
if (MongoThroughputRegressionBreached && MongoThroughputRegressionRatio is { } mongoRatio)
if (InsertThroughputRegressionBreached && InsertThroughputRegressionRatio is { } insertRatio)
{
var delta = (mongoRatio - 1d) * 100d;
yield return $"{Result.Id} Mongo throughput regressed: min {Result.InsertThroughputStatistics.MinPerSecond:N0} ops/s vs baseline {Baseline.MinMongoThroughputPerSecond:N0} ops/s (-{delta:F1}%).";
var delta = (insertRatio - 1d) * 100d;
yield return $"{Result.Id} insert throughput regressed: min {Result.InsertThroughputStatistics.MinPerSecond:N0} ops/s vs baseline {Baseline.MinInsertThroughputPerSecond:N0} ops/s (-{delta:F1}%).";
}
}

View File

@@ -22,12 +22,12 @@ internal static class PrometheusWriter
builder.AppendLine("# TYPE linknotmerge_bench_total_ms gauge");
builder.AppendLine("# HELP linknotmerge_bench_correlation_ms Link-Not-Merge benchmark correlation duration metrics (milliseconds).");
builder.AppendLine("# TYPE linknotmerge_bench_correlation_ms gauge");
builder.AppendLine("# HELP linknotmerge_bench_insert_ms Link-Not-Merge benchmark Mongo insert duration metrics (milliseconds).");
builder.AppendLine("# HELP linknotmerge_bench_insert_ms Link-Not-Merge benchmark insert duration metrics (milliseconds).");
builder.AppendLine("# TYPE linknotmerge_bench_insert_ms gauge");
builder.AppendLine("# HELP linknotmerge_bench_throughput_per_sec Link-Not-Merge benchmark throughput metrics (observations per second).");
builder.AppendLine("# TYPE linknotmerge_bench_throughput_per_sec gauge");
builder.AppendLine("# HELP linknotmerge_bench_mongo_throughput_per_sec Link-Not-Merge benchmark Mongo throughput metrics (operations per second).");
builder.AppendLine("# TYPE linknotmerge_bench_mongo_throughput_per_sec gauge");
builder.AppendLine("# HELP linknotmerge_bench_insert_throughput_per_sec Link-Not-Merge benchmark insert throughput metrics (operations per second).");
builder.AppendLine("# TYPE linknotmerge_bench_insert_throughput_per_sec gauge");
builder.AppendLine("# HELP linknotmerge_bench_allocated_mb Link-Not-Merge benchmark allocation metrics (megabytes).");
builder.AppendLine("# TYPE linknotmerge_bench_allocated_mb gauge");
@@ -46,9 +46,9 @@ internal static class PrometheusWriter
AppendMetric(builder, "linknotmerge_bench_min_throughput_per_sec", scenario, report.Result.TotalThroughputStatistics.MinPerSecond);
AppendMetric(builder, "linknotmerge_bench_throughput_floor_per_sec", scenario, report.Result.MinThroughputThresholdPerSecond);
AppendMetric(builder, "linknotmerge_bench_mean_mongo_throughput_per_sec", scenario, report.Result.InsertThroughputStatistics.MeanPerSecond);
AppendMetric(builder, "linknotmerge_bench_min_mongo_throughput_per_sec", scenario, report.Result.InsertThroughputStatistics.MinPerSecond);
AppendMetric(builder, "linknotmerge_bench_mongo_throughput_floor_per_sec", scenario, report.Result.MinMongoThroughputThresholdPerSecond);
AppendMetric(builder, "linknotmerge_bench_mean_insert_throughput_per_sec", scenario, report.Result.InsertThroughputStatistics.MeanPerSecond);
AppendMetric(builder, "linknotmerge_bench_min_insert_throughput_per_sec", scenario, report.Result.InsertThroughputStatistics.MinPerSecond);
AppendMetric(builder, "linknotmerge_bench_insert_throughput_floor_per_sec", scenario, report.Result.MinInsertThroughputThresholdPerSecond);
AppendMetric(builder, "linknotmerge_bench_max_allocated_mb", scenario, report.Result.AllocationStatistics.MaxAllocatedMb);
AppendMetric(builder, "linknotmerge_bench_max_allocated_threshold_mb", scenario, report.Result.MaxAllocatedThresholdMb);
@@ -57,7 +57,7 @@ internal static class PrometheusWriter
{
AppendMetric(builder, "linknotmerge_bench_baseline_max_total_ms", scenario, baseline.MaxTotalMs);
AppendMetric(builder, "linknotmerge_bench_baseline_min_throughput_per_sec", scenario, baseline.MinThroughputPerSecond);
AppendMetric(builder, "linknotmerge_bench_baseline_min_mongo_throughput_per_sec", scenario, baseline.MinMongoThroughputPerSecond);
AppendMetric(builder, "linknotmerge_bench_baseline_min_insert_throughput_per_sec", scenario, baseline.MinInsertThroughputPerSecond);
}
if (report.DurationRegressionRatio is { } durationRatio)
@@ -70,9 +70,9 @@ internal static class PrometheusWriter
AppendMetric(builder, "linknotmerge_bench_throughput_regression_ratio", scenario, throughputRatio);
}
if (report.MongoThroughputRegressionRatio is { } mongoRatio)
if (report.InsertThroughputRegressionRatio is { } insertRatio)
{
AppendMetric(builder, "linknotmerge_bench_mongo_throughput_regression_ratio", scenario, mongoRatio);
AppendMetric(builder, "linknotmerge_bench_insert_throughput_regression_ratio", scenario, insertRatio);
}
AppendMetric(builder, "linknotmerge_bench_regression_limit", scenario, report.RegressionLimit);

View File

@@ -17,7 +17,7 @@ internal sealed record ScenarioResult(
AllocationStatistics AllocationStatistics,
double? ThresholdMs,
double? MinThroughputThresholdPerSecond,
double? MinMongoThroughputThresholdPerSecond,
double? MinInsertThroughputThresholdPerSecond,
double? MaxAllocatedThresholdMb)
{
public string IdColumn => Id.Length <= 28 ? Id.PadRight(28) : Id[..28];
@@ -36,7 +36,7 @@ internal sealed record ScenarioResult(
public string ThroughputColumn => (TotalThroughputStatistics.MinPerSecond / 1_000d).ToString("F2", CultureInfo.InvariantCulture).PadLeft(11);
public string MongoThroughputColumn => (InsertThroughputStatistics.MinPerSecond / 1_000d).ToString("F2", CultureInfo.InvariantCulture).PadLeft(11);
public string InsertThroughputColumn => (InsertThroughputStatistics.MinPerSecond / 1_000d).ToString("F2", CultureInfo.InvariantCulture).PadLeft(11);
public string AllocatedColumn => AllocationStatistics.MaxAllocatedMb.ToString("F2", CultureInfo.InvariantCulture).PadLeft(9);
}

View File

@@ -1,4 +1,4 @@
scenario,iterations,observations,aliases,linksets,mean_total_ms,p95_total_ms,max_total_ms,mean_insert_ms,mean_correlation_ms,mean_throughput_per_sec,min_throughput_per_sec,mean_mongo_throughput_per_sec,min_mongo_throughput_per_sec,max_allocated_mb
scenario,iterations,observations,aliases,linksets,mean_total_ms,p95_total_ms,max_total_ms,mean_insert_ms,mean_correlation_ms,mean_throughput_per_sec,min_throughput_per_sec,mean_insert_throughput_per_sec,min_insert_throughput_per_sec,max_allocated_mb
lnm_ingest_baseline,5,5000,500,6000,555.1984,823.4957,866.6236,366.2635,188.9349,9877.7916,5769.5175,15338.0851,8405.1257,62.4477
lnm_ingest_fanout_medium,5,10000,800,14800,785.8909,841.6247,842.8815,453.5087,332.3822,12794.9550,11864.0639,22086.0320,20891.0579,145.8328
lnm_ingest_fanout_high,5,15000,1200,17400,1299.3458,1367.0934,1369.9430,741.6265,557.7193,11571.0991,10949.3607,20232.5180,19781.6762,238.3450
1 scenario iterations observations aliases linksets mean_total_ms p95_total_ms max_total_ms mean_insert_ms mean_correlation_ms mean_throughput_per_sec min_throughput_per_sec mean_mongo_throughput_per_sec mean_insert_throughput_per_sec min_mongo_throughput_per_sec min_insert_throughput_per_sec max_allocated_mb
2 lnm_ingest_baseline 5 5000 500 6000 555.1984 823.4957 866.6236 366.2635 188.9349 9877.7916 5769.5175 15338.0851 15338.0851 8405.1257 8405.1257 62.4477
3 lnm_ingest_fanout_medium 5 10000 800 14800 785.8909 841.6247 842.8815 453.5087 332.3822 12794.9550 11864.0639 22086.0320 22086.0320 20891.0579 20891.0579 145.8328
4 lnm_ingest_fanout_high 5 15000 1200 17400 1299.3458 1367.0934 1369.9430 741.6265 557.7193 11571.0991 10949.3607 20232.5180 20232.5180 19781.6762 19781.6762 238.3450

View File

@@ -1,7 +1,7 @@
{
"thresholdMs": 2000,
"minThroughputPerSecond": 7000,
"minMongoThroughputPerSecond": 12000,
"minInsertThroughputPerSecond": 12000,
"maxAllocatedMb": 600,
"iterations": 5,
"scenarios": [
@@ -18,7 +18,7 @@
"seed": 42022,
"thresholdMs": 900,
"minThroughputPerSecond": 5500,
"minMongoThroughputPerSecond": 8000,
"minInsertThroughputPerSecond": 8000,
"maxAllocatedMb": 160
},
{
@@ -34,7 +34,7 @@
"seed": 52022,
"thresholdMs": 1300,
"minThroughputPerSecond": 8000,
"minMongoThroughputPerSecond": 13000,
"minInsertThroughputPerSecond": 13000,
"maxAllocatedMb": 220
},
{
@@ -50,7 +50,7 @@
"seed": 62022,
"thresholdMs": 2200,
"minThroughputPerSecond": 7000,
"minMongoThroughputPerSecond": 13000,
"minInsertThroughputPerSecond": 13000,
"maxAllocatedMb": 300
}
]

View File

@@ -1,6 +1,6 @@
<Project>
<PropertyGroup>
<!-- Keep Concelier test harness active while trimming Mongo dependencies. Allow opt-out per project. -->
<!-- Keep Concelier test harness active while trimming legacy dependencies. Allow opt-out per project. -->
<UseConcelierTestInfra Condition="'$(UseConcelierTestInfra)'==''">true</UseConcelierTestInfra>
<!-- Suppress noisy warnings from duplicate usings and analyzer fixture hints during Concelier test harness runs. -->
<NoWarn>$(NoWarn);CS0105;CS1591;CS8601;CS8602;CS8604;CS0618;RS1032;RS2007;xUnit1041;xUnit1031;xUnit2013;NU1510;NETSDK1023;SYSLIB0057</NoWarn>

View File

@@ -6,7 +6,7 @@ using StellaOps.Concelier.Storage.Postgres.Advisories;
namespace StellaOps.Concelier.WebService.DualWrite;
/// <summary>
/// Postgres-backed advisory store that implements the legacy Mongo contracts.
/// Postgres-backed advisory store that implements the legacy storage contracts.
/// </summary>
public sealed class DualWriteAdvisoryStore : IAdvisoryStore
{

View File

@@ -7,8 +7,8 @@ namespace StellaOps.Concelier.WebService.Options;
public sealed class ConcelierOptions
{
[Obsolete("Mongo storage has been removed; use PostgresStorage.")]
public StorageOptions Storage { get; set; } = new();
[Obsolete("Legacy storage has been removed; use PostgresStorage.")]
public LegacyStorageOptions LegacyStorage { get; set; } = new();
public PostgresStorageOptions? PostgresStorage { get; set; } = new PostgresStorageOptions
{
@@ -37,10 +37,10 @@ public sealed class ConcelierOptions
/// </summary>
public AirGapOptions AirGap { get; set; } = new();
[Obsolete("Mongo storage has been removed; use PostgresStorage.")]
public sealed class StorageOptions
[Obsolete("Legacy storage has been removed; use PostgresStorage.")]
public sealed class LegacyStorageOptions
{
public string Driver { get; set; } = "mongo";
public string Driver { get; set; } = "postgres";
public string Dsn { get; set; } = string.Empty;
@@ -56,7 +56,6 @@ public sealed class ConcelierOptions
{
/// <summary>
/// Enable PostgreSQL storage for LNM linkset cache.
/// When true, the linkset cache is stored in PostgreSQL instead of MongoDB.
/// </summary>
public bool Enabled { get; set; }

View File

@@ -226,7 +226,7 @@ builder.Services.AddOptions<AdvisoryObservationEventPublisherOptions>()
{
options.Subject ??= "concelier.advisory.observation.updated.v1";
options.Stream ??= "CONCELIER_OBS";
options.Transport = string.IsNullOrWhiteSpace(options.Transport) ? "mongo" : options.Transport;
options.Transport = string.IsNullOrWhiteSpace(options.Transport) ? "inmemory" : options.Transport;
})
.ValidateOnStart();
builder.Services.AddConcelierAocGuards();

View File

@@ -673,7 +673,7 @@ public sealed class AcscConnector : IFeedConnector
private async Task<AcscCursor> GetCursorCoreAsync(CancellationToken cancellationToken)
{
var state = await _stateRepository.TryGetAsync(SourceName, cancellationToken).ConfigureAwait(false);
return state is null ? AcscCursor.Empty : AcscCursor.FromBson(state.Cursor);
return state is null ? AcscCursor.Empty : AcscCursor.FromDocument(state.Cursor);
}
private Task UpdateCursorAsync(AcscCursor cursor, CancellationToken cancellationToken)

View File

@@ -70,7 +70,7 @@ internal sealed record AcscCursor(
return document;
}
public static AcscCursor FromBson(DocumentObject? document)
public static AcscCursor FromDocument(DocumentObject? document)
{
if (document is null || document.ElementCount == 0)
{

View File

@@ -332,8 +332,8 @@ public sealed class CccsConnector : IFeedConnector
}
var dtoJson = JsonSerializer.Serialize(dto, DtoSerializerOptions);
var dtoBson = DocumentObject.Parse(dtoJson);
var dtoRecord = new DtoRecord(Guid.NewGuid(), document.Id, SourceName, DtoSchemaVersion, dtoBson, now);
var dtoDoc = DocumentObject.Parse(dtoJson);
var dtoRecord = new DtoRecord(Guid.NewGuid(), document.Id, SourceName, DtoSchemaVersion, dtoDoc, now);
await _dtoStore.UpsertAsync(dtoRecord, cancellationToken).ConfigureAwait(false);
await _documentStore.UpdateStatusAsync(document.Id, DocumentStatuses.PendingMap, cancellationToken).ConfigureAwait(false);
@@ -459,7 +459,7 @@ public sealed class CccsConnector : IFeedConnector
private async Task<CccsCursor> GetCursorAsync(CancellationToken cancellationToken)
{
var state = await _stateRepository.TryGetAsync(SourceName, cancellationToken).ConfigureAwait(false);
return state is null ? CccsCursor.Empty : CccsCursor.FromBson(state.Cursor);
return state is null ? CccsCursor.Empty : CccsCursor.FromDocument(state.Cursor);
}
private Task UpdateCursorAsync(CccsCursor cursor, CancellationToken cancellationToken)

View File

@@ -70,7 +70,7 @@ internal sealed record CccsCursor(
return doc;
}
public static CccsCursor FromBson(DocumentObject? document)
public static CccsCursor FromDocument(DocumentObject? document)
{
if (document is null || document.ElementCount == 0)
{

View File

@@ -286,8 +286,8 @@ public sealed class CertBundConnector : IFeedConnector
_diagnostics.ParseSuccess(dto.Products.Count, dto.CveIds.Count);
parsedCount++;
var bson = DocumentObject.Parse(JsonSerializer.Serialize(dto, SerializerOptions));
var dtoRecord = new DtoRecord(Guid.NewGuid(), document.Id, SourceName, "cert-bund.detail.v1", bson, now);
var doc = DocumentObject.Parse(JsonSerializer.Serialize(dto, SerializerOptions));
var dtoRecord = new DtoRecord(Guid.NewGuid(), document.Id, SourceName, "cert-bund.detail.v1", doc, now);
await _dtoStore.UpsertAsync(dtoRecord, cancellationToken).ConfigureAwait(false);
await _documentStore.UpdateStatusAsync(document.Id, DocumentStatuses.PendingMap, cancellationToken).ConfigureAwait(false);
@@ -423,7 +423,7 @@ public sealed class CertBundConnector : IFeedConnector
private async Task<CertBundCursor> GetCursorAsync(CancellationToken cancellationToken)
{
var state = await _stateRepository.TryGetAsync(SourceName, cancellationToken).ConfigureAwait(false);
return state is null ? CertBundCursor.Empty : CertBundCursor.FromBson(state.Cursor);
return state is null ? CertBundCursor.Empty : CertBundCursor.FromDocument(state.Cursor);
}
private Task UpdateCursorAsync(CertBundCursor cursor, CancellationToken cancellationToken)

View File

@@ -53,7 +53,7 @@ internal sealed record CertBundCursor(
return document;
}
public static CertBundCursor FromBson(DocumentObject? document)
public static CertBundCursor FromDocument(DocumentObject? document)
{
if (document is null || document.ElementCount == 0)
{

View File

@@ -672,7 +672,7 @@ public sealed class CertCcConnector : IFeedConnector
private async Task<CertCcCursor> GetCursorAsync(CancellationToken cancellationToken)
{
var record = await _stateRepository.TryGetAsync(SourceName, cancellationToken).ConfigureAwait(false);
return CertCcCursor.FromBson(record?.Cursor);
return CertCcCursor.FromDocument(record?.Cursor);
}
private async Task UpdateCursorAsync(CertCcCursor cursor, CancellationToken cancellationToken)

View File

@@ -43,7 +43,7 @@ internal sealed record CertCcCursor(
return document;
}
public static CertCcCursor FromBson(DocumentObject? document)
public static CertCcCursor FromDocument(DocumentObject? document)
{
if (document is null || document.ElementCount == 0)
{
@@ -124,10 +124,10 @@ internal sealed record CertCcCursor(
{
switch (element)
{
case DocumentString bsonString when !string.IsNullOrWhiteSpace(bsonString.AsString):
results.Add(bsonString.AsString.Trim());
case DocumentString docString when !string.IsNullOrWhiteSpace(docString.AsString):
results.Add(docString.AsString.Trim());
break;
case DocumentObject bsonDocument when bsonDocument.TryGetValue("value", out var inner) && inner.IsString:
case DocumentObject docObject when docObject.TryGetValue("value", out var inner) && inner.IsString:
results.Add(inner.AsString.Trim());
break;
}
@@ -144,7 +144,7 @@ internal sealed record CertCcCursor(
private static bool TryReadGuid(DocumentValue value, out Guid guid)
{
if (value is DocumentString bsonString && Guid.TryParse(bsonString.AsString, out guid))
if (value is DocumentString docString && Guid.TryParse(docString.AsString, out guid))
{
return true;
}

View File

@@ -326,7 +326,7 @@ public sealed class CertFrConnector : IFeedConnector
private async Task<CertFrCursor> GetCursorAsync(CancellationToken cancellationToken)
{
var record = await _stateRepository.TryGetAsync(SourceName, cancellationToken).ConfigureAwait(false);
return CertFrCursor.FromBson(record?.Cursor);
return CertFrCursor.FromDocument(record?.Cursor);
}
private async Task UpdateCursorAsync(CertFrCursor cursor, CancellationToken cancellationToken)

View File

@@ -28,7 +28,7 @@ internal sealed record CertFrCursor(
return document;
}
public static CertFrCursor FromBson(DocumentObject? document)
public static CertFrCursor FromDocument(DocumentObject? document)
{
if (document is null || document.ElementCount == 0)
{

View File

@@ -418,7 +418,7 @@ public sealed class CertInConnector : IFeedConnector
private async Task<CertInCursor> GetCursorAsync(CancellationToken cancellationToken)
{
var state = await _stateRepository.TryGetAsync(SourceName, cancellationToken).ConfigureAwait(false);
return state is null ? CertInCursor.Empty : CertInCursor.FromBson(state.Cursor);
return state is null ? CertInCursor.Empty : CertInCursor.FromDocument(state.Cursor);
}
private Task UpdateCursorAsync(CertInCursor cursor, CancellationToken cancellationToken)

View File

@@ -28,7 +28,7 @@ internal sealed record CertInCursor(
return document;
}
public static CertInCursor FromBson(DocumentObject? document)
public static CertInCursor FromDocument(DocumentObject? document)
{
if (document is null || document.ElementCount == 0)
{

View File

@@ -10,7 +10,7 @@ using System.Text;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using StellaOps.Concelier.Documents;
using MongoContracts = StellaOps.Concelier.Storage;
using LegacyContracts = StellaOps.Concelier.Storage;
using StorageContracts = StellaOps.Concelier.Storage.Contracts;
using StellaOps.Concelier.Connector.Common.Http;
using StellaOps.Concelier.Connector.Common.Telemetry;
@@ -32,12 +32,12 @@ public sealed class SourceFetchService
private readonly IHttpClientFactory _httpClientFactory;
private readonly RawDocumentStorage _rawDocumentStorage;
private readonly MongoContracts.IDocumentStore _documentStore;
private readonly LegacyContracts.IDocumentStore _documentStore;
private readonly StorageContracts.IStorageDocumentStore _storageDocumentStore;
private readonly ILogger<SourceFetchService> _logger;
private readonly TimeProvider _timeProvider;
private readonly IOptionsMonitor<SourceHttpClientOptions> _httpClientOptions;
private readonly IOptions<MongoContracts.StorageOptions> _storageOptions;
private readonly IOptions<LegacyContracts.StorageOptions> _storageOptions;
private readonly IJitterSource _jitterSource;
private readonly IAdvisoryRawWriteGuard _guard;
private readonly IAdvisoryLinksetMapper _linksetMapper;
@@ -47,7 +47,7 @@ public sealed class SourceFetchService
public SourceFetchService(
IHttpClientFactory httpClientFactory,
RawDocumentStorage rawDocumentStorage,
MongoContracts.IDocumentStore documentStore,
LegacyContracts.IDocumentStore documentStore,
StorageContracts.IStorageDocumentStore storageDocumentStore,
ILogger<SourceFetchService> logger,
IJitterSource jitterSource,
@@ -56,7 +56,7 @@ public sealed class SourceFetchService
ICryptoHash hash,
TimeProvider? timeProvider = null,
IOptionsMonitor<SourceHttpClientOptions>? httpClientOptions = null,
IOptions<MongoContracts.StorageOptions>? storageOptions = null)
IOptions<LegacyContracts.StorageOptions>? storageOptions = null)
{
_httpClientFactory = httpClientFactory ?? throw new ArgumentNullException(nameof(httpClientFactory));
_rawDocumentStorage = rawDocumentStorage ?? throw new ArgumentNullException(nameof(rawDocumentStorage));
@@ -77,7 +77,7 @@ public sealed class SourceFetchService
public SourceFetchService(
IHttpClientFactory httpClientFactory,
RawDocumentStorage rawDocumentStorage,
MongoContracts.IDocumentStore documentStore,
LegacyContracts.IDocumentStore documentStore,
ILogger<SourceFetchService> logger,
IJitterSource jitterSource,
IAdvisoryRawWriteGuard guard,
@@ -85,7 +85,7 @@ public sealed class SourceFetchService
ICryptoHash hash,
TimeProvider? timeProvider = null,
IOptionsMonitor<SourceHttpClientOptions>? httpClientOptions = null,
IOptions<MongoContracts.StorageOptions>? storageOptions = null)
IOptions<LegacyContracts.StorageOptions>? storageOptions = null)
: this(
httpClientFactory,
rawDocumentStorage,

View File

@@ -2,7 +2,7 @@ using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using StellaOps.Concelier.Documents;
using StellaOps.Concelier.Connector.Common.Fetch;
using MongoContracts = StellaOps.Concelier.Storage;
using LegacyContracts = StellaOps.Concelier.Storage;
using StellaOps.Cryptography;
namespace StellaOps.Concelier.Connector.Common.State;
@@ -12,17 +12,17 @@ namespace StellaOps.Concelier.Connector.Common.State;
/// </summary>
public sealed class SourceStateSeedProcessor
{
private readonly MongoContracts.IDocumentStore _documentStore;
private readonly LegacyContracts.IDocumentStore _documentStore;
private readonly RawDocumentStorage _rawDocumentStorage;
private readonly MongoContracts.ISourceStateRepository _stateRepository;
private readonly LegacyContracts.ISourceStateRepository _stateRepository;
private readonly TimeProvider _timeProvider;
private readonly ILogger<SourceStateSeedProcessor> _logger;
private readonly ICryptoHash _hash;
public SourceStateSeedProcessor(
MongoContracts.IDocumentStore documentStore,
LegacyContracts.IDocumentStore documentStore,
RawDocumentStorage rawDocumentStorage,
MongoContracts.ISourceStateRepository stateRepository,
LegacyContracts.ISourceStateRepository stateRepository,
ICryptoHash hash,
TimeProvider? timeProvider = null,
ILogger<SourceStateSeedProcessor>? logger = null)
@@ -173,7 +173,7 @@ public sealed class SourceStateSeedProcessor
var metadata = CloneDictionary(document.Metadata);
var record = new MongoContracts.DocumentRecord(
var record = new LegacyContracts.DocumentRecord(
recordId,
source,
document.Uri,

View File

@@ -571,7 +571,7 @@ public sealed class CveConnector : IFeedConnector
private async Task<CveCursor> GetCursorAsync(CancellationToken cancellationToken)
{
var state = await _stateRepository.TryGetAsync(SourceName, cancellationToken).ConfigureAwait(false);
return state is null ? CveCursor.Empty : CveCursor.FromBson(state.Cursor);
return state is null ? CveCursor.Empty : CveCursor.FromDocument(state.Cursor);
}
private async Task UpdateCursorAsync(CveCursor cursor, CancellationToken cancellationToken)

View File

@@ -49,7 +49,7 @@ internal sealed record CveCursor(
return document;
}
public static CveCursor FromBson(DocumentObject? document)
public static CveCursor FromDocument(DocumentObject? document)
{
if (document is null || document.ElementCount == 0)
{

View File

@@ -368,7 +368,7 @@ public sealed class DebianConnector : IFeedConnector
continue;
}
var payload = ToBson(dto);
var payload = ToDocument(dto);
var dtoRecord = new DtoRecord(Guid.NewGuid(), document.Id, SourceName, SchemaVersion, payload, _timeProvider.GetUtcNow());
await _dtoStore.UpsertAsync(dtoRecord, cancellationToken).ConfigureAwait(false);
await _documentStore.UpdateStatusAsync(document.Id, DocumentStatuses.PendingMap, cancellationToken).ConfigureAwait(false);
@@ -414,7 +414,7 @@ public sealed class DebianConnector : IFeedConnector
DebianAdvisoryDto dto;
try
{
dto = FromBson(dtoRecord.Payload);
dto = FromDocument(dtoRecord.Payload);
}
catch (Exception ex)
{
@@ -438,7 +438,7 @@ public sealed class DebianConnector : IFeedConnector
private async Task<DebianCursor> GetCursorAsync(CancellationToken cancellationToken)
{
var state = await _stateRepository.TryGetAsync(SourceName, cancellationToken).ConfigureAwait(false);
return state is null ? DebianCursor.Empty : DebianCursor.FromBson(state.Cursor);
return state is null ? DebianCursor.Empty : DebianCursor.FromDocument(state.Cursor);
}
private async Task UpdateCursorAsync(DebianCursor cursor, CancellationToken cancellationToken)
@@ -508,7 +508,7 @@ public sealed class DebianConnector : IFeedConnector
cveList);
}
private static DocumentObject ToBson(DebianAdvisoryDto dto)
private static DocumentObject ToDocument(DebianAdvisoryDto dto)
{
var packages = new DocumentArray();
foreach (var package in dto.Packages)
@@ -575,15 +575,15 @@ public sealed class DebianConnector : IFeedConnector
};
}
private static DebianAdvisoryDto FromBson(DocumentObject document)
private static DebianAdvisoryDto FromDocument(DocumentObject document)
{
var advisoryId = document.GetValue("advisoryId", "").AsString;
var sourcePackage = document.GetValue("sourcePackage", advisoryId).AsString;
var title = document.GetValue("title", advisoryId).AsString;
var description = document.TryGetValue("description", out var desc) ? desc.AsString : null;
var cves = document.TryGetValue("cves", out var cveArray) && cveArray is DocumentArray cvesBson
? cvesBson.OfType<DocumentValue>()
var cves = document.TryGetValue("cves", out var cveArray) && cveArray is DocumentArray cvesArr
? cvesArr.OfType<DocumentValue>()
.Select(static value => value.ToString())
.Where(static s => !string.IsNullOrWhiteSpace(s))
.Select(static s => s!)
@@ -591,9 +591,9 @@ public sealed class DebianConnector : IFeedConnector
: Array.Empty<string>();
var packages = new List<DebianPackageStateDto>();
if (document.TryGetValue("packages", out var packageArray) && packageArray is DocumentArray packagesBson)
if (document.TryGetValue("packages", out var packageArray) && packageArray is DocumentArray packagesArr)
{
foreach (var element in packagesBson.OfType<DocumentObject>())
foreach (var element in packagesArr.OfType<DocumentObject>())
{
packages.Add(new DebianPackageStateDto(
element.GetValue("package", sourcePackage).AsString,
@@ -614,9 +614,9 @@ public sealed class DebianConnector : IFeedConnector
}
var references = new List<DebianReferenceDto>();
if (document.TryGetValue("references", out var referenceArray) && referenceArray is DocumentArray refBson)
if (document.TryGetValue("references", out var referenceArray) && referenceArray is DocumentArray refArr)
{
foreach (var element in refBson.OfType<DocumentObject>())
foreach (var element in refArr.OfType<DocumentObject>())
{
references.Add(new DebianReferenceDto(
element.GetValue("url", "").AsString,

View File

@@ -19,7 +19,7 @@ internal sealed record DebianCursor(
public static DebianCursor Empty { get; } = new(null, EmptyIds, EmptyGuidList, EmptyGuidList, EmptyCache);
public static DebianCursor FromBson(DocumentObject? document)
public static DebianCursor FromDocument(DocumentObject? document)
{
if (document is null || document.ElementCount == 0)
{
@@ -168,7 +168,7 @@ internal sealed record DebianCursor(
{
if (element.Value is DocumentObject entry)
{
cache[element.Name] = DebianFetchCacheEntry.FromBson(entry);
cache[element.Name] = DebianFetchCacheEntry.FromDocument(entry);
}
}

Some files were not shown because too many files have changed in this diff Show More