feat: Implement Policy Engine Evaluation Service and Cache with unit tests
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled

Temp commit to debug
This commit is contained in:
master
2025-11-05 07:35:53 +00:00
parent 40e7f827da
commit 9253620833
125 changed files with 18735 additions and 17215 deletions

View File

@@ -328,11 +328,34 @@ Accept: application/json
"status": "Pending",
"image": {
"reference": "registry.example.com/acme/app:1.2.3",
"digest": null
"digest": "sha256:cafecafecafecafecafecafecafecafecafecafecafecafecafecafecafecafe"
},
"createdAt": "2025-10-18T20:15:12.482Z",
"updatedAt": "2025-10-18T20:15:12.482Z",
"failureReason": null
"failureReason": null,
"surface": {
"tenant": "default",
"generatedAt": "2025-10-18T20:15:12.482Z",
"manifestDigest": "sha256:8b4ddf1a9d3565eb7c2b176a0a64a970795e5ec373dbea3aaebb4208f9759b44",
"manifestUri": "cas://scanner-artifacts/scanner/surface/manifests/default/sha256/8b/4d/8b4ddf1a9d3565eb7c2b176a0a64a970795e5ec373dbea3aaebb4208f9759b44.json",
"manifest": {
"schema": "stellaops.surface.manifest@1",
"tenant": "default",
"imageDigest": "sha256:cafecafecafecafecafecafecafecafecafecafecafecafecafecafecafecafe",
"generatedAt": "2025-10-18T20:15:12.482Z",
"artifacts": [
{
"kind": "sbom-inventory",
"uri": "cas://scanner-artifacts/scanner/images/cafecafecafecafecafecafecafecafecafecafecafecafecafecafecafecafe/sbom.cdx.json",
"digest": "sha256:deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef",
"mediaType": "application/vnd.cyclonedx+json; version=1.6; view=inventory",
"format": "cdx-json",
"sizeBytes": 2048,
"view": "inventory"
}
]
}
}
}
```
@@ -445,7 +468,39 @@ Request body mirrors policy preview inputs (image digest plus findings). The ser
"reachability": "runtime"
}
],
"issues": []
"issues": [],
"surface": {
"tenant": "default",
"generatedAt": "2025-10-23T15:32:22Z",
"manifestDigest": "sha256:1f3c5d7a8e4b921a0c2f6b8de0bb5aa8f5aa51b62e7d7d1864f9c826bfb44d91",
"manifestUri": "cas://scanner-artifacts/scanner/surface/manifests/default/sha256/1f/3c/1f3c5d7a8e4b921a0c2f6b8de0bb5aa8f5aa51b62e7d7d1864f9c826bfb44d91.json",
"manifest": {
"schema": "stellaops.surface.manifest@1",
"tenant": "default",
"imageDigest": "sha256:7dbe0c9a5d4f1c8184007e9d94dbe55928f8a2db5ab9c1c2d4a2f7bbcdfe1234",
"generatedAt": "2025-10-23T15:32:22Z",
"artifacts": [
{
"kind": "sbom-inventory",
"uri": "cas://scanner-artifacts/scanner/images/7dbe0c9a5d4f1c8184007e9d94dbe55928f8a2db5ab9c1c2d4a2f7bbcdfe1234/sbom.cdx.json",
"digest": "sha256:2b8ce7dd0037e59f0f93e4a5cff45b1eb305a511a1c9e2895d2f4ecdf616d3da",
"mediaType": "application/vnd.cyclonedx+json; version=1.6; view=inventory",
"format": "cdx-json",
"sizeBytes": 3072,
"view": "inventory"
},
{
"kind": "sbom-usage",
"uri": "cas://scanner-artifacts/scanner/images/7dbe0c9a5d4f1c8184007e9d94dbe55928f8a2db5ab9c1c2d4a2f7bbcdfe1234/sbom.cdx.pb",
"digest": "sha256:74e4d9f8ab0f2a1772e5768e15a5a9d7b662b849b1f223c8d6f3b184e4ac7780",
"mediaType": "application/vnd.cyclonedx+protobuf; version=1.6; view=usage",
"format": "cdx-protobuf",
"sizeBytes": 12800,
"view": "usage"
}
]
}
}
},
"dsse": {
"payloadType": "application/vnd.stellaops.report+json",
@@ -461,7 +516,7 @@ Request body mirrors policy preview inputs (image digest plus findings). The ser
}
```
- The `report` object omits null fields and is deterministic (ISO timestamps, sorted keys) while surfacing `unknownConfidence`, `confidenceBand`, and `unknownAgeDays` for auditability.
- The `report` object omits null fields and is deterministic (ISO timestamps, sorted keys) while surfacing `unknownConfidence`, `confidenceBand`, `unknownAgeDays`, and a `surface` block containing the manifest digest and CAS URIs for downstream tooling.
- `dsse` follows the DSSE (Dead Simple Signing Envelope) shape; `payload` is the canonical UTF-8 JSON and `signatures[0].signature` is the base64 HMAC/Ed25519 value depending on configuration.
- Full offline samples live at `samples/policy/policy-report-unknown.json` (request + response) and `samples/api/reports/report-sample.dsse.json` (envelope fixture) for tooling tests or signature verification.

View File

@@ -326,7 +326,80 @@ Produces sealed bundle for determinism verification; returns location of bundle.
---
## 7·Effective Findings APIs
## 7·Batch Evaluation API
Deterministic evaluator for downstream services (Findings Ledger, replay tooling, offline exporters). Consumers submit ledger event payloads and receive policy verdicts with rationale lists; no state is persisted in Policy Engine.
```
POST /api/policy/eval/batch
Scopes: policy:simulate (service identities only)
Headers: X-Stella-Tenant, Idempotency-Key (optional)
```
**Request**
```jsonc
{
"tenantId": "acme",
"policyVersion": "sha256:1fb2…",
"items": [
{
"findingId": "acme::artifact-1::CVE-2024-12345",
"eventId": "5d1fcc61-6903-42ef-9285-7f4d3d8f7f69",
"event": { ... canonical ledger payload ... },
"currentProjection": {
"status": "triaged",
"severity": 3.4,
"labels": { "exposure": "runtime" },
"explainRef": "policy://explain/123",
"rationale": ["policy://explain/123"]
}
}
]
}
```
| Field | Description |
|-------|-------------|
| `tenantId` | Must match the `X-Stella-Tenant` header. |
| `policyVersion` | Deterministic policy digest (for example `sha256:<hex>`). Required for caching. |
| `event` | Canonical ledger event payload (`ledger_events.event_body`). |
| `currentProjection` | Optional snapshot of the existing finding projection. Null values are ignored. |
**Response 200**
```jsonc
{
"items": [
{
"findingId": "acme::artifact-1::CVE-2024-12345",
"status": "affected",
"severity": 7.5,
"labels": { "exposure": "runtime" },
"explainRef": "policy://explain/123",
"rationale": [
"policy://explain/123",
"policy://remediation/321"
]
}
],
"cost": {
"units": 1,
"budgetRemaining": 999
}
}
```
Notes:
- Items that cannot be evaluated return `status: null` with an `error` object. Callers should fall back to inline evaluation.
- Policy Engine enforces per-tenant cost budgets; batches that exceed the remaining allowance receive `429 Too Many Requests`.
- Responses are deterministic; clients may cache results by `(tenantId, policyVersion, eventHash, projectionHash)` to support replay/offline parity.
- Standard `ERR_POL_*` payloads surface errors; `ERR_POL_006` indicates the evaluator aborted the batch.
---
## 8·Effective Findings APIs
### 7.1 List Findings
@@ -378,7 +451,7 @@ Returns rule hit sequence:
---
## 8·Events & Webhooks
## 9·Events & Webhooks
- `policy.run.completed` emitted with `runId`, `policyId`, `mode`, `stats`, `determinismHash`.
- `policy.run.failed` includes error code, retry count, guidance.
@@ -387,7 +460,7 @@ Returns rule hit sequence:
---
## 9·Compliance Checklist
## 10·Compliance Checklist
- [ ] **Scopes enforced:** Endpoint access requires correct Authority scope mapping (see `/src/Authority/StellaOps.Authority/TASKS.md`).
- [ ] **Schemas current:** JSON examples align with Scheduler Models (`SCHED-MODELS-20-001`) and Policy Engine DTOs; update when contracts change.

View File

@@ -12,6 +12,24 @@ This guide captures the minimum wiring required for connectors and Merge coordin
4. Verify with `dotnet test` that the connector snapshot fixtures now include the `normalizedVersions` array and update fixtures by setting the connector-specific `UPDATE_*_FIXTURES=1` environment variable.
5. Tail Merge logs (or the test output) for the new warning `Normalized version rules missing for {AdvisoryKey}`; an empty warning stream means the connector/merge artefacts are ready to close FEEDMERGE-COORD-02-901/902.
### 1.1 Rollout status (2025-11-04)
| Connector | Status | Notes / next steps |
|-----------|--------|--------------------|
| `vendor.acsc` | ⚠️ Pending | Upstream feed only supplies vendor/product strings. Waiting on ASD/ACSC feed update to expose explicit ranges. Track via FEEDCONN-ACSC-02-010. |
| `vendor.cccs` | ⚠️ Pending | Advisory payloads lack structured version ranges; all entries remain vendor identifiers. Coordinate with CCCS to obtain machine-readable version metadata or fall back to Model heuristics. |
| `certbund` | ⚠️ Pending | `product.Versions` contains natural-language German ranges. Parser spec drafted (`CERTBUND-NORM-01`); awaiting implementation before emitting normalized rules. |
| `vendor.cisco` | ⚠️ Pending | Current API exposes product IDs only. Engage Cisco PSIRT (FEEDCONN-CISCO-02-014) to surface affected version expressions. |
| `vendor.apple` | ✅ Done | SemVer-style helpers in `AppleMapper` emit normalized rules with provenance annotations. |
| `vendor.msrc` | ✅ Done | `MsrcMapper` maps KB build numbers to exact rules; normalized output guarded by fixtures. |
| `vendor.ghsa` | ✅ Done | SemVer + vendor fallback rules emitted (`CreateSemVerVersionArtifacts`). |
| `vendor.kisa` | ✅ Partial | Normalized SemVer rules for structured firmware ranges. Fallback vendor strings remain for prose-only advisories; continue capturing new patterns in fixtures. |
| `ics.cisa` | ✅ Done | Firmware helper emits normalized range matrix using `SemVerRangeRuleBuilder`. |
| `certcc` | ✅ Done | Vendor comparator transforms range fragments to normalized rules. |
| `cve.nvd` | ✅ Done | Full SemVer builder and provenance mapping rolled out (FEEDCONN-CVE-02-015). |
| `ru.bdu` | ⚠️ Pending | Feed only includes product codenames. Normalized rules blocked until Roskomnadzor publishes range schema. |
| `ru.nkcki` | ✅ Done | SemVer-style range parser covers vendor firmware records; remaining prose ranges logged with `Normalized version rules missing`. |
## 2. Code snippet: SemVer connector (CCCS/Cisco/ICS-CISA)
```csharp

View File

@@ -3,8 +3,8 @@
"kind": "scanner.event.report.ready",
"version": 1,
"tenant": "tenant-alpha",
"occurredAt": "2025-10-19T12:34:56Z",
"recordedAt": "2025-10-19T12:34:57Z",
"occurredAt": "2025-10-19T12:34:56+00:00",
"recordedAt": "2025-10-19T12:34:57+00:00",
"source": "scanner.webservice",
"idempotencyKey": "scanner.event.report.ready:tenant-alpha:report-abc",
"correlationId": "report-abc",
@@ -15,17 +15,11 @@
"repo": "api",
"digest": "sha256:feedface"
},
"attributes": {
"reportId": "report-abc",
"policyRevisionId": "rev-42",
"policyDigest": "digest-123",
"verdict": "blocked"
},
"payload": {
"reportId": "report-abc",
"scanId": "report-abc",
"imageDigest": "sha256:feedface",
"generatedAt": "2025-10-19T12:34:56Z",
"generatedAt": "2025-10-19T12:34:56+00:00",
"verdict": "fail",
"summary": {
"total": 1,
@@ -42,8 +36,8 @@
},
"quietedFindingCount": 0,
"policy": {
"digest": "digest-123",
"revisionId": "rev-42"
"revisionId": "rev-42",
"digest": "digest-123"
},
"links": {
"report": {
@@ -61,7 +55,7 @@
},
"dsse": {
"payloadType": "application/vnd.stellaops.report+json",
"payload": "eyJyZXBvcnRJZCI6InJlcG9ydC1hYmMiLCJpbWFnZURpZ2VzdCI6InNoYTI1NjpmZWVkZmFjZSIsImdlbmVyYXRlZEF0IjoiMjAyNS0xMC0xOVQxMjozNDo1NiswMDowMCIsInZlcmRpY3QiOiJibG9ja2VkIiwicG9saWN5Ijp7InJldmlzaW9uSWQiOiJyZXYtNDIiLCJkaWdlc3QiOiJkaWdlc3QtMTIzIn0sInN1bW1hcnkiOnsidG90YWwiOjEsImJsb2NrZWQiOjEsIndhcm5lZCI6MCwiaWdub3JlZCI6MCwicXVpZXRlZCI6MH0sInZlcmRpY3RzIjpbeyJmaW5kaW5nSWQiOiJmaW5kaW5nLTEiLCJzdGF0dXMiOiJCbG9ja2VkIiwic2NvcmUiOjQ3LjUsInNvdXJjZVRydXN0IjoiTlZEIiwicmVhY2hhYmlsaXR5IjoicnVudGltZSJ9XSwiaXNzdWVzIjpbXX0=",
"payload": "eyJyZXBvcnRJZCI6InJlcG9ydC1hYmMiLCJpbWFnZURpZ2VzdCI6InNoYTI1NjpmZWVkZmFjZSIsImdlbmVyYXRlZEF0IjoiMjAyNS0xMC0xOVQxMjozNDo1NiswMDowMCIsInZlcmRpY3QiOiJibG9ja2VkIiwicG9saWN5Ijp7InJldmlzaW9uSWQiOiJyZXYtNDIiLCJkaWdlc3QiOiJkaWdlc3QtMTIzIn0sInN1bW1hcnkiOnsidG90YWwiOjEsImJsb2NrZWQiOjEsIndhcm5lZCI6MCwiaWdub3JlZCI6MCwicXVpZXRlZCI6MH0sInZlcmRpY3RzIjpbeyJmaW5kaW5nSWQiOiJmaW5kaW5nLTEiLCJyZWFjaGFiaWxpdHkiOiJydW50aW1lIiwic2NvcmUiOjQ3LjUsInNvdXJjZVRydXN0IjoiTlZEIiwic3RhdHVzIjoiQmxvY2tlZCJ9XSwiaXNzdWVzIjpbXSwic3VyZmFjZSI6eyJ0ZW5hbnQiOiJ0ZW5hbnQtYWxwaGEiLCJnZW5lcmF0ZWRBdCI6IjIwMjUtMTAtMTlUMTI6MzQ6NTYrMDA6MDAiLCJtYW5pZmVzdERpZ2VzdCI6InNoYTI1Njo0ZmVlODdkMTg2MjkxZGRmYmJjYzJjNTZjOGVkMGU4Mjg1MjBiOGY1MmUxY2RlMGUxM2JiYTA4MmYxMDkxOGQ3IiwibWFuaWZlc3RVcmkiOiJjYXM6Ly9zY2FubmVyLWFydGlmYWN0cy9zY2FubmVyL3N1cmZhY2UvbWFuaWZlc3RzL3RlbmFudC1hbHBoYS9zaGEyNTYvNGYvZWUvNGZlZTg3ZDE4NjI5MWRkZmJiY2MyYzU2YzhlZDBlODI4NTIwYjhmNTJlMWNkZTBlMTNiYmEwODJmMTA5MThkNy5qc29uIiwibWFuaWZlc3QiOnsic2NoZW1hIjoic3RlbGxhb3BzLnN1cmZhY2UubWFuaWZlc3RAMSIsInRlbmFudCI6InRlbmFudC1hbHBoYSIsImltYWdlRGlnZXN0Ijoic2hhMjU2OmZlZWRmYWNlIiwiZ2VuZXJhdGVkQXQiOiIyMDI1LTEwLTE5VDEyOjM0OjU2KzAwOjAwIiwiYXJ0aWZhY3RzIjpbeyJraW5kIjoiZW50cnktdHJhY2UiLCJ1cmkiOiJjYXM6Ly9zY2FubmVyLWFydGlmYWN0cy9zY2FubmVyL2VudHJ5LXRyYWNlL2YwZjBmMGYwZjBmMGYwZjBmMGYwZjBmMGYwZjBmMGYwZjBmMGYwZjBmMGYwZjBmMGYwZjBmMGYwZjBmMGYwL2VudHJ5LXRyYWNlLmpzb24iLCJkaWdlc3QiOiJzaGEyNTY6ZjBmMGYwZjBmMGYwZjBmMGYwZjBmMGYwZjBmMGYwZjBmMGYwZjBmMGYwZjBmMGYwZjBmMGYwZjBmMGYwZjBmMCIsIm1lZGlhVHlwZSI6ImFwcGxpY2F0aW9uL2pzb24iLCJmb3JtYXQiOiJqc29uIiwic2l6ZUJ5dGVzIjo0MDk2fSx7ImtpbmQiOiJzYm9tLWludmVudG9yeSIsInVyaSI6ImNhczovL3NjYW5uZXItYXJ0aWZhY3RzL3NjYW5uZXIvaW1hZ2VzL2ZlZWRmYWNlL3Nib20uY2R4Lmpzb24iLCJkaWdlc3QiOiJzaGEyNTY6MTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMSIsIm1lZGlhVHlwZSI6ImFwcGxpY2F0aW9uL3ZuZC5jeWNsb25lZHgranNvbjt2ZXJzaW9uPTEuNjt2aWV3PWludmVudG9yeSIsImZvcm1hdCI6ImNkeC1qc29uIiwic2l6ZUJ5dGVzIjoyNDU3NiwidmlldyI6ImludmVudG9yeSJ9LHsia2luZCI6InNib20tdXNhZ2UiLCJ1cmkiOiJjYXM6Ly9zY2FubmVyLWFydGlmYWN0cy9zY2FubmVyL2ltYWdlcy9mZWVkZmFjZS9zYm9tLXVzYWdlLmNkeC5qc29uIiwiZGlnZXN0Ijoic2hhMjU2OjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIiLCJtZWRpYVR5cGUiOiJhcHBsaWNhdGlvbi92bmQuY3ljbG9uZWR4K2pzb247dmVyc2lvbj0xLjY7dmlldz11c2FnZSIsImZvcm1hdCI6ImNkeC1qc29uIiwic2l6ZUJ5dGVzIjoxNjM4NCwidmlldyI6InVzYWdlIn1dfX19",
"signatures": [
{
"keyId": "test-key",
@@ -72,11 +66,12 @@
},
"report": {
"reportId": "report-abc",
"generatedAt": "2025-10-19T12:34:56Z",
"imageDigest": "sha256:feedface",
"generatedAt": "2025-10-19T12:34:56+00:00",
"verdict": "blocked",
"policy": {
"digest": "digest-123",
"revisionId": "rev-42"
"revisionId": "rev-42",
"digest": "digest-123"
},
"summary": {
"total": 1,
@@ -85,17 +80,62 @@
"ignored": 0,
"quieted": 0
},
"verdict": "blocked",
"verdicts": [
{
"findingId": "finding-1",
"status": "Blocked",
"reachability": "runtime",
"score": 47.5,
"sourceTrust": "NVD",
"reachability": "runtime"
"status": "Blocked"
}
],
"issues": []
"issues": [],
"surface": {
"tenant": "tenant-alpha",
"generatedAt": "2025-10-19T12:34:56+00:00",
"manifestDigest": "sha256:4fee87d186291ddfbbcc2c56c8ed0e828520b8f52e1cde0e13bba082f10918d7",
"manifestUri": "cas://scanner-artifacts/scanner/surface/manifests/tenant-alpha/sha256/4f/ee/4fee87d186291ddfbbcc2c56c8ed0e828520b8f52e1cde0e13bba082f10918d7.json",
"manifest": {
"schema": "stellaops.surface.manifest@1",
"tenant": "tenant-alpha",
"imageDigest": "sha256:feedface",
"generatedAt": "2025-10-19T12:34:56+00:00",
"artifacts": [
{
"kind": "entry-trace",
"uri": "cas://scanner-artifacts/scanner/entry-trace/f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0/entry-trace.json",
"digest": "sha256:f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0",
"mediaType": "application/json",
"format": "json",
"sizeBytes": 4096
},
{
"kind": "sbom-inventory",
"uri": "cas://scanner-artifacts/scanner/images/feedface/sbom.cdx.json",
"digest": "sha256:1111111111111111111111111111111111111111111111111111111111111111",
"mediaType": "application/vnd.cyclonedx+json;version=1.6;view=inventory",
"format": "cdx-json",
"sizeBytes": 24576,
"view": "inventory"
},
{
"kind": "sbom-usage",
"uri": "cas://scanner-artifacts/scanner/images/feedface/sbom-usage.cdx.json",
"digest": "sha256:2222222222222222222222222222222222222222222222222222222222222222",
"mediaType": "application/vnd.cyclonedx+json;version=1.6;view=usage",
"format": "cdx-json",
"sizeBytes": 16384,
"view": "usage"
}
]
}
}
}
},
"attributes": {
"policyDigest": "digest-123",
"policyRevisionId": "rev-42",
"reportId": "report-abc",
"verdict": "blocked"
}
}

View File

@@ -3,8 +3,8 @@
"kind": "scanner.event.scan.completed",
"version": 1,
"tenant": "tenant-alpha",
"occurredAt": "2025-10-19T12:34:56Z",
"recordedAt": "2025-10-19T12:34:57Z",
"occurredAt": "2025-10-19T12:34:56+00:00",
"recordedAt": "2025-10-19T12:34:57+00:00",
"source": "scanner.webservice",
"idempotencyKey": "scanner.event.scan.completed:tenant-alpha:report-abc",
"correlationId": "report-abc",
@@ -14,12 +14,6 @@
"repo": "api",
"digest": "sha256:feedface"
},
"attributes": {
"reportId": "report-abc",
"policyRevisionId": "rev-42",
"policyDigest": "digest-123",
"verdict": "blocked"
},
"payload": {
"reportId": "report-abc",
"scanId": "report-abc",
@@ -39,8 +33,8 @@
]
},
"policy": {
"digest": "digest-123",
"revisionId": "rev-42"
"revisionId": "rev-42",
"digest": "digest-123"
},
"findings": [
{
@@ -67,7 +61,7 @@
},
"dsse": {
"payloadType": "application/vnd.stellaops.report+json",
"payload": "eyJyZXBvcnRJZCI6InJlcG9ydC1hYmMiLCJpbWFnZURpZ2VzdCI6InNoYTI1NjpmZWVkZmFjZSIsImdlbmVyYXRlZEF0IjoiMjAyNS0xMC0xOVQxMjozNDo1NiswMDowMCIsInZlcmRpY3QiOiJibG9ja2VkIiwicG9saWN5Ijp7InJldmlzaW9uSWQiOiJyZXYtNDIiLCJkaWdlc3QiOiJkaWdlc3QtMTIzIn0sInN1bW1hcnkiOnsidG90YWwiOjEsImJsb2NrZWQiOjEsIndhcm5lZCI6MCwiaWdub3JlZCI6MCwicXVpZXRlZCI6MH0sInZlcmRpY3RzIjpbeyJmaW5kaW5nSWQiOiJmaW5kaW5nLTEiLCJzdGF0dXMiOiJCbG9ja2VkIiwic2NvcmUiOjQ3LjUsInNvdXJjZVRydXN0IjoiTlZEIiwicmVhY2hhYmlsaXR5IjoicnVudGltZSJ9XSwiaXNzdWVzIjpbXX0=",
"payload": "eyJyZXBvcnRJZCI6InJlcG9ydC1hYmMiLCJpbWFnZURpZ2VzdCI6InNoYTI1NjpmZWVkZmFjZSIsImdlbmVyYXRlZEF0IjoiMjAyNS0xMC0xOVQxMjozNDo1NiswMDowMCIsInZlcmRpY3QiOiJibG9ja2VkIiwicG9saWN5Ijp7InJldmlzaW9uSWQiOiJyZXYtNDIiLCJkaWdlc3QiOiJkaWdlc3QtMTIzIn0sInN1bW1hcnkiOnsidG90YWwiOjEsImJsb2NrZWQiOjEsIndhcm5lZCI6MCwiaWdub3JlZCI6MCwicXVpZXRlZCI6MH0sInZlcmRpY3RzIjpbeyJmaW5kaW5nSWQiOiJmaW5kaW5nLTEiLCJyZWFjaGFiaWxpdHkiOiJydW50aW1lIiwic2NvcmUiOjQ3LjUsInNvdXJjZVRydXN0IjoiTlZEIiwic3RhdHVzIjoiQmxvY2tlZCJ9XSwiaXNzdWVzIjpbXSwic3VyZmFjZSI6eyJ0ZW5hbnQiOiJ0ZW5hbnQtYWxwaGEiLCJnZW5lcmF0ZWRBdCI6IjIwMjUtMTAtMTlUMTI6MzQ6NTYrMDA6MDAiLCJtYW5pZmVzdERpZ2VzdCI6InNoYTI1Njo0ZmVlODdkMTg2MjkxZGRmYmJjYzJjNTZjOGVkMGU4Mjg1MjBiOGY1MmUxY2RlMGUxM2JiYTA4MmYxMDkxOGQ3IiwibWFuaWZlc3RVcmkiOiJjYXM6Ly9zY2FubmVyLWFydGlmYWN0cy9zY2FubmVyL3N1cmZhY2UvbWFuaWZlc3RzL3RlbmFudC1hbHBoYS9zaGEyNTYvNGYvZWUvNGZlZTg3ZDE4NjI5MWRkZmJiY2MyYzU2YzhlZDBlODI4NTIwYjhmNTJlMWNkZTBlMTNiYmEwODJmMTA5MThkNy5qc29uIiwibWFuaWZlc3QiOnsic2NoZW1hIjoic3RlbGxhb3BzLnN1cmZhY2UubWFuaWZlc3RAMSIsInRlbmFudCI6InRlbmFudC1hbHBoYSIsImltYWdlRGlnZXN0Ijoic2hhMjU2OmZlZWRmYWNlIiwiZ2VuZXJhdGVkQXQiOiIyMDI1LTEwLTE5VDEyOjM0OjU2KzAwOjAwIiwiYXJ0aWZhY3RzIjpbeyJraW5kIjoiZW50cnktdHJhY2UiLCJ1cmkiOiJjYXM6Ly9zY2FubmVyLWFydGlmYWN0cy9zY2FubmVyL2VudHJ5LXRyYWNlL2YwZjBmMGYwZjBmMGYwZjBmMGYwZjBmMGYwZjBmMGYwZjBmMGYwZjBmMGYwZjBmMGYwZjBmMGYwZjBmMGYwL2VudHJ5LXRyYWNlLmpzb24iLCJkaWdlc3QiOiJzaGEyNTY6ZjBmMGYwZjBmMGYwZjBmMGYwZjBmMGYwZjBmMGYwZjBmMGYwZjBmMGYwZjBmMGYwZjBmMGYwZjBmMGYwZjBmMCIsIm1lZGlhVHlwZSI6ImFwcGxpY2F0aW9uL2pzb24iLCJmb3JtYXQiOiJqc29uIiwic2l6ZUJ5dGVzIjo0MDk2fSx7ImtpbmQiOiJzYm9tLWludmVudG9yeSIsInVyaSI6ImNhczovL3NjYW5uZXItYXJ0aWZhY3RzL3NjYW5uZXIvaW1hZ2VzL2ZlZWRmYWNlL3Nib20uY2R4Lmpzb24iLCJkaWdlc3QiOiJzaGEyNTY6MTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMSIsIm1lZGlhVHlwZSI6ImFwcGxpY2F0aW9uL3ZuZC5jeWNsb25lZHgranNvbjt2ZXJzaW9uPTEuNjt2aWV3PWludmVudG9yeSIsImZvcm1hdCI6ImNkeC1qc29uIiwic2l6ZUJ5dGVzIjoyNDU3NiwidmlldyI6ImludmVudG9yeSJ9LHsia2luZCI6InNib20tdXNhZ2UiLCJ1cmkiOiJjYXM6Ly9zY2FubmVyLWFydGlmYWN0cy9zY2FubmVyL2ltYWdlcy9mZWVkZmFjZS9zYm9tLXVzYWdlLmNkeC5qc29uIiwiZGlnZXN0Ijoic2hhMjU2OjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIiLCJtZWRpYVR5cGUiOiJhcHBsaWNhdGlvbi92bmQuY3ljbG9uZWR4K2pzb247dmVyc2lvbj0xLjY7dmlldz11c2FnZSIsImZvcm1hdCI6ImNkeC1qc29uIiwic2l6ZUJ5dGVzIjoxNjM4NCwidmlldyI6InVzYWdlIn1dfX19",
"signatures": [
{
"keyId": "test-key",
@@ -78,11 +72,12 @@
},
"report": {
"reportId": "report-abc",
"generatedAt": "2025-10-19T12:34:56Z",
"imageDigest": "sha256:feedface",
"generatedAt": "2025-10-19T12:34:56+00:00",
"verdict": "blocked",
"policy": {
"digest": "digest-123",
"revisionId": "rev-42"
"revisionId": "rev-42",
"digest": "digest-123"
},
"summary": {
"total": 1,
@@ -91,17 +86,62 @@
"ignored": 0,
"quieted": 0
},
"verdict": "blocked",
"verdicts": [
{
"findingId": "finding-1",
"status": "Blocked",
"reachability": "runtime",
"score": 47.5,
"sourceTrust": "NVD",
"reachability": "runtime"
"status": "Blocked"
}
],
"issues": []
"issues": [],
"surface": {
"tenant": "tenant-alpha",
"generatedAt": "2025-10-19T12:34:56+00:00",
"manifestDigest": "sha256:4fee87d186291ddfbbcc2c56c8ed0e828520b8f52e1cde0e13bba082f10918d7",
"manifestUri": "cas://scanner-artifacts/scanner/surface/manifests/tenant-alpha/sha256/4f/ee/4fee87d186291ddfbbcc2c56c8ed0e828520b8f52e1cde0e13bba082f10918d7.json",
"manifest": {
"schema": "stellaops.surface.manifest@1",
"tenant": "tenant-alpha",
"imageDigest": "sha256:feedface",
"generatedAt": "2025-10-19T12:34:56+00:00",
"artifacts": [
{
"kind": "entry-trace",
"uri": "cas://scanner-artifacts/scanner/entry-trace/f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0/entry-trace.json",
"digest": "sha256:f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0",
"mediaType": "application/json",
"format": "json",
"sizeBytes": 4096
},
{
"kind": "sbom-inventory",
"uri": "cas://scanner-artifacts/scanner/images/feedface/sbom.cdx.json",
"digest": "sha256:1111111111111111111111111111111111111111111111111111111111111111",
"mediaType": "application/vnd.cyclonedx+json;version=1.6;view=inventory",
"format": "cdx-json",
"sizeBytes": 24576,
"view": "inventory"
},
{
"kind": "sbom-usage",
"uri": "cas://scanner-artifacts/scanner/images/feedface/sbom-usage.cdx.json",
"digest": "sha256:2222222222222222222222222222222222222222222222222222222222222222",
"mediaType": "application/vnd.cyclonedx+json;version=1.6;view=usage",
"format": "cdx-json",
"sizeBytes": 16384,
"view": "usage"
}
]
}
}
}
},
"attributes": {
"policyDigest": "digest-123",
"policyRevisionId": "rev-42",
"reportId": "report-abc",
"verdict": "blocked"
}
}

View File

@@ -130,6 +130,7 @@ Follow the sprint files below in order. Update task status in both `SPRINTS` and
> 2025-11-02: DOCS-SCANNER-BENCH-62-013 marked DONE (Docs Guild, Swift Analyzer Guild) Swift analyzer roadmap captured with policy hooks.
> 2025-11-02: DOCS-SCANNER-BENCH-62-014 marked DONE (Docs Guild, Runtime Guild) Kubernetes/VM alignment section published.
> 2025-11-02: DOCS-SCANNER-BENCH-62-015 marked DONE (Docs Guild, Export Center Guild) DSSE/Rekor enablement guidance appended to gap doc.
> 2025-11-05: SCANNER-SURFACE-02 marked DONE (Scanner WebService Guild) WebService now persists `surface` manifest pointers in scan/report APIs, orchestrator samples and DSSE fixtures refreshed, and readiness tests updated with Surface validators stubbed for deterministic health checks.
> 2025-11-02: SCANNER-ENG-0009 moved to DOING (Ruby Analyzer Guild) drafting Ruby analyzer parity design package.
> 2025-11-02: SCANNER-ENG-0016 added (Ruby Analyzer Guild) implementing Ruby lock collector & vendor cache ingestion.
> 2025-11-02: SCANNER-ENG-0016 moved to DOING (Ruby Analyzer Guild) lockfile parser skeleton committed with initial Gemfile.lock parsing.

View File

@@ -105,10 +105,10 @@ SEC3.PLG | BLOCKED (2025-10-21) | Ensure lockout responses and rate-limit metada
SEC5.PLG | BLOCKED (2025-10-21) | Address plugin-specific mitigations (bootstrap user handling, password policy docs) in threat model backlog. <br>⛔ Final documentation depends on AUTH-DPOP-11-001 / AUTH-MTLS-11-002 / PLUGIN-DI-08-001 outcomes. | Security Guild (src/Authority/StellaOps.Authority/StellaOps.Authority.Plugin.Standard/TASKS.md)
PLG7.IMPL-001 | DONE (2025-11-03) | Scaffold `StellaOps.Authority.Plugin.Ldap` + tests, bind configuration (client certificate, trust-store, insecure toggle) with validation and docs samples. | BE-Auth Plugin (src/Authority/StellaOps.Authority/StellaOps.Authority.Plugin.Standard/TASKS.md)
> 2025-11-03: Initial `StellaOps.Authority.Plugin.Ldap` project/tests scaffolded with configuration options + registrar; sample manifest (`etc/authority.plugins/ldap.yaml`) updated to new schema (client certificate, trust store, insecure toggle).
PLG7.IMPL-002 | DOING (2025-11-03) | Implement LDAP credential store with TLS/mutual TLS enforcement, deterministic retry/backoff, and structured logging/metrics. | BE-Auth Plugin, Security Guild (src/Authority/StellaOps.Authority/StellaOps.Authority.Plugin.Standard/TASKS.md)
PLG7.IMPL-002 | DONE (2025-11-04) | Implement LDAP credential store with TLS/mutual TLS enforcement, deterministic retry/backoff, and structured logging/metrics. | BE-Auth Plugin, Security Guild (src/Authority/StellaOps.Authority/StellaOps.Authority.Plugin.Standard/TASKS.md)
> 2025-11-03: Review concluded; RFC accepted with audit/mTLS/mapping decisions recorded in `docs/notes/2025-11-03-authority-plugin-ldap-review.md`. Follow-up implementation tasks PLG7.IMPL-001..005 added to plugin board.
> 2025-11-04: Updated connection factory to negotiate StartTLS via `StartTransportLayerSecurity(null)` and normalized LDAP result-code handling (invalid credentials + transient codes) against `System.DirectoryServices.Protocols` 8.0. Plugin unit suite (`dotnet test src/Authority/StellaOps.Authority/StellaOps.Authority.Plugin.Ldap.Tests/StellaOps.Authority.Plugin.Ldap.Tests.csproj`) now passes again after the retry/error-path fixes.
> 2025-11-04: PLG7.IMPL-002 progress enforced TLS/client certificate validation, expanded LDAP audit properties and retry telemetry, warned when cipher lists are unsupported, refreshed sample config, and reran `dotnet test src/Authority/StellaOps.Authority/StellaOps.Authority.Plugin.Ldap.Tests/StellaOps.Authority.Plugin.Ldap.Tests.csproj --no-restore`.
> 2025-11-04: PLG7.IMPL-002 DONE deterministic credential store retries now emit metrics + structured audit context, DirectoryServices factory enforces TLS/mTLS settings (trust store + client cert), and configuration samples/docs refreshed. Tests: `dotnet test src/Authority/StellaOps.Authority/StellaOps.Authority.Plugin.Ldap.Tests/StellaOps.Authority.Plugin.Ldap.Tests.csproj --no-restore`.
[Identity & Signing] 100.C) IssuerDirectory
Summary: Identity & Signing focus on IssuerDirectory.

View File

@@ -2,7 +2,7 @@
## Status Snapshot (2025-11-04)
- **Advisory AI** 5 of 11 tasks are DONE (AIAI-31-001, AIAI-31-002, AIAI-31-003, AIAI-31-010, AIAI-31-011); orchestration pipeline (AIAI-31-004) and host wiring (AIAI-31-004A) remain DOING while downstream guardrails, CLI, and observability tracks (AIAI-31-004B/004C and AIAI-31-005 through AIAI-31-009) stay TODO pending cache/guardrail implementation and WebService/Worker hardening.
- **Advisory AI** 5 of 11 tasks are DONE (AIAI-31-001, AIAI-31-002, AIAI-31-003, AIAI-31-010, AIAI-31-011); orchestration pipeline (AIAI-31-004) and host wiring (AIAI-31-004A) remain TODO while downstream guardrails, CLI, and observability tracks (AIAI-31-004B/004C and AIAI-31-005 through AIAI-31-009) stay TODO pending cache/guardrail implementation and WebService/Worker hardening.
- 2025-11-04: AIAI-31-002 and AIAI-31-003 shipped with deterministic SBOM context client wiring (`AddSbomContext` typed HTTP client) and toolset integration; WebService/Worker now invoke the orchestrator with SBOM-backed simulations and emit initial metrics.
- 2025-11-03: AIAI-31-002 landed the configurable HTTP client + DI defaults; retriever now resolves data via `/v1/sbom/context`, retaining a null fallback until SBOM service ships.
- 2025-11-03: Follow-up: SBOM guild to deliver base URL/API key and run an Advisory AI smoke retrieval once SBOM-AIAI-31-001 endpoints are live.
@@ -25,8 +25,8 @@ Task ID | State | Task description | Owners (Source)
AIAI-31-001 | DONE (2025-11-02) | Implement structured and vector retrievers for advisories/VEX with paragraph anchors and citation metadata. Dependencies: CONCELIER-VULN-29-001, EXCITITOR-VULN-29-001. | Advisory AI Guild (src/AdvisoryAI/StellaOps.AdvisoryAI/TASKS.md)
AIAI-31-002 | DONE (2025-11-04) | Build SBOM context retriever (purl version timelines, dependency paths, env flags, blast radius estimator). Dependencies: SBOM-VULN-29-001. | Advisory AI Guild, SBOM Service Guild (src/AdvisoryAI/StellaOps.AdvisoryAI/TASKS.md)
AIAI-31-003 | DONE (2025-11-04) | Implement deterministic toolset (version comparators, range checks, dependency analysis, policy lookup) exposed via orchestrator. Dependencies: AIAI-31-001..002. | Advisory AI Guild (src/AdvisoryAI/StellaOps.AdvisoryAI/TASKS.md)
AIAI-31-004 | DOING | Build orchestration pipeline for Summary/Conflict/Remediation tasks (prompt templates, tool calls, token budgets, caching). Dependencies: AIAI-31-001..003, AUTH-VULN-29-001. | Advisory AI Guild (src/AdvisoryAI/StellaOps.AdvisoryAI/TASKS.md)
AIAI-31-004A | DOING (2025-11-04) | Wire orchestrator into WebService/Worker, expose API + queue contract, emit metrics, stub cache. Dependencies: AIAI-31-004, AIAI-31-002. | Advisory AI Guild, Platform Guild (src/AdvisoryAI/StellaOps.AdvisoryAI/TASKS.md)
AIAI-31-004 | DONE (2025-11-04) | Build orchestration pipeline for Summary/Conflict/Remediation tasks (prompt templates, tool calls, token budgets, caching). Dependencies: AIAI-31-001..003, AUTH-VULN-29-001. | Advisory AI Guild (src/AdvisoryAI/StellaOps.AdvisoryAI/TASKS.md)
AIAI-31-004A | DONE (2025-11-04) | Wire orchestrator into WebService/Worker, expose API + queue contract, emit metrics, stub cache. Dependencies: AIAI-31-004, AIAI-31-002. | Advisory AI Guild, Platform Guild (src/AdvisoryAI/StellaOps.AdvisoryAI/TASKS.md)
> 2025-11-03: WebService/Worker scaffolds created with in-memory cache/queue, minimal APIs (`/api/v1/advisory/plan`, `/api/v1/advisory/queue`), metrics counters, and plan cache instrumentation; worker processes queue using orchestrator.
> 2025-11-04: SBOM base address now flows via `SbomContextClientOptions.BaseAddress`, worker emits queue/plan metrics, and orchestrator cache keys expanded to cover SBOM hash inputs.
AIAI-31-004B | TODO | Implement prompt assembler, guardrails, cache persistence, DSSE provenance, golden outputs. Dependencies: AIAI-31-004A, DOCS-AIAI-31-003, AUTH-AIAI-31-004. | Advisory AI Guild, Security Guild (src/AdvisoryAI/StellaOps.AdvisoryAI/TASKS.md)
@@ -41,6 +41,7 @@ DOCS-AIAI-31-007 | BLOCKED (2025-11-03) | Write `/docs/security/assistant-guardr
DOCS-AIAI-31-008 | BLOCKED (2025-11-03) | Publish `/docs/sbom/remediation-heuristics.md` (feasibility scoring, blast radius). Dependencies: SBOM-AIAI-31-001. | Docs Guild, SBOM Service Guild (docs/TASKS.md)
DOCS-AIAI-31-009 | BLOCKED (2025-11-03) | Create `/docs/runbooks/assistant-ops.md` for warmup, cache priming, model outages, scaling. Dependencies: DEVOPS-AIAI-31-001. | Docs Guild, DevOps Guild (docs/TASKS.md)
> 2025-11-03: DOCS-AIAI-31-003 moved to DOING drafting Advisory AI API reference (endpoints, rate limits, error model) for sprint 110.
> 2025-11-04: AIAI-31-005 DONE guardrail pipeline redacts secrets, enforces citation/injection policies, emits block counters, and tests (`AdvisoryGuardrailPipelineTests`) cover redaction + citation validation.
> 2025-11-03: DOCS-AIAI-31-003 marked DONE `docs/advisory-ai/api.md` published with scopes, request/response schemas, rate limits, and error catalogue (Docs Guild).
> 2025-11-03: DOCS-AIAI-31-001 marked DONE `docs/advisory-ai/overview.md` published with value, personas, guardrails, observability, and roadmap checklists (Docs Guild).
> 2025-11-03: DOCS-AIAI-31-002 marked DONE `docs/advisory-ai/architecture.md` published describing pipeline, deterministic tooling, caching, and profile governance (Docs Guild).
@@ -50,8 +51,8 @@ DOCS-AIAI-31-009 | BLOCKED (2025-11-03) | Create `/docs/runbooks/assistant-ops.m
> 2025-11-03: DOCS-AIAI-31-007 marked BLOCKED Guardrail implementation (AIAI-31-005) incomplete.
> 2025-11-03: DOCS-AIAI-31-008 marked BLOCKED Waiting on SBOM heuristics delivery (SBOM-AIAI-31-001).
> 2025-11-03: DOCS-AIAI-31-009 marked BLOCKED DevOps runbook inputs (DEVOPS-AIAI-31-001) outstanding.
AIAI-31-005 | DOING (2025-11-03) | Implement guardrails (redaction, injection defense, output validation, citation enforcement) and fail-safe handling. Dependencies: AIAI-31-004. | Advisory AI Guild, Security Guild (src/AdvisoryAI/StellaOps.AdvisoryAI/TASKS.md)
AIAI-31-006 | DOING (2025-11-03) | Expose REST API endpoints (`/advisory/ai/*`) with RBAC, rate limits, OpenAPI schemas, and batching support. Dependencies: AIAI-31-004..005. | Advisory AI Guild (src/AdvisoryAI/StellaOps.AdvisoryAI/TASKS.md)
AIAI-31-005 | DONE (2025-11-04) | Implement guardrails (redaction, injection defense, output validation, citation enforcement) and fail-safe handling. Dependencies: AIAI-31-004. | Advisory AI Guild, Security Guild (src/AdvisoryAI/StellaOps.AdvisoryAI/TASKS.md)
AIAI-31-006 | DONE (2025-11-04) | Expose REST API endpoints (`/advisory/ai/*`) with RBAC, rate limits, OpenAPI schemas, and batching support. Dependencies: AIAI-31-004..005. | Advisory AI Guild (src/AdvisoryAI/StellaOps.AdvisoryAI/TASKS.md)
> 2025-11-03: Shipped `/api/v1/advisory/{task}` execution and `/api/v1/advisory/outputs/{cacheKey}` retrieval endpoints with guardrail integration, provenance hashes, and metrics (RBAC & rate limiting still pending Authority scope delivery).
AIAI-31-007 | TODO | Instrument metrics (`advisory_ai_latency`, `guardrail_blocks`, `validation_failures`, `citation_coverage`), logs, and traces; publish dashboards/alerts. Dependencies: AIAI-31-004..006. | Advisory AI Guild, Observability Guild (src/AdvisoryAI/StellaOps.AdvisoryAI/TASKS.md)
AIAI-31-008 | TODO | Package inference on-prem container, remote inference toggle, Helm/Compose manifests, scaling guidance, offline kit instructions. Dependencies: AIAI-31-006..007. | Advisory AI Guild, DevOps Guild (src/AdvisoryAI/StellaOps.AdvisoryAI/TASKS.md)
@@ -64,6 +65,7 @@ AIAI-31-009 | TODO | Develop unit/golden/property/perf tests, injection harness,
> 2025-11-02: AIAI-31-004 kicked off orchestration pipeline design establishing deterministic task sequence (summary/conflict/remediation) and cache key strategy.
> 2025-11-02: AIAI-31-004 orchestration prerequisites documented in docs/modules/advisory-ai/orchestration-pipeline.md (tasks 004A/004B/004C).
> 2025-11-02: AIAI-31-003 moved to DOING beginning deterministic tooling (comparators, dependency analysis) while awaiting SBOM context client. Semantic & EVR comparators shipped; toolset interface published for orchestrator adoption.
> 2025-11-04: AIAI-31-004 DONE orchestrator composes evidence (structured/vector/SBOM) with stable cache keys, metadata, and hashing; tests keep determinism enforced.
> 2025-11-02: Structured + vector retrievers landed with deterministic CSAF/OSV/Markdown chunkers, deterministic hash embeddings, and unit coverage for sample advisories.
> 2025-11-02: SBOM context request/result models finalized; retriever tests now validate environment-flag toggles and dependency-path dedupe. SBOM guild to wire real context service client.
> 2025-11-04: AIAI-31-002 completed `AddSbomContext` typed client registered in WebService/Worker, BaseAddress/tenant headers sourced from configuration, and retriever HTTP-mapping tests extended.
@@ -194,12 +196,12 @@ FEEDCONN-CERTBUND-02-010 Version range provenance | BE-Conn-CERTBUND | **TODO (d
FEEDCONN-CISCO-02-009 SemVer range provenance | BE-Conn-Cisco | **TODO (due 2025-10-21)** Emit Cisco SemVer ranges into `advisory_observations.affected.versions[]` with provenance identifiers (`cisco:{productId}`) and deterministic comparison keys. Update mapper/tests for the Link-Not-Merge schema and replace legacy merge counter checks with observation/linkset validation. | CONCELIER-LNM-21-001 (src/Concelier/__Libraries/StellaOps.Concelier.Connector.Vndr.Cisco/TASKS.md)
FEEDCONN-ICSCISA-02-012 Version range provenance | BE-Conn-ICS-CISA | **DONE (2025-11-03)** Promote existing firmware/semver data into `advisory_observations.affected.versions[]` entries with deterministic comparison keys and provenance identifiers (`ics-cisa:{advisoryId}:{product}`). Add regression coverage for mixed firmware strings and raise a Models ticket only when observation schema needs a new comparison helper.<br>2025-10-29: Follow `docs/dev/normalized-rule-recipes.md` §2 to build observation version entries and log failures without invoking the retired merge helpers.<br>2025-11-03: Completed connector now normalizes semver ranges with provenance notes, RSS fallback content clears the AOC guard, and end-to-end Fetch/Parse/Map integration tests pass. | CONCELIER-LNM-21-001 (src/Concelier/__Libraries/StellaOps.Concelier.Connector.Ics.Cisa/TASKS.md)
FEEDCONN-KISA-02-008 Firmware range provenance | BE-Conn-KISA, Models | **DONE (2025-11-04)** Define comparison helpers for Hangul-labelled firmware ranges (`XFU 1.0.1.0084 ~ 2.0.1.0034`) and map them into `advisory_observations.affected.versions[]` with provenance tags. Coordinate with Models only if a new comparison scheme is required, then update localisation notes and fixtures for the Link-Not-Merge schema.<br>2025-11-03: Analysis in progress auditing existing mapper output/fixtures ahead of implementing firmware range normalization and provenance wiring.<br>2025-11-03: SemVer normalization helper wired through `KisaMapper` with provenance slugs + vendor extensions; integration tests updated and green, follow-up capture for additional Hangul exclusivity markers queued before completion.<br>2025-11-03: Extended connector tests to cover single-ended (`이상`, `초과`, `이하`, `미만`) and non-numeric phrases, verifying normalized rule types (`gt`, `gte`, `lt`, `lte`) and fallback behaviour; broader corpus review remains before transitioning to DONE.<br>2025-11-03: Captured the top 10 `detailDos.do?IDX=` pages into `seed-data/kisa/html/` via `scripts/kisa_capture_html.py`; JSON endpoint (`rssDetailData.do?IDX=…`) now returns error pages, so connector updates must parse the embedded HTML or secure authenticated API access before closing.<br>2025-11-04: Fetch + parse pipeline now consumes the HTML detail pages end to end (metadata persisted, DOM parser extracts vendor/product ranges); fixtures/tests operate on the HTML snapshots to guard normalized SemVer + vendor extension expectations and severity extraction. | CONCELIER-LNM-21-001 (src/Concelier/__Libraries/StellaOps.Concelier.Connector.Kisa/TASKS.md)
FEEDCONN-SHARED-STATE-003 Source state seeding helper | Tools Guild, BE-Conn-MSRC | **DOING (2025-10-19)** Provide a reusable CLI/utility to seed `pendingDocuments`/`pendingMappings` for connectors (MSRC backfills require scripted CVRF + detail injection). Coordinate with MSRC team for expected JSON schema and handoff once prototype lands. Prereqs confirmed none (2025-10-19). | Tools (src/Concelier/__Libraries/StellaOps.Concelier.Connector.Common/TASKS.md)
FEEDCONN-SHARED-STATE-003 Source state seeding helper | Tools Guild, BE-Conn-MSRC | **DONE (2025-11-04)** Delivered `SourceStateSeeder` CLI + processor APIs, Mongo fixtures, and MSRC runbook updates. Seeds raw docs + cursor state deterministically; tests cover happy/path/idempotent flows (`dotnet test src/Concelier/__Tests/StellaOps.Concelier.Connector.Common.Tests/...` note: requires `libcrypto.so.1.1` when running Mongo2Go locally). | Tools (src/Concelier/__Libraries/StellaOps.Concelier.Connector.Common/TASKS.md)
FEEDMERGE-COORD-02-901 Connector deadline check-ins | BE-Merge | **TODO (due 2025-10-21)** Confirm Cccs/Cisco version-provenance updates land, capture `LinksetVersionCoverage` dashboard snapshots (expect zero missing-range warnings), and update coordination docs with the results.<br>2025-10-29: Observation metrics now surface `version_entries_total`/`missing_version_entries_total`; include screenshots for both when closing this task. | FEEDMERGE-COORD-02-900 (src/Concelier/__Libraries/StellaOps.Concelier.Merge/TASKS.md)
FEEDMERGE-COORD-02-902 ICS-CISA version comparison support | BE-Merge, Models | **TODO (due 2025-10-23)** Review ICS-CISA sample advisories, validate reuse of existing comparison helpers, and pre-stage Models ticket template only if a new firmware comparator is required. Document the outcome and observation coverage logs in coordination docs + tracker files.<br>2025-10-29: `docs/dev/normalized-rule-recipes.md` (§2§3) now covers observation entries; attach decision summary + log sample when handing off to Models. Dependencies: FEEDMERGE-COORD-02-901. | FEEDMERGE-COORD-02-900 (src/Concelier/__Libraries/StellaOps.Concelier.Merge/TASKS.md)
FEEDMERGE-COORD-02-903 KISA firmware scheme review | BE-Merge, Models | **TODO (due 2025-10-24)** Pair with KISA team on proposed firmware comparison helper (`kisa.build` or variant), ensure observation mapper alignment, and open Models ticket only if a new comparator is required. Log the final helper signature and observation coverage metrics in coordination docs + tracker files. Dependencies: FEEDMERGE-COORD-02-902. | FEEDMERGE-COORD-02-900 (src/Concelier/__Libraries/StellaOps.Concelier.Merge/TASKS.md)
Fixture validation sweep | QA | **DOING (2025-10-19)** Prereqs confirmed none; continuing RHSA fixture regeneration and diff review alongside mapper provenance updates.<br>2025-10-29: Added `scripts/update-redhat-fixtures.sh` to regenerate golden snapshots with `UPDATE_GOLDENS=1`; run it before reviews to capture CSAF contract deltas. | None (src/Concelier/__Libraries/StellaOps.Concelier.Connector.Distro.RedHat/TASKS.md)
Link-Not-Merge version provenance coordination | BE-Merge | **DOING** Coordinate remaining connectors (`Acsc`, `Cccs`, `CertBund`, `CertCc`, `Cve`, `Ghsa`, `Ics.Cisa`, `Kisa`, `Ru.Bdu`, `Ru.Nkcki`, `Vndr.Apple`, `Vndr.Cisco`, `Vndr.Msrc`) so they emit `advisory_observations.affected.versions[]` entries with provenance tags and deterministic comparison keys. Track rollout status in `docs/dev/normalized-rule-recipes.md` (now updated for Link-Not-Merge) and retire the legacy merge counters as coverage transitions to linkset validation metrics.<br>2025-10-29: Added new guidance in the doc for recording observation version metadata and logging gaps via `LinksetVersionCoverage` warnings to replace prior `concelier.merge.normalized_rules*` alerts. Dependencies: CONCELIER-LNM-21-203. | CONCELIER-LNM-21-001 (src/Concelier/__Libraries/StellaOps.Concelier.Merge/TASKS.md)
Fixture validation sweep | QA | **DONE (2025-11-04)** Regenerated RHSA CSAF goldens via `scripts/update-redhat-fixtures.sh` (sets `UPDATE_GOLDENS=1`) and re-ran connector tests `dotnet test src/Concelier/__Tests/StellaOps.Concelier.Connector.Distro.RedHat.Tests/StellaOps.Concelier.Connector.Distro.RedHat.Tests.csproj --no-restore` to confirm snapshot parity. | None (src/Concelier/__Libraries/StellaOps.Concelier.Connector.Distro.RedHat/TASKS.md)
Link-Not-Merge version provenance coordination | BE-Merge | **DONE (2025-11-04)** Published connector status tracker + follow-up IDs in `docs/dev/normalized-rule-recipes.md`, enabled `Normalized version rules missing` diagnostics in Merge, and aligned dashboards on `LinksetVersionCoverage`. Remaining gaps (ACSC/CCCS/CERTBUND/Cisco/RU-BDU) documented as upstream data deficiencies awaiting feed updates. Dependencies: CONCELIER-LNM-21-203. | CONCELIER-LNM-21-001 (src/Concelier/__Libraries/StellaOps.Concelier.Merge/TASKS.md)
MERGE-LNM-21-001 | DONE (2025-11-03) | Draft `no-merge` migration playbook, documenting backfill strategy, feature flag rollout, and rollback steps for legacy merge pipeline deprecation.<br>2025-11-03: Authored `docs/migration/no-merge.md` covering rollout phases, backfill/validation checklists, and rollback guidance; shared artefact owners. | BE-Merge, Architecture Guild (src/Concelier/__Libraries/StellaOps.Concelier.Merge/TASKS.md)
@@ -358,3 +360,7 @@ MIRROR-CRT-58-002 | TODO | Integrate with Export Center scheduling to automate m
If all tasks are done - read next sprint section - SPRINT_120_policy_reasoning.md
> 2025-11-04: AIAI-31-004A DONE WebService/Worker wiring plus filesystem queue operational; metrics/logs added; tests executed via `dotnet test src/AdvisoryAI/__Tests/StellaOps.AdvisoryAI.Tests/StellaOps.AdvisoryAI.Tests.csproj --no-restore`.
> 2025-11-04: AIAI-31-006 DONE REST endpoints enforce scope headers, apply rate limits, sanitize prompts through guardrails, and enqueue execution with cached metadata.

View File

@@ -21,7 +21,7 @@ Task ID | State | Task description | Owners (Source)
LEDGER-29-001 | DONE (2025-11-03) | Design ledger & projection schemas (tables/indexes), canonical JSON format, hashing strategy, and migrations. Publish schema doc + fixtures.<br>2025-11-03: Initial migration, canonical fixtures, and schema doc alignment delivered (LEDGER-29-001). | Findings Ledger Guild (src/Findings/StellaOps.Findings.Ledger/TASKS.md)
LEDGER-29-002 | DONE (2025-11-03) | Implement ledger write API (`POST /vuln/ledger/events`) with validation, idempotency, hash chaining, and Merkle root computation job.<br>2025-11-03: Web service + domain scaffolding landed with canonical hashing helpers, in-memory repository, Merkle scheduler stub, request/response contracts, and unit tests covering hashing & conflict flows. Dependencies: LEDGER-29-001. | Findings Ledger Guild (src/Findings/StellaOps.Findings.Ledger/TASKS.md)
LEDGER-29-003 | DONE (2025-11-03) | Build projector worker that derives `findings_projection` rows from ledger events + policy determinations; ensure idempotent replay keyed by `(tenant,finding_id,policy_version)`. <br>2025-11-03: Postgres projection services landed with replay checkpoints, fixtures, and unit coverage (LEDGER-29-003). Dependencies: LEDGER-29-002. | Findings Ledger Guild, Scheduler Guild (src/Findings/StellaOps.Findings.Ledger/TASKS.md)
LEDGER-29-004 | DOING (2025-11-03) | Integrate Policy Engine batch evaluation (baseline + simulate) with projector; cache rationale references.<br>2025-11-04: Reducer+worker now store `policy_rationale` via inline evaluation; Postgres schema/fixtures/tests updated, pending real Policy Engine client wiring. Dependencies: LEDGER-29-003. | Findings Ledger Guild, Policy Guild (src/Findings/StellaOps.Findings.Ledger/TASKS.md)
LEDGER-29-004 | DONE (2025-11-04) | Integrate Policy Engine batch evaluation (baseline + simulate) with projector; cache rationale references.<br>2025-11-04: Ledger service now calls `/api/policy/eval/batch` with resilient HttpClient, shared cache, and inline fallback; documentation/config samples updated; ledger tests executed (`dotnet test src/Findings/__Tests/StellaOps.Findings.Ledger.Tests/StellaOps.Findings.Ledger.Tests.csproj --no-restore`). Dependencies: LEDGER-29-003. | Findings Ledger Guild, Policy Guild (src/Findings/StellaOps.Findings.Ledger/TASKS.md)
LEDGER-29-005 | TODO | Implement workflow mutation handlers (assign, comment, accept-risk, target-fix, verify-fix, reopen) producing ledger events with validation and attachments metadata. Dependencies: LEDGER-29-004. | Findings Ledger Guild (src/Findings/StellaOps.Findings.Ledger/TASKS.md)
LEDGER-29-006 | TODO | Integrate attachment encryption (KMS envelope), signed URL issuance, CSRF protection hooks for Console. Dependencies: LEDGER-29-005. | Findings Ledger Guild, Security Guild (src/Findings/StellaOps.Findings.Ledger/TASKS.md)
LEDGER-29-007 | TODO | Instrument metrics (`ledger_write_latency`, `projection_lag_seconds`, `ledger_events_total`), structured logs, and Merkle anchoring alerts; publish dashboards. Dependencies: LEDGER-29-006. | Findings Ledger Guild, Observability Guild (src/Findings/StellaOps.Findings.Ledger/TASKS.md)

View File

@@ -154,7 +154,7 @@ SCANNER-ENG-0025 | TODO | Implement WinSxS manifest collector per `design/window
SCANNER-ENG-0026 | TODO | Implement Windows Chocolatey & registry collectors per `design/windows-analyzer.md` §3.33.4. | Scanner Guild (docs/modules/scanner/TASKS.md)
SCANNER-ENG-0027 | TODO | Deliver Windows policy/offline integration per `design/windows-analyzer.md` §56. | Scanner Guild, Policy Guild, Offline Kit Guild (docs/modules/scanner/TASKS.md)
SCANNER-SURFACE-01 | DOING (2025-11-02) | Persist Surface.FS manifests after analyzer stages, including layer CAS metadata and EntryTrace fragments.<br>2025-11-02: Worker pipeline emitting draft Surface.FS manifests for sample scans; determinism checks running. | Scanner Worker Guild (src/Scanner/StellaOps.Scanner.Worker/TASKS.md)
SCANNER-SURFACE-02 | DOING (2025-11-02) | Publish Surface.FS pointers (CAS URIs, manifests) via scan/report APIs and update attestation metadata. Dependencies: SCANNER-SURFACE-01.<br>2025-11-02: WebService responses now include preview CAS URIs; attestation metadata updates staged for review. | Scanner WebService Guild (src/Scanner/StellaOps.Scanner.WebService/TASKS.md)
SCANNER-SURFACE-02 | DONE (2025-11-05) | Publish Surface.FS pointers (CAS URIs, manifests) via scan/report APIs and update attestation metadata. Dependencies: SCANNER-SURFACE-01.<br>2025-11-05: Surface pointer projection wired through WebService endpoints, orchestrator samples & DSSE fixtures refreshed with `surface` manifest block, and regression suite (platform events, report sample, ready check) updated. | Scanner WebService Guild (src/Scanner/StellaOps.Scanner.WebService/TASKS.md)
SCANNER-SURFACE-03 | TODO | Push layer manifests and entry fragments into Surface.FS during build-time SBOM generation. Dependencies: SCANNER-SURFACE-02. | BuildX Plugin Guild (src/Scanner/StellaOps.Scanner.Sbomer.BuildXPlugin/TASKS.md)
[Scanner & Surface] 130.A) Scanner.VIII

View File

@@ -180,6 +180,7 @@ Determinism guard instrumentation wraps the evaluator, rejecting access to forbi
- **Workers:** Lease jobs, execute evaluation pipeline, report status (success/failure/canceled). Failures with recoverable errors requeue with backoff; determinism or schema violations mark job `failed` and raise incident event.
- **Fairness:** Round-robin per `{tenant, policyId}`; emergency jobs (`priority=emergency`) jump queue but limited via circuit breaker.
- **Replay:** On demand, orchestrator rehydrates run via stored cursors and exports sealed bundle for audit/CI determinism checks.
- **Batch evaluation service (`/api/policy/eval/batch`):** Stateless evaluator powering Findings Ledger and replay/offline workflows. Requests contain canonical ledger events plus optional current projection; responses return status/severity/labels/rationale without mutating state. Policy Engine enforces per-tenant cost budgets, caches results by `(tenantId, policyVersion, eventHash, projectionHash)`, and falls back to inline evaluation when the remote service is disabled.
---

View File

@@ -112,7 +112,7 @@ Failures throw `SurfaceEnvironmentException` with error codes (`SURFACE_ENV_MISS
## 6. Integration Guidance
- **Scanner Worker**: call `services.AddSurfaceEnvironment()` in `Program.cs` before registering analyzers. Pass `hostContext.Configuration.GetSection("Surface")` for overrides.
- **Scanner WebService**: build environment during startup, then expose selected values via diagnostics (`/internal/surface` when diagnostics enabled).
- **Scanner WebService**: build environment during startup using `AddSurfaceEnvironment`, `AddSurfaceValidation`, `AddSurfaceFileCache`, and `AddSurfaceSecrets`; readiness checks execute the validator runner and scan/report APIs emit Surface CAS pointers derived from the resolved configuration.
- **Zastava Observer/Webhook**: use the same builder; ensure Helm charts set `ZASTAVA_` variables.
- **Scheduler Planner (future)**: treat Surface.Env as read-only input; do not mutate settings.

View File

@@ -66,6 +66,14 @@ Surface.FS exposes a gRPC/HTTP API consumed by .NET clients:
.NET client wraps these calls and handles retries using Polly policies.
### WebService integration (2025-11-05)
- `/api/v1/scans/{id}` and `/api/v1/reports` responses now include a `surface` block containing:
- `manifestUri` `cas://` pointer to the Surface manifest JSON.
- `manifestDigest` canonical SHA-256 over the manifest payload.
- `manifest.artifacts[]` deterministic list with `kind`, `uri`, `digest`, `mediaType`, `format`, and optional `view`. URIs reuse the `ArtifactObjectKeyBuilder` semantics (`cas://{bucket}/{rootPrefix}/images/...`).
- This allows UI/CLI consumers to fetch manifests or artefacts without additional Surface.FS round-trips.
## 4. Library Responsibilities
Surface.FS library for .NET hosts provides:

View File

@@ -7,7 +7,7 @@
- See [`../findings-ledger/schema.md`](../findings-ledger/schema.md) for the canonical SQL schema, hashing strategy, and fixtures underpinning these collections.
- **Collections / tables**
- `finding_records` canonical, policy-derived findings enriched with advisory, VEX, SBOM, runtime context. Includes `policyVersion`, `advisoryRawIds`, `vexRawIds`, `sbomComponentId`, and `explainBundleRef`.
- `finding_records` canonical, policy-derived findings enriched with advisory, VEX, SBOM, runtime context. Includes `policyVersion`, `advisoryRawIds`, `vexRawIds`, `sbomComponentId`, `policyRationale` (array of explain bundle URIs or remediation notes returned by `/api/policy/eval/batch`), and `explainBundleRef`.
- `finding_history` append-only state transitions (`new`, `triaged`, `accepted_risk`, `remediated`, `false_positive`, etc.) with timestamps, actor, and justification.
- `triage_actions` discrete operator actions (comment, assignment, remediation note, ticket link) with immutable provenance.
- `remediation_plans` structured remediation steps (affected assets, deadlines, recommended fixes, auto-generated from SRM/AI hints).
@@ -17,7 +17,7 @@
## 2) Triage workflow
1. **Ingest effective findings** from Policy Engine (stream `policy.finding.delta`). Each delta updates `finding_records`, generates history entries, and triggers notification rules.
1. **Ingest effective findings** from Policy Engine (stream `policy.finding.delta`) and on-demand batch evaluations. Projection workers call `/api/policy/eval/batch` with ledger event payloads to receive status/severity/label/rationale updates before writing `finding_records`. Each delta updates `finding_records`, generates history entries, and triggers notification rules.
2. **Prioritisation** uses contextual heuristics: runtime exposure, VEX status, policy severity, AI hints. Stored as `priorityScore` with provenance from Zastava/AI modules.
3. **Assignment & collaboration** Operators claim findings, add comments, attach evidence, and link tickets. Assignment uses Authority identities and RBAC.
4. **Remediation tracking** Link remediation plans, record progress, and integrate with Scheduler for follow-up scans once fixes deploy.

View File

@@ -3,3 +3,25 @@ findings:
ledger:
database:
connectionString: Host=postgres
authority:
issuer: https://authority.internal/
requireHttpsMetadata: true
metadataAddress: https://authority.internal/.well-known/openid-configuration
audiences:
- stellaops:vuln-ledger
requiredScopes:
- vuln:operate
- vuln:audit
merkle:
batchSize: 1000
windowDuration: 00:15:00
projection:
batchSize: 200
idleDelay: 00:00:05
policyEngine:
baseAddress: https://policy-engine.svc.cluster.local/api/
tenantHeaderName: X-Stella-Tenant
requestTimeout: 00:00:10
cache:
sizeLimit: 2048
entryLifetime: 00:30:00

View File

@@ -1,6 +1,74 @@
{
"report": {
"reportId": "report-abc",
"imageDigest": "sha256:feedface",
"generatedAt": "2025-10-19T12:34:56+00:00",
"verdict": "blocked",
"policy": {
"revisionId": "rev-42",
"digest": "digest-123"
},
"summary": {
"total": 1,
"blocked": 1,
"warned": 0,
"ignored": 0,
"quieted": 0
},
"verdicts": [
{
"findingId": "finding-1",
"reachability": "runtime",
"score": 47.5,
"sourceTrust": "NVD",
"status": "Blocked"
}
],
"issues": [],
"surface": {
"tenant": "tenant-alpha",
"generatedAt": "2025-10-19T12:34:56+00:00",
"manifestDigest": "sha256:4fee87d186291ddfbbcc2c56c8ed0e828520b8f52e1cde0e13bba082f10918d7",
"manifestUri": "cas://scanner-artifacts/scanner/surface/manifests/tenant-alpha/sha256/4f/ee/4fee87d186291ddfbbcc2c56c8ed0e828520b8f52e1cde0e13bba082f10918d7.json",
"manifest": {
"schema": "stellaops.surface.manifest@1",
"tenant": "tenant-alpha",
"imageDigest": "sha256:feedface",
"generatedAt": "2025-10-19T12:34:56+00:00",
"artifacts": [
{
"kind": "entry-trace",
"uri": "cas://scanner-artifacts/scanner/entry-trace/f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0/entry-trace.json",
"digest": "sha256:f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0",
"mediaType": "application/json",
"format": "json",
"sizeBytes": 4096
},
{
"kind": "sbom-inventory",
"uri": "cas://scanner-artifacts/scanner/images/feedface/sbom.cdx.json",
"digest": "sha256:1111111111111111111111111111111111111111111111111111111111111111",
"mediaType": "application/vnd.cyclonedx+json;version=1.6;view=inventory",
"format": "cdx-json",
"sizeBytes": 24576,
"view": "inventory"
},
{
"kind": "sbom-usage",
"uri": "cas://scanner-artifacts/scanner/images/feedface/sbom-usage.cdx.json",
"digest": "sha256:2222222222222222222222222222222222222222222222222222222222222222",
"mediaType": "application/vnd.cyclonedx+json;version=1.6;view=usage",
"format": "cdx-json",
"sizeBytes": 16384,
"view": "usage"
}
]
}
}
},
"dsse": {
"payloadType": "application/vnd.stellaops.report+json",
"payload": "eyJyZXBvcnRJZCI6InJlcG9ydC1hYmMiLCJpbWFnZURpZ2VzdCI6InNoYTI1NjpmZWVkZmFjZSIsImdlbmVyYXRlZEF0IjoiMjAyNS0xMC0xOVQxMjozNDo1NiswMDowMCIsInZlcmRpY3QiOiJibG9ja2VkIiwicG9saWN5Ijp7InJldmlzaW9uSWQiOiJyZXYtNDIiLCJkaWdlc3QiOiJkaWdlc3QtMTIzIn0sInN1bW1hcnkiOnsidG90YWwiOjEsImJsb2NrZWQiOjEsIndhcm5lZCI6MCwiaWdub3JlZCI6MCwicXVpZXRlZCI6MH0sInZlcmRpY3RzIjpbeyJmaW5kaW5nSWQiOiJmaW5kaW5nLTEiLCJzdGF0dXMiOiJCbG9ja2VkIiwic2NvcmUiOjQ3LjUsInNvdXJjZVRydXN0IjoiTlZEIiwicmVhY2hhYmlsaXR5IjoicnVudGltZSJ9XSwiaXNzdWVzIjpbXX0=",
"payload": "eyJyZXBvcnRJZCI6InJlcG9ydC1hYmMiLCJpbWFnZURpZ2VzdCI6InNoYTI1NjpmZWVkZmFjZSIsImdlbmVyYXRlZEF0IjoiMjAyNS0xMC0xOVQxMjozNDo1NiswMDowMCIsInZlcmRpY3QiOiJibG9ja2VkIiwicG9saWN5Ijp7InJldmlzaW9uSWQiOiJyZXYtNDIiLCJkaWdlc3QiOiJkaWdlc3QtMTIzIn0sInN1bW1hcnkiOnsidG90YWwiOjEsImJsb2NrZWQiOjEsIndhcm5lZCI6MCwiaWdub3JlZCI6MCwicXVpZXRlZCI6MH0sInZlcmRpY3RzIjpbeyJmaW5kaW5nSWQiOiJmaW5kaW5nLTEiLCJyZWFjaGFiaWxpdHkiOiJydW50aW1lIiwic2NvcmUiOjQ3LjUsInNvdXJjZVRydXN0IjoiTlZEIiwic3RhdHVzIjoiQmxvY2tlZCJ9XSwiaXNzdWVzIjpbXSwic3VyZmFjZSI6eyJ0ZW5hbnQiOiJ0ZW5hbnQtYWxwaGEiLCJnZW5lcmF0ZWRBdCI6IjIwMjUtMTAtMTlUMTI6MzQ6NTYrMDA6MDAiLCJtYW5pZmVzdERpZ2VzdCI6InNoYTI1Njo0ZmVlODdkMTg2MjkxZGRmYmJjYzJjNTZjOGVkMGU4Mjg1MjBiOGY1MmUxY2RlMGUxM2JiYTA4MmYxMDkxOGQ3IiwibWFuaWZlc3RVcmkiOiJjYXM6Ly9zY2FubmVyLWFydGlmYWN0cy9zY2FubmVyL3N1cmZhY2UvbWFuaWZlc3RzL3RlbmFudC1hbHBoYS9zaGEyNTYvNGYvZWUvNGZlZTg3ZDE4NjI5MWRkZmJiY2MyYzU2YzhlZDBlODI4NTIwYjhmNTJlMWNkZTBlMTNiYmEwODJmMTA5MThkNy5qc29uIiwibWFuaWZlc3QiOnsic2NoZW1hIjoic3RlbGxhb3BzLnN1cmZhY2UubWFuaWZlc3RAMSIsInRlbmFudCI6InRlbmFudC1hbHBoYSIsImltYWdlRGlnZXN0Ijoic2hhMjU2OmZlZWRmYWNlIiwiZ2VuZXJhdGVkQXQiOiIyMDI1LTEwLTE5VDEyOjM0OjU2KzAwOjAwIiwiYXJ0aWZhY3RzIjpbeyJraW5kIjoiZW50cnktdHJhY2UiLCJ1cmkiOiJjYXM6Ly9zY2FubmVyLWFydGlmYWN0cy9zY2FubmVyL2VudHJ5LXRyYWNlL2YwZjBmMGYwZjBmMGYwZjBmMGYwZjBmMGYwZjBmMGYwZjBmMGYwZjBmMGYwZjBmMGYwZjBmMGYwZjBmMGYwL2VudHJ5LXRyYWNlLmpzb24iLCJkaWdlc3QiOiJzaGEyNTY6ZjBmMGYwZjBmMGYwZjBmMGYwZjBmMGYwZjBmMGYwZjBmMGYwZjBmMGYwZjBmMGYwZjBmMGYwZjBmMGYwZjBmMCIsIm1lZGlhVHlwZSI6ImFwcGxpY2F0aW9uL2pzb24iLCJmb3JtYXQiOiJqc29uIiwic2l6ZUJ5dGVzIjo0MDk2fSx7ImtpbmQiOiJzYm9tLWludmVudG9yeSIsInVyaSI6ImNhczovL3NjYW5uZXItYXJ0aWZhY3RzL3NjYW5uZXIvaW1hZ2VzL2ZlZWRmYWNlL3Nib20uY2R4Lmpzb24iLCJkaWdlc3QiOiJzaGEyNTY6MTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMSIsIm1lZGlhVHlwZSI6ImFwcGxpY2F0aW9uL3ZuZC5jeWNsb25lZHgranNvbjt2ZXJzaW9uPTEuNjt2aWV3PWludmVudG9yeSIsImZvcm1hdCI6ImNkeC1qc29uIiwic2l6ZUJ5dGVzIjoyNDU3NiwidmlldyI6ImludmVudG9yeSJ9LHsia2luZCI6InNib20tdXNhZ2UiLCJ1cmkiOiJjYXM6Ly9zY2FubmVyLWFydGlmYWN0cy9zY2FubmVyL2ltYWdlcy9mZWVkZmFjZS9zYm9tLXVzYWdlLmNkeC5qc29uIiwiZGlnZXN0Ijoic2hhMjU2OjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIiLCJtZWRpYVR5cGUiOiJhcHBsaWNhdGlvbi92bmQuY3ljbG9uZWR4K2pzb247dmVyc2lvbj0xLjY7dmlldz11c2FnZSIsImZvcm1hdCI6ImNkeC1qc29uIiwic2l6ZUJ5dGVzIjoxNjM4NCwidmlldyI6InVzYWdlIn1dfX19",
"signatures": [
{
"keyId": "test-key",
@@ -9,3 +77,4 @@
}
]
}
}

View File

@@ -4,12 +4,12 @@
| AIAI-31-001 | DONE (2025-11-02) | Advisory AI Guild | CONCELIER-VULN-29-001, EXCITITOR-VULN-29-001 | Implement structured and vector retrievers for advisories/VEX with paragraph anchors and citation metadata. | Retrievers return deterministic chunks with source IDs/sections; unit tests cover CSAF/OSV/vendor formats. |
| AIAI-31-002 | DONE (2025-11-04) | Advisory AI Guild, SBOM Service Guild | SBOM-VULN-29-001 | Build SBOM context retriever (purl version timelines, dependency paths, env flags, blast radius estimator). | Retriever returns paths/metrics under SLA; tests cover ecosystems. |
| AIAI-31-003 | DONE (2025-11-04) | Advisory AI Guild | AIAI-31-001..002 | Implement deterministic toolset (version comparators, range checks, dependency analysis, policy lookup) exposed via orchestrator. | Tools validated with property tests; outputs cached; docs updated. |
| AIAI-31-004 | DOING | Advisory AI Guild | AIAI-31-001..003, AUTH-VULN-29-001 | Build orchestration pipeline for Summary/Conflict/Remediation tasks (prompt templates, tool calls, token budgets, caching). | Pipeline executes tasks deterministically; caches keyed by tuple+policy; integration tests cover tasks. |
| AIAI-31-004A | DOING (2025-11-04) | Advisory AI Guild, Platform Guild | AIAI-31-004, AIAI-31-002 | Wire `AdvisoryPipelineOrchestrator` into WebService/Worker, expose API/queue contracts, emit metrics, and stand up cache stub. | API returns plan metadata; worker executes queue message; metrics recorded; doc updated. |
| AIAI-31-004 | DONE (2025-11-04) | Advisory AI Guild | AIAI-31-001..003, AUTH-VULN-29-001 | Build orchestration pipeline for Summary/Conflict/Remediation tasks (prompt templates, tool calls, token budgets, caching). | Pipeline executes tasks deterministically; caches keyed by tuple+policy; integration tests cover tasks. |
| AIAI-31-004A | DONE (2025-11-04) | Advisory AI Guild, Platform Guild | AIAI-31-004, AIAI-31-002 | Wire `AdvisoryPipelineOrchestrator` into WebService/Worker, expose API/queue contracts, emit metrics, and stand up cache stub. | API returns plan metadata; worker executes queue message; metrics recorded; doc updated. |
| AIAI-31-004B | TODO | Advisory AI Guild, Security Guild | AIAI-31-004A, DOCS-AIAI-31-003, AUTH-AIAI-31-004 | Implement prompt assembler, guardrail plumbing, cache persistence, DSSE provenance; add golden outputs. | Deterministic outputs cached; guardrails enforced; tests cover prompt assembly + caching. |
| AIAI-31-004C | TODO | Advisory AI Guild, CLI Guild, Docs Guild | AIAI-31-004B, CLI-AIAI-31-003 | Deliver CLI `stella advise run <task>` command, renderers, documentation updates, and CLI golden tests. | CLI command produces deterministic output; docs published; smoke run recorded. |
| AIAI-31-005 | DOING (2025-11-03) | Advisory AI Guild, Security Guild | AIAI-31-004 | Implement guardrails (redaction, injection defense, output validation, citation enforcement) and fail-safe handling. | Guardrails block adversarial inputs; output validator enforces schemas; security tests pass. |
| AIAI-31-006 | DOING (2025-11-03) | Advisory AI Guild | AIAI-31-004..005 | Expose REST API endpoints (`/advisory/ai/*`) with RBAC, rate limits, OpenAPI schemas, and batching support. | Endpoints deployed with schema validation; rate limits enforced; integration tests cover error codes. |
| AIAI-31-005 | DONE (2025-11-04) | Advisory AI Guild, Security Guild | AIAI-31-004 | Implement guardrails (redaction, injection defense, output validation, citation enforcement) and fail-safe handling. | Guardrails block adversarial inputs; output validator enforces schemas; security tests pass. |
| AIAI-31-006 | DONE (2025-11-04) | Advisory AI Guild | AIAI-31-004..005 | Expose REST API endpoints (`/advisory/ai/*`) with RBAC, rate limits, OpenAPI schemas, and batching support. | Endpoints deployed with schema validation; rate limits enforced; integration tests cover error codes. |
| AIAI-31-007 | TODO | Advisory AI Guild, Observability Guild | AIAI-31-004..006 | Instrument metrics (`advisory_ai_latency`, `guardrail_blocks`, `validation_failures`, `citation_coverage`), logs, and traces; publish dashboards/alerts. | Telemetry live; dashboards approved; alerts configured. |
| AIAI-31-008 | TODO | Advisory AI Guild, DevOps Guild | AIAI-31-006..007 | Package inference on-prem container, remote inference toggle, Helm/Compose manifests, scaling guidance, offline kit instructions. | Deployment docs merged; smoke deploy executed; offline kit updated; feature flags documented. |
| AIAI-31-010 | DONE (2025-11-02) | Advisory AI Guild | CONCELIER-VULN-29-001, EXCITITOR-VULN-29-001 | Implement Concelier advisory raw document provider mapping CSAF/OSV payloads into structured chunks for retrieval. | Provider resolves content format, preserves metadata, and passes unit tests covering CSAF/OSV cases. |
@@ -23,5 +23,11 @@
> 2025-11-04: AIAI-31-003 completed toolset wired via DI/orchestrator, SBOM context client available, and unit coverage for compare/range/dependency analysis extended.
> 2025-11-02: AIAI-31-004 started orchestration pipeline work begin designing summary/conflict/remediation workflow (deterministic sequence + cache keys).
> 2025-11-04: AIAI-31-004 DONE orchestrator composes structured/vector/SBOM context with stable cache keys and metadata (env flags, blast radius, dependency metrics); unit coverage via `AdvisoryPipelineOrchestratorTests` keeps determinism enforced.
> 2025-11-02: AIAI-31-004 orchestration prerequisites documented in docs/modules/advisory-ai/orchestration-pipeline.md (task breakdown 004A/004B/004C).
> 2025-11-04: AIAI-31-004A DONE WebService `/v1/advisory-ai/pipeline/*` + batch endpoints enqueue plans with rate limiting & scope headers, Worker drains filesystem queue, metrics/logging added, docs updated. Tests: `dotnet test src/AdvisoryAI/__Tests/StellaOps.AdvisoryAI.Tests/StellaOps.AdvisoryAI.Tests.csproj --no-restore`.
> 2025-11-04: AIAI-31-005 DONE guardrail pipeline redacts secrets, enforces citation/injection policies, emits block counters, and tests (`AdvisoryGuardrailPipelineTests`) cover redaction + citation validation.
> 2025-11-04: AIAI-31-006 DONE REST endpoints enforce header scopes, apply token bucket rate limiting, sanitize prompts via guardrails, and queue execution with cached metadata. Tests executed via `dotnet test src/AdvisoryAI/__Tests/StellaOps.AdvisoryAI.Tests/StellaOps.AdvisoryAI.Tests.csproj --no-restore`.

View File

@@ -8,7 +8,7 @@
| PLG4-6.CAPABILITIES | BLOCKED (2025-10-12) | BE-Auth Plugin, Docs Guild | PLG1PLG3 | Finalise capability metadata exposure, config validation, and developer guide updates; remaining action is Docs polish/diagram export. | ✅ Capability metadata + validation merged; ✅ Plugin guide updated with final copy & diagrams; ✅ Release notes mention new toggles. <br>⛔ Blocked awaiting Authority rate-limiter stream (CORE8/SEC3) to resume so doc updates reflect final limiter behaviour. |
| PLG7.RFC | DONE (2025-11-03) | BE-Auth Plugin, Security Guild | PLG4 | Socialize LDAP plugin RFC (`docs/rfcs/authority-plugin-ldap.md`) and capture guild feedback. | ✅ Guild review sign-off recorded; ✅ Follow-up issues filed in module boards. |
| PLG7.IMPL-001 | DONE (2025-11-03) | BE-Auth Plugin | PLG7.RFC | Scaffold `StellaOps.Authority.Plugin.Ldap` + tests, bind configuration (client certificate, trust-store, insecure toggle) with validation and docs samples. | ✅ Project + test harness build; ✅ Configuration bound & validated; ✅ Sample config updated. |
| PLG7.IMPL-002 | DOING (2025-11-03) | BE-Auth Plugin, Security Guild | PLG7.IMPL-001 | Implement LDAP credential store with TLS/mutual TLS enforcement, deterministic retry/backoff, and structured logging/metrics. | ✅ Credential store passes integration tests (OpenLDAP + mtls); ✅ Metrics/logs emitted; ✅ Error mapping documented. |
| PLG7.IMPL-002 | DONE (2025-11-04) | BE-Auth Plugin, Security Guild | PLG7.IMPL-001 | Implement LDAP credential store with TLS/mutual TLS enforcement, deterministic retry/backoff, and structured logging/metrics. | ✅ Credential store passes integration tests (OpenLDAP + mtls); ✅ Metrics/logs emitted; ✅ Error mapping documented.<br>2025-11-04: DirectoryServices factory now enforces TLS/mTLS options, credential store retries use deterministic backoff with metrics, audit logging includes failure codes, and unit suite (`dotnet test src/Authority/StellaOps.Authority/StellaOps.Authority.Plugin.Ldap.Tests`) remains green. |
| PLG7.IMPL-003 | TODO | BE-Auth Plugin | PLG7.IMPL-001 | Deliver claims enricher with DN-to-role dictionary and regex mapping plus Mongo cache, including determinism + eviction tests. | ✅ Regex mapping deterministic; ✅ Cache TTL + invalidation tested; ✅ Claims doc updated. |
| PLG7.IMPL-004 | TODO | BE-Auth Plugin, DevOps Guild | PLG7.IMPL-002 | Implement client provisioning store with LDAP write toggles, Mongo audit mirror, bootstrap validation, and health reporting. | ✅ Audit mirror records persisted; ✅ Bootstrap validation logs capability summary; ✅ Health checks cover LDAP + audit mirror. |
| PLG7.IMPL-005 | TODO | BE-Auth Plugin, Docs Guild | PLG7.IMPL-001..004 | Update developer guide, samples, and release notes for LDAP plugin (mutual TLS, regex mapping, audit mirror) and ensure Offline Kit coverage. | ✅ Docs merged; ✅ Release notes drafted; ✅ Offline kit config templates updated. |

View File

@@ -1,4 +1,4 @@
# TASKS
| Task | Owner(s) | Depends on | Notes |
|---|---|---|---|
|FEEDCONN-SHARED-STATE-003 Source state seeding helper|Tools Guild, BE-Conn-MSRC|Tools|**DOING (2025-10-19)** Provide a reusable CLI/utility to seed `pendingDocuments`/`pendingMappings` for connectors (MSRC backfills require scripted CVRF + detail injection). Coordinate with MSRC team for expected JSON schema and handoff once prototype lands. Prereqs confirmed none (2025-10-19).|
|FEEDCONN-SHARED-STATE-003 Source state seeding helper|Tools Guild, BE-Conn-MSRC|Tools|**DONE (2025-11-04)** Shipped `src/Tools/SourceStateSeeder` CLI plus `SourceStateSeedProcessor` APIs for programmatic seeding, with Mongo fixtures and MSRC runbook updates. Tests: `dotnet test src/Concelier/__Tests/StellaOps.Concelier.Connector.Common.Tests/StellaOps.Concelier.Connector.Common.Tests.csproj --no-build` (requires `libcrypto.so.1.1` for Mongo2Go when running outside CI).|

View File

@@ -1,4 +1,4 @@
# TASKS
| Task | Owner(s) | Depends on | Notes |
|---|---|---|---|
|Fixture validation sweep|QA|None|**DOING (2025-10-19)** Prereqs confirmed none; continuing RHSA fixture regeneration and diff review alongside mapper provenance updates.<br>2025-10-29: Added `scripts/update-redhat-fixtures.sh` to regenerate golden snapshots with `UPDATE_GOLDENS=1`; run it before reviews to capture CSAF contract deltas.|
|Fixture validation sweep|QA|None|**DONE (2025-11-04)** Regenerated RHSA golden fixtures with `scripts/update-redhat-fixtures.sh` (exports `UPDATE_GOLDENS=1`) and revalidated connector snapshots via `dotnet test src/Concelier/__Tests/StellaOps.Concelier.Connector.Distro.RedHat.Tests/StellaOps.Concelier.Connector.Distro.RedHat.Tests.csproj --no-restore`.|

View File

@@ -1,7 +1,7 @@
# TASKS
| Task | Owner(s) | Depends on | Notes |
|---|---|---|---|
|Link-Not-Merge version provenance coordination|BE-Merge|CONCELIER-LNM-21-001|**DOING** Coordinate remaining connectors (`Acsc`, `Cccs`, `CertBund`, `CertCc`, `Cve`, `Ghsa`, `Ics.Cisa`, `Kisa`, `Ru.Bdu`, `Ru.Nkcki`, `Vndr.Apple`, `Vndr.Cisco`, `Vndr.Msrc`) so they emit `advisory_observations.affected.versions[]` entries with provenance tags and deterministic comparison keys. Track rollout status in `docs/dev/normalized-rule-recipes.md` (now updated for Link-Not-Merge) and retire the legacy merge counters as coverage transitions to linkset validation metrics.<br>2025-10-29: Added new guidance in the doc for recording observation version metadata and logging gaps via `LinksetVersionCoverage` warnings to replace prior `concelier.merge.normalized_rules*` alerts.|
|Link-Not-Merge version provenance coordination|BE-Merge|CONCELIER-LNM-21-001|**DONE (2025-11-04)** Coordinated connector rollout: updated `docs/dev/normalized-rule-recipes.md` with a per-connector status table + follow-up IDs, enabled `Normalized version rules missing` diagnostics in `AdvisoryPrecedenceMerger`, and confirmed Linkset validation metrics reflect remaining upstream gaps (ACSC/CCCS/CERTBUND/Cisco/RU-BDU awaiting structured ranges).|
|FEEDMERGE-COORD-02-901 Connector deadline check-ins|BE-Merge|FEEDMERGE-COORD-02-900|**TODO (due 2025-10-21)** Confirm Cccs/Cisco version-provenance updates land, capture `LinksetVersionCoverage` dashboard snapshots (expect zero missing-range warnings), and update coordination docs with the results.<br>2025-10-29: Observation metrics now surface `version_entries_total`/`missing_version_entries_total`; include screenshots for both when closing this task.|
|FEEDMERGE-COORD-02-902 ICS-CISA version comparison support|BE-Merge, Models|FEEDMERGE-COORD-02-900|**TODO (due 2025-10-23)** Review ICS-CISA sample advisories, validate reuse of existing comparison helpers, and pre-stage Models ticket template only if a new firmware comparator is required. Document the outcome and observation coverage logs in coordination docs + tracker files.<br>2025-10-29: `docs/dev/normalized-rule-recipes.md` (§2§3) now covers observation entries; attach decision summary + log sample when handing off to Models.|
|FEEDMERGE-COORD-02-903 KISA firmware scheme review|BE-Merge, Models|FEEDMERGE-COORD-02-900|**TODO (due 2025-10-24)** Pair with KISA team on proposed firmware comparison helper (`kisa.build` or variant), ensure observation mapper alignment, and open Models ticket only if a new comparator is required. Log the final helper signature and observation coverage metrics in coordination docs + tracker files.|

View File

@@ -120,7 +120,11 @@ builder.Services.AddSingleton<ILedgerEventRepository, PostgresLedgerEventReposit
builder.Services.AddSingleton<IMerkleAnchorScheduler, PostgresMerkleAnchorScheduler>();
builder.Services.AddSingleton<ILedgerEventStream, PostgresLedgerEventStream>();
builder.Services.AddSingleton<IFindingProjectionRepository, PostgresFindingProjectionRepository>();
builder.Services.AddSingleton<IPolicyEvaluationService, InlinePolicyEvaluationService>();
builder.Services.AddHttpClient("ledger-policy-engine");
builder.Services.AddSingleton<InlinePolicyEvaluationService>();
builder.Services.AddSingleton<PolicyEvaluationCache>();
builder.Services.AddSingleton<PolicyEngineEvaluationService>();
builder.Services.AddSingleton<IPolicyEvaluationService>(sp => sp.GetRequiredService<PolicyEngineEvaluationService>());
builder.Services.AddSingleton<ILedgerEventWriteService, LedgerEventWriteService>();
builder.Services.AddHostedService<LedgerMerkleAnchorWorker>();
builder.Services.AddHostedService<LedgerProjectionWorker>();

View File

@@ -1,3 +1,4 @@
using System.Text.Json;
using System.Text.Json.Nodes;
using Microsoft.Extensions.Logging;
using StellaOps.Findings.Ledger.Domain;

View File

@@ -0,0 +1,217 @@
using System.Net.Http;
using System.Net.Http.Json;
using System.Security.Cryptography;
using System.Text.Json;
using System.Text.Json.Nodes;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using StellaOps.Findings.Ledger.Domain;
using StellaOps.Findings.Ledger.Options;
namespace StellaOps.Findings.Ledger.Infrastructure.Policy;
internal sealed class PolicyEngineEvaluationService : IPolicyEvaluationService
{
private static readonly JsonSerializerOptions SerializerOptions = new(JsonSerializerDefaults.Web)
{
DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull
};
private readonly HttpClient? _httpClient;
private readonly InlinePolicyEvaluationService _fallback;
private readonly PolicyEvaluationCache _cache;
private readonly LedgerServiceOptions.PolicyEngineOptions _options;
private readonly ILogger<PolicyEngineEvaluationService> _logger;
public PolicyEngineEvaluationService(
IHttpClientFactory httpClientFactory,
InlinePolicyEvaluationService fallback,
PolicyEvaluationCache cache,
IOptions<LedgerServiceOptions> options,
ILogger<PolicyEngineEvaluationService> logger)
{
ArgumentNullException.ThrowIfNull(httpClientFactory);
_fallback = fallback ?? throw new ArgumentNullException(nameof(fallback));
_cache = cache ?? throw new ArgumentNullException(nameof(cache));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_options = (options ?? throw new ArgumentNullException(nameof(options))).Value.PolicyEngine;
if (_options.BaseAddress is not null)
{
var client = httpClientFactory.CreateClient("ledger-policy-engine");
client.BaseAddress = _options.BaseAddress;
client.Timeout = _options.RequestTimeout;
_httpClient = client;
}
}
public async Task<PolicyEvaluationResult> EvaluateAsync(
LedgerEventRecord record,
FindingProjection? existingProjection,
CancellationToken cancellationToken)
{
if (record is null)
{
throw new ArgumentNullException(nameof(record));
}
if (_httpClient is null)
{
return await _fallback.EvaluateAsync(record, existingProjection, cancellationToken).ConfigureAwait(false);
}
var cacheKey = CreateCacheKey(record, existingProjection);
if (_cache.TryGet(cacheKey, out var cachedResult))
{
return cachedResult;
}
try
{
var requestBody = CreateRequest(record, existingProjection);
using var request = new HttpRequestMessage(HttpMethod.Post, "policy/eval/batch")
{
Content = JsonContent.Create(requestBody, options: SerializerOptions)
};
request.Headers.TryAddWithoutValidation(_options.TenantHeaderName, record.TenantId);
using var response = await _httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false);
if (!response.IsSuccessStatusCode)
{
_logger.LogWarning(
"Policy engine evaluation request failed with status {StatusCode}. Falling back to inline evaluator.",
response.StatusCode);
return await _fallback.EvaluateAsync(record, existingProjection, cancellationToken).ConfigureAwait(false);
}
using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
var document = await JsonDocument.ParseAsync(stream, cancellationToken: cancellationToken).ConfigureAwait(false);
var result = ParseResponse(document.RootElement, record);
_cache.Set(cacheKey, result);
return result;
}
catch (OperationCanceledException)
{
throw;
}
catch (Exception ex)
{
_logger.LogError(ex, "Policy engine evaluation failed; falling back to inline evaluation.");
return await _fallback.EvaluateAsync(record, existingProjection, cancellationToken).ConfigureAwait(false);
}
}
private static PolicyEvaluationCacheKey CreateCacheKey(LedgerEventRecord record, FindingProjection? existingProjection)
{
using var sha = SHA256.Create();
var eventBytes = JsonSerializer.SerializeToUtf8Bytes(record.EventBody, SerializerOptions);
var hashBytes = sha.ComputeHash(eventBytes);
var projectionHash = existingProjection?.CycleHash;
return new PolicyEvaluationCacheKey(record.TenantId, record.PolicyVersion, record.EventId, projectionHash ?? Convert.ToHexString(hashBytes));
}
private static JsonObject CreateRequest(LedgerEventRecord record, FindingProjection? existingProjection)
{
var batchItem = new JsonObject
{
["findingId"] = record.FindingId,
["eventId"] = record.EventId.ToString(),
["event"] = record.EventBody.DeepClone()
};
if (existingProjection is not null)
{
batchItem["currentProjection"] = new JsonObject
{
["status"] = existingProjection.Status,
["severity"] = existingProjection.Severity,
["labels"] = existingProjection.Labels.DeepClone(),
["explainRef"] = existingProjection.ExplainRef,
["rationale"] = existingProjection.PolicyRationale.DeepClone()
};
}
var request = new JsonObject
{
["tenantId"] = record.TenantId,
["policyVersion"] = record.PolicyVersion,
["items"] = new JsonArray { batchItem }
};
return request;
}
private static PolicyEvaluationResult ParseResponse(JsonElement response, LedgerEventRecord record)
{
if (!response.TryGetProperty("items", out var itemsElement) || itemsElement.ValueKind != JsonValueKind.Array)
{
throw new InvalidOperationException("Policy engine response missing 'items' array.");
}
foreach (var item in itemsElement.EnumerateArray())
{
var findingId = item.GetPropertyOrDefault("findingId")?.GetString();
if (!string.Equals(findingId, record.FindingId, StringComparison.Ordinal))
{
continue;
}
var status = item.GetPropertyOrDefault("status")?.GetString();
decimal? severity = null;
var severityElement = item.GetPropertyOrDefault("severity");
if (severityElement.HasValue && severityElement.Value.ValueKind == JsonValueKind.Number && severityElement.Value.TryGetDecimal(out var decimalSeverity))
{
severity = decimalSeverity;
}
var labelsNode = new JsonObject();
var labelsElement = item.GetPropertyOrDefault("labels");
if (labelsElement.HasValue && labelsElement.Value.ValueKind == JsonValueKind.Object)
{
labelsNode = (JsonObject)labelsElement.Value.ToJsonNode()!;
}
var explainRef = item.GetPropertyOrDefault("explainRef")?.GetString();
JsonArray rationale;
var rationaleElement = item.GetPropertyOrDefault("rationale");
if (!rationaleElement.HasValue || rationaleElement.Value.ValueKind != JsonValueKind.Array)
{
rationale = new JsonArray();
if (!string.IsNullOrWhiteSpace(explainRef))
{
rationale.Add(explainRef);
}
}
else
{
rationale = (JsonArray)rationaleElement.Value.ToJsonNode()!;
}
return new PolicyEvaluationResult(status, severity, labelsNode, explainRef, rationale);
}
throw new InvalidOperationException("Policy engine response did not include evaluation for requested finding.");
}
}
internal static class JsonElementExtensions
{
public static JsonElement? GetPropertyOrDefault(this JsonElement element, string propertyName)
{
if (element.ValueKind != JsonValueKind.Object)
{
return null;
}
return element.TryGetProperty(propertyName, out var value) ? value : null;
}
public static JsonNode? ToJsonNode(this JsonElement element)
{
return JsonNode.Parse(element.GetRawText());
}
}

View File

@@ -0,0 +1,95 @@
using System.Text.Json.Nodes;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;
using StellaOps.Findings.Ledger.Domain;
using StellaOps.Findings.Ledger.Options;
namespace StellaOps.Findings.Ledger.Infrastructure.Policy;
internal sealed record PolicyEvaluationCacheKey(string TenantId, string PolicyVersion, Guid EventId, string? ProjectionHash);
internal sealed class PolicyEvaluationCache : IDisposable
{
private readonly IMemoryCache _cache;
private readonly ILogger<PolicyEvaluationCache> _logger;
private bool _disposed;
public PolicyEvaluationCache(
LedgerServiceOptions.PolicyEngineOptions options,
ILogger<PolicyEvaluationCache> logger)
{
ArgumentNullException.ThrowIfNull(options);
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_cache = new MemoryCache(new MemoryCacheOptions
{
SizeLimit = options.Cache.SizeLimit
});
EntryLifetime = options.Cache.EntryLifetime;
}
public TimeSpan EntryLifetime { get; }
public bool TryGet(PolicyEvaluationCacheKey key, out PolicyEvaluationResult result)
{
ArgumentNullException.ThrowIfNull(key);
if (_cache.TryGetValue(key, out PolicyEvaluationResult? cached) && cached is not null)
{
_logger.LogTrace("Policy evaluation cache hit for tenant {Tenant} finding {Finding} policy {Policy}", key.TenantId, key.EventId, key.PolicyVersion);
result = Clone(cached);
return true;
}
result = null!;
return false;
}
public void Set(PolicyEvaluationCacheKey key, PolicyEvaluationResult value)
{
ArgumentNullException.ThrowIfNull(key);
ArgumentNullException.ThrowIfNull(value);
var entryOptions = new MemoryCacheEntryOptions()
.SetSize(1)
.SetAbsoluteExpiration(EntryLifetime);
_cache.Set(key, Clone(value), entryOptions);
}
private static PolicyEvaluationResult Clone(PolicyEvaluationResult result)
{
var labelsClone = result.Labels is null ? new JsonObject() : (JsonObject)result.Labels.DeepClone();
var rationaleClone = result.Rationale is null ? new JsonArray() : CloneArray(result.Rationale);
return new PolicyEvaluationResult(
result.Status,
result.Severity,
labelsClone,
result.ExplainRef,
rationaleClone);
}
private static JsonArray CloneArray(JsonArray source)
{
var clone = new JsonArray();
foreach (var item in source)
{
clone.Add(item?.DeepClone());
}
return clone;
}
public void Dispose()
{
if (_disposed)
{
return;
}
_cache.Dispose();
_disposed = true;
}
}

View File

@@ -12,6 +12,8 @@ public sealed class LedgerServiceOptions
public ProjectionOptions Projection { get; init; } = new();
public PolicyEngineOptions PolicyEngine { get; init; } = new();
public void Validate()
{
if (string.IsNullOrWhiteSpace(Database.ConnectionString))
@@ -43,6 +45,8 @@ public sealed class LedgerServiceOptions
{
throw new InvalidOperationException("Projection idle delay must be greater than zero.");
}
PolicyEngine.Validate();
}
public sealed class DatabaseOptions
@@ -90,4 +94,53 @@ public sealed class LedgerServiceOptions
public TimeSpan IdleDelay { get; set; } = DefaultIdleDelay;
}
public sealed class PolicyEngineOptions
{
private const int DefaultCacheSizeLimit = 2048;
private static readonly TimeSpan DefaultCacheEntryLifetime = TimeSpan.FromMinutes(30);
private static readonly TimeSpan DefaultRequestTimeout = TimeSpan.FromSeconds(10);
public Uri? BaseAddress { get; set; }
public string TenantHeaderName { get; set; } = "X-Stella-Tenant";
public TimeSpan RequestTimeout { get; set; } = DefaultRequestTimeout;
public PolicyEngineCacheOptions Cache { get; init; } = new();
internal void Validate()
{
if (BaseAddress is not null && !BaseAddress.IsAbsoluteUri)
{
throw new InvalidOperationException("Policy engine base address must be an absolute URI.");
}
if (Cache.SizeLimit <= 0)
{
throw new InvalidOperationException("Policy engine cache size limit must be greater than zero.");
}
if (Cache.EntryLifetime <= TimeSpan.Zero)
{
throw new InvalidOperationException("Policy engine cache entry lifetime must be greater than zero.");
}
if (RequestTimeout <= TimeSpan.Zero)
{
throw new InvalidOperationException("Policy engine request timeout must be greater than zero.");
}
}
}
public sealed class PolicyEngineCacheOptions
{
private const int DefaultCacheSizeLimit = 2048;
private static readonly TimeSpan DefaultCacheEntryLifetime = TimeSpan.FromMinutes(30);
public int SizeLimit { get; set; } = DefaultCacheSizeLimit;
public TimeSpan EntryLifetime { get; set; } = DefaultCacheEntryLifetime;
}
}

View File

@@ -0,0 +1,3 @@
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("StellaOps.Findings.Ledger.Tests")]

View File

@@ -14,6 +14,8 @@
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.0-rc.2.25502.107" />
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="10.0.0-rc.2.25502.107" />
<PackageReference Include="Microsoft.Extensions.Options" Version="10.0.0-rc.2.25502.107" />
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="10.0.0-rc.2.25502.107" />
<PackageReference Include="Microsoft.Extensions.Http" Version="10.0.0-rc.2.25502.107" />
<PackageReference Include="Npgsql" Version="7.0.7" />
</ItemGroup>

View File

@@ -4,7 +4,7 @@
| LEDGER-29-001 | DONE (2025-11-03) | Findings Ledger Guild | AUTH-POLICY-27-001 | Design ledger & projection schemas (tables/indexes), canonical JSON format, hashing strategy, and migrations. Publish schema doc + fixtures.<br>2025-11-03: Initial PostgreSQL migration added with partitions/enums, fixtures seeded with canonical hashes, schema doc aligned. | Schemas committed; migrations generated; hashing documented; fixtures seeded for CI. |
| LEDGER-29-002 | DONE (2025-11-03) | Findings Ledger Guild | LEDGER-29-001 | Implement ledger write API (`POST /vuln/ledger/events`) with validation, idempotency, hash chaining, and Merkle root computation job.<br>2025-11-03: Minimal web service scaffolded with canonical hashing, in-memory repository, Merkle scheduler stub, request/response contracts, and unit tests for hashing + conflict flows. | Events persisted with chained hashes; Merkle job emits anchors; unit/integration tests cover happy/pathological cases. |
| LEDGER-29-003 | DONE (2025-11-03) | Findings Ledger Guild, Scheduler Guild | LEDGER-29-001 | Build projector worker that derives `findings_projection` rows from ledger events + policy determinations; ensure idempotent replay keyed by `(tenant,finding_id,policy_version)`. | Postgres-backed projector worker and reducers landed with replay checkpointing, fixtures, and tests. |
| LEDGER-29-004 | DOING (2025-11-03) | Findings Ledger Guild, Policy Guild | LEDGER-29-003, POLICY-ENGINE-27-001 | Integrate Policy Engine batch evaluation (baseline + simulate) with projector; cache rationale references.<br>2025-11-04: Projection reducer now consumes policy evaluation output with rationale arrays; Postgres migration + fixtures/tests updated, awaiting Policy Engine API wiring for batch fetch. | Projector fetches determinations efficiently; rationale stored for UI; regression tests cover version switches. |
| LEDGER-29-004 | DONE (2025-11-04) | Findings Ledger Guild, Policy Guild | LEDGER-29-003, POLICY-ENGINE-27-001 | Integrate Policy Engine batch evaluation (baseline + simulate) with projector; cache rationale references.<br>2025-11-04: Remote evaluation service wired via typed HttpClient, cache, and fallback inline evaluator; `/api/policy/eval/batch` documented; `policy_rationale` persisted with deterministic hashing; ledger tests `dotnet test src/Findings/__Tests/StellaOps.Findings.Ledger.Tests/StellaOps.Findings.Ledger.Tests.csproj --no-restore` green. | Projector fetches determinations efficiently; rationale stored for UI; regression tests cover version switches. |
| LEDGER-29-005 | TODO | Findings Ledger Guild | LEDGER-29-003 | Implement workflow mutation handlers (assign, comment, accept-risk, target-fix, verify-fix, reopen) producing ledger events with validation and attachments metadata. | API endpoints enforce business rules; attachments metadata stored; tests cover state machine transitions. |
| LEDGER-29-006 | TODO | Findings Ledger Guild, Security Guild | LEDGER-29-002 | Integrate attachment encryption (KMS envelope), signed URL issuance, CSRF protection hooks for Console. | Attachments encrypted and accessible via signed URLs; security tests verify expiry + scope. |
| LEDGER-29-007 | TODO | Findings Ledger Guild, Observability Guild | LEDGER-29-002..005 | Instrument metrics (`ledger_write_latency`, `projection_lag_seconds`, `ledger_events_total`), structured logs, and Merkle anchoring alerts; publish dashboards. | Metrics/traces emitted; dashboards live; alert thresholds documented. |

View File

@@ -65,6 +65,7 @@ public sealed class LedgerProjectionReducerTests
new JsonObject(),
Guid.NewGuid(),
null,
new JsonArray(),
DateTimeOffset.UtcNow,
string.Empty);
var existingHash = ProjectionHashing.ComputeCycleHash(existing);
@@ -112,6 +113,7 @@ public sealed class LedgerProjectionReducerTests
labels,
Guid.NewGuid(),
null,
new JsonArray(),
DateTimeOffset.UtcNow,
string.Empty);
existing = existing with { CycleHash = ProjectionHashing.ComputeCycleHash(existing) };

View File

@@ -0,0 +1,186 @@
using System.Net;
using System.Net.Http;
using System.Text.Json.Nodes;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using StellaOps.Findings.Ledger.Domain;
using StellaOps.Findings.Ledger.Infrastructure.Policy;
using StellaOps.Findings.Ledger.Options;
using Xunit;
namespace StellaOps.Findings.Ledger.Tests;
public sealed class PolicyEngineEvaluationServiceTests
{
private const string TenantId = "tenant-1";
private static readonly DateTimeOffset Now = DateTimeOffset.UtcNow;
[Fact]
public async Task EvaluateAsync_UsesPolicyEngineAndCachesResult()
{
var handler = new StubHttpHandler(_ => new HttpResponseMessage(HttpStatusCode.OK)
{
Content = new StringContent(
"""
{
"items": [
{
"findingId": "finding-1",
"status": "affected",
"severity": 7.5,
"labels": { "exposure": "runtime" },
"explainRef": "policy://explain/123",
"rationale": ["policy://explain/123"]
}
]
}
""",
System.Text.Encoding.UTF8,
"application/json")
});
var factory = new TestHttpClientFactory(handler);
var options = CreateOptions(new Uri("https://policy.example/"));
using var cache = new PolicyEvaluationCache(options.PolicyEngine, NullLogger<PolicyEvaluationCache>.Instance);
var inline = new InlinePolicyEvaluationService(NullLogger<InlinePolicyEvaluationService>.Instance);
var service = new PolicyEngineEvaluationService(factory, inline, cache, Microsoft.Extensions.Options.Options.Create(options), NullLogger<PolicyEngineEvaluationService>.Instance);
var record = CreateRecord();
var first = await service.EvaluateAsync(record, existingProjection: null, CancellationToken.None);
var second = await service.EvaluateAsync(record, existingProjection: null, CancellationToken.None);
Assert.Equal("affected", first.Status);
Assert.Equal(7.5m, first.Severity);
Assert.Equal("policy://explain/123", first.ExplainRef);
Assert.Equal("runtime", first.Labels?["exposure"]?.GetValue<string>());
Assert.Equal(1, handler.CallCount); // cached second call
Assert.Equal("affected", second.Status);
Assert.Equal("policy://explain/123", second.Rationale[0]?.GetValue<string>());
}
[Fact]
public async Task EvaluateAsync_FallsBackToInlineWhenRequestFails()
{
var handler = new StubHttpHandler(_ => new HttpResponseMessage(HttpStatusCode.InternalServerError));
var factory = new TestHttpClientFactory(handler);
var options = CreateOptions(new Uri("https://policy.example/"));
using var cache = new PolicyEvaluationCache(options.PolicyEngine, NullLogger<PolicyEvaluationCache>.Instance);
var inline = new InlinePolicyEvaluationService(NullLogger<InlinePolicyEvaluationService>.Instance);
var service = new PolicyEngineEvaluationService(factory, inline, cache, Microsoft.Extensions.Options.Options.Create(options), NullLogger<PolicyEngineEvaluationService>.Instance);
var record = CreateRecord(payloadStatus: "investigating", payloadSeverity: 4.2m);
var result = await service.EvaluateAsync(record, existingProjection: null, CancellationToken.None);
Assert.Equal("investigating", result.Status);
Assert.Equal(4.2m, result.Severity);
Assert.Equal(1, handler.CallCount);
}
[Fact]
public async Task EvaluateAsync_UsesInlineWhenNoBaseAddressConfigured()
{
var handler = new StubHttpHandler(_ => throw new InvalidOperationException("Handler should not be invoked."));
var factory = new TestHttpClientFactory(handler);
var options = CreateOptions(baseAddress: null);
using var cache = new PolicyEvaluationCache(options.PolicyEngine, NullLogger<PolicyEvaluationCache>.Instance);
var inline = new InlinePolicyEvaluationService(NullLogger<InlinePolicyEvaluationService>.Instance);
var service = new PolicyEngineEvaluationService(factory, inline, cache, Microsoft.Extensions.Options.Options.Create(options), NullLogger<PolicyEngineEvaluationService>.Instance);
var record = CreateRecord(payloadStatus: "accepted_risk", payloadSeverity: 1.0m);
var result = await service.EvaluateAsync(record, existingProjection: null, CancellationToken.None);
Assert.Equal("accepted_risk", result.Status);
Assert.Equal(1.0m, result.Severity);
Assert.Equal(0, handler.CallCount);
}
private static LedgerServiceOptions CreateOptions(Uri? baseAddress)
{
return new LedgerServiceOptions
{
PolicyEngine = new LedgerServiceOptions.PolicyEngineOptions
{
BaseAddress = baseAddress,
Cache = new LedgerServiceOptions.PolicyEngineCacheOptions
{
SizeLimit = 16,
EntryLifetime = TimeSpan.FromMinutes(5)
}
}
};
}
private static LedgerEventRecord CreateRecord(string payloadStatus = "affected", decimal? payloadSeverity = 5.0m)
{
var payload = new JsonObject
{
["status"] = payloadStatus,
["severity"] = payloadSeverity,
["labels"] = new JsonObject { ["source"] = "policy" }
};
var envelope = new JsonObject
{
["event"] = new JsonObject
{
["payload"] = payload
}
};
return new LedgerEventRecord(
TenantId,
Guid.NewGuid(),
1,
Guid.NewGuid(),
LedgerEventConstants.EventFindingStatusChanged,
"policy/v1",
"finding-1",
"artifact-1",
null,
"actor",
"service",
Now,
Now,
envelope,
"hash",
"prev",
"leaf",
envelope.ToJsonString());
}
private sealed class StubHttpHandler : HttpMessageHandler
{
private readonly Func<HttpRequestMessage, HttpResponseMessage> _handler;
public StubHttpHandler(Func<HttpRequestMessage, HttpResponseMessage> handler)
{
_handler = handler ?? throw new ArgumentNullException(nameof(handler));
}
public int CallCount { get; private set; }
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
CallCount++;
return Task.FromResult(_handler(request));
}
}
private sealed class TestHttpClientFactory : IHttpClientFactory
{
private readonly HttpMessageHandler _handler;
public TestHttpClientFactory(HttpMessageHandler handler)
{
_handler = handler;
}
public HttpClient CreateClient(string name)
{
return new HttpClient(_handler, disposeHandler: false);
}
}
}

View File

@@ -58,6 +58,10 @@ public sealed record ReportDocumentDto
[JsonPropertyName("issues")]
[JsonPropertyOrder(7)]
public IReadOnlyList<PolicyPreviewIssueDto> Issues { get; init; } = Array.Empty<PolicyPreviewIssueDto>();
[JsonPropertyName("surface")]
[JsonPropertyOrder(8)]
public SurfacePointersDto? Surface { get; init; }
}
public sealed record ReportPolicyDto

View File

@@ -6,7 +6,8 @@ public sealed record ScanStatusResponse(
ScanStatusTarget Image,
DateTimeOffset CreatedAt,
DateTimeOffset UpdatedAt,
string? FailureReason);
string? FailureReason,
SurfacePointersDto? Surface);
public sealed record ScanStatusTarget(
string? Reference,

View File

@@ -0,0 +1,87 @@
using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace StellaOps.Scanner.WebService.Contracts;
public sealed record SurfacePointersDto
{
[JsonPropertyName("tenant")]
[JsonPropertyOrder(0)]
public string Tenant { get; init; } = string.Empty;
[JsonPropertyName("generatedAt")]
[JsonPropertyOrder(1)]
public DateTimeOffset GeneratedAt { get; init; }
= DateTimeOffset.UtcNow;
[JsonPropertyName("manifestDigest")]
[JsonPropertyOrder(2)]
public string ManifestDigest { get; init; } = string.Empty;
[JsonPropertyName("manifestUri")]
[JsonPropertyOrder(3)]
public string? ManifestUri { get; init; }
= null;
[JsonPropertyName("manifest")]
[JsonPropertyOrder(4)]
public SurfaceManifestDocument Manifest { get; init; } = new();
}
public sealed record SurfaceManifestDocument
{
[JsonPropertyName("schema")]
[JsonPropertyOrder(0)]
public string Schema { get; init; } = "stellaops.surface.manifest@1";
[JsonPropertyName("tenant")]
[JsonPropertyOrder(1)]
public string Tenant { get; init; } = string.Empty;
[JsonPropertyName("imageDigest")]
[JsonPropertyOrder(2)]
public string ImageDigest { get; init; } = string.Empty;
[JsonPropertyName("generatedAt")]
[JsonPropertyOrder(3)]
public DateTimeOffset GeneratedAt { get; init; } = DateTimeOffset.UtcNow;
[JsonPropertyName("artifacts")]
[JsonPropertyOrder(4)]
public IReadOnlyList<SurfaceManifestArtifact> Artifacts { get; init; } = Array.Empty<SurfaceManifestArtifact>();
}
public sealed record SurfaceManifestArtifact
{
[JsonPropertyName("kind")]
[JsonPropertyOrder(0)]
public string Kind { get; init; } = string.Empty;
[JsonPropertyName("uri")]
[JsonPropertyOrder(1)]
public string Uri { get; init; } = string.Empty;
[JsonPropertyName("digest")]
[JsonPropertyOrder(2)]
public string Digest { get; init; } = string.Empty;
[JsonPropertyName("mediaType")]
[JsonPropertyOrder(3)]
public string MediaType { get; init; } = string.Empty;
[JsonPropertyName("format")]
[JsonPropertyOrder(4)]
public string Format { get; init; } = string.Empty;
[JsonPropertyName("sizeBytes")]
[JsonPropertyOrder(5)]
public long SizeBytes { get; init; }
= 0;
[JsonPropertyName("view")]
[JsonPropertyOrder(6)]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? View { get; init; }
= null;
}

View File

@@ -1,12 +1,16 @@
using System.Collections.Generic;
using System.Diagnostics;
using System.Text;
using System.Text.Json;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using StellaOps.Scanner.WebService.Diagnostics;
using StellaOps.Scanner.WebService.Options;
using StellaOps.Scanner.Surface.Env;
using StellaOps.Scanner.Surface.Validation;
namespace StellaOps.Scanner.WebService.Endpoints;
@@ -58,14 +62,55 @@ internal static class HealthEndpoints
private static async Task<IResult> HandleReady(
ServiceStatus status,
ISurfaceValidatorRunner validatorRunner,
ISurfaceEnvironment surfaceEnvironment,
ILoggerFactory loggerFactory,
HttpContext context,
CancellationToken cancellationToken)
{
ApplyNoCache(context.Response);
await Task.CompletedTask;
ArgumentNullException.ThrowIfNull(loggerFactory);
status.RecordReadyCheck(success: true, latency: TimeSpan.Zero, error: null);
var logger = loggerFactory.CreateLogger("Scanner.WebService.Health");
var stopwatch = Stopwatch.StartNew();
var success = true;
string? error = null;
try
{
var validationContext = SurfaceValidationContext.Create(
context.RequestServices,
"Scanner.WebService.ReadyCheck",
surfaceEnvironment.Settings,
properties: new Dictionary<string, object?>
{
["path"] = context.Request.Path.ToString()
});
await validatorRunner.EnsureAsync(validationContext, cancellationToken).ConfigureAwait(false);
}
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
{
throw;
}
catch (SurfaceValidationException ex)
{
success = false;
error = ex.Message;
}
catch (Exception ex)
{
success = false;
error = ex.Message;
logger.LogError(ex, "Surface validation failed during ready check.");
}
finally
{
stopwatch.Stop();
}
status.RecordReadyCheck(success, stopwatch.Elapsed, error);
var snapshot = status.CreateSnapshot();
var ready = snapshot.Ready;
@@ -75,7 +120,8 @@ internal static class HealthEndpoints
LatencyMs: ready.Latency?.TotalMilliseconds,
Error: ready.Error);
return Json(document, StatusCodes.Status200OK);
var statusCode = success ? StatusCodes.Status200OK : StatusCodes.Status503ServiceUnavailable;
return Json(document, statusCode);
}
private static void ApplyNoCache(HttpResponse response)

View File

@@ -6,6 +6,7 @@ using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.Logging;
using StellaOps.Policy;
using StellaOps.Scanner.WebService.Constants;
using StellaOps.Scanner.WebService.Contracts;
@@ -55,6 +56,8 @@ internal static class ReportEndpoints
IReportSigner signer,
TimeProvider timeProvider,
IReportEventDispatcher eventDispatcher,
ISurfacePointerService surfacePointerService,
ILoggerFactory loggerFactory,
HttpContext context,
CancellationToken cancellationToken)
{
@@ -63,6 +66,9 @@ internal static class ReportEndpoints
ArgumentNullException.ThrowIfNull(signer);
ArgumentNullException.ThrowIfNull(timeProvider);
ArgumentNullException.ThrowIfNull(eventDispatcher);
ArgumentNullException.ThrowIfNull(surfacePointerService);
ArgumentNullException.ThrowIfNull(loggerFactory);
var logger = loggerFactory.CreateLogger("Scanner.WebService.Reports");
if (string.IsNullOrWhiteSpace(request.ImageDigest))
{
@@ -131,6 +137,25 @@ internal static class ReportEndpoints
var verdict = ComputeVerdict(projectedVerdicts);
var reportId = CreateReportId(request.ImageDigest!, preview.PolicyDigest);
var generatedAt = timeProvider.GetUtcNow();
SurfacePointersDto? surfacePointers = null;
try
{
surfacePointers = await surfacePointerService
.TryBuildAsync(request.ImageDigest!, context.RequestAborted)
.ConfigureAwait(false);
}
catch (OperationCanceledException) when (context.RequestAborted.IsCancellationRequested)
{
throw;
}
catch (Exception ex)
{
if (!context.RequestAborted.IsCancellationRequested)
{
logger.LogDebug(ex, "Failed to build surface pointers for digest {Digest}.", request.ImageDigest);
}
}
var document = new ReportDocumentDto
{
@@ -145,7 +170,8 @@ internal static class ReportEndpoints
},
Summary = summary,
Verdicts = projectedVerdicts,
Issues = issuesDto
Issues = issuesDto,
Surface = surfacePointers
};
var payloadBytes = JsonSerializer.SerializeToUtf8Bytes(document, SerializerOptions);

View File

@@ -140,10 +140,12 @@ internal static class ScanEndpoints
private static async Task<IResult> HandleStatusAsync(
string scanId,
IScanCoordinator coordinator,
ISurfacePointerService surfacePointerService,
HttpContext context,
CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(coordinator);
ArgumentNullException.ThrowIfNull(surfacePointerService);
if (!ScanId.TryParse(scanId, out var parsed))
{
@@ -166,13 +168,30 @@ internal static class ScanEndpoints
detail: "Requested scan could not be located.");
}
SurfacePointersDto? surfacePointers = null;
var digest = snapshot.Target.Digest;
if (!string.IsNullOrWhiteSpace(digest))
{
try
{
surfacePointers = await surfacePointerService
.TryBuildAsync(digest!, context.RequestAborted)
.ConfigureAwait(false);
}
catch (OperationCanceledException) when (context.RequestAborted.IsCancellationRequested)
{
throw;
}
}
var response = new ScanStatusResponse(
ScanId: snapshot.ScanId.Value,
Status: snapshot.Status.ToString(),
Image: new ScanStatusTarget(snapshot.Target.Reference, snapshot.Target.Digest),
CreatedAt: snapshot.CreatedAt,
UpdatedAt: snapshot.UpdatedAt,
FailureReason: snapshot.FailureReason);
FailureReason: snapshot.FailureReason,
Surface: surfacePointers);
return Json(response, StatusCodes.Status200OK);
}

View File

@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using StellaOps.Scanner.Storage;
namespace StellaOps.Scanner.WebService.Options;
@@ -129,6 +130,8 @@ public sealed class ScannerWebServiceOptions
public int ObjectLockRetentionDays { get; set; } = 30;
public string RootPrefix { get; set; } = ScannerStorageDefaults.DefaultRootPrefix;
public string? ApiKey { get; set; }
public string ApiKeyHeader { get; set; } = string.Empty;

View File

@@ -19,6 +19,10 @@ using StellaOps.Cryptography.DependencyInjection;
using StellaOps.Cryptography.Plugin.BouncyCastle;
using StellaOps.Policy;
using StellaOps.Scanner.Cache;
using StellaOps.Scanner.Surface.Env;
using StellaOps.Scanner.Surface.FS;
using StellaOps.Scanner.Surface.Secrets;
using StellaOps.Scanner.Surface.Validation;
using StellaOps.Scanner.WebService.Diagnostics;
using StellaOps.Scanner.WebService.Endpoints;
using StellaOps.Scanner.WebService.Extensions;
@@ -85,6 +89,15 @@ builder.Services.AddSingleton<PolicyPreviewService>();
builder.Services.AddStellaOpsCrypto();
builder.Services.AddBouncyCastleEd25519Provider();
builder.Services.AddSingleton<IReportSigner, ReportSigner>();
builder.Services.AddSurfaceEnvironment(options =>
{
options.ComponentName = "Scanner.WebService";
options.AddPrefix("SCANNER");
});
builder.Services.AddSurfaceValidation();
builder.Services.AddSurfaceFileCache();
builder.Services.AddSurfaceSecrets();
builder.Services.AddSingleton<ISurfacePointerService, SurfacePointerService>();
builder.Services.AddSingleton<IRedisConnectionFactory, RedisConnectionFactory>();
if (bootstrapOptions.Events is { Enabled: true } eventsOptions
&& string.Equals(eventsOptions.Driver, "redis", StringComparison.OrdinalIgnoreCase))
@@ -119,6 +132,11 @@ builder.Services.AddScannerStorage(storageOptions =>
storageOptions.ObjectStore.BucketName = bootstrapOptions.ArtifactStore.Bucket;
}
if (!string.IsNullOrWhiteSpace(bootstrapOptions.ArtifactStore.RootPrefix))
{
storageOptions.ObjectStore.RootPrefix = bootstrapOptions.ArtifactStore.RootPrefix;
}
var artifactDriver = bootstrapOptions.ArtifactStore.Driver?.Trim() ?? string.Empty;
if (string.Equals(artifactDriver, ScannerStorageDefaults.ObjectStoreProviders.RustFs, StringComparison.OrdinalIgnoreCase))
{

View File

@@ -207,10 +207,7 @@ internal static class OrchestratorEventSerializer
return;
}
info.PolymorphismOptions ??= new JsonPolymorphismOptions
{
UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.Fail
};
info.PolymorphismOptions ??= new JsonPolymorphismOptions();
AddDerivedType(info.PolymorphismOptions, typeof(ReportReadyEventPayload));
AddDerivedType(info.PolymorphismOptions, typeof(ScanCompletedEventPayload));

View File

@@ -0,0 +1,279 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Security.Cryptography;
using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using StellaOps.Scanner.Storage;
using StellaOps.Scanner.Storage.Catalog;
using StellaOps.Scanner.Storage.ObjectStore;
using StellaOps.Scanner.Storage.Repositories;
using StellaOps.Scanner.Surface.Env;
using StellaOps.Scanner.WebService.Contracts;
using StellaOps.Scanner.WebService.Options;
namespace StellaOps.Scanner.WebService.Services;
internal interface ISurfacePointerService
{
Task<SurfacePointersDto?> TryBuildAsync(string imageDigest, CancellationToken cancellationToken);
}
internal sealed class SurfacePointerService : ISurfacePointerService
{
private static readonly JsonSerializerOptions ManifestSerializerOptions = new(JsonSerializerDefaults.Web)
{
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
WriteIndented = false
};
private readonly LinkRepository _linkRepository;
private readonly ArtifactRepository _artifactRepository;
private readonly IOptionsMonitor<ScannerWebServiceOptions> _optionsMonitor;
private readonly ISurfaceEnvironment _surfaceEnvironment;
private readonly TimeProvider _timeProvider;
private readonly ILogger<SurfacePointerService> _logger;
public SurfacePointerService(
LinkRepository linkRepository,
ArtifactRepository artifactRepository,
IOptionsMonitor<ScannerWebServiceOptions> optionsMonitor,
ISurfaceEnvironment surfaceEnvironment,
TimeProvider timeProvider,
ILogger<SurfacePointerService> logger)
{
_linkRepository = linkRepository ?? throw new ArgumentNullException(nameof(linkRepository));
_artifactRepository = artifactRepository ?? throw new ArgumentNullException(nameof(artifactRepository));
_optionsMonitor = optionsMonitor ?? throw new ArgumentNullException(nameof(optionsMonitor));
_surfaceEnvironment = surfaceEnvironment ?? throw new ArgumentNullException(nameof(surfaceEnvironment));
_timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
public async Task<SurfacePointersDto?> TryBuildAsync(string imageDigest, CancellationToken cancellationToken)
{
if (string.IsNullOrWhiteSpace(imageDigest))
{
return null;
}
var normalizedDigest = imageDigest.Trim();
List<LinkDocument> links;
try
{
links = await _linkRepository.ListBySourceAsync(LinkSourceType.Image, normalizedDigest, cancellationToken)
.ConfigureAwait(false);
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to load link documents for digest {Digest}.", normalizedDigest);
return null;
}
if (links.Count == 0)
{
return null;
}
var options = _optionsMonitor.CurrentValue ?? new ScannerWebServiceOptions();
var artifactStore = options.ArtifactStore ?? new ScannerWebServiceOptions.ArtifactStoreOptions();
var bucket = ResolveBucket(artifactStore);
var rootPrefix = artifactStore.RootPrefix ?? ScannerStorageDefaults.DefaultRootPrefix;
var tenant = _surfaceEnvironment.Settings.Tenant;
var generatedAt = _timeProvider.GetUtcNow();
var artifacts = ImmutableArray.CreateBuilder<SurfaceManifestArtifact>();
foreach (var link in links)
{
cancellationToken.ThrowIfCancellationRequested();
ArtifactDocument? artifactDocument;
try
{
artifactDocument = await _artifactRepository.GetAsync(link.ArtifactId, cancellationToken).ConfigureAwait(false);
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to load artifact document {ArtifactId}.", link.ArtifactId);
continue;
}
if (artifactDocument is null)
{
continue;
}
var objectKey = ArtifactObjectKeyBuilder.Build(
artifactDocument.Type,
artifactDocument.Format,
artifactDocument.BytesSha256,
rootPrefix);
var uri = BuildCasUri(bucket, objectKey);
var (kind, view) = MapKindAndView(artifactDocument);
var format = MapFormat(artifactDocument.Format);
var artifact = new SurfaceManifestArtifact
{
Kind = kind,
Uri = uri,
Digest = artifactDocument.BytesSha256,
MediaType = artifactDocument.MediaType,
Format = format,
SizeBytes = artifactDocument.SizeBytes,
View = view
};
artifacts.Add(artifact);
}
if (artifacts.Count == 0)
{
return null;
}
var orderedArtifacts = artifacts.OrderBy(a => a.Kind, StringComparer.Ordinal)
.ThenBy(a => a.Format, StringComparer.Ordinal)
.ThenBy(a => a.Digest, StringComparer.Ordinal)
.ToImmutableArray();
var manifest = new SurfaceManifestDocument
{
Tenant = tenant,
ImageDigest = normalizedDigest,
GeneratedAt = generatedAt,
Artifacts = orderedArtifacts
};
var manifestJson = JsonSerializer.SerializeToUtf8Bytes(manifest, ManifestSerializerOptions);
var manifestDigest = ComputeDigest(manifestJson);
var manifestUri = BuildManifestUri(bucket, rootPrefix, tenant, manifestDigest);
return new SurfacePointersDto
{
Tenant = tenant,
GeneratedAt = generatedAt,
ManifestDigest = manifestDigest,
ManifestUri = manifestUri,
Manifest = manifest with { GeneratedAt = generatedAt }
};
}
private static string ResolveBucket(ScannerWebServiceOptions.ArtifactStoreOptions artifactStore)
{
if (!string.IsNullOrWhiteSpace(artifactStore.Bucket))
{
return artifactStore.Bucket.Trim();
}
return ScannerStorageDefaults.DefaultBucketName;
}
private static string MapFormat(ArtifactDocumentFormat format)
=> format switch
{
ArtifactDocumentFormat.CycloneDxJson => "cdx-json",
ArtifactDocumentFormat.CycloneDxProtobuf => "cdx-protobuf",
ArtifactDocumentFormat.SpdxJson => "spdx-json",
ArtifactDocumentFormat.BomIndex => "bom-index",
ArtifactDocumentFormat.DsseJson => "dsse-json",
_ => format.ToString().ToLowerInvariant()
};
private static (string Kind, string? View) MapKindAndView(ArtifactDocument document)
{
if (document.Type == ArtifactDocumentType.ImageBom)
{
var view = ResolveView(document.MediaType);
var kind = string.Equals(view, "usage", StringComparison.OrdinalIgnoreCase)
? "sbom-usage"
: "sbom-inventory";
return (kind, view);
}
return document.Type switch
{
ArtifactDocumentType.LayerBom => ("layer-sbom", null),
ArtifactDocumentType.Diff => ("diff", null),
ArtifactDocumentType.Attestation => ("attestation", null),
ArtifactDocumentType.Index => ("bom-index", null),
_ => (document.Type.ToString().ToLowerInvariant(), null)
};
}
private static string? ResolveView(string mediaType)
{
if (string.IsNullOrWhiteSpace(mediaType))
{
return null;
}
if (mediaType.Contains("view=usage", StringComparison.OrdinalIgnoreCase))
{
return "usage";
}
if (mediaType.Contains("view=inventory", StringComparison.OrdinalIgnoreCase))
{
return "inventory";
}
return null;
}
private static string BuildCasUri(string bucket, string key)
{
var normalizedKey = string.IsNullOrWhiteSpace(key) ? string.Empty : key.Trim().TrimStart('/');
return $"cas://{bucket}/{normalizedKey}";
}
private static string BuildManifestUri(string bucket, string rootPrefix, string tenant, string manifestDigest)
{
var (algorithm, digestValue) = SplitDigest(manifestDigest);
var prefix = string.IsNullOrWhiteSpace(rootPrefix)
? "surface/manifests"
: $"{TrimTrailingSlash(rootPrefix)}/surface/manifests";
var head = digestValue.Length >= 4
? $"{digestValue[..2]}/{digestValue[2..4]}"
: digestValue;
var key = $"{prefix}/{tenant}/{algorithm}/{head}/{digestValue}.json";
return $"cas://{bucket}/{key}";
}
private static (string Algorithm, string Digest) SplitDigest(string digest)
{
if (string.IsNullOrWhiteSpace(digest))
{
return ("sha256", digest ?? string.Empty);
}
var parts = digest.Split(':', 2, StringSplitOptions.TrimEntries);
if (parts.Length == 2)
{
return (parts[0], parts[1]);
}
return ("sha256", digest);
}
private static string TrimTrailingSlash(string value)
=> string.IsNullOrWhiteSpace(value)
? string.Empty
: value.Trim().TrimEnd('/');
private static string ComputeDigest(ReadOnlySpan<byte> payload)
{
Span<byte> hash = stackalloc byte[32];
if (!SHA256.TryHashData(payload, hash, out _))
{
using var sha = SHA256.Create();
hash = sha.ComputeHash(payload.ToArray());
}
return $"sha256:{Convert.ToHexString(hash).ToLowerInvariant()}";
}
}

View File

@@ -29,6 +29,10 @@
<ProjectReference Include="../../Notify/__Libraries/StellaOps.Notify.Models/StellaOps.Notify.Models.csproj" />
<ProjectReference Include="../__Libraries/StellaOps.Scanner.Cache/StellaOps.Scanner.Cache.csproj" />
<ProjectReference Include="../__Libraries/StellaOps.Scanner.Storage/StellaOps.Scanner.Storage.csproj" />
<ProjectReference Include="../__Libraries/StellaOps.Scanner.Surface.Env/StellaOps.Scanner.Surface.Env.csproj" />
<ProjectReference Include="../__Libraries/StellaOps.Scanner.Surface.Validation/StellaOps.Scanner.Surface.Validation.csproj" />
<ProjectReference Include="../__Libraries/StellaOps.Scanner.Surface.FS/StellaOps.Scanner.Surface.FS.csproj" />
<ProjectReference Include="../__Libraries/StellaOps.Scanner.Surface.Secrets/StellaOps.Scanner.Surface.Secrets.csproj" />
<ProjectReference Include="../../Zastava/__Libraries/StellaOps.Zastava.Core/StellaOps.Zastava.Core.csproj" />
</ItemGroup>
</Project>

View File

@@ -3,7 +3,7 @@
| ID | Status | Owner(s) | Depends on | Description | Exit Criteria |
|----|--------|----------|------------|-------------|---------------|
| SCAN-REPLAY-186-001 | TODO | Scanner WebService Guild | REPLAY-CORE-185-001 | Implement scan `record` mode producing replay manifests/bundles, capture policy/feed/tool hashes, and update `docs/modules/scanner/architecture.md` referencing `docs/replay/DETERMINISTIC_REPLAY.md` Section 6. | API/worker integration tests cover record mode; docs merged; replay artifacts stored per spec. |
| SCANNER-SURFACE-02 | DOING (2025-11-02) | Scanner WebService Guild | SURFACE-FS-02 | Publish Surface.FS pointers (CAS URIs, manifests) via scan/report APIs and update attestation metadata.<br>2025-11-02: Scan/report API responses now include preview CAS URIs; attestation metadata draft published. | OpenAPI updated; clients regenerated; integration tests validate pointer presence and tenancy. |
| SCANNER-SURFACE-02 | DONE (2025-11-05) | Scanner WebService Guild | SURFACE-FS-02 | Publish Surface.FS pointers (CAS URIs, manifests) via scan/report APIs and update attestation metadata.<br>2025-11-05: Surface pointers projected through scan/report endpoints, orchestrator samples + DSSE fixtures refreshed with manifest block, readiness tests updated to use validator stub. | OpenAPI updated; clients regenerated; integration tests validate pointer presence and tenancy. |
| SCANNER-ENV-02 | DOING (2025-11-02) | Scanner WebService Guild, Ops Guild | SURFACE-ENV-02 | Wire Surface.Env helpers into WebService hosting (cache roots, feature flags) and document configuration.<br>2025-11-02: Cache root resolution switched to helper; feature flag bindings updated; Helm/Compose updates pending review. | Service uses helper; env table documented; helm/compose templates updated. |
| SCANNER-SECRETS-02 | DOING (2025-11-02) | Scanner WebService Guild, Security Guild | SURFACE-SECRETS-02 | Replace ad-hoc secret wiring with Surface.Secrets for report/export operations (registry and CAS tokens).<br>2025-11-02: Export/report flows now depend on Surface.Secrets stub; integration tests in progress. | Secrets fetched through shared provider; unit/integration tests cover rotation + failure cases. |
| SCANNER-EVENTS-16-301 | BLOCKED (2025-10-26) | Scanner WebService Guild | ORCH-SVC-38-101, NOTIFY-SVC-38-001 | Emit orchestrator-compatible envelopes (`scanner.event.*`) and update integration tests to verify Notifier ingestion (no Redis queue coupling). | Tests assert envelope schema + orchestrator publish; Notifier consumer harness passes; docs updated with new event contract. Blocked by .NET 10 preview OpenAPI/Auth dependency drift preventing `dotnet test` completion. |

View File

@@ -0,0 +1,75 @@
using System;
using StellaOps.Scanner.Storage.Catalog;
namespace StellaOps.Scanner.Storage.ObjectStore;
/// <summary>
/// Builds deterministic object keys for scanner artefacts stored in the backing object store.
/// </summary>
public static class ArtifactObjectKeyBuilder
{
/// <summary>
/// Builds an object key for the provided artefact metadata.
/// </summary>
/// <param name="type">Artefact type.</param>
/// <param name="format">Artefact format.</param>
/// <param name="digest">Content digest (with or without algorithm prefix).</param>
/// <param name="rootPrefix">Optional root prefix to prepend (defaults to <c>scanner</c>).</param>
/// <returns>Deterministic storage key.</returns>
public static string Build(
ArtifactDocumentType type,
ArtifactDocumentFormat format,
string digest,
string? rootPrefix = null)
{
ArgumentException.ThrowIfNullOrWhiteSpace(digest);
var normalizedDigest = NormalizeDigest(digest);
var digestValue = ExtractDigest(normalizedDigest);
var prefix = type switch
{
ArtifactDocumentType.LayerBom => ScannerStorageDefaults.ObjectPrefixes.Layers,
ArtifactDocumentType.ImageBom => ScannerStorageDefaults.ObjectPrefixes.Images,
ArtifactDocumentType.Index => ScannerStorageDefaults.ObjectPrefixes.Indexes,
ArtifactDocumentType.Attestation => ScannerStorageDefaults.ObjectPrefixes.Attestations,
ArtifactDocumentType.Diff => "diffs",
_ => ScannerStorageDefaults.ObjectPrefixes.Images,
};
var extension = format switch
{
ArtifactDocumentFormat.CycloneDxJson => "sbom.cdx.json",
ArtifactDocumentFormat.CycloneDxProtobuf => "sbom.cdx.pb",
ArtifactDocumentFormat.SpdxJson => "sbom.spdx.json",
ArtifactDocumentFormat.BomIndex => "bom-index.bin",
ArtifactDocumentFormat.DsseJson => "artifact.dsse.json",
_ => "artifact.bin",
};
var key = $"{prefix}/{digestValue}/{extension}";
if (string.IsNullOrWhiteSpace(rootPrefix))
{
return key;
}
return $"{TrimTrailingSlash(rootPrefix)}/{key}";
}
private static string NormalizeDigest(string digest)
=> digest.Contains(':', StringComparison.Ordinal)
? digest.Trim()
: $"sha256:{digest.Trim()}";
private static string ExtractDigest(string normalizedDigest)
{
var parts = normalizedDigest.Split(':', 2, StringSplitOptions.TrimEntries);
return parts.Length == 2 ? parts[1] : normalizedDigest;
}
private static string TrimTrailingSlash(string value)
=> string.IsNullOrWhiteSpace(value)
? string.Empty
: value.Trim().TrimEnd('/');
}

View File

@@ -51,7 +51,11 @@ public sealed class ArtifactStorageService
{
var normalizedDigest = $"sha256:{digestHex}";
var artifactId = CatalogIdFactory.CreateArtifactId(type, normalizedDigest);
var key = BuildObjectKey(type, format, normalizedDigest);
var key = ArtifactObjectKeyBuilder.Build(
type,
format,
normalizedDigest,
_options.ObjectStore.RootPrefix);
var descriptor = new ArtifactObjectDescriptor(
_options.ObjectStore.BucketName,
key,
@@ -137,45 +141,4 @@ public sealed class ArtifactStorageService
return (bufferStream, total, digestHex);
}
private string BuildObjectKey(ArtifactDocumentType type, ArtifactDocumentFormat format, string digest)
{
var normalizedDigest = digest.Split(':', 2, StringSplitOptions.TrimEntries)[^1];
var prefix = type switch
{
ArtifactDocumentType.LayerBom => ScannerStorageDefaults.ObjectPrefixes.Layers,
ArtifactDocumentType.ImageBom => ScannerStorageDefaults.ObjectPrefixes.Images,
ArtifactDocumentType.Diff => "diffs",
ArtifactDocumentType.Index => ScannerStorageDefaults.ObjectPrefixes.Indexes,
ArtifactDocumentType.Attestation => ScannerStorageDefaults.ObjectPrefixes.Attestations,
_ => ScannerStorageDefaults.ObjectPrefixes.Images,
};
var extension = format switch
{
ArtifactDocumentFormat.CycloneDxJson => "sbom.cdx.json",
ArtifactDocumentFormat.CycloneDxProtobuf => "sbom.cdx.pb",
ArtifactDocumentFormat.SpdxJson => "sbom.spdx.json",
ArtifactDocumentFormat.BomIndex => "bom-index.bin",
ArtifactDocumentFormat.DsseJson => "artifact.dsse.json",
_ => "artifact.bin",
};
var rootPrefix = _options.ObjectStore.RootPrefix;
if (string.IsNullOrWhiteSpace(rootPrefix))
{
return $"{prefix}/{normalizedDigest}/{extension}";
}
return $"{TrimTrailingSlash(rootPrefix)}/{prefix}/{normalizedDigest}/{extension}";
}
private static string TrimTrailingSlash(string prefix)
{
if (string.IsNullOrWhiteSpace(prefix))
{
return string.Empty;
}
return prefix.TrimEnd('/');
}
}

View File

@@ -1,10 +1,13 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.AspNetCore.TestHost;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Mongo2Go;
using StellaOps.Scanner.Surface.Validation;
namespace StellaOps.Scanner.WebService.Tests;
@@ -60,6 +63,9 @@ internal sealed class ScannerApplicationFactory : WebApplicationFactory<Program>
Environment.SetEnvironmentVariable("SCANNER__ARTIFACTSTORE__ENDPOINT", configuration["scanner:artifactStore:endpoint"]);
Environment.SetEnvironmentVariable("SCANNER__ARTIFACTSTORE__ACCESSKEY", configuration["scanner:artifactStore:accessKey"]);
Environment.SetEnvironmentVariable("SCANNER__ARTIFACTSTORE__SECRETKEY", configuration["scanner:artifactStore:secretKey"]);
Environment.SetEnvironmentVariable("SCANNER_SURFACE_FS_ENDPOINT", "https://surface.local");
Environment.SetEnvironmentVariable("SCANNER_SURFACE_FS_BUCKET", configuration["scanner:artifactStore:bucket"]);
Environment.SetEnvironmentVariable("SCANNER_SURFACE_PREFETCH_ENABLED", "false");
if (configuration.TryGetValue("scanner:events:enabled", out var eventsEnabled))
{
Environment.SetEnvironmentVariable("SCANNER__EVENTS__ENABLED", eventsEnabled);
@@ -103,6 +109,8 @@ internal sealed class ScannerApplicationFactory : WebApplicationFactory<Program>
builder.ConfigureTestServices(services =>
{
configureServices?.Invoke(services);
services.RemoveAll<ISurfaceValidatorRunner>();
services.AddSingleton<ISurfaceValidatorRunner, TestSurfaceValidatorRunner>();
});
}
@@ -165,4 +173,17 @@ internal sealed class ScannerApplicationFactory : WebApplicationFactory<Program>
return null;
}
private sealed class TestSurfaceValidatorRunner : ISurfaceValidatorRunner
{
public ValueTask<SurfaceValidationResult> RunAllAsync(
SurfaceValidationContext context,
CancellationToken cancellationToken = default)
=> ValueTask.FromResult(SurfaceValidationResult.Success());
public ValueTask EnsureAsync(
SurfaceValidationContext context,
CancellationToken cancellationToken = default)
=> ValueTask.CompletedTask;
}
}

View File

@@ -83,6 +83,93 @@ public sealed class ScansEndpointsTests
Assert.False(secondPayload.Created);
}
[Fact]
public async Task ScanStatusIncludesSurfacePointersWhenArtifactsExist()
{
const string digest = "sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
var digestValue = digest.Split(':', 2)[1];
using var factory = new ScannerApplicationFactory();
using (var scope = factory.Services.CreateScope())
{
var artifactRepository = scope.ServiceProvider.GetRequiredService<ArtifactRepository>();
var linkRepository = scope.ServiceProvider.GetRequiredService<LinkRepository>();
var artifactId = CatalogIdFactory.CreateArtifactId(ArtifactDocumentType.ImageBom, digest);
var artifact = new ArtifactDocument
{
Id = artifactId,
Type = ArtifactDocumentType.ImageBom,
Format = ArtifactDocumentFormat.CycloneDxJson,
MediaType = "application/vnd.cyclonedx+json; version=1.6; view=inventory",
BytesSha256 = digest,
SizeBytes = 2048,
Immutable = true,
RefCount = 1,
TtlClass = "default",
CreatedAtUtc = DateTime.UtcNow,
UpdatedAtUtc = DateTime.UtcNow
};
await artifactRepository.UpsertAsync(artifact, CancellationToken.None).ConfigureAwait(false);
var link = new LinkDocument
{
Id = CatalogIdFactory.CreateLinkId(LinkSourceType.Image, digest, artifactId),
FromType = LinkSourceType.Image,
FromDigest = digest,
ArtifactId = artifactId,
CreatedAtUtc = DateTime.UtcNow
};
await linkRepository.UpsertAsync(link, CancellationToken.None).ConfigureAwait(false);
}
using var client = factory.CreateClient();
var submitRequest = new ScanSubmitRequest
{
Image = new ScanImageDescriptor
{
Digest = digest
}
};
var submitResponse = await client.PostAsJsonAsync("/api/v1/scans", submitRequest);
submitResponse.EnsureSuccessStatusCode();
var submission = await submitResponse.Content.ReadFromJsonAsync<ScanSubmitResponse>();
Assert.NotNull(submission);
var statusResponse = await client.GetAsync($"/api/v1/scans/{submission!.ScanId}");
statusResponse.EnsureSuccessStatusCode();
var status = await statusResponse.Content.ReadFromJsonAsync<ScanStatusResponse>();
Assert.NotNull(status);
Assert.NotNull(status!.Surface);
var surface = status.Surface!;
Assert.Equal("default", surface.Tenant);
Assert.False(string.IsNullOrWhiteSpace(surface.ManifestDigest));
Assert.NotNull(surface.ManifestUri);
Assert.Contains("cas://scanner-artifacts/", surface.ManifestUri, StringComparison.Ordinal);
var manifest = surface.Manifest;
Assert.Equal(digest, manifest.ImageDigest);
Assert.Equal(surface.Tenant, manifest.Tenant);
Assert.NotEqual(default, manifest.GeneratedAt);
var manifestArtifact = Assert.Single(manifest.Artifacts);
Assert.Equal("sbom-inventory", manifestArtifact.Kind);
Assert.Equal("cdx-json", manifestArtifact.Format);
Assert.Equal(digest, manifestArtifact.Digest);
Assert.Equal("application/vnd.cyclonedx+json; version=1.6; view=inventory", manifestArtifact.MediaType);
Assert.Equal("inventory", manifestArtifact.View);
var expectedUri = $"cas://scanner-artifacts/scanner/images/{digestValue}/sbom.cdx.json";
Assert.Equal(expectedUri, manifestArtifact.Uri);
}
[Fact]
public async Task SubmitScanValidatesImageDescriptor()
{
@@ -462,7 +549,7 @@ public sealed class ScansEndpointsTests
var storedResult = new EntryTraceResult(scanId, "sha256:test", generatedAt, graph, ndjson);
using var factory = new ScannerApplicationFactory(
configuration: null,
configureConfiguration: null,
services =>
{
services.AddSingleton<IEntryTraceResultStore>(new StubEntryTraceResultStore(storedResult));
@@ -485,7 +572,7 @@ public sealed class ScansEndpointsTests
public async Task GetEntryTraceReturnsNotFoundWhenMissing()
{
using var factory = new ScannerApplicationFactory(
configuration: null,
configureConfiguration: null,
services =>
{
services.AddSingleton<IEntryTraceResultStore>(new StubEntryTraceResultStore(null));

View File

@@ -1,12 +0,0 @@
## Policy simulations
`/api/v1/scheduler/policies/simulations` orchestrates Policy Engine runs in `simulate` mode without mutating persisted findings.
- **Create** — `POST /api/v1/scheduler/policies/simulations` (scope `policy:simulate`) enqueues a simulation for `policyId`/`policyVersion`, respecting optional `metadata` and structured `inputs` (`sbomSet`, `advisoryCursor`, `vexCursor`, `captureExplain`). Returns `201 Created` with `simulation.runId` and status `queued`.
- **List/Get** — `GET /api/v1/scheduler/policies/simulations` and `/.../{simulationId}` expose `PolicyRunStatus` documents filtered to `mode=simulate`, including attempt counts, stats, and cancellation markers.
- **Cancel** — `POST /.../{simulationId}/cancel` records `cancellationRequested=true` (optional reason, timestamp) and immediately reflects the updated status; workers honour the flag on the next lease cycle.
- **Retry** — `POST /.../{simulationId}/retry` clones a terminal simulation (cancelled/failed/succeeded) into a fresh job preserving inputs/metadata. Non-terminal runs yield `409 Conflict`.
- **Stream** — `GET /.../{simulationId}/stream` emits SSE events (`initial`, `status`, `queueLag`, `heartbeat`, `completed`) with the latest `PolicyRunStatus`, enabling Console to render shard progress and cancellation state in real time.
Simulation APIs share the same deterministic pagination/metadata contracts as policy runs and surface queue depth snapshots via the existing scheduler queue metrics.