feat: Implement Policy Engine Evaluation Service and Cache with unit tests
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
Temp commit to debug
This commit is contained in:
@@ -323,17 +323,40 @@ Accept: application/json
|
|||||||
**Response 200**:
|
**Response 200**:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"scanId": "2f6c17f9b3f548e2a28b9c412f4d63f8",
|
"scanId": "2f6c17f9b3f548e2a28b9c412f4d63f8",
|
||||||
"status": "Pending",
|
"status": "Pending",
|
||||||
"image": {
|
"image": {
|
||||||
"reference": "registry.example.com/acme/app:1.2.3",
|
"reference": "registry.example.com/acme/app:1.2.3",
|
||||||
"digest": null
|
"digest": "sha256:cafecafecafecafecafecafecafecafecafecafecafecafecafecafecafecafe"
|
||||||
},
|
},
|
||||||
"createdAt": "2025-10-18T20:15:12.482Z",
|
"createdAt": "2025-10-18T20:15:12.482Z",
|
||||||
"updatedAt": "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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Statuses: `Pending`, `Running`, `Succeeded`, `Failed`, `Cancelled`.
|
Statuses: `Pending`, `Running`, `Succeeded`, `Failed`, `Cancelled`.
|
||||||
@@ -445,8 +468,40 @@ Request body mirrors policy preview inputs (image digest plus findings). The ser
|
|||||||
"reachability": "runtime"
|
"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": {
|
"dsse": {
|
||||||
"payloadType": "application/vnd.stellaops.report+json",
|
"payloadType": "application/vnd.stellaops.report+json",
|
||||||
"payload": "eyJyZXBvcnQiOnsicmVwb3J0SWQiOiJyZXBvcnQtOWY4Y2RlMjFhYWI1NDMyMSJ9fQ==",
|
"payload": "eyJyZXBvcnQiOnsicmVwb3J0SWQiOiJyZXBvcnQtOWY4Y2RlMjFhYWI1NDMyMSJ9fQ==",
|
||||||
@@ -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.
|
- `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.
|
- 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.
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
### 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.completed` – emitted with `runId`, `policyId`, `mode`, `stats`, `determinismHash`.
|
||||||
- `policy.run.failed` – includes error code, retry count, guidance.
|
- `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`).
|
- [ ] **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.
|
- [ ] **Schemas current:** JSON examples align with Scheduler Models (`SCHED-MODELS-20-001`) and Policy Engine DTOs; update when contracts change.
|
||||||
|
|||||||
@@ -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.
|
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.
|
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)
|
## 2. Code snippet: SemVer connector (CCCS/Cisco/ICS-CISA)
|
||||||
|
|
||||||
```csharp
|
```csharp
|
||||||
|
|||||||
@@ -1,101 +1,141 @@
|
|||||||
{
|
{
|
||||||
"eventId": "6d2d1b77-f3c3-4f70-8a9d-6f2d0c8801ab",
|
"eventId": "6d2d1b77-f3c3-4f70-8a9d-6f2d0c8801ab",
|
||||||
"kind": "scanner.event.report.ready",
|
"kind": "scanner.event.report.ready",
|
||||||
"version": 1,
|
"version": 1,
|
||||||
"tenant": "tenant-alpha",
|
"tenant": "tenant-alpha",
|
||||||
"occurredAt": "2025-10-19T12:34:56Z",
|
"occurredAt": "2025-10-19T12:34:56+00:00",
|
||||||
"recordedAt": "2025-10-19T12:34:57Z",
|
"recordedAt": "2025-10-19T12:34:57+00:00",
|
||||||
"source": "scanner.webservice",
|
"source": "scanner.webservice",
|
||||||
"idempotencyKey": "scanner.event.report.ready:tenant-alpha:report-abc",
|
"idempotencyKey": "scanner.event.report.ready:tenant-alpha:report-abc",
|
||||||
"correlationId": "report-abc",
|
"correlationId": "report-abc",
|
||||||
"traceId": "0af7651916cd43dd8448eb211c80319c",
|
"traceId": "0af7651916cd43dd8448eb211c80319c",
|
||||||
"spanId": "b7ad6b7169203331",
|
"spanId": "b7ad6b7169203331",
|
||||||
"scope": {
|
"scope": {
|
||||||
"namespace": "acme/edge",
|
"namespace": "acme/edge",
|
||||||
"repo": "api",
|
"repo": "api",
|
||||||
"digest": "sha256:feedface"
|
"digest": "sha256:feedface"
|
||||||
},
|
},
|
||||||
"attributes": {
|
"payload": {
|
||||||
"reportId": "report-abc",
|
"reportId": "report-abc",
|
||||||
"policyRevisionId": "rev-42",
|
"scanId": "report-abc",
|
||||||
"policyDigest": "digest-123",
|
"imageDigest": "sha256:feedface",
|
||||||
"verdict": "blocked"
|
"generatedAt": "2025-10-19T12:34:56+00:00",
|
||||||
},
|
"verdict": "fail",
|
||||||
"payload": {
|
"summary": {
|
||||||
"reportId": "report-abc",
|
"total": 1,
|
||||||
"scanId": "report-abc",
|
"blocked": 1,
|
||||||
"imageDigest": "sha256:feedface",
|
"warned": 0,
|
||||||
"generatedAt": "2025-10-19T12:34:56Z",
|
"ignored": 0,
|
||||||
"verdict": "fail",
|
"quieted": 0
|
||||||
"summary": {
|
|
||||||
"total": 1,
|
|
||||||
"blocked": 1,
|
|
||||||
"warned": 0,
|
|
||||||
"ignored": 0,
|
|
||||||
"quieted": 0
|
|
||||||
},
|
|
||||||
"delta": {
|
|
||||||
"newCritical": 1,
|
|
||||||
"kev": [
|
|
||||||
"CVE-2024-9999"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"quietedFindingCount": 0,
|
|
||||||
"policy": {
|
|
||||||
"digest": "digest-123",
|
|
||||||
"revisionId": "rev-42"
|
|
||||||
},
|
|
||||||
"links": {
|
|
||||||
"report": {
|
|
||||||
"ui": "https://scanner.example/ui/reports/report-abc",
|
|
||||||
"api": "https://scanner.example/api/v1/reports/report-abc"
|
|
||||||
},
|
},
|
||||||
|
"delta": {
|
||||||
|
"newCritical": 1,
|
||||||
|
"kev": [
|
||||||
|
"CVE-2024-9999"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"quietedFindingCount": 0,
|
||||||
"policy": {
|
"policy": {
|
||||||
"ui": "https://scanner.example/ui/policy/revisions/rev-42",
|
"revisionId": "rev-42",
|
||||||
"api": "https://scanner.example/api/v1/policy/revisions/rev-42"
|
"digest": "digest-123"
|
||||||
},
|
},
|
||||||
"attestation": {
|
"links": {
|
||||||
"ui": "https://scanner.example/ui/attestations/report-abc",
|
"report": {
|
||||||
"api": "https://scanner.example/api/v1/reports/report-abc/attestation"
|
"ui": "https://scanner.example/ui/reports/report-abc",
|
||||||
|
"api": "https://scanner.example/api/v1/reports/report-abc"
|
||||||
|
},
|
||||||
|
"policy": {
|
||||||
|
"ui": "https://scanner.example/ui/policy/revisions/rev-42",
|
||||||
|
"api": "https://scanner.example/api/v1/policy/revisions/rev-42"
|
||||||
|
},
|
||||||
|
"attestation": {
|
||||||
|
"ui": "https://scanner.example/ui/attestations/report-abc",
|
||||||
|
"api": "https://scanner.example/api/v1/reports/report-abc/attestation"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"dsse": {
|
||||||
|
"payloadType": "application/vnd.stellaops.report+json",
|
||||||
|
"payload": "eyJyZXBvcnRJZCI6InJlcG9ydC1hYmMiLCJpbWFnZURpZ2VzdCI6InNoYTI1NjpmZWVkZmFjZSIsImdlbmVyYXRlZEF0IjoiMjAyNS0xMC0xOVQxMjozNDo1NiswMDowMCIsInZlcmRpY3QiOiJibG9ja2VkIiwicG9saWN5Ijp7InJldmlzaW9uSWQiOiJyZXYtNDIiLCJkaWdlc3QiOiJkaWdlc3QtMTIzIn0sInN1bW1hcnkiOnsidG90YWwiOjEsImJsb2NrZWQiOjEsIndhcm5lZCI6MCwiaWdub3JlZCI6MCwicXVpZXRlZCI6MH0sInZlcmRpY3RzIjpbeyJmaW5kaW5nSWQiOiJmaW5kaW5nLTEiLCJyZWFjaGFiaWxpdHkiOiJydW50aW1lIiwic2NvcmUiOjQ3LjUsInNvdXJjZVRydXN0IjoiTlZEIiwic3RhdHVzIjoiQmxvY2tlZCJ9XSwiaXNzdWVzIjpbXSwic3VyZmFjZSI6eyJ0ZW5hbnQiOiJ0ZW5hbnQtYWxwaGEiLCJnZW5lcmF0ZWRBdCI6IjIwMjUtMTAtMTlUMTI6MzQ6NTYrMDA6MDAiLCJtYW5pZmVzdERpZ2VzdCI6InNoYTI1Njo0ZmVlODdkMTg2MjkxZGRmYmJjYzJjNTZjOGVkMGU4Mjg1MjBiOGY1MmUxY2RlMGUxM2JiYTA4MmYxMDkxOGQ3IiwibWFuaWZlc3RVcmkiOiJjYXM6Ly9zY2FubmVyLWFydGlmYWN0cy9zY2FubmVyL3N1cmZhY2UvbWFuaWZlc3RzL3RlbmFudC1hbHBoYS9zaGEyNTYvNGYvZWUvNGZlZTg3ZDE4NjI5MWRkZmJiY2MyYzU2YzhlZDBlODI4NTIwYjhmNTJlMWNkZTBlMTNiYmEwODJmMTA5MThkNy5qc29uIiwibWFuaWZlc3QiOnsic2NoZW1hIjoic3RlbGxhb3BzLnN1cmZhY2UubWFuaWZlc3RAMSIsInRlbmFudCI6InRlbmFudC1hbHBoYSIsImltYWdlRGlnZXN0Ijoic2hhMjU2OmZlZWRmYWNlIiwiZ2VuZXJhdGVkQXQiOiIyMDI1LTEwLTE5VDEyOjM0OjU2KzAwOjAwIiwiYXJ0aWZhY3RzIjpbeyJraW5kIjoiZW50cnktdHJhY2UiLCJ1cmkiOiJjYXM6Ly9zY2FubmVyLWFydGlmYWN0cy9zY2FubmVyL2VudHJ5LXRyYWNlL2YwZjBmMGYwZjBmMGYwZjBmMGYwZjBmMGYwZjBmMGYwZjBmMGYwZjBmMGYwZjBmMGYwZjBmMGYwZjBmMGYwL2VudHJ5LXRyYWNlLmpzb24iLCJkaWdlc3QiOiJzaGEyNTY6ZjBmMGYwZjBmMGYwZjBmMGYwZjBmMGYwZjBmMGYwZjBmMGYwZjBmMGYwZjBmMGYwZjBmMGYwZjBmMGYwZjBmMCIsIm1lZGlhVHlwZSI6ImFwcGxpY2F0aW9uL2pzb24iLCJmb3JtYXQiOiJqc29uIiwic2l6ZUJ5dGVzIjo0MDk2fSx7ImtpbmQiOiJzYm9tLWludmVudG9yeSIsInVyaSI6ImNhczovL3NjYW5uZXItYXJ0aWZhY3RzL3NjYW5uZXIvaW1hZ2VzL2ZlZWRmYWNlL3Nib20uY2R4Lmpzb24iLCJkaWdlc3QiOiJzaGEyNTY6MTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMSIsIm1lZGlhVHlwZSI6ImFwcGxpY2F0aW9uL3ZuZC5jeWNsb25lZHgranNvbjt2ZXJzaW9uPTEuNjt2aWV3PWludmVudG9yeSIsImZvcm1hdCI6ImNkeC1qc29uIiwic2l6ZUJ5dGVzIjoyNDU3NiwidmlldyI6ImludmVudG9yeSJ9LHsia2luZCI6InNib20tdXNhZ2UiLCJ1cmkiOiJjYXM6Ly9zY2FubmVyLWFydGlmYWN0cy9zY2FubmVyL2ltYWdlcy9mZWVkZmFjZS9zYm9tLXVzYWdlLmNkeC5qc29uIiwiZGlnZXN0Ijoic2hhMjU2OjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIiLCJtZWRpYVR5cGUiOiJhcHBsaWNhdGlvbi92bmQuY3ljbG9uZWR4K2pzb247dmVyc2lvbj0xLjY7dmlldz11c2FnZSIsImZvcm1hdCI6ImNkeC1qc29uIiwic2l6ZUJ5dGVzIjoxNjM4NCwidmlldyI6InVzYWdlIn1dfX19",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"keyId": "test-key",
|
||||||
|
"algorithm": "hs256",
|
||||||
|
"signature": "signature-value"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"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": {
|
"attributes": {
|
||||||
"payloadType": "application/vnd.stellaops.report+json",
|
"policyDigest": "digest-123",
|
||||||
"payload": "eyJyZXBvcnRJZCI6InJlcG9ydC1hYmMiLCJpbWFnZURpZ2VzdCI6InNoYTI1NjpmZWVkZmFjZSIsImdlbmVyYXRlZEF0IjoiMjAyNS0xMC0xOVQxMjozNDo1NiswMDowMCIsInZlcmRpY3QiOiJibG9ja2VkIiwicG9saWN5Ijp7InJldmlzaW9uSWQiOiJyZXYtNDIiLCJkaWdlc3QiOiJkaWdlc3QtMTIzIn0sInN1bW1hcnkiOnsidG90YWwiOjEsImJsb2NrZWQiOjEsIndhcm5lZCI6MCwiaWdub3JlZCI6MCwicXVpZXRlZCI6MH0sInZlcmRpY3RzIjpbeyJmaW5kaW5nSWQiOiJmaW5kaW5nLTEiLCJzdGF0dXMiOiJCbG9ja2VkIiwic2NvcmUiOjQ3LjUsInNvdXJjZVRydXN0IjoiTlZEIiwicmVhY2hhYmlsaXR5IjoicnVudGltZSJ9XSwiaXNzdWVzIjpbXX0=",
|
"policyRevisionId": "rev-42",
|
||||||
"signatures": [
|
"reportId": "report-abc",
|
||||||
{
|
"verdict": "blocked"
|
||||||
"keyId": "test-key",
|
}
|
||||||
"algorithm": "hs256",
|
}
|
||||||
"signature": "signature-value"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"report": {
|
|
||||||
"reportId": "report-abc",
|
|
||||||
"generatedAt": "2025-10-19T12:34:56Z",
|
|
||||||
"imageDigest": "sha256:feedface",
|
|
||||||
"policy": {
|
|
||||||
"digest": "digest-123",
|
|
||||||
"revisionId": "rev-42"
|
|
||||||
},
|
|
||||||
"summary": {
|
|
||||||
"total": 1,
|
|
||||||
"blocked": 1,
|
|
||||||
"warned": 0,
|
|
||||||
"ignored": 0,
|
|
||||||
"quieted": 0
|
|
||||||
},
|
|
||||||
"verdict": "blocked",
|
|
||||||
"verdicts": [
|
|
||||||
{
|
|
||||||
"findingId": "finding-1",
|
|
||||||
"status": "Blocked",
|
|
||||||
"score": 47.5,
|
|
||||||
"sourceTrust": "NVD",
|
|
||||||
"reachability": "runtime"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"issues": []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,56 +1,50 @@
|
|||||||
{
|
{
|
||||||
"eventId": "08a6de24-4a94-4d14-8432-9d14f36f6da3",
|
"eventId": "08a6de24-4a94-4d14-8432-9d14f36f6da3",
|
||||||
"kind": "scanner.event.scan.completed",
|
"kind": "scanner.event.scan.completed",
|
||||||
"version": 1,
|
"version": 1,
|
||||||
"tenant": "tenant-alpha",
|
"tenant": "tenant-alpha",
|
||||||
"occurredAt": "2025-10-19T12:34:56Z",
|
"occurredAt": "2025-10-19T12:34:56+00:00",
|
||||||
"recordedAt": "2025-10-19T12:34:57Z",
|
"recordedAt": "2025-10-19T12:34:57+00:00",
|
||||||
"source": "scanner.webservice",
|
"source": "scanner.webservice",
|
||||||
"idempotencyKey": "scanner.event.scan.completed:tenant-alpha:report-abc",
|
"idempotencyKey": "scanner.event.scan.completed:tenant-alpha:report-abc",
|
||||||
"correlationId": "report-abc",
|
"correlationId": "report-abc",
|
||||||
"traceId": "4bf92f3577b34da6a3ce929d0e0e4736",
|
"traceId": "4bf92f3577b34da6a3ce929d0e0e4736",
|
||||||
"scope": {
|
"scope": {
|
||||||
"namespace": "acme/edge",
|
"namespace": "acme/edge",
|
||||||
"repo": "api",
|
"repo": "api",
|
||||||
"digest": "sha256:feedface"
|
"digest": "sha256:feedface"
|
||||||
},
|
},
|
||||||
"attributes": {
|
"payload": {
|
||||||
"reportId": "report-abc",
|
"reportId": "report-abc",
|
||||||
"policyRevisionId": "rev-42",
|
"scanId": "report-abc",
|
||||||
"policyDigest": "digest-123",
|
"imageDigest": "sha256:feedface",
|
||||||
"verdict": "blocked"
|
"verdict": "fail",
|
||||||
},
|
"summary": {
|
||||||
"payload": {
|
"total": 1,
|
||||||
"reportId": "report-abc",
|
"blocked": 1,
|
||||||
"scanId": "report-abc",
|
"warned": 0,
|
||||||
"imageDigest": "sha256:feedface",
|
"ignored": 0,
|
||||||
"verdict": "fail",
|
"quieted": 0
|
||||||
"summary": {
|
},
|
||||||
"total": 1,
|
"delta": {
|
||||||
"blocked": 1,
|
"newCritical": 1,
|
||||||
"warned": 0,
|
"kev": [
|
||||||
"ignored": 0,
|
"CVE-2024-9999"
|
||||||
"quieted": 0
|
]
|
||||||
},
|
},
|
||||||
"delta": {
|
"policy": {
|
||||||
"newCritical": 1,
|
"revisionId": "rev-42",
|
||||||
"kev": [
|
"digest": "digest-123"
|
||||||
"CVE-2024-9999"
|
},
|
||||||
]
|
"findings": [
|
||||||
},
|
{
|
||||||
"policy": {
|
"id": "finding-1",
|
||||||
"digest": "digest-123",
|
"severity": "Critical",
|
||||||
"revisionId": "rev-42"
|
"cve": "CVE-2024-9999",
|
||||||
},
|
"purl": "pkg:docker/acme/edge-api@sha256-feedface",
|
||||||
"findings": [
|
"reachability": "runtime"
|
||||||
{
|
}
|
||||||
"id": "finding-1",
|
],
|
||||||
"severity": "Critical",
|
|
||||||
"cve": "CVE-2024-9999",
|
|
||||||
"purl": "pkg:docker/acme/edge-api@sha256-feedface",
|
|
||||||
"reachability": "runtime"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"links": {
|
"links": {
|
||||||
"report": {
|
"report": {
|
||||||
"ui": "https://scanner.example/ui/reports/report-abc",
|
"ui": "https://scanner.example/ui/reports/report-abc",
|
||||||
@@ -65,43 +59,89 @@
|
|||||||
"api": "https://scanner.example/api/v1/reports/report-abc/attestation"
|
"api": "https://scanner.example/api/v1/reports/report-abc/attestation"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dsse": {
|
"dsse": {
|
||||||
"payloadType": "application/vnd.stellaops.report+json",
|
"payloadType": "application/vnd.stellaops.report+json",
|
||||||
"payload": "eyJyZXBvcnRJZCI6InJlcG9ydC1hYmMiLCJpbWFnZURpZ2VzdCI6InNoYTI1NjpmZWVkZmFjZSIsImdlbmVyYXRlZEF0IjoiMjAyNS0xMC0xOVQxMjozNDo1NiswMDowMCIsInZlcmRpY3QiOiJibG9ja2VkIiwicG9saWN5Ijp7InJldmlzaW9uSWQiOiJyZXYtNDIiLCJkaWdlc3QiOiJkaWdlc3QtMTIzIn0sInN1bW1hcnkiOnsidG90YWwiOjEsImJsb2NrZWQiOjEsIndhcm5lZCI6MCwiaWdub3JlZCI6MCwicXVpZXRlZCI6MH0sInZlcmRpY3RzIjpbeyJmaW5kaW5nSWQiOiJmaW5kaW5nLTEiLCJzdGF0dXMiOiJCbG9ja2VkIiwic2NvcmUiOjQ3LjUsInNvdXJjZVRydXN0IjoiTlZEIiwicmVhY2hhYmlsaXR5IjoicnVudGltZSJ9XSwiaXNzdWVzIjpbXX0=",
|
"payload": "eyJyZXBvcnRJZCI6InJlcG9ydC1hYmMiLCJpbWFnZURpZ2VzdCI6InNoYTI1NjpmZWVkZmFjZSIsImdlbmVyYXRlZEF0IjoiMjAyNS0xMC0xOVQxMjozNDo1NiswMDowMCIsInZlcmRpY3QiOiJibG9ja2VkIiwicG9saWN5Ijp7InJldmlzaW9uSWQiOiJyZXYtNDIiLCJkaWdlc3QiOiJkaWdlc3QtMTIzIn0sInN1bW1hcnkiOnsidG90YWwiOjEsImJsb2NrZWQiOjEsIndhcm5lZCI6MCwiaWdub3JlZCI6MCwicXVpZXRlZCI6MH0sInZlcmRpY3RzIjpbeyJmaW5kaW5nSWQiOiJmaW5kaW5nLTEiLCJyZWFjaGFiaWxpdHkiOiJydW50aW1lIiwic2NvcmUiOjQ3LjUsInNvdXJjZVRydXN0IjoiTlZEIiwic3RhdHVzIjoiQmxvY2tlZCJ9XSwiaXNzdWVzIjpbXSwic3VyZmFjZSI6eyJ0ZW5hbnQiOiJ0ZW5hbnQtYWxwaGEiLCJnZW5lcmF0ZWRBdCI6IjIwMjUtMTAtMTlUMTI6MzQ6NTYrMDA6MDAiLCJtYW5pZmVzdERpZ2VzdCI6InNoYTI1Njo0ZmVlODdkMTg2MjkxZGRmYmJjYzJjNTZjOGVkMGU4Mjg1MjBiOGY1MmUxY2RlMGUxM2JiYTA4MmYxMDkxOGQ3IiwibWFuaWZlc3RVcmkiOiJjYXM6Ly9zY2FubmVyLWFydGlmYWN0cy9zY2FubmVyL3N1cmZhY2UvbWFuaWZlc3RzL3RlbmFudC1hbHBoYS9zaGEyNTYvNGYvZWUvNGZlZTg3ZDE4NjI5MWRkZmJiY2MyYzU2YzhlZDBlODI4NTIwYjhmNTJlMWNkZTBlMTNiYmEwODJmMTA5MThkNy5qc29uIiwibWFuaWZlc3QiOnsic2NoZW1hIjoic3RlbGxhb3BzLnN1cmZhY2UubWFuaWZlc3RAMSIsInRlbmFudCI6InRlbmFudC1hbHBoYSIsImltYWdlRGlnZXN0Ijoic2hhMjU2OmZlZWRmYWNlIiwiZ2VuZXJhdGVkQXQiOiIyMDI1LTEwLTE5VDEyOjM0OjU2KzAwOjAwIiwiYXJ0aWZhY3RzIjpbeyJraW5kIjoiZW50cnktdHJhY2UiLCJ1cmkiOiJjYXM6Ly9zY2FubmVyLWFydGlmYWN0cy9zY2FubmVyL2VudHJ5LXRyYWNlL2YwZjBmMGYwZjBmMGYwZjBmMGYwZjBmMGYwZjBmMGYwZjBmMGYwZjBmMGYwZjBmMGYwZjBmMGYwZjBmMGYwL2VudHJ5LXRyYWNlLmpzb24iLCJkaWdlc3QiOiJzaGEyNTY6ZjBmMGYwZjBmMGYwZjBmMGYwZjBmMGYwZjBmMGYwZjBmMGYwZjBmMGYwZjBmMGYwZjBmMGYwZjBmMGYwZjBmMCIsIm1lZGlhVHlwZSI6ImFwcGxpY2F0aW9uL2pzb24iLCJmb3JtYXQiOiJqc29uIiwic2l6ZUJ5dGVzIjo0MDk2fSx7ImtpbmQiOiJzYm9tLWludmVudG9yeSIsInVyaSI6ImNhczovL3NjYW5uZXItYXJ0aWZhY3RzL3NjYW5uZXIvaW1hZ2VzL2ZlZWRmYWNlL3Nib20uY2R4Lmpzb24iLCJkaWdlc3QiOiJzaGEyNTY6MTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMSIsIm1lZGlhVHlwZSI6ImFwcGxpY2F0aW9uL3ZuZC5jeWNsb25lZHgranNvbjt2ZXJzaW9uPTEuNjt2aWV3PWludmVudG9yeSIsImZvcm1hdCI6ImNkeC1qc29uIiwic2l6ZUJ5dGVzIjoyNDU3NiwidmlldyI6ImludmVudG9yeSJ9LHsia2luZCI6InNib20tdXNhZ2UiLCJ1cmkiOiJjYXM6Ly9zY2FubmVyLWFydGlmYWN0cy9zY2FubmVyL2ltYWdlcy9mZWVkZmFjZS9zYm9tLXVzYWdlLmNkeC5qc29uIiwiZGlnZXN0Ijoic2hhMjU2OjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIiLCJtZWRpYVR5cGUiOiJhcHBsaWNhdGlvbi92bmQuY3ljbG9uZWR4K2pzb247dmVyc2lvbj0xLjY7dmlldz11c2FnZSIsImZvcm1hdCI6ImNkeC1qc29uIiwic2l6ZUJ5dGVzIjoxNjM4NCwidmlldyI6InVzYWdlIn1dfX19",
|
||||||
"signatures": [
|
"signatures": [
|
||||||
{
|
{
|
||||||
"keyId": "test-key",
|
"keyId": "test-key",
|
||||||
"algorithm": "hs256",
|
"algorithm": "hs256",
|
||||||
"signature": "signature-value"
|
"signature": "signature-value"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"report": {
|
"report": {
|
||||||
"reportId": "report-abc",
|
"reportId": "report-abc",
|
||||||
"generatedAt": "2025-10-19T12:34:56Z",
|
"imageDigest": "sha256:feedface",
|
||||||
"imageDigest": "sha256:feedface",
|
"generatedAt": "2025-10-19T12:34:56+00:00",
|
||||||
"policy": {
|
"verdict": "blocked",
|
||||||
"digest": "digest-123",
|
"policy": {
|
||||||
"revisionId": "rev-42"
|
"revisionId": "rev-42",
|
||||||
},
|
"digest": "digest-123"
|
||||||
"summary": {
|
},
|
||||||
"total": 1,
|
"summary": {
|
||||||
"blocked": 1,
|
"total": 1,
|
||||||
"warned": 0,
|
"blocked": 1,
|
||||||
"ignored": 0,
|
"warned": 0,
|
||||||
"quieted": 0
|
"ignored": 0,
|
||||||
},
|
"quieted": 0
|
||||||
"verdict": "blocked",
|
},
|
||||||
"verdicts": [
|
"verdicts": [
|
||||||
{
|
{
|
||||||
"findingId": "finding-1",
|
"findingId": "finding-1",
|
||||||
"status": "Blocked",
|
"reachability": "runtime",
|
||||||
"score": 47.5,
|
"score": 47.5,
|
||||||
"sourceTrust": "NVD",
|
"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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -129,7 +129,8 @@ Follow the sprint files below in order. Update task status in both `SPRINTS` and
|
|||||||
> 2025-11-02: DOCS-SCANNER-BENCH-62-012 marked DONE (Docs Guild, Language Analyzer Guild) – Dart coverage section fleshed out with detection strategies.
|
> 2025-11-02: DOCS-SCANNER-BENCH-62-012 marked DONE (Docs Guild, Language Analyzer Guild) – Dart coverage section fleshed out with detection strategies.
|
||||||
> 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-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-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-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-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 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.
|
> 2025-11-02: SCANNER-ENG-0016 moved to DOING (Ruby Analyzer Guild) – lockfile parser skeleton committed with initial Gemfile.lock parsing.
|
||||||
|
|||||||
@@ -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)
|
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)
|
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).
|
> 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-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: 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
|
[Identity & Signing] 100.C) IssuerDirectory
|
||||||
Summary: Identity & Signing focus on IssuerDirectory.
|
Summary: Identity & Signing focus on IssuerDirectory.
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
## Status Snapshot (2025-11-04)
|
## 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-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: 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.
|
- 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-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-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-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-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 | 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-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-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.
|
> 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)
|
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-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)
|
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-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-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-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).
|
> 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-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-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.
|
> 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-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 | 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-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).
|
> 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-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)
|
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 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-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-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: 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-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.
|
> 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-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-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-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-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-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)
|
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)
|
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 | **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)
|
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)
|
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
|
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.
|
||||||
|
|||||||
@@ -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-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-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-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-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-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)
|
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)
|
||||||
|
|||||||
@@ -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.3–3.4. | Scanner Guild (docs/modules/scanner/TASKS.md)
|
SCANNER-ENG-0026 | TODO | Implement Windows Chocolatey & registry collectors per `design/windows-analyzer.md` §3.3–3.4. | Scanner Guild (docs/modules/scanner/TASKS.md)
|
||||||
SCANNER-ENG-0027 | TODO | Deliver Windows policy/offline integration per `design/windows-analyzer.md` §5–6. | Scanner Guild, Policy Guild, Offline Kit Guild (docs/modules/scanner/TASKS.md)
|
SCANNER-ENG-0027 | TODO | Deliver Windows policy/offline integration per `design/windows-analyzer.md` §5–6. | 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-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-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
|
[Scanner & Surface] 130.A) Scanner.VIII
|
||||||
|
|||||||
@@ -1,139 +1,139 @@
|
|||||||
# Advisory AI architecture
|
# Advisory AI architecture
|
||||||
|
|
||||||
> Captures the retrieval, guardrail, and inference packaging requirements defined in the Advisory AI implementation plan and related module guides.
|
> Captures the retrieval, guardrail, and inference packaging requirements defined in the Advisory AI implementation plan and related module guides.
|
||||||
|
|
||||||
## 1) Goals
|
## 1) Goals
|
||||||
|
|
||||||
- Summarise advisories/VEX evidence into operator-ready briefs with citations.
|
- Summarise advisories/VEX evidence into operator-ready briefs with citations.
|
||||||
- Explain conflicting statements with provenance and trust weights (using VEX Lens & Excititor data).
|
- Explain conflicting statements with provenance and trust weights (using VEX Lens & Excititor data).
|
||||||
- Suggest remediation plans aligned with Offline Kit deployment models and scheduler follow-ups.
|
- Suggest remediation plans aligned with Offline Kit deployment models and scheduler follow-ups.
|
||||||
- Operate deterministically where possible; cache generated artefacts with digests for audit.
|
- Operate deterministically where possible; cache generated artefacts with digests for audit.
|
||||||
|
|
||||||
## 2) Pipeline overview
|
## 2) Pipeline overview
|
||||||
|
|
||||||
```
|
```
|
||||||
+---------------------+
|
+---------------------+
|
||||||
Concelier/VEX Lens | Evidence Retriever |
|
Concelier/VEX Lens | Evidence Retriever |
|
||||||
Policy Engine ----> | (vector + keyword) | ---> Context Pack (JSON)
|
Policy Engine ----> | (vector + keyword) | ---> Context Pack (JSON)
|
||||||
Zastava runtime +---------------------+
|
Zastava runtime +---------------------+
|
||||||
|
|
|
|
||||||
v
|
v
|
||||||
+-------------+
|
+-------------+
|
||||||
| Prompt |
|
| Prompt |
|
||||||
| Assembler |
|
| Assembler |
|
||||||
+-------------+
|
+-------------+
|
||||||
|
|
|
|
||||||
v
|
v
|
||||||
+-------------+
|
+-------------+
|
||||||
| Guarded LLM |
|
| Guarded LLM |
|
||||||
| (local/host)|
|
| (local/host)|
|
||||||
+-------------+
|
+-------------+
|
||||||
|
|
|
|
||||||
v
|
v
|
||||||
+-----------------+
|
+-----------------+
|
||||||
| Citation & |
|
| Citation & |
|
||||||
| Validation |
|
| Validation |
|
||||||
+-----------------+
|
+-----------------+
|
||||||
|
|
|
|
||||||
v
|
v
|
||||||
+----------------+
|
+----------------+
|
||||||
| Output cache |
|
| Output cache |
|
||||||
| (hash, bundle) |
|
| (hash, bundle) |
|
||||||
+----------------+
|
+----------------+
|
||||||
```
|
```
|
||||||
|
|
||||||
## 3) Retrieval & context
|
## 3) Retrieval & context
|
||||||
|
|
||||||
- Hybrid search: vector embeddings (SBERT-compatible) + keyword filters for advisory IDs, PURLs, CVEs.
|
- Hybrid search: vector embeddings (SBERT-compatible) + keyword filters for advisory IDs, PURLs, CVEs.
|
||||||
- Context packs include:
|
- Context packs include:
|
||||||
- Advisory raw excerpts with highlighted sections and source URLs.
|
- Advisory raw excerpts with highlighted sections and source URLs.
|
||||||
- VEX statements (normalized tuples + trust metadata).
|
- VEX statements (normalized tuples + trust metadata).
|
||||||
- Policy explain traces for the affected finding.
|
- Policy explain traces for the affected finding.
|
||||||
- Runtime/impact hints from Zastava (exposure, entrypoints).
|
- Runtime/impact hints from Zastava (exposure, entrypoints).
|
||||||
- Export-ready remediation data (fixed versions, patches).
|
- Export-ready remediation data (fixed versions, patches).
|
||||||
- **SBOM context retriever** (AIAI-31-002) hydrates:
|
- **SBOM context retriever** (AIAI-31-002) hydrates:
|
||||||
- Version timelines (first/last observed, status, fix availability).
|
- Version timelines (first/last observed, status, fix availability).
|
||||||
- Dependency paths (runtime vs build/test, deduped by coordinate chain).
|
- Dependency paths (runtime vs build/test, deduped by coordinate chain).
|
||||||
- Tenant environment flags (prod/stage toggles) with optional blast radius summary.
|
- Tenant environment flags (prod/stage toggles) with optional blast radius summary.
|
||||||
- Service-side clamps: max 500 timeline entries, 200 dependency paths, with client-provided toggles for env/blast data.
|
- Service-side clamps: max 500 timeline entries, 200 dependency paths, with client-provided toggles for env/blast data.
|
||||||
- `AddSbomContextHttpClient(...)` registers the typed HTTP client that calls `/v1/sbom/context`, while `NullSbomContextClient` remains the safe default for environments that have not yet exposed the SBOM service.
|
- `AddSbomContextHttpClient(...)` registers the typed HTTP client that calls `/v1/sbom/context`, while `NullSbomContextClient` remains the safe default for environments that have not yet exposed the SBOM service.
|
||||||
|
|
||||||
**Sample configuration** (wire real SBOM base URL + API key):
|
**Sample configuration** (wire real SBOM base URL + API key):
|
||||||
|
|
||||||
```csharp
|
```csharp
|
||||||
services.AddSbomContextHttpClient(options =>
|
services.AddSbomContextHttpClient(options =>
|
||||||
{
|
{
|
||||||
options.BaseAddress = new Uri("https://sbom-service.internal");
|
options.BaseAddress = new Uri("https://sbom-service.internal");
|
||||||
options.Endpoint = "/v1/sbom/context";
|
options.Endpoint = "/v1/sbom/context";
|
||||||
options.ApiKey = configuration["SBOM_SERVICE_API_KEY"];
|
options.ApiKey = configuration["SBOM_SERVICE_API_KEY"];
|
||||||
options.UserAgent = "stellaops-advisoryai/1.0";
|
options.UserAgent = "stellaops-advisoryai/1.0";
|
||||||
options.Tenant = configuration["TENANT_ID"];
|
options.Tenant = configuration["TENANT_ID"];
|
||||||
});
|
});
|
||||||
|
|
||||||
services.AddAdvisoryPipeline();
|
services.AddAdvisoryPipeline();
|
||||||
```
|
```
|
||||||
|
|
||||||
After configuration, issue a smoke request (e.g., `ISbomContextRetriever.RetrieveAsync`) during deployment validation to confirm end-to-end connectivity and credentials before enabling Advisory AI endpoints.
|
After configuration, issue a smoke request (e.g., `ISbomContextRetriever.RetrieveAsync`) during deployment validation to confirm end-to-end connectivity and credentials before enabling Advisory AI endpoints.
|
||||||
|
|
||||||
Retriever requests and results are trimmed/normalized before hashing; metadata (counts, provenance keys) is returned for downstream guardrails. Unit coverage ensures deterministic ordering and flag handling.
|
Retriever requests and results are trimmed/normalized before hashing; metadata (counts, provenance keys) is returned for downstream guardrails. Unit coverage ensures deterministic ordering and flag handling.
|
||||||
|
|
||||||
All context references include `content_hash` and `source_id` enabling verifiable citations.
|
All context references include `content_hash` and `source_id` enabling verifiable citations.
|
||||||
|
|
||||||
## 4) Guardrails
|
## 4) Guardrails
|
||||||
|
|
||||||
- Prompt templates enforce structure: summary, conflicts, remediation, references.
|
- Prompt templates enforce structure: summary, conflicts, remediation, references.
|
||||||
- Response validator ensures:
|
- Response validator ensures:
|
||||||
- No hallucinated advisories (every fact must map to input context).
|
- No hallucinated advisories (every fact must map to input context).
|
||||||
- Citations follow `[n]` indexing referencing actual sources.
|
- Citations follow `[n]` indexing referencing actual sources.
|
||||||
- Remediation suggestions only cite policy-approved sources (fixed versions, vendor hotfixes).
|
- Remediation suggestions only cite policy-approved sources (fixed versions, vendor hotfixes).
|
||||||
- Moderation/PII filters prevent leaking secrets; responses failing validation are rejected and logged.
|
- Moderation/PII filters prevent leaking secrets; responses failing validation are rejected and logged.
|
||||||
- Pre-flight guardrails redact secrets (AWS keys, generic API tokens, PEM blobs), block "ignore previous instructions"-style prompt injection attempts, enforce citation presence, and cap prompt payload length (default 16 kB). Guardrail outcomes and redaction counts surface via `advisory_guardrail_blocks` / `advisory_outputs_stored` metrics.
|
- Pre-flight guardrails redact secrets (AWS keys, generic API tokens, PEM blobs), block "ignore previous instructions"-style prompt injection attempts, enforce citation presence, and cap prompt payload length (default 16 kB). Guardrail outcomes and redaction counts surface via `advisory_guardrail_blocks` / `advisory_outputs_stored` metrics.
|
||||||
|
|
||||||
## 5) Deterministic tooling
|
## 5) Deterministic tooling
|
||||||
|
|
||||||
- **Version comparators** — offline semantic version + RPM EVR parsers with range evaluators. Supports chained constraints (`>=`, `<=`, `!=`) used by remediation advice and blast radius calcs.
|
- **Version comparators** — offline semantic version + RPM EVR parsers with range evaluators. Supports chained constraints (`>=`, `<=`, `!=`) used by remediation advice and blast radius calcs.
|
||||||
- Registered via `AddAdvisoryDeterministicToolset` for reuse across orchestrator, CLI, and services.
|
- Registered via `AddAdvisoryDeterministicToolset` for reuse across orchestrator, CLI, and services.
|
||||||
- **Orchestration pipeline** — see `orchestration-pipeline.md` for prerequisites, task breakdown, and cross-guild responsibilities before wiring the execution flows.
|
- **Orchestration pipeline** — see `orchestration-pipeline.md` for prerequisites, task breakdown, and cross-guild responsibilities before wiring the execution flows.
|
||||||
- **Planned extensions** — NEVRA/EVR comparators, ecosystem-specific normalisers, dependency chain scorers (AIAI-31-003 scope).
|
- **Planned extensions** — NEVRA/EVR comparators, ecosystem-specific normalisers, dependency chain scorers (AIAI-31-003 scope).
|
||||||
- Exposed via internal interfaces to allow orchestrator/toolchain reuse; all helpers stay side-effect free and deterministic for golden testing.
|
- Exposed via internal interfaces to allow orchestrator/toolchain reuse; all helpers stay side-effect free and deterministic for golden testing.
|
||||||
|
|
||||||
## 6) Output persistence
|
## 6) Output persistence
|
||||||
|
|
||||||
- Cached artefacts stored in `advisory_ai_outputs` with fields:
|
- Cached artefacts stored in `advisory_ai_outputs` with fields:
|
||||||
- `output_hash` (sha256 of JSON response).
|
- `output_hash` (sha256 of JSON response).
|
||||||
- `input_digest` (hash of context pack).
|
- `input_digest` (hash of context pack).
|
||||||
- `summary`, `conflicts`, `remediation`, `citations`.
|
- `summary`, `conflicts`, `remediation`, `citations`.
|
||||||
- `generated_at`, `model_id`, `profile` (Sovereign/FIPS etc.).
|
- `generated_at`, `model_id`, `profile` (Sovereign/FIPS etc.).
|
||||||
- `signatures` (optional DSSE if run in deterministic mode).
|
- `signatures` (optional DSSE if run in deterministic mode).
|
||||||
- Offline bundle format contains `summary.md`, `citations.json`, `context_manifest.json`, `signatures/`.
|
- Offline bundle format contains `summary.md`, `citations.json`, `context_manifest.json`, `signatures/`.
|
||||||
|
|
||||||
## 7) Profiles & sovereignty
|
## 7) Profiles & sovereignty
|
||||||
|
|
||||||
- **Profiles:** `default`, `fips-local` (FIPS-compliant local model), `gost-local`, `cloud-openai` (optional, disabled by default). Each profile defines allowed models, key management, and telemetry endpoints.
|
- **Profiles:** `default`, `fips-local` (FIPS-compliant local model), `gost-local`, `cloud-openai` (optional, disabled by default). Each profile defines allowed models, key management, and telemetry endpoints.
|
||||||
- **CryptoProfile/RootPack integration:** generated artefacts can be signed using configured CryptoProfile to satisfy procurement/trust requirements.
|
- **CryptoProfile/RootPack integration:** generated artefacts can be signed using configured CryptoProfile to satisfy procurement/trust requirements.
|
||||||
|
|
||||||
## 8) APIs
|
## 8) APIs
|
||||||
|
|
||||||
- `POST /api/v1/advisory/{task}` — executes Summary/Conflict/Remediation pipeline (`task` ∈ `summary|conflict|remediation`). Requests accept `{advisoryKey, artifactId?, policyVersion?, profile, preferredSections?, forceRefresh}` and return sanitized prompt payloads, citations, guardrail metadata, provenance hash, and cache hints.
|
- `POST /api/v1/advisory/{task}` — executes Summary/Conflict/Remediation pipeline (`task` ∈ `summary|conflict|remediation`). Requests accept `{advisoryKey, artifactId?, policyVersion?, profile, preferredSections?, forceRefresh}` and return sanitized prompt payloads, citations, guardrail metadata, provenance hash, and cache hints.
|
||||||
- `GET /api/v1/advisory/outputs/{cacheKey}?taskType=SUMMARY&profile=default` — retrieves cached artefacts for downstream consumers (Console, CLI, Export Center). Guardrail state and provenance hash accompany results.
|
- `GET /api/v1/advisory/outputs/{cacheKey}?taskType=SUMMARY&profile=default` — retrieves cached artefacts for downstream consumers (Console, CLI, Export Center). Guardrail state and provenance hash accompany results.
|
||||||
|
|
||||||
All endpoints accept `profile` parameter (default `fips-local`) and return `output_hash`, `input_digest`, and `citations` for verification.
|
All endpoints accept `profile` parameter (default `fips-local`) and return `output_hash`, `input_digest`, and `citations` for verification.
|
||||||
|
|
||||||
## 9) Observability
|
## 9) Observability
|
||||||
|
|
||||||
- Metrics: `advisory_ai_requests_total{profile,type}`, `advisory_ai_latency_seconds`, `advisory_ai_validation_failures_total`.
|
- Metrics: `advisory_ai_requests_total{profile,type}`, `advisory_ai_latency_seconds`, `advisory_ai_validation_failures_total`.
|
||||||
- Logs: include `output_hash`, `input_digest`, `profile`, `model_id`, `tenant`, `artifacts`. Sensitive context is not logged.
|
- Logs: include `output_hash`, `input_digest`, `profile`, `model_id`, `tenant`, `artifacts`. Sensitive context is not logged.
|
||||||
- Traces: spans for retrieval, prompt assembly, model inference, validation, cache write.
|
- Traces: spans for retrieval, prompt assembly, model inference, validation, cache write.
|
||||||
|
|
||||||
## 10) Operational controls
|
## 10) Operational controls
|
||||||
|
|
||||||
- Feature flags per tenant (`ai.summary.enabled`, `ai.remediation.enabled`).
|
- Feature flags per tenant (`ai.summary.enabled`, `ai.remediation.enabled`).
|
||||||
- Rate limits (per tenant, per profile) enforced by Orchestrator to prevent runaway usage.
|
- Rate limits (per tenant, per profile) enforced by Orchestrator to prevent runaway usage.
|
||||||
- Offline/air-gapped deployments run local models packaged with Offline Kit; model weights validated via manifest digests.
|
- Offline/air-gapped deployments run local models packaged with Offline Kit; model weights validated via manifest digests.
|
||||||
|
|
||||||
## 11) Hosting surfaces
|
## 11) Hosting surfaces
|
||||||
|
|
||||||
- **WebService** — exposes `/v1/advisory-ai/pipeline/{task}` to materialise plans and enqueue execution messages.
|
- **WebService** — exposes `/v1/advisory-ai/pipeline/{task}` to materialise plans and enqueue execution messages.
|
||||||
- **Worker** — background service draining the advisory pipeline queue (file-backed stub) pending integration with shared transport.
|
- **Worker** — background service draining the advisory pipeline queue (file-backed stub) pending integration with shared transport.
|
||||||
- Both hosts register `AddAdvisoryAiCore`, which wires the SBOM context client, deterministic toolset, pipeline orchestrator, and queue metrics.
|
- Both hosts register `AddAdvisoryAiCore`, which wires the SBOM context client, deterministic toolset, pipeline orchestrator, and queue metrics.
|
||||||
- SBOM base address + tenant metadata are configured via `AdvisoryAI:SbomBaseAddress` and propagated through `AddSbomContext`.
|
- SBOM base address + tenant metadata are configured via `AdvisoryAI:SbomBaseAddress` and propagated through `AddSbomContext`.
|
||||||
|
|||||||
@@ -1,96 +1,96 @@
|
|||||||
# Advisory AI Orchestration Pipeline (Planning Notes)
|
# Advisory AI Orchestration Pipeline (Planning Notes)
|
||||||
|
|
||||||
> **Status:** In progress – orchestration metadata and cache-key wiring underway for AIAI-31-004.
|
> **Status:** In progress – orchestration metadata and cache-key wiring underway for AIAI-31-004.
|
||||||
> **Audience:** Advisory AI guild, WebService/Worker guilds, CLI guild, Docs/QA support teams.
|
> **Audience:** Advisory AI guild, WebService/Worker guilds, CLI guild, Docs/QA support teams.
|
||||||
|
|
||||||
## 1. Goal
|
## 1. Goal
|
||||||
|
|
||||||
Wire the deterministic pipeline (Summary / Conflict / Remediation flows) into the Advisory AI service, workers, and CLI with deterministic caching, prompt preparation, and guardrail fallback. This document captures the pre-integration checklist and task breakdown so each guild understands their responsibilities before coding begins.
|
Wire the deterministic pipeline (Summary / Conflict / Remediation flows) into the Advisory AI service, workers, and CLI with deterministic caching, prompt preparation, and guardrail fallback. This document captures the pre-integration checklist and task breakdown so each guild understands their responsibilities before coding begins.
|
||||||
|
|
||||||
## 2. Prerequisites
|
## 2. Prerequisites
|
||||||
|
|
||||||
| Area | Requirement | Owner | Status |
|
| Area | Requirement | Owner | Status |
|
||||||
|------|-------------|-------|--------|
|
|------|-------------|-------|--------|
|
||||||
| **Toolset** | Deterministic comparators, dependency analyzer (`IDeterministicToolset`, `AdvisoryPipelineOrchestrator`) | Advisory AI | ✅ landed (AIAI-31-003) |
|
| **Toolset** | Deterministic comparators, dependency analyzer (`IDeterministicToolset`, `AdvisoryPipelineOrchestrator`) | Advisory AI | ✅ landed (AIAI-31-003) |
|
||||||
| **SBOM context** | Real SBOM context client delivering timelines + dependency paths | SBOM Service Guild | ✅ typed client and DI helper ready; supply host BaseAddress at integration time |
|
| **SBOM context** | Real SBOM context client delivering timelines + dependency paths | SBOM Service Guild | ✅ typed client and DI helper ready; supply host BaseAddress at integration time |
|
||||||
| **Prompt artifacts** | Liquid/Handlebars prompt templates for summary/conflict/remediation | Advisory AI Docs Guild | ⏳ authoring needed |
|
| **Prompt artifacts** | Liquid/Handlebars prompt templates for summary/conflict/remediation | Advisory AI Docs Guild | ⏳ authoring needed |
|
||||||
| **Cache strategy** | Decision on DSSE or hash-only cache entries, TTLs, and eviction policy | Advisory AI + Platform | ⏳ hash-only plan keys implemented; persistence decision outstanding |
|
| **Cache strategy** | Decision on DSSE or hash-only cache entries, TTLs, and eviction policy | Advisory AI + Platform | ⏳ hash-only plan keys implemented; persistence decision outstanding |
|
||||||
| **Auth scopes** | Confirm service account scopes for new API endpoints/worker-to-service calls | Authority Guild | 🔲 define |
|
| **Auth scopes** | Confirm service account scopes for new API endpoints/worker-to-service calls | Authority Guild | 🔲 define |
|
||||||
|
|
||||||
**Blocking risk:** SBOM client and prompt templates must exist (even stubbed) before the orchestrator can produce stable plans.
|
**Blocking risk:** SBOM client and prompt templates must exist (even stubbed) before the orchestrator can produce stable plans.
|
||||||
|
|
||||||
## 3. Integration plan (high-level)
|
## 3. Integration plan (high-level)
|
||||||
|
|
||||||
1. **Service layer (WebService / Worker)**
|
1. **Service layer (WebService / Worker)**
|
||||||
- Inject `IAdvisoryPipelineOrchestrator` via `AddAdvisoryPipeline`.
|
- Inject `IAdvisoryPipelineOrchestrator` via `AddAdvisoryPipeline`.
|
||||||
- Define REST endpoint `POST /v1/advisories/{key}/pipeline/{task}` (task ∈ summary/conflict/remediation).
|
- Define REST endpoint `POST /v1/advisories/{key}/pipeline/{task}` (task ∈ summary/conflict/remediation).
|
||||||
- Worker consumes queue messages (`advisory.pipeline.execute`) -> fetches plan -> executes prompt -> persists output & provenance.
|
- Worker consumes queue messages (`advisory.pipeline.execute`) -> fetches plan -> executes prompt -> persists output & provenance.
|
||||||
- Add metrics: `advisory_pipeline_requests_total`, `advisory_pipeline_plan_cache_hits_total`, `advisory_pipeline_latency_seconds`.
|
- Add metrics: `advisory_pipeline_requests_total`, `advisory_pipeline_plan_cache_hits_total`, `advisory_pipeline_latency_seconds`.
|
||||||
2. **CLI**
|
2. **CLI**
|
||||||
- New command `stella advise run <task>` with flags for artifact id, profile, policy version, `--force-refresh`.
|
- New command `stella advise run <task>` with flags for artifact id, profile, policy version, `--force-refresh`.
|
||||||
- Render JSON/Markdown outputs; handle caching hints (print cache key, status).
|
- Render JSON/Markdown outputs; handle caching hints (print cache key, status).
|
||||||
3. **Caching / storage**
|
3. **Caching / storage**
|
||||||
- Choose storage (Mongo collection vs existing DSSE output store).
|
- Choose storage (Mongo collection vs existing DSSE output store).
|
||||||
- Persist `AdvisoryTaskPlan` metadata + generated output keyed by cache key + policy version.
|
- Persist `AdvisoryTaskPlan` metadata + generated output keyed by cache key + policy version.
|
||||||
- Expose TTL/force-refresh semantics.
|
- Expose TTL/force-refresh semantics.
|
||||||
4. **Docs & QA**
|
4. **Docs & QA**
|
||||||
- Publish API spec (`docs/advisory-ai/api.md`) + CLI docs.
|
- Publish API spec (`docs/advisory-ai/api.md`) + CLI docs.
|
||||||
- Add golden outputs for deterministic runs; property tests for cache key stability (unit coverage landed for cache hashing + option clamps).
|
- Add golden outputs for deterministic runs; property tests for cache key stability (unit coverage landed for cache hashing + option clamps).
|
||||||
|
|
||||||
## 4. Task Breakdown
|
## 4. Task Breakdown
|
||||||
|
|
||||||
### AIAI-31-004A (Service orchestration wiring)
|
### AIAI-31-004A (Service orchestration wiring)
|
||||||
|
|
||||||
- **Scope:** WebService/Worker injection, REST/queue plumbing, metrics counters, basic cache stub.
|
- **Scope:** WebService/Worker injection, REST/queue plumbing, metrics counters, basic cache stub.
|
||||||
- **Dependencies:** `AddAdvisoryPipeline`, SBOM client stub.
|
- **Dependencies:** `AddAdvisoryPipeline`, SBOM client stub.
|
||||||
- **Exit:** API responds with plan metadata + queue message; worker logs execution attempt; metrics emitted.
|
- **Exit:** API responds with plan metadata + queue message; worker logs execution attempt; metrics emitted.
|
||||||
|
|
||||||
### AIAI-31-004B (Prompt assembly & cache persistence)
|
### AIAI-31-004B (Prompt assembly & cache persistence)
|
||||||
|
|
||||||
- **Scope:** Implement prompt assembler, connect to guardrails, persist cache entries w/ DSSE metadata.
|
- **Scope:** Implement prompt assembler, connect to guardrails, persist cache entries w/ DSSE metadata.
|
||||||
- **Dependencies:** Prompt templates, cache storage decision, guardrail interface.
|
- **Dependencies:** Prompt templates, cache storage decision, guardrail interface.
|
||||||
- **Exit:** Deterministic outputs stored; force-refresh honoured; tests cover prompt assembly + caching.
|
- **Exit:** Deterministic outputs stored; force-refresh honoured; tests cover prompt assembly + caching.
|
||||||
> 2025-11-03: Prompt assembler now emits deterministic JSON payloads, guardrail pipeline wiring is stubbed for upcoming security hardening, and outputs persist with DSSE-ready provenance metadata plus golden test coverage.
|
> 2025-11-03: Prompt assembler now emits deterministic JSON payloads, guardrail pipeline wiring is stubbed for upcoming security hardening, and outputs persist with DSSE-ready provenance metadata plus golden test coverage.
|
||||||
|
|
||||||
### AIAI-31-004C (CLI integration & docs)
|
### AIAI-31-004C (CLI integration & docs)
|
||||||
|
|
||||||
- **Scope:** CLI command + output renderer, docs updates, CLI tests (golden outputs).
|
- **Scope:** CLI command + output renderer, docs updates, CLI tests (golden outputs).
|
||||||
- **Dependencies:** Service endpoints stable, caching semantics documented.
|
- **Dependencies:** Service endpoints stable, caching semantics documented.
|
||||||
- **Exit:** CLI command produces deterministic output, docs updated, smoke tests recorded.
|
- **Exit:** CLI command produces deterministic output, docs updated, smoke tests recorded.
|
||||||
|
|
||||||
### AIAI-31-006 (Service API surface)
|
### AIAI-31-006 (Service API surface)
|
||||||
|
|
||||||
- **Scope:** Expose REST endpoints for summary/conflict/remediation execution plus cached output retrieval (`POST /api/v1/advisory/{task}`, `GET /api/v1/advisory/outputs/{cacheKey}`). Include guardrail execution, provenance hashing, metrics, and stubs for RBAC/rate limits.
|
- **Scope:** Expose REST endpoints for summary/conflict/remediation execution plus cached output retrieval (`POST /api/v1/advisory/{task}`, `GET /api/v1/advisory/outputs/{cacheKey}`). Include guardrail execution, provenance hashing, metrics, and stubs for RBAC/rate limits.
|
||||||
- **Dependencies:** Guardrail enforcement (AIAI-31-005), Authority scope wiring (`advisory-ai:view` / `advisory-ai:operate`), Offline kit docs.
|
- **Dependencies:** Guardrail enforcement (AIAI-31-005), Authority scope wiring (`advisory-ai:view` / `advisory-ai:operate`), Offline kit docs.
|
||||||
- **Exit:** Endpoints return sanitized prompts with citations, guardrail metadata, DSSE hash, and plan cache indicators; OpenAPI description updated; rate-limit hooks ready for Authority integration.
|
- **Exit:** Endpoints return sanitized prompts with citations, guardrail metadata, DSSE hash, and plan cache indicators; OpenAPI description updated; rate-limit hooks ready for Authority integration.
|
||||||
> 2025-11-03: Initial REST surface shipped – direct execution runs through guardrail pipeline, outputs persist with DSSE-ready provenance, metrics `advisory_outputs_stored`/`advisory_guardrail_blocks` emit, and cache retrieval endpoint exposes stored artefacts (RBAC/header enforcement pending scope delivery).
|
> 2025-11-03: Initial REST surface shipped – direct execution runs through guardrail pipeline, outputs persist with DSSE-ready provenance, metrics `advisory_outputs_stored`/`advisory_guardrail_blocks` emit, and cache retrieval endpoint exposes stored artefacts (RBAC/header enforcement pending scope delivery).
|
||||||
|
|
||||||
### Supporting tasks (other guilds)
|
### Supporting tasks (other guilds)
|
||||||
|
|
||||||
- **AUTH-AIAI-31-004** – Update scopes and DSSE policy (Authority guild).
|
- **AUTH-AIAI-31-004** – Update scopes and DSSE policy (Authority guild).
|
||||||
- **DOCS-AIAI-31-003** – Publish API documentation, CLI guide updates (Docs guild).
|
- **DOCS-AIAI-31-003** – Publish API documentation, CLI guide updates (Docs guild).
|
||||||
- **QA-AIAI-31-004** – Golden/properties/perf suite for pipeline (QA guild).
|
- **QA-AIAI-31-004** – Golden/properties/perf suite for pipeline (QA guild).
|
||||||
|
|
||||||
## 5. Acceptance checklist (per task)
|
## 5. Acceptance checklist (per task)
|
||||||
|
|
||||||
| Item | Notes |
|
| Item | Notes |
|
||||||
|------|-------|
|
|------|-------|
|
||||||
| Cache key stability | `AdvisoryPipelineOrchestrator` hash must remain stable under re-run of identical inputs. |
|
| Cache key stability | `AdvisoryPipelineOrchestrator` hash must remain stable under re-run of identical inputs. |
|
||||||
| Metrics & logging | Request id, cache key, task type, profile, latency; guardrail results logged without sensitive prompt data. |
|
| Metrics & logging | Request id, cache key, task type, profile, latency; guardrail results logged without sensitive prompt data. |
|
||||||
| Offline readiness | All prompt templates bundled with Offline Kit; CLI works in air-gapped mode with cached data. |
|
| Offline readiness | All prompt templates bundled with Offline Kit; CLI works in air-gapped mode with cached data. |
|
||||||
| Policy awareness | Plans encode policy version used; outputs reference policy digest for audit. |
|
| Policy awareness | Plans encode policy version used; outputs reference policy digest for audit. |
|
||||||
| Testing | Unit tests (plan generation, cache keys, DI), integration (service endpoint, worker, CLI), deterministic golden outputs. |
|
| Testing | Unit tests (plan generation, cache keys, DI), integration (service endpoint, worker, CLI), deterministic golden outputs. |
|
||||||
|
|
||||||
## 6. Next steps
|
## 6. Next steps
|
||||||
|
|
||||||
1. Finalize SBOM context client (AIAI-31-002) and prompt templates.
|
1. Finalize SBOM context client (AIAI-31-002) and prompt templates.
|
||||||
2. Create queue schema spec (`docs/modules/advisory-ai/queue-contracts.md`) if not already available.
|
2. Create queue schema spec (`docs/modules/advisory-ai/queue-contracts.md`) if not already available.
|
||||||
3. Schedule cross-guild kickoff to agree on cache store & DSSE policy.
|
3. Schedule cross-guild kickoff to agree on cache store & DSSE policy.
|
||||||
|
|
||||||
## 7. Recent updates
|
## 7. Recent updates
|
||||||
|
|
||||||
- 2025-11-04 — Orchestrator metadata now captures SBOM environment flags, blast-radius metrics, and dependency analysis details; cache-key normalization covers ordering.
|
- 2025-11-04 — Orchestrator metadata now captures SBOM environment flags, blast-radius metrics, and dependency analysis details; cache-key normalization covers ordering.
|
||||||
- 2025-11-04 — Unit tests added for SBOM-absent requests, option-limit enforcement, and cache-key stability.
|
- 2025-11-04 — Unit tests added for SBOM-absent requests, option-limit enforcement, and cache-key stability.
|
||||||
- 2025-11-04 — `AddSbomContext` DI helper enforces tenant header + base address wiring for downstream hosts.
|
- 2025-11-04 — `AddSbomContext` DI helper enforces tenant header + base address wiring for downstream hosts.
|
||||||
|
|
||||||
_Last updated: 2025-11-04_
|
_Last updated: 2025-11-04_
|
||||||
|
|||||||
@@ -1,298 +1,298 @@
|
|||||||
# CLI AOC Commands Reference
|
# CLI AOC Commands Reference
|
||||||
|
|
||||||
> **Audience:** DevEx engineers, operators, and CI authors integrating the `stella` CLI with Aggregation-Only Contract (AOC) workflows.
|
> **Audience:** DevEx engineers, operators, and CI authors integrating the `stella` CLI with Aggregation-Only Contract (AOC) workflows.
|
||||||
> **Scope:** Command synopsis, options, exit codes, and offline considerations for `stella sources ingest --dry-run` and `stella aoc verify` as introduced in Sprint 19.
|
> **Scope:** Command synopsis, options, exit codes, and offline considerations for `stella sources ingest --dry-run` and `stella aoc verify` as introduced in Sprint 19.
|
||||||
|
|
||||||
Both commands are designed to enforce the AOC guardrails documented in the [aggregation-only reference](../../../ingestion/aggregation-only-contract.md) and the [architecture overview](../architecture.md). They consume Authority-issued tokens with tenant scopes and never mutate ingestion stores.
|
Both commands are designed to enforce the AOC guardrails documented in the [aggregation-only reference](../../../ingestion/aggregation-only-contract.md) and the [architecture overview](../architecture.md). They consume Authority-issued tokens with tenant scopes and never mutate ingestion stores.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 1 · Prerequisites
|
## 1 · Prerequisites
|
||||||
|
|
||||||
- CLI version: `stella` ≥ 0.19.0 (AOC feature gate enabled).
|
- CLI version: `stella` ≥ 0.19.0 (AOC feature gate enabled).
|
||||||
- Required scopes (DPoP-bound):
|
- Required scopes (DPoP-bound):
|
||||||
- `advisory:read` for Concelier sources.
|
- `advisory:read` for Concelier sources.
|
||||||
- `vex:read` for Excititor sources (optional but required for VEX checks).
|
- `vex:read` for Excititor sources (optional but required for VEX checks).
|
||||||
- `aoc:verify` to invoke guard verification endpoints.
|
- `aoc:verify` to invoke guard verification endpoints.
|
||||||
- `tenant:select` if your deployment uses tenant switching.
|
- `tenant:select` if your deployment uses tenant switching.
|
||||||
- Connectivity: direct access to Concelier/Excititor APIs or Offline Kit snapshot (see § 4).
|
- Connectivity: direct access to Concelier/Excititor APIs or Offline Kit snapshot (see § 4).
|
||||||
- Environment: set `STELLA_AUTHORITY_URL`, `STELLA_TENANT`, and export a valid OpTok via `stella auth login` or existing token cache.
|
- Environment: set `STELLA_AUTHORITY_URL`, `STELLA_TENANT`, and export a valid OpTok via `stella auth login` or existing token cache.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 2 · `stella sources ingest --dry-run`
|
## 2 · `stella sources ingest --dry-run`
|
||||||
|
|
||||||
### 2.1 Synopsis
|
### 2.1 Synopsis
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
stella sources ingest --dry-run \
|
stella sources ingest --dry-run \
|
||||||
--source <source-key> \
|
--source <source-key> \
|
||||||
--input <path-or-uri> \
|
--input <path-or-uri> \
|
||||||
[--tenant <tenant-id>] \
|
[--tenant <tenant-id>] \
|
||||||
[--format json|table] \
|
[--format json|table] \
|
||||||
[--no-color] \
|
[--no-color] \
|
||||||
[--output <file>]
|
[--output <file>]
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2.2 Description
|
### 2.2 Description
|
||||||
|
|
||||||
Previews an ingestion write without touching MongoDB. The command loads an upstream advisory or VEX document, computes the would-write payload, runs it through the `AOCWriteGuard`, and reports any forbidden fields, provenance gaps, or idempotency issues. Use it during connector development, CI validation, or while triaging incidents.
|
Previews an ingestion write without touching MongoDB. The command loads an upstream advisory or VEX document, computes the would-write payload, runs it through the `AOCWriteGuard`, and reports any forbidden fields, provenance gaps, or idempotency issues. Use it during connector development, CI validation, or while triaging incidents.
|
||||||
|
|
||||||
### 2.3 Options
|
### 2.3 Options
|
||||||
|
|
||||||
| Option | Description |
|
| Option | Description |
|
||||||
|--------|-------------|
|
|--------|-------------|
|
||||||
| `--source <source-key>` | Logical source name (`redhat`, `ubuntu`, `osv`, etc.). Mirrors connector configuration. |
|
| `--source <source-key>` | Logical source name (`redhat`, `ubuntu`, `osv`, etc.). Mirrors connector configuration. |
|
||||||
| `--input <path-or-uri>` | Path to local CSAF/OSV/VEX file or HTTPS URI. CLI normalises transport (gzip/base64) before guard evaluation. |
|
| `--input <path-or-uri>` | Path to local CSAF/OSV/VEX file or HTTPS URI. CLI normalises transport (gzip/base64) before guard evaluation. |
|
||||||
| `--tenant <tenant-id>` | Overrides default tenant for multi-tenant deployments. Mandatory when `STELLA_TENANT` is not set. |
|
| `--tenant <tenant-id>` | Overrides default tenant for multi-tenant deployments. Mandatory when `STELLA_TENANT` is not set. |
|
||||||
| `--format json|table` | Output format. `table` (default) prints summary with highlighted violations; `json` emits machine-readable report (see below). |
|
| `--format json|table` | Output format. `table` (default) prints summary with highlighted violations; `json` emits machine-readable report (see below). |
|
||||||
| `--no-color` | Disables ANSI colour output for CI logs. |
|
| `--no-color` | Disables ANSI colour output for CI logs. |
|
||||||
| `--output <file>` | Writes the JSON report to file while still printing human-readable summary to stdout. |
|
| `--output <file>` | Writes the JSON report to file while still printing human-readable summary to stdout. |
|
||||||
|
|
||||||
### 2.4 Output schema (JSON)
|
### 2.4 Output schema (JSON)
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"source": "redhat",
|
"source": "redhat",
|
||||||
"tenant": "default",
|
"tenant": "default",
|
||||||
"guardVersion": "1.0.0",
|
"guardVersion": "1.0.0",
|
||||||
"status": "ok",
|
"status": "ok",
|
||||||
"document": {
|
"document": {
|
||||||
"contentHash": "sha256:…",
|
"contentHash": "sha256:…",
|
||||||
"supersedes": null,
|
"supersedes": null,
|
||||||
"provenance": {
|
"provenance": {
|
||||||
"signature": { "format": "pgp", "present": true }
|
"signature": { "format": "pgp", "present": true }
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"violations": []
|
"violations": []
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
When violations exist, `status` becomes `error` and `violations` contains entries with `code` (`ERR_AOC_00x`), a short `message`, and JSON Pointer `path` values indicating offending fields.
|
When violations exist, `status` becomes `error` and `violations` contains entries with `code` (`ERR_AOC_00x`), a short `message`, and JSON Pointer `path` values indicating offending fields.
|
||||||
|
|
||||||
### 2.5 Exit codes
|
### 2.5 Exit codes
|
||||||
|
|
||||||
| Exit code | Meaning |
|
| Exit code | Meaning |
|
||||||
|-----------|---------|
|
|-----------|---------|
|
||||||
| `0` | Guard passed; would-write payload is AOC compliant. |
|
| `0` | Guard passed; would-write payload is AOC compliant. |
|
||||||
| `11` | `ERR_AOC_001` – Forbidden field (`severity`, `cvss`, etc.) detected. |
|
| `11` | `ERR_AOC_001` – Forbidden field (`severity`, `cvss`, etc.) detected. |
|
||||||
| `12` | `ERR_AOC_002` – Merge attempt (multiple upstream sources fused). |
|
| `12` | `ERR_AOC_002` – Merge attempt (multiple upstream sources fused). |
|
||||||
| `13` | `ERR_AOC_003` – Idempotency violation (duplicate without supersedes). |
|
| `13` | `ERR_AOC_003` – Idempotency violation (duplicate without supersedes). |
|
||||||
| `14` | `ERR_AOC_004` – Missing provenance fields. |
|
| `14` | `ERR_AOC_004` – Missing provenance fields. |
|
||||||
| `15` | `ERR_AOC_005` – Signature/checksum mismatch. |
|
| `15` | `ERR_AOC_005` – Signature/checksum mismatch. |
|
||||||
| `16` | `ERR_AOC_006` – Effective findings present (Policy-only data). |
|
| `16` | `ERR_AOC_006` – Effective findings present (Policy-only data). |
|
||||||
| `17` | `ERR_AOC_007` – Unknown top-level fields / schema violation. |
|
| `17` | `ERR_AOC_007` – Unknown top-level fields / schema violation. |
|
||||||
| `70` | Transport error (network, auth, malformed input). |
|
| `70` | Transport error (network, auth, malformed input). |
|
||||||
|
|
||||||
> Exit codes map directly to the `ERR_AOC_00x` table for scripting consistency. Multiple violations yield the highest-priority code (e.g., 11 takes precedence over 14).
|
> Exit codes map directly to the `ERR_AOC_00x` table for scripting consistency. Multiple violations yield the highest-priority code (e.g., 11 takes precedence over 14).
|
||||||
|
|
||||||
### 2.6 Examples
|
### 2.6 Examples
|
||||||
|
|
||||||
Dry-run a local CSAF file:
|
Dry-run a local CSAF file:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
stella sources ingest --dry-run \
|
stella sources ingest --dry-run \
|
||||||
--source redhat \
|
--source redhat \
|
||||||
--input ./fixtures/redhat/RHSA-2025-1234.json
|
--input ./fixtures/redhat/RHSA-2025-1234.json
|
||||||
```
|
```
|
||||||
|
|
||||||
Stream from HTTPS and emit JSON for CI:
|
Stream from HTTPS and emit JSON for CI:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
stella sources ingest --dry-run \
|
stella sources ingest --dry-run \
|
||||||
--source osv \
|
--source osv \
|
||||||
--input https://osv.dev/vulnerability/GHSA-aaaa-bbbb \
|
--input https://osv.dev/vulnerability/GHSA-aaaa-bbbb \
|
||||||
--format json \
|
--format json \
|
||||||
--output artifacts/osv-dry-run.json
|
--output artifacts/osv-dry-run.json
|
||||||
|
|
||||||
cat artifacts/osv-dry-run.json | jq '.violations'
|
cat artifacts/osv-dry-run.json | jq '.violations'
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2.7 Offline notes
|
### 2.7 Offline notes
|
||||||
|
|
||||||
When operating in sealed/offline mode:
|
When operating in sealed/offline mode:
|
||||||
|
|
||||||
- Use `--input` paths pointing to Offline Kit snapshots (`offline-kit/advisories/*.json`).
|
- Use `--input` paths pointing to Offline Kit snapshots (`offline-kit/advisories/*.json`).
|
||||||
- Provide `--tenant` explicitly if the offline bundle contains multiple tenants.
|
- Provide `--tenant` explicitly if the offline bundle contains multiple tenants.
|
||||||
- The command does not attempt network access when given a file path.
|
- The command does not attempt network access when given a file path.
|
||||||
- Store reports with `--output` to include in transfer packages for policy review.
|
- Store reports with `--output` to include in transfer packages for policy review.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 3 · `stella aoc verify`
|
## 3 · `stella aoc verify`
|
||||||
|
|
||||||
### 3.1 Synopsis
|
### 3.1 Synopsis
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
stella aoc verify \
|
stella aoc verify \
|
||||||
[--since <iso8601|duration>] \
|
[--since <iso8601|duration>] \
|
||||||
[--limit <count>] \
|
[--limit <count>] \
|
||||||
[--sources <list>] \
|
[--sources <list>] \
|
||||||
[--codes <ERR_AOC_00x,...>] \
|
[--codes <ERR_AOC_00x,...>] \
|
||||||
[--format table|json] \
|
[--format table|json] \
|
||||||
[--export <file>] \
|
[--export <file>] \
|
||||||
[--tenant <tenant-id>] \
|
[--tenant <tenant-id>] \
|
||||||
[--no-color]
|
[--no-color]
|
||||||
```
|
```
|
||||||
|
|
||||||
### 3.2 Description
|
### 3.2 Description
|
||||||
|
|
||||||
Replays the AOC guard against stored raw documents. By default it checks all advisories and VEX statements ingested in the last 24 hours for the active tenant, reporting totals, top violation codes, and sample documents. Use it in CI pipelines, scheduled verifications, or during incident response.
|
Replays the AOC guard against stored raw documents. By default it checks all advisories and VEX statements ingested in the last 24 hours for the active tenant, reporting totals, top violation codes, and sample documents. Use it in CI pipelines, scheduled verifications, or during incident response.
|
||||||
|
|
||||||
### 3.3 Options
|
### 3.3 Options
|
||||||
|
|
||||||
| Option | Description |
|
| Option | Description |
|
||||||
|--------|-------------|
|
|--------|-------------|
|
||||||
| `--since <value>` | Verification window. Accepts ISO 8601 timestamp (`2025-10-25T12:00:00Z`) or duration (`48h`, `7d`). Defaults to `24h`. |
|
| `--since <value>` | Verification window. Accepts ISO 8601 timestamp (`2025-10-25T12:00:00Z`) or duration (`48h`, `7d`). Defaults to `24h`. |
|
||||||
| `--limit <count>` | Maximum number of violations to display (per code). `0` means show all. Defaults to `20`. |
|
| `--limit <count>` | Maximum number of violations to display (per code). `0` means show all. Defaults to `20`. |
|
||||||
| `--sources <list>` | Comma-separated list of sources (`redhat,ubuntu,osv`). Filters both advisories and VEX entries. |
|
| `--sources <list>` | Comma-separated list of sources (`redhat,ubuntu,osv`). Filters both advisories and VEX entries. |
|
||||||
| `--codes <list>` | Restricts output to specific `ERR_AOC_00x` codes. Useful for regression tracking. |
|
| `--codes <list>` | Restricts output to specific `ERR_AOC_00x` codes. Useful for regression tracking. |
|
||||||
| `--format table|json` | `table` (default) prints summary plus top violations; `json` outputs machine-readable report identical to the `/aoc/verify` API. |
|
| `--format table|json` | `table` (default) prints summary plus top violations; `json` outputs machine-readable report identical to the `/aoc/verify` API. |
|
||||||
| `--export <file>` | Writes the JSON report to disk (useful for audits/offline uploads). |
|
| `--export <file>` | Writes the JSON report to disk (useful for audits/offline uploads). |
|
||||||
| `--tenant <tenant-id>` | Overrides tenant context. Required for cross-tenant verifications when run by platform operators. |
|
| `--tenant <tenant-id>` | Overrides tenant context. Required for cross-tenant verifications when run by platform operators. |
|
||||||
| `--no-color` | Disables ANSI colours. |
|
| `--no-color` | Disables ANSI colours. |
|
||||||
|
|
||||||
`table` mode prints a summary showing the active tenant, evaluated window, counts of checked advisories/VEX statements, the active limit, total writes/violations, and whether the page was truncated. Status is colour-coded as `ok`, `violations`, or `truncated`. When violations exist the detail table lists the code, total occurrences, first sample document (`source` + `documentId` + `contentHash`), and JSON pointer path.
|
`table` mode prints a summary showing the active tenant, evaluated window, counts of checked advisories/VEX statements, the active limit, total writes/violations, and whether the page was truncated. Status is colour-coded as `ok`, `violations`, or `truncated`. When violations exist the detail table lists the code, total occurrences, first sample document (`source` + `documentId` + `contentHash`), and JSON pointer path.
|
||||||
|
|
||||||
### 3.4 Report structure (JSON)
|
### 3.4 Report structure (JSON)
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"tenant": "default",
|
"tenant": "default",
|
||||||
"window": {
|
"window": {
|
||||||
"from": "2025-10-25T12:00:00Z",
|
"from": "2025-10-25T12:00:00Z",
|
||||||
"to": "2025-10-26T12:00:00Z"
|
"to": "2025-10-26T12:00:00Z"
|
||||||
},
|
},
|
||||||
"checked": {
|
"checked": {
|
||||||
"advisories": 482,
|
"advisories": 482,
|
||||||
"vex": 75
|
"vex": 75
|
||||||
},
|
},
|
||||||
"violations": [
|
"violations": [
|
||||||
{
|
{
|
||||||
"code": "ERR_AOC_001",
|
"code": "ERR_AOC_001",
|
||||||
"count": 2,
|
"count": 2,
|
||||||
"examples": [
|
"examples": [
|
||||||
{
|
{
|
||||||
"source": "redhat",
|
"source": "redhat",
|
||||||
"documentId": "advisory_raw:redhat:RHSA-2025:1",
|
"documentId": "advisory_raw:redhat:RHSA-2025:1",
|
||||||
"contentHash": "sha256:…",
|
"contentHash": "sha256:…",
|
||||||
"path": "/content/raw/cvss"
|
"path": "/content/raw/cvss"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"metrics": {
|
"metrics": {
|
||||||
"ingestion_write_total": 557,
|
"ingestion_write_total": 557,
|
||||||
"aoc_violation_total": 2
|
"aoc_violation_total": 2
|
||||||
},
|
},
|
||||||
"truncated": false
|
"truncated": false
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### 3.5 Exit codes
|
### 3.5 Exit codes
|
||||||
|
|
||||||
| Exit code | Meaning |
|
| Exit code | Meaning |
|
||||||
|-----------|---------|
|
|-----------|---------|
|
||||||
| `0` | Verification succeeded with zero violations. |
|
| `0` | Verification succeeded with zero violations. |
|
||||||
| `11…17` | Same mapping as § 2.5 when violations are detected. Highest-priority code returned. |
|
| `11…17` | Same mapping as § 2.5 when violations are detected. Highest-priority code returned. |
|
||||||
| `18` | Verification ran but results truncated (limit reached) – treat as warning; rerun with higher `--limit`. |
|
| `18` | Verification ran but results truncated (limit reached) – treat as warning; rerun with higher `--limit`. |
|
||||||
| `70` | Transport/authentication error. |
|
| `70` | Transport/authentication error. |
|
||||||
| `71` | CLI misconfiguration (missing tenant, invalid `--since`, etc.). |
|
| `71` | CLI misconfiguration (missing tenant, invalid `--since`, etc.). |
|
||||||
|
|
||||||
### 3.6 Examples
|
### 3.6 Examples
|
||||||
|
|
||||||
Daily verification across all sources:
|
Daily verification across all sources:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
stella aoc verify --since 24h --format table
|
stella aoc verify --since 24h --format table
|
||||||
```
|
```
|
||||||
|
|
||||||
CI pipeline focusing on errant sources and exporting evidence:
|
CI pipeline focusing on errant sources and exporting evidence:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
stella aoc verify \
|
stella aoc verify \
|
||||||
--sources redhat,ubuntu \
|
--sources redhat,ubuntu \
|
||||||
--codes ERR_AOC_001,ERR_AOC_004 \
|
--codes ERR_AOC_001,ERR_AOC_004 \
|
||||||
--format json \
|
--format json \
|
||||||
--limit 100 \
|
--limit 100 \
|
||||||
--export artifacts/aoc-verify.json
|
--export artifacts/aoc-verify.json
|
||||||
|
|
||||||
jq '.violations[] | {code, count}' artifacts/aoc-verify.json
|
jq '.violations[] | {code, count}' artifacts/aoc-verify.json
|
||||||
```
|
```
|
||||||
|
|
||||||
Air-gapped verification using Offline Kit snapshot (example script):
|
Air-gapped verification using Offline Kit snapshot (example script):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
stella aoc verify \
|
stella aoc verify \
|
||||||
--since 7d \
|
--since 7d \
|
||||||
--format json \
|
--format json \
|
||||||
--export /mnt/offline/aoc-verify-$(date +%F).json
|
--export /mnt/offline/aoc-verify-$(date +%F).json
|
||||||
|
|
||||||
sha256sum /mnt/offline/aoc-verify-*.json > /mnt/offline/checksums.txt
|
sha256sum /mnt/offline/aoc-verify-*.json > /mnt/offline/checksums.txt
|
||||||
```
|
```
|
||||||
|
|
||||||
### 3.7 Automation tips
|
### 3.7 Automation tips
|
||||||
|
|
||||||
- Schedule with `cron` or platform scheduler and fail the job when exit code ≥ 11.
|
- Schedule with `cron` or platform scheduler and fail the job when exit code ≥ 11.
|
||||||
- Pair with `stella sources ingest --dry-run` for pre-flight validation before re-enabling a paused source.
|
- Pair with `stella sources ingest --dry-run` for pre-flight validation before re-enabling a paused source.
|
||||||
- Push JSON exports to observability pipelines for historical tracking of violation counts.
|
- Push JSON exports to observability pipelines for historical tracking of violation counts.
|
||||||
|
|
||||||
### 3.8 Offline notes
|
### 3.8 Offline notes
|
||||||
|
|
||||||
- Works against Offline Kit Mongo snapshots when CLI is pointed at the local API gateway included in the bundle.
|
- Works against Offline Kit Mongo snapshots when CLI is pointed at the local API gateway included in the bundle.
|
||||||
- When fully disconnected, run against exported `aoc verify` reports generated on production and replay them using `--format json --export` (automation recipe above).
|
- When fully disconnected, run against exported `aoc verify` reports generated on production and replay them using `--format json --export` (automation recipe above).
|
||||||
- Include verification output in compliance packages alongside Offline Kit manifests.
|
- Include verification output in compliance packages alongside Offline Kit manifests.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 4 · Global exit-code reference
|
## 4 · Global exit-code reference
|
||||||
|
|
||||||
| Code | Summary |
|
| Code | Summary |
|
||||||
|------|---------|
|
|------|---------|
|
||||||
| `0` | Success / no violations. |
|
| `0` | Success / no violations. |
|
||||||
| `11` | `ERR_AOC_001` – Forbidden field present. |
|
| `11` | `ERR_AOC_001` – Forbidden field present. |
|
||||||
| `12` | `ERR_AOC_002` – Merge attempt detected. |
|
| `12` | `ERR_AOC_002` – Merge attempt detected. |
|
||||||
| `13` | `ERR_AOC_003` – Idempotency violation. |
|
| `13` | `ERR_AOC_003` – Idempotency violation. |
|
||||||
| `14` | `ERR_AOC_004` – Missing provenance/signature metadata. |
|
| `14` | `ERR_AOC_004` – Missing provenance/signature metadata. |
|
||||||
| `15` | `ERR_AOC_005` – Signature/checksum mismatch. |
|
| `15` | `ERR_AOC_005` – Signature/checksum mismatch. |
|
||||||
| `16` | `ERR_AOC_006` – Effective findings in ingestion payload. |
|
| `16` | `ERR_AOC_006` – Effective findings in ingestion payload. |
|
||||||
| `17` | `ERR_AOC_007` – Schema violation / unknown fields. |
|
| `17` | `ERR_AOC_007` – Schema violation / unknown fields. |
|
||||||
| `18` | Partial verification (limit reached). |
|
| `18` | Partial verification (limit reached). |
|
||||||
| `70` | Transport or HTTP failure. |
|
| `70` | Transport or HTTP failure. |
|
||||||
| `71` | CLI usage error (invalid arguments, missing tenant). |
|
| `71` | CLI usage error (invalid arguments, missing tenant). |
|
||||||
|
|
||||||
Use these codes in CI to map outcomes to build statuses or alert severities.
|
Use these codes in CI to map outcomes to build statuses or alert severities.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 4 · `stella vuln observations` (Overlay paging)
|
## 4 · `stella vuln observations` (Overlay paging)
|
||||||
|
|
||||||
`stella vuln observations` lists raw advisory observations for downstream overlays (Graph Explorer, Policy simulations, Console). Large tenants can now page through results deterministically.
|
`stella vuln observations` lists raw advisory observations for downstream overlays (Graph Explorer, Policy simulations, Console). Large tenants can now page through results deterministically.
|
||||||
|
|
||||||
| Option | Description |
|
| Option | Description |
|
||||||
|--------|-------------|
|
|--------|-------------|
|
||||||
| `--limit <count>` | Caps the number of observations returned in a single call. Defaults to `200`; values above `500` are clamped server-side. |
|
| `--limit <count>` | Caps the number of observations returned in a single call. Defaults to `200`; values above `500` are clamped server-side. |
|
||||||
| `--cursor <token>` | Opaque continuation token produced by the previous page (`nextCursor` in JSON output). Pass it back to resume iteration. |
|
| `--cursor <token>` | Opaque continuation token produced by the previous page (`nextCursor` in JSON output). Pass it back to resume iteration. |
|
||||||
|
|
||||||
Additional notes:
|
Additional notes:
|
||||||
|
|
||||||
- Table mode prints a hint when `hasMore` is `true`:
|
- Table mode prints a hint when `hasMore` is `true`:
|
||||||
`[yellow]More observations available. Continue with --cursor <token>[/]`.
|
`[yellow]More observations available. Continue with --cursor <token>[/]`.
|
||||||
- JSON mode returns `nextCursor` and `hasMore` alongside the observation list so automation can loop until `hasMore` is `false`.
|
- JSON mode returns `nextCursor` and `hasMore` alongside the observation list so automation can loop until `hasMore` is `false`.
|
||||||
- Supplying a non-positive limit falls back to the default (`200`). Invalid/expired cursors yield `400 Bad Request`; restart without `--cursor` to begin a fresh iteration.
|
- Supplying a non-positive limit falls back to the default (`200`). Invalid/expired cursors yield `400 Bad Request`; restart without `--cursor` to begin a fresh iteration.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 5 · Related references
|
## 5 · Related references
|
||||||
|
|
||||||
- [Aggregation-Only Contract reference](../../../ingestion/aggregation-only-contract.md)
|
- [Aggregation-Only Contract reference](../../../ingestion/aggregation-only-contract.md)
|
||||||
- [Architecture overview](../../platform/architecture-overview.md)
|
- [Architecture overview](../../platform/architecture-overview.md)
|
||||||
- [Console AOC dashboard](../../../ui/console.md)
|
- [Console AOC dashboard](../../../ui/console.md)
|
||||||
- [Authority scopes](../../authority/architecture.md)
|
- [Authority scopes](../../authority/architecture.md)
|
||||||
- [Task Pack CLI profiles](./packs-profiles.md)
|
- [Task Pack CLI profiles](./packs-profiles.md)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 6 · Compliance checklist
|
## 6 · Compliance checklist
|
||||||
|
|
||||||
- [ ] Usage documented for both table and JSON formats.
|
- [ ] Usage documented for both table and JSON formats.
|
||||||
@@ -320,7 +320,7 @@ All publish/promote operations require interactive identities with `policy:publi
|
|||||||
*Last updated: 2025-11-03 (Sprint 100).*
|
*Last updated: 2025-11-03 (Sprint 100).*
|
||||||
|
|
||||||
## 13. Authority configuration quick reference
|
## 13. Authority configuration quick reference
|
||||||
|
|
||||||
| Setting | Purpose | How to set |
|
| Setting | Purpose | How to set |
|
||||||
|---------|---------|------------|
|
|---------|---------|------------|
|
||||||
| `StellaOps:Authority:OperatorReason` | Incident/change description recorded with `orch:operate` tokens. | CLI flag `--Authority:OperatorReason=...` or env `STELLAOPS_ORCH_REASON`. |
|
| `StellaOps:Authority:OperatorReason` | Incident/change description recorded with `orch:operate` tokens. | CLI flag `--Authority:OperatorReason=...` or env `STELLAOPS_ORCH_REASON`. |
|
||||||
@@ -332,4 +332,4 @@ All publish/promote operations require interactive identities with `policy:publi
|
|||||||
| `StellaOps:Authority:Scope` | Default scope string requested during `stella auth login`. | CLI flag `--Authority:Scope=\"packs.read packs.run\"` or env `STELLAOPS_AUTHORITY_SCOPE`; see `docs/modules/cli/guides/packs-profiles.md` for common Task Pack profiles. |
|
| `StellaOps:Authority:Scope` | Default scope string requested during `stella auth login`. | CLI flag `--Authority:Scope=\"packs.read packs.run\"` or env `STELLAOPS_AUTHORITY_SCOPE`; see `docs/modules/cli/guides/packs-profiles.md` for common Task Pack profiles. |
|
||||||
|
|
||||||
> Tokens requesting `orch:operate` fail with `invalid_request` unless both operator values are present. `orch:quota` tokens require `quota_reason` (≤256 chars) and accept an optional `quota_ticket` (≤128 chars). `orch:backfill` tokens require both `backfill_reason` (≤256 chars) and `backfill_ticket` (≤128 chars). Avoid embedding secrets in any value.
|
> Tokens requesting `orch:operate` fail with `invalid_request` unless both operator values are present. `orch:quota` tokens require `quota_reason` (≤256 chars) and accept an optional `quota_ticket` (≤128 chars). `orch:backfill` tokens require both `backfill_reason` (≤256 chars) and `backfill_ticket` (≤128 chars). Avoid embedding secrets in any value.
|
||||||
|
|
||||||
|
|||||||
@@ -1,35 +1,35 @@
|
|||||||
# DevOps agent guide
|
# DevOps agent guide
|
||||||
|
|
||||||
## Mission
|
## Mission
|
||||||
The DevOps module captures release, deployment, and migration playbooks that keep StellaOps deterministic across environments.
|
The DevOps module captures release, deployment, and migration playbooks that keep StellaOps deterministic across environments.
|
||||||
|
|
||||||
## Key docs
|
## Key docs
|
||||||
- [Module README](./README.md)
|
- [Module README](./README.md)
|
||||||
- [Architecture](./architecture.md)
|
- [Architecture](./architecture.md)
|
||||||
- [Implementation plan](./implementation_plan.md)
|
- [Implementation plan](./implementation_plan.md)
|
||||||
- [Task board](./TASKS.md)
|
- [Task board](./TASKS.md)
|
||||||
- [Task Runner simulation notes](./task-runner-simulation.md)
|
- [Task Runner simulation notes](./task-runner-simulation.md)
|
||||||
|
|
||||||
## How to get started
|
## How to get started
|
||||||
1. Open ../../implplan/SPRINTS.md and locate the stories referencing this module.
|
1. Open ../../implplan/SPRINTS.md and locate the stories referencing this module.
|
||||||
2. Review ./TASKS.md for local follow-ups and confirm status transitions (TODO → DOING → DONE/BLOCKED).
|
2. Review ./TASKS.md for local follow-ups and confirm status transitions (TODO → DOING → DONE/BLOCKED).
|
||||||
3. Read the architecture and README for domain context before editing code or docs.
|
3. Read the architecture and README for domain context before editing code or docs.
|
||||||
4. Coordinate cross-module changes in the main /AGENTS.md description and through the sprint plan.
|
4. Coordinate cross-module changes in the main /AGENTS.md description and through the sprint plan.
|
||||||
|
|
||||||
## Guardrails
|
## Guardrails
|
||||||
- Honour the Aggregation-Only Contract where applicable (see ../../ingestion/aggregation-only-contract.md).
|
- Honour the Aggregation-Only Contract where applicable (see ../../ingestion/aggregation-only-contract.md).
|
||||||
- Preserve determinism: sort outputs, normalise timestamps (UTC ISO-8601), and avoid machine-specific artefacts.
|
- Preserve determinism: sort outputs, normalise timestamps (UTC ISO-8601), and avoid machine-specific artefacts.
|
||||||
- Keep Offline Kit parity in mind—document air-gapped workflows for any new feature.
|
- Keep Offline Kit parity in mind—document air-gapped workflows for any new feature.
|
||||||
- Update runbooks/observability assets when operational characteristics change.
|
- Update runbooks/observability assets when operational characteristics change.
|
||||||
## Required Reading
|
## Required Reading
|
||||||
- `docs/modules/devops/README.md`
|
- `docs/modules/devops/README.md`
|
||||||
- `docs/modules/devops/architecture.md`
|
- `docs/modules/devops/architecture.md`
|
||||||
- `docs/modules/devops/implementation_plan.md`
|
- `docs/modules/devops/implementation_plan.md`
|
||||||
- `docs/modules/platform/architecture-overview.md`
|
- `docs/modules/platform/architecture-overview.md`
|
||||||
|
|
||||||
## Working Agreement
|
## Working Agreement
|
||||||
- 1. Update task status to `DOING`/`DONE` in both `docs/implplan/SPRINTS.md` and the local `TASKS.md` when you start or finish work.
|
- 1. Update task status to `DOING`/`DONE` in both `docs/implplan/SPRINTS.md` and the local `TASKS.md` when you start or finish work.
|
||||||
- 2. Review this charter and the Required Reading documents before coding; confirm prerequisites are met.
|
- 2. Review this charter and the Required Reading documents before coding; confirm prerequisites are met.
|
||||||
- 3. Keep changes deterministic (stable ordering, timestamps, hashes) and align with offline/air-gap expectations.
|
- 3. Keep changes deterministic (stable ordering, timestamps, hashes) and align with offline/air-gap expectations.
|
||||||
- 4. Coordinate doc updates, tests, and cross-guild communication whenever contracts or workflows change.
|
- 4. Coordinate doc updates, tests, and cross-guild communication whenever contracts or workflows change.
|
||||||
- 5. Revert to `TODO` if you pause the task without shipping changes; leave notes in commit/PR descriptions for context.
|
- 5. Revert to `TODO` if you pause the task without shipping changes; leave notes in commit/PR descriptions for context.
|
||||||
|
|||||||
@@ -1,42 +1,42 @@
|
|||||||
# StellaOps DevOps
|
# StellaOps DevOps
|
||||||
|
|
||||||
The DevOps module captures release, deployment, and migration playbooks that keep StellaOps deterministic across environments.
|
The DevOps module captures release, deployment, and migration playbooks that keep StellaOps deterministic across environments.
|
||||||
|
|
||||||
## Responsibilities
|
## Responsibilities
|
||||||
- Maintain CI pipelines, signing workflows, and release packaging steps.
|
- Maintain CI pipelines, signing workflows, and release packaging steps.
|
||||||
- Operate shared runbooks for launch readiness, upgrades, and NuGet previews.
|
- Operate shared runbooks for launch readiness, upgrades, and NuGet previews.
|
||||||
- Provide offline kit assembly instructions and tooling integration.
|
- Provide offline kit assembly instructions and tooling integration.
|
||||||
- Wrap observability/telemetry bootstrap flows for platform teams.
|
- Wrap observability/telemetry bootstrap flows for platform teams.
|
||||||
|
|
||||||
## Key components
|
## Key components
|
||||||
- Runbooks under ./runbooks/ (launch, deployment, nuget).
|
- Runbooks under ./runbooks/ (launch, deployment, nuget).
|
||||||
- Migration guidance under ./migrations/.
|
- Migration guidance under ./migrations/.
|
||||||
- Architecture overview bridging CI/CD & infrastructure concerns.
|
- Architecture overview bridging CI/CD & infrastructure concerns.
|
||||||
|
|
||||||
## Integrations & dependencies
|
## Integrations & dependencies
|
||||||
- Ops pipelines (Gitea, GitHub Actions) and artifact registries.
|
- Ops pipelines (Gitea, GitHub Actions) and artifact registries.
|
||||||
- Authority/Signer for supply chain signing.
|
- Authority/Signer for supply chain signing.
|
||||||
- Telemetry stack bootstrap scripts.
|
- Telemetry stack bootstrap scripts.
|
||||||
|
|
||||||
## Operational notes
|
## Operational notes
|
||||||
- Offline bundle packaging guidance in docs/modules/export-center/operations/runbook.md.
|
- Offline bundle packaging guidance in docs/modules/export-center/operations/runbook.md.
|
||||||
- Dashboards for launch cutover rehearsals.
|
- Dashboards for launch cutover rehearsals.
|
||||||
- Coordination with Security for enforced guardrails.
|
- Coordination with Security for enforced guardrails.
|
||||||
|
|
||||||
## Related resources
|
## Related resources
|
||||||
- ./runbooks/launch-readiness.md
|
- ./runbooks/launch-readiness.md
|
||||||
- ./runbooks/launch-cutover.md
|
- ./runbooks/launch-cutover.md
|
||||||
- ./runbooks/deployment-upgrade.md
|
- ./runbooks/deployment-upgrade.md
|
||||||
- ./runbooks/nuget-preview-bootstrap.md
|
- ./runbooks/nuget-preview-bootstrap.md
|
||||||
- ./migrations/semver-style.md
|
- ./migrations/semver-style.md
|
||||||
- ./task-runner-simulation.md
|
- ./task-runner-simulation.md
|
||||||
|
|
||||||
## Backlog references
|
## Backlog references
|
||||||
- DEVOPS-LAUNCH-18-001 / 18-900 runbooks in ../../TASKS.md.
|
- DEVOPS-LAUNCH-18-001 / 18-900 runbooks in ../../TASKS.md.
|
||||||
- Telemetry bootstrap automation tracked in `ops/devops/TASKS.md`.
|
- Telemetry bootstrap automation tracked in `ops/devops/TASKS.md`.
|
||||||
|
|
||||||
## Epic alignment
|
## Epic alignment
|
||||||
- **Epic 1 – AOC enforcement:** bake AOC verifier steps, CI guards, and schema validation into pipelines.
|
- **Epic 1 – AOC enforcement:** bake AOC verifier steps, CI guards, and schema validation into pipelines.
|
||||||
- **Epic 9 – Orchestrator Dashboard:** support operational dashboards, job recovery runbooks, and rate-limit governance.
|
- **Epic 9 – Orchestrator Dashboard:** support operational dashboards, job recovery runbooks, and rate-limit governance.
|
||||||
- **Epic 10 – Export Center:** manage signing workflows, Offline Kit packaging, and release promotion for exports.
|
- **Epic 10 – Export Center:** manage signing workflows, Offline Kit packaging, and release promotion for exports.
|
||||||
- **Epic 15 – Observability & Forensics:** coordinate telemetry deployment, evidence retention, and forensic automation.
|
- **Epic 15 – Observability & Forensics:** coordinate telemetry deployment, evidence retention, and forensic automation.
|
||||||
|
|||||||
@@ -1,139 +1,139 @@
|
|||||||
# StellaOps Architecture Overview (Sprint 19)
|
# StellaOps Architecture Overview (Sprint 19)
|
||||||
|
|
||||||
> **Ownership:** Architecture Guild • Docs Guild
|
> **Ownership:** Architecture Guild • Docs Guild
|
||||||
> **Audience:** Service owners, platform engineers, solution architects
|
> **Audience:** Service owners, platform engineers, solution architects
|
||||||
> **Related:** [High-Level Architecture](../../07_HIGH_LEVEL_ARCHITECTURE.md), [Concelier Architecture](../concelier/architecture.md), [Policy Engine Architecture](../policy/architecture.md), [Aggregation-Only Contract](../../ingestion/aggregation-only-contract.md)
|
> **Related:** [High-Level Architecture](../../07_HIGH_LEVEL_ARCHITECTURE.md), [Concelier Architecture](../concelier/architecture.md), [Policy Engine Architecture](../policy/architecture.md), [Aggregation-Only Contract](../../ingestion/aggregation-only-contract.md)
|
||||||
|
|
||||||
This dossier summarises the end-to-end runtime topology after the Aggregation-Only Contract (AOC) rollout. It highlights where raw facts live, how ingest services enforce guardrails, and how downstream components consume those facts to derive policy decisions and user-facing experiences.
|
This dossier summarises the end-to-end runtime topology after the Aggregation-Only Contract (AOC) rollout. It highlights where raw facts live, how ingest services enforce guardrails, and how downstream components consume those facts to derive policy decisions and user-facing experiences.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 1 · System landscape
|
## 1 · System landscape
|
||||||
|
|
||||||
```mermaid
|
```mermaid
|
||||||
graph TD
|
graph TD
|
||||||
subgraph Edge["Clients & Automation"]
|
subgraph Edge["Clients & Automation"]
|
||||||
CLI[stella CLI]
|
CLI[stella CLI]
|
||||||
UI[Console SPA]
|
UI[Console SPA]
|
||||||
APIClients[CI / API Clients]
|
APIClients[CI / API Clients]
|
||||||
end
|
end
|
||||||
Gateway[API Gateway<br/>(JWT + DPoP scopes)]
|
Gateway[API Gateway<br/>(JWT + DPoP scopes)]
|
||||||
subgraph Scanner["Fact Collection"]
|
subgraph Scanner["Fact Collection"]
|
||||||
ScannerWeb[Scanner.WebService]
|
ScannerWeb[Scanner.WebService]
|
||||||
ScannerWorkers[Scanner.Workers]
|
ScannerWorkers[Scanner.Workers]
|
||||||
Agent[Agent Runtime]
|
Agent[Agent Runtime]
|
||||||
end
|
end
|
||||||
subgraph Ingestion["Aggregation-Only Ingestion (AOC)"]
|
subgraph Ingestion["Aggregation-Only Ingestion (AOC)"]
|
||||||
Concelier[Concelier.WebService]
|
Concelier[Concelier.WebService]
|
||||||
Excititor[Excititor.WebService]
|
Excititor[Excititor.WebService]
|
||||||
RawStore[(MongoDB<br/>advisory_raw / vex_raw)]
|
RawStore[(MongoDB<br/>advisory_raw / vex_raw)]
|
||||||
end
|
end
|
||||||
subgraph Derivation["Policy & Overlay"]
|
subgraph Derivation["Policy & Overlay"]
|
||||||
Policy[Policy Engine]
|
Policy[Policy Engine]
|
||||||
Scheduler[Scheduler Services]
|
Scheduler[Scheduler Services]
|
||||||
Notify[Notifier]
|
Notify[Notifier]
|
||||||
end
|
end
|
||||||
subgraph Experience["UX & Export"]
|
subgraph Experience["UX & Export"]
|
||||||
UIService[Console Backend]
|
UIService[Console Backend]
|
||||||
Exporters[Export / Offline Kit]
|
Exporters[Export / Offline Kit]
|
||||||
end
|
end
|
||||||
Observability[Telemetry Stack]
|
Observability[Telemetry Stack]
|
||||||
|
|
||||||
CLI --> Gateway
|
CLI --> Gateway
|
||||||
UI --> Gateway
|
UI --> Gateway
|
||||||
APIClients --> Gateway
|
APIClients --> Gateway
|
||||||
Gateway --> ScannerWeb
|
Gateway --> ScannerWeb
|
||||||
ScannerWeb --> ScannerWorkers
|
ScannerWeb --> ScannerWorkers
|
||||||
ScannerWorkers --> Concelier
|
ScannerWorkers --> Concelier
|
||||||
ScannerWorkers --> Excititor
|
ScannerWorkers --> Excititor
|
||||||
Concelier --> RawStore
|
Concelier --> RawStore
|
||||||
Excititor --> RawStore
|
Excititor --> RawStore
|
||||||
RawStore --> Policy
|
RawStore --> Policy
|
||||||
Policy --> Scheduler
|
Policy --> Scheduler
|
||||||
Policy --> Notify
|
Policy --> Notify
|
||||||
Policy --> UIService
|
Policy --> UIService
|
||||||
Scheduler --> UIService
|
Scheduler --> UIService
|
||||||
UIService --> Exporters
|
UIService --> Exporters
|
||||||
Exporters --> CLI
|
Exporters --> CLI
|
||||||
Exporters --> Offline[Offline Kit]
|
Exporters --> Offline[Offline Kit]
|
||||||
Observability -.-> ScannerWeb
|
Observability -.-> ScannerWeb
|
||||||
Observability -.-> Concelier
|
Observability -.-> Concelier
|
||||||
Observability -.-> Excititor
|
Observability -.-> Excititor
|
||||||
Observability -.-> Policy
|
Observability -.-> Policy
|
||||||
Observability -.-> Scheduler
|
Observability -.-> Scheduler
|
||||||
Observability -.-> Notify
|
Observability -.-> Notify
|
||||||
```
|
```
|
||||||
|
|
||||||
Key boundaries:
|
Key boundaries:
|
||||||
|
|
||||||
- **AOC border.** Everything inside the Ingestion subgraph writes only immutable raw facts plus link hints. Derived severity, consensus, and risk remain outside the border.
|
- **AOC border.** Everything inside the Ingestion subgraph writes only immutable raw facts plus link hints. Derived severity, consensus, and risk remain outside the border.
|
||||||
- **Policy-only derivation.** Policy Engine materialises `effective_finding_*` collections and emits overlays; other services consume but never mutate them.
|
- **Policy-only derivation.** Policy Engine materialises `effective_finding_*` collections and emits overlays; other services consume but never mutate them.
|
||||||
- **Tenant enforcement.** Authority-issued DPoP scopes flow through Gateway to every service; raw stores and overlays include `tenant` strictly.
|
- **Tenant enforcement.** Authority-issued DPoP scopes flow through Gateway to every service; raw stores and overlays include `tenant` strictly.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 2 · Aggregation-Only Contract focus
|
## 2 · Aggregation-Only Contract focus
|
||||||
|
|
||||||
### 2.1 Responsibilities at the boundary
|
### 2.1 Responsibilities at the boundary
|
||||||
|
|
||||||
| Area | Services | Responsibilities under AOC | Forbidden under AOC |
|
| Area | Services | Responsibilities under AOC | Forbidden under AOC |
|
||||||
|------|----------|-----------------------------|---------------------|
|
|------|----------|-----------------------------|---------------------|
|
||||||
| **Ingestion (Concelier / Excititor)** | `StellaOps.Concelier.WebService`, `StellaOps.Excititor.WebService` | Fetch upstream advisories/VEX, verify signatures, compute linksets, append immutable documents to `advisory_raw` / `vex_raw`, emit observability signals, expose raw read APIs. | Computing severity, consensus, suppressions, or policy hints; merging upstream sources into a single derived record; mutating existing documents. |
|
| **Ingestion (Concelier / Excititor)** | `StellaOps.Concelier.WebService`, `StellaOps.Excititor.WebService` | Fetch upstream advisories/VEX, verify signatures, compute linksets, append immutable documents to `advisory_raw` / `vex_raw`, emit observability signals, expose raw read APIs. | Computing severity, consensus, suppressions, or policy hints; merging upstream sources into a single derived record; mutating existing documents. |
|
||||||
| **Policy & Overlay** | `StellaOps.Policy.Engine`, Scheduler | Join SBOM inventory with raw advisories/VEX, evaluate policies, issue `effective_finding_*` overlays, drive remediation workflows. | Writing to raw collections; bypassing guard scopes; running without recorded provenance. |
|
| **Policy & Overlay** | `StellaOps.Policy.Engine`, Scheduler | Join SBOM inventory with raw advisories/VEX, evaluate policies, issue `effective_finding_*` overlays, drive remediation workflows. | Writing to raw collections; bypassing guard scopes; running without recorded provenance. |
|
||||||
| **Experience layers** | Console, CLI, Exporters | Surface raw facts + policy overlays; run `stella aoc verify`; render AOC dashboards and reports. | Accepting ingestion payloads that lack provenance or violate guard results. |
|
| **Experience layers** | Console, CLI, Exporters | Surface raw facts + policy overlays; run `stella aoc verify`; render AOC dashboards and reports. | Accepting ingestion payloads that lack provenance or violate guard results. |
|
||||||
|
|
||||||
### 2.2 Raw stores
|
### 2.2 Raw stores
|
||||||
|
|
||||||
| Collection | Purpose | Key fields | Notes |
|
| Collection | Purpose | Key fields | Notes |
|
||||||
|------------|---------|------------|-------|
|
|------------|---------|------------|-------|
|
||||||
| `advisory_raw` | Immutable vendor/ecosystem advisory documents. | `_id`, `tenant`, `source.*`, `upstream.*`, `content.raw`, `linkset`, `supersedes`. | Idempotent by `(source.vendor, upstream.upstream_id, upstream.content_hash)`. |
|
| `advisory_raw` | Immutable vendor/ecosystem advisory documents. | `_id`, `tenant`, `source.*`, `upstream.*`, `content.raw`, `linkset`, `supersedes`. | Idempotent by `(source.vendor, upstream.upstream_id, upstream.content_hash)`. |
|
||||||
| `vex_raw` | Immutable vendor VEX statements. | Mirrors `advisory_raw`; `identifiers.statements` summarises affected components. | Maintains supersedes chain identical to advisory flow. |
|
| `vex_raw` | Immutable vendor VEX statements. | Mirrors `advisory_raw`; `identifiers.statements` summarises affected components. | Maintains supersedes chain identical to advisory flow. |
|
||||||
| Change streams (`advisory_raw_stream`, `vex_raw_stream`) | Feed Policy Engine and Scheduler. | `operationType`, `documentKey`, `fullDocument`, `tenant`, `traceId`. | Scope filtered per tenant before delivery. |
|
| Change streams (`advisory_raw_stream`, `vex_raw_stream`) | Feed Policy Engine and Scheduler. | `operationType`, `documentKey`, `fullDocument`, `tenant`, `traceId`. | Scope filtered per tenant before delivery. |
|
||||||
|
|
||||||
### 2.3 Guarded ingestion sequence
|
### 2.3 Guarded ingestion sequence
|
||||||
|
|
||||||
```mermaid
|
```mermaid
|
||||||
sequenceDiagram
|
sequenceDiagram
|
||||||
participant Upstream as Upstream Source
|
participant Upstream as Upstream Source
|
||||||
participant Connector as Concelier/Excititor Connector
|
participant Connector as Concelier/Excititor Connector
|
||||||
participant Guard as AOCWriteGuard
|
participant Guard as AOCWriteGuard
|
||||||
participant Mongo as MongoDB (advisory_raw / vex_raw)
|
participant Mongo as MongoDB (advisory_raw / vex_raw)
|
||||||
participant Stream as Change Stream
|
participant Stream as Change Stream
|
||||||
participant Policy as Policy Engine
|
participant Policy as Policy Engine
|
||||||
|
|
||||||
Upstream-->>Connector: CSAF / OSV / VEX document
|
Upstream-->>Connector: CSAF / OSV / VEX document
|
||||||
Connector->>Connector: Normalize transport, compute content_hash
|
Connector->>Connector: Normalize transport, compute content_hash
|
||||||
Connector->>Guard: Candidate raw doc (source + upstream + content + linkset)
|
Connector->>Guard: Candidate raw doc (source + upstream + content + linkset)
|
||||||
Guard-->>Connector: ERR_AOC_00x on violation
|
Guard-->>Connector: ERR_AOC_00x on violation
|
||||||
Guard->>Mongo: Append immutable document (with tenant & supersedes)
|
Guard->>Mongo: Append immutable document (with tenant & supersedes)
|
||||||
Mongo-->>Stream: Change event (tenant scoped)
|
Mongo-->>Stream: Change event (tenant scoped)
|
||||||
Stream->>Policy: Raw delta payload
|
Stream->>Policy: Raw delta payload
|
||||||
Policy->>Policy: Evaluate policies, compute effective findings
|
Policy->>Policy: Evaluate policies, compute effective findings
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 2.4 Authority scopes & tenancy
|
### 2.4 Authority scopes & tenancy
|
||||||
|
|
||||||
| Scope | Holder | Purpose | Notes |
|
| Scope | Holder | Purpose | Notes |
|
||||||
|-------|--------|---------|-------|
|
|-------|--------|---------|-------|
|
||||||
| `advisory:ingest` / `vex:ingest` | Concelier / Excititor collectors | Append raw documents through ingestion endpoints. | Paired with tenant claims; requests without tenant are rejected. |
|
| `advisory:ingest` / `vex:ingest` | Concelier / Excititor collectors | Append raw documents through ingestion endpoints. | Paired with tenant claims; requests without tenant are rejected. |
|
||||||
| `advisory:read` / `vex:read` | DevOps verify identity, CLI | Run `stella aoc verify` or call `/aoc/verify`. | Read-only; cannot mutate raw docs. |
|
| `advisory:read` / `vex:read` | DevOps verify identity, CLI | Run `stella aoc verify` or call `/aoc/verify`. | Read-only; cannot mutate raw docs. |
|
||||||
| `effective:write` | Policy Engine | Materialise `effective_finding_*` overlays. | Only Policy Engine identity may hold; ingestion contexts receive `ERR_AOC_006` if they attempt. |
|
| `effective:write` | Policy Engine | Materialise `effective_finding_*` overlays. | Only Policy Engine identity may hold; ingestion contexts receive `ERR_AOC_006` if they attempt. |
|
||||||
| `findings:read` | Console, CLI, exports | Consume derived findings. | Enforced by Gateway and downstream services. |
|
| `findings:read` | Console, CLI, exports | Consume derived findings. | Enforced by Gateway and downstream services. |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 3 · Data & control flow highlights
|
## 3 · Data & control flow highlights
|
||||||
|
|
||||||
1. **Ingestion:** Concelier / Excititor connectors fetch upstream documents, compute linksets, and hand payloads to `AOCWriteGuard`. Guards validate schema, provenance, forbidden fields, supersedes pointers, and append-only rules before writing to Mongo.
|
1. **Ingestion:** Concelier / Excititor connectors fetch upstream documents, compute linksets, and hand payloads to `AOCWriteGuard`. Guards validate schema, provenance, forbidden fields, supersedes pointers, and append-only rules before writing to Mongo.
|
||||||
2. **Verification:** `stella aoc verify` (CLI/CI) and `/aoc/verify` endpoints replay guard checks against stored documents, mapping `ERR_AOC_00x` codes to exit codes for automation.
|
2. **Verification:** `stella aoc verify` (CLI/CI) and `/aoc/verify` endpoints replay guard checks against stored documents, mapping `ERR_AOC_00x` codes to exit codes for automation.
|
||||||
3. **Policy evaluation:** Mongo change streams deliver tenant-scoped raw deltas. Policy Engine joins SBOM inventory (via BOM Index), executes deterministic policies, writes overlays, and emits events to Scheduler/Notify.
|
3. **Policy evaluation:** Mongo change streams deliver tenant-scoped raw deltas. Policy Engine joins SBOM inventory (via BOM Index), executes deterministic policies, writes overlays, and emits events to Scheduler/Notify.
|
||||||
4. **Experience surfaces:** Console renders an AOC dashboard showing ingestion latency, guard violations, and supersedes depth. CLI exposes raw-document fetch helpers for auditing. Offline Kit bundles raw collections alongside guard configs to keep air-gapped installs verifiable.
|
4. **Experience surfaces:** Console renders an AOC dashboard showing ingestion latency, guard violations, and supersedes depth. CLI exposes raw-document fetch helpers for auditing. Offline Kit bundles raw collections alongside guard configs to keep air-gapped installs verifiable.
|
||||||
5. **Observability:** All services emit `ingestion_write_total`, `aoc_violation_total{code}`, `ingestion_latency_seconds`, and trace spans `ingest.fetch`, `ingest.transform`, `ingest.write`, `aoc.guard`. Logs correlate via `traceId`, `tenant`, `source.vendor`, and `content_hash`.
|
5. **Observability:** All services emit `ingestion_write_total`, `aoc_violation_total{code}`, `ingestion_latency_seconds`, and trace spans `ingest.fetch`, `ingest.transform`, `ingest.write`, `aoc.guard`. Logs correlate via `traceId`, `tenant`, `source.vendor`, and `content_hash`.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 4 · Offline & disaster readiness
|
## 4 · Offline & disaster readiness
|
||||||
|
|
||||||
- **Offline Kit:** Packages raw Mongo snapshots (`advisory_raw`, `vex_raw`) plus guard configuration and CLI verifier binaries so air-gapped sites can re-run AOC checks before promotion.
|
- **Offline Kit:** Packages raw Mongo snapshots (`advisory_raw`, `vex_raw`) plus guard configuration and CLI verifier binaries so air-gapped sites can re-run AOC checks before promotion.
|
||||||
@@ -177,10 +177,10 @@ sequenceDiagram
|
|||||||
- [ ] Mongo schema validators deployed for `advisory_raw` and `vex_raw`; change streams scoped per tenant.
|
- [ ] Mongo schema validators deployed for `advisory_raw` and `vex_raw`; change streams scoped per tenant.
|
||||||
- [ ] Authority scopes (`advisory:*`, `vex:*`, `effective:*`) configured in Gateway and validated via integration tests.
|
- [ ] Authority scopes (`advisory:*`, `vex:*`, `effective:*`) configured in Gateway and validated via integration tests.
|
||||||
- [ ] `stella aoc verify` wired into CI/CD pipelines with seeded violation fixtures.
|
- [ ] `stella aoc verify` wired into CI/CD pipelines with seeded violation fixtures.
|
||||||
- [ ] Console AOC dashboard and CLI documentation reference the new ingestion contract.
|
- [ ] Console AOC dashboard and CLI documentation reference the new ingestion contract.
|
||||||
- [ ] Offline Kit bundles include guard configs, verifier tooling, and documentation updates.
|
- [ ] Offline Kit bundles include guard configs, verifier tooling, and documentation updates.
|
||||||
- [ ] Observability dashboards include violation, latency, and supersedes depth metrics with alert thresholds.
|
- [ ] Observability dashboards include violation, latency, and supersedes depth metrics with alert thresholds.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
*Last updated: 2025-11-03 (Replay planning refresh).*
|
*Last updated: 2025-11-03 (Replay planning refresh).*
|
||||||
|
|||||||
@@ -179,9 +179,10 @@ Determinism guard instrumentation wraps the evaluator, rejecting access to forbi
|
|||||||
- **Queue:** Mongo queue with lease; each job assigned `leaseDuration`, `maxAttempts`.
|
- **Queue:** Mongo queue with lease; each job assigned `leaseDuration`, `maxAttempts`.
|
||||||
- **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.
|
- **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.
|
- **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.
|
- **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.
|
||||||
---
|
|
||||||
|
---
|
||||||
|
|
||||||
## 7 · Security & Tenancy
|
## 7 · Security & Tenancy
|
||||||
|
|
||||||
|
|||||||
@@ -112,7 +112,7 @@ Failures throw `SurfaceEnvironmentException` with error codes (`SURFACE_ENV_MISS
|
|||||||
## 6. Integration Guidance
|
## 6. Integration Guidance
|
||||||
|
|
||||||
- **Scanner Worker**: call `services.AddSurfaceEnvironment()` in `Program.cs` before registering analyzers. Pass `hostContext.Configuration.GetSection("Surface")` for overrides.
|
- **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.
|
- **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.
|
- **Scheduler Planner (future)**: treat Surface.Env as read-only input; do not mutate settings.
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
.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
|
## 4. Library Responsibilities
|
||||||
|
|
||||||
Surface.FS library for .NET hosts provides:
|
Surface.FS library for .NET hosts provides:
|
||||||
|
|||||||
@@ -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.
|
- See [`../findings-ledger/schema.md`](../findings-ledger/schema.md) for the canonical SQL schema, hashing strategy, and fixtures underpinning these collections.
|
||||||
|
|
||||||
- **Collections / tables**
|
- **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.
|
- `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.
|
- `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).
|
- `remediation_plans` – structured remediation steps (affected assets, deadlines, recommended fixes, auto-generated from SRM/AI hints).
|
||||||
@@ -17,7 +17,7 @@
|
|||||||
|
|
||||||
## 2) Triage workflow
|
## 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.
|
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.
|
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.
|
4. **Remediation tracking** – Link remediation plans, record progress, and integrate with Scheduler for follow-up scans once fixes deploy.
|
||||||
|
|||||||
@@ -1,78 +1,78 @@
|
|||||||
# Notifications Overview
|
# Notifications Overview
|
||||||
|
|
||||||
> **Imposed rule:** Work of this type or tasks of this type on this component must also be applied everywhere else it should be applied.
|
> **Imposed rule:** Work of this type or tasks of this type on this component must also be applied everywhere else it should be applied.
|
||||||
|
|
||||||
Notifications Studio turns raw platform events into concise, tenant-scoped alerts that reach the right responders without overwhelming them. The service is sovereign/offline-first, follows the Aggregation-Only Contract (AOC), and produces deterministic outputs so the same configuration yields identical deliveries across environments.
|
Notifications Studio turns raw platform events into concise, tenant-scoped alerts that reach the right responders without overwhelming them. The service is sovereign/offline-first, follows the Aggregation-Only Contract (AOC), and produces deterministic outputs so the same configuration yields identical deliveries across environments.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 1. Mission & value
|
## 1. Mission & value
|
||||||
|
|
||||||
- **Reduce noise.** Only materially new or high-impact changes reach chat, email, or webhooks thanks to rule filters, throttles, and digest windows.
|
- **Reduce noise.** Only materially new or high-impact changes reach chat, email, or webhooks thanks to rule filters, throttles, and digest windows.
|
||||||
- **Explainable results.** Every delivery is traceable back to a rule, action, and event payload stored in the delivery ledger; operators can audit what fired and why.
|
- **Explainable results.** Every delivery is traceable back to a rule, action, and event payload stored in the delivery ledger; operators can audit what fired and why.
|
||||||
- **Safe by default.** Secrets remain in external stores, templates are sandboxed, quiet hours and throttles prevent storms, and idempotency guarantees protect downstream systems.
|
- **Safe by default.** Secrets remain in external stores, templates are sandboxed, quiet hours and throttles prevent storms, and idempotency guarantees protect downstream systems.
|
||||||
- **Offline-aligned.** All configuration, templates, and plug-ins ship with Offline Kits; no external SaaS is required to send notifications.
|
- **Offline-aligned.** All configuration, templates, and plug-ins ship with Offline Kits; no external SaaS is required to send notifications.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 2. Core capabilities
|
## 2. Core capabilities
|
||||||
|
|
||||||
| Capability | What it does | Key docs |
|
| Capability | What it does | Key docs |
|
||||||
|------------|--------------|----------|
|
|------------|--------------|----------|
|
||||||
| Rules engine | Declarative matchers for event kinds, severities, namespaces, VEX context, KEV flags, and more. | [`notifications/rules.md`](rules.md) |
|
| Rules engine | Declarative matchers for event kinds, severities, namespaces, VEX context, KEV flags, and more. | [`notifications/rules.md`](rules.md) |
|
||||||
| Channel catalog | Slack, Teams, Email, Webhook connectors loaded via restart-time plug-ins; metadata stored without secrets. | [`notifications/architecture.md`](architecture.md) |
|
| Channel catalog | Slack, Teams, Email, Webhook connectors loaded via restart-time plug-ins; metadata stored without secrets. | [`notifications/architecture.md`](architecture.md) |
|
||||||
| Templates | Locale-aware, deterministic rendering via safe helpers; channel defaults plus tenant-specific overrides. | [`notifications/templates.md`](templates.md) |
|
| Templates | Locale-aware, deterministic rendering via safe helpers; channel defaults plus tenant-specific overrides. | [`notifications/templates.md`](templates.md) |
|
||||||
| Digests | Coalesce bursts into periodic summaries with deterministic IDs and audit trails. | [`notifications/digests.md`](digests.md) |
|
| Digests | Coalesce bursts into periodic summaries with deterministic IDs and audit trails. | [`notifications/digests.md`](digests.md) |
|
||||||
| Delivery ledger | Tracks rendered payload hashes, attempts, throttles, and outcomes for every action. | [`modules/notify/architecture.md`](../modules/notify/architecture.md#7-data-model-mongo) |
|
| Delivery ledger | Tracks rendered payload hashes, attempts, throttles, and outcomes for every action. | [`modules/notify/architecture.md`](../modules/notify/architecture.md#7-data-model-mongo) |
|
||||||
| Ack tokens | DSSE-signed acknowledgement tokens with webhook allowlists and escalation guardrails enforced by Authority. | [`modules/notify/architecture.md`](../modules/notify/architecture.md#81-ack-tokens--escalation-workflows) |
|
| Ack tokens | DSSE-signed acknowledgement tokens with webhook allowlists and escalation guardrails enforced by Authority. | [`modules/notify/architecture.md`](../modules/notify/architecture.md#81-ack-tokens--escalation-workflows) |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 3. How it fits into Stella Ops
|
## 3. How it fits into Stella Ops
|
||||||
|
|
||||||
1. **Producers emit events.** Scanner, Scheduler, VEX Lens, Attestor, and Zastava publish canonical envelopes (`NotifyEvent`) onto the internal bus.
|
1. **Producers emit events.** Scanner, Scheduler, VEX Lens, Attestor, and Zastava publish canonical envelopes (`NotifyEvent`) onto the internal bus.
|
||||||
2. **Notify.Worker evaluates rules.** For each tenant, the worker applies match filters, VEX gates, throttles, and digest policies before rendering the action.
|
2. **Notify.Worker evaluates rules.** For each tenant, the worker applies match filters, VEX gates, throttles, and digest policies before rendering the action.
|
||||||
3. **Connectors deliver.** Channel plug-ins send the rendered payload to Slack/Teams/Email/Webhook targets and report back attempts and outcomes.
|
3. **Connectors deliver.** Channel plug-ins send the rendered payload to Slack/Teams/Email/Webhook targets and report back attempts and outcomes.
|
||||||
4. **Consumers investigate.** Operators pivot from message links into Console dashboards, SBOM views, or policy overlays with correlation IDs preserved.
|
4. **Consumers investigate.** Operators pivot from message links into Console dashboards, SBOM views, or policy overlays with correlation IDs preserved.
|
||||||
|
|
||||||
The Notify WebService fronts worker state with REST APIs used by the UI and CLI. Tenants authenticate via StellaOps Authority scopes `notify.viewer`, `notify.operator`, and (for escalated actions) `notify.admin`. All operations require the tenant header (`X-StellaOps-Tenant`) to preserve sovereignty boundaries.
|
The Notify WebService fronts worker state with REST APIs used by the UI and CLI. Tenants authenticate via StellaOps Authority scopes `notify.viewer`, `notify.operator`, and (for escalated actions) `notify.admin`. All operations require the tenant header (`X-StellaOps-Tenant`) to preserve sovereignty boundaries.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 4. Operating model
|
## 4. Operating model
|
||||||
|
|
||||||
| Area | Guidance |
|
| Area | Guidance |
|
||||||
|------|----------|
|
|------|----------|
|
||||||
| **Tenancy** | Each rule, channel, template, and delivery belongs to exactly one tenant. Cross-tenant sharing is intentionally unsupported. |
|
| **Tenancy** | Each rule, channel, template, and delivery belongs to exactly one tenant. Cross-tenant sharing is intentionally unsupported. |
|
||||||
| **Determinism** | Configuration persistence normalises strings and sorts collections. Template rendering produces identical `bodyHash` values when inputs match. |
|
| **Determinism** | Configuration persistence normalises strings and sorts collections. Template rendering produces identical `bodyHash` values when inputs match. |
|
||||||
| **Scaling** | Workers scale horizontally; per-tenant rule snapshots are cached and refreshed from Mongo change streams. Redis (or equivalent) guards throttles and locks. |
|
| **Scaling** | Workers scale horizontally; per-tenant rule snapshots are cached and refreshed from Mongo change streams. Redis (or equivalent) guards throttles and locks. |
|
||||||
| **Offline** | Offline Kits include plug-ins, default templates, and seed rules. Operators can edit YAML/JSON manifests before air-gapped deployment. |
|
| **Offline** | Offline Kits include plug-ins, default templates, and seed rules. Operators can edit YAML/JSON manifests before air-gapped deployment. |
|
||||||
| **Security** | Channel secrets use indirection (`secretRef`), Authority-protected OAuth clients secure API access, and delivery payloads are redacted before storage where required. |
|
| **Security** | Channel secrets use indirection (`secretRef`), Authority-protected OAuth clients secure API access, and delivery payloads are redacted before storage where required. |
|
||||||
| **Module boundaries** | 2025-11-02 decision: keep `src/Notify/` as the shared notification toolkit and `src/Notifier/` as the Notifications Studio runtime host until a packaging RFC covers the implications of merging. |
|
| **Module boundaries** | 2025-11-02 decision: keep `src/Notify/` as the shared notification toolkit and `src/Notifier/` as the Notifications Studio runtime host until a packaging RFC covers the implications of merging. |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 5. Getting started (first 30 minutes)
|
## 5. Getting started (first 30 minutes)
|
||||||
|
|
||||||
| Step | Goal | Reference |
|
| Step | Goal | Reference |
|
||||||
|------|------|-----------|
|
|------|------|-----------|
|
||||||
| 1 | Deploy Notify WebService + Worker with Mongo and Redis | [`modules/notify/architecture.md`](../modules/notify/architecture.md#1-runtime-shape--projects) |
|
| 1 | Deploy Notify WebService + Worker with Mongo and Redis | [`modules/notify/architecture.md`](../modules/notify/architecture.md#1-runtime-shape--projects) |
|
||||||
| 2 | Register OAuth clients/scopes in Authority | [`etc/authority.yaml.sample`](../../etc/authority.yaml.sample) |
|
| 2 | Register OAuth clients/scopes in Authority | [`etc/authority.yaml.sample`](../../etc/authority.yaml.sample) |
|
||||||
| 3 | Install channel plug-ins and capture secret references | [`plugins/notify`](../../plugins) |
|
| 3 | Install channel plug-ins and capture secret references | [`plugins/notify`](../../plugins) |
|
||||||
| 4 | Create a tenant rule and test preview | [`POST /channels/{id}/test`](../modules/notify/architecture.md#8-external-apis-webservice) |
|
| 4 | Create a tenant rule and test preview | [`POST /channels/{id}/test`](../modules/notify/architecture.md#8-external-apis-webservice) |
|
||||||
| 5 | Inspect deliveries and digests | `/api/v1/notify/deliveries`, `/api/v1/notify/digests` |
|
| 5 | Inspect deliveries and digests | `/api/v1/notify/deliveries`, `/api/v1/notify/digests` |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 6. Alignment with implementation work
|
## 6. Alignment with implementation work
|
||||||
|
|
||||||
| Backlog item | Impact on docs | Status |
|
| Backlog item | Impact on docs | Status |
|
||||||
|--------------|----------------|--------|
|
|--------------|----------------|--------|
|
||||||
| `NOTIFY-SVC-38-001..004` | Foundational correlation, throttling, simulation hooks. | **In progress** – align behaviour once services publish beta APIs. |
|
| `NOTIFY-SVC-38-001..004` | Foundational correlation, throttling, simulation hooks. | **In progress** – align behaviour once services publish beta APIs. |
|
||||||
| `NOTIFY-SVC-39-001..004` | Adds correlation engine, digest generator, simulation API, quiet hours. | **Pending** – revisit rule/digest sections when these tasks merge. |
|
| `NOTIFY-SVC-39-001..004` | Adds correlation engine, digest generator, simulation API, quiet hours. | **Pending** – revisit rule/digest sections when these tasks merge. |
|
||||||
|
|
||||||
Action: coordinate with the Notifications Service Guild when `NOTIFY-SVC-39-001..004` land to validate payload fields, quiet-hours semantics, and any new connector metadata that should be documented here and in the channel-specific guides.
|
Action: coordinate with the Notifications Service Guild when `NOTIFY-SVC-39-001..004` land to validate payload fields, quiet-hours semantics, and any new connector metadata that should be documented here and in the channel-specific guides.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
> **Imposed rule reminder:** Work of this type or tasks of this type on this component must also be applied everywhere else it should be applied.
|
> **Imposed rule reminder:** Work of this type or tasks of this type on this component must also be applied everywhere else it should be applied.
|
||||||
|
|||||||
@@ -1,423 +1,423 @@
|
|||||||
# Stella Ops — Deterministic Replay Specification
|
# Stella Ops — Deterministic Replay Specification
|
||||||
|
|
||||||
Version: 1.0
|
Version: 1.0
|
||||||
Status: Draft / Internal Technical Reference
|
Status: Draft / Internal Technical Reference
|
||||||
Audience: Core developers, module maintainers, audit engineers.
|
Audience: Core developers, module maintainers, audit engineers.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 1. Purpose
|
## 1. Purpose
|
||||||
|
|
||||||
Deterministic Replay allows any completed Stella Ops scan to be **reproduced byte-for-byte** with full cryptographic validation.
|
Deterministic Replay allows any completed Stella Ops scan to be **reproduced byte-for-byte** with full cryptographic validation.
|
||||||
It guarantees that SBOMs, Findings, and VEX evaluations can be re-executed later to:
|
It guarantees that SBOMs, Findings, and VEX evaluations can be re-executed later to:
|
||||||
|
|
||||||
- prove historical compliance decisions,
|
- prove historical compliance decisions,
|
||||||
- attribute changes precisely to feeds, rules, or tools,
|
- attribute changes precisely to feeds, rules, or tools,
|
||||||
- support dual-signing (FIPS + regional crypto),
|
- support dual-signing (FIPS + regional crypto),
|
||||||
- and anchor cryptographic evidence in offline or public ledgers.
|
- and anchor cryptographic evidence in offline or public ledgers.
|
||||||
|
|
||||||
Replay requires that all inputs and environmental conditions are **captured, hashed, and sealed** at scan time.
|
Replay requires that all inputs and environmental conditions are **captured, hashed, and sealed** at scan time.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 2. Architecture Overview
|
## 2. Architecture Overview
|
||||||
|
|
||||||
```mermaid
|
```mermaid
|
||||||
graph TD
|
graph TD
|
||||||
A[Scanner.WebService] --> B[Replay Manifest]
|
A[Scanner.WebService] --> B[Replay Manifest]
|
||||||
A --> C[InputBundle]
|
A --> C[InputBundle]
|
||||||
A --> D[OutputBundle]
|
A --> D[OutputBundle]
|
||||||
B --> E[DSSE Envelope]
|
B --> E[DSSE Envelope]
|
||||||
C --> F[Feedser Snapshot Export]
|
C --> F[Feedser Snapshot Export]
|
||||||
C --> G[Policy/Lattice Bundle]
|
C --> G[Policy/Lattice Bundle]
|
||||||
D --> H[DSSE Outputs (SBOM, Findings, VEX)]
|
D --> H[DSSE Outputs (SBOM, Findings, VEX)]
|
||||||
E --> I[MongoDB: replay_runs]
|
E --> I[MongoDB: replay_runs]
|
||||||
C --> J[Blob Store: Input/Output Bundles]
|
C --> J[Blob Store: Input/Output Bundles]
|
||||||
````
|
````
|
||||||
|
|
||||||
### Core Artifacts
|
### Core Artifacts
|
||||||
|
|
||||||
| Artifact | Description | Format |
|
| Artifact | Description | Format |
|
||||||
| ------------------- | ------------------------------------------------------ | -------------------------- |
|
| ------------------- | ------------------------------------------------------ | -------------------------- |
|
||||||
| **Replay Manifest** | Immutable JSON describing all scan inputs and outputs. | JSON (canonicalized) |
|
| **Replay Manifest** | Immutable JSON describing all scan inputs and outputs. | JSON (canonicalized) |
|
||||||
| **InputBundle** | Feeds, rules, policies, tool binaries (hashed). | `.tar.zst` |
|
| **InputBundle** | Feeds, rules, policies, tool binaries (hashed). | `.tar.zst` |
|
||||||
| **OutputBundle** | SBOM, Findings, VEX, logs. | `.tar.zst` |
|
| **OutputBundle** | SBOM, Findings, VEX, logs. | `.tar.zst` |
|
||||||
| **DSSE Envelope** | Signed metadata for each artifact. | JSON / JWS |
|
| **DSSE Envelope** | Signed metadata for each artifact. | JSON / JWS |
|
||||||
| **Merkle Map** | Layer and feed chunk trees. | JSON (embedded or sidecar) |
|
| **Merkle Map** | Layer and feed chunk trees. | JSON (embedded or sidecar) |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 3. Replay Manifest Schema (v1)
|
## 3. Replay Manifest Schema (v1)
|
||||||
|
|
||||||
### 3.1 Top-level Layout
|
### 3.1 Top-level Layout
|
||||||
|
|
||||||
```jsonc
|
```jsonc
|
||||||
{
|
{
|
||||||
"schemaVersion": "1.0",
|
"schemaVersion": "1.0",
|
||||||
"scan": {
|
"scan": {
|
||||||
"id": "uuid",
|
"id": "uuid",
|
||||||
"time": "2025-10-29T13:05:33Z",
|
"time": "2025-10-29T13:05:33Z",
|
||||||
"mode": "record",
|
"mode": "record",
|
||||||
"scannerVersion": "10.1.3",
|
"scannerVersion": "10.1.3",
|
||||||
"cryptoProfile": "FIPS-140-3+GOST-R-34.10-2012"
|
"cryptoProfile": "FIPS-140-3+GOST-R-34.10-2012"
|
||||||
},
|
},
|
||||||
"subject": {
|
"subject": {
|
||||||
"ociDigest": "sha256:abcd...",
|
"ociDigest": "sha256:abcd...",
|
||||||
"layers": [
|
"layers": [
|
||||||
{ "layerDigest": "...", "merkleRoot": "...", "leafCount": 144 }
|
{ "layerDigest": "...", "merkleRoot": "...", "leafCount": 144 }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"feeds": [
|
"feeds": [
|
||||||
{
|
{
|
||||||
"name": "nvd",
|
"name": "nvd",
|
||||||
"snapshotHash": "sha256:...",
|
"snapshotHash": "sha256:...",
|
||||||
"snapshotTime": "2025-10-29T12:00:00Z",
|
"snapshotTime": "2025-10-29T12:00:00Z",
|
||||||
"merkleRoot": "..."
|
"merkleRoot": "..."
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"rulesBundleHash": "sha256:...",
|
"rulesBundleHash": "sha256:...",
|
||||||
"tools": [
|
"tools": [
|
||||||
{ "name": "sbomer", "version": "10.1.3", "sha256": "..." },
|
{ "name": "sbomer", "version": "10.1.3", "sha256": "..." },
|
||||||
{ "name": "scanner", "version": "10.1.3", "sha256": "..." },
|
{ "name": "scanner", "version": "10.1.3", "sha256": "..." },
|
||||||
{ "name": "vexer", "version": "10.1.3", "sha256": "..." }
|
{ "name": "vexer", "version": "10.1.3", "sha256": "..." }
|
||||||
],
|
],
|
||||||
"env": {
|
"env": {
|
||||||
"os": "linux",
|
"os": "linux",
|
||||||
"arch": "x64",
|
"arch": "x64",
|
||||||
"locale": "en_US.UTF-8",
|
"locale": "en_US.UTF-8",
|
||||||
"tz": "UTC",
|
"tz": "UTC",
|
||||||
"seed": "H(scan.id||merkleRootAllLayers)",
|
"seed": "H(scan.id||merkleRootAllLayers)",
|
||||||
"flags": ["offline"]
|
"flags": ["offline"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"policy": {
|
"policy": {
|
||||||
"latticeHash": "sha256:...",
|
"latticeHash": "sha256:...",
|
||||||
"mutes": [
|
"mutes": [
|
||||||
{ "id": "MUTE-1234", "reason": "vendor ack", "approvedBy": "authority@example.com", "approvedAt": "2025-10-29T12:55Z" }
|
{ "id": "MUTE-1234", "reason": "vendor ack", "approvedBy": "authority@example.com", "approvedAt": "2025-10-29T12:55Z" }
|
||||||
],
|
],
|
||||||
"trustProfile": "sha256:..."
|
"trustProfile": "sha256:..."
|
||||||
},
|
},
|
||||||
"outputs": {
|
"outputs": {
|
||||||
"sbomHash": "sha256:...",
|
"sbomHash": "sha256:...",
|
||||||
"findingsHash": "sha256:...",
|
"findingsHash": "sha256:...",
|
||||||
"vexHash": "sha256:...",
|
"vexHash": "sha256:...",
|
||||||
"logHash": "sha256:..."
|
"logHash": "sha256:..."
|
||||||
},
|
},
|
||||||
"provenance": {
|
"provenance": {
|
||||||
"signer": "scanner.authority",
|
"signer": "scanner.authority",
|
||||||
"dsseEnvelopeHash": "sha256:...",
|
"dsseEnvelopeHash": "sha256:...",
|
||||||
"rekorEntry": "optional"
|
"rekorEntry": "optional"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 4. Deterministic Execution Rules
|
## 4. Deterministic Execution Rules
|
||||||
|
|
||||||
### 4.1 Environment Normalization
|
### 4.1 Environment Normalization
|
||||||
|
|
||||||
* **Clock:** frozen to `scan.time` unless a rule explicitly requires “now”.
|
* **Clock:** frozen to `scan.time` unless a rule explicitly requires “now”.
|
||||||
* **Random seed:** derived as `H(scan.id || MerkleRootAllLayers)`.
|
* **Random seed:** derived as `H(scan.id || MerkleRootAllLayers)`.
|
||||||
* **Locale/TZ:** enforced per manifest; deviations cause validation error.
|
* **Locale/TZ:** enforced per manifest; deviations cause validation error.
|
||||||
* **Filesystem normalization:**
|
* **Filesystem normalization:**
|
||||||
|
|
||||||
* Normalize perms to 0644/0755.
|
* Normalize perms to 0644/0755.
|
||||||
* Path separators = `/`.
|
* Path separators = `/`.
|
||||||
* Newlines = LF.
|
* Newlines = LF.
|
||||||
* JSON key order = lexical.
|
* JSON key order = lexical.
|
||||||
|
|
||||||
### 4.2 Concurrency & I/O
|
### 4.2 Concurrency & I/O
|
||||||
|
|
||||||
* File traversal: stable lexicographic order.
|
* File traversal: stable lexicographic order.
|
||||||
* Parallel jobs: ordered reduction by subject path.
|
* Parallel jobs: ordered reduction by subject path.
|
||||||
* Temporary directories: ephemeral but deterministic hash seeds.
|
* Temporary directories: ephemeral but deterministic hash seeds.
|
||||||
|
|
||||||
### 4.3 Feeds & Policies
|
### 4.3 Feeds & Policies
|
||||||
|
|
||||||
* All network I/O disabled; feeds must be read from snapshot bundles.
|
* All network I/O disabled; feeds must be read from snapshot bundles.
|
||||||
* Policies and suppressions must resolve by hash, not name.
|
* Policies and suppressions must resolve by hash, not name.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 5. DSSE and Signing
|
## 5. DSSE and Signing
|
||||||
|
|
||||||
### 5.1 Envelope Structure
|
### 5.1 Envelope Structure
|
||||||
|
|
||||||
```jsonc
|
```jsonc
|
||||||
{
|
{
|
||||||
"payloadType": "application/vnd.stella.replay.manifest+json",
|
"payloadType": "application/vnd.stella.replay.manifest+json",
|
||||||
"payload": "<base64-encoded canonical JSON>",
|
"payload": "<base64-encoded canonical JSON>",
|
||||||
"signatures": [
|
"signatures": [
|
||||||
{ "keyid": "authority-root-fips", "sig": "..." },
|
{ "keyid": "authority-root-fips", "sig": "..." },
|
||||||
{ "keyid": "authority-root-gost", "sig": "..." }
|
{ "keyid": "authority-root-gost", "sig": "..." }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### 5.2 Verification Steps
|
### 5.2 Verification Steps
|
||||||
|
|
||||||
1. Decode payload → verify canonical form.
|
1. Decode payload → verify canonical form.
|
||||||
2. Verify each signature chain against RootPack (offline trust anchors).
|
2. Verify each signature chain against RootPack (offline trust anchors).
|
||||||
3. Recompute hash and compare to `dsseEnvelopeHash` in manifest.
|
3. Recompute hash and compare to `dsseEnvelopeHash` in manifest.
|
||||||
4. Optionally verify Rekor inclusion proof.
|
4. Optionally verify Rekor inclusion proof.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 6. CLI Interface
|
## 6. CLI Interface
|
||||||
|
|
||||||
### 6.1 Recording a Scan
|
### 6.1 Recording a Scan
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
stella scan image:tag --record ./out/
|
stella scan image:tag --record ./out/
|
||||||
```
|
```
|
||||||
|
|
||||||
Produces:
|
Produces:
|
||||||
|
|
||||||
```
|
```
|
||||||
out/
|
out/
|
||||||
├─ manifest.json
|
├─ manifest.json
|
||||||
├─ manifest.dsse.json
|
├─ manifest.dsse.json
|
||||||
├─ inputbundle.tar.zst
|
├─ inputbundle.tar.zst
|
||||||
├─ outputbundle.tar.zst
|
├─ outputbundle.tar.zst
|
||||||
└─ signatures/
|
└─ signatures/
|
||||||
```
|
```
|
||||||
|
|
||||||
### 6.2 Verifying
|
### 6.2 Verifying
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
stella verify manifest.json
|
stella verify manifest.json
|
||||||
```
|
```
|
||||||
|
|
||||||
* Checks all hashes and DSSE envelopes.
|
* Checks all hashes and DSSE envelopes.
|
||||||
* Prints summary:
|
* Prints summary:
|
||||||
|
|
||||||
```
|
```
|
||||||
✅ Verified: SBOM, Findings, VEX, Tools, Feeds, Policy
|
✅ Verified: SBOM, Findings, VEX, Tools, Feeds, Policy
|
||||||
```
|
```
|
||||||
|
|
||||||
### 6.3 Replaying
|
### 6.3 Replaying
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
stella replay manifest.json --strict
|
stella replay manifest.json --strict
|
||||||
stella replay manifest.json --what-if --vary=feeds
|
stella replay manifest.json --what-if --vary=feeds
|
||||||
```
|
```
|
||||||
|
|
||||||
* `--strict`: all inputs locked; identical result expected.
|
* `--strict`: all inputs locked; identical result expected.
|
||||||
* `--what-if`: varies only specified dimension(s).
|
* `--what-if`: varies only specified dimension(s).
|
||||||
|
|
||||||
### 6.4 Diffing
|
### 6.4 Diffing
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
stella diff manifestA.json manifestB.json
|
stella diff manifestA.json manifestB.json
|
||||||
```
|
```
|
||||||
|
|
||||||
Shows field-level differences (feed snapshot, tool, or policy hash).
|
Shows field-level differences (feed snapshot, tool, or policy hash).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 7. MongoDB Schema
|
## 7. MongoDB Schema
|
||||||
|
|
||||||
### 7.1 `replay_runs`
|
### 7.1 `replay_runs`
|
||||||
|
|
||||||
```jsonc
|
```jsonc
|
||||||
{
|
{
|
||||||
"_id": "uuid",
|
"_id": "uuid",
|
||||||
"manifestHash": "sha256:...",
|
"manifestHash": "sha256:...",
|
||||||
"status": "verified|failed|replayed",
|
"status": "verified|failed|replayed",
|
||||||
"createdAt": "...",
|
"createdAt": "...",
|
||||||
"updatedAt": "...",
|
"updatedAt": "...",
|
||||||
"signatures": [{ "profile": "FIPS", "verified": true }],
|
"signatures": [{ "profile": "FIPS", "verified": true }],
|
||||||
"outputs": {
|
"outputs": {
|
||||||
"sbom": "sha256:...",
|
"sbom": "sha256:...",
|
||||||
"findings": "sha256:..."
|
"findings": "sha256:..."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### 7.2 `bundles`
|
### 7.2 `bundles`
|
||||||
|
|
||||||
```jsonc
|
```jsonc
|
||||||
{
|
{
|
||||||
"_id": "sha256:...",
|
"_id": "sha256:...",
|
||||||
"type": "input|output|rootpack",
|
"type": "input|output|rootpack",
|
||||||
"size": 4123123,
|
"size": 4123123,
|
||||||
"location": "/var/lib/stella/bundles/<sha>.tar.zst"
|
"location": "/var/lib/stella/bundles/<sha>.tar.zst"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### 7.3 `subjects`
|
### 7.3 `subjects`
|
||||||
|
|
||||||
```jsonc
|
```jsonc
|
||||||
{
|
{
|
||||||
"ociDigest": "sha256:abcd...",
|
"ociDigest": "sha256:abcd...",
|
||||||
"layers": [
|
"layers": [
|
||||||
{ "layerDigest": "...", "merkleRoot": "...", "leafCount": 120 }
|
{ "layerDigest": "...", "merkleRoot": "...", "leafCount": 120 }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 8. Layer Merkle Implementation
|
## 8. Layer Merkle Implementation
|
||||||
|
|
||||||
### 8.1 Algorithm
|
### 8.1 Algorithm
|
||||||
|
|
||||||
```csharp
|
```csharp
|
||||||
static string ComputeMerkleRoot(string layerTarPath)
|
static string ComputeMerkleRoot(string layerTarPath)
|
||||||
{
|
{
|
||||||
const int ChunkSize = 4 * 1024 * 1024;
|
const int ChunkSize = 4 * 1024 * 1024;
|
||||||
var hashes = new List<byte[]>();
|
var hashes = new List<byte[]>();
|
||||||
using var fs = File.OpenRead(layerTarPath);
|
using var fs = File.OpenRead(layerTarPath);
|
||||||
var buffer = new byte[ChunkSize];
|
var buffer = new byte[ChunkSize];
|
||||||
int read;
|
int read;
|
||||||
using var sha = SHA256.Create();
|
using var sha = SHA256.Create();
|
||||||
while ((read = fs.Read(buffer, 0, buffer.Length)) > 0)
|
while ((read = fs.Read(buffer, 0, buffer.Length)) > 0)
|
||||||
hashes.Add(sha.ComputeHash(buffer, 0, read));
|
hashes.Add(sha.ComputeHash(buffer, 0, read));
|
||||||
while (hashes.Count > 1)
|
while (hashes.Count > 1)
|
||||||
hashes = hashes
|
hashes = hashes
|
||||||
.Select((h, i) => (h, i))
|
.Select((h, i) => (h, i))
|
||||||
.GroupBy(x => x.i / 2)
|
.GroupBy(x => x.i / 2)
|
||||||
.Select(g => sha.ComputeHash(g.SelectMany(x => x.h).ToArray()))
|
.Select(g => sha.ComputeHash(g.SelectMany(x => x.h).ToArray()))
|
||||||
.ToList();
|
.ToList();
|
||||||
return Convert.ToHexString(hashes.Single());
|
return Convert.ToHexString(hashes.Single());
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### 8.2 Stored Values
|
### 8.2 Stored Values
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"layerDigest": "sha256:...",
|
"layerDigest": "sha256:...",
|
||||||
"merkleRoot": "b81f...",
|
"merkleRoot": "b81f...",
|
||||||
"leafCount": 240,
|
"leafCount": 240,
|
||||||
"leavesHash": "sha256:..."
|
"leavesHash": "sha256:..."
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 9. Replay Engine Implementation Notes (.NET 10)
|
## 9. Replay Engine Implementation Notes (.NET 10)
|
||||||
|
|
||||||
### 9.1 Manifest Parsing
|
### 9.1 Manifest Parsing
|
||||||
|
|
||||||
Use `System.Text.Json` with deterministic ordering:
|
Use `System.Text.Json` with deterministic ordering:
|
||||||
|
|
||||||
```csharp
|
```csharp
|
||||||
var options = new JsonSerializerOptions {
|
var options = new JsonSerializerOptions {
|
||||||
WriteIndented = false,
|
WriteIndented = false,
|
||||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||||
TypeInfoResolverChain = { new OrderedResolver() }
|
TypeInfoResolverChain = { new OrderedResolver() }
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
### 9.2 Stable Output
|
### 9.2 Stable Output
|
||||||
|
|
||||||
Normalize SBOM/Findings/VEX JSON:
|
Normalize SBOM/Findings/VEX JSON:
|
||||||
|
|
||||||
```csharp
|
```csharp
|
||||||
string Canonicalize(string json) =>
|
string Canonicalize(string json) =>
|
||||||
JsonSerializer.Serialize(
|
JsonSerializer.Serialize(
|
||||||
JsonSerializer.Deserialize<JsonDocument>(json),
|
JsonSerializer.Deserialize<JsonDocument>(json),
|
||||||
options);
|
options);
|
||||||
```
|
```
|
||||||
|
|
||||||
### 9.3 Verification Flow
|
### 9.3 Verification Flow
|
||||||
|
|
||||||
```csharp
|
```csharp
|
||||||
var manifest = Manifest.Load("manifest.json");
|
var manifest = Manifest.Load("manifest.json");
|
||||||
VerifySignatures(manifest);
|
VerifySignatures(manifest);
|
||||||
VerifyHashes(manifest);
|
VerifyHashes(manifest);
|
||||||
if (mode == Strict) RunPipeline(manifest);
|
if (mode == Strict) RunPipeline(manifest);
|
||||||
else RunPipelineWithVariation(manifest, vary);
|
else RunPipelineWithVariation(manifest, vary);
|
||||||
```
|
```
|
||||||
|
|
||||||
### 9.4 Failure Modes
|
### 9.4 Failure Modes
|
||||||
|
|
||||||
| Condition | Action |
|
| Condition | Action |
|
||||||
| -------------------------------- | ----------------------------- |
|
| -------------------------------- | ----------------------------- |
|
||||||
| Missing snapshot or bundle | Error: `InputBundleMissing` |
|
| Missing snapshot or bundle | Error: `InputBundleMissing` |
|
||||||
| Feed hash mismatch | Error: `FeedSnapshotDrift` |
|
| Feed hash mismatch | Error: `FeedSnapshotDrift` |
|
||||||
| Tool binary hash mismatch | Reject replay |
|
| Tool binary hash mismatch | Reject replay |
|
||||||
| Output hash drift in strict mode | Mark as failed, emit diff log |
|
| Output hash drift in strict mode | Mark as failed, emit diff log |
|
||||||
| Invalid signature | Reject manifest |
|
| Invalid signature | Reject manifest |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 10. Crypto Profiles and RootPack
|
## 10. Crypto Profiles and RootPack
|
||||||
|
|
||||||
### 10.1 Example Profiles
|
### 10.1 Example Profiles
|
||||||
|
|
||||||
| Profile | Algorithms | Notes |
|
| Profile | Algorithms | Notes |
|
||||||
| -------------- | ------------------------------------- | ----------------------- |
|
| -------------- | ------------------------------------- | ----------------------- |
|
||||||
| **FIPS-140-3** | ECDSA-P256 / SHA-256 / AES-GCM | Default for US/EU |
|
| **FIPS-140-3** | ECDSA-P256 / SHA-256 / AES-GCM | Default for US/EU |
|
||||||
| **GOST** | GOST R 34.10-2012 / GOST R 34.11-2012 | Russia |
|
| **GOST** | GOST R 34.10-2012 / GOST R 34.11-2012 | Russia |
|
||||||
| **SM** | SM2 / SM3 / SM4 | China |
|
| **SM** | SM2 / SM3 / SM4 | China |
|
||||||
| **eIDAS** | RSA-PSS / SHA-256 | EU qualified signatures |
|
| **eIDAS** | RSA-PSS / SHA-256 | EU qualified signatures |
|
||||||
|
|
||||||
### 10.2 Dual-Signing Example
|
### 10.2 Dual-Signing Example
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
stella sign manifest.json --profiles=FIPS,GOST
|
stella sign manifest.json --profiles=FIPS,GOST
|
||||||
```
|
```
|
||||||
|
|
||||||
Produces:
|
Produces:
|
||||||
|
|
||||||
```
|
```
|
||||||
signatures/
|
signatures/
|
||||||
├─ manifest.dsse.fips.json
|
├─ manifest.dsse.fips.json
|
||||||
└─ manifest.dsse.gost.json
|
└─ manifest.dsse.gost.json
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 11. Test Strategy
|
## 11. Test Strategy
|
||||||
|
|
||||||
| Test | Description | Expected Result |
|
| Test | Description | Expected Result |
|
||||||
| ---------------------- | ------------------------------------ | --------------------------- |
|
| ---------------------- | ------------------------------------ | --------------------------- |
|
||||||
| **Golden Replay** | Repeat identical scan → same outputs | ✅ identical hashes |
|
| **Golden Replay** | Repeat identical scan → same outputs | ✅ identical hashes |
|
||||||
| **Feed Drift Test** | Replay with updated feeds | Only `inputs.feeds` changes |
|
| **Feed Drift Test** | Replay with updated feeds | Only `inputs.feeds` changes |
|
||||||
| **Tool Upgrade Test** | Replay with new scanner version | Reject or diff by `tools` |
|
| **Tool Upgrade Test** | Replay with new scanner version | Reject or diff by `tools` |
|
||||||
| **Policy Change Test** | Different lattice/mutes | Diff by `policy` section |
|
| **Policy Change Test** | Different lattice/mutes | Diff by `policy` section |
|
||||||
| **Cross-Arch Test** | x64 vs arm64 | Identical outputs |
|
| **Cross-Arch Test** | x64 vs arm64 | Identical outputs |
|
||||||
| **Corrupted Bundle** | Tamper bundle | Verification fails |
|
| **Corrupted Bundle** | Tamper bundle | Verification fails |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 12. Example Verification Output
|
## 12. Example Verification Output
|
||||||
|
|
||||||
```
|
```
|
||||||
$ stella verify manifest.json
|
$ stella verify manifest.json
|
||||||
|
|
||||||
[✓] Manifest integrity: OK
|
[✓] Manifest integrity: OK
|
||||||
[✓] DSSE signatures (FIPS,GOST): OK
|
[✓] DSSE signatures (FIPS,GOST): OK
|
||||||
[✓] Feeds snapshot hash: OK
|
[✓] Feeds snapshot hash: OK
|
||||||
[✓] Policy + mutes hash: OK
|
[✓] Policy + mutes hash: OK
|
||||||
[✓] Toolchain hash: OK
|
[✓] Toolchain hash: OK
|
||||||
[✓] SBOM/VEX outputs: OK
|
[✓] SBOM/VEX outputs: OK
|
||||||
|
|
||||||
Result: VERIFIED
|
Result: VERIFIED
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 13. Future Extensions
|
## 13. Future Extensions
|
||||||
|
|
||||||
* Support **SPDX 3.0.1** alongside CycloneDX 1.6.
|
* Support **SPDX 3.0.1** alongside CycloneDX 1.6.
|
||||||
* Add **per-file Merkle proofs** for local scans.
|
* Add **per-file Merkle proofs** for local scans.
|
||||||
* Ledger anchoring (Rekor, distributed Proof-Market).
|
* Ledger anchoring (Rekor, distributed Proof-Market).
|
||||||
* Post-quantum signatures (Dilithium/Falcon).
|
* Post-quantum signatures (Dilithium/Falcon).
|
||||||
* Replay orchestration API (`/api/replay/:id`).
|
* Replay orchestration API (`/api/replay/:id`).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 14. Summary
|
## 14. Summary
|
||||||
|
|
||||||
Deterministic Replay freezes every element of a scan:
|
Deterministic Replay freezes every element of a scan:
|
||||||
|
|
||||||
> *image → feeds → policy → toolchain → environment → outputs → signatures.*
|
> *image → feeds → policy → toolchain → environment → outputs → signatures.*
|
||||||
|
|
||||||
By enforcing canonical input/output states and verifiable cryptographic bindings, Stella Ops achieves **regulatory-grade replayability**, **regional crypto compliance**, and **immutable provenance** across all scans.
|
By enforcing canonical input/output states and verifiable cryptographic bindings, Stella Ops achieves **regulatory-grade replayability**, **regional crypto compliance**, and **immutable provenance** across all scans.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -1,113 +1,113 @@
|
|||||||
# Stella Ops — Developer Guide: Deterministic Replay
|
# Stella Ops — Developer Guide: Deterministic Replay
|
||||||
|
|
||||||
## Purpose
|
## Purpose
|
||||||
Deterministic Replay ensures any past scan can be re-executed byte-for-byte, producing identical SBOM, Findings, and VEX results, cryptographically verifiable for audits or compliance.
|
Deterministic Replay ensures any past scan can be re-executed byte-for-byte, producing identical SBOM, Findings, and VEX results, cryptographically verifiable for audits or compliance.
|
||||||
|
|
||||||
Replay is the foundation for:
|
Replay is the foundation for:
|
||||||
- **Audit proofs** (exact past state reproduction)
|
- **Audit proofs** (exact past state reproduction)
|
||||||
- **Diff analysis** (feeds, policies, tool versions)
|
- **Diff analysis** (feeds, policies, tool versions)
|
||||||
- **Cross-region verification** (same outputs on different hosts)
|
- **Cross-region verification** (same outputs on different hosts)
|
||||||
- **Long-term cryptographic trust** (re-sign with new crypto profiles)
|
- **Long-term cryptographic trust** (re-sign with new crypto profiles)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Core Concepts
|
## Core Concepts
|
||||||
|
|
||||||
| Term | Description |
|
| Term | Description |
|
||||||
|------|--------------|
|
|------|--------------|
|
||||||
| **Replay Manifest** | Immutable JSON describing all inputs, tools, env, and outputs of a scan. |
|
| **Replay Manifest** | Immutable JSON describing all inputs, tools, env, and outputs of a scan. |
|
||||||
| **InputBundle** | Snapshot of feeds, rules, policies, and toolchain binaries used. |
|
| **InputBundle** | Snapshot of feeds, rules, policies, and toolchain binaries used. |
|
||||||
| **OutputBundle** | SBOM, Findings, VEX, and logs from a completed scan. |
|
| **OutputBundle** | SBOM, Findings, VEX, and logs from a completed scan. |
|
||||||
| **Layer Merkle** | Per-layer hash tree for precise deduplication and drift detection. |
|
| **Layer Merkle** | Per-layer hash tree for precise deduplication and drift detection. |
|
||||||
| **DSSE Envelope** | Digital signature wrapper for each attestation (SBOM, Findings, Manifest, etc.). |
|
| **DSSE Envelope** | Digital signature wrapper for each attestation (SBOM, Findings, Manifest, etc.). |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## What to Freeze
|
## What to Freeze
|
||||||
|
|
||||||
| Category | Example Contents | Required in Manifest |
|
| Category | Example Contents | Required in Manifest |
|
||||||
|-----------|------------------|----------------------|
|
|-----------|------------------|----------------------|
|
||||||
| **Subject** | OCI image digest, per-layer Merkle roots | ✅ |
|
| **Subject** | OCI image digest, per-layer Merkle roots | ✅ |
|
||||||
| **Outputs** | SBOM, Findings, VEX, logs (content hashes) | ✅ |
|
| **Outputs** | SBOM, Findings, VEX, logs (content hashes) | ✅ |
|
||||||
| **Toolchain** | Sbomer, Scanner, Vexer binaries + versions + SHA256 | ✅ |
|
| **Toolchain** | Sbomer, Scanner, Vexer binaries + versions + SHA256 | ✅ |
|
||||||
| **Feeds/VEX sources** | Full or pruned snapshot with Merkle proofs | ✅ |
|
| **Feeds/VEX sources** | Full or pruned snapshot with Merkle proofs | ✅ |
|
||||||
| **Policy Bundle** | Lattice rules, mutes, trust profiles, thresholds | ✅ |
|
| **Policy Bundle** | Lattice rules, mutes, trust profiles, thresholds | ✅ |
|
||||||
| **Environment** | OS, arch, locale, TZ, deterministic seed, runtime flags | ✅ |
|
| **Environment** | OS, arch, locale, TZ, deterministic seed, runtime flags | ✅ |
|
||||||
| **Crypto Profile** | Algorithm suites (FIPS, GOST, SM, eIDAS) | ✅ |
|
| **Crypto Profile** | Algorithm suites (FIPS, GOST, SM, eIDAS) | ✅ |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Replay Modes
|
## Replay Modes
|
||||||
|
|
||||||
| Mode | Purpose | Input Variation | Expected Output |
|
| Mode | Purpose | Input Variation | Expected Output |
|
||||||
|------|----------|-----------------|-----------------|
|
|------|----------|-----------------|-----------------|
|
||||||
| **Strict Replay** | Audit proof | None | Bit-for-bit identical |
|
| **Strict Replay** | Audit proof | None | Bit-for-bit identical |
|
||||||
| **What-If Replay** | Change impact analysis | One dimension (feeds/tools/policy) | Deterministic diff |
|
| **What-If Replay** | Change impact analysis | One dimension (feeds/tools/policy) | Deterministic diff |
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
```
|
```
|
||||||
|
|
||||||
stella replay manifest.json --strict
|
stella replay manifest.json --strict
|
||||||
stella replay manifest.json --what-if --vary=feeds
|
stella replay manifest.json --what-if --vary=feeds
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Developer Responsibilities
|
## Developer Responsibilities
|
||||||
|
|
||||||
| Module | Role |
|
| Module | Role |
|
||||||
|---------|------|
|
|---------|------|
|
||||||
| **Scanner.WebService** | Capture full input set and produce Replay Manifest + DSSE sigs. |
|
| **Scanner.WebService** | Capture full input set and produce Replay Manifest + DSSE sigs. |
|
||||||
| **Sbomer** | Generate deterministic SBOM; normalize ordering and JSON formatting. |
|
| **Sbomer** | Generate deterministic SBOM; normalize ordering and JSON formatting. |
|
||||||
| **Vexer/Excititor** | Apply lattice and mutes from policy bundle; record gating logic. |
|
| **Vexer/Excititor** | Apply lattice and mutes from policy bundle; record gating logic. |
|
||||||
| **Feedser/Concelier** | Freeze and export feed snapshots or Merkle proofs. |
|
| **Feedser/Concelier** | Freeze and export feed snapshots or Merkle proofs. |
|
||||||
| **Authority** | Manage signer keys and crypto profiles; issue DSSE envelopes. |
|
| **Authority** | Manage signer keys and crypto profiles; issue DSSE envelopes. |
|
||||||
| **CLI** | Provide `scan --record`, `replay`, `verify`, `diff` commands. |
|
| **CLI** | Provide `scan --record`, `replay`, `verify`, `diff` commands. |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Workflow
|
## Workflow
|
||||||
|
|
||||||
1. `stella scan image:tag --record out/`
|
1. `stella scan image:tag --record out/`
|
||||||
- Generates Replay Manifest, InputBundle, OutputBundle, DSSE sigs.
|
- Generates Replay Manifest, InputBundle, OutputBundle, DSSE sigs.
|
||||||
2. `stella verify manifest.json`
|
2. `stella verify manifest.json`
|
||||||
- Validates hashes, signatures, and completeness.
|
- Validates hashes, signatures, and completeness.
|
||||||
3. `stella replay manifest.json --strict`
|
3. `stella replay manifest.json --strict`
|
||||||
- Re-executes in sealed mode; expect byte-identical results.
|
- Re-executes in sealed mode; expect byte-identical results.
|
||||||
4. `stella replay manifest.json --what-if --vary=feeds`
|
4. `stella replay manifest.json --what-if --vary=feeds`
|
||||||
- Runs with new feeds; diff is attributed to feeds only.
|
- Runs with new feeds; diff is attributed to feeds only.
|
||||||
5. `stella diff manifestA manifestB`
|
5. `stella diff manifestA manifestB`
|
||||||
- Attribute differences by hash comparison.
|
- Attribute differences by hash comparison.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Storage
|
## Storage
|
||||||
|
|
||||||
- **Mongo collections**
|
- **Mongo collections**
|
||||||
- `replay_runs`: manifest + DSSE envelopes + status
|
- `replay_runs`: manifest + DSSE envelopes + status
|
||||||
- `bundles`: content-addressed (input/output/rootpack)
|
- `bundles`: content-addressed (input/output/rootpack)
|
||||||
- `subjects`: OCI digests, Merkle roots per layer
|
- `subjects`: OCI digests, Merkle roots per layer
|
||||||
- **File store**
|
- **File store**
|
||||||
- Bundles stored as `<sha256>.tar.zst`
|
- Bundles stored as `<sha256>.tar.zst`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Developer Checklist
|
## Developer Checklist
|
||||||
|
|
||||||
- [ ] All inputs (feeds, policies, tools, env) hashed and recorded.
|
- [ ] All inputs (feeds, policies, tools, env) hashed and recorded.
|
||||||
- [ ] JSON normalization: key order, number format, newline mode.
|
- [ ] JSON normalization: key order, number format, newline mode.
|
||||||
- [ ] Random seed = `H(scan.id || MerkleRootAllLayers)`.
|
- [ ] Random seed = `H(scan.id || MerkleRootAllLayers)`.
|
||||||
- [ ] Clock fixed to `scan.time` unless policy requires “now”.
|
- [ ] Clock fixed to `scan.time` unless policy requires “now”.
|
||||||
- [ ] DSSE multi-sig supported (FIPS + regional).
|
- [ ] DSSE multi-sig supported (FIPS + regional).
|
||||||
- [ ] Manifest signed + optionally anchored to Rekor ledger.
|
- [ ] Manifest signed + optionally anchored to Rekor ledger.
|
||||||
- [ ] Replay comparison mode tested across x64/arm64.
|
- [ ] Replay comparison mode tested across x64/arm64.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## References
|
## References
|
||||||
See also:
|
See also:
|
||||||
- `DETERMINISTIC_REPLAY.md` — detailed manifest schema & CLI examples.
|
- `DETERMINISTIC_REPLAY.md` — detailed manifest schema & CLI examples.
|
||||||
- `../docs/CRYPTO_SOVEREIGN_READY.md` — RootPack and dual-signature handling.
|
- `../docs/CRYPTO_SOVEREIGN_READY.md` — RootPack and dual-signature handling.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -3,3 +3,25 @@ findings:
|
|||||||
ledger:
|
ledger:
|
||||||
database:
|
database:
|
||||||
connectionString: Host=postgres
|
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
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,233 +1,233 @@
|
|||||||
<?xml version="1.0"?>
|
<?xml version="1.0"?>
|
||||||
<doc>
|
<doc>
|
||||||
<assembly>
|
<assembly>
|
||||||
<name>StellaOps.Auth.Client</name>
|
<name>StellaOps.Auth.Client</name>
|
||||||
</assembly>
|
</assembly>
|
||||||
<members>
|
<members>
|
||||||
<member name="T:StellaOps.Auth.Client.FileTokenCache">
|
<member name="T:StellaOps.Auth.Client.FileTokenCache">
|
||||||
<summary>
|
<summary>
|
||||||
File-based token cache suitable for CLI/offline usage.
|
File-based token cache suitable for CLI/offline usage.
|
||||||
</summary>
|
</summary>
|
||||||
</member>
|
</member>
|
||||||
<member name="T:StellaOps.Auth.Client.InMemoryTokenCache">
|
<member name="T:StellaOps.Auth.Client.InMemoryTokenCache">
|
||||||
<summary>
|
<summary>
|
||||||
In-memory token cache suitable for service scenarios.
|
In-memory token cache suitable for service scenarios.
|
||||||
</summary>
|
</summary>
|
||||||
</member>
|
</member>
|
||||||
<member name="T:StellaOps.Auth.Client.IStellaOpsTokenCache">
|
<member name="T:StellaOps.Auth.Client.IStellaOpsTokenCache">
|
||||||
<summary>
|
<summary>
|
||||||
Abstraction for caching StellaOps tokens.
|
Abstraction for caching StellaOps tokens.
|
||||||
</summary>
|
</summary>
|
||||||
</member>
|
</member>
|
||||||
<member name="M:StellaOps.Auth.Client.IStellaOpsTokenCache.GetAsync(System.String,System.Threading.CancellationToken)">
|
<member name="M:StellaOps.Auth.Client.IStellaOpsTokenCache.GetAsync(System.String,System.Threading.CancellationToken)">
|
||||||
<summary>
|
<summary>
|
||||||
Retrieves a cached token entry, if present.
|
Retrieves a cached token entry, if present.
|
||||||
</summary>
|
</summary>
|
||||||
</member>
|
</member>
|
||||||
<member name="M:StellaOps.Auth.Client.IStellaOpsTokenCache.SetAsync(System.String,StellaOps.Auth.Client.StellaOpsTokenCacheEntry,System.Threading.CancellationToken)">
|
<member name="M:StellaOps.Auth.Client.IStellaOpsTokenCache.SetAsync(System.String,StellaOps.Auth.Client.StellaOpsTokenCacheEntry,System.Threading.CancellationToken)">
|
||||||
<summary>
|
<summary>
|
||||||
Stores or updates a token entry for the specified key.
|
Stores or updates a token entry for the specified key.
|
||||||
</summary>
|
</summary>
|
||||||
</member>
|
</member>
|
||||||
<member name="M:StellaOps.Auth.Client.IStellaOpsTokenCache.RemoveAsync(System.String,System.Threading.CancellationToken)">
|
<member name="M:StellaOps.Auth.Client.IStellaOpsTokenCache.RemoveAsync(System.String,System.Threading.CancellationToken)">
|
||||||
<summary>
|
<summary>
|
||||||
Removes the cached entry for the specified key.
|
Removes the cached entry for the specified key.
|
||||||
</summary>
|
</summary>
|
||||||
</member>
|
</member>
|
||||||
<member name="T:StellaOps.Auth.Client.IStellaOpsTokenClient">
|
<member name="T:StellaOps.Auth.Client.IStellaOpsTokenClient">
|
||||||
<summary>
|
<summary>
|
||||||
Abstraction for requesting tokens from StellaOps Authority.
|
Abstraction for requesting tokens from StellaOps Authority.
|
||||||
</summary>
|
</summary>
|
||||||
</member>
|
</member>
|
||||||
<member name="M:StellaOps.Auth.Client.IStellaOpsTokenClient.RequestPasswordTokenAsync(System.String,System.String,System.String,System.Collections.Generic.IReadOnlyDictionary{System.String,System.String},System.Threading.CancellationToken)">
|
<member name="M:StellaOps.Auth.Client.IStellaOpsTokenClient.RequestPasswordTokenAsync(System.String,System.String,System.String,System.Collections.Generic.IReadOnlyDictionary{System.String,System.String},System.Threading.CancellationToken)">
|
||||||
<summary>
|
<summary>
|
||||||
Requests an access token using the resource owner password credentials flow.
|
Requests an access token using the resource owner password credentials flow.
|
||||||
</summary>
|
</summary>
|
||||||
</member>
|
</member>
|
||||||
<member name="M:StellaOps.Auth.Client.IStellaOpsTokenClient.RequestClientCredentialsTokenAsync(System.String,System.Collections.Generic.IReadOnlyDictionary{System.String,System.String},System.Threading.CancellationToken)">
|
<member name="M:StellaOps.Auth.Client.IStellaOpsTokenClient.RequestClientCredentialsTokenAsync(System.String,System.Collections.Generic.IReadOnlyDictionary{System.String,System.String},System.Threading.CancellationToken)">
|
||||||
<summary>
|
<summary>
|
||||||
Requests an access token using the client credentials flow.
|
Requests an access token using the client credentials flow.
|
||||||
</summary>
|
</summary>
|
||||||
</member>
|
</member>
|
||||||
<member name="M:StellaOps.Auth.Client.IStellaOpsTokenClient.GetJsonWebKeySetAsync(System.Threading.CancellationToken)">
|
<member name="M:StellaOps.Auth.Client.IStellaOpsTokenClient.GetJsonWebKeySetAsync(System.Threading.CancellationToken)">
|
||||||
<summary>
|
<summary>
|
||||||
Retrieves the cached JWKS document.
|
Retrieves the cached JWKS document.
|
||||||
</summary>
|
</summary>
|
||||||
</member>
|
</member>
|
||||||
<member name="M:StellaOps.Auth.Client.IStellaOpsTokenClient.GetCachedTokenAsync(System.String,System.Threading.CancellationToken)">
|
<member name="M:StellaOps.Auth.Client.IStellaOpsTokenClient.GetCachedTokenAsync(System.String,System.Threading.CancellationToken)">
|
||||||
<summary>
|
<summary>
|
||||||
Retrieves a cached token entry.
|
Retrieves a cached token entry.
|
||||||
</summary>
|
</summary>
|
||||||
</member>
|
</member>
|
||||||
<member name="M:StellaOps.Auth.Client.IStellaOpsTokenClient.CacheTokenAsync(System.String,StellaOps.Auth.Client.StellaOpsTokenCacheEntry,System.Threading.CancellationToken)">
|
<member name="M:StellaOps.Auth.Client.IStellaOpsTokenClient.CacheTokenAsync(System.String,StellaOps.Auth.Client.StellaOpsTokenCacheEntry,System.Threading.CancellationToken)">
|
||||||
<summary>
|
<summary>
|
||||||
Persists a token entry in the cache.
|
Persists a token entry in the cache.
|
||||||
</summary>
|
</summary>
|
||||||
</member>
|
</member>
|
||||||
<member name="M:StellaOps.Auth.Client.IStellaOpsTokenClient.ClearCachedTokenAsync(System.String,System.Threading.CancellationToken)">
|
<member name="M:StellaOps.Auth.Client.IStellaOpsTokenClient.ClearCachedTokenAsync(System.String,System.Threading.CancellationToken)">
|
||||||
<summary>
|
<summary>
|
||||||
Removes a cached entry.
|
Removes a cached entry.
|
||||||
</summary>
|
</summary>
|
||||||
</member>
|
</member>
|
||||||
<member name="T:StellaOps.Auth.Client.ServiceCollectionExtensions">
|
<member name="T:StellaOps.Auth.Client.ServiceCollectionExtensions">
|
||||||
<summary>
|
<summary>
|
||||||
DI helpers for the StellaOps auth client.
|
DI helpers for the StellaOps auth client.
|
||||||
</summary>
|
</summary>
|
||||||
</member>
|
</member>
|
||||||
<member name="M:StellaOps.Auth.Client.ServiceCollectionExtensions.AddStellaOpsAuthClient(Microsoft.Extensions.DependencyInjection.IServiceCollection,System.Action{StellaOps.Auth.Client.StellaOpsAuthClientOptions})">
|
<member name="M:StellaOps.Auth.Client.ServiceCollectionExtensions.AddStellaOpsAuthClient(Microsoft.Extensions.DependencyInjection.IServiceCollection,System.Action{StellaOps.Auth.Client.StellaOpsAuthClientOptions})">
|
||||||
<summary>
|
<summary>
|
||||||
Registers the StellaOps auth client with the provided configuration.
|
Registers the StellaOps auth client with the provided configuration.
|
||||||
</summary>
|
</summary>
|
||||||
</member>
|
</member>
|
||||||
<member name="M:StellaOps.Auth.Client.ServiceCollectionExtensions.AddStellaOpsFileTokenCache(Microsoft.Extensions.DependencyInjection.IServiceCollection,System.String)">
|
<member name="M:StellaOps.Auth.Client.ServiceCollectionExtensions.AddStellaOpsFileTokenCache(Microsoft.Extensions.DependencyInjection.IServiceCollection,System.String)">
|
||||||
<summary>
|
<summary>
|
||||||
Registers a file-backed token cache implementation.
|
Registers a file-backed token cache implementation.
|
||||||
</summary>
|
</summary>
|
||||||
</member>
|
</member>
|
||||||
<member name="T:StellaOps.Auth.Client.StellaOpsAuthClientOptions">
|
<member name="T:StellaOps.Auth.Client.StellaOpsAuthClientOptions">
|
||||||
<summary>
|
<summary>
|
||||||
Options controlling the StellaOps authentication client.
|
Options controlling the StellaOps authentication client.
|
||||||
</summary>
|
</summary>
|
||||||
</member>
|
</member>
|
||||||
<member name="P:StellaOps.Auth.Client.StellaOpsAuthClientOptions.Authority">
|
<member name="P:StellaOps.Auth.Client.StellaOpsAuthClientOptions.Authority">
|
||||||
<summary>
|
<summary>
|
||||||
Authority (issuer) base URL.
|
Authority (issuer) base URL.
|
||||||
</summary>
|
</summary>
|
||||||
</member>
|
</member>
|
||||||
<member name="P:StellaOps.Auth.Client.StellaOpsAuthClientOptions.ClientId">
|
<member name="P:StellaOps.Auth.Client.StellaOpsAuthClientOptions.ClientId">
|
||||||
<summary>
|
<summary>
|
||||||
OAuth client identifier (optional for password flow).
|
OAuth client identifier (optional for password flow).
|
||||||
</summary>
|
</summary>
|
||||||
</member>
|
</member>
|
||||||
<member name="P:StellaOps.Auth.Client.StellaOpsAuthClientOptions.ClientSecret">
|
<member name="P:StellaOps.Auth.Client.StellaOpsAuthClientOptions.ClientSecret">
|
||||||
<summary>
|
<summary>
|
||||||
OAuth client secret (optional for public clients).
|
OAuth client secret (optional for public clients).
|
||||||
</summary>
|
</summary>
|
||||||
</member>
|
</member>
|
||||||
<member name="P:StellaOps.Auth.Client.StellaOpsAuthClientOptions.DefaultScopes">
|
<member name="P:StellaOps.Auth.Client.StellaOpsAuthClientOptions.DefaultScopes">
|
||||||
<summary>
|
<summary>
|
||||||
Default scopes requested for flows that do not explicitly override them.
|
Default scopes requested for flows that do not explicitly override them.
|
||||||
</summary>
|
</summary>
|
||||||
</member>
|
</member>
|
||||||
<member name="P:StellaOps.Auth.Client.StellaOpsAuthClientOptions.RetryDelays">
|
<member name="P:StellaOps.Auth.Client.StellaOpsAuthClientOptions.RetryDelays">
|
||||||
<summary>
|
<summary>
|
||||||
Retry delays applied by HTTP retry policy (empty uses defaults).
|
Retry delays applied by HTTP retry policy (empty uses defaults).
|
||||||
</summary>
|
</summary>
|
||||||
</member>
|
</member>
|
||||||
<member name="P:StellaOps.Auth.Client.StellaOpsAuthClientOptions.EnableRetries">
|
<member name="P:StellaOps.Auth.Client.StellaOpsAuthClientOptions.EnableRetries">
|
||||||
<summary>
|
<summary>
|
||||||
Gets or sets a value indicating whether HTTP retry policies are enabled.
|
Gets or sets a value indicating whether HTTP retry policies are enabled.
|
||||||
</summary>
|
</summary>
|
||||||
</member>
|
</member>
|
||||||
<member name="P:StellaOps.Auth.Client.StellaOpsAuthClientOptions.HttpTimeout">
|
<member name="P:StellaOps.Auth.Client.StellaOpsAuthClientOptions.HttpTimeout">
|
||||||
<summary>
|
<summary>
|
||||||
Timeout applied to discovery and token HTTP requests.
|
Timeout applied to discovery and token HTTP requests.
|
||||||
</summary>
|
</summary>
|
||||||
</member>
|
</member>
|
||||||
<member name="P:StellaOps.Auth.Client.StellaOpsAuthClientOptions.DiscoveryCacheLifetime">
|
<member name="P:StellaOps.Auth.Client.StellaOpsAuthClientOptions.DiscoveryCacheLifetime">
|
||||||
<summary>
|
<summary>
|
||||||
Lifetime of cached discovery metadata.
|
Lifetime of cached discovery metadata.
|
||||||
</summary>
|
</summary>
|
||||||
</member>
|
</member>
|
||||||
<member name="P:StellaOps.Auth.Client.StellaOpsAuthClientOptions.JwksCacheLifetime">
|
<member name="P:StellaOps.Auth.Client.StellaOpsAuthClientOptions.JwksCacheLifetime">
|
||||||
<summary>
|
<summary>
|
||||||
Lifetime of cached JWKS metadata.
|
Lifetime of cached JWKS metadata.
|
||||||
</summary>
|
</summary>
|
||||||
</member>
|
</member>
|
||||||
<member name="P:StellaOps.Auth.Client.StellaOpsAuthClientOptions.ExpirationSkew">
|
<member name="P:StellaOps.Auth.Client.StellaOpsAuthClientOptions.ExpirationSkew">
|
||||||
<summary>
|
<summary>
|
||||||
Buffer applied when determining cache expiration (default: 30 seconds).
|
Buffer applied when determining cache expiration (default: 30 seconds).
|
||||||
</summary>
|
</summary>
|
||||||
</member>
|
</member>
|
||||||
<member name="P:StellaOps.Auth.Client.StellaOpsAuthClientOptions.AllowOfflineCacheFallback">
|
<member name="P:StellaOps.Auth.Client.StellaOpsAuthClientOptions.AllowOfflineCacheFallback">
|
||||||
<summary>
|
<summary>
|
||||||
Gets or sets a value indicating whether cached discovery/JWKS responses may be served when the Authority is unreachable.
|
Gets or sets a value indicating whether cached discovery/JWKS responses may be served when the Authority is unreachable.
|
||||||
</summary>
|
</summary>
|
||||||
</member>
|
</member>
|
||||||
<member name="P:StellaOps.Auth.Client.StellaOpsAuthClientOptions.OfflineCacheTolerance">
|
<member name="P:StellaOps.Auth.Client.StellaOpsAuthClientOptions.OfflineCacheTolerance">
|
||||||
<summary>
|
<summary>
|
||||||
Additional tolerance window during which stale cache entries remain valid if offline fallback is allowed.
|
Additional tolerance window during which stale cache entries remain valid if offline fallback is allowed.
|
||||||
</summary>
|
</summary>
|
||||||
</member>
|
</member>
|
||||||
<member name="P:StellaOps.Auth.Client.StellaOpsAuthClientOptions.AuthorityUri">
|
<member name="P:StellaOps.Auth.Client.StellaOpsAuthClientOptions.AuthorityUri">
|
||||||
<summary>
|
<summary>
|
||||||
Parsed Authority URI (populated after validation).
|
Parsed Authority URI (populated after validation).
|
||||||
</summary>
|
</summary>
|
||||||
</member>
|
</member>
|
||||||
<member name="P:StellaOps.Auth.Client.StellaOpsAuthClientOptions.NormalizedScopes">
|
<member name="P:StellaOps.Auth.Client.StellaOpsAuthClientOptions.NormalizedScopes">
|
||||||
<summary>
|
<summary>
|
||||||
Normalised scope list (populated after validation).
|
Normalised scope list (populated after validation).
|
||||||
</summary>
|
</summary>
|
||||||
</member>
|
</member>
|
||||||
<member name="P:StellaOps.Auth.Client.StellaOpsAuthClientOptions.NormalizedRetryDelays">
|
<member name="P:StellaOps.Auth.Client.StellaOpsAuthClientOptions.NormalizedRetryDelays">
|
||||||
<summary>
|
<summary>
|
||||||
Normalised retry delays (populated after validation).
|
Normalised retry delays (populated after validation).
|
||||||
</summary>
|
</summary>
|
||||||
</member>
|
</member>
|
||||||
<member name="M:StellaOps.Auth.Client.StellaOpsAuthClientOptions.Validate">
|
<member name="M:StellaOps.Auth.Client.StellaOpsAuthClientOptions.Validate">
|
||||||
<summary>
|
<summary>
|
||||||
Validates required values and normalises scope entries.
|
Validates required values and normalises scope entries.
|
||||||
</summary>
|
</summary>
|
||||||
</member>
|
</member>
|
||||||
<member name="T:StellaOps.Auth.Client.StellaOpsDiscoveryCache">
|
<member name="T:StellaOps.Auth.Client.StellaOpsDiscoveryCache">
|
||||||
<summary>
|
<summary>
|
||||||
Caches Authority discovery metadata.
|
Caches Authority discovery metadata.
|
||||||
</summary>
|
</summary>
|
||||||
</member>
|
</member>
|
||||||
<member name="T:StellaOps.Auth.Client.OpenIdConfiguration">
|
<member name="T:StellaOps.Auth.Client.OpenIdConfiguration">
|
||||||
<summary>
|
<summary>
|
||||||
Minimal OpenID Connect configuration representation.
|
Minimal OpenID Connect configuration representation.
|
||||||
</summary>
|
</summary>
|
||||||
</member>
|
</member>
|
||||||
<member name="M:StellaOps.Auth.Client.OpenIdConfiguration.#ctor(System.Uri,System.Uri)">
|
<member name="M:StellaOps.Auth.Client.OpenIdConfiguration.#ctor(System.Uri,System.Uri)">
|
||||||
<summary>
|
<summary>
|
||||||
Minimal OpenID Connect configuration representation.
|
Minimal OpenID Connect configuration representation.
|
||||||
</summary>
|
</summary>
|
||||||
</member>
|
</member>
|
||||||
<member name="T:StellaOps.Auth.Client.StellaOpsJwksCache">
|
<member name="T:StellaOps.Auth.Client.StellaOpsJwksCache">
|
||||||
<summary>
|
<summary>
|
||||||
Caches JWKS documents for Authority.
|
Caches JWKS documents for Authority.
|
||||||
</summary>
|
</summary>
|
||||||
</member>
|
</member>
|
||||||
<member name="T:StellaOps.Auth.Client.StellaOpsTokenCacheEntry">
|
<member name="T:StellaOps.Auth.Client.StellaOpsTokenCacheEntry">
|
||||||
<summary>
|
<summary>
|
||||||
Represents a cached token entry.
|
Represents a cached token entry.
|
||||||
</summary>
|
</summary>
|
||||||
</member>
|
</member>
|
||||||
<member name="M:StellaOps.Auth.Client.StellaOpsTokenCacheEntry.#ctor(System.String,System.String,System.DateTimeOffset,System.Collections.Generic.IReadOnlyList{System.String},System.String,System.String,System.Collections.Generic.IReadOnlyDictionary{System.String,System.String})">
|
<member name="M:StellaOps.Auth.Client.StellaOpsTokenCacheEntry.#ctor(System.String,System.String,System.DateTimeOffset,System.Collections.Generic.IReadOnlyList{System.String},System.String,System.String,System.Collections.Generic.IReadOnlyDictionary{System.String,System.String})">
|
||||||
<summary>
|
<summary>
|
||||||
Represents a cached token entry.
|
Represents a cached token entry.
|
||||||
</summary>
|
</summary>
|
||||||
</member>
|
</member>
|
||||||
<member name="M:StellaOps.Auth.Client.StellaOpsTokenCacheEntry.IsExpired(System.TimeProvider,System.Nullable{System.TimeSpan})">
|
<member name="M:StellaOps.Auth.Client.StellaOpsTokenCacheEntry.IsExpired(System.TimeProvider,System.Nullable{System.TimeSpan})">
|
||||||
<summary>
|
<summary>
|
||||||
Determines whether the token is expired given the provided <see cref="T:System.TimeProvider"/>.
|
Determines whether the token is expired given the provided <see cref="T:System.TimeProvider"/>.
|
||||||
</summary>
|
</summary>
|
||||||
</member>
|
</member>
|
||||||
<member name="M:StellaOps.Auth.Client.StellaOpsTokenCacheEntry.NormalizeScopes">
|
<member name="M:StellaOps.Auth.Client.StellaOpsTokenCacheEntry.NormalizeScopes">
|
||||||
<summary>
|
<summary>
|
||||||
Creates a copy with scopes normalised.
|
Creates a copy with scopes normalised.
|
||||||
</summary>
|
</summary>
|
||||||
</member>
|
</member>
|
||||||
<member name="T:StellaOps.Auth.Client.StellaOpsTokenClient">
|
<member name="T:StellaOps.Auth.Client.StellaOpsTokenClient">
|
||||||
<summary>
|
<summary>
|
||||||
Default implementation of <see cref="T:StellaOps.Auth.Client.IStellaOpsTokenClient"/>.
|
Default implementation of <see cref="T:StellaOps.Auth.Client.IStellaOpsTokenClient"/>.
|
||||||
</summary>
|
</summary>
|
||||||
</member>
|
</member>
|
||||||
<member name="T:StellaOps.Auth.Client.StellaOpsTokenResult">
|
<member name="T:StellaOps.Auth.Client.StellaOpsTokenResult">
|
||||||
<summary>
|
<summary>
|
||||||
Represents an issued token with metadata.
|
Represents an issued token with metadata.
|
||||||
</summary>
|
</summary>
|
||||||
</member>
|
</member>
|
||||||
<member name="M:StellaOps.Auth.Client.StellaOpsTokenResult.#ctor(System.String,System.String,System.DateTimeOffset,System.Collections.Generic.IReadOnlyList{System.String},System.String,System.String,System.String)">
|
<member name="M:StellaOps.Auth.Client.StellaOpsTokenResult.#ctor(System.String,System.String,System.DateTimeOffset,System.Collections.Generic.IReadOnlyList{System.String},System.String,System.String,System.String)">
|
||||||
<summary>
|
<summary>
|
||||||
Represents an issued token with metadata.
|
Represents an issued token with metadata.
|
||||||
</summary>
|
</summary>
|
||||||
</member>
|
</member>
|
||||||
<member name="M:StellaOps.Auth.Client.StellaOpsTokenResult.ToCacheEntry">
|
<member name="M:StellaOps.Auth.Client.StellaOpsTokenResult.ToCacheEntry">
|
||||||
<summary>
|
<summary>
|
||||||
Converts the result to a cache entry.
|
Converts the result to a cache entry.
|
||||||
</summary>
|
</summary>
|
||||||
</member>
|
</member>
|
||||||
</members>
|
</members>
|
||||||
</doc>
|
</doc>
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,233 +1,233 @@
|
|||||||
<?xml version="1.0"?>
|
<?xml version="1.0"?>
|
||||||
<doc>
|
<doc>
|
||||||
<assembly>
|
<assembly>
|
||||||
<name>StellaOps.Auth.Client</name>
|
<name>StellaOps.Auth.Client</name>
|
||||||
</assembly>
|
</assembly>
|
||||||
<members>
|
<members>
|
||||||
<member name="T:StellaOps.Auth.Client.FileTokenCache">
|
<member name="T:StellaOps.Auth.Client.FileTokenCache">
|
||||||
<summary>
|
<summary>
|
||||||
File-based token cache suitable for CLI/offline usage.
|
File-based token cache suitable for CLI/offline usage.
|
||||||
</summary>
|
</summary>
|
||||||
</member>
|
</member>
|
||||||
<member name="T:StellaOps.Auth.Client.InMemoryTokenCache">
|
<member name="T:StellaOps.Auth.Client.InMemoryTokenCache">
|
||||||
<summary>
|
<summary>
|
||||||
In-memory token cache suitable for service scenarios.
|
In-memory token cache suitable for service scenarios.
|
||||||
</summary>
|
</summary>
|
||||||
</member>
|
</member>
|
||||||
<member name="T:StellaOps.Auth.Client.IStellaOpsTokenCache">
|
<member name="T:StellaOps.Auth.Client.IStellaOpsTokenCache">
|
||||||
<summary>
|
<summary>
|
||||||
Abstraction for caching StellaOps tokens.
|
Abstraction for caching StellaOps tokens.
|
||||||
</summary>
|
</summary>
|
||||||
</member>
|
</member>
|
||||||
<member name="M:StellaOps.Auth.Client.IStellaOpsTokenCache.GetAsync(System.String,System.Threading.CancellationToken)">
|
<member name="M:StellaOps.Auth.Client.IStellaOpsTokenCache.GetAsync(System.String,System.Threading.CancellationToken)">
|
||||||
<summary>
|
<summary>
|
||||||
Retrieves a cached token entry, if present.
|
Retrieves a cached token entry, if present.
|
||||||
</summary>
|
</summary>
|
||||||
</member>
|
</member>
|
||||||
<member name="M:StellaOps.Auth.Client.IStellaOpsTokenCache.SetAsync(System.String,StellaOps.Auth.Client.StellaOpsTokenCacheEntry,System.Threading.CancellationToken)">
|
<member name="M:StellaOps.Auth.Client.IStellaOpsTokenCache.SetAsync(System.String,StellaOps.Auth.Client.StellaOpsTokenCacheEntry,System.Threading.CancellationToken)">
|
||||||
<summary>
|
<summary>
|
||||||
Stores or updates a token entry for the specified key.
|
Stores or updates a token entry for the specified key.
|
||||||
</summary>
|
</summary>
|
||||||
</member>
|
</member>
|
||||||
<member name="M:StellaOps.Auth.Client.IStellaOpsTokenCache.RemoveAsync(System.String,System.Threading.CancellationToken)">
|
<member name="M:StellaOps.Auth.Client.IStellaOpsTokenCache.RemoveAsync(System.String,System.Threading.CancellationToken)">
|
||||||
<summary>
|
<summary>
|
||||||
Removes the cached entry for the specified key.
|
Removes the cached entry for the specified key.
|
||||||
</summary>
|
</summary>
|
||||||
</member>
|
</member>
|
||||||
<member name="T:StellaOps.Auth.Client.IStellaOpsTokenClient">
|
<member name="T:StellaOps.Auth.Client.IStellaOpsTokenClient">
|
||||||
<summary>
|
<summary>
|
||||||
Abstraction for requesting tokens from StellaOps Authority.
|
Abstraction for requesting tokens from StellaOps Authority.
|
||||||
</summary>
|
</summary>
|
||||||
</member>
|
</member>
|
||||||
<member name="M:StellaOps.Auth.Client.IStellaOpsTokenClient.RequestPasswordTokenAsync(System.String,System.String,System.String,System.Collections.Generic.IReadOnlyDictionary{System.String,System.String},System.Threading.CancellationToken)">
|
<member name="M:StellaOps.Auth.Client.IStellaOpsTokenClient.RequestPasswordTokenAsync(System.String,System.String,System.String,System.Collections.Generic.IReadOnlyDictionary{System.String,System.String},System.Threading.CancellationToken)">
|
||||||
<summary>
|
<summary>
|
||||||
Requests an access token using the resource owner password credentials flow.
|
Requests an access token using the resource owner password credentials flow.
|
||||||
</summary>
|
</summary>
|
||||||
</member>
|
</member>
|
||||||
<member name="M:StellaOps.Auth.Client.IStellaOpsTokenClient.RequestClientCredentialsTokenAsync(System.String,System.Collections.Generic.IReadOnlyDictionary{System.String,System.String},System.Threading.CancellationToken)">
|
<member name="M:StellaOps.Auth.Client.IStellaOpsTokenClient.RequestClientCredentialsTokenAsync(System.String,System.Collections.Generic.IReadOnlyDictionary{System.String,System.String},System.Threading.CancellationToken)">
|
||||||
<summary>
|
<summary>
|
||||||
Requests an access token using the client credentials flow.
|
Requests an access token using the client credentials flow.
|
||||||
</summary>
|
</summary>
|
||||||
</member>
|
</member>
|
||||||
<member name="M:StellaOps.Auth.Client.IStellaOpsTokenClient.GetJsonWebKeySetAsync(System.Threading.CancellationToken)">
|
<member name="M:StellaOps.Auth.Client.IStellaOpsTokenClient.GetJsonWebKeySetAsync(System.Threading.CancellationToken)">
|
||||||
<summary>
|
<summary>
|
||||||
Retrieves the cached JWKS document.
|
Retrieves the cached JWKS document.
|
||||||
</summary>
|
</summary>
|
||||||
</member>
|
</member>
|
||||||
<member name="M:StellaOps.Auth.Client.IStellaOpsTokenClient.GetCachedTokenAsync(System.String,System.Threading.CancellationToken)">
|
<member name="M:StellaOps.Auth.Client.IStellaOpsTokenClient.GetCachedTokenAsync(System.String,System.Threading.CancellationToken)">
|
||||||
<summary>
|
<summary>
|
||||||
Retrieves a cached token entry.
|
Retrieves a cached token entry.
|
||||||
</summary>
|
</summary>
|
||||||
</member>
|
</member>
|
||||||
<member name="M:StellaOps.Auth.Client.IStellaOpsTokenClient.CacheTokenAsync(System.String,StellaOps.Auth.Client.StellaOpsTokenCacheEntry,System.Threading.CancellationToken)">
|
<member name="M:StellaOps.Auth.Client.IStellaOpsTokenClient.CacheTokenAsync(System.String,StellaOps.Auth.Client.StellaOpsTokenCacheEntry,System.Threading.CancellationToken)">
|
||||||
<summary>
|
<summary>
|
||||||
Persists a token entry in the cache.
|
Persists a token entry in the cache.
|
||||||
</summary>
|
</summary>
|
||||||
</member>
|
</member>
|
||||||
<member name="M:StellaOps.Auth.Client.IStellaOpsTokenClient.ClearCachedTokenAsync(System.String,System.Threading.CancellationToken)">
|
<member name="M:StellaOps.Auth.Client.IStellaOpsTokenClient.ClearCachedTokenAsync(System.String,System.Threading.CancellationToken)">
|
||||||
<summary>
|
<summary>
|
||||||
Removes a cached entry.
|
Removes a cached entry.
|
||||||
</summary>
|
</summary>
|
||||||
</member>
|
</member>
|
||||||
<member name="T:StellaOps.Auth.Client.ServiceCollectionExtensions">
|
<member name="T:StellaOps.Auth.Client.ServiceCollectionExtensions">
|
||||||
<summary>
|
<summary>
|
||||||
DI helpers for the StellaOps auth client.
|
DI helpers for the StellaOps auth client.
|
||||||
</summary>
|
</summary>
|
||||||
</member>
|
</member>
|
||||||
<member name="M:StellaOps.Auth.Client.ServiceCollectionExtensions.AddStellaOpsAuthClient(Microsoft.Extensions.DependencyInjection.IServiceCollection,System.Action{StellaOps.Auth.Client.StellaOpsAuthClientOptions})">
|
<member name="M:StellaOps.Auth.Client.ServiceCollectionExtensions.AddStellaOpsAuthClient(Microsoft.Extensions.DependencyInjection.IServiceCollection,System.Action{StellaOps.Auth.Client.StellaOpsAuthClientOptions})">
|
||||||
<summary>
|
<summary>
|
||||||
Registers the StellaOps auth client with the provided configuration.
|
Registers the StellaOps auth client with the provided configuration.
|
||||||
</summary>
|
</summary>
|
||||||
</member>
|
</member>
|
||||||
<member name="M:StellaOps.Auth.Client.ServiceCollectionExtensions.AddStellaOpsFileTokenCache(Microsoft.Extensions.DependencyInjection.IServiceCollection,System.String)">
|
<member name="M:StellaOps.Auth.Client.ServiceCollectionExtensions.AddStellaOpsFileTokenCache(Microsoft.Extensions.DependencyInjection.IServiceCollection,System.String)">
|
||||||
<summary>
|
<summary>
|
||||||
Registers a file-backed token cache implementation.
|
Registers a file-backed token cache implementation.
|
||||||
</summary>
|
</summary>
|
||||||
</member>
|
</member>
|
||||||
<member name="T:StellaOps.Auth.Client.StellaOpsAuthClientOptions">
|
<member name="T:StellaOps.Auth.Client.StellaOpsAuthClientOptions">
|
||||||
<summary>
|
<summary>
|
||||||
Options controlling the StellaOps authentication client.
|
Options controlling the StellaOps authentication client.
|
||||||
</summary>
|
</summary>
|
||||||
</member>
|
</member>
|
||||||
<member name="P:StellaOps.Auth.Client.StellaOpsAuthClientOptions.Authority">
|
<member name="P:StellaOps.Auth.Client.StellaOpsAuthClientOptions.Authority">
|
||||||
<summary>
|
<summary>
|
||||||
Authority (issuer) base URL.
|
Authority (issuer) base URL.
|
||||||
</summary>
|
</summary>
|
||||||
</member>
|
</member>
|
||||||
<member name="P:StellaOps.Auth.Client.StellaOpsAuthClientOptions.ClientId">
|
<member name="P:StellaOps.Auth.Client.StellaOpsAuthClientOptions.ClientId">
|
||||||
<summary>
|
<summary>
|
||||||
OAuth client identifier (optional for password flow).
|
OAuth client identifier (optional for password flow).
|
||||||
</summary>
|
</summary>
|
||||||
</member>
|
</member>
|
||||||
<member name="P:StellaOps.Auth.Client.StellaOpsAuthClientOptions.ClientSecret">
|
<member name="P:StellaOps.Auth.Client.StellaOpsAuthClientOptions.ClientSecret">
|
||||||
<summary>
|
<summary>
|
||||||
OAuth client secret (optional for public clients).
|
OAuth client secret (optional for public clients).
|
||||||
</summary>
|
</summary>
|
||||||
</member>
|
</member>
|
||||||
<member name="P:StellaOps.Auth.Client.StellaOpsAuthClientOptions.DefaultScopes">
|
<member name="P:StellaOps.Auth.Client.StellaOpsAuthClientOptions.DefaultScopes">
|
||||||
<summary>
|
<summary>
|
||||||
Default scopes requested for flows that do not explicitly override them.
|
Default scopes requested for flows that do not explicitly override them.
|
||||||
</summary>
|
</summary>
|
||||||
</member>
|
</member>
|
||||||
<member name="P:StellaOps.Auth.Client.StellaOpsAuthClientOptions.RetryDelays">
|
<member name="P:StellaOps.Auth.Client.StellaOpsAuthClientOptions.RetryDelays">
|
||||||
<summary>
|
<summary>
|
||||||
Retry delays applied by HTTP retry policy (empty uses defaults).
|
Retry delays applied by HTTP retry policy (empty uses defaults).
|
||||||
</summary>
|
</summary>
|
||||||
</member>
|
</member>
|
||||||
<member name="P:StellaOps.Auth.Client.StellaOpsAuthClientOptions.EnableRetries">
|
<member name="P:StellaOps.Auth.Client.StellaOpsAuthClientOptions.EnableRetries">
|
||||||
<summary>
|
<summary>
|
||||||
Gets or sets a value indicating whether HTTP retry policies are enabled.
|
Gets or sets a value indicating whether HTTP retry policies are enabled.
|
||||||
</summary>
|
</summary>
|
||||||
</member>
|
</member>
|
||||||
<member name="P:StellaOps.Auth.Client.StellaOpsAuthClientOptions.HttpTimeout">
|
<member name="P:StellaOps.Auth.Client.StellaOpsAuthClientOptions.HttpTimeout">
|
||||||
<summary>
|
<summary>
|
||||||
Timeout applied to discovery and token HTTP requests.
|
Timeout applied to discovery and token HTTP requests.
|
||||||
</summary>
|
</summary>
|
||||||
</member>
|
</member>
|
||||||
<member name="P:StellaOps.Auth.Client.StellaOpsAuthClientOptions.DiscoveryCacheLifetime">
|
<member name="P:StellaOps.Auth.Client.StellaOpsAuthClientOptions.DiscoveryCacheLifetime">
|
||||||
<summary>
|
<summary>
|
||||||
Lifetime of cached discovery metadata.
|
Lifetime of cached discovery metadata.
|
||||||
</summary>
|
</summary>
|
||||||
</member>
|
</member>
|
||||||
<member name="P:StellaOps.Auth.Client.StellaOpsAuthClientOptions.JwksCacheLifetime">
|
<member name="P:StellaOps.Auth.Client.StellaOpsAuthClientOptions.JwksCacheLifetime">
|
||||||
<summary>
|
<summary>
|
||||||
Lifetime of cached JWKS metadata.
|
Lifetime of cached JWKS metadata.
|
||||||
</summary>
|
</summary>
|
||||||
</member>
|
</member>
|
||||||
<member name="P:StellaOps.Auth.Client.StellaOpsAuthClientOptions.ExpirationSkew">
|
<member name="P:StellaOps.Auth.Client.StellaOpsAuthClientOptions.ExpirationSkew">
|
||||||
<summary>
|
<summary>
|
||||||
Buffer applied when determining cache expiration (default: 30 seconds).
|
Buffer applied when determining cache expiration (default: 30 seconds).
|
||||||
</summary>
|
</summary>
|
||||||
</member>
|
</member>
|
||||||
<member name="P:StellaOps.Auth.Client.StellaOpsAuthClientOptions.AllowOfflineCacheFallback">
|
<member name="P:StellaOps.Auth.Client.StellaOpsAuthClientOptions.AllowOfflineCacheFallback">
|
||||||
<summary>
|
<summary>
|
||||||
Gets or sets a value indicating whether cached discovery/JWKS responses may be served when the Authority is unreachable.
|
Gets or sets a value indicating whether cached discovery/JWKS responses may be served when the Authority is unreachable.
|
||||||
</summary>
|
</summary>
|
||||||
</member>
|
</member>
|
||||||
<member name="P:StellaOps.Auth.Client.StellaOpsAuthClientOptions.OfflineCacheTolerance">
|
<member name="P:StellaOps.Auth.Client.StellaOpsAuthClientOptions.OfflineCacheTolerance">
|
||||||
<summary>
|
<summary>
|
||||||
Additional tolerance window during which stale cache entries remain valid if offline fallback is allowed.
|
Additional tolerance window during which stale cache entries remain valid if offline fallback is allowed.
|
||||||
</summary>
|
</summary>
|
||||||
</member>
|
</member>
|
||||||
<member name="P:StellaOps.Auth.Client.StellaOpsAuthClientOptions.AuthorityUri">
|
<member name="P:StellaOps.Auth.Client.StellaOpsAuthClientOptions.AuthorityUri">
|
||||||
<summary>
|
<summary>
|
||||||
Parsed Authority URI (populated after validation).
|
Parsed Authority URI (populated after validation).
|
||||||
</summary>
|
</summary>
|
||||||
</member>
|
</member>
|
||||||
<member name="P:StellaOps.Auth.Client.StellaOpsAuthClientOptions.NormalizedScopes">
|
<member name="P:StellaOps.Auth.Client.StellaOpsAuthClientOptions.NormalizedScopes">
|
||||||
<summary>
|
<summary>
|
||||||
Normalised scope list (populated after validation).
|
Normalised scope list (populated after validation).
|
||||||
</summary>
|
</summary>
|
||||||
</member>
|
</member>
|
||||||
<member name="P:StellaOps.Auth.Client.StellaOpsAuthClientOptions.NormalizedRetryDelays">
|
<member name="P:StellaOps.Auth.Client.StellaOpsAuthClientOptions.NormalizedRetryDelays">
|
||||||
<summary>
|
<summary>
|
||||||
Normalised retry delays (populated after validation).
|
Normalised retry delays (populated after validation).
|
||||||
</summary>
|
</summary>
|
||||||
</member>
|
</member>
|
||||||
<member name="M:StellaOps.Auth.Client.StellaOpsAuthClientOptions.Validate">
|
<member name="M:StellaOps.Auth.Client.StellaOpsAuthClientOptions.Validate">
|
||||||
<summary>
|
<summary>
|
||||||
Validates required values and normalises scope entries.
|
Validates required values and normalises scope entries.
|
||||||
</summary>
|
</summary>
|
||||||
</member>
|
</member>
|
||||||
<member name="T:StellaOps.Auth.Client.StellaOpsDiscoveryCache">
|
<member name="T:StellaOps.Auth.Client.StellaOpsDiscoveryCache">
|
||||||
<summary>
|
<summary>
|
||||||
Caches Authority discovery metadata.
|
Caches Authority discovery metadata.
|
||||||
</summary>
|
</summary>
|
||||||
</member>
|
</member>
|
||||||
<member name="T:StellaOps.Auth.Client.OpenIdConfiguration">
|
<member name="T:StellaOps.Auth.Client.OpenIdConfiguration">
|
||||||
<summary>
|
<summary>
|
||||||
Minimal OpenID Connect configuration representation.
|
Minimal OpenID Connect configuration representation.
|
||||||
</summary>
|
</summary>
|
||||||
</member>
|
</member>
|
||||||
<member name="M:StellaOps.Auth.Client.OpenIdConfiguration.#ctor(System.Uri,System.Uri)">
|
<member name="M:StellaOps.Auth.Client.OpenIdConfiguration.#ctor(System.Uri,System.Uri)">
|
||||||
<summary>
|
<summary>
|
||||||
Minimal OpenID Connect configuration representation.
|
Minimal OpenID Connect configuration representation.
|
||||||
</summary>
|
</summary>
|
||||||
</member>
|
</member>
|
||||||
<member name="T:StellaOps.Auth.Client.StellaOpsJwksCache">
|
<member name="T:StellaOps.Auth.Client.StellaOpsJwksCache">
|
||||||
<summary>
|
<summary>
|
||||||
Caches JWKS documents for Authority.
|
Caches JWKS documents for Authority.
|
||||||
</summary>
|
</summary>
|
||||||
</member>
|
</member>
|
||||||
<member name="T:StellaOps.Auth.Client.StellaOpsTokenCacheEntry">
|
<member name="T:StellaOps.Auth.Client.StellaOpsTokenCacheEntry">
|
||||||
<summary>
|
<summary>
|
||||||
Represents a cached token entry.
|
Represents a cached token entry.
|
||||||
</summary>
|
</summary>
|
||||||
</member>
|
</member>
|
||||||
<member name="M:StellaOps.Auth.Client.StellaOpsTokenCacheEntry.#ctor(System.String,System.String,System.DateTimeOffset,System.Collections.Generic.IReadOnlyList{System.String},System.String,System.String,System.Collections.Generic.IReadOnlyDictionary{System.String,System.String})">
|
<member name="M:StellaOps.Auth.Client.StellaOpsTokenCacheEntry.#ctor(System.String,System.String,System.DateTimeOffset,System.Collections.Generic.IReadOnlyList{System.String},System.String,System.String,System.Collections.Generic.IReadOnlyDictionary{System.String,System.String})">
|
||||||
<summary>
|
<summary>
|
||||||
Represents a cached token entry.
|
Represents a cached token entry.
|
||||||
</summary>
|
</summary>
|
||||||
</member>
|
</member>
|
||||||
<member name="M:StellaOps.Auth.Client.StellaOpsTokenCacheEntry.IsExpired(System.TimeProvider,System.Nullable{System.TimeSpan})">
|
<member name="M:StellaOps.Auth.Client.StellaOpsTokenCacheEntry.IsExpired(System.TimeProvider,System.Nullable{System.TimeSpan})">
|
||||||
<summary>
|
<summary>
|
||||||
Determines whether the token is expired given the provided <see cref="T:System.TimeProvider"/>.
|
Determines whether the token is expired given the provided <see cref="T:System.TimeProvider"/>.
|
||||||
</summary>
|
</summary>
|
||||||
</member>
|
</member>
|
||||||
<member name="M:StellaOps.Auth.Client.StellaOpsTokenCacheEntry.NormalizeScopes">
|
<member name="M:StellaOps.Auth.Client.StellaOpsTokenCacheEntry.NormalizeScopes">
|
||||||
<summary>
|
<summary>
|
||||||
Creates a copy with scopes normalised.
|
Creates a copy with scopes normalised.
|
||||||
</summary>
|
</summary>
|
||||||
</member>
|
</member>
|
||||||
<member name="T:StellaOps.Auth.Client.StellaOpsTokenClient">
|
<member name="T:StellaOps.Auth.Client.StellaOpsTokenClient">
|
||||||
<summary>
|
<summary>
|
||||||
Default implementation of <see cref="T:StellaOps.Auth.Client.IStellaOpsTokenClient"/>.
|
Default implementation of <see cref="T:StellaOps.Auth.Client.IStellaOpsTokenClient"/>.
|
||||||
</summary>
|
</summary>
|
||||||
</member>
|
</member>
|
||||||
<member name="T:StellaOps.Auth.Client.StellaOpsTokenResult">
|
<member name="T:StellaOps.Auth.Client.StellaOpsTokenResult">
|
||||||
<summary>
|
<summary>
|
||||||
Represents an issued token with metadata.
|
Represents an issued token with metadata.
|
||||||
</summary>
|
</summary>
|
||||||
</member>
|
</member>
|
||||||
<member name="M:StellaOps.Auth.Client.StellaOpsTokenResult.#ctor(System.String,System.String,System.DateTimeOffset,System.Collections.Generic.IReadOnlyList{System.String},System.String,System.String,System.String)">
|
<member name="M:StellaOps.Auth.Client.StellaOpsTokenResult.#ctor(System.String,System.String,System.DateTimeOffset,System.Collections.Generic.IReadOnlyList{System.String},System.String,System.String,System.String)">
|
||||||
<summary>
|
<summary>
|
||||||
Represents an issued token with metadata.
|
Represents an issued token with metadata.
|
||||||
</summary>
|
</summary>
|
||||||
</member>
|
</member>
|
||||||
<member name="M:StellaOps.Auth.Client.StellaOpsTokenResult.ToCacheEntry">
|
<member name="M:StellaOps.Auth.Client.StellaOpsTokenResult.ToCacheEntry">
|
||||||
<summary>
|
<summary>
|
||||||
Converts the result to a cache entry.
|
Converts the result to a cache entry.
|
||||||
</summary>
|
</summary>
|
||||||
</member>
|
</member>
|
||||||
</members>
|
</members>
|
||||||
</doc>
|
</doc>
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,11 +1,80 @@
|
|||||||
{
|
{
|
||||||
"payloadType": "application/vnd.stellaops.report+json",
|
"report": {
|
||||||
"payload": "eyJyZXBvcnRJZCI6InJlcG9ydC1hYmMiLCJpbWFnZURpZ2VzdCI6InNoYTI1NjpmZWVkZmFjZSIsImdlbmVyYXRlZEF0IjoiMjAyNS0xMC0xOVQxMjozNDo1NiswMDowMCIsInZlcmRpY3QiOiJibG9ja2VkIiwicG9saWN5Ijp7InJldmlzaW9uSWQiOiJyZXYtNDIiLCJkaWdlc3QiOiJkaWdlc3QtMTIzIn0sInN1bW1hcnkiOnsidG90YWwiOjEsImJsb2NrZWQiOjEsIndhcm5lZCI6MCwiaWdub3JlZCI6MCwicXVpZXRlZCI6MH0sInZlcmRpY3RzIjpbeyJmaW5kaW5nSWQiOiJmaW5kaW5nLTEiLCJzdGF0dXMiOiJCbG9ja2VkIiwic2NvcmUiOjQ3LjUsInNvdXJjZVRydXN0IjoiTlZEIiwicmVhY2hhYmlsaXR5IjoicnVudGltZSJ9XSwiaXNzdWVzIjpbXX0=",
|
"reportId": "report-abc",
|
||||||
"signatures": [
|
"imageDigest": "sha256:feedface",
|
||||||
{
|
"generatedAt": "2025-10-19T12:34:56+00:00",
|
||||||
"keyId": "test-key",
|
"verdict": "blocked",
|
||||||
"algorithm": "hs256",
|
"policy": {
|
||||||
"signature": "signature-value"
|
"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": "eyJyZXBvcnRJZCI6InJlcG9ydC1hYmMiLCJpbWFnZURpZ2VzdCI6InNoYTI1NjpmZWVkZmFjZSIsImdlbmVyYXRlZEF0IjoiMjAyNS0xMC0xOVQxMjozNDo1NiswMDowMCIsInZlcmRpY3QiOiJibG9ja2VkIiwicG9saWN5Ijp7InJldmlzaW9uSWQiOiJyZXYtNDIiLCJkaWdlc3QiOiJkaWdlc3QtMTIzIn0sInN1bW1hcnkiOnsidG90YWwiOjEsImJsb2NrZWQiOjEsIndhcm5lZCI6MCwiaWdub3JlZCI6MCwicXVpZXRlZCI6MH0sInZlcmRpY3RzIjpbeyJmaW5kaW5nSWQiOiJmaW5kaW5nLTEiLCJyZWFjaGFiaWxpdHkiOiJydW50aW1lIiwic2NvcmUiOjQ3LjUsInNvdXJjZVRydXN0IjoiTlZEIiwic3RhdHVzIjoiQmxvY2tlZCJ9XSwiaXNzdWVzIjpbXSwic3VyZmFjZSI6eyJ0ZW5hbnQiOiJ0ZW5hbnQtYWxwaGEiLCJnZW5lcmF0ZWRBdCI6IjIwMjUtMTAtMTlUMTI6MzQ6NTYrMDA6MDAiLCJtYW5pZmVzdERpZ2VzdCI6InNoYTI1Njo0ZmVlODdkMTg2MjkxZGRmYmJjYzJjNTZjOGVkMGU4Mjg1MjBiOGY1MmUxY2RlMGUxM2JiYTA4MmYxMDkxOGQ3IiwibWFuaWZlc3RVcmkiOiJjYXM6Ly9zY2FubmVyLWFydGlmYWN0cy9zY2FubmVyL3N1cmZhY2UvbWFuaWZlc3RzL3RlbmFudC1hbHBoYS9zaGEyNTYvNGYvZWUvNGZlZTg3ZDE4NjI5MWRkZmJiY2MyYzU2YzhlZDBlODI4NTIwYjhmNTJlMWNkZTBlMTNiYmEwODJmMTA5MThkNy5qc29uIiwibWFuaWZlc3QiOnsic2NoZW1hIjoic3RlbGxhb3BzLnN1cmZhY2UubWFuaWZlc3RAMSIsInRlbmFudCI6InRlbmFudC1hbHBoYSIsImltYWdlRGlnZXN0Ijoic2hhMjU2OmZlZWRmYWNlIiwiZ2VuZXJhdGVkQXQiOiIyMDI1LTEwLTE5VDEyOjM0OjU2KzAwOjAwIiwiYXJ0aWZhY3RzIjpbeyJraW5kIjoiZW50cnktdHJhY2UiLCJ1cmkiOiJjYXM6Ly9zY2FubmVyLWFydGlmYWN0cy9zY2FubmVyL2VudHJ5LXRyYWNlL2YwZjBmMGYwZjBmMGYwZjBmMGYwZjBmMGYwZjBmMGYwZjBmMGYwZjBmMGYwZjBmMGYwZjBmMGYwZjBmMGYwL2VudHJ5LXRyYWNlLmpzb24iLCJkaWdlc3QiOiJzaGEyNTY6ZjBmMGYwZjBmMGYwZjBmMGYwZjBmMGYwZjBmMGYwZjBmMGYwZjBmMGYwZjBmMGYwZjBmMGYwZjBmMGYwZjBmMCIsIm1lZGlhVHlwZSI6ImFwcGxpY2F0aW9uL2pzb24iLCJmb3JtYXQiOiJqc29uIiwic2l6ZUJ5dGVzIjo0MDk2fSx7ImtpbmQiOiJzYm9tLWludmVudG9yeSIsInVyaSI6ImNhczovL3NjYW5uZXItYXJ0aWZhY3RzL3NjYW5uZXIvaW1hZ2VzL2ZlZWRmYWNlL3Nib20uY2R4Lmpzb24iLCJkaWdlc3QiOiJzaGEyNTY6MTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMSIsIm1lZGlhVHlwZSI6ImFwcGxpY2F0aW9uL3ZuZC5jeWNsb25lZHgranNvbjt2ZXJzaW9uPTEuNjt2aWV3PWludmVudG9yeSIsImZvcm1hdCI6ImNkeC1qc29uIiwic2l6ZUJ5dGVzIjoyNDU3NiwidmlldyI6ImludmVudG9yeSJ9LHsia2luZCI6InNib20tdXNhZ2UiLCJ1cmkiOiJjYXM6Ly9zY2FubmVyLWFydGlmYWN0cy9zY2FubmVyL2ltYWdlcy9mZWVkZmFjZS9zYm9tLXVzYWdlLmNkeC5qc29uIiwiZGlnZXN0Ijoic2hhMjU2OjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIiLCJtZWRpYVR5cGUiOiJhcHBsaWNhdGlvbi92bmQuY3ljbG9uZWR4K2pzb247dmVyc2lvbj0xLjY7dmlldz11c2FnZSIsImZvcm1hdCI6ImNkeC1qc29uIiwic2l6ZUJ5dGVzIjoxNjM4NCwidmlldyI6InVzYWdlIn1dfX19",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"keyId": "test-key",
|
||||||
|
"algorithm": "hs256",
|
||||||
|
"signature": "signature-value"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,28 +1,28 @@
|
|||||||
using System.Diagnostics.Metrics;
|
using System.Diagnostics.Metrics;
|
||||||
|
|
||||||
namespace StellaOps.AdvisoryAI.Hosting;
|
namespace StellaOps.AdvisoryAI.Hosting;
|
||||||
|
|
||||||
public sealed class AdvisoryAiMetrics
|
public sealed class AdvisoryAiMetrics
|
||||||
{
|
{
|
||||||
private static readonly Meter Meter = new("StellaOps.AdvisoryAI", "1.0.0");
|
private static readonly Meter Meter = new("StellaOps.AdvisoryAI", "1.0.0");
|
||||||
|
|
||||||
private readonly Counter<long> _requests;
|
private readonly Counter<long> _requests;
|
||||||
private readonly Counter<long> _queuePublished;
|
private readonly Counter<long> _queuePublished;
|
||||||
private readonly Counter<long> _queueProcessed;
|
private readonly Counter<long> _queueProcessed;
|
||||||
|
|
||||||
public AdvisoryAiMetrics()
|
public AdvisoryAiMetrics()
|
||||||
{
|
{
|
||||||
_requests = Meter.CreateCounter<long>("advisory_ai_pipeline_requests_total");
|
_requests = Meter.CreateCounter<long>("advisory_ai_pipeline_requests_total");
|
||||||
_queuePublished = Meter.CreateCounter<long>("advisory_ai_pipeline_messages_enqueued_total");
|
_queuePublished = Meter.CreateCounter<long>("advisory_ai_pipeline_messages_enqueued_total");
|
||||||
_queueProcessed = Meter.CreateCounter<long>("advisory_ai_pipeline_messages_processed_total");
|
_queueProcessed = Meter.CreateCounter<long>("advisory_ai_pipeline_messages_processed_total");
|
||||||
}
|
}
|
||||||
|
|
||||||
public void RecordRequest(string taskType)
|
public void RecordRequest(string taskType)
|
||||||
=> _requests.Add(1, KeyValuePair.Create<string, object?>("task_type", taskType));
|
=> _requests.Add(1, KeyValuePair.Create<string, object?>("task_type", taskType));
|
||||||
|
|
||||||
public void RecordEnqueued(string taskType)
|
public void RecordEnqueued(string taskType)
|
||||||
=> _queuePublished.Add(1, KeyValuePair.Create<string, object?>("task_type", taskType));
|
=> _queuePublished.Add(1, KeyValuePair.Create<string, object?>("task_type", taskType));
|
||||||
|
|
||||||
public void RecordProcessed(string taskType)
|
public void RecordProcessed(string taskType)
|
||||||
=> _queueProcessed.Add(1, KeyValuePair.Create<string, object?>("task_type", taskType));
|
=> _queueProcessed.Add(1, KeyValuePair.Create<string, object?>("task_type", taskType));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,33 +1,33 @@
|
|||||||
using System;
|
using System;
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using StellaOps.AdvisoryAI.DependencyInjection;
|
using StellaOps.AdvisoryAI.DependencyInjection;
|
||||||
using StellaOps.AdvisoryAI.Providers;
|
using StellaOps.AdvisoryAI.Providers;
|
||||||
using StellaOps.AdvisoryAI.Queue;
|
using StellaOps.AdvisoryAI.Queue;
|
||||||
|
|
||||||
namespace StellaOps.AdvisoryAI.Hosting;
|
namespace StellaOps.AdvisoryAI.Hosting;
|
||||||
|
|
||||||
public static class ServiceCollectionExtensions
|
public static class ServiceCollectionExtensions
|
||||||
{
|
{
|
||||||
public static IServiceCollection AddAdvisoryAiCore(
|
public static IServiceCollection AddAdvisoryAiCore(
|
||||||
this IServiceCollection services,
|
this IServiceCollection services,
|
||||||
IConfiguration configuration,
|
IConfiguration configuration,
|
||||||
Action<AdvisoryAiServiceOptions>? configure = null)
|
Action<AdvisoryAiServiceOptions>? configure = null)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(services);
|
ArgumentNullException.ThrowIfNull(services);
|
||||||
ArgumentNullException.ThrowIfNull(configuration);
|
ArgumentNullException.ThrowIfNull(configuration);
|
||||||
|
|
||||||
services.AddOptions<AdvisoryAiServiceOptions>()
|
services.AddOptions<AdvisoryAiServiceOptions>()
|
||||||
.Bind(configuration.GetSection("AdvisoryAI"))
|
.Bind(configuration.GetSection("AdvisoryAI"))
|
||||||
.PostConfigure(options =>
|
.PostConfigure(options =>
|
||||||
{
|
{
|
||||||
configure?.Invoke(options);
|
configure?.Invoke(options);
|
||||||
AdvisoryAiServiceOptionsValidator.Validate(options);
|
AdvisoryAiServiceOptionsValidator.Validate(options);
|
||||||
})
|
})
|
||||||
.ValidateOnStart();
|
.ValidateOnStart();
|
||||||
|
|
||||||
services.AddOptions<SbomContextClientOptions>()
|
services.AddOptions<SbomContextClientOptions>()
|
||||||
.Configure<IOptions<AdvisoryAiServiceOptions>>((target, source) =>
|
.Configure<IOptions<AdvisoryAiServiceOptions>>((target, source) =>
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net10.0</TargetFramework>
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
<LangVersion>preview</LangVersion>
|
<LangVersion>preview</LangVersion>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\StellaOps.AdvisoryAI\StellaOps.AdvisoryAI.csproj" />
|
<ProjectReference Include="..\StellaOps.AdvisoryAI\StellaOps.AdvisoryAI.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net10.0</TargetFramework>
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
<LangVersion>preview</LangVersion>
|
<LangVersion>preview</LangVersion>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\StellaOps.AdvisoryAI\StellaOps.AdvisoryAI.csproj" />
|
<ProjectReference Include="..\StellaOps.AdvisoryAI\StellaOps.AdvisoryAI.csproj" />
|
||||||
<ProjectReference Include="..\StellaOps.AdvisoryAI.Hosting\StellaOps.AdvisoryAI.Hosting.csproj" />
|
<ProjectReference Include="..\StellaOps.AdvisoryAI.Hosting\StellaOps.AdvisoryAI.Hosting.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"Logging": {
|
"Logging": {
|
||||||
"LogLevel": {
|
"LogLevel": {
|
||||||
"Default": "Debug",
|
"Default": "Debug",
|
||||||
"Microsoft.AspNetCore": "Warning"
|
"Microsoft.AspNetCore": "Warning"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
{
|
{
|
||||||
"AdvisoryAI": {
|
"AdvisoryAI": {
|
||||||
"SbomBaseAddress": "http://localhost:5210/",
|
"SbomBaseAddress": "http://localhost:5210/",
|
||||||
"Queue": {
|
"Queue": {
|
||||||
"DirectoryPath": "../var/advisory-ai-queue"
|
"DirectoryPath": "../var/advisory-ai-queue"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Logging": {
|
"Logging": {
|
||||||
"LogLevel": {
|
"LogLevel": {
|
||||||
"Default": "Information",
|
"Default": "Information",
|
||||||
"Microsoft.AspNetCore": "Warning"
|
"Microsoft.AspNetCore": "Warning"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk.Worker">
|
<Project Sdk="Microsoft.NET.Sdk.Worker">
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net10.0</TargetFramework>
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
<LangVersion>preview</LangVersion>
|
<LangVersion>preview</LangVersion>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\StellaOps.AdvisoryAI\StellaOps.AdvisoryAI.csproj" />
|
<ProjectReference Include="..\StellaOps.AdvisoryAI\StellaOps.AdvisoryAI.csproj" />
|
||||||
<ProjectReference Include="..\StellaOps.AdvisoryAI.Hosting\StellaOps.AdvisoryAI.Hosting.csproj" />
|
<ProjectReference Include="..\StellaOps.AdvisoryAI.Hosting\StellaOps.AdvisoryAI.Hosting.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"Logging": {
|
"Logging": {
|
||||||
"LogLevel": {
|
"LogLevel": {
|
||||||
"Default": "Debug"
|
"Default": "Debug"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
{
|
{
|
||||||
"AdvisoryAI": {
|
"AdvisoryAI": {
|
||||||
"SbomBaseAddress": "http://localhost:5210/",
|
"SbomBaseAddress": "http://localhost:5210/",
|
||||||
"Queue": {
|
"Queue": {
|
||||||
"DirectoryPath": "../var/advisory-ai-queue"
|
"DirectoryPath": "../var/advisory-ai-queue"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Logging": {
|
"Logging": {
|
||||||
"LogLevel": {
|
"LogLevel": {
|
||||||
"Default": "Information"
|
"Default": "Information"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,235 +1,235 @@
|
|||||||
|
|
||||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||||
# Visual Studio Version 17
|
# Visual Studio Version 17
|
||||||
VisualStudioVersion = 17.0.31903.59
|
VisualStudioVersion = 17.0.31903.59
|
||||||
MinimumVisualStudioVersion = 10.0.40219.1
|
MinimumVisualStudioVersion = 10.0.40219.1
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.AdvisoryAI", "StellaOps.AdvisoryAI\StellaOps.AdvisoryAI.csproj", "{E41E2FDA-3827-4B18-8596-B25BDE882D5F}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.AdvisoryAI", "StellaOps.AdvisoryAI\StellaOps.AdvisoryAI.csproj", "{E41E2FDA-3827-4B18-8596-B25BDE882D5F}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "__Tests", "__Tests", "{56BCE1BF-7CBA-7CE8-203D-A88051F1D642}"
|
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "__Tests", "__Tests", "{56BCE1BF-7CBA-7CE8-203D-A88051F1D642}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.AdvisoryAI.Tests", "__Tests\StellaOps.AdvisoryAI.Tests\StellaOps.AdvisoryAI.Tests.csproj", "{F6860DE5-0C7C-4848-8356-7555E3C391A3}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.AdvisoryAI.Tests", "__Tests\StellaOps.AdvisoryAI.Tests\StellaOps.AdvisoryAI.Tests.csproj", "{F6860DE5-0C7C-4848-8356-7555E3C391A3}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Testing", "..\Concelier\__Libraries\StellaOps.Concelier.Testing\StellaOps.Concelier.Testing.csproj", "{B53E4FED-8988-4354-8D1A-D3C618DBFD78}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Testing", "..\Concelier\__Libraries\StellaOps.Concelier.Testing\StellaOps.Concelier.Testing.csproj", "{B53E4FED-8988-4354-8D1A-D3C618DBFD78}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Connector.Common", "..\Concelier\__Libraries\StellaOps.Concelier.Connector.Common\StellaOps.Concelier.Connector.Common.csproj", "{E98A7C01-1619-41A0-A586-84EF9952F75D}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Connector.Common", "..\Concelier\__Libraries\StellaOps.Concelier.Connector.Common\StellaOps.Concelier.Connector.Common.csproj", "{E98A7C01-1619-41A0-A586-84EF9952F75D}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Storage.Mongo", "..\Concelier\__Libraries\StellaOps.Concelier.Storage.Mongo\StellaOps.Concelier.Storage.Mongo.csproj", "{973DD52D-AD3C-4526-92CB-F35FDD9AEA10}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Storage.Mongo", "..\Concelier\__Libraries\StellaOps.Concelier.Storage.Mongo\StellaOps.Concelier.Storage.Mongo.csproj", "{973DD52D-AD3C-4526-92CB-F35FDD9AEA10}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Core", "..\Concelier\__Libraries\StellaOps.Concelier.Core\StellaOps.Concelier.Core.csproj", "{F7FB8ABD-31D7-4B4D-8B2A-F4D2B696ACAF}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Core", "..\Concelier\__Libraries\StellaOps.Concelier.Core\StellaOps.Concelier.Core.csproj", "{F7FB8ABD-31D7-4B4D-8B2A-F4D2B696ACAF}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Models", "..\Concelier\__Libraries\StellaOps.Concelier.Models\StellaOps.Concelier.Models.csproj", "{BBB5CD3C-866A-4298-ACE1-598413631CF5}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Models", "..\Concelier\__Libraries\StellaOps.Concelier.Models\StellaOps.Concelier.Models.csproj", "{BBB5CD3C-866A-4298-ACE1-598413631CF5}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.RawModels", "..\Concelier\__Libraries\StellaOps.Concelier.RawModels\StellaOps.Concelier.RawModels.csproj", "{7E3D9A33-BD0E-424A-88E6-F4440E386A3C}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.RawModels", "..\Concelier\__Libraries\StellaOps.Concelier.RawModels\StellaOps.Concelier.RawModels.csproj", "{7E3D9A33-BD0E-424A-88E6-F4440E386A3C}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Normalization", "..\Concelier\__Libraries\StellaOps.Concelier.Normalization\StellaOps.Concelier.Normalization.csproj", "{1313202A-E8A8-41E3-80BC-472096074681}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Normalization", "..\Concelier\__Libraries\StellaOps.Concelier.Normalization\StellaOps.Concelier.Normalization.csproj", "{1313202A-E8A8-41E3-80BC-472096074681}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Plugin", "..\__Libraries\StellaOps.Plugin\StellaOps.Plugin.csproj", "{1CC5F6F8-DF9A-4BCC-8C69-79E2DF604F6D}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Plugin", "..\__Libraries\StellaOps.Plugin\StellaOps.Plugin.csproj", "{1CC5F6F8-DF9A-4BCC-8C69-79E2DF604F6D}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.DependencyInjection", "..\__Libraries\StellaOps.DependencyInjection\StellaOps.DependencyInjection.csproj", "{F567F20C-552F-4761-941A-0552CEF68160}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.DependencyInjection", "..\__Libraries\StellaOps.DependencyInjection\StellaOps.DependencyInjection.csproj", "{F567F20C-552F-4761-941A-0552CEF68160}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Aoc", "..\Aoc\__Libraries\StellaOps.Aoc\StellaOps.Aoc.csproj", "{C8CE71D3-952A-43F7-9346-20113E37F672}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Aoc", "..\Aoc\__Libraries\StellaOps.Aoc\StellaOps.Aoc.csproj", "{C8CE71D3-952A-43F7-9346-20113E37F672}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.AdvisoryAI.Hosting", "StellaOps.AdvisoryAI.Hosting\StellaOps.AdvisoryAI.Hosting.csproj", "{F3E0EA9E-E4F0-428A-804B-A599870B971D}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.AdvisoryAI.Hosting", "StellaOps.AdvisoryAI.Hosting\StellaOps.AdvisoryAI.Hosting.csproj", "{F3E0EA9E-E4F0-428A-804B-A599870B971D}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.AdvisoryAI.WebService", "StellaOps.AdvisoryAI.WebService\StellaOps.AdvisoryAI.WebService.csproj", "{AD5CEACE-7BF5-4D48-B473-D60188844A0A}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.AdvisoryAI.WebService", "StellaOps.AdvisoryAI.WebService\StellaOps.AdvisoryAI.WebService.csproj", "{AD5CEACE-7BF5-4D48-B473-D60188844A0A}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.AdvisoryAI.Worker", "StellaOps.AdvisoryAI.Worker\StellaOps.AdvisoryAI.Worker.csproj", "{BC68381E-B6EF-4481-8487-00267624D18C}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.AdvisoryAI.Worker", "StellaOps.AdvisoryAI.Worker\StellaOps.AdvisoryAI.Worker.csproj", "{BC68381E-B6EF-4481-8487-00267624D18C}"
|
||||||
EndProject
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
Debug|x64 = Debug|x64
|
Debug|x64 = Debug|x64
|
||||||
Debug|x86 = Debug|x86
|
Debug|x86 = Debug|x86
|
||||||
Release|Any CPU = Release|Any CPU
|
Release|Any CPU = Release|Any CPU
|
||||||
Release|x64 = Release|x64
|
Release|x64 = Release|x64
|
||||||
Release|x86 = Release|x86
|
Release|x86 = Release|x86
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||||
{E41E2FDA-3827-4B18-8596-B25BDE882D5F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{E41E2FDA-3827-4B18-8596-B25BDE882D5F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{E41E2FDA-3827-4B18-8596-B25BDE882D5F}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{E41E2FDA-3827-4B18-8596-B25BDE882D5F}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{E41E2FDA-3827-4B18-8596-B25BDE882D5F}.Debug|x64.ActiveCfg = Debug|Any CPU
|
{E41E2FDA-3827-4B18-8596-B25BDE882D5F}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
{E41E2FDA-3827-4B18-8596-B25BDE882D5F}.Debug|x64.Build.0 = Debug|Any CPU
|
{E41E2FDA-3827-4B18-8596-B25BDE882D5F}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
{E41E2FDA-3827-4B18-8596-B25BDE882D5F}.Debug|x86.ActiveCfg = Debug|Any CPU
|
{E41E2FDA-3827-4B18-8596-B25BDE882D5F}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
{E41E2FDA-3827-4B18-8596-B25BDE882D5F}.Debug|x86.Build.0 = Debug|Any CPU
|
{E41E2FDA-3827-4B18-8596-B25BDE882D5F}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
{E41E2FDA-3827-4B18-8596-B25BDE882D5F}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{E41E2FDA-3827-4B18-8596-B25BDE882D5F}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{E41E2FDA-3827-4B18-8596-B25BDE882D5F}.Release|Any CPU.Build.0 = Release|Any CPU
|
{E41E2FDA-3827-4B18-8596-B25BDE882D5F}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
{E41E2FDA-3827-4B18-8596-B25BDE882D5F}.Release|x64.ActiveCfg = Release|Any CPU
|
{E41E2FDA-3827-4B18-8596-B25BDE882D5F}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
{E41E2FDA-3827-4B18-8596-B25BDE882D5F}.Release|x64.Build.0 = Release|Any CPU
|
{E41E2FDA-3827-4B18-8596-B25BDE882D5F}.Release|x64.Build.0 = Release|Any CPU
|
||||||
{E41E2FDA-3827-4B18-8596-B25BDE882D5F}.Release|x86.ActiveCfg = Release|Any CPU
|
{E41E2FDA-3827-4B18-8596-B25BDE882D5F}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
{E41E2FDA-3827-4B18-8596-B25BDE882D5F}.Release|x86.Build.0 = Release|Any CPU
|
{E41E2FDA-3827-4B18-8596-B25BDE882D5F}.Release|x86.Build.0 = Release|Any CPU
|
||||||
{F6860DE5-0C7C-4848-8356-7555E3C391A3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{F6860DE5-0C7C-4848-8356-7555E3C391A3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{F6860DE5-0C7C-4848-8356-7555E3C391A3}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{F6860DE5-0C7C-4848-8356-7555E3C391A3}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{F6860DE5-0C7C-4848-8356-7555E3C391A3}.Debug|x64.ActiveCfg = Debug|Any CPU
|
{F6860DE5-0C7C-4848-8356-7555E3C391A3}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
{F6860DE5-0C7C-4848-8356-7555E3C391A3}.Debug|x64.Build.0 = Debug|Any CPU
|
{F6860DE5-0C7C-4848-8356-7555E3C391A3}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
{F6860DE5-0C7C-4848-8356-7555E3C391A3}.Debug|x86.ActiveCfg = Debug|Any CPU
|
{F6860DE5-0C7C-4848-8356-7555E3C391A3}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
{F6860DE5-0C7C-4848-8356-7555E3C391A3}.Debug|x86.Build.0 = Debug|Any CPU
|
{F6860DE5-0C7C-4848-8356-7555E3C391A3}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
{F6860DE5-0C7C-4848-8356-7555E3C391A3}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{F6860DE5-0C7C-4848-8356-7555E3C391A3}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{F6860DE5-0C7C-4848-8356-7555E3C391A3}.Release|Any CPU.Build.0 = Release|Any CPU
|
{F6860DE5-0C7C-4848-8356-7555E3C391A3}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
{F6860DE5-0C7C-4848-8356-7555E3C391A3}.Release|x64.ActiveCfg = Release|Any CPU
|
{F6860DE5-0C7C-4848-8356-7555E3C391A3}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
{F6860DE5-0C7C-4848-8356-7555E3C391A3}.Release|x64.Build.0 = Release|Any CPU
|
{F6860DE5-0C7C-4848-8356-7555E3C391A3}.Release|x64.Build.0 = Release|Any CPU
|
||||||
{F6860DE5-0C7C-4848-8356-7555E3C391A3}.Release|x86.ActiveCfg = Release|Any CPU
|
{F6860DE5-0C7C-4848-8356-7555E3C391A3}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
{F6860DE5-0C7C-4848-8356-7555E3C391A3}.Release|x86.Build.0 = Release|Any CPU
|
{F6860DE5-0C7C-4848-8356-7555E3C391A3}.Release|x86.Build.0 = Release|Any CPU
|
||||||
{B53E4FED-8988-4354-8D1A-D3C618DBFD78}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{B53E4FED-8988-4354-8D1A-D3C618DBFD78}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{B53E4FED-8988-4354-8D1A-D3C618DBFD78}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{B53E4FED-8988-4354-8D1A-D3C618DBFD78}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{B53E4FED-8988-4354-8D1A-D3C618DBFD78}.Debug|x64.ActiveCfg = Debug|Any CPU
|
{B53E4FED-8988-4354-8D1A-D3C618DBFD78}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
{B53E4FED-8988-4354-8D1A-D3C618DBFD78}.Debug|x64.Build.0 = Debug|Any CPU
|
{B53E4FED-8988-4354-8D1A-D3C618DBFD78}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
{B53E4FED-8988-4354-8D1A-D3C618DBFD78}.Debug|x86.ActiveCfg = Debug|Any CPU
|
{B53E4FED-8988-4354-8D1A-D3C618DBFD78}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
{B53E4FED-8988-4354-8D1A-D3C618DBFD78}.Debug|x86.Build.0 = Debug|Any CPU
|
{B53E4FED-8988-4354-8D1A-D3C618DBFD78}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
{B53E4FED-8988-4354-8D1A-D3C618DBFD78}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{B53E4FED-8988-4354-8D1A-D3C618DBFD78}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{B53E4FED-8988-4354-8D1A-D3C618DBFD78}.Release|Any CPU.Build.0 = Release|Any CPU
|
{B53E4FED-8988-4354-8D1A-D3C618DBFD78}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
{B53E4FED-8988-4354-8D1A-D3C618DBFD78}.Release|x64.ActiveCfg = Release|Any CPU
|
{B53E4FED-8988-4354-8D1A-D3C618DBFD78}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
{B53E4FED-8988-4354-8D1A-D3C618DBFD78}.Release|x64.Build.0 = Release|Any CPU
|
{B53E4FED-8988-4354-8D1A-D3C618DBFD78}.Release|x64.Build.0 = Release|Any CPU
|
||||||
{B53E4FED-8988-4354-8D1A-D3C618DBFD78}.Release|x86.ActiveCfg = Release|Any CPU
|
{B53E4FED-8988-4354-8D1A-D3C618DBFD78}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
{B53E4FED-8988-4354-8D1A-D3C618DBFD78}.Release|x86.Build.0 = Release|Any CPU
|
{B53E4FED-8988-4354-8D1A-D3C618DBFD78}.Release|x86.Build.0 = Release|Any CPU
|
||||||
{E98A7C01-1619-41A0-A586-84EF9952F75D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{E98A7C01-1619-41A0-A586-84EF9952F75D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{E98A7C01-1619-41A0-A586-84EF9952F75D}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{E98A7C01-1619-41A0-A586-84EF9952F75D}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{E98A7C01-1619-41A0-A586-84EF9952F75D}.Debug|x64.ActiveCfg = Debug|Any CPU
|
{E98A7C01-1619-41A0-A586-84EF9952F75D}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
{E98A7C01-1619-41A0-A586-84EF9952F75D}.Debug|x64.Build.0 = Debug|Any CPU
|
{E98A7C01-1619-41A0-A586-84EF9952F75D}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
{E98A7C01-1619-41A0-A586-84EF9952F75D}.Debug|x86.ActiveCfg = Debug|Any CPU
|
{E98A7C01-1619-41A0-A586-84EF9952F75D}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
{E98A7C01-1619-41A0-A586-84EF9952F75D}.Debug|x86.Build.0 = Debug|Any CPU
|
{E98A7C01-1619-41A0-A586-84EF9952F75D}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
{E98A7C01-1619-41A0-A586-84EF9952F75D}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{E98A7C01-1619-41A0-A586-84EF9952F75D}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{E98A7C01-1619-41A0-A586-84EF9952F75D}.Release|Any CPU.Build.0 = Release|Any CPU
|
{E98A7C01-1619-41A0-A586-84EF9952F75D}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
{E98A7C01-1619-41A0-A586-84EF9952F75D}.Release|x64.ActiveCfg = Release|Any CPU
|
{E98A7C01-1619-41A0-A586-84EF9952F75D}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
{E98A7C01-1619-41A0-A586-84EF9952F75D}.Release|x64.Build.0 = Release|Any CPU
|
{E98A7C01-1619-41A0-A586-84EF9952F75D}.Release|x64.Build.0 = Release|Any CPU
|
||||||
{E98A7C01-1619-41A0-A586-84EF9952F75D}.Release|x86.ActiveCfg = Release|Any CPU
|
{E98A7C01-1619-41A0-A586-84EF9952F75D}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
{E98A7C01-1619-41A0-A586-84EF9952F75D}.Release|x86.Build.0 = Release|Any CPU
|
{E98A7C01-1619-41A0-A586-84EF9952F75D}.Release|x86.Build.0 = Release|Any CPU
|
||||||
{973DD52D-AD3C-4526-92CB-F35FDD9AEA10}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{973DD52D-AD3C-4526-92CB-F35FDD9AEA10}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{973DD52D-AD3C-4526-92CB-F35FDD9AEA10}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{973DD52D-AD3C-4526-92CB-F35FDD9AEA10}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{973DD52D-AD3C-4526-92CB-F35FDD9AEA10}.Debug|x64.ActiveCfg = Debug|Any CPU
|
{973DD52D-AD3C-4526-92CB-F35FDD9AEA10}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
{973DD52D-AD3C-4526-92CB-F35FDD9AEA10}.Debug|x64.Build.0 = Debug|Any CPU
|
{973DD52D-AD3C-4526-92CB-F35FDD9AEA10}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
{973DD52D-AD3C-4526-92CB-F35FDD9AEA10}.Debug|x86.ActiveCfg = Debug|Any CPU
|
{973DD52D-AD3C-4526-92CB-F35FDD9AEA10}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
{973DD52D-AD3C-4526-92CB-F35FDD9AEA10}.Debug|x86.Build.0 = Debug|Any CPU
|
{973DD52D-AD3C-4526-92CB-F35FDD9AEA10}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
{973DD52D-AD3C-4526-92CB-F35FDD9AEA10}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{973DD52D-AD3C-4526-92CB-F35FDD9AEA10}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{973DD52D-AD3C-4526-92CB-F35FDD9AEA10}.Release|Any CPU.Build.0 = Release|Any CPU
|
{973DD52D-AD3C-4526-92CB-F35FDD9AEA10}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
{973DD52D-AD3C-4526-92CB-F35FDD9AEA10}.Release|x64.ActiveCfg = Release|Any CPU
|
{973DD52D-AD3C-4526-92CB-F35FDD9AEA10}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
{973DD52D-AD3C-4526-92CB-F35FDD9AEA10}.Release|x64.Build.0 = Release|Any CPU
|
{973DD52D-AD3C-4526-92CB-F35FDD9AEA10}.Release|x64.Build.0 = Release|Any CPU
|
||||||
{973DD52D-AD3C-4526-92CB-F35FDD9AEA10}.Release|x86.ActiveCfg = Release|Any CPU
|
{973DD52D-AD3C-4526-92CB-F35FDD9AEA10}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
{973DD52D-AD3C-4526-92CB-F35FDD9AEA10}.Release|x86.Build.0 = Release|Any CPU
|
{973DD52D-AD3C-4526-92CB-F35FDD9AEA10}.Release|x86.Build.0 = Release|Any CPU
|
||||||
{F7FB8ABD-31D7-4B4D-8B2A-F4D2B696ACAF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{F7FB8ABD-31D7-4B4D-8B2A-F4D2B696ACAF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{F7FB8ABD-31D7-4B4D-8B2A-F4D2B696ACAF}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{F7FB8ABD-31D7-4B4D-8B2A-F4D2B696ACAF}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{F7FB8ABD-31D7-4B4D-8B2A-F4D2B696ACAF}.Debug|x64.ActiveCfg = Debug|Any CPU
|
{F7FB8ABD-31D7-4B4D-8B2A-F4D2B696ACAF}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
{F7FB8ABD-31D7-4B4D-8B2A-F4D2B696ACAF}.Debug|x64.Build.0 = Debug|Any CPU
|
{F7FB8ABD-31D7-4B4D-8B2A-F4D2B696ACAF}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
{F7FB8ABD-31D7-4B4D-8B2A-F4D2B696ACAF}.Debug|x86.ActiveCfg = Debug|Any CPU
|
{F7FB8ABD-31D7-4B4D-8B2A-F4D2B696ACAF}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
{F7FB8ABD-31D7-4B4D-8B2A-F4D2B696ACAF}.Debug|x86.Build.0 = Debug|Any CPU
|
{F7FB8ABD-31D7-4B4D-8B2A-F4D2B696ACAF}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
{F7FB8ABD-31D7-4B4D-8B2A-F4D2B696ACAF}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{F7FB8ABD-31D7-4B4D-8B2A-F4D2B696ACAF}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{F7FB8ABD-31D7-4B4D-8B2A-F4D2B696ACAF}.Release|Any CPU.Build.0 = Release|Any CPU
|
{F7FB8ABD-31D7-4B4D-8B2A-F4D2B696ACAF}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
{F7FB8ABD-31D7-4B4D-8B2A-F4D2B696ACAF}.Release|x64.ActiveCfg = Release|Any CPU
|
{F7FB8ABD-31D7-4B4D-8B2A-F4D2B696ACAF}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
{F7FB8ABD-31D7-4B4D-8B2A-F4D2B696ACAF}.Release|x64.Build.0 = Release|Any CPU
|
{F7FB8ABD-31D7-4B4D-8B2A-F4D2B696ACAF}.Release|x64.Build.0 = Release|Any CPU
|
||||||
{F7FB8ABD-31D7-4B4D-8B2A-F4D2B696ACAF}.Release|x86.ActiveCfg = Release|Any CPU
|
{F7FB8ABD-31D7-4B4D-8B2A-F4D2B696ACAF}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
{F7FB8ABD-31D7-4B4D-8B2A-F4D2B696ACAF}.Release|x86.Build.0 = Release|Any CPU
|
{F7FB8ABD-31D7-4B4D-8B2A-F4D2B696ACAF}.Release|x86.Build.0 = Release|Any CPU
|
||||||
{BBB5CD3C-866A-4298-ACE1-598413631CF5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{BBB5CD3C-866A-4298-ACE1-598413631CF5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{BBB5CD3C-866A-4298-ACE1-598413631CF5}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{BBB5CD3C-866A-4298-ACE1-598413631CF5}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{BBB5CD3C-866A-4298-ACE1-598413631CF5}.Debug|x64.ActiveCfg = Debug|Any CPU
|
{BBB5CD3C-866A-4298-ACE1-598413631CF5}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
{BBB5CD3C-866A-4298-ACE1-598413631CF5}.Debug|x64.Build.0 = Debug|Any CPU
|
{BBB5CD3C-866A-4298-ACE1-598413631CF5}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
{BBB5CD3C-866A-4298-ACE1-598413631CF5}.Debug|x86.ActiveCfg = Debug|Any CPU
|
{BBB5CD3C-866A-4298-ACE1-598413631CF5}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
{BBB5CD3C-866A-4298-ACE1-598413631CF5}.Debug|x86.Build.0 = Debug|Any CPU
|
{BBB5CD3C-866A-4298-ACE1-598413631CF5}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
{BBB5CD3C-866A-4298-ACE1-598413631CF5}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{BBB5CD3C-866A-4298-ACE1-598413631CF5}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{BBB5CD3C-866A-4298-ACE1-598413631CF5}.Release|Any CPU.Build.0 = Release|Any CPU
|
{BBB5CD3C-866A-4298-ACE1-598413631CF5}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
{BBB5CD3C-866A-4298-ACE1-598413631CF5}.Release|x64.ActiveCfg = Release|Any CPU
|
{BBB5CD3C-866A-4298-ACE1-598413631CF5}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
{BBB5CD3C-866A-4298-ACE1-598413631CF5}.Release|x64.Build.0 = Release|Any CPU
|
{BBB5CD3C-866A-4298-ACE1-598413631CF5}.Release|x64.Build.0 = Release|Any CPU
|
||||||
{BBB5CD3C-866A-4298-ACE1-598413631CF5}.Release|x86.ActiveCfg = Release|Any CPU
|
{BBB5CD3C-866A-4298-ACE1-598413631CF5}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
{BBB5CD3C-866A-4298-ACE1-598413631CF5}.Release|x86.Build.0 = Release|Any CPU
|
{BBB5CD3C-866A-4298-ACE1-598413631CF5}.Release|x86.Build.0 = Release|Any CPU
|
||||||
{7E3D9A33-BD0E-424A-88E6-F4440E386A3C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{7E3D9A33-BD0E-424A-88E6-F4440E386A3C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{7E3D9A33-BD0E-424A-88E6-F4440E386A3C}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{7E3D9A33-BD0E-424A-88E6-F4440E386A3C}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{7E3D9A33-BD0E-424A-88E6-F4440E386A3C}.Debug|x64.ActiveCfg = Debug|Any CPU
|
{7E3D9A33-BD0E-424A-88E6-F4440E386A3C}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
{7E3D9A33-BD0E-424A-88E6-F4440E386A3C}.Debug|x64.Build.0 = Debug|Any CPU
|
{7E3D9A33-BD0E-424A-88E6-F4440E386A3C}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
{7E3D9A33-BD0E-424A-88E6-F4440E386A3C}.Debug|x86.ActiveCfg = Debug|Any CPU
|
{7E3D9A33-BD0E-424A-88E6-F4440E386A3C}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
{7E3D9A33-BD0E-424A-88E6-F4440E386A3C}.Debug|x86.Build.0 = Debug|Any CPU
|
{7E3D9A33-BD0E-424A-88E6-F4440E386A3C}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
{7E3D9A33-BD0E-424A-88E6-F4440E386A3C}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{7E3D9A33-BD0E-424A-88E6-F4440E386A3C}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{7E3D9A33-BD0E-424A-88E6-F4440E386A3C}.Release|Any CPU.Build.0 = Release|Any CPU
|
{7E3D9A33-BD0E-424A-88E6-F4440E386A3C}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
{7E3D9A33-BD0E-424A-88E6-F4440E386A3C}.Release|x64.ActiveCfg = Release|Any CPU
|
{7E3D9A33-BD0E-424A-88E6-F4440E386A3C}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
{7E3D9A33-BD0E-424A-88E6-F4440E386A3C}.Release|x64.Build.0 = Release|Any CPU
|
{7E3D9A33-BD0E-424A-88E6-F4440E386A3C}.Release|x64.Build.0 = Release|Any CPU
|
||||||
{7E3D9A33-BD0E-424A-88E6-F4440E386A3C}.Release|x86.ActiveCfg = Release|Any CPU
|
{7E3D9A33-BD0E-424A-88E6-F4440E386A3C}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
{7E3D9A33-BD0E-424A-88E6-F4440E386A3C}.Release|x86.Build.0 = Release|Any CPU
|
{7E3D9A33-BD0E-424A-88E6-F4440E386A3C}.Release|x86.Build.0 = Release|Any CPU
|
||||||
{1313202A-E8A8-41E3-80BC-472096074681}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{1313202A-E8A8-41E3-80BC-472096074681}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{1313202A-E8A8-41E3-80BC-472096074681}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{1313202A-E8A8-41E3-80BC-472096074681}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{1313202A-E8A8-41E3-80BC-472096074681}.Debug|x64.ActiveCfg = Debug|Any CPU
|
{1313202A-E8A8-41E3-80BC-472096074681}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
{1313202A-E8A8-41E3-80BC-472096074681}.Debug|x64.Build.0 = Debug|Any CPU
|
{1313202A-E8A8-41E3-80BC-472096074681}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
{1313202A-E8A8-41E3-80BC-472096074681}.Debug|x86.ActiveCfg = Debug|Any CPU
|
{1313202A-E8A8-41E3-80BC-472096074681}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
{1313202A-E8A8-41E3-80BC-472096074681}.Debug|x86.Build.0 = Debug|Any CPU
|
{1313202A-E8A8-41E3-80BC-472096074681}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
{1313202A-E8A8-41E3-80BC-472096074681}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{1313202A-E8A8-41E3-80BC-472096074681}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{1313202A-E8A8-41E3-80BC-472096074681}.Release|Any CPU.Build.0 = Release|Any CPU
|
{1313202A-E8A8-41E3-80BC-472096074681}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
{1313202A-E8A8-41E3-80BC-472096074681}.Release|x64.ActiveCfg = Release|Any CPU
|
{1313202A-E8A8-41E3-80BC-472096074681}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
{1313202A-E8A8-41E3-80BC-472096074681}.Release|x64.Build.0 = Release|Any CPU
|
{1313202A-E8A8-41E3-80BC-472096074681}.Release|x64.Build.0 = Release|Any CPU
|
||||||
{1313202A-E8A8-41E3-80BC-472096074681}.Release|x86.ActiveCfg = Release|Any CPU
|
{1313202A-E8A8-41E3-80BC-472096074681}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
{1313202A-E8A8-41E3-80BC-472096074681}.Release|x86.Build.0 = Release|Any CPU
|
{1313202A-E8A8-41E3-80BC-472096074681}.Release|x86.Build.0 = Release|Any CPU
|
||||||
{1CC5F6F8-DF9A-4BCC-8C69-79E2DF604F6D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{1CC5F6F8-DF9A-4BCC-8C69-79E2DF604F6D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{1CC5F6F8-DF9A-4BCC-8C69-79E2DF604F6D}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{1CC5F6F8-DF9A-4BCC-8C69-79E2DF604F6D}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{1CC5F6F8-DF9A-4BCC-8C69-79E2DF604F6D}.Debug|x64.ActiveCfg = Debug|Any CPU
|
{1CC5F6F8-DF9A-4BCC-8C69-79E2DF604F6D}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
{1CC5F6F8-DF9A-4BCC-8C69-79E2DF604F6D}.Debug|x64.Build.0 = Debug|Any CPU
|
{1CC5F6F8-DF9A-4BCC-8C69-79E2DF604F6D}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
{1CC5F6F8-DF9A-4BCC-8C69-79E2DF604F6D}.Debug|x86.ActiveCfg = Debug|Any CPU
|
{1CC5F6F8-DF9A-4BCC-8C69-79E2DF604F6D}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
{1CC5F6F8-DF9A-4BCC-8C69-79E2DF604F6D}.Debug|x86.Build.0 = Debug|Any CPU
|
{1CC5F6F8-DF9A-4BCC-8C69-79E2DF604F6D}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
{1CC5F6F8-DF9A-4BCC-8C69-79E2DF604F6D}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{1CC5F6F8-DF9A-4BCC-8C69-79E2DF604F6D}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{1CC5F6F8-DF9A-4BCC-8C69-79E2DF604F6D}.Release|Any CPU.Build.0 = Release|Any CPU
|
{1CC5F6F8-DF9A-4BCC-8C69-79E2DF604F6D}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
{1CC5F6F8-DF9A-4BCC-8C69-79E2DF604F6D}.Release|x64.ActiveCfg = Release|Any CPU
|
{1CC5F6F8-DF9A-4BCC-8C69-79E2DF604F6D}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
{1CC5F6F8-DF9A-4BCC-8C69-79E2DF604F6D}.Release|x64.Build.0 = Release|Any CPU
|
{1CC5F6F8-DF9A-4BCC-8C69-79E2DF604F6D}.Release|x64.Build.0 = Release|Any CPU
|
||||||
{1CC5F6F8-DF9A-4BCC-8C69-79E2DF604F6D}.Release|x86.ActiveCfg = Release|Any CPU
|
{1CC5F6F8-DF9A-4BCC-8C69-79E2DF604F6D}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
{1CC5F6F8-DF9A-4BCC-8C69-79E2DF604F6D}.Release|x86.Build.0 = Release|Any CPU
|
{1CC5F6F8-DF9A-4BCC-8C69-79E2DF604F6D}.Release|x86.Build.0 = Release|Any CPU
|
||||||
{F567F20C-552F-4761-941A-0552CEF68160}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{F567F20C-552F-4761-941A-0552CEF68160}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{F567F20C-552F-4761-941A-0552CEF68160}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{F567F20C-552F-4761-941A-0552CEF68160}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{F567F20C-552F-4761-941A-0552CEF68160}.Debug|x64.ActiveCfg = Debug|Any CPU
|
{F567F20C-552F-4761-941A-0552CEF68160}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
{F567F20C-552F-4761-941A-0552CEF68160}.Debug|x64.Build.0 = Debug|Any CPU
|
{F567F20C-552F-4761-941A-0552CEF68160}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
{F567F20C-552F-4761-941A-0552CEF68160}.Debug|x86.ActiveCfg = Debug|Any CPU
|
{F567F20C-552F-4761-941A-0552CEF68160}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
{F567F20C-552F-4761-941A-0552CEF68160}.Debug|x86.Build.0 = Debug|Any CPU
|
{F567F20C-552F-4761-941A-0552CEF68160}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
{F567F20C-552F-4761-941A-0552CEF68160}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{F567F20C-552F-4761-941A-0552CEF68160}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{F567F20C-552F-4761-941A-0552CEF68160}.Release|Any CPU.Build.0 = Release|Any CPU
|
{F567F20C-552F-4761-941A-0552CEF68160}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
{F567F20C-552F-4761-941A-0552CEF68160}.Release|x64.ActiveCfg = Release|Any CPU
|
{F567F20C-552F-4761-941A-0552CEF68160}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
{F567F20C-552F-4761-941A-0552CEF68160}.Release|x64.Build.0 = Release|Any CPU
|
{F567F20C-552F-4761-941A-0552CEF68160}.Release|x64.Build.0 = Release|Any CPU
|
||||||
{F567F20C-552F-4761-941A-0552CEF68160}.Release|x86.ActiveCfg = Release|Any CPU
|
{F567F20C-552F-4761-941A-0552CEF68160}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
{F567F20C-552F-4761-941A-0552CEF68160}.Release|x86.Build.0 = Release|Any CPU
|
{F567F20C-552F-4761-941A-0552CEF68160}.Release|x86.Build.0 = Release|Any CPU
|
||||||
{C8CE71D3-952A-43F7-9346-20113E37F672}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{C8CE71D3-952A-43F7-9346-20113E37F672}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{C8CE71D3-952A-43F7-9346-20113E37F672}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{C8CE71D3-952A-43F7-9346-20113E37F672}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{C8CE71D3-952A-43F7-9346-20113E37F672}.Debug|x64.ActiveCfg = Debug|Any CPU
|
{C8CE71D3-952A-43F7-9346-20113E37F672}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
{C8CE71D3-952A-43F7-9346-20113E37F672}.Debug|x64.Build.0 = Debug|Any CPU
|
{C8CE71D3-952A-43F7-9346-20113E37F672}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
{C8CE71D3-952A-43F7-9346-20113E37F672}.Debug|x86.ActiveCfg = Debug|Any CPU
|
{C8CE71D3-952A-43F7-9346-20113E37F672}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
{C8CE71D3-952A-43F7-9346-20113E37F672}.Debug|x86.Build.0 = Debug|Any CPU
|
{C8CE71D3-952A-43F7-9346-20113E37F672}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
{C8CE71D3-952A-43F7-9346-20113E37F672}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{C8CE71D3-952A-43F7-9346-20113E37F672}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{C8CE71D3-952A-43F7-9346-20113E37F672}.Release|Any CPU.Build.0 = Release|Any CPU
|
{C8CE71D3-952A-43F7-9346-20113E37F672}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
{C8CE71D3-952A-43F7-9346-20113E37F672}.Release|x64.ActiveCfg = Release|Any CPU
|
{C8CE71D3-952A-43F7-9346-20113E37F672}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
{C8CE71D3-952A-43F7-9346-20113E37F672}.Release|x64.Build.0 = Release|Any CPU
|
{C8CE71D3-952A-43F7-9346-20113E37F672}.Release|x64.Build.0 = Release|Any CPU
|
||||||
{C8CE71D3-952A-43F7-9346-20113E37F672}.Release|x86.ActiveCfg = Release|Any CPU
|
{C8CE71D3-952A-43F7-9346-20113E37F672}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
{C8CE71D3-952A-43F7-9346-20113E37F672}.Release|x86.Build.0 = Release|Any CPU
|
{C8CE71D3-952A-43F7-9346-20113E37F672}.Release|x86.Build.0 = Release|Any CPU
|
||||||
{F3E0EA9E-E4F0-428A-804B-A599870B971D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{F3E0EA9E-E4F0-428A-804B-A599870B971D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{F3E0EA9E-E4F0-428A-804B-A599870B971D}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{F3E0EA9E-E4F0-428A-804B-A599870B971D}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{F3E0EA9E-E4F0-428A-804B-A599870B971D}.Debug|x64.ActiveCfg = Debug|Any CPU
|
{F3E0EA9E-E4F0-428A-804B-A599870B971D}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
{F3E0EA9E-E4F0-428A-804B-A599870B971D}.Debug|x64.Build.0 = Debug|Any CPU
|
{F3E0EA9E-E4F0-428A-804B-A599870B971D}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
{F3E0EA9E-E4F0-428A-804B-A599870B971D}.Debug|x86.ActiveCfg = Debug|Any CPU
|
{F3E0EA9E-E4F0-428A-804B-A599870B971D}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
{F3E0EA9E-E4F0-428A-804B-A599870B971D}.Debug|x86.Build.0 = Debug|Any CPU
|
{F3E0EA9E-E4F0-428A-804B-A599870B971D}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
{F3E0EA9E-E4F0-428A-804B-A599870B971D}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{F3E0EA9E-E4F0-428A-804B-A599870B971D}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{F3E0EA9E-E4F0-428A-804B-A599870B971D}.Release|Any CPU.Build.0 = Release|Any CPU
|
{F3E0EA9E-E4F0-428A-804B-A599870B971D}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
{F3E0EA9E-E4F0-428A-804B-A599870B971D}.Release|x64.ActiveCfg = Release|Any CPU
|
{F3E0EA9E-E4F0-428A-804B-A599870B971D}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
{F3E0EA9E-E4F0-428A-804B-A599870B971D}.Release|x64.Build.0 = Release|Any CPU
|
{F3E0EA9E-E4F0-428A-804B-A599870B971D}.Release|x64.Build.0 = Release|Any CPU
|
||||||
{F3E0EA9E-E4F0-428A-804B-A599870B971D}.Release|x86.ActiveCfg = Release|Any CPU
|
{F3E0EA9E-E4F0-428A-804B-A599870B971D}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
{F3E0EA9E-E4F0-428A-804B-A599870B971D}.Release|x86.Build.0 = Release|Any CPU
|
{F3E0EA9E-E4F0-428A-804B-A599870B971D}.Release|x86.Build.0 = Release|Any CPU
|
||||||
{AD5CEACE-7BF5-4D48-B473-D60188844A0A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{AD5CEACE-7BF5-4D48-B473-D60188844A0A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{AD5CEACE-7BF5-4D48-B473-D60188844A0A}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{AD5CEACE-7BF5-4D48-B473-D60188844A0A}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{AD5CEACE-7BF5-4D48-B473-D60188844A0A}.Debug|x64.ActiveCfg = Debug|Any CPU
|
{AD5CEACE-7BF5-4D48-B473-D60188844A0A}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
{AD5CEACE-7BF5-4D48-B473-D60188844A0A}.Debug|x64.Build.0 = Debug|Any CPU
|
{AD5CEACE-7BF5-4D48-B473-D60188844A0A}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
{AD5CEACE-7BF5-4D48-B473-D60188844A0A}.Debug|x86.ActiveCfg = Debug|Any CPU
|
{AD5CEACE-7BF5-4D48-B473-D60188844A0A}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
{AD5CEACE-7BF5-4D48-B473-D60188844A0A}.Debug|x86.Build.0 = Debug|Any CPU
|
{AD5CEACE-7BF5-4D48-B473-D60188844A0A}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
{AD5CEACE-7BF5-4D48-B473-D60188844A0A}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{AD5CEACE-7BF5-4D48-B473-D60188844A0A}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{AD5CEACE-7BF5-4D48-B473-D60188844A0A}.Release|Any CPU.Build.0 = Release|Any CPU
|
{AD5CEACE-7BF5-4D48-B473-D60188844A0A}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
{AD5CEACE-7BF5-4D48-B473-D60188844A0A}.Release|x64.ActiveCfg = Release|Any CPU
|
{AD5CEACE-7BF5-4D48-B473-D60188844A0A}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
{AD5CEACE-7BF5-4D48-B473-D60188844A0A}.Release|x64.Build.0 = Release|Any CPU
|
{AD5CEACE-7BF5-4D48-B473-D60188844A0A}.Release|x64.Build.0 = Release|Any CPU
|
||||||
{AD5CEACE-7BF5-4D48-B473-D60188844A0A}.Release|x86.ActiveCfg = Release|Any CPU
|
{AD5CEACE-7BF5-4D48-B473-D60188844A0A}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
{AD5CEACE-7BF5-4D48-B473-D60188844A0A}.Release|x86.Build.0 = Release|Any CPU
|
{AD5CEACE-7BF5-4D48-B473-D60188844A0A}.Release|x86.Build.0 = Release|Any CPU
|
||||||
{BC68381E-B6EF-4481-8487-00267624D18C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{BC68381E-B6EF-4481-8487-00267624D18C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{BC68381E-B6EF-4481-8487-00267624D18C}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{BC68381E-B6EF-4481-8487-00267624D18C}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{BC68381E-B6EF-4481-8487-00267624D18C}.Debug|x64.ActiveCfg = Debug|Any CPU
|
{BC68381E-B6EF-4481-8487-00267624D18C}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
{BC68381E-B6EF-4481-8487-00267624D18C}.Debug|x64.Build.0 = Debug|Any CPU
|
{BC68381E-B6EF-4481-8487-00267624D18C}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
{BC68381E-B6EF-4481-8487-00267624D18C}.Debug|x86.ActiveCfg = Debug|Any CPU
|
{BC68381E-B6EF-4481-8487-00267624D18C}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
{BC68381E-B6EF-4481-8487-00267624D18C}.Debug|x86.Build.0 = Debug|Any CPU
|
{BC68381E-B6EF-4481-8487-00267624D18C}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
{BC68381E-B6EF-4481-8487-00267624D18C}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{BC68381E-B6EF-4481-8487-00267624D18C}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{BC68381E-B6EF-4481-8487-00267624D18C}.Release|Any CPU.Build.0 = Release|Any CPU
|
{BC68381E-B6EF-4481-8487-00267624D18C}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
{BC68381E-B6EF-4481-8487-00267624D18C}.Release|x64.ActiveCfg = Release|Any CPU
|
{BC68381E-B6EF-4481-8487-00267624D18C}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
{BC68381E-B6EF-4481-8487-00267624D18C}.Release|x64.Build.0 = Release|Any CPU
|
{BC68381E-B6EF-4481-8487-00267624D18C}.Release|x64.Build.0 = Release|Any CPU
|
||||||
{BC68381E-B6EF-4481-8487-00267624D18C}.Release|x86.ActiveCfg = Release|Any CPU
|
{BC68381E-B6EF-4481-8487-00267624D18C}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
{BC68381E-B6EF-4481-8487-00267624D18C}.Release|x86.Build.0 = Release|Any CPU
|
{BC68381E-B6EF-4481-8487-00267624D18C}.Release|x86.Build.0 = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(NestedProjects) = preSolution
|
GlobalSection(NestedProjects) = preSolution
|
||||||
{F6860DE5-0C7C-4848-8356-7555E3C391A3} = {56BCE1BF-7CBA-7CE8-203D-A88051F1D642}
|
{F6860DE5-0C7C-4848-8356-7555E3C391A3} = {56BCE1BF-7CBA-7CE8-203D-A88051F1D642}
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
EndGlobal
|
EndGlobal
|
||||||
|
|||||||
@@ -1,41 +1,41 @@
|
|||||||
using System;
|
using System;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using StellaOps.AdvisoryAI.Abstractions;
|
using StellaOps.AdvisoryAI.Abstractions;
|
||||||
using StellaOps.AdvisoryAI.Providers;
|
using StellaOps.AdvisoryAI.Providers;
|
||||||
using StellaOps.AdvisoryAI.Retrievers;
|
using StellaOps.AdvisoryAI.Retrievers;
|
||||||
|
|
||||||
namespace StellaOps.AdvisoryAI.DependencyInjection;
|
namespace StellaOps.AdvisoryAI.DependencyInjection;
|
||||||
|
|
||||||
public static class SbomContextServiceCollectionExtensions
|
public static class SbomContextServiceCollectionExtensions
|
||||||
{
|
{
|
||||||
public static IServiceCollection AddSbomContext(this IServiceCollection services, Action<SbomContextClientOptions>? configure = null)
|
public static IServiceCollection AddSbomContext(this IServiceCollection services, Action<SbomContextClientOptions>? configure = null)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(services);
|
ArgumentNullException.ThrowIfNull(services);
|
||||||
|
|
||||||
var optionsBuilder = services.AddOptions<SbomContextClientOptions>();
|
var optionsBuilder = services.AddOptions<SbomContextClientOptions>();
|
||||||
if (configure is not null)
|
if (configure is not null)
|
||||||
{
|
{
|
||||||
optionsBuilder.Configure(configure);
|
optionsBuilder.Configure(configure);
|
||||||
}
|
}
|
||||||
|
|
||||||
services.AddHttpClient<ISbomContextClient, SbomContextHttpClient>((serviceProvider, client) =>
|
services.AddHttpClient<ISbomContextClient, SbomContextHttpClient>((serviceProvider, client) =>
|
||||||
{
|
{
|
||||||
var options = serviceProvider.GetRequiredService<IOptions<SbomContextClientOptions>>().Value;
|
var options = serviceProvider.GetRequiredService<IOptions<SbomContextClientOptions>>().Value;
|
||||||
if (options.BaseAddress is not null)
|
if (options.BaseAddress is not null)
|
||||||
{
|
{
|
||||||
client.BaseAddress = options.BaseAddress;
|
client.BaseAddress = options.BaseAddress;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(options.Tenant) && !string.IsNullOrWhiteSpace(options.TenantHeaderName))
|
if (!string.IsNullOrWhiteSpace(options.Tenant) && !string.IsNullOrWhiteSpace(options.TenantHeaderName))
|
||||||
{
|
{
|
||||||
client.DefaultRequestHeaders.Remove(options.TenantHeaderName);
|
client.DefaultRequestHeaders.Remove(options.TenantHeaderName);
|
||||||
client.DefaultRequestHeaders.Add(options.TenantHeaderName, options.Tenant);
|
client.DefaultRequestHeaders.Add(options.TenantHeaderName, options.Tenant);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
services.TryAddSingleton<ISbomContextRetriever, SbomContextRetriever>();
|
services.TryAddSingleton<ISbomContextRetriever, SbomContextRetriever>();
|
||||||
return services;
|
return services;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,118 +4,118 @@ using System.Globalization;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Security.Cryptography;
|
using System.Security.Cryptography;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using StellaOps.AdvisoryAI.Abstractions;
|
using StellaOps.AdvisoryAI.Abstractions;
|
||||||
using StellaOps.AdvisoryAI.Context;
|
using StellaOps.AdvisoryAI.Context;
|
||||||
using StellaOps.AdvisoryAI.Tools;
|
using StellaOps.AdvisoryAI.Tools;
|
||||||
|
|
||||||
namespace StellaOps.AdvisoryAI.Orchestration;
|
namespace StellaOps.AdvisoryAI.Orchestration;
|
||||||
|
|
||||||
internal sealed class AdvisoryPipelineOrchestrator : IAdvisoryPipelineOrchestrator
|
internal sealed class AdvisoryPipelineOrchestrator : IAdvisoryPipelineOrchestrator
|
||||||
{
|
{
|
||||||
private readonly IAdvisoryStructuredRetriever _structuredRetriever;
|
private readonly IAdvisoryStructuredRetriever _structuredRetriever;
|
||||||
private readonly IAdvisoryVectorRetriever _vectorRetriever;
|
private readonly IAdvisoryVectorRetriever _vectorRetriever;
|
||||||
private readonly ISbomContextRetriever _sbomContextRetriever;
|
private readonly ISbomContextRetriever _sbomContextRetriever;
|
||||||
private readonly IDeterministicToolset _toolset;
|
private readonly IDeterministicToolset _toolset;
|
||||||
private readonly AdvisoryPipelineOptions _options;
|
private readonly AdvisoryPipelineOptions _options;
|
||||||
private readonly ILogger<AdvisoryPipelineOrchestrator>? _logger;
|
private readonly ILogger<AdvisoryPipelineOrchestrator>? _logger;
|
||||||
|
|
||||||
public AdvisoryPipelineOrchestrator(
|
public AdvisoryPipelineOrchestrator(
|
||||||
IAdvisoryStructuredRetriever structuredRetriever,
|
IAdvisoryStructuredRetriever structuredRetriever,
|
||||||
IAdvisoryVectorRetriever vectorRetriever,
|
IAdvisoryVectorRetriever vectorRetriever,
|
||||||
ISbomContextRetriever sbomContextRetriever,
|
ISbomContextRetriever sbomContextRetriever,
|
||||||
IDeterministicToolset toolset,
|
IDeterministicToolset toolset,
|
||||||
IOptions<AdvisoryPipelineOptions> options,
|
IOptions<AdvisoryPipelineOptions> options,
|
||||||
ILogger<AdvisoryPipelineOrchestrator>? logger = null)
|
ILogger<AdvisoryPipelineOrchestrator>? logger = null)
|
||||||
{
|
{
|
||||||
_structuredRetriever = structuredRetriever ?? throw new ArgumentNullException(nameof(structuredRetriever));
|
_structuredRetriever = structuredRetriever ?? throw new ArgumentNullException(nameof(structuredRetriever));
|
||||||
_vectorRetriever = vectorRetriever ?? throw new ArgumentNullException(nameof(vectorRetriever));
|
_vectorRetriever = vectorRetriever ?? throw new ArgumentNullException(nameof(vectorRetriever));
|
||||||
_sbomContextRetriever = sbomContextRetriever ?? throw new ArgumentNullException(nameof(sbomContextRetriever));
|
_sbomContextRetriever = sbomContextRetriever ?? throw new ArgumentNullException(nameof(sbomContextRetriever));
|
||||||
_toolset = toolset ?? throw new ArgumentNullException(nameof(toolset));
|
_toolset = toolset ?? throw new ArgumentNullException(nameof(toolset));
|
||||||
_options = options?.Value ?? throw new ArgumentNullException(nameof(options));
|
_options = options?.Value ?? throw new ArgumentNullException(nameof(options));
|
||||||
_options.ApplyDefaults();
|
_options.ApplyDefaults();
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<AdvisoryTaskPlan> CreatePlanAsync(AdvisoryTaskRequest request, CancellationToken cancellationToken)
|
public async Task<AdvisoryTaskPlan> CreatePlanAsync(AdvisoryTaskRequest request, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(request);
|
ArgumentNullException.ThrowIfNull(request);
|
||||||
|
|
||||||
var config = _options.GetConfiguration(request.TaskType);
|
var config = _options.GetConfiguration(request.TaskType);
|
||||||
|
|
||||||
var structuredRequest = new AdvisoryRetrievalRequest(
|
var structuredRequest = new AdvisoryRetrievalRequest(
|
||||||
request.AdvisoryKey,
|
request.AdvisoryKey,
|
||||||
request.PreferredSections,
|
request.PreferredSections,
|
||||||
config.StructuredMaxChunks);
|
config.StructuredMaxChunks);
|
||||||
|
|
||||||
var structured = await _structuredRetriever
|
var structured = await _structuredRetriever
|
||||||
.RetrieveAsync(structuredRequest, cancellationToken)
|
.RetrieveAsync(structuredRequest, cancellationToken)
|
||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
var vectorResults = await RetrieveVectorMatchesAsync(request, structuredRequest, config, cancellationToken).ConfigureAwait(false);
|
var vectorResults = await RetrieveVectorMatchesAsync(request, structuredRequest, config, cancellationToken).ConfigureAwait(false);
|
||||||
var (sbomContext, dependencyAnalysis) = await RetrieveSbomContextAsync(request, config, cancellationToken).ConfigureAwait(false);
|
var (sbomContext, dependencyAnalysis) = await RetrieveSbomContextAsync(request, config, cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
var metadata = BuildMetadata(request, structured, vectorResults, sbomContext, dependencyAnalysis);
|
var metadata = BuildMetadata(request, structured, vectorResults, sbomContext, dependencyAnalysis);
|
||||||
var cacheKey = ComputeCacheKey(request, structured, vectorResults, sbomContext, dependencyAnalysis);
|
var cacheKey = ComputeCacheKey(request, structured, vectorResults, sbomContext, dependencyAnalysis);
|
||||||
|
|
||||||
var plan = new AdvisoryTaskPlan(
|
var plan = new AdvisoryTaskPlan(
|
||||||
request,
|
request,
|
||||||
cacheKey,
|
cacheKey,
|
||||||
config.PromptTemplate,
|
config.PromptTemplate,
|
||||||
structured.Chunks.ToImmutableArray(),
|
structured.Chunks.ToImmutableArray(),
|
||||||
vectorResults,
|
vectorResults,
|
||||||
sbomContext,
|
sbomContext,
|
||||||
dependencyAnalysis,
|
dependencyAnalysis,
|
||||||
config.Budget,
|
config.Budget,
|
||||||
metadata);
|
metadata);
|
||||||
|
|
||||||
return plan;
|
return plan;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<ImmutableArray<AdvisoryVectorResult>> RetrieveVectorMatchesAsync(
|
private async Task<ImmutableArray<AdvisoryVectorResult>> RetrieveVectorMatchesAsync(
|
||||||
AdvisoryTaskRequest request,
|
AdvisoryTaskRequest request,
|
||||||
AdvisoryRetrievalRequest structuredRequest,
|
AdvisoryRetrievalRequest structuredRequest,
|
||||||
AdvisoryTaskConfiguration configuration,
|
AdvisoryTaskConfiguration configuration,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
if (configuration.VectorQueries.Count == 0)
|
if (configuration.VectorQueries.Count == 0)
|
||||||
{
|
{
|
||||||
return ImmutableArray<AdvisoryVectorResult>.Empty;
|
return ImmutableArray<AdvisoryVectorResult>.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
var builder = ImmutableArray.CreateBuilder<AdvisoryVectorResult>(configuration.VectorQueries.Count);
|
var builder = ImmutableArray.CreateBuilder<AdvisoryVectorResult>(configuration.VectorQueries.Count);
|
||||||
foreach (var query in configuration.GetVectorQueries())
|
foreach (var query in configuration.GetVectorQueries())
|
||||||
{
|
{
|
||||||
var vectorRequest = new VectorRetrievalRequest(structuredRequest, query, configuration.VectorTopK);
|
var vectorRequest = new VectorRetrievalRequest(structuredRequest, query, configuration.VectorTopK);
|
||||||
var matches = await _vectorRetriever
|
var matches = await _vectorRetriever
|
||||||
.SearchAsync(vectorRequest, cancellationToken)
|
.SearchAsync(vectorRequest, cancellationToken)
|
||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
builder.Add(new AdvisoryVectorResult(query, matches.ToImmutableArray()));
|
builder.Add(new AdvisoryVectorResult(query, matches.ToImmutableArray()));
|
||||||
}
|
}
|
||||||
|
|
||||||
return builder.MoveToImmutable();
|
return builder.MoveToImmutable();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<(SbomContextResult? Context, DependencyAnalysisResult? Analysis)> RetrieveSbomContextAsync(
|
private async Task<(SbomContextResult? Context, DependencyAnalysisResult? Analysis)> RetrieveSbomContextAsync(
|
||||||
AdvisoryTaskRequest request,
|
AdvisoryTaskRequest request,
|
||||||
AdvisoryTaskConfiguration configuration,
|
AdvisoryTaskConfiguration configuration,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(request.ArtifactId))
|
if (string.IsNullOrEmpty(request.ArtifactId))
|
||||||
{
|
{
|
||||||
return (null, null);
|
return (null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
var sbomRequest = new SbomContextRequest(
|
var sbomRequest = new SbomContextRequest(
|
||||||
artifactId: request.ArtifactId!,
|
artifactId: request.ArtifactId!,
|
||||||
purl: request.ArtifactPurl,
|
purl: request.ArtifactPurl,
|
||||||
maxTimelineEntries: configuration.SbomMaxTimelineEntries,
|
maxTimelineEntries: configuration.SbomMaxTimelineEntries,
|
||||||
maxDependencyPaths: configuration.SbomMaxDependencyPaths,
|
maxDependencyPaths: configuration.SbomMaxDependencyPaths,
|
||||||
includeEnvironmentFlags: configuration.IncludeEnvironmentFlags,
|
includeEnvironmentFlags: configuration.IncludeEnvironmentFlags,
|
||||||
includeBlastRadius: configuration.IncludeBlastRadius);
|
includeBlastRadius: configuration.IncludeBlastRadius);
|
||||||
|
|
||||||
var context = await _sbomContextRetriever
|
var context = await _sbomContextRetriever
|
||||||
.RetrieveAsync(sbomRequest, cancellationToken)
|
.RetrieveAsync(sbomRequest, cancellationToken)
|
||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
@@ -128,73 +128,73 @@ internal sealed class AdvisoryPipelineOrchestrator : IAdvisoryPipelineOrchestrat
|
|||||||
private static ImmutableDictionary<string, string> BuildMetadata(
|
private static ImmutableDictionary<string, string> BuildMetadata(
|
||||||
AdvisoryTaskRequest request,
|
AdvisoryTaskRequest request,
|
||||||
AdvisoryRetrievalResult structured,
|
AdvisoryRetrievalResult structured,
|
||||||
ImmutableArray<AdvisoryVectorResult> vectors,
|
ImmutableArray<AdvisoryVectorResult> vectors,
|
||||||
SbomContextResult? sbom,
|
SbomContextResult? sbom,
|
||||||
DependencyAnalysisResult? dependency)
|
DependencyAnalysisResult? dependency)
|
||||||
{
|
{
|
||||||
var builder = ImmutableDictionary.CreateBuilder<string, string>(StringComparer.Ordinal);
|
var builder = ImmutableDictionary.CreateBuilder<string, string>(StringComparer.Ordinal);
|
||||||
builder["task_type"] = request.TaskType.ToString();
|
builder["task_type"] = request.TaskType.ToString();
|
||||||
builder["advisory_key"] = request.AdvisoryKey;
|
builder["advisory_key"] = request.AdvisoryKey;
|
||||||
builder["profile"] = request.Profile;
|
builder["profile"] = request.Profile;
|
||||||
builder["structured_chunk_count"] = structured.Chunks.Count().ToString(CultureInfo.InvariantCulture);
|
builder["structured_chunk_count"] = structured.Chunks.Count().ToString(CultureInfo.InvariantCulture);
|
||||||
builder["vector_query_count"] = vectors.Length.ToString(CultureInfo.InvariantCulture);
|
builder["vector_query_count"] = vectors.Length.ToString(CultureInfo.InvariantCulture);
|
||||||
builder["vector_match_count"] = vectors.Sum(result => result.Matches.Length).ToString(CultureInfo.InvariantCulture);
|
builder["vector_match_count"] = vectors.Sum(result => result.Matches.Length).ToString(CultureInfo.InvariantCulture);
|
||||||
builder["includes_sbom"] = (sbom is not null).ToString();
|
builder["includes_sbom"] = (sbom is not null).ToString();
|
||||||
builder["dependency_node_count"] = (dependency?.Nodes.Length ?? 0).ToString(CultureInfo.InvariantCulture);
|
builder["dependency_node_count"] = (dependency?.Nodes.Length ?? 0).ToString(CultureInfo.InvariantCulture);
|
||||||
builder["force_refresh"] = request.ForceRefresh.ToString();
|
builder["force_refresh"] = request.ForceRefresh.ToString();
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(request.PolicyVersion))
|
if (!string.IsNullOrEmpty(request.PolicyVersion))
|
||||||
{
|
{
|
||||||
builder["policy_version"] = request.PolicyVersion!;
|
builder["policy_version"] = request.PolicyVersion!;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sbom is not null)
|
if (sbom is not null)
|
||||||
{
|
{
|
||||||
builder["sbom_version_count"] = sbom.VersionTimeline.Length.ToString(CultureInfo.InvariantCulture);
|
builder["sbom_version_count"] = sbom.VersionTimeline.Length.ToString(CultureInfo.InvariantCulture);
|
||||||
builder["sbom_dependency_path_count"] = sbom.DependencyPaths.Length.ToString(CultureInfo.InvariantCulture);
|
builder["sbom_dependency_path_count"] = sbom.DependencyPaths.Length.ToString(CultureInfo.InvariantCulture);
|
||||||
|
|
||||||
if (!sbom.EnvironmentFlags.IsEmpty)
|
if (!sbom.EnvironmentFlags.IsEmpty)
|
||||||
{
|
{
|
||||||
foreach (var flag in sbom.EnvironmentFlags.OrderBy(pair => pair.Key, StringComparer.Ordinal))
|
foreach (var flag in sbom.EnvironmentFlags.OrderBy(pair => pair.Key, StringComparer.Ordinal))
|
||||||
{
|
{
|
||||||
builder[$"sbom_env_{flag.Key}"] = flag.Value;
|
builder[$"sbom_env_{flag.Key}"] = flag.Value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sbom.BlastRadius is not null)
|
if (sbom.BlastRadius is not null)
|
||||||
{
|
{
|
||||||
builder["sbom_blast_impacted_assets"] = sbom.BlastRadius.ImpactedAssets.ToString(CultureInfo.InvariantCulture);
|
builder["sbom_blast_impacted_assets"] = sbom.BlastRadius.ImpactedAssets.ToString(CultureInfo.InvariantCulture);
|
||||||
builder["sbom_blast_impacted_workloads"] = sbom.BlastRadius.ImpactedWorkloads.ToString(CultureInfo.InvariantCulture);
|
builder["sbom_blast_impacted_workloads"] = sbom.BlastRadius.ImpactedWorkloads.ToString(CultureInfo.InvariantCulture);
|
||||||
builder["sbom_blast_impacted_namespaces"] = sbom.BlastRadius.ImpactedNamespaces.ToString(CultureInfo.InvariantCulture);
|
builder["sbom_blast_impacted_namespaces"] = sbom.BlastRadius.ImpactedNamespaces.ToString(CultureInfo.InvariantCulture);
|
||||||
if (sbom.BlastRadius.ImpactedPercentage is not null)
|
if (sbom.BlastRadius.ImpactedPercentage is not null)
|
||||||
{
|
{
|
||||||
builder["sbom_blast_impacted_percentage"] = sbom.BlastRadius.ImpactedPercentage.Value.ToString("G", CultureInfo.InvariantCulture);
|
builder["sbom_blast_impacted_percentage"] = sbom.BlastRadius.ImpactedPercentage.Value.ToString("G", CultureInfo.InvariantCulture);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!sbom.BlastRadius.Metadata.IsEmpty)
|
if (!sbom.BlastRadius.Metadata.IsEmpty)
|
||||||
{
|
{
|
||||||
foreach (var kvp in sbom.BlastRadius.Metadata.OrderBy(pair => pair.Key, StringComparer.Ordinal))
|
foreach (var kvp in sbom.BlastRadius.Metadata.OrderBy(pair => pair.Key, StringComparer.Ordinal))
|
||||||
{
|
{
|
||||||
builder[$"sbom_blast_meta_{kvp.Key}"] = kvp.Value;
|
builder[$"sbom_blast_meta_{kvp.Key}"] = kvp.Value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!sbom.Metadata.IsEmpty)
|
if (!sbom.Metadata.IsEmpty)
|
||||||
{
|
{
|
||||||
foreach (var kvp in sbom.Metadata.OrderBy(pair => pair.Key, StringComparer.Ordinal))
|
foreach (var kvp in sbom.Metadata.OrderBy(pair => pair.Key, StringComparer.Ordinal))
|
||||||
{
|
{
|
||||||
builder[$"sbom_meta_{kvp.Key}"] = kvp.Value;
|
builder[$"sbom_meta_{kvp.Key}"] = kvp.Value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (dependency is not null)
|
if (dependency is not null)
|
||||||
{
|
{
|
||||||
foreach (var kvp in dependency.Metadata.OrderBy(pair => pair.Key, StringComparer.Ordinal))
|
foreach (var kvp in dependency.Metadata.OrderBy(pair => pair.Key, StringComparer.Ordinal))
|
||||||
{
|
{
|
||||||
builder[$"dependency_{kvp.Key}"] = kvp.Value;
|
builder[$"dependency_{kvp.Key}"] = kvp.Value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return builder.ToImmutable();
|
return builder.ToImmutable();
|
||||||
@@ -228,178 +228,178 @@ internal sealed class AdvisoryPipelineOrchestrator : IAdvisoryPipelineOrchestrat
|
|||||||
context.Metadata);
|
context.Metadata);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string ComputeCacheKey(
|
private static string ComputeCacheKey(
|
||||||
AdvisoryTaskRequest request,
|
AdvisoryTaskRequest request,
|
||||||
AdvisoryRetrievalResult structured,
|
AdvisoryRetrievalResult structured,
|
||||||
ImmutableArray<AdvisoryVectorResult> vectors,
|
ImmutableArray<AdvisoryVectorResult> vectors,
|
||||||
SbomContextResult? sbom,
|
SbomContextResult? sbom,
|
||||||
DependencyAnalysisResult? dependency)
|
DependencyAnalysisResult? dependency)
|
||||||
{
|
{
|
||||||
var builder = new StringBuilder();
|
var builder = new StringBuilder();
|
||||||
builder.Append(request.TaskType)
|
builder.Append(request.TaskType)
|
||||||
.Append('|').Append(request.AdvisoryKey)
|
.Append('|').Append(request.AdvisoryKey)
|
||||||
.Append('|').Append(request.ArtifactId ?? string.Empty)
|
.Append('|').Append(request.ArtifactId ?? string.Empty)
|
||||||
.Append('|').Append(request.PolicyVersion ?? string.Empty)
|
.Append('|').Append(request.PolicyVersion ?? string.Empty)
|
||||||
.Append('|').Append(request.Profile);
|
.Append('|').Append(request.Profile);
|
||||||
|
|
||||||
if (request.PreferredSections is not null)
|
if (request.PreferredSections is not null)
|
||||||
{
|
{
|
||||||
foreach (var section in request.PreferredSections.OrderBy(s => s, StringComparer.OrdinalIgnoreCase))
|
foreach (var section in request.PreferredSections.OrderBy(s => s, StringComparer.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
builder.Append('|').Append(section);
|
builder.Append('|').Append(section);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var chunkId in structured.Chunks
|
foreach (var chunkId in structured.Chunks
|
||||||
.Select(chunk => chunk.ChunkId)
|
.Select(chunk => chunk.ChunkId)
|
||||||
.OrderBy(id => id, StringComparer.Ordinal))
|
.OrderBy(id => id, StringComparer.Ordinal))
|
||||||
{
|
{
|
||||||
builder.Append("|chunk:").Append(chunkId);
|
builder.Append("|chunk:").Append(chunkId);
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var vector in vectors)
|
foreach (var vector in vectors)
|
||||||
{
|
{
|
||||||
builder.Append("|query:").Append(vector.Query);
|
builder.Append("|query:").Append(vector.Query);
|
||||||
foreach (var match in vector.Matches
|
foreach (var match in vector.Matches
|
||||||
.OrderBy(m => m.ChunkId, StringComparer.Ordinal)
|
.OrderBy(m => m.ChunkId, StringComparer.Ordinal)
|
||||||
.ThenBy(m => m.Score))
|
.ThenBy(m => m.Score))
|
||||||
{
|
{
|
||||||
builder.Append("|match:")
|
builder.Append("|match:")
|
||||||
.Append(match.ChunkId)
|
.Append(match.ChunkId)
|
||||||
.Append('@')
|
.Append('@')
|
||||||
.Append(match.Score.ToString("G", CultureInfo.InvariantCulture));
|
.Append(match.Score.ToString("G", CultureInfo.InvariantCulture));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sbom is not null)
|
if (sbom is not null)
|
||||||
{
|
{
|
||||||
builder.Append("|sbom:timeline=").Append(sbom.VersionTimeline.Length);
|
builder.Append("|sbom:timeline=").Append(sbom.VersionTimeline.Length);
|
||||||
builder.Append("|sbom:paths=").Append(sbom.DependencyPaths.Length);
|
builder.Append("|sbom:paths=").Append(sbom.DependencyPaths.Length);
|
||||||
foreach (var entry in sbom.VersionTimeline
|
foreach (var entry in sbom.VersionTimeline
|
||||||
.OrderBy(e => e.Version, StringComparer.Ordinal)
|
.OrderBy(e => e.Version, StringComparer.Ordinal)
|
||||||
.ThenBy(e => e.FirstObserved.ToUnixTimeMilliseconds())
|
.ThenBy(e => e.FirstObserved.ToUnixTimeMilliseconds())
|
||||||
.ThenBy(e => e.LastObserved?.ToUnixTimeMilliseconds() ?? long.MinValue)
|
.ThenBy(e => e.LastObserved?.ToUnixTimeMilliseconds() ?? long.MinValue)
|
||||||
.ThenBy(e => e.Status, StringComparer.Ordinal)
|
.ThenBy(e => e.Status, StringComparer.Ordinal)
|
||||||
.ThenBy(e => e.Source, StringComparer.Ordinal))
|
.ThenBy(e => e.Source, StringComparer.Ordinal))
|
||||||
{
|
{
|
||||||
builder.Append("|timeline:")
|
builder.Append("|timeline:")
|
||||||
.Append(entry.Version)
|
.Append(entry.Version)
|
||||||
.Append('@')
|
.Append('@')
|
||||||
.Append(entry.FirstObserved.ToUnixTimeMilliseconds())
|
.Append(entry.FirstObserved.ToUnixTimeMilliseconds())
|
||||||
.Append('@')
|
.Append('@')
|
||||||
.Append(entry.LastObserved?.ToUnixTimeMilliseconds() ?? -1)
|
.Append(entry.LastObserved?.ToUnixTimeMilliseconds() ?? -1)
|
||||||
.Append('@')
|
.Append('@')
|
||||||
.Append(entry.Status)
|
.Append(entry.Status)
|
||||||
.Append('@')
|
.Append('@')
|
||||||
.Append(entry.Source);
|
.Append(entry.Source);
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var path in sbom.DependencyPaths
|
foreach (var path in sbom.DependencyPaths
|
||||||
.OrderBy(path => path.IsRuntime)
|
.OrderBy(path => path.IsRuntime)
|
||||||
.ThenBy(path => string.Join(">", path.Nodes.Select(node => node.Identifier)), StringComparer.Ordinal))
|
.ThenBy(path => string.Join(">", path.Nodes.Select(node => node.Identifier)), StringComparer.Ordinal))
|
||||||
{
|
{
|
||||||
builder.Append("|path:")
|
builder.Append("|path:")
|
||||||
.Append(path.IsRuntime ? 'R' : 'D');
|
.Append(path.IsRuntime ? 'R' : 'D');
|
||||||
|
|
||||||
foreach (var node in path.Nodes)
|
foreach (var node in path.Nodes)
|
||||||
{
|
{
|
||||||
builder.Append(":")
|
builder.Append(":")
|
||||||
.Append(node.Identifier)
|
.Append(node.Identifier)
|
||||||
.Append('@')
|
.Append('@')
|
||||||
.Append(node.Version ?? string.Empty);
|
.Append(node.Version ?? string.Empty);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(path.Source))
|
if (!string.IsNullOrWhiteSpace(path.Source))
|
||||||
{
|
{
|
||||||
builder.Append("|pathsrc:").Append(path.Source);
|
builder.Append("|pathsrc:").Append(path.Source);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!path.Metadata.IsEmpty)
|
if (!path.Metadata.IsEmpty)
|
||||||
{
|
{
|
||||||
foreach (var kvp in path.Metadata.OrderBy(pair => pair.Key, StringComparer.Ordinal))
|
foreach (var kvp in path.Metadata.OrderBy(pair => pair.Key, StringComparer.Ordinal))
|
||||||
{
|
{
|
||||||
builder.Append("|pathmeta:")
|
builder.Append("|pathmeta:")
|
||||||
.Append(kvp.Key)
|
.Append(kvp.Key)
|
||||||
.Append('=')
|
.Append('=')
|
||||||
.Append(kvp.Value);
|
.Append(kvp.Value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!sbom.EnvironmentFlags.IsEmpty)
|
if (!sbom.EnvironmentFlags.IsEmpty)
|
||||||
{
|
{
|
||||||
foreach (var flag in sbom.EnvironmentFlags.OrderBy(pair => pair.Key, StringComparer.Ordinal))
|
foreach (var flag in sbom.EnvironmentFlags.OrderBy(pair => pair.Key, StringComparer.Ordinal))
|
||||||
{
|
{
|
||||||
builder.Append("|env:")
|
builder.Append("|env:")
|
||||||
.Append(flag.Key)
|
.Append(flag.Key)
|
||||||
.Append('=')
|
.Append('=')
|
||||||
.Append(flag.Value);
|
.Append(flag.Value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sbom.BlastRadius is not null)
|
if (sbom.BlastRadius is not null)
|
||||||
{
|
{
|
||||||
builder.Append("|blast:")
|
builder.Append("|blast:")
|
||||||
.Append(sbom.BlastRadius.ImpactedAssets)
|
.Append(sbom.BlastRadius.ImpactedAssets)
|
||||||
.Append(',')
|
.Append(',')
|
||||||
.Append(sbom.BlastRadius.ImpactedWorkloads)
|
.Append(sbom.BlastRadius.ImpactedWorkloads)
|
||||||
.Append(',')
|
.Append(',')
|
||||||
.Append(sbom.BlastRadius.ImpactedNamespaces)
|
.Append(sbom.BlastRadius.ImpactedNamespaces)
|
||||||
.Append(',')
|
.Append(',')
|
||||||
.Append(sbom.BlastRadius.ImpactedPercentage?.ToString("G", CultureInfo.InvariantCulture) ?? string.Empty);
|
.Append(sbom.BlastRadius.ImpactedPercentage?.ToString("G", CultureInfo.InvariantCulture) ?? string.Empty);
|
||||||
|
|
||||||
if (!sbom.BlastRadius.Metadata.IsEmpty)
|
if (!sbom.BlastRadius.Metadata.IsEmpty)
|
||||||
{
|
{
|
||||||
foreach (var kvp in sbom.BlastRadius.Metadata.OrderBy(pair => pair.Key, StringComparer.Ordinal))
|
foreach (var kvp in sbom.BlastRadius.Metadata.OrderBy(pair => pair.Key, StringComparer.Ordinal))
|
||||||
{
|
{
|
||||||
builder.Append("|blastmeta:")
|
builder.Append("|blastmeta:")
|
||||||
.Append(kvp.Key)
|
.Append(kvp.Key)
|
||||||
.Append('=')
|
.Append('=')
|
||||||
.Append(kvp.Value);
|
.Append(kvp.Value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!sbom.Metadata.IsEmpty)
|
if (!sbom.Metadata.IsEmpty)
|
||||||
{
|
{
|
||||||
foreach (var kvp in sbom.Metadata.OrderBy(pair => pair.Key, StringComparer.Ordinal))
|
foreach (var kvp in sbom.Metadata.OrderBy(pair => pair.Key, StringComparer.Ordinal))
|
||||||
{
|
{
|
||||||
builder.Append("|sbommeta:")
|
builder.Append("|sbommeta:")
|
||||||
.Append(kvp.Key)
|
.Append(kvp.Key)
|
||||||
.Append('=')
|
.Append('=')
|
||||||
.Append(kvp.Value);
|
.Append(kvp.Value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (dependency is not null)
|
if (dependency is not null)
|
||||||
{
|
{
|
||||||
foreach (var node in dependency.Nodes
|
foreach (var node in dependency.Nodes
|
||||||
.OrderBy(n => n.Identifier, StringComparer.Ordinal))
|
.OrderBy(n => n.Identifier, StringComparer.Ordinal))
|
||||||
{
|
{
|
||||||
builder.Append("|dep:")
|
builder.Append("|dep:")
|
||||||
.Append(node.Identifier)
|
.Append(node.Identifier)
|
||||||
.Append(':')
|
.Append(':')
|
||||||
.Append(node.RuntimeOccurrences)
|
.Append(node.RuntimeOccurrences)
|
||||||
.Append(':')
|
.Append(':')
|
||||||
.Append(node.DevelopmentOccurrences)
|
.Append(node.DevelopmentOccurrences)
|
||||||
.Append(':')
|
.Append(':')
|
||||||
.Append(string.Join(',', node.Versions));
|
.Append(string.Join(',', node.Versions));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!dependency.Metadata.IsEmpty)
|
if (!dependency.Metadata.IsEmpty)
|
||||||
{
|
{
|
||||||
foreach (var kvp in dependency.Metadata.OrderBy(pair => pair.Key, StringComparer.Ordinal))
|
foreach (var kvp in dependency.Metadata.OrderBy(pair => pair.Key, StringComparer.Ordinal))
|
||||||
{
|
{
|
||||||
builder.Append("|depmeta:")
|
builder.Append("|depmeta:")
|
||||||
.Append(kvp.Key)
|
.Append(kvp.Key)
|
||||||
.Append('=')
|
.Append('=')
|
||||||
.Append(kvp.Value);
|
.Append(kvp.Value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var hash = SHA256.HashData(Encoding.UTF8.GetBytes(builder.ToString()));
|
var hash = SHA256.HashData(Encoding.UTF8.GetBytes(builder.ToString()));
|
||||||
return Convert.ToHexString(hash);
|
return Convert.ToHexString(hash);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,70 +1,70 @@
|
|||||||
using System.Collections.Immutable;
|
using System.Collections.Immutable;
|
||||||
using StellaOps.AdvisoryAI.Abstractions;
|
using StellaOps.AdvisoryAI.Abstractions;
|
||||||
using StellaOps.AdvisoryAI.Context;
|
using StellaOps.AdvisoryAI.Context;
|
||||||
using StellaOps.AdvisoryAI.Documents;
|
using StellaOps.AdvisoryAI.Documents;
|
||||||
using StellaOps.AdvisoryAI.Tools;
|
using StellaOps.AdvisoryAI.Tools;
|
||||||
|
|
||||||
namespace StellaOps.AdvisoryAI.Orchestration;
|
namespace StellaOps.AdvisoryAI.Orchestration;
|
||||||
|
|
||||||
public sealed class AdvisoryTaskPlan
|
public sealed class AdvisoryTaskPlan
|
||||||
{
|
{
|
||||||
public AdvisoryTaskPlan(
|
public AdvisoryTaskPlan(
|
||||||
AdvisoryTaskRequest request,
|
AdvisoryTaskRequest request,
|
||||||
string cacheKey,
|
string cacheKey,
|
||||||
string promptTemplate,
|
string promptTemplate,
|
||||||
ImmutableArray<AdvisoryChunk> structuredChunks,
|
ImmutableArray<AdvisoryChunk> structuredChunks,
|
||||||
ImmutableArray<AdvisoryVectorResult> vectorResults,
|
ImmutableArray<AdvisoryVectorResult> vectorResults,
|
||||||
SbomContextResult? sbomContext,
|
SbomContextResult? sbomContext,
|
||||||
DependencyAnalysisResult? dependencyAnalysis,
|
DependencyAnalysisResult? dependencyAnalysis,
|
||||||
AdvisoryTaskBudget budget,
|
AdvisoryTaskBudget budget,
|
||||||
ImmutableDictionary<string, string> metadata)
|
ImmutableDictionary<string, string> metadata)
|
||||||
{
|
{
|
||||||
Request = request ?? throw new ArgumentNullException(nameof(request));
|
Request = request ?? throw new ArgumentNullException(nameof(request));
|
||||||
CacheKey = cacheKey ?? throw new ArgumentNullException(nameof(cacheKey));
|
CacheKey = cacheKey ?? throw new ArgumentNullException(nameof(cacheKey));
|
||||||
PromptTemplate = promptTemplate ?? throw new ArgumentNullException(nameof(promptTemplate));
|
PromptTemplate = promptTemplate ?? throw new ArgumentNullException(nameof(promptTemplate));
|
||||||
StructuredChunks = structuredChunks;
|
StructuredChunks = structuredChunks;
|
||||||
VectorResults = vectorResults;
|
VectorResults = vectorResults;
|
||||||
SbomContext = sbomContext;
|
SbomContext = sbomContext;
|
||||||
DependencyAnalysis = dependencyAnalysis;
|
DependencyAnalysis = dependencyAnalysis;
|
||||||
Budget = budget ?? throw new ArgumentNullException(nameof(budget));
|
Budget = budget ?? throw new ArgumentNullException(nameof(budget));
|
||||||
Metadata = metadata ?? throw new ArgumentNullException(nameof(metadata));
|
Metadata = metadata ?? throw new ArgumentNullException(nameof(metadata));
|
||||||
}
|
}
|
||||||
|
|
||||||
public AdvisoryTaskRequest Request { get; }
|
public AdvisoryTaskRequest Request { get; }
|
||||||
|
|
||||||
public string CacheKey { get; }
|
public string CacheKey { get; }
|
||||||
|
|
||||||
public string PromptTemplate { get; }
|
public string PromptTemplate { get; }
|
||||||
|
|
||||||
public ImmutableArray<AdvisoryChunk> StructuredChunks { get; }
|
public ImmutableArray<AdvisoryChunk> StructuredChunks { get; }
|
||||||
|
|
||||||
public ImmutableArray<AdvisoryVectorResult> VectorResults { get; }
|
public ImmutableArray<AdvisoryVectorResult> VectorResults { get; }
|
||||||
|
|
||||||
public SbomContextResult? SbomContext { get; }
|
public SbomContextResult? SbomContext { get; }
|
||||||
|
|
||||||
public DependencyAnalysisResult? DependencyAnalysis { get; }
|
public DependencyAnalysisResult? DependencyAnalysis { get; }
|
||||||
|
|
||||||
public AdvisoryTaskBudget Budget { get; }
|
public AdvisoryTaskBudget Budget { get; }
|
||||||
|
|
||||||
public ImmutableDictionary<string, string> Metadata { get; }
|
public ImmutableDictionary<string, string> Metadata { get; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed class AdvisoryVectorResult
|
public sealed class AdvisoryVectorResult
|
||||||
{
|
{
|
||||||
public AdvisoryVectorResult(string query, ImmutableArray<VectorRetrievalMatch> matches)
|
public AdvisoryVectorResult(string query, ImmutableArray<VectorRetrievalMatch> matches)
|
||||||
{
|
{
|
||||||
Query = string.IsNullOrWhiteSpace(query) ? throw new ArgumentException(nameof(query)) : query;
|
Query = string.IsNullOrWhiteSpace(query) ? throw new ArgumentException(nameof(query)) : query;
|
||||||
Matches = matches;
|
Matches = matches;
|
||||||
}
|
}
|
||||||
|
|
||||||
public string Query { get; }
|
public string Query { get; }
|
||||||
|
|
||||||
public ImmutableArray<VectorRetrievalMatch> Matches { get; }
|
public ImmutableArray<VectorRetrievalMatch> Matches { get; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed class AdvisoryTaskBudget
|
public sealed class AdvisoryTaskBudget
|
||||||
{
|
{
|
||||||
public int PromptTokens { get; init; } = 2048;
|
public int PromptTokens { get; init; } = 2048;
|
||||||
|
|
||||||
public int CompletionTokens { get; init; } = 512;
|
public int CompletionTokens { get; init; } = 512;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,30 +1,30 @@
|
|||||||
using System;
|
using System;
|
||||||
|
|
||||||
namespace StellaOps.AdvisoryAI.Providers;
|
namespace StellaOps.AdvisoryAI.Providers;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Configuration for the SBOM context HTTP client.
|
/// Configuration for the SBOM context HTTP client.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class SbomContextClientOptions
|
public sealed class SbomContextClientOptions
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Base address for the SBOM service. Required.
|
/// Base address for the SBOM service. Required.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public Uri? BaseAddress { get; set; }
|
public Uri? BaseAddress { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Relative endpoint that returns SBOM context payloads.
|
/// Relative endpoint that returns SBOM context payloads.
|
||||||
/// Defaults to <c>api/sbom/context</c>.
|
/// Defaults to <c>api/sbom/context</c>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string ContextEndpoint { get; set; } = "api/sbom/context";
|
public string ContextEndpoint { get; set; } = "api/sbom/context";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Optional tenant identifier that should be forwarded to the SBOM service.
|
/// Optional tenant identifier that should be forwarded to the SBOM service.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string? Tenant { get; set; }
|
public string? Tenant { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Header name used when forwarding the tenant. Defaults to <c>X-StellaOps-Tenant</c>.
|
/// Header name used when forwarding the tenant. Defaults to <c>X-StellaOps-Tenant</c>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string TenantHeaderName { get; set; } = "X-StellaOps-Tenant";
|
public string TenantHeaderName { get; set; } = "X-StellaOps-Tenant";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,234 +1,234 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Collections.Immutable;
|
using System.Collections.Immutable;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Net.Http.Json;
|
using System.Net.Http.Json;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
namespace StellaOps.AdvisoryAI.Providers;
|
namespace StellaOps.AdvisoryAI.Providers;
|
||||||
|
|
||||||
internal sealed class SbomContextHttpClient : ISbomContextClient
|
internal sealed class SbomContextHttpClient : ISbomContextClient
|
||||||
{
|
{
|
||||||
private static readonly JsonSerializerOptions SerializerOptions = new(JsonSerializerDefaults.Web)
|
private static readonly JsonSerializerOptions SerializerOptions = new(JsonSerializerDefaults.Web)
|
||||||
{
|
{
|
||||||
PropertyNameCaseInsensitive = true
|
PropertyNameCaseInsensitive = true
|
||||||
};
|
};
|
||||||
|
|
||||||
private readonly HttpClient httpClient;
|
private readonly HttpClient httpClient;
|
||||||
private readonly SbomContextClientOptions options;
|
private readonly SbomContextClientOptions options;
|
||||||
private readonly ILogger<SbomContextHttpClient>? logger;
|
private readonly ILogger<SbomContextHttpClient>? logger;
|
||||||
|
|
||||||
public SbomContextHttpClient(
|
public SbomContextHttpClient(
|
||||||
HttpClient httpClient,
|
HttpClient httpClient,
|
||||||
IOptions<SbomContextClientOptions> options,
|
IOptions<SbomContextClientOptions> options,
|
||||||
ILogger<SbomContextHttpClient>? logger = null)
|
ILogger<SbomContextHttpClient>? logger = null)
|
||||||
{
|
{
|
||||||
this.httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
|
this.httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
|
||||||
if (options is null)
|
if (options is null)
|
||||||
{
|
{
|
||||||
throw new ArgumentNullException(nameof(options));
|
throw new ArgumentNullException(nameof(options));
|
||||||
}
|
}
|
||||||
|
|
||||||
this.options = options.Value ?? throw new ArgumentNullException(nameof(options));
|
this.options = options.Value ?? throw new ArgumentNullException(nameof(options));
|
||||||
|
|
||||||
if (this.options.BaseAddress is not null && this.httpClient.BaseAddress is null)
|
if (this.options.BaseAddress is not null && this.httpClient.BaseAddress is null)
|
||||||
{
|
{
|
||||||
this.httpClient.BaseAddress = this.options.BaseAddress;
|
this.httpClient.BaseAddress = this.options.BaseAddress;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.httpClient.BaseAddress is null)
|
if (this.httpClient.BaseAddress is null)
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException("SBOM context client requires a BaseAddress to be configured.");
|
throw new InvalidOperationException("SBOM context client requires a BaseAddress to be configured.");
|
||||||
}
|
}
|
||||||
|
|
||||||
this.httpClient.DefaultRequestHeaders.Accept.ParseAdd("application/json");
|
this.httpClient.DefaultRequestHeaders.Accept.ParseAdd("application/json");
|
||||||
this.logger = logger;
|
this.logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<SbomContextDocument?> GetContextAsync(SbomContextQuery query, CancellationToken cancellationToken)
|
public async Task<SbomContextDocument?> GetContextAsync(SbomContextQuery query, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
if (query is null)
|
if (query is null)
|
||||||
{
|
{
|
||||||
throw new ArgumentNullException(nameof(query));
|
throw new ArgumentNullException(nameof(query));
|
||||||
}
|
}
|
||||||
|
|
||||||
var endpoint = options.ContextEndpoint?.Trim() ?? string.Empty;
|
var endpoint = options.ContextEndpoint?.Trim() ?? string.Empty;
|
||||||
if (endpoint.Length == 0)
|
if (endpoint.Length == 0)
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException("SBOM context endpoint must be configured.");
|
throw new InvalidOperationException("SBOM context endpoint must be configured.");
|
||||||
}
|
}
|
||||||
|
|
||||||
var requestUri = BuildRequestUri(endpoint, query);
|
var requestUri = BuildRequestUri(endpoint, query);
|
||||||
using var request = new HttpRequestMessage(HttpMethod.Get, requestUri);
|
using var request = new HttpRequestMessage(HttpMethod.Get, requestUri);
|
||||||
ApplyTenantHeader(request);
|
ApplyTenantHeader(request);
|
||||||
|
|
||||||
using var response = await httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false);
|
using var response = await httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false);
|
||||||
if (response.StatusCode == HttpStatusCode.NotFound || response.StatusCode == HttpStatusCode.NoContent)
|
if (response.StatusCode == HttpStatusCode.NotFound || response.StatusCode == HttpStatusCode.NoContent)
|
||||||
{
|
{
|
||||||
logger?.LogDebug("Received {StatusCode} for SBOM context request {Uri}; returning null.", (int)response.StatusCode, requestUri);
|
logger?.LogDebug("Received {StatusCode} for SBOM context request {Uri}; returning null.", (int)response.StatusCode, requestUri);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!response.IsSuccessStatusCode)
|
if (!response.IsSuccessStatusCode)
|
||||||
{
|
{
|
||||||
var content = response.Content is null
|
var content = response.Content is null
|
||||||
? string.Empty
|
? string.Empty
|
||||||
: await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
|
: await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
logger?.LogWarning(
|
logger?.LogWarning(
|
||||||
"SBOM context request {Uri} failed with status {StatusCode}. Payload: {Payload}",
|
"SBOM context request {Uri} failed with status {StatusCode}. Payload: {Payload}",
|
||||||
requestUri,
|
requestUri,
|
||||||
(int)response.StatusCode,
|
(int)response.StatusCode,
|
||||||
content);
|
content);
|
||||||
|
|
||||||
response.EnsureSuccessStatusCode();
|
response.EnsureSuccessStatusCode();
|
||||||
}
|
}
|
||||||
|
|
||||||
var httpContent = response.Content ?? throw new InvalidOperationException("SBOM context response did not include content.");
|
var httpContent = response.Content ?? throw new InvalidOperationException("SBOM context response did not include content.");
|
||||||
var payload = await httpContent.ReadFromJsonAsync<SbomContextPayload>(SerializerOptions, cancellationToken: cancellationToken)
|
var payload = await httpContent.ReadFromJsonAsync<SbomContextPayload>(SerializerOptions, cancellationToken: cancellationToken)
|
||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
if (payload is null)
|
if (payload is null)
|
||||||
{
|
{
|
||||||
logger?.LogWarning("SBOM context response for {Uri} was empty.", requestUri);
|
logger?.LogWarning("SBOM context response for {Uri} was empty.", requestUri);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return payload.ToDocument();
|
return payload.ToDocument();
|
||||||
}
|
}
|
||||||
|
|
||||||
private Uri BuildRequestUri(string endpoint, SbomContextQuery query)
|
private Uri BuildRequestUri(string endpoint, SbomContextQuery query)
|
||||||
{
|
{
|
||||||
var relative = endpoint.StartsWith("/", StringComparison.Ordinal)
|
var relative = endpoint.StartsWith("/", StringComparison.Ordinal)
|
||||||
? endpoint[1..]
|
? endpoint[1..]
|
||||||
: endpoint;
|
: endpoint;
|
||||||
|
|
||||||
var queryBuilder = new StringBuilder();
|
var queryBuilder = new StringBuilder();
|
||||||
|
|
||||||
AppendQuery(queryBuilder, "artifactId", query.ArtifactId);
|
AppendQuery(queryBuilder, "artifactId", query.ArtifactId);
|
||||||
AppendQuery(queryBuilder, "maxTimelineEntries", query.MaxTimelineEntries.ToString(CultureInfo.InvariantCulture));
|
AppendQuery(queryBuilder, "maxTimelineEntries", query.MaxTimelineEntries.ToString(CultureInfo.InvariantCulture));
|
||||||
AppendQuery(queryBuilder, "maxDependencyPaths", query.MaxDependencyPaths.ToString(CultureInfo.InvariantCulture));
|
AppendQuery(queryBuilder, "maxDependencyPaths", query.MaxDependencyPaths.ToString(CultureInfo.InvariantCulture));
|
||||||
AppendQuery(queryBuilder, "includeEnvironmentFlags", query.IncludeEnvironmentFlags ? "true" : "false");
|
AppendQuery(queryBuilder, "includeEnvironmentFlags", query.IncludeEnvironmentFlags ? "true" : "false");
|
||||||
AppendQuery(queryBuilder, "includeBlastRadius", query.IncludeBlastRadius ? "true" : "false");
|
AppendQuery(queryBuilder, "includeBlastRadius", query.IncludeBlastRadius ? "true" : "false");
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(query.Purl))
|
if (!string.IsNullOrWhiteSpace(query.Purl))
|
||||||
{
|
{
|
||||||
AppendQuery(queryBuilder, "purl", query.Purl!);
|
AppendQuery(queryBuilder, "purl", query.Purl!);
|
||||||
}
|
}
|
||||||
|
|
||||||
var uriString = queryBuilder.Length > 0 ? $"{relative}?{queryBuilder}" : relative;
|
var uriString = queryBuilder.Length > 0 ? $"{relative}?{queryBuilder}" : relative;
|
||||||
return new Uri(httpClient.BaseAddress!, uriString);
|
return new Uri(httpClient.BaseAddress!, uriString);
|
||||||
|
|
||||||
static void AppendQuery(StringBuilder builder, string name, string value)
|
static void AppendQuery(StringBuilder builder, string name, string value)
|
||||||
{
|
{
|
||||||
if (builder.Length > 0)
|
if (builder.Length > 0)
|
||||||
{
|
{
|
||||||
builder.Append('&');
|
builder.Append('&');
|
||||||
}
|
}
|
||||||
|
|
||||||
builder.Append(Uri.EscapeDataString(name));
|
builder.Append(Uri.EscapeDataString(name));
|
||||||
builder.Append('=');
|
builder.Append('=');
|
||||||
builder.Append(Uri.EscapeDataString(value));
|
builder.Append(Uri.EscapeDataString(value));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ApplyTenantHeader(HttpRequestMessage request)
|
private void ApplyTenantHeader(HttpRequestMessage request)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(options.Tenant) || string.IsNullOrWhiteSpace(options.TenantHeaderName))
|
if (string.IsNullOrWhiteSpace(options.Tenant) || string.IsNullOrWhiteSpace(options.TenantHeaderName))
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!request.Headers.Contains(options.TenantHeaderName))
|
if (!request.Headers.Contains(options.TenantHeaderName))
|
||||||
{
|
{
|
||||||
request.Headers.Add(options.TenantHeaderName, options.Tenant);
|
request.Headers.Add(options.TenantHeaderName, options.Tenant);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private sealed record SbomContextPayload(
|
private sealed record SbomContextPayload(
|
||||||
[property: JsonPropertyName("artifactId")] string ArtifactId,
|
[property: JsonPropertyName("artifactId")] string ArtifactId,
|
||||||
[property: JsonPropertyName("purl")] string? Purl,
|
[property: JsonPropertyName("purl")] string? Purl,
|
||||||
[property: JsonPropertyName("versions")] ImmutableArray<SbomVersionPayload> Versions,
|
[property: JsonPropertyName("versions")] ImmutableArray<SbomVersionPayload> Versions,
|
||||||
[property: JsonPropertyName("dependencyPaths")] ImmutableArray<SbomDependencyPathPayload> DependencyPaths,
|
[property: JsonPropertyName("dependencyPaths")] ImmutableArray<SbomDependencyPathPayload> DependencyPaths,
|
||||||
[property: JsonPropertyName("environmentFlags")] ImmutableDictionary<string, string> EnvironmentFlags,
|
[property: JsonPropertyName("environmentFlags")] ImmutableDictionary<string, string> EnvironmentFlags,
|
||||||
[property: JsonPropertyName("blastRadius")] SbomBlastRadiusPayload? BlastRadius,
|
[property: JsonPropertyName("blastRadius")] SbomBlastRadiusPayload? BlastRadius,
|
||||||
[property: JsonPropertyName("metadata")] ImmutableDictionary<string, string> Metadata)
|
[property: JsonPropertyName("metadata")] ImmutableDictionary<string, string> Metadata)
|
||||||
{
|
{
|
||||||
public SbomContextDocument ToDocument()
|
public SbomContextDocument ToDocument()
|
||||||
=> new(
|
=> new(
|
||||||
ArtifactId,
|
ArtifactId,
|
||||||
Purl,
|
Purl,
|
||||||
Versions.IsDefault ? ImmutableArray<SbomVersionRecord>.Empty : Versions.Select(v => v.ToRecord()).ToImmutableArray(),
|
Versions.IsDefault ? ImmutableArray<SbomVersionRecord>.Empty : Versions.Select(v => v.ToRecord()).ToImmutableArray(),
|
||||||
DependencyPaths.IsDefault ? ImmutableArray<SbomDependencyPathRecord>.Empty : DependencyPaths.Select(p => p.ToRecord()).ToImmutableArray(),
|
DependencyPaths.IsDefault ? ImmutableArray<SbomDependencyPathRecord>.Empty : DependencyPaths.Select(p => p.ToRecord()).ToImmutableArray(),
|
||||||
EnvironmentFlags == default ? ImmutableDictionary<string, string>.Empty : EnvironmentFlags,
|
EnvironmentFlags == default ? ImmutableDictionary<string, string>.Empty : EnvironmentFlags,
|
||||||
BlastRadius?.ToRecord(),
|
BlastRadius?.ToRecord(),
|
||||||
Metadata == default ? ImmutableDictionary<string, string>.Empty : Metadata);
|
Metadata == default ? ImmutableDictionary<string, string>.Empty : Metadata);
|
||||||
}
|
}
|
||||||
|
|
||||||
private sealed record SbomVersionPayload(
|
private sealed record SbomVersionPayload(
|
||||||
[property: JsonPropertyName("version")] string Version,
|
[property: JsonPropertyName("version")] string Version,
|
||||||
[property: JsonPropertyName("firstObserved")] DateTimeOffset FirstObserved,
|
[property: JsonPropertyName("firstObserved")] DateTimeOffset FirstObserved,
|
||||||
[property: JsonPropertyName("lastObserved")] DateTimeOffset? LastObserved,
|
[property: JsonPropertyName("lastObserved")] DateTimeOffset? LastObserved,
|
||||||
[property: JsonPropertyName("status")] string Status,
|
[property: JsonPropertyName("status")] string Status,
|
||||||
[property: JsonPropertyName("source")] string Source,
|
[property: JsonPropertyName("source")] string Source,
|
||||||
[property: JsonPropertyName("isFixAvailable")] bool IsFixAvailable,
|
[property: JsonPropertyName("isFixAvailable")] bool IsFixAvailable,
|
||||||
[property: JsonPropertyName("metadata")] ImmutableDictionary<string, string> Metadata)
|
[property: JsonPropertyName("metadata")] ImmutableDictionary<string, string> Metadata)
|
||||||
{
|
{
|
||||||
public SbomVersionRecord ToRecord()
|
public SbomVersionRecord ToRecord()
|
||||||
=> new(
|
=> new(
|
||||||
Version,
|
Version,
|
||||||
FirstObserved,
|
FirstObserved,
|
||||||
LastObserved,
|
LastObserved,
|
||||||
Status,
|
Status,
|
||||||
Source,
|
Source,
|
||||||
IsFixAvailable,
|
IsFixAvailable,
|
||||||
Metadata == default ? ImmutableDictionary<string, string>.Empty : Metadata);
|
Metadata == default ? ImmutableDictionary<string, string>.Empty : Metadata);
|
||||||
}
|
}
|
||||||
|
|
||||||
private sealed record SbomDependencyPathPayload(
|
private sealed record SbomDependencyPathPayload(
|
||||||
[property: JsonPropertyName("nodes")] ImmutableArray<SbomDependencyNodePayload> Nodes,
|
[property: JsonPropertyName("nodes")] ImmutableArray<SbomDependencyNodePayload> Nodes,
|
||||||
[property: JsonPropertyName("isRuntime")] bool IsRuntime,
|
[property: JsonPropertyName("isRuntime")] bool IsRuntime,
|
||||||
[property: JsonPropertyName("source")] string? Source,
|
[property: JsonPropertyName("source")] string? Source,
|
||||||
[property: JsonPropertyName("metadata")] ImmutableDictionary<string, string> Metadata)
|
[property: JsonPropertyName("metadata")] ImmutableDictionary<string, string> Metadata)
|
||||||
{
|
{
|
||||||
public SbomDependencyPathRecord ToRecord()
|
public SbomDependencyPathRecord ToRecord()
|
||||||
=> new(
|
=> new(
|
||||||
Nodes.IsDefault ? ImmutableArray<SbomDependencyNodeRecord>.Empty : Nodes.Select(n => n.ToRecord()).ToImmutableArray(),
|
Nodes.IsDefault ? ImmutableArray<SbomDependencyNodeRecord>.Empty : Nodes.Select(n => n.ToRecord()).ToImmutableArray(),
|
||||||
IsRuntime,
|
IsRuntime,
|
||||||
Source,
|
Source,
|
||||||
Metadata == default ? ImmutableDictionary<string, string>.Empty : Metadata);
|
Metadata == default ? ImmutableDictionary<string, string>.Empty : Metadata);
|
||||||
}
|
}
|
||||||
|
|
||||||
private sealed record SbomDependencyNodePayload(
|
private sealed record SbomDependencyNodePayload(
|
||||||
[property: JsonPropertyName("identifier")] string Identifier,
|
[property: JsonPropertyName("identifier")] string Identifier,
|
||||||
[property: JsonPropertyName("version")] string? Version)
|
[property: JsonPropertyName("version")] string? Version)
|
||||||
{
|
{
|
||||||
public SbomDependencyNodeRecord ToRecord()
|
public SbomDependencyNodeRecord ToRecord()
|
||||||
=> new(Identifier, Version);
|
=> new(Identifier, Version);
|
||||||
}
|
}
|
||||||
|
|
||||||
private sealed record SbomBlastRadiusPayload(
|
private sealed record SbomBlastRadiusPayload(
|
||||||
[property: JsonPropertyName("impactedAssets")] int ImpactedAssets,
|
[property: JsonPropertyName("impactedAssets")] int ImpactedAssets,
|
||||||
[property: JsonPropertyName("impactedWorkloads")] int ImpactedWorkloads,
|
[property: JsonPropertyName("impactedWorkloads")] int ImpactedWorkloads,
|
||||||
[property: JsonPropertyName("impactedNamespaces")] int ImpactedNamespaces,
|
[property: JsonPropertyName("impactedNamespaces")] int ImpactedNamespaces,
|
||||||
[property: JsonPropertyName("impactedPercentage")] double? ImpactedPercentage,
|
[property: JsonPropertyName("impactedPercentage")] double? ImpactedPercentage,
|
||||||
[property: JsonPropertyName("metadata")] ImmutableDictionary<string, string> Metadata)
|
[property: JsonPropertyName("metadata")] ImmutableDictionary<string, string> Metadata)
|
||||||
{
|
{
|
||||||
public SbomBlastRadiusRecord ToRecord()
|
public SbomBlastRadiusRecord ToRecord()
|
||||||
=> new(
|
=> new(
|
||||||
ImpactedAssets,
|
ImpactedAssets,
|
||||||
ImpactedWorkloads,
|
ImpactedWorkloads,
|
||||||
ImpactedNamespaces,
|
ImpactedNamespaces,
|
||||||
ImpactedPercentage,
|
ImpactedPercentage,
|
||||||
Metadata == default ? ImmutableDictionary<string, string>.Empty : Metadata);
|
Metadata == default ? ImmutableDictionary<string, string>.Empty : Metadata);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +1,20 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net10.0</TargetFramework>
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
<LangVersion>preview</LangVersion>
|
<LangVersion>preview</LangVersion>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.0-rc.2.25502.107" />
|
<PackageReference Include="Microsoft.Extensions.Logging.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.Options" Version="10.0.0-rc.2.25502.107" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Http" Version="10.0.0-rc.2.25502.107" />
|
<PackageReference Include="Microsoft.Extensions.Http" Version="10.0.0-rc.2.25502.107" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\..\Concelier\__Libraries\StellaOps.Concelier.Core\StellaOps.Concelier.Core.csproj" />
|
<ProjectReference Include="..\..\Concelier\__Libraries\StellaOps.Concelier.Core\StellaOps.Concelier.Core.csproj" />
|
||||||
<ProjectReference Include="..\..\Concelier\__Libraries\StellaOps.Concelier.RawModels\StellaOps.Concelier.RawModels.csproj" />
|
<ProjectReference Include="..\..\Concelier\__Libraries\StellaOps.Concelier.RawModels\StellaOps.Concelier.RawModels.csproj" />
|
||||||
<ProjectReference Include="..\..\Excititor\__Libraries\StellaOps.Excititor.Core\StellaOps.Excititor.Core.csproj" />
|
<ProjectReference Include="..\..\Excititor\__Libraries\StellaOps.Excititor.Core\StellaOps.Excititor.Core.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -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-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-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-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-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 | 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-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-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-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-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 | 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-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-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-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. |
|
| 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-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-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-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`.
|
||||||
|
|||||||
@@ -1,79 +1,79 @@
|
|||||||
using System.Collections.Immutable;
|
using System.Collections.Immutable;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using FluentAssertions;
|
using FluentAssertions;
|
||||||
using StellaOps.AdvisoryAI.Context;
|
using StellaOps.AdvisoryAI.Context;
|
||||||
using StellaOps.AdvisoryAI.Tools;
|
using StellaOps.AdvisoryAI.Tools;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
namespace StellaOps.AdvisoryAI.Tests;
|
namespace StellaOps.AdvisoryAI.Tests;
|
||||||
|
|
||||||
public sealed class DeterministicToolsetTests
|
public sealed class DeterministicToolsetTests
|
||||||
{
|
{
|
||||||
[Fact]
|
[Fact]
|
||||||
public void AnalyzeDependencies_ComputesRuntimeAndDevelopmentCounts()
|
public void AnalyzeDependencies_ComputesRuntimeAndDevelopmentCounts()
|
||||||
{
|
{
|
||||||
var context = SbomContextResult.Create(
|
var context = SbomContextResult.Create(
|
||||||
"artifact-123",
|
"artifact-123",
|
||||||
purl: null,
|
purl: null,
|
||||||
versionTimeline: Array.Empty<SbomVersionTimelineEntry>(),
|
versionTimeline: Array.Empty<SbomVersionTimelineEntry>(),
|
||||||
dependencyPaths: new[]
|
dependencyPaths: new[]
|
||||||
{
|
{
|
||||||
new SbomDependencyPath(
|
new SbomDependencyPath(
|
||||||
new[]
|
new[]
|
||||||
{
|
{
|
||||||
new SbomDependencyNode("root", "1.0.0"),
|
new SbomDependencyNode("root", "1.0.0"),
|
||||||
new SbomDependencyNode("lib-a", "2.0.0"),
|
new SbomDependencyNode("lib-a", "2.0.0"),
|
||||||
},
|
},
|
||||||
isRuntime: true),
|
isRuntime: true),
|
||||||
new SbomDependencyPath(
|
new SbomDependencyPath(
|
||||||
new[]
|
new[]
|
||||||
{
|
{
|
||||||
new SbomDependencyNode("root", "1.0.0"),
|
new SbomDependencyNode("root", "1.0.0"),
|
||||||
new SbomDependencyNode("lib-b", "3.1.4"),
|
new SbomDependencyNode("lib-b", "3.1.4"),
|
||||||
},
|
},
|
||||||
isRuntime: false),
|
isRuntime: false),
|
||||||
});
|
});
|
||||||
|
|
||||||
IDeterministicToolset toolset = new DeterministicToolset();
|
IDeterministicToolset toolset = new DeterministicToolset();
|
||||||
var analysis = toolset.AnalyzeDependencies(context);
|
var analysis = toolset.AnalyzeDependencies(context);
|
||||||
|
|
||||||
analysis.ArtifactId.Should().Be("artifact-123");
|
analysis.ArtifactId.Should().Be("artifact-123");
|
||||||
analysis.Metadata["path_count"].Should().Be("2");
|
analysis.Metadata["path_count"].Should().Be("2");
|
||||||
analysis.Metadata["runtime_path_count"].Should().Be("1");
|
analysis.Metadata["runtime_path_count"].Should().Be("1");
|
||||||
analysis.Metadata["development_path_count"].Should().Be("1");
|
analysis.Metadata["development_path_count"].Should().Be("1");
|
||||||
analysis.Nodes.Should().HaveCount(3);
|
analysis.Nodes.Should().HaveCount(3);
|
||||||
|
|
||||||
var libA = analysis.Nodes.Single(node => node.Identifier == "lib-a");
|
var libA = analysis.Nodes.Single(node => node.Identifier == "lib-a");
|
||||||
libA.RuntimeOccurrences.Should().Be(1);
|
libA.RuntimeOccurrences.Should().Be(1);
|
||||||
libA.DevelopmentOccurrences.Should().Be(0);
|
libA.DevelopmentOccurrences.Should().Be(0);
|
||||||
|
|
||||||
var libB = analysis.Nodes.Single(node => node.Identifier == "lib-b");
|
var libB = analysis.Nodes.Single(node => node.Identifier == "lib-b");
|
||||||
libB.RuntimeOccurrences.Should().Be(0);
|
libB.RuntimeOccurrences.Should().Be(0);
|
||||||
libB.DevelopmentOccurrences.Should().Be(1);
|
libB.DevelopmentOccurrences.Should().Be(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[InlineData("semver", "1.2.3", "1.2.4", -1)]
|
[InlineData("semver", "1.2.3", "1.2.4", -1)]
|
||||||
[InlineData("semver", "1.2.3", "1.2.3", 0)]
|
[InlineData("semver", "1.2.3", "1.2.3", 0)]
|
||||||
[InlineData("semver", "1.2.4", "1.2.3", 1)]
|
[InlineData("semver", "1.2.4", "1.2.3", 1)]
|
||||||
[InlineData("evr", "1:1.0-1", "1:1.0-2", -1)]
|
[InlineData("evr", "1:1.0-1", "1:1.0-2", -1)]
|
||||||
[InlineData("evr", "0:2.0-0", "0:2.0-0", 0)]
|
[InlineData("evr", "0:2.0-0", "0:2.0-0", 0)]
|
||||||
[InlineData("evr", "0:2.1-0", "0:2.0-5", 1)]
|
[InlineData("evr", "0:2.1-0", "0:2.0-5", 1)]
|
||||||
public void TryCompare_SucceedsForSupportedSchemes(string scheme, string left, string right, int expected)
|
public void TryCompare_SucceedsForSupportedSchemes(string scheme, string left, string right, int expected)
|
||||||
{
|
{
|
||||||
IDeterministicToolset toolset = new DeterministicToolset();
|
IDeterministicToolset toolset = new DeterministicToolset();
|
||||||
toolset.TryCompare(scheme, left, right, out var comparison).Should().BeTrue();
|
toolset.TryCompare(scheme, left, right, out var comparison).Should().BeTrue();
|
||||||
comparison.Should().Be(expected);
|
comparison.Should().Be(expected);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[InlineData("semver", "1.2.3", ">=1.0.0 <2.0.0")]
|
[InlineData("semver", "1.2.3", ">=1.0.0 <2.0.0")]
|
||||||
[InlineData("semver", "2.0.0", ">=2.0.0")]
|
[InlineData("semver", "2.0.0", ">=2.0.0")]
|
||||||
[InlineData("evr", "0:1.2-3", ">=0:1.0-0 <0:2.0-0")]
|
[InlineData("evr", "0:1.2-3", ">=0:1.0-0 <0:2.0-0")]
|
||||||
[InlineData("evr", "1:3.4-1", ">=1:3.0-0")]
|
[InlineData("evr", "1:3.4-1", ">=1:3.0-0")]
|
||||||
public void SatisfiesRange_HonoursExpressions(string scheme, string version, string range)
|
public void SatisfiesRange_HonoursExpressions(string scheme, string version, string range)
|
||||||
{
|
{
|
||||||
IDeterministicToolset toolset = new DeterministicToolset();
|
IDeterministicToolset toolset = new DeterministicToolset();
|
||||||
toolset.SatisfiesRange(scheme, version, range).Should().BeTrue();
|
toolset.SatisfiesRange(scheme, version, range).Should().BeTrue();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,144 +1,144 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.Extensions.Logging.Abstractions;
|
using Microsoft.Extensions.Logging.Abstractions;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using StellaOps.AdvisoryAI.Providers;
|
using StellaOps.AdvisoryAI.Providers;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
namespace StellaOps.AdvisoryAI.Tests;
|
namespace StellaOps.AdvisoryAI.Tests;
|
||||||
|
|
||||||
public sealed class SbomContextHttpClientTests
|
public sealed class SbomContextHttpClientTests
|
||||||
{
|
{
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task GetContextAsync_MapsPayloadToDocument()
|
public async Task GetContextAsync_MapsPayloadToDocument()
|
||||||
{
|
{
|
||||||
const string payload = """
|
const string payload = """
|
||||||
{
|
{
|
||||||
"artifactId": "artifact-001",
|
"artifactId": "artifact-001",
|
||||||
"purl": "pkg:npm/react@18.3.0",
|
"purl": "pkg:npm/react@18.3.0",
|
||||||
"versions": [
|
"versions": [
|
||||||
{
|
{
|
||||||
"version": "18.3.0",
|
"version": "18.3.0",
|
||||||
"firstObserved": "2025-10-01T00:00:00Z",
|
"firstObserved": "2025-10-01T00:00:00Z",
|
||||||
"lastObserved": null,
|
"lastObserved": null,
|
||||||
"status": "affected",
|
"status": "affected",
|
||||||
"source": "inventory",
|
"source": "inventory",
|
||||||
"isFixAvailable": false,
|
"isFixAvailable": false,
|
||||||
"metadata": { "note": "current" }
|
"metadata": { "note": "current" }
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"dependencyPaths": [
|
"dependencyPaths": [
|
||||||
{
|
{
|
||||||
"nodes": [
|
"nodes": [
|
||||||
{ "identifier": "app", "version": "1.0.0" },
|
{ "identifier": "app", "version": "1.0.0" },
|
||||||
{ "identifier": "react", "version": "18.3.0" }
|
{ "identifier": "react", "version": "18.3.0" }
|
||||||
],
|
],
|
||||||
"isRuntime": true,
|
"isRuntime": true,
|
||||||
"source": "scanner",
|
"source": "scanner",
|
||||||
"metadata": { "scope": "production" }
|
"metadata": { "scope": "production" }
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"environmentFlags": {
|
"environmentFlags": {
|
||||||
"environment/prod": "true"
|
"environment/prod": "true"
|
||||||
},
|
},
|
||||||
"blastRadius": {
|
"blastRadius": {
|
||||||
"impactedAssets": 10,
|
"impactedAssets": 10,
|
||||||
"impactedWorkloads": 4,
|
"impactedWorkloads": 4,
|
||||||
"impactedNamespaces": 2,
|
"impactedNamespaces": 2,
|
||||||
"impactedPercentage": 0.25,
|
"impactedPercentage": 0.25,
|
||||||
"metadata": { "note": "simulated" }
|
"metadata": { "note": "simulated" }
|
||||||
},
|
},
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"source": "sbom-service"
|
"source": "sbom-service"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
""";
|
""";
|
||||||
|
|
||||||
var handler = new StubHttpMessageHandler(_ => new HttpResponseMessage(HttpStatusCode.OK)
|
var handler = new StubHttpMessageHandler(_ => new HttpResponseMessage(HttpStatusCode.OK)
|
||||||
{
|
{
|
||||||
Content = new StringContent(payload, Encoding.UTF8, "application/json")
|
Content = new StringContent(payload, Encoding.UTF8, "application/json")
|
||||||
});
|
});
|
||||||
|
|
||||||
var httpClient = new HttpClient(handler)
|
var httpClient = new HttpClient(handler)
|
||||||
{
|
{
|
||||||
BaseAddress = new Uri("https://sbom.example/")
|
BaseAddress = new Uri("https://sbom.example/")
|
||||||
};
|
};
|
||||||
|
|
||||||
var options = Options.Create(new SbomContextClientOptions
|
var options = Options.Create(new SbomContextClientOptions
|
||||||
{
|
{
|
||||||
ContextEndpoint = "api/sbom/context",
|
ContextEndpoint = "api/sbom/context",
|
||||||
Tenant = "tenant-alpha",
|
Tenant = "tenant-alpha",
|
||||||
TenantHeaderName = "X-StellaOps-Tenant"
|
TenantHeaderName = "X-StellaOps-Tenant"
|
||||||
});
|
});
|
||||||
|
|
||||||
var client = new SbomContextHttpClient(httpClient, options, NullLogger<SbomContextHttpClient>.Instance);
|
var client = new SbomContextHttpClient(httpClient, options, NullLogger<SbomContextHttpClient>.Instance);
|
||||||
|
|
||||||
var query = new SbomContextQuery("artifact-001", "pkg:npm/react@18.3.0", 25, 10, includeEnvironmentFlags: true, includeBlastRadius: true);
|
var query = new SbomContextQuery("artifact-001", "pkg:npm/react@18.3.0", 25, 10, includeEnvironmentFlags: true, includeBlastRadius: true);
|
||||||
var document = await client.GetContextAsync(query, CancellationToken.None);
|
var document = await client.GetContextAsync(query, CancellationToken.None);
|
||||||
|
|
||||||
Assert.NotNull(document);
|
Assert.NotNull(document);
|
||||||
Assert.Equal("artifact-001", document!.ArtifactId);
|
Assert.Equal("artifact-001", document!.ArtifactId);
|
||||||
Assert.Equal("pkg:npm/react@18.3.0", document.Purl);
|
Assert.Equal("pkg:npm/react@18.3.0", document.Purl);
|
||||||
Assert.Single(document.Versions);
|
Assert.Single(document.Versions);
|
||||||
Assert.Single(document.DependencyPaths);
|
Assert.Single(document.DependencyPaths);
|
||||||
Assert.Single(document.EnvironmentFlags);
|
Assert.Single(document.EnvironmentFlags);
|
||||||
Assert.NotNull(document.BlastRadius);
|
Assert.NotNull(document.BlastRadius);
|
||||||
Assert.Equal("sbom-service", document.Metadata["source"]);
|
Assert.Equal("sbom-service", document.Metadata["source"]);
|
||||||
|
|
||||||
Assert.NotNull(handler.LastRequest);
|
Assert.NotNull(handler.LastRequest);
|
||||||
Assert.Equal("tenant-alpha", handler.LastRequest!.Headers.GetValues("X-StellaOps-Tenant").Single());
|
Assert.Equal("tenant-alpha", handler.LastRequest!.Headers.GetValues("X-StellaOps-Tenant").Single());
|
||||||
Assert.Contains("artifactId=artifact-001", handler.LastRequest.RequestUri!.Query);
|
Assert.Contains("artifactId=artifact-001", handler.LastRequest.RequestUri!.Query);
|
||||||
Assert.Contains("purl=pkg%3Anpm%2Freact%4018.3.0", handler.LastRequest.RequestUri!.Query);
|
Assert.Contains("purl=pkg%3Anpm%2Freact%4018.3.0", handler.LastRequest.RequestUri!.Query);
|
||||||
Assert.Contains("includeEnvironmentFlags=true", handler.LastRequest.RequestUri!.Query);
|
Assert.Contains("includeEnvironmentFlags=true", handler.LastRequest.RequestUri!.Query);
|
||||||
Assert.Contains("includeBlastRadius=true", handler.LastRequest.RequestUri!.Query);
|
Assert.Contains("includeBlastRadius=true", handler.LastRequest.RequestUri!.Query);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task GetContextAsync_ReturnsNullOnNotFound()
|
public async Task GetContextAsync_ReturnsNullOnNotFound()
|
||||||
{
|
{
|
||||||
var handler = new StubHttpMessageHandler(_ => new HttpResponseMessage(HttpStatusCode.NotFound));
|
var handler = new StubHttpMessageHandler(_ => new HttpResponseMessage(HttpStatusCode.NotFound));
|
||||||
var httpClient = new HttpClient(handler) { BaseAddress = new Uri("https://sbom.example/") };
|
var httpClient = new HttpClient(handler) { BaseAddress = new Uri("https://sbom.example/") };
|
||||||
var options = Options.Create(new SbomContextClientOptions());
|
var options = Options.Create(new SbomContextClientOptions());
|
||||||
var client = new SbomContextHttpClient(httpClient, options, NullLogger<SbomContextHttpClient>.Instance);
|
var client = new SbomContextHttpClient(httpClient, options, NullLogger<SbomContextHttpClient>.Instance);
|
||||||
|
|
||||||
var result = await client.GetContextAsync(new SbomContextQuery("missing", null, 10, 5, false, false), CancellationToken.None);
|
var result = await client.GetContextAsync(new SbomContextQuery("missing", null, 10, 5, false, false), CancellationToken.None);
|
||||||
Assert.Null(result);
|
Assert.Null(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task GetContextAsync_ThrowsForServerError()
|
public async Task GetContextAsync_ThrowsForServerError()
|
||||||
{
|
{
|
||||||
var handler = new StubHttpMessageHandler(_ => new HttpResponseMessage(HttpStatusCode.InternalServerError)
|
var handler = new StubHttpMessageHandler(_ => new HttpResponseMessage(HttpStatusCode.InternalServerError)
|
||||||
{
|
{
|
||||||
Content = new StringContent("{\"error\":\"boom\"}", Encoding.UTF8, "application/json")
|
Content = new StringContent("{\"error\":\"boom\"}", Encoding.UTF8, "application/json")
|
||||||
});
|
});
|
||||||
var httpClient = new HttpClient(handler) { BaseAddress = new Uri("https://sbom.example/") };
|
var httpClient = new HttpClient(handler) { BaseAddress = new Uri("https://sbom.example/") };
|
||||||
var options = Options.Create(new SbomContextClientOptions());
|
var options = Options.Create(new SbomContextClientOptions());
|
||||||
var client = new SbomContextHttpClient(httpClient, options, NullLogger<SbomContextHttpClient>.Instance);
|
var client = new SbomContextHttpClient(httpClient, options, NullLogger<SbomContextHttpClient>.Instance);
|
||||||
|
|
||||||
await Assert.ThrowsAsync<HttpRequestException>(() => client.GetContextAsync(new SbomContextQuery("artifact", null, 5, 5, false, false), CancellationToken.None));
|
await Assert.ThrowsAsync<HttpRequestException>(() => client.GetContextAsync(new SbomContextQuery("artifact", null, 5, 5, false, false), CancellationToken.None));
|
||||||
}
|
}
|
||||||
|
|
||||||
private sealed class StubHttpMessageHandler : HttpMessageHandler
|
private sealed class StubHttpMessageHandler : HttpMessageHandler
|
||||||
{
|
{
|
||||||
private readonly Func<HttpRequestMessage, HttpResponseMessage> responder;
|
private readonly Func<HttpRequestMessage, HttpResponseMessage> responder;
|
||||||
|
|
||||||
public StubHttpMessageHandler(Func<HttpRequestMessage, HttpResponseMessage> responder)
|
public StubHttpMessageHandler(Func<HttpRequestMessage, HttpResponseMessage> responder)
|
||||||
{
|
{
|
||||||
this.responder = responder ?? throw new ArgumentNullException(nameof(responder));
|
this.responder = responder ?? throw new ArgumentNullException(nameof(responder));
|
||||||
}
|
}
|
||||||
|
|
||||||
public HttpRequestMessage? LastRequest { get; private set; }
|
public HttpRequestMessage? LastRequest { get; private set; }
|
||||||
|
|
||||||
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
|
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
LastRequest = request;
|
LastRequest = request;
|
||||||
return Task.FromResult(responder(request));
|
return Task.FromResult(responder(request));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,29 +1,29 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net10.0</TargetFramework>
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
<LangVersion>preview</LangVersion>
|
<LangVersion>preview</LangVersion>
|
||||||
<IsPackable>false</IsPackable>
|
<IsPackable>false</IsPackable>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="FluentAssertions" Version="6.12.0" />
|
<PackageReference Include="FluentAssertions" Version="6.12.0" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.0-rc.2.25502.107" />
|
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.0-rc.2.25502.107" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\..\StellaOps.AdvisoryAI\StellaOps.AdvisoryAI.csproj" />
|
<ProjectReference Include="..\..\StellaOps.AdvisoryAI\StellaOps.AdvisoryAI.csproj" />
|
||||||
<ProjectReference Include="..\..\StellaOps.AdvisoryAI.Hosting\StellaOps.AdvisoryAI.Hosting.csproj" />
|
<ProjectReference Include="..\..\StellaOps.AdvisoryAI.Hosting\StellaOps.AdvisoryAI.Hosting.csproj" />
|
||||||
<ProjectReference Include="..\..\..\Concelier\__Libraries\StellaOps.Concelier.Core\StellaOps.Concelier.Core.csproj" />
|
<ProjectReference Include="..\..\..\Concelier\__Libraries\StellaOps.Concelier.Core\StellaOps.Concelier.Core.csproj" />
|
||||||
<ProjectReference Include="..\..\..\Concelier\__Libraries\StellaOps.Concelier.RawModels\StellaOps.Concelier.RawModels.csproj" />
|
<ProjectReference Include="..\..\..\Concelier\__Libraries\StellaOps.Concelier.RawModels\StellaOps.Concelier.RawModels.csproj" />
|
||||||
<ProjectReference Include="..\..\..\Excititor\__Libraries\StellaOps.Excititor.Core\StellaOps.Excititor.Core.csproj" />
|
<ProjectReference Include="..\..\..\Excititor\__Libraries\StellaOps.Excititor.Core\StellaOps.Excititor.Core.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<None Update="TestData/*.json">
|
<None Update="TestData/*.json">
|
||||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
</None>
|
</None>
|
||||||
<None Update="TestData/*.md">
|
<None Update="TestData/*.md">
|
||||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
</None>
|
</None>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -10,30 +10,30 @@ using StellaOps.AdvisoryAI.Tools;
|
|||||||
using StellaOps.AdvisoryAI.Abstractions;
|
using StellaOps.AdvisoryAI.Abstractions;
|
||||||
using StellaOps.AdvisoryAI.Documents;
|
using StellaOps.AdvisoryAI.Documents;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
namespace StellaOps.AdvisoryAI.Tests;
|
namespace StellaOps.AdvisoryAI.Tests;
|
||||||
|
|
||||||
public sealed class ToolsetServiceCollectionExtensionsTests
|
public sealed class ToolsetServiceCollectionExtensionsTests
|
||||||
{
|
{
|
||||||
[Fact]
|
[Fact]
|
||||||
public void AddAdvisoryDeterministicToolset_RegistersSingleton()
|
public void AddAdvisoryDeterministicToolset_RegistersSingleton()
|
||||||
{
|
{
|
||||||
var services = new ServiceCollection();
|
var services = new ServiceCollection();
|
||||||
|
|
||||||
services.AddAdvisoryDeterministicToolset();
|
services.AddAdvisoryDeterministicToolset();
|
||||||
|
|
||||||
var provider = services.BuildServiceProvider();
|
var provider = services.BuildServiceProvider();
|
||||||
var toolsetA = provider.GetRequiredService<IDeterministicToolset>();
|
var toolsetA = provider.GetRequiredService<IDeterministicToolset>();
|
||||||
var toolsetB = provider.GetRequiredService<IDeterministicToolset>();
|
var toolsetB = provider.GetRequiredService<IDeterministicToolset>();
|
||||||
|
|
||||||
Assert.Same(toolsetA, toolsetB);
|
Assert.Same(toolsetA, toolsetB);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void AddAdvisoryPipeline_RegistersOrchestrator()
|
public void AddAdvisoryPipeline_RegistersOrchestrator()
|
||||||
{
|
{
|
||||||
var services = new ServiceCollection();
|
var services = new ServiceCollection();
|
||||||
|
|
||||||
services.AddSbomContext(options =>
|
services.AddSbomContext(options =>
|
||||||
{
|
{
|
||||||
options.BaseAddress = new Uri("https://sbom.example/");
|
options.BaseAddress = new Uri("https://sbom.example/");
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
| PLG4-6.CAPABILITIES | BLOCKED (2025-10-12) | BE-Auth Plugin, Docs Guild | PLG1–PLG3 | 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. |
|
| PLG4-6.CAPABILITIES | BLOCKED (2025-10-12) | BE-Auth Plugin, Docs Guild | PLG1–PLG3 | 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.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-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-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-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. |
|
| 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. |
|
||||||
|
|||||||
@@ -1,282 +1,282 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Net.Http.Json;
|
using System.Net.Http.Json;
|
||||||
using System.Security.Cryptography;
|
using System.Security.Cryptography;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.Authentication;
|
using Microsoft.AspNetCore.Authentication;
|
||||||
using Microsoft.AspNetCore.Hosting;
|
using Microsoft.AspNetCore.Hosting;
|
||||||
using System.Net.Http.Headers;
|
using System.Net.Http.Headers;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using MongoDB.Bson;
|
using MongoDB.Bson;
|
||||||
using MongoDB.Driver;
|
using MongoDB.Driver;
|
||||||
using StellaOps.Authority.Tests.Infrastructure;
|
using StellaOps.Authority.Tests.Infrastructure;
|
||||||
using StellaOps.Auth.Abstractions;
|
using StellaOps.Auth.Abstractions;
|
||||||
using StellaOps.Configuration;
|
using StellaOps.Configuration;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
using Microsoft.AspNetCore.TestHost;
|
using Microsoft.AspNetCore.TestHost;
|
||||||
|
|
||||||
namespace StellaOps.Authority.Tests.AdvisoryAi;
|
namespace StellaOps.Authority.Tests.AdvisoryAi;
|
||||||
|
|
||||||
public sealed class AdvisoryAiRemoteInferenceEndpointTests : IClassFixture<AuthorityWebApplicationFactory>
|
public sealed class AdvisoryAiRemoteInferenceEndpointTests : IClassFixture<AuthorityWebApplicationFactory>
|
||||||
{
|
{
|
||||||
private readonly AuthorityWebApplicationFactory factory;
|
private readonly AuthorityWebApplicationFactory factory;
|
||||||
|
|
||||||
public AdvisoryAiRemoteInferenceEndpointTests(AuthorityWebApplicationFactory factory)
|
public AdvisoryAiRemoteInferenceEndpointTests(AuthorityWebApplicationFactory factory)
|
||||||
{
|
{
|
||||||
this.factory = factory;
|
this.factory = factory;
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task RemoteInference_ReturnsForbidden_WhenDisabled()
|
public async Task RemoteInference_ReturnsForbidden_WhenDisabled()
|
||||||
{
|
{
|
||||||
using var client = CreateClient(
|
using var client = CreateClient(
|
||||||
configureOptions: options =>
|
configureOptions: options =>
|
||||||
{
|
{
|
||||||
options.AdvisoryAi.RemoteInference.Enabled = false;
|
options.AdvisoryAi.RemoteInference.Enabled = false;
|
||||||
options.AdvisoryAi.RemoteInference.AllowedProfiles.Clear();
|
options.AdvisoryAi.RemoteInference.AllowedProfiles.Clear();
|
||||||
options.AdvisoryAi.RemoteInference.AllowedProfiles.Add("cloud-openai");
|
options.AdvisoryAi.RemoteInference.AllowedProfiles.Add("cloud-openai");
|
||||||
});
|
});
|
||||||
|
|
||||||
var response = await client.PostAsJsonAsync(
|
var response = await client.PostAsJsonAsync(
|
||||||
"/advisory-ai/remote-inference/logs",
|
"/advisory-ai/remote-inference/logs",
|
||||||
CreatePayload(profile: "cloud-openai"));
|
CreatePayload(profile: "cloud-openai"));
|
||||||
|
|
||||||
Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode);
|
Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode);
|
||||||
var body = await response.Content.ReadFromJsonAsync<Dictionary<string, string>>();
|
var body = await response.Content.ReadFromJsonAsync<Dictionary<string, string>>();
|
||||||
Assert.NotNull(body);
|
Assert.NotNull(body);
|
||||||
Assert.Equal("remote_inference_disabled", body!["error"]);
|
Assert.Equal("remote_inference_disabled", body!["error"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task RemoteInference_ReturnsForbidden_WhenConsentMissing()
|
public async Task RemoteInference_ReturnsForbidden_WhenConsentMissing()
|
||||||
{
|
{
|
||||||
using var client = CreateClient(
|
using var client = CreateClient(
|
||||||
configureOptions: options =>
|
configureOptions: options =>
|
||||||
{
|
{
|
||||||
SeedRemoteInferenceEnabled(options);
|
SeedRemoteInferenceEnabled(options);
|
||||||
SeedTenantConsent(options);
|
SeedTenantConsent(options);
|
||||||
options.Tenants[0].AdvisoryAi.RemoteInference.ConsentGranted = false;
|
options.Tenants[0].AdvisoryAi.RemoteInference.ConsentGranted = false;
|
||||||
options.Tenants[0].AdvisoryAi.RemoteInference.ConsentVersion = null;
|
options.Tenants[0].AdvisoryAi.RemoteInference.ConsentVersion = null;
|
||||||
options.Tenants[0].AdvisoryAi.RemoteInference.ConsentedAt = null;
|
options.Tenants[0].AdvisoryAi.RemoteInference.ConsentedAt = null;
|
||||||
options.Tenants[0].AdvisoryAi.RemoteInference.ConsentedBy = null;
|
options.Tenants[0].AdvisoryAi.RemoteInference.ConsentedBy = null;
|
||||||
});
|
});
|
||||||
|
|
||||||
client.DefaultRequestHeaders.Add("X-Test-Tenant", "tenant-default");
|
client.DefaultRequestHeaders.Add("X-Test-Tenant", "tenant-default");
|
||||||
|
|
||||||
var response = await client.PostAsJsonAsync(
|
var response = await client.PostAsJsonAsync(
|
||||||
"/advisory-ai/remote-inference/logs",
|
"/advisory-ai/remote-inference/logs",
|
||||||
CreatePayload(profile: "cloud-openai"));
|
CreatePayload(profile: "cloud-openai"));
|
||||||
|
|
||||||
Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode);
|
Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode);
|
||||||
var body = await response.Content.ReadFromJsonAsync<Dictionary<string, string>>();
|
var body = await response.Content.ReadFromJsonAsync<Dictionary<string, string>>();
|
||||||
Assert.NotNull(body);
|
Assert.NotNull(body);
|
||||||
Assert.Equal("remote_inference_consent_required", body!["error"]);
|
Assert.Equal("remote_inference_consent_required", body!["error"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task RemoteInference_ReturnsBadRequest_WhenProfileNotAllowed()
|
public async Task RemoteInference_ReturnsBadRequest_WhenProfileNotAllowed()
|
||||||
{
|
{
|
||||||
using var client = CreateClient(
|
using var client = CreateClient(
|
||||||
configureOptions: options =>
|
configureOptions: options =>
|
||||||
{
|
{
|
||||||
SeedRemoteInferenceEnabled(options);
|
SeedRemoteInferenceEnabled(options);
|
||||||
SeedTenantConsent(options);
|
SeedTenantConsent(options);
|
||||||
});
|
});
|
||||||
|
|
||||||
client.DefaultRequestHeaders.Add("X-Test-Tenant", "tenant-default");
|
client.DefaultRequestHeaders.Add("X-Test-Tenant", "tenant-default");
|
||||||
|
|
||||||
var response = await client.PostAsJsonAsync(
|
var response = await client.PostAsJsonAsync(
|
||||||
"/advisory-ai/remote-inference/logs",
|
"/advisory-ai/remote-inference/logs",
|
||||||
CreatePayload(profile: "other-profile"));
|
CreatePayload(profile: "other-profile"));
|
||||||
|
|
||||||
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
|
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
|
||||||
var body = await response.Content.ReadFromJsonAsync<Dictionary<string, string>>();
|
var body = await response.Content.ReadFromJsonAsync<Dictionary<string, string>>();
|
||||||
Assert.NotNull(body);
|
Assert.NotNull(body);
|
||||||
Assert.Equal("profile_not_allowed", body!["error"]);
|
Assert.Equal("profile_not_allowed", body!["error"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task RemoteInference_LogsPrompt_WhenConsentGranted()
|
public async Task RemoteInference_LogsPrompt_WhenConsentGranted()
|
||||||
{
|
{
|
||||||
using var client = CreateClient(
|
using var client = CreateClient(
|
||||||
configureOptions: options =>
|
configureOptions: options =>
|
||||||
{
|
{
|
||||||
SeedRemoteInferenceEnabled(options);
|
SeedRemoteInferenceEnabled(options);
|
||||||
SeedTenantConsent(options);
|
SeedTenantConsent(options);
|
||||||
});
|
});
|
||||||
|
|
||||||
client.DefaultRequestHeaders.Add("X-Test-Tenant", "tenant-default");
|
client.DefaultRequestHeaders.Add("X-Test-Tenant", "tenant-default");
|
||||||
|
|
||||||
var database = new MongoClient(factory.ConnectionString).GetDatabase("authority-tests");
|
var database = new MongoClient(factory.ConnectionString).GetDatabase("authority-tests");
|
||||||
var collection = database.GetCollection<BsonDocument>("authority_login_attempts");
|
var collection = database.GetCollection<BsonDocument>("authority_login_attempts");
|
||||||
await collection.DeleteManyAsync(FilterDefinition<BsonDocument>.Empty);
|
await collection.DeleteManyAsync(FilterDefinition<BsonDocument>.Empty);
|
||||||
|
|
||||||
var payload = CreatePayload(profile: "cloud-openai", prompt: "Generate remediation plan.");
|
var payload = CreatePayload(profile: "cloud-openai", prompt: "Generate remediation plan.");
|
||||||
var response = await client.PostAsJsonAsync("/advisory-ai/remote-inference/logs", payload);
|
var response = await client.PostAsJsonAsync("/advisory-ai/remote-inference/logs", payload);
|
||||||
|
|
||||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||||
var body = await response.Content.ReadFromJsonAsync<Dictionary<string, string>>();
|
var body = await response.Content.ReadFromJsonAsync<Dictionary<string, string>>();
|
||||||
Assert.NotNull(body);
|
Assert.NotNull(body);
|
||||||
Assert.Equal("logged", body!["status"]);
|
Assert.Equal("logged", body!["status"]);
|
||||||
|
|
||||||
var expectedHash = ComputeSha256(payload.Prompt);
|
var expectedHash = ComputeSha256(payload.Prompt);
|
||||||
Assert.Equal(expectedHash, body["prompt_hash"]);
|
Assert.Equal(expectedHash, body["prompt_hash"]);
|
||||||
|
|
||||||
var doc = await collection.Find(Builders<BsonDocument>.Filter.Eq("eventType", "authority.advisory_ai.remote_inference")).SingleAsync();
|
var doc = await collection.Find(Builders<BsonDocument>.Filter.Eq("eventType", "authority.advisory_ai.remote_inference")).SingleAsync();
|
||||||
Assert.Equal("authority.advisory_ai.remote_inference", doc["eventType"].AsString);
|
Assert.Equal("authority.advisory_ai.remote_inference", doc["eventType"].AsString);
|
||||||
|
|
||||||
var properties = ExtractProperties(doc);
|
var properties = ExtractProperties(doc);
|
||||||
Assert.Equal(expectedHash, properties["advisory_ai.prompt.hash"]);
|
Assert.Equal(expectedHash, properties["advisory_ai.prompt.hash"]);
|
||||||
Assert.Equal("sha256", properties["advisory_ai.prompt.algorithm"]);
|
Assert.Equal("sha256", properties["advisory_ai.prompt.algorithm"]);
|
||||||
Assert.Equal(payload.Profile, properties["advisory_ai.profile"]);
|
Assert.Equal(payload.Profile, properties["advisory_ai.profile"]);
|
||||||
Assert.False(properties.ContainsKey("advisory_ai.prompt.raw"));
|
Assert.False(properties.ContainsKey("advisory_ai.prompt.raw"));
|
||||||
}
|
}
|
||||||
|
|
||||||
private HttpClient CreateClient(Action<StellaOpsAuthorityOptions>? configureOptions = null)
|
private HttpClient CreateClient(Action<StellaOpsAuthorityOptions>? configureOptions = null)
|
||||||
{
|
{
|
||||||
const string schemeName = "StellaOpsBearer";
|
const string schemeName = "StellaOpsBearer";
|
||||||
|
|
||||||
var builder = factory.WithWebHostBuilder(hostBuilder =>
|
var builder = factory.WithWebHostBuilder(hostBuilder =>
|
||||||
{
|
{
|
||||||
hostBuilder.ConfigureTestServices(services =>
|
hostBuilder.ConfigureTestServices(services =>
|
||||||
{
|
{
|
||||||
services.AddAuthentication(options =>
|
services.AddAuthentication(options =>
|
||||||
{
|
{
|
||||||
options.DefaultAuthenticateScheme = schemeName;
|
options.DefaultAuthenticateScheme = schemeName;
|
||||||
options.DefaultChallengeScheme = schemeName;
|
options.DefaultChallengeScheme = schemeName;
|
||||||
})
|
})
|
||||||
.AddScheme<AuthenticationSchemeOptions, TestAuthHandler>(schemeName, _ => { });
|
.AddScheme<AuthenticationSchemeOptions, TestAuthHandler>(schemeName, _ => { });
|
||||||
|
|
||||||
services.PostConfigure<StellaOpsAuthorityOptions>(opts =>
|
services.PostConfigure<StellaOpsAuthorityOptions>(opts =>
|
||||||
{
|
{
|
||||||
opts.Issuer ??= new Uri("https://authority.test");
|
opts.Issuer ??= new Uri("https://authority.test");
|
||||||
if (string.IsNullOrWhiteSpace(opts.Storage.ConnectionString))
|
if (string.IsNullOrWhiteSpace(opts.Storage.ConnectionString))
|
||||||
{
|
{
|
||||||
opts.Storage.ConnectionString = factory.ConnectionString;
|
opts.Storage.ConnectionString = factory.ConnectionString;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(opts.Storage.DatabaseName))
|
if (string.IsNullOrWhiteSpace(opts.Storage.DatabaseName))
|
||||||
{
|
{
|
||||||
opts.Storage.DatabaseName = "authority-tests";
|
opts.Storage.DatabaseName = "authority-tests";
|
||||||
}
|
}
|
||||||
|
|
||||||
opts.AdvisoryAi.RemoteInference.Enabled = true;
|
opts.AdvisoryAi.RemoteInference.Enabled = true;
|
||||||
opts.AdvisoryAi.RemoteInference.RequireTenantConsent = true;
|
opts.AdvisoryAi.RemoteInference.RequireTenantConsent = true;
|
||||||
opts.AdvisoryAi.RemoteInference.AllowedProfiles.Clear();
|
opts.AdvisoryAi.RemoteInference.AllowedProfiles.Clear();
|
||||||
opts.AdvisoryAi.RemoteInference.AllowedProfiles.Add("cloud-openai");
|
opts.AdvisoryAi.RemoteInference.AllowedProfiles.Add("cloud-openai");
|
||||||
|
|
||||||
opts.Tenants.Clear();
|
opts.Tenants.Clear();
|
||||||
opts.Tenants.Add(new AuthorityTenantOptions
|
opts.Tenants.Add(new AuthorityTenantOptions
|
||||||
{
|
{
|
||||||
Id = "tenant-default",
|
Id = "tenant-default",
|
||||||
DisplayName = "Tenant Default",
|
DisplayName = "Tenant Default",
|
||||||
AdvisoryAi =
|
AdvisoryAi =
|
||||||
{
|
{
|
||||||
RemoteInference =
|
RemoteInference =
|
||||||
{
|
{
|
||||||
ConsentGranted = true,
|
ConsentGranted = true,
|
||||||
ConsentVersion = "2025-10",
|
ConsentVersion = "2025-10",
|
||||||
ConsentedAt = DateTimeOffset.Parse("2025-10-31T12:34:56Z"),
|
ConsentedAt = DateTimeOffset.Parse("2025-10-31T12:34:56Z"),
|
||||||
ConsentedBy = "legal@example.com"
|
ConsentedBy = "legal@example.com"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
configureOptions?.Invoke(opts);
|
configureOptions?.Invoke(opts);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
var client = builder.CreateClient();
|
var client = builder.CreateClient();
|
||||||
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(schemeName);
|
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(schemeName);
|
||||||
return client;
|
return client;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void SeedRemoteInferenceEnabled(StellaOpsAuthorityOptions options)
|
private static void SeedRemoteInferenceEnabled(StellaOpsAuthorityOptions options)
|
||||||
{
|
{
|
||||||
options.AdvisoryAi.RemoteInference.Enabled = true;
|
options.AdvisoryAi.RemoteInference.Enabled = true;
|
||||||
options.AdvisoryAi.RemoteInference.RequireTenantConsent = true;
|
options.AdvisoryAi.RemoteInference.RequireTenantConsent = true;
|
||||||
options.AdvisoryAi.RemoteInference.AllowedProfiles.Clear();
|
options.AdvisoryAi.RemoteInference.AllowedProfiles.Clear();
|
||||||
options.AdvisoryAi.RemoteInference.AllowedProfiles.Add("cloud-openai");
|
options.AdvisoryAi.RemoteInference.AllowedProfiles.Add("cloud-openai");
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void SeedTenantConsent(StellaOpsAuthorityOptions options)
|
private static void SeedTenantConsent(StellaOpsAuthorityOptions options)
|
||||||
{
|
{
|
||||||
if (options.Tenants.Count == 0)
|
if (options.Tenants.Count == 0)
|
||||||
{
|
{
|
||||||
options.Tenants.Add(new AuthorityTenantOptions { Id = "tenant-default", DisplayName = "Tenant Default" });
|
options.Tenants.Add(new AuthorityTenantOptions { Id = "tenant-default", DisplayName = "Tenant Default" });
|
||||||
}
|
}
|
||||||
|
|
||||||
var tenant = options.Tenants[0];
|
var tenant = options.Tenants[0];
|
||||||
tenant.Id = "tenant-default";
|
tenant.Id = "tenant-default";
|
||||||
tenant.DisplayName = "Tenant Default";
|
tenant.DisplayName = "Tenant Default";
|
||||||
tenant.AdvisoryAi.RemoteInference.ConsentGranted = true;
|
tenant.AdvisoryAi.RemoteInference.ConsentGranted = true;
|
||||||
tenant.AdvisoryAi.RemoteInference.ConsentVersion = "2025-10";
|
tenant.AdvisoryAi.RemoteInference.ConsentVersion = "2025-10";
|
||||||
tenant.AdvisoryAi.RemoteInference.ConsentedAt = DateTimeOffset.Parse("2025-10-31T12:34:56Z");
|
tenant.AdvisoryAi.RemoteInference.ConsentedAt = DateTimeOffset.Parse("2025-10-31T12:34:56Z");
|
||||||
tenant.AdvisoryAi.RemoteInference.ConsentedBy = "legal@example.com";
|
tenant.AdvisoryAi.RemoteInference.ConsentedBy = "legal@example.com";
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string ComputeSha256(string value)
|
private static string ComputeSha256(string value)
|
||||||
{
|
{
|
||||||
var hash = SHA256.HashData(Encoding.UTF8.GetBytes(value));
|
var hash = SHA256.HashData(Encoding.UTF8.GetBytes(value));
|
||||||
return Convert.ToHexString(hash).ToLowerInvariant();
|
return Convert.ToHexString(hash).ToLowerInvariant();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Dictionary<string, string> ExtractProperties(BsonDocument document)
|
private static Dictionary<string, string> ExtractProperties(BsonDocument document)
|
||||||
{
|
{
|
||||||
var result = new Dictionary<string, string>(StringComparer.Ordinal);
|
var result = new Dictionary<string, string>(StringComparer.Ordinal);
|
||||||
if (!document.TryGetValue("properties", out var propertiesValue))
|
if (!document.TryGetValue("properties", out var propertiesValue))
|
||||||
{
|
{
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var item in propertiesValue.AsBsonArray)
|
foreach (var item in propertiesValue.AsBsonArray)
|
||||||
{
|
{
|
||||||
if (item is not BsonDocument property)
|
if (item is not BsonDocument property)
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
var name = property.TryGetValue("name", out var nameValue) ? nameValue.AsString : null;
|
var name = property.TryGetValue("name", out var nameValue) ? nameValue.AsString : null;
|
||||||
var value = property.TryGetValue("value", out var valueNode) ? valueNode.AsString : null;
|
var value = property.TryGetValue("value", out var valueNode) ? valueNode.AsString : null;
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(name))
|
if (!string.IsNullOrWhiteSpace(name))
|
||||||
{
|
{
|
||||||
result[name] = value ?? string.Empty;
|
result[name] = value ?? string.Empty;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static RemoteInferencePayload CreatePayload(string profile, string prompt = "Summarize remedations.")
|
private static RemoteInferencePayload CreatePayload(string profile, string prompt = "Summarize remedations.")
|
||||||
{
|
{
|
||||||
return new RemoteInferencePayload(
|
return new RemoteInferencePayload(
|
||||||
TaskType: "summary",
|
TaskType: "summary",
|
||||||
Profile: profile,
|
Profile: profile,
|
||||||
ModelId: "gpt-4o-mini",
|
ModelId: "gpt-4o-mini",
|
||||||
Prompt: prompt,
|
Prompt: prompt,
|
||||||
ContextDigest: "sha256:context",
|
ContextDigest: "sha256:context",
|
||||||
OutputHash: "sha256:output",
|
OutputHash: "sha256:output",
|
||||||
TaskId: "task-123",
|
TaskId: "task-123",
|
||||||
Metadata: new Dictionary<string, string>
|
Metadata: new Dictionary<string, string>
|
||||||
{
|
{
|
||||||
["channel"] = "cli",
|
["channel"] = "cli",
|
||||||
["env"] = "test"
|
["env"] = "test"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private sealed record RemoteInferencePayload(
|
private sealed record RemoteInferencePayload(
|
||||||
[property: JsonPropertyName("taskType")] string TaskType,
|
[property: JsonPropertyName("taskType")] string TaskType,
|
||||||
[property: JsonPropertyName("profile")] string Profile,
|
[property: JsonPropertyName("profile")] string Profile,
|
||||||
[property: JsonPropertyName("modelId")] string ModelId,
|
[property: JsonPropertyName("modelId")] string ModelId,
|
||||||
[property: JsonPropertyName("prompt")] string Prompt,
|
[property: JsonPropertyName("prompt")] string Prompt,
|
||||||
[property: JsonPropertyName("contextDigest")] string ContextDigest,
|
[property: JsonPropertyName("contextDigest")] string ContextDigest,
|
||||||
[property: JsonPropertyName("outputHash")] string OutputHash,
|
[property: JsonPropertyName("outputHash")] string OutputHash,
|
||||||
[property: JsonPropertyName("taskId")] string TaskId,
|
[property: JsonPropertyName("taskId")] string TaskId,
|
||||||
[property: JsonPropertyName("metadata")] IDictionary<string, string> Metadata);
|
[property: JsonPropertyName("metadata")] IDictionary<string, string> Metadata);
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,44 +1,44 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
|
||||||
namespace StellaOps.Authority.Tests.Infrastructure;
|
namespace StellaOps.Authority.Tests.Infrastructure;
|
||||||
|
|
||||||
internal sealed class EnvironmentVariableScope : IDisposable
|
internal sealed class EnvironmentVariableScope : IDisposable
|
||||||
{
|
{
|
||||||
private readonly Dictionary<string, string?> originals = new(StringComparer.Ordinal);
|
private readonly Dictionary<string, string?> originals = new(StringComparer.Ordinal);
|
||||||
private bool disposed;
|
private bool disposed;
|
||||||
|
|
||||||
public EnvironmentVariableScope(IEnumerable<KeyValuePair<string, string?>> overrides)
|
public EnvironmentVariableScope(IEnumerable<KeyValuePair<string, string?>> overrides)
|
||||||
{
|
{
|
||||||
if (overrides is null)
|
if (overrides is null)
|
||||||
{
|
{
|
||||||
throw new ArgumentNullException(nameof(overrides));
|
throw new ArgumentNullException(nameof(overrides));
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var kvp in overrides)
|
foreach (var kvp in overrides)
|
||||||
{
|
{
|
||||||
if (originals.ContainsKey(kvp.Key))
|
if (originals.ContainsKey(kvp.Key))
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
originals.Add(kvp.Key, Environment.GetEnvironmentVariable(kvp.Key));
|
originals.Add(kvp.Key, Environment.GetEnvironmentVariable(kvp.Key));
|
||||||
Environment.SetEnvironmentVariable(kvp.Key, kvp.Value);
|
Environment.SetEnvironmentVariable(kvp.Key, kvp.Value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
if (disposed)
|
if (disposed)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var kvp in originals)
|
foreach (var kvp in originals)
|
||||||
{
|
{
|
||||||
Environment.SetEnvironmentVariable(kvp.Key, kvp.Value);
|
Environment.SetEnvironmentVariable(kvp.Key, kvp.Value);
|
||||||
}
|
}
|
||||||
|
|
||||||
disposed = true;
|
disposed = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,57 +1,57 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Security.Claims;
|
using System.Security.Claims;
|
||||||
using System.Text.Encodings.Web;
|
using System.Text.Encodings.Web;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.Authentication;
|
using Microsoft.AspNetCore.Authentication;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using StellaOps.Auth.Abstractions;
|
using StellaOps.Auth.Abstractions;
|
||||||
|
|
||||||
namespace StellaOps.Authority.Tests.Infrastructure;
|
namespace StellaOps.Authority.Tests.Infrastructure;
|
||||||
|
|
||||||
internal sealed class TestAuthHandler : AuthenticationHandler<AuthenticationSchemeOptions>
|
internal sealed class TestAuthHandler : AuthenticationHandler<AuthenticationSchemeOptions>
|
||||||
{
|
{
|
||||||
public const string SchemeName = "TestAuth";
|
public const string SchemeName = "TestAuth";
|
||||||
|
|
||||||
public TestAuthHandler(
|
public TestAuthHandler(
|
||||||
IOptionsMonitor<AuthenticationSchemeOptions> options,
|
IOptionsMonitor<AuthenticationSchemeOptions> options,
|
||||||
ILoggerFactory logger,
|
ILoggerFactory logger,
|
||||||
UrlEncoder encoder)
|
UrlEncoder encoder)
|
||||||
: base(options, logger, encoder)
|
: base(options, logger, encoder)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
|
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
|
||||||
{
|
{
|
||||||
var tenantHeader = Request.Headers.TryGetValue("X-Test-Tenant", out var tenantValues)
|
var tenantHeader = Request.Headers.TryGetValue("X-Test-Tenant", out var tenantValues)
|
||||||
? tenantValues.ToString()
|
? tenantValues.ToString()
|
||||||
: "tenant-default";
|
: "tenant-default";
|
||||||
|
|
||||||
var scopesHeader = Request.Headers.TryGetValue("X-Test-Scopes", out var scopeValues)
|
var scopesHeader = Request.Headers.TryGetValue("X-Test-Scopes", out var scopeValues)
|
||||||
? scopeValues.ToString()
|
? scopeValues.ToString()
|
||||||
: StellaOpsScopes.AdvisoryAiOperate;
|
: StellaOpsScopes.AdvisoryAiOperate;
|
||||||
|
|
||||||
var claims = new List<Claim>
|
var claims = new List<Claim>
|
||||||
{
|
{
|
||||||
new Claim(StellaOpsClaimTypes.ClientId, "test-client")
|
new Claim(StellaOpsClaimTypes.ClientId, "test-client")
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(tenantHeader) &&
|
if (!string.IsNullOrWhiteSpace(tenantHeader) &&
|
||||||
!string.Equals(tenantHeader, "none", StringComparison.OrdinalIgnoreCase))
|
!string.Equals(tenantHeader, "none", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
claims.Add(new Claim(StellaOpsClaimTypes.Tenant, tenantHeader.Trim()));
|
claims.Add(new Claim(StellaOpsClaimTypes.Tenant, tenantHeader.Trim()));
|
||||||
}
|
}
|
||||||
|
|
||||||
var scopes = scopesHeader.Split(' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
var scopes = scopesHeader.Split(' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||||
foreach (var scope in scopes)
|
foreach (var scope in scopes)
|
||||||
{
|
{
|
||||||
claims.Add(new Claim(StellaOpsClaimTypes.ScopeItem, scope));
|
claims.Add(new Claim(StellaOpsClaimTypes.ScopeItem, scope));
|
||||||
}
|
}
|
||||||
|
|
||||||
var identity = new ClaimsIdentity(claims, Scheme.Name);
|
var identity = new ClaimsIdentity(claims, Scheme.Name);
|
||||||
var principal = new ClaimsPrincipal(identity);
|
var principal = new ClaimsPrincipal(identity);
|
||||||
var ticket = new AuthenticationTicket(principal, Scheme.Name);
|
var ticket = new AuthenticationTicket(principal, Scheme.Name);
|
||||||
return Task.FromResult(AuthenticateResult.Success(ticket));
|
return Task.FromResult(AuthenticateResult.Success(ticket));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,259 +1,259 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Net.Http.Headers;
|
using System.Net.Http.Headers;
|
||||||
using System.Net.Http.Json;
|
using System.Net.Http.Json;
|
||||||
using System.Security.Cryptography;
|
using System.Security.Cryptography;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.Authentication;
|
using Microsoft.AspNetCore.Authentication;
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||||
using Microsoft.Extensions.Time.Testing;
|
using Microsoft.Extensions.Time.Testing;
|
||||||
using StellaOps.Auth.Abstractions;
|
using StellaOps.Auth.Abstractions;
|
||||||
using StellaOps.Authority;
|
using StellaOps.Authority;
|
||||||
using StellaOps.Authority.Tests.Infrastructure;
|
using StellaOps.Authority.Tests.Infrastructure;
|
||||||
using StellaOps.Cryptography;
|
using StellaOps.Cryptography;
|
||||||
using StellaOps.Cryptography.Audit;
|
using StellaOps.Cryptography.Audit;
|
||||||
using StellaOps.Configuration;
|
using StellaOps.Configuration;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
namespace StellaOps.Authority.Tests.Notifications;
|
namespace StellaOps.Authority.Tests.Notifications;
|
||||||
|
|
||||||
public sealed class NotifyAckTokenRotationEndpointTests : IClassFixture<AuthorityWebApplicationFactory>
|
public sealed class NotifyAckTokenRotationEndpointTests : IClassFixture<AuthorityWebApplicationFactory>
|
||||||
{
|
{
|
||||||
private readonly AuthorityWebApplicationFactory factory;
|
private readonly AuthorityWebApplicationFactory factory;
|
||||||
|
|
||||||
public NotifyAckTokenRotationEndpointTests(AuthorityWebApplicationFactory factory)
|
public NotifyAckTokenRotationEndpointTests(AuthorityWebApplicationFactory factory)
|
||||||
{
|
{
|
||||||
this.factory = factory ?? throw new ArgumentNullException(nameof(factory));
|
this.factory = factory ?? throw new ArgumentNullException(nameof(factory));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Rotate_ReturnsOk_AndEmitsAuditEvent()
|
public async Task Rotate_ReturnsOk_AndEmitsAuditEvent()
|
||||||
{
|
{
|
||||||
const string AckEnabledKey = "STELLAOPS_AUTHORITY_AUTHORITY__NOTIFICATIONS__ACKTOKENS__ENABLED";
|
const string AckEnabledKey = "STELLAOPS_AUTHORITY_AUTHORITY__NOTIFICATIONS__ACKTOKENS__ENABLED";
|
||||||
const string AckActiveKeyIdKey = "STELLAOPS_AUTHORITY_AUTHORITY__NOTIFICATIONS__ACKTOKENS__ACTIVEKEYID";
|
const string AckActiveKeyIdKey = "STELLAOPS_AUTHORITY_AUTHORITY__NOTIFICATIONS__ACKTOKENS__ACTIVEKEYID";
|
||||||
const string AckKeyPathKey = "STELLAOPS_AUTHORITY_AUTHORITY__NOTIFICATIONS__ACKTOKENS__KEYPATH";
|
const string AckKeyPathKey = "STELLAOPS_AUTHORITY_AUTHORITY__NOTIFICATIONS__ACKTOKENS__KEYPATH";
|
||||||
const string AckKeySourceKey = "STELLAOPS_AUTHORITY_AUTHORITY__NOTIFICATIONS__ACKTOKENS__KEYSOURCE";
|
const string AckKeySourceKey = "STELLAOPS_AUTHORITY_AUTHORITY__NOTIFICATIONS__ACKTOKENS__KEYSOURCE";
|
||||||
const string AckAlgorithmKey = "STELLAOPS_AUTHORITY_AUTHORITY__NOTIFICATIONS__ACKTOKENS__ALGORITHM";
|
const string AckAlgorithmKey = "STELLAOPS_AUTHORITY_AUTHORITY__NOTIFICATIONS__ACKTOKENS__ALGORITHM";
|
||||||
const string WebhooksEnabledKey = "STELLAOPS_AUTHORITY_AUTHORITY__NOTIFICATIONS__WEBHOOKS__ENABLED";
|
const string WebhooksEnabledKey = "STELLAOPS_AUTHORITY_AUTHORITY__NOTIFICATIONS__WEBHOOKS__ENABLED";
|
||||||
const string WebhooksAllowedHost0Key = "STELLAOPS_AUTHORITY_AUTHORITY__NOTIFICATIONS__WEBHOOKS__ALLOWEDHOSTS__0";
|
const string WebhooksAllowedHost0Key = "STELLAOPS_AUTHORITY_AUTHORITY__NOTIFICATIONS__WEBHOOKS__ALLOWEDHOSTS__0";
|
||||||
|
|
||||||
var tempDir = Directory.CreateTempSubdirectory("ack-rotation-success");
|
var tempDir = Directory.CreateTempSubdirectory("ack-rotation-success");
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var key1Path = Path.Combine(tempDir.FullName, "ack-key-1.pem");
|
var key1Path = Path.Combine(tempDir.FullName, "ack-key-1.pem");
|
||||||
var key2Path = Path.Combine(tempDir.FullName, "ack-key-2.pem");
|
var key2Path = Path.Combine(tempDir.FullName, "ack-key-2.pem");
|
||||||
CreateEcPrivateKey(key1Path);
|
CreateEcPrivateKey(key1Path);
|
||||||
CreateEcPrivateKey(key2Path);
|
CreateEcPrivateKey(key2Path);
|
||||||
|
|
||||||
using var env = new EnvironmentVariableScope(new[]
|
using var env = new EnvironmentVariableScope(new[]
|
||||||
{
|
{
|
||||||
new KeyValuePair<string, string?>(AckEnabledKey, "true"),
|
new KeyValuePair<string, string?>(AckEnabledKey, "true"),
|
||||||
new KeyValuePair<string, string?>(AckActiveKeyIdKey, "ack-key-1"),
|
new KeyValuePair<string, string?>(AckActiveKeyIdKey, "ack-key-1"),
|
||||||
new KeyValuePair<string, string?>(AckKeyPathKey, key1Path),
|
new KeyValuePair<string, string?>(AckKeyPathKey, key1Path),
|
||||||
new KeyValuePair<string, string?>(AckKeySourceKey, "file"),
|
new KeyValuePair<string, string?>(AckKeySourceKey, "file"),
|
||||||
new KeyValuePair<string, string?>(AckAlgorithmKey, SignatureAlgorithms.Es256),
|
new KeyValuePair<string, string?>(AckAlgorithmKey, SignatureAlgorithms.Es256),
|
||||||
new KeyValuePair<string, string?>(WebhooksEnabledKey, "true"),
|
new KeyValuePair<string, string?>(WebhooksEnabledKey, "true"),
|
||||||
new KeyValuePair<string, string?>(WebhooksAllowedHost0Key, "hooks.slack.com")
|
new KeyValuePair<string, string?>(WebhooksAllowedHost0Key, "hooks.slack.com")
|
||||||
});
|
});
|
||||||
|
|
||||||
var sink = new RecordingAuthEventSink();
|
var sink = new RecordingAuthEventSink();
|
||||||
var timeProvider = new FakeTimeProvider(DateTimeOffset.Parse("2025-11-02T12:00:00Z"));
|
var timeProvider = new FakeTimeProvider(DateTimeOffset.Parse("2025-11-02T12:00:00Z"));
|
||||||
|
|
||||||
using var scopedFactory = factory.WithWebHostBuilder(host =>
|
using var scopedFactory = factory.WithWebHostBuilder(host =>
|
||||||
{
|
{
|
||||||
host.ConfigureAppConfiguration((_, configuration) =>
|
host.ConfigureAppConfiguration((_, configuration) =>
|
||||||
{
|
{
|
||||||
configuration.AddInMemoryCollection(new Dictionary<string, string?>
|
configuration.AddInMemoryCollection(new Dictionary<string, string?>
|
||||||
{
|
{
|
||||||
["Authority:Notifications:AckTokens:Enabled"] = "true",
|
["Authority:Notifications:AckTokens:Enabled"] = "true",
|
||||||
["Authority:Notifications:AckTokens:ActiveKeyId"] = "ack-key-1",
|
["Authority:Notifications:AckTokens:ActiveKeyId"] = "ack-key-1",
|
||||||
["Authority:Notifications:AckTokens:KeyPath"] = key1Path,
|
["Authority:Notifications:AckTokens:KeyPath"] = key1Path,
|
||||||
["Authority:Notifications:AckTokens:KeySource"] = "file",
|
["Authority:Notifications:AckTokens:KeySource"] = "file",
|
||||||
["Authority:Notifications:AckTokens:Algorithm"] = SignatureAlgorithms.Es256,
|
["Authority:Notifications:AckTokens:Algorithm"] = SignatureAlgorithms.Es256,
|
||||||
["Authority:Notifications:Webhooks:Enabled"] = "true",
|
["Authority:Notifications:Webhooks:Enabled"] = "true",
|
||||||
["Authority:Notifications:Webhooks:AllowedHosts:0"] = "hooks.slack.com",
|
["Authority:Notifications:Webhooks:AllowedHosts:0"] = "hooks.slack.com",
|
||||||
["Authority:Notifications:Escalation:Scope"] = "notify.escalate",
|
["Authority:Notifications:Escalation:Scope"] = "notify.escalate",
|
||||||
["Authority:Notifications:Escalation:RequireAdminScope"] = "true"
|
["Authority:Notifications:Escalation:RequireAdminScope"] = "true"
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
host.ConfigureServices(services =>
|
host.ConfigureServices(services =>
|
||||||
{
|
{
|
||||||
services.RemoveAll<IAuthEventSink>();
|
services.RemoveAll<IAuthEventSink>();
|
||||||
services.AddSingleton<IAuthEventSink>(sink);
|
services.AddSingleton<IAuthEventSink>(sink);
|
||||||
services.Replace(ServiceDescriptor.Singleton<TimeProvider>(timeProvider));
|
services.Replace(ServiceDescriptor.Singleton<TimeProvider>(timeProvider));
|
||||||
services.PostConfigure<StellaOpsAuthorityOptions>(options =>
|
services.PostConfigure<StellaOpsAuthorityOptions>(options =>
|
||||||
{
|
{
|
||||||
options.Notifications.AckTokens.Enabled = true;
|
options.Notifications.AckTokens.Enabled = true;
|
||||||
options.Notifications.AckTokens.ActiveKeyId = "ack-key-1";
|
options.Notifications.AckTokens.ActiveKeyId = "ack-key-1";
|
||||||
options.Notifications.AckTokens.KeyPath = key1Path;
|
options.Notifications.AckTokens.KeyPath = key1Path;
|
||||||
options.Notifications.AckTokens.KeySource = "file";
|
options.Notifications.AckTokens.KeySource = "file";
|
||||||
options.Notifications.AckTokens.Algorithm = SignatureAlgorithms.Es256;
|
options.Notifications.AckTokens.Algorithm = SignatureAlgorithms.Es256;
|
||||||
});
|
});
|
||||||
var authBuilder = services.AddAuthentication(options =>
|
var authBuilder = services.AddAuthentication(options =>
|
||||||
{
|
{
|
||||||
options.DefaultAuthenticateScheme = TestAuthHandler.SchemeName;
|
options.DefaultAuthenticateScheme = TestAuthHandler.SchemeName;
|
||||||
options.DefaultChallengeScheme = TestAuthHandler.SchemeName;
|
options.DefaultChallengeScheme = TestAuthHandler.SchemeName;
|
||||||
});
|
});
|
||||||
authBuilder.AddScheme<AuthenticationSchemeOptions, TestAuthHandler>(TestAuthHandler.SchemeName, _ => { });
|
authBuilder.AddScheme<AuthenticationSchemeOptions, TestAuthHandler>(TestAuthHandler.SchemeName, _ => { });
|
||||||
authBuilder.AddScheme<AuthenticationSchemeOptions, TestAuthHandler>(StellaOpsAuthenticationDefaults.AuthenticationScheme, _ => { });
|
authBuilder.AddScheme<AuthenticationSchemeOptions, TestAuthHandler>(StellaOpsAuthenticationDefaults.AuthenticationScheme, _ => { });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
using var client = scopedFactory.CreateClient();
|
using var client = scopedFactory.CreateClient();
|
||||||
|
|
||||||
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(TestAuthHandler.SchemeName);
|
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(TestAuthHandler.SchemeName);
|
||||||
client.DefaultRequestHeaders.Add("X-Test-Scopes", StellaOpsScopes.NotifyAdmin);
|
client.DefaultRequestHeaders.Add("X-Test-Scopes", StellaOpsScopes.NotifyAdmin);
|
||||||
client.DefaultRequestHeaders.Add("X-Test-Tenant", "tenant-default");
|
client.DefaultRequestHeaders.Add("X-Test-Tenant", "tenant-default");
|
||||||
client.DefaultRequestHeaders.Add(AuthorityHttpHeaders.Tenant, "tenant-default");
|
client.DefaultRequestHeaders.Add(AuthorityHttpHeaders.Tenant, "tenant-default");
|
||||||
|
|
||||||
var response = await client.PostAsJsonAsync("/notify/ack-tokens/rotate", new
|
var response = await client.PostAsJsonAsync("/notify/ack-tokens/rotate", new
|
||||||
{
|
{
|
||||||
keyId = "ack-key-2",
|
keyId = "ack-key-2",
|
||||||
location = key2Path
|
location = key2Path
|
||||||
});
|
});
|
||||||
|
|
||||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||||
|
|
||||||
var payload = await response.Content.ReadFromJsonAsync<AckRotateResponse>();
|
var payload = await response.Content.ReadFromJsonAsync<AckRotateResponse>();
|
||||||
Assert.NotNull(payload);
|
Assert.NotNull(payload);
|
||||||
Assert.Equal("ack-key-2", payload!.ActiveKeyId);
|
Assert.Equal("ack-key-2", payload!.ActiveKeyId);
|
||||||
Assert.Equal("ack-key-1", payload.PreviousKeyId);
|
Assert.Equal("ack-key-1", payload.PreviousKeyId);
|
||||||
|
|
||||||
var rotationEvent = Assert.Single(sink.Events, evt => evt.EventType == "notify.ack.key_rotated");
|
var rotationEvent = Assert.Single(sink.Events, evt => evt.EventType == "notify.ack.key_rotated");
|
||||||
Assert.Equal(AuthEventOutcome.Success, rotationEvent.Outcome);
|
Assert.Equal(AuthEventOutcome.Success, rotationEvent.Outcome);
|
||||||
Assert.Contains(rotationEvent.Properties, property =>
|
Assert.Contains(rotationEvent.Properties, property =>
|
||||||
string.Equals(property.Name, "notify.ack.key_id", StringComparison.Ordinal) &&
|
string.Equals(property.Name, "notify.ack.key_id", StringComparison.Ordinal) &&
|
||||||
string.Equals(property.Value.Value, "ack-key-2", StringComparison.Ordinal));
|
string.Equals(property.Value.Value, "ack-key-2", StringComparison.Ordinal));
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
TryDeleteDirectory(tempDir.FullName);
|
TryDeleteDirectory(tempDir.FullName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Rotate_ReturnsBadRequest_WhenKeyIdMissing_AndAuditsFailure()
|
public async Task Rotate_ReturnsBadRequest_WhenKeyIdMissing_AndAuditsFailure()
|
||||||
{
|
{
|
||||||
var tempDir = Directory.CreateTempSubdirectory("ack-rotation-failure");
|
var tempDir = Directory.CreateTempSubdirectory("ack-rotation-failure");
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var key1Path = Path.Combine(tempDir.FullName, "ack-key-1.pem");
|
var key1Path = Path.Combine(tempDir.FullName, "ack-key-1.pem");
|
||||||
var key2Path = Path.Combine(tempDir.FullName, "ack-key-2.pem");
|
var key2Path = Path.Combine(tempDir.FullName, "ack-key-2.pem");
|
||||||
CreateEcPrivateKey(key1Path);
|
CreateEcPrivateKey(key1Path);
|
||||||
CreateEcPrivateKey(key2Path);
|
CreateEcPrivateKey(key2Path);
|
||||||
|
|
||||||
var sink = new RecordingAuthEventSink();
|
var sink = new RecordingAuthEventSink();
|
||||||
var timeProvider = new FakeTimeProvider(DateTimeOffset.Parse("2025-11-02T13:00:00Z"));
|
var timeProvider = new FakeTimeProvider(DateTimeOffset.Parse("2025-11-02T13:00:00Z"));
|
||||||
|
|
||||||
using var app = factory.WithWebHostBuilder(host =>
|
using var app = factory.WithWebHostBuilder(host =>
|
||||||
{
|
{
|
||||||
host.ConfigureAppConfiguration((_, configuration) =>
|
host.ConfigureAppConfiguration((_, configuration) =>
|
||||||
{
|
{
|
||||||
configuration.AddInMemoryCollection(new Dictionary<string, string?>
|
configuration.AddInMemoryCollection(new Dictionary<string, string?>
|
||||||
{
|
{
|
||||||
["Authority:Notifications:AckTokens:Enabled"] = "true",
|
["Authority:Notifications:AckTokens:Enabled"] = "true",
|
||||||
["Authority:Notifications:AckTokens:ActiveKeyId"] = "ack-key-1",
|
["Authority:Notifications:AckTokens:ActiveKeyId"] = "ack-key-1",
|
||||||
["Authority:Notifications:AckTokens:KeyPath"] = key1Path,
|
["Authority:Notifications:AckTokens:KeyPath"] = key1Path,
|
||||||
["Authority:Notifications:AckTokens:KeySource"] = "file",
|
["Authority:Notifications:AckTokens:KeySource"] = "file",
|
||||||
["Authority:Notifications:AckTokens:Algorithm"] = SignatureAlgorithms.Es256,
|
["Authority:Notifications:AckTokens:Algorithm"] = SignatureAlgorithms.Es256,
|
||||||
["Authority:Notifications:Webhooks:Enabled"] = "true",
|
["Authority:Notifications:Webhooks:Enabled"] = "true",
|
||||||
["Authority:Notifications:Webhooks:AllowedHosts:0"] = "hooks.slack.com"
|
["Authority:Notifications:Webhooks:AllowedHosts:0"] = "hooks.slack.com"
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
host.ConfigureServices(services =>
|
host.ConfigureServices(services =>
|
||||||
{
|
{
|
||||||
services.RemoveAll<IAuthEventSink>();
|
services.RemoveAll<IAuthEventSink>();
|
||||||
services.AddSingleton<IAuthEventSink>(sink);
|
services.AddSingleton<IAuthEventSink>(sink);
|
||||||
services.Replace(ServiceDescriptor.Singleton<TimeProvider>(timeProvider));
|
services.Replace(ServiceDescriptor.Singleton<TimeProvider>(timeProvider));
|
||||||
services.PostConfigure<StellaOpsAuthorityOptions>(options =>
|
services.PostConfigure<StellaOpsAuthorityOptions>(options =>
|
||||||
{
|
{
|
||||||
options.Notifications.AckTokens.Enabled = true;
|
options.Notifications.AckTokens.Enabled = true;
|
||||||
options.Notifications.AckTokens.ActiveKeyId = "ack-key-1";
|
options.Notifications.AckTokens.ActiveKeyId = "ack-key-1";
|
||||||
options.Notifications.AckTokens.KeyPath = key1Path;
|
options.Notifications.AckTokens.KeyPath = key1Path;
|
||||||
options.Notifications.AckTokens.KeySource = "file";
|
options.Notifications.AckTokens.KeySource = "file";
|
||||||
options.Notifications.AckTokens.Algorithm = SignatureAlgorithms.Es256;
|
options.Notifications.AckTokens.Algorithm = SignatureAlgorithms.Es256;
|
||||||
});
|
});
|
||||||
var authBuilder = services.AddAuthentication(options =>
|
var authBuilder = services.AddAuthentication(options =>
|
||||||
{
|
{
|
||||||
options.DefaultAuthenticateScheme = TestAuthHandler.SchemeName;
|
options.DefaultAuthenticateScheme = TestAuthHandler.SchemeName;
|
||||||
options.DefaultChallengeScheme = TestAuthHandler.SchemeName;
|
options.DefaultChallengeScheme = TestAuthHandler.SchemeName;
|
||||||
});
|
});
|
||||||
authBuilder.AddScheme<AuthenticationSchemeOptions, TestAuthHandler>(TestAuthHandler.SchemeName, _ => { });
|
authBuilder.AddScheme<AuthenticationSchemeOptions, TestAuthHandler>(TestAuthHandler.SchemeName, _ => { });
|
||||||
authBuilder.AddScheme<AuthenticationSchemeOptions, TestAuthHandler>(StellaOpsAuthenticationDefaults.AuthenticationScheme, _ => { });
|
authBuilder.AddScheme<AuthenticationSchemeOptions, TestAuthHandler>(StellaOpsAuthenticationDefaults.AuthenticationScheme, _ => { });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
using var client = app.CreateClient();
|
using var client = app.CreateClient();
|
||||||
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(TestAuthHandler.SchemeName);
|
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(TestAuthHandler.SchemeName);
|
||||||
client.DefaultRequestHeaders.Add("X-Test-Scopes", StellaOpsScopes.NotifyAdmin);
|
client.DefaultRequestHeaders.Add("X-Test-Scopes", StellaOpsScopes.NotifyAdmin);
|
||||||
client.DefaultRequestHeaders.Add("X-Test-Tenant", "tenant-default");
|
client.DefaultRequestHeaders.Add("X-Test-Tenant", "tenant-default");
|
||||||
client.DefaultRequestHeaders.Add(AuthorityHttpHeaders.Tenant, "tenant-default");
|
client.DefaultRequestHeaders.Add(AuthorityHttpHeaders.Tenant, "tenant-default");
|
||||||
|
|
||||||
var response = await client.PostAsJsonAsync("/notify/ack-tokens/rotate", new
|
var response = await client.PostAsJsonAsync("/notify/ack-tokens/rotate", new
|
||||||
{
|
{
|
||||||
location = key2Path
|
location = key2Path
|
||||||
});
|
});
|
||||||
|
|
||||||
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
|
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
|
||||||
|
|
||||||
var failureEvent = Assert.Single(sink.Events, evt => evt.EventType == "notify.ack.key_rotation_failed");
|
var failureEvent = Assert.Single(sink.Events, evt => evt.EventType == "notify.ack.key_rotation_failed");
|
||||||
Assert.Equal(AuthEventOutcome.Failure, failureEvent.Outcome);
|
Assert.Equal(AuthEventOutcome.Failure, failureEvent.Outcome);
|
||||||
Assert.Contains("keyId", failureEvent.Reason, StringComparison.OrdinalIgnoreCase);
|
Assert.Contains("keyId", failureEvent.Reason, StringComparison.OrdinalIgnoreCase);
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
TryDeleteDirectory(tempDir.FullName);
|
TryDeleteDirectory(tempDir.FullName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void CreateEcPrivateKey(string path)
|
private static void CreateEcPrivateKey(string path)
|
||||||
{
|
{
|
||||||
Directory.CreateDirectory(Path.GetDirectoryName(path)!);
|
Directory.CreateDirectory(Path.GetDirectoryName(path)!);
|
||||||
using var ecdsa = ECDsa.Create(ECCurve.NamedCurves.nistP256);
|
using var ecdsa = ECDsa.Create(ECCurve.NamedCurves.nistP256);
|
||||||
File.WriteAllText(path, ecdsa.ExportECPrivateKeyPem());
|
File.WriteAllText(path, ecdsa.ExportECPrivateKeyPem());
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void TryDeleteDirectory(string path)
|
private static void TryDeleteDirectory(string path)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (Directory.Exists(path))
|
if (Directory.Exists(path))
|
||||||
{
|
{
|
||||||
Directory.Delete(path, recursive: true);
|
Directory.Delete(path, recursive: true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
// Ignore cleanup failures in tests.
|
// Ignore cleanup failures in tests.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private sealed record AckRotateResponse(
|
private sealed record AckRotateResponse(
|
||||||
string ActiveKeyId,
|
string ActiveKeyId,
|
||||||
string? Provider,
|
string? Provider,
|
||||||
string? Source,
|
string? Source,
|
||||||
string? Location,
|
string? Location,
|
||||||
string? PreviousKeyId,
|
string? PreviousKeyId,
|
||||||
IReadOnlyCollection<string> RetiredKeyIds);
|
IReadOnlyCollection<string> RetiredKeyIds);
|
||||||
|
|
||||||
private sealed class RecordingAuthEventSink : IAuthEventSink
|
private sealed class RecordingAuthEventSink : IAuthEventSink
|
||||||
{
|
{
|
||||||
private readonly ConcurrentQueue<AuthEventRecord> events = new();
|
private readonly ConcurrentQueue<AuthEventRecord> events = new();
|
||||||
|
|
||||||
public IReadOnlyCollection<AuthEventRecord> Events => events.ToArray();
|
public IReadOnlyCollection<AuthEventRecord> Events => events.ToArray();
|
||||||
|
|
||||||
public ValueTask WriteAsync(AuthEventRecord record, CancellationToken cancellationToken)
|
public ValueTask WriteAsync(AuthEventRecord record, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
events.Enqueue(record);
|
events.Enqueue(record);
|
||||||
return ValueTask.CompletedTask;
|
return ValueTask.CompletedTask;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,48 +1,48 @@
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using StellaOps.Authority.Tests.Infrastructure;
|
using StellaOps.Authority.Tests.Infrastructure;
|
||||||
using StellaOps.Auth.Abstractions;
|
using StellaOps.Auth.Abstractions;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
namespace StellaOps.Authority.Tests.OpenIddict;
|
namespace StellaOps.Authority.Tests.OpenIddict;
|
||||||
|
|
||||||
public sealed class DiscoveryMetadataTests : IClassFixture<AuthorityWebApplicationFactory>
|
public sealed class DiscoveryMetadataTests : IClassFixture<AuthorityWebApplicationFactory>
|
||||||
{
|
{
|
||||||
private readonly AuthorityWebApplicationFactory factory;
|
private readonly AuthorityWebApplicationFactory factory;
|
||||||
|
|
||||||
public DiscoveryMetadataTests(AuthorityWebApplicationFactory factory)
|
public DiscoveryMetadataTests(AuthorityWebApplicationFactory factory)
|
||||||
{
|
{
|
||||||
this.factory = factory;
|
this.factory = factory;
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task OpenIdDiscovery_IncludesAdvisoryAiMetadata()
|
public async Task OpenIdDiscovery_IncludesAdvisoryAiMetadata()
|
||||||
{
|
{
|
||||||
using var client = factory.CreateClient();
|
using var client = factory.CreateClient();
|
||||||
|
|
||||||
using var response = await client.GetAsync("/.well-known/openid-configuration");
|
using var response = await client.GetAsync("/.well-known/openid-configuration");
|
||||||
|
|
||||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||||
|
|
||||||
var payload = await response.Content.ReadAsStringAsync();
|
var payload = await response.Content.ReadAsStringAsync();
|
||||||
using var document = JsonDocument.Parse(payload);
|
using var document = JsonDocument.Parse(payload);
|
||||||
|
|
||||||
var root = document.RootElement;
|
var root = document.RootElement;
|
||||||
Assert.True(root.TryGetProperty("stellaops_advisory_ai_scopes_supported", out var scopesNode));
|
Assert.True(root.TryGetProperty("stellaops_advisory_ai_scopes_supported", out var scopesNode));
|
||||||
|
|
||||||
var scopes = scopesNode.EnumerateArray().Select(element => element.GetString()).ToArray();
|
var scopes = scopesNode.EnumerateArray().Select(element => element.GetString()).ToArray();
|
||||||
Assert.Contains(StellaOpsScopes.AdvisoryAiView, scopes);
|
Assert.Contains(StellaOpsScopes.AdvisoryAiView, scopes);
|
||||||
Assert.Contains(StellaOpsScopes.AdvisoryAiOperate, scopes);
|
Assert.Contains(StellaOpsScopes.AdvisoryAiOperate, scopes);
|
||||||
Assert.Contains(StellaOpsScopes.AdvisoryAiAdmin, scopes);
|
Assert.Contains(StellaOpsScopes.AdvisoryAiAdmin, scopes);
|
||||||
|
|
||||||
Assert.True(root.TryGetProperty("stellaops_advisory_ai_remote_inference", out var remoteNode));
|
Assert.True(root.TryGetProperty("stellaops_advisory_ai_remote_inference", out var remoteNode));
|
||||||
Assert.False(remoteNode.GetProperty("enabled").GetBoolean());
|
Assert.False(remoteNode.GetProperty("enabled").GetBoolean());
|
||||||
Assert.True(remoteNode.GetProperty("require_tenant_consent").GetBoolean());
|
Assert.True(remoteNode.GetProperty("require_tenant_consent").GetBoolean());
|
||||||
|
|
||||||
var profiles = remoteNode.GetProperty("allowed_profiles").EnumerateArray().ToArray();
|
var profiles = remoteNode.GetProperty("allowed_profiles").EnumerateArray().ToArray();
|
||||||
Assert.Empty(profiles);
|
Assert.Empty(profiles);
|
||||||
|
|
||||||
Assert.True(root.TryGetProperty("stellaops_airgap_scopes_supported", out var airgapNode));
|
Assert.True(root.TryGetProperty("stellaops_airgap_scopes_supported", out var airgapNode));
|
||||||
var airgapScopes = airgapNode.EnumerateArray().Select(element => element.GetString()).ToArray();
|
var airgapScopes = airgapNode.EnumerateArray().Select(element => element.GetString()).ToArray();
|
||||||
Assert.Contains(StellaOpsScopes.AirgapSeal, airgapScopes);
|
Assert.Contains(StellaOpsScopes.AirgapSeal, airgapScopes);
|
||||||
@@ -61,10 +61,10 @@ public sealed class DiscoveryMetadataTests : IClassFixture<AuthorityWebApplicati
|
|||||||
Assert.Contains(StellaOpsScopes.ObservabilityRead, observabilityScopes);
|
Assert.Contains(StellaOpsScopes.ObservabilityRead, observabilityScopes);
|
||||||
Assert.Contains(StellaOpsScopes.TimelineRead, observabilityScopes);
|
Assert.Contains(StellaOpsScopes.TimelineRead, observabilityScopes);
|
||||||
Assert.Contains(StellaOpsScopes.TimelineWrite, observabilityScopes);
|
Assert.Contains(StellaOpsScopes.TimelineWrite, observabilityScopes);
|
||||||
Assert.Contains(StellaOpsScopes.EvidenceCreate, observabilityScopes);
|
Assert.Contains(StellaOpsScopes.EvidenceCreate, observabilityScopes);
|
||||||
Assert.Contains(StellaOpsScopes.EvidenceRead, observabilityScopes);
|
Assert.Contains(StellaOpsScopes.EvidenceRead, observabilityScopes);
|
||||||
Assert.Contains(StellaOpsScopes.EvidenceHold, observabilityScopes);
|
Assert.Contains(StellaOpsScopes.EvidenceHold, observabilityScopes);
|
||||||
Assert.Contains(StellaOpsScopes.AttestRead, observabilityScopes);
|
Assert.Contains(StellaOpsScopes.AttestRead, observabilityScopes);
|
||||||
Assert.Contains(StellaOpsScopes.ObservabilityIncident, observabilityScopes);
|
Assert.Contains(StellaOpsScopes.ObservabilityIncident, observabilityScopes);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,112 +1,112 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||||
using StellaOps.Authority.Tests.Infrastructure;
|
using StellaOps.Authority.Tests.Infrastructure;
|
||||||
using StellaOps.Cryptography.Audit;
|
using StellaOps.Cryptography.Audit;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
namespace StellaOps.Authority.Tests.OpenIddict;
|
namespace StellaOps.Authority.Tests.OpenIddict;
|
||||||
|
|
||||||
public sealed class LegacyAuthDeprecationTests : IClassFixture<AuthorityWebApplicationFactory>
|
public sealed class LegacyAuthDeprecationTests : IClassFixture<AuthorityWebApplicationFactory>
|
||||||
{
|
{
|
||||||
private static readonly string ExpectedDeprecationHeader = new DateTimeOffset(2025, 11, 1, 0, 0, 0, TimeSpan.Zero)
|
private static readonly string ExpectedDeprecationHeader = new DateTimeOffset(2025, 11, 1, 0, 0, 0, TimeSpan.Zero)
|
||||||
.UtcDateTime.ToString("r", CultureInfo.InvariantCulture);
|
.UtcDateTime.ToString("r", CultureInfo.InvariantCulture);
|
||||||
|
|
||||||
private static readonly string ExpectedSunsetHeader = new DateTimeOffset(2026, 5, 1, 0, 0, 0, TimeSpan.Zero)
|
private static readonly string ExpectedSunsetHeader = new DateTimeOffset(2026, 5, 1, 0, 0, 0, TimeSpan.Zero)
|
||||||
.UtcDateTime.ToString("r", CultureInfo.InvariantCulture);
|
.UtcDateTime.ToString("r", CultureInfo.InvariantCulture);
|
||||||
|
|
||||||
private static readonly string ExpectedSunsetIso = new DateTimeOffset(2026, 5, 1, 0, 0, 0, TimeSpan.Zero)
|
private static readonly string ExpectedSunsetIso = new DateTimeOffset(2026, 5, 1, 0, 0, 0, TimeSpan.Zero)
|
||||||
.ToString("O", CultureInfo.InvariantCulture);
|
.ToString("O", CultureInfo.InvariantCulture);
|
||||||
|
|
||||||
private readonly AuthorityWebApplicationFactory factory;
|
private readonly AuthorityWebApplicationFactory factory;
|
||||||
|
|
||||||
public LegacyAuthDeprecationTests(AuthorityWebApplicationFactory factory)
|
public LegacyAuthDeprecationTests(AuthorityWebApplicationFactory factory)
|
||||||
=> this.factory = factory ?? throw new ArgumentNullException(nameof(factory));
|
=> this.factory = factory ?? throw new ArgumentNullException(nameof(factory));
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task LegacyTokenEndpoint_IncludesDeprecationHeaders()
|
public async Task LegacyTokenEndpoint_IncludesDeprecationHeaders()
|
||||||
{
|
{
|
||||||
using var client = factory.CreateClient();
|
using var client = factory.CreateClient();
|
||||||
|
|
||||||
using var response = await client.PostAsync(
|
using var response = await client.PostAsync(
|
||||||
"/oauth/token",
|
"/oauth/token",
|
||||||
new FormUrlEncodedContent(new Dictionary<string, string>
|
new FormUrlEncodedContent(new Dictionary<string, string>
|
||||||
{
|
{
|
||||||
["grant_type"] = "client_credentials"
|
["grant_type"] = "client_credentials"
|
||||||
}));
|
}));
|
||||||
|
|
||||||
Assert.NotNull(response);
|
Assert.NotNull(response);
|
||||||
Assert.True(response.Headers.TryGetValues("Deprecation", out var deprecationValues));
|
Assert.True(response.Headers.TryGetValues("Deprecation", out var deprecationValues));
|
||||||
Assert.Contains(ExpectedDeprecationHeader, deprecationValues);
|
Assert.Contains(ExpectedDeprecationHeader, deprecationValues);
|
||||||
|
|
||||||
Assert.True(response.Headers.TryGetValues("Sunset", out var sunsetValues));
|
Assert.True(response.Headers.TryGetValues("Sunset", out var sunsetValues));
|
||||||
Assert.Contains(ExpectedSunsetHeader, sunsetValues);
|
Assert.Contains(ExpectedSunsetHeader, sunsetValues);
|
||||||
|
|
||||||
Assert.True(response.Headers.TryGetValues("Warning", out var warningValues));
|
Assert.True(response.Headers.TryGetValues("Warning", out var warningValues));
|
||||||
Assert.Contains(warningValues, warning => warning.Contains("Legacy Authority endpoint", StringComparison.OrdinalIgnoreCase));
|
Assert.Contains(warningValues, warning => warning.Contains("Legacy Authority endpoint", StringComparison.OrdinalIgnoreCase));
|
||||||
|
|
||||||
Assert.True(response.Headers.TryGetValues("Link", out var linkValues));
|
Assert.True(response.Headers.TryGetValues("Link", out var linkValues));
|
||||||
Assert.Contains(linkValues, value => value.Contains("rel=\"sunset\"", StringComparison.OrdinalIgnoreCase));
|
Assert.Contains(linkValues, value => value.Contains("rel=\"sunset\"", StringComparison.OrdinalIgnoreCase));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task LegacyTokenEndpoint_EmitsAuditEvent()
|
public async Task LegacyTokenEndpoint_EmitsAuditEvent()
|
||||||
{
|
{
|
||||||
var sink = new RecordingAuthEventSink();
|
var sink = new RecordingAuthEventSink();
|
||||||
|
|
||||||
using var customFactory = factory.WithWebHostBuilder(builder =>
|
using var customFactory = factory.WithWebHostBuilder(builder =>
|
||||||
{
|
{
|
||||||
builder.ConfigureServices(services =>
|
builder.ConfigureServices(services =>
|
||||||
{
|
{
|
||||||
services.RemoveAll<IAuthEventSink>();
|
services.RemoveAll<IAuthEventSink>();
|
||||||
services.AddSingleton<IAuthEventSink>(sink);
|
services.AddSingleton<IAuthEventSink>(sink);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
using var client = customFactory.CreateClient();
|
using var client = customFactory.CreateClient();
|
||||||
|
|
||||||
using var response = await client.PostAsync(
|
using var response = await client.PostAsync(
|
||||||
"/oauth/token",
|
"/oauth/token",
|
||||||
new FormUrlEncodedContent(new Dictionary<string, string>
|
new FormUrlEncodedContent(new Dictionary<string, string>
|
||||||
{
|
{
|
||||||
["grant_type"] = "client_credentials"
|
["grant_type"] = "client_credentials"
|
||||||
}));
|
}));
|
||||||
|
|
||||||
Assert.NotNull(response);
|
Assert.NotNull(response);
|
||||||
|
|
||||||
var record = Assert.Single(sink.Events);
|
var record = Assert.Single(sink.Events);
|
||||||
Assert.Equal("authority.api.legacy_endpoint", record.EventType);
|
Assert.Equal("authority.api.legacy_endpoint", record.EventType);
|
||||||
|
|
||||||
Assert.Contains(record.Properties, property =>
|
Assert.Contains(record.Properties, property =>
|
||||||
string.Equals(property.Name, "legacy.endpoint.original", StringComparison.Ordinal) &&
|
string.Equals(property.Name, "legacy.endpoint.original", StringComparison.Ordinal) &&
|
||||||
string.Equals(property.Value.Value, "/oauth/token", StringComparison.Ordinal));
|
string.Equals(property.Value.Value, "/oauth/token", StringComparison.Ordinal));
|
||||||
|
|
||||||
Assert.Contains(record.Properties, property =>
|
Assert.Contains(record.Properties, property =>
|
||||||
string.Equals(property.Name, "legacy.endpoint.canonical", StringComparison.Ordinal) &&
|
string.Equals(property.Name, "legacy.endpoint.canonical", StringComparison.Ordinal) &&
|
||||||
string.Equals(property.Value.Value, "/token", StringComparison.Ordinal));
|
string.Equals(property.Value.Value, "/token", StringComparison.Ordinal));
|
||||||
|
|
||||||
Assert.Contains(record.Properties, property =>
|
Assert.Contains(record.Properties, property =>
|
||||||
string.Equals(property.Name, "legacy.sunset_at", StringComparison.Ordinal) &&
|
string.Equals(property.Name, "legacy.sunset_at", StringComparison.Ordinal) &&
|
||||||
string.Equals(property.Value.Value, ExpectedSunsetIso, StringComparison.Ordinal));
|
string.Equals(property.Value.Value, ExpectedSunsetIso, StringComparison.Ordinal));
|
||||||
}
|
}
|
||||||
|
|
||||||
private sealed class RecordingAuthEventSink : IAuthEventSink
|
private sealed class RecordingAuthEventSink : IAuthEventSink
|
||||||
{
|
{
|
||||||
private readonly ConcurrentQueue<AuthEventRecord> events = new();
|
private readonly ConcurrentQueue<AuthEventRecord> events = new();
|
||||||
|
|
||||||
public IReadOnlyCollection<AuthEventRecord> Events => events.ToArray();
|
public IReadOnlyCollection<AuthEventRecord> Events => events.ToArray();
|
||||||
|
|
||||||
public ValueTask WriteAsync(AuthEventRecord record, CancellationToken cancellationToken)
|
public ValueTask WriteAsync(AuthEventRecord record, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
events.Enqueue(record);
|
events.Enqueue(record);
|
||||||
return ValueTask.CompletedTask;
|
return ValueTask.CompletedTask;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,21 +1,21 @@
|
|||||||
<?xml version='1.0' encoding='utf-8'?>
|
<?xml version='1.0' encoding='utf-8'?>
|
||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net10.0</TargetFramework>
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<IsPackable>false</IsPackable>
|
<IsPackable>false</IsPackable>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\StellaOps.Authority\StellaOps.Authority.csproj" />
|
<ProjectReference Include="..\StellaOps.Authority\StellaOps.Authority.csproj" />
|
||||||
<ProjectReference Include="..\StellaOps.Authority.Plugins.Abstractions\StellaOps.Authority.Plugins.Abstractions.csproj" />
|
<ProjectReference Include="..\StellaOps.Authority.Plugins.Abstractions\StellaOps.Authority.Plugins.Abstractions.csproj" />
|
||||||
<ProjectReference Include="../../../__Libraries/StellaOps.Auth.Security/StellaOps.Auth.Security.csproj" />
|
<ProjectReference Include="../../../__Libraries/StellaOps.Auth.Security/StellaOps.Auth.Security.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="MongoDB.Driver" Version="3.5.0" />
|
<PackageReference Include="MongoDB.Driver" Version="3.5.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Compile Include="../../../../tests/shared/OpenSslLegacyShim.cs" Link="Infrastructure/OpenSslLegacyShim.cs" />
|
<Compile Include="../../../../tests/shared/OpenSslLegacyShim.cs" Link="Infrastructure/OpenSslLegacyShim.cs" />
|
||||||
<None Include="../../../../tests/native/openssl-1.1/linux-x64/*" Link="native/linux-x64/%(Filename)%(Extension)" CopyToOutputDirectory="PreserveNewest" />
|
<None Include="../../../../tests/native/openssl-1.1/linux-x64/*" Link="native/linux-x64/%(Filename)%(Extension)" CopyToOutputDirectory="PreserveNewest" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -1,457 +1,457 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Net.Http.Headers;
|
using System.Net.Http.Headers;
|
||||||
using System.Net.Http.Json;
|
using System.Net.Http.Json;
|
||||||
using System.Security.Cryptography;
|
using System.Security.Cryptography;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.Authentication;
|
using Microsoft.AspNetCore.Authentication;
|
||||||
using Microsoft.AspNetCore.Mvc.Testing;
|
using Microsoft.AspNetCore.Mvc.Testing;
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||||
using Microsoft.Extensions.Time.Testing;
|
using Microsoft.Extensions.Time.Testing;
|
||||||
using StellaOps.Auth.Abstractions;
|
using StellaOps.Auth.Abstractions;
|
||||||
using StellaOps.Authority;
|
using StellaOps.Authority;
|
||||||
using StellaOps.Authority.Tests.Infrastructure;
|
using StellaOps.Authority.Tests.Infrastructure;
|
||||||
using StellaOps.Authority.Vulnerability.Attachments;
|
using StellaOps.Authority.Vulnerability.Attachments;
|
||||||
using StellaOps.Authority.Vulnerability.Workflow;
|
using StellaOps.Authority.Vulnerability.Workflow;
|
||||||
using StellaOps.Configuration;
|
using StellaOps.Configuration;
|
||||||
using StellaOps.Cryptography;
|
using StellaOps.Cryptography;
|
||||||
using StellaOps.Cryptography.Audit;
|
using StellaOps.Cryptography.Audit;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
namespace StellaOps.Authority.Tests.Vulnerability;
|
namespace StellaOps.Authority.Tests.Vulnerability;
|
||||||
|
|
||||||
public sealed class VulnWorkflowTokenEndpointTests : IClassFixture<AuthorityWebApplicationFactory>
|
public sealed class VulnWorkflowTokenEndpointTests : IClassFixture<AuthorityWebApplicationFactory>
|
||||||
{
|
{
|
||||||
private readonly AuthorityWebApplicationFactory factory;
|
private readonly AuthorityWebApplicationFactory factory;
|
||||||
private const string SigningEnabledKey = "STELLAOPS_AUTHORITY_AUTHORITY__SIGNING__ENABLED";
|
private const string SigningEnabledKey = "STELLAOPS_AUTHORITY_AUTHORITY__SIGNING__ENABLED";
|
||||||
private const string SigningActiveKeyIdKey = "STELLAOPS_AUTHORITY_AUTHORITY__SIGNING__ACTIVEKEYID";
|
private const string SigningActiveKeyIdKey = "STELLAOPS_AUTHORITY_AUTHORITY__SIGNING__ACTIVEKEYID";
|
||||||
private const string SigningKeyPathKey = "STELLAOPS_AUTHORITY_AUTHORITY__SIGNING__KEYPATH";
|
private const string SigningKeyPathKey = "STELLAOPS_AUTHORITY_AUTHORITY__SIGNING__KEYPATH";
|
||||||
private const string SigningKeySourceKey = "STELLAOPS_AUTHORITY_AUTHORITY__SIGNING__KEYSOURCE";
|
private const string SigningKeySourceKey = "STELLAOPS_AUTHORITY_AUTHORITY__SIGNING__KEYSOURCE";
|
||||||
private const string SigningAlgorithmKey = "STELLAOPS_AUTHORITY_AUTHORITY__SIGNING__ALGORITHM";
|
private const string SigningAlgorithmKey = "STELLAOPS_AUTHORITY_AUTHORITY__SIGNING__ALGORITHM";
|
||||||
|
|
||||||
public VulnWorkflowTokenEndpointTests(AuthorityWebApplicationFactory factory)
|
public VulnWorkflowTokenEndpointTests(AuthorityWebApplicationFactory factory)
|
||||||
{
|
{
|
||||||
this.factory = factory ?? throw new ArgumentNullException(nameof(factory));
|
this.factory = factory ?? throw new ArgumentNullException(nameof(factory));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task IssueAndVerifyWorkflowToken_SucceedsAndAudits()
|
public async Task IssueAndVerifyWorkflowToken_SucceedsAndAudits()
|
||||||
{
|
{
|
||||||
var tempDir = Directory.CreateTempSubdirectory("workflow-token-success");
|
var tempDir = Directory.CreateTempSubdirectory("workflow-token-success");
|
||||||
var keyPath = Path.Combine(tempDir.FullName, "signing-key.pem");
|
var keyPath = Path.Combine(tempDir.FullName, "signing-key.pem");
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
CreateEcPrivateKey(keyPath);
|
CreateEcPrivateKey(keyPath);
|
||||||
|
|
||||||
using var env = new EnvironmentVariableScope(new[]
|
using var env = new EnvironmentVariableScope(new[]
|
||||||
{
|
{
|
||||||
new KeyValuePair<string, string?>(SigningEnabledKey, "true"),
|
new KeyValuePair<string, string?>(SigningEnabledKey, "true"),
|
||||||
new KeyValuePair<string, string?>(SigningActiveKeyIdKey, "workflow-key"),
|
new KeyValuePair<string, string?>(SigningActiveKeyIdKey, "workflow-key"),
|
||||||
new KeyValuePair<string, string?>(SigningKeyPathKey, keyPath),
|
new KeyValuePair<string, string?>(SigningKeyPathKey, keyPath),
|
||||||
new KeyValuePair<string, string?>(SigningKeySourceKey, "file"),
|
new KeyValuePair<string, string?>(SigningKeySourceKey, "file"),
|
||||||
new KeyValuePair<string, string?>(SigningAlgorithmKey, SignatureAlgorithms.Es256)
|
new KeyValuePair<string, string?>(SigningAlgorithmKey, SignatureAlgorithms.Es256)
|
||||||
});
|
});
|
||||||
|
|
||||||
var sink = new RecordingAuthEventSink();
|
var sink = new RecordingAuthEventSink();
|
||||||
var timeProvider = new FakeTimeProvider(DateTimeOffset.Parse("2025-11-02T09:00:00Z"));
|
var timeProvider = new FakeTimeProvider(DateTimeOffset.Parse("2025-11-02T09:00:00Z"));
|
||||||
|
|
||||||
using var app = CreateSignedAuthorityApp(sink, timeProvider, "workflow-key", keyPath);
|
using var app = CreateSignedAuthorityApp(sink, timeProvider, "workflow-key", keyPath);
|
||||||
using var client = app.CreateClient();
|
using var client = app.CreateClient();
|
||||||
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(TestAuthHandler.SchemeName);
|
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(TestAuthHandler.SchemeName);
|
||||||
client.DefaultRequestHeaders.Add("X-Test-Scopes", StellaOpsScopes.VulnOperate);
|
client.DefaultRequestHeaders.Add("X-Test-Scopes", StellaOpsScopes.VulnOperate);
|
||||||
client.DefaultRequestHeaders.Add("X-Test-Tenant", "tenant-default");
|
client.DefaultRequestHeaders.Add("X-Test-Tenant", "tenant-default");
|
||||||
client.DefaultRequestHeaders.Add(AuthorityHttpHeaders.Tenant, "tenant-default");
|
client.DefaultRequestHeaders.Add(AuthorityHttpHeaders.Tenant, "tenant-default");
|
||||||
|
|
||||||
var issuePayload = new
|
var issuePayload = new
|
||||||
{
|
{
|
||||||
tenant = "tenant-default",
|
tenant = "tenant-default",
|
||||||
actions = new[] { "assign", "comment" },
|
actions = new[] { "assign", "comment" },
|
||||||
context = new Dictionary<string, string> { ["finding_id"] = "F-123" },
|
context = new Dictionary<string, string> { ["finding_id"] = "F-123" },
|
||||||
nonce = "workflow-nonce-123456",
|
nonce = "workflow-nonce-123456",
|
||||||
expiresInSeconds = 600
|
expiresInSeconds = 600
|
||||||
};
|
};
|
||||||
|
|
||||||
var issueResponse = await client.PostAsJsonAsync("/vuln/workflow/anti-forgery/issue", issuePayload);
|
var issueResponse = await client.PostAsJsonAsync("/vuln/workflow/anti-forgery/issue", issuePayload);
|
||||||
var issueBody = await issueResponse.Content.ReadAsStringAsync();
|
var issueBody = await issueResponse.Content.ReadAsStringAsync();
|
||||||
Assert.True(issueResponse.StatusCode == HttpStatusCode.OK, $"Issue anti-forgery failed: {issueResponse.StatusCode} {issueBody}");
|
Assert.True(issueResponse.StatusCode == HttpStatusCode.OK, $"Issue anti-forgery failed: {issueResponse.StatusCode} {issueBody}");
|
||||||
|
|
||||||
var issued = System.Text.Json.JsonSerializer.Deserialize<VulnWorkflowAntiForgeryIssueResponse>(
|
var issued = System.Text.Json.JsonSerializer.Deserialize<VulnWorkflowAntiForgeryIssueResponse>(
|
||||||
issueBody,
|
issueBody,
|
||||||
new System.Text.Json.JsonSerializerOptions { PropertyNameCaseInsensitive = true });
|
new System.Text.Json.JsonSerializerOptions { PropertyNameCaseInsensitive = true });
|
||||||
Assert.NotNull(issued);
|
Assert.NotNull(issued);
|
||||||
Assert.Equal("workflow-nonce-123456", issued!.Nonce);
|
Assert.Equal("workflow-nonce-123456", issued!.Nonce);
|
||||||
Assert.Contains("assign", issued.Actions);
|
Assert.Contains("assign", issued.Actions);
|
||||||
Assert.Contains("comment", issued.Actions);
|
Assert.Contains("comment", issued.Actions);
|
||||||
|
|
||||||
var verifyPayload = new VulnWorkflowAntiForgeryVerifyRequest
|
var verifyPayload = new VulnWorkflowAntiForgeryVerifyRequest
|
||||||
{
|
{
|
||||||
Token = issued.Token,
|
Token = issued.Token,
|
||||||
RequiredAction = "assign",
|
RequiredAction = "assign",
|
||||||
Tenant = "tenant-default",
|
Tenant = "tenant-default",
|
||||||
Nonce = "workflow-nonce-123456"
|
Nonce = "workflow-nonce-123456"
|
||||||
};
|
};
|
||||||
|
|
||||||
var verifyResponse = await client.PostAsJsonAsync("/vuln/workflow/anti-forgery/verify", verifyPayload);
|
var verifyResponse = await client.PostAsJsonAsync("/vuln/workflow/anti-forgery/verify", verifyPayload);
|
||||||
var verifyBody = await verifyResponse.Content.ReadAsStringAsync();
|
var verifyBody = await verifyResponse.Content.ReadAsStringAsync();
|
||||||
Assert.True(verifyResponse.StatusCode == HttpStatusCode.OK, $"Verify anti-forgery failed: {verifyResponse.StatusCode} {verifyBody}");
|
Assert.True(verifyResponse.StatusCode == HttpStatusCode.OK, $"Verify anti-forgery failed: {verifyResponse.StatusCode} {verifyBody}");
|
||||||
|
|
||||||
var verified = System.Text.Json.JsonSerializer.Deserialize<VulnWorkflowAntiForgeryVerifyResponse>(
|
var verified = System.Text.Json.JsonSerializer.Deserialize<VulnWorkflowAntiForgeryVerifyResponse>(
|
||||||
verifyBody,
|
verifyBody,
|
||||||
new System.Text.Json.JsonSerializerOptions { PropertyNameCaseInsensitive = true });
|
new System.Text.Json.JsonSerializerOptions { PropertyNameCaseInsensitive = true });
|
||||||
Assert.NotNull(verified);
|
Assert.NotNull(verified);
|
||||||
Assert.Equal("tenant-default", verified!.Tenant);
|
Assert.Equal("tenant-default", verified!.Tenant);
|
||||||
Assert.Equal("workflow-nonce-123456", verified.Nonce);
|
Assert.Equal("workflow-nonce-123456", verified.Nonce);
|
||||||
|
|
||||||
var issuedEvent = Assert.Single(sink.Events, evt => evt.EventType == "vuln.workflow.csrf.issued");
|
var issuedEvent = Assert.Single(sink.Events, evt => evt.EventType == "vuln.workflow.csrf.issued");
|
||||||
Assert.Contains(issuedEvent.Properties, property => property.Name == "vuln.workflow.actor");
|
Assert.Contains(issuedEvent.Properties, property => property.Name == "vuln.workflow.actor");
|
||||||
|
|
||||||
var verifiedEvent = Assert.Single(sink.Events, evt => evt.EventType == "vuln.workflow.csrf.verified");
|
var verifiedEvent = Assert.Single(sink.Events, evt => evt.EventType == "vuln.workflow.csrf.verified");
|
||||||
Assert.Contains(verifiedEvent.Properties, property => property.Name == "vuln.workflow.nonce" && property.Value.Value == "workflow-nonce-123456");
|
Assert.Contains(verifiedEvent.Properties, property => property.Name == "vuln.workflow.nonce" && property.Value.Value == "workflow-nonce-123456");
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
TryDeleteDirectory(tempDir.FullName);
|
TryDeleteDirectory(tempDir.FullName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task IssueWorkflowToken_ReturnsBadRequest_WhenActionsMissing()
|
public async Task IssueWorkflowToken_ReturnsBadRequest_WhenActionsMissing()
|
||||||
{
|
{
|
||||||
var tempDir = Directory.CreateTempSubdirectory("workflow-token-missing-actions");
|
var tempDir = Directory.CreateTempSubdirectory("workflow-token-missing-actions");
|
||||||
var keyPath = Path.Combine(tempDir.FullName, "signing-key.pem");
|
var keyPath = Path.Combine(tempDir.FullName, "signing-key.pem");
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
CreateEcPrivateKey(keyPath);
|
CreateEcPrivateKey(keyPath);
|
||||||
|
|
||||||
using var env = new EnvironmentVariableScope(new[]
|
using var env = new EnvironmentVariableScope(new[]
|
||||||
{
|
{
|
||||||
new KeyValuePair<string, string?>(SigningEnabledKey, "true"),
|
new KeyValuePair<string, string?>(SigningEnabledKey, "true"),
|
||||||
new KeyValuePair<string, string?>(SigningActiveKeyIdKey, "workflow-key"),
|
new KeyValuePair<string, string?>(SigningActiveKeyIdKey, "workflow-key"),
|
||||||
new KeyValuePair<string, string?>(SigningKeyPathKey, keyPath),
|
new KeyValuePair<string, string?>(SigningKeyPathKey, keyPath),
|
||||||
new KeyValuePair<string, string?>(SigningKeySourceKey, "file"),
|
new KeyValuePair<string, string?>(SigningKeySourceKey, "file"),
|
||||||
new KeyValuePair<string, string?>(SigningAlgorithmKey, SignatureAlgorithms.Es256)
|
new KeyValuePair<string, string?>(SigningAlgorithmKey, SignatureAlgorithms.Es256)
|
||||||
});
|
});
|
||||||
|
|
||||||
var sink = new RecordingAuthEventSink();
|
var sink = new RecordingAuthEventSink();
|
||||||
var timeProvider = new FakeTimeProvider(DateTimeOffset.Parse("2025-11-02T09:10:00Z"));
|
var timeProvider = new FakeTimeProvider(DateTimeOffset.Parse("2025-11-02T09:10:00Z"));
|
||||||
|
|
||||||
using var app = CreateSignedAuthorityApp(sink, timeProvider, "workflow-key", keyPath);
|
using var app = CreateSignedAuthorityApp(sink, timeProvider, "workflow-key", keyPath);
|
||||||
using var client = app.CreateClient();
|
using var client = app.CreateClient();
|
||||||
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(TestAuthHandler.SchemeName);
|
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(TestAuthHandler.SchemeName);
|
||||||
client.DefaultRequestHeaders.Add("X-Test-Scopes", StellaOpsScopes.VulnOperate);
|
client.DefaultRequestHeaders.Add("X-Test-Scopes", StellaOpsScopes.VulnOperate);
|
||||||
client.DefaultRequestHeaders.Add("X-Test-Tenant", "tenant-default");
|
client.DefaultRequestHeaders.Add("X-Test-Tenant", "tenant-default");
|
||||||
client.DefaultRequestHeaders.Add(AuthorityHttpHeaders.Tenant, "tenant-default");
|
client.DefaultRequestHeaders.Add(AuthorityHttpHeaders.Tenant, "tenant-default");
|
||||||
|
|
||||||
var issuePayload = new
|
var issuePayload = new
|
||||||
{
|
{
|
||||||
tenant = "tenant-default",
|
tenant = "tenant-default",
|
||||||
actions = Array.Empty<string>()
|
actions = Array.Empty<string>()
|
||||||
};
|
};
|
||||||
|
|
||||||
var response = await client.PostAsJsonAsync("/vuln/workflow/anti-forgery/issue", issuePayload);
|
var response = await client.PostAsJsonAsync("/vuln/workflow/anti-forgery/issue", issuePayload);
|
||||||
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
|
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
|
||||||
|
|
||||||
var error = await response.Content.ReadFromJsonAsync<Dictionary<string, string>>();
|
var error = await response.Content.ReadFromJsonAsync<Dictionary<string, string>>();
|
||||||
Assert.NotNull(error);
|
Assert.NotNull(error);
|
||||||
Assert.Equal("invalid_request", error!["error"]);
|
Assert.Equal("invalid_request", error!["error"]);
|
||||||
Assert.Contains("action", error["message"], StringComparison.OrdinalIgnoreCase);
|
Assert.Contains("action", error["message"], StringComparison.OrdinalIgnoreCase);
|
||||||
|
|
||||||
Assert.DoesNotContain(sink.Events, evt => evt.EventType == "vuln.workflow.csrf.issued");
|
Assert.DoesNotContain(sink.Events, evt => evt.EventType == "vuln.workflow.csrf.issued");
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
TryDeleteDirectory(tempDir.FullName);
|
TryDeleteDirectory(tempDir.FullName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task VerifyWorkflowToken_ReturnsBadRequest_WhenActionNotPermitted()
|
public async Task VerifyWorkflowToken_ReturnsBadRequest_WhenActionNotPermitted()
|
||||||
{
|
{
|
||||||
var tempDir = Directory.CreateTempSubdirectory("workflow-token-invalid-action");
|
var tempDir = Directory.CreateTempSubdirectory("workflow-token-invalid-action");
|
||||||
var keyPath = Path.Combine(tempDir.FullName, "signing-key.pem");
|
var keyPath = Path.Combine(tempDir.FullName, "signing-key.pem");
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
CreateEcPrivateKey(keyPath);
|
CreateEcPrivateKey(keyPath);
|
||||||
|
|
||||||
using var env = new EnvironmentVariableScope(new[]
|
using var env = new EnvironmentVariableScope(new[]
|
||||||
{
|
{
|
||||||
new KeyValuePair<string, string?>(SigningEnabledKey, "true"),
|
new KeyValuePair<string, string?>(SigningEnabledKey, "true"),
|
||||||
new KeyValuePair<string, string?>(SigningActiveKeyIdKey, "workflow-key"),
|
new KeyValuePair<string, string?>(SigningActiveKeyIdKey, "workflow-key"),
|
||||||
new KeyValuePair<string, string?>(SigningKeyPathKey, keyPath),
|
new KeyValuePair<string, string?>(SigningKeyPathKey, keyPath),
|
||||||
new KeyValuePair<string, string?>(SigningKeySourceKey, "file"),
|
new KeyValuePair<string, string?>(SigningKeySourceKey, "file"),
|
||||||
new KeyValuePair<string, string?>(SigningAlgorithmKey, SignatureAlgorithms.Es256)
|
new KeyValuePair<string, string?>(SigningAlgorithmKey, SignatureAlgorithms.Es256)
|
||||||
});
|
});
|
||||||
|
|
||||||
var sink = new RecordingAuthEventSink();
|
var sink = new RecordingAuthEventSink();
|
||||||
var timeProvider = new FakeTimeProvider(DateTimeOffset.Parse("2025-11-02T09:20:00Z"));
|
var timeProvider = new FakeTimeProvider(DateTimeOffset.Parse("2025-11-02T09:20:00Z"));
|
||||||
|
|
||||||
using var app = CreateSignedAuthorityApp(sink, timeProvider, "workflow-key", keyPath);
|
using var app = CreateSignedAuthorityApp(sink, timeProvider, "workflow-key", keyPath);
|
||||||
using var client = app.CreateClient();
|
using var client = app.CreateClient();
|
||||||
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(TestAuthHandler.SchemeName);
|
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(TestAuthHandler.SchemeName);
|
||||||
client.DefaultRequestHeaders.Add("X-Test-Scopes", StellaOpsScopes.VulnOperate);
|
client.DefaultRequestHeaders.Add("X-Test-Scopes", StellaOpsScopes.VulnOperate);
|
||||||
client.DefaultRequestHeaders.Add("X-Test-Tenant", "tenant-default");
|
client.DefaultRequestHeaders.Add("X-Test-Tenant", "tenant-default");
|
||||||
client.DefaultRequestHeaders.Add(AuthorityHttpHeaders.Tenant, "tenant-default");
|
client.DefaultRequestHeaders.Add(AuthorityHttpHeaders.Tenant, "tenant-default");
|
||||||
|
|
||||||
var issuePayload = new
|
var issuePayload = new
|
||||||
{
|
{
|
||||||
tenant = "tenant-default",
|
tenant = "tenant-default",
|
||||||
actions = new[] { "assign" },
|
actions = new[] { "assign" },
|
||||||
nonce = "workflow-nonce-789012"
|
nonce = "workflow-nonce-789012"
|
||||||
};
|
};
|
||||||
|
|
||||||
var issueResponse = await client.PostAsJsonAsync("/vuln/workflow/anti-forgery/issue", issuePayload);
|
var issueResponse = await client.PostAsJsonAsync("/vuln/workflow/anti-forgery/issue", issuePayload);
|
||||||
Assert.Equal(HttpStatusCode.OK, issueResponse.StatusCode);
|
Assert.Equal(HttpStatusCode.OK, issueResponse.StatusCode);
|
||||||
var issued = await issueResponse.Content.ReadFromJsonAsync<VulnWorkflowAntiForgeryIssueResponse>();
|
var issued = await issueResponse.Content.ReadFromJsonAsync<VulnWorkflowAntiForgeryIssueResponse>();
|
||||||
Assert.NotNull(issued);
|
Assert.NotNull(issued);
|
||||||
|
|
||||||
var verifyPayload = new VulnWorkflowAntiForgeryVerifyRequest
|
var verifyPayload = new VulnWorkflowAntiForgeryVerifyRequest
|
||||||
{
|
{
|
||||||
Token = issued!.Token,
|
Token = issued!.Token,
|
||||||
RequiredAction = "close",
|
RequiredAction = "close",
|
||||||
Tenant = "tenant-default",
|
Tenant = "tenant-default",
|
||||||
Nonce = "workflow-nonce-789012"
|
Nonce = "workflow-nonce-789012"
|
||||||
};
|
};
|
||||||
|
|
||||||
var verifyResponse = await client.PostAsJsonAsync("/vuln/workflow/anti-forgery/verify", verifyPayload);
|
var verifyResponse = await client.PostAsJsonAsync("/vuln/workflow/anti-forgery/verify", verifyPayload);
|
||||||
Assert.Equal(HttpStatusCode.BadRequest, verifyResponse.StatusCode);
|
Assert.Equal(HttpStatusCode.BadRequest, verifyResponse.StatusCode);
|
||||||
|
|
||||||
var error = await verifyResponse.Content.ReadFromJsonAsync<Dictionary<string, string>>();
|
var error = await verifyResponse.Content.ReadFromJsonAsync<Dictionary<string, string>>();
|
||||||
Assert.NotNull(error);
|
Assert.NotNull(error);
|
||||||
Assert.Equal("invalid_token", error!["error"]);
|
Assert.Equal("invalid_token", error!["error"]);
|
||||||
Assert.Contains("Token does not permit action", error["message"], StringComparison.Ordinal);
|
Assert.Contains("Token does not permit action", error["message"], StringComparison.Ordinal);
|
||||||
|
|
||||||
Assert.Single(sink.Events, evt => evt.EventType == "vuln.workflow.csrf.issued");
|
Assert.Single(sink.Events, evt => evt.EventType == "vuln.workflow.csrf.issued");
|
||||||
Assert.DoesNotContain(sink.Events, evt => evt.EventType == "vuln.workflow.csrf.verified");
|
Assert.DoesNotContain(sink.Events, evt => evt.EventType == "vuln.workflow.csrf.verified");
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
TryDeleteDirectory(tempDir.FullName);
|
TryDeleteDirectory(tempDir.FullName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task IssueAndVerifyAttachmentToken_SucceedsAndAudits()
|
public async Task IssueAndVerifyAttachmentToken_SucceedsAndAudits()
|
||||||
{
|
{
|
||||||
var tempDir = Directory.CreateTempSubdirectory("attachment-token-success");
|
var tempDir = Directory.CreateTempSubdirectory("attachment-token-success");
|
||||||
var keyPath = Path.Combine(tempDir.FullName, "attachment-key.pem");
|
var keyPath = Path.Combine(tempDir.FullName, "attachment-key.pem");
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
CreateEcPrivateKey(keyPath);
|
CreateEcPrivateKey(keyPath);
|
||||||
|
|
||||||
using var env = new EnvironmentVariableScope(new[]
|
using var env = new EnvironmentVariableScope(new[]
|
||||||
{
|
{
|
||||||
new KeyValuePair<string, string?>(SigningEnabledKey, "true"),
|
new KeyValuePair<string, string?>(SigningEnabledKey, "true"),
|
||||||
new KeyValuePair<string, string?>(SigningActiveKeyIdKey, "attachment-key"),
|
new KeyValuePair<string, string?>(SigningActiveKeyIdKey, "attachment-key"),
|
||||||
new KeyValuePair<string, string?>(SigningKeyPathKey, keyPath),
|
new KeyValuePair<string, string?>(SigningKeyPathKey, keyPath),
|
||||||
new KeyValuePair<string, string?>(SigningKeySourceKey, "file"),
|
new KeyValuePair<string, string?>(SigningKeySourceKey, "file"),
|
||||||
new KeyValuePair<string, string?>(SigningAlgorithmKey, SignatureAlgorithms.Es256)
|
new KeyValuePair<string, string?>(SigningAlgorithmKey, SignatureAlgorithms.Es256)
|
||||||
});
|
});
|
||||||
|
|
||||||
var sink = new RecordingAuthEventSink();
|
var sink = new RecordingAuthEventSink();
|
||||||
var timeProvider = new FakeTimeProvider(DateTimeOffset.Parse("2025-11-02T11:00:00Z"));
|
var timeProvider = new FakeTimeProvider(DateTimeOffset.Parse("2025-11-02T11:00:00Z"));
|
||||||
|
|
||||||
using var app = CreateSignedAuthorityApp(sink, timeProvider, "attachment-key", keyPath);
|
using var app = CreateSignedAuthorityApp(sink, timeProvider, "attachment-key", keyPath);
|
||||||
using var client = app.CreateClient();
|
using var client = app.CreateClient();
|
||||||
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(TestAuthHandler.SchemeName);
|
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(TestAuthHandler.SchemeName);
|
||||||
client.DefaultRequestHeaders.Add("X-Test-Scopes", StellaOpsScopes.VulnInvestigate);
|
client.DefaultRequestHeaders.Add("X-Test-Scopes", StellaOpsScopes.VulnInvestigate);
|
||||||
client.DefaultRequestHeaders.Add("X-Test-Tenant", "tenant-default");
|
client.DefaultRequestHeaders.Add("X-Test-Tenant", "tenant-default");
|
||||||
client.DefaultRequestHeaders.Add(AuthorityHttpHeaders.Tenant, "tenant-default");
|
client.DefaultRequestHeaders.Add(AuthorityHttpHeaders.Tenant, "tenant-default");
|
||||||
|
|
||||||
var issuePayload = new VulnAttachmentTokenIssueRequest
|
var issuePayload = new VulnAttachmentTokenIssueRequest
|
||||||
{
|
{
|
||||||
Tenant = "tenant-default",
|
Tenant = "tenant-default",
|
||||||
LedgerEventHash = "ledger-hash-001",
|
LedgerEventHash = "ledger-hash-001",
|
||||||
AttachmentId = "attach-123",
|
AttachmentId = "attach-123",
|
||||||
FindingId = "find-456",
|
FindingId = "find-456",
|
||||||
ContentHash = "sha256:abc123",
|
ContentHash = "sha256:abc123",
|
||||||
ContentType = "application/pdf",
|
ContentType = "application/pdf",
|
||||||
Metadata = new Dictionary<string, string?> { ["origin"] = "vuln-workflow" }
|
Metadata = new Dictionary<string, string?> { ["origin"] = "vuln-workflow" }
|
||||||
};
|
};
|
||||||
|
|
||||||
var issueResponse = await client.PostAsJsonAsync("/vuln/attachments/tokens/issue", issuePayload);
|
var issueResponse = await client.PostAsJsonAsync("/vuln/attachments/tokens/issue", issuePayload);
|
||||||
Assert.Equal(HttpStatusCode.OK, issueResponse.StatusCode);
|
Assert.Equal(HttpStatusCode.OK, issueResponse.StatusCode);
|
||||||
var issued = await issueResponse.Content.ReadFromJsonAsync<VulnAttachmentTokenIssueResponse>();
|
var issued = await issueResponse.Content.ReadFromJsonAsync<VulnAttachmentTokenIssueResponse>();
|
||||||
Assert.NotNull(issued);
|
Assert.NotNull(issued);
|
||||||
Assert.Equal("attach-123", issued!.AttachmentId);
|
Assert.Equal("attach-123", issued!.AttachmentId);
|
||||||
|
|
||||||
var verifyPayload = new VulnAttachmentTokenVerifyRequest
|
var verifyPayload = new VulnAttachmentTokenVerifyRequest
|
||||||
{
|
{
|
||||||
Token = issued.Token,
|
Token = issued.Token,
|
||||||
Tenant = "tenant-default",
|
Tenant = "tenant-default",
|
||||||
LedgerEventHash = "ledger-hash-001",
|
LedgerEventHash = "ledger-hash-001",
|
||||||
AttachmentId = "attach-123"
|
AttachmentId = "attach-123"
|
||||||
};
|
};
|
||||||
|
|
||||||
var verifyResponse = await client.PostAsJsonAsync("/vuln/attachments/tokens/verify", verifyPayload);
|
var verifyResponse = await client.PostAsJsonAsync("/vuln/attachments/tokens/verify", verifyPayload);
|
||||||
Assert.Equal(HttpStatusCode.OK, verifyResponse.StatusCode);
|
Assert.Equal(HttpStatusCode.OK, verifyResponse.StatusCode);
|
||||||
var verified = await verifyResponse.Content.ReadFromJsonAsync<VulnAttachmentTokenVerifyResponse>();
|
var verified = await verifyResponse.Content.ReadFromJsonAsync<VulnAttachmentTokenVerifyResponse>();
|
||||||
Assert.NotNull(verified);
|
Assert.NotNull(verified);
|
||||||
Assert.Equal("ledger-hash-001", verified!.LedgerEventHash);
|
Assert.Equal("ledger-hash-001", verified!.LedgerEventHash);
|
||||||
|
|
||||||
var issuedEvent = Assert.Single(sink.Events, evt => evt.EventType == "vuln.attachment.token.issued");
|
var issuedEvent = Assert.Single(sink.Events, evt => evt.EventType == "vuln.attachment.token.issued");
|
||||||
Assert.Contains(issuedEvent.Properties, property => property.Name == "vuln.attachment.ledger_hash" && property.Value.Value == "ledger-hash-001");
|
Assert.Contains(issuedEvent.Properties, property => property.Name == "vuln.attachment.ledger_hash" && property.Value.Value == "ledger-hash-001");
|
||||||
|
|
||||||
var verifiedEvent = Assert.Single(sink.Events, evt => evt.EventType == "vuln.attachment.token.verified");
|
var verifiedEvent = Assert.Single(sink.Events, evt => evt.EventType == "vuln.attachment.token.verified");
|
||||||
Assert.Contains(verifiedEvent.Properties, property => property.Name == "vuln.attachment.ledger_hash" && property.Value.Value == "ledger-hash-001");
|
Assert.Contains(verifiedEvent.Properties, property => property.Name == "vuln.attachment.ledger_hash" && property.Value.Value == "ledger-hash-001");
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
TryDeleteDirectory(tempDir.FullName);
|
TryDeleteDirectory(tempDir.FullName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task VerifyAttachmentToken_ReturnsBadRequest_WhenLedgerMismatch()
|
public async Task VerifyAttachmentToken_ReturnsBadRequest_WhenLedgerMismatch()
|
||||||
{
|
{
|
||||||
var tempDir = Directory.CreateTempSubdirectory("attachment-token-ledger-mismatch");
|
var tempDir = Directory.CreateTempSubdirectory("attachment-token-ledger-mismatch");
|
||||||
var keyPath = Path.Combine(tempDir.FullName, "attachment-key.pem");
|
var keyPath = Path.Combine(tempDir.FullName, "attachment-key.pem");
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
CreateEcPrivateKey(keyPath);
|
CreateEcPrivateKey(keyPath);
|
||||||
|
|
||||||
using var env = new EnvironmentVariableScope(new[]
|
using var env = new EnvironmentVariableScope(new[]
|
||||||
{
|
{
|
||||||
new KeyValuePair<string, string?>(SigningEnabledKey, "true"),
|
new KeyValuePair<string, string?>(SigningEnabledKey, "true"),
|
||||||
new KeyValuePair<string, string?>(SigningActiveKeyIdKey, "attachment-key"),
|
new KeyValuePair<string, string?>(SigningActiveKeyIdKey, "attachment-key"),
|
||||||
new KeyValuePair<string, string?>(SigningKeyPathKey, keyPath),
|
new KeyValuePair<string, string?>(SigningKeyPathKey, keyPath),
|
||||||
new KeyValuePair<string, string?>(SigningKeySourceKey, "file"),
|
new KeyValuePair<string, string?>(SigningKeySourceKey, "file"),
|
||||||
new KeyValuePair<string, string?>(SigningAlgorithmKey, SignatureAlgorithms.Es256)
|
new KeyValuePair<string, string?>(SigningAlgorithmKey, SignatureAlgorithms.Es256)
|
||||||
});
|
});
|
||||||
|
|
||||||
var sink = new RecordingAuthEventSink();
|
var sink = new RecordingAuthEventSink();
|
||||||
var timeProvider = new FakeTimeProvider(DateTimeOffset.Parse("2025-11-02T11:10:00Z"));
|
var timeProvider = new FakeTimeProvider(DateTimeOffset.Parse("2025-11-02T11:10:00Z"));
|
||||||
|
|
||||||
using var app = CreateSignedAuthorityApp(sink, timeProvider, "attachment-key", keyPath);
|
using var app = CreateSignedAuthorityApp(sink, timeProvider, "attachment-key", keyPath);
|
||||||
using var client = app.CreateClient();
|
using var client = app.CreateClient();
|
||||||
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(TestAuthHandler.SchemeName);
|
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(TestAuthHandler.SchemeName);
|
||||||
client.DefaultRequestHeaders.Add("X-Test-Scopes", StellaOpsScopes.VulnInvestigate);
|
client.DefaultRequestHeaders.Add("X-Test-Scopes", StellaOpsScopes.VulnInvestigate);
|
||||||
client.DefaultRequestHeaders.Add("X-Test-Tenant", "tenant-default");
|
client.DefaultRequestHeaders.Add("X-Test-Tenant", "tenant-default");
|
||||||
client.DefaultRequestHeaders.Add(AuthorityHttpHeaders.Tenant, "tenant-default");
|
client.DefaultRequestHeaders.Add(AuthorityHttpHeaders.Tenant, "tenant-default");
|
||||||
|
|
||||||
var issuePayload = new VulnAttachmentTokenIssueRequest
|
var issuePayload = new VulnAttachmentTokenIssueRequest
|
||||||
{
|
{
|
||||||
Tenant = "tenant-default",
|
Tenant = "tenant-default",
|
||||||
LedgerEventHash = "ledger-hash-001",
|
LedgerEventHash = "ledger-hash-001",
|
||||||
AttachmentId = "attach-123"
|
AttachmentId = "attach-123"
|
||||||
};
|
};
|
||||||
|
|
||||||
var issueResponse = await client.PostAsJsonAsync("/vuln/attachments/tokens/issue", issuePayload);
|
var issueResponse = await client.PostAsJsonAsync("/vuln/attachments/tokens/issue", issuePayload);
|
||||||
Assert.Equal(HttpStatusCode.OK, issueResponse.StatusCode);
|
Assert.Equal(HttpStatusCode.OK, issueResponse.StatusCode);
|
||||||
var issued = await issueResponse.Content.ReadFromJsonAsync<VulnAttachmentTokenIssueResponse>();
|
var issued = await issueResponse.Content.ReadFromJsonAsync<VulnAttachmentTokenIssueResponse>();
|
||||||
Assert.NotNull(issued);
|
Assert.NotNull(issued);
|
||||||
|
|
||||||
var verifyPayload = new VulnAttachmentTokenVerifyRequest
|
var verifyPayload = new VulnAttachmentTokenVerifyRequest
|
||||||
{
|
{
|
||||||
Token = issued!.Token,
|
Token = issued!.Token,
|
||||||
Tenant = "tenant-default",
|
Tenant = "tenant-default",
|
||||||
LedgerEventHash = "ledger-hash-999",
|
LedgerEventHash = "ledger-hash-999",
|
||||||
AttachmentId = "attach-123"
|
AttachmentId = "attach-123"
|
||||||
};
|
};
|
||||||
|
|
||||||
var verifyResponse = await client.PostAsJsonAsync("/vuln/attachments/tokens/verify", verifyPayload);
|
var verifyResponse = await client.PostAsJsonAsync("/vuln/attachments/tokens/verify", verifyPayload);
|
||||||
Assert.Equal(HttpStatusCode.BadRequest, verifyResponse.StatusCode);
|
Assert.Equal(HttpStatusCode.BadRequest, verifyResponse.StatusCode);
|
||||||
|
|
||||||
var error = await verifyResponse.Content.ReadFromJsonAsync<Dictionary<string, string>>();
|
var error = await verifyResponse.Content.ReadFromJsonAsync<Dictionary<string, string>>();
|
||||||
Assert.NotNull(error);
|
Assert.NotNull(error);
|
||||||
Assert.Equal("invalid_token", error!["error"]);
|
Assert.Equal("invalid_token", error!["error"]);
|
||||||
Assert.Contains("ledger reference", error["message"], StringComparison.OrdinalIgnoreCase);
|
Assert.Contains("ledger reference", error["message"], StringComparison.OrdinalIgnoreCase);
|
||||||
|
|
||||||
Assert.Single(sink.Events, evt => evt.EventType == "vuln.attachment.token.issued");
|
Assert.Single(sink.Events, evt => evt.EventType == "vuln.attachment.token.issued");
|
||||||
Assert.DoesNotContain(sink.Events, evt => evt.EventType == "vuln.attachment.token.verified");
|
Assert.DoesNotContain(sink.Events, evt => evt.EventType == "vuln.attachment.token.verified");
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
TryDeleteDirectory(tempDir.FullName);
|
TryDeleteDirectory(tempDir.FullName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private WebApplicationFactory<Program> CreateSignedAuthorityApp(
|
private WebApplicationFactory<Program> CreateSignedAuthorityApp(
|
||||||
RecordingAuthEventSink sink,
|
RecordingAuthEventSink sink,
|
||||||
FakeTimeProvider timeProvider,
|
FakeTimeProvider timeProvider,
|
||||||
string signingKeyId,
|
string signingKeyId,
|
||||||
string signingKeyPath)
|
string signingKeyPath)
|
||||||
{
|
{
|
||||||
return factory.WithWebHostBuilder(host =>
|
return factory.WithWebHostBuilder(host =>
|
||||||
{
|
{
|
||||||
host.ConfigureAppConfiguration((_, configuration) =>
|
host.ConfigureAppConfiguration((_, configuration) =>
|
||||||
{
|
{
|
||||||
configuration.AddInMemoryCollection(new Dictionary<string, string?>
|
configuration.AddInMemoryCollection(new Dictionary<string, string?>
|
||||||
{
|
{
|
||||||
["Authority:Signing:Enabled"] = "true",
|
["Authority:Signing:Enabled"] = "true",
|
||||||
["Authority:Signing:ActiveKeyId"] = signingKeyId,
|
["Authority:Signing:ActiveKeyId"] = signingKeyId,
|
||||||
["Authority:Signing:KeyPath"] = signingKeyPath,
|
["Authority:Signing:KeyPath"] = signingKeyPath,
|
||||||
["Authority:Signing:KeySource"] = "file",
|
["Authority:Signing:KeySource"] = "file",
|
||||||
["Authority:Signing:Algorithm"] = SignatureAlgorithms.Es256
|
["Authority:Signing:Algorithm"] = SignatureAlgorithms.Es256
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
host.ConfigureServices(services =>
|
host.ConfigureServices(services =>
|
||||||
{
|
{
|
||||||
services.RemoveAll<IAuthEventSink>();
|
services.RemoveAll<IAuthEventSink>();
|
||||||
services.AddSingleton<IAuthEventSink>(sink);
|
services.AddSingleton<IAuthEventSink>(sink);
|
||||||
services.Replace(ServiceDescriptor.Singleton<TimeProvider>(timeProvider));
|
services.Replace(ServiceDescriptor.Singleton<TimeProvider>(timeProvider));
|
||||||
services.PostConfigure<StellaOpsAuthorityOptions>(options =>
|
services.PostConfigure<StellaOpsAuthorityOptions>(options =>
|
||||||
{
|
{
|
||||||
options.Signing.Enabled = true;
|
options.Signing.Enabled = true;
|
||||||
options.Signing.ActiveKeyId = signingKeyId;
|
options.Signing.ActiveKeyId = signingKeyId;
|
||||||
options.Signing.KeyPath = signingKeyPath;
|
options.Signing.KeyPath = signingKeyPath;
|
||||||
options.Signing.KeySource = "file";
|
options.Signing.KeySource = "file";
|
||||||
options.Signing.Algorithm = SignatureAlgorithms.Es256;
|
options.Signing.Algorithm = SignatureAlgorithms.Es256;
|
||||||
options.VulnerabilityExplorer.Workflow.AntiForgery.Enabled = true;
|
options.VulnerabilityExplorer.Workflow.AntiForgery.Enabled = true;
|
||||||
options.VulnerabilityExplorer.Attachments.Enabled = true;
|
options.VulnerabilityExplorer.Attachments.Enabled = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
var authBuilder = services.AddAuthentication(options =>
|
var authBuilder = services.AddAuthentication(options =>
|
||||||
{
|
{
|
||||||
options.DefaultAuthenticateScheme = TestAuthHandler.SchemeName;
|
options.DefaultAuthenticateScheme = TestAuthHandler.SchemeName;
|
||||||
options.DefaultChallengeScheme = TestAuthHandler.SchemeName;
|
options.DefaultChallengeScheme = TestAuthHandler.SchemeName;
|
||||||
});
|
});
|
||||||
|
|
||||||
authBuilder.AddScheme<AuthenticationSchemeOptions, TestAuthHandler>(TestAuthHandler.SchemeName, _ => { });
|
authBuilder.AddScheme<AuthenticationSchemeOptions, TestAuthHandler>(TestAuthHandler.SchemeName, _ => { });
|
||||||
authBuilder.AddScheme<AuthenticationSchemeOptions, TestAuthHandler>(StellaOpsAuthenticationDefaults.AuthenticationScheme, _ => { });
|
authBuilder.AddScheme<AuthenticationSchemeOptions, TestAuthHandler>(StellaOpsAuthenticationDefaults.AuthenticationScheme, _ => { });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void CreateEcPrivateKey(string path)
|
private static void CreateEcPrivateKey(string path)
|
||||||
{
|
{
|
||||||
Directory.CreateDirectory(Path.GetDirectoryName(path)!);
|
Directory.CreateDirectory(Path.GetDirectoryName(path)!);
|
||||||
using var ecdsa = ECDsa.Create(ECCurve.NamedCurves.nistP256);
|
using var ecdsa = ECDsa.Create(ECCurve.NamedCurves.nistP256);
|
||||||
File.WriteAllText(path, ecdsa.ExportECPrivateKeyPem());
|
File.WriteAllText(path, ecdsa.ExportECPrivateKeyPem());
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void TryDeleteDirectory(string directory)
|
private static void TryDeleteDirectory(string directory)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
Directory.Delete(directory, recursive: true);
|
Directory.Delete(directory, recursive: true);
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
// Ignored during cleanup.
|
// Ignored during cleanup.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private sealed class RecordingAuthEventSink : IAuthEventSink
|
private sealed class RecordingAuthEventSink : IAuthEventSink
|
||||||
{
|
{
|
||||||
private readonly List<AuthEventRecord> events = new();
|
private readonly List<AuthEventRecord> events = new();
|
||||||
|
|
||||||
public IReadOnlyList<AuthEventRecord> Events => events;
|
public IReadOnlyList<AuthEventRecord> Events => events;
|
||||||
|
|
||||||
public ValueTask WriteAsync(AuthEventRecord record, CancellationToken cancellationToken)
|
public ValueTask WriteAsync(AuthEventRecord record, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
events.Add(record);
|
events.Add(record);
|
||||||
return ValueTask.CompletedTask;
|
return ValueTask.CompletedTask;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,254 +1,254 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using Microsoft.AspNetCore.Builder;
|
using Microsoft.AspNetCore.Builder;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using Microsoft.Net.Http.Headers;
|
using Microsoft.Net.Http.Headers;
|
||||||
using StellaOps.Configuration;
|
using StellaOps.Configuration;
|
||||||
using StellaOps.Cryptography.Audit;
|
using StellaOps.Cryptography.Audit;
|
||||||
|
|
||||||
namespace StellaOps.Authority;
|
namespace StellaOps.Authority;
|
||||||
|
|
||||||
internal sealed class LegacyAuthDeprecationMiddleware
|
internal sealed class LegacyAuthDeprecationMiddleware
|
||||||
{
|
{
|
||||||
private const string LegacyEventType = "authority.api.legacy_endpoint";
|
private const string LegacyEventType = "authority.api.legacy_endpoint";
|
||||||
private const string SunsetHeaderName = "Sunset";
|
private const string SunsetHeaderName = "Sunset";
|
||||||
|
|
||||||
private static readonly IReadOnlyDictionary<PathString, PathString> LegacyEndpointMap =
|
private static readonly IReadOnlyDictionary<PathString, PathString> LegacyEndpointMap =
|
||||||
new Dictionary<PathString, PathString>(PathStringComparer.Instance)
|
new Dictionary<PathString, PathString>(PathStringComparer.Instance)
|
||||||
{
|
{
|
||||||
[new PathString("/oauth/token")] = new PathString("/token"),
|
[new PathString("/oauth/token")] = new PathString("/token"),
|
||||||
[new PathString("/oauth/introspect")] = new PathString("/introspect"),
|
[new PathString("/oauth/introspect")] = new PathString("/introspect"),
|
||||||
[new PathString("/oauth/revoke")] = new PathString("/revoke")
|
[new PathString("/oauth/revoke")] = new PathString("/revoke")
|
||||||
};
|
};
|
||||||
|
|
||||||
private readonly RequestDelegate next;
|
private readonly RequestDelegate next;
|
||||||
private readonly AuthorityLegacyAuthEndpointOptions options;
|
private readonly AuthorityLegacyAuthEndpointOptions options;
|
||||||
private readonly IAuthEventSink auditSink;
|
private readonly IAuthEventSink auditSink;
|
||||||
private readonly TimeProvider clock;
|
private readonly TimeProvider clock;
|
||||||
private readonly ILogger<LegacyAuthDeprecationMiddleware> logger;
|
private readonly ILogger<LegacyAuthDeprecationMiddleware> logger;
|
||||||
|
|
||||||
public LegacyAuthDeprecationMiddleware(
|
public LegacyAuthDeprecationMiddleware(
|
||||||
RequestDelegate next,
|
RequestDelegate next,
|
||||||
IOptions<StellaOpsAuthorityOptions> authorityOptions,
|
IOptions<StellaOpsAuthorityOptions> authorityOptions,
|
||||||
IAuthEventSink auditSink,
|
IAuthEventSink auditSink,
|
||||||
TimeProvider clock,
|
TimeProvider clock,
|
||||||
ILogger<LegacyAuthDeprecationMiddleware> logger)
|
ILogger<LegacyAuthDeprecationMiddleware> logger)
|
||||||
{
|
{
|
||||||
this.next = next ?? throw new ArgumentNullException(nameof(next));
|
this.next = next ?? throw new ArgumentNullException(nameof(next));
|
||||||
if (authorityOptions is null)
|
if (authorityOptions is null)
|
||||||
{
|
{
|
||||||
throw new ArgumentNullException(nameof(authorityOptions));
|
throw new ArgumentNullException(nameof(authorityOptions));
|
||||||
}
|
}
|
||||||
|
|
||||||
options = authorityOptions.Value.ApiLifecycle.LegacyAuth ??
|
options = authorityOptions.Value.ApiLifecycle.LegacyAuth ??
|
||||||
throw new InvalidOperationException("Authority legacy auth endpoint options are not configured.");
|
throw new InvalidOperationException("Authority legacy auth endpoint options are not configured.");
|
||||||
this.auditSink = auditSink ?? throw new ArgumentNullException(nameof(auditSink));
|
this.auditSink = auditSink ?? throw new ArgumentNullException(nameof(auditSink));
|
||||||
this.clock = clock ?? throw new ArgumentNullException(nameof(clock));
|
this.clock = clock ?? throw new ArgumentNullException(nameof(clock));
|
||||||
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task InvokeAsync(HttpContext context)
|
public async Task InvokeAsync(HttpContext context)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(context);
|
ArgumentNullException.ThrowIfNull(context);
|
||||||
|
|
||||||
if (!options.Enabled)
|
if (!options.Enabled)
|
||||||
{
|
{
|
||||||
await next(context).ConfigureAwait(false);
|
await next(context).ConfigureAwait(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!TryResolveLegacyPath(context.Request.Path, out var canonicalPath))
|
if (!TryResolveLegacyPath(context.Request.Path, out var canonicalPath))
|
||||||
{
|
{
|
||||||
await next(context).ConfigureAwait(false);
|
await next(context).ConfigureAwait(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var originalPath = context.Request.Path;
|
var originalPath = context.Request.Path;
|
||||||
context.Request.Path = canonicalPath;
|
context.Request.Path = canonicalPath;
|
||||||
|
|
||||||
logger.LogInformation(
|
logger.LogInformation(
|
||||||
"Legacy Authority endpoint {OriginalPath} invoked; routing to {CanonicalPath} and emitting deprecation headers.",
|
"Legacy Authority endpoint {OriginalPath} invoked; routing to {CanonicalPath} and emitting deprecation headers.",
|
||||||
originalPath,
|
originalPath,
|
||||||
canonicalPath);
|
canonicalPath);
|
||||||
|
|
||||||
AppendDeprecationHeaders(context.Response);
|
AppendDeprecationHeaders(context.Response);
|
||||||
|
|
||||||
await next(context).ConfigureAwait(false);
|
await next(context).ConfigureAwait(false);
|
||||||
|
|
||||||
await EmitAuditAsync(context, originalPath, canonicalPath).ConfigureAwait(false);
|
await EmitAuditAsync(context, originalPath, canonicalPath).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool TryResolveLegacyPath(PathString path, out PathString canonicalPath)
|
private static bool TryResolveLegacyPath(PathString path, out PathString canonicalPath)
|
||||||
{
|
{
|
||||||
if (LegacyEndpointMap.TryGetValue(Normalize(path), out canonicalPath))
|
if (LegacyEndpointMap.TryGetValue(Normalize(path), out canonicalPath))
|
||||||
{
|
{
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
canonicalPath = PathString.Empty;
|
canonicalPath = PathString.Empty;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static PathString Normalize(PathString value)
|
private static PathString Normalize(PathString value)
|
||||||
{
|
{
|
||||||
if (!value.HasValue)
|
if (!value.HasValue)
|
||||||
{
|
{
|
||||||
return PathString.Empty;
|
return PathString.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
var trimmed = value.Value!.TrimEnd('/');
|
var trimmed = value.Value!.TrimEnd('/');
|
||||||
return new PathString(trimmed.Length == 0 ? "/" : trimmed.ToLowerInvariant());
|
return new PathString(trimmed.Length == 0 ? "/" : trimmed.ToLowerInvariant());
|
||||||
}
|
}
|
||||||
|
|
||||||
private void AppendDeprecationHeaders(HttpResponse response)
|
private void AppendDeprecationHeaders(HttpResponse response)
|
||||||
{
|
{
|
||||||
if (response.HasStarted)
|
if (response.HasStarted)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var deprecation = FormatHttpDate(options.DeprecationDate);
|
var deprecation = FormatHttpDate(options.DeprecationDate);
|
||||||
response.Headers["Deprecation"] = deprecation;
|
response.Headers["Deprecation"] = deprecation;
|
||||||
|
|
||||||
var sunset = FormatHttpDate(options.SunsetDate);
|
var sunset = FormatHttpDate(options.SunsetDate);
|
||||||
response.Headers[SunsetHeaderName] = sunset;
|
response.Headers[SunsetHeaderName] = sunset;
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(options.DocumentationUrl))
|
if (!string.IsNullOrWhiteSpace(options.DocumentationUrl))
|
||||||
{
|
{
|
||||||
var linkValue = $"<{options.DocumentationUrl}>; rel=\"sunset\"";
|
var linkValue = $"<{options.DocumentationUrl}>; rel=\"sunset\"";
|
||||||
response.Headers.Append(HeaderNames.Link, linkValue);
|
response.Headers.Append(HeaderNames.Link, linkValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
var warning = $"299 - \"Legacy Authority endpoint will be removed after {sunset}. Migrate to canonical endpoints before the sunset date.\"";
|
var warning = $"299 - \"Legacy Authority endpoint will be removed after {sunset}. Migrate to canonical endpoints before the sunset date.\"";
|
||||||
response.Headers[HeaderNames.Warning] = warning;
|
response.Headers[HeaderNames.Warning] = warning;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task EmitAuditAsync(HttpContext context, PathString originalPath, PathString canonicalPath)
|
private async Task EmitAuditAsync(HttpContext context, PathString originalPath, PathString canonicalPath)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var correlation = Activity.Current?.TraceId.ToString() ?? context.TraceIdentifier;
|
var correlation = Activity.Current?.TraceId.ToString() ?? context.TraceIdentifier;
|
||||||
|
|
||||||
var network = BuildNetwork(context);
|
var network = BuildNetwork(context);
|
||||||
|
|
||||||
var record = new AuthEventRecord
|
var record = new AuthEventRecord
|
||||||
{
|
{
|
||||||
EventType = LegacyEventType,
|
EventType = LegacyEventType,
|
||||||
OccurredAt = clock.GetUtcNow(),
|
OccurredAt = clock.GetUtcNow(),
|
||||||
CorrelationId = correlation,
|
CorrelationId = correlation,
|
||||||
Outcome = AuthEventOutcome.Success,
|
Outcome = AuthEventOutcome.Success,
|
||||||
Reason = null,
|
Reason = null,
|
||||||
Subject = null,
|
Subject = null,
|
||||||
Client = null,
|
Client = null,
|
||||||
Tenant = ClassifiedString.Empty,
|
Tenant = ClassifiedString.Empty,
|
||||||
Project = ClassifiedString.Empty,
|
Project = ClassifiedString.Empty,
|
||||||
Scopes = Array.Empty<string>(),
|
Scopes = Array.Empty<string>(),
|
||||||
Network = network,
|
Network = network,
|
||||||
Properties = BuildProperties(
|
Properties = BuildProperties(
|
||||||
("legacy.endpoint.original", originalPath.Value),
|
("legacy.endpoint.original", originalPath.Value),
|
||||||
("legacy.endpoint.canonical", canonicalPath.Value),
|
("legacy.endpoint.canonical", canonicalPath.Value),
|
||||||
("legacy.deprecation_at", options.DeprecationDate.ToString("O", CultureInfo.InvariantCulture)),
|
("legacy.deprecation_at", options.DeprecationDate.ToString("O", CultureInfo.InvariantCulture)),
|
||||||
("legacy.sunset_at", options.SunsetDate.ToString("O", CultureInfo.InvariantCulture)),
|
("legacy.sunset_at", options.SunsetDate.ToString("O", CultureInfo.InvariantCulture)),
|
||||||
("http.status_code", context.Response.StatusCode.ToString(CultureInfo.InvariantCulture)))
|
("http.status_code", context.Response.StatusCode.ToString(CultureInfo.InvariantCulture)))
|
||||||
};
|
};
|
||||||
|
|
||||||
await auditSink.WriteAsync(record, context.RequestAborted).ConfigureAwait(false);
|
await auditSink.WriteAsync(record, context.RequestAborted).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
logger.LogWarning(ex, "Failed to emit legacy auth endpoint audit event.");
|
logger.LogWarning(ex, "Failed to emit legacy auth endpoint audit event.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static AuthEventNetwork? BuildNetwork(HttpContext context)
|
private static AuthEventNetwork? BuildNetwork(HttpContext context)
|
||||||
{
|
{
|
||||||
var remote = context.Connection.RemoteIpAddress?.ToString();
|
var remote = context.Connection.RemoteIpAddress?.ToString();
|
||||||
var forwarded = context.Request.Headers["X-Forwarded-For"].ToString();
|
var forwarded = context.Request.Headers["X-Forwarded-For"].ToString();
|
||||||
var userAgent = context.Request.Headers.UserAgent.ToString();
|
var userAgent = context.Request.Headers.UserAgent.ToString();
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(remote) &&
|
if (string.IsNullOrWhiteSpace(remote) &&
|
||||||
string.IsNullOrWhiteSpace(forwarded) &&
|
string.IsNullOrWhiteSpace(forwarded) &&
|
||||||
string.IsNullOrWhiteSpace(userAgent))
|
string.IsNullOrWhiteSpace(userAgent))
|
||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return new AuthEventNetwork
|
return new AuthEventNetwork
|
||||||
{
|
{
|
||||||
RemoteAddress = ClassifiedString.Personal(Normalize(remote)),
|
RemoteAddress = ClassifiedString.Personal(Normalize(remote)),
|
||||||
ForwardedFor = ClassifiedString.Personal(Normalize(forwarded)),
|
ForwardedFor = ClassifiedString.Personal(Normalize(forwarded)),
|
||||||
UserAgent = ClassifiedString.Personal(Normalize(userAgent))
|
UserAgent = ClassifiedString.Personal(Normalize(userAgent))
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string? Normalize(string? value)
|
private static string? Normalize(string? value)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(value))
|
if (string.IsNullOrWhiteSpace(value))
|
||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
var trimmed = value.Trim();
|
var trimmed = value.Trim();
|
||||||
return trimmed.Length == 0 ? null : trimmed;
|
return trimmed.Length == 0 ? null : trimmed;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static IReadOnlyList<AuthEventProperty> BuildProperties(params (string Name, string? Value)[] entries)
|
private static IReadOnlyList<AuthEventProperty> BuildProperties(params (string Name, string? Value)[] entries)
|
||||||
{
|
{
|
||||||
if (entries.Length == 0)
|
if (entries.Length == 0)
|
||||||
{
|
{
|
||||||
return Array.Empty<AuthEventProperty>();
|
return Array.Empty<AuthEventProperty>();
|
||||||
}
|
}
|
||||||
|
|
||||||
var list = new List<AuthEventProperty>(entries.Length);
|
var list = new List<AuthEventProperty>(entries.Length);
|
||||||
foreach (var (name, value) in entries)
|
foreach (var (name, value) in entries)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(name))
|
if (string.IsNullOrWhiteSpace(name))
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
list.Add(new AuthEventProperty
|
list.Add(new AuthEventProperty
|
||||||
{
|
{
|
||||||
Name = name,
|
Name = name,
|
||||||
Value = string.IsNullOrWhiteSpace(value)
|
Value = string.IsNullOrWhiteSpace(value)
|
||||||
? ClassifiedString.Empty
|
? ClassifiedString.Empty
|
||||||
: ClassifiedString.Public(value)
|
: ClassifiedString.Public(value)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return list.Count == 0 ? Array.Empty<AuthEventProperty>() : list;
|
return list.Count == 0 ? Array.Empty<AuthEventProperty>() : list;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string FormatHttpDate(DateTimeOffset value)
|
private static string FormatHttpDate(DateTimeOffset value)
|
||||||
{
|
{
|
||||||
return value.UtcDateTime.ToString("r", CultureInfo.InvariantCulture);
|
return value.UtcDateTime.ToString("r", CultureInfo.InvariantCulture);
|
||||||
}
|
}
|
||||||
|
|
||||||
private sealed class PathStringComparer : IEqualityComparer<PathString>
|
private sealed class PathStringComparer : IEqualityComparer<PathString>
|
||||||
{
|
{
|
||||||
public static readonly PathStringComparer Instance = new();
|
public static readonly PathStringComparer Instance = new();
|
||||||
|
|
||||||
public bool Equals(PathString x, PathString y)
|
public bool Equals(PathString x, PathString y)
|
||||||
{
|
{
|
||||||
return string.Equals(Normalize(x).Value, Normalize(y).Value, StringComparison.Ordinal);
|
return string.Equals(Normalize(x).Value, Normalize(y).Value, StringComparison.Ordinal);
|
||||||
}
|
}
|
||||||
|
|
||||||
public int GetHashCode(PathString obj)
|
public int GetHashCode(PathString obj)
|
||||||
{
|
{
|
||||||
return Normalize(obj).Value?.GetHashCode(StringComparison.Ordinal) ?? 0;
|
return Normalize(obj).Value?.GetHashCode(StringComparison.Ordinal) ?? 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static class LegacyAuthDeprecationExtensions
|
internal static class LegacyAuthDeprecationExtensions
|
||||||
{
|
{
|
||||||
public static IApplicationBuilder UseLegacyAuthDeprecation(this IApplicationBuilder app)
|
public static IApplicationBuilder UseLegacyAuthDeprecation(this IApplicationBuilder app)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(app);
|
ArgumentNullException.ThrowIfNull(app);
|
||||||
return app.UseMiddleware<LegacyAuthDeprecationMiddleware>();
|
return app.UseMiddleware<LegacyAuthDeprecationMiddleware>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,181 +1,181 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Security.Cryptography;
|
using System.Security.Cryptography;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
using Microsoft.Extensions.Caching.Memory;
|
using Microsoft.Extensions.Caching.Memory;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using StellaOps.Configuration;
|
using StellaOps.Configuration;
|
||||||
using StellaOps.Cryptography;
|
using StellaOps.Cryptography;
|
||||||
|
|
||||||
namespace StellaOps.Authority.Signing;
|
namespace StellaOps.Authority.Signing;
|
||||||
|
|
||||||
internal sealed class AuthorityJwksService
|
internal sealed class AuthorityJwksService
|
||||||
{
|
{
|
||||||
private const string CacheKey = "authority:jwks:current";
|
private const string CacheKey = "authority:jwks:current";
|
||||||
private static readonly JsonSerializerOptions SerializerOptions = new(JsonSerializerDefaults.Web)
|
private static readonly JsonSerializerOptions SerializerOptions = new(JsonSerializerDefaults.Web)
|
||||||
{
|
{
|
||||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
|
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
|
||||||
};
|
};
|
||||||
|
|
||||||
private readonly ICryptoProviderRegistry registry;
|
private readonly ICryptoProviderRegistry registry;
|
||||||
private readonly ILogger<AuthorityJwksService> logger;
|
private readonly ILogger<AuthorityJwksService> logger;
|
||||||
private readonly IMemoryCache cache;
|
private readonly IMemoryCache cache;
|
||||||
private readonly TimeProvider timeProvider;
|
private readonly TimeProvider timeProvider;
|
||||||
private readonly StellaOpsAuthorityOptions authorityOptions;
|
private readonly StellaOpsAuthorityOptions authorityOptions;
|
||||||
|
|
||||||
public AuthorityJwksService(
|
public AuthorityJwksService(
|
||||||
ICryptoProviderRegistry registry,
|
ICryptoProviderRegistry registry,
|
||||||
ILogger<AuthorityJwksService> logger,
|
ILogger<AuthorityJwksService> logger,
|
||||||
IMemoryCache cache,
|
IMemoryCache cache,
|
||||||
TimeProvider timeProvider,
|
TimeProvider timeProvider,
|
||||||
IOptions<StellaOpsAuthorityOptions> authorityOptions)
|
IOptions<StellaOpsAuthorityOptions> authorityOptions)
|
||||||
{
|
{
|
||||||
this.registry = registry ?? throw new ArgumentNullException(nameof(registry));
|
this.registry = registry ?? throw new ArgumentNullException(nameof(registry));
|
||||||
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||||
this.cache = cache ?? throw new ArgumentNullException(nameof(cache));
|
this.cache = cache ?? throw new ArgumentNullException(nameof(cache));
|
||||||
this.timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider));
|
this.timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider));
|
||||||
if (authorityOptions is null)
|
if (authorityOptions is null)
|
||||||
{
|
{
|
||||||
throw new ArgumentNullException(nameof(authorityOptions));
|
throw new ArgumentNullException(nameof(authorityOptions));
|
||||||
}
|
}
|
||||||
|
|
||||||
this.authorityOptions = authorityOptions.Value ?? throw new ArgumentNullException(nameof(authorityOptions));
|
this.authorityOptions = authorityOptions.Value ?? throw new ArgumentNullException(nameof(authorityOptions));
|
||||||
}
|
}
|
||||||
|
|
||||||
public AuthorityJwksResult Get()
|
public AuthorityJwksResult Get()
|
||||||
{
|
{
|
||||||
if (cache.TryGetValue(CacheKey, out AuthorityJwksCacheEntry? cached) &&
|
if (cache.TryGetValue(CacheKey, out AuthorityJwksCacheEntry? cached) &&
|
||||||
cached is not null &&
|
cached is not null &&
|
||||||
cached.ExpiresAt > timeProvider.GetUtcNow())
|
cached.ExpiresAt > timeProvider.GetUtcNow())
|
||||||
{
|
{
|
||||||
return cached.Result;
|
return cached.Result;
|
||||||
}
|
}
|
||||||
|
|
||||||
var response = new AuthorityJwksResponse(BuildKeys());
|
var response = new AuthorityJwksResponse(BuildKeys());
|
||||||
var signingOptions = authorityOptions.Signing;
|
var signingOptions = authorityOptions.Signing;
|
||||||
var lifetime = signingOptions.JwksCacheLifetime > TimeSpan.Zero
|
var lifetime = signingOptions.JwksCacheLifetime > TimeSpan.Zero
|
||||||
? signingOptions.JwksCacheLifetime
|
? signingOptions.JwksCacheLifetime
|
||||||
: TimeSpan.FromMinutes(5);
|
: TimeSpan.FromMinutes(5);
|
||||||
var expires = timeProvider.GetUtcNow().Add(lifetime);
|
var expires = timeProvider.GetUtcNow().Add(lifetime);
|
||||||
var etag = ComputeEtag(response, expires);
|
var etag = ComputeEtag(response, expires);
|
||||||
var cacheControl = $"public, max-age={(int)lifetime.TotalSeconds}";
|
var cacheControl = $"public, max-age={(int)lifetime.TotalSeconds}";
|
||||||
|
|
||||||
var result = new AuthorityJwksResult(response, etag, expires, cacheControl);
|
var result = new AuthorityJwksResult(response, etag, expires, cacheControl);
|
||||||
var entry = new AuthorityJwksCacheEntry(result, expires);
|
var entry = new AuthorityJwksCacheEntry(result, expires);
|
||||||
|
|
||||||
cache.Set(CacheKey, entry, new MemoryCacheEntryOptions
|
cache.Set(CacheKey, entry, new MemoryCacheEntryOptions
|
||||||
{
|
{
|
||||||
AbsoluteExpirationRelativeToNow = lifetime
|
AbsoluteExpirationRelativeToNow = lifetime
|
||||||
});
|
});
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Invalidate()
|
public void Invalidate()
|
||||||
{
|
{
|
||||||
cache.Remove(CacheKey);
|
cache.Remove(CacheKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
private IReadOnlyCollection<JwksKeyEntry> BuildKeys()
|
private IReadOnlyCollection<JwksKeyEntry> BuildKeys()
|
||||||
{
|
{
|
||||||
var keys = new List<JwksKeyEntry>();
|
var keys = new List<JwksKeyEntry>();
|
||||||
var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
foreach (var provider in registry.Providers)
|
foreach (var provider in registry.Providers)
|
||||||
{
|
{
|
||||||
foreach (var signingKey in provider.GetSigningKeys())
|
foreach (var signingKey in provider.GetSigningKeys())
|
||||||
{
|
{
|
||||||
var keyId = signingKey.Reference.KeyId;
|
var keyId = signingKey.Reference.KeyId;
|
||||||
if (!seen.Add(keyId))
|
if (!seen.Add(keyId))
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var signer = provider.GetSigner(signingKey.AlgorithmId, signingKey.Reference);
|
var signer = provider.GetSigner(signingKey.AlgorithmId, signingKey.Reference);
|
||||||
var jwk = signer.ExportPublicJsonWebKey();
|
var jwk = signer.ExportPublicJsonWebKey();
|
||||||
var keyUse = signingKey.Metadata.TryGetValue("use", out var metadataUse) && !string.IsNullOrWhiteSpace(metadataUse)
|
var keyUse = signingKey.Metadata.TryGetValue("use", out var metadataUse) && !string.IsNullOrWhiteSpace(metadataUse)
|
||||||
? metadataUse
|
? metadataUse
|
||||||
: jwk.Use;
|
: jwk.Use;
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(keyUse))
|
if (string.IsNullOrWhiteSpace(keyUse))
|
||||||
{
|
{
|
||||||
keyUse = "sig";
|
keyUse = "sig";
|
||||||
}
|
}
|
||||||
|
|
||||||
var entry = new JwksKeyEntry
|
var entry = new JwksKeyEntry
|
||||||
{
|
{
|
||||||
Kid = jwk.Kid,
|
Kid = jwk.Kid,
|
||||||
Kty = jwk.Kty,
|
Kty = jwk.Kty,
|
||||||
Use = keyUse,
|
Use = keyUse,
|
||||||
Alg = jwk.Alg,
|
Alg = jwk.Alg,
|
||||||
Crv = jwk.Crv,
|
Crv = jwk.Crv,
|
||||||
X = jwk.X,
|
X = jwk.X,
|
||||||
Y = jwk.Y,
|
Y = jwk.Y,
|
||||||
Status = signingKey.Metadata.TryGetValue("status", out var status) ? status : "active"
|
Status = signingKey.Metadata.TryGetValue("status", out var status) ? status : "active"
|
||||||
};
|
};
|
||||||
keys.Add(entry);
|
keys.Add(entry);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
logger.LogWarning(ex, "Failed to export JWKS entry for key {KeyId}.", keyId);
|
logger.LogWarning(ex, "Failed to export JWKS entry for key {KeyId}.", keyId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
keys.Sort(static (left, right) => string.Compare(left.Kid, right.Kid, StringComparison.Ordinal));
|
keys.Sort(static (left, right) => string.Compare(left.Kid, right.Kid, StringComparison.Ordinal));
|
||||||
return keys;
|
return keys;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string ComputeEtag(AuthorityJwksResponse response, DateTimeOffset expiresAt)
|
private static string ComputeEtag(AuthorityJwksResponse response, DateTimeOffset expiresAt)
|
||||||
{
|
{
|
||||||
var payload = JsonSerializer.Serialize(response, SerializerOptions);
|
var payload = JsonSerializer.Serialize(response, SerializerOptions);
|
||||||
var buffer = Encoding.UTF8.GetBytes(payload + "|" + expiresAt.ToUnixTimeSeconds().ToString(CultureInfo.InvariantCulture));
|
var buffer = Encoding.UTF8.GetBytes(payload + "|" + expiresAt.ToUnixTimeSeconds().ToString(CultureInfo.InvariantCulture));
|
||||||
var hash = SHA256.HashData(buffer);
|
var hash = SHA256.HashData(buffer);
|
||||||
return $"\"{Convert.ToHexString(hash)}\"";
|
return $"\"{Convert.ToHexString(hash)}\"";
|
||||||
}
|
}
|
||||||
|
|
||||||
private sealed record AuthorityJwksCacheEntry(AuthorityJwksResult Result, DateTimeOffset ExpiresAt);
|
private sealed record AuthorityJwksCacheEntry(AuthorityJwksResult Result, DateTimeOffset ExpiresAt);
|
||||||
}
|
}
|
||||||
|
|
||||||
internal sealed record AuthorityJwksResponse([property: JsonPropertyName("keys")] IReadOnlyCollection<JwksKeyEntry> Keys);
|
internal sealed record AuthorityJwksResponse([property: JsonPropertyName("keys")] IReadOnlyCollection<JwksKeyEntry> Keys);
|
||||||
|
|
||||||
internal sealed record AuthorityJwksResult(
|
internal sealed record AuthorityJwksResult(
|
||||||
AuthorityJwksResponse Response,
|
AuthorityJwksResponse Response,
|
||||||
string ETag,
|
string ETag,
|
||||||
DateTimeOffset ExpiresAt,
|
DateTimeOffset ExpiresAt,
|
||||||
string CacheControl);
|
string CacheControl);
|
||||||
|
|
||||||
internal sealed class JwksKeyEntry
|
internal sealed class JwksKeyEntry
|
||||||
{
|
{
|
||||||
[JsonPropertyName("kty")]
|
[JsonPropertyName("kty")]
|
||||||
public string? Kty { get; set; }
|
public string? Kty { get; set; }
|
||||||
|
|
||||||
[JsonPropertyName("use")]
|
[JsonPropertyName("use")]
|
||||||
public string? Use { get; set; }
|
public string? Use { get; set; }
|
||||||
|
|
||||||
[JsonPropertyName("kid")]
|
[JsonPropertyName("kid")]
|
||||||
public string? Kid { get; set; }
|
public string? Kid { get; set; }
|
||||||
|
|
||||||
[JsonPropertyName("alg")]
|
[JsonPropertyName("alg")]
|
||||||
public string? Alg { get; set; }
|
public string? Alg { get; set; }
|
||||||
|
|
||||||
[JsonPropertyName("crv")]
|
[JsonPropertyName("crv")]
|
||||||
public string? Crv { get; set; }
|
public string? Crv { get; set; }
|
||||||
|
|
||||||
[JsonPropertyName("x")]
|
[JsonPropertyName("x")]
|
||||||
public string? X { get; set; }
|
public string? X { get; set; }
|
||||||
|
|
||||||
[JsonPropertyName("y")]
|
[JsonPropertyName("y")]
|
||||||
public string? Y { get; set; }
|
public string? Y { get; set; }
|
||||||
|
|
||||||
[JsonPropertyName("status")]
|
[JsonPropertyName("status")]
|
||||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||||
public string? Status { get; set; }
|
public string? Status { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,170 +1,170 @@
|
|||||||
# Authority Host Task Board — Epic 1: Aggregation-Only Contract
|
# Authority Host Task Board — Epic 1: Aggregation-Only Contract
|
||||||
| ID | Status | Owner(s) | Depends on | Description | Exit Criteria |
|
| ID | Status | Owner(s) | Depends on | Description | Exit Criteria |
|
||||||
|----|--------|----------|------------|-------------|---------------|
|
|----|--------|----------|------------|-------------|---------------|
|
||||||
> 2025-10-26: Rate limiter metadata/audit records now include tenants, password grant scopes/tenants enforced, token persistence + tests updated. Docs refresh tracked via AUTH-AOC-19-003.
|
> 2025-10-26: Rate limiter metadata/audit records now include tenants, password grant scopes/tenants enforced, token persistence + tests updated. Docs refresh tracked via AUTH-AOC-19-003.
|
||||||
> 2025-10-27: Client credential ingestion scopes now require tenant assignment; access token validation backfills tenants and rejects cross-tenant mismatches with tests.
|
> 2025-10-27: Client credential ingestion scopes now require tenant assignment; access token validation backfills tenants and rejects cross-tenant mismatches with tests.
|
||||||
> 2025-10-27: `dotnet test` blocked — Concelier build fails (`AdvisoryObservationQueryService` returns `ImmutableHashSet<string?>`), preventing Authority test suite run; waiting on Concelier fix before rerun.
|
> 2025-10-27: `dotnet test` blocked — Concelier build fails (`AdvisoryObservationQueryService` returns `ImmutableHashSet<string?>`), preventing Authority test suite run; waiting on Concelier fix before rerun.
|
||||||
> 2025-10-26: Docs updated (`docs/11_AUTHORITY.md`, Concelier audit runbook, `docs/security/authority-scopes.md`); sample config highlights tenant-aware clients. Release notes + smoke verification pending (blocked on Concelier/Excititor smoke updates).
|
> 2025-10-26: Docs updated (`docs/11_AUTHORITY.md`, Concelier audit runbook, `docs/security/authority-scopes.md`); sample config highlights tenant-aware clients. Release notes + smoke verification pending (blocked on Concelier/Excititor smoke updates).
|
||||||
> 2025-10-27: Scope catalogue aligned with `advisory:ingest/advisory:read/vex:ingest/vex:read`, `aoc:verify` pairing documented, console/CLI references refreshed, and `etc/authority.yaml.sample` updated to require read scopes for verification clients.
|
> 2025-10-27: Scope catalogue aligned with `advisory:ingest/advisory:read/vex:ingest/vex:read`, `aoc:verify` pairing documented, console/CLI references refreshed, and `etc/authority.yaml.sample` updated to require read scopes for verification clients.
|
||||||
> 2025-10-31: Client credentials and password grants now reject advisory/vex read or signals scopes without `aoc:verify`, enforce tenant assignment for `aoc:verify`, tag violations via `authority.aoc_scope_violation`, extend tests, and refresh scope catalogue docs/sample roles.
|
> 2025-10-31: Client credentials and password grants now reject advisory/vex read or signals scopes without `aoc:verify`, enforce tenant assignment for `aoc:verify`, tag violations via `authority.aoc_scope_violation`, extend tests, and refresh scope catalogue docs/sample roles.
|
||||||
|
|
||||||
## Link-Not-Merge v1
|
## Link-Not-Merge v1
|
||||||
|
|
||||||
| ID | Status | Owner(s) | Depends on | Description | Exit Criteria |
|
| ID | Status | Owner(s) | Depends on | Description | Exit Criteria |
|
||||||
|----|--------|----------|------------|-------------|---------------|
|
|----|--------|----------|------------|-------------|---------------|
|
||||||
> 2025-10-29: Rejected legacy `concelier.merge` scope during client credential validation, removed it from known scope catalog, blocked discovery/issuance, added regression tests, and refreshed scope documentation.
|
> 2025-10-29: Rejected legacy `concelier.merge` scope during client credential validation, removed it from known scope catalog, blocked discovery/issuance, added regression tests, and refreshed scope documentation.
|
||||||
|
|
||||||
## Policy Engine v2
|
## Policy Engine v2
|
||||||
|
|
||||||
| ID | Status | Owner(s) | Depends on | Description | Exit Criteria |
|
| ID | Status | Owner(s) | Depends on | Description | Exit Criteria |
|
||||||
|----|--------|----------|------------|-------------|---------------|
|
|----|--------|----------|------------|-------------|---------------|
|
||||||
> 2025-10-26: Restricted `effective:write` to Policy Engine service identities with tenant requirement, registered full scope set, and tightened resource server default scope enforcement (unit tests pass).
|
> 2025-10-26: Restricted `effective:write` to Policy Engine service identities with tenant requirement, registered full scope set, and tightened resource server default scope enforcement (unit tests pass).
|
||||||
> 2025-10-26: Authority docs now detail policy scopes/service identity guardrails with checklist; `authority.yaml.sample` includes `properties.serviceIdentity` example.
|
> 2025-10-26: Authority docs now detail policy scopes/service identity guardrails with checklist; `authority.yaml.sample` includes `properties.serviceIdentity` example.
|
||||||
|
|
||||||
## Graph Explorer v1
|
## Graph Explorer v1
|
||||||
|
|
||||||
| ID | Status | Owner(s) | Depends on | Description | Exit Criteria |
|
| ID | Status | Owner(s) | Depends on | Description | Exit Criteria |
|
||||||
|----|--------|----------|------------|-------------|---------------|
|
|----|--------|----------|------------|-------------|---------------|
|
||||||
|
|
||||||
## Policy Engine + Editor v1
|
## Policy Engine + Editor v1
|
||||||
|
|
||||||
| ID | Status | Owner(s) | Depends on | Description | Exit Criteria |
|
| ID | Status | Owner(s) | Depends on | Description | Exit Criteria |
|
||||||
|----|--------|----------|------------|-------------|---------------|
|
|----|--------|----------|------------|-------------|---------------|
|
||||||
| AUTH-POLICY-23-002 | BLOCKED (2025-10-29) | Authority Core & Security Guild | AUTH-POLICY-23-001 | Implement optional two-person rule for activation: require two distinct `policy:activate` approvals when configured; emit audit logs. | Activation endpoint enforces rule; audit logs contain approver IDs; tests cover 2-person path. |
|
| AUTH-POLICY-23-002 | BLOCKED (2025-10-29) | Authority Core & Security Guild | AUTH-POLICY-23-001 | Implement optional two-person rule for activation: require two distinct `policy:activate` approvals when configured; emit audit logs. | Activation endpoint enforces rule; audit logs contain approver IDs; tests cover 2-person path. |
|
||||||
> Blocked: Policy Engine/Studio have not yet exposed activation workflow endpoints or approval payloads needed to enforce dual-control (`WEB-POLICY-23-002`, `POLICY-ENGINE-23-002`). Revisit once activation contract lands.
|
> Blocked: Policy Engine/Studio have not yet exposed activation workflow endpoints or approval payloads needed to enforce dual-control (`WEB-POLICY-23-002`, `POLICY-ENGINE-23-002`). Revisit once activation contract lands.
|
||||||
| AUTH-POLICY-23-003 | BLOCKED (2025-10-29) | Authority Core & Docs Guild | AUTH-POLICY-23-001 | Update documentation and sample configs for policy roles, approval workflow, and signing requirements. | Docs updated with reviewer checklist; configuration examples validated. |
|
| AUTH-POLICY-23-003 | BLOCKED (2025-10-29) | Authority Core & Docs Guild | AUTH-POLICY-23-001 | Update documentation and sample configs for policy roles, approval workflow, and signing requirements. | Docs updated with reviewer checklist; configuration examples validated. |
|
||||||
> Blocked pending AUTH-POLICY-23-002 dual-approval implementation so docs can capture final activation behaviour.
|
> Blocked pending AUTH-POLICY-23-002 dual-approval implementation so docs can capture final activation behaviour.
|
||||||
> 2025-10-27: Added `policy-cli` defaults to Authority config/secrets, refreshed CLI/CI documentation with the new scope bundle, recorded release migration guidance, and introduced `scripts/verify-policy-scopes.py` to guard against regressions.
|
> 2025-10-27: Added `policy-cli` defaults to Authority config/secrets, refreshed CLI/CI documentation with the new scope bundle, recorded release migration guidance, and introduced `scripts/verify-policy-scopes.py` to guard against regressions.
|
||||||
|
|
||||||
## Graph & Vuln Explorer v1
|
## Graph & Vuln Explorer v1
|
||||||
|
|
||||||
| ID | Status | Owner(s) | Depends on | Description | Exit Criteria |
|
| ID | Status | Owner(s) | Depends on | Description | Exit Criteria |
|
||||||
|----|--------|----------|------------|-------------|---------------|
|
|----|--------|----------|------------|-------------|---------------|
|
||||||
> 2025-10-27: Paused work after exploratory spike (scope enforcement still outstanding); no functional changes merged.
|
> 2025-10-27: Paused work after exploratory spike (scope enforcement still outstanding); no functional changes merged.
|
||||||
|
|
||||||
## Orchestrator Dashboard
|
## Orchestrator Dashboard
|
||||||
|
|
||||||
| ID | Status | Owner(s) | Depends on | Description | Exit Criteria |
|
| ID | Status | Owner(s) | Depends on | Description | Exit Criteria |
|
||||||
|----|--------|----------|------------|-------------|---------------|
|
|----|--------|----------|------------|-------------|---------------|
|
||||||
> 2025-10-31: Picked up during Console/Orchestrator alignment; focusing on scope catalog + tenant enforcement first.
|
> 2025-10-31: Picked up during Console/Orchestrator alignment; focusing on scope catalog + tenant enforcement first.
|
||||||
> 2025-10-31: `orch:read` added to scope catalogue and Authority runtime, Console defaults include the scope, `Orch.Viewer` role documented, and client-credential tests enforce tenant requirements.
|
> 2025-10-31: `orch:read` added to scope catalogue and Authority runtime, Console defaults include the scope, `Orch.Viewer` role documented, and client-credential tests enforce tenant requirements.
|
||||||
> 2025-10-27: Added `orch:operate` scope, enforced `operator_reason`/`operator_ticket` on token issuance, updated Authority configs/docs, and captured audit metadata for control actions.
|
> 2025-10-27: Added `orch:operate` scope, enforced `operator_reason`/`operator_ticket` on token issuance, updated Authority configs/docs, and captured audit metadata for control actions.
|
||||||
> 2025-10-28: Policy gateway + scanner now pass the expanded token client signature (`null` metadata by default), test stubs capture the optional parameters, and Policy Gateway/Scanner suites are green after fixing the Concelier storage build break.
|
> 2025-10-28: Policy gateway + scanner now pass the expanded token client signature (`null` metadata by default), test stubs capture the optional parameters, and Policy Gateway/Scanner suites are green after fixing the Concelier storage build break.
|
||||||
> 2025-10-28: Authority password-grant tests now hit the new constructors but still need updates to drop obsolete `IOptions` arguments before the suite can pass.
|
> 2025-10-28: Authority password-grant tests now hit the new constructors but still need updates to drop obsolete `IOptions` arguments before the suite can pass.
|
||||||
| AUTH-ORCH-34-001 | DONE (2025-11-02) | Authority Core & Security Guild | AUTH-ORCH-33-001 | Introduce `Orch.Admin` role with quota/backfill scopes, enforce audit reason on quota changes, and update offline defaults/docs. | Admin role available; quotas/backfills require scope + reason; tests confirm tenant isolation; documentation updated. |
|
| AUTH-ORCH-34-001 | DONE (2025-11-02) | Authority Core & Security Guild | AUTH-ORCH-33-001 | Introduce `Orch.Admin` role with quota/backfill scopes, enforce audit reason on quota changes, and update offline defaults/docs. | Admin role available; quotas/backfills require scope + reason; tests confirm tenant isolation; documentation updated. |
|
||||||
> 2025-11-02: `orch:backfill` scope added with mandatory `backfill_reason`/`backfill_ticket`, client-credential validation and resource authorization paths emit audit fields, CLI picks up new configuration/env vars, and Authority docs/config samples updated for `Orch.Admin`.
|
> 2025-11-02: `orch:backfill` scope added with mandatory `backfill_reason`/`backfill_ticket`, client-credential validation and resource authorization paths emit audit fields, CLI picks up new configuration/env vars, and Authority docs/config samples updated for `Orch.Admin`.
|
||||||
|
|
||||||
## StellaOps Console (Sprint 23)
|
## StellaOps Console (Sprint 23)
|
||||||
|
|
||||||
| ID | Status | Owner(s) | Depends on | Description | Exit Criteria |
|
| ID | Status | Owner(s) | Depends on | Description | Exit Criteria |
|
||||||
|----|--------|----------|------------|-------------|---------------|
|
|----|--------|----------|------------|-------------|---------------|
|
||||||
> 2025-10-29: Authorization code flow enabled with PKCE requirement, console client seeded in `authority.yaml.sample`, discovery docs updated, and console runbook guidance added.
|
> 2025-10-29: Authorization code flow enabled with PKCE requirement, console client seeded in `authority.yaml.sample`, discovery docs updated, and console runbook guidance added.
|
||||||
> 2025-10-31: Added `/console/tenants`, `/console/profile`, `/console/token/introspect` endpoints with tenant header filter, scope enforcement (`ui.read`, `authority:tenants.read`), and structured audit events. Console test harness covers success/mismatch cases.
|
> 2025-10-31: Added `/console/tenants`, `/console/profile`, `/console/token/introspect` endpoints with tenant header filter, scope enforcement (`ui.read`, `authority:tenants.read`), and structured audit events. Console test harness covers success/mismatch cases.
|
||||||
> 2025-10-28: `docs/security/console-security.md` drafted with PKCE + DPoP (120 s OpTok, 300 s fresh-auth) and scope table. Authority Core to confirm `/fresh-auth` semantics, token lifetimes, and scope bundles align before closing task.
|
> 2025-10-28: `docs/security/console-security.md` drafted with PKCE + DPoP (120 s OpTok, 300 s fresh-auth) and scope table. Authority Core to confirm `/fresh-auth` semantics, token lifetimes, and scope bundles align before closing task.
|
||||||
> 2025-10-31: Security guide expanded for `/console` endpoints & orchestrator scope, sample YAML annotated, ops runbook updated, and release note `docs/updates/2025-10-31-console-security-refresh.md` published.
|
> 2025-10-31: Security guide expanded for `/console` endpoints & orchestrator scope, sample YAML annotated, ops runbook updated, and release note `docs/updates/2025-10-31-console-security-refresh.md` published.
|
||||||
> 2025-10-31: Default access-token lifetime reduced to 120 s, console tests updated with dual auth schemes, docs/config/ops notes refreshed, release note logged.
|
> 2025-10-31: Default access-token lifetime reduced to 120 s, console tests updated with dual auth schemes, docs/config/ops notes refreshed, release note logged.
|
||||||
|
|
||||||
## Policy Studio (Sprint 27)
|
## Policy Studio (Sprint 27)
|
||||||
|
|
||||||
| ID | Status | Owner(s) | Depends on | Description | Exit Criteria |
|
| ID | Status | Owner(s) | Depends on | Description | Exit Criteria |
|
||||||
|----|--------|----------|------------|-------------|---------------|
|
|----|--------|----------|------------|-------------|---------------|
|
||||||
> 2025-10-31: Added Policy Studio scope family (`policy:author/review/operate/audit`), updated OpenAPI + discovery headers, enforced tenant requirements in grant handlers, seeded new roles in Authority config/offline kit docs, and refreshed CLI/Console documentation + tests to validate the new catalogue.
|
> 2025-10-31: Added Policy Studio scope family (`policy:author/review/operate/audit`), updated OpenAPI + discovery headers, enforced tenant requirements in grant handlers, seeded new roles in Authority config/offline kit docs, and refreshed CLI/Console documentation + tests to validate the new catalogue.
|
||||||
| AUTH-POLICY-27-002 | DONE (2025-11-02) | Authority Core & Security Guild | AUTH-POLICY-27-001, REGISTRY-API-27-007 | Provide attestation signing service bindings (OIDC token exchange, cosign integration) and enforce publish/promote scope checks, fresh-auth requirements, and audit logging. | Publish/promote requests require fresh auth + correct scopes; attestations signed with validated identity; audit logs enriched with digest + tenant; integration tests pass. |
|
| AUTH-POLICY-27-002 | DONE (2025-11-02) | Authority Core & Security Guild | AUTH-POLICY-27-001, REGISTRY-API-27-007 | Provide attestation signing service bindings (OIDC token exchange, cosign integration) and enforce publish/promote scope checks, fresh-auth requirements, and audit logging. | Publish/promote requests require fresh auth + correct scopes; attestations signed with validated identity; audit logs enriched with digest + tenant; integration tests pass. |
|
||||||
> Docs dependency: `DOCS-POLICY-27-009` awaiting signing guidance from this work.
|
> Docs dependency: `DOCS-POLICY-27-009` awaiting signing guidance from this work.
|
||||||
> 2025-11-02: Added `policy:publish`/`policy:promote` scopes with interactive-only enforcement, metadata parameters (`policy_reason`, `policy_ticket`, `policy_digest`), fresh-auth token validation, audit augmentations, and updated config/docs references.
|
> 2025-11-02: Added `policy:publish`/`policy:promote` scopes with interactive-only enforcement, metadata parameters (`policy_reason`, `policy_ticket`, `policy_digest`), fresh-auth token validation, audit augmentations, and updated config/docs references.
|
||||||
| AUTH-POLICY-27-003 | DONE (2025-11-04) | Authority Core & Docs Guild | AUTH-POLICY-27-001, AUTH-POLICY-27-002 | Update Authority configuration/docs for Policy Studio roles, signing policies, approval workflows, and CLI integration; include compliance checklist. | Docs merged; samples validated; governance checklist appended; release notes updated. |
|
| AUTH-POLICY-27-003 | DONE (2025-11-04) | Authority Core & Docs Guild | AUTH-POLICY-27-001, AUTH-POLICY-27-002 | Update Authority configuration/docs for Policy Studio roles, signing policies, approval workflows, and CLI integration; include compliance checklist. | Docs merged; samples validated; governance checklist appended; release notes updated. |
|
||||||
> 2025-11-04: Policy Studio roles/scopes documented across `docs/11_AUTHORITY.md`, sample configs, and OpenAPI; compliance checklist appended and Authority tests rerun to validate fresh-auth + scope enforcement.
|
> 2025-11-04: Policy Studio roles/scopes documented across `docs/11_AUTHORITY.md`, sample configs, and OpenAPI; compliance checklist appended and Authority tests rerun to validate fresh-auth + scope enforcement.
|
||||||
|
|
||||||
## Exceptions v1
|
## Exceptions v1
|
||||||
|
|
||||||
| ID | Status | Owner(s) | Depends on | Description | Exit Criteria |
|
| ID | Status | Owner(s) | Depends on | Description | Exit Criteria |
|
||||||
|----|--------|----------|------------|-------------|---------------|
|
|----|--------|----------|------------|-------------|---------------|
|
||||||
> 2025-10-29: Added exception scopes + routing template options, enforced MFA requirement in password grant handlers, updated configuration samples.
|
> 2025-10-29: Added exception scopes + routing template options, enforced MFA requirement in password grant handlers, updated configuration samples.
|
||||||
> 2025-10-31: Authority scopes/routing docs updated (`docs/security/authority-scopes.md`, `docs/11_AUTHORITY.md`, `docs/policy/exception-effects.md`), monitoring guide covers new MFA audit events, and `etc/authority.yaml.sample` now demonstrates exception clients/templates.
|
> 2025-10-31: Authority scopes/routing docs updated (`docs/security/authority-scopes.md`, `docs/11_AUTHORITY.md`, `docs/policy/exception-effects.md`), monitoring guide covers new MFA audit events, and `etc/authority.yaml.sample` now demonstrates exception clients/templates.
|
||||||
|
|
||||||
## Reachability v1
|
## Reachability v1
|
||||||
|
|
||||||
| ID | Status | Owner(s) | Depends on | Description | Exit Criteria |
|
| ID | Status | Owner(s) | Depends on | Description | Exit Criteria |
|
||||||
|----|--------|----------|------------|-------------|---------------|
|
|----|--------|----------|------------|-------------|---------------|
|
||||||
> 2025-10-29: Signals scopes added with tenant + aoc:verify enforcement; sensors guided via SignalsUploader template; tests cover gating.
|
> 2025-10-29: Signals scopes added with tenant + aoc:verify enforcement; sensors guided via SignalsUploader template; tests cover gating.
|
||||||
|
|
||||||
## Vulnerability Explorer (Sprint 29)
|
## Vulnerability Explorer (Sprint 29)
|
||||||
|
|
||||||
| ID | Status | Owner(s) | Depends on | Description | Exit Criteria |
|
| ID | Status | Owner(s) | Depends on | Description | Exit Criteria |
|
||||||
|----|--------|----------|------------|-------------|---------------|
|
|----|--------|----------|------------|-------------|---------------|
|
||||||
| AUTH-VULN-29-001 | DONE (2025-11-03) | Authority Core & Security Guild | AUTH-POLICY-27-001 | Define Vuln Explorer scopes/roles (`vuln:view`, `vuln:investigate`, `vuln:operate`, `vuln:audit`) with ABAC attributes (env, owner, business_tier) and update discovery metadata/offline kit defaults. | Roles/scopes published; issuer templates updated; integration tests cover ABAC filters; docs refreshed. |
|
| AUTH-VULN-29-001 | DONE (2025-11-03) | Authority Core & Security Guild | AUTH-POLICY-27-001 | Define Vuln Explorer scopes/roles (`vuln:view`, `vuln:investigate`, `vuln:operate`, `vuln:audit`) with ABAC attributes (env, owner, business_tier) and update discovery metadata/offline kit defaults. | Roles/scopes published; issuer templates updated; integration tests cover ABAC filters; docs refreshed. |
|
||||||
| AUTH-VULN-29-002 | DONE (2025-11-03) | Authority Core & Security Guild | AUTH-VULN-29-001, LEDGER-29-002 | Enforce CSRF/anti-forgery tokens for workflow actions, sign attachment tokens, and record audit logs with ledger event hashes. | Workflow calls require valid tokens; audit logs include ledger references; security tests cover token expiry/abuse. |
|
| AUTH-VULN-29-002 | DONE (2025-11-03) | Authority Core & Security Guild | AUTH-VULN-29-001, LEDGER-29-002 | Enforce CSRF/anti-forgery tokens for workflow actions, sign attachment tokens, and record audit logs with ledger event hashes. | Workflow calls require valid tokens; audit logs include ledger references; security tests cover token expiry/abuse. |
|
||||||
| AUTH-VULN-29-003 | DONE (2025-11-04) | Authority Core & Docs Guild | AUTH-VULN-29-001..002 | Update security docs/config samples for Vuln Explorer roles, ABAC policies, attachment signing, and ledger verification guidance. | Docs merged with compliance checklist; configuration examples validated; release notes updated. |
|
| AUTH-VULN-29-003 | DONE (2025-11-04) | Authority Core & Docs Guild | AUTH-VULN-29-001..002 | Update security docs/config samples for Vuln Explorer roles, ABAC policies, attachment signing, and ledger verification guidance. | Docs merged with compliance checklist; configuration examples validated; release notes updated. |
|
||||||
> 2025-11-03: Vuln workflow CSRF + attachment token services live with audit enrichment and negative-path tests. Awaiting completion of full Authority suite run after repository-wide build finishes.
|
> 2025-11-03: Vuln workflow CSRF + attachment token services live with audit enrichment and negative-path tests. Awaiting completion of full Authority suite run after repository-wide build finishes.
|
||||||
> 2025-11-04: Verified Vuln Explorer RBAC/ABAC coverage in Authority docs/security guides, attachment token guidance, and offline samples; Authority tests rerun confirming ledger-token + anti-forgery behaviours.
|
> 2025-11-04: Verified Vuln Explorer RBAC/ABAC coverage in Authority docs/security guides, attachment token guidance, and offline samples; Authority tests rerun confirming ledger-token + anti-forgery behaviours.
|
||||||
|
|
||||||
## Advisory AI (Sprint 31)
|
## Advisory AI (Sprint 31)
|
||||||
|
|
||||||
| ID | Status | Owner(s) | Depends on | Description | Exit Criteria |
|
| ID | Status | Owner(s) | Depends on | Description | Exit Criteria |
|
||||||
|----|--------|----------|------------|-------------|---------------|
|
|----|--------|----------|------------|-------------|---------------|
|
||||||
| AUTH-AIAI-31-001 | DONE (2025-11-01) | Authority Core & Security Guild | AUTH-VULN-29-001 | Define Advisory AI scopes (`advisory-ai:view`, `advisory-ai:operate`, `advisory-ai:admin`) and remote inference toggles; update discovery metadata/offline defaults. | Scopes/flags published; integration tests cover RBAC + opt-in settings; docs updated. |
|
| AUTH-AIAI-31-001 | DONE (2025-11-01) | Authority Core & Security Guild | AUTH-VULN-29-001 | Define Advisory AI scopes (`advisory-ai:view`, `advisory-ai:operate`, `advisory-ai:admin`) and remote inference toggles; update discovery metadata/offline defaults. | Scopes/flags published; integration tests cover RBAC + opt-in settings; docs updated. |
|
||||||
| AUTH-AIAI-31-002 | DONE (2025-11-01) | Authority Core & Security Guild | AUTH-AIAI-31-001, AIAI-31-006 | Enforce anonymized prompt logging, tenant consent for remote inference, and audit logging of assistant tasks. | Logging/audit flows verified; privacy review passed; docs updated. |
|
| AUTH-AIAI-31-002 | DONE (2025-11-01) | Authority Core & Security Guild | AUTH-AIAI-31-001, AIAI-31-006 | Enforce anonymized prompt logging, tenant consent for remote inference, and audit logging of assistant tasks. | Logging/audit flows verified; privacy review passed; docs updated. |
|
||||||
|
|
||||||
## Export Center
|
## Export Center
|
||||||
| ID | Status | Owner(s) | Depends on | Description | Exit Criteria |
|
| ID | Status | Owner(s) | Depends on | Description | Exit Criteria |
|
||||||
|----|--------|----------|------------|-------------|---------------|
|
|----|--------|----------|------------|-------------|---------------|
|
||||||
|
|
||||||
## Notifications Studio
|
## Notifications Studio
|
||||||
| ID | Status | Owner(s) | Depends on | Description | Exit Criteria |
|
| ID | Status | Owner(s) | Depends on | Description | Exit Criteria |
|
||||||
|----|--------|----------|------------|-------------|---------------|
|
|----|--------|----------|------------|-------------|---------------|
|
||||||
| AUTH-NOTIFY-38-001 | DONE (2025-11-01) | Authority Core & Security Guild | — | Define `Notify.Viewer`, `Notify.Operator`, `Notify.Admin` scopes/roles, update discovery metadata, offline defaults, and issuer templates. | Scopes available; metadata updated; tests ensure enforcement; offline kit defaults refreshed. |
|
| AUTH-NOTIFY-38-001 | DONE (2025-11-01) | Authority Core & Security Guild | — | Define `Notify.Viewer`, `Notify.Operator`, `Notify.Admin` scopes/roles, update discovery metadata, offline defaults, and issuer templates. | Scopes available; metadata updated; tests ensure enforcement; offline kit defaults refreshed. |
|
||||||
| AUTH-NOTIFY-40-001 | DONE (2025-11-02) | Authority Core & Security Guild | AUTH-NOTIFY-38-001, WEB-NOTIFY-40-001 | Implement signed ack token key rotation, webhook allowlists, admin-only escalation settings, and audit logging of ack actions. | Ack tokens signed/rotated; webhook allowlists enforced; admin enforcement validated; audit logs capture ack/resolution. |
|
| AUTH-NOTIFY-40-001 | DONE (2025-11-02) | Authority Core & Security Guild | AUTH-NOTIFY-38-001, WEB-NOTIFY-40-001 | Implement signed ack token key rotation, webhook allowlists, admin-only escalation settings, and audit logging of ack actions. | Ack tokens signed/rotated; webhook allowlists enforced; admin enforcement validated; audit logs capture ack/resolution. |
|
||||||
> 2025-11-02: `/notify/ack-tokens/rotate` exposed (notify.admin), emits `notify.ack.key_rotated|notify.ack.key_rotation_failed`, and DSSE rotation tests cover allowlist + scope enforcement.
|
> 2025-11-02: `/notify/ack-tokens/rotate` exposed (notify.admin), emits `notify.ack.key_rotated|notify.ack.key_rotation_failed`, and DSSE rotation tests cover allowlist + scope enforcement.
|
||||||
| AUTH-NOTIFY-42-001 | DONE (2025-11-02) | Authority Core & Security Guild | AUTH-NOTIFY-40-001 | Investigate ack token rotation 500 errors (test Rotate_ReturnsBadRequest_WhenKeyIdMissing_AndAuditsFailure still failing). Capture logs, identify root cause, and patch handler. | Failure mode understood; fix merged; regression test passes. |
|
| AUTH-NOTIFY-42-001 | DONE (2025-11-02) | Authority Core & Security Guild | AUTH-NOTIFY-40-001 | Investigate ack token rotation 500 errors (test Rotate_ReturnsBadRequest_WhenKeyIdMissing_AndAuditsFailure still failing). Capture logs, identify root cause, and patch handler. | Failure mode understood; fix merged; regression test passes. |
|
||||||
> 2025-11-02: Aliased `StellaOpsBearer` to the test auth handler, corrected bootstrap `/notifications/ack-tokens/rotate` defaults, and validated `Rotate_ReturnsBadRequest_WhenKeyIdMissing_AndAuditsFailure` via targeted `dotnet test`.
|
> 2025-11-02: Aliased `StellaOpsBearer` to the test auth handler, corrected bootstrap `/notifications/ack-tokens/rotate` defaults, and validated `Rotate_ReturnsBadRequest_WhenKeyIdMissing_AndAuditsFailure` via targeted `dotnet test`.
|
||||||
|
|
||||||
|
|
||||||
## CLI Parity & Task Packs
|
## CLI Parity & Task Packs
|
||||||
| ID | Status | Owner(s) | Depends on | Description | Exit Criteria |
|
| ID | Status | Owner(s) | Depends on | Description | Exit Criteria |
|
||||||
|----|--------|----------|------------|-------------|---------------|
|
|----|--------|----------|------------|-------------|---------------|
|
||||||
| AUTH-PACKS-41-001 | DONE (2025-11-04) | Authority Core & Security Guild | AUTH-AOC-19-001 | Define CLI SSO profiles and pack scopes (`Packs.Read`, `Packs.Write`, `Packs.Run`, `Packs.Approve`), update discovery metadata, offline defaults, and issuer templates. | Scopes available; metadata updated; tests ensure enforcement; offline kit templates refreshed. |
|
| AUTH-PACKS-41-001 | DONE (2025-11-04) | Authority Core & Security Guild | AUTH-AOC-19-001 | Define CLI SSO profiles and pack scopes (`Packs.Read`, `Packs.Write`, `Packs.Run`, `Packs.Approve`), update discovery metadata, offline defaults, and issuer templates. | Scopes available; metadata updated; tests ensure enforcement; offline kit templates refreshed. |
|
||||||
> 2025-11-02: Added Pack scope policies, Authority role defaults, and CLI profile guidance covering operator/publisher/approver flows.
|
> 2025-11-02: Added Pack scope policies, Authority role defaults, and CLI profile guidance covering operator/publisher/approver flows.
|
||||||
> 2025-11-02: Shared OpenSSL 1.1 shim feeds Authority & Signals Mongo2Go harnesses so pack scope coverage keeps running on OpenSSL 3 hosts (AUTH-PACKS-41-001).
|
> 2025-11-02: Shared OpenSSL 1.1 shim feeds Authority & Signals Mongo2Go harnesses so pack scope coverage keeps running on OpenSSL 3 hosts (AUTH-PACKS-41-001).
|
||||||
> 2025-11-04: Discovery metadata/OpenAPI advertise packs scopes, configs/offline kit templates bundle new roles, and Authority tests re-run to validate tenant gating for `packs.*`.
|
> 2025-11-04: Discovery metadata/OpenAPI advertise packs scopes, configs/offline kit templates bundle new roles, and Authority tests re-run to validate tenant gating for `packs.*`.
|
||||||
| AUTH-PACKS-43-001 | BLOCKED (2025-10-27) | Authority Core & Security Guild | AUTH-PACKS-41-001, TASKRUN-42-001, ORCH-SVC-42-101 | Enforce pack signing policies, approval RBAC checks, CLI CI token scopes, and audit logging for approvals. | Signing policies enforced; approvals require correct roles; CI token scope tests pass; audit logs recorded. |
|
| AUTH-PACKS-43-001 | BLOCKED (2025-10-27) | Authority Core & Security Guild | AUTH-PACKS-41-001, TASKRUN-42-001, ORCH-SVC-42-101 | Enforce pack signing policies, approval RBAC checks, CLI CI token scopes, and audit logging for approvals. | Signing policies enforced; approvals require correct roles; CI token scope tests pass; audit logs recorded. |
|
||||||
> Blocked: Task Runner approval APIs (`ORCH-SVC-42-101`, `TASKRUN-42-001`) still outstanding. Pack scope catalog (AUTH-PACKS-41-001) landed 2025-11-04; resume once execution/approval contracts are published.
|
> Blocked: Task Runner approval APIs (`ORCH-SVC-42-101`, `TASKRUN-42-001`) still outstanding. Pack scope catalog (AUTH-PACKS-41-001) landed 2025-11-04; resume once execution/approval contracts are published.
|
||||||
|
|
||||||
## Authority-Backed Scopes & Tenancy (Epic 14)
|
## Authority-Backed Scopes & Tenancy (Epic 14)
|
||||||
| ID | Status | Owner(s) | Depends on | Description | Exit Criteria |
|
| ID | Status | Owner(s) | Depends on | Description | Exit Criteria |
|
||||||
|----|--------|----------|------------|-------------|---------------|
|
|----|--------|----------|------------|-------------|---------------|
|
||||||
> 2025-10-28: Tidied advisory raw idempotency migration to avoid LINQ-on-`BsonValue` (explicit array copy) while continuing duplicate guardrail validation; scoped scanner/policy token call sites updated to honor new metadata parameter.
|
> 2025-10-28: Tidied advisory raw idempotency migration to avoid LINQ-on-`BsonValue` (explicit array copy) while continuing duplicate guardrail validation; scoped scanner/policy token call sites updated to honor new metadata parameter.
|
||||||
| AUTH-TEN-49-001 | DONE (2025-11-04) | Authority Core & Security Guild | AUTH-TEN-47-001 | Implement service accounts & delegation tokens (`act` chain), per-tenant quotas, audit stream of auth decisions, and revocation APIs. | Service tokens minted with scopes/TTL; delegation logged; quotas configurable; audit stream live; docs updated. |
|
| AUTH-TEN-49-001 | DONE (2025-11-04) | Authority Core & Security Guild | AUTH-TEN-47-001 | Implement service accounts & delegation tokens (`act` chain), per-tenant quotas, audit stream of auth decisions, and revocation APIs. | Service tokens minted with scopes/TTL; delegation logged; quotas configurable; audit stream live; docs updated. |
|
||||||
> 2025-11-02: Authority bootstrap test harness now seeds service accounts via AuthorityDelegation options; `/internal/service-accounts` endpoints validated with targeted vstest run.
|
> 2025-11-02: Authority bootstrap test harness now seeds service accounts via AuthorityDelegation options; `/internal/service-accounts` endpoints validated with targeted vstest run.
|
||||||
> 2025-11-02: Added Mongo service-account store, seeded options/collection initializers, token persistence metadata (`tokenKind`, `serviceAccountId`, `actorChain`), and docs/config samples. Introduced quota checks + tests covering service account issuance and persistence.
|
> 2025-11-02: Added Mongo service-account store, seeded options/collection initializers, token persistence metadata (`tokenKind`, `serviceAccountId`, `actorChain`), and docs/config samples. Introduced quota checks + tests covering service account issuance and persistence.
|
||||||
> 2025-11-02: Documented bootstrap service-account admin APIs in `docs/11_AUTHORITY.md`, noting API key requirements and stable upsert behaviour.
|
> 2025-11-02: Documented bootstrap service-account admin APIs in `docs/11_AUTHORITY.md`, noting API key requirements and stable upsert behaviour.
|
||||||
> 2025-11-03: Seeded explicit enabled service-account fixtures for integration tests and reran `StellaOps.Authority.Tests` to greenlight `/internal/service-accounts` listing + revocation scenarios.
|
> 2025-11-03: Seeded explicit enabled service-account fixtures for integration tests and reran `StellaOps.Authority.Tests` to greenlight `/internal/service-accounts` listing + revocation scenarios.
|
||||||
> 2025-11-04: Confirmed service-account docs/config examples, quota tuning, and audit stream wiring; Authority suite re-executed to cover issuance/listing/revocation flows.
|
> 2025-11-04: Confirmed service-account docs/config examples, quota tuning, and audit stream wiring; Authority suite re-executed to cover issuance/listing/revocation flows.
|
||||||
|
|
||||||
## Observability & Forensics (Epic 15)
|
## Observability & Forensics (Epic 15)
|
||||||
|
|
||||||
| ID | Status | Owner(s) | Depends on | Description | Exit Criteria |
|
| ID | Status | Owner(s) | Depends on | Description | Exit Criteria |
|
||||||
|----|--------|----------|------------|-------------|---------------|
|
|----|--------|----------|------------|-------------|---------------|
|
||||||
| AUTH-OBS-50-001 | DONE (2025-11-02) | Authority Core & Security Guild | AUTH-AOC-19-001 | Introduce scopes `obs:read`, `timeline:read`, `timeline:write`, `evidence:create`, `evidence:read`, `evidence:hold`, `attest:read`, and `obs:incident` (all tenant-scoped). Update discovery metadata, offline defaults, and scope grammar docs. | Scopes exposed via metadata; issuer templates updated; offline kit seeded; integration tests cover new scopes. |
|
| AUTH-OBS-50-001 | DONE (2025-11-02) | Authority Core & Security Guild | AUTH-AOC-19-001 | Introduce scopes `obs:read`, `timeline:read`, `timeline:write`, `evidence:create`, `evidence:read`, `evidence:hold`, `attest:read`, and `obs:incident` (all tenant-scoped). Update discovery metadata, offline defaults, and scope grammar docs. | Scopes exposed via metadata; issuer templates updated; offline kit seeded; integration tests cover new scopes. |
|
||||||
| AUTH-OBS-52-001 | DONE (2025-11-02) | Authority Core & Security Guild | AUTH-OBS-50-001, TIMELINE-OBS-52-003, EVID-OBS-53-003 | Configure resource server policies for Timeline Indexer, Evidence Locker, Exporter, and Observability APIs enforcing new scopes + tenant claims. Emit audit events including scope usage and trace IDs. | Policies deployed; unauthorized access blocked; audit logs prove scope usage; contract tests updated. |
|
| AUTH-OBS-52-001 | DONE (2025-11-02) | Authority Core & Security Guild | AUTH-OBS-50-001, TIMELINE-OBS-52-003, EVID-OBS-53-003 | Configure resource server policies for Timeline Indexer, Evidence Locker, Exporter, and Observability APIs enforcing new scopes + tenant claims. Emit audit events including scope usage and trace IDs. | Policies deployed; unauthorized access blocked; audit logs prove scope usage; contract tests updated. |
|
||||||
| AUTH-OBS-55-001 | DONE (2025-11-02) | Authority Core & Security Guild, Ops Guild | AUTH-OBS-50-001, WEB-OBS-55-001 | Harden incident mode authorization: require `obs:incident` scope + fresh auth, log activation reason, and expose verification endpoint for auditors. Update docs/runbooks. | Incident activate/deactivate requires scope; audit entries logged; docs updated with imposed rule reminder. |
|
| AUTH-OBS-55-001 | DONE (2025-11-02) | Authority Core & Security Guild, Ops Guild | AUTH-OBS-50-001, WEB-OBS-55-001 | Harden incident mode authorization: require `obs:incident` scope + fresh auth, log activation reason, and expose verification endpoint for auditors. Update docs/runbooks. | Incident activate/deactivate requires scope; audit entries logged; docs updated with imposed rule reminder. |
|
||||||
|
|
||||||
## Air-Gapped Mode (Epic 16)
|
## Air-Gapped Mode (Epic 16)
|
||||||
|
|
||||||
| ID | Status | Owner(s) | Depends on | Description | Exit Criteria |
|
| ID | Status | Owner(s) | Depends on | Description | Exit Criteria |
|
||||||
|----|--------|----------|------------|-------------|---------------|
|
|----|--------|----------|------------|-------------|---------------|
|
||||||
| AUTH-AIRGAP-56-001 | DONE (2025-11-04) | Authority Core & Security Guild | AIRGAP-CTL-56-001 | Provision new scopes (`airgap:seal`, `airgap:import`, `airgap:status:read`) in configuration metadata, offline kit defaults, and issuer templates. | Scopes exposed in discovery docs; offline kit updated; integration tests cover issuance. |
|
| AUTH-AIRGAP-56-001 | DONE (2025-11-04) | Authority Core & Security Guild | AIRGAP-CTL-56-001 | Provision new scopes (`airgap:seal`, `airgap:import`, `airgap:status:read`) in configuration metadata, offline kit defaults, and issuer templates. | Scopes exposed in discovery docs; offline kit updated; integration tests cover issuance. |
|
||||||
| AUTH-AIRGAP-56-002 | DONE (2025-11-04) | Authority Core & Security Guild | AUTH-AIRGAP-56-001, AIRGAP-IMP-58-001 | Audit import actions with actor, tenant, bundle ID, and trace ID; expose `/authority/audit/airgap` endpoint. | Audit records persisted; endpoint paginates results; tests cover RBAC + filtering. |
|
| AUTH-AIRGAP-56-002 | DONE (2025-11-04) | Authority Core & Security Guild | AUTH-AIRGAP-56-001, AIRGAP-IMP-58-001 | Audit import actions with actor, tenant, bundle ID, and trace ID; expose `/authority/audit/airgap` endpoint. | Audit records persisted; endpoint paginates results; tests cover RBAC + filtering. |
|
||||||
> 2025-11-04: Airgap scope constants are wired through discovery metadata, `etc/authority.yaml.sample`, and offline kit docs; scope issuance tests executed via `dotnet test`.
|
> 2025-11-04: Airgap scope constants are wired through discovery metadata, `etc/authority.yaml.sample`, and offline kit docs; scope issuance tests executed via `dotnet test`.
|
||||||
> 2025-11-04: `/authority/audit/airgap` API persists tenant-scoped audit entries with pagination and authorization guards validated by the Authority integration suite (187 tests).
|
> 2025-11-04: `/authority/audit/airgap` API persists tenant-scoped audit entries with pagination and authorization guards validated by the Authority integration suite (187 tests).
|
||||||
| AUTH-AIRGAP-57-001 | BLOCKED (2025-11-01) | Authority Core & Security Guild, DevOps Guild | AUTH-AIRGAP-56-001, DEVOPS-AIRGAP-57-002 | Enforce sealed-mode CI gating by refusing token issuance when declared sealed install lacks sealing confirmation. | Awaiting clarified sealed-confirmation contract and configuration structure before implementation. |
|
| AUTH-AIRGAP-57-001 | BLOCKED (2025-11-01) | Authority Core & Security Guild, DevOps Guild | AUTH-AIRGAP-56-001, DEVOPS-AIRGAP-57-002 | Enforce sealed-mode CI gating by refusing token issuance when declared sealed install lacks sealing confirmation. | Awaiting clarified sealed-confirmation contract and configuration structure before implementation. |
|
||||||
> 2025-11-01: AUTH-AIRGAP-57-001 blocked pending guidance on sealed-confirmation contract and configuration expectations before gating changes (Authority Core & Security Guild, DevOps Guild).
|
> 2025-11-01: AUTH-AIRGAP-57-001 blocked pending guidance on sealed-confirmation contract and configuration expectations before gating changes (Authority Core & Security Guild, DevOps Guild).
|
||||||
|
|
||||||
## SDKs & OpenAPI (Epic 17)
|
## SDKs & OpenAPI (Epic 17)
|
||||||
| ID | Status | Owner(s) | Depends on | Description | Exit Criteria |
|
| ID | Status | Owner(s) | Depends on | Description | Exit Criteria |
|
||||||
|----|--------|----------|------------|-------------|---------------|
|
|----|--------|----------|------------|-------------|---------------|
|
||||||
> 2025-10-28: Auth OpenAPI authored at `src/Api/StellaOps.Api.OpenApi/authority/openapi.yaml` covering `/token`, `/introspect`, `/revoke`, `/jwks`, scope catalog, and error envelopes; parsed via PyYAML sanity check and referenced in Epic 17 docs.
|
> 2025-10-28: Auth OpenAPI authored at `src/Api/StellaOps.Api.OpenApi/authority/openapi.yaml` covering `/token`, `/introspect`, `/revoke`, `/jwks`, scope catalog, and error envelopes; parsed via PyYAML sanity check and referenced in Epic 17 docs.
|
||||||
> 2025-10-28: Added `/.well-known/openapi` endpoint wiring cached spec metadata, YAML/JSON negotiation, HTTP cache headers, and tests verifying ETag + Accept handling. Authority spec (`src/Api/StellaOps.Api.OpenApi/authority/openapi.yaml`) now includes grant/scope extensions.
|
> 2025-10-28: Added `/.well-known/openapi` endpoint wiring cached spec metadata, YAML/JSON negotiation, HTTP cache headers, and tests verifying ETag + Accept handling. Authority spec (`src/Api/StellaOps.Api.OpenApi/authority/openapi.yaml`) now includes grant/scope extensions.
|
||||||
| AUTH-OAS-62-001 | DONE (2025-11-02) | Authority Core & Security Guild, SDK Generator Guild | AUTH-OAS-61-001, SDKGEN-63-001 | Provide SDK helpers for OAuth2/PAT flows, tenancy override header; add integration tests. | SDKs expose auth helpers; tests cover token issuance; docs updated. |
|
| AUTH-OAS-62-001 | DONE (2025-11-02) | Authority Core & Security Guild, SDK Generator Guild | AUTH-OAS-61-001, SDKGEN-63-001 | Provide SDK helpers for OAuth2/PAT flows, tenancy override header; add integration tests. | SDKs expose auth helpers; tests cover token issuance; docs updated. |
|
||||||
> 2025-11-02: `AddStellaOpsApiAuthentication` shipped (OAuth2 + PAT), tenant header injection added, and client tests updated for caching behaviour.
|
> 2025-11-02: `AddStellaOpsApiAuthentication` shipped (OAuth2 + PAT), tenant header injection added, and client tests updated for caching behaviour.
|
||||||
| AUTH-OAS-63-001 | DONE (2025-11-02) | Authority Core & Security Guild, API Governance Guild | APIGOV-63-001 | Emit deprecation headers and notifications for legacy auth endpoints. | Headers emitted; notifications verified; migration guide published. |
|
| AUTH-OAS-63-001 | DONE (2025-11-02) | Authority Core & Security Guild, API Governance Guild | APIGOV-63-001 | Emit deprecation headers and notifications for legacy auth endpoints. | Headers emitted; notifications verified; migration guide published. |
|
||||||
> 2025-11-02: AUTH-OAS-63-001 completed — legacy OAuth shims emit Deprecation/Sunset/Warning headers, audit events captured, and migration guide published (Authority Core & Security Guild, API Governance Guild).
|
> 2025-11-02: AUTH-OAS-63-001 completed — legacy OAuth shims emit Deprecation/Sunset/Warning headers, audit events captured, and migration guide published (Authority Core & Security Guild, API Governance Guild).
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# TASKS
|
# TASKS
|
||||||
| Task | Owner(s) | Depends on | Notes |
|
| 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).|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# TASKS
|
# TASKS
|
||||||
| Task | Owner(s) | Depends on | Notes |
|
| 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`.|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# TASKS
|
# TASKS
|
||||||
| Task | Owner(s) | Depends on | Notes |
|
| 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-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-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.|
|
|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.|
|
||||||
|
|||||||
@@ -120,7 +120,11 @@ builder.Services.AddSingleton<ILedgerEventRepository, PostgresLedgerEventReposit
|
|||||||
builder.Services.AddSingleton<IMerkleAnchorScheduler, PostgresMerkleAnchorScheduler>();
|
builder.Services.AddSingleton<IMerkleAnchorScheduler, PostgresMerkleAnchorScheduler>();
|
||||||
builder.Services.AddSingleton<ILedgerEventStream, PostgresLedgerEventStream>();
|
builder.Services.AddSingleton<ILedgerEventStream, PostgresLedgerEventStream>();
|
||||||
builder.Services.AddSingleton<IFindingProjectionRepository, PostgresFindingProjectionRepository>();
|
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.AddSingleton<ILedgerEventWriteService, LedgerEventWriteService>();
|
||||||
builder.Services.AddHostedService<LedgerMerkleAnchorWorker>();
|
builder.Services.AddHostedService<LedgerMerkleAnchorWorker>();
|
||||||
builder.Services.AddHostedService<LedgerProjectionWorker>();
|
builder.Services.AddHostedService<LedgerProjectionWorker>();
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using System.Text.Json;
|
||||||
using System.Text.Json.Nodes;
|
using System.Text.Json.Nodes;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using StellaOps.Findings.Ledger.Domain;
|
using StellaOps.Findings.Ledger.Domain;
|
||||||
|
|||||||
@@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,6 +12,8 @@ public sealed class LedgerServiceOptions
|
|||||||
|
|
||||||
public ProjectionOptions Projection { get; init; } = new();
|
public ProjectionOptions Projection { get; init; } = new();
|
||||||
|
|
||||||
|
public PolicyEngineOptions PolicyEngine { get; init; } = new();
|
||||||
|
|
||||||
public void Validate()
|
public void Validate()
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(Database.ConnectionString))
|
if (string.IsNullOrWhiteSpace(Database.ConnectionString))
|
||||||
@@ -43,6 +45,8 @@ public sealed class LedgerServiceOptions
|
|||||||
{
|
{
|
||||||
throw new InvalidOperationException("Projection idle delay must be greater than zero.");
|
throw new InvalidOperationException("Projection idle delay must be greater than zero.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
PolicyEngine.Validate();
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed class DatabaseOptions
|
public sealed class DatabaseOptions
|
||||||
@@ -90,4 +94,53 @@ public sealed class LedgerServiceOptions
|
|||||||
|
|
||||||
public TimeSpan IdleDelay { get; set; } = DefaultIdleDelay;
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
|
||||||
|
[assembly: InternalsVisibleTo("StellaOps.Findings.Ledger.Tests")]
|
||||||
@@ -14,6 +14,8 @@
|
|||||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.0-rc.2.25502.107" />
|
<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.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.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" />
|
<PackageReference Include="Npgsql" Version="7.0.7" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|||||||
@@ -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-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-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-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-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-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. |
|
| 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. |
|
||||||
|
|||||||
@@ -65,6 +65,7 @@ public sealed class LedgerProjectionReducerTests
|
|||||||
new JsonObject(),
|
new JsonObject(),
|
||||||
Guid.NewGuid(),
|
Guid.NewGuid(),
|
||||||
null,
|
null,
|
||||||
|
new JsonArray(),
|
||||||
DateTimeOffset.UtcNow,
|
DateTimeOffset.UtcNow,
|
||||||
string.Empty);
|
string.Empty);
|
||||||
var existingHash = ProjectionHashing.ComputeCycleHash(existing);
|
var existingHash = ProjectionHashing.ComputeCycleHash(existing);
|
||||||
@@ -112,6 +113,7 @@ public sealed class LedgerProjectionReducerTests
|
|||||||
labels,
|
labels,
|
||||||
Guid.NewGuid(),
|
Guid.NewGuid(),
|
||||||
null,
|
null,
|
||||||
|
new JsonArray(),
|
||||||
DateTimeOffset.UtcNow,
|
DateTimeOffset.UtcNow,
|
||||||
string.Empty);
|
string.Empty);
|
||||||
existing = existing with { CycleHash = ProjectionHashing.ComputeCycleHash(existing) };
|
existing = existing with { CycleHash = ProjectionHashing.ComputeCycleHash(existing) };
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,12 +1,12 @@
|
|||||||
# Issuer Directory Task Board — Epic 7
|
# Issuer Directory Task Board — Epic 7
|
||||||
| ID | Status | Owner(s) | Depends on | Description | Exit Criteria |
|
| ID | Status | Owner(s) | Depends on | Description | Exit Criteria |
|
||||||
|----|--------|----------|------------|-------------|---------------|
|
|----|--------|----------|------------|-------------|---------------|
|
||||||
| ISSUER-30-001 | DONE (2025-11-01) | Issuer Directory Guild | AUTH-VULN-29-001 | Implement issuer CRUD API with RBAC, audit logging, and tenant scoping; seed CSAF publisher metadata. | APIs deployed; audit logs capture actor/reason; seed data imported; tests cover RBAC. |
|
| ISSUER-30-001 | DONE (2025-11-01) | Issuer Directory Guild | AUTH-VULN-29-001 | Implement issuer CRUD API with RBAC, audit logging, and tenant scoping; seed CSAF publisher metadata. | APIs deployed; audit logs capture actor/reason; seed data imported; tests cover RBAC. |
|
||||||
| ISSUER-30-002 | DONE (2025-11-01) | Issuer Directory Guild, Security Guild | ISSUER-30-001 | Implement key management endpoints (add/rotate/revoke keys), enforce expiry, validate formats (Ed25519, X.509, DSSE). | Keys stored securely; expiry enforced; validation tests cover key types; docs updated. |
|
| ISSUER-30-002 | DONE (2025-11-01) | Issuer Directory Guild, Security Guild | ISSUER-30-001 | Implement key management endpoints (add/rotate/revoke keys), enforce expiry, validate formats (Ed25519, X.509, DSSE). | Keys stored securely; expiry enforced; validation tests cover key types; docs updated. |
|
||||||
| ISSUER-30-003 | DONE (2025-11-04) | Issuer Directory Guild, Policy Guild | ISSUER-30-001 | Provide trust weight APIs and tenant overrides with validation (+/- bounds) and audit trails. | Trust overrides persisted; policy integration confirmed; tests cover overrides. |
|
| ISSUER-30-003 | DONE (2025-11-04) | Issuer Directory Guild, Policy Guild | ISSUER-30-001 | Provide trust weight APIs and tenant overrides with validation (+/- bounds) and audit trails. | Trust overrides persisted; policy integration confirmed; tests cover overrides. |
|
||||||
> 2025-11-04: `/issuer-directory/issuers/{id}/trust` endpoints deliver bounded overrides with audit logging, Mongo indexes seeded for uniqueness, config/docs updated, and core tests executed (`dotnet test`).
|
> 2025-11-04: `/issuer-directory/issuers/{id}/trust` endpoints deliver bounded overrides with audit logging, Mongo indexes seeded for uniqueness, config/docs updated, and core tests executed (`dotnet test`).
|
||||||
| ISSUER-30-004 | DONE (2025-11-01) | Issuer Directory Guild, VEX Lens Guild | ISSUER-30-001..003 | Integrate with VEX Lens and Excitor signature verification (client SDK, caching, retries). | Lens/Excitor resolve issuer metadata via SDK; integration tests cover network failures. |
|
| ISSUER-30-004 | DONE (2025-11-01) | Issuer Directory Guild, VEX Lens Guild | ISSUER-30-001..003 | Integrate with VEX Lens and Excitor signature verification (client SDK, caching, retries). | Lens/Excitor resolve issuer metadata via SDK; integration tests cover network failures. |
|
||||||
| ISSUER-30-005 | DONE (2025-11-01) | Issuer Directory Guild, Observability Guild | ISSUER-30-001..004 | Instrument metrics/logs (issuer changes, key rotation, verification failures) and dashboards/alerts. | Telemetry live; alerts configured; docs updated. |
|
| ISSUER-30-005 | DONE (2025-11-01) | Issuer Directory Guild, Observability Guild | ISSUER-30-001..004 | Instrument metrics/logs (issuer changes, key rotation, verification failures) and dashboards/alerts. | Telemetry live; alerts configured; docs updated. |
|
||||||
| ISSUER-30-006 | DONE (2025-11-02) | Issuer Directory Guild, DevOps Guild | ISSUER-30-001..005 | Provide deployment manifests, backup/restore, secure secret storage, and offline kit instructions. | Deployment docs merged; smoke deploy validated; backup tested; offline kit updated. |
|
| ISSUER-30-006 | DONE (2025-11-02) | Issuer Directory Guild, DevOps Guild | ISSUER-30-001..005 | Provide deployment manifests, backup/restore, secure secret storage, and offline kit instructions. | Deployment docs merged; smoke deploy validated; backup tested; offline kit updated. |
|
||||||
|
|
||||||
> 2025-11-01: Excititor worker now queries Issuer Directory via during attestation verification, caching active key metadata and trust weights for tenant/global scopes.
|
> 2025-11-01: Excititor worker now queries Issuer Directory via during attestation verification, caching active key metadata and trust weights for tenant/global scopes.
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
{
|
{
|
||||||
"schemaVersion": "1.0",
|
"schemaVersion": "1.0",
|
||||||
"id": "stellaops.notify.connector.email",
|
"id": "stellaops.notify.connector.email",
|
||||||
"displayName": "StellaOps Email Notify Connector",
|
"displayName": "StellaOps Email Notify Connector",
|
||||||
"version": "0.1.0-alpha",
|
"version": "0.1.0-alpha",
|
||||||
"requiresRestart": true,
|
"requiresRestart": true,
|
||||||
"entryPoint": {
|
"entryPoint": {
|
||||||
"type": "dotnet",
|
"type": "dotnet",
|
||||||
"assembly": "StellaOps.Notify.Connectors.Email.dll"
|
"assembly": "StellaOps.Notify.Connectors.Email.dll"
|
||||||
},
|
},
|
||||||
"capabilities": [
|
"capabilities": [
|
||||||
"notify-connector",
|
"notify-connector",
|
||||||
"email"
|
"email"
|
||||||
],
|
],
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"org.stellaops.notify.channel.type": "email"
|
"org.stellaops.notify.channel.type": "email"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,19 @@
|
|||||||
{
|
{
|
||||||
"schemaVersion": "1.0",
|
"schemaVersion": "1.0",
|
||||||
"id": "stellaops.notify.connector.slack",
|
"id": "stellaops.notify.connector.slack",
|
||||||
"displayName": "StellaOps Slack Notify Connector",
|
"displayName": "StellaOps Slack Notify Connector",
|
||||||
"version": "0.1.0-alpha",
|
"version": "0.1.0-alpha",
|
||||||
"requiresRestart": true,
|
"requiresRestart": true,
|
||||||
"entryPoint": {
|
"entryPoint": {
|
||||||
"type": "dotnet",
|
"type": "dotnet",
|
||||||
"assembly": "StellaOps.Notify.Connectors.Slack.dll"
|
"assembly": "StellaOps.Notify.Connectors.Slack.dll"
|
||||||
},
|
},
|
||||||
"capabilities": [
|
"capabilities": [
|
||||||
"notify-connector",
|
"notify-connector",
|
||||||
"slack"
|
"slack"
|
||||||
],
|
],
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"org.stellaops.notify.channel.type": "slack",
|
"org.stellaops.notify.channel.type": "slack",
|
||||||
"org.stellaops.notify.connector.requiredScopes": "chat:write,chat:write.public"
|
"org.stellaops.notify.connector.requiredScopes": "chat:write,chat:write.public"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,19 @@
|
|||||||
{
|
{
|
||||||
"schemaVersion": "1.0",
|
"schemaVersion": "1.0",
|
||||||
"id": "stellaops.notify.connector.teams",
|
"id": "stellaops.notify.connector.teams",
|
||||||
"displayName": "StellaOps Teams Notify Connector",
|
"displayName": "StellaOps Teams Notify Connector",
|
||||||
"version": "0.1.0-alpha",
|
"version": "0.1.0-alpha",
|
||||||
"requiresRestart": true,
|
"requiresRestart": true,
|
||||||
"entryPoint": {
|
"entryPoint": {
|
||||||
"type": "dotnet",
|
"type": "dotnet",
|
||||||
"assembly": "StellaOps.Notify.Connectors.Teams.dll"
|
"assembly": "StellaOps.Notify.Connectors.Teams.dll"
|
||||||
},
|
},
|
||||||
"capabilities": [
|
"capabilities": [
|
||||||
"notify-connector",
|
"notify-connector",
|
||||||
"teams"
|
"teams"
|
||||||
],
|
],
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"org.stellaops.notify.channel.type": "teams",
|
"org.stellaops.notify.channel.type": "teams",
|
||||||
"org.stellaops.notify.connector.cardVersion": "1.5"
|
"org.stellaops.notify.connector.cardVersion": "1.5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
{
|
{
|
||||||
"schemaVersion": "1.0",
|
"schemaVersion": "1.0",
|
||||||
"id": "stellaops.notify.connector.webhook",
|
"id": "stellaops.notify.connector.webhook",
|
||||||
"displayName": "StellaOps Webhook Notify Connector",
|
"displayName": "StellaOps Webhook Notify Connector",
|
||||||
"version": "0.1.0-alpha",
|
"version": "0.1.0-alpha",
|
||||||
"requiresRestart": true,
|
"requiresRestart": true,
|
||||||
"entryPoint": {
|
"entryPoint": {
|
||||||
"type": "dotnet",
|
"type": "dotnet",
|
||||||
"assembly": "StellaOps.Notify.Connectors.Webhook.dll"
|
"assembly": "StellaOps.Notify.Connectors.Webhook.dll"
|
||||||
},
|
},
|
||||||
"capabilities": [
|
"capabilities": [
|
||||||
"notify-connector",
|
"notify-connector",
|
||||||
"webhook"
|
"webhook"
|
||||||
],
|
],
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"org.stellaops.notify.channel.type": "webhook"
|
"org.stellaops.notify.channel.type": "webhook"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,9 +55,13 @@ public sealed record ReportDocumentDto
|
|||||||
[JsonPropertyOrder(6)]
|
[JsonPropertyOrder(6)]
|
||||||
public IReadOnlyList<PolicyPreviewVerdictDto> Verdicts { get; init; } = Array.Empty<PolicyPreviewVerdictDto>();
|
public IReadOnlyList<PolicyPreviewVerdictDto> Verdicts { get; init; } = Array.Empty<PolicyPreviewVerdictDto>();
|
||||||
|
|
||||||
[JsonPropertyName("issues")]
|
[JsonPropertyName("issues")]
|
||||||
[JsonPropertyOrder(7)]
|
[JsonPropertyOrder(7)]
|
||||||
public IReadOnlyList<PolicyPreviewIssueDto> Issues { get; init; } = Array.Empty<PolicyPreviewIssueDto>();
|
public IReadOnlyList<PolicyPreviewIssueDto> Issues { get; init; } = Array.Empty<PolicyPreviewIssueDto>();
|
||||||
|
|
||||||
|
[JsonPropertyName("surface")]
|
||||||
|
[JsonPropertyOrder(8)]
|
||||||
|
public SurfacePointersDto? Surface { get; init; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed record ReportPolicyDto
|
public sealed record ReportPolicyDto
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
namespace StellaOps.Scanner.WebService.Contracts;
|
namespace StellaOps.Scanner.WebService.Contracts;
|
||||||
|
|
||||||
public sealed record ScanStatusResponse(
|
public sealed record ScanStatusResponse(
|
||||||
string ScanId,
|
string ScanId,
|
||||||
string Status,
|
string Status,
|
||||||
ScanStatusTarget Image,
|
ScanStatusTarget Image,
|
||||||
DateTimeOffset CreatedAt,
|
DateTimeOffset CreatedAt,
|
||||||
DateTimeOffset UpdatedAt,
|
DateTimeOffset UpdatedAt,
|
||||||
string? FailureReason);
|
string? FailureReason,
|
||||||
|
SurfacePointersDto? Surface);
|
||||||
public sealed record ScanStatusTarget(
|
|
||||||
string? Reference,
|
public sealed record ScanStatusTarget(
|
||||||
string? Digest);
|
string? Reference,
|
||||||
|
string? Digest);
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -1,12 +1,16 @@
|
|||||||
using System.Diagnostics;
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using Microsoft.AspNetCore.Builder;
|
using Microsoft.AspNetCore.Builder;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Routing;
|
using Microsoft.AspNetCore.Routing;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Logging;
|
||||||
using StellaOps.Scanner.WebService.Diagnostics;
|
using Microsoft.Extensions.Options;
|
||||||
using StellaOps.Scanner.WebService.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;
|
namespace StellaOps.Scanner.WebService.Endpoints;
|
||||||
|
|
||||||
@@ -56,27 +60,69 @@ internal static class HealthEndpoints
|
|||||||
return Json(document, StatusCodes.Status200OK);
|
return Json(document, StatusCodes.Status200OK);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task<IResult> HandleReady(
|
private static async Task<IResult> HandleReady(
|
||||||
ServiceStatus status,
|
ServiceStatus status,
|
||||||
HttpContext context,
|
ISurfaceValidatorRunner validatorRunner,
|
||||||
CancellationToken cancellationToken)
|
ISurfaceEnvironment surfaceEnvironment,
|
||||||
{
|
ILoggerFactory loggerFactory,
|
||||||
ApplyNoCache(context.Response);
|
HttpContext context,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
await Task.CompletedTask;
|
{
|
||||||
|
ApplyNoCache(context.Response);
|
||||||
status.RecordReadyCheck(success: true, latency: TimeSpan.Zero, error: null);
|
|
||||||
var snapshot = status.CreateSnapshot();
|
ArgumentNullException.ThrowIfNull(loggerFactory);
|
||||||
var ready = snapshot.Ready;
|
|
||||||
|
var logger = loggerFactory.CreateLogger("Scanner.WebService.Health");
|
||||||
var document = new ReadyDocument(
|
var stopwatch = Stopwatch.StartNew();
|
||||||
Status: ready.IsReady ? "ready" : "unready",
|
var success = true;
|
||||||
CheckedAt: ready.CheckedAt,
|
string? error = null;
|
||||||
LatencyMs: ready.Latency?.TotalMilliseconds,
|
|
||||||
Error: ready.Error);
|
try
|
||||||
|
{
|
||||||
return Json(document, StatusCodes.Status200OK);
|
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;
|
||||||
|
|
||||||
|
var document = new ReadyDocument(
|
||||||
|
Status: ready.IsReady ? "ready" : "unready",
|
||||||
|
CheckedAt: ready.CheckedAt,
|
||||||
|
LatencyMs: ready.Latency?.TotalMilliseconds,
|
||||||
|
Error: ready.Error);
|
||||||
|
|
||||||
|
var statusCode = success ? StatusCodes.Status200OK : StatusCodes.Status503ServiceUnavailable;
|
||||||
|
return Json(document, statusCode);
|
||||||
|
}
|
||||||
|
|
||||||
private static void ApplyNoCache(HttpResponse response)
|
private static void ApplyNoCache(HttpResponse response)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,17 +1,18 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Security.Cryptography;
|
using System.Security.Cryptography;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Routing;
|
using Microsoft.AspNetCore.Routing;
|
||||||
using StellaOps.Policy;
|
using Microsoft.Extensions.Logging;
|
||||||
using StellaOps.Scanner.WebService.Constants;
|
using StellaOps.Policy;
|
||||||
using StellaOps.Scanner.WebService.Contracts;
|
using StellaOps.Scanner.WebService.Constants;
|
||||||
using StellaOps.Scanner.WebService.Infrastructure;
|
using StellaOps.Scanner.WebService.Contracts;
|
||||||
using StellaOps.Scanner.WebService.Security;
|
using StellaOps.Scanner.WebService.Infrastructure;
|
||||||
using StellaOps.Scanner.WebService.Services;
|
using StellaOps.Scanner.WebService.Security;
|
||||||
|
using StellaOps.Scanner.WebService.Services;
|
||||||
|
|
||||||
namespace StellaOps.Scanner.WebService.Endpoints;
|
namespace StellaOps.Scanner.WebService.Endpoints;
|
||||||
|
|
||||||
@@ -49,25 +50,30 @@ internal static class ReportEndpoints
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task<IResult> HandleCreateReportAsync(
|
private static async Task<IResult> HandleCreateReportAsync(
|
||||||
ReportRequestDto request,
|
ReportRequestDto request,
|
||||||
PolicyPreviewService previewService,
|
PolicyPreviewService previewService,
|
||||||
IReportSigner signer,
|
IReportSigner signer,
|
||||||
TimeProvider timeProvider,
|
TimeProvider timeProvider,
|
||||||
IReportEventDispatcher eventDispatcher,
|
IReportEventDispatcher eventDispatcher,
|
||||||
HttpContext context,
|
ISurfacePointerService surfacePointerService,
|
||||||
CancellationToken cancellationToken)
|
ILoggerFactory loggerFactory,
|
||||||
{
|
HttpContext context,
|
||||||
ArgumentNullException.ThrowIfNull(request);
|
CancellationToken cancellationToken)
|
||||||
ArgumentNullException.ThrowIfNull(previewService);
|
{
|
||||||
ArgumentNullException.ThrowIfNull(signer);
|
ArgumentNullException.ThrowIfNull(request);
|
||||||
ArgumentNullException.ThrowIfNull(timeProvider);
|
ArgumentNullException.ThrowIfNull(previewService);
|
||||||
ArgumentNullException.ThrowIfNull(eventDispatcher);
|
ArgumentNullException.ThrowIfNull(signer);
|
||||||
|
ArgumentNullException.ThrowIfNull(timeProvider);
|
||||||
if (string.IsNullOrWhiteSpace(request.ImageDigest))
|
ArgumentNullException.ThrowIfNull(eventDispatcher);
|
||||||
{
|
ArgumentNullException.ThrowIfNull(surfacePointerService);
|
||||||
return ProblemResultFactory.Create(
|
ArgumentNullException.ThrowIfNull(loggerFactory);
|
||||||
context,
|
var logger = loggerFactory.CreateLogger("Scanner.WebService.Reports");
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(request.ImageDigest))
|
||||||
|
{
|
||||||
|
return ProblemResultFactory.Create(
|
||||||
|
context,
|
||||||
ProblemTypes.Validation,
|
ProblemTypes.Validation,
|
||||||
"Invalid report request",
|
"Invalid report request",
|
||||||
StatusCodes.Status400BadRequest,
|
StatusCodes.Status400BadRequest,
|
||||||
@@ -127,26 +133,46 @@ internal static class ReportEndpoints
|
|||||||
.ToArray();
|
.ToArray();
|
||||||
|
|
||||||
var issuesDto = preview.Issues.Select(PolicyDtoMapper.ToIssueDto).ToArray();
|
var issuesDto = preview.Issues.Select(PolicyDtoMapper.ToIssueDto).ToArray();
|
||||||
var summary = BuildSummary(projectedVerdicts);
|
var summary = BuildSummary(projectedVerdicts);
|
||||||
var verdict = ComputeVerdict(projectedVerdicts);
|
var verdict = ComputeVerdict(projectedVerdicts);
|
||||||
var reportId = CreateReportId(request.ImageDigest!, preview.PolicyDigest);
|
var reportId = CreateReportId(request.ImageDigest!, preview.PolicyDigest);
|
||||||
var generatedAt = timeProvider.GetUtcNow();
|
var generatedAt = timeProvider.GetUtcNow();
|
||||||
|
SurfacePointersDto? surfacePointers = null;
|
||||||
var document = new ReportDocumentDto
|
|
||||||
{
|
try
|
||||||
ReportId = reportId,
|
{
|
||||||
ImageDigest = request.ImageDigest!,
|
surfacePointers = await surfacePointerService
|
||||||
GeneratedAt = generatedAt,
|
.TryBuildAsync(request.ImageDigest!, context.RequestAborted)
|
||||||
Verdict = verdict,
|
.ConfigureAwait(false);
|
||||||
Policy = new ReportPolicyDto
|
}
|
||||||
{
|
catch (OperationCanceledException) when (context.RequestAborted.IsCancellationRequested)
|
||||||
RevisionId = preview.RevisionId,
|
{
|
||||||
Digest = preview.PolicyDigest
|
throw;
|
||||||
},
|
}
|
||||||
Summary = summary,
|
catch (Exception ex)
|
||||||
Verdicts = projectedVerdicts,
|
{
|
||||||
Issues = issuesDto
|
if (!context.RequestAborted.IsCancellationRequested)
|
||||||
};
|
{
|
||||||
|
logger.LogDebug(ex, "Failed to build surface pointers for digest {Digest}.", request.ImageDigest);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var document = new ReportDocumentDto
|
||||||
|
{
|
||||||
|
ReportId = reportId,
|
||||||
|
ImageDigest = request.ImageDigest!,
|
||||||
|
GeneratedAt = generatedAt,
|
||||||
|
Verdict = verdict,
|
||||||
|
Policy = new ReportPolicyDto
|
||||||
|
{
|
||||||
|
RevisionId = preview.RevisionId,
|
||||||
|
Digest = preview.PolicyDigest
|
||||||
|
},
|
||||||
|
Summary = summary,
|
||||||
|
Verdicts = projectedVerdicts,
|
||||||
|
Issues = issuesDto,
|
||||||
|
Surface = surfacePointers
|
||||||
|
};
|
||||||
|
|
||||||
var payloadBytes = JsonSerializer.SerializeToUtf8Bytes(document, SerializerOptions);
|
var payloadBytes = JsonSerializer.SerializeToUtf8Bytes(document, SerializerOptions);
|
||||||
var signature = signer.Sign(payloadBytes);
|
var signature = signer.Sign(payloadBytes);
|
||||||
@@ -169,11 +195,11 @@ internal static class ReportEndpoints
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
var response = new ReportResponseDto
|
var response = new ReportResponseDto
|
||||||
{
|
{
|
||||||
Report = document,
|
Report = document,
|
||||||
Dsse = envelope
|
Dsse = envelope
|
||||||
};
|
};
|
||||||
|
|
||||||
await eventDispatcher
|
await eventDispatcher
|
||||||
.PublishAsync(request, preview, document, envelope, context, cancellationToken)
|
.PublishAsync(request, preview, document, envelope, context, cancellationToken)
|
||||||
|
|||||||
@@ -140,10 +140,12 @@ internal static class ScanEndpoints
|
|||||||
private static async Task<IResult> HandleStatusAsync(
|
private static async Task<IResult> HandleStatusAsync(
|
||||||
string scanId,
|
string scanId,
|
||||||
IScanCoordinator coordinator,
|
IScanCoordinator coordinator,
|
||||||
|
ISurfacePointerService surfacePointerService,
|
||||||
HttpContext context,
|
HttpContext context,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(coordinator);
|
ArgumentNullException.ThrowIfNull(coordinator);
|
||||||
|
ArgumentNullException.ThrowIfNull(surfacePointerService);
|
||||||
|
|
||||||
if (!ScanId.TryParse(scanId, out var parsed))
|
if (!ScanId.TryParse(scanId, out var parsed))
|
||||||
{
|
{
|
||||||
@@ -163,7 +165,23 @@ internal static class ScanEndpoints
|
|||||||
ProblemTypes.NotFound,
|
ProblemTypes.NotFound,
|
||||||
"Scan not found",
|
"Scan not found",
|
||||||
StatusCodes.Status404NotFound,
|
StatusCodes.Status404NotFound,
|
||||||
detail: "Requested scan could not be located.");
|
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(
|
var response = new ScanStatusResponse(
|
||||||
@@ -172,7 +190,8 @@ internal static class ScanEndpoints
|
|||||||
Image: new ScanStatusTarget(snapshot.Target.Reference, snapshot.Target.Digest),
|
Image: new ScanStatusTarget(snapshot.Target.Reference, snapshot.Target.Digest),
|
||||||
CreatedAt: snapshot.CreatedAt,
|
CreatedAt: snapshot.CreatedAt,
|
||||||
UpdatedAt: snapshot.UpdatedAt,
|
UpdatedAt: snapshot.UpdatedAt,
|
||||||
FailureReason: snapshot.FailureReason);
|
FailureReason: snapshot.FailureReason,
|
||||||
|
Surface: surfacePointers);
|
||||||
|
|
||||||
return Json(response, StatusCodes.Status200OK);
|
return Json(response, StatusCodes.Status200OK);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using StellaOps.Scanner.Storage;
|
||||||
|
|
||||||
namespace StellaOps.Scanner.WebService.Options;
|
namespace StellaOps.Scanner.WebService.Options;
|
||||||
|
|
||||||
@@ -129,6 +130,8 @@ public sealed class ScannerWebServiceOptions
|
|||||||
|
|
||||||
public int ObjectLockRetentionDays { get; set; } = 30;
|
public int ObjectLockRetentionDays { get; set; } = 30;
|
||||||
|
|
||||||
|
public string RootPrefix { get; set; } = ScannerStorageDefaults.DefaultRootPrefix;
|
||||||
|
|
||||||
public string? ApiKey { get; set; }
|
public string? ApiKey { get; set; }
|
||||||
|
|
||||||
public string ApiKeyHeader { get; set; } = string.Empty;
|
public string ApiKeyHeader { get; set; } = string.Empty;
|
||||||
|
|||||||
@@ -19,6 +19,10 @@ using StellaOps.Cryptography.DependencyInjection;
|
|||||||
using StellaOps.Cryptography.Plugin.BouncyCastle;
|
using StellaOps.Cryptography.Plugin.BouncyCastle;
|
||||||
using StellaOps.Policy;
|
using StellaOps.Policy;
|
||||||
using StellaOps.Scanner.Cache;
|
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.Diagnostics;
|
||||||
using StellaOps.Scanner.WebService.Endpoints;
|
using StellaOps.Scanner.WebService.Endpoints;
|
||||||
using StellaOps.Scanner.WebService.Extensions;
|
using StellaOps.Scanner.WebService.Extensions;
|
||||||
@@ -80,11 +84,20 @@ builder.Services.AddSingleton<IScanProgressReader>(sp => sp.GetRequiredService<S
|
|||||||
builder.Services.AddSingleton<IScanCoordinator, InMemoryScanCoordinator>();
|
builder.Services.AddSingleton<IScanCoordinator, InMemoryScanCoordinator>();
|
||||||
builder.Services.AddSingleton<IPolicySnapshotRepository, InMemoryPolicySnapshotRepository>();
|
builder.Services.AddSingleton<IPolicySnapshotRepository, InMemoryPolicySnapshotRepository>();
|
||||||
builder.Services.AddSingleton<IPolicyAuditRepository, InMemoryPolicyAuditRepository>();
|
builder.Services.AddSingleton<IPolicyAuditRepository, InMemoryPolicyAuditRepository>();
|
||||||
builder.Services.AddSingleton<PolicySnapshotStore>();
|
builder.Services.AddSingleton<PolicySnapshotStore>();
|
||||||
builder.Services.AddSingleton<PolicyPreviewService>();
|
builder.Services.AddSingleton<PolicyPreviewService>();
|
||||||
builder.Services.AddStellaOpsCrypto();
|
builder.Services.AddStellaOpsCrypto();
|
||||||
builder.Services.AddBouncyCastleEd25519Provider();
|
builder.Services.AddBouncyCastleEd25519Provider();
|
||||||
builder.Services.AddSingleton<IReportSigner, ReportSigner>();
|
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>();
|
builder.Services.AddSingleton<IRedisConnectionFactory, RedisConnectionFactory>();
|
||||||
if (bootstrapOptions.Events is { Enabled: true } eventsOptions
|
if (bootstrapOptions.Events is { Enabled: true } eventsOptions
|
||||||
&& string.Equals(eventsOptions.Driver, "redis", StringComparison.OrdinalIgnoreCase))
|
&& string.Equals(eventsOptions.Driver, "redis", StringComparison.OrdinalIgnoreCase))
|
||||||
@@ -119,6 +132,11 @@ builder.Services.AddScannerStorage(storageOptions =>
|
|||||||
storageOptions.ObjectStore.BucketName = bootstrapOptions.ArtifactStore.Bucket;
|
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;
|
var artifactDriver = bootstrapOptions.ArtifactStore.Driver?.Trim() ?? string.Empty;
|
||||||
if (string.Equals(artifactDriver, ScannerStorageDefaults.ObjectStoreProviders.RustFs, StringComparison.OrdinalIgnoreCase))
|
if (string.Equals(artifactDriver, ScannerStorageDefaults.ObjectStoreProviders.RustFs, StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -207,10 +207,7 @@ internal static class OrchestratorEventSerializer
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
info.PolymorphismOptions ??= new JsonPolymorphismOptions
|
info.PolymorphismOptions ??= new JsonPolymorphismOptions();
|
||||||
{
|
|
||||||
UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.Fail
|
|
||||||
};
|
|
||||||
|
|
||||||
AddDerivedType(info.PolymorphismOptions, typeof(ReportReadyEventPayload));
|
AddDerivedType(info.PolymorphismOptions, typeof(ReportReadyEventPayload));
|
||||||
AddDerivedType(info.PolymorphismOptions, typeof(ScanCompletedEventPayload));
|
AddDerivedType(info.PolymorphismOptions, typeof(ScanCompletedEventPayload));
|
||||||
|
|||||||
@@ -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()}";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -29,6 +29,10 @@
|
|||||||
<ProjectReference Include="../../Notify/__Libraries/StellaOps.Notify.Models/StellaOps.Notify.Models.csproj" />
|
<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.Cache/StellaOps.Scanner.Cache.csproj" />
|
||||||
<ProjectReference Include="../__Libraries/StellaOps.Scanner.Storage/StellaOps.Scanner.Storage.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" />
|
<ProjectReference Include="../../Zastava/__Libraries/StellaOps.Zastava.Core/StellaOps.Zastava.Core.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
| ID | Status | Owner(s) | Depends on | Description | Exit Criteria |
|
| 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. |
|
| 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-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-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. |
|
| 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. |
|
||||||
|
|||||||
@@ -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('/');
|
||||||
|
}
|
||||||
@@ -50,8 +50,12 @@ public sealed class ArtifactStorageService
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
var normalizedDigest = $"sha256:{digestHex}";
|
var normalizedDigest = $"sha256:{digestHex}";
|
||||||
var artifactId = CatalogIdFactory.CreateArtifactId(type, normalizedDigest);
|
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(
|
var descriptor = new ArtifactObjectDescriptor(
|
||||||
_options.ObjectStore.BucketName,
|
_options.ObjectStore.BucketName,
|
||||||
key,
|
key,
|
||||||
@@ -137,45 +141,4 @@ public sealed class ArtifactStorageService
|
|||||||
return (bufferStream, total, digestHex);
|
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('/');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using Microsoft.AspNetCore.Hosting;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.Mvc.Testing;
|
using Microsoft.AspNetCore.Hosting;
|
||||||
using Microsoft.AspNetCore.TestHost;
|
using Microsoft.AspNetCore.Mvc.Testing;
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.AspNetCore.TestHost;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.Configuration;
|
||||||
using Mongo2Go;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||||
|
using Mongo2Go;
|
||||||
|
using StellaOps.Scanner.Surface.Validation;
|
||||||
|
|
||||||
namespace StellaOps.Scanner.WebService.Tests;
|
namespace StellaOps.Scanner.WebService.Tests;
|
||||||
|
|
||||||
@@ -56,14 +59,17 @@ internal sealed class ScannerApplicationFactory : WebApplicationFactory<Program>
|
|||||||
Environment.SetEnvironmentVariable("SCANNER__AUTHORITY__CLIENTID", null);
|
Environment.SetEnvironmentVariable("SCANNER__AUTHORITY__CLIENTID", null);
|
||||||
Environment.SetEnvironmentVariable("SCANNER__AUTHORITY__CLIENTSECRET", null);
|
Environment.SetEnvironmentVariable("SCANNER__AUTHORITY__CLIENTSECRET", null);
|
||||||
Environment.SetEnvironmentVariable("SCANNER__STORAGE__DSN", configuration["scanner:storage:dsn"]);
|
Environment.SetEnvironmentVariable("SCANNER__STORAGE__DSN", configuration["scanner:storage:dsn"]);
|
||||||
Environment.SetEnvironmentVariable("SCANNER__QUEUE__DSN", configuration["scanner:queue:dsn"]);
|
Environment.SetEnvironmentVariable("SCANNER__QUEUE__DSN", configuration["scanner:queue:dsn"]);
|
||||||
Environment.SetEnvironmentVariable("SCANNER__ARTIFACTSTORE__ENDPOINT", configuration["scanner:artifactStore:endpoint"]);
|
Environment.SetEnvironmentVariable("SCANNER__ARTIFACTSTORE__ENDPOINT", configuration["scanner:artifactStore:endpoint"]);
|
||||||
Environment.SetEnvironmentVariable("SCANNER__ARTIFACTSTORE__ACCESSKEY", configuration["scanner:artifactStore:accessKey"]);
|
Environment.SetEnvironmentVariable("SCANNER__ARTIFACTSTORE__ACCESSKEY", configuration["scanner:artifactStore:accessKey"]);
|
||||||
Environment.SetEnvironmentVariable("SCANNER__ARTIFACTSTORE__SECRETKEY", configuration["scanner:artifactStore:secretKey"]);
|
Environment.SetEnvironmentVariable("SCANNER__ARTIFACTSTORE__SECRETKEY", configuration["scanner:artifactStore:secretKey"]);
|
||||||
if (configuration.TryGetValue("scanner:events:enabled", out var eventsEnabled))
|
Environment.SetEnvironmentVariable("SCANNER_SURFACE_FS_ENDPOINT", "https://surface.local");
|
||||||
{
|
Environment.SetEnvironmentVariable("SCANNER_SURFACE_FS_BUCKET", configuration["scanner:artifactStore:bucket"]);
|
||||||
Environment.SetEnvironmentVariable("SCANNER__EVENTS__ENABLED", eventsEnabled);
|
Environment.SetEnvironmentVariable("SCANNER_SURFACE_PREFETCH_ENABLED", "false");
|
||||||
}
|
if (configuration.TryGetValue("scanner:events:enabled", out var eventsEnabled))
|
||||||
|
{
|
||||||
|
Environment.SetEnvironmentVariable("SCANNER__EVENTS__ENABLED", eventsEnabled);
|
||||||
|
}
|
||||||
|
|
||||||
if (configuration.TryGetValue("scanner:authority:enabled", out var authorityEnabled))
|
if (configuration.TryGetValue("scanner:authority:enabled", out var authorityEnabled))
|
||||||
{
|
{
|
||||||
@@ -100,11 +106,13 @@ internal sealed class ScannerApplicationFactory : WebApplicationFactory<Program>
|
|||||||
configBuilder.AddInMemoryCollection(configuration);
|
configBuilder.AddInMemoryCollection(configuration);
|
||||||
});
|
});
|
||||||
|
|
||||||
builder.ConfigureTestServices(services =>
|
builder.ConfigureTestServices(services =>
|
||||||
{
|
{
|
||||||
configureServices?.Invoke(services);
|
configureServices?.Invoke(services);
|
||||||
});
|
services.RemoveAll<ISurfaceValidatorRunner>();
|
||||||
}
|
services.AddSingleton<ISurfaceValidatorRunner, TestSurfaceValidatorRunner>();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
protected override void Dispose(bool disposing)
|
protected override void Dispose(bool disposing)
|
||||||
{
|
{
|
||||||
@@ -163,6 +171,19 @@ internal sealed class ScannerApplicationFactory : WebApplicationFactory<Program>
|
|||||||
current = parent.FullName;
|
current = parent.FullName;
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user