up
This commit is contained in:
3
.gitattributes
vendored
3
.gitattributes
vendored
@@ -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
|
||||
|
||||
87
datasets/reachability/README.md
Normal file
87
datasets/reachability/README.md
Normal 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)
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
27
datasets/reachability/samples/csharp/dead-code/manifest.json
Normal file
27
datasets/reachability/samples/csharp/dead-code/manifest.json
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
189
datasets/reachability/schema/ground-truth.schema.json
Normal file
189
datasets/reachability/schema/ground-truth.schema.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
94
datasets/reachability/schema/manifest.schema.json
Normal file
94
datasets/reachability/schema/manifest.schema.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
```
|
||||
|
||||
@@ -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`)
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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 VT1–VT10: 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 TTE1–TTE10: 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 VT1–VT10: 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 TTE1–TTE10: 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*
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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` (0–1), `reachability.state` (`reachable|unreachable|unknown`), `reachability.score` (0–1), `entropy_penalty` (0–0.3), `uncertainty.level` (`U1`–`U3`), `runtime_hits` (bool). |
|
||||
| `signals` | Normalised signal dictionary: `trust_score` (0–1), `reachability.state` (`reachable|unreachable|unknown|under_investigation`), `reachability.score` (0–1), `reachability.confidence` (0–1), `reachability.evidence_ref` (string), `entropy_penalty` (0–0.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. |
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
337
docs/reachability/ground-truth-schema.md
Normal file
337
docs/reachability/ground-truth-schema.md
Normal 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 |
|
||||
@@ -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 (0–9)
|
||||
< POSSIBLE (10–29)
|
||||
< STATIC_PATH (30–59)
|
||||
< DYNAMIC_SEEN (60–79)
|
||||
< DYNAMIC_USER_TAINTED (80–99)
|
||||
< 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 0–100.
|
||||
### 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 | 0–9 |
|
||||
| POSSIBLE | 10–29 |
|
||||
| STATIC_PATH | 30–59 |
|
||||
| DYNAMIC_SEEN | 60–79 |
|
||||
| DYNAMIC_USER_TAINTED | 80–99 |
|
||||
| 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.
|
||||
|
||||
269
docs/reachability/policy-gate.md
Normal file
269
docs/reachability/policy-gate.md
Normal 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 |
|
||||
174
docs/schemas/tte-event.schema.json
Normal file
174
docs/schemas/tte-event.schema.json
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,28 +1,73 @@
|
||||
# Uncertainty States & Entropy Scoring
|
||||
|
||||
> **Status:** Draft – aligns with the November 2025 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.
|
||||
|
||||
Stella Ops 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. |
|
||||
| Code | Name | Meaning |
|
||||
|------|------|---------|
|
||||
| `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` (0–1) 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` (0–1) 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` (0–1) 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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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" "$@"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -46,7 +46,7 @@ paths:
|
||||
value:
|
||||
status: degraded
|
||||
service: policy
|
||||
reason: mongo unavailable
|
||||
reason: database unavailable
|
||||
timestamp: '2025-11-18T00:00:00Z'
|
||||
/healthz:
|
||||
get:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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; }
|
||||
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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!);
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace StellaOps.Authority.Storage.Mongo.Documents;
|
||||
namespace StellaOps.Authority.Storage.Documents;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a bootstrap invite document.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace StellaOps.Authority.Storage.Mongo.Documents;
|
||||
namespace StellaOps.Authority.Storage.Documents;
|
||||
|
||||
/// <summary>
|
||||
/// Result status for token usage recording.
|
||||
|
||||
@@ -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>();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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";
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -74,7 +74,7 @@ public class AuthorityRateLimiterTests
|
||||
SchemaVersion = 1
|
||||
};
|
||||
|
||||
options.Storage.ConnectionString = "mongodb://localhost/authority";
|
||||
options.Storage.ConnectionString = "Host=localhost;Database=authority";
|
||||
return options;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 =
|
||||
{
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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>();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -13,6 +13,6 @@ internal sealed record BaselineEntry(
|
||||
double MeanCorrelationMs,
|
||||
double MeanThroughputPerSecond,
|
||||
double MinThroughputPerSecond,
|
||||
double MeanMongoThroughputPerSecond,
|
||||
double MinMongoThroughputPerSecond,
|
||||
double MeanInsertThroughputPerSecond,
|
||||
double MinInsertThroughputPerSecond,
|
||||
double MaxAllocatedMb);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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}%).";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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,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
|
||||
}
|
||||
]
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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; }
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user