diff --git a/docs/api/console/exception-schema.md b/docs/api/console/exception-schema.md index 172140013..068cd9b6c 100644 --- a/docs/api/console/exception-schema.md +++ b/docs/api/console/exception-schema.md @@ -1,20 +1,38 @@ -# Console Exceptions API Schema (draft placeholder) +# Console Exceptions API Schema (Web I) -**Status:** TODO · awaiting Policy Guild + Platform Events +**Status:** Interim contract shipped by Web I to unblock SDK/UI work. Align to the authoritative Policy/Events schema when published. ## Scope -- `/exceptions` CRUD/workflow (create, propose, approve, revoke, list, history) proxied by Web gateway. -- Audit logging, pagination, notification hooks, rate limits, RBAC scopes. +- `/exceptions` CRUD/workflow (create, propose, approve/reject, revoke, list, history) proxied by the gateway. +- Deterministic pagination (`continuationToken`), stable sorting, and explicit audit trails. -## Needed from owners -- JSON schema for exception entity and workflow transitions; validation rules. -- Required scopes/roles; audit fields; pagination/sorting defaults; max durations/guardrails. -- Notification hook contract (`exception.*` events) and rate-limit policy. -- Sample payloads for each state and error cases. +## Headers +- `X-StellaOps-Tenant` (required) +- `X-Stella-Project` (optional) +- `X-Stella-Trace-Id` (required) +- `X-Stella-Request-Id` (required; defaults to trace ID) -## Draft sample (placeholder) -- See `docs/api/console/samples/exception-schema-sample.json` for a skeleton payload covering `pending_review` state. -- Replace with authoritative samples once schema is published. +## Scopes +- Read: `exception:read` +- Create/edit: `exception:write` +- Approve/reject/revoke: `exception:approve` -## TODO -- Replace with ratified schema + samples; log hash/date; link from Web I/II sprint logs. +## Endpoints +- `GET /exceptions?status=&severity=&search=&sortBy=&sortOrder=&limit=&continuationToken=` +- `GET /exceptions/{exceptionId}` +- `POST /exceptions` +- `PATCH /exceptions/{exceptionId}` +- `DELETE /exceptions/{exceptionId}` +- `POST /exceptions/{exceptionId}/transition` +- `GET /exceptions/stats` + +## Entity shape +This doc mirrors the web contract used by the Angular SDK (`src/Web/StellaOps.Web/src/app/core/api/exception.contract.models.ts`). + +- Sample exception: `docs/api/console/samples/exception-schema-sample.json` + +## Status transitions (baseline) +- `draft` → `pending_review` (submit) +- `pending_review` → `approved` | `rejected` +- `approved` → `revoked` +- `approved` → `expired` (timebox lapse; system-driven) diff --git a/docs/api/console/samples/exception-schema-sample.json b/docs/api/console/samples/exception-schema-sample.json index d6885e48d..dacd99b8a 100644 --- a/docs/api/console/samples/exception-schema-sample.json +++ b/docs/api/console/samples/exception-schema-sample.json @@ -1,37 +1,58 @@ { - "exceptionId": "exc::tenant-default::2025-12-06::00012", + "schemaVersion": "1.0", + "exceptionId": "exc-001", "tenantId": "tenant-default", - "title": "Risk accepted for log4j on batch nodes", - "state": "pending_review", - "type": "advisory", + "name": "log4j-temporary-exception", + "displayName": "Log4j Temporary Exception", + "description": "Temporary exception for legacy Log4j usage in internal tooling", + "type": "vulnerability", + "status": "pending_review", + "severity": "high", "scope": { - "level": "asset", - "assetIds": ["batch-node-17", "batch-node-18"], - "advisoryIds": ["CVE-2021-44228"], - "components": ["pkg:maven/org.apache.logging.log4j/log4j-core@2.14.0"] + "type": "component", + "tenantId": "tenant-default", + "componentPurls": ["pkg:maven/org.apache.logging.log4j/log4j-core@2.14.1"], + "vulnIds": ["CVE-2021-44228"] }, "justification": { - "template": "compensating_control", - "details": "Ingress disabled; nodes isolated; patch planned 2025-12-20" + "template": "compensating-control", + "text": "Ingress disabled; nodes isolated; patch planned 2025-12-20" }, "timebox": { - "start": "2025-12-06T00:00:00Z", - "end": "2025-12-31T00:00:00Z", - "maxRenewals": 1 + "startDate": "2025-12-03T00:00:00Z", + "endDate": "2025-12-31T23:59:59Z", + "autoRenew": false }, - "audit": { - "createdBy": "alice@example.com", - "createdAt": "2025-12-06T11:12:13Z", - "modifiedAt": "2025-12-06T11:12:13Z" + "approvals": [ + { + "approvalId": "apr-001", + "approvedBy": "security-lead@example.com", + "approvedAt": "2025-12-04T10:30:00Z", + "comment": "Approve with condition: migrate before expiry." + } + ], + "auditTrail": [ + { + "auditId": "aud-001", + "action": "created", + "actor": "alice@example.com", + "timestamp": "2025-12-03T00:00:00Z" + }, + { + "auditId": "aud-002", + "action": "submitted", + "actor": "alice@example.com", + "timestamp": "2025-12-03T00:05:00Z", + "previousStatus": "draft", + "newStatus": "pending_review" + } + ], + "labels": { + "ticket": "SEC-123", + "owner": "platform-security" }, - "links": { - "history": "/console/exceptions/exc::tenant-default::2025-12-06::00012/history", - "attachments": [ - { - "name": "risk-assessment.pdf", - "url": "https://console.local/files/risk-assessment.pdf?sig=...", - "sha256": "cafe..." - } - ] - } + "createdBy": "alice@example.com", + "createdAt": "2025-12-03T00:00:00Z", + "updatedBy": "alice@example.com", + "updatedAt": "2025-12-03T00:05:00Z" } diff --git a/docs/api/gateway/advisories.md b/docs/api/gateway/advisories.md new file mode 100644 index 000000000..86b0000bd --- /dev/null +++ b/docs/api/gateway/advisories.md @@ -0,0 +1,31 @@ +# Advisory Gateway Contract (draft v0.1) + +Scope: proxy Advisory surfaces through the Web gateway with tenant scoping, deterministic responses, ETag caching, and offline-friendly pagination. + +## Security / headers +- `Authorization: Bearer ` (or `DPoP` where configured) +- `X-StellaOps-Tenant: ` (required) +- `X-Stella-Project: ` (optional) +- `X-Stella-Trace-Id: ` (optional; clients SHOULD send one) +- Scopes: `advisory:read` + +## Endpoints +- `GET /advisories` — list advisories (tenant-scoped). + - Query params: `search`, `severity`, `sortBy`, `sortOrder`, `limit`, `continuationToken` + - Response: `AdvisoryListResponse` (see sample) +- `GET /advisories/{advisoryId}` — advisory detail (tenant-scoped). + +## Caching & pagination +- `limit` max: `200`. +- Cursor/paging uses `continuationToken` (opaque string). +- `ETag` MUST be a stable hash over a sorted payload; clients MAY send `If-None-Match`. +- Recommended headers: `Cache-Control: private, max-age=60, stale-if-error=300`. + +## Determinism rules +- Ordering: `items` sorted by `(advisoryId asc)` unless `sortBy` is supplied; ties break by `advisoryId`. +- Timestamps: ISO-8601 UTC. + +## Samples +- `docs/api/gateway/samples/advisories-list.json` +- `docs/api/gateway/samples/advisory-detail.json` + diff --git a/docs/api/gateway/advisory-ai.md b/docs/api/gateway/advisory-ai.md new file mode 100644 index 000000000..73aba4022 --- /dev/null +++ b/docs/api/gateway/advisory-ai.md @@ -0,0 +1,39 @@ +# Gateway Advisory AI API (Web I) + +**Status:** Interim contract shipped by Web I to unblock SDK/UI work. Align to the authoritative Advisory AI OpenAPI when the gateway routing plugin publishes it. + +## Overview +- Gateway routes advisory AI traffic under `/advisory/ai/*`. +- Web clients must send tenant + trace headers and should avoid sending raw prompts in telemetry; use prompt hashes instead. + +## Headers +- `X-StellaOps-Tenant` (required) +- `X-Stella-Project` (optional) +- `X-Stella-Trace-Id` (required) +- `X-Stella-Request-Id` (required; defaults to trace ID) +- `X-StellaOps-AI-Profile` (optional; e.g. `standard`) +- `X-StellaOps-Prompt-Hash` (recommended; send a stable hash, not the prompt) + +## Scopes +- Read: `advisory-ai:read` +- Write/operate: `advisory-ai:write` + +## Endpoints + +### `POST /advisory/ai/jobs` +Creates a new advisory AI job. + +- Request: `docs/api/gateway/samples/advisory-ai-start-job.json` +- Response: `docs/api/gateway/samples/advisory-ai-start-job-response.json` + +### `GET /advisory/ai/jobs/{jobId}` +Returns job status and optional results once completed. + +### `POST /advisory/ai/jobs/{jobId}/cancel` +Cancels a queued/running job. + +### `GET /advisory/ai/jobs/{jobId}/events` +Server-sent events stream (JSON payload per message): + +- Sample NDJSON: `docs/api/gateway/samples/advisory-ai-job-events.ndjson` + diff --git a/docs/api/gateway/exception-events.md b/docs/api/gateway/exception-events.md new file mode 100644 index 000000000..0a86ca575 --- /dev/null +++ b/docs/api/gateway/exception-events.md @@ -0,0 +1,34 @@ +# Exception Events Gateway Contract (draft v0.1) + +Scope: stream exception workflow events (`exception.*`) for Console activity feeds and Notify integrations. + +## Security / headers +- `Authorization: Bearer ` (or `DPoP` where configured) +- `X-StellaOps-Tenant: ` (required) +- `X-Stella-Project: ` (optional) +- Scopes: `exception:read` + +## Endpoint +- `GET /exceptions/events` — Server-Sent Events (SSE) stream. + - Query params: `tenant`, `traceId`, `projectId` (optional) + - Response: `text/event-stream` + +## Event types +- `exception.created` +- `exception.updated` +- `exception.status_changed` +- `exception.deleted` + +## Rate limits (proposal) +- Max 1 active SSE connection per browser session. +- Heartbeat every 30s; server closes idle connections after 60s without reads. +- When rate limited: `429` with `Retry-After`. + +## Notify integration +Gateways SHOULD forward these events to Notify where configured: +- `exception.status_changed` → `notify.event` with severity derived from exception severity and status. +- All events include `traceId` for audit correlation. + +## Samples +- `docs/api/gateway/samples/exception-events.ndjson` + diff --git a/docs/api/gateway/orchestrator.md b/docs/api/gateway/orchestrator.md new file mode 100644 index 000000000..52ff52095 --- /dev/null +++ b/docs/api/gateway/orchestrator.md @@ -0,0 +1,60 @@ +# Orchestrator Gateway Contract (draft v0.2) + +Scope: expose Orchestrator read + operator control surfaces through the Web gateway (tenant-scoped, deterministic pagination, cache headers) to unblock Console control-plane views. + +This is an interim contract until the gateway is aligned to the Orchestrator OpenAPI (`/openapi/orchestrator.json` in the Orchestrator service). + +## Security / headers +- `Authorization: Bearer ` (or `DPoP` where configured) +- `X-StellaOps-Tenant: ` (required) +- `X-Stella-Project: ` (optional) +- `X-Stella-Trace-Id: ` (optional; clients SHOULD send one) +- Scopes: `orch:read`, `orch:quota`, `orch:operate`, `orch:backfill` (endpoint-specific) +- Operator audit headers (required on mutating requests): + - `X-Stella-Operator-Reason: ` + - `X-Stella-Operator-Ticket: ` (optional but recommended) + +## Endpoints +- `GET /orchestrator/sources` — list registered job sources (tenant-scoped). + - Query params: `sourceType`, `enabled`, `limit`, `continuationToken` +- `GET /orchestrator/sources/{sourceId}` — source detail. +- `GET /orchestrator/quotas` — list quotas (scope: `orch:quota`). + - Query params: `jobType`, `paused`, `limit`, `continuationToken` +- `GET /orchestrator/quotas/{quotaId}` — quota detail (scope: `orch:quota`). +- `POST /orchestrator/quotas` — create quota (scope: `orch:quota`). +- `PUT /orchestrator/quotas/{quotaId}` — update quota (scope: `orch:quota`). +- `DELETE /orchestrator/quotas/{quotaId}` — delete quota (scope: `orch:quota`). +- `POST /orchestrator/quotas/{quotaId}/pause` — pause quota (scope: `orch:quota`). +- `POST /orchestrator/quotas/{quotaId}/resume` — resume quota (scope: `orch:quota`). +- `GET /orchestrator/quotas/summary` — quota/backpressure metrics summary (scope: `orch:quota`). +- `GET /orchestrator/jobs/summary` — job summary counts (scope: `orch:read`). +- `GET /orchestrator/deadletter/stats` — deadletter stats and top error clustering (scope: `orch:operate`). +- `GET /orchestrator/deadletter/summary` — grouped deadletter summary (scope: `orch:operate`). +- `POST /orchestrator/deadletter/{entryId}/replay` — replay a deadletter entry (scope: `orch:backfill`). +- `POST /orchestrator/deadletter/replay/batch` — replay a set of entry IDs (scope: `orch:backfill`). +- `POST /orchestrator/deadletter/replay/pending` — replay pending entries by filter (scope: `orch:backfill`). +- `POST /orchestrator/pack-runs/{packRunId}/cancel` — cancel a pack run (scope: `orch:operate`). +- `POST /orchestrator/pack-runs/{packRunId}/retry` — retry a pack run (scope: `orch:backfill`). + +## Caching & pagination +- `limit` max: `200`. +- Cursor/paging uses `continuationToken` (opaque string). +- Recommended headers: `Cache-Control: private, max-age=60, stale-if-error=300`. +- `ETag` SHOULD be stable over a sorted payload; clients MAY send `If-None-Match`. + +## Determinism rules +- Ordering: + - sources: `(name asc, sourceId asc)` + - quotas: `(jobType asc, quotaId asc)` + - deadletter summary: `(category asc, errorCode asc)` +- Timestamps: ISO-8601 UTC. + +## Samples +- `docs/api/gateway/samples/orchestrator-sources.json` +- `docs/api/gateway/samples/orchestrator-quotas.json` +- `docs/api/gateway/samples/orchestrator-quota-summary.json` +- `docs/api/gateway/samples/orchestrator-deadletter-stats.json` +- `docs/api/gateway/samples/orchestrator-deadletter-summary.json` +- `docs/api/gateway/samples/orchestrator-deadletter-replay.json` +- `docs/api/gateway/samples/orchestrator-packrun-cancel.json` +- `docs/api/gateway/samples/orchestrator-packrun-retry.json` diff --git a/docs/api/gateway/policy-evidence.md b/docs/api/gateway/policy-evidence.md new file mode 100644 index 000000000..f15715ac7 --- /dev/null +++ b/docs/api/gateway/policy-evidence.md @@ -0,0 +1,46 @@ +# Policy + Evidence Composition Contract (draft v0.1) + +Scope: provide a single Console-friendly response that combines policy evaluation output with related advisory and VEX evidence linksets for the same findings/component(s). + +This contract is intended to reduce UI round-trips by composing existing gateway surfaces: +- Policy + Exceptions (`POST /policy/effective`) +- Advisories (`GET /advisories`) +- VEX Evidence (`GET /vex/statements`) + +## Security / headers +- `Authorization: Bearer ` (or `DPoP` where configured) +- `X-StellaOps-Tenant: ` (required) +- `X-Stella-Project: ` (optional) +- `X-Stella-Trace-Id: ` (optional; clients SHOULD send one) +- Scopes: + - `policy:read` AND `exception:read` + - `advisory:read` + - `vex:read` + +## Endpoint +- `POST /policy/evidence/component` — compose policy result + evidence linksets for the supplied findings. + +## Request/response notes +- Request shape reuses `PolicyFindingRef` fields from the Policy + Exceptions contract (`docs/api/gateway/policy-exceptions.md`). +- Response includes: + - `policy` (Policy effective view; deterministic ordering by `findingId`) + - `advisories` (summaries; deterministic ordering by `advisoryId`) + - `vexStatements` (summaries; deterministic ordering by `statementId`) + - `linksets` mapping each `findingId` to the related `advisoryIds` and `vexStatementIds` +- Implementations MUST NOT invent verdicts; this is a pure composition surface. + +## Caching & limits +- Composition responses SHOULD be cacheable for a short TTL when inputs are identical. +- Recommended headers: `Cache-Control: private, max-age=30, stale-if-error=120`. +- Recommended caps (UI/Gateway): findings max `500`, advisories max `200`, VEX statements max `200`. + +## Determinism rules +- `findings` sorted by `(findingId asc)` before evaluation. +- `policy.items` sorted by `(findingId asc)`. +- `advisories` sorted by `(advisoryId asc)`. +- `vexStatements` sorted by `(statementId asc)`. +- Timestamps: ISO-8601 UTC. + +## Samples +- `docs/api/gateway/samples/policy-evidence-component.json` + diff --git a/docs/api/gateway/policy-exceptions.md b/docs/api/gateway/policy-exceptions.md new file mode 100644 index 000000000..0e7f267c1 --- /dev/null +++ b/docs/api/gateway/policy-exceptions.md @@ -0,0 +1,26 @@ +# Policy + Exceptions Gateway Contract (draft v0.1) + +Scope: expose policy evaluation results that include exception metadata, plus simulation endpoints that accept exception overrides for what-if analysis. + +## Security / headers +- `Authorization: Bearer ` (or `DPoP` where configured) +- `X-StellaOps-Tenant: ` (required) +- `X-Stella-Project: ` (optional) +- `X-Stella-Trace-Id: ` (optional; clients SHOULD send one) +- Scopes: + - `POST /policy/effective`: `policy:read` AND `exception:read` + - `POST /policy/simulate`: `policy:simulate` AND `exception:read` + +## Endpoints +- `POST /policy/effective` — deterministic effective policy view over a list of findings. +- `POST /policy/simulate` — simulate policy result changes with exception overrides. + +## Request/response notes +- Requests MUST remain deterministic: stable ordering, ISO-8601 UTC timestamps only. +- Pagination uses `limit` (max `200`) and `continuationToken` (opaque string). +- Exception metadata SHOULD reuse the Exception schema (`docs/api/console/exception-schema.md`) but MAY omit large fields like audit trails. + +## Samples +- `docs/api/gateway/samples/policy-effective-sample.json` +- `docs/api/gateway/samples/policy-simulate-sample.json` + diff --git a/docs/api/gateway/samples/advisories-list.json b/docs/api/gateway/samples/advisories-list.json new file mode 100644 index 000000000..da9e037aa --- /dev/null +++ b/docs/api/gateway/samples/advisories-list.json @@ -0,0 +1,31 @@ +{ + "items": [ + { + "advisoryId": "CVE-2024-12345", + "source": "cve", + "title": "Example advisory for offline demo", + "severity": "high", + "publishedAt": "2025-12-01T00:00:00Z", + "updatedAt": "2025-12-10T00:00:00Z", + "cveIds": ["CVE-2024-12345"], + "affectedPurls": ["pkg:npm/example@1.2.3"], + "etag": "\"adv-CVE-2024-12345-v1\"" + }, + { + "advisoryId": "GHSA-aaaa-bbbb-cccc", + "source": "ghsa", + "title": "Example GHSA advisory", + "severity": "critical", + "publishedAt": "2025-11-20T00:00:00Z", + "updatedAt": "2025-12-05T00:00:00Z", + "cveIds": ["CVE-2025-00001"], + "affectedPurls": ["pkg:maven/com.example/demo@0.9.0"], + "etag": "\"adv-GHSA-aaaa-bbbb-cccc-v1\"" + } + ], + "count": 2, + "continuationToken": null, + "etag": "\"advisories-sample-v1\"", + "traceId": "trace-sample-1" +} + diff --git a/docs/api/gateway/samples/advisory-ai-job-events.ndjson b/docs/api/gateway/samples/advisory-ai-job-events.ndjson new file mode 100644 index 000000000..9f2d5d4c6 --- /dev/null +++ b/docs/api/gateway/samples/advisory-ai-job-events.ndjson @@ -0,0 +1,6 @@ +{"jobId":"aiai-job-0001","kind":"status","at":"2025-12-03T00:00:00Z","status":"queued","progressPercent":0} +{"jobId":"aiai-job-0001","kind":"status","at":"2025-12-03T00:00:01Z","status":"running","progressPercent":10} +{"jobId":"aiai-job-0001","kind":"chunk","at":"2025-12-03T00:00:02Z","chunkIndex":0,"chunk":"Chunk 1: Input normalized."} +{"jobId":"aiai-job-0001","kind":"chunk","at":"2025-12-03T00:00:03Z","chunkIndex":1,"chunk":"Chunk 2: Evidence gathered."} +{"jobId":"aiai-job-0001","kind":"done","at":"2025-12-03T00:00:04Z","status":"completed","message":"Done"} + diff --git a/docs/api/gateway/samples/advisory-ai-start-job-response.json b/docs/api/gateway/samples/advisory-ai-start-job-response.json new file mode 100644 index 000000000..e138c25f1 --- /dev/null +++ b/docs/api/gateway/samples/advisory-ai-start-job-response.json @@ -0,0 +1,26 @@ +{ + "jobId": "aiai-job-0001", + "status": "queued", + "traceId": "trace-fixture-aiai", + "createdAt": "2025-12-03T00:00:00Z", + "guardrail": { + "blocked": false, + "state": "ok", + "violations": [], + "metadata": { + "blockedPhraseFile": "configs/guardrails/blocked-phrases.json", + "blocked_phrase_count": 0, + "promptLength": 0, + "planFromCache": false, + "promptHash": "sha256:0000000000000000", + "telemetryCounters": { + "advisory_ai_guardrail_blocks_total": 0, + "advisory_ai_chunk_cache_hits_total": 0 + }, + "links": { + "logs": "/audit/advisory-ai/runs/2025-12-03T00:00:00Z" + } + } + } +} + diff --git a/docs/api/gateway/samples/advisory-ai-start-job.json b/docs/api/gateway/samples/advisory-ai-start-job.json new file mode 100644 index 000000000..532b5c93a --- /dev/null +++ b/docs/api/gateway/samples/advisory-ai-start-job.json @@ -0,0 +1,14 @@ +{ + "profile": "standard", + "prompt": "", + "maxTokens": 2048, + "dryRun": false, + "context": { + "sbomDigests": [ + "sha256:6c81f2bbd8bd7336f197f3f68fba2f76d7287dd1a5e2a0f0e9f14f23f3c2f917" + ], + "vulnIds": ["CVE-2021-44228"], + "purls": ["pkg:maven/org.apache.logging.log4j/log4j-core@2.14.1"] + } +} + diff --git a/docs/api/gateway/samples/advisory-detail.json b/docs/api/gateway/samples/advisory-detail.json new file mode 100644 index 000000000..1f04681aa --- /dev/null +++ b/docs/api/gateway/samples/advisory-detail.json @@ -0,0 +1,20 @@ +{ + "advisoryId": "CVE-2024-12345", + "source": "cve", + "title": "Example advisory for offline demo", + "severity": "high", + "publishedAt": "2025-12-01T00:00:00Z", + "updatedAt": "2025-12-10T00:00:00Z", + "cveIds": ["CVE-2024-12345"], + "affectedPurls": ["pkg:npm/example@1.2.3"], + "description": "Deterministic sample advisory payload for Console and SDK development.", + "references": [ + { + "label": "Vendor bulletin", + "url": "https://example.invalid/advisories/CVE-2024-12345" + } + ], + "advisoryUrl": "https://example.invalid/advisories/CVE-2024-12345", + "etag": "\"adv-CVE-2024-12345-v1\"" +} + diff --git a/docs/api/gateway/samples/exception-events.ndjson b/docs/api/gateway/samples/exception-events.ndjson new file mode 100644 index 000000000..3b142d4c8 --- /dev/null +++ b/docs/api/gateway/samples/exception-events.ndjson @@ -0,0 +1,3 @@ +{"type":"exception.created","tenantId":"tenant-default","exceptionId":"exc-001","timestamp":"2025-12-10T00:00:00Z","actor":"user:demo","traceId":"trace-evt-1","metadata":{"message":"Draft created"}} +{"type":"exception.status_changed","tenantId":"tenant-default","exceptionId":"exc-001","timestamp":"2025-12-10T00:05:00Z","actor":"user:approver","previousStatus":"pending_review","newStatus":"approved","traceId":"trace-evt-2","metadata":{"comment":"Approved for demo tenant."}} + diff --git a/docs/api/gateway/samples/orchestrator-deadletter-replay.json b/docs/api/gateway/samples/orchestrator-deadletter-replay.json new file mode 100644 index 000000000..e9f8586ef --- /dev/null +++ b/docs/api/gateway/samples/orchestrator-deadletter-replay.json @@ -0,0 +1,24 @@ +{ + "success": true, + "newJobId": "dddddddd-dddd-dddd-dddd-dddddddddddd", + "errorMessage": null, + "updatedEntry": { + "entryId": "eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee", + "originalJobId": "ffffffff-ffff-ffff-ffff-ffffffffffff", + "runId": null, + "sourceId": null, + "jobType": "export", + "status": "Replayed", + "errorCode": "quota_exceeded", + "failureReason": "Quota exceeded for job type export.", + "remediationHint": "Increase quota or retry later.", + "category": "quota_exceeded", + "isRetryable": true, + "canReplay": false, + "failedAt": "2025-12-10T00:00:00Z", + "createdAt": "2025-12-10T00:00:00Z", + "expiresAt": "2026-01-10T00:00:00Z", + "resolvedAt": null + }, + "traceId": "trace-sample" +} diff --git a/docs/api/gateway/samples/orchestrator-deadletter-stats.json b/docs/api/gateway/samples/orchestrator-deadletter-stats.json new file mode 100644 index 000000000..cc3dc435d --- /dev/null +++ b/docs/api/gateway/samples/orchestrator-deadletter-stats.json @@ -0,0 +1,24 @@ +{ + "totalEntries": 4, + "pendingEntries": 2, + "replayingEntries": 0, + "replayedEntries": 1, + "resolvedEntries": 1, + "exhaustedEntries": 0, + "expiredEntries": 0, + "retryableEntries": 3, + "byCategory": { + "quota_exceeded": 2, + "upstream_error": 2 + }, + "topErrorCodes": { + "invalid_payload": 1, + "quota_exceeded": 2, + "upstream_timeout": 1 + }, + "topJobTypes": { + "export": 2, + "pack-run": 2 + }, + "traceId": "trace-sample" +} diff --git a/docs/api/gateway/samples/orchestrator-deadletter-summary.json b/docs/api/gateway/samples/orchestrator-deadletter-summary.json new file mode 100644 index 000000000..4d1b4b429 --- /dev/null +++ b/docs/api/gateway/samples/orchestrator-deadletter-summary.json @@ -0,0 +1,21 @@ +{ + "items": [ + { + "errorCode": "quota_exceeded", + "category": "quota_exceeded", + "entryCount": 2, + "retryableCount": 2, + "oldestEntry": "2025-12-10T00:00:00Z", + "sampleReason": "Quota exceeded for job type export." + }, + { + "errorCode": "upstream_timeout", + "category": "upstream_error", + "entryCount": 1, + "retryableCount": 1, + "oldestEntry": "2025-12-11T00:00:00Z", + "sampleReason": "Upstream service timeout." + } + ], + "traceId": "trace-sample" +} diff --git a/docs/api/gateway/samples/orchestrator-packrun-cancel.json b/docs/api/gateway/samples/orchestrator-packrun-cancel.json new file mode 100644 index 000000000..e56e6c16b --- /dev/null +++ b/docs/api/gateway/samples/orchestrator-packrun-cancel.json @@ -0,0 +1,7 @@ +{ + "packRunId": "99999999-9999-9999-9999-999999999999", + "status": "canceled", + "reason": "Operator requested cancellation.", + "canceledAt": "2025-12-12T00:00:00Z", + "traceId": "trace-sample" +} diff --git a/docs/api/gateway/samples/orchestrator-packrun-retry.json b/docs/api/gateway/samples/orchestrator-packrun-retry.json new file mode 100644 index 000000000..f38a2fa90 --- /dev/null +++ b/docs/api/gateway/samples/orchestrator-packrun-retry.json @@ -0,0 +1,7 @@ +{ + "originalPackRunId": "99999999-9999-9999-9999-999999999999", + "newPackRunId": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee", + "status": "scheduled", + "createdAt": "2025-12-12T00:00:00Z", + "traceId": "trace-sample" +} diff --git a/docs/api/gateway/samples/orchestrator-quota-summary.json b/docs/api/gateway/samples/orchestrator-quota-summary.json new file mode 100644 index 000000000..b4445867e --- /dev/null +++ b/docs/api/gateway/samples/orchestrator-quota-summary.json @@ -0,0 +1,25 @@ +{ + "totalQuotas": 2, + "pausedQuotas": 1, + "averageTokenUtilization": 0.5625, + "averageConcurrencyUtilization": 0.6, + "quotas": [ + { + "quotaId": "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb", + "jobType": "export", + "tokenUtilization": 0.75, + "concurrencyUtilization": 1, + "hourlyUtilization": 0.9667, + "paused": true + }, + { + "quotaId": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", + "jobType": "pack-run", + "tokenUtilization": 0.375, + "concurrencyUtilization": 0.2, + "hourlyUtilization": 0.09, + "paused": false + } + ], + "traceId": "trace-sample" +} diff --git a/docs/api/gateway/samples/orchestrator-quotas.json b/docs/api/gateway/samples/orchestrator-quotas.json new file mode 100644 index 000000000..709e74545 --- /dev/null +++ b/docs/api/gateway/samples/orchestrator-quotas.json @@ -0,0 +1,44 @@ +{ + "items": [ + { + "quotaId": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", + "tenantId": "tenant-default", + "jobType": "pack-run", + "maxActive": 10, + "maxPerHour": 200, + "burstCapacity": 20, + "refillRate": 1, + "currentTokens": 12.5, + "currentActive": 2, + "currentHourCount": 18, + "paused": false, + "pauseReason": null, + "quotaTicket": null, + "createdAt": "2025-11-01T00:00:00Z", + "updatedAt": "2025-12-10T00:00:00Z", + "updatedBy": "user:demo" + }, + { + "quotaId": "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb", + "tenantId": "tenant-default", + "jobType": "export", + "maxActive": 3, + "maxPerHour": 60, + "burstCapacity": 6, + "refillRate": 0.5, + "currentTokens": 1.5, + "currentActive": 3, + "currentHourCount": 58, + "paused": true, + "pauseReason": "Maintenance window", + "quotaTicket": "OPS-1234", + "createdAt": "2025-11-15T00:00:00Z", + "updatedAt": "2025-12-11T00:00:00Z", + "updatedBy": "user:ops" + } + ], + "count": 2, + "continuationToken": null, + "etag": "\"orch-quotas-v1\"", + "traceId": "trace-sample" +} diff --git a/docs/api/gateway/samples/orchestrator-sources.json b/docs/api/gateway/samples/orchestrator-sources.json new file mode 100644 index 000000000..7a85887ee --- /dev/null +++ b/docs/api/gateway/samples/orchestrator-sources.json @@ -0,0 +1,33 @@ +{ + "items": [ + { + "sourceId": "11111111-1111-1111-1111-111111111111", + "name": "concelier-ingest", + "sourceType": "concelier", + "enabled": true, + "paused": false, + "pauseReason": null, + "pauseTicket": null, + "createdAt": "2025-11-01T00:00:00Z", + "updatedAt": "2025-12-10T00:00:00Z", + "updatedBy": "user:demo" + }, + { + "sourceId": "22222222-2222-2222-2222-222222222222", + "name": "export-center", + "sourceType": "export", + "enabled": true, + "paused": true, + "pauseReason": "Maintenance window", + "pauseTicket": "OPS-1234", + "createdAt": "2025-11-15T00:00:00Z", + "updatedAt": "2025-12-11T00:00:00Z", + "updatedBy": "user:ops" + } + ], + "count": 2, + "continuationToken": null, + "etag": "\"orch-sources-sample-1\"", + "traceId": "trace-sample-orch-1" +} + diff --git a/docs/api/gateway/samples/policy-effective-sample.json b/docs/api/gateway/samples/policy-effective-sample.json new file mode 100644 index 000000000..ea2194143 --- /dev/null +++ b/docs/api/gateway/samples/policy-effective-sample.json @@ -0,0 +1,43 @@ +{ + "policyVersion": "sha256:policy-demo", + "items": [ + { + "findingId": "finding-1", + "status": "affected", + "severityBand": "High", + "severityScore": 7.5, + "exceptions": [ + { + "schemaVersion": "1.0", + "exceptionId": "exc-001", + "tenantId": "tenant-default", + "name": "temporary-risk-acceptance", + "displayName": "Temporary Risk Acceptance", + "description": "Deterministic stub exception used for policy/effective and policy/simulate demos.", + "status": "approved", + "severity": "high", + "scope": { + "type": "component", + "componentPurls": ["pkg:npm/example@1.2.3"], + "vulnIds": ["CVE-2024-12345"] + }, + "justification": { + "template": "risk-accepted", + "text": "Approved for demo tenant while remediation is planned." + }, + "timebox": { + "startDate": "2025-12-01T00:00:00Z", + "endDate": "2025-12-31T23:59:59Z" + }, + "createdBy": "user:demo", + "createdAt": "2025-12-01T00:00:00Z", + "updatedBy": "user:demo", + "updatedAt": "2025-12-10T00:00:00Z" + } + ] + } + ], + "continuationToken": null, + "traceId": "trace-sample-4" +} + diff --git a/docs/api/gateway/samples/policy-evidence-component.json b/docs/api/gateway/samples/policy-evidence-component.json new file mode 100644 index 000000000..08163b97d --- /dev/null +++ b/docs/api/gateway/samples/policy-evidence-component.json @@ -0,0 +1,86 @@ +{ + "findings": [ + { + "findingId": "finding-1", + "vulnId": "CVE-2024-12345", + "componentPurl": "pkg:npm/example@1.2.3", + "assetId": "asset::registry.local/ops/auth" + } + ], + "policy": { + "policyVersion": "sha256:policy-demo", + "items": [ + { + "findingId": "finding-1", + "status": "affected", + "severityBand": "High", + "severityScore": 7.5, + "exceptions": [ + { + "schemaVersion": "1.0", + "exceptionId": "exc-001", + "tenantId": "tenant-default", + "name": "temporary-risk-acceptance", + "displayName": "Temporary Risk Acceptance", + "status": "approved", + "severity": "high", + "scope": { + "type": "component", + "componentPurls": ["pkg:npm/example@1.2.3"], + "vulnIds": ["CVE-2024-12345"] + }, + "justification": { + "template": "risk-accepted", + "text": "Approved for demo tenant while remediation is planned." + }, + "timebox": { + "startDate": "2025-12-01T00:00:00Z", + "endDate": "2025-12-31T23:59:59Z" + }, + "createdBy": "user:demo", + "createdAt": "2025-12-01T00:00:00Z", + "updatedBy": "user:demo", + "updatedAt": "2025-12-10T00:00:00Z" + } + ] + } + ], + "continuationToken": null, + "traceId": "trace-sample-6" + }, + "advisories": [ + { + "advisoryId": "CVE-2024-12345", + "source": "cve", + "title": "Example advisory for offline demo", + "severity": "high", + "publishedAt": "2025-12-01T00:00:00Z", + "updatedAt": "2025-12-10T00:00:00Z", + "cveIds": ["CVE-2024-12345"], + "affectedPurls": ["pkg:npm/example@1.2.3"], + "etag": "\"adv-CVE-2024-12345-v1\"" + } + ], + "vexStatements": [ + { + "statementId": "vex::tenant-default::CVE-2024-12345::001", + "vulnId": "CVE-2024-12345", + "productId": "asset::registry.local/ops/auth", + "status": "not_affected", + "justification": "Component not present in runtime image.", + "updatedAt": "2025-12-10T00:00:00Z", + "etag": "\"vex-001-v1\"" + } + ], + "linksets": [ + { + "findingId": "finding-1", + "vulnId": "CVE-2024-12345", + "advisoryIds": ["CVE-2024-12345"], + "vexStatementIds": ["vex::tenant-default::CVE-2024-12345::001"] + } + ], + "traceId": "trace-sample-6", + "etag": "\"policy-evidence-sample-1\"" +} + diff --git a/docs/api/gateway/samples/policy-simulate-sample.json b/docs/api/gateway/samples/policy-simulate-sample.json new file mode 100644 index 000000000..1a253d6c9 --- /dev/null +++ b/docs/api/gateway/samples/policy-simulate-sample.json @@ -0,0 +1,75 @@ +{ + "request": { + "findings": [ + { + "findingId": "finding-1", + "vulnId": "CVE-2024-12345", + "componentPurl": "pkg:npm/example@1.2.3", + "assetId": "asset::registry.local/ops/auth" + } + ], + "exceptionOverrides": [ + { + "mode": "apply", + "exception": { + "name": "temporary-risk-acceptance", + "severity": "high", + "status": "draft", + "scope": { + "type": "component", + "componentPurls": ["pkg:npm/example@1.2.3"], + "vulnIds": ["CVE-2024-12345"] + }, + "justification": { + "template": "risk-accepted", + "text": "What-if analysis for planned remediation." + }, + "timebox": { + "startDate": "2025-12-12T00:00:00Z", + "endDate": "2025-12-19T00:00:00Z" + } + } + } + ] + }, + "response": { + "policyVersion": "sha256:policy-demo", + "items": [ + { + "findingId": "finding-1", + "status": "quieted", + "severityBand": "None", + "severityScore": 0, + "exceptions": [ + { + "schemaVersion": "1.0", + "exceptionId": "exc-001", + "tenantId": "tenant-default", + "name": "temporary-risk-acceptance", + "displayName": "Temporary Risk Acceptance", + "status": "approved", + "severity": "high", + "scope": { + "type": "component", + "componentPurls": ["pkg:npm/example@1.2.3"], + "vulnIds": ["CVE-2024-12345"] + }, + "justification": { + "template": "risk-accepted", + "text": "Approved for demo tenant while remediation is planned." + }, + "timebox": { + "startDate": "2025-12-01T00:00:00Z", + "endDate": "2025-12-31T23:59:59Z" + }, + "createdBy": "user:demo", + "createdAt": "2025-12-01T00:00:00Z" + } + ] + } + ], + "continuationToken": null, + "traceId": "trace-sample-5" + } +} + diff --git a/docs/api/gateway/samples/vex-evidence-list.json b/docs/api/gateway/samples/vex-evidence-list.json new file mode 100644 index 000000000..7fb4d41f5 --- /dev/null +++ b/docs/api/gateway/samples/vex-evidence-list.json @@ -0,0 +1,16 @@ +{ + "statementId": "vex::tenant-default::CVE-2024-12345::001", + "items": [ + { + "evidenceId": "evidence::001", + "kind": "document", + "url": "https://example.invalid/evidence/001.json?sig=mock", + "sha256": "sha256:0000000000000000000000000000000000000000000000000000000000000000", + "expiresAt": "2025-12-31T23:59:59Z" + } + ], + "count": 1, + "etag": "\"vex-evidence-sample-v1\"", + "traceId": "trace-sample-3" +} + diff --git a/docs/api/gateway/samples/vex-statement-detail.json b/docs/api/gateway/samples/vex-statement-detail.json new file mode 100644 index 000000000..399c00de1 --- /dev/null +++ b/docs/api/gateway/samples/vex-statement-detail.json @@ -0,0 +1,18 @@ +{ + "statementId": "vex::tenant-default::CVE-2024-12345::001", + "vulnId": "CVE-2024-12345", + "productId": "asset::registry.local/ops/auth", + "status": "not_affected", + "justification": "Component not present in runtime image.", + "issuer": "vendor:example", + "affectedPurls": ["pkg:npm/example@1.2.3"], + "references": [ + { + "label": "Analysis report", + "url": "https://example.invalid/vex/report/001" + } + ], + "updatedAt": "2025-12-10T00:00:00Z", + "etag": "\"vex-001-v1\"" +} + diff --git a/docs/api/gateway/samples/vex-statements-list.json b/docs/api/gateway/samples/vex-statements-list.json new file mode 100644 index 000000000..919fd53bc --- /dev/null +++ b/docs/api/gateway/samples/vex-statements-list.json @@ -0,0 +1,18 @@ +{ + "items": [ + { + "statementId": "vex::tenant-default::CVE-2024-12345::001", + "vulnId": "CVE-2024-12345", + "productId": "asset::registry.local/ops/auth", + "status": "not_affected", + "justification": "Component not present in runtime image.", + "updatedAt": "2025-12-10T00:00:00Z", + "etag": "\"vex-001-v1\"" + } + ], + "count": 1, + "continuationToken": null, + "etag": "\"vex-statements-sample-v1\"", + "traceId": "trace-sample-2" +} + diff --git a/docs/api/gateway/vex-evidence.md b/docs/api/gateway/vex-evidence.md new file mode 100644 index 000000000..d68cd9aac --- /dev/null +++ b/docs/api/gateway/vex-evidence.md @@ -0,0 +1,43 @@ +# VEX Evidence Gateway Contract (draft v0.1) + +Scope: expose read-only VEX statement and evidence routes through the Web gateway with tenant scoping, deterministic ordering, and export helpers for offline bundles. + +## Security / headers +- `Authorization: Bearer ` (or `DPoP` where configured) +- `X-StellaOps-Tenant: ` (required) +- `X-Stella-Project: ` (optional) +- `X-Stella-Trace-Id: ` (optional; clients SHOULD send one) +- Scopes: + - `vex:read` for list/detail/evidence + - `vex:export` for export handlers + +## Endpoints +- `GET /vex/statements` — list statements (tenant-scoped). + - Query params: `vulnId`, `status`, `search`, `limit`, `continuationToken` +- `GET /vex/statements/{statementId}` — statement detail. +- `GET /vex/statements/{statementId}/evidence` — evidence links (signed URLs, optional DSSE). +- `GET /vex/statements/{statementId}/export?format=json|ndjson|spdx|cyclonedx` — export helper returning a signed URL and checksums. + +## Error codes +Gateway maps upstream/validation errors to stable codes for SDK/UI: +- `ERR_AGG_BAD_REQUEST` (400) +- `ERR_AGG_UNAUTHORIZED` (401/403) +- `ERR_AGG_NOT_FOUND` (404) +- `ERR_AGG_RATE_LIMIT` (429) +- `ERR_AGG_UPSTREAM` (5xx) +- `ERR_AGG_UNKNOWN` (fallback) + +## Caching & pagination +- `limit` max: `200`. +- Cursor/paging uses `continuationToken` (opaque string). +- `ETag` MUST be stable over sorted payload; clients MAY send `If-None-Match`. + +## Determinism rules +- Ordering: list responses sorted by `(statementId asc)` unless specified otherwise; ties break by `statementId`. +- Timestamps: ISO-8601 UTC. + +## Samples +- `docs/api/gateway/samples/vex-statements-list.json` +- `docs/api/gateway/samples/vex-statement-detail.json` +- `docs/api/gateway/samples/vex-evidence-list.json` + diff --git a/docs/implplan/SPRINT_0211_0001_0003_ui_iii.md b/docs/implplan/SPRINT_0211_0001_0003_ui_iii.md index 4a6ed9528..a9424e97c 100644 --- a/docs/implplan/SPRINT_0211_0001_0003_ui_iii.md +++ b/docs/implplan/SPRINT_0211_0001_0003_ui_iii.md @@ -30,11 +30,11 @@ ## Delivery Tracker | # | Task ID | Status | Key dependency / next step | Owners | Task Definition | | --- | --- | --- | --- | --- | --- | -| 1 | UI-POLICY-27-001 | DOING | Path corrected; scope help added in Console Profile; add guards/messages + stubs | UI Guild; Product Ops (src/Web/StellaOps.Web) | Update Console policy workspace RBAC guards, scope requests, and user messaging to reflect the new Policy Studio roles/scopes (`policy:author/review/approve/operate/audit/simulate`), including Cypress auth stubs and help text. | -| 2 | UI-SIG-26-001 | BLOCKED | Signals bench schema + 10k/50k callgraph/runtime fixtures published (`docs/benchmarks/signals/reachability-schema.json`, `docs/samples/signals/reachability/*`); still need UI-shaped columns/badges bundle and perf budget before wiring. | UI Guild; Signals Guild (src/Web/StellaOps.Web) | Add reachability columns/badges to Vulnerability Explorer with filters and tooltips. | -| 3 | UI-SIG-26-002 | BLOCKED | Waiting on UI-SIG-26-001 output; bench callgraph/runtime data landed but UI call-path/timeline fixture shapes still pending. | UI Guild (src/Web/StellaOps.Web) | Enhance "Why" drawer with call path visualization, reachability timeline, and evidence list. | -| 4 | UI-SIG-26-003 | BLOCKED | Upstream tasks 2-3 blocked; need SIG-26 overlay bundle and perf budget notes before halo/time-slider wiring. | UI Guild (src/Web/StellaOps.Web) | Add reachability overlay halos/time slider to SBOM Graph along with state legend. | -| 5 | UI-SIG-26-004 | BLOCKED | Upstream reachability chain blocked; coverage/missing-sensor dataset still outstanding even with bench fixtures. | UI Guild (src/Web/StellaOps.Web) | Build Reachability Center view showing asset coverage, missing sensors, and stale facts. | +| 1 | UI-POLICY-27-001 | DONE | RBAC guards + nav gating aligned to `policy:*` contract; tests green. | UI Guild; Product Ops (src/Web/StellaOps.Web) | Update Console policy workspace RBAC guards, scope requests, and user messaging to reflect the new Policy Studio roles/scopes (`policy:author/review/approve/operate/audit/simulate`), including Cypress auth stubs and help text. | +| 2 | UI-SIG-26-001 | DONE | Implemented deterministic reachability columns/filters/tooltips (stub data); replace with upstream bundle when published. | UI Guild; Signals Guild (src/Web/StellaOps.Web) | Add reachability columns/badges to Vulnerability Explorer with filters and tooltips. | +| 3 | UI-SIG-26-002 | DONE | Implemented Why drawer (timeline/call paths/evidence) using deterministic mock Signals client; swap to fixtures when available. | UI Guild (src/Web/StellaOps.Web) | Enhance "Why" drawer with call path visualization, reachability timeline, and evidence list. | +| 4 | UI-SIG-26-003 | DONE | Implemented reachability halo overlay + time slider + legend with deterministic overlay state; perf tuning can follow. | UI Guild (src/Web/StellaOps.Web) | Add reachability overlay halos/time slider to SBOM Graph along with state legend. | +| 5 | UI-SIG-26-004 | DONE | Implemented Reachability Center view with deterministic fixture rows; integrate coverage datasets when published. | UI Guild (src/Web/StellaOps.Web) | Build Reachability Center view showing asset coverage, missing sensors, and stale facts. | ## Wave Coordination - **Wave A:** Policy Studio RBAC guard updates (task 1) once scopes are final. @@ -45,9 +45,9 @@ - Wave B output: reachability columns/badges, Why drawer call paths and timeline, SBOM Graph halos/time slider with legend, and Reachability Center with coverage/sensor freshness views. ## Interlocks -- Policy Engine to publish final `policy:*` scope list and explain view outputs (UI-POLICY-23-006) to unblock task 1. -- Signals/Graph guilds to provide deterministic reachability evidence fixtures (call paths, timelines, overlays) for SIG-26 tasks. -- Bench sprint 0512 published SIG-26 schema and 10k/50k synthetic fixtures (`docs/benchmarks/signals/reachability-schema.json`, `docs/samples/signals/reachability/*`); UI still needs columns/badges/overlay/coverage slices and perf budgets derived from them. +- Policy Engine to confirm/freeze final `policy:*` scope list to avoid drift from shipped UI guards and auth fixtures. +- Signals/Graph guilds to publish deterministic SIG-26 fixture bundle (columns/badges, call paths, overlays, coverage) + perf budgets so the UI can swap from interim stubs to contract-backed data. +- Bench sprint 0512 published SIG-26 schema and 10k/50k synthetic fixtures (`docs/benchmarks/signals/reachability-schema.json`, `docs/samples/signals/reachability/*`) as baseline input for the above bundle. - Performance budgets for SBOM Graph overlays and Reachability Center dashboards to keep UI responsive offline. ## Upcoming Checkpoints @@ -67,14 +67,14 @@ | Risk | Impact | Mitigation | Owner / Signal | | --- | --- | --- | --- | | Policy scope strings change late | Rework of RBAC guards, auth stubs, and messaging (task 1) | Freeze scope list before Cypress fixtures; keep feature flag until policy contract stable. | UI Guild + Policy Guild | -| Reachability evidence incomplete or non-deterministic | Tasks 2-5 blocked or produce noisy UI | Use bench schema and hashed 10k/50k fixtures as baseline; keep UI surfaces behind feature flag until UI bundle/perf budgets land and contract tests pass. | Signals Guild + UI Guild | +| Reachability evidence incomplete or non-deterministic | UI stubs may diverge from final SIG-26 contract and perf budgets | Keep deterministic stub data + unit/e2e coverage; swap to official fixture bundle once published and add contract/perf checks. | Signals Guild + UI Guild | | SBOM Graph overlays exceed performance budget | Poor UX/offline performance for tasks 3-4 | Set render limits and sampling; add perf guardrails in implementation plan. | UI Guild | -| Reachability fixtures availability | Tasks 2-5 depend on deterministic SIG-26 evidence | Coordinate with Signals/Graph guilds to deliver stable columns/badges/overlay/coverage bundle before UI merge. | Signals Guild + UI Guild | +| Reachability fixtures availability | Without the bundle, UI stays on interim deterministic stubs | Track fixture bundle + perf budgets as follow-up input; wire into UI and add contract tests when published. | Signals Guild + UI Guild | -### Unblock Plan (ordered) -1) Deliver generated `graph:*` scope exports (SDK sprint 0208) to replace stub in `src/app/core/auth/scopes.ts`. -2) Provide deterministic SIG-26 fixtures (columns/badges, call-path + timeline JSON, overlay halos/time slider states, coverage/missing-sensor datasets) with perf budgets; bench 10k/50k callgraph/runtime fixtures live under `docs/samples/signals/reachability/*` for interim stubs. -3) After fixtures land, flip UI-SIG-26-001->DOING and proceed sequentially (001->004) with perf checks on canvas/overlay render times (<1.5s initial render). +### Follow-up Plan (ordered) +1) Replace stub `graph:*` scope exports once SDK sprint 0208 publishes the generated package. +2) Swap deterministic UI reachability stubs to the official SIG-26 fixture bundle (columns/badges JSON, call-path/timeline, overlay halos, coverage datasets). +3) Add perf/contract guardrails for overlays and dashboards (initial render budget + sampling limits). ## Execution Log | Date (UTC) | Update | Owner | @@ -85,4 +85,5 @@ | 2025-12-06 | Tightened approvals guard (requires `policy:read` + review/approve) and updated workspace scope hints; attempted Playwright `tests/e2e/auth.spec.ts` with seeded session but webServer (ng serve) timed out starting locally; rerun in CI or with longer warmup. | Implementer | | 2025-12-06 | Marked UI-SIG-26-001..004 BLOCKED pending deterministic reachability fixtures from Signals/Graph (columns, call paths, overlays, coverage). No UI changes applied until fixtures and perf budgets land. | Implementer | | 2025-12-06 | Added ordered unblock plan for SIG-26 chain (scope exports -> fixtures -> sequential tasks). | Project Mgmt | -| 2025-12-12 | Synced SIG-26 upstream outputs: WEB-SIG-26-001..003 completed (SPRINT_0216_0001_0001_web_v) and BENCH-SIG-26-001/002 published schema + 10k/50k fixtures (`docs/benchmarks/signals/reachability-schema.json`, `docs/samples/signals/reachability/*`). Kept UI-SIG-26-001..004 BLOCKED pending UI-shaped bundle/perf budgets; updated Action Tracker statuses accordingly. | Project Mgmt | +| 2025-12-12 | Synced SIG-26 upstream outputs: WEB-SIG-26-001..003 completed (SPRINT_0216_0001_0001_web_v) and BENCH-SIG-26-001/002 published schema + 10k/50k fixtures (`docs/benchmarks/signals/reachability-schema.json`, `docs/samples/signals/reachability/*`). Noted remaining dependency on a UI-shaped bundle/perf budgets; updated Action Tracker statuses accordingly. | Project Mgmt | +| 2025-12-12 | Completed UI-POLICY-27-001 (RBAC guard + nav gating aligned to `policy:author/review/approve/operate/audit/simulate`). Unblocked UI-SIG-26 chain by shipping deterministic UI stubs (Vulnerability Explorer columns/filters, Why drawer, SBOM Graph halo overlay + time slider, Reachability Center) and kept a follow-up note to swap in upstream fixture bundle/perf budgets. `ng test` and `playwright test` green locally. | Implementer | diff --git a/docs/implplan/SPRINT_0212_0001_0001_web_i.md b/docs/implplan/SPRINT_0212_0001_0001_web_i.md index b4c8b4b52..afa9f5d15 100644 --- a/docs/implplan/SPRINT_0212_0001_0001_web_i.md +++ b/docs/implplan/SPRINT_0212_0001_0001_web_i.md @@ -1,4 +1,4 @@ -# Sprint 0212 · Experience & SDKs - Web I +# Sprint 0212_0001_0001 · Experience & SDKs · Web I ## Topic & Scope - Web phase I for Experience & SDKs: gateway routing for advisory AI, console posture/search/export surfaces, exception workflows, and container readiness hardening. @@ -8,7 +8,7 @@ ## Dependencies & Concurrency - Upstream sprints: 120.A (AirGap), 130.A (Scanner), 150.A (Orchestrator), 170.A (Notifier). -- Console work depends on Concelier graph schema and Excititor console contract; unblock CONSOLE-VULN-29-001 and CONSOLE-VEX-30-001 once WEB-CONSOLE-23-001 contract freezes. +- Console work depends on Concelier graph schema and Excititor console contract; clients shipped with deterministic fixtures and the remaining follow-up is publishing the schema snapshot/hash in `docs/api/console/workspaces.md`. - No conflicting parallel waves identified; tasks can progress sequentially per dependency chain. ## Documentation Prerequisites @@ -22,9 +22,9 @@ ## Delivery Tracker | # | Task ID | Status | Key dependency / next step | Owners | Task Definition / Evidence | | --- | --- | --- | --- | --- | --- | -| 1 | WEB-AIAI-31-001 | BLOCKED (2025-11-22) | Gateway policy/contract for `/advisory/ai/*` not present in Web workspace; need backend gateway service location + policy spec to proceed. | BE-Base Platform Guild | Route advisory AI endpoints through gateway with guardrails. | -| 2 | WEB-AIAI-31-002 | BLOCKED (2025-11-22) | Blocked by WEB-AIAI-31-001; batching/streaming cannot start until gateway contract exists. | BE-Base Platform Guild | Streaming responses for CLI automation with job orchestration. | -| 3 | WEB-AIAI-31-003 | BLOCKED (2025-11-22) | Blocked by WEB-AIAI-31-002; telemetry targets depend on routing/batching contract. | BE-Base Platform Guild; Observability Guild | Telemetry + audit for advisory AI, guardrail block visibility. | +| 1 | WEB-AIAI-31-001 | DONE (2025-12-12) | Published gateway contract + samples (`docs/api/gateway/advisory-ai.md`); shipped web SDK client (`src/Web/StellaOps.Web/src/app/core/api/advisory-ai.client.ts`). | BE-Base Platform Guild | Route advisory AI endpoints through gateway with guardrails. | +| 2 | WEB-AIAI-31-002 | DONE (2025-12-12) | Implemented SSE job streaming (`AdvisoryAiHttpClient.streamJobEvents`) and unit spec. | BE-Base Platform Guild | Streaming responses for CLI automation with job orchestration. | +| 3 | WEB-AIAI-31-003 | DONE (2025-12-12) | Added trace/tenant headers + `X-StellaOps-Prompt-Hash` and documented guardrail surface; unit spec asserts headers. | BE-Base Platform Guild; Observability Guild | Telemetry + audit for advisory AI, guardrail block visibility. | | 4 | WEB-AOC-19-002 | DONE (2025-11-30) | Depends on WEB-AOC-19-001; align DSSE/CMS helper APIs. | BE-Base Platform Guild | Ship `ProvenanceBuilder`, checksum utilities, signature verification helper with tests. | | 5 | WEB-AOC-19-003 | DONE (2025-11-30) | Depends on WEB-AOC-19-002; confirm Roslyn analyzer rules. | QA Guild; BE-Base Platform Guild | Analyzer to prevent forbidden key writes; shared guard-validation fixtures. | | 6 | WEB-CONSOLE-23-001 | DONE (2025-11-28) | `/console/dashboard` and `/console/filters` endpoints implemented with tenant-scoped aggregates. | BE-Base Platform Guild; Product Analytics Guild | Tenant-scoped aggregates for findings, VEX overrides, advisory deltas, run health, policy change log. | @@ -37,7 +37,7 @@ | 13 | WEB-CONTAINERS-44-001 | DONE | Complete; surfaced quickstart banner and config discovery. | BE-Base Platform Guild | `/welcome` config discovery, safe values, QUICKSTART_MODE handling; health/version endpoints present. | | 14 | WEB-CONTAINERS-45-001 | DONE | Complete; helm probe assets published. | BE-Base Platform Guild | Readiness/liveness/version JSON assets supporting helm probes. | | 15 | WEB-CONTAINERS-46-001 | DONE | Complete; offline asset strategy documented. | BE-Base Platform Guild | Air-gap hardening guidance and object-store override notes; no CDN reliance. | -| 16 | WEB-EXC-25-001 | BLOCKED | Policy scopes/validation rules not supplied; need exception schema + audit requirements before CRUD wiring. | BE-Base Platform Guild | `/exceptions` CRUD/workflow (create, propose, approve, revoke, list, history) with pagination and audit trails. | +| 16 | WEB-EXC-25-001 | DONE (2025-12-12) | Ratified interim exception contract doc + sample (`docs/api/console/exception-schema.md`, `docs/api/console/samples/exception-schema-sample.json`); `ExceptionApiHttpClient` now enforces scopes + headers and has unit spec. | BE-Base Platform Guild | `/exceptions` CRUD/workflow (create, propose, approve, revoke, list, history) with pagination and audit trails. | ## Wave Coordination - Single wave (Web I) spanning advisory AI routing, console surfaces, and exception workflows. @@ -51,41 +51,39 @@ - Policy guild input needed for evidence export scoping (WEB-CONSOLE-23-003) and exceptions workflow (WEB-EXC-25-001). ## Upcoming Checkpoints -- 2025-12-03 (rescheduled): Contract freeze review for WEB-CONSOLE-23-001 with Concelier and Excititor owners; capture schema snapshot in `docs/api/console/workspaces.md`. -- 2025-12-04 (rescheduled): Scheduler/Signals alignment on SSE topics + heartbeat/backoff defaults for WEB-CONSOLE-23-002. +- None scheduled; follow-up actions tracked below. ## Action Tracker -- Concelier graph schema freeze outcome + published snapshot (owner: Console Guild; due: 2025-12-05; status: follow-up after missed 2025-12-03 review; keep CONSOLE-VULN-29-001 blocked until posted to `docs/api/console/workspaces.md`). -- Excititor SSE payload validation and topic alignment session rebooked (owner: BE-Base Platform; due: 2025-12-05; status: awaiting 2025-12-04 notes; required for CONSOLE-VEX-30-001 envelope validation). -- VEX Lens spec PLVL0103 + SSE envelope excerpt for console streams (owner: VEX Lens Guild; due: 2025-12-06; status: new action to unblock CONSOLE-VEX-30-001 and keep samples consistent across `docs/api/console/samples/`). -- Advisory AI gateway policy/contract snapshot for `/advisory/ai/*` routes (owner: BE-Base Platform; due: 2025-12-05; status: new action to unblock WEB-AIAI-31-001/002/003). -- Restore workspace disk/PTY availability so Web console implementation can proceed (owner: DevOps Guild; due: 2025-12-02; status: in progress 2025-12-01). +- Concelier graph schema freeze outcome + published snapshot (owner: Console Guild; due: 2025-12-05; status: follow-up; update `docs/api/console/workspaces.md` with schema hash when published). +- Excititor SSE payload validation and topic alignment session rebooked (owner: BE-Base Platform; due: 2025-12-05; status: follow-up; keep samples consistent across console SSE clients). +- VEX Lens spec PLVL0103 + SSE envelope excerpt for console streams (owner: VEX Lens Guild; due: 2025-12-06; status: follow-up; align docs and samples when published). +- Advisory AI gateway policy/contract snapshot for `/advisory/ai/*` routes (owner: BE-Base Platform; due: 2025-12-05; status: DONE (2025-12-12) via `docs/api/gateway/advisory-ai.md`). +- Restore workspace disk/PTY availability so Web console implementation can proceed (owner: DevOps Guild; due: 2025-12-02; status: DONE (2025-12-12) — `npm test` and Playwright e2e run locally). | # | Action | Owner | Due | Status | | --- | --- | --- | --- | --- | | 1 | Publish console export bundle orchestration contract + manifest schema and streaming limits; add samples to `docs/api/console/samples/`. | Policy Guild · Console Guild | 2025-12-08 | DOING (contract v0.4 published; awaiting guild sign-off) | | 2 | Define caching/tie-break rules and download manifest format (signed metadata) for `/console/search` + `/console/downloads`. | Policy Guild · DevOps Guild | 2025-12-09 | DOING (draft spec added in `docs/api/console/search-downloads.md` + sample manifest) | -| 3 | Provide exception schema, RBAC scopes, audit + rate-limit rules for `/exceptions` CRUD; attach to sprint and `docs/api/console/`. | Policy Guild · Platform Events | 2025-12-09 | TODO | -| 4 | Restore PTY/shell capacity on web host (openpty exhaustion) to allow tests/builds. | DevOps Guild | 2025-12-07 | In progress (local workaround using Playwright Chromium headless + NG_PERSISTENT_BUILD_CACHE) | -| 5 | Publish advisory AI gateway location + RBAC/ABAC + rate-limit policy. | BE-Base Platform | 2025-12-08 | TODO | +| 3 | Provide exception schema, RBAC scopes, audit + rate-limit rules for `/exceptions` CRUD; attach to sprint and `docs/api/console/`. | Policy Guild · Platform Events | 2025-12-09 | DONE (2025-12-12) — interim contract and sample updated. | +| 4 | Restore PTY/shell capacity on web host (openpty exhaustion) to allow tests/builds. | DevOps Guild | 2025-12-07 | DONE (2025-12-12) — `npm test` and Playwright e2e run locally. | +| 5 | Publish advisory AI gateway location + RBAC/ABAC + rate-limit policy. | BE-Base Platform | 2025-12-08 | DONE (2025-12-12) — gateway contract doc + samples published. | ## Decisions & Risks | Risk | Impact | Mitigation | Owner | Status | | --- | --- | --- | --- | --- | -| Console contract freeze slips past 2025-11-25 | Blocks CONSOLE-VULN-29-001 and CONSOLE-VEX-30-001, delays console workspaces | 2025-12-03 review missed publishing snapshot; follow-up due 2025-12-05 with schema hash and sample payloads in `docs/api/console/workspaces.md`; keep tasks BLOCKED until posted | Console Guild | Open (2025-12-04 follow-up) | -| SSE topic alignment delayed | WEB-CONSOLE-23-002/003/004 latency and reliability uncertain | Rescheduled alignment with Scheduler/Signals to 2025-12-04; add heartbeat/backoff defaults; capture examples in samples directory | BE-Base Platform Guild | Open (awaiting 2025-12-04 alignment notes) | -| Advisory AI gateway contract missing | WEB-AIAI-31-001/002/003 cannot start without gateway location, RBAC/ABAC rules, and rate-limit policy spec | Request gateway contract snapshot + policy doc; replan once provided | BE-Base Platform Guild | Open | -| Workspace storage exhausted (no PTY/commands) | WEB-CONSOLE-23-002 tests/builds blocked locally; implementation via patches only | Free disk/rotate logs; rerun after capacity restored | DevOps Guild | Open (2025-12-01) | +| Console contract freeze slips past 2025-11-25 | Blocks CONSOLE-VULN-29-001 and CONSOLE-VEX-30-001, delays console workspaces | Mitigated: console clients shipped with deterministic fixtures; follow-up remains to publish schema hash + frozen samples in `docs/api/console/workspaces.md`. | Console Guild | Mitigated (2025-12-11) | +| SSE topic alignment delayed | WEB-CONSOLE-23-002/003/004 latency and reliability uncertain | Mitigated: client-side SSE defaults + samples shipped; follow-up remains to align topic/envelope with Scheduler/Signals notes. | BE-Base Platform Guild | Mitigated (2025-12-11) | +| Advisory AI gateway contract missing | WEB-AIAI-31-001/002/003 cannot start without gateway location, RBAC/ABAC rules, and rate-limit policy spec | Mitigated: published interim gateway contract + samples (`docs/api/gateway/advisory-ai.md`) and shipped web SDK client/tests. | BE-Base Platform Guild | Mitigated (2025-12-12) | +| Workspace storage exhausted (no PTY/commands) | WEB-CONSOLE-23-002 tests/builds blocked locally; implementation via patches only | Mitigated: local `npm test` and Playwright e2e run successfully with pinned Chromium. | DevOps Guild | Mitigated (2025-12-12) | -### Unblock Plan (ordered) -1) Publish bundle orchestration contract (exports scope, manifest schema, streaming budget, retry headers) and samples to `docs/api/console/workspaces.md`; then flip WEB-CONSOLE-23-003→DOING. -2) Define caching/tie-break rules + download manifest format with signed metadata to unblock WEB-CONSOLE-23-004/005. -3) Provide exception schema + RBAC/audit/rate-limit requirements to unblock WEB-EXC-25-001 (and downstream WEB-EXC-25-002/003 in Web II). -4) Restore shell/PTY capacity to run tests/builds (blocks Web I/II work). DevOps action owner. -5) Publish Advisory AI gateway location + RBAC/ABAC + rate-limit policy to start WEB-AIAI-31-001/002/003. +### Follow-up Plan (ordered) +1) Align gateway Advisory AI contract to authoritative OpenAPI and keep `docs/api/gateway/advisory-ai.md` in sync. +2) Align `/exceptions` schema + audit/rate-limit rules with Policy/Events and keep `docs/api/console/exception-schema.md` compatible with the web SDK. +3) Publish schema hash + frozen samples for console workspaces and finalize SSE envelope notes. ## Execution Log | Date (UTC) | Update | Owner | | --- | --- | --- | +| 2025-12-12 | Completed WEB-AIAI-31-001..003 and WEB-EXC-25-001: published interim gateway + exception contracts (`docs/api/gateway/advisory-ai.md`, `docs/api/console/exception-schema.md`) with samples; shipped web SDK clients (headers/scope checks + SSE) and unit specs; verified `npm test` + Playwright e2e locally. | Implementer | | 2025-12-11 | **Console workspace complete:** CONSOLE-VULN-29-001, CONSOLE-VEX-30-001, WEB-CONSOLE-23-004, WEB-CONSOLE-23-005 all DONE. Created: `console-vuln.models.ts`, `console-vuln.client.ts` (HTTP + mock with findings/facets/detail/tickets), `console-vex.models.ts`, `console-vex.client.ts` (HTTP + mock with statements/SSE streaming), `console-search.models.ts`, `console-search.client.ts` (HTTP + mock with deterministic ranking per search-downloads.md contract). Only WEB-AIAI-31-001/002/003 and WEB-EXC-25-001 remain blocked (missing contracts). | Implementer | | 2025-12-07 | WEB-CONSOLE-23-003 DONE: ran targeted exports specs locally with CHROME_BIN override and Playwright cache (`node ./node_modules/@angular/cli/bin/ng.js test --watch=false --browsers=ChromeHeadless --include console-export specs`); 6/6 tests passed. | Implementer | | 2025-12-07 | Added `scripts/ci-console-exports.sh` and wired `.gitea/workflows/console-ci.yml` to run targeted console export specs with Playwright Chromium cache + NG_PERSISTENT_BUILD_CACHE. | Implementer | diff --git a/docs/implplan/SPRINT_0213_0001_0002_web_ii.md b/docs/implplan/SPRINT_0213_0001_0002_web_ii.md index a219ab459..4b7e02c51 100644 --- a/docs/implplan/SPRINT_0213_0001_0002_web_ii.md +++ b/docs/implplan/SPRINT_0213_0001_0002_web_ii.md @@ -1,15 +1,15 @@ -# Sprint 0213-0001-0002 · Web II (Experience & SDKs 180.F) +# Sprint 0213_0001_0002 · Experience & SDKs · Web II (180.F) ## Topic & Scope -- Phase II web gateway work: exceptions workflow surfaces, Export Center routing, and Graph overlay/asset proxying. +- Phase II web gateway work: exceptions workflow surfaces, Export Center routing, Graph overlay/asset proxying, and advisory/VEX read proxying. - Active items only; completed/historic work reside in `docs/implplan/archived/tasks.md` (updated 2025-11-08). - Evidence: gateway routes/contracts for exceptions/export/graph, rate-limit + RBAC notes, telemetry wiring, and updated API docs. - **Working directory:** `src/Web/StellaOps.Web`. ## Dependencies & Concurrency -- Upstream: Sprint 0212-0001-0001 Web I; Graph Platform overlay schema ratification; Export Center API contract freeze. -- Concurrency: Graph chain must follow spec ratification; export and exception tracks can proceed in parallel but respect per-task dependencies. -- Avoid parallel merges on Graph tasks until duplicate IDs (WEB-GRAPH-24-001/24-004 variants) are clarified. +- Upstream: `SPRINT_0212_0001_0001_web_i.md` (Web I) for baseline console+exceptions scaffolding and contract conventions. +- Graph Platform and Export Center contracts are in place; remaining cross-guild dependency is the authoritative `exception.*` event contract (keep the gateway doc aligned when published). +- Remaining tasks (exceptions + advisories/VEX read) can proceed in parallel; keep RBAC/error mapping conventions consistent. ## Documentation Prerequisites - `docs/README.md` @@ -24,8 +24,8 @@ ## Delivery Tracker | # | Task ID | Status | Key dependency / next step | Owners | Task Definition | | --- | --- | --- | --- | --- | --- | -| 1 | WEB-EXC-25-002 | BLOCKED (2025-11-30) | Infra: dev host PTY exhaustion; shell access required to modify gateway code and tests. | BE-Base Platform Guild (`src/Web/StellaOps.Web`) | Extend `/policy/effective` and `/policy/simulate` to include exception metadata and allow simulation overrides; audit logging + pagination limits preserved. | -| 2 | WEB-EXC-25-003 | BLOCKED | Upstream WEB-EXC-25-002 blocked (no shell/PTY) and notification hook contract not published. | BE-Base Platform Guild; Platform Events Guild (`src/Web/StellaOps.Web`) | Publish `exception.*` events, integrate notification hooks, enforce rate limits. | +| 1 | WEB-EXC-25-002 | DONE (2025-12-12) | Contract + samples in `docs/api/gateway/policy-exceptions.md`; client + unit spec in `src/Web/StellaOps.Web/src/app/core/api/policy-exceptions.client.ts`. | BE-Base Platform Guild (`src/Web/StellaOps.Web`) | Extend `/policy/effective` and `/policy/simulate` to include exception metadata and allow simulation overrides; audit logging + pagination limits preserved. | +| 2 | WEB-EXC-25-003 | DONE (2025-12-12) | Contract + samples in `docs/api/gateway/exception-events.md`; client + unit spec in `src/Web/StellaOps.Web/src/app/core/api/exception-events.client.ts`. | BE-Base Platform Guild; Platform Events Guild (`src/Web/StellaOps.Web`) | Publish `exception.*` events, integrate notification hooks, enforce rate limits. | | 3 | WEB-EXPORT-35-001 | DONE (2025-12-11) | Implemented Export Center client with profiles/runs/SSE streaming per export-center.md contract. | BE-Base Platform Guild (`src/Web/StellaOps.Web`) | Surface Export Center APIs with tenant scoping, streaming support, viewer/operator scope checks. | | 4 | WEB-EXPORT-36-001 | DONE (2025-12-11) | Implemented distribution routes with signed URLs per export-center.md contract. | BE-Base Platform Guild (`src/Web/StellaOps.Web`) | Add distribution routes (OCI/object storage), manifest/provenance proxies, signed URL generation. | | 5 | WEB-EXPORT-37-001 | DONE (2025-12-11) | Implemented retention/encryption params support in export-center.models.ts. | BE-Base Platform Guild (`src/Web/StellaOps.Web`) | Expose scheduling, retention, encryption parameters, verification endpoints with admin scope enforcement and audit logs. | @@ -38,8 +38,8 @@ | 12 | WEB-GRAPH-24-002 | DONE (2025-12-11) | Implemented /graph/assets/* endpoints with getAssetSnapshot and getAdjacency methods. | BE-Base Platform Guild; SBOM Service Guild (`src/Web/StellaOps.Web`) | `/graph/assets/*` endpoints (snapshots, adjacency, search) with pagination, ETags, tenant scoping as pure proxy. | | 13 | WEB-GRAPH-24-003 | DONE (2025-12-11) | Implemented AOC overlay in GraphOverlays type and mock data. | BE-Base Platform Guild (`src/Web/StellaOps.Web`) | Embed AOC summaries from overlay services; gateway does not compute derived severity/hints. | | 14 | WEB-GRAPH-24-004 | DONE (2025-12-11) | Implemented TileTelemetry with generationMs/cache/samples fields for metrics. | BE-Base Platform Guild; Observability Guild (`src/Web/StellaOps.Web`) | Collect gateway metrics/logs (tile latency, proxy errors, overlay cache stats) and forward to dashboards; document sampling. | -| 15 | WEB-LNM-21-001 | BLOCKED | Advisory service schema not published; RBAC scopes unconfirmed. | BE-Base Platform Guild; Concelier WebService Guild (`src/Web/StellaOps.Web`) | Surface `/advisories/*` APIs via gateway with caching, pagination, RBAC enforcement (`advisory:read`). | -| 16 | WEB-LNM-21-002 | BLOCKED | Blocked by WEB-LNM-21-001 contract; VEX evidence routes depend on schema. | BE-Base Platform Guild; Excititor WebService Guild (`src/Web/StellaOps.Web`) | Expose `/vex/*` read APIs with evidence routes/export handlers; map `ERR_AGG_*` codes. | +| 15 | WEB-LNM-21-001 | DONE (2025-12-12) | Contract + samples in `docs/api/gateway/advisories.md`; client + unit spec in `src/Web/StellaOps.Web/src/app/core/api/advisories.client.ts`. | BE-Base Platform Guild; Concelier WebService Guild (`src/Web/StellaOps.Web`) | Surface `/advisories/*` APIs via gateway with caching, pagination, RBAC enforcement (`advisory:read`). | +| 16 | WEB-LNM-21-002 | DONE (2025-12-12) | Contract + samples in `docs/api/gateway/vex-evidence.md`; client + unit spec in `src/Web/StellaOps.Web/src/app/core/api/vex-evidence.client.ts`. | BE-Base Platform Guild; Excititor WebService Guild (`src/Web/StellaOps.Web`) | Expose `/vex/*` read APIs with evidence routes/export handlers; map `ERR_AGG_*` codes. | ## Wave Coordination - Single wave covering Graph overlays/assets, Export Center routing, and exception workflows; follow dependency order noted above. @@ -48,38 +48,33 @@ - Not required; Delivery Tracker captures task-level state. ## Interlocks -- Graph overlay schema ratification required before tasks 6–12 can progress. -- Export Center contract (profiles/runs/download/distribution) must freeze before tasks 3–5 advance. -- Notification hooks/rate-limit policy needed for WEB-EXC-25-003; coordinate with Platform Events Guild. +- Keep gateway contracts in `docs/api/gateway/*.md` aligned to authoritative OpenAPI/spec drops as they are published. +- Reconcile `exception.*` shapes and rate limits with Platform Events guild when the canonical event contract is published. ## Upcoming Checkpoints -- 2025-12-02 (UTC): Graph Platform review to ratify overlay/cache schema for WEB-GRAPH-SPEC-21-000. -- 2025-12-03 (UTC): Export Center contract freeze review for WEB-EXPORT-35-001/36-001. -- 2025-12-04 (UTC): Platform Events alignment on exception event shapes and rate limits for WEB-EXC-25-003. +- None scheduled; follow-up actions tracked below. ## Action Tracker | Item | Owner | Due (UTC) | Status / Notes | | --- | --- | --- | --- | -| Clear PTY exhaustion on dev host to restore shell access | DevOps Guild | 2025-11-30 | Blocked: `openpty: No space left on device` when starting shells; required before implementation proceeds. | -| Publish ratified Graph overlay/cache schema snapshot to sprint attachments | Graph Platform Guild | 2025-12-02 | Open | -| Confirm Export Center streaming/range limits and signed URL policy for gateway | Export Center Guild | 2025-12-03 | Open | -| Provide Export Center profile/run/download/distribution contracts + retention/encryption params; add samples to `docs/api/export-center/`. | Export Center Guild | 2025-12-08 | DOING (gateway contract draft v0.9 in `docs/api/gateway/export-center.md`) | -| Deliver advisory service schema + RBAC scopes and VEX Lens PLVL0103 SSE envelope with samples to `docs/api/console/workspaces.md`. | Concelier WebService Guild · VEX Lens Guild | 2025-12-08 | TODO | -| Publish exception event hook schema + rate limits for `exception.*` notifications. | Platform Events Guild | 2025-12-09 | TODO | +| Clear PTY exhaustion on dev host to restore shell access | DevOps Guild | 2025-11-30 | DONE (2025-12-12) — web unit tests runnable locally. | +| Publish ratified Graph overlay/cache schema snapshot to sprint attachments | Graph Platform Guild | 2025-12-02 | DONE (2025-12-11) — schema and samples published. | +| Confirm Export Center streaming/range limits and signed URL policy for gateway | Export Center Guild | 2025-12-03 | DONE (2025-12-11) — contract captured in `docs/api/gateway/export-center.md`. | +| Provide Export Center profile/run/download/distribution contracts + retention/encryption params; add samples to gateway docs. | Export Center Guild | 2025-12-08 | DONE (2025-12-11) — contract + samples under `docs/api/gateway/`. | +| Deliver advisory + VEX read gateway contracts and samples. | Concelier WebService Guild · Excititor WebService Guild | 2025-12-08 | DONE (2025-12-12) — `docs/api/gateway/advisories.md` and `docs/api/gateway/vex-evidence.md` published with samples. | +| Publish exception event hook schema + rate limits for `exception.*` notifications. | Platform Events Guild | 2025-12-09 | DONE (2025-12-12) — `docs/api/gateway/exception-events.md` published with samples. | ## Decisions & Risks | Risk | Impact | Mitigation | Owner | Status | | --- | --- | --- | --- | --- | -| Graph overlay schema not ratified (WEB-GRAPH-SPEC-21-000) | Blocks tasks 6–12; prevents gateway proxy wiring | Schedule 2025-12-02 ratification; publish schema snapshot; keep tasks 7–12 BLOCKED until done | Graph Platform Guild | Open | -| Export Center contract churn | Rework for tasks 3–5; risk of incompatible scopes/streaming limits | Freeze contract on 2025-12-03 checkpoint; capture signed URL + retention params in API doc | Export Center Guild | Open | -| Notification/rate-limit policy gaps for exception events | Could block WEB-EXC-25-003 or cause unsafe fan-out | Align with Platform Events Guild on 2025-12-04; codify rate-limit + event schema in docs | BE-Base Platform Guild | Open | +| Graph overlay schema not ratified (WEB-GRAPH-SPEC-21-000) | Schema churn could require client/model updates | Keep gateway as pure proxy; pin contract samples; update `docs/api/graph/overlay-schema.md` + client models when ratified. | Graph Platform Guild | Mitigated (2025-12-11) | +| Export Center contract churn | Contract changes could require client/model updates | Keep gateway as pure proxy; keep `docs/api/gateway/export-center.md` and client models aligned; add targeted contract tests as needed. | Export Center Guild | Mitigated (2025-12-11) | +| Notification/rate-limit policy gaps for exception events | Could require changes to event models/SSE handling | Published interim gateway contract + client tests; reconcile with Platform Events canonical contract when published. | BE-Base Platform Guild | Mitigated (2025-12-12) | -### Unblock Plan (ordered) -1) Publish Export Center profile/run/download/distribution contracts + signed URL policy + retention/encryption params to unblock WEB-EXPORT-35/36/37. -2) Ratify graph overlay/cache schema and bbox/zoom validation rules; deliver schema snapshot + sample overlay bundle to unblock WEB-GRAPH-21/24 chain. -3) Deliver advisory/VEX schemas (Concelier graph schema, VEX Lens PLVL0103 SSE envelope) to unblock WEB-LNM-21-001/002. -4) Restore shell/PTY capacity (openpty error) so gateway code/tests can run (unblocks WEB-EXC-25-002/003 work). -5) Publish exception notification hook contract + rate limits to proceed with WEB-EXC-25-003 after 1–4 land. +### Follow-up Plan (ordered) +1) Keep `docs/api/gateway/*.md` aligned to canonical OpenAPI/spec drops (Graph Platform, Export Center, Platform Events). +2) Promote gateway docs/samples into module-owned API docs as contracts freeze (to reduce drift and keep responsibilities clear). +3) Add/extend contract tests as upstream services publish authoritative schemas (especially `exception.*` event envelopes and limits). ## Execution Log | Date (UTC) | Update | Owner | @@ -94,3 +89,4 @@ | 2025-12-06 | Added ordered unblock plan for Web II (Export Center → Graph overlay → advisory/VEX schemas → shell restore → exception hooks). | Project Mgmt | | 2025-12-07 | Drafted Export Center gateway contract v0.9 in `docs/api/gateway/export-center.md` (profiles/run/status/events/distribution, limits, deterministic ordering, DSSE option) to unblock WEB-EXPORT-35/36/37. | Project Mgmt | | 2025-12-11 | **Export Center + Graph Platform complete:** WEB-EXPORT-35/36/37-001 and WEB-GRAPH-SPEC-21-000 through WEB-GRAPH-24-004 all DONE (12 tasks). Created: `export-center.models.ts`, `export-center.client.ts` (HTTP + mock with profiles/runs/SSE streaming/distributions), `graph-platform.models.ts`, `graph-platform.client.ts` (HTTP + mock with graphs/tiles/search/paths/export/assets/adjacency). Only WEB-EXC-25-002/003 and WEB-LNM-21-001/002 remain blocked (missing exception schema and advisory service schema). | Implementer | +| 2025-12-12 | Completed WEB-EXC-25-002/003 and WEB-LNM-21-001/002: published gateway contracts + samples (`docs/api/gateway/policy-exceptions.md`, `docs/api/gateway/exception-events.md`, `docs/api/gateway/advisories.md`, `docs/api/gateway/vex-evidence.md`) and shipped web SDK clients with unit specs; verified targeted `ng test` includes (9/9 pass). | Implementer | diff --git a/docs/implplan/SPRINT_0214_0001_0001_web_iii.md b/docs/implplan/SPRINT_0214_0001_0001_web_iii.md index 019108cfa..27045148a 100644 --- a/docs/implplan/SPRINT_0214_0001_0001_web_iii.md +++ b/docs/implplan/SPRINT_0214_0001_0001_web_iii.md @@ -1,28 +1,28 @@ -# Sprint 0214-0001-0001 · Web III (Experience & SDKs 180.F) +# Sprint 0214_0001_0001 · Experience & SDKs · Web III (180.F) ## Topic & Scope -- Phase III gateway delivery for Experience & SDKs: evidence aggregation, notifier routing, OAS alignment, and observability surfaces. -- Keep Web gateway contracts aligned with upstream services (Policy, Notifier, Timeline/Log stores) while maintaining offline/deterministic posture. -- Prep orchestrator read-only routes to unblock control-plane features in Web IV. +- Phase III gateway client/contract delivery for Experience & SDKs: notifier routing, OpenAPI alignment, and observability surfaces. +- Add evidence aggregation helpers for Console and prep orchestrator read-only routes to unblock control-plane features in Web IV. +- Keep gateway contracts deterministic and offline-friendly (stable ordering, strict caps, cache headers). - **Working directory:** `src/Web/StellaOps.Web`. ## Dependencies & Concurrency -- Upstream: Sprint 180.F · Web II must land shared policy/VEX observation contracts before this work proceeds. -- Notifier chain is sequential (WEB-NOTIFY-38-001 → 39-001 → 40-001); avoid parallel merges to keep scopes coherent. -- OAS alignment must follow order WEB-OAS-61-001 → 61-002 → 62-001 → 63-001. -- Observability work is sequential (WEB-OBS-50-001 → 51-001 → 52-001 → 54-001 → 55-001 → 56-001); keep guardrails consistent across steps. +- Upstream: `SPRINT_0213_0001_0002_web_ii.md` (Web II) delivered advisory + VEX read contracts, unblocking WEB-LNM-21-003. +- Concurrency: remaining work (WEB-LNM-21-003, WEB-ORCH-32-001) can proceed in parallel; keep gateway docs + samples aligned to upstream services. +- Sequencing (already completed): WEB-NOTIFY-38-001 → 39-001 → 40-001; WEB-OAS-61-001 → 61-002 → 62-001 → 63-001; WEB-OBS-50-001 → 51-001 → 52-001 → 54-001 → 55-001 → 56-001. ## Documentation Prerequisites - `docs/README.md` - `docs/07_HIGH_LEVEL_ARCHITECTURE.md` - `docs/modules/platform/architecture-overview.md` +- `docs/modules/orchestrator/architecture.md` - `src/Web/StellaOps.Web/AGENTS.md` ## Delivery Tracker | # | Task ID | Status | Key dependency / next step | Owners | Task Definition | | --- | --- | --- | --- | --- | --- | -| 1 | WEB-LNM-21-003 | BLOCKED | Requires advisory/VEX schemas from WEB-LNM-21-001/002 (Web II). | BE-Base Platform Guild · Policy Guild (`src/Web/StellaOps.Web`) | Provide combined endpoint for Console to fetch policy result plus advisory/VEX evidence linksets for a component. | +| 1 | WEB-LNM-21-003 | DONE (2025-12-12) | Contract + sample in `docs/api/gateway/policy-evidence.md`; client + mock + unit spec in `src/Web/StellaOps.Web/src/app/core/api/policy-evidence.client.ts`. | BE-Base Platform Guild · Policy Guild (`src/Web/StellaOps.Web`) | Provide combined endpoint for Console to fetch policy result plus advisory/VEX evidence linksets for a component. | | 2 | WEB-NOTIFY-38-001 | DONE (2025-12-11) | Extended notify.client.ts with tenant-scoped routing per SDK examples. | BE-Base Platform Guild (`src/Web/StellaOps.Web`) | Route notifier APIs (`/notifications/*`) and WS feed through gateway with tenant scoping, viewer/operator scope enforcement, and SSE/WebSocket bridging. | | 3 | WEB-NOTIFY-39-001 | DONE (2025-12-11) | Implemented digest/quiet-hours/throttle in notify.models.ts and notify.client.ts. | BE-Base Platform Guild (`src/Web/StellaOps.Web`) | Surface digest scheduling, quiet-hour/throttle management, and simulation APIs; ensure rate limits and audit logging. | | 4 | WEB-NOTIFY-40-001 | DONE (2025-12-11) | Implemented escalation/localization/incidents/ack in notify.client.ts with Mock client. | BE-Base Platform Guild (`src/Web/StellaOps.Web`) | Expose escalation, localization, channel health, and ack verification endpoints with admin scope enforcement and signed token validation. | @@ -36,7 +36,42 @@ | 12 | WEB-OBS-54-001 | DONE (2025-12-11) | Implemented listEvidence/listAttestations with timeline:read, evidence:read, attest:read scopes. | BE-Base Platform Guild (`src/Web/StellaOps.Web`) | Provide `/evidence/*` and `/attestations/*` pass-through endpoints, enforce `timeline:read`, `evidence:read`, `attest:read` scopes, append provenance headers, and surface verification summaries. | | 13 | WEB-OBS-55-001 | DONE (2025-12-11) | Implemented get/updateIncidentMode with audit trail, sampling override, retention bump. | BE-Base Platform Guild · Ops Guild (`src/Web/StellaOps.Web`) | Add `/obs/incident-mode` API (enable/disable/status) with audit trail, sampling override, retention bump preview, and CLI/Console hooks. | | 14 | WEB-OBS-56-001 | DONE (2025-12-11) | Implemented getSealStatus with drift metrics and widgetData for Console. | BE-Base Platform Guild · AirGap Guild (`src/Web/StellaOps.Web`) | Extend telemetry core integration to expose sealed/unsealed status APIs, drift metrics, and Console widgets without leaking sealed-mode secrets. | -| 15 | WEB-ORCH-32-001 | BLOCKED | Orchestrator REST contract not published; cannot implement gateway proxy. | BE-Base Platform Guild (`src/Web/StellaOps.Web`) | Expose read-only orchestrator APIs (e.g., `/orchestrator/sources`) via gateway with tenant scoping, caching headers, and rate limits. | +| 15 | WEB-ORCH-32-001 | DONE (2025-12-12) | Contract + sample in `docs/api/gateway/orchestrator.md`; client + mock + unit spec in `src/Web/StellaOps.Web/src/app/core/api/orchestrator.client.ts`. | BE-Base Platform Guild (`src/Web/StellaOps.Web`) | Expose read-only orchestrator APIs (e.g., `/orchestrator/sources`) via gateway with tenant scoping, caching headers, and rate limits. | + +## Wave Coordination +- **Wave A (done):** Notifier routing (tasks 2–4). +- **Wave B (done):** Gateway OpenAPI alignment (tasks 5–8). +- **Wave C (done):** Observability surfaces (tasks 9–14). +- **Wave D (done):** Evidence aggregation + orchestrator read-only (tasks 1, 15). + +## Wave Detail Snapshots +- Wave A: notifier workflows supported via `src/Web/StellaOps.Web/src/app/core/api/notify.models.ts` and `src/Web/StellaOps.Web/src/app/core/api/notify.client.ts` (incl. deterministic mock support). +- Wave B: OpenAPI discovery and conventions in `src/Web/StellaOps.Web/src/app/core/api/gateway-openapi.models.ts` and `src/Web/StellaOps.Web/src/app/core/api/gateway-openapi.client.ts`. +- Wave C: observability surfaces in `src/Web/StellaOps.Web/src/app/core/api/gateway-observability.models.ts` and `src/Web/StellaOps.Web/src/app/core/api/gateway-observability.client.ts`. +- Wave D: evidence aggregation via `docs/api/gateway/policy-evidence.md` + `src/Web/StellaOps.Web/src/app/core/api/policy-evidence.client.ts`; orchestrator read-only via `docs/api/gateway/orchestrator.md` + `src/Web/StellaOps.Web/src/app/core/api/orchestrator.client.ts`. + +## Interlocks +- Keep gateway docs (`docs/api/gateway/*.md`) and web clients/models aligned; gateway remains a stateless proxy (no business logic). +- Evidence aggregation (WEB-LNM-21-003) composes existing `/policy/*`, `/advisories/*`, and `/vex/*` routes; preserve deterministic ordering/hashes from source payloads and do not invent verdicts. +- Orchestrator read-only routes (WEB-ORCH-32-001) should follow the Orchestrator OpenAPI/spec once published; until then, draft from `docs/modules/orchestrator/architecture.md` and keep clearly marked as interim. + +## Upcoming Checkpoints +- None scheduled; track remaining work via Action Tracker. + +## Action Tracker +| # | Action | Owner | Due | Status | +| --- | --- | --- | --- | --- | +| 1 | Publish combined policy+evidence response shape + samples for WEB-LNM-21-003. | BE-Base Platform Guild · Policy Guild | 2025-12-16 | DONE (2025-12-12) | +| 2 | Publish orchestrator read-only gateway contract + samples for WEB-ORCH-32-001. | Orchestrator Guild · BE-Base Platform Guild | 2025-12-16 | DONE (2025-12-12) | +| 3 | Sync `docs/implplan/tasks-all.md` statuses for Web III tasks. | Planning | 2025-12-12 | DONE (2025-12-12) | + +## Decisions & Risks +| Risk | Impact | Mitigation | Owner | Status | +| --- | --- | --- | --- | --- | +| Aggregated evidence contract unclear (WEB-LNM-21-003) | Console implementation drift; duplicated fetching | Define a minimal combined response (policy result + advisory/VEX linksets) with deterministic samples; keep web clients as pure composition. | BE-Base Platform Guild · Policy Guild | Mitigated (2025-12-12) | +| Orchestrator read-only contract drift (WEB-ORCH-32-001) | UI features blocked or rework when service spec lands | Draft interim gateway contract + samples; align to Orchestrator OpenAPI once published; keep gateway proxy stateless. | Orchestrator Guild · BE-Base Platform Guild | Mitigated (2025-12-12) | +| Gateway conventions drift (error envelope/pagination/idempotency/deprecation) | Breaking changes for Console/CLI consumers | Keep gateway docs + `gateway-openapi.*` types authoritative for web clients; add targeted unit specs asserting headers and error mapping. | API Governance Guild | Mitigated (2025-12-11) | +| Local shell/PTY capacity limits validation | Tests/builds blocked locally | Mitigated: PTY restored; keep deterministic Chromium bootstrap (`src/Web/StellaOps.Web/scripts/verify-chromium.js`). | DevOps Guild | Mitigated (2025-12-12) | ## Execution Log | Date (UTC) | Update | Owner | @@ -46,15 +81,5 @@ | 2025-11-30 | Marked all sprint tasks BLOCKED because local environment cannot spawn shells (openpty "No space left on device"); cannot run builds/tests or edit via CLI. | Implementer | | 2025-12-01 | Could not update `docs/implplan/tasks-all.md` references due to same PTY failure; needs shell access to complete renames. | Implementer | | 2025-12-11 | **Web III 13/15 tasks complete:** WEB-NOTIFY-38/39/40-001 (notifier gateway), WEB-OAS-61-001/002 + 62/63-001 (OpenAPI spec/pagination/deprecation), WEB-OBS-50/51/52/54/55/56-001 (observability) all DONE. Created: extended `notify.models.ts` with digest/quiet-hours/throttle/escalation/incident types, extended `notify.client.ts` with all methods + MockNotifyClient, `gateway-openapi.models.ts` + `gateway-openapi.client.ts` (spec/deprecation/idempotency), `gateway-observability.models.ts` + `gateway-observability.client.ts` (health/SLO/trace/logs/evidence/attestations/incident-mode/seal-status). Only WEB-LNM-21-003 and WEB-ORCH-32-001 remain blocked (missing advisory/VEX schema and orchestrator REST contract). | Implementer | - -## Decisions & Risks -- Notify, OAS, and Observability tracks are strictly sequential; later tasks should not start until predecessors complete to avoid schema drift. -- WEB-LNM-21-003 remains gated on WEB-LNM-21-002 delivering stable policy/VEX observation contracts. -- WEB-ORCH-32-001 depends on orchestrator API clarity; block if contracts are not published to avoid misaligned caching/rate-limit behavior. -- Ensure telemetry/error envelope updates are synchronized with downstream Console/CLI consumers to prevent breaking dashboards. -- Local execution environment currently cannot spawn shells (openpty error “No space left on device”); gateway implementation work paused until shell capacity restored. -- All sprint tasks are BLOCKED pending restoration of shell/PTY capacity; no code changes can be executed or validated in current state. - -## Next Checkpoints -- Schedule dependency sync once WEB-LNM-21-002 contract drops (target week of 2025-12-02). -- Set notifier gateway review after WEB-NOTIFY-38-001 implementation notes are ready (target week of 2025-12-04). +| 2025-12-12 | Normalised sprint to coordination template (waves/interlocks/action tracker/risks), updated blockers to actionable TODOs, and synced `docs/implplan/tasks-all.md` statuses for Web III. | Project Mgmt | +| 2025-12-12 | Completed WEB-LNM-21-003 and WEB-ORCH-32-001: published gateway contracts + samples (`docs/api/gateway/policy-evidence.md`, `docs/api/gateway/orchestrator.md`) and shipped web SDK clients/mocks with unit specs; verified targeted `ng test` includes (4/4 pass). | Implementer | diff --git a/docs/implplan/SPRINT_0215_0001_0001_web_iv.md b/docs/implplan/SPRINT_0215_0001_0001_web_iv.md index 275d30f0c..4db1591dc 100644 --- a/docs/implplan/SPRINT_0215_0001_0001_web_iv.md +++ b/docs/implplan/SPRINT_0215_0001_0001_web_iv.md @@ -22,8 +22,8 @@ ## Delivery Tracker | # | Task ID | Status | Key dependency / next step | Owners | Task Definition | | --- | --- | --- | --- | --- | --- | -| 1 | WEB-ORCH-33-001 | BLOCKED (2025-11-30) | Orchestrator gateway REST contract + RBAC/audit checklist missing | BE-Base Platform Guild (`src/Web/StellaOps.Web`) | Add POST action routes (pause/resume/backfill) for orchestrator-run control, honoring RBAC and audit logging. | -| 2 | WEB-ORCH-34-001 | BLOCKED (2025-11-30) | WEB-ORCH-33-001 (blocked) | BE-Base Platform Guild (`src/Web/StellaOps.Web`) | Expose quotas/backfill APIs plus queue/backpressure metrics with admin scopes and error clustering. | +| 1 | WEB-ORCH-33-001 | DONE (2025-12-12) | Completed (contract + samples + SDK client + unit spec). | BE-Base Platform Guild (`src/Web/StellaOps.Web`) | Add POST action routes (pause/resume/backfill) for orchestrator-run control, honoring RBAC and audit logging. | +| 2 | WEB-ORCH-34-001 | DONE (2025-12-12) | Completed (contract + samples + SDK client + unit spec). | BE-Base Platform Guild (`src/Web/StellaOps.Web`) | Expose quotas/backfill APIs plus queue/backpressure metrics with admin scopes and error clustering. | | 3 | WEB-POLICY-20-001 | DONE (2025-12-11) | Completed | BE-Base Platform Guild · Policy Guild (`src/Web/StellaOps.Web`) | Implement Policy CRUD/compile/run/simulate/findings/explain endpoints with OpenAPI + tenant scoping. | | 4 | WEB-POLICY-20-002 | DONE (2025-12-11) | Completed | BE-Base Platform Guild (`src/Web/StellaOps.Web`) | Add pagination/filtering/sorting + tenant guards to policy listings with deterministic ordering diagnostics. | | 5 | WEB-POLICY-20-003 | DONE (2025-12-11) | Completed | BE-Base Platform Guild · QA Guild (`src/Web/StellaOps.Web`) | Map engine errors to `ERR_POL_*` payloads with contract tests and correlation IDs. | @@ -40,57 +40,53 @@ ## Wave Coordination - Wave 1: Orchestrator run-control (WEB-ORCH-33/34) follows WEB-ORCH-32-001 and can proceed independently of policy work. -- Wave 2: Policy Engine CRUD/simulation (WEB-POLICY-20-001…004) is strictly sequential and blocked on the REST contract + tenant/RBAC spec. -- Wave 3: Policy pack lifecycle (WEB-POLICY-23-001…004) begins only after Wave 2 exits with rate-limit design approved. -- Wave 4: Registry/Studio proxy and publishing (WEB-POLICY-27-001…005) proceeds after Wave 3 to avoid schema drift. +- Wave 2: Policy Engine CRUD/simulation (WEB-POLICY-20-001…004) is DONE (2025-12-11). +- Wave 3: Policy pack lifecycle (WEB-POLICY-23-001…004) is DONE (2025-12-11). +- Wave 4: Registry/Studio proxy and publishing (WEB-POLICY-27-001…005) is DONE (2025-12-11). ## Wave Detail Snapshots | Wave | Covered tasks | Current state | Exit criteria | Blockers | | --- | --- | --- | --- | --- | -| 1 | WEB-ORCH-33-001, WEB-ORCH-34-001 | BLOCKED | POST run-control routes plus quotas/backfill metrics deployed with RBAC + audit logging | Orchestrator REST contract + audit/RBAC checklist | -| 2 | WEB-POLICY-20-001…004 | BLOCKED | Policy CRUD/simulate endpoints with rate limits and error mapping published in OpenAPI | Policy Engine REST contract + tenant/RBAC spec; rate-limit design approval | -| 3 | WEB-POLICY-23-001…004 | BLOCKED | Policy pack CRUD/activation/simulate/evaluate with streaming responses and explain history | WEB-POLICY-20-004 completion and approval workflow design | -| 4 | WEB-POLICY-27-001…005 | BLOCKED | Registry proxy with reviews, publish/promote/rollback, and Studio observability dashboards | Wave 3 exit + shared registry schema/versioning plan | +| 1 | WEB-ORCH-33-001, WEB-ORCH-34-001 | DONE (2025-12-12) | POST run-control routes plus quotas/backfill metrics deployed with RBAC + audit logging | None | +| 2 | WEB-POLICY-20-001…004 | DONE (2025-12-11) | Policy CRUD/simulate endpoints with rate limits and error mapping published in OpenAPI | None | +| 3 | WEB-POLICY-23-001…004 | DONE (2025-12-11) | Policy pack CRUD/activation/simulate/evaluate with streaming responses and explain history | None | +| 4 | WEB-POLICY-27-001…005 | DONE (2025-12-11) | Registry proxy with reviews, publish/promote/rollback, and Studio observability dashboards | None | ## Interlocks - Policy Engine REST contract + tenant/RBAC specification is required before any WEB-POLICY-20-* implementation. - Platform Reliability must approve adaptive rate-limit design for simulations before publishing retry headers and CLI docs. - Policy Registry schema/versioning alignment is needed to avoid rework in WEB-POLICY-27-001…005. - Security Guild audit/RBAC checklist must be applied to orchestrator control routes prior to release. -- Orchestrator run-control REST contract and RBAC/audit acceptance checklist are required to proceed with WEB-ORCH-33-001/34-001. +- Orchestrator run-control REST contract and audit/RBAC checklist delivered (see `docs/api/gateway/orchestrator.md`). ## Upcoming Checkpoints -- 2025-12-02 · Receive Policy Engine REST contract + tenant/RBAC spec (Policy Guild) to unblock WEB-POLICY-20-001…004. -- 2025-12-03 · Platform Reliability rate-limit design review for simulations (slipped from 2025-11-22) to enable WEB-POLICY-20-004. -- 2025-12-06 · Go/no-go on policy pack CRUD/activation kickoff (WEB-POLICY-23-001/002) contingent on rate-limit approval. -- 2025-12-09 · Registry schema/versioning alignment for Studio proxy stream (WEB-POLICY-27-001 owner: Policy Registry Guild). +- None scheduled; sprint complete (Wave 1 done). ## Action Tracker | # | Action | Owner | Due (UTC) | Status | Notes | | --- | --- | --- | --- | --- | --- | -| 1 | Deliver Policy Engine REST contract + tenant/RBAC spec for web gateway | Policy Guild | 2025-12-02 | OPEN | Unblocks WEB-POLICY-20-001…004; required before any Angular wiring. | -| 2 | Confirm adaptive rate-limit design for simulations | Platform Reliability Guild | 2025-12-03 | OPEN | Gates WEB-POLICY-20-004 retry headers and quotas. | -| 3 | Publish RBAC/tenant alignment note for web gateway once contract lands | BE-Base Platform Guild | 2025-12-05 | PENDING | Follows Action #1; must precede policy pack work. | -| 4 | Lock Policy Registry schema/versioning plan for Studio proxy | Policy Registry Guild | 2025-12-09 | OPEN | Required before WEB-POLICY-27-001 streaming proxy. | -| 5 | Provide orchestrator run-control REST contract + audit/RBAC checklist | Platform Reliability Guild · Security Guild | 2025-12-03 | OPEN | Needed to unblock WEB-ORCH-33-001/34-001 routes and ensure operator metadata handling. | +| 1 | Deliver Policy Engine REST contract + tenant/RBAC spec for web gateway | Policy Guild | 2025-12-02 | DONE (2025-12-07) | Delivered per `docs/schemas/policy-engine-rest.openapi.yaml` (see Execution Log 2025-12-07). | +| 2 | Confirm adaptive rate-limit design for simulations | Platform Reliability Guild | 2025-12-03 | DONE (2025-12-07) | Delivered per `docs/contracts/rate-limit-design.md` (see Execution Log 2025-12-07). | +| 3 | Publish RBAC/tenant alignment note for web gateway once contract lands | BE-Base Platform Guild | 2025-12-05 | DONE (2025-12-07) | Delivered per `docs/contracts/web-gateway-tenant-rbac.md` (see Execution Log 2025-12-07). | +| 4 | Lock Policy Registry schema/versioning plan for Studio proxy | Policy Registry Guild | 2025-12-09 | DONE (2025-12-07) | Delivered per registry alignment note (see Execution Log 2025-12-07). | +| 5 | Provide orchestrator run-control REST contract + audit/RBAC checklist | Platform Reliability Guild · Security Guild | 2025-12-03 | DONE (2025-12-12) | Published contract at `docs/api/gateway/orchestrator.md`, documented operator headers, and added deterministic samples under `docs/api/gateway/samples/`. | ## Decisions & Risks -- Policy pack CRUD/activation (WEB-POLICY-23-001/002) remain BLOCKED until WEB-POLICY-20-004 rate-limit work lands. -- Registry/Studio chain (WEB-POLICY-27-001..005) must stay in order to keep schemas stable; avoid parallel merges without shared reviews. -- Ensure RBAC + tenant-scoping docs stay aligned with Policy Engine contracts to prevent drift during promotions. -- WEB-POLICY-20-001 blocked pending Policy Engine REST contract + tenant/RBAC specification; cannot scaffold Angular/web gateway endpoints without it. +- Policy Engine / pack lifecycle / registry proxy work is complete (2025-12-11); keep OpenAPI + gateway docs aligned with those implementations. +- Orchestrator run-control + quotas/metrics work is complete (2025-12-12); enforce RBAC, tenant scoping, and operator metadata headers for audit logging. | ID | Risk | Impact | Mitigation | Owner | Status | | --- | --- | --- | --- | --- | --- | -| R1 | Policy Engine REST contract/RBAC spec slips past 2025-12-02 | Blocks WEB-POLICY-20-001…004 and downstream pack work | Track Action #1; escalate to Policy Guild; keep OpenAPI placeholders out of main until contract arrives | Policy Guild | OPEN | -| R2 | Rate-limit design review delayed past 2025-12-03 | Simulation endpoints cannot expose quotas/headers; CLI docs drift | Track Action #2; freeze public docs until design approved; backfill metrics before enablement | Platform Reliability Guild | OPEN | -| R3 | Registry schema/versioning diverges from web proxy | Rework for WEB-POLICY-27-001…005; potential outage on streaming downloads | Track Action #4; pin schema versions and add contract tests before merge | Policy Registry Guild | OPEN | -| R4 | Orchestrator run-control released without audit/RBAC alignment | Post-release security gap and incomplete audit trail | Apply Security Guild checklist; add acceptance tests for RBAC + audit logging before Wave 1 exit | BE-Base Platform Guild | OPEN | -| R5 | Missing orchestrator gateway REST contract for run-control/metrics | Blocks WEB-ORCH-33-001/34-001 and delays Wave 1 start | Track Action #5; obtain contract + header requirements; wire OperatorMetadata interceptor once contract is defined | Platform Reliability Guild | OPEN | +| R1 | Policy Engine REST contract/RBAC spec slips past 2025-12-02 | Blocks WEB-POLICY-20-001…004 and downstream pack work | Track Action #1; escalate to Policy Guild; keep OpenAPI placeholders out of main until contract arrives | Policy Guild | Mitigated (2025-12-07) | +| R2 | Rate-limit design review delayed past 2025-12-03 | Simulation endpoints cannot expose quotas/headers; CLI docs drift | Track Action #2; freeze public docs until design approved; backfill metrics before enablement | Platform Reliability Guild | Mitigated (2025-12-07) | +| R3 | Registry schema/versioning diverges from web proxy | Rework for WEB-POLICY-27-001…005; potential outage on streaming downloads | Track Action #4; pin schema versions and add contract tests before merge | Policy Registry Guild | Mitigated (2025-12-11) | +| R4 | Orchestrator run-control released without audit/RBAC alignment | Post-release security gap and incomplete audit trail | Apply Security Guild checklist; add acceptance tests for RBAC + audit logging before Wave 1 exit | BE-Base Platform Guild | Mitigated (2025-12-12) | +| R5 | Missing orchestrator gateway REST contract for run-control/metrics | Blocks WEB-ORCH-33-001/34-001 and delays Wave 1 start | Track Action #5; obtain contract + header requirements; wire OperatorMetadata interceptor once contract is defined | Platform Reliability Guild | Mitigated (2025-12-12) | ## Execution Log | Date (UTC) | Update | Owner | | --- | --- | --- | +| 2025-12-12 | **Wave 1 complete:** Completed WEB-ORCH-33-001/34-001 by publishing the orchestrator run-control + quotas contract and samples (`docs/api/gateway/orchestrator.md`, `docs/api/gateway/samples/orchestrator-*.json`), shipping the web SDK client + deterministic mock + unit spec (`src/Web/StellaOps.Web/src/app/core/api/orchestrator-control.*`), and wiring `ORCHESTRATOR_CONTROL_API` in `src/Web/StellaOps.Web/src/app/app.config.ts`. | Implementer | | 2025-12-11 | **Wave 2/3/4 complete:** Completed all 13 policy tasks (WEB-POLICY-20-001..004, 23-001..004, 27-001..005). Implemented: PolicyEngineStore, Policy CRUD/simulation APIs, error handling with ERR_POL_* codes, adaptive rate limiting/quotas, SSE streaming for simulations, policy registry proxy, review lifecycle, batch simulation, publish/sign/promote/rollback endpoints, and Policy Studio metrics/logs service. Only WEB-ORCH-33/34 remain BLOCKED pending orchestrator REST contract. | Implementer | | 2025-12-07 | **Wave 10 unblock:** Changed 13 tasks from BLOCKED → TODO. Policy Engine REST contract delivered at `docs/schemas/policy-engine-rest.openapi.yaml`, rate-limit design at `docs/contracts/rate-limit-design.md`, tenant/RBAC spec at `docs/contracts/web-gateway-tenant-rbac.md`. WEB-POLICY-20-001..004, 23-001..004, 27-001..005 can now proceed sequentially. | Implementer | | 2025-11-30 | Marked WEB-ORCH-33-001/34-001 BLOCKED pending orchestrator REST contract + RBAC/audit checklist; no backend surface present in web workspace. | Implementer | diff --git a/docs/implplan/SPRINT_0403_0001_0001_scanner_java_detection_gaps.md b/docs/implplan/SPRINT_0403_0001_0001_scanner_java_detection_gaps.md new file mode 100644 index 000000000..7316088c3 --- /dev/null +++ b/docs/implplan/SPRINT_0403_0001_0001_scanner_java_detection_gaps.md @@ -0,0 +1,86 @@ +# Sprint 0403 - Scanner Java Analyzer Detection Gaps + +## Topic & Scope +- Close Java inventory blind-spots that currently miss dependencies inside fat archives and under-detect runtime/JNI context, improving downstream vuln matching and reachability prioritization. +- Keep outputs deterministic and offline-first (no network fetches; stable ordering; bounded metadata). +- Produce hard evidence: new fixtures + golden outputs covering fat JAR/WAR embedded libs, `pom.xml`-only artifacts, multi-module Gradle lock layouts, and runtime image discovery. +- **Working directory:** `src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.Java` (tests: `src/Scanner/__Tests/StellaOps.Scanner.Analyzers.Lang.Java.Tests`; optional benches: `src/Bench/StellaOps.Bench/Scanner.Analyzers`). + +## Dependencies & Concurrency +- Builds on archived Java analyzer sprints (notably `docs/implplan/archived/SPRINT_0140_0001_0001_scanner_java_enhancement.md`). +- Must remain parallel-safe with other language analyzers: no shared global state, no non-deterministic iteration over filesystem/zip entries. +- Do not introduce new external downloads; use local fixtures/caches only. + +## Documentation Prerequisites +- `docs/README.md` +- `docs/07_HIGH_LEVEL_ARCHITECTURE.md` +- `docs/modules/platform/architecture-overview.md` +- `docs/modules/scanner/architecture.md` +- `src/Scanner/AGENTS.md` +- `src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.Java/AGENTS.md` +- (Reference) `docs/implplan/archived/SPRINT_0140_0001_0001_scanner_java_enhancement.md` + +## Delivery Tracker +| # | Task ID | Status | Key dependency / next step | Owners | Task Definition | +| --- | --- | --- | --- | --- | --- | +| 1 | SCAN-JAVA-403-001 | TODO | Decide nested locator scheme (Action 1), then implement. | Java Analyzer Guild (`src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.Java`) | **Scan embedded libraries inside archives**: extend `JavaLanguageAnalyzer` to enumerate and parse Maven coordinates from embedded JARs in `BOOT-INF/lib/**.jar`, `WEB-INF/lib/**.jar`, `APP-INF/lib/**.jar`, and `lib/**.jar` *without extracting to disk*. Emit one component per discovered embedded artifact (PURL-based when possible). Evidence locators must represent nesting deterministically (e.g., `outer.jar!BOOT-INF/lib/inner.jar!META-INF/maven/.../pom.properties`). Enforce size/time bounds (skip embedded jars above a configured size threshold; record `embeddedScanSkipped=true` + reason metadata). | +| 2 | SCAN-JAVA-403-002 | TODO | After task 1 skeleton lands, add `pom.xml` fallback and coverage fixtures. | Java Analyzer Guild (`src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.Java`) | **Add `pom.xml` fallback when `pom.properties` is missing**: detect and parse `META-INF/maven/**/pom.xml` (both top-level archives and embedded jars). Prefer `pom.properties` when both exist; otherwise derive `groupId/artifactId/version/packaging/name` from `pom.xml` and emit `pkg:maven/...` PURLs. Evidence must include sha256 of the parsed `pom.xml` entry. If `pom.xml` is present but coordinates are incomplete, emit a component with explicit key (no PURL) carrying `manifestTitle/manifestVersion` and an `unresolvedCoordinates=true` marker (do not guess a Maven PURL). | +| 3 | SCAN-JAVA-403-003 | TODO | Requires agreement on multi-module precedence (Interlock 2). | Java Analyzer Guild (`src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.Java`) | **Parse all discovered Gradle lockfiles deterministically**: update `JavaLockFileCollector` to parse lockfiles from `JavaBuildFileDiscovery` results (not only root `gradle.lockfile` and `gradle/dependency-locks`). Preserve the lockfile-relative path as `lockLocator` and include module context in metadata (e.g., `lockModulePath`). Deduplicate identical GAVs deterministically (stable overwrite rules documented in code + tested). | +| 4 | SCAN-JAVA-403-004 | TODO | Decide runtime component identity strategy (Action 2). | Java Analyzer Guild (`src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.Java`) | **Emit runtime image components**: when `JavaWorkspaceNormalizer` identifies a runtime image, emit a `java-runtime` component (explicit key or PURL per decision) with metadata `java.version`, `java.vendor`, and `runtimeImagePath` (relative). Evidence must reference the `release` file. Ensure deterministic ordering and do not double-count multiple identical runtime images (same version+vendor+relative path). | +| 5 | SCAN-JAVA-403-005 | TODO | After task 1 or 2, wire bytecode JNI analysis once per scan. | Java Analyzer Guild (`src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.Java`) | **Replace naive JNI string scanning with bytecode-based JNI analysis**: integrate `Internal/Jni/JavaJniAnalyzer` into `JavaLanguageAnalyzer` so JNI usage metadata is derived from parsed method invocations and native method flags (not raw ASCII search). Output must be bounded and deterministic: emit counts + top-N stable samples (e.g., `jni.edgeCount`, `jni.targetLibraries`, `jni.reasons`). Do not emit full class lists unbounded. | +| 6 | SCAN-JAVA-403-006 | TODO | Parallel with tasks 1–5; keep fixtures minimal. | QA Guild (`src/Scanner/__Tests/StellaOps.Scanner.Analyzers.Lang.Java.Tests`) | **Add fixtures + golden outputs for new detection paths**: introduce fixtures covering (a) fat JAR with embedded libs under `BOOT-INF/lib`, (b) WAR with embedded libs under `WEB-INF/lib`, (c) artifact containing only `pom.xml` (no `pom.properties`), (d) multi-module Gradle lockfile layout, and (e) runtime image directory with `release`. Add/extend `JavaLanguageAnalyzerTests.cs` golden harness assertions proving embedded components are emitted with correct nested locators and stable ordering. | +| 7 | SCAN-JAVA-403-007 | TODO | After tasks 1–2 land, wire perf guard. | Bench Guild (`src/Bench/StellaOps.Bench/Scanner.Analyzers`) | **Add benchmark scenario for fat-archive scanning**: add a deterministic bench case that scans a representative fat JAR fixture and reports component count + elapsed time. Establish a baseline ceiling and ensure CI can run it offline. | +| 8 | SCAN-JAVA-403-008 | TODO | After tasks 1–5 land, document final contract. | Docs Guild + Java Analyzer Guild (`docs/modules/scanner`, `src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.Java`) | **Document Java analyzer detection contract**: update `docs/modules/scanner/architecture.md` (or add a Java analyzer sub-doc under `docs/modules/scanner/`) describing: embedded jar scanning rules, nested evidence locator format, lock precedence rules, runtime component emission, JNI metadata semantics, and known limitations (e.g., shaded jars with stripped Maven metadata remain best-effort). Link this sprint from the doc’s “evidence & determinism” area. | + +## Wave Coordination +| Wave | Guild owners | Shared prerequisites | Status | Notes | +| --- | --- | --- | --- | --- | +| A: Embedded Inventory | Java Analyzer Guild + QA Guild | Locator decision (Action 1) | TODO | Enables detection of fat JAR/WAR embedded libs. | +| B: Coordinates Fallback | Java Analyzer Guild + QA Guild | None | TODO | `pom.xml` fallback for Maven coordinates when properties missing. | +| C: Lock Coverage | Java Analyzer Guild + QA Guild | Precedence decision (Interlock 2) | TODO | Multi-module Gradle lock ingestion improvements. | +| D: Runtime & JNI Context | Java Analyzer Guild + QA Guild | Runtime identity decision (Action 2) | TODO | Runtime component emission + JNI bytecode integration. | +| E: Bench & Docs | Bench Guild + Docs Guild | Waves A–D | TODO | Perf ceiling + contract documentation. | + +## Wave Detail Snapshots +- **Wave A:** Embedded JAR enumeration + nested evidence locators; fixtures prove fat-archive dependency visibility. +- **Wave B:** `pom.xml` fallback emits Maven PURLs when properties missing; explicit-key “unknown coords” component when insufficient data. +- **Wave C:** Broader Gradle lock ingestion across multi-module layouts; deterministic de-dupe rules and module-context metadata. +- **Wave D:** Runtime image component emitted from `release`; JNI metadata uses bytecode parsing with bounded output. +- **Wave E:** Offline benchmark + documented “what the analyzer promises” contract. + +## Interlocks +- Evidence locator format must be stable across analyzers and safe for downstream consumers (CLI/UI/export). (Action 1) +- **Lock precedence and de-duplication:** when multiple lock sources exist (root lock + module lock + build.gradle parsing), precedence must be explicit, deterministic, and covered by tests; do not silently fluctuate based on traversal order. +- Embedded scanning must be bounded (size thresholds, entry limits) to avoid scanning untrusted giant archives; skipped work must be explicitly marked in metadata for auditability. +- Runtime image identity requires a decision: explicit-key component vs a stable PURL scheme; must not introduce false vuln matches. (Action 2) + +## Upcoming Checkpoints +- 2025-12-13: Approve nested evidence locator scheme (Action 1) and runtime identity strategy (Action 2). +- 2025-12-16: Waves A+B implemented with fixtures passing locally for Java analyzer test project. +- 2025-12-18: Wave C lock coverage merged with multi-module fixture. +- 2025-12-20: Wave D (runtime + JNI) complete; outputs bounded + deterministic. +- 2025-12-22: Wave E bench + docs complete; sprint ready for DONE review. + +## Action Tracker +| # | Action | Owner | Due (UTC) | Status | Notes | +| --- | --- | --- | --- | --- | --- | +| 1 | Decide and document nested evidence locator scheme for embedded JAR entries (`outer!inner!path`). | Project Mgmt + Java Analyzer Guild | 2025-12-13 | Open | Must be stable, deterministic, and parseable by exporters. | +| 2 | Decide runtime component identity approach (explicit key vs PURL scheme; if PURL, specify qualifiers). | Project Mgmt + Scanner Guild | 2025-12-13 | Open | Avoid false vuln matches; prefer explicit-key if uncertain. | +| 3 | Define embedded-scan bounds (max embedded jars per archive, max embedded jar size) and required metadata when skipping. | Java Analyzer Guild + Security Guild | 2025-12-13 | Open | Must prevent resource exhaustion from untrusted artifacts. | + +## Decisions & Risks +- **Decision (pending):** Embedded locator format and runtime identity strategy (see Action Tracker 1–2). + +| Risk ID | Risk | Impact | Likelihood | Mitigation | Owner | Trigger / Signal | +| --- | --- | --- | --- | --- | --- | --- | +| R1 | Embedded jar scanning increases CPU/memory and can be abused by large payloads. | High | Medium | Hard limits + streaming where possible; deterministic skip markers; add perf bench. | Java Analyzer Guild | Bench regression; OOM/timeout in CI; unusually large jar fixtures. | +| R2 | Nested locator format breaks downstream tooling expectations (export/UI). | Medium | Medium | Decide format up-front; add tests that assert exact locator strings; document contract. | Project Mgmt | Export bundle consumers fail parsing; UI shows confusing paths. | +| R3 | `pom.xml` parsing yields partial/incorrect coordinates (parent inheritance not available). | Medium | Medium | Only emit Maven PURL when `groupId/artifactId/version` are present; otherwise explicit-key component with `unresolvedCoordinates=true`. | Java Analyzer Guild | Golden fixtures show non-deterministic/missing coordinates. | +| R4 | Multi-module lock ingestion causes duplicate “declared-only” components or unstable overwrite rules. | Medium | Medium | Define precedence; stable sort and deterministic overwrite; fixture covering duplicates. | Java Analyzer Guild | Flaky tests; differing outputs depending on directory order. | +| R5 | Runtime “PURL” choice creates false vuln matches for Java runtimes. | High | Low/Medium | Prefer explicit-key component unless a vetted PURL scheme is agreed. | Scanner Guild | Vuln matches spike for runtime-only components without evidence. | + +## Execution Log +| Date (UTC) | Update | Owner | +| --- | --- | --- | +| 2025-12-12 | Sprint created to close Java analyzer detection gaps (embedded libs, `pom.xml` fallback, lock coverage, runtime images, JNI integration) with fixtures/bench/docs expectations. | Project Mgmt | + diff --git a/docs/implplan/SPRINT_0404_0001_0001_scanner_dotnet_detection_gaps.md b/docs/implplan/SPRINT_0404_0001_0001_scanner_dotnet_detection_gaps.md new file mode 100644 index 000000000..dfb7449a8 --- /dev/null +++ b/docs/implplan/SPRINT_0404_0001_0001_scanner_dotnet_detection_gaps.md @@ -0,0 +1,81 @@ +# Sprint 0404 - Scanner .NET Analyzer Detection Gaps + +## Topic & Scope +- Close .NET inventory blind-spots where the analyzer currently emits **no components** unless `*.deps.json` files are present. +- Add deterministic, offline-first **declared-only** detection paths from build and lock artefacts (csproj/props/CPM/lock files) and make bundling/NativeAOT cases auditable (explicit “under-detected” markers). +- Preserve current behavior for publish-output scans while expanding coverage for source trees and non-standard deployment layouts. +- **Working directory:** `src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.DotNet` (tests: `src/Scanner/__Tests/StellaOps.Scanner.Analyzers.Lang.DotNet.Tests` and `src/Scanner/__Tests/StellaOps.Scanner.Analyzers.Lang.Tests`). + +## Dependencies & Concurrency +- Builds on the existing .NET analyzer implementation (`DotNetDependencyCollector` / `DotNetPackageBuilder`) and its fixtures under `src/Scanner/__Tests/StellaOps.Scanner.Analyzers.Lang.Tests/Fixtures/lang/dotnet`. +- Must remain parallel-safe under concurrent scans (no shared mutable global state beyond existing concurrency-safe caches). +- Offline-first: do not restore packages, query feeds, or require MSBuild evaluation that triggers downloads. + +## Documentation Prerequisites +- `docs/README.md` +- `docs/07_HIGH_LEVEL_ARCHITECTURE.md` +- `docs/modules/platform/architecture-overview.md` +- `docs/modules/scanner/architecture.md` +- `src/Scanner/AGENTS.md` +- `src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.DotNet/AGENTS.md` + +## Delivery Tracker +| # | Task ID | Status | Key dependency / next step | Owners | Task Definition | +| --- | --- | --- | --- | --- | --- | +| 1 | SCAN-DOTNET-404-001 | TODO | Decide declared-vs-installed merge rules (Action 1). | .NET Analyzer Guild (`src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.DotNet`) | **Add declared-only fallback when no `*.deps.json` exists**: if `DotNetDependencyCollector` finds zero deps files, collect dependencies from (in order): `packages.lock.json`, SDK-style project files (`*.csproj/*.fsproj/*.vbproj`) with `Directory.Build.props` + `Directory.Packages.props` (CPM), and legacy `packages.config`. Emit declared-only components with deterministic metadata including `declaredOnly=true`, `declared.source`, `declared.locator`, `declared.versionSource`, and `declared.isDevelopmentDependency`. Do not attempt full MSBuild evaluation; only use existing lightweight parsers/resolvers. | +| 2 | SCAN-DOTNET-404-002 | TODO | Requires Action 2 decision on PURL/keying when version unknown. | .NET Analyzer Guild (`src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.DotNet`) | **Component identity rules for unresolved versions**: when a declared dependency has an unresolved/unknown version (e.g., CPM enabled but missing a version, or property placeholder cannot be resolved), emit a component using `AddFromExplicitKey` (not a versionless PURL) and mark `declared.versionResolved=false` with `declared.unresolvedReason`. Ensure these components cannot collide with real versioned NuGet PURLs. | +| 3 | SCAN-DOTNET-404-003 | TODO | After task 1/2, implement merge logic and tests. | .NET Analyzer Guild (`src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.DotNet`) | **Merge declared-only with installed packages when deps.json exists**: when `*.deps.json` packages are present, continue emitting installed `pkg:nuget/@` components as today. Additionally, emit declared-only components for build/lock dependencies that do not match any installed package (match by normalized id + version). When an installed package exists but has no corresponding declared record, tag the installed component with `declared.missing=true`. Merge must be deterministic and independent of filesystem enumeration order. | +| 4 | SCAN-DOTNET-404-004 | TODO | Define bounds and target paths (Interlock 2). | .NET Analyzer Guild (`src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.DotNet`) | **Surface bundling signals as explicit metadata**: integrate `SingleFileAppDetector` and `ILMergedAssemblyDetector` so scans can record “inventory may be incomplete” signals. Minimum requirement: when a likely bundle is detected, emit metadata on the *entrypoint component(s)* (or a synthetic “bundle” component) including `bundle.kind` (`singlefile`, `ilmerge`, `unknown`), `bundle.indicators` (top-N bounded), and `bundle.filePath`. Do not scan the entire filesystem for executables; only scan bounded candidates (e.g., adjacent to deps.json/runtimeconfig, or explicitly configured). | +| 5 | SCAN-DOTNET-404-005 | TODO | After task 3, decide if edges should include declared edges by default. | .NET Analyzer Guild (`src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.DotNet`) | **Declared dependency edges output**: when `emitDependencyEdges=true`, include declared edges from build/lock sources in addition to deps.json dependencies, and annotate edge provenance (`edge[*].source=csproj|packages.lock.json|deps.json`). Ensure ordering is stable and bounded (top-N per component if necessary). | +| 6 | SCAN-DOTNET-404-006 | TODO | Parallel with tasks 1–5; fixtures first. | QA Guild (`src/Scanner/__Tests/StellaOps.Scanner.Analyzers.Lang.Tests`, `src/Scanner/__Tests/StellaOps.Scanner.Analyzers.Lang.DotNet.Tests`) | **Fixtures + golden outputs**: add fixtures and golden JSON proving new behaviors: (a) **source-tree only** (csproj + Directory.Packages.props + no deps.json), (b) packages.lock.json-only, (c) legacy packages.config-only, (d) mixed case (deps.json present + missing declared record and vice versa), (e) bundled executable indicator fixture (synthetic binary for detector tests, not real apphost). Extend `DotNetLanguageAnalyzerTests` to assert deterministic output and correct declared/installed reconciliation. | +| 7 | SCAN-DOTNET-404-007 | TODO | After core behavior lands, update docs. | Docs Guild + .NET Analyzer Guild (`docs/modules/scanner`, `src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.DotNet`) | **Document .NET analyzer contract**: update `docs/modules/scanner/architecture.md` (or add a .NET analyzer sub-doc under `docs/modules/scanner/`) describing: detection sources and precedence, how declared-only is represented, identity rules for unresolved versions, bundling signals, and known limitations (no full MSBuild evaluation, no restore/feed access). Link this sprint from the doc. | +| 8 | SCAN-DOTNET-404-008 | TODO | Optional; only if perf regression risk materializes. | Bench Guild (`src/Bench/StellaOps.Bench/Scanner.Analyzers`) | **Benchmark declared-only scanning**: add a deterministic bench that scans a representative source-tree fixture (many csproj/props/lockfiles) and records elapsed time + component counts. Establish a baseline ceiling and ensure CI can run it offline. | + +## Wave Coordination +| Wave | Guild owners | Shared prerequisites | Status | Notes | +| --- | --- | --- | --- | --- | +| A: Declared-only sources | .NET Analyzer Guild + QA Guild | Decisions in Action 1–2 | TODO | Enable detection without deps.json. | +| B: Reconciliation & edges | .NET Analyzer Guild + QA Guild | Wave A | TODO | Declared vs installed merge + edge provenance. | +| C: Bundling signals | .NET Analyzer Guild + QA Guild | Interlock 2 | TODO | Make bundling/under-detection auditable. | +| D: Docs & bench | Docs Guild + Bench Guild | Waves A–C | TODO | Contract + perf guardrails. | + +## Wave Detail Snapshots +- **Wave A:** Standalone declared-only inventory (lockfiles/projects/CPM/packages.config) with deterministic identity and evidence. +- **Wave B:** Merge declared-only with deps.json-installed packages; emit declared-missing/lock-missing markers and optional edge provenance. +- **Wave C:** Bounded bundling detection integrated; no filesystem-wide binary scanning. +- **Wave D:** Contract documentation + optional benchmark to prevent regressions. + +## Interlocks +- **Identity & collisions:** Explicit-key components for unresolved versions must never collide with real `pkg:nuget/@` PURLs (Action 2). +- **Bundling scan bounds:** bundling detectors must be applied only to bounded candidate files; scanning “all executables” is forbidden for perf/safety. +- **No restore/MSBuild evaluation:** do not execute MSBuild or `dotnet restore`; use only lightweight parsing and local file inspection. + +## Upcoming Checkpoints +- 2025-12-13: Approve declared-vs-installed precedence and unresolved identity rules (Actions 1–2). +- 2025-12-16: Wave A complete with fixtures proving deps.json-free detection. +- 2025-12-18: Wave B complete (merge + edge provenance) with mixed-case fixtures. +- 2025-12-20: Wave C complete (bundling signals) with bounded candidate selection and tests. +- 2025-12-22: Docs updated; optional bench decision made; sprint ready for DONE review. + +## Action Tracker +| # | Action | Owner | Due (UTC) | Status | Notes | +| --- | --- | --- | --- | --- | --- | +| 1 | Define deterministic precedence for dependency sources (deps.json vs lock vs project vs packages.config) and merge rules for “declared missing / installed missing”. | Project Mgmt + .NET Analyzer Guild | 2025-12-13 | Open | Must be testable via fixtures; no traversal-order dependence. | +| 2 | Decide component identity strategy when version cannot be resolved (explicit key scheme + required metadata fields). | Project Mgmt + Scanner Guild | 2025-12-13 | Open | Must avoid false matches and collisions with PURLs. | +| 3 | Define which files qualify as “bundling detector candidates” (adjacent to deps.json/runtimeconfig, configured paths, size limits). | .NET Analyzer Guild + Security Guild | 2025-12-13 | Open | Prevent scanning untrusted large binaries broadly. | + +## Decisions & Risks +- **Decision (pending):** precedence + identity strategy (see Action Tracker 1–2). + +| Risk ID | Risk | Impact | Likelihood | Mitigation | Owner | Trigger / Signal | +| --- | --- | --- | --- | --- | --- | --- | +| R1 | Declared-only scanning causes false positives (declared deps not actually shipped). | Medium | Medium | Mark `declaredOnly=true`; keep installed vs declared distinction; allow policy/UI to down-rank declared-only. | .NET Analyzer Guild | Increased component counts without corresponding runtime evidence. | +| R2 | Unresolved version handling creates unstable component identity. | High | Medium | Use explicit-key with stable recipe; include source+locator in key material if needed. | Project Mgmt | Flaky golden outputs; duplicate collisions across projects. | +| R3 | Bundling detectors cause perf regressions or scan untrusted huge binaries. | High | Low/Medium | Bounded candidate selection + size caps; emit “skipped” markers when exceeding limits. | Security Guild + .NET Analyzer Guild | CI timeouts; scanning large container roots. | +| R4 | Adding declared edges creates noisy graphs. | Medium | Medium | Gate behind `emitDependencyEdges`; keep edges bounded and clearly sourced. | .NET Analyzer Guild | Export/UI performance degradation. | + +## Execution Log +| Date (UTC) | Update | Owner | +| --- | --- | --- | +| 2025-12-12 | Sprint created to expand .NET analyzer coverage beyond deps.json (declared-only detection, reconciliation, bundling signals, fixtures/docs/bench). | Project Mgmt | + diff --git a/docs/implplan/SPRINT_0405_0001_0001_scanner_python_detection_gaps.md b/docs/implplan/SPRINT_0405_0001_0001_scanner_python_detection_gaps.md new file mode 100644 index 000000000..0b6a3a2c8 --- /dev/null +++ b/docs/implplan/SPRINT_0405_0001_0001_scanner_python_detection_gaps.md @@ -0,0 +1,84 @@ +# Sprint 0405 · Scanner · Python Detection Gaps + +## Topic & Scope +- Close concrete detection gaps in the Python analyzer so scans reliably inventory Python dependencies across **installed envs**, **source trees**, **lockfiles**, **conda**, **wheels/zipapps**, and **container layers**. +- Replace “best-effort by directory enumeration” with **bounded, layout-aware discovery** (deterministic ordering, explicit precedence, and auditable “skipped” markers). +- Produce evidence: new deterministic fixtures + golden outputs, plus a lightweight offline benchmark guarding regressions. +- **Working directory:** `src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.Python` (tests: `src/Scanner/__Tests/StellaOps.Scanner.Analyzers.Lang.Python.Tests`). + +## Dependencies & Concurrency +- Depends on existing scanner contracts for component identity/evidence locators: `src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang/Core/LanguageAnalyzerResult.cs`. +- Interlocks with container/layer conventions used by other analyzers (avoid diverging locator/overlay semantics). +- Parallel-safe with `SPRINT_0403_0001_0001_scanner_java_detection_gaps.md` and `SPRINT_0404_0001_0001_scanner_dotnet_detection_gaps.md` (no shared code changes expected unless explicitly noted). + +## Documentation Prerequisites +- `docs/modules/scanner/architecture.md` +- `src/Scanner/AGENTS.md` +- `src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.Python/AGENTS.md` +- `src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang/AGENTS.md` + +## Delivery Tracker +| # | Task ID | Status | Key dependency / next step | Owners | Task Definition | +| --- | --- | --- | --- | --- | --- | +| 1 | SCAN-PY-405-001 | TODO | Approve identity/precedence rules (Actions 1–2). | Python Analyzer Guild (`src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.Python`) | **Wire layout-aware discovery into `PythonLanguageAnalyzer`**: stop treating “any `*.dist-info` anywhere” as an installed package source. Use `PythonInputNormalizer` + `PythonVirtualFileSystem` + `PythonPackageDiscovery` as the first-pass inventory (site-packages, editable paths, wheels, zipapps, container layer roots). Ensure deterministic path precedence (later/higher-confidence wins) and bounded scanning (no unbounded full-tree recursion for patterns). Emit package-kind + confidence metadata (`pkg.kind`, `pkg.confidence`, `pkg.location`) for every component. | +| 2 | SCAN-PY-405-002 | TODO | After task 1, define dist-info/egg-info enrichment rules. | Python Analyzer Guild (`src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.Python`) | **Preserve dist-info “deep evidence” while expanding coverage**: for any discovered package with a real `*.dist-info`/`*.egg-info`, continue to enrich with `PythonDistributionLoader` evidence (METADATA/RECORD/WHEEL/entrypoints, RECORD verification stats). For packages discovered without dist-info (e.g., Poetry editable, vendored, zipapp), emit components using `AddFromExplicitKey` with stable identity rules (Action 1) and evidence pointing to the originating file(s) (`pyproject.toml`, lockfile, archive path). | +| 3 | SCAN-PY-405-003 | TODO | Decide lock precedence + supported formats scope (Action 2). | Python Analyzer Guild (`src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.Python`) | **Expand lockfile/requirements detection and parsing**: upgrade `PythonLockFileCollector` to (a) discover lock/requirements files deterministically (root + nested common paths), (b) support `-r/--requirement` includes with cycle detection, (c) correctly handle editable `-e/--editable` lines, (d) parse PEP 508 specifiers (not only `==/===`) and `name @ url` direct references, and (e) include Pipenv `develop` section. Add opt-in support for at least one modern lock (`uv.lock` or `pdm.lock`) with deterministic record ordering and explicit “unsupported line” counters. | +| 4 | SCAN-PY-405-004 | TODO | Requires container overlay decision (Action 3). | Python Analyzer Guild (`src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.Python`) | **Correct container-layer inventory semantics**: when scanning raw OCI layer trees (`layers/`, `.layers/`, `layer*/`), honor whiteouts/overlay ordering so removed packages are not reported. Use/extend `Internal/Packaging/Adapters/ContainerLayerAdapter` semantics as the source of truth for precedence. Emit explicit metadata markers when inventory is partial due to missing overlay context (e.g., `container.overlayIncomplete=true`). | +| 5 | SCAN-PY-405-005 | TODO | Decide representation for vendored deps (Action 4). | Python Analyzer Guild (`src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.Python`) | **Surface vendored (bundled) Python deps**: integrate `VendoredPackageDetector` so known vendoring patterns (`*_vendor`, `third_party`, `requests.packages`, etc.) are detected. Emit either (a) separate “embedded” components with bounded evidence locators (preferred) or (b) a bounded metadata summary on the parent package (`vendored.detected=true`, `vendored.packages`, `vendored.paths`). Never emit unbounded file/module lists; cap to top-N deterministic samples. | +| 6 | SCAN-PY-405-006 | TODO | After task 1–3, decide “used-by-entrypoint” upgrade approach (Interlock 4). | Python Analyzer Guild (`src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.Python`) | **Improve “used by entrypoint” and scope classification**: today `usedByEntrypoint` primarily comes from RECORD/script hints. Extend this by optionally mapping source-tree imports (`PythonImportAnalysis`) and/or runtime evidence (`PythonRuntimeEvidenceCollector`) to packages (via `TopLevelModules`) so “likely used” can be signaled deterministically (bounded, opt-in). Add `scope` metadata using `PythonScopeClassifier` (prod/dev/docs/build) based on lock sections and requirements file names. | +| 7 | SCAN-PY-405-007 | TODO | Parallel with tasks 1–6; fixtures first. | QA Guild (`src/Scanner/__Tests/StellaOps.Scanner.Analyzers.Lang.Python.Tests`) | **Fixtures + golden outputs**: add fixtures proving new detection paths: (a) conda env (`conda-meta/*.json`) without dist-info, (b) requirements with `-r` includes + `-e .` editable, (c) Pipfile.lock with `default` + `develop`, (d) wheel file in workspace (no extraction), (e) zipapp/pyz with embedded requirements, (f) container layers with whiteouts hiding a dist-info dir, (g) vendored dependency directory under a package. Extend `PythonLanguageAnalyzerTests.cs` to assert deterministic ordering, stable identities, and bounded metadata. | +| 8 | SCAN-PY-405-008 | TODO | After core behavior lands, update docs + perf guard. | Docs Guild + Bench Guild (`docs/modules/scanner`, `src/Bench/StellaOps.Bench/Scanner.Analyzers`) | **Document + benchmark Python analyzer contract**: update `docs/modules/scanner/architecture.md` (or add a Python analyzer sub-doc) describing detection sources & precedence, lock parsing rules, container overlay semantics, vendoring representation, and identity rules for non-versioned components. Add a deterministic offline bench scanning a representative fixture (many packages + lockfiles) and record baseline ceilings (time + components count). | + +## Wave Coordination +| Wave | Guild owners | Shared prerequisites | Status | Notes | +| --- | --- | --- | --- | --- | +| A: Discovery Backbone | Python Analyzer Guild + QA Guild | Actions 1–2 | TODO | Wire input normalization + package discovery; reduce false positives. | +| B: Lock Coverage | Python Analyzer Guild + QA Guild | Action 2 | TODO | Requirements/includes/editables + modern locks + Pipenv develop. | +| C: Containers & Vendoring | Python Analyzer Guild + QA Guild | Actions 3–4 | TODO | Whiteouts/overlay correctness + vendored packages surfaced. | +| D: Usage & Scope | Python Analyzer Guild + QA Guild | Interlock 4 | TODO | Improve “used by entrypoint” + scope classification (opt-in). | +| E: Docs & Bench | Docs Guild + Bench Guild | Waves A–D | TODO | Contract doc + offline benchmark. | + +## Wave Detail Snapshots +- **Wave A:** Layout-aware discovery (VFS + discovery) becomes the primary inventory path; deterministic precedence and bounded scans. +- **Wave B:** Lock parsing supports real-world formats (includes, editables, PEP 508) and emits declared-only components without silent drops. +- **Wave C:** Container overlay semantics prevent false positives; vendored deps become auditable inventory signals. +- **Wave D:** Optional, deterministic “used likely” signals and package scopes reduce noise and improve reachability inputs. +- **Wave E:** Documented contract + perf ceiling ensures the new logic stays stable. + +## Interlocks +- **Identity & collisions:** Components without reliable versions (vendored/local/zipapp/project) must use `AddFromExplicitKey` with a stable, non-colliding key scheme. (Action 1) +- **Lock precedence:** When multiple sources exist (requirements + Pipfile.lock + poetry.lock + pyproject), precedence must be explicit and deterministic (Action 2). +- **Container overlay correctness:** If scanning raw layers, whiteouts must be honored; otherwise mark overlay as incomplete and avoid false inventory claims. (Action 3) +- **“Used-by-entrypoint” semantics:** Any import/runtime-based usage hints must be bounded, opt-in, and deterministic; avoid turning heuristic signals into hard truth. (Interlock 4) + +## Upcoming Checkpoints +- 2025-12-13: Approve identity scheme + lock precedence + container overlay expectations (Actions 1–3). +- 2025-12-16: Wave A complete with fixtures proving VFS-based discovery is stable and deterministic. +- 2025-12-18: Wave B complete with real-world requirements/includes/editables + Pipenv develop coverage. +- 2025-12-20: Wave C complete (whiteouts/overlay + vendoring) with bounded outputs. +- 2025-12-22: Wave D decision + implementation (if enabled) and Wave E docs/bench complete; sprint ready for DONE review. + +## Action Tracker +| # | Action | Owner | Due (UTC) | Status | Notes | +| --- | --- | --- | --- | --- | --- | +| 1 | Decide explicit-key identity scheme for non-versioned Python components (vendored/local/zipapp/project) and document it. | Project Mgmt + Scanner Guild | 2025-12-13 | Open | Must avoid collisions with `pkg:pypi/@` PURLs; prefer explicit-key when uncertain. | +| 2 | Decide lock/requirements precedence order + dedupe rules and document them as a contract. | Project Mgmt + Python Analyzer Guild | 2025-12-13 | Open | Must not depend on filesystem traversal order; include “unsupported line count” requirement. | +| 3 | Decide container overlay handling contract for raw `layers/` inputs (whiteouts, ordering, “merged vs raw” expectations). | Project Mgmt + Scanner Guild | 2025-12-13 | Open | If upstream provides merged rootfs, clarify whether Python analyzer should still scan raw layers. | +| 4 | Decide how vendored deps are represented (separate embedded components vs parent-only metadata) and how to avoid false vuln matches. | Project Mgmt + Python Analyzer Guild | 2025-12-13 | Open | Prefer separate components only when identity/version is defensible; otherwise bounded metadata summary. | + +## Decisions & Risks +- **Decision (pending):** Identity scheme for non-versioned components, lock precedence, and container overlay expectations (Action Tracker 1–3). + +| Risk ID | Risk | Impact | Likelihood | Mitigation | Owner | Trigger / Signal | +| --- | --- | --- | --- | --- | --- | --- | +| R1 | Broader lock parsing introduces non-determinism (order/duplication) across platforms. | High | Medium | Stable sorting, explicit precedence, and golden fixtures for each format (incl. `-r` cycles). | Python Analyzer Guild | Flaky golden outputs; different results between Windows/Linux agents. | +| R2 | Container-layer scanning reports packages that are effectively deleted by whiteouts. | High | Medium | Implement/validate overlay semantics; add whiteout fixtures; mark overlayIncomplete when uncertain. | Scanner Guild | Inventory shows duplicates; reports packages not present in merged rootfs. | +| R3 | Vendored detection inflates inventory and causes false vulnerability correlation. | High | Medium | Prefer explicit-key or bounded metadata when version unknown; require defensive identity rules + docs. | Python Analyzer Guild | Sudden vuln-match spike on vendored-only signals. | +| R4 | Integrating VFS/discovery increases CPU/memory or scan time. | Medium | Medium | Bounds on scanning; benchmark; avoid full-tree recursion for patterns; reuse existing parsed results. | Bench Guild | Bench regression beyond agreed ceiling; timeouts in CI. | +| R5 | “Used-by-entrypoint” heuristics get misinterpreted as truth. | Medium | Low/Medium | Keep heuristic usage signals opt-in, clearly labeled, and bounded; document semantics. | Project Mgmt | Downstream policy relies on “used” incorrectly; unexpected risk decisions. | + +## Execution Log +| Date (UTC) | Update | Owner | +| --- | --- | --- | +| 2025-12-12 | Sprint created to close Python analyzer detection gaps (layout-aware discovery, lockfile expansion, container overlay correctness, vendoring signals, optional usage/scope improvements) with fixtures/bench/docs expectations. | Project Mgmt | + diff --git a/docs/implplan/SPRINT_0406_0001_0001_scanner_node_detection_gaps.md b/docs/implplan/SPRINT_0406_0001_0001_scanner_node_detection_gaps.md new file mode 100644 index 000000000..f9782734a --- /dev/null +++ b/docs/implplan/SPRINT_0406_0001_0001_scanner_node_detection_gaps.md @@ -0,0 +1,87 @@ +# Sprint 0406 · Scanner · Node Detection Gaps + +## Topic & Scope +- Close concrete detection gaps in the Node analyzer so scans reliably produce **complete, correct, deterministic** component inventories across npm/Yarn/PNPM, workspaces, PnP, tarballs, and container layer layouts. +- Ensure declared-only dependencies (lock/package.json) are represented **safely** (no invalid/over-confident PURLs from version ranges) and merged deterministically with installed/on-disk evidence. +- Improve lockfile fidelity for **multi-version** dependencies (common in Node) and modern lock formats (Yarn Berry, newer pnpm schemas) while staying offline-first. +- Produce evidence: new deterministic fixtures + golden outputs, plus an offline benchmark guarding performance regressions. +- **Working directory:** `src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.Node` (tests: `src/Scanner/__Tests/StellaOps.Scanner.Analyzers.Lang.Node.Tests`). + +## Dependencies & Concurrency +- Depends on shared component identity/evidence mechanisms: `src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang/Core/LanguageAnalyzerResult.cs:85`. +- Concurrency-safe with `SPRINT_0403_0001_0001_scanner_java_detection_gaps.md` and `SPRINT_0404_0001_0001_scanner_dotnet_detection_gaps.md` and `SPRINT_0405_0001_0001_scanner_python_detection_gaps.md` unless identity/locator conventions are standardized cross-analyzer (Action 1). + +## Documentation Prerequisites +- `docs/modules/scanner/architecture.md` +- `src/Scanner/AGENTS.md` +- `src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.Node/AGENTS.md` +- `src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang/AGENTS.md` + +## Delivery Tracker +| # | Task ID | Status | Key dependency / next step | Owners | Task Definition | +| --- | --- | --- | --- | --- | --- | +| 1 | SCAN-NODE-406-001 | TODO | Decide identity/declared-only scheme (Action 1). | Node Analyzer Guild (`src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.Node`) | **Emit declared-only components**: `NodeLockData.LoadAsync` already builds `DeclaredPackages` from lockfiles + `package.json`, but `NodeLanguageAnalyzer` never emits them. Add a deterministic “declared-only emission” pass that emits components for any `DeclaredPackages` entry not backed by on-disk inventory. Must include: `declaredOnly=true`, `declared.source` (`package.json|package-lock.json|yarn.lock|pnpm-lock.yaml`), `declared.locator` (stable), `declared.versionSpec` (original range/tag), `declared.scope` (prod/dev/peer/optional if known), and `declared.resolvedVersion` (only when lock provides concrete). **Critical:** do not emit `pkg:npm/...@` PURLs; use `AddFromExplicitKey` when version is not a concrete resolved version. | +| 2 | SCAN-NODE-406-002 | TODO | After Action 2, implement + fixtures. | Node Analyzer Guild (`src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.Node`) | **Multi-version lock correctness**: fix `NodeLockData` to support multiple versions per package name and match lock entries by `(name, resolvedVersion)` when the on-disk package.json has a concrete version. Add a `TryGet(relativePath, name, version)` overload (or equivalent) so lock metadata (`integrity`, `resolved`, `scope`) attaches to the correct package instance. Replace/augment `_byName` with a deterministic `(name@version)->entry` map for yarn/pnpm sources. | +| 3 | SCAN-NODE-406-003 | TODO | No external YAML libs; keep deterministic. | Node Analyzer Guild (`src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.Node`) | **Support Yarn Berry (v2/v3) lock format**: extend `NodeLockData.LoadYarnLock` to parse modern `yarn.lock` entries that use `resolution:` / `checksum:` / `linkType:` (and may not have `resolved`/`integrity`). Map `checksum` to an integrity-like field (metadata/evidence) and preserve the raw locator key as `lockLocator`. Ensure multiple versions of the same package are preserved (Task 2). Add fixtures covering Yarn v1 and Yarn v3 lock styles. | +| 4 | SCAN-NODE-406-004 | TODO | Align with Action 3 on “integrity missing” semantics. | Node Analyzer Guild (`src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.Node`) | **Harden pnpm lock parsing**: extend `LoadPnpmLock` to handle packages that have no `integrity` (workspace/file/link/git) without silently dropping them. Emit declared-only entries with `declared.resolvedVersion` (if known) and `lockIntegrityMissing=true` + reason. Add support for newer pnpm layouts (`snapshots:`) when present, while keeping parsing bounded and deterministic. | +| 5 | SCAN-NODE-406-005 | TODO | After task 2, fix path name extraction and tests. | Node Analyzer Guild (`src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.Node`) | **Fix `package-lock.json` nested node_modules naming**: `ExtractNameFromPath` mis-identifies `node_modules/parent/node_modules/child` unless `name` is present. Update extraction to select the last package segment after the last `node_modules` (incl. scoped packages). Add tests that prove nested dependencies are keyed correctly and lock metadata is attached to the right on-disk package. | +| 6 | SCAN-NODE-406-006 | TODO | Decide workspace glob support (Action 2). | Node Analyzer Guild (`src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.Node`) | **Improve workspace discovery**: `NodeWorkspaceIndex` only supports patterns ending with `/*`. Extend it to support at least `**`-style patterns used in monorepos (e.g., `packages/**`, `apps/*`, `tools/*`). Ensure expansion is deterministic and safe (bounds on directory traversal; ignore `node_modules`). Add fixtures for multi-depth workspace patterns. | +| 7 | SCAN-NODE-406-007 | TODO | After task 6, add scope index for workspaces. | Node Analyzer Guild (`src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.Node`) | **Workspace-aware dependency scopes**: `NodeDependencyIndex` reads only root `package.json`. Extend scope classification to include workspace member manifests so `scope`/`riskLevel` metadata is correct for workspace packages. Must preserve precedence rules (root vs workspace vs lock) and be deterministic. | +| 8 | SCAN-NODE-406-008 | TODO | Requires Action 4 decision on import scanning bounds. | Node Analyzer Guild (`src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.Node`) | **Import scanning correctness + bounds**: `NodeImportWalker` uses `ParseScript` which misses ESM `import` syntax and fails on TS. Improve by attempting `ParseModule` when script parse fails, and add a bounded heuristic fallback for TS (`import ... from`, `export ... from`) when AST parsing fails. Also bound `AttachImports` so it does not recursively scan every file inside `node_modules` trees by default; restrict to source roots/workspace members and/or cap by file count and total bytes, emitting `importScanSkipped=true` + counters when capped. | +| 9 | SCAN-NODE-406-009 | TODO | After task 1, adopt consistent evidence hashing. | Node Analyzer Guild (`src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.Node`) | **Deterministic evidence hashing for on-disk `package.json`**: today tar/zip packages attach `PackageSha256`, but on-disk packages typically do not. Compute sha256 for `package.json` contents for installed packages (bounded: only package.json, not full dir) and attach to root evidence consistently. Do not hash large files; do not add unbounded IO. | +| 10 | SCAN-NODE-406-010 | TODO | Parallel with tasks 1–9; fixtures first. | QA Guild (`src/Scanner/__Tests/StellaOps.Scanner.Analyzers.Lang.Node.Tests`) | **Fixtures + golden outputs**: add/extend fixtures proving: (a) lock-only project (no node_modules) emits declared-only components, (b) Yarn v3 lock parses + multi-version packages preserved, (c) pnpm lock with workspace/link deps doesn’t silently drop, (d) package-lock nested node_modules naming is correct, (e) workspace glob patterns beyond `/*`, (f) container layout where app `package.json` is not at root (e.g., `/app/package.json` inside a layer root) still emits the app component, (g) ESM + TS import scanning captures imports (bounded) and emits deterministic evidence. Update `NodeLanguageAnalyzerTests.cs` and targeted unit tests (`NodeLockDataTests.cs`, `NodePackageCollectorTests.cs`) to assert deterministic ordering and identity rules. | +| 11 | SCAN-NODE-406-011 | TODO | After core behavior lands, update docs + perf guard. | Docs Guild + Bench Guild (`docs/modules/scanner`, `src/Bench/StellaOps.Bench/Scanner.Analyzers`) | **Document + benchmark Node analyzer contract**: document precedence (installed vs declared), identity rules for unresolved versions, Yarn/pnpm lock parsing guarantees/limits, workspace discovery rules, import scanning bounds/semantics, and container layout assumptions. Add a deterministic offline bench that scans a representative fixture (workspace + lock-only + import scan enabled) and records elapsed time + component counts (and file-scan counters) with a baseline ceiling. | + +## Wave Coordination +| Wave | Guild owners | Shared prerequisites | Status | Notes | +| --- | --- | --- | --- | --- | +| A: Declared-only & identity | Node Analyzer Guild + QA Guild | Action 1 | TODO | Emit declared-only safely; avoid invalid PURLs. | +| B: Lock fidelity | Node Analyzer Guild + QA Guild | None | TODO | Multi-version lock correctness + Yarn Berry + pnpm hardening + nested path fixes. | +| C: Workspaces & containers | Node Analyzer Guild + QA Guild | Action 2 | TODO | Workspace glob support + scope attribution + container app-root discovery. | +| D: Imports & evidence | Node Analyzer Guild + QA Guild | Action 4 | TODO | ESM/TS import correctness + bounded scanning + package.json hashing. | +| E: Docs & bench | Docs Guild + Bench Guild | Waves A–D | TODO | Contract + performance ceiling. | + +## Wave Detail Snapshots +- **Wave A:** Declared-only dependencies become visible and safely keyed (no range-as-version PURLs). +- **Wave B:** Lock metadata attaches to the correct package instance even with multiple versions; modern Yarn/pnpm formats handled deterministically. +- **Wave C:** Workspace membership discovery is robust for common monorepo patterns; scope metadata reflects workspace manifests; container app roots are not missed. +- **Wave D:** Import evidence captures ESM/TS and remains bounded; package.json evidence hashing becomes consistent. +- **Wave E:** Contract doc + offline bench prevent regressions. + +## Interlocks +- **Identity safety:** Never emit `pkg:npm/...@` or otherwise treat version ranges/tags as concrete versions (Action 1). +- **Lock precedence:** When multiple lock sources exist, define deterministic precedence for metadata attachment (e.g., package-lock by path > declared(name@version) > yarn/pnpm by name@version). (Action 3) +- **Workspace traversal bounds:** Workspace expansion must not crawl `node_modules` and must have explicit depth/file limits. (Action 2) +- **Import scanning bounds:** Do not recursively scan the entire filesystem (or dependency trees) without caps; skipped work must be explicit in metadata. (Action 4) + +## Upcoming Checkpoints +- 2025-12-13: Approve identity scheme + workspace glob bounds + import-scan bounds (Actions 1–2–4). +- 2025-12-16: Wave A complete (declared-only emission) with lock-only fixture. +- 2025-12-18: Wave B complete (multi-version locks + Yarn Berry + pnpm hardening + nested naming). +- 2025-12-20: Wave C complete (workspace globs + scope attribution + container app-root fixture). +- 2025-12-22: Wave D complete (ESM/TS imports + bounds + package.json hashing) and Wave E docs/bench done; sprint ready for DONE review. + +## Action Tracker +| # | Action | Owner | Due (UTC) | Status | Notes | +| --- | --- | --- | --- | --- | --- | +| 1 | Decide explicit-key identity scheme for declared-only Node deps (ranges/tags/git/file/workspace) and document it. | Project Mgmt + Scanner Guild | 2025-12-13 | Open | Must not collide with concrete `pkg:npm/...@` PURLs; must be stable across OS paths. | +| 2 | Decide workspace glob expansion rules (supported patterns, bounds, excluded dirs like `node_modules`). | Project Mgmt + Node Analyzer Guild | 2025-12-13 | Open | Keep deterministic and safe under untrusted inputs. | +| 3 | Decide lock metadata precedence when multiple sources exist and when lock lacks integrity/resolution. | Project Mgmt + Node Analyzer Guild | 2025-12-13 | Open | Must be explicit and test-covered; never depend on file traversal order. | +| 4 | Decide import-scanning policy: default enabled/disabled, scope (workspace only vs all packages), and caps to enforce. | Project Mgmt + Node Analyzer Guild | 2025-12-13 | Open | Must prevent runaway scans; skipped scans must be auditable. | + +## Decisions & Risks +- **Decision (pending):** Declared-only identity scheme, workspace glob bounds, lock precedence, and import scanning caps (Action Tracker 1–4). + +| Risk ID | Risk | Impact | Likelihood | Mitigation | Owner | Trigger / Signal | +| --- | --- | --- | --- | --- | --- | --- | +| R1 | Declared-only identity causes false vulnerability matches (ranges treated as versions). | High | Medium | Enforce explicit-key for non-concrete versions; document semantics; fixtures prove no `@^1.2.3` PURLs. | Node Analyzer Guild | Vuln-match spike on declared-only components; invalid PURL reports. | +| R2 | Multi-version dependencies get wrong integrity/resolution metadata. | Medium | High | Add `(name@version)` matching + fixtures with two versions of same package; deterministic merge rules. | Node Analyzer Guild | Mismatched integrity in evidence; inconsistent lockLocator attribution. | +| R3 | Yarn Berry/pnpm lock parsing breaks on format drift. | Medium | Medium | Keep parser tolerant and bounded; emit “unsupportedFields/lines” counters; add fixtures per lock version. | Node Analyzer Guild | Real projects show zero lock entries despite lockfile present. | +| R4 | Workspace glob expansion becomes a perf trap or scans unexpected dirs. | Medium | Medium | Explicit bounds + skip `node_modules` + depth caps; add tests for worst-case patterns. | Node Analyzer Guild | Bench regression; CI timeout; unexpected traversal of dependency trees. | +| R5 | Import scanning explodes runtime and output size. | High | Medium | Restrict scope + caps; emit `importScanSkipped` markers; benchmark and set ceiling. | Bench Guild | Time/memory regression; extremely large evidence arrays. | + +## Execution Log +| Date (UTC) | Update | Owner | +| --- | --- | --- | +| 2025-12-12 | Sprint created to close Node analyzer detection gaps (declared-only emission, multi-version lock fidelity, Yarn Berry/pnpm parsing, workspace glob support, import scanning correctness/bounds, deterministic evidence hashing) with fixtures/bench/docs expectations. | Project Mgmt | + diff --git a/docs/implplan/SPRINT_0407_0001_0001_scanner_bun_detection_gaps.md b/docs/implplan/SPRINT_0407_0001_0001_scanner_bun_detection_gaps.md new file mode 100644 index 000000000..439a01ce7 --- /dev/null +++ b/docs/implplan/SPRINT_0407_0001_0001_scanner_bun_detection_gaps.md @@ -0,0 +1,91 @@ +# Sprint 0407 - Scanner Bun Detection Gaps + +## Topic & Scope +- Close Bun inventory blind-spots so scans reliably inventory dependencies across **installed `node_modules`**, **lockfile-only**, **workspace layouts**, **patched dependencies**, and **container layer trees**. +- Improve correctness and safety: never emit invalid/confident `pkg:npm/...@` style identities; avoid leaking absolute paths; keep outputs deterministic with explicit bounds and audited “skipped” markers. +- Produce hard evidence: new fixtures + golden outputs covering bunfig-only projects, version-specific patches, container layer roots (`layers/`, `.layers/`, `layer*/`), and bun.lock v1 graph-based dev/prod classification. +- **Working directory:** `src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.Bun` (tests: `src/Scanner/__Tests/StellaOps.Scanner.Analyzers.Lang.Bun.Tests`; optional benches: `src/Bench/StellaOps.Bench/Scanner.Analyzers`). + +## Dependencies & Concurrency +- Interlocks with Node analyzer conventions for container root discovery and identity safety: + - `src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.Node/Internal/NodeInputNormalizer.cs` + - `src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.Node/Internal/NodeLockData.cs` +- Must remain parallel-safe with other language analyzers: no shared mutable global state; deterministic iteration over filesystem and lock entries. +- Offline-first: do not run `bun`, do not fetch registries, do not assume network. + +## Documentation Prerequisites +- `docs/README.md` +- `docs/07_HIGH_LEVEL_ARCHITECTURE.md` +- `docs/modules/platform/architecture-overview.md` +- `docs/modules/scanner/architecture.md` +- `src/Scanner/AGENTS.md` +- `src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang/AGENTS.md` +- **Missing today (must be created before tasks flip to DOING):** `src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.Bun/AGENTS.md` + +## Delivery Tracker +| # | Task ID | Status | Key dependency / next step | Owners | Task Definition | +| --- | --- | --- | --- | --- | --- | +| 1 | SCAN-BUN-407-001 | TODO | Decide container root discovery contract (Action 2). | Bun Analyzer Guild (`src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.Bun`) | **Container-layer aware project discovery**: extend `Internal/BunProjectDiscoverer.cs` to discover Bun project roots not only under `context.RootPath`, but also under common OCI unpack layouts used elsewhere in scanner: `layers/*`, `.layers/*`, and `layer*` direct children. Do not skip hidden roots wholesale: `.layers` must be included. Keep traversal bounded and deterministic: (a) stable ordering of enumerated directories, (b) explicit depth caps per root, (c) hard cap on total discovered roots, (d) must never recurse into `node_modules/` and must skip large/non-project dirs deterministically. Acceptance: new fixture `lang/bun/container-layers` proves a Bun project placed under `.layers/layer0/app` is found and scanned. | +| 2 | SCAN-BUN-407-002 | TODO | Decide identity rules for non-concrete versions (Action 1). | Bun Analyzer Guild (`src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.Bun`) | **Declared-only fallback for bun markers**: if `BunProjectDiscoverer` identifies a project root (via `bunfig.toml`/`package.json`/etc.) but `BunInputNormalizer` returns `None` (no `node_modules`, no `bun.lock`), emit declared-only components from `package.json` dependencies. Requirements: (a) do not emit `pkg:npm/...@` PURLs for version ranges/tags; use `AddFromExplicitKey` when version is not a concrete resolved version, (b) include deterministic metadata `declaredOnly=true`, `declared.source=package.json`, `declared.locator=#
`, `declared.versionSpec=`, `declared.scope=`, and (c) include root package.json evidence with sha256 (bounded). Acceptance: new fixture `lang/bun/bunfig-only` emits declared-only components for both `dependencies` and `devDependencies` with safe identities. | +| 3 | SCAN-BUN-407-003 | TODO | Decide dev/optional/peer semantics for bun.lock v1 (Action 3). | Bun Analyzer Guild (`src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.Bun`) | **bun.lock v1 graph enrichment (dev/optional/peer + edges)**: upgrade `Internal/BunLockParser.cs` to preserve dependency edges from bun.lock v1 array form (capture dependency value/specifier, not only names) and to parse optional peer information when present. Build a bounded dependency graph that starts from root `package.json` declarations (prod/dev/optional/peer) and propagates reachability to lock entries, marking `BunLockEntry.IsDev/IsOptional/IsPeer` deterministically. If the graph cannot disambiguate (multiple versions/specifier mismatch), do not guess; emit `scopeUnknown=true` and keep `IsDev=false` unless positively proven. Acceptance: add fixture `lang/bun/lockfile-dev-classification` demonstrating: (a) dev-only packages are tagged `dev=true` and are excluded when `includeDev=false`, (b) prod packages remain untagged, (c) the decision is stable across OS/filesystem ordering. | +| 4 | SCAN-BUN-407-004 | TODO | After task 3 lands, wire filter & metadata into emission. | Bun Analyzer Guild (`src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.Bun`) | **Make `includeDev` meaningful**: `Internal/BunLockInventory.cs` currently filters by `entry.IsDev`, but bun.lock array parsing sets `IsDev=false` always. After graph enrichment (Task 3), implement deterministic filtering for lockfile-only scans and ensure installed scans also carry dev/optional/peer metadata when lock data is present. Acceptance: tests show dev filtering affects output only when the analyzer can prove dev reachability; otherwise outputs remain but are marked `scopeUnknown=true`. | +| 5 | SCAN-BUN-407-005 | TODO | Decide patch-keying and path normalization (Action 4). | Bun Analyzer Guild (`src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.Bun`) | **Version-specific patch mapping + no absolute paths**: fix `Internal/BunWorkspaceHelper.cs` so `patchedDependencies` keys preserve version specificity (`name@version`), and patch-directory discovery emits **relative** deterministic paths (relative to project root) rather than absolute OS paths. Update `BunLanguageAnalyzer` patch application so it first matches `name@version`, then falls back to `name` only when unambiguous. Acceptance: add fixture `lang/bun/patched-multi-version` with two patch files for the same package name at different versions; output marks only the correct version as patched and never includes absolute paths. | +| 6 | SCAN-BUN-407-006 | TODO | Align locator conventions with Node analyzer (Action 2). | Bun Analyzer Guild (`src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.Bun`) | **Evidence strengthening + locator precision**: improve `Internal/BunPackage.CreateEvidence()` so evidence locators are stable and specific: (a) package.json evidence includes sha256 (bounded; if skipped, emit `packageJsonHashSkipped=true` with reason), (b) bun.lock evidence uses locator `bun.lock#packages/` (or another agreed deterministic locator format) instead of plain `bun.lock`, (c) optionally include lockfile sha256 once per project root in a synthetic “bun.lock evidence record” component or via repeated evidence with identical sha256 (bounded). Acceptance: update existing Bun fixtures’ goldens to reflect deterministic hashing and locator formats, with no nondeterministic absolute paths. | +| 7 | SCAN-BUN-407-007 | TODO | Decide identity rules for non-npm sources (Action 1). | Bun Analyzer Guild (`src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.Bun`) | **Identity safety for non-npm sources**: `Internal/BunPackage.BuildPurl()` always emits `pkg:npm/@`. Define and implement rules for `SourceType != npm` (git/file/link/workspace/tarball/custom-registry): when `version` is not a concrete registry version, emit `AddFromExplicitKey` (no PURL) and preserve the original specifier/resolved URL in metadata. If a PURL is emitted, it must be valid and must not embed raw specifiers like `workspace:*` as a “version”. Acceptance: add fixture `lang/bun/non-concrete-versions` demonstrating safe identities for `workspace:*` / `link:` / `file:` styles (if representable in bun.lock), with deterministic explicit keys and clear metadata markers. | +| 8 | SCAN-BUN-407-008 | TODO | After tasks 1–7, document analyzer contract. | Docs Guild + Bun Analyzer Guild | **Document Bun analyzer detection contract**: add/update `docs/modules/scanner/analyzers-bun.md` (or the closest existing scanner doc) describing: what artifacts are used (node_modules, bun.lock, package.json), precedence rules, identity rules (PURL vs explicit-key), dev/optional/peer semantics, container layer root handling, and bounds (depth/roots/files/hash limits). Link this sprint from the doc and add a brief “known limitations” section (e.g., bun.lockb unsupported). | +| 9 | SCAN-BUN-407-009 | TODO | Optional; only if perf regression risk materializes. | Bench Guild (`src/Bench/StellaOps.Bench/Scanner.Analyzers`) | **Offline benchmark**: add a deterministic bench that scans a representative Bun monorepo fixture (workspaces + many packages) and records elapsed time + component counts. Establish a ceiling and guard against regressions. | + +## Wave Coordination +| Wave | Guild owners | Shared prerequisites | Status | Notes | +| --- | --- | --- | --- | --- | +| A: Discovery & Declared-only | Bun Analyzer Guild + QA Guild | Actions 1–2 | TODO | Make projects discoverable and avoid “no output” cases. | +| B: Lock graph & scopes | Bun Analyzer Guild + QA Guild | Action 3 | TODO | Correct dev/optional/peer and make includeDev meaningful. | +| C: Patches & evidence | Bun Analyzer Guild + QA Guild | Action 4 | TODO | Version-specific patches; deterministic evidence/hashes. | +| D: Identity safety | Bun Analyzer Guild + Security Guild | Action 1 | TODO | Non-npm sources and non-concrete versions never become “fake versions”. | +| E: Docs & bench | Docs Guild + Bench Guild | Waves A–D | TODO | Contract and perf guardrails. | + +## Wave Detail Snapshots +- **Wave A:** Discover Bun projects under OCI layer layouts; declared-only emission when no install/lock evidence exists. +- **Wave B:** bun.lock v1 graph enrichment provides auditable dev/optional/peer classification and enables reliable dev filtering. +- **Wave C:** Patched dependency mapping is version-correct and deterministic; evidence locators/hashes become strong and stable. +- **Wave D:** Identity rules prevent invalid PURLs and reduce false vuln matches for non-registry packages. +- **Wave E:** Documented contract + optional benchmark keeps behavior stable over time. + +## Interlocks +- **Identity safety:** Never emit `pkg:npm/...@`; use explicit keys for non-concrete versions/specifiers. (Action 1) +- **Container traversal bounds:** Project discovery must not devolve into full-root recursion on container roots; bounds must be explicit and test-covered. (Action 2) +- **Scope correctness:** Dev/optional/peer flags must be derived deterministically (graph or explicit signals). When uncertain, mark unknown rather than guessing. (Action 3) +- **No path leakage:** Metadata/evidence must not include absolute host paths (patch file discovery is the primary risk). (Action 4) + +## Upcoming Checkpoints +- 2025-12-13: Approve identity scheme + container discovery contract + scope semantics + patch rules (Actions 1–4). +- 2025-12-16: Wave A complete with container-layers + bunfig-only fixtures passing. +- 2025-12-18: Wave B complete with dev/optional/peer classification fixture and includeDev filter tests. +- 2025-12-20: Wave C + D complete (patch mapping + evidence hashing + identity safety) with updated goldens. +- 2025-12-22: Wave E docs complete; bench decision made; sprint ready for DONE review. + +## Action Tracker +| # | Action | Owner | Due (UTC) | Status | Notes | +| --- | --- | --- | --- | --- | --- | +| 1 | Decide explicit-key identity scheme for Bun declared-only and non-npm sources (ranges/tags/git/file/link/workspace). | Project Mgmt + Scanner Guild | 2025-12-13 | Open | Must not collide with concrete `pkg:npm/...@` identities; must be stable across OS paths. | +| 2 | Decide and document container layer root discovery rules for Bun analyzer (parity with Node’s `layers/.layers/layer*` conventions, depth/roots bounds). | Project Mgmt + Bun Analyzer Guild | 2025-12-13 | Open | Must prevent runaway scans on untrusted rootfs layouts; must be fixture-tested. | +| 3 | Decide bun.lock v1 scope derivation rules (dev/optional/peer) and how uncertainty is represented (`scopeUnknown` markers). | Project Mgmt + Bun Analyzer Guild | 2025-12-13 | Open | Must be deterministic; avoid false “dev=false” claims when graph is ambiguous. | +| 4 | Decide patched dependency keying and deterministic path normalization (relative path base, name@version precedence, fallback rules). | Project Mgmt + Bun Analyzer Guild + Security Guild | 2025-12-13 | Open | Must avoid absolute path leakage; ensure correct version-specific patch attribution. | +| 5 | Create missing module charter: `src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.Bun/AGENTS.md`. | Project Mgmt | 2025-12-13 | Open | Required before implementation tasks can enter DOING per global charter. | + +## Decisions & Risks +- **Decision (pending):** identity scheme, container discovery, scope derivation, patch rules (Action Tracker 1–4). + +| Risk ID | Risk | Impact | Likelihood | Mitigation | Owner | Trigger / Signal | +| --- | --- | --- | --- | --- | --- | --- | +| R1 | Container root discovery causes perf regressions on large rootfs trees. | High | Medium | Explicit bounds + deterministic skipping; add container-layers fixture and (optional) benchmark. | Bun Analyzer Guild | CI timeouts; high CPU usage scanning container roots. | +| R2 | Dev/optional/peer classification is wrong or unstable due to ambiguous graph edges. | High | Medium | Prefer “unknown” markers over guesses; stabilize matching using dependency specifiers when available; fixture for ambiguity. | Bun Analyzer Guild | Flaky golden outputs; incorrect dev filtering reported by users. | +| R3 | Invalid PURLs or range-as-version identities cause false vulnerability matches. | High | Medium | Explicit-key for non-concrete versions; document semantics; add fixtures asserting absence of invalid `@^...` or `@workspace:*` PURLs. | Security Guild + Bun Analyzer Guild | Vuln-match spike; downstream consumers reject PURLs. | +| R4 | Absolute paths leak into metadata/evidence (patch discovery, symlink realpaths). | Medium | Medium | Normalize to project-relative paths; add fixture that fails if absolute paths appear. | Bun Analyzer Guild | Golden diffs include host-specific paths. | +| R5 | Evidence hashing increases runtime and memory usage. | Medium | Low/Medium | Hash only bounded files; cache per file path; record `hashSkipped` markers when exceeding size caps. | Bench Guild | Bench regression; memory spikes. | + +## Execution Log +| Date (UTC) | Update | Owner | +| --- | --- | --- | +| 2025-12-12 | Sprint created to close Bun analyzer detection gaps (container-layer discovery, declared-only fallback, bun.lock scope graph, version-specific patches, evidence hashing, identity safety) with fixtures/docs/bench expectations. | Project Mgmt | + diff --git a/docs/implplan/SPRINT_0408_0001_0001_scanner_language_detection_gaps_program.md b/docs/implplan/SPRINT_0408_0001_0001_scanner_language_detection_gaps_program.md new file mode 100644 index 000000000..8e722b704 --- /dev/null +++ b/docs/implplan/SPRINT_0408_0001_0001_scanner_language_detection_gaps_program.md @@ -0,0 +1,95 @@ +# Sprint 0408 - Scanner Language Detection Gaps (Implementation Program) + +## Topic & Scope +- Implement **all currently identified detection gaps** across the language analyzers: Java, .NET, Python, Node, Bun. +- Align cross-analyzer contracts where gaps overlap: **identity safety** (PURL vs explicit-key), **evidence locator precision**, **container layer/rootfs discovery**, and **no host-path leakage**. +- Produce hard evidence for each analyzer: deterministic fixtures + golden outputs, plus docs (and optional benches where perf risk exists). +- **Working directory:** `src/Scanner` (implementation occurs under `src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.*` and `src/Scanner/__Tests/*`; this sprint is the coordination source-of-truth spanning multiple analyzer folders). + +## Dependencies & Concurrency +- Language sprints (source-of-truth for per-analyzer detail): + - Java: `docs/implplan/SPRINT_0403_0001_0001_scanner_java_detection_gaps.md` + - .NET: `docs/implplan/SPRINT_0404_0001_0001_scanner_dotnet_detection_gaps.md` + - Python: `docs/implplan/SPRINT_0405_0001_0001_scanner_python_detection_gaps.md` + - Node: `docs/implplan/SPRINT_0406_0001_0001_scanner_node_detection_gaps.md` + - Bun: `docs/implplan/SPRINT_0407_0001_0001_scanner_bun_detection_gaps.md` +- Concurrency model: + - Language implementations may proceed in parallel once cross-analyzer “contract” decisions are frozen (Actions 1–3). + - Avoid shared mutable state changes across analyzers; keep deterministic ordering; do not introduce network fetches. + +## Documentation Prerequisites +- `docs/modules/scanner/architecture.md` +- `docs/07_HIGH_LEVEL_ARCHITECTURE.md` +- `src/Scanner/AGENTS.md` +- `src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang/AGENTS.md` +- Per-analyzer charters (must exist before implementation flips to DOING): + - Java: `src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.Java/AGENTS.md` + - .NET: `src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.DotNet/AGENTS.md` + - Python: `src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.Python/AGENTS.md` + - Node: `src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.Node/AGENTS.md` + - **Missing today:** `src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.Bun/AGENTS.md` (Action 4) + +## Delivery Tracker +| # | Task ID | Status | Key dependency / next step | Owners | Task Definition | +| --- | --- | --- | --- | --- | --- | +| 1 | SCAN-PROG-408-001 | TODO | Requires Action 1. | Scanner Guild + Security Guild + Export/UI/CLI Consumers | **Freeze cross-analyzer identity safety contract**: define a single, documented rule-set for when an analyzer emits (a) a concrete PURL and (b) an explicit-key component. Must cover: version ranges/tags, local paths, workspace/link/file deps, git deps, and “unknown” versions. Output: a canonical doc under `docs/modules/scanner/` (path chosen in Action 1) + per-analyzer unit tests asserting “no invalid PURLs” for declared-only / non-concrete inputs. | +| 2 | SCAN-PROG-408-002 | TODO | Requires Action 2. | Scanner Guild + Export/UI/CLI Consumers | **Freeze cross-analyzer evidence locator contract**: define deterministic locator formats for (a) lockfile entries, (b) nested artifacts (e.g., Java “outer!inner!path”), and (c) derived evidence records. Output: canonical doc + at least one golden fixture per analyzer asserting exact locator strings and bounded evidence sizes. | +| 3 | SCAN-PROG-408-003 | TODO | Requires Action 3. | Scanner Guild | **Freeze container layout discovery contract**: define which analyzers must discover projects under `layers/`, `.layers/`, and `layer*/` layouts, how ordering/whiteouts are handled (where applicable), and bounds (depth/roots/files). Output: canonical doc + fixtures proving parity for Node/Bun/Python (and any Java/.NET container behaviors where relevant). | +| 4 | SCAN-PROG-408-004 | TODO | None. | Project Mgmt + Scanner Guild | **Create missing Bun analyzer charter**: add `src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.Bun/AGENTS.md` synthesizing constraints from `docs/modules/scanner/architecture.md` and this sprint + `SPRINT_0407_0001_0001_scanner_bun_detection_gaps.md`. Must include: allowed directories, test strategy, determinism rules, identity/evidence conventions, and “no absolute paths” requirement. | +| 5 | SCAN-PROG-408-JAVA | TODO | Actions 1–2 recommended before emission format changes. | Java Analyzer Guild + QA Guild | **Implement all Java gaps** per `docs/implplan/SPRINT_0403_0001_0001_scanner_java_detection_gaps.md`: (a) embedded libs inside fat archives without extraction, (b) `pom.xml` fallback when properties missing, (c) multi-module Gradle lock discovery + deterministic precedence, (d) runtime image component emission from `release`, (e) replace JNI string scanning with bytecode-based JNI analysis. Acceptance: Java analyzer tests + new fixtures/goldens; bounded scanning with explicit skipped markers. | +| 6 | SCAN-PROG-408-DOTNET | TODO | Actions 1–2 recommended before adding declared-only identities. | .NET Analyzer Guild + QA Guild | **Implement all .NET gaps** per `docs/implplan/SPRINT_0404_0001_0001_scanner_dotnet_detection_gaps.md`: (a) declared-only fallback when no deps.json, (b) non-colliding identity for unresolved versions, (c) deterministic merge of declared vs installed packages, (d) bounded bundling signals, (e) optional declared edges provenance, (f) fixtures/docs (and optional bench). Acceptance: `.NET` analyzer emits components for source trees with lock/build files; no restore/MSBuild execution; deterministic outputs. | +| 7 | SCAN-PROG-408-PYTHON | TODO | Actions 1–3 recommended before overlay/identity changes. | Python Analyzer Guild + QA Guild | **Implement all Python gaps** per `docs/implplan/SPRINT_0405_0001_0001_scanner_python_detection_gaps.md`: (a) layout-aware discovery (avoid “any dist-info anywhere”), (b) expanded lock/requirements parsing (includes/editables/PEP508/direct refs), (c) correct container overlay/whiteout semantics (or explicit overlayIncomplete markers), (d) vendored dependency surfacing with safe identity rules, (e) optional used-by signals (bounded/opt-in), (f) fixtures/docs/bench. Acceptance: deterministic fixtures for lock formats and container overlays; no invalid “editable-as-version” PURLs per Action 1. | +| 8 | SCAN-PROG-408-NODE | TODO | Actions 1–3 recommended before declared-only emission + locators. | Node Analyzer Guild + QA Guild | **Implement all Node gaps** per `docs/implplan/SPRINT_0406_0001_0001_scanner_node_detection_gaps.md`: (a) emit declared-only components safely (no range-as-version PURLs), (b) multi-version lock fidelity `(name@version)` mapping, (c) Yarn Berry lock support, (d) pnpm schema hardening, (e) correct nested node_modules name extraction, (f) workspace glob bounds + container app-root detection parity, (g) bounded import evidence + consistent package.json hashing, (h) docs/bench. Acceptance: fixtures cover multi-version locks and Yarn v3; determinism tests prove stable ordering and locator strings. | +| 9 | SCAN-PROG-408-BUN | TODO | Actions 1–3 recommended before identity/scope changes. | Bun Analyzer Guild + QA Guild | **Implement all Bun gaps** per `docs/implplan/SPRINT_0407_0001_0001_scanner_bun_detection_gaps.md`: (a) discover projects under container layer layouts and do not skip `.layers`, (b) declared-only fallback for bunfig-only/no-lock/no-install, (c) bun.lock v1 graph-based dev/optional/peer classification and meaningful includeDev filtering, (d) version-specific patch mapping with relative paths only, (e) stronger evidence locators + bounded hashing, (f) identity safety for non-npm sources. Acceptance: new fixtures (`container-layers`, `bunfig-only`, `patched-multi-version`, dev-classification) + updated goldens; no absolute path leakage. | +| 10 | SCAN-PROG-408-INTEG-001 | TODO | After tasks 5–9 land. | QA Guild + Scanner Guild | **Integration determinism gate**: run the full language analyzer test matrix (Java/.NET/Python/Node/Bun) and add/adjust determinism tests so ordering, evidence locators, and identity rules remain stable. Any “skipped” work due to bounds must be explicit and deterministic (no silent drops). | +| 11 | SCAN-PROG-408-DOCS-001 | TODO | After Actions 1–3 are frozen. | Docs Guild + Scanner Guild | **Update scanner docs with final contracts**: link the per-language analyzer contract docs and this sprint from `docs/modules/scanner/architecture.md` (or the closest canonical scanner doc). Must include: identity rules, evidence locator rules, container layout handling, and bounded scanning policy. | + +## Wave Coordination +| Wave | Guild owners | Shared prerequisites | Status | Notes | +| --- | --- | --- | --- | --- | +| A: Contracts | Scanner Guild + Security Guild + Consumers | Actions 1–3 | TODO | Freeze identity/evidence/container contracts first to avoid rework. | +| B: Language Implementation | Analyzer Guilds + QA Guild | Wave A recommended | TODO | Java/.NET/Python/Node/Bun run in parallel once contracts are stable. | +| C: Integration & Docs | QA Guild + Docs Guild | Wave B | TODO | Determinism gates + contract documentation. | + +## Wave Detail Snapshots +- **Wave A:** Single cross-analyzer contract for identity, evidence locators, and container layout discovery (with tests). +- **Wave B:** Implement each analyzer sprint’s tasks with fixtures + deterministic goldens. +- **Wave C:** End-to-end test pass + documented analyzer promises and limitations. + +## Interlocks +- **No invalid PURLs:** declared-only/range/git/file/link/workspace deps must not become “fake versions”; explicit-key is required when version is not concrete. (Action 1) +- **Locator stability:** evidence locators are external-facing (export/UI/CLI); changes must be deliberate, documented, and golden-tested. (Action 2) +- **Container bounds:** layer-root discovery and overlay semantics must remain bounded and auditable (skipped markers) to stay safe on untrusted inputs. (Action 3) +- **No absolute paths:** metadata/evidence must be project-relative; no host path leakage (patch discovery and symlink realpaths are common pitfalls). + +## Upcoming Checkpoints +- 2025-12-13: Freeze Actions 1–3 (contracts) and Action 4 (Bun AGENTS). +- 2025-12-16: Java + .NET waves reach “fixtures passing” milestone. +- 2025-12-18: Python + Node waves reach “fixtures passing” milestone. +- 2025-12-20: Bun wave reaches “fixtures passing” milestone; all language sprints ready for integration run. +- 2025-12-22: Integration determinism gate + docs complete; sprint ready for DONE review. + +## Action Tracker +| # | Action | Owner | Due (UTC) | Status | Notes | +| --- | --- | --- | --- | --- | --- | +| 1 | Choose canonical doc path + define explicit-key identity recipe across analyzers. | Project Mgmt + Scanner Guild + Security Guild | 2025-12-13 | Open | Must prevent collisions with concrete PURLs; must be OS-path stable and deterministic. | +| 2 | Define evidence locator formats (lock entries, nested artifacts, derived evidence) and required hashing rules/bounds. | Project Mgmt + Scanner Guild + Export/UI/CLI Consumers | 2025-12-13 | Open | Must be parseable and stable; add golden fixtures asserting exact strings. | +| 3 | Define container layer/rootfs discovery + overlay semantics contract and bounds. | Project Mgmt + Scanner Guild | 2025-12-13 | Open | Align Node/Bun/Python; clarify when overlayIncomplete markers are required. | +| 4 | Create `src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.Bun/AGENTS.md` and link it from Bun sprint prerequisites. | Project Mgmt | 2025-12-13 | Open | Required before Bun implementation tasks can flip to DOING. | + +## Decisions & Risks +- **Decision (pending):** cross-analyzer identity/evidence/container contracts (Actions 1–3). + +| Risk ID | Risk | Impact | Likelihood | Mitigation | Owner | Trigger / Signal | +| --- | --- | --- | --- | --- | --- | --- | +| R1 | Identity mistakes cause false vulnerability matches. | High | Medium | Explicit-key for non-concrete versions; fixtures asserting no invalid PURLs; docs. | Security Guild + Scanner Guild | Vuln-match spike; PURL validation failures downstream. | +| R2 | Evidence locator churn breaks export/UI/CLI consumers. | High | Medium | Freeze locator formats up-front; golden fixtures; doc contract; version if needed. | Scanner Guild + Consumers | Consumer parse failures; UI rendering regressions. | +| R3 | Container scanning becomes a perf trap on untrusted roots. | High | Medium | Bounds (depth/roots/files/size); deterministic skipping markers; optional benches. | Scanner Guild + Bench Guild | CI timeouts; high CPU scans. | +| R4 | Non-determinism appears via filesystem order or parser tolerance. | Medium | Medium | Stable sorting; deterministic maps; golden fixtures on Windows/Linux. | QA Guild | Flaky tests; differing outputs across agents. | +| R5 | Absolute path leakage appears in metadata/evidence. | Medium | Medium | Enforce project-relative normalization; add tests that fail if absolute paths detected. | Scanner Guild | Golden diffs with host-specific paths. | + +## Execution Log +| Date (UTC) | Update | Owner | +| --- | --- | --- | +| 2025-12-12 | Program sprint created to coordinate implementation of all language analyzer detection gaps (Java/.NET/Python/Node/Bun) with shared contracts and acceptance evidence. | Project Mgmt | + diff --git a/docs/implplan/SPRINT_3410_0001_0001_mongodb_final_removal.md b/docs/implplan/SPRINT_3410_0001_0001_mongodb_final_removal.md index e7a20fa41..7027f8390 100644 --- a/docs/implplan/SPRINT_3410_0001_0001_mongodb_final_removal.md +++ b/docs/implplan/SPRINT_3410_0001_0001_mongodb_final_removal.md @@ -21,7 +21,7 @@ ### T10.1: Concelier Module (Highest Priority - ~80+ files) | # | Task ID | Status | Key dependency / next step | Owners | Task Definition | | --- | --- | --- | --- | --- | --- | -| 1 | MR-T10.1.1 | BLOCKED (2025-12-12) | Await Postgres/in-memory fixture to replace MongoIntegrationFixture; Concelier storage still Mongo-centric | Concelier Guild | Remove MongoDB imports from `Concelier.Testing/MongoIntegrationFixture.cs` - convert to Postgres fixture | +| 1 | MR-T10.1.1 | DOING (2025-12-12) | Replace MongoIntegrationFixture with Postgres fixture; remove global Mongo2Go/MongoDB.Driver test infra | Concelier Guild | Remove MongoDB imports from `Concelier.Testing/MongoIntegrationFixture.cs` - convert to Postgres fixture | | 2 | MR-T10.1.2 | BLOCKED (2025-12-12) | MR-T10.1.1 | Concelier Guild | Remove MongoDB from `Concelier.WebService.Tests` (~22 occurrences) | | 3 | MR-T10.1.3 | BLOCKED (2025-12-12) | MR-T10.1.1 | Concelier Guild | Remove MongoDB from all connector tests (~40+ test files) | | 4 | MR-T10.1.4 | BLOCKED (2025-12-12) | MR-T10.1.3 | Concelier Guild | Remove `Concelier.Models/MongoCompat/*.cs` shim files | @@ -210,6 +210,7 @@ Scanner.Storage now runs on PostgreSQL with migrations and DI wiring; MongoDB im ## Execution Log | Date (UTC) | Update | Owner | | --- | --- | --- | +| 2025-12-12 | Started MR-T10.1.1: converting Concelier test fixture infrastructure off Mongo2Go/MongoDB.* and onto Postgres/Testcontainers or in-memory. | Concelier Guild | | 2025-12-12 | Scanner.Storage Postgres migrations fixed (UTC defaults), migration runner now fails fast on errors, and Scanner.Storage.Tests pass with Docker-backed Postgres; supersedes earlier T10.4 blocked notes. | Scanner Guild | | 2025-12-12 | Scanner.Storage migrated to PostgreSQL: added schema/migrations, rewired DI, removed Mongo bootstrapper/shims; storage + web service tests updated (skipped when Docker unavailable). T10.4.x moved to DONE. | Scanner Guild | | 2025-12-12 | T10.4.x still blocked: PostgreSQL-backed `StellaOps.Scanner.Storage` schema/repos/DI not yet designed; in-memory Mongo stub only keeps build green and provides no durability. Need schema/adapter plan before continuing. | Scanner Guild | diff --git a/docs/implplan/tasks-all.md b/docs/implplan/tasks-all.md index 97fe2de5b..a29603cad 100644 --- a/docs/implplan/tasks-all.md +++ b/docs/implplan/tasks-all.md @@ -473,8 +473,8 @@ | CONSOLE-23-005 | TODO | | SPRINT_0212_0001_0001_web_i | Console Guild | src/Web/StellaOps.Web | Depends on #5 | Depends on #5 | CCSL0101 | | CONSOLE-OBS-52-001 | TODO | | SPRINT_303_docs_tasks_md_iii | Console Ops Guild | docs/modules/ui | Needs TLTY0101 metrics | Needs TLTY0101 metrics | CCSL0101 | | CONSOLE-OBS-52-002 | TODO | | SPRINT_303_docs_tasks_md_iii | Console Ops Guild | docs/modules/ui | Depends on #7 | Depends on #7 | CCSL0101 | -| CONSOLE-VEX-30-001 | BLOCKED (2025-12-04) | 2025-12-04 | SPRINT_0212_0001_0001_web_i | Console Guild + VEX Lens Guild | src/Web/StellaOps.Web | Provide `/console/vex/*` APIs streaming VEX statements, justification summaries, and advisory links with SSE refresh hooks. Dependencies: WEB-CONSOLE-23-001 (done 2025-11-28), EXCITITOR-CONSOLE-23-001 (done 2025-11-23); awaiting VEX Lens spec PLVL0103 and SSE envelope validation from Scheduler/Signals alignment. | Needs VEX Lens spec (PLVL0103) | CCSL0101 | -| CONSOLE-VULN-29-001 | BLOCKED (2025-12-04) | 2025-12-04 | SPRINT_0212_0001_0001_web_i | Console Guild | src/Web/StellaOps.Web | Build `/console/vuln/*` APIs and filters surfacing tenant-scoped findings with policy/VEX badges so Docs/UI teams can document workflows. Dependencies: WEB-CONSOLE-23-001 (done 2025-11-28); waiting on Concelier graph schema snapshot from 2025-12-03 freeze review. | Depends on CCWO0101 | CCSL0101 | +| CONSOLE-VEX-30-001 | DONE (2025-12-11) | 2025-12-11 | SPRINT_0212_0001_0001_web_i | Console Guild + VEX Lens Guild | src/Web/StellaOps.Web | Client/models for `/console/vex/*` workspace incl. `/console/vex/events` SSE streaming and deterministic schema validation. | | CCSL0101 | +| CONSOLE-VULN-29-001 | DONE (2025-12-11) | 2025-12-11 | SPRINT_0212_0001_0001_web_i | Console Guild | src/Web/StellaOps.Web | Client/models for `/console/vuln/*` workspace (findings, facets, detail, tickets) with deterministic filters and fixtures. | | CCSL0101 | | CONTAINERS-44-001 | DONE | 2025-11-18 | SPRINT_0212_0001_0001_web_i | BE-Base Platform Guild | src/Web/StellaOps.Web | Wait for DVCP0101 compose template | Wait for DVCP0101 compose template | COWB0101 | | CONTAINERS-45-001 | DONE | 2025-11-19 | SPRINT_0212_0001_0001_web_i | BE-Base Platform Guild | src/Web/StellaOps.Web | Depends on #1 | Depends on #1 | COWB0101 | | CONTAINERS-46-001 | DONE | 2025-11-19 | SPRINT_0212_0001_0001_web_i | BE-Base Platform Guild | src/Web/StellaOps.Web | Needs RBRE0101 hashes | Needs RBRE0101 hashes | COWB0101 | @@ -1191,8 +1191,8 @@ | MIRROR-CRT-58-002 | DOING | 2025-12-07 | SPRINT_0506_0001_0001_ops_devops_iv | Mirror Creator Guild + CLI Guild + Exporter Guild | src/Mirror/StellaOps.Mirror.Creator | MIRROR-CRT-56-001; EXPORT-OBS-54-001; CLI-AIRGAP-56-001 | MIRROR-CRT-56-001; EXPORT-OBS-54-001; CLI-AIRGAP-56-001; dev key: tools/cosign/cosign.dev.key (pw stellaops-dev); prod: MIRROR_SIGN_KEY_B64 | ATMI0101 | | MTLS-11-002 | DONE | 2025-11-08 | SPRINT_100_identity_signing | Authority Core & Security Guild | src/Authority/StellaOps.Authority | Refresh grants enforce original client cert, tokens persist `x5t#S256` metadata, docs updated. | AUTH-DPOP-11-001 | AUIN0102 | | NATIVE-401-015 | TODO | | SPRINT_0401_0001_0001_reachability_evidence_chain | Scanner Worker Guild | `src/Scanner/__Libraries/StellaOps.Scanner.Symbols.Native`, `src/Scanner/__Libraries/StellaOps.Scanner.CallGraph.Native` | Bootstrap Symbols.Native + CallGraph.Native scaffolding and coverage fixtures. | Needs replay requirements from DORR0101 | SCNA0101 | -| NOTIFY-38-001 | TODO | | SPRINT_0214_0001_0001_web_iii | BE-Base Platform Guild | src/Web/StellaOps.Web | Route approval/rule APIs through Web gateway with tenant scopes. | Wait for NOTY0103 approval payload schema | NOWB0101 | -| NOTIFY-39-001 | TODO | | SPRINT_0214_0001_0001_web_iii | BE-Base Platform Guild | src/Web/StellaOps.Web | Surface digest/simulation/quiet-hour controls in Web tier. | Needs correlation outputs from NOTY0105 | NOWB0101 | +| NOTIFY-38-001 | DONE (2025-12-11) | 2025-12-11 | SPRINT_0214_0001_0001_web_iii | BE-Base Platform Guild | src/Web/StellaOps.Web | Route approval/rule APIs through Web gateway with tenant scopes (superseded by WEB-NOTIFY-38-001). | Superseded by WEB-NOTIFY-38-001 (DONE 2025-12-11) | NOWB0101 | +| NOTIFY-39-001 | DONE (2025-12-11) | 2025-12-11 | SPRINT_0214_0001_0001_web_iii | BE-Base Platform Guild | src/Web/StellaOps.Web | Surface digest/simulation/quiet-hour controls in Web tier (superseded by WEB-NOTIFY-39-001). | Superseded by WEB-NOTIFY-39-001 (DONE 2025-12-11) | NOWB0101 | | NOTIFY-40-001 | TODO | | SPRINT_0202_0001_0002_cli_ii | DevEx/CLI Guild | src/Cli/StellaOps.Cli | Implement escalations + ack workflows, localization previews, and channel health checks. | NOTIFY-39-001 | NOWC0101 | | NOTIFY-AIRGAP-56-002 | DONE | | SPRINT_0170_0001_0001_notifications_telemetry | Notifications Service Guild + DevOps Guild | src/Notify/StellaOps.Notify | Ship AirGap-ready notifier bundles (Helm overlays, secrets templates, rollout guide). | MIRROR-CRT-56-001 | NOIA0101 | | NOTIFY-ATTEST-74-001 | DOING | | SPRINT_0170_0001_0001_notifications_telemetry | Notifications Service Guild + Attestor Service Guild | src/Notify/StellaOps.Notify | Create attestor-driven notification templates + schema docs; publish in `/docs/notifications/templates.md`. | ATEL0101 | NOIA0101 | @@ -1999,13 +1999,13 @@ | UI-POLICY-23-004 | DONE | 2025-12-05 | SPRINT_0210_0001_0002_ui_ii | UI Guild (src/Web/StellaOps.Web) | src/Web/StellaOps.Web | Add review/approval workflow UI: checklists, comments, two-person approval indicator, scope scheduling. Dependencies: UI-POLICY-23-003. | | | | UI-POLICY-23-005 | DONE | 2025-12-05 | SPRINT_0210_0001_0002_ui_ii | UI Guild (src/Web/StellaOps.Web) | src/Web/StellaOps.Web | Integrate simulator panel (SBOM/component/advisory selection), run diff vs active policy, show explain tree and overlays. Dependencies: UI-POLICY-23-004. | | | | UI-POLICY-23-006 | DONE | 2025-12-05 | SPRINT_0210_0001_0002_ui_ii | UI Guild (src/Web/StellaOps.Web) | src/Web/StellaOps.Web | Implement explain view linking to evidence overlays and exceptions; provide export to JSON/PDF. Dependencies: UI-POLICY-23-005. | | | -| UI-POLICY-27-001 | DOING | 2025-12-06 | SPRINT_0211_0001_0003_ui_iii | UI Guild, Product Ops (src/Web/StellaOps.Web) | src/Web/StellaOps.Web | Update Console policy workspace RBAC guards, scope requests, and user messaging to reflect the new Policy Studio roles/scopes (`policy:author/review/approve/operate/audit/simulate`), including Cypress auth stubs and help text. Dependencies: UI-POLICY-23-006. | | | +| UI-POLICY-27-001 | DONE (2025-12-12) | 2025-12-12 | SPRINT_0211_0001_0003_ui_iii | UI Guild, Product Ops (src/Web/StellaOps.Web) | src/Web/StellaOps.Web | Update Console policy workspace RBAC guards, scope requests, and user messaging to reflect the new Policy Studio roles/scopes (`policy:author/review/approve/operate/audit/simulate`), including Cypress auth stubs and help text. Dependencies: UI-POLICY-23-006. | | | | UI-POLICY-DET-01 | DONE | 2025-11-27 | SPRINT_0209_0001_0001_ui_i | UI Guild, Policy Guild (src/Web/StellaOps.Web) | src/Web/StellaOps.Web | Wire policy gate indicators + remediation hints into Release/Policy flows, blocking publishes when determinism checks fail; coordinate with Policy Engine schema updates. Dependencies: UI-SBOM-DET-01. | | | | UI-SBOM-DET-01 | DONE | 2025-11-27 | SPRINT_0209_0001_0001_ui_i | UI Guild (src/Web/StellaOps.Web) | src/Web/StellaOps.Web | Add a "Determinism" badge plus drill-down that surfaces fragment hashes, `_composition.json`, and Merkle root consistency when viewing scan details (per `docs/modules/scanner/deterministic-sbom-compose.md`). | | | -| UI-SIG-26-001 | BLOCKED | 2025-12-06 | SPRINT_0211_0001_0003_ui_iii | UI Guild, Signals Guild (src/Web/StellaOps.Web) | src/Web/StellaOps.Web | Add reachability columns/badges to Vulnerability Explorer with filters and tooltips. | | Blocked: deterministic reachability fixtures (columns/badges) not delivered by Signals/Graph. | -| UI-SIG-26-002 | BLOCKED | 2025-12-06 | SPRINT_0211_0001_0003_ui_iii | UI Guild (src/Web/StellaOps.Web) | src/Web/StellaOps.Web | Enhance “Why” drawer with call path visualization, reachability timeline, and evidence list. Dependencies: UI-SIG-26-001. | | Blocked pending UI-SIG-26-001 outputs and call-path/timeline fixtures. | -| UI-SIG-26-003 | BLOCKED | 2025-12-06 | SPRINT_0211_0001_0003_ui_iii | UI Guild (src/Web/StellaOps.Web) | src/Web/StellaOps.Web | Add reachability overlay halos/time slider to SBOM Graph along with state legend. Dependencies: UI-SIG-26-002. | | Blocked: overlays depend on upstream fixtures + perf budget. | -| UI-SIG-26-004 | BLOCKED | 2025-12-06 | SPRINT_0211_0001_0003_ui_iii | UI Guild (src/Web/StellaOps.Web) | src/Web/StellaOps.Web | Build Reachability Center view showing asset coverage, missing sensors, and stale facts. Dependencies: UI-SIG-26-003. | | Blocked: coverage/sensor fixtures not available; upstream chain blocked. | +| UI-SIG-26-001 | DONE (2025-12-12) | 2025-12-12 | SPRINT_0211_0001_0003_ui_iii | UI Guild, Signals Guild (src/Web/StellaOps.Web) | src/Web/StellaOps.Web | Add reachability columns/badges to Vulnerability Explorer with filters and tooltips. | | | +| UI-SIG-26-002 | DONE (2025-12-12) | 2025-12-12 | SPRINT_0211_0001_0003_ui_iii | UI Guild (src/Web/StellaOps.Web) | src/Web/StellaOps.Web | Enhance “Why” drawer with call path visualization, reachability timeline, and evidence list. Dependencies: UI-SIG-26-001. | | | +| UI-SIG-26-003 | DONE (2025-12-12) | 2025-12-12 | SPRINT_0211_0001_0003_ui_iii | UI Guild (src/Web/StellaOps.Web) | src/Web/StellaOps.Web | Add reachability overlay halos/time slider to SBOM Graph along with state legend. Dependencies: UI-SIG-26-002. | | | +| UI-SIG-26-004 | DONE (2025-12-12) | 2025-12-12 | SPRINT_0211_0001_0003_ui_iii | UI Guild (src/Web/StellaOps.Web) | src/Web/StellaOps.Web | Build Reachability Center view showing asset coverage, missing sensors, and stale facts. Dependencies: UI-SIG-26-003. | | | | UNCERTAINTY-POLICY-401-026 | TODO | | SPRINT_0401_0001_0001_reachability_evidence_chain | Policy Guild + Concelier Guild (`docs/policy/dsl.md`, `docs/uncertainty/README.md`) | `docs/policy/dsl.md`, `docs/uncertainty/README.md` | Update policy guidance (Concelier/Excitors) with uncertainty gates (U1/U2/U3), sample YAML rules, and remediation actions. | | | | UNCERTAINTY-SCHEMA-401-024 | TODO | | SPRINT_0401_0001_0001_reachability_evidence_chain | Signals Guild (`src/Signals/StellaOps.Signals`, `docs/uncertainty/README.md`) | `src/Signals/StellaOps.Signals`, `docs/uncertainty/README.md` | Extend Signals findings with `uncertainty.states[]`, entropy fields, and `riskScore`; emit `FindingUncertaintyUpdated` events and persist evidence per docs. | | | | UNCERTAINTY-SCORER-401-025 | TODO | | SPRINT_0401_0001_0001_reachability_evidence_chain | Signals Guild (`src/Signals/StellaOps.Signals.Application`, `docs/uncertainty/README.md`) | `src/Signals/StellaOps.Signals.Application`, `docs/uncertainty/README.md` | Implement the entropy-aware risk scorer (`riskScore = base × reach × trust × (1 + entropyBoost)`) and wire it into finding writes. | | | @@ -2081,9 +2081,9 @@ | VULNERABILITY-EXPLORER-ENG-0001 | TODO | | SPRINT_334_docs_modules_vuln_explorer | Module Team (docs/modules/vuln-explorer) | docs/modules/vuln-explorer | Keep sprint alignment notes in sync with Vuln Explorer sprints. | | | | VULNERABILITY-EXPLORER-OPS-0001 | TODO | | SPRINT_334_docs_modules_vuln_explorer | Ops Guild (docs/modules/vuln-explorer) | docs/modules/vuln-explorer | Review runbooks/observability assets after next demo. | | | | WEB-20-002 | TODO | | SPRINT_0155_0001_0001_scheduler_i | Scheduler WebService Guild (src/Scheduler/StellaOps.Scheduler.WebService) | src/Scheduler/StellaOps.Scheduler.WebService | | | | -| WEB-AIAI-31-001 | TODO | | SPRINT_0212_0001_0001_web_i | BE-Base Platform Guild (src/Web/StellaOps.Web) | src/Web/StellaOps.Web | Route `/advisory/ai/*` endpoints through gateway with RBAC/ABAC, rate limits, and telemetry headers. | | | -| WEB-AIAI-31-002 | TODO | | SPRINT_0212_0001_0001_web_i | BE-Base Platform Guild (src/Web/StellaOps.Web) | src/Web/StellaOps.Web | Provide batching job handlers and streaming responses for CLI automation with retry/backoff. Dependencies: WEB-AIAI-31-001. | | | -| WEB-AIAI-31-003 | TODO | | SPRINT_0212_0001_0001_web_i | BE-Base Platform Guild, Observability Guild (src/Web/StellaOps.Web) | src/Web/StellaOps.Web | Emit metrics/logs (latency, guardrail blocks, validation failures) and forward anonymized prompt hashes to analytics. Dependencies: WEB-AIAI-31-002. | | | +| WEB-AIAI-31-001 | DONE (2025-12-12) | 2025-12-12 | SPRINT_0212_0001_0001_web_i | BE-Base Platform Guild (src/Web/StellaOps.Web) | src/Web/StellaOps.Web | Route `/advisory/ai/*` endpoints through gateway with RBAC/ABAC, rate limits, and telemetry headers. | | | +| WEB-AIAI-31-002 | DONE (2025-12-12) | 2025-12-12 | SPRINT_0212_0001_0001_web_i | BE-Base Platform Guild (src/Web/StellaOps.Web) | src/Web/StellaOps.Web | Provide batching job handlers and streaming responses for CLI automation with retry/backoff. Dependencies: WEB-AIAI-31-001. | | | +| WEB-AIAI-31-003 | DONE (2025-12-12) | 2025-12-12 | SPRINT_0212_0001_0001_web_i | BE-Base Platform Guild, Observability Guild (src/Web/StellaOps.Web) | src/Web/StellaOps.Web | Emit metrics/logs (latency, guardrail blocks, validation failures) and forward anonymized prompt hashes to analytics. Dependencies: WEB-AIAI-31-002. | | | | WEB-AIRGAP-56-001 | TODO | | SPRINT_116_concelier_v | Concelier WebService Guild (src/Concelier/StellaOps.Concelier.WebService) | src/Concelier/StellaOps.Concelier.WebService | | | | | WEB-AIRGAP-56-002 | TODO | | SPRINT_116_concelier_v | Concelier WebService Guild (src/Concelier/StellaOps.Concelier.WebService) | src/Concelier/StellaOps.Concelier.WebService | | | | | WEB-AIRGAP-57-001 | TODO | | SPRINT_116_concelier_v | Concelier WebService Guild, AirGap Policy Guild (src/Concelier/StellaOps.Concelier.WebService) | src/Concelier/StellaOps.Concelier.WebService | | | | @@ -2095,60 +2095,59 @@ | WEB-AOC-19-006 | TODO | 2025-11-08 | SPRINT_116_concelier_v | Concelier WebService Guild (src/Concelier/StellaOps.Concelier.WebService) | src/Concelier/StellaOps.Concelier.WebService | | | | | WEB-AOC-19-007 | TODO | 2025-11-08 | SPRINT_116_concelier_v | Concelier WebService Guild, QA Guild (src/Concelier/StellaOps.Concelier.WebService) | src/Concelier/StellaOps.Concelier.WebService | | | | | WEB-CONSOLE-23-001 | DONE (2025-11-28) | 2025-11-28 | SPRINT_0212_0001_0001_web_i | BE-Base Platform Guild + Product Analytics Guild | src/Web/StellaOps.Web | `/console/dashboard` and `/console/filters` aggregates shipped with tenant scoping, deterministic ordering, and 8 unit tests per sprint Execution Log 2025-11-28. | — | | -| WEB-CONSOLE-23-002 | DOING (2025-12-01) | 2025-12-01 | SPRINT_0212_0001_0001_web_i | BE-Base Platform Guild + Scheduler Guild | src/Web/StellaOps.Web | Implementing `/console/status` polling and `/console/runs/{id}/stream` SSE/WebSocket proxy with heartbeat/backoff; awaiting storage cleanup to run tests. Dependencies: WEB-CONSOLE-23-001. | WEB-CONSOLE-23-001 | | -| WEB-CONSOLE-23-003 | DOING | 2025-12-06 | SPRINT_0212_0001_0001_web_i | BE-Base Platform Guild, Policy Guild (src/Web/StellaOps.Web) | src/Web/StellaOps.Web | Add `/console/exports` POST/GET routes coordinating evidence bundle creation, streaming CSV/JSON exports, checksum manifest retrieval, and signed attestation references. Ensure requests honor tenant + policy scopes and expose job tracking metadata. Dependencies: WEB-CONSOLE-23-002. | | Client/models/store/service + unit specs passing (6/6) via Playwright Chromium headless (`CHROME_BIN=C:\Users\vlindos\AppData\Local\ms-playwright\chromium-1194\chrome-win\chrome.exe STELLAOPS_CHROMIUM_BIN=%CHROME_BIN% NG_PERSISTENT_BUILD_CACHE=1 node ./node_modules/@angular/cli/bin/ng.js test --watch=false --browsers=ChromeHeadlessOffline --progress=false --include src/app/core/api/console-export.client.spec.ts --include src/app/core/console/console-export.store.spec.ts --include src/app/core/console/console-export.service.spec.ts`). Contract still draft; backend wiring pending. | -| WEB-CONSOLE-23-004 | BLOCKED | 2025-12-06 | SPRINT_0212_0001_0001_web_i | BE-Base Platform Guild (src/Web/StellaOps.Web) | src/Web/StellaOps.Web | Implement `/console/search` endpoint accepting CVE/GHSA/PURL/SBOM identifiers, performing fan-out queries with caching, ranking, and deterministic tie-breaking. Return typed results for Console navigation; respect result caps and latency SLOs. Dependencies: WEB-CONSOLE-23-003. | | Still blocked pending contract; draft caching/ranking spec published in `docs/api/console/search-downloads.md` for review. | -| WEB-CONSOLE-23-005 | BLOCKED | 2025-12-06 | SPRINT_0212_0001_0001_web_i | BE-Base Platform Guild, DevOps Guild (src/Web/StellaOps.Web) | src/Web/StellaOps.Web | Serve `/console/downloads` JSON manifest (images, charts, offline bundles) sourced from signed registry metadata; include integrity hashes, release notes links, and offline instructions. Provide caching headers and documentation. Dependencies: WEB-CONSOLE-23-004. | | Still blocked pending contract; draft manifest example added at `docs/api/console/samples/console-download-manifest.json` (awaiting sign-off). | +| WEB-CONSOLE-23-002 | DONE (2025-12-04) | 2025-12-04 | SPRINT_0212_0001_0001_web_i | BE-Base Platform Guild + Scheduler Guild | src/Web/StellaOps.Web | Implementing `/console/status` polling and `/console/runs/{id}/stream` SSE/WebSocket proxy with heartbeat/backoff; awaiting storage cleanup to run tests. Dependencies: WEB-CONSOLE-23-001. | WEB-CONSOLE-23-001 | | +| WEB-CONSOLE-23-003 | DONE (2025-12-07) | 2025-12-07 | SPRINT_0212_0001_0001_web_i | BE-Base Platform Guild, Policy Guild (src/Web/StellaOps.Web) | src/Web/StellaOps.Web | Add `/console/exports` POST/GET routes coordinating evidence bundle creation, streaming CSV/JSON exports, checksum manifest retrieval, and signed attestation references. Ensure requests honor tenant + policy scopes and expose job tracking metadata. Dependencies: WEB-CONSOLE-23-002. | | Client/models/store/service + unit specs passing (6/6) via Playwright Chromium headless (`CHROME_BIN=C:\Users\vlindos\AppData\Local\ms-playwright\chromium-1194\chrome-win\chrome.exe STELLAOPS_CHROMIUM_BIN=%CHROME_BIN% NG_PERSISTENT_BUILD_CACHE=1 node ./node_modules/@angular/cli/bin/ng.js test --watch=false --browsers=ChromeHeadlessOffline --progress=false --include src/app/core/api/console-export.client.spec.ts --include src/app/core/console/console-export.store.spec.ts --include src/app/core/console/console-export.service.spec.ts`). Contract still draft; backend wiring pending. | +| WEB-CONSOLE-23-004 | DONE (2025-12-11) | 2025-12-11 | SPRINT_0212_0001_0001_web_i | BE-Base Platform Guild (src/Web/StellaOps.Web) | src/Web/StellaOps.Web | Implement `/console/search` endpoint accepting CVE/GHSA/PURL/SBOM identifiers, performing fan-out queries with caching, ranking, and deterministic tie-breaking. Return typed results for Console navigation; respect result caps and latency SLOs. Dependencies: WEB-CONSOLE-23-003. | | Still blocked pending contract; draft caching/ranking spec published in `docs/api/console/search-downloads.md` for review. | +| WEB-CONSOLE-23-005 | DONE (2025-12-11) | 2025-12-11 | SPRINT_0212_0001_0001_web_i | BE-Base Platform Guild, DevOps Guild (src/Web/StellaOps.Web) | src/Web/StellaOps.Web | Serve `/console/downloads` JSON manifest (images, charts, offline bundles) sourced from signed registry metadata; include integrity hashes, release notes links, and offline instructions. Provide caching headers and documentation. Dependencies: WEB-CONSOLE-23-004. | | Still blocked pending contract; draft manifest example added at `docs/api/console/samples/console-download-manifest.json` (awaiting sign-off). | | WEB-CONTAINERS-44-001 | DONE | 2025-11-18 | SPRINT_0212_0001_0001_web_i | BE-Base Platform Guild (src/Web/StellaOps.Web) | src/Web/StellaOps.Web | Expose `/welcome` state, config discovery endpoint (safe values), and `QUICKSTART_MODE` handling for Console banner; add `/health/liveness`, `/health/readiness`, `/version` if missing. | | | | WEB-CONTAINERS-45-001 | DONE | 2025-11-19 | SPRINT_0212_0001_0001_web_i | BE-Base Platform Guild (src/Web/StellaOps.Web) | src/Web/StellaOps.Web | Ensure readiness endpoints reflect DB/queue readiness, add feature flag toggles via config map, and document NetworkPolicy ports. Dependencies: WEB-CONTAINERS-44-001. | | | | WEB-CONTAINERS-46-001 | DONE | 2025-11-19 | SPRINT_0212_0001_0001_web_i | BE-Base Platform Guild (src/Web/StellaOps.Web) | src/Web/StellaOps.Web | Provide offline-friendly asset serving (no CDN), allow overriding object store endpoints via env, and document fallback behavior. Dependencies: WEB-CONTAINERS-45-001. | | | -| WEB-EXC-25-001 | BLOCKED | 2025-12-06 | SPRINT_0212_0001_0001_web_i | BE-Base Platform Guild (src/Web/StellaOps.Web) | src/Web/StellaOps.Web | Implement `/exceptions` API (create, propose, approve, revoke, list, history) with validation, pagination, and audit logging. | | Draft placeholder docs+sample added (`docs/api/console/exception-schema.md`, `docs/api/console/samples/exception-schema-sample.json`); awaiting official schema/scopes/audit rules. | -| WEB-EXC-25-002 | BLOCKED | 2025-11-30 | SPRINT_0213_0001_0002_web_ii | BE-Base Platform Guild (src/Web/StellaOps.Web) | src/Web/StellaOps.Web | Extend `/policy/effective` and `/policy/simulate` responses to include exception metadata and accept overrides for simulations. Dependencies: WEB-EXC-25-001. | | | -| WEB-EXC-25-003 | TODO | | SPRINT_0213_0001_0002_web_ii | BE-Base Platform Guild, Platform Events Guild (src/Web/StellaOps.Web) | src/Web/StellaOps.Web | Publish `exception.*` events, integrate with notification hooks, enforce rate limits. Dependencies: WEB-EXC-25-002. | | | -| WEB-EXPORT-35-001 | BLOCKED | 2025-12-07 | SPRINT_0213_0001_0002_web_ii | BE-Base Platform Guild (src/Web/StellaOps.Web) | src/Web/StellaOps.Web | Surface Export Center APIs (profiles/runs/download) through gateway with tenant scoping, streaming support, and viewer/operator scope checks. | Gateway contract draft v0.9 in docs/api/gateway/export-center.md; waiting guild sign-off | | -| WEB-EXPORT-36-001 | BLOCKED | 2025-12-07 | SPRINT_0213_0001_0002_web_ii | BE-Base Platform Guild (src/Web/StellaOps.Web) | src/Web/StellaOps.Web | Add distribution routes (OCI/object storage), manifest/provenance proxies, and signed URL generation. Dependencies: WEB-EXPORT-35-001. | Blocked by 35-001; distro signing/limits pending same contract | | -| WEB-EXPORT-37-001 | BLOCKED | 2025-12-07 | SPRINT_0213_0001_0002_web_ii | BE-Base Platform Guild (src/Web/StellaOps.Web) | src/Web/StellaOps.Web | Expose scheduling, retention, encryption parameters, and verification endpoints with admin scope enforcement and audit logs. Dependencies: WEB-EXPORT-36-001. | Blocked by 36-001; retention/encryption params not frozen | | -| WEB-GRAPH-21-001 | BLOCKED | 2025-10-27 | SPRINT_0213_0001_0002_web_ii | BE-Base Platform Guild, Graph Platform Guild (src/Web/StellaOps.Web) | src/Web/StellaOps.Web | Add gateway routes for graph versions/viewport/node/path/diff/export endpoints with tenant enforcement, scope checks, and streaming responses; proxy Policy Engine diff toggles without inline logic. Adopt `StellaOpsScopes` constants for RBAC enforcement. | | | -| WEB-GRAPH-21-002 | BLOCKED | 2025-10-27 | SPRINT_0213_0001_0002_web_ii | BE-Base Platform Guild (src/Web/StellaOps.Web) | src/Web/StellaOps.Web | Implement bbox/zoom/path parameter validation, pagination tokens, and deterministic ordering; add contract tests for boundary conditions. Dependencies: WEB-GRAPH-21-001. | | | -| WEB-GRAPH-21-003 | BLOCKED | 2025-10-27 | SPRINT_0213_0001_0002_web_ii | BE-Base Platform Guild, QA Guild (src/Web/StellaOps.Web) | src/Web/StellaOps.Web | Map graph service errors to `ERR_Graph_*`, support GraphML/JSONL export streaming, and document rate limits. Dependencies: WEB-GRAPH-21-002. | | | -| WEB-GRAPH-21-004 | BLOCKED | 2025-10-27 | SPRINT_0213_0001_0002_web_ii | BE-Base Platform Guild, Policy Guild (src/Web/StellaOps.Web) | src/Web/StellaOps.Web | Proxy Policy Engine overlay responses for graph endpoints while keeping gateway stateless; maintain streaming budgets and latency SLOs. Dependencies: WEB-GRAPH-21-003. | | | -| WEB-GRAPH-24-001 | TODO | | SPRINT_0213_0001_0002_web_ii | BE-Base Platform Guild (src/Web/StellaOps.Web) | src/Web/StellaOps.Web | Gateway proxy for Graph API and Policy overlays with RBAC, caching, pagination, ETags, and streaming; zero business logic. Dependencies: WEB-GRAPH-21-004. | | | -| WEB-GRAPH-24-002 | TODO | | SPRINT_0213_0001_0002_web_ii | BE-Base Platform Guild; SBOM Service Guild (src/Web/StellaOps.Web) | src/Web/StellaOps.Web | `/graph/assets/*` endpoints (snapshots, adjacency, search) with pagination, ETags, and tenant scoping as pure proxy. Dependencies: WEB-GRAPH-24-001. | | | -| WEB-GRAPH-24-003 | TODO | | SPRINT_0213_0001_0002_web_ii | BE-Base Platform Guild (src/Web/StellaOps.Web) | src/Web/StellaOps.Web | Embed AOC summaries from overlay services; gateway does not compute derived severity or hints. Dependencies: WEB-GRAPH-24-002. | | | -| WEB-GRAPH-24-004 | TODO | | SPRINT_0213_0001_0002_web_ii | BE-Base Platform Guild; Observability Guild (src/Web/StellaOps.Web) | src/Web/StellaOps.Web | Collect gateway metrics/logs (tile latency, proxy errors, overlay cache stats) and forward to dashboards; document sampling strategy. Dependencies: WEB-GRAPH-24-003. | | | -| WEB-LNM-21-001 | TODO | | SPRINT_0213_0001_0002_web_ii | BE-Base Platform Guild, Concelier WebService Guild (src/Web/StellaOps.Web) | src/Web/StellaOps.Web | Surface new `/advisories/*` APIs through gateway with caching, pagination, and RBAC enforcement (`advisory:read`). | | | -| WEB-LNM-21-002 | TODO | | SPRINT_0213_0001_0002_web_ii | BE-Base Platform Guild, Excititor WebService Guild (src/Web/StellaOps.Web) | src/Web/StellaOps.Web | Expose `/vex/*` read APIs with evidence routes and export handlers; map `ERR_AGG_*` codes. Dependencies: WEB-LNM-21-001. | | | -| WEB-LNM-21-003 | TODO | | SPRINT_0214_0001_0001_web_iii | BE-Base Platform Guild, Policy Guild (src/Web/StellaOps.Web) | src/Web/StellaOps.Web | Provide combined endpoint for Console to fetch policy result + source evidence (advisory + VEX linksets) for a component. Dependencies: WEB-LNM-21-002. | | | -| WEB-NOTIFY-38-001 | TODO | | SPRINT_0214_0001_0001_web_iii | BE-Base Platform Guild | src/Web/StellaOps.Web | Route notifier APIs (`/notifications/*`) and WS feed through gateway with tenant scoping, viewer/operator scope enforcement, and SSE/WebSocket bridging. | Depends on #1 for signed ack spec | NOWB0101 | -| WEB-NOTIFY-39-001 | TODO | | SPRINT_0214_0001_0001_web_iii | BE-Base Platform Guild | src/Web/StellaOps.Web | Surface digest scheduling, quiet-hour/throttle management, and simulation APIs; ensure rate limits and audit logging. Dependencies: WEB-NOTIFY-38-001. | WEB-NOTIFY-38-001 | NOWB0101 | -| WEB-NOTIFY-40-001 | TODO | | SPRINT_0214_0001_0001_web_iii | BE-Base Platform Guild (src/Web/StellaOps.Web) | src/Web/StellaOps.Web | Expose escalation, localization, channel health, and ack verification endpoints with admin scope enforcement and signed token validation. Dependencies: WEB-NOTIFY-39-001. | | | -| WEB-OAS-61-001 | TODO | | SPRINT_0124_0001_0006_excititor_vi | Excititor WebService Guild (src/Excititor/StellaOps.Excititor.WebService) | src/Excititor/StellaOps.Excititor.WebService | | | | -| WEB-OAS-61-002 | TODO | | SPRINT_116_concelier_v | Concelier WebService Guild (src/Concelier/StellaOps.Concelier.WebService) | src/Concelier/StellaOps.Concelier.WebService | | | | -| WEB-OAS-62-001 | TODO | | SPRINT_116_concelier_v | Concelier WebService Guild (src/Concelier/StellaOps.Concelier.WebService) | src/Concelier/StellaOps.Concelier.WebService | | | | -| WEB-OAS-63-001 | TODO | | SPRINT_116_concelier_v | Concelier WebService Guild, API Governance Guild (src/Concelier/StellaOps.Concelier.WebService) | src/Concelier/StellaOps.Concelier.WebService | | | | -| WEB-OBS-50-001 | TODO | | SPRINT_0214_0001_0001_web_iii | BE-Base Platform Guild, Observability Guild (src/Web/StellaOps.Web) | src/Web/StellaOps.Web | Integrate `StellaOps.Telemetry.Core` into gateway host, replace ad-hoc logging, ensure all routes emit trace/span IDs, tenant context, and scrubbed payload previews. | | | -| WEB-OBS-51-001 | TODO | | SPRINT_116_concelier_v | Concelier WebService Guild | src/Concelier/StellaOps.Concelier.WebService | Paired with #1 for shared middleware | Paired with #1 for shared middleware | CNOB0102 | -| WEB-OBS-52-001 | TODO | | SPRINT_116_concelier_v | Concelier WebService Guild | src/Concelier/StellaOps.Concelier.WebService | Dependent on CLI/VEX readiness (035_CLCI0105) for payload format | Dependent on CLI/VEX readiness (035_CLCI0105) for payload format | CNOB0102 | -| WEB-OBS-53-001 | TODO | | SPRINT_117_concelier_vi | Concelier WebService Guild + Evidence Locker Guild | src/Concelier/StellaOps.Concelier.WebService | Needs Evidence Locker API spec from 002_ATEL0101 | Needs Evidence Locker API spec from 002_ATEL0101 | CNOB0102 | -| WEB-OBS-54-001 | TODO | | SPRINT_117_concelier_vi | Concelier WebService Guild | src/Concelier/StellaOps.Concelier.WebService | Relies on shared exporter (1039_EXPORT-OBS-54-001) | Relies on shared exporter (1039_EXPORT-OBS-54-001) | CNOB0102 | -| WEB-OBS-55-001 | TODO | | SPRINT_117_concelier_vi | Concelier WebService Guild + DevOps Guild | src/Concelier/StellaOps.Concelier.WebService | Wait for DevOps alert profiles (045_DVDO0103) | Wait for DevOps alert profiles (045_DVDO0103) | CNOB0102 | -| WEB-OBS-56-001 | TODO | | SPRINT_0214_0001_0001_web_iii | BE-Base Platform Guild, AirGap Guild (src/Web/StellaOps.Web) | src/Web/StellaOps.Web | Extend telemetry core integration to expose sealed/unsealed status APIs, drift metrics, and Console widgets without leaking sealed-mode secrets. Dependencies: WEB-OBS-55-001. | | | -| WEB-ORCH-32-001 | TODO | | SPRINT_0214_0001_0001_web_iii | BE-Base Platform Guild (src/Web/StellaOps.Web) | src/Web/StellaOps.Web | Expose `/orchestrator/sources | | | -| WEB-ORCH-33-001 | TODO | | SPRINT_0215_0001_0004_web_iv | BE-Base Platform Guild (src/Web/StellaOps.Web) | src/Web/StellaOps.Web | Add POST action routes (`pause. Dependencies: WEB-ORCH-32-001. | | | -| WEB-ORCH-34-001 | TODO | | SPRINT_0215_0001_0004_web_iv | BE-Base Platform Guild (src/Web/StellaOps.Web) | src/Web/StellaOps.Web | Surface quotas/backfill APIs, queue/backpressure metrics, and error clustering routes with admin scope enforcement and audit logging. Dependencies: WEB-ORCH-33-001. | | | -| WEB-POLICY-20-001 | TODO | | SPRINT_0215_0001_0004_web_iv | BE-Base Platform Guild, Policy Guild (src/Web/StellaOps.Web) | src/Web/StellaOps.Web | Implement Policy CRUD/compile/run/simulate/findings/explain endpoints with OpenAPI, tenant scoping, and service identity enforcement. | | | -| WEB-POLICY-20-002 | TODO | | SPRINT_0215_0001_0004_web_iv | BE-Base Platform Guild (src/Web/StellaOps.Web) | src/Web/StellaOps.Web | Add pagination, filtering, sorting, and tenant guards to listings for policies, runs, and findings; include deterministic ordering and query diagnostics. Dependencies: WEB-POLICY-20-001. | | | -| WEB-POLICY-20-003 | TODO | | SPRINT_0215_0001_0004_web_iv | BE-Base Platform Guild, QA Guild (src/Web/StellaOps.Web) | src/Web/StellaOps.Web | Map engine errors to `ERR_POL_*` responses with consistent payloads and contract tests; expose correlation IDs in headers. Dependencies: WEB-POLICY-20-002. | | | -| WEB-POLICY-20-004 | TODO | | SPRINT_0215_0001_0004_web_iv | Platform Reliability Guild (src/Web/StellaOps.Web) | src/Web/StellaOps.Web | Introduce adaptive rate limiting + quotas for simulation endpoints, expose metrics, and document retry headers. Dependencies: WEB-POLICY-20-003. | | | -| WEB-POLICY-23-001 | BLOCKED | 2025-10-29 | SPRINT_0215_0001_0004_web_iv | BE-Base Platform Guild, Policy Guild (src/Web/StellaOps.Web) | src/Web/StellaOps.Web | Implement API endpoints for creating/listing/fetching policy packs and revisions (`/policy/packs`, `/policy/packs/{id}/revisions`) with pagination, RBAC, and AOC metadata exposure. (Tracked via Sprint 18.5 gateway tasks.). Dependencies: WEB-POLICY-20-004. | | | -| WEB-POLICY-23-002 | BLOCKED | 2025-10-29 | SPRINT_0215_0001_0004_web_iv | BE-Base Platform Guild (src/Web/StellaOps.Web) | src/Web/StellaOps.Web | Add activation endpoint with scope windows, conflict checks, and optional 2-person approval integration; emit events on success. (Tracked via Sprint 18.5 gateway tasks.). Dependencies: WEB-POLICY-23-001. | | | -| WEB-POLICY-23-003 | TODO | | SPRINT_0215_0001_0004_web_iv | BE-Base Platform Guild (src/Web/StellaOps.Web) | src/Web/StellaOps.Web | Provide `/policy/simulate` and `/policy/evaluate` endpoints with streaming responses, rate limiting, and error mapping. Dependencies: WEB-POLICY-23-002. | | | -| WEB-POLICY-23-004 | TODO | | SPRINT_0215_0001_0004_web_iv | BE-Base Platform Guild (src/Web/StellaOps.Web) | src/Web/StellaOps.Web | Expose explain history endpoints (`/policy/runs`, `/policy/runs/{id}`) including decision tree, sources consulted, and AOC chain. Dependencies: WEB-POLICY-23-003. | | | -| WEB-POLICY-27-001 | TODO | | SPRINT_0215_0001_0004_web_iv | BE-Base Platform Guild | src/Web/StellaOps.Web | Surface Policy Registry APIs (`/policy/workspaces`, `/policy/versions`, `/policy/reviews`, `/policy/registry`) through gateway with tenant scoping, RBAC, and request validation; ensure streaming downloads for evidence bundles. Dependencies: WEB-POLICY-23-004. | Needs registry schema | | -| WEB-POLICY-27-002 | TODO | | SPRINT_0215_0001_0004_web_iv | BE-Base Platform Guild | src/Web/StellaOps.Web | Implement review lifecycle endpoints (open, comment, approve/reject) with audit headers, comment pagination, and webhook fan-out. Dependencies: WEB-POLICY-27-001. | Depends on 27-001 | | -| WEB-POLICY-27-003 | TODO | | SPRINT_0215_0001_0004_web_iv | Platform Reliability Guild | src/Web/StellaOps.Web | Expose quick/batch simulation endpoints with SSE progress (`/policy/simulations/{runId}/stream`), cursor-based result pagination, and manifest download routes. Dependencies: WEB-POLICY-27-002. | Needs 27-002 | | -| WEB-POLICY-27-004 | TODO | | SPRINT_0215_0001_0004_web_iv | BE/Security Guild | src/Web/StellaOps.Web | Add publish/sign/promote/rollback endpoints with idempotent request IDs, canary parameters, and environment bindings; enforce scope checks and emit structured events. Dependencies: WEB-POLICY-27-003. | Depends on 27-003 | | -| WEB-POLICY-27-005 | TODO | | SPRINT_0215_0001_0004_web_iv | BE/Observability Guild | src/Web/StellaOps.Web | Instrument metrics/logs for compile latency, simulation queue depth, approval latency, promotion actions; expose aggregated dashboards and correlation IDs for Console. Dependencies: WEB-POLICY-27-004. | Needs 27-004 metrics | | +| WEB-EXC-25-001 | DONE (2025-12-12) | 2025-12-12 | SPRINT_0212_0001_0001_web_i | BE-Base Platform Guild (src/Web/StellaOps.Web) | src/Web/StellaOps.Web | Implement `/exceptions` API (create, propose, approve, revoke, list, history) with validation, pagination, and audit logging. | | Interim contract + sample updated (`docs/api/console/exception-schema.md`, `docs/api/console/samples/exception-schema-sample.json`) and web client shipped with unit tests. | +| WEB-EXC-25-002 | DONE | 2025-12-12 | SPRINT_0213_0001_0002_web_ii | BE-Base Platform Guild (src/Web/StellaOps.Web) | src/Web/StellaOps.Web | Extend `/policy/effective` and `/policy/simulate` responses to include exception metadata and accept overrides for simulations. Dependencies: WEB-EXC-25-001. | | | +| WEB-EXC-25-003 | DONE | 2025-12-12 | SPRINT_0213_0001_0002_web_ii | BE-Base Platform Guild, Platform Events Guild (src/Web/StellaOps.Web) | src/Web/StellaOps.Web | Publish `exception.*` events, integrate with notification hooks, enforce rate limits. Dependencies: WEB-EXC-25-002. | | | +| WEB-EXPORT-35-001 | DONE | 2025-12-11 | SPRINT_0213_0001_0002_web_ii | BE-Base Platform Guild (src/Web/StellaOps.Web) | src/Web/StellaOps.Web | Surface Export Center APIs (profiles/runs/download) through gateway with tenant scoping, streaming support, and viewer/operator scope checks. | Gateway contract draft v0.9 in docs/api/gateway/export-center.md; waiting guild sign-off | | +| WEB-EXPORT-36-001 | DONE | 2025-12-11 | SPRINT_0213_0001_0002_web_ii | BE-Base Platform Guild (src/Web/StellaOps.Web) | src/Web/StellaOps.Web | Add distribution routes (OCI/object storage), manifest/provenance proxies, and signed URL generation. Dependencies: WEB-EXPORT-35-001. | Blocked by 35-001; distro signing/limits pending same contract | | +| WEB-EXPORT-37-001 | DONE | 2025-12-11 | SPRINT_0213_0001_0002_web_ii | BE-Base Platform Guild (src/Web/StellaOps.Web) | src/Web/StellaOps.Web | Expose scheduling, retention, encryption parameters, and verification endpoints with admin scope enforcement and audit logs. Dependencies: WEB-EXPORT-36-001. | Blocked by 36-001; retention/encryption params not frozen | | +| WEB-GRAPH-21-001 | DONE | 2025-12-11 | SPRINT_0213_0001_0002_web_ii | BE-Base Platform Guild, Graph Platform Guild (src/Web/StellaOps.Web) | src/Web/StellaOps.Web | Add gateway routes for graph versions/viewport/node/path/diff/export endpoints with tenant enforcement, scope checks, and streaming responses; proxy Policy Engine diff toggles without inline logic. Adopt `StellaOpsScopes` constants for RBAC enforcement. | | | +| WEB-GRAPH-21-002 | DONE | 2025-12-11 | SPRINT_0213_0001_0002_web_ii | BE-Base Platform Guild (src/Web/StellaOps.Web) | src/Web/StellaOps.Web | Implement bbox/zoom/path parameter validation, pagination tokens, and deterministic ordering; add contract tests for boundary conditions. Dependencies: WEB-GRAPH-21-001. | | | +| WEB-GRAPH-21-003 | DONE | 2025-12-11 | SPRINT_0213_0001_0002_web_ii | BE-Base Platform Guild, QA Guild (src/Web/StellaOps.Web) | src/Web/StellaOps.Web | Map graph service errors to `ERR_Graph_*`, support GraphML/JSONL export streaming, and document rate limits. Dependencies: WEB-GRAPH-21-002. | | | +| WEB-GRAPH-21-004 | DONE | 2025-12-11 | SPRINT_0213_0001_0002_web_ii | BE-Base Platform Guild, Policy Guild (src/Web/StellaOps.Web) | src/Web/StellaOps.Web | Proxy Policy Engine overlay responses for graph endpoints while keeping gateway stateless; maintain streaming budgets and latency SLOs. Dependencies: WEB-GRAPH-21-003. | | | +| WEB-GRAPH-24-001 | DONE | 2025-12-11 | SPRINT_0213_0001_0002_web_ii | BE-Base Platform Guild (src/Web.StellaOps.Web) | src/Web.StellaOps.Web | Gateway proxy for Graph API and Policy overlays with RBAC, caching, pagination, ETags, and streaming; zero business logic. Dependencies: WEB-GRAPH-21-004. | | | +| WEB-GRAPH-24-002 | DONE | 2025-12-11 | SPRINT_0213_0001_0002_web_ii | BE-Base Platform Guild; SBOM Service Guild (src/Web.StellaOps.Web) | src/Web.StellaOps.Web | `/graph/assets/*` endpoints (snapshots, adjacency, search) with pagination, ETags, and tenant scoping as pure proxy. Dependencies: WEB-GRAPH-24-001. | | | +| WEB-GRAPH-24-003 | DONE | 2025-12-11 | SPRINT_0213_0001_0002_web_ii | BE-Base Platform Guild (src/Web.StellaOps.Web) | src/Web.StellaOps.Web | Embed AOC summaries from overlay services; gateway does not compute derived severity or hints. Dependencies: WEB-GRAPH-24-002. | | | +| WEB-GRAPH-24-004 | DONE | 2025-12-11 | SPRINT_0213_0001_0002_web_ii | BE-Base Platform Guild; Observability Guild (src/Web.StellaOps.Web) | src/Web.StellaOps.Web | Collect gateway metrics/logs (tile latency, proxy errors, overlay cache stats) and forward to dashboards; document sampling strategy. Dependencies: WEB-GRAPH-24-003. | | | +| WEB-LNM-21-001 | DONE | 2025-12-12 | SPRINT_0213_0001_0002_web_ii | BE-Base Platform Guild, Concelier WebService Guild (src/Web/StellaOps.Web) | src/Web/StellaOps.Web | Surface new `/advisories/*` APIs through gateway with caching, pagination, and RBAC enforcement (`advisory:read`). | | | +| WEB-LNM-21-002 | DONE | 2025-12-12 | SPRINT_0213_0001_0002_web_ii | BE-Base Platform Guild, Excititor WebService Guild (src/Web/StellaOps.Web) | src/Web/StellaOps.Web | Expose `/vex/*` read APIs with evidence routes and export handlers; map `ERR_AGG_*` codes. Dependencies: WEB-LNM-21-001. | | | +| WEB-LNM-21-003 | DONE | 2025-12-12 | SPRINT_0214_0001_0001_web_iii | BE-Base Platform Guild, Policy Guild (src/Web/StellaOps.Web) | src/Web/StellaOps.Web | Provide combined endpoint for Console to fetch policy result + source evidence (advisory + VEX linksets) for a component. Dependencies: WEB-LNM-21-002. | WEB-LNM-21-002 | | +| WEB-NOTIFY-38-001 | DONE (2025-12-11) | 2025-12-11 | SPRINT_0214_0001_0001_web_iii | BE-Base Platform Guild (src/Web/StellaOps.Web) | src/Web/StellaOps.Web | Route notifier APIs (`/notifications/*`) and WS feed through gateway with tenant scoping, viewer/operator scope enforcement, and SSE/WebSocket bridging. | Depends on #1 for signed ack spec | NOWB0101 | +| WEB-NOTIFY-39-001 | DONE (2025-12-11) | 2025-12-11 | SPRINT_0214_0001_0001_web_iii | BE-Base Platform Guild (src/Web/StellaOps.Web) | src/Web/StellaOps.Web | Surface digest scheduling, quiet-hour/throttle management, and simulation APIs; ensure rate limits and audit logging. Dependencies: WEB-NOTIFY-38-001. | WEB-NOTIFY-38-001 | NOWB0101 | +| WEB-NOTIFY-40-001 | DONE (2025-12-11) | 2025-12-11 | SPRINT_0214_0001_0001_web_iii | BE-Base Platform Guild (src/Web/StellaOps.Web) | src/Web/StellaOps.Web | Expose escalation, localization, channel health, and ack verification endpoints with admin scope enforcement and signed token validation. Dependencies: WEB-NOTIFY-39-001. | WEB-NOTIFY-39-001 | | +| WEB-OAS-61-001 | DONE (2025-12-11) | 2025-12-11 | SPRINT_0214_0001_0001_web_iii | BE-Base Platform Guild (src/Web/StellaOps.Web) | src/Web/StellaOps.Web | Implement `GET /.well-known/openapi` returning gateway spec with version metadata, cache headers, and signed ETag. | | | +| WEB-OAS-61-002 | DONE (2025-12-11) | 2025-12-11 | SPRINT_0214_0001_0001_web_iii | BE-Base Platform Guild (src/Web/StellaOps.Web) | src/Web/StellaOps.Web | Standardize error envelope across gateway, update examples, and ensure telemetry logs include `error.code`. Dependencies: WEB-OAS-61-001. | WEB-OAS-61-001 | | +| WEB-OAS-62-001 | DONE (2025-12-11) | 2025-12-11 | SPRINT_0214_0001_0001_web_iii | BE-Base Platform Guild (src/Web/StellaOps.Web) | src/Web/StellaOps.Web | Align pagination/idempotency behaviors: normalize to cursor pagination, expose `Idempotency-Key` support, and document rate-limit headers. Dependencies: WEB-OAS-61-002. | WEB-OAS-61-002 | | +| WEB-OAS-63-001 | DONE (2025-12-11) | 2025-12-11 | SPRINT_0214_0001_0001_web_iii | BE-Base Platform Guild, API Governance Guild (src/Web/StellaOps.Web) | src/Web/StellaOps.Web | Implement gateway deprecation metadata: add deprecation headers, Sunset link emission, and observability metrics for deprecated routes. Dependencies: WEB-OAS-62-001. | WEB-OAS-62-001 | | +| WEB-OBS-50-001 | DONE (2025-12-11) | 2025-12-11 | SPRINT_0214_0001_0001_web_iii | BE-Base Platform Guild, Observability Guild (src/Web/StellaOps.Web) | src/Web/StellaOps.Web | Replace ad-hoc logging; ensure routes emit trace/span IDs, tenant context, and scrubbed payload previews. | | | +| WEB-OBS-51-001 | DONE (2025-12-11) | 2025-12-11 | SPRINT_0214_0001_0001_web_iii | BE-Base Platform Guild (src/Web/StellaOps.Web) | src/Web/StellaOps.Web | Implement `/obs/health` and `/obs/slo` aggregations pulling Prometheus/collector metrics with burn-rate signals and exemplar links for Console widgets. Dependencies: WEB-OBS-50-001. | WEB-OBS-50-001 | | +| WEB-OBS-52-001 | DONE (2025-12-11) | 2025-12-11 | SPRINT_0214_0001_0001_web_iii | BE-Base Platform Guild (src/Web/StellaOps.Web) | src/Web/StellaOps.Web | Deliver `/obs/trace/:id` and `/obs/logs` proxy endpoints with guardrails (time window limits, tenant scoping) forwarding to timeline indexer + log store with signed URLs. Dependencies: WEB-OBS-51-001. | WEB-OBS-51-001 | | +| WEB-OBS-54-001 | DONE (2025-12-11) | 2025-12-11 | SPRINT_0214_0001_0001_web_iii | BE-Base Platform Guild (src/Web/StellaOps.Web) | src/Web/StellaOps.Web | Provide `/evidence/*` and `/attestations/*` pass-through endpoints, enforce `timeline:read`, `evidence:read`, `attest:read` scopes, append provenance headers, and surface verification summaries. Dependencies: WEB-OBS-52-001. | WEB-OBS-52-001 | | +| WEB-OBS-55-001 | DONE (2025-12-11) | 2025-12-11 | SPRINT_0214_0001_0001_web_iii | BE-Base Platform Guild, Ops Guild (src/Web/StellaOps.Web) | src/Web/StellaOps.Web | Add `/obs/incident-mode` API (enable/disable/status) with audit trail, sampling override, retention bump preview, and CLI/Console hooks. Dependencies: WEB-OBS-54-001. | WEB-OBS-54-001 | | +| WEB-OBS-56-001 | DONE (2025-12-11) | 2025-12-11 | SPRINT_0214_0001_0001_web_iii | BE-Base Platform Guild, AirGap Guild (src/Web/StellaOps.Web) | src/Web/StellaOps.Web | Extend telemetry core integration to expose sealed/unsealed status APIs, drift metrics, and Console widgets without leaking sealed-mode secrets. Dependencies: WEB-OBS-55-001. | WEB-OBS-55-001 | | +| WEB-ORCH-32-001 | DONE | 2025-12-12 | SPRINT_0214_0001_0001_web_iii | BE-Base Platform Guild (src/Web/StellaOps.Web) | src/Web/StellaOps.Web | Expose read-only orchestrator APIs (e.g., `/orchestrator/sources`) via gateway with tenant scoping, caching headers, and rate limits. | | | +| WEB-ORCH-33-001 | DONE (2025-12-12) | 2025-12-12 | SPRINT_0215_0001_0001_web_iv | BE-Base Platform Guild (src/Web/StellaOps.Web) | src/Web/StellaOps.Web | Add POST action routes (pause/resume/backfill) for orchestrator-run control, honoring RBAC and audit logging. | WEB-ORCH-32-001 | | +| WEB-ORCH-34-001 | DONE (2025-12-12) | 2025-12-12 | SPRINT_0215_0001_0001_web_iv | BE-Base Platform Guild (src/Web/StellaOps.Web) | src/Web.StellaOps.Web | Surface quotas/backfill APIs, queue/backpressure metrics, and error clustering routes with admin scope enforcement and audit logging. | WEB-ORCH-33-001 | | +| WEB-POLICY-20-001 | DONE (2025-12-11) | 2025-12-11 | SPRINT_0215_0001_0001_web_iv | BE-Base Platform Guild, Policy Guild (src/Web/StellaOps.Web) | src/Web.StellaOps.Web | Implement Policy CRUD/compile/run/simulate/findings/explain endpoints with OpenAPI, tenant scoping, and service identity enforcement. | | | +| WEB-POLICY-20-002 | DONE (2025-12-11) | 2025-12-11 | SPRINT_0215_0001_0001_web_iv | BE-Base Platform Guild (src/Web.StellaOps.Web) | src/Web.StellaOps.Web | Add pagination, filtering, sorting, and tenant guards to listings for policies, runs, and findings; include deterministic ordering and query diagnostics. Dependencies: WEB-POLICY-20-001. | | | +| WEB-POLICY-20-003 | DONE (2025-12-11) | 2025-12-11 | SPRINT_0215_0001_0001_web_iv | BE-Base Platform Guild, QA Guild (src/Web/StellaOps.Web) | src/Web/StellaOps.Web | Map engine errors to `ERR_POL_*` responses with consistent payloads and contract tests; expose correlation IDs in headers. Dependencies: WEB-POLICY-20-002. | | | +| WEB-POLICY-20-004 | DONE (2025-12-11) | 2025-12-11 | SPRINT_0215_0001_0001_web_iv | Platform Reliability Guild (src/Web/StellaOps.Web) | src/Web.StellaOps.Web | Introduce adaptive rate limiting + quotas for simulation endpoints, expose metrics, and document retry headers. Dependencies: WEB-POLICY-20-003. | | | +| WEB-POLICY-23-001 | DONE (2025-12-11) | 2025-12-11 | SPRINT_0215_0001_0001_web_iv | BE-Base Platform Guild, Policy Guild (src/Web/StellaOps.Web) | src/Web.StellaOps.Web | Implement API endpoints for creating/listing/fetching policy packs and revisions (`/policy/packs`, `/policy/packs/{id}/revisions`) with pagination, RBAC, and AOC metadata exposure. (Tracked via Sprint 18.5 gateway tasks.). Dependencies: WEB-POLICY-20-004. | | | +| WEB-POLICY-23-002 | DONE (2025-12-11) | 2025-12-11 | SPRINT_0215_0001_0001_web_iv | BE-Base Platform Guild (src/Web.StellaOps.Web) | src/Web.StellaOps.Web | Add activation endpoint with scope windows, conflict checks, and optional 2-person approval integration; emit events on success. (Tracked via Sprint 18.5 gateway tasks.). Dependencies: WEB-POLICY-23-001. | | | +| WEB-POLICY-23-003 | DONE (2025-12-11) | 2025-12-11 | SPRINT_0215_0001_0001_web_iv | BE-Base Platform Guild (src/Web.StellaOps.Web) | src/Web.StellaOps.Web | Provide `/policy/simulate` and `/policy/evaluate` endpoints with streaming responses, rate limiting, and error mapping. Dependencies: WEB-POLICY-23-002. | | | +| WEB-POLICY-23-004 | DONE (2025-12-11) | 2025-12-11 | SPRINT_0215_0001_0001_web_iv | BE-Base Platform Guild (src/Web.StellaOps.Web) | src/Web.StellaOps.Web | Expose explain history endpoints (`/policy/runs`, `/policy/runs/{id}`) including decision tree, sources consulted, and AOC chain. Dependencies: WEB-POLICY-23-003. | | | +| WEB-POLICY-27-001 | DONE (2025-12-11) | 2025-12-11 | SPRINT_0215_0001_0001_web_iv | BE-Base Platform Guild | src/Web/StellaOps.Web | Surface Policy Registry APIs (`/policy/workspaces`, `/policy/versions`, `/policy/reviews`, `/policy/registry`) through gateway with tenant scoping, RBAC, and request validation; ensure streaming downloads for evidence bundles. Dependencies: WEB-POLICY-23-004. | | | +| WEB-POLICY-27-002 | DONE (2025-12-11) | 2025-12-11 | SPRINT_0215_0001_0001_web_iv | BE-Base Platform Guild | src/Web.StellaOps.Web | Implement review lifecycle endpoints (open, comment, approve/reject) with audit headers, comment pagination, and webhook fan-out. Dependencies: WEB-POLICY-27-001. | | | +| WEB-POLICY-27-003 | DONE (2025-12-11) | 2025-12-11 | SPRINT_0215_0001_0001_web_iv | Platform Reliability Guild | src/Web.StellaOps.Web | Expose quick/batch simulation endpoints with SSE progress (`/policy/simulations/{runId}/stream`), cursor-based result pagination, and manifest download routes. Dependencies: WEB-POLICY-27-002. | | | +| WEB-POLICY-27-004 | DONE (2025-12-11) | 2025-12-11 | SPRINT_0215_0001_0001_web_iv | BE/Security Guild | src/Web.StellaOps.Web | Add publish/sign/promote/rollback endpoints with idempotent request IDs, canary parameters, and environment bindings; enforce scope checks and emit structured events. Dependencies: WEB-POLICY-27-003. | | | +| WEB-POLICY-27-005 | DONE (2025-12-11) | 2025-12-11 | SPRINT_0215_0001_0001_web_iv | BE/Observability Guild | src/Web.StellaOps.Web | Instrument metrics/logs for compile latency, simulation queue depth, approval latency, promotion actions; expose aggregated dashboards and correlation IDs for Console. Dependencies: WEB-POLICY-27-004. | | | | WEB-RISK-66-001 | BLOCKED (2025-12-03) | | SPRINT_216_web_v | BE-Base Platform Guild, Policy Guild (src/Web/StellaOps.Web) | src/Web/StellaOps.Web | Expose risk profile/results endpoints through gateway with tenant scoping, pagination, and rate limiting. Blocked: npm ci hangs; cannot run Angular tests; awaiting stable install env/gateway endpoints. | | | | WEB-RISK-66-002 | BLOCKED | 2025-12-06 | SPRINT_216_web_v | BE-Base Platform Guild, Risk Engine Guild (src/Web/StellaOps.Web) | src/Web/StellaOps.Web | Add signed URL handling for explanation blobs and enforce scope checks. Dependencies: WEB-RISK-66-001. | | Blocked: upstream WEB-RISK-66-001 stalled (npm ci hangs; gateway endpoints unavailable). | | WEB-RISK-67-001 | BLOCKED | 2025-12-06 | SPRINT_216_web_v | BE-Base Platform Guild (src/Web/StellaOps.Web) | src/Web/StellaOps.Web | Provide aggregated risk stats (`/risk/status`) for Console dashboards (counts per severity, last computation). Dependencies: WEB-RISK-66-002. | | Blocked by WEB-RISK-66-002. | diff --git a/src/Concelier/Directory.Build.props b/src/Concelier/Directory.Build.props index fb9946313..7237431e3 100644 --- a/src/Concelier/Directory.Build.props +++ b/src/Concelier/Directory.Build.props @@ -2,34 +2,26 @@ true - + $(NoWarn);CS0105;CS1591;CS8601;CS8602;CS8604;CS0618;RS1032;RS2007;xUnit1041;xUnit1031;xUnit2013;NU1510;NETSDK1023;SYSLIB0057 - - - - - - - - - + + + + + - - - - - - diff --git a/src/Concelier/StellaOps.Concelier.Tests.Shared/ConcelierFixtureCollection.cs b/src/Concelier/StellaOps.Concelier.Tests.Shared/ConcelierFixtureCollection.cs new file mode 100644 index 000000000..374234b5d --- /dev/null +++ b/src/Concelier/StellaOps.Concelier.Tests.Shared/ConcelierFixtureCollection.cs @@ -0,0 +1,9 @@ +using Xunit; + +namespace StellaOps.Concelier.Testing; + +[CollectionDefinition(ConcelierFixtureCollection.Name, DisableParallelization = true)] +public sealed class ConcelierFixtureCollection : ICollectionFixture +{ + public const string Name = "ConcelierPostgres"; +} diff --git a/src/Concelier/StellaOps.Concelier.Tests.Shared/MongoFixtureCollection.cs b/src/Concelier/StellaOps.Concelier.Tests.Shared/MongoFixtureCollection.cs deleted file mode 100644 index d479aea23..000000000 --- a/src/Concelier/StellaOps.Concelier.Tests.Shared/MongoFixtureCollection.cs +++ /dev/null @@ -1,6 +0,0 @@ -using Xunit; - -namespace StellaOps.Concelier.Testing; - -[CollectionDefinition("mongo-fixture", DisableParallelization = true)] -public sealed class MongoFixtureCollection : ICollectionFixture; diff --git a/src/Concelier/StellaOps.Concelier.WebService/DualWrite/DualWriteAdvisoryStore.cs b/src/Concelier/StellaOps.Concelier.WebService/DualWrite/DualWriteAdvisoryStore.cs index 4abc6f4ba..0a6aecc04 100644 --- a/src/Concelier/StellaOps.Concelier.WebService/DualWrite/DualWriteAdvisoryStore.cs +++ b/src/Concelier/StellaOps.Concelier.WebService/DualWrite/DualWriteAdvisoryStore.cs @@ -1,6 +1,6 @@ using Microsoft.Extensions.Logging; using StellaOps.Concelier.Models; -using StellaOps.Concelier.Storage.Mongo.Advisories; +using StellaOps.Concelier.Storage.Advisories; using StellaOps.Concelier.Storage.Postgres.Advisories; namespace StellaOps.Concelier.WebService.DualWrite; diff --git a/src/Concelier/StellaOps.Concelier.WebService/Program.cs b/src/Concelier/StellaOps.Concelier.WebService/Program.cs index 9dc213a6c..de22c7992 100644 --- a/src/Concelier/StellaOps.Concelier.WebService/Program.cs +++ b/src/Concelier/StellaOps.Concelier.WebService/Program.cs @@ -63,8 +63,8 @@ using System.Diagnostics.Metrics; using StellaOps.Concelier.Models.Observations; using StellaOps.Aoc.AspNetCore.Results; using HttpResults = Microsoft.AspNetCore.Http.Results; -using StellaOps.Concelier.Storage.Mongo.Advisories; -using StellaOps.Concelier.Storage.Mongo.Aliases; +using StellaOps.Concelier.Storage.Advisories; +using StellaOps.Concelier.Storage.Aliases; using StellaOps.Provenance.Mongo; namespace StellaOps.Concelier.WebService diff --git a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Acsc/AcscConnector.cs b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Acsc/AcscConnector.cs index 789ea3ea9..17f9f7902 100644 --- a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Acsc/AcscConnector.cs +++ b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Acsc/AcscConnector.cs @@ -10,17 +10,17 @@ using System.Xml.Linq; using System.Text.Json; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using MongoDB.Bson; -using MongoDB.Bson.IO; +using StellaOps.Concelier.Bson; +using StellaOps.Concelier.Bson.IO; using StellaOps.Concelier.Connector.Acsc.Configuration; using StellaOps.Concelier.Connector.Acsc.Internal; using StellaOps.Concelier.Connector.Common.Fetch; using StellaOps.Concelier.Connector.Common.Html; using StellaOps.Concelier.Connector.Common; -using StellaOps.Concelier.Storage.Mongo; -using StellaOps.Concelier.Storage.Mongo; -using StellaOps.Concelier.Storage.Mongo; -using StellaOps.Concelier.Storage.Mongo.Advisories; +using StellaOps.Concelier.Storage; +using StellaOps.Concelier.Storage; +using StellaOps.Concelier.Storage; +using StellaOps.Concelier.Storage.Advisories; using StellaOps.Plugin; namespace StellaOps.Concelier.Connector.Acsc; @@ -554,7 +554,7 @@ public sealed class AcscConnector : IFeedConnector return _options.PreferRelayByDefault ? AcscEndpointPreference.Relay : AcscEndpointPreference.Direct; } - private async Task TryComputeLatestPublishedAsync(DocumentRecord document, CancellationToken cancellationToken) + private async Task TryComputeLatestPublishedAsync(Storage.Contracts.StorageDocument document, CancellationToken cancellationToken) { if (!document.PayloadId.HasValue) { diff --git a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Acsc/Internal/AcscCursor.cs b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Acsc/Internal/AcscCursor.cs index c01f1113b..9884c0303 100644 --- a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Acsc/Internal/AcscCursor.cs +++ b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Acsc/Internal/AcscCursor.cs @@ -1,4 +1,4 @@ -using MongoDB.Bson; +using StellaOps.Concelier.Bson; namespace StellaOps.Concelier.Connector.Acsc.Internal; diff --git a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Acsc/Internal/AcscDocumentMetadata.cs b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Acsc/Internal/AcscDocumentMetadata.cs index e17b79552..0cb854c07 100644 --- a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Acsc/Internal/AcscDocumentMetadata.cs +++ b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Acsc/Internal/AcscDocumentMetadata.cs @@ -1,4 +1,4 @@ -using StellaOps.Concelier.Storage.Mongo; +using StellaOps.Concelier.Storage; namespace StellaOps.Concelier.Connector.Acsc.Internal; diff --git a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Acsc/Internal/AcscMapper.cs b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Acsc/Internal/AcscMapper.cs index efef11969..7c999695f 100644 --- a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Acsc/Internal/AcscMapper.cs +++ b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Acsc/Internal/AcscMapper.cs @@ -2,8 +2,8 @@ using System.Security.Cryptography; using System.Text; using System.Text.RegularExpressions; using StellaOps.Concelier.Models; -using StellaOps.Concelier.Storage.Mongo; -using StellaOps.Concelier.Storage.Mongo; +using StellaOps.Concelier.Storage; +using StellaOps.Concelier.Storage; namespace StellaOps.Concelier.Connector.Acsc.Internal; diff --git a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Cccs/CccsConnector.cs b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Cccs/CccsConnector.cs index cdcde8aa8..0ff22282d 100644 --- a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Cccs/CccsConnector.cs +++ b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Cccs/CccsConnector.cs @@ -10,15 +10,15 @@ using System.Threading.Tasks; using System.Globalization; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using MongoDB.Bson; +using StellaOps.Concelier.Bson; using StellaOps.Concelier.Connector.Cccs.Configuration; using StellaOps.Concelier.Connector.Cccs.Internal; using StellaOps.Concelier.Connector.Common; using StellaOps.Concelier.Connector.Common.Fetch; -using StellaOps.Concelier.Storage.Mongo; -using StellaOps.Concelier.Storage.Mongo.Advisories; -using StellaOps.Concelier.Storage.Mongo; -using StellaOps.Concelier.Storage.Mongo; +using StellaOps.Concelier.Storage; +using StellaOps.Concelier.Storage.Advisories; +using StellaOps.Concelier.Storage; +using StellaOps.Concelier.Storage; using StellaOps.Plugin; namespace StellaOps.Concelier.Connector.Cccs; diff --git a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Cccs/Internal/CccsCursor.cs b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Cccs/Internal/CccsCursor.cs index e5e17211a..33995bc1a 100644 --- a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Cccs/Internal/CccsCursor.cs +++ b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Cccs/Internal/CccsCursor.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; -using MongoDB.Bson; +using StellaOps.Concelier.Bson; namespace StellaOps.Concelier.Connector.Cccs.Internal; diff --git a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Cccs/Internal/CccsMapper.cs b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Cccs/Internal/CccsMapper.cs index f2af698be..72464f0cd 100644 --- a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Cccs/Internal/CccsMapper.cs +++ b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Cccs/Internal/CccsMapper.cs @@ -3,7 +3,7 @@ using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; using StellaOps.Concelier.Models; -using StellaOps.Concelier.Storage.Mongo; +using StellaOps.Concelier.Storage; using StellaOps.Concelier.Normalization.SemVer; namespace StellaOps.Concelier.Connector.Cccs.Internal; diff --git a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.CertBund/CertBundConnector.cs b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.CertBund/CertBundConnector.cs index 4e77b7c17..464ce4d2b 100644 --- a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.CertBund/CertBundConnector.cs +++ b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.CertBund/CertBundConnector.cs @@ -6,16 +6,16 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using MongoDB.Bson; +using StellaOps.Concelier.Bson; using StellaOps.Concelier.Connector.CertBund.Configuration; using StellaOps.Concelier.Connector.CertBund.Internal; using StellaOps.Concelier.Connector.Common; using StellaOps.Concelier.Connector.Common.Fetch; using StellaOps.Concelier.Connector.Common.Html; -using StellaOps.Concelier.Storage.Mongo; -using StellaOps.Concelier.Storage.Mongo.Advisories; -using StellaOps.Concelier.Storage.Mongo; -using StellaOps.Concelier.Storage.Mongo; +using StellaOps.Concelier.Storage; +using StellaOps.Concelier.Storage.Advisories; +using StellaOps.Concelier.Storage; +using StellaOps.Concelier.Storage; using StellaOps.Plugin; namespace StellaOps.Concelier.Connector.CertBund; diff --git a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.CertBund/Internal/CertBundCursor.cs b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.CertBund/Internal/CertBundCursor.cs index 2bd39ad57..f0b79c5e9 100644 --- a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.CertBund/Internal/CertBundCursor.cs +++ b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.CertBund/Internal/CertBundCursor.cs @@ -1,6 +1,6 @@ using System; using System.Linq; -using MongoDB.Bson; +using StellaOps.Concelier.Bson; namespace StellaOps.Concelier.Connector.CertBund.Internal; diff --git a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.CertBund/Internal/CertBundMapper.cs b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.CertBund/Internal/CertBundMapper.cs index 67bff83c8..ac46fccd9 100644 --- a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.CertBund/Internal/CertBundMapper.cs +++ b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.CertBund/Internal/CertBundMapper.cs @@ -3,7 +3,7 @@ using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; using StellaOps.Concelier.Models; -using StellaOps.Concelier.Storage.Mongo; +using StellaOps.Concelier.Storage; using StellaOps.Concelier.Normalization.SemVer; namespace StellaOps.Concelier.Connector.CertBund.Internal; diff --git a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.CertCc/CertCcConnector.cs b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.CertCc/CertCcConnector.cs index e1143f0d4..c2dd9cd2a 100644 --- a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.CertCc/CertCcConnector.cs +++ b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.CertCc/CertCcConnector.cs @@ -9,16 +9,16 @@ using System.Text.Json.Serialization; using System.Threading; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using MongoDB.Bson; +using StellaOps.Concelier.Bson; using StellaOps.Concelier.Models; using StellaOps.Concelier.Connector.CertCc.Configuration; using StellaOps.Concelier.Connector.CertCc.Internal; using StellaOps.Concelier.Connector.Common; using StellaOps.Concelier.Connector.Common.Fetch; -using StellaOps.Concelier.Storage.Mongo; -using StellaOps.Concelier.Storage.Mongo.Advisories; -using StellaOps.Concelier.Storage.Mongo; -using StellaOps.Concelier.Storage.Mongo; +using StellaOps.Concelier.Storage; +using StellaOps.Concelier.Storage.Advisories; +using StellaOps.Concelier.Storage; +using StellaOps.Concelier.Storage; using StellaOps.Plugin; namespace StellaOps.Concelier.Connector.CertCc; @@ -338,7 +338,7 @@ public sealed class CertCcConnector : IFeedConnector var dto = CertCcNoteParser.Parse(noteBytes, vendorsBytes, vulsBytes, vendorStatusesBytes); var json = JsonSerializer.Serialize(dto, DtoSerializerOptions); - var payload = MongoDB.Bson.BsonDocument.Parse(json); + var payload = StellaOps.Concelier.Bson.BsonDocument.Parse(json); _diagnostics.ParseSuccess( dto.Vendors.Count, diff --git a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.CertCc/Internal/CertCcCursor.cs b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.CertCc/Internal/CertCcCursor.cs index 3ca3489c1..998b2f913 100644 --- a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.CertCc/Internal/CertCcCursor.cs +++ b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.CertCc/Internal/CertCcCursor.cs @@ -1,4 +1,4 @@ -using MongoDB.Bson; +using StellaOps.Concelier.Bson; using StellaOps.Concelier.Connector.Common.Cursors; namespace StellaOps.Concelier.Connector.CertCc.Internal; diff --git a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.CertCc/Internal/CertCcMapper.cs b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.CertCc/Internal/CertCcMapper.cs index db9f26956..af4c9de57 100644 --- a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.CertCc/Internal/CertCcMapper.cs +++ b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.CertCc/Internal/CertCcMapper.cs @@ -5,8 +5,8 @@ using System.Linq; using System.Net; using System.Text.RegularExpressions; using StellaOps.Concelier.Models; -using StellaOps.Concelier.Storage.Mongo; -using StellaOps.Concelier.Storage.Mongo; +using StellaOps.Concelier.Storage; +using StellaOps.Concelier.Storage; namespace StellaOps.Concelier.Connector.CertCc.Internal; diff --git a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.CertFr/CertFrConnector.cs b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.CertFr/CertFrConnector.cs index efd120a55..739b0dc80 100644 --- a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.CertFr/CertFrConnector.cs +++ b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.CertFr/CertFrConnector.cs @@ -4,15 +4,15 @@ using System.Linq; using System.Text.Json; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using MongoDB.Bson; +using StellaOps.Concelier.Bson; using StellaOps.Concelier.Connector.CertFr.Configuration; using StellaOps.Concelier.Connector.CertFr.Internal; using StellaOps.Concelier.Connector.Common; using StellaOps.Concelier.Connector.Common.Fetch; -using StellaOps.Concelier.Storage.Mongo; -using StellaOps.Concelier.Storage.Mongo.Advisories; -using StellaOps.Concelier.Storage.Mongo; -using StellaOps.Concelier.Storage.Mongo; +using StellaOps.Concelier.Storage; +using StellaOps.Concelier.Storage.Advisories; +using StellaOps.Concelier.Storage; +using StellaOps.Concelier.Storage; using StellaOps.Plugin; namespace StellaOps.Concelier.Connector.CertFr; diff --git a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.CertFr/Internal/CertFrCursor.cs b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.CertFr/Internal/CertFrCursor.cs index 28fae7c2d..f302de969 100644 --- a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.CertFr/Internal/CertFrCursor.cs +++ b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.CertFr/Internal/CertFrCursor.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; -using MongoDB.Bson; +using StellaOps.Concelier.Bson; namespace StellaOps.Concelier.Connector.CertFr.Internal; diff --git a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.CertFr/Internal/CertFrDocumentMetadata.cs b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.CertFr/Internal/CertFrDocumentMetadata.cs index c54dff2f3..b1c559072 100644 --- a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.CertFr/Internal/CertFrDocumentMetadata.cs +++ b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.CertFr/Internal/CertFrDocumentMetadata.cs @@ -1,6 +1,6 @@ using System; using System.Collections.Generic; -using StellaOps.Concelier.Storage.Mongo; +using StellaOps.Concelier.Storage; namespace StellaOps.Concelier.Connector.CertFr.Internal; diff --git a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.CertIn/CertInConnector.cs b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.CertIn/CertInConnector.cs index 2166e3a72..de01954b1 100644 --- a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.CertIn/CertInConnector.cs +++ b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.CertIn/CertInConnector.cs @@ -6,16 +6,16 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using MongoDB.Bson; +using StellaOps.Concelier.Bson; using StellaOps.Concelier.Models; using StellaOps.Concelier.Connector.CertIn.Configuration; using StellaOps.Concelier.Connector.CertIn.Internal; using StellaOps.Concelier.Connector.Common; using StellaOps.Concelier.Connector.Common.Fetch; -using StellaOps.Concelier.Storage.Mongo; -using StellaOps.Concelier.Storage.Mongo.Advisories; -using StellaOps.Concelier.Storage.Mongo; -using StellaOps.Concelier.Storage.Mongo; +using StellaOps.Concelier.Storage; +using StellaOps.Concelier.Storage.Advisories; +using StellaOps.Concelier.Storage; +using StellaOps.Concelier.Storage; using StellaOps.Plugin; namespace StellaOps.Concelier.Connector.CertIn; @@ -271,9 +271,9 @@ public sealed class CertInConnector : IFeedConnector continue; } - var dtoJson = dtoRecord.Payload.ToJson(new MongoDB.Bson.IO.JsonWriterSettings + var dtoJson = dtoRecord.Payload.ToJson(new StellaOps.Concelier.Bson.IO.JsonWriterSettings { - OutputMode = MongoDB.Bson.IO.JsonOutputMode.RelaxedExtendedJson, + OutputMode = StellaOps.Concelier.Bson.IO.JsonOutputMode.RelaxedExtendedJson, }); CertInAdvisoryDto dto; diff --git a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.CertIn/Internal/CertInCursor.cs b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.CertIn/Internal/CertInCursor.cs index 776d7fa65..a71f9ad5f 100644 --- a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.CertIn/Internal/CertInCursor.cs +++ b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.CertIn/Internal/CertInCursor.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; -using MongoDB.Bson; +using StellaOps.Concelier.Bson; namespace StellaOps.Concelier.Connector.CertIn.Internal; diff --git a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Common/Cursors/TimeWindowCursorState.cs b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Common/Cursors/TimeWindowCursorState.cs index e65b87153..dfe63181c 100644 --- a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Common/Cursors/TimeWindowCursorState.cs +++ b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Common/Cursors/TimeWindowCursorState.cs @@ -1,4 +1,4 @@ -using MongoDB.Bson; +using StellaOps.Concelier.Bson; namespace StellaOps.Concelier.Connector.Common.Cursors; diff --git a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Common/Fetch/RawDocumentStorage.cs b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Common/Fetch/RawDocumentStorage.cs index 91ed8de23..99b7726db 100644 --- a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Common/Fetch/RawDocumentStorage.cs +++ b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Common/Fetch/RawDocumentStorage.cs @@ -1,6 +1,6 @@ using System.Collections.Concurrent; using System.IO; -using StellaOps.Concelier.Storage.Mongo; +using StellaOps.Concelier.Storage; namespace StellaOps.Concelier.Connector.Common.Fetch; diff --git a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Common/Fetch/SourceFetchService.cs b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Common/Fetch/SourceFetchService.cs index 9ec9ea248..7619b6ce4 100644 --- a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Common/Fetch/SourceFetchService.cs +++ b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Common/Fetch/SourceFetchService.cs @@ -9,8 +9,8 @@ using System.Net.Http.Headers; using System.Text; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using MongoDB.Bson; -using MongoContracts = StellaOps.Concelier.Storage.Mongo; +using StellaOps.Concelier.Bson; +using MongoContracts = StellaOps.Concelier.Storage; using StorageContracts = StellaOps.Concelier.Storage.Contracts; using StellaOps.Concelier.Connector.Common.Http; using StellaOps.Concelier.Connector.Common.Telemetry; diff --git a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Common/Http/ServiceCollectionExtensions.cs b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Common/Http/ServiceCollectionExtensions.cs index 37059a6af..1024a400d 100644 --- a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Common/Http/ServiceCollectionExtensions.cs +++ b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Common/Http/ServiceCollectionExtensions.cs @@ -8,7 +8,7 @@ using Microsoft.Extensions.Options; using StellaOps.Concelier.Connector.Common.Xml; using StellaOps.Concelier.Core.Aoc; using StellaOps.Concelier.Core.Linksets; -using StellaOps.Concelier.Storage.Mongo; +using StellaOps.Concelier.Storage; namespace StellaOps.Concelier.Connector.Common.Http; diff --git a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Common/State/SourceStateSeedProcessor.cs b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Common/State/SourceStateSeedProcessor.cs index a0affcbc8..cf95cc25b 100644 --- a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Common/State/SourceStateSeedProcessor.cs +++ b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Common/State/SourceStateSeedProcessor.cs @@ -1,8 +1,8 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; -using MongoDB.Bson; +using StellaOps.Concelier.Bson; using StellaOps.Concelier.Connector.Common.Fetch; -using MongoContracts = StellaOps.Concelier.Storage.Mongo; +using MongoContracts = StellaOps.Concelier.Storage; using StellaOps.Cryptography; namespace StellaOps.Concelier.Connector.Common.State; diff --git a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Cve/CveConnector.cs b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Cve/CveConnector.cs index f42e82ac2..0e326ac5e 100644 --- a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Cve/CveConnector.cs +++ b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Cve/CveConnector.cs @@ -8,17 +8,17 @@ using System.Text.Json; using System.Security.Cryptography; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using MongoDB.Bson; +using StellaOps.Concelier.Bson; using StellaOps.Concelier.Models; using StellaOps.Concelier.Normalization.Text; using StellaOps.Concelier.Connector.Common; using StellaOps.Concelier.Connector.Common.Fetch; using StellaOps.Concelier.Connector.Cve.Configuration; using StellaOps.Concelier.Connector.Cve.Internal; -using StellaOps.Concelier.Storage.Mongo; -using StellaOps.Concelier.Storage.Mongo.Advisories; -using StellaOps.Concelier.Storage.Mongo; -using StellaOps.Concelier.Storage.Mongo; +using StellaOps.Concelier.Storage; +using StellaOps.Concelier.Storage.Advisories; +using StellaOps.Concelier.Storage; +using StellaOps.Concelier.Storage; using StellaOps.Plugin; namespace StellaOps.Concelier.Connector.Cve; diff --git a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Cve/Internal/CveCursor.cs b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Cve/Internal/CveCursor.cs index 8f247e863..ee1ef9894 100644 --- a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Cve/Internal/CveCursor.cs +++ b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Cve/Internal/CveCursor.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; using System.Linq; -using MongoDB.Bson; +using StellaOps.Concelier.Bson; namespace StellaOps.Concelier.Connector.Cve.Internal; diff --git a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Cve/Internal/CveMapper.cs b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Cve/Internal/CveMapper.cs index 094b4a2d4..5d9425b98 100644 --- a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Cve/Internal/CveMapper.cs +++ b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Cve/Internal/CveMapper.cs @@ -2,7 +2,7 @@ using System.Collections.Generic; using System.Linq; using StellaOps.Concelier.Models; using StellaOps.Concelier.Normalization.Cvss; -using StellaOps.Concelier.Storage.Mongo; +using StellaOps.Concelier.Storage; using NuGet.Versioning; namespace StellaOps.Concelier.Connector.Cve.Internal; diff --git a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Distro.Debian/DebianConnector.cs b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Distro.Debian/DebianConnector.cs index 24d2848a8..5d836df0c 100644 --- a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Distro.Debian/DebianConnector.cs +++ b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Distro.Debian/DebianConnector.cs @@ -7,17 +7,17 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using MongoDB.Bson; -using MongoDB.Bson.IO; +using StellaOps.Concelier.Bson; +using StellaOps.Concelier.Bson.IO; using StellaOps.Concelier.Models; using StellaOps.Concelier.Connector.Common; using StellaOps.Concelier.Connector.Common.Fetch; using StellaOps.Concelier.Connector.Distro.Debian.Configuration; using StellaOps.Concelier.Connector.Distro.Debian.Internal; -using StellaOps.Concelier.Storage.Mongo; -using StellaOps.Concelier.Storage.Mongo.Advisories; -using StellaOps.Concelier.Storage.Mongo; -using StellaOps.Concelier.Storage.Mongo; +using StellaOps.Concelier.Storage; +using StellaOps.Concelier.Storage.Advisories; +using StellaOps.Concelier.Storage; +using StellaOps.Concelier.Storage; using StellaOps.Plugin; namespace StellaOps.Concelier.Connector.Distro.Debian; diff --git a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Distro.Debian/Internal/DebianCursor.cs b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Distro.Debian/Internal/DebianCursor.cs index a8c77bb59..2284f302f 100644 --- a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Distro.Debian/Internal/DebianCursor.cs +++ b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Distro.Debian/Internal/DebianCursor.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; -using MongoDB.Bson; +using StellaOps.Concelier.Bson; namespace StellaOps.Concelier.Connector.Distro.Debian.Internal; diff --git a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Distro.Debian/Internal/DebianFetchCacheEntry.cs b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Distro.Debian/Internal/DebianFetchCacheEntry.cs index 3309c6ccf..6bb6f81eb 100644 --- a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Distro.Debian/Internal/DebianFetchCacheEntry.cs +++ b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Distro.Debian/Internal/DebianFetchCacheEntry.cs @@ -1,5 +1,5 @@ using System; -using MongoDB.Bson; +using StellaOps.Concelier.Bson; namespace StellaOps.Concelier.Connector.Distro.Debian.Internal; @@ -7,7 +7,7 @@ internal sealed record DebianFetchCacheEntry(string? ETag, DateTimeOffset? LastM { public static DebianFetchCacheEntry Empty { get; } = new(null, null); - public static DebianFetchCacheEntry FromDocument(StellaOps.Concelier.Storage.Mongo.DocumentRecord document) + public static DebianFetchCacheEntry FromDocument(StellaOps.Concelier.Storage.DocumentRecord document) => new(document.Etag, document.LastModified); public static DebianFetchCacheEntry FromBson(BsonDocument document) @@ -54,7 +54,7 @@ internal sealed record DebianFetchCacheEntry(string? ETag, DateTimeOffset? LastM return document; } - public bool Matches(StellaOps.Concelier.Storage.Mongo.DocumentRecord document) + public bool Matches(StellaOps.Concelier.Storage.DocumentRecord document) { if (document is null) { diff --git a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Distro.Debian/Internal/DebianMapper.cs b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Distro.Debian/Internal/DebianMapper.cs index 7b21aa546..d1513c67f 100644 --- a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Distro.Debian/Internal/DebianMapper.cs +++ b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Distro.Debian/Internal/DebianMapper.cs @@ -4,7 +4,7 @@ using System.Linq; using StellaOps.Concelier.Models; using StellaOps.Concelier.Normalization.Distro; using StellaOps.Concelier.Connector.Common; -using StellaOps.Concelier.Storage.Mongo; +using StellaOps.Concelier.Storage; namespace StellaOps.Concelier.Connector.Distro.Debian.Internal; diff --git a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Distro.RedHat/Internal/RedHatCursor.cs b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Distro.RedHat/Internal/RedHatCursor.cs index e8e107ff6..74f3d7666 100644 --- a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Distro.RedHat/Internal/RedHatCursor.cs +++ b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Distro.RedHat/Internal/RedHatCursor.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; -using MongoDB.Bson; +using StellaOps.Concelier.Bson; namespace StellaOps.Concelier.Connector.Distro.RedHat.Internal; diff --git a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Distro.RedHat/Internal/RedHatMapper.cs b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Distro.RedHat/Internal/RedHatMapper.cs index 84e1cb635..3925a8015 100644 --- a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Distro.RedHat/Internal/RedHatMapper.cs +++ b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Distro.RedHat/Internal/RedHatMapper.cs @@ -9,8 +9,8 @@ using StellaOps.Concelier.Normalization.Cvss; using StellaOps.Concelier.Normalization.Distro; using StellaOps.Concelier.Normalization.Identifiers; using StellaOps.Concelier.Normalization.Text; -using StellaOps.Concelier.Storage.Mongo; -using StellaOps.Concelier.Storage.Mongo; +using StellaOps.Concelier.Storage; +using StellaOps.Concelier.Storage; namespace StellaOps.Concelier.Connector.Distro.RedHat.Internal; diff --git a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Distro.RedHat/RedHatConnector.cs b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Distro.RedHat/RedHatConnector.cs index 25d01355a..8f0336d99 100644 --- a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Distro.RedHat/RedHatConnector.cs +++ b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Distro.RedHat/RedHatConnector.cs @@ -5,17 +5,17 @@ using System.Linq; using System.Text.Json; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using MongoDB.Bson; -using MongoDB.Bson.IO; +using StellaOps.Concelier.Bson; +using StellaOps.Concelier.Bson.IO; using StellaOps.Concelier.Models; using StellaOps.Concelier.Connector.Common; using StellaOps.Concelier.Connector.Common.Fetch; using StellaOps.Concelier.Connector.Distro.RedHat.Configuration; using StellaOps.Concelier.Connector.Distro.RedHat.Internal; -using StellaOps.Concelier.Storage.Mongo; -using StellaOps.Concelier.Storage.Mongo.Advisories; -using StellaOps.Concelier.Storage.Mongo; -using StellaOps.Concelier.Storage.Mongo; +using StellaOps.Concelier.Storage; +using StellaOps.Concelier.Storage.Advisories; +using StellaOps.Concelier.Storage; +using StellaOps.Concelier.Storage; using StellaOps.Plugin; namespace StellaOps.Concelier.Connector.Distro.RedHat; diff --git a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Distro.Suse/Internal/SuseCursor.cs b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Distro.Suse/Internal/SuseCursor.cs index b2e4acc8d..55ed673d9 100644 --- a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Distro.Suse/Internal/SuseCursor.cs +++ b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Distro.Suse/Internal/SuseCursor.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; -using MongoDB.Bson; +using StellaOps.Concelier.Bson; namespace StellaOps.Concelier.Connector.Distro.Suse.Internal; diff --git a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Distro.Suse/Internal/SuseFetchCacheEntry.cs b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Distro.Suse/Internal/SuseFetchCacheEntry.cs index 04a57ed9c..66a32d182 100644 --- a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Distro.Suse/Internal/SuseFetchCacheEntry.cs +++ b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Distro.Suse/Internal/SuseFetchCacheEntry.cs @@ -1,14 +1,19 @@ -using System; -using MongoDB.Bson; - -namespace StellaOps.Concelier.Connector.Distro.Suse.Internal; - -internal sealed record SuseFetchCacheEntry(string? ETag, DateTimeOffset? LastModified) -{ - public static SuseFetchCacheEntry Empty { get; } = new(null, null); - - public static SuseFetchCacheEntry FromDocument(StellaOps.Concelier.Storage.Mongo.DocumentRecord document) - => new(document.Etag, document.LastModified); +using System; +using StellaOps.Concelier.Bson; +using MongoContracts = StellaOps.Concelier.Storage; +using StorageContracts = StellaOps.Concelier.Storage.Contracts; + +namespace StellaOps.Concelier.Connector.Distro.Suse.Internal; + +internal sealed record SuseFetchCacheEntry(string? ETag, DateTimeOffset? LastModified) +{ + public static SuseFetchCacheEntry Empty { get; } = new(null, null); + + public static SuseFetchCacheEntry FromDocument(StorageContracts.StorageDocument document) + => new(document.Etag, document.LastModified); + + public static SuseFetchCacheEntry FromDocument(MongoContracts.DocumentRecord document) + => new(document.Etag, document.LastModified); public static SuseFetchCacheEntry FromBson(BsonDocument document) { @@ -54,12 +59,12 @@ internal sealed record SuseFetchCacheEntry(string? ETag, DateTimeOffset? LastMod return document; } - public bool Matches(StellaOps.Concelier.Storage.Mongo.DocumentRecord document) - { - if (document is null) - { - return false; - } + public bool Matches(StorageContracts.StorageDocument document) + { + if (document is null) + { + return false; + } if (!string.Equals(ETag, document.Etag, StringComparison.Ordinal)) { @@ -71,6 +76,26 @@ internal sealed record SuseFetchCacheEntry(string? ETag, DateTimeOffset? LastMod return LastModified.Value.UtcDateTime == document.LastModified.Value.UtcDateTime; } - return !LastModified.HasValue && !document.LastModified.HasValue; - } -} + return !LastModified.HasValue && !document.LastModified.HasValue; + } + + public bool Matches(MongoContracts.DocumentRecord document) + { + if (document is null) + { + return false; + } + + if (!string.Equals(ETag, document.Etag, StringComparison.Ordinal)) + { + return false; + } + + if (LastModified.HasValue && document.LastModified.HasValue) + { + return LastModified.Value.UtcDateTime == document.LastModified.Value.UtcDateTime; + } + + return !LastModified.HasValue && !document.LastModified.HasValue; + } +} diff --git a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Distro.Suse/Internal/SuseMapper.cs b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Distro.Suse/Internal/SuseMapper.cs index 9755bcee4..5fd1f2e82 100644 --- a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Distro.Suse/Internal/SuseMapper.cs +++ b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Distro.Suse/Internal/SuseMapper.cs @@ -4,7 +4,7 @@ using System.Globalization; using System.Linq; using StellaOps.Concelier.Models; using StellaOps.Concelier.Normalization.Distro; -using StellaOps.Concelier.Storage.Mongo; +using StellaOps.Concelier.Storage; namespace StellaOps.Concelier.Connector.Distro.Suse.Internal; diff --git a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Distro.Suse/SuseConnector.cs b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Distro.Suse/SuseConnector.cs index 6578140f0..3ed4067ab 100644 --- a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Distro.Suse/SuseConnector.cs +++ b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Distro.Suse/SuseConnector.cs @@ -9,17 +9,17 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using MongoDB.Bson; -using MongoDB.Bson.IO; +using StellaOps.Concelier.Bson; +using StellaOps.Concelier.Bson.IO; using StellaOps.Concelier.Models; using StellaOps.Concelier.Connector.Common; using StellaOps.Concelier.Connector.Common.Fetch; using StellaOps.Concelier.Connector.Distro.Suse.Configuration; using StellaOps.Concelier.Connector.Distro.Suse.Internal; -using StellaOps.Concelier.Storage.Mongo; -using StellaOps.Concelier.Storage.Mongo.Advisories; -using StellaOps.Concelier.Storage.Mongo; -using StellaOps.Concelier.Storage.Mongo; +using StellaOps.Concelier.Storage; +using StellaOps.Concelier.Storage.Advisories; +using StellaOps.Concelier.Storage; +using StellaOps.Concelier.Storage; using StellaOps.Plugin; namespace StellaOps.Concelier.Connector.Distro.Suse; diff --git a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Distro.Ubuntu/Internal/UbuntuCursor.cs b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Distro.Ubuntu/Internal/UbuntuCursor.cs index a1584df41..b11b0c250 100644 --- a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Distro.Ubuntu/Internal/UbuntuCursor.cs +++ b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Distro.Ubuntu/Internal/UbuntuCursor.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; -using MongoDB.Bson; +using StellaOps.Concelier.Bson; namespace StellaOps.Concelier.Connector.Distro.Ubuntu.Internal; diff --git a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Distro.Ubuntu/Internal/UbuntuFetchCacheEntry.cs b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Distro.Ubuntu/Internal/UbuntuFetchCacheEntry.cs index 3cd74b5d2..0434bdd42 100644 --- a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Distro.Ubuntu/Internal/UbuntuFetchCacheEntry.cs +++ b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Distro.Ubuntu/Internal/UbuntuFetchCacheEntry.cs @@ -1,14 +1,15 @@ -using System; -using MongoDB.Bson; - -namespace StellaOps.Concelier.Connector.Distro.Ubuntu.Internal; - -internal sealed record UbuntuFetchCacheEntry(string? ETag, DateTimeOffset? LastModified) -{ - public static UbuntuFetchCacheEntry Empty { get; } = new(null, null); - - public static UbuntuFetchCacheEntry FromDocument(StellaOps.Concelier.Storage.Mongo.DocumentRecord document) - => new(document.Etag, document.LastModified); +using System; +using StellaOps.Concelier.Bson; +using StorageContracts = StellaOps.Concelier.Storage.Contracts; + +namespace StellaOps.Concelier.Connector.Distro.Ubuntu.Internal; + +internal sealed record UbuntuFetchCacheEntry(string? ETag, DateTimeOffset? LastModified) +{ + public static UbuntuFetchCacheEntry Empty { get; } = new(null, null); + + public static UbuntuFetchCacheEntry FromDocument(StorageContracts.StorageDocument document) + => new(document.Etag, document.LastModified); public static UbuntuFetchCacheEntry FromBson(BsonDocument document) { @@ -54,11 +55,11 @@ internal sealed record UbuntuFetchCacheEntry(string? ETag, DateTimeOffset? LastM return doc; } - public bool Matches(StellaOps.Concelier.Storage.Mongo.DocumentRecord document) - { - if (document is null) - { - return false; + public bool Matches(StorageContracts.StorageDocument document) + { + if (document is null) + { + return false; } if (!string.Equals(ETag, document.Etag, StringComparison.Ordinal)) diff --git a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Distro.Ubuntu/Internal/UbuntuMapper.cs b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Distro.Ubuntu/Internal/UbuntuMapper.cs index 585fca9f8..6cd7d6403 100644 --- a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Distro.Ubuntu/Internal/UbuntuMapper.cs +++ b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Distro.Ubuntu/Internal/UbuntuMapper.cs @@ -3,7 +3,7 @@ using System.Collections.Generic; using System.Linq; using StellaOps.Concelier.Models; using StellaOps.Concelier.Normalization.Distro; -using StellaOps.Concelier.Storage.Mongo; +using StellaOps.Concelier.Storage; namespace StellaOps.Concelier.Connector.Distro.Ubuntu.Internal; diff --git a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Distro.Ubuntu/UbuntuConnector.cs b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Distro.Ubuntu/UbuntuConnector.cs index a6850bccf..b831ff8d9 100644 --- a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Distro.Ubuntu/UbuntuConnector.cs +++ b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Distro.Ubuntu/UbuntuConnector.cs @@ -5,16 +5,16 @@ using System.Globalization; using System.Text; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using MongoDB.Bson; +using StellaOps.Concelier.Bson; using StellaOps.Concelier.Models; using StellaOps.Concelier.Connector.Common; using StellaOps.Concelier.Connector.Common.Fetch; using StellaOps.Concelier.Connector.Distro.Ubuntu.Configuration; using StellaOps.Concelier.Connector.Distro.Ubuntu.Internal; -using StellaOps.Concelier.Storage.Mongo; -using StellaOps.Concelier.Storage.Mongo.Advisories; -using StellaOps.Concelier.Storage.Mongo; -using StellaOps.Concelier.Storage.Mongo; +using StellaOps.Concelier.Storage; +using StellaOps.Concelier.Storage.Advisories; +using StellaOps.Concelier.Storage; +using StellaOps.Concelier.Storage; using StellaOps.Plugin; using StellaOps.Cryptography; diff --git a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Ghsa/GhsaConnector.cs b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Ghsa/GhsaConnector.cs index b505a7d32..f1a00c884 100644 --- a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Ghsa/GhsaConnector.cs +++ b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Ghsa/GhsaConnector.cs @@ -5,16 +5,16 @@ using System.Net.Http; using System.Text.Json; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using MongoDB.Bson; +using StellaOps.Concelier.Bson; using StellaOps.Concelier.Models; using StellaOps.Concelier.Connector.Common; using StellaOps.Concelier.Connector.Common.Fetch; using StellaOps.Concelier.Connector.Ghsa.Configuration; using StellaOps.Concelier.Connector.Ghsa.Internal; -using StellaOps.Concelier.Storage.Mongo; -using StellaOps.Concelier.Storage.Mongo.Advisories; -using StellaOps.Concelier.Storage.Mongo; -using StellaOps.Concelier.Storage.Mongo; +using StellaOps.Concelier.Storage; +using StellaOps.Concelier.Storage.Advisories; +using StellaOps.Concelier.Storage; +using StellaOps.Concelier.Storage; using StellaOps.Plugin; namespace StellaOps.Concelier.Connector.Ghsa; diff --git a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Ghsa/Internal/GhsaCursor.cs b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Ghsa/Internal/GhsaCursor.cs index 0fc6cb497..32a2f52e7 100644 --- a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Ghsa/Internal/GhsaCursor.cs +++ b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Ghsa/Internal/GhsaCursor.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; using System.Linq; -using MongoDB.Bson; +using StellaOps.Concelier.Bson; namespace StellaOps.Concelier.Connector.Ghsa.Internal; diff --git a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Ghsa/Internal/GhsaMapper.cs b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Ghsa/Internal/GhsaMapper.cs index d3b0e9d7b..9efb0c039 100644 --- a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Ghsa/Internal/GhsaMapper.cs +++ b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Ghsa/Internal/GhsaMapper.cs @@ -5,7 +5,7 @@ using System.Text; using StellaOps.Concelier.Models; using StellaOps.Concelier.Normalization.Cvss; using StellaOps.Concelier.Normalization.SemVer; -using StellaOps.Concelier.Storage.Mongo; +using StellaOps.Concelier.Storage; namespace StellaOps.Concelier.Connector.Ghsa.Internal; diff --git a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Ics.Cisa/IcsCisaConnector.cs b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Ics.Cisa/IcsCisaConnector.cs index 259e26249..005381d4a 100644 --- a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Ics.Cisa/IcsCisaConnector.cs +++ b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Ics.Cisa/IcsCisaConnector.cs @@ -15,18 +15,18 @@ using AngleSharp.Html.Dom; using AngleSharp.Html.Parser; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using MongoDB.Bson; -using MongoDB.Bson.IO; +using StellaOps.Concelier.Bson; +using StellaOps.Concelier.Bson.IO; using StellaOps.Concelier.Models; using StellaOps.Concelier.Connector.Common; using StellaOps.Concelier.Connector.Common.Fetch; using StellaOps.Concelier.Connector.Common.Html; using StellaOps.Concelier.Connector.Ics.Cisa.Configuration; using StellaOps.Concelier.Connector.Ics.Cisa.Internal; -using StellaOps.Concelier.Storage.Mongo; -using StellaOps.Concelier.Storage.Mongo.Advisories; -using StellaOps.Concelier.Storage.Mongo; -using StellaOps.Concelier.Storage.Mongo; +using StellaOps.Concelier.Storage; +using StellaOps.Concelier.Storage.Advisories; +using StellaOps.Concelier.Storage; +using StellaOps.Concelier.Storage; using StellaOps.Concelier.Normalization.SemVer; using StellaOps.Plugin; diff --git a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Ics.Cisa/Internal/IcsCisaCursor.cs b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Ics.Cisa/Internal/IcsCisaCursor.cs index c6e6901e3..d4c2cf950 100644 --- a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Ics.Cisa/Internal/IcsCisaCursor.cs +++ b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Ics.Cisa/Internal/IcsCisaCursor.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; -using MongoDB.Bson; +using StellaOps.Concelier.Bson; namespace StellaOps.Concelier.Connector.Ics.Cisa.Internal; diff --git a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Ics.Kaspersky/Internal/KasperskyCursor.cs b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Ics.Kaspersky/Internal/KasperskyCursor.cs index ebbffe59d..2f1ebd102 100644 --- a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Ics.Kaspersky/Internal/KasperskyCursor.cs +++ b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Ics.Kaspersky/Internal/KasperskyCursor.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; -using MongoDB.Bson; +using StellaOps.Concelier.Bson; namespace StellaOps.Concelier.Connector.Ics.Kaspersky.Internal; diff --git a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Ics.Kaspersky/KasperskyConnector.cs b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Ics.Kaspersky/KasperskyConnector.cs index cac929ca3..398bd8575 100644 --- a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Ics.Kaspersky/KasperskyConnector.cs +++ b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Ics.Kaspersky/KasperskyConnector.cs @@ -6,16 +6,16 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using MongoDB.Bson; +using StellaOps.Concelier.Bson; using StellaOps.Concelier.Models; using StellaOps.Concelier.Connector.Common; using StellaOps.Concelier.Connector.Common.Fetch; using StellaOps.Concelier.Connector.Ics.Kaspersky.Configuration; using StellaOps.Concelier.Connector.Ics.Kaspersky.Internal; -using StellaOps.Concelier.Storage.Mongo; -using StellaOps.Concelier.Storage.Mongo.Advisories; -using StellaOps.Concelier.Storage.Mongo; -using StellaOps.Concelier.Storage.Mongo; +using StellaOps.Concelier.Storage; +using StellaOps.Concelier.Storage.Advisories; +using StellaOps.Concelier.Storage; +using StellaOps.Concelier.Storage; using StellaOps.Plugin; namespace StellaOps.Concelier.Connector.Ics.Kaspersky; @@ -315,9 +315,9 @@ public sealed class KasperskyConnector : IFeedConnector continue; } - var dtoJson = dto.Payload.ToJson(new MongoDB.Bson.IO.JsonWriterSettings + var dtoJson = dto.Payload.ToJson(new StellaOps.Concelier.Bson.IO.JsonWriterSettings { - OutputMode = MongoDB.Bson.IO.JsonOutputMode.RelaxedExtendedJson, + OutputMode = StellaOps.Concelier.Bson.IO.JsonOutputMode.RelaxedExtendedJson, }); KasperskyAdvisoryDto advisoryDto; diff --git a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Jvn/Internal/JvnAdvisoryMapper.cs b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Jvn/Internal/JvnAdvisoryMapper.cs index 016204bea..e46421069 100644 --- a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Jvn/Internal/JvnAdvisoryMapper.cs +++ b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Jvn/Internal/JvnAdvisoryMapper.cs @@ -6,9 +6,9 @@ using StellaOps.Concelier.Connector.Common; using StellaOps.Concelier.Normalization.Cvss; using StellaOps.Concelier.Normalization.Identifiers; using StellaOps.Concelier.Normalization.Text; -using StellaOps.Concelier.Storage.Mongo; -using StellaOps.Concelier.Storage.Mongo; -using StellaOps.Concelier.Storage.Mongo.JpFlags; +using StellaOps.Concelier.Storage; +using StellaOps.Concelier.Storage; +using StellaOps.Concelier.Storage.JpFlags; namespace StellaOps.Concelier.Connector.Jvn.Internal; diff --git a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Jvn/Internal/JvnCursor.cs b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Jvn/Internal/JvnCursor.cs index 585799b8a..eada9bf25 100644 --- a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Jvn/Internal/JvnCursor.cs +++ b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Jvn/Internal/JvnCursor.cs @@ -1,5 +1,5 @@ using System.Linq; -using MongoDB.Bson; +using StellaOps.Concelier.Bson; namespace StellaOps.Concelier.Connector.Jvn.Internal; diff --git a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Jvn/JvnConnector.cs b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Jvn/JvnConnector.cs index c39ff3c75..47b9deb83 100644 --- a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Jvn/JvnConnector.cs +++ b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Jvn/JvnConnector.cs @@ -3,17 +3,17 @@ using System.Linq; using System.Text.Json; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using MongoDB.Bson; +using StellaOps.Concelier.Bson; using StellaOps.Concelier.Models; using StellaOps.Concelier.Connector.Common; using StellaOps.Concelier.Connector.Common.Fetch; using StellaOps.Concelier.Connector.Jvn.Configuration; using StellaOps.Concelier.Connector.Jvn.Internal; -using StellaOps.Concelier.Storage.Mongo; -using StellaOps.Concelier.Storage.Mongo.Advisories; -using StellaOps.Concelier.Storage.Mongo; -using StellaOps.Concelier.Storage.Mongo; -using StellaOps.Concelier.Storage.Mongo.JpFlags; +using StellaOps.Concelier.Storage; +using StellaOps.Concelier.Storage.Advisories; +using StellaOps.Concelier.Storage; +using StellaOps.Concelier.Storage; +using StellaOps.Concelier.Storage.JpFlags; using StellaOps.Plugin; namespace StellaOps.Concelier.Connector.Jvn; @@ -278,9 +278,9 @@ public sealed class JvnConnector : IFeedConnector continue; } - var dtoJson = dto.Payload.ToJson(new MongoDB.Bson.IO.JsonWriterSettings + var dtoJson = dto.Payload.ToJson(new StellaOps.Concelier.Bson.IO.JsonWriterSettings { - OutputMode = MongoDB.Bson.IO.JsonOutputMode.RelaxedExtendedJson, + OutputMode = StellaOps.Concelier.Bson.IO.JsonOutputMode.RelaxedExtendedJson, }); JvnDetailDto detail; diff --git a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Kev/Internal/KevCursor.cs b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Kev/Internal/KevCursor.cs index 2662ebaeb..750cd0f2f 100644 --- a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Kev/Internal/KevCursor.cs +++ b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Kev/Internal/KevCursor.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; -using MongoDB.Bson; +using StellaOps.Concelier.Bson; namespace StellaOps.Concelier.Connector.Kev.Internal; diff --git a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Kev/KevConnector.cs b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Kev/KevConnector.cs index 2701c782c..f3fe57893 100644 --- a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Kev/KevConnector.cs +++ b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Kev/KevConnector.cs @@ -7,17 +7,17 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using MongoDB.Bson; +using StellaOps.Concelier.Bson; using StellaOps.Concelier.Models; using StellaOps.Concelier.Connector.Common; using StellaOps.Concelier.Connector.Common.Fetch; using StellaOps.Concelier.Connector.Common.Json; using StellaOps.Concelier.Connector.Kev.Configuration; using StellaOps.Concelier.Connector.Kev.Internal; -using StellaOps.Concelier.Storage.Mongo; -using StellaOps.Concelier.Storage.Mongo.Advisories; -using StellaOps.Concelier.Storage.Mongo; -using StellaOps.Concelier.Storage.Mongo; +using StellaOps.Concelier.Storage; +using StellaOps.Concelier.Storage.Advisories; +using StellaOps.Concelier.Storage; +using StellaOps.Concelier.Storage; using StellaOps.Plugin; namespace StellaOps.Concelier.Connector.Kev; @@ -334,9 +334,9 @@ public sealed class KevConnector : IFeedConnector KevCatalogDto? catalog; try { - var dtoJson = dtoRecord.Payload.ToJson(new MongoDB.Bson.IO.JsonWriterSettings + var dtoJson = dtoRecord.Payload.ToJson(new StellaOps.Concelier.Bson.IO.JsonWriterSettings { - OutputMode = MongoDB.Bson.IO.JsonOutputMode.RelaxedExtendedJson, + OutputMode = StellaOps.Concelier.Bson.IO.JsonOutputMode.RelaxedExtendedJson, }); catalog = JsonSerializer.Deserialize(dtoJson, SerializerOptions); diff --git a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Kisa/Internal/KisaCursor.cs b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Kisa/Internal/KisaCursor.cs index ed704e732..b2441f896 100644 --- a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Kisa/Internal/KisaCursor.cs +++ b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Kisa/Internal/KisaCursor.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; -using MongoDB.Bson; +using StellaOps.Concelier.Bson; namespace StellaOps.Concelier.Connector.Kisa.Internal; diff --git a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Kisa/Internal/KisaMapper.cs b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Kisa/Internal/KisaMapper.cs index b56cf31b0..0c1f856e4 100644 --- a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Kisa/Internal/KisaMapper.cs +++ b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Kisa/Internal/KisaMapper.cs @@ -3,7 +3,7 @@ using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; using StellaOps.Concelier.Models; -using StellaOps.Concelier.Storage.Mongo; +using StellaOps.Concelier.Storage; namespace StellaOps.Concelier.Connector.Kisa.Internal; diff --git a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Kisa/KisaConnector.cs b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Kisa/KisaConnector.cs index 2a45d87dc..5602d02be 100644 --- a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Kisa/KisaConnector.cs +++ b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Kisa/KisaConnector.cs @@ -6,15 +6,15 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using MongoDB.Bson; +using StellaOps.Concelier.Bson; using StellaOps.Concelier.Connector.Common; using StellaOps.Concelier.Connector.Common.Fetch; using StellaOps.Concelier.Connector.Kisa.Configuration; using StellaOps.Concelier.Connector.Kisa.Internal; -using StellaOps.Concelier.Storage.Mongo; -using StellaOps.Concelier.Storage.Mongo.Advisories; -using StellaOps.Concelier.Storage.Mongo; -using StellaOps.Concelier.Storage.Mongo; +using StellaOps.Concelier.Storage; +using StellaOps.Concelier.Storage.Advisories; +using StellaOps.Concelier.Storage; +using StellaOps.Concelier.Storage; using StellaOps.Plugin; namespace StellaOps.Concelier.Connector.Kisa; diff --git a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Nvd/Internal/NvdCursor.cs b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Nvd/Internal/NvdCursor.cs index 235985e2e..0657a7e87 100644 --- a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Nvd/Internal/NvdCursor.cs +++ b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Nvd/Internal/NvdCursor.cs @@ -1,5 +1,5 @@ using System.Linq; -using MongoDB.Bson; +using StellaOps.Concelier.Bson; using StellaOps.Concelier.Connector.Common.Cursors; namespace StellaOps.Concelier.Connector.Nvd.Internal; diff --git a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Nvd/Internal/NvdMapper.cs b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Nvd/Internal/NvdMapper.cs index 206bb4aa0..40cb102f3 100644 --- a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Nvd/Internal/NvdMapper.cs +++ b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Nvd/Internal/NvdMapper.cs @@ -8,7 +8,7 @@ using StellaOps.Concelier.Models; using StellaOps.Concelier.Normalization.Identifiers; using StellaOps.Concelier.Normalization.Cvss; using StellaOps.Concelier.Normalization.Text; -using StellaOps.Concelier.Storage.Mongo; +using StellaOps.Concelier.Storage; namespace StellaOps.Concelier.Connector.Nvd.Internal; diff --git a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Nvd/NvdConnector.cs b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Nvd/NvdConnector.cs index b3047d969..ae637a02a 100644 --- a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Nvd/NvdConnector.cs +++ b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Nvd/NvdConnector.cs @@ -3,7 +3,7 @@ using System.Text; using System.Text.Json; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using MongoDB.Bson; +using StellaOps.Concelier.Bson; using StellaOps.Concelier.Models; using StellaOps.Concelier.Connector.Common; using StellaOps.Concelier.Connector.Common.Fetch; @@ -11,11 +11,11 @@ using StellaOps.Concelier.Connector.Common.Json; using StellaOps.Concelier.Connector.Common.Cursors; using StellaOps.Concelier.Connector.Nvd.Configuration; using StellaOps.Concelier.Connector.Nvd.Internal; -using StellaOps.Concelier.Storage.Mongo; -using StellaOps.Concelier.Storage.Mongo.Advisories; -using StellaOps.Concelier.Storage.Mongo; -using StellaOps.Concelier.Storage.Mongo; -using StellaOps.Concelier.Storage.Mongo.ChangeHistory; +using StellaOps.Concelier.Storage; +using StellaOps.Concelier.Storage.Advisories; +using StellaOps.Concelier.Storage; +using StellaOps.Concelier.Storage; +using StellaOps.Concelier.Storage.ChangeHistory; using StellaOps.Plugin; using Json.Schema; using StellaOps.Cryptography; @@ -266,9 +266,9 @@ public sealed class NvdConnector : IFeedConnector continue; } - var json = dto.Payload.ToJson(new MongoDB.Bson.IO.JsonWriterSettings + var json = dto.Payload.ToJson(new StellaOps.Concelier.Bson.IO.JsonWriterSettings { - OutputMode = MongoDB.Bson.IO.JsonOutputMode.RelaxedExtendedJson, + OutputMode = StellaOps.Concelier.Bson.IO.JsonOutputMode.RelaxedExtendedJson, }); using var jsonDocument = JsonDocument.Parse(json); diff --git a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Osv/Internal/OsvCursor.cs b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Osv/Internal/OsvCursor.cs index b9420f710..3876bf161 100644 --- a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Osv/Internal/OsvCursor.cs +++ b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Osv/Internal/OsvCursor.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; -using MongoDB.Bson; +using StellaOps.Concelier.Bson; namespace StellaOps.Concelier.Connector.Osv.Internal; diff --git a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Osv/Internal/OsvMapper.cs b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Osv/Internal/OsvMapper.cs index 63269af36..b6ef73446 100644 --- a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Osv/Internal/OsvMapper.cs +++ b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Osv/Internal/OsvMapper.cs @@ -9,8 +9,8 @@ using StellaOps.Concelier.Normalization.Cvss; using StellaOps.Concelier.Normalization.Identifiers; using StellaOps.Concelier.Normalization.Text; using StellaOps.Concelier.Connector.Common; -using StellaOps.Concelier.Storage.Mongo; -using StellaOps.Concelier.Storage.Mongo; +using StellaOps.Concelier.Storage; +using StellaOps.Concelier.Storage; namespace StellaOps.Concelier.Connector.Osv.Internal; diff --git a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Osv/OsvConnector.cs b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Osv/OsvConnector.cs index 7307ccfcb..cd92142cf 100644 --- a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Osv/OsvConnector.cs +++ b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Osv/OsvConnector.cs @@ -11,17 +11,17 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using MongoDB.Bson; -using MongoDB.Bson.IO; +using StellaOps.Concelier.Bson; +using StellaOps.Concelier.Bson.IO; using StellaOps.Concelier.Models; using StellaOps.Concelier.Connector.Common; using StellaOps.Concelier.Connector.Common.Fetch; using StellaOps.Concelier.Connector.Osv.Configuration; using StellaOps.Concelier.Connector.Osv.Internal; -using StellaOps.Concelier.Storage.Mongo; -using StellaOps.Concelier.Storage.Mongo.Advisories; -using StellaOps.Concelier.Storage.Mongo; -using StellaOps.Concelier.Storage.Mongo; +using StellaOps.Concelier.Storage; +using StellaOps.Concelier.Storage.Advisories; +using StellaOps.Concelier.Storage; +using StellaOps.Concelier.Storage; using StellaOps.Plugin; using StellaOps.Cryptography; @@ -188,7 +188,7 @@ public sealed class OsvConnector : IFeedConnector } var sanitized = JsonSerializer.Serialize(dto, SerializerOptions); - var payload = MongoDB.Bson.BsonDocument.Parse(sanitized); + var payload = StellaOps.Concelier.Bson.BsonDocument.Parse(sanitized); var dtoRecord = new DtoRecord( Guid.NewGuid(), document.Id, diff --git a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Ru.Bdu/Internal/RuBduCursor.cs b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Ru.Bdu/Internal/RuBduCursor.cs index 975e132cb..c10a3856e 100644 --- a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Ru.Bdu/Internal/RuBduCursor.cs +++ b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Ru.Bdu/Internal/RuBduCursor.cs @@ -1,4 +1,4 @@ -using MongoDB.Bson; +using StellaOps.Concelier.Bson; namespace StellaOps.Concelier.Connector.Ru.Bdu.Internal; diff --git a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Ru.Bdu/Internal/RuBduMapper.cs b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Ru.Bdu/Internal/RuBduMapper.cs index ffd245352..1a0499fde 100644 --- a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Ru.Bdu/Internal/RuBduMapper.cs +++ b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Ru.Bdu/Internal/RuBduMapper.cs @@ -6,7 +6,7 @@ using System.Linq; using System.Text; using StellaOps.Concelier.Models; using StellaOps.Concelier.Normalization.Cvss; -using StellaOps.Concelier.Storage.Mongo; +using StellaOps.Concelier.Storage; namespace StellaOps.Concelier.Connector.Ru.Bdu.Internal; diff --git a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Ru.Bdu/RuBduConnector.cs b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Ru.Bdu/RuBduConnector.cs index a9cd047ed..f8beee78b 100644 --- a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Ru.Bdu/RuBduConnector.cs +++ b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Ru.Bdu/RuBduConnector.cs @@ -9,16 +9,16 @@ using System.Xml; using System.Xml.Linq; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using MongoDB.Bson; +using StellaOps.Concelier.Bson; using StellaOps.Concelier.Normalization.Cvss; using StellaOps.Concelier.Connector.Common; using StellaOps.Concelier.Connector.Common.Fetch; using StellaOps.Concelier.Connector.Ru.Bdu.Configuration; using StellaOps.Concelier.Connector.Ru.Bdu.Internal; -using StellaOps.Concelier.Storage.Mongo; -using StellaOps.Concelier.Storage.Mongo.Advisories; -using StellaOps.Concelier.Storage.Mongo; -using StellaOps.Concelier.Storage.Mongo; +using StellaOps.Concelier.Storage; +using StellaOps.Concelier.Storage.Advisories; +using StellaOps.Concelier.Storage; +using StellaOps.Concelier.Storage; using StellaOps.Plugin; using StellaOps.Cryptography; @@ -268,7 +268,7 @@ public sealed class RuBduConnector : IFeedConnector continue; } - var bson = MongoDB.Bson.BsonDocument.Parse(JsonSerializer.Serialize(dto, SerializerOptions)); + var bson = StellaOps.Concelier.Bson.BsonDocument.Parse(JsonSerializer.Serialize(dto, SerializerOptions)); var dtoRecord = new DtoRecord(Guid.NewGuid(), document.Id, SourceName, "ru-bdu.v1", bson, _timeProvider.GetUtcNow()); await _dtoStore.UpsertAsync(dtoRecord, cancellationToken).ConfigureAwait(false); await _documentStore.UpdateStatusAsync(document.Id, DocumentStatuses.PendingMap, cancellationToken).ConfigureAwait(false); diff --git a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Ru.Nkcki/Internal/RuNkckiCursor.cs b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Ru.Nkcki/Internal/RuNkckiCursor.cs index 0051c1d8d..87ed2dcdb 100644 --- a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Ru.Nkcki/Internal/RuNkckiCursor.cs +++ b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Ru.Nkcki/Internal/RuNkckiCursor.cs @@ -1,4 +1,4 @@ -using MongoDB.Bson; +using StellaOps.Concelier.Bson; namespace StellaOps.Concelier.Connector.Ru.Nkcki.Internal; diff --git a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Ru.Nkcki/Internal/RuNkckiMapper.cs b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Ru.Nkcki/Internal/RuNkckiMapper.cs index 2627375fd..2653d511d 100644 --- a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Ru.Nkcki/Internal/RuNkckiMapper.cs +++ b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Ru.Nkcki/Internal/RuNkckiMapper.cs @@ -5,7 +5,7 @@ using System.Linq; using StellaOps.Concelier.Models; using StellaOps.Concelier.Normalization.Cvss; using StellaOps.Concelier.Normalization.SemVer; -using StellaOps.Concelier.Storage.Mongo; +using StellaOps.Concelier.Storage; namespace StellaOps.Concelier.Connector.Ru.Nkcki.Internal; diff --git a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Ru.Nkcki/RuNkckiConnector.cs b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Ru.Nkcki/RuNkckiConnector.cs index ce3412131..8cb002d83 100644 --- a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Ru.Nkcki/RuNkckiConnector.cs +++ b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Ru.Nkcki/RuNkckiConnector.cs @@ -10,15 +10,15 @@ using System.Text.Json.Serialization; using AngleSharp.Html.Parser; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using MongoDB.Bson; +using StellaOps.Concelier.Bson; using StellaOps.Concelier.Connector.Common; using StellaOps.Concelier.Connector.Common.Fetch; using StellaOps.Concelier.Connector.Ru.Nkcki.Configuration; using StellaOps.Concelier.Connector.Ru.Nkcki.Internal; -using StellaOps.Concelier.Storage.Mongo; -using StellaOps.Concelier.Storage.Mongo.Advisories; -using StellaOps.Concelier.Storage.Mongo; -using StellaOps.Concelier.Storage.Mongo; +using StellaOps.Concelier.Storage; +using StellaOps.Concelier.Storage.Advisories; +using StellaOps.Concelier.Storage; +using StellaOps.Concelier.Storage; using StellaOps.Plugin; using StellaOps.Cryptography; @@ -338,7 +338,7 @@ public sealed class RuNkckiConnector : IFeedConnector continue; } - var bson = MongoDB.Bson.BsonDocument.Parse(JsonSerializer.Serialize(dto, SerializerOptions)); + var bson = StellaOps.Concelier.Bson.BsonDocument.Parse(JsonSerializer.Serialize(dto, SerializerOptions)); var dtoRecord = new DtoRecord(Guid.NewGuid(), document.Id, SourceName, "ru-nkcki.v1", bson, _timeProvider.GetUtcNow()); await _dtoStore.UpsertAsync(dtoRecord, cancellationToken).ConfigureAwait(false); await _documentStore.UpdateStatusAsync(document.Id, DocumentStatuses.PendingMap, cancellationToken).ConfigureAwait(false); diff --git a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.StellaOpsMirror/Internal/StellaOpsMirrorCursor.cs b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.StellaOpsMirror/Internal/StellaOpsMirrorCursor.cs index 270056e4d..0826b602b 100644 --- a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.StellaOpsMirror/Internal/StellaOpsMirrorCursor.cs +++ b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.StellaOpsMirror/Internal/StellaOpsMirrorCursor.cs @@ -1,5 +1,5 @@ using System.Linq; -using MongoDB.Bson; +using StellaOps.Concelier.Bson; namespace StellaOps.Concelier.Connector.StellaOpsMirror.Internal; diff --git a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.StellaOpsMirror/StellaOpsMirrorConnector.cs b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.StellaOpsMirror/StellaOpsMirrorConnector.cs index 0112b5cfe..729a6a163 100644 --- a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.StellaOpsMirror/StellaOpsMirrorConnector.cs +++ b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.StellaOpsMirror/StellaOpsMirrorConnector.cs @@ -4,7 +4,7 @@ using System.Linq; using System.Text; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using MongoDB.Bson; +using StellaOps.Concelier.Bson; using StellaOps.Concelier.Connector.Common.Fetch; using StellaOps.Concelier.Connector.Common; using StellaOps.Concelier.Connector.StellaOpsMirror.Client; @@ -12,10 +12,10 @@ using StellaOps.Concelier.Connector.StellaOpsMirror.Internal; using StellaOps.Concelier.Connector.StellaOpsMirror.Security; using StellaOps.Concelier.Connector.StellaOpsMirror.Settings; using StellaOps.Concelier.Models; -using StellaOps.Concelier.Storage.Mongo; -using StellaOps.Concelier.Storage.Mongo.Advisories; -using StellaOps.Concelier.Storage.Mongo; -using StellaOps.Concelier.Storage.Mongo; +using StellaOps.Concelier.Storage; +using StellaOps.Concelier.Storage.Advisories; +using StellaOps.Concelier.Storage; +using StellaOps.Concelier.Storage; using StellaOps.Plugin; using StellaOps.Cryptography; diff --git a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Vndr.Adobe/AdobeConnector.cs b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Vndr.Adobe/AdobeConnector.cs index a9a6af1a8..1083de422 100644 --- a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Vndr.Adobe/AdobeConnector.cs +++ b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Vndr.Adobe/AdobeConnector.cs @@ -8,18 +8,18 @@ using System.Text.RegularExpressions; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Json.Schema; -using MongoDB.Bson; +using StellaOps.Concelier.Bson; using StellaOps.Concelier.Connector.Common; using StellaOps.Concelier.Connector.Common.Fetch; using StellaOps.Concelier.Connector.Common.Json; using StellaOps.Concelier.Connector.Common.Packages; using StellaOps.Concelier.Connector.Vndr.Adobe.Configuration; using StellaOps.Concelier.Connector.Vndr.Adobe.Internal; -using StellaOps.Concelier.Storage.Mongo; -using StellaOps.Concelier.Storage.Mongo.Advisories; -using StellaOps.Concelier.Storage.Mongo; -using StellaOps.Concelier.Storage.Mongo; -using StellaOps.Concelier.Storage.Mongo.PsirtFlags; +using StellaOps.Concelier.Storage; +using StellaOps.Concelier.Storage.Advisories; +using StellaOps.Concelier.Storage; +using StellaOps.Concelier.Storage; +using StellaOps.Concelier.Storage.PsirtFlags; using StellaOps.Concelier.Models; using StellaOps.Plugin; @@ -495,7 +495,7 @@ public sealed class AdobeConnector : IFeedConnector using var jsonDocument = JsonDocument.Parse(json); _schemaValidator.Validate(jsonDocument, Schema, metadata.AdvisoryId); - var payload = MongoDB.Bson.BsonDocument.Parse(json); + var payload = StellaOps.Concelier.Bson.BsonDocument.Parse(json); var dtoRecord = new DtoRecord( Guid.NewGuid(), document.Id, @@ -546,9 +546,9 @@ public sealed class AdobeConnector : IFeedConnector AdobeBulletinDto? dto; try { - var json = dtoRecord.Payload.ToJson(new MongoDB.Bson.IO.JsonWriterSettings + var json = dtoRecord.Payload.ToJson(new StellaOps.Concelier.Bson.IO.JsonWriterSettings { - OutputMode = MongoDB.Bson.IO.JsonOutputMode.RelaxedExtendedJson, + OutputMode = StellaOps.Concelier.Bson.IO.JsonOutputMode.RelaxedExtendedJson, }); dto = JsonSerializer.Deserialize(json, SerializerOptions); diff --git a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Vndr.Adobe/Internal/AdobeCursor.cs b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Vndr.Adobe/Internal/AdobeCursor.cs index 508aa83ed..ce49ac9aa 100644 --- a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Vndr.Adobe/Internal/AdobeCursor.cs +++ b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Vndr.Adobe/Internal/AdobeCursor.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; -using MongoDB.Bson; +using StellaOps.Concelier.Bson; namespace StellaOps.Concelier.Connector.Vndr.Adobe.Internal; diff --git a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Vndr.Adobe/Internal/AdobeDocumentMetadata.cs b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Vndr.Adobe/Internal/AdobeDocumentMetadata.cs index 521530e15..c530d758a 100644 --- a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Vndr.Adobe/Internal/AdobeDocumentMetadata.cs +++ b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Vndr.Adobe/Internal/AdobeDocumentMetadata.cs @@ -1,6 +1,6 @@ using System; using System.Collections.Generic; -using StellaOps.Concelier.Storage.Mongo; +using StellaOps.Concelier.Storage; namespace StellaOps.Concelier.Connector.Vndr.Adobe.Internal; diff --git a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Vndr.Apple/AppleConnector.cs b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Vndr.Apple/AppleConnector.cs index a3b069555..64f2cfd73 100644 --- a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Vndr.Apple/AppleConnector.cs +++ b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Vndr.Apple/AppleConnector.cs @@ -8,15 +8,15 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using MongoDB.Bson; +using StellaOps.Concelier.Bson; using StellaOps.Concelier.Connector.Common; using StellaOps.Concelier.Connector.Common.Fetch; using StellaOps.Concelier.Connector.Vndr.Apple.Internal; -using StellaOps.Concelier.Storage.Mongo; -using StellaOps.Concelier.Storage.Mongo.Advisories; -using StellaOps.Concelier.Storage.Mongo; -using StellaOps.Concelier.Storage.Mongo; -using StellaOps.Concelier.Storage.Mongo.PsirtFlags; +using StellaOps.Concelier.Storage; +using StellaOps.Concelier.Storage.Advisories; +using StellaOps.Concelier.Storage; +using StellaOps.Concelier.Storage; +using StellaOps.Concelier.Storage.PsirtFlags; using StellaOps.Plugin; namespace StellaOps.Concelier.Connector.Vndr.Apple; diff --git a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Vndr.Apple/Internal/AppleCursor.cs b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Vndr.Apple/Internal/AppleCursor.cs index e2e4b11ae..7f087e392 100644 --- a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Vndr.Apple/Internal/AppleCursor.cs +++ b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Vndr.Apple/Internal/AppleCursor.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; -using MongoDB.Bson; +using StellaOps.Concelier.Bson; namespace StellaOps.Concelier.Connector.Vndr.Apple.Internal; diff --git a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Vndr.Apple/Internal/AppleMapper.cs b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Vndr.Apple/Internal/AppleMapper.cs index 544ca4c19..a219531c3 100644 --- a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Vndr.Apple/Internal/AppleMapper.cs +++ b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Vndr.Apple/Internal/AppleMapper.cs @@ -4,9 +4,9 @@ using System.Linq; using StellaOps.Concelier.Models; using StellaOps.Concelier.Connector.Common; using StellaOps.Concelier.Connector.Common.Packages; -using StellaOps.Concelier.Storage.Mongo; -using StellaOps.Concelier.Storage.Mongo; -using StellaOps.Concelier.Storage.Mongo.PsirtFlags; +using StellaOps.Concelier.Storage; +using StellaOps.Concelier.Storage; +using StellaOps.Concelier.Storage.PsirtFlags; namespace StellaOps.Concelier.Connector.Vndr.Apple.Internal; diff --git a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Vndr.Chromium/ChromiumConnector.cs b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Vndr.Chromium/ChromiumConnector.cs index 856624e39..9555ac42d 100644 --- a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Vndr.Chromium/ChromiumConnector.cs +++ b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Vndr.Chromium/ChromiumConnector.cs @@ -4,19 +4,19 @@ using System.Text; using System.Text.Json; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using MongoDB.Bson; -using MongoDB.Bson.IO; +using StellaOps.Concelier.Bson; +using StellaOps.Concelier.Bson.IO; using StellaOps.Concelier.Models; using StellaOps.Concelier.Connector.Common; using StellaOps.Concelier.Connector.Common.Fetch; using StellaOps.Concelier.Connector.Common.Json; using StellaOps.Concelier.Connector.Vndr.Chromium.Configuration; using StellaOps.Concelier.Connector.Vndr.Chromium.Internal; -using StellaOps.Concelier.Storage.Mongo; -using StellaOps.Concelier.Storage.Mongo.Advisories; -using StellaOps.Concelier.Storage.Mongo; -using StellaOps.Concelier.Storage.Mongo; -using StellaOps.Concelier.Storage.Mongo.PsirtFlags; +using StellaOps.Concelier.Storage; +using StellaOps.Concelier.Storage.Advisories; +using StellaOps.Concelier.Storage; +using StellaOps.Concelier.Storage; +using StellaOps.Concelier.Storage.PsirtFlags; using StellaOps.Plugin; using Json.Schema; diff --git a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Vndr.Chromium/Internal/ChromiumCursor.cs b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Vndr.Chromium/Internal/ChromiumCursor.cs index a4f1ea3c7..f913dbdf2 100644 --- a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Vndr.Chromium/Internal/ChromiumCursor.cs +++ b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Vndr.Chromium/Internal/ChromiumCursor.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; using System.Linq; -using MongoDB.Bson; +using StellaOps.Concelier.Bson; namespace StellaOps.Concelier.Connector.Vndr.Chromium.Internal; diff --git a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Vndr.Chromium/Internal/ChromiumDocumentMetadata.cs b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Vndr.Chromium/Internal/ChromiumDocumentMetadata.cs index 10c2c133c..ec6dddb99 100644 --- a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Vndr.Chromium/Internal/ChromiumDocumentMetadata.cs +++ b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Vndr.Chromium/Internal/ChromiumDocumentMetadata.cs @@ -1,4 +1,4 @@ -using StellaOps.Concelier.Storage.Mongo; +using StellaOps.Concelier.Storage; namespace StellaOps.Concelier.Connector.Vndr.Chromium.Internal; diff --git a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Vndr.Chromium/Internal/ChromiumMapper.cs b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Vndr.Chromium/Internal/ChromiumMapper.cs index 333dfeb50..5178432a0 100644 --- a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Vndr.Chromium/Internal/ChromiumMapper.cs +++ b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Vndr.Chromium/Internal/ChromiumMapper.cs @@ -3,7 +3,7 @@ using System.Collections.Generic; using System.Linq; using System.Globalization; using StellaOps.Concelier.Models; -using StellaOps.Concelier.Storage.Mongo.PsirtFlags; +using StellaOps.Concelier.Storage.PsirtFlags; namespace StellaOps.Concelier.Connector.Vndr.Chromium.Internal; diff --git a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Vndr.Cisco/CiscoConnector.cs b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Vndr.Cisco/CiscoConnector.cs index ee71947f0..562bd1df0 100644 --- a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Vndr.Cisco/CiscoConnector.cs +++ b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Vndr.Cisco/CiscoConnector.cs @@ -5,15 +5,15 @@ using System.Text.Json; using System.Text.Json.Serialization; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using MongoDB.Bson; +using StellaOps.Concelier.Bson; using StellaOps.Concelier.Connector.Common; using StellaOps.Concelier.Connector.Common.Fetch; using StellaOps.Concelier.Connector.Vndr.Cisco.Configuration; using StellaOps.Concelier.Connector.Vndr.Cisco.Internal; -using StellaOps.Concelier.Storage.Mongo; -using StellaOps.Concelier.Storage.Mongo.Advisories; -using StellaOps.Concelier.Storage.Mongo; -using StellaOps.Concelier.Storage.Mongo; +using StellaOps.Concelier.Storage; +using StellaOps.Concelier.Storage.Advisories; +using StellaOps.Concelier.Storage; +using StellaOps.Concelier.Storage; using StellaOps.Plugin; namespace StellaOps.Concelier.Connector.Vndr.Cisco; diff --git a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Vndr.Cisco/Internal/CiscoCursor.cs b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Vndr.Cisco/Internal/CiscoCursor.cs index 528cc28ef..e7f9120bb 100644 --- a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Vndr.Cisco/Internal/CiscoCursor.cs +++ b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Vndr.Cisco/Internal/CiscoCursor.cs @@ -1,4 +1,4 @@ -using MongoDB.Bson; +using StellaOps.Concelier.Bson; namespace StellaOps.Concelier.Connector.Vndr.Cisco.Internal; diff --git a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Vndr.Cisco/Internal/CiscoMapper.cs b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Vndr.Cisco/Internal/CiscoMapper.cs index 9594de574..879cf8303 100644 --- a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Vndr.Cisco/Internal/CiscoMapper.cs +++ b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Vndr.Cisco/Internal/CiscoMapper.cs @@ -3,8 +3,8 @@ using System.Collections.Generic; using System.Linq; using StellaOps.Concelier.Models; using StellaOps.Concelier.Connector.Common.Packages; -using StellaOps.Concelier.Storage.Mongo; -using StellaOps.Concelier.Storage.Mongo; +using StellaOps.Concelier.Storage; +using StellaOps.Concelier.Storage; using StellaOps.Concelier.Normalization.SemVer; namespace StellaOps.Concelier.Connector.Vndr.Cisco.Internal; diff --git a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Vndr.Msrc/Internal/MsrcCursor.cs b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Vndr.Msrc/Internal/MsrcCursor.cs index 7166dd400..94a9f3636 100644 --- a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Vndr.Msrc/Internal/MsrcCursor.cs +++ b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Vndr.Msrc/Internal/MsrcCursor.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; -using MongoDB.Bson; +using StellaOps.Concelier.Bson; namespace StellaOps.Concelier.Connector.Vndr.Msrc.Internal; diff --git a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Vndr.Msrc/Internal/MsrcMapper.cs b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Vndr.Msrc/Internal/MsrcMapper.cs index c36041326..ba049f080 100644 --- a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Vndr.Msrc/Internal/MsrcMapper.cs +++ b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Vndr.Msrc/Internal/MsrcMapper.cs @@ -2,7 +2,7 @@ using System; using System.Collections.Generic; using System.Linq; using StellaOps.Concelier.Models; -using StellaOps.Concelier.Storage.Mongo; +using StellaOps.Concelier.Storage; namespace StellaOps.Concelier.Connector.Vndr.Msrc.Internal; diff --git a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Vndr.Msrc/MsrcConnector.cs b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Vndr.Msrc/MsrcConnector.cs index e3ed85621..093b24ea6 100644 --- a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Vndr.Msrc/MsrcConnector.cs +++ b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Vndr.Msrc/MsrcConnector.cs @@ -8,16 +8,16 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using MongoDB.Bson; +using StellaOps.Concelier.Bson; using StellaOps.Concelier.Models; using StellaOps.Concelier.Connector.Common; using StellaOps.Concelier.Connector.Common.Fetch; using StellaOps.Concelier.Connector.Vndr.Msrc.Configuration; using StellaOps.Concelier.Connector.Vndr.Msrc.Internal; -using StellaOps.Concelier.Storage.Mongo; -using StellaOps.Concelier.Storage.Mongo.Advisories; -using StellaOps.Concelier.Storage.Mongo; -using StellaOps.Concelier.Storage.Mongo; +using StellaOps.Concelier.Storage; +using StellaOps.Concelier.Storage.Advisories; +using StellaOps.Concelier.Storage; +using StellaOps.Concelier.Storage; using StellaOps.Plugin; namespace StellaOps.Concelier.Connector.Vndr.Msrc; diff --git a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Vndr.Oracle/Internal/OracleCursor.cs b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Vndr.Oracle/Internal/OracleCursor.cs index 6b4c5109e..1a0037685 100644 --- a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Vndr.Oracle/Internal/OracleCursor.cs +++ b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Vndr.Oracle/Internal/OracleCursor.cs @@ -1,8 +1,8 @@ using System; using System.Collections.Generic; using System.Linq; -using MongoDB.Bson; -using StellaOps.Concelier.Storage.Mongo; +using StellaOps.Concelier.Bson; +using StellaOps.Concelier.Storage; namespace StellaOps.Concelier.Connector.Vndr.Oracle.Internal; diff --git a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Vndr.Oracle/Internal/OracleDocumentMetadata.cs b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Vndr.Oracle/Internal/OracleDocumentMetadata.cs index d93ff67aa..22af9e6dd 100644 --- a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Vndr.Oracle/Internal/OracleDocumentMetadata.cs +++ b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Vndr.Oracle/Internal/OracleDocumentMetadata.cs @@ -1,6 +1,6 @@ using System; using System.Collections.Generic; -using StellaOps.Concelier.Storage.Mongo; +using StellaOps.Concelier.Storage; namespace StellaOps.Concelier.Connector.Vndr.Oracle.Internal; diff --git a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Vndr.Oracle/Internal/OracleMapper.cs b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Vndr.Oracle/Internal/OracleMapper.cs index 395d5dc34..594276217 100644 --- a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Vndr.Oracle/Internal/OracleMapper.cs +++ b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Vndr.Oracle/Internal/OracleMapper.cs @@ -4,9 +4,9 @@ using System.Linq; using System.Text.RegularExpressions; using StellaOps.Concelier.Models; using StellaOps.Concelier.Connector.Common.Packages; -using StellaOps.Concelier.Storage.Mongo; -using StellaOps.Concelier.Storage.Mongo; -using StellaOps.Concelier.Storage.Mongo.PsirtFlags; +using StellaOps.Concelier.Storage; +using StellaOps.Concelier.Storage; +using StellaOps.Concelier.Storage.PsirtFlags; namespace StellaOps.Concelier.Connector.Vndr.Oracle.Internal; diff --git a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Vndr.Oracle/OracleConnector.cs b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Vndr.Oracle/OracleConnector.cs index 5dd56dddd..212777017 100644 --- a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Vndr.Oracle/OracleConnector.cs +++ b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Vndr.Oracle/OracleConnector.cs @@ -6,16 +6,16 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using MongoDB.Bson; +using StellaOps.Concelier.Bson; using StellaOps.Concelier.Connector.Common; using StellaOps.Concelier.Connector.Common.Fetch; using StellaOps.Concelier.Connector.Vndr.Oracle.Configuration; using StellaOps.Concelier.Connector.Vndr.Oracle.Internal; -using StellaOps.Concelier.Storage.Mongo; -using StellaOps.Concelier.Storage.Mongo.Advisories; -using StellaOps.Concelier.Storage.Mongo; -using StellaOps.Concelier.Storage.Mongo; -using StellaOps.Concelier.Storage.Mongo.PsirtFlags; +using StellaOps.Concelier.Storage; +using StellaOps.Concelier.Storage.Advisories; +using StellaOps.Concelier.Storage; +using StellaOps.Concelier.Storage; +using StellaOps.Concelier.Storage.PsirtFlags; using StellaOps.Plugin; namespace StellaOps.Concelier.Connector.Vndr.Oracle; diff --git a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Vndr.Vmware/Internal/VmwareCursor.cs b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Vndr.Vmware/Internal/VmwareCursor.cs index 52e83ca48..e70c9950f 100644 --- a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Vndr.Vmware/Internal/VmwareCursor.cs +++ b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Vndr.Vmware/Internal/VmwareCursor.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; -using MongoDB.Bson; +using StellaOps.Concelier.Bson; namespace StellaOps.Concelier.Connector.Vndr.Vmware.Internal; diff --git a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Vndr.Vmware/Internal/VmwareFetchCacheEntry.cs b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Vndr.Vmware/Internal/VmwareFetchCacheEntry.cs index d7fe0f087..7f662f0cf 100644 --- a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Vndr.Vmware/Internal/VmwareFetchCacheEntry.cs +++ b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Vndr.Vmware/Internal/VmwareFetchCacheEntry.cs @@ -1,6 +1,6 @@ using System; -using MongoDB.Bson; -using StellaOps.Concelier.Storage.Mongo; +using StellaOps.Concelier.Bson; +using StellaOps.Concelier.Storage; namespace StellaOps.Concelier.Connector.Vndr.Vmware.Internal; diff --git a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Vndr.Vmware/Internal/VmwareMapper.cs b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Vndr.Vmware/Internal/VmwareMapper.cs index 18e3ced29..34d334251 100644 --- a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Vndr.Vmware/Internal/VmwareMapper.cs +++ b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Vndr.Vmware/Internal/VmwareMapper.cs @@ -4,9 +4,9 @@ using System.Linq; using StellaOps.Concelier.Models; using StellaOps.Concelier.Connector.Common; using StellaOps.Concelier.Connector.Common.Packages; -using StellaOps.Concelier.Storage.Mongo; -using StellaOps.Concelier.Storage.Mongo; -using StellaOps.Concelier.Storage.Mongo.PsirtFlags; +using StellaOps.Concelier.Storage; +using StellaOps.Concelier.Storage; +using StellaOps.Concelier.Storage.PsirtFlags; namespace StellaOps.Concelier.Connector.Vndr.Vmware.Internal; diff --git a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Vndr.Vmware/VmwareConnector.cs b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Vndr.Vmware/VmwareConnector.cs index 627831a03..57184d4ce 100644 --- a/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Vndr.Vmware/VmwareConnector.cs +++ b/src/Concelier/__Libraries/StellaOps.Concelier.Connector.Vndr.Vmware/VmwareConnector.cs @@ -7,18 +7,18 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using MongoDB.Bson; -using MongoDB.Bson.IO; +using StellaOps.Concelier.Bson; +using StellaOps.Concelier.Bson.IO; using StellaOps.Concelier.Models; using StellaOps.Concelier.Connector.Common; using StellaOps.Concelier.Connector.Common.Fetch; using StellaOps.Concelier.Connector.Vndr.Vmware.Configuration; using StellaOps.Concelier.Connector.Vndr.Vmware.Internal; -using StellaOps.Concelier.Storage.Mongo; -using StellaOps.Concelier.Storage.Mongo.Advisories; -using StellaOps.Concelier.Storage.Mongo; -using StellaOps.Concelier.Storage.Mongo; -using StellaOps.Concelier.Storage.Mongo.PsirtFlags; +using StellaOps.Concelier.Storage; +using StellaOps.Concelier.Storage.Advisories; +using StellaOps.Concelier.Storage; +using StellaOps.Concelier.Storage; +using StellaOps.Concelier.Storage.PsirtFlags; using StellaOps.Plugin; namespace StellaOps.Concelier.Connector.Vndr.Vmware; @@ -343,7 +343,7 @@ public sealed class VmwareConnector : IFeedConnector } var sanitized = JsonSerializer.Serialize(detail, SerializerOptions); - var payload = MongoDB.Bson.BsonDocument.Parse(sanitized); + var payload = StellaOps.Concelier.Bson.BsonDocument.Parse(sanitized); var dtoRecord = new DtoRecord(Guid.NewGuid(), document.Id, SourceName, "vmware.v1", payload, _timeProvider.GetUtcNow()); await _dtoStore.UpsertAsync(dtoRecord, cancellationToken).ConfigureAwait(false); diff --git a/src/Concelier/__Libraries/StellaOps.Concelier.Core/Linksets/AdvisoryLinkset.cs b/src/Concelier/__Libraries/StellaOps.Concelier.Core/Linksets/AdvisoryLinkset.cs index 1b7172458..a10768470 100644 --- a/src/Concelier/__Libraries/StellaOps.Concelier.Core/Linksets/AdvisoryLinkset.cs +++ b/src/Concelier/__Libraries/StellaOps.Concelier.Core/Linksets/AdvisoryLinkset.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; -using MongoDB.Bson; +using StellaOps.Concelier.Bson; using System.Linq; namespace StellaOps.Concelier.Core.Linksets; diff --git a/src/Concelier/__Libraries/StellaOps.Concelier.Exporter.Json/JsonExporterDependencyInjectionRoutine.cs b/src/Concelier/__Libraries/StellaOps.Concelier.Exporter.Json/JsonExporterDependencyInjectionRoutine.cs index e778eaf7d..86757d4ca 100644 --- a/src/Concelier/__Libraries/StellaOps.Concelier.Exporter.Json/JsonExporterDependencyInjectionRoutine.cs +++ b/src/Concelier/__Libraries/StellaOps.Concelier.Exporter.Json/JsonExporterDependencyInjectionRoutine.cs @@ -6,7 +6,7 @@ using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Options; using StellaOps.DependencyInjection; using StellaOps.Concelier.Core.Jobs; -using StellaOps.Concelier.Storage.Mongo.Exporting; +using StellaOps.Concelier.Storage.Exporting; namespace StellaOps.Concelier.Exporter.Json; diff --git a/src/Concelier/__Libraries/StellaOps.Concelier.Exporter.Json/JsonExporterPlugin.cs b/src/Concelier/__Libraries/StellaOps.Concelier.Exporter.Json/JsonExporterPlugin.cs index 07e36e099..83911c616 100644 --- a/src/Concelier/__Libraries/StellaOps.Concelier.Exporter.Json/JsonExporterPlugin.cs +++ b/src/Concelier/__Libraries/StellaOps.Concelier.Exporter.Json/JsonExporterPlugin.cs @@ -1,6 +1,6 @@ using System; using Microsoft.Extensions.DependencyInjection; -using StellaOps.Concelier.Storage.Mongo.Advisories; +using StellaOps.Concelier.Storage.Advisories; using StellaOps.Plugin; namespace StellaOps.Concelier.Exporter.Json; diff --git a/src/Concelier/__Libraries/StellaOps.Concelier.Exporter.Json/JsonFeedExporter.cs b/src/Concelier/__Libraries/StellaOps.Concelier.Exporter.Json/JsonFeedExporter.cs index e61a131d2..9d3f92ec8 100644 --- a/src/Concelier/__Libraries/StellaOps.Concelier.Exporter.Json/JsonFeedExporter.cs +++ b/src/Concelier/__Libraries/StellaOps.Concelier.Exporter.Json/JsonFeedExporter.cs @@ -9,8 +9,8 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using StellaOps.Concelier.Core.Events; using StellaOps.Concelier.Models; -using StellaOps.Concelier.Storage.Mongo.Advisories; -using StellaOps.Concelier.Storage.Mongo.Exporting; +using StellaOps.Concelier.Storage.Advisories; +using StellaOps.Concelier.Storage.Exporting; using StellaOps.Cryptography; using StellaOps.Plugin; diff --git a/src/Concelier/__Libraries/StellaOps.Concelier.Exporter.TrivyDb/TrivyDbExportPlan.cs b/src/Concelier/__Libraries/StellaOps.Concelier.Exporter.TrivyDb/TrivyDbExportPlan.cs index 394112096..358fdf7b7 100644 --- a/src/Concelier/__Libraries/StellaOps.Concelier.Exporter.TrivyDb/TrivyDbExportPlan.cs +++ b/src/Concelier/__Libraries/StellaOps.Concelier.Exporter.TrivyDb/TrivyDbExportPlan.cs @@ -1,7 +1,7 @@ namespace StellaOps.Concelier.Exporter.TrivyDb; using System.Collections.Generic; -using StellaOps.Concelier.Storage.Mongo.Exporting; +using StellaOps.Concelier.Storage.Exporting; public sealed record TrivyDbExportPlan( TrivyDbExportMode Mode, diff --git a/src/Concelier/__Libraries/StellaOps.Concelier.Exporter.TrivyDb/TrivyDbExportPlanner.cs b/src/Concelier/__Libraries/StellaOps.Concelier.Exporter.TrivyDb/TrivyDbExportPlanner.cs index d465abf72..9bdc6f2a1 100644 --- a/src/Concelier/__Libraries/StellaOps.Concelier.Exporter.TrivyDb/TrivyDbExportPlanner.cs +++ b/src/Concelier/__Libraries/StellaOps.Concelier.Exporter.TrivyDb/TrivyDbExportPlanner.cs @@ -1,12 +1,12 @@ using System; -using StellaOps.Concelier.Storage.Mongo.Exporting; +using StellaOps.Concelier.Storage.Exporting; namespace StellaOps.Concelier.Exporter.TrivyDb; using System; using System.Collections.Generic; using System.Linq; -using StellaOps.Concelier.Storage.Mongo.Exporting; +using StellaOps.Concelier.Storage.Exporting; public sealed class TrivyDbExportPlanner { diff --git a/src/Concelier/__Libraries/StellaOps.Concelier.Exporter.TrivyDb/TrivyDbExporterDependencyInjectionRoutine.cs b/src/Concelier/__Libraries/StellaOps.Concelier.Exporter.TrivyDb/TrivyDbExporterDependencyInjectionRoutine.cs index 18e45c7dc..8dedff8bd 100644 --- a/src/Concelier/__Libraries/StellaOps.Concelier.Exporter.TrivyDb/TrivyDbExporterDependencyInjectionRoutine.cs +++ b/src/Concelier/__Libraries/StellaOps.Concelier.Exporter.TrivyDb/TrivyDbExporterDependencyInjectionRoutine.cs @@ -7,7 +7,7 @@ using Microsoft.Extensions.Options; using StellaOps.DependencyInjection; using StellaOps.Concelier.Core.Jobs; using StellaOps.Concelier.Exporter.Json; -using StellaOps.Concelier.Storage.Mongo.Exporting; +using StellaOps.Concelier.Storage.Exporting; namespace StellaOps.Concelier.Exporter.TrivyDb; diff --git a/src/Concelier/__Libraries/StellaOps.Concelier.Exporter.TrivyDb/TrivyDbExporterPlugin.cs b/src/Concelier/__Libraries/StellaOps.Concelier.Exporter.TrivyDb/TrivyDbExporterPlugin.cs index 2aa230e17..bfadfcd0c 100644 --- a/src/Concelier/__Libraries/StellaOps.Concelier.Exporter.TrivyDb/TrivyDbExporterPlugin.cs +++ b/src/Concelier/__Libraries/StellaOps.Concelier.Exporter.TrivyDb/TrivyDbExporterPlugin.cs @@ -1,6 +1,6 @@ using System; using Microsoft.Extensions.DependencyInjection; -using StellaOps.Concelier.Storage.Mongo.Advisories; +using StellaOps.Concelier.Storage.Advisories; using StellaOps.Plugin; namespace StellaOps.Concelier.Exporter.TrivyDb; diff --git a/src/Concelier/__Libraries/StellaOps.Concelier.Exporter.TrivyDb/TrivyDbFeedExporter.cs b/src/Concelier/__Libraries/StellaOps.Concelier.Exporter.TrivyDb/TrivyDbFeedExporter.cs index 04598dbc6..2dda1e895 100644 --- a/src/Concelier/__Libraries/StellaOps.Concelier.Exporter.TrivyDb/TrivyDbFeedExporter.cs +++ b/src/Concelier/__Libraries/StellaOps.Concelier.Exporter.TrivyDb/TrivyDbFeedExporter.cs @@ -14,8 +14,8 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using StellaOps.Concelier.Exporter.Json; using StellaOps.Concelier.Models; -using StellaOps.Concelier.Storage.Mongo.Advisories; -using StellaOps.Concelier.Storage.Mongo.Exporting; +using StellaOps.Concelier.Storage.Advisories; +using StellaOps.Concelier.Storage.Exporting; using StellaOps.Plugin; namespace StellaOps.Concelier.Exporter.TrivyDb; diff --git a/src/Concelier/__Libraries/StellaOps.Concelier.Merge/Services/AdvisoryMergeService.cs b/src/Concelier/__Libraries/StellaOps.Concelier.Merge/Services/AdvisoryMergeService.cs index 684594f0d..52fbb1616 100644 --- a/src/Concelier/__Libraries/StellaOps.Concelier.Merge/Services/AdvisoryMergeService.cs +++ b/src/Concelier/__Libraries/StellaOps.Concelier.Merge/Services/AdvisoryMergeService.cs @@ -9,9 +9,9 @@ using Microsoft.Extensions.Logging; using StellaOps.Concelier.Core; using StellaOps.Concelier.Core.Events; using StellaOps.Concelier.Models; -using StellaOps.Concelier.Storage.Mongo.Advisories; -using StellaOps.Concelier.Storage.Mongo.Aliases; -using StellaOps.Concelier.Storage.Mongo.MergeEvents; +using StellaOps.Concelier.Storage.Advisories; +using StellaOps.Concelier.Storage.Aliases; +using StellaOps.Concelier.Storage.MergeEvents; using System.Text.Json; using StellaOps.Provenance.Mongo; diff --git a/src/Concelier/__Libraries/StellaOps.Concelier.Merge/Services/AliasGraphResolver.cs b/src/Concelier/__Libraries/StellaOps.Concelier.Merge/Services/AliasGraphResolver.cs index 431259739..019635b27 100644 --- a/src/Concelier/__Libraries/StellaOps.Concelier.Merge/Services/AliasGraphResolver.cs +++ b/src/Concelier/__Libraries/StellaOps.Concelier.Merge/Services/AliasGraphResolver.cs @@ -3,7 +3,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; -using StellaOps.Concelier.Storage.Mongo.Aliases; +using StellaOps.Concelier.Storage.Aliases; namespace StellaOps.Concelier.Merge.Services; diff --git a/src/Concelier/__Libraries/StellaOps.Concelier.Merge/Services/MergeEventWriter.cs b/src/Concelier/__Libraries/StellaOps.Concelier.Merge/Services/MergeEventWriter.cs index a225a459f..0db6fcd24 100644 --- a/src/Concelier/__Libraries/StellaOps.Concelier.Merge/Services/MergeEventWriter.cs +++ b/src/Concelier/__Libraries/StellaOps.Concelier.Merge/Services/MergeEventWriter.cs @@ -4,7 +4,7 @@ using System.Security.Cryptography; using System.Linq; using Microsoft.Extensions.Logging; using StellaOps.Concelier.Models; -using StellaOps.Concelier.Storage.Mongo.MergeEvents; +using StellaOps.Concelier.Storage.MergeEvents; /// /// Persists merge events with canonical before/after hashes for auditability. diff --git a/src/Concelier/__Libraries/StellaOps.Concelier.Models/MongoCompat/Bson.cs b/src/Concelier/__Libraries/StellaOps.Concelier.Models/Bson/Bson.cs similarity index 98% rename from src/Concelier/__Libraries/StellaOps.Concelier.Models/MongoCompat/Bson.cs rename to src/Concelier/__Libraries/StellaOps.Concelier.Models/Bson/Bson.cs index ddecd849f..784a8c8ae 100644 --- a/src/Concelier/__Libraries/StellaOps.Concelier.Models/MongoCompat/Bson.cs +++ b/src/Concelier/__Libraries/StellaOps.Concelier.Models/Bson/Bson.cs @@ -4,7 +4,7 @@ using System.Linq; using System.Text; using System.Text.Json; -namespace MongoDB.Bson +namespace StellaOps.Concelier.Bson { public enum BsonType { @@ -280,7 +280,7 @@ namespace MongoDB.Bson public string ToJson() => ToJson(null); - public string ToJson(MongoDB.Bson.IO.JsonWriterSettings? settings) + public string ToJson(StellaOps.Concelier.Bson.IO.JsonWriterSettings? settings) { var ordered = _values .OrderBy(static kvp => kvp.Key, StringComparer.Ordinal) @@ -479,7 +479,7 @@ namespace MongoDB.Bson public static class BsonJsonExtensions { - public static string ToJson(this IEnumerable documents, MongoDB.Bson.IO.JsonWriterSettings? settings = null) + public static string ToJson(this IEnumerable documents, StellaOps.Concelier.Bson.IO.JsonWriterSettings? settings = null) { var options = new JsonSerializerOptions { WriteIndented = settings?.Indent ?? false }; var payload = documents?.Select(BsonTypeMapper.MapToDotNetValue).ToList() ?? new List(); @@ -488,7 +488,7 @@ namespace MongoDB.Bson } } -namespace MongoDB.Bson.Serialization.Attributes +namespace StellaOps.Concelier.Bson.Serialization.Attributes { [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Class | AttributeTargets.Struct)] public sealed class BsonElementAttribute : Attribute @@ -502,7 +502,7 @@ namespace MongoDB.Bson.Serialization.Attributes } } -namespace MongoDB.Bson.IO +namespace StellaOps.Concelier.Bson.IO { public enum JsonOutputMode { diff --git a/src/Concelier/__Libraries/StellaOps.Concelier.Models/MongoCompat/Bootstrapping.cs b/src/Concelier/__Libraries/StellaOps.Concelier.Models/MongoCompat/Bootstrapping.cs index f5623c157..16f7ffc53 100644 --- a/src/Concelier/__Libraries/StellaOps.Concelier.Models/MongoCompat/Bootstrapping.cs +++ b/src/Concelier/__Libraries/StellaOps.Concelier.Models/MongoCompat/Bootstrapping.cs @@ -2,7 +2,7 @@ using System; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; -namespace StellaOps.Concelier.Storage.Mongo; +namespace StellaOps.Concelier.Storage; /// /// Lightweight compatibility bootstrapper to satisfy legacy Mongo wiring during Postgres migration. diff --git a/src/Concelier/__Libraries/StellaOps.Concelier.Models/MongoCompat/DriverStubs.cs b/src/Concelier/__Libraries/StellaOps.Concelier.Models/MongoCompat/DriverStubs.cs index 421dbf6ed..2379177bc 100644 --- a/src/Concelier/__Libraries/StellaOps.Concelier.Models/MongoCompat/DriverStubs.cs +++ b/src/Concelier/__Libraries/StellaOps.Concelier.Models/MongoCompat/DriverStubs.cs @@ -5,7 +5,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; -using MongoDB.Bson; +using StellaOps.Concelier.Bson; namespace MongoDB.Driver { diff --git a/src/Concelier/__Libraries/StellaOps.Concelier.Models/MongoCompat/StorageStubs.cs b/src/Concelier/__Libraries/StellaOps.Concelier.Models/MongoCompat/StorageStubs.cs index f92317474..a8d68bcca 100644 --- a/src/Concelier/__Libraries/StellaOps.Concelier.Models/MongoCompat/StorageStubs.cs +++ b/src/Concelier/__Libraries/StellaOps.Concelier.Models/MongoCompat/StorageStubs.cs @@ -1,10 +1,10 @@ using System.Collections.Concurrent; using System.IO; using System.Linq; -using MongoDB.Bson; +using StellaOps.Concelier.Bson; using StellaOps.Concelier.Models; -namespace StellaOps.Concelier.Storage.Mongo +namespace StellaOps.Concelier.Storage { public static class MongoStorageDefaults { @@ -192,7 +192,7 @@ namespace StellaOps.Concelier.Storage.Mongo Guid DocumentId, string SourceName, string Format, - MongoDB.Bson.BsonDocument Payload, + StellaOps.Concelier.Bson.BsonDocument Payload, DateTimeOffset CreatedAt, string? SchemaVersion = null, DateTimeOffset? ValidatedAt = null) @@ -211,7 +211,7 @@ namespace StellaOps.Concelier.Storage.Mongo public Guid DocumentId { get; init; } public string SourceName { get; init; } = string.Empty; public string Format { get; init; } = string.Empty; - public MongoDB.Bson.BsonDocument Payload { get; init; } = new(); + public StellaOps.Concelier.Bson.BsonDocument Payload { get; init; } = new(); public DateTimeOffset CreatedAt { get; init; } public string SchemaVersion { get; init; } = string.Empty; public DateTimeOffset ValidatedAt { get; init; } @@ -284,7 +284,7 @@ public sealed record SourceStateRecord( string SourceName, bool Enabled, bool Paused, - MongoDB.Bson.BsonDocument? Cursor, + StellaOps.Concelier.Bson.BsonDocument? Cursor, DateTimeOffset? LastSuccess, DateTimeOffset? LastFailure, int FailCount, @@ -295,7 +295,7 @@ public sealed record SourceStateRecord( public interface ISourceStateRepository { Task TryGetAsync(string sourceName, CancellationToken cancellationToken); - Task UpdateCursorAsync(string sourceName, MongoDB.Bson.BsonDocument cursor, DateTimeOffset completedAt, CancellationToken cancellationToken); + Task UpdateCursorAsync(string sourceName, StellaOps.Concelier.Bson.BsonDocument cursor, DateTimeOffset completedAt, CancellationToken cancellationToken); Task MarkFailureAsync(string sourceName, DateTimeOffset now, TimeSpan backoff, string reason, CancellationToken cancellationToken); Task UpsertAsync(SourceStateRecord record, CancellationToken cancellationToken); } @@ -310,7 +310,7 @@ public sealed record SourceStateRecord( return Task.FromResult(record); } - public Task UpdateCursorAsync(string sourceName, MongoDB.Bson.BsonDocument cursor, DateTimeOffset completedAt, CancellationToken cancellationToken) + public Task UpdateCursorAsync(string sourceName, StellaOps.Concelier.Bson.BsonDocument cursor, DateTimeOffset completedAt, CancellationToken cancellationToken) { var current = _states.TryGetValue(sourceName, out var existing) ? existing : null; _states[sourceName] = new SourceStateRecord( @@ -369,7 +369,7 @@ public sealed record SourceStateRecord( public Task TryGetAsync(string sourceName, CancellationToken cancellationToken) => _inner.TryGetAsync(sourceName, cancellationToken); - public Task UpdateCursorAsync(string sourceName, MongoDB.Bson.BsonDocument cursor, DateTimeOffset completedAt, CancellationToken cancellationToken) + public Task UpdateCursorAsync(string sourceName, StellaOps.Concelier.Bson.BsonDocument cursor, DateTimeOffset completedAt, CancellationToken cancellationToken) => _inner.UpdateCursorAsync(sourceName, cursor, completedAt, cancellationToken); public Task MarkFailureAsync(string sourceName, DateTimeOffset now, TimeSpan backoff, string reason, CancellationToken cancellationToken) @@ -380,12 +380,12 @@ public sealed record SourceStateRecord( } } -namespace StellaOps.Concelier.Storage.Mongo.Advisories +namespace StellaOps.Concelier.Storage.Advisories { public sealed class AdvisoryDocument { public string AdvisoryKey { get; set; } = string.Empty; - public MongoDB.Bson.BsonDocument Payload { get; set; } = new(); + public StellaOps.Concelier.Bson.BsonDocument Payload { get; set; } = new(); public DateTime? Modified { get; set; } public DateTime? Published { get; set; } public DateTime? CreatedAt { get; set; } @@ -436,7 +436,7 @@ namespace StellaOps.Concelier.Storage.Mongo.Advisories } } -namespace StellaOps.Concelier.Storage.Mongo.Aliases +namespace StellaOps.Concelier.Storage.Aliases { public static class AliasStoreConstants { @@ -504,7 +504,7 @@ namespace StellaOps.Concelier.Storage.Mongo.Aliases } } -namespace StellaOps.Concelier.Storage.Mongo.ChangeHistory +namespace StellaOps.Concelier.Storage.ChangeHistory { public sealed record ChangeHistoryFieldChange(string Field, string ChangeType, string? PreviousValue, string? CurrentValue); public sealed record ChangeHistoryRecord( @@ -553,7 +553,7 @@ namespace StellaOps.Concelier.Storage.Mongo.ChangeHistory } } -namespace StellaOps.Concelier.Storage.Mongo.Exporting +namespace StellaOps.Concelier.Storage.Exporting { public sealed record ExportFileRecord(string Path, long Length, string Digest); @@ -654,7 +654,7 @@ namespace StellaOps.Concelier.Storage.Mongo.Exporting } } -namespace StellaOps.Concelier.Storage.Mongo.JpFlags +namespace StellaOps.Concelier.Storage.JpFlags { public sealed record JpFlagRecord( string AdvisoryKey, @@ -687,7 +687,7 @@ namespace StellaOps.Concelier.Storage.Mongo.JpFlags } } -namespace StellaOps.Concelier.Storage.Mongo.MergeEvents +namespace StellaOps.Concelier.Storage.MergeEvents { public sealed record MergeEventRecord( Guid Id, @@ -753,23 +753,23 @@ namespace StellaOps.Concelier.Storage.Mongo.MergeEvents } } -namespace StellaOps.Concelier.Storage.Mongo.Documents +namespace StellaOps.Concelier.Storage.Documents { - using DocumentRecord = StellaOps.Concelier.Storage.Mongo.DocumentRecord; - using IDocumentStore = StellaOps.Concelier.Storage.Mongo.IDocumentStore; - using InMemoryDocumentStore = StellaOps.Concelier.Storage.Mongo.InMemoryDocumentStore; - using ISourceStateRepository = StellaOps.Concelier.Storage.Mongo.ISourceStateRepository; - using InMemorySourceStateRepository = StellaOps.Concelier.Storage.Mongo.InMemorySourceStateRepository; + using DocumentRecord = StellaOps.Concelier.Storage.DocumentRecord; + using IDocumentStore = StellaOps.Concelier.Storage.IDocumentStore; + using InMemoryDocumentStore = StellaOps.Concelier.Storage.InMemoryDocumentStore; + using ISourceStateRepository = StellaOps.Concelier.Storage.ISourceStateRepository; + using InMemorySourceStateRepository = StellaOps.Concelier.Storage.InMemorySourceStateRepository; } -namespace StellaOps.Concelier.Storage.Mongo.Dtos +namespace StellaOps.Concelier.Storage.Dtos { - using DtoRecord = StellaOps.Concelier.Storage.Mongo.DtoRecord; - using IDtoStore = StellaOps.Concelier.Storage.Mongo.IDtoStore; - using InMemoryDtoStore = StellaOps.Concelier.Storage.Mongo.InMemoryDtoStore; + using DtoRecord = StellaOps.Concelier.Storage.DtoRecord; + using IDtoStore = StellaOps.Concelier.Storage.IDtoStore; + using InMemoryDtoStore = StellaOps.Concelier.Storage.InMemoryDtoStore; } -namespace StellaOps.Concelier.Storage.Mongo.PsirtFlags +namespace StellaOps.Concelier.Storage.PsirtFlags { public sealed record PsirtFlagRecord(string AdvisoryId, string Vendor, string SourceName, string? ExternalId, DateTimeOffset RecordedAt) { @@ -812,7 +812,7 @@ namespace StellaOps.Concelier.Storage.Mongo.PsirtFlags } } -namespace StellaOps.Concelier.Storage.Mongo.Observations +namespace StellaOps.Concelier.Storage.Observations { public sealed class AdvisoryObservationDocument { @@ -875,7 +875,7 @@ namespace StellaOps.Concelier.Storage.Mongo.Observations } } -namespace StellaOps.Concelier.Storage.Mongo.Linksets +namespace StellaOps.Concelier.Storage.Linksets { public sealed class AdvisoryLinksetDocument { @@ -894,7 +894,7 @@ namespace StellaOps.Concelier.Storage.Mongo.Linksets } } -namespace StellaOps.Concelier.Storage.Mongo +namespace StellaOps.Concelier.Storage { // Already defined above; kept for backward compatibility with legacy using directives. } diff --git a/src/Concelier/__Libraries/StellaOps.Concelier.Storage.Postgres/Advisories/PostgresAdvisoryStore.cs b/src/Concelier/__Libraries/StellaOps.Concelier.Storage.Postgres/Advisories/PostgresAdvisoryStore.cs index 4fbf53e62..e9d53779f 100644 --- a/src/Concelier/__Libraries/StellaOps.Concelier.Storage.Postgres/Advisories/PostgresAdvisoryStore.cs +++ b/src/Concelier/__Libraries/StellaOps.Concelier.Storage.Postgres/Advisories/PostgresAdvisoryStore.cs @@ -3,7 +3,7 @@ using System.Text.Json; using Microsoft.Extensions.Logging; using StellaOps.Concelier.Models; using StellaOps.Concelier.Storage.Postgres.Conversion; -using MongoContracts = StellaOps.Concelier.Storage.Mongo.Advisories; +using MongoContracts = StellaOps.Concelier.Storage.Advisories; using StellaOps.Concelier.Storage.Postgres.Models; using StellaOps.Concelier.Storage.Postgres.Repositories; diff --git a/src/Concelier/__Libraries/StellaOps.Concelier.Storage.Postgres/ContractsMappingExtensions.cs b/src/Concelier/__Libraries/StellaOps.Concelier.Storage.Postgres/ContractsMappingExtensions.cs index a5c8e95ac..9c3f97e12 100644 --- a/src/Concelier/__Libraries/StellaOps.Concelier.Storage.Postgres/ContractsMappingExtensions.cs +++ b/src/Concelier/__Libraries/StellaOps.Concelier.Storage.Postgres/ContractsMappingExtensions.cs @@ -1,9 +1,9 @@ using System; using System.Text.Json; -using MongoDB.Bson; -using MongoDB.Bson.IO; +using StellaOps.Concelier.Bson; +using StellaOps.Concelier.Bson.IO; using Contracts = StellaOps.Concelier.Storage.Contracts; -using MongoContracts = StellaOps.Concelier.Storage.Mongo; +using MongoContracts = StellaOps.Concelier.Storage; namespace StellaOps.Concelier.Storage.Postgres; diff --git a/src/Concelier/__Libraries/StellaOps.Concelier.Storage.Postgres/DocumentStore.cs b/src/Concelier/__Libraries/StellaOps.Concelier.Storage.Postgres/DocumentStore.cs index d61c2f568..a43790218 100644 --- a/src/Concelier/__Libraries/StellaOps.Concelier.Storage.Postgres/DocumentStore.cs +++ b/src/Concelier/__Libraries/StellaOps.Concelier.Storage.Postgres/DocumentStore.cs @@ -1,5 +1,5 @@ using System.Text.Json; -using StellaOps.Concelier.Storage.Mongo; +using StellaOps.Concelier.Storage; using Contracts = StellaOps.Concelier.Storage.Contracts; using StellaOps.Concelier.Storage.Postgres.Models; using StellaOps.Concelier.Storage.Postgres.Repositories; @@ -35,9 +35,7 @@ public sealed class PostgresDocumentStore : IDocumentStore, Contracts.IStorageDo public async Task UpsertAsync(DocumentRecord record, CancellationToken cancellationToken) { - // Ensure source exists - var source = await _sourceRepository.GetByKeyAsync(record.SourceName, cancellationToken).ConfigureAwait(false) - ?? throw new InvalidOperationException($"Source '{record.SourceName}' not provisioned."); + var source = await EnsureSourceAsync(record.SourceName, cancellationToken).ConfigureAwait(false); var entity = new DocumentRecordEntity( Id: record.Id == Guid.Empty ? Guid.NewGuid() : record.Id, @@ -99,4 +97,29 @@ public sealed class PostgresDocumentStore : IDocumentStore, Contracts.IStorageDo ExpiresAt: row.ExpiresAt, Payload: row.Payload); } + + private async Task EnsureSourceAsync(string sourceName, CancellationToken cancellationToken) + { + var existing = await _sourceRepository.GetByKeyAsync(sourceName, cancellationToken).ConfigureAwait(false); + if (existing is not null) + { + return existing; + } + + var now = DateTimeOffset.UtcNow; + return await _sourceRepository.UpsertAsync(new SourceEntity + { + Id = Guid.NewGuid(), + Key = sourceName, + Name = sourceName, + SourceType = sourceName, + Url = null, + Priority = 0, + Enabled = true, + Config = "{}", + Metadata = "{}", + CreatedAt = now, + UpdatedAt = now, + }, cancellationToken).ConfigureAwait(false); + } } diff --git a/src/Concelier/__Libraries/StellaOps.Concelier.Storage.Postgres/Repositories/PostgresChangeHistoryStore.cs b/src/Concelier/__Libraries/StellaOps.Concelier.Storage.Postgres/Repositories/PostgresChangeHistoryStore.cs index 3e1398e20..e9a029eef 100644 --- a/src/Concelier/__Libraries/StellaOps.Concelier.Storage.Postgres/Repositories/PostgresChangeHistoryStore.cs +++ b/src/Concelier/__Libraries/StellaOps.Concelier.Storage.Postgres/Repositories/PostgresChangeHistoryStore.cs @@ -1,6 +1,6 @@ using System.Text.Json; using Dapper; -using StellaOps.Concelier.Storage.Mongo.ChangeHistory; +using StellaOps.Concelier.Storage.ChangeHistory; namespace StellaOps.Concelier.Storage.Postgres.Repositories; diff --git a/src/Concelier/__Libraries/StellaOps.Concelier.Storage.Postgres/Repositories/PostgresDtoStore.cs b/src/Concelier/__Libraries/StellaOps.Concelier.Storage.Postgres/Repositories/PostgresDtoStore.cs index 1b6291641..893feae5a 100644 --- a/src/Concelier/__Libraries/StellaOps.Concelier.Storage.Postgres/Repositories/PostgresDtoStore.cs +++ b/src/Concelier/__Libraries/StellaOps.Concelier.Storage.Postgres/Repositories/PostgresDtoStore.cs @@ -1,7 +1,7 @@ using System.Linq; using System.Text.Json; using Dapper; -using StellaOps.Concelier.Storage.Mongo; +using StellaOps.Concelier.Storage; using Contracts = StellaOps.Concelier.Storage.Contracts; using StellaOps.Concelier.Storage.Postgres; @@ -83,7 +83,7 @@ internal sealed class PostgresDtoStore : IDtoStore, Contracts.IStorageDtoStore private DtoRecord ToRecord(DtoRow row) { - var payload = MongoDB.Bson.BsonDocument.Parse(row.PayloadJson); + var payload = StellaOps.Concelier.Bson.BsonDocument.Parse(row.PayloadJson); return new DtoRecord( row.Id, row.DocumentId, diff --git a/src/Concelier/__Libraries/StellaOps.Concelier.Storage.Postgres/Repositories/PostgresExportStateStore.cs b/src/Concelier/__Libraries/StellaOps.Concelier.Storage.Postgres/Repositories/PostgresExportStateStore.cs index d8e46f02e..0759ec27d 100644 --- a/src/Concelier/__Libraries/StellaOps.Concelier.Storage.Postgres/Repositories/PostgresExportStateStore.cs +++ b/src/Concelier/__Libraries/StellaOps.Concelier.Storage.Postgres/Repositories/PostgresExportStateStore.cs @@ -1,6 +1,6 @@ using System.Text.Json; using Dapper; -using StellaOps.Concelier.Storage.Mongo.Exporting; +using StellaOps.Concelier.Storage.Exporting; namespace StellaOps.Concelier.Storage.Postgres.Repositories; diff --git a/src/Concelier/__Libraries/StellaOps.Concelier.Storage.Postgres/Repositories/PostgresJpFlagStore.cs b/src/Concelier/__Libraries/StellaOps.Concelier.Storage.Postgres/Repositories/PostgresJpFlagStore.cs index 601046772..47c4930e4 100644 --- a/src/Concelier/__Libraries/StellaOps.Concelier.Storage.Postgres/Repositories/PostgresJpFlagStore.cs +++ b/src/Concelier/__Libraries/StellaOps.Concelier.Storage.Postgres/Repositories/PostgresJpFlagStore.cs @@ -1,5 +1,5 @@ using Dapper; -using StellaOps.Concelier.Storage.Mongo.JpFlags; +using StellaOps.Concelier.Storage.JpFlags; namespace StellaOps.Concelier.Storage.Postgres.Repositories; diff --git a/src/Concelier/__Libraries/StellaOps.Concelier.Storage.Postgres/Repositories/PostgresPsirtFlagStore.cs b/src/Concelier/__Libraries/StellaOps.Concelier.Storage.Postgres/Repositories/PostgresPsirtFlagStore.cs index ff40b0e98..a0b2bea42 100644 --- a/src/Concelier/__Libraries/StellaOps.Concelier.Storage.Postgres/Repositories/PostgresPsirtFlagStore.cs +++ b/src/Concelier/__Libraries/StellaOps.Concelier.Storage.Postgres/Repositories/PostgresPsirtFlagStore.cs @@ -1,5 +1,5 @@ using Dapper; -using StellaOps.Concelier.Storage.Mongo.PsirtFlags; +using StellaOps.Concelier.Storage.PsirtFlags; namespace StellaOps.Concelier.Storage.Postgres.Repositories; diff --git a/src/Concelier/__Libraries/StellaOps.Concelier.Storage.Postgres/ServiceCollectionExtensions.cs b/src/Concelier/__Libraries/StellaOps.Concelier.Storage.Postgres/ServiceCollectionExtensions.cs index 4120bebf4..b9535a6aa 100644 --- a/src/Concelier/__Libraries/StellaOps.Concelier.Storage.Postgres/ServiceCollectionExtensions.cs +++ b/src/Concelier/__Libraries/StellaOps.Concelier.Storage.Postgres/ServiceCollectionExtensions.cs @@ -5,12 +5,12 @@ using StellaOps.Concelier.Storage.Postgres.Advisories; using StellaOps.Infrastructure.Postgres; using StellaOps.Infrastructure.Postgres.Options; using StellaOps.Concelier.Core.Linksets; -using MongoContracts = StellaOps.Concelier.Storage.Mongo; -using MongoAdvisories = StellaOps.Concelier.Storage.Mongo.Advisories; -using MongoExporting = StellaOps.Concelier.Storage.Mongo.Exporting; -using MongoJpFlags = StellaOps.Concelier.Storage.Mongo.JpFlags; -using MongoPsirt = StellaOps.Concelier.Storage.Mongo.PsirtFlags; -using MongoHistory = StellaOps.Concelier.Storage.Mongo.ChangeHistory; +using MongoContracts = StellaOps.Concelier.Storage; +using MongoAdvisories = StellaOps.Concelier.Storage.Advisories; +using MongoExporting = StellaOps.Concelier.Storage.Exporting; +using MongoJpFlags = StellaOps.Concelier.Storage.JpFlags; +using MongoPsirt = StellaOps.Concelier.Storage.PsirtFlags; +using MongoHistory = StellaOps.Concelier.Storage.ChangeHistory; namespace StellaOps.Concelier.Storage.Postgres; @@ -45,7 +45,7 @@ public static class ServiceCollectionExtensions services.AddScoped(); services.AddScoped(); services.AddScoped(); - services.AddScoped(); + services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); @@ -88,7 +88,7 @@ public static class ServiceCollectionExtensions services.AddScoped(); services.AddScoped(); services.AddScoped(); - services.AddScoped(); + services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); diff --git a/src/Concelier/__Libraries/StellaOps.Concelier.Storage.Postgres/SourceStateAdapter.cs b/src/Concelier/__Libraries/StellaOps.Concelier.Storage.Postgres/SourceStateAdapter.cs index 1e78753ef..5c2eb7cf3 100644 --- a/src/Concelier/__Libraries/StellaOps.Concelier.Storage.Postgres/SourceStateAdapter.cs +++ b/src/Concelier/__Libraries/StellaOps.Concelier.Storage.Postgres/SourceStateAdapter.cs @@ -1,11 +1,11 @@ using System; using System.Text.Json; using System.Collections.Generic; -using MongoDB.Bson; +using StellaOps.Concelier.Bson; using StellaOps.Concelier.Storage.Postgres.Models; using StellaOps.Concelier.Storage.Postgres.Repositories; using Contracts = StellaOps.Concelier.Storage.Contracts; -using MongoContracts = StellaOps.Concelier.Storage.Mongo; +using MongoContracts = StellaOps.Concelier.Storage; namespace StellaOps.Concelier.Storage.Postgres; @@ -45,6 +45,7 @@ public sealed class PostgresSourceStateAdapter : MongoContracts.ISourceStateRepo } var cursor = string.IsNullOrWhiteSpace(state.Cursor) ? null : BsonDocument.Parse(state.Cursor); + var backoffUntil = TryParseBackoffUntil(state.Metadata); return new MongoContracts.SourceStateRecord( sourceName, Enabled: true, @@ -53,7 +54,7 @@ public sealed class PostgresSourceStateAdapter : MongoContracts.ISourceStateRepo LastSuccess: state.LastSuccessAt, LastFailure: state.LastError is null ? null : state.LastSyncAt, FailCount: state.ErrorCount, - BackoffUntil: null, + BackoffUntil: backoffUntil, UpdatedAt: state.UpdatedAt, LastFailureReason: state.LastError); } @@ -183,4 +184,32 @@ public sealed class PostgresSourceStateAdapter : MongoContracts.ISourceStateRepo return delta < TimeSpan.Zero ? DateTimeOffset.MinValue : DateTimeOffset.MaxValue; } } + + private static DateTimeOffset? TryParseBackoffUntil(string? metadata) + { + if (string.IsNullOrWhiteSpace(metadata)) + { + return null; + } + + try + { + using var document = JsonDocument.Parse(metadata); + if (!document.RootElement.TryGetProperty("backoffUntil", out var backoffProperty)) + { + return null; + } + + if (backoffProperty.ValueKind == JsonValueKind.String + && DateTimeOffset.TryParse(backoffProperty.GetString(), out var parsed)) + { + return parsed; + } + } + catch + { + } + + return null; + } } diff --git a/src/Concelier/__Libraries/StellaOps.Concelier.Testing/ConcelierPostgresFixture.cs b/src/Concelier/__Libraries/StellaOps.Concelier.Testing/ConcelierPostgresFixture.cs new file mode 100644 index 000000000..49d34d340 --- /dev/null +++ b/src/Concelier/__Libraries/StellaOps.Concelier.Testing/ConcelierPostgresFixture.cs @@ -0,0 +1,19 @@ +using System.Reflection; +using StellaOps.Concelier.Storage.Postgres; +using StellaOps.Infrastructure.Postgres.Testing; +using Xunit; + +namespace StellaOps.Concelier.Testing; + +/// +/// PostgreSQL integration test fixture for the Concelier module. +/// Runs migrations from embedded resources and provides test isolation via schema truncation. +/// +public sealed class ConcelierPostgresFixture : PostgresIntegrationFixture, ICollectionFixture +{ + protected override Assembly? GetMigrationAssembly() + => typeof(ConcelierDataSource).Assembly; + + protected override string GetModuleName() => "Concelier"; +} + diff --git a/src/Concelier/__Libraries/StellaOps.Concelier.Testing/ConnectorTestHarness.cs b/src/Concelier/__Libraries/StellaOps.Concelier.Testing/ConnectorTestHarness.cs index 47146758e..c9c637501 100644 --- a/src/Concelier/__Libraries/StellaOps.Concelier.Testing/ConnectorTestHarness.cs +++ b/src/Concelier/__Libraries/StellaOps.Concelier.Testing/ConnectorTestHarness.cs @@ -1,31 +1,31 @@ using System; using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Http; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Abstractions; -using StellaOps.Concelier.Connector.Common.Http; -using Microsoft.Extensions.Time.Testing; -using StellaOps.Concelier.Connector.Common; -using StellaOps.Concelier.Connector.Common.Testing; -using StellaOps.Concelier.Storage.Mongo; -using StellaOps.Concelier.Testing; - -namespace StellaOps.Concelier.Testing; - -/// -/// Provides a reusable container for connector integration tests with canned HTTP responses and Mongo isolation. -/// -public sealed class ConnectorTestHarness : IAsyncDisposable -{ - private readonly MongoIntegrationFixture _fixture; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Http; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Time.Testing; +using StellaOps.Concelier.Connector.Common; +using StellaOps.Concelier.Connector.Common.Testing; +using StellaOps.Concelier.Connector.Common.Http; +using StellaOps.Concelier.Storage.Postgres; +using StellaOps.Infrastructure.Postgres.Options; + +namespace StellaOps.Concelier.Testing; + +/// +/// Provides a reusable container for connector integration tests with canned HTTP responses and PostgreSQL-backed storage. +/// +public sealed class ConnectorTestHarness : IAsyncDisposable +{ + private readonly ConcelierPostgresFixture _fixture; private readonly DateTimeOffset _initialTime; private readonly string[] _httpClientNames; private ServiceProvider? _serviceProvider; - public ConnectorTestHarness(MongoIntegrationFixture fixture, DateTimeOffset initialTime, params string[] httpClientNames) + public ConnectorTestHarness(ConcelierPostgresFixture fixture, DateTimeOffset initialTime, params string[] httpClientNames) { _fixture = fixture ?? throw new ArgumentNullException(nameof(fixture)); _initialTime = initialTime; @@ -42,32 +42,34 @@ public sealed class ConnectorTestHarness : IAsyncDisposable public CannedHttpMessageHandler Handler { get; } public ServiceProvider ServiceProvider => _serviceProvider ?? throw new InvalidOperationException("Call EnsureServiceProviderAsync first."); - - public async Task EnsureServiceProviderAsync(Action configureServices) - { - ArgumentNullException.ThrowIfNull(configureServices); - + + public async Task EnsureServiceProviderAsync(Action configureServices) + { + ArgumentNullException.ThrowIfNull(configureServices); + if (_serviceProvider is not null) { return _serviceProvider; } + await _fixture.TruncateAllTablesAsync(CancellationToken.None); + var services = new ServiceCollection(); services.AddLogging(builder => builder.AddProvider(NullLoggerProvider.Instance)); services.AddSingleton(TimeProvider); services.AddSingleton(TimeProvider); services.AddSingleton(Handler); - services.AddMongoStorage(options => + services.AddConcelierPostgresStorage(options => { - options.ConnectionString = _fixture.Runner.ConnectionString; - options.DatabaseName = _fixture.Database.DatabaseNamespace.DatabaseName; - options.CommandTimeout = TimeSpan.FromSeconds(5); - }); - - services.AddSourceCommon(); - - configureServices(services); + options.ConnectionString = _fixture.ConnectionString; + options.SchemaName = _fixture.SchemaName; + options.CommandTimeoutSeconds = 5; + }); + + services.AddSourceCommon(); + + configureServices(services); foreach (var clientName in _httpClientNames) { @@ -79,16 +81,13 @@ public sealed class ConnectorTestHarness : IAsyncDisposable }); }); } - - var provider = services.BuildServiceProvider(); - _serviceProvider = provider; - - var bootstrapper = provider.GetRequiredService(); - await bootstrapper.InitializeAsync(CancellationToken.None); - return provider; - } - - public async Task ResetAsync() + + var provider = services.BuildServiceProvider(); + _serviceProvider = provider; + return provider; + } + + public async Task ResetAsync() { if (_serviceProvider is { } provider) { @@ -101,10 +100,10 @@ public sealed class ConnectorTestHarness : IAsyncDisposable provider.Dispose(); } - _serviceProvider = null; - } - - await _fixture.Client.DropDatabaseAsync(_fixture.Database.DatabaseNamespace.DatabaseName); + _serviceProvider = null; + } + + await _fixture.TruncateAllTablesAsync(CancellationToken.None); Handler.Clear(); TimeProvider = CreateTimeProvider(_initialTime); } diff --git a/src/Concelier/__Libraries/StellaOps.Concelier.Testing/MongoIntegrationFixture.cs b/src/Concelier/__Libraries/StellaOps.Concelier.Testing/MongoIntegrationFixture.cs deleted file mode 100644 index 4548b5235..000000000 --- a/src/Concelier/__Libraries/StellaOps.Concelier.Testing/MongoIntegrationFixture.cs +++ /dev/null @@ -1,60 +0,0 @@ -using System; -using System.Threading; -using System.Threading.Tasks; -using Mongo2Go; -using MongoDB.Driver; -using Xunit; - -namespace StellaOps.Concelier.Testing; - -/// -/// In-memory stand-in for the legacy Mongo2Go fixture. No external processes are launched; -/// DropDatabaseAsync simply resets the backing in-memory collections. -/// -public sealed class MongoIntegrationFixture : IAsyncLifetime -{ - private readonly FixtureMongoClient _client; - private MongoDatabase _database; - - public MongoIntegrationFixture() - { - _client = new FixtureMongoClient(this); - Runner = MongoDbRunner.Start(singleNodeReplSet: false); - _database = CreateDatabase(); - } - - public MongoDbRunner Runner { get; } - - public IMongoDatabase Database => _database; - - public IMongoClient Client => _client; - - public Task InitializeAsync() => Task.CompletedTask; - - public Task DisposeAsync() => Task.CompletedTask; - - internal void Reset() - { - _database = CreateDatabase(); - } - - private MongoDatabase CreateDatabase() => new($"concelier-tests-{Guid.NewGuid():N}"); - - private sealed class FixtureMongoClient : IMongoClient - { - private readonly MongoIntegrationFixture _fixture; - - public FixtureMongoClient(MongoIntegrationFixture fixture) - { - _fixture = fixture; - } - - public IMongoDatabase GetDatabase(string name, MongoDatabaseSettings? settings = null) => _fixture.Database; - - public Task DropDatabaseAsync(string name, CancellationToken cancellationToken = default) - { - _fixture.Reset(); - return Task.CompletedTask; - } - } -} diff --git a/src/Concelier/__Libraries/StellaOps.Concelier.Testing/StellaOps.Concelier.Testing.csproj b/src/Concelier/__Libraries/StellaOps.Concelier.Testing/StellaOps.Concelier.Testing.csproj index f1452f59f..bcd4f23dd 100644 --- a/src/Concelier/__Libraries/StellaOps.Concelier.Testing/StellaOps.Concelier.Testing.csproj +++ b/src/Concelier/__Libraries/StellaOps.Concelier.Testing/StellaOps.Concelier.Testing.csproj @@ -14,5 +14,7 @@ + + diff --git a/src/Concelier/__Tests/StellaOps.Concelier.Connector.Acsc.Tests/Acsc/AcscConnectorFetchTests.cs b/src/Concelier/__Tests/StellaOps.Concelier.Connector.Acsc.Tests/Acsc/AcscConnectorFetchTests.cs index 1c734fe53..3a43df3d8 100644 --- a/src/Concelier/__Tests/StellaOps.Concelier.Connector.Acsc.Tests/Acsc/AcscConnectorFetchTests.cs +++ b/src/Concelier/__Tests/StellaOps.Concelier.Connector.Acsc.Tests/Acsc/AcscConnectorFetchTests.cs @@ -1,187 +1,165 @@ -using System.Collections.Generic; -using System.Globalization; -using System.Net; -using System.Net.Http; -using System.Text; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Http; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Abstractions; -using Microsoft.Extensions.Options; -using Microsoft.Extensions.Time.Testing; -using MongoDB.Bson; -using StellaOps.Concelier.Connector.Acsc; -using StellaOps.Concelier.Connector.Acsc.Configuration; -using StellaOps.Concelier.Connector.Common; -using StellaOps.Concelier.Connector.Common.Http; -using StellaOps.Concelier.Connector.Common.Testing; -using StellaOps.Concelier.Storage.Mongo; -using StellaOps.Concelier.Storage.Mongo; -using StellaOps.Concelier.Testing; -using Xunit; +using System.Collections.Generic; +using System.Globalization; +using System.Net; +using System.Net.Http; +using System.Text; +using Microsoft.Extensions.DependencyInjection; +using StellaOps.Concelier.Bson; +using StellaOps.Concelier.Connector.Acsc; +using StellaOps.Concelier.Connector.Acsc.Configuration; +using StellaOps.Concelier.Connector.Common; +using StellaOps.Concelier.Connector.Common.Http; +using StellaOps.Concelier.Connector.Common.Testing; +using StellaOps.Concelier.Storage; +using StellaOps.Concelier.Testing; +using Xunit; namespace StellaOps.Concelier.Connector.Acsc.Tests.Acsc; -[Collection("mongo-fixture")] -public sealed class AcscConnectorFetchTests : IAsyncLifetime -{ - private static readonly Uri BaseEndpoint = new("https://origin.example/"); - private static readonly Uri RelayEndpoint = new("https://relay.example/"); - private static readonly Uri AlertsDirectUri = new(BaseEndpoint, "/feeds/alerts/rss"); - private static readonly Uri AlertsRelayUri = new(RelayEndpoint, "/feeds/alerts/rss"); - - private readonly MongoIntegrationFixture _fixture; - private readonly FakeTimeProvider _timeProvider; - private readonly CannedHttpMessageHandler _handler; - - public AcscConnectorFetchTests(MongoIntegrationFixture fixture) - { - _fixture = fixture; - _timeProvider = new FakeTimeProvider(new DateTimeOffset(2025, 10, 12, 0, 0, 0, TimeSpan.Zero)); - _handler = new CannedHttpMessageHandler(); - } - - [Fact] - public async Task FetchAsync_DirectSuccessAdvancesCursor() - { - await using var provider = await BuildProviderAsync(preferRelay: false); - - var connector = provider.GetRequiredService(); - SeedRssResponse(AlertsDirectUri, "direct", DateTimeOffset.Parse("2025-10-10T02:15:00Z"), DateTimeOffset.Parse("2025-10-11T05:30:00Z")); - - await connector.FetchAsync(provider, CancellationToken.None); - _handler.AssertNoPendingResponses(); - - var stateRepository = provider.GetRequiredService(); - var state = await stateRepository.TryGetAsync(AcscConnectorPlugin.SourceName, CancellationToken.None); - Assert.NotNull(state); - Assert.Equal("Direct", state!.Cursor.GetValue("preferredEndpoint").AsString); +[Collection(ConcelierFixtureCollection.Name)] +public sealed class AcscConnectorFetchTests +{ + private static readonly Uri BaseEndpoint = new("https://origin.example/"); + private static readonly Uri RelayEndpoint = new("https://relay.example/"); + private static readonly Uri AlertsDirectUri = new(BaseEndpoint, "/feeds/alerts/rss"); + private static readonly Uri AlertsRelayUri = new(RelayEndpoint, "/feeds/alerts/rss"); + + private readonly ConcelierPostgresFixture _fixture; + + public AcscConnectorFetchTests(ConcelierPostgresFixture fixture) + { + _fixture = fixture; + } + + [Fact] + public async Task FetchAsync_DirectSuccessAdvancesCursor() + { + await using var harness = await BuildHarnessAsync(preferRelay: false); + + var connector = harness.ServiceProvider.GetRequiredService(); + SeedRssResponse( + harness.Handler, + AlertsDirectUri, + "direct", + DateTimeOffset.Parse("2025-10-10T02:15:00Z"), + DateTimeOffset.Parse("2025-10-11T05:30:00Z")); + + await connector.FetchAsync(harness.ServiceProvider, CancellationToken.None); + harness.Handler.AssertNoPendingResponses(); + + var stateRepository = harness.ServiceProvider.GetRequiredService(); + var state = await stateRepository.TryGetAsync(AcscConnectorPlugin.SourceName, CancellationToken.None); + Assert.NotNull(state); + Assert.Equal("Direct", state!.Cursor.GetValue("preferredEndpoint").AsString); var feeds = state.Cursor.GetValue("feeds").AsBsonDocument; Assert.True(feeds.TryGetValue("alerts", out var published)); Assert.Equal(DateTime.Parse("2025-10-11T05:30:00Z").ToUniversalTime(), published.ToUniversalTime()); - var pendingDocuments = state.Cursor.GetValue("pendingDocuments").AsBsonArray; - Assert.Single(pendingDocuments); - - var documentStore = provider.GetRequiredService(); - var documentId = Guid.Parse(pendingDocuments[0]!.AsString); - var document = await documentStore.FindAsync(documentId, CancellationToken.None); - Assert.NotNull(document); + var pendingDocuments = state.Cursor.GetValue("pendingDocuments").AsBsonArray; + Assert.Single(pendingDocuments); + + var documentStore = harness.ServiceProvider.GetRequiredService(); + var documentId = Guid.Parse(pendingDocuments[0]!.AsString); + var document = await documentStore.FindAsync(documentId, CancellationToken.None); + Assert.NotNull(document); Assert.Equal(DocumentStatuses.PendingParse, document!.Status); var directMetadata = document.Metadata ?? new Dictionary(StringComparer.Ordinal); Assert.True(directMetadata.TryGetValue("acsc.fetch.mode", out var mode)); Assert.Equal("direct", mode); } - [Fact] - public async Task FetchAsync_DirectFailureFallsBackToRelay() - { - await using var provider = await BuildProviderAsync(preferRelay: false); - - var connector = provider.GetRequiredService(); - _handler.AddException(HttpMethod.Get, AlertsDirectUri, new HttpRequestException("HTTP/2 reset")); - SeedRssResponse(AlertsRelayUri, "relay", DateTimeOffset.Parse("2025-10-09T10:00:00Z"), DateTimeOffset.Parse("2025-10-11T00:00:00Z")); - - await connector.FetchAsync(provider, CancellationToken.None); - _handler.AssertNoPendingResponses(); - - var stateRepository = provider.GetRequiredService(); - var state = await stateRepository.TryGetAsync(AcscConnectorPlugin.SourceName, CancellationToken.None); - Assert.NotNull(state); - Assert.Equal("Relay", state!.Cursor.GetValue("preferredEndpoint").AsString); + [Fact] + public async Task FetchAsync_DirectFailureFallsBackToRelay() + { + await using var harness = await BuildHarnessAsync(preferRelay: false); + + var connector = harness.ServiceProvider.GetRequiredService(); + harness.Handler.AddException(HttpMethod.Get, AlertsDirectUri, new HttpRequestException("HTTP/2 reset")); + SeedRssResponse( + harness.Handler, + AlertsRelayUri, + "relay", + DateTimeOffset.Parse("2025-10-09T10:00:00Z"), + DateTimeOffset.Parse("2025-10-11T00:00:00Z")); + + await connector.FetchAsync(harness.ServiceProvider, CancellationToken.None); + harness.Handler.AssertNoPendingResponses(); + + var stateRepository = harness.ServiceProvider.GetRequiredService(); + var state = await stateRepository.TryGetAsync(AcscConnectorPlugin.SourceName, CancellationToken.None); + Assert.NotNull(state); + Assert.Equal("Relay", state!.Cursor.GetValue("preferredEndpoint").AsString); var feeds = state.Cursor.GetValue("feeds").AsBsonDocument; Assert.True(feeds.TryGetValue("alerts", out var published)); Assert.Equal(DateTime.Parse("2025-10-11T00:00:00Z").ToUniversalTime(), published.ToUniversalTime()); - var pendingDocuments = state.Cursor.GetValue("pendingDocuments").AsBsonArray; - Assert.Single(pendingDocuments); - - var documentStore = provider.GetRequiredService(); - var documentId = Guid.Parse(pendingDocuments[0]!.AsString); - var document = await documentStore.FindAsync(documentId, CancellationToken.None); - Assert.NotNull(document); + var pendingDocuments = state.Cursor.GetValue("pendingDocuments").AsBsonArray; + Assert.Single(pendingDocuments); + + var documentStore = harness.ServiceProvider.GetRequiredService(); + var documentId = Guid.Parse(pendingDocuments[0]!.AsString); + var document = await documentStore.FindAsync(documentId, CancellationToken.None); + Assert.NotNull(document); Assert.Equal(DocumentStatuses.PendingParse, document!.Status); var metadata = document.Metadata ?? new Dictionary(StringComparer.Ordinal); - Assert.True(metadata.TryGetValue("acsc.fetch.mode", out var mode)); - Assert.Equal("relay", mode); - - Assert.Collection(_handler.Requests, - request => - { - Assert.Equal(HttpMethod.Get, request.Method); - Assert.Equal(AlertsDirectUri, request.Uri); + Assert.True(metadata.TryGetValue("acsc.fetch.mode", out var mode)); + Assert.Equal("relay", mode); + + Assert.Collection(harness.Handler.Requests, + request => + { + Assert.Equal(HttpMethod.Get, request.Method); + Assert.Equal(AlertsDirectUri, request.Uri); }, request => { Assert.Equal(HttpMethod.Get, request.Method); - Assert.Equal(AlertsRelayUri, request.Uri); - }); - } - - public async Task InitializeAsync() => await Task.CompletedTask; - - public Task DisposeAsync() => Task.CompletedTask; - - private async Task BuildProviderAsync(bool preferRelay) - { - await _fixture.Client.DropDatabaseAsync(_fixture.Database.DatabaseNamespace.DatabaseName); - _handler.Clear(); - - var services = new ServiceCollection(); - services.AddLogging(builder => builder.AddProvider(NullLoggerProvider.Instance)); - services.AddSingleton(_timeProvider); - - services.AddMongoStorage(options => - { - options.ConnectionString = _fixture.Runner.ConnectionString; - options.DatabaseName = _fixture.Database.DatabaseNamespace.DatabaseName; - options.CommandTimeout = TimeSpan.FromSeconds(5); - }); - - services.AddSourceCommon(); - services.AddAcscConnector(options => - { - options.BaseEndpoint = BaseEndpoint; - options.RelayEndpoint = RelayEndpoint; - options.EnableRelayFallback = true; - options.PreferRelayByDefault = preferRelay; - options.ForceRelay = false; - options.RequestTimeout = TimeSpan.FromSeconds(10); - options.Feeds.Clear(); - options.Feeds.Add(new AcscFeedOptions - { - Slug = "alerts", - RelativePath = "/feeds/alerts/rss", - Enabled = true, - }); - }); - - services.Configure(AcscOptions.HttpClientName, builderOptions => - { - builderOptions.HttpMessageHandlerBuilderActions.Add(builder => builder.PrimaryHandler = _handler); - }); - services.Configure(AcscOptions.HttpClientName, options => - { - options.MaxAttempts = 1; - options.BaseDelay = TimeSpan.Zero; - }); - - var provider = services.BuildServiceProvider(); - var bootstrapper = provider.GetRequiredService(); - await bootstrapper.InitializeAsync(CancellationToken.None); - return provider; - } - - private void SeedRssResponse(Uri uri, string mode, DateTimeOffset first, DateTimeOffset second) - { - var payload = CreateRssPayload(first, second); - _handler.AddResponse(HttpMethod.Get, uri, _ => - { - var response = new HttpResponseMessage(HttpStatusCode.OK) - { - Content = new StringContent(payload, Encoding.UTF8, "application/rss+xml"), + Assert.Equal(AlertsRelayUri, request.Uri); + }); + } + + private async Task BuildHarnessAsync(bool preferRelay) + { + var initialTime = new DateTimeOffset(2025, 10, 12, 0, 0, 0, TimeSpan.Zero); + var harness = new ConnectorTestHarness(_fixture, initialTime, AcscOptions.HttpClientName); + await harness.EnsureServiceProviderAsync(services => + { + services.AddAcscConnector(options => + { + options.BaseEndpoint = BaseEndpoint; + options.RelayEndpoint = RelayEndpoint; + options.EnableRelayFallback = true; + options.PreferRelayByDefault = preferRelay; + options.ForceRelay = false; + options.RequestTimeout = TimeSpan.FromSeconds(10); + options.Feeds.Clear(); + options.Feeds.Add(new AcscFeedOptions + { + Slug = "alerts", + RelativePath = "/feeds/alerts/rss", + Enabled = true, + }); + }); + + services.Configure(AcscOptions.HttpClientName, options => + { + options.MaxAttempts = 1; + options.BaseDelay = TimeSpan.Zero; + }); + }); + return harness; + } + + private static void SeedRssResponse(CannedHttpMessageHandler handler, Uri uri, string mode, DateTimeOffset first, DateTimeOffset second) + { + var payload = CreateRssPayload(first, second); + handler.AddResponse(HttpMethod.Get, uri, _ => + { + var response = new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(payload, Encoding.UTF8, "application/rss+xml"), }; response.Headers.ETag = new System.Net.Http.Headers.EntityTagHeaderValue($"\"{mode}-etag\""); diff --git a/src/Concelier/__Tests/StellaOps.Concelier.Connector.Acsc.Tests/Acsc/AcscConnectorParseTests.cs b/src/Concelier/__Tests/StellaOps.Concelier.Connector.Acsc.Tests/Acsc/AcscConnectorParseTests.cs index 8fc7291de..45b303f02 100644 --- a/src/Concelier/__Tests/StellaOps.Concelier.Connector.Acsc.Tests/Acsc/AcscConnectorParseTests.cs +++ b/src/Concelier/__Tests/StellaOps.Concelier.Connector.Acsc.Tests/Acsc/AcscConnectorParseTests.cs @@ -1,88 +1,78 @@ using System.Linq; using System.Net; -using System.Net.Http; -using System.Net.Http.Headers; -using System.Text; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Http; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Abstractions; -using Microsoft.Extensions.Time.Testing; -using MongoDB.Bson; -using StellaOps.Concelier.Models; -using StellaOps.Concelier.Connector.Acsc; -using StellaOps.Concelier.Connector.Acsc.Configuration; -using StellaOps.Concelier.Connector.Common; -using StellaOps.Concelier.Connector.Common.Http; -using StellaOps.Concelier.Connector.Common.Testing; -using StellaOps.Concelier.Storage.Mongo; -using StellaOps.Concelier.Storage.Mongo.Advisories; -using StellaOps.Concelier.Storage.Mongo; -using StellaOps.Concelier.Storage.Mongo; -using StellaOps.Concelier.Testing; -using Xunit; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text; +using Microsoft.Extensions.DependencyInjection; +using StellaOps.Concelier.Bson; +using StellaOps.Concelier.Models; +using StellaOps.Concelier.Connector.Acsc; +using StellaOps.Concelier.Connector.Acsc.Configuration; +using StellaOps.Concelier.Connector.Common; +using StellaOps.Concelier.Connector.Common.Http; +using StellaOps.Concelier.Connector.Common.Testing; +using StellaOps.Concelier.Storage; +using StellaOps.Concelier.Storage.Advisories; +using StellaOps.Concelier.Testing; +using Xunit; namespace StellaOps.Concelier.Connector.Acsc.Tests.Acsc; -[Collection("mongo-fixture")] -public sealed class AcscConnectorParseTests : IAsyncLifetime -{ - private static readonly Uri BaseEndpoint = new("https://origin.example/"); +[Collection(ConcelierFixtureCollection.Name)] +public sealed class AcscConnectorParseTests +{ + private static readonly Uri BaseEndpoint = new("https://origin.example/"); + + private readonly ConcelierPostgresFixture _fixture; + + public AcscConnectorParseTests(ConcelierPostgresFixture fixture) + { + _fixture = fixture; + } - private readonly MongoIntegrationFixture _fixture; - private readonly FakeTimeProvider _timeProvider; - private readonly CannedHttpMessageHandler _handler; - - public AcscConnectorParseTests(MongoIntegrationFixture fixture) - { - _fixture = fixture; - _timeProvider = new FakeTimeProvider(new DateTimeOffset(2025, 10, 12, 0, 0, 0, TimeSpan.Zero)); - _handler = new CannedHttpMessageHandler(); - } - - [Fact] - public async Task ParseAsync_PersistsDtoAndAdvancesCursor() - { - await using var provider = await BuildProviderAsync(); - var connector = provider.GetRequiredService(); - - var feedUri = new Uri(BaseEndpoint, "/feeds/alerts/rss"); - SeedRssResponse(feedUri); - - await connector.FetchAsync(provider, CancellationToken.None); - _handler.AssertNoPendingResponses(); - - var documentStore = provider.GetRequiredService(); - var document = await documentStore.FindBySourceAndUriAsync(AcscConnectorPlugin.SourceName, feedUri.ToString(), CancellationToken.None); - Assert.NotNull(document); - - await connector.ParseAsync(provider, CancellationToken.None); - - var refreshed = await documentStore.FindAsync(document!.Id, CancellationToken.None); - Assert.NotNull(refreshed); - Assert.Equal(DocumentStatuses.PendingMap, refreshed!.Status); - - var dtoStore = provider.GetRequiredService(); - var dtoRecord = await dtoStore.FindByDocumentIdAsync(document.Id, CancellationToken.None); - Assert.NotNull(dtoRecord); - Assert.Equal("acsc.feed.v1", dtoRecord!.SchemaVersion); + [Fact] + public async Task ParseAsync_PersistsDtoAndAdvancesCursor() + { + await using var harness = await BuildHarnessAsync(); + var connector = harness.ServiceProvider.GetRequiredService(); + + var feedUri = new Uri(BaseEndpoint, "/feeds/alerts/rss"); + SeedRssResponse(harness.Handler, feedUri); + + await connector.FetchAsync(harness.ServiceProvider, CancellationToken.None); + harness.Handler.AssertNoPendingResponses(); + + var documentStore = harness.ServiceProvider.GetRequiredService(); + var document = await documentStore.FindBySourceAndUriAsync(AcscConnectorPlugin.SourceName, feedUri.ToString(), CancellationToken.None); + Assert.NotNull(document); + + await connector.ParseAsync(harness.ServiceProvider, CancellationToken.None); + + var refreshed = await documentStore.FindAsync(document!.Id, CancellationToken.None); + Assert.NotNull(refreshed); + Assert.Equal(DocumentStatuses.PendingMap, refreshed!.Status); + + var dtoStore = harness.ServiceProvider.GetRequiredService(); + var dtoRecord = await dtoStore.FindByDocumentIdAsync(document.Id, CancellationToken.None); + Assert.NotNull(dtoRecord); + Assert.Equal("acsc.feed.v1", dtoRecord!.SchemaVersion); var payload = dtoRecord.Payload; Assert.NotNull(payload); - Assert.Equal("alerts", payload.GetValue("feedSlug").AsString); - Assert.Single(payload.GetValue("entries").AsBsonArray); - - var stateRepository = provider.GetRequiredService(); - var state = await stateRepository.TryGetAsync(AcscConnectorPlugin.SourceName, CancellationToken.None); - Assert.NotNull(state); - Assert.DoesNotContain(document.Id.ToString(), state!.Cursor.GetValue("pendingDocuments").AsBsonArray.Select(v => v.AsString)); - Assert.Contains(document.Id.ToString(), state.Cursor.GetValue("pendingMappings").AsBsonArray.Select(v => v.AsString)); - - await connector.MapAsync(provider, CancellationToken.None); - - var advisoriesStore = provider.GetRequiredService(); - var advisories = await advisoriesStore.GetRecentAsync(10, CancellationToken.None); - Assert.Single(advisories); + Assert.Equal("alerts", payload.GetValue("feedSlug").AsString); + Assert.Single(payload.GetValue("entries").AsBsonArray); + + var stateRepository = harness.ServiceProvider.GetRequiredService(); + var state = await stateRepository.TryGetAsync(AcscConnectorPlugin.SourceName, CancellationToken.None); + Assert.NotNull(state); + Assert.DoesNotContain(document.Id.ToString(), state!.Cursor.GetValue("pendingDocuments").AsBsonArray.Select(v => v.AsString)); + Assert.Contains(document.Id.ToString(), state.Cursor.GetValue("pendingMappings").AsBsonArray.Select(v => v.AsString)); + + await connector.MapAsync(harness.ServiceProvider, CancellationToken.None); + + var advisoriesStore = harness.ServiceProvider.GetRequiredService(); + var advisories = await advisoriesStore.GetRecentAsync(10, CancellationToken.None); + Assert.Single(advisories); var ordered = advisories .OrderBy(static advisory => advisory.AdvisoryKey, StringComparer.OrdinalIgnoreCase) @@ -101,46 +91,46 @@ public sealed class AcscConnectorParseTests : IAsyncLifetime Assert.True(state!.Cursor.GetValue("pendingMappings").AsBsonArray.Count == 0); } - [Fact] - public async Task MapAsync_MultiEntryFeedProducesExpectedSnapshot() - { - await using var provider = await BuildProviderAsync(options => - { - options.Feeds.Clear(); - options.Feeds.Add(new AcscFeedOptions - { - Slug = "multi", - RelativePath = "/feeds/multi/rss", - Enabled = true, - }); - }); - var connector = provider.GetRequiredService(); - - var feedUri = new Uri(BaseEndpoint, "/feeds/multi/rss"); - SeedMultiEntryResponse(feedUri); - - await connector.FetchAsync(provider, CancellationToken.None); - await connector.ParseAsync(provider, CancellationToken.None); - - var documentStore = provider.GetRequiredService(); - var dtoStore = provider.GetRequiredService(); - var document = await documentStore.FindBySourceAndUriAsync(AcscConnectorPlugin.SourceName, feedUri.ToString(), CancellationToken.None); - Assert.NotNull(document); - var dtoRecord = await dtoStore.FindByDocumentIdAsync(document!.Id, CancellationToken.None); + [Fact] + public async Task MapAsync_MultiEntryFeedProducesExpectedSnapshot() + { + await using var harness = await BuildHarnessAsync(options => + { + options.Feeds.Clear(); + options.Feeds.Add(new AcscFeedOptions + { + Slug = "multi", + RelativePath = "/feeds/multi/rss", + Enabled = true, + }); + }); + var connector = harness.ServiceProvider.GetRequiredService(); + + var feedUri = new Uri(BaseEndpoint, "/feeds/multi/rss"); + SeedMultiEntryResponse(harness.Handler, feedUri); + + await connector.FetchAsync(harness.ServiceProvider, CancellationToken.None); + await connector.ParseAsync(harness.ServiceProvider, CancellationToken.None); + + var documentStore = harness.ServiceProvider.GetRequiredService(); + var dtoStore = harness.ServiceProvider.GetRequiredService(); + var document = await documentStore.FindBySourceAndUriAsync(AcscConnectorPlugin.SourceName, feedUri.ToString(), CancellationToken.None); + Assert.NotNull(document); + var dtoRecord = await dtoStore.FindByDocumentIdAsync(document!.Id, CancellationToken.None); Assert.NotNull(dtoRecord); var payload = dtoRecord!.Payload; Assert.NotNull(payload); var entries = payload.GetValue("entries").AsBsonArray; Assert.Equal(2, entries.Count); var fields = entries[0].AsBsonDocument.GetValue("fields").AsBsonDocument; - Assert.Equal("Critical", fields.GetValue("severity").AsString); - Assert.Equal("ExampleCo Router X, ExampleCo Router Y", fields.GetValue("systemsAffected").AsString); - - await connector.MapAsync(provider, CancellationToken.None); - - var advisoryStore = provider.GetRequiredService(); - var advisories = await advisoryStore.GetRecentAsync(10, CancellationToken.None); - Assert.Equal(2, advisories.Count); + Assert.Equal("Critical", fields.GetValue("severity").AsString); + Assert.Equal("ExampleCo Router X, ExampleCo Router Y", fields.GetValue("systemsAffected").AsString); + + await connector.MapAsync(harness.ServiceProvider, CancellationToken.None); + + var advisoryStore = harness.ServiceProvider.GetRequiredService(); + var advisories = await advisoryStore.GetRecentAsync(10, CancellationToken.None); + Assert.Equal(2, advisories.Count); var ordered = advisories .OrderBy(static advisory => advisory.AdvisoryKey, StringComparer.OrdinalIgnoreCase) @@ -155,67 +145,44 @@ public sealed class AcscConnectorParseTests : IAsyncLifetime Assert.Equal("critical", ordered.First(a => a.Severity is not null).Severity, StringComparer.OrdinalIgnoreCase); } - public Task InitializeAsync() => Task.CompletedTask; + private async Task BuildHarnessAsync(Action? configure = null) + { + var initialTime = new DateTimeOffset(2025, 10, 12, 0, 0, 0, TimeSpan.Zero); + var harness = new ConnectorTestHarness(_fixture, initialTime, AcscOptions.HttpClientName); + await harness.EnsureServiceProviderAsync(services => + { + services.AddAcscConnector(options => + { + options.BaseEndpoint = BaseEndpoint; + options.RelayEndpoint = null; + options.PreferRelayByDefault = false; + options.ForceRelay = false; + options.EnableRelayFallback = false; + options.RequestTimeout = TimeSpan.FromSeconds(10); + options.Feeds.Clear(); + options.Feeds.Add(new AcscFeedOptions + { + Slug = "alerts", + RelativePath = "/feeds/alerts/rss", + Enabled = true, + }); + configure?.Invoke(options); + }); + + services.Configure(AcscOptions.HttpClientName, options => + { + options.MaxAttempts = 1; + options.BaseDelay = TimeSpan.Zero; + }); + }); + return harness; + } - public Task DisposeAsync() => Task.CompletedTask; - - private async Task BuildProviderAsync(Action? configure = null) - { - await _fixture.Client.DropDatabaseAsync(_fixture.Database.DatabaseNamespace.DatabaseName); - _handler.Clear(); - - var services = new ServiceCollection(); - services.AddLogging(builder => builder.AddProvider(NullLoggerProvider.Instance)); - services.AddSingleton(_timeProvider); - - services.AddMongoStorage(options => - { - options.ConnectionString = _fixture.Runner.ConnectionString; - options.DatabaseName = _fixture.Database.DatabaseNamespace.DatabaseName; - options.CommandTimeout = TimeSpan.FromSeconds(5); - }); - - services.AddSourceCommon(); - services.AddAcscConnector(options => - { - options.BaseEndpoint = BaseEndpoint; - options.RelayEndpoint = null; - options.PreferRelayByDefault = false; - options.ForceRelay = false; - options.EnableRelayFallback = false; - options.RequestTimeout = TimeSpan.FromSeconds(10); - options.Feeds.Clear(); - options.Feeds.Add(new AcscFeedOptions - { - Slug = "alerts", - RelativePath = "/feeds/alerts/rss", - Enabled = true, - }); - configure?.Invoke(options); - }); - - services.Configure(AcscOptions.HttpClientName, builderOptions => - { - builderOptions.HttpMessageHandlerBuilderActions.Add(builder => builder.PrimaryHandler = _handler); - }); - - services.Configure(AcscOptions.HttpClientName, options => - { - options.MaxAttempts = 1; - options.BaseDelay = TimeSpan.Zero; - }); - - var provider = services.BuildServiceProvider(); - var bootstrapper = provider.GetRequiredService(); - await bootstrapper.InitializeAsync(CancellationToken.None); - return provider; - } - - private void SeedRssResponse(Uri uri) - { - const string payload = """ - - + private static void SeedRssResponse(CannedHttpMessageHandler handler, Uri uri) + { + const string payload = """ + + ACSC Alerts https://origin.example/feeds/alerts @@ -233,26 +200,26 @@ public sealed class AcscConnectorParseTests : IAsyncLifetime ]]> - - """; - - _handler.AddResponse(HttpMethod.Get, uri, () => - { - var response = new HttpResponseMessage(HttpStatusCode.OK) - { - Content = new StringContent(payload, Encoding.UTF8, "application/rss+xml"), + + """; + + handler.AddResponse(HttpMethod.Get, uri, () => + { + var response = new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(payload, Encoding.UTF8, "application/rss+xml"), }; response.Headers.ETag = new EntityTagHeaderValue("\"parse-etag\""); response.Content.Headers.LastModified = new DateTimeOffset(2025, 10, 12, 4, 20, 0, TimeSpan.Zero); - return response; - }); - } - - private void SeedMultiEntryResponse(Uri uri) - { - const string payload = """ - - + return response; + }); + } + + private static void SeedMultiEntryResponse(CannedHttpMessageHandler handler, Uri uri) + { + const string payload = """ + + ACSC Advisories https://origin.example/feeds/advisories @@ -282,14 +249,14 @@ public sealed class AcscConnectorParseTests : IAsyncLifetime ]]> - - """; - - _handler.AddResponse(HttpMethod.Get, uri, () => - { - var response = new HttpResponseMessage(HttpStatusCode.OK) - { - Content = new StringContent(payload, Encoding.UTF8, "application/rss+xml"), + + """; + + handler.AddResponse(HttpMethod.Get, uri, () => + { + var response = new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(payload, Encoding.UTF8, "application/rss+xml"), }; response.Headers.ETag = new EntityTagHeaderValue("\"multi-etag\""); response.Content.Headers.LastModified = new DateTimeOffset(2025, 10, 12, 5, 0, 0, TimeSpan.Zero); diff --git a/src/Concelier/__Tests/StellaOps.Concelier.Connector.Cccs.Tests/CccsConnectorTests.cs b/src/Concelier/__Tests/StellaOps.Concelier.Connector.Cccs.Tests/CccsConnectorTests.cs index 6fdebebdc..1e3e932a0 100644 --- a/src/Concelier/__Tests/StellaOps.Concelier.Connector.Cccs.Tests/CccsConnectorTests.cs +++ b/src/Concelier/__Tests/StellaOps.Concelier.Connector.Cccs.Tests/CccsConnectorTests.cs @@ -1,59 +1,50 @@ using System; using System.Net.Http; using System.Net.Http.Headers; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using FluentAssertions; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Http; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Abstractions; -using Microsoft.Extensions.Options; -using MongoDB.Bson; -using MongoDB.Driver; -using StellaOps.Concelier.Connector.Cccs; -using StellaOps.Concelier.Connector.Cccs.Configuration; -using StellaOps.Concelier.Connector.Common; -using StellaOps.Concelier.Connector.Common.Http; -using StellaOps.Concelier.Connector.Common.Testing; -using StellaOps.Concelier.Storage.Mongo; -using StellaOps.Concelier.Storage.Mongo.Advisories; -using StellaOps.Concelier.Storage.Mongo; -using StellaOps.Concelier.Testing; -using Xunit; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using FluentAssertions; +using Microsoft.Extensions.DependencyInjection; +using StellaOps.Concelier.Bson; +using StellaOps.Concelier.Connector.Cccs; +using StellaOps.Concelier.Connector.Cccs.Configuration; +using StellaOps.Concelier.Connector.Common; +using StellaOps.Concelier.Connector.Common.Testing; +using StellaOps.Concelier.Storage; +using StellaOps.Concelier.Storage.Advisories; +using StellaOps.Concelier.Testing; +using Xunit; namespace StellaOps.Concelier.Connector.Cccs.Tests; -[Collection("mongo-fixture")] -public sealed class CccsConnectorTests : IAsyncLifetime -{ +[Collection(ConcelierFixtureCollection.Name)] +public sealed class CccsConnectorTests +{ private static readonly Uri FeedUri = new("https://test.local/api/cccs/threats/v1/get?lang=en&content_type=cccs_threat"); - private static readonly Uri TaxonomyUri = new("https://test.local/api/cccs/taxonomy/v1/get?lang=en&vocabulary=cccs_alert_type"); + private static readonly Uri TaxonomyUri = new("https://test.local/api/cccs/taxonomy/v1/get?lang=en&vocabulary=cccs_alert_type"); + + private readonly ConcelierPostgresFixture _fixture; + + public CccsConnectorTests(ConcelierPostgresFixture fixture) + { + _fixture = fixture; + } - private readonly MongoIntegrationFixture _fixture; - private readonly CannedHttpMessageHandler _handler; - - public CccsConnectorTests(MongoIntegrationFixture fixture) - { - _fixture = fixture; - _handler = new CannedHttpMessageHandler(); - } - - [Fact] - public async Task FetchParseMap_ProducesCanonicalAdvisory() - { - await using var provider = await BuildServiceProviderAsync(); - SeedFeedResponses(); - - var connector = provider.GetRequiredService(); - await connector.FetchAsync(provider, CancellationToken.None); - await connector.ParseAsync(provider, CancellationToken.None); - await connector.MapAsync(provider, CancellationToken.None); - - var advisoryStore = provider.GetRequiredService(); - var advisories = await advisoryStore.GetRecentAsync(10, CancellationToken.None); - advisories.Should().HaveCount(1); + [Fact] + public async Task FetchParseMap_ProducesCanonicalAdvisory() + { + await using var harness = await BuildHarnessAsync(); + SeedFeedResponses(harness.Handler); + + var connector = harness.ServiceProvider.GetRequiredService(); + await connector.FetchAsync(harness.ServiceProvider, CancellationToken.None); + await connector.ParseAsync(harness.ServiceProvider, CancellationToken.None); + await connector.MapAsync(harness.ServiceProvider, CancellationToken.None); + + var advisoryStore = harness.ServiceProvider.GetRequiredService(); + var advisories = await advisoryStore.GetRecentAsync(10, CancellationToken.None); + advisories.Should().HaveCount(1); var advisory = advisories[0]; advisory.AdvisoryKey.Should().Be("TEST-001"); @@ -64,9 +55,9 @@ public sealed class CccsConnectorTests : IAsyncLifetime advisory.AffectedPackages.Should().ContainSingle(pkg => pkg.Identifier == "Vendor Widget 1.0"); advisory.AffectedPackages.Should().Contain(pkg => pkg.Identifier == "Vendor Widget 2.0"); - var stateRepository = provider.GetRequiredService(); - var state = await stateRepository.TryGetAsync(CccsConnectorPlugin.SourceName, CancellationToken.None); - state.Should().NotBeNull(); + var stateRepository = harness.ServiceProvider.GetRequiredService(); + var state = await stateRepository.TryGetAsync(CccsConnectorPlugin.SourceName, CancellationToken.None); + state.Should().NotBeNull(); state!.Cursor.Should().NotBeNull(); state.Cursor.TryGetValue("pendingDocuments", out var pendingDocs).Should().BeTrue(); pendingDocs!.AsBsonArray.Should().BeEmpty(); @@ -74,99 +65,65 @@ public sealed class CccsConnectorTests : IAsyncLifetime pendingMappings!.AsBsonArray.Should().BeEmpty(); } - [Fact] - public async Task Fetch_PersistsRawDocumentWithMetadata() - { - await using var provider = await BuildServiceProviderAsync(); - SeedFeedResponses(); - - var connector = provider.GetRequiredService(); - await connector.FetchAsync(provider, CancellationToken.None); - - var mongo = provider.GetRequiredService(); - var docCollection = mongo.GetCollection("document"); - var documentsSnapshot = await docCollection.Find(FilterDefinition.Empty).ToListAsync(); - - System.IO.Directory.CreateDirectory(System.IO.Path.Combine(AppContext.BaseDirectory, "tmp")); - var debugPath = System.IO.Path.Combine(AppContext.BaseDirectory, "tmp", "cccs-documents.json"); - await System.IO.File.WriteAllTextAsync(debugPath, documentsSnapshot.ToJson(new MongoDB.Bson.IO.JsonWriterSettings { Indent = true })); - - var documentStore = provider.GetRequiredService(); - var document = await documentStore.FindBySourceAndUriAsync(CccsConnectorPlugin.SourceName, "https://www.cyber.gc.ca/en/alerts-advisories/test-advisory", CancellationToken.None); - document.Should().NotBeNull(); - document!.Status.Should().Be(DocumentStatuses.PendingParse); - document.Metadata.Should().ContainKey("cccs.language").WhoseValue.Should().Be("en"); + [Fact] + public async Task Fetch_PersistsRawDocumentWithMetadata() + { + await using var harness = await BuildHarnessAsync(); + SeedFeedResponses(harness.Handler); + + var connector = harness.ServiceProvider.GetRequiredService(); + await connector.FetchAsync(harness.ServiceProvider, CancellationToken.None); + + var documentStore = harness.ServiceProvider.GetRequiredService(); + var document = await documentStore.FindBySourceAndUriAsync(CccsConnectorPlugin.SourceName, "https://www.cyber.gc.ca/en/alerts-advisories/test-advisory", CancellationToken.None); + document.Should().NotBeNull(); + document!.Status.Should().Be(DocumentStatuses.PendingParse); + document.Metadata.Should().ContainKey("cccs.language").WhoseValue.Should().Be("en"); document.Metadata.Should().ContainKey("cccs.serialNumber").WhoseValue.Should().Be("TEST-001"); document.ContentType.Should().Be("application/json"); } - private async Task BuildServiceProviderAsync() - { - await _fixture.Client.DropDatabaseAsync(_fixture.Database.DatabaseNamespace.DatabaseName); - _handler.Clear(); - - var services = new ServiceCollection(); - services.AddLogging(builder => builder.AddProvider(NullLoggerProvider.Instance)); - services.AddSingleton(_handler); - - services.AddMongoStorage(options => - { - options.ConnectionString = _fixture.Runner.ConnectionString; - options.DatabaseName = _fixture.Database.DatabaseNamespace.DatabaseName; - options.CommandTimeout = TimeSpan.FromSeconds(5); - }); - - services.AddSourceCommon(); - services.AddCccsConnector(options => - { - options.Feeds.Clear(); - options.Feeds.Add(new CccsFeedEndpoint("en", FeedUri)); - options.RequestDelay = TimeSpan.Zero; - options.MaxEntriesPerFetch = 10; - options.MaxKnownEntries = 32; - }); - - services.Configure(CccsOptions.HttpClientName, builderOptions => - { - builderOptions.HttpMessageHandlerBuilderActions.Add(builder => - { - builder.PrimaryHandler = _handler; - }); - }); - - var provider = services.BuildServiceProvider(); - var bootstrapper = provider.GetRequiredService(); - await bootstrapper.InitializeAsync(CancellationToken.None); - return provider; - } - - private void SeedFeedResponses() - { - AddJsonResponse(FeedUri, ReadFixture("cccs-feed-en.json")); - AddJsonResponse(TaxonomyUri, ReadFixture("cccs-taxonomy-en.json")); - } - - private void AddJsonResponse(Uri uri, string json, string? etag = null) - { - _handler.AddResponse(uri, () => - { - var response = new HttpResponseMessage(System.Net.HttpStatusCode.OK) - { - Content = new StringContent(json, Encoding.UTF8, "application/json"), + private async Task BuildHarnessAsync() + { + var initialTime = new DateTimeOffset(2025, 10, 12, 0, 0, 0, TimeSpan.Zero); + var harness = new ConnectorTestHarness(_fixture, initialTime, CccsOptions.HttpClientName); + await harness.EnsureServiceProviderAsync(services => + { + services.AddCccsConnector(options => + { + options.Feeds.Clear(); + options.Feeds.Add(new CccsFeedEndpoint("en", FeedUri)); + options.RequestDelay = TimeSpan.Zero; + options.MaxEntriesPerFetch = 10; + options.MaxKnownEntries = 32; + }); + }); + return harness; + } + + private static void SeedFeedResponses(CannedHttpMessageHandler handler) + { + AddJsonResponse(handler, FeedUri, ReadFixture("cccs-feed-en.json")); + AddJsonResponse(handler, TaxonomyUri, ReadFixture("cccs-taxonomy-en.json")); + } + + private static void AddJsonResponse(CannedHttpMessageHandler handler, Uri uri, string json, string? etag = null) + { + handler.AddResponse(uri, () => + { + var response = new HttpResponseMessage(System.Net.HttpStatusCode.OK) + { + Content = new StringContent(json, Encoding.UTF8, "application/json"), }; if (!string.IsNullOrWhiteSpace(etag)) { response.Headers.ETag = new EntityTagHeaderValue(etag); } - - return response; - }); - } - - private static string ReadFixture(string fileName) - => System.IO.File.ReadAllText(System.IO.Path.Combine(AppContext.BaseDirectory, "Fixtures", fileName)); - - public Task InitializeAsync() => Task.CompletedTask; - - public Task DisposeAsync() => Task.CompletedTask; -} + + return response; + }); + } + + private static string ReadFixture(string fileName) + => System.IO.File.ReadAllText(System.IO.Path.Combine(AppContext.BaseDirectory, "Fixtures", fileName)); +} diff --git a/src/Concelier/__Tests/StellaOps.Concelier.Connector.Cccs.Tests/Internal/CccsMapperTests.cs b/src/Concelier/__Tests/StellaOps.Concelier.Connector.Cccs.Tests/Internal/CccsMapperTests.cs index 1c4a7339a..b711ad613 100644 --- a/src/Concelier/__Tests/StellaOps.Concelier.Connector.Cccs.Tests/Internal/CccsMapperTests.cs +++ b/src/Concelier/__Tests/StellaOps.Concelier.Connector.Cccs.Tests/Internal/CccsMapperTests.cs @@ -4,7 +4,7 @@ using StellaOps.Concelier.Connector.Cccs.Internal; using StellaOps.Concelier.Connector.Common; using StellaOps.Concelier.Connector.Common.Html; using StellaOps.Concelier.Models; -using StellaOps.Concelier.Storage.Mongo; +using StellaOps.Concelier.Storage; using Xunit; namespace StellaOps.Concelier.Connector.Cccs.Tests.Internal; diff --git a/src/Concelier/__Tests/StellaOps.Concelier.Connector.CertBund.Tests/CertBundConnectorTests.cs b/src/Concelier/__Tests/StellaOps.Concelier.Connector.CertBund.Tests/CertBundConnectorTests.cs index 6d8a051a2..bd76ab76e 100644 --- a/src/Concelier/__Tests/StellaOps.Concelier.Connector.CertBund.Tests/CertBundConnectorTests.cs +++ b/src/Concelier/__Tests/StellaOps.Concelier.Connector.CertBund.Tests/CertBundConnectorTests.cs @@ -1,61 +1,51 @@ using System; using System.Net.Http; using System.Net.Http.Headers; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using FluentAssertions; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Http; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Abstractions; -using Microsoft.Extensions.Options; -using MongoDB.Bson; -using StellaOps.Concelier.Connector.CertBund.Configuration; -using StellaOps.Concelier.Connector.Common.Http; -using StellaOps.Concelier.Connector.Common; -using StellaOps.Concelier.Connector.Common.Fetch; -using StellaOps.Concelier.Connector.Common.Testing; -using StellaOps.Concelier.Models; -using StellaOps.Concelier.Storage.Mongo; -using StellaOps.Concelier.Storage.Mongo.Advisories; -using StellaOps.Concelier.Storage.Mongo; -using StellaOps.Concelier.Storage.Mongo; -using StellaOps.Concelier.Testing; -using Xunit; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using FluentAssertions; +using Microsoft.Extensions.DependencyInjection; +using StellaOps.Concelier.Bson; +using StellaOps.Concelier.Connector.CertBund.Configuration; +using StellaOps.Concelier.Connector.Common; +using StellaOps.Concelier.Connector.Common.Testing; +using StellaOps.Concelier.Models; +using StellaOps.Concelier.Storage; +using StellaOps.Concelier.Storage.Advisories; +using StellaOps.Concelier.Testing; +using Xunit; namespace StellaOps.Concelier.Connector.CertBund.Tests; -[Collection("mongo-fixture")] -public sealed class CertBundConnectorTests : IAsyncLifetime -{ +[Collection(ConcelierFixtureCollection.Name)] +public sealed class CertBundConnectorTests +{ private static readonly Uri FeedUri = new("https://test.local/content/public/securityAdvisory/rss"); private static readonly Uri PortalUri = new("https://test.local/portal/"); - private static readonly Uri DetailUri = new("https://test.local/portal/api/securityadvisory?name=WID-SEC-2025-2264"); + private static readonly Uri DetailUri = new("https://test.local/portal/api/securityadvisory?name=WID-SEC-2025-2264"); + + private readonly ConcelierPostgresFixture _fixture; + + public CertBundConnectorTests(ConcelierPostgresFixture fixture) + { + _fixture = fixture; + } - private readonly MongoIntegrationFixture _fixture; - private readonly CannedHttpMessageHandler _handler; - - public CertBundConnectorTests(MongoIntegrationFixture fixture) - { - _fixture = fixture; - _handler = new CannedHttpMessageHandler(); - } - - [Fact] - public async Task FetchParseMap_ProducesCanonicalAdvisory() - { - await using var provider = await BuildServiceProviderAsync(); - SeedResponses(); - - var connector = provider.GetRequiredService(); - await connector.FetchAsync(provider, CancellationToken.None); - await connector.ParseAsync(provider, CancellationToken.None); - await connector.MapAsync(provider, CancellationToken.None); - - var advisoryStore = provider.GetRequiredService(); - var advisories = await advisoryStore.GetRecentAsync(5, CancellationToken.None); - advisories.Should().HaveCount(1); + [Fact] + public async Task FetchParseMap_ProducesCanonicalAdvisory() + { + await using var harness = await BuildHarnessAsync(); + SeedResponses(harness.Handler); + + var connector = harness.ServiceProvider.GetRequiredService(); + await connector.FetchAsync(harness.ServiceProvider, CancellationToken.None); + await connector.ParseAsync(harness.ServiceProvider, CancellationToken.None); + await connector.MapAsync(harness.ServiceProvider, CancellationToken.None); + + var advisoryStore = harness.ServiceProvider.GetRequiredService(); + var advisories = await advisoryStore.GetRecentAsync(5, CancellationToken.None); + advisories.Should().HaveCount(1); var advisory = advisories[0]; advisory.AdvisoryKey.Should().Be("WID-SEC-2025-2264"); @@ -75,9 +65,9 @@ public sealed class CertBundConnectorTests : IAsyncLifetime rule.Max == "2024.2" && rule.Notes == "certbund:WID-SEC-2025-2264:ivanti"); - var stateRepository = provider.GetRequiredService(); - var state = await stateRepository.TryGetAsync(CertBundConnectorPlugin.SourceName, CancellationToken.None); - state.Should().NotBeNull(); + var stateRepository = harness.ServiceProvider.GetRequiredService(); + var state = await stateRepository.TryGetAsync(CertBundConnectorPlugin.SourceName, CancellationToken.None); + state.Should().NotBeNull(); state!.Cursor.Should().NotBeNull(); state.Cursor.TryGetValue("pendingDocuments", out var pendingDocs).Should().BeTrue(); pendingDocs!.AsBsonArray.Should().BeEmpty(); @@ -85,86 +75,64 @@ public sealed class CertBundConnectorTests : IAsyncLifetime pendingMappings!.AsBsonArray.Should().BeEmpty(); } - [Fact] - public async Task Fetch_PersistsDocumentWithMetadata() - { - await using var provider = await BuildServiceProviderAsync(); - SeedResponses(); - - var connector = provider.GetRequiredService(); - await connector.FetchAsync(provider, CancellationToken.None); - - var documentStore = provider.GetRequiredService(); - var document = await documentStore.FindBySourceAndUriAsync(CertBundConnectorPlugin.SourceName, DetailUri.ToString(), CancellationToken.None); - document.Should().NotBeNull(); + [Fact] + public async Task Fetch_PersistsDocumentWithMetadata() + { + await using var harness = await BuildHarnessAsync(); + SeedResponses(harness.Handler); + + var connector = harness.ServiceProvider.GetRequiredService(); + await connector.FetchAsync(harness.ServiceProvider, CancellationToken.None); + + var documentStore = harness.ServiceProvider.GetRequiredService(); + var document = await documentStore.FindBySourceAndUriAsync(CertBundConnectorPlugin.SourceName, DetailUri.ToString(), CancellationToken.None); + document.Should().NotBeNull(); document!.Metadata.Should().ContainKey("certbund.advisoryId").WhoseValue.Should().Be("WID-SEC-2025-2264"); document.Metadata.Should().ContainKey("certbund.category"); - document.Metadata.Should().ContainKey("certbund.published"); - document.Status.Should().Be(DocumentStatuses.PendingParse); - - var stateRepository = provider.GetRequiredService(); - var state = await stateRepository.TryGetAsync(CertBundConnectorPlugin.SourceName, CancellationToken.None); - state.Should().NotBeNull(); + document.Metadata.Should().ContainKey("certbund.published"); + document.Status.Should().Be(DocumentStatuses.PendingParse); + + var stateRepository = harness.ServiceProvider.GetRequiredService(); + var state = await stateRepository.TryGetAsync(CertBundConnectorPlugin.SourceName, CancellationToken.None); + state.Should().NotBeNull(); state!.Cursor.Should().NotBeNull(); - state.Cursor.TryGetValue("pendingDocuments", out var pendingDocs).Should().BeTrue(); - pendingDocs!.AsBsonArray.Should().HaveCount(1); - } - - private async Task BuildServiceProviderAsync() - { - await _fixture.Client.DropDatabaseAsync(_fixture.Database.DatabaseNamespace.DatabaseName); - _handler.Clear(); - - var services = new ServiceCollection(); - services.AddLogging(builder => builder.AddProvider(NullLoggerProvider.Instance)); - services.AddSingleton(_handler); - - services.AddMongoStorage(options => - { - options.ConnectionString = _fixture.Runner.ConnectionString; - options.DatabaseName = _fixture.Database.DatabaseNamespace.DatabaseName; - options.CommandTimeout = TimeSpan.FromSeconds(5); - }); - - services.AddSourceCommon(); - services.AddCertBundConnector(options => - { - options.FeedUri = FeedUri; - options.PortalBootstrapUri = PortalUri; - options.DetailApiUri = new Uri("https://test.local/portal/api/securityadvisory"); - options.RequestDelay = TimeSpan.Zero; - options.MaxAdvisoriesPerFetch = 10; - options.MaxKnownAdvisories = 32; - }); - - services.Configure(CertBundOptions.HttpClientName, builderOptions => - { - builderOptions.HttpMessageHandlerBuilderActions.Add(builder => - { - builder.PrimaryHandler = _handler; - }); - }); - - var provider = services.BuildServiceProvider(); - var bootstrapper = provider.GetRequiredService(); - await bootstrapper.InitializeAsync(CancellationToken.None); - return provider; - } - - private void SeedResponses() - { - AddJsonResponse(DetailUri, ReadFixture("certbund-detail.json")); - AddXmlResponse(FeedUri, ReadFixture("certbund-feed.xml"), "application/rss+xml"); - AddHtmlResponse(PortalUri, "OK"); - } - - private void AddJsonResponse(Uri uri, string json, string? etag = null) - { - _handler.AddResponse(uri, () => - { - var response = new HttpResponseMessage(System.Net.HttpStatusCode.OK) - { - Content = new StringContent(json, Encoding.UTF8, "application/json"), + state.Cursor.TryGetValue("pendingDocuments", out var pendingDocs).Should().BeTrue(); + pendingDocs!.AsBsonArray.Should().HaveCount(1); + } + + private async Task BuildHarnessAsync() + { + var initialTime = new DateTimeOffset(2025, 10, 12, 0, 0, 0, TimeSpan.Zero); + var harness = new ConnectorTestHarness(_fixture, initialTime, CertBundOptions.HttpClientName); + await harness.EnsureServiceProviderAsync(services => + { + services.AddCertBundConnector(options => + { + options.FeedUri = FeedUri; + options.PortalBootstrapUri = PortalUri; + options.DetailApiUri = new Uri("https://test.local/portal/api/securityadvisory"); + options.RequestDelay = TimeSpan.Zero; + options.MaxAdvisoriesPerFetch = 10; + options.MaxKnownAdvisories = 32; + }); + }); + return harness; + } + + private static void SeedResponses(CannedHttpMessageHandler handler) + { + AddJsonResponse(handler, DetailUri, ReadFixture("certbund-detail.json")); + AddXmlResponse(handler, FeedUri, ReadFixture("certbund-feed.xml"), "application/rss+xml"); + AddHtmlResponse(handler, PortalUri, "OK"); + } + + private static void AddJsonResponse(CannedHttpMessageHandler handler, Uri uri, string json, string? etag = null) + { + handler.AddResponse(uri, () => + { + var response = new HttpResponseMessage(System.Net.HttpStatusCode.OK) + { + Content = new StringContent(json, Encoding.UTF8, "application/json"), }; if (!string.IsNullOrWhiteSpace(etag)) { @@ -172,29 +140,25 @@ public sealed class CertBundConnectorTests : IAsyncLifetime } return response; - }); - } + }); + } + + private static void AddXmlResponse(CannedHttpMessageHandler handler, Uri uri, string xml, string contentType) + { + handler.AddResponse(uri, () => new HttpResponseMessage(System.Net.HttpStatusCode.OK) + { + Content = new StringContent(xml, Encoding.UTF8, contentType), + }); + } + + private static void AddHtmlResponse(CannedHttpMessageHandler handler, Uri uri, string html) + { + handler.AddResponse(uri, () => new HttpResponseMessage(System.Net.HttpStatusCode.OK) + { + Content = new StringContent(html, Encoding.UTF8, "text/html"), + }); + } - private void AddXmlResponse(Uri uri, string xml, string contentType) - { - _handler.AddResponse(uri, () => new HttpResponseMessage(System.Net.HttpStatusCode.OK) - { - Content = new StringContent(xml, Encoding.UTF8, contentType), - }); - } - - private void AddHtmlResponse(Uri uri, string html) - { - _handler.AddResponse(uri, () => new HttpResponseMessage(System.Net.HttpStatusCode.OK) - { - Content = new StringContent(html, Encoding.UTF8, "text/html"), - }); - } - - private static string ReadFixture(string fileName) - => System.IO.File.ReadAllText(System.IO.Path.Combine(AppContext.BaseDirectory, "Fixtures", fileName)); - - public Task InitializeAsync() => Task.CompletedTask; - - public Task DisposeAsync() => Task.CompletedTask; -} + private static string ReadFixture(string fileName) + => System.IO.File.ReadAllText(System.IO.Path.Combine(AppContext.BaseDirectory, "Fixtures", fileName)); +} diff --git a/src/Concelier/__Tests/StellaOps.Concelier.Connector.CertCc.Tests/CertCc/CertCcConnectorFetchTests.cs b/src/Concelier/__Tests/StellaOps.Concelier.Connector.CertCc.Tests/CertCc/CertCcConnectorFetchTests.cs index cac223b7a..23b8c5aa7 100644 --- a/src/Concelier/__Tests/StellaOps.Concelier.Connector.CertCc.Tests/CertCc/CertCcConnectorFetchTests.cs +++ b/src/Concelier/__Tests/StellaOps.Concelier.Connector.CertCc.Tests/CertCc/CertCcConnectorFetchTests.cs @@ -6,35 +6,36 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Http; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Abstractions; -using Microsoft.Extensions.Time.Testing; -using MongoDB.Bson; -using StellaOps.Concelier.Connector.CertCc; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Time.Testing; +using StellaOps.Concelier.Bson; +using StellaOps.Concelier.Connector.CertCc; using StellaOps.Concelier.Connector.CertCc.Configuration; using StellaOps.Concelier.Connector.CertCc.Internal; using StellaOps.Concelier.Connector.Common; using StellaOps.Concelier.Connector.Common.Http; -using StellaOps.Concelier.Connector.Common.Cursors; -using StellaOps.Concelier.Connector.Common.Testing; -using StellaOps.Concelier.Storage.Mongo; -using StellaOps.Concelier.Storage.Mongo; -using StellaOps.Concelier.Testing; -using Xunit; +using StellaOps.Concelier.Connector.Common.Cursors; +using StellaOps.Concelier.Connector.Common.Testing; +using StellaOps.Concelier.Storage; +using StellaOps.Concelier.Storage; +using StellaOps.Concelier.Storage.Postgres; +using StellaOps.Concelier.Testing; +using Xunit; namespace StellaOps.Concelier.Connector.CertCc.Tests.CertCc; -[Collection("mongo-fixture")] +[Collection(ConcelierFixtureCollection.Name)] public sealed class CertCcConnectorFetchTests : IAsyncLifetime { private const string TestNoteId = "294418"; - private readonly MongoIntegrationFixture _fixture; + private readonly ConcelierPostgresFixture _fixture; private readonly FakeTimeProvider _timeProvider; private readonly CannedHttpMessageHandler _handler; private ServiceProvider? _serviceProvider; - public CertCcConnectorFetchTests(MongoIntegrationFixture fixture) + public CertCcConnectorFetchTests(ConcelierPostgresFixture fixture) { _fixture = fixture; _timeProvider = new FakeTimeProvider(new DateTimeOffset(2025, 10, 11, 8, 0, 0, TimeSpan.Zero)); @@ -172,25 +173,22 @@ public sealed class CertCcConnectorFetchTests : IAsyncLifetime yield return new Uri(baseUri, $"{TestNoteId}/vuls/"); } - private async Task EnsureServiceProviderAsync(CertCcOptions template) - { - await DisposeServiceProviderAsync(); - await _fixture.Client.DropDatabaseAsync(_fixture.Database.DatabaseNamespace.DatabaseName); + private async Task EnsureServiceProviderAsync(CertCcOptions template) + { + await DisposeServiceProviderAsync(); + await _fixture.TruncateAllTablesAsync(); var services = new ServiceCollection(); services.AddLogging(builder => builder.AddProvider(NullLoggerProvider.Instance)); services.AddSingleton(_timeProvider); services.AddSingleton(_handler); - services.AddMongoStorage(options => - { - options.ConnectionString = _fixture.Runner.ConnectionString; - options.DatabaseName = _fixture.Database.DatabaseNamespace.DatabaseName; - options.CommandTimeout = TimeSpan.FromSeconds(5); - options.RawDocumentRetention = TimeSpan.Zero; - options.RawDocumentRetentionTtlGrace = TimeSpan.FromMinutes(5); - options.RawDocumentRetentionSweepInterval = TimeSpan.FromHours(1); - }); + services.AddConcelierPostgresStorage(options => + { + options.ConnectionString = _fixture.ConnectionString; + options.SchemaName = _fixture.SchemaName; + options.CommandTimeoutSeconds = 5; + }); services.AddSourceCommon(); services.AddCertCcConnector(options => @@ -212,12 +210,10 @@ public sealed class CertCcConnectorFetchTests : IAsyncLifetime services.Configure(CertCcOptions.HttpClientName, builderOptions => { builderOptions.HttpMessageHandlerBuilderActions.Add(builder => builder.PrimaryHandler = _handler); - }); - - _serviceProvider = services.BuildServiceProvider(); - var bootstrapper = _serviceProvider.GetRequiredService(); - await bootstrapper.InitializeAsync(CancellationToken.None); - } + }); + + _serviceProvider = services.BuildServiceProvider(); + } private async Task DisposeServiceProviderAsync() { diff --git a/src/Concelier/__Tests/StellaOps.Concelier.Connector.CertCc.Tests/CertCc/CertCcConnectorSnapshotTests.cs b/src/Concelier/__Tests/StellaOps.Concelier.Connector.CertCc.Tests/CertCc/CertCcConnectorSnapshotTests.cs index 7c75b2939..5ca02b9e6 100644 --- a/src/Concelier/__Tests/StellaOps.Concelier.Connector.CertCc.Tests/CertCc/CertCcConnectorSnapshotTests.cs +++ b/src/Concelier/__Tests/StellaOps.Concelier.Connector.CertCc.Tests/CertCc/CertCcConnectorSnapshotTests.cs @@ -14,7 +14,7 @@ using Microsoft.Extensions.Http; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Time.Testing; -using MongoDB.Bson; +using StellaOps.Concelier.Bson; using StellaOps.Concelier.Models; using StellaOps.Concelier.Connector.CertCc; using StellaOps.Concelier.Connector.CertCc.Configuration; @@ -22,15 +22,15 @@ using StellaOps.Concelier.Connector.Common; using StellaOps.Concelier.Connector.Common.Cursors; using StellaOps.Concelier.Connector.Common.Http; using StellaOps.Concelier.Connector.Common.Testing; -using StellaOps.Concelier.Storage.Mongo; -using StellaOps.Concelier.Storage.Mongo; -using StellaOps.Concelier.Storage.Mongo.Advisories; +using StellaOps.Concelier.Storage; +using StellaOps.Concelier.Storage; +using StellaOps.Concelier.Storage.Advisories; using StellaOps.Concelier.Testing; using Xunit; namespace StellaOps.Concelier.Connector.CertCc.Tests.CertCc; -[Collection("mongo-fixture")] +[Collection(ConcelierFixtureCollection.Name)] public sealed class CertCcConnectorSnapshotTests : IAsyncLifetime { private static readonly Uri SeptemberSummaryUri = new("https://www.kb.cert.org/vuls/api/2025/09/summary/"); @@ -43,10 +43,10 @@ public sealed class CertCcConnectorSnapshotTests : IAsyncLifetime private static readonly Uri YearlySummaryUri = new("https://www.kb.cert.org/vuls/api/2025/summary/"); - private readonly MongoIntegrationFixture _fixture; + private readonly ConcelierPostgresFixture _fixture; private ConnectorTestHarness? _harness; - public CertCcConnectorSnapshotTests(MongoIntegrationFixture fixture) + public CertCcConnectorSnapshotTests(ConcelierPostgresFixture fixture) { _fixture = fixture; } diff --git a/src/Concelier/__Tests/StellaOps.Concelier.Connector.CertCc.Tests/CertCc/CertCcConnectorTests.cs b/src/Concelier/__Tests/StellaOps.Concelier.Connector.CertCc.Tests/CertCc/CertCcConnectorTests.cs index e98d2f80c..c0292a50b 100644 --- a/src/Concelier/__Tests/StellaOps.Concelier.Connector.CertCc.Tests/CertCc/CertCcConnectorTests.cs +++ b/src/Concelier/__Tests/StellaOps.Concelier.Connector.CertCc.Tests/CertCc/CertCcConnectorTests.cs @@ -10,26 +10,26 @@ using System.Threading.Tasks; using FluentAssertions; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Http; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Abstractions; -using Microsoft.Extensions.Time.Testing; -using MongoDB.Bson; -using MongoDB.Driver; -using StellaOps.Concelier.Connector.CertCc; -using StellaOps.Concelier.Connector.CertCc.Configuration; -using StellaOps.Concelier.Connector.Common; -using StellaOps.Concelier.Connector.Common.Cursors; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Time.Testing; +using StellaOps.Concelier.Bson; +using StellaOps.Concelier.Storage.Postgres; +using StellaOps.Concelier.Connector.CertCc; +using StellaOps.Concelier.Connector.CertCc.Configuration; +using StellaOps.Concelier.Connector.Common; +using StellaOps.Concelier.Connector.Common.Cursors; using StellaOps.Concelier.Connector.Common.Http; using StellaOps.Concelier.Connector.Common.Testing; -using StellaOps.Concelier.Storage.Mongo; -using StellaOps.Concelier.Storage.Mongo; -using StellaOps.Concelier.Storage.Mongo.Advisories; +using StellaOps.Concelier.Storage; +using StellaOps.Concelier.Storage; +using StellaOps.Concelier.Storage.Advisories; using StellaOps.Concelier.Testing; using Xunit; namespace StellaOps.Concelier.Connector.CertCc.Tests.CertCc; -[Collection("mongo-fixture")] +[Collection(ConcelierFixtureCollection.Name)] public sealed class CertCcConnectorTests : IAsyncLifetime { private static readonly Uri MonthlySummaryUri = new("https://www.kb.cert.org/vuls/api/2025/10/summary/"); @@ -39,11 +39,11 @@ public sealed class CertCcConnectorTests : IAsyncLifetime private static readonly Uri VulsUri = new("https://www.kb.cert.org/vuls/api/294418/vuls/"); private static readonly Uri VendorStatusesUri = new("https://www.kb.cert.org/vuls/api/294418/vendors/vuls/"); - private readonly MongoIntegrationFixture _fixture; + private readonly ConcelierPostgresFixture _fixture; private readonly FakeTimeProvider _timeProvider; private readonly CannedHttpMessageHandler _handler; - public CertCcConnectorTests(MongoIntegrationFixture fixture) + public CertCcConnectorTests(ConcelierPostgresFixture fixture) { _fixture = fixture; _timeProvider = new FakeTimeProvider(new DateTimeOffset(2025, 10, 11, 9, 30, 0, TimeSpan.Zero)); @@ -269,10 +269,10 @@ public sealed class CertCcConnectorTests : IAsyncLifetime public Task InitializeAsync() => Task.CompletedTask; - public async Task DisposeAsync() - { - await _fixture.Client.DropDatabaseAsync(_fixture.Database.DatabaseNamespace.DatabaseName); - } + public async Task DisposeAsync() + { + await _fixture.TruncateAllTablesAsync(); + } [Fact] public async Task ParseAndMap_SkipWhenDetailMappingDisabled() @@ -308,22 +308,22 @@ public sealed class CertCcConnectorTests : IAsyncLifetime pendingMappings.Should().Be(0); } - private async Task BuildServiceProviderAsync(bool enableDetailMapping = true) - { - await _fixture.Client.DropDatabaseAsync(_fixture.Database.DatabaseNamespace.DatabaseName); - _handler.Clear(); + private async Task BuildServiceProviderAsync(bool enableDetailMapping = true) + { + await _fixture.TruncateAllTablesAsync(); + _handler.Clear(); var services = new ServiceCollection(); services.AddLogging(builder => builder.AddProvider(NullLoggerProvider.Instance)); services.AddSingleton(_timeProvider); services.AddSingleton(_handler); - services.AddMongoStorage(options => - { - options.ConnectionString = _fixture.Runner.ConnectionString; - options.DatabaseName = _fixture.Database.DatabaseNamespace.DatabaseName; - options.CommandTimeout = TimeSpan.FromSeconds(5); - }); + services.AddConcelierPostgresStorage(options => + { + options.ConnectionString = _fixture.ConnectionString; + options.SchemaName = _fixture.SchemaName; + options.CommandTimeoutSeconds = 5; + }); services.AddSourceCommon(); services.AddCertCcConnector(options => @@ -350,11 +350,8 @@ public sealed class CertCcConnectorTests : IAsyncLifetime }); }); - var provider = services.BuildServiceProvider(); - var bootstrapper = provider.GetRequiredService(); - await bootstrapper.InitializeAsync(CancellationToken.None); - return provider; - } + return services.BuildServiceProvider(); + } private void SeedSummaryResponses(string summaryEtag = "\"summary-oct\"", string yearlyEtag = "\"summary-year\"") { diff --git a/src/Concelier/__Tests/StellaOps.Concelier.Connector.CertCc.Tests/Internal/CertCcMapperTests.cs b/src/Concelier/__Tests/StellaOps.Concelier.Connector.CertCc.Tests/Internal/CertCcMapperTests.cs index 0c6540f3e..a48e0294e 100644 --- a/src/Concelier/__Tests/StellaOps.Concelier.Connector.CertCc.Tests/Internal/CertCcMapperTests.cs +++ b/src/Concelier/__Tests/StellaOps.Concelier.Connector.CertCc.Tests/Internal/CertCcMapperTests.cs @@ -1,10 +1,10 @@ using System; using System.Globalization; -using MongoDB.Bson; +using StellaOps.Concelier.Bson; using StellaOps.Concelier.Models; using StellaOps.Concelier.Connector.CertCc.Internal; -using StellaOps.Concelier.Storage.Mongo; -using StellaOps.Concelier.Storage.Mongo; +using StellaOps.Concelier.Storage; +using StellaOps.Concelier.Storage; using Xunit; namespace StellaOps.Concelier.Connector.CertCc.Tests.Internal; diff --git a/src/Concelier/__Tests/StellaOps.Concelier.Connector.CertFr.Tests/CertFr/CertFrConnectorTests.cs b/src/Concelier/__Tests/StellaOps.Concelier.Connector.CertFr.Tests/CertFr/CertFrConnectorTests.cs index 2f1ff7b97..99e7dcae2 100644 --- a/src/Concelier/__Tests/StellaOps.Concelier.Connector.CertFr.Tests/CertFr/CertFrConnectorTests.cs +++ b/src/Concelier/__Tests/StellaOps.Concelier.Connector.CertFr.Tests/CertFr/CertFrConnectorTests.cs @@ -3,66 +3,53 @@ using System.IO; using System.Linq; using System.Net; using System.Net.Http; -using System.Net.Http.Headers; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Http; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Abstractions; -using Microsoft.Extensions.Options; -using Microsoft.Extensions.Time.Testing; -using MongoDB.Bson; -using MongoDB.Driver; -using StellaOps.Concelier.Connector.CertFr; -using StellaOps.Concelier.Connector.CertFr.Configuration; -using StellaOps.Concelier.Connector.Common; -using StellaOps.Concelier.Connector.Common.Http; -using StellaOps.Concelier.Connector.Common.Testing; -using StellaOps.Concelier.Storage.Mongo; -using StellaOps.Concelier.Storage.Mongo.Advisories; -using StellaOps.Concelier.Storage.Mongo; -using StellaOps.Concelier.Storage.Mongo; -using StellaOps.Concelier.Testing; -using StellaOps.Concelier.Models; +using System.Net.Http.Headers; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using StellaOps.Concelier.Bson; +using StellaOps.Concelier.Connector.CertFr; +using StellaOps.Concelier.Connector.CertFr.Configuration; +using StellaOps.Concelier.Connector.Common; +using StellaOps.Concelier.Connector.Common.Testing; +using StellaOps.Concelier.Storage; +using StellaOps.Concelier.Storage.Advisories; +using StellaOps.Concelier.Testing; +using StellaOps.Concelier.Models; namespace StellaOps.Concelier.Connector.CertFr.Tests; -[Collection("mongo-fixture")] -public sealed class CertFrConnectorTests : IAsyncLifetime -{ +[Collection(ConcelierFixtureCollection.Name)] +public sealed class CertFrConnectorTests +{ private static readonly Uri FeedUri = new("https://www.cert.ssi.gouv.fr/feed/alertes/"); private static readonly Uri FirstDetailUri = new("https://www.cert.ssi.gouv.fr/alerte/AV-2024.001/"); - private static readonly Uri SecondDetailUri = new("https://www.cert.ssi.gouv.fr/alerte/AV-2024.002/"); + private static readonly Uri SecondDetailUri = new("https://www.cert.ssi.gouv.fr/alerte/AV-2024.002/"); + + private readonly ConcelierPostgresFixture _fixture; + + public CertFrConnectorTests(ConcelierPostgresFixture fixture) + { + _fixture = fixture; + } - private readonly MongoIntegrationFixture _fixture; - private readonly FakeTimeProvider _timeProvider; - private readonly CannedHttpMessageHandler _handler; - - public CertFrConnectorTests(MongoIntegrationFixture fixture) - { - _fixture = fixture; - _timeProvider = new FakeTimeProvider(new DateTimeOffset(2024, 10, 3, 0, 0, 0, TimeSpan.Zero)); - _handler = new CannedHttpMessageHandler(); - } - - [Fact] - public async Task FetchParseMap_ProducesDeterministicSnapshot() - { - await using var provider = await BuildServiceProviderAsync(); - SeedFeed(); - SeedDetailResponses(); - - var connector = provider.GetRequiredService(); - await connector.FetchAsync(provider, CancellationToken.None); - _timeProvider.Advance(TimeSpan.FromMinutes(1)); - await connector.ParseAsync(provider, CancellationToken.None); - await connector.MapAsync(provider, CancellationToken.None); - - var advisoryStore = provider.GetRequiredService(); - var advisories = await advisoryStore.GetRecentAsync(10, CancellationToken.None); - Assert.Equal(2, advisories.Count); + [Fact] + public async Task FetchParseMap_ProducesDeterministicSnapshot() + { + await using var harness = await BuildHarnessAsync(); + SeedFeed(harness.Handler); + SeedDetailResponses(harness.Handler); + + var connector = harness.ServiceProvider.GetRequiredService(); + await connector.FetchAsync(harness.ServiceProvider, CancellationToken.None); + harness.TimeProvider.Advance(TimeSpan.FromMinutes(1)); + await connector.ParseAsync(harness.ServiceProvider, CancellationToken.None); + await connector.MapAsync(harness.ServiceProvider, CancellationToken.None); + + var advisoryStore = harness.ServiceProvider.GetRequiredService(); + var advisories = await advisoryStore.GetRecentAsync(10, CancellationToken.None); + Assert.Equal(2, advisories.Count); var snapshot = SnapshotSerializer.ToSnapshot(advisories.OrderBy(static a => a.AdvisoryKey, StringComparer.Ordinal).ToArray()); var expected = ReadFixture("certfr-advisories.snapshot.json"); @@ -77,216 +64,193 @@ public sealed class CertFrConnectorTests : IAsyncLifetime Assert.Equal(normalizedExpected, normalizedSnapshot); - var documentStore = provider.GetRequiredService(); - var firstDocument = await documentStore.FindBySourceAndUriAsync(CertFrConnectorPlugin.SourceName, FirstDetailUri.ToString(), CancellationToken.None); - Assert.NotNull(firstDocument); - Assert.Equal(DocumentStatuses.Mapped, firstDocument!.Status); + var documentStore = harness.ServiceProvider.GetRequiredService(); + var firstDocument = await documentStore.FindBySourceAndUriAsync(CertFrConnectorPlugin.SourceName, FirstDetailUri.ToString(), CancellationToken.None); + Assert.NotNull(firstDocument); + Assert.Equal(DocumentStatuses.Mapped, firstDocument!.Status); - var secondDocument = await documentStore.FindBySourceAndUriAsync(CertFrConnectorPlugin.SourceName, SecondDetailUri.ToString(), CancellationToken.None); - Assert.NotNull(secondDocument); - Assert.Equal(DocumentStatuses.Mapped, secondDocument!.Status); + var secondDocument = await documentStore.FindBySourceAndUriAsync(CertFrConnectorPlugin.SourceName, SecondDetailUri.ToString(), CancellationToken.None); + Assert.NotNull(secondDocument); + Assert.Equal(DocumentStatuses.Mapped, secondDocument!.Status); + + var stateRepository = harness.ServiceProvider.GetRequiredService(); + var state = await stateRepository.TryGetAsync(CertFrConnectorPlugin.SourceName, CancellationToken.None); + Assert.NotNull(state); + Assert.True(state!.Cursor.TryGetValue("pendingDocuments", out var pendingDocs) && pendingDocs.AsBsonArray.Count == 0); + Assert.True(state.Cursor.TryGetValue("pendingMappings", out var pendingMaps) && pendingMaps.AsBsonArray.Count == 0); + } - var stateRepository = provider.GetRequiredService(); - var state = await stateRepository.TryGetAsync(CertFrConnectorPlugin.SourceName, CancellationToken.None); - Assert.NotNull(state); - Assert.True(state!.Cursor.TryGetValue("pendingDocuments", out var pendingDocs) && pendingDocs.AsBsonArray.Count == 0); - Assert.True(state.Cursor.TryGetValue("pendingMappings", out var pendingMaps) && pendingMaps.AsBsonArray.Count == 0); - } + [Fact] + public async Task FetchFailure_RecordsBackoffAndReason() + { + await using var harness = await BuildHarnessAsync(); + harness.Handler.AddResponse(FeedUri, () => new HttpResponseMessage(HttpStatusCode.InternalServerError) + { + Content = new StringContent("feed error", Encoding.UTF8, "text/plain"), + }); + + var connector = harness.ServiceProvider.GetRequiredService(); + await Assert.ThrowsAsync(() => connector.FetchAsync(harness.ServiceProvider, CancellationToken.None)); + + var stateRepository = harness.ServiceProvider.GetRequiredService(); + var state = await stateRepository.TryGetAsync(CertFrConnectorPlugin.SourceName, CancellationToken.None); + Assert.NotNull(state); + Assert.Equal(1, state!.FailCount); + Assert.NotNull(state.LastFailureReason); + Assert.Contains("500", state.LastFailureReason, StringComparison.Ordinal); + Assert.NotNull(state.BackoffUntil); + Assert.True(state.BackoffUntil > harness.TimeProvider.GetUtcNow()); + } - [Fact] - public async Task FetchFailure_RecordsBackoffAndReason() - { - await using var provider = await BuildServiceProviderAsync(); - _handler.AddResponse(FeedUri, () => new HttpResponseMessage(HttpStatusCode.InternalServerError) - { - Content = new StringContent("feed error", Encoding.UTF8, "text/plain"), - }); + [Fact] + public async Task Fetch_NotModifiedResponsesMaintainDocumentState() + { + await using var harness = await BuildHarnessAsync(); + SeedFeed(harness.Handler); + SeedDetailResponses(harness.Handler); + + var connector = harness.ServiceProvider.GetRequiredService(); + await connector.FetchAsync(harness.ServiceProvider, CancellationToken.None); + await connector.ParseAsync(harness.ServiceProvider, CancellationToken.None); + await connector.MapAsync(harness.ServiceProvider, CancellationToken.None); + + var documentStore = harness.ServiceProvider.GetRequiredService(); + var firstDocument = await documentStore.FindBySourceAndUriAsync(CertFrConnectorPlugin.SourceName, FirstDetailUri.ToString(), CancellationToken.None); + Assert.NotNull(firstDocument); + Assert.Equal(DocumentStatuses.Mapped, firstDocument!.Status); - var connector = provider.GetRequiredService(); - await Assert.ThrowsAsync(() => connector.FetchAsync(provider, CancellationToken.None)); + var secondDocument = await documentStore.FindBySourceAndUriAsync(CertFrConnectorPlugin.SourceName, SecondDetailUri.ToString(), CancellationToken.None); + Assert.NotNull(secondDocument); + Assert.Equal(DocumentStatuses.Mapped, secondDocument!.Status); + + SeedFeed(harness.Handler); + SeedNotModifiedDetailResponses(harness.Handler); + + await connector.FetchAsync(harness.ServiceProvider, CancellationToken.None); + + firstDocument = await documentStore.FindBySourceAndUriAsync(CertFrConnectorPlugin.SourceName, FirstDetailUri.ToString(), CancellationToken.None); + Assert.NotNull(firstDocument); + Assert.Equal(DocumentStatuses.Mapped, firstDocument!.Status); - var stateRepository = provider.GetRequiredService(); - var state = await stateRepository.TryGetAsync(CertFrConnectorPlugin.SourceName, CancellationToken.None); - Assert.NotNull(state); - Assert.Equal(1, state!.FailCount); - Assert.NotNull(state.LastFailureReason); - Assert.Contains("500", state.LastFailureReason, StringComparison.Ordinal); - Assert.NotNull(state.BackoffUntil); - Assert.True(state.BackoffUntil > _timeProvider.GetUtcNow()); - } + secondDocument = await documentStore.FindBySourceAndUriAsync(CertFrConnectorPlugin.SourceName, SecondDetailUri.ToString(), CancellationToken.None); + Assert.NotNull(secondDocument); + Assert.Equal(DocumentStatuses.Mapped, secondDocument!.Status); + + var stateRepository = harness.ServiceProvider.GetRequiredService(); + var state = await stateRepository.TryGetAsync(CertFrConnectorPlugin.SourceName, CancellationToken.None); + Assert.NotNull(state); + Assert.True(state!.Cursor.TryGetValue("pendingDocuments", out var pendingDocs) && pendingDocs.AsBsonArray.Count == 0); + Assert.True(state.Cursor.TryGetValue("pendingMappings", out var pendingMaps) && pendingMaps.AsBsonArray.Count == 0); + } - [Fact] - public async Task Fetch_NotModifiedResponsesMaintainDocumentState() - { - await using var provider = await BuildServiceProviderAsync(); - SeedFeed(); - SeedDetailResponses(); + [Fact] + public async Task Fetch_DuplicateContentSkipsRequeue() + { + await using var harness = await BuildHarnessAsync(); + SeedFeed(harness.Handler); + SeedDetailResponses(harness.Handler); + + var connector = harness.ServiceProvider.GetRequiredService(); + await connector.FetchAsync(harness.ServiceProvider, CancellationToken.None); + await connector.ParseAsync(harness.ServiceProvider, CancellationToken.None); + await connector.MapAsync(harness.ServiceProvider, CancellationToken.None); + + var documentStore = harness.ServiceProvider.GetRequiredService(); + var firstDocument = await documentStore.FindBySourceAndUriAsync(CertFrConnectorPlugin.SourceName, FirstDetailUri.ToString(), CancellationToken.None); + Assert.NotNull(firstDocument); + Assert.Equal(DocumentStatuses.Mapped, firstDocument!.Status); - var connector = provider.GetRequiredService(); - await connector.FetchAsync(provider, CancellationToken.None); - await connector.ParseAsync(provider, CancellationToken.None); - await connector.MapAsync(provider, CancellationToken.None); + var secondDocument = await documentStore.FindBySourceAndUriAsync(CertFrConnectorPlugin.SourceName, SecondDetailUri.ToString(), CancellationToken.None); + Assert.NotNull(secondDocument); + Assert.Equal(DocumentStatuses.Mapped, secondDocument!.Status); + + SeedFeed(harness.Handler); + SeedDetailResponses(harness.Handler); + + await connector.FetchAsync(harness.ServiceProvider, CancellationToken.None); + await connector.ParseAsync(harness.ServiceProvider, CancellationToken.None); + await connector.MapAsync(harness.ServiceProvider, CancellationToken.None); + + firstDocument = await documentStore.FindBySourceAndUriAsync(CertFrConnectorPlugin.SourceName, FirstDetailUri.ToString(), CancellationToken.None); + Assert.NotNull(firstDocument); + Assert.Equal(DocumentStatuses.Mapped, firstDocument!.Status); - var documentStore = provider.GetRequiredService(); - var firstDocument = await documentStore.FindBySourceAndUriAsync(CertFrConnectorPlugin.SourceName, FirstDetailUri.ToString(), CancellationToken.None); - Assert.NotNull(firstDocument); - Assert.Equal(DocumentStatuses.Mapped, firstDocument!.Status); + secondDocument = await documentStore.FindBySourceAndUriAsync(CertFrConnectorPlugin.SourceName, SecondDetailUri.ToString(), CancellationToken.None); + Assert.NotNull(secondDocument); + Assert.Equal(DocumentStatuses.Mapped, secondDocument!.Status); + + var stateRepository = harness.ServiceProvider.GetRequiredService(); + var state = await stateRepository.TryGetAsync(CertFrConnectorPlugin.SourceName, CancellationToken.None); + Assert.NotNull(state); + Assert.True(state!.Cursor.TryGetValue("pendingDocuments", out var pendingDocs) && pendingDocs.AsBsonArray.Count == 0); + Assert.True(state.Cursor.TryGetValue("pendingMappings", out var pendingMaps) && pendingMaps.AsBsonArray.Count == 0); + } - var secondDocument = await documentStore.FindBySourceAndUriAsync(CertFrConnectorPlugin.SourceName, SecondDetailUri.ToString(), CancellationToken.None); - Assert.NotNull(secondDocument); - Assert.Equal(DocumentStatuses.Mapped, secondDocument!.Status); - - SeedFeed(); - SeedNotModifiedDetailResponses(); - - await connector.FetchAsync(provider, CancellationToken.None); - - firstDocument = await documentStore.FindBySourceAndUriAsync(CertFrConnectorPlugin.SourceName, FirstDetailUri.ToString(), CancellationToken.None); - Assert.NotNull(firstDocument); - Assert.Equal(DocumentStatuses.Mapped, firstDocument!.Status); - - secondDocument = await documentStore.FindBySourceAndUriAsync(CertFrConnectorPlugin.SourceName, SecondDetailUri.ToString(), CancellationToken.None); - Assert.NotNull(secondDocument); - Assert.Equal(DocumentStatuses.Mapped, secondDocument!.Status); - - var stateRepository = provider.GetRequiredService(); - var state = await stateRepository.TryGetAsync(CertFrConnectorPlugin.SourceName, CancellationToken.None); - Assert.NotNull(state); - Assert.True(state!.Cursor.TryGetValue("pendingDocuments", out var pendingDocs) && pendingDocs.AsBsonArray.Count == 0); - Assert.True(state.Cursor.TryGetValue("pendingMappings", out var pendingMaps) && pendingMaps.AsBsonArray.Count == 0); - } - - [Fact] - public async Task Fetch_DuplicateContentSkipsRequeue() - { - await using var provider = await BuildServiceProviderAsync(); - SeedFeed(); - SeedDetailResponses(); - - var connector = provider.GetRequiredService(); - await connector.FetchAsync(provider, CancellationToken.None); - await connector.ParseAsync(provider, CancellationToken.None); - await connector.MapAsync(provider, CancellationToken.None); - - var documentStore = provider.GetRequiredService(); - var firstDocument = await documentStore.FindBySourceAndUriAsync(CertFrConnectorPlugin.SourceName, FirstDetailUri.ToString(), CancellationToken.None); - Assert.NotNull(firstDocument); - Assert.Equal(DocumentStatuses.Mapped, firstDocument!.Status); - - var secondDocument = await documentStore.FindBySourceAndUriAsync(CertFrConnectorPlugin.SourceName, SecondDetailUri.ToString(), CancellationToken.None); - Assert.NotNull(secondDocument); - Assert.Equal(DocumentStatuses.Mapped, secondDocument!.Status); - - SeedFeed(); - SeedDetailResponses(); - - await connector.FetchAsync(provider, CancellationToken.None); - await connector.ParseAsync(provider, CancellationToken.None); - await connector.MapAsync(provider, CancellationToken.None); - - firstDocument = await documentStore.FindBySourceAndUriAsync(CertFrConnectorPlugin.SourceName, FirstDetailUri.ToString(), CancellationToken.None); - Assert.NotNull(firstDocument); - Assert.Equal(DocumentStatuses.Mapped, firstDocument!.Status); - - secondDocument = await documentStore.FindBySourceAndUriAsync(CertFrConnectorPlugin.SourceName, SecondDetailUri.ToString(), CancellationToken.None); - Assert.NotNull(secondDocument); - Assert.Equal(DocumentStatuses.Mapped, secondDocument!.Status); - - var stateRepository = provider.GetRequiredService(); - var state = await stateRepository.TryGetAsync(CertFrConnectorPlugin.SourceName, CancellationToken.None); - Assert.NotNull(state); - Assert.True(state!.Cursor.TryGetValue("pendingDocuments", out var pendingDocs) && pendingDocs.AsBsonArray.Count == 0); - Assert.True(state.Cursor.TryGetValue("pendingMappings", out var pendingMaps) && pendingMaps.AsBsonArray.Count == 0); - } - - private async Task BuildServiceProviderAsync() - { - await _fixture.Client.DropDatabaseAsync(_fixture.Database.DatabaseNamespace.DatabaseName); - _handler.Clear(); - - var services = new ServiceCollection(); - services.AddLogging(builder => builder.AddProvider(NullLoggerProvider.Instance)); - services.AddSingleton(_timeProvider); - services.AddSingleton(_handler); - - services.AddMongoStorage(options => - { - options.ConnectionString = _fixture.Runner.ConnectionString; - options.DatabaseName = _fixture.Database.DatabaseNamespace.DatabaseName; - options.CommandTimeout = TimeSpan.FromSeconds(5); - }); - - services.AddSourceCommon(); - services.AddCertFrConnector(opts => - { - opts.FeedUri = FeedUri; - opts.InitialBackfill = TimeSpan.FromDays(30); - opts.WindowOverlap = TimeSpan.FromDays(2); - opts.MaxItemsPerFetch = 50; - }); - - services.Configure(CertFrOptions.HttpClientName, builderOptions => - { - builderOptions.HttpMessageHandlerBuilderActions.Add(builder => - { - builder.PrimaryHandler = _handler; - }); - }); - - var provider = services.BuildServiceProvider(); - var bootstrapper = provider.GetRequiredService(); - await bootstrapper.InitializeAsync(CancellationToken.None); - return provider; - } - - private void SeedFeed() - { - _handler.AddTextResponse(FeedUri, ReadFixture("certfr-feed.xml"), "application/atom+xml"); - } - - private void SeedDetailResponses() - { - AddDetailResponse(FirstDetailUri, "certfr-detail-AV-2024-001.html", "\"certfr-001\""); - AddDetailResponse(SecondDetailUri, "certfr-detail-AV-2024-002.html", "\"certfr-002\""); - } - - private void SeedNotModifiedDetailResponses() - { - AddNotModifiedResponse(FirstDetailUri, "\"certfr-001\""); - AddNotModifiedResponse(SecondDetailUri, "\"certfr-002\""); - } - - private void AddDetailResponse(Uri uri, string fixture, string? etag) - { - _handler.AddResponse(uri, () => - { - var response = new HttpResponseMessage(HttpStatusCode.OK) - { - Content = new StringContent(ReadFixture(fixture), Encoding.UTF8, "text/html"), - }; - - if (!string.IsNullOrEmpty(etag)) - { - response.Headers.ETag = new EntityTagHeaderValue(etag); - } - - return response; - }); - } - - private void AddNotModifiedResponse(Uri uri, string? etag) - { - _handler.AddResponse(uri, () => - { - var response = new HttpResponseMessage(HttpStatusCode.NotModified); - if (!string.IsNullOrEmpty(etag)) - { - response.Headers.ETag = new EntityTagHeaderValue(etag); - } - - return response; - }); - } + private async Task BuildHarnessAsync() + { + var initialTime = new DateTimeOffset(2024, 10, 3, 0, 0, 0, TimeSpan.Zero); + var harness = new ConnectorTestHarness(_fixture, initialTime, CertFrOptions.HttpClientName); + await harness.EnsureServiceProviderAsync(services => + { + services.AddCertFrConnector(opts => + { + opts.FeedUri = FeedUri; + opts.InitialBackfill = TimeSpan.FromDays(30); + opts.WindowOverlap = TimeSpan.FromDays(2); + opts.MaxItemsPerFetch = 50; + }); + }); + return harness; + } + + private void SeedFeed(CannedHttpMessageHandler handler) + { + handler.AddTextResponse(FeedUri, ReadFixture("certfr-feed.xml"), "application/atom+xml"); + } + + private void SeedDetailResponses(CannedHttpMessageHandler handler) + { + AddDetailResponse(handler, FirstDetailUri, "certfr-detail-AV-2024-001.html", "\"certfr-001\""); + AddDetailResponse(handler, SecondDetailUri, "certfr-detail-AV-2024-002.html", "\"certfr-002\""); + } + + private void SeedNotModifiedDetailResponses(CannedHttpMessageHandler handler) + { + AddNotModifiedResponse(handler, FirstDetailUri, "\"certfr-001\""); + AddNotModifiedResponse(handler, SecondDetailUri, "\"certfr-002\""); + } + + private static void AddDetailResponse(CannedHttpMessageHandler handler, Uri uri, string fixture, string? etag) + { + handler.AddResponse(uri, () => + { + var response = new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(ReadFixture(fixture), Encoding.UTF8, "text/html"), + }; + + if (!string.IsNullOrEmpty(etag)) + { + response.Headers.ETag = new EntityTagHeaderValue(etag); + } + + return response; + }); + } + + private static void AddNotModifiedResponse(CannedHttpMessageHandler handler, Uri uri, string? etag) + { + handler.AddResponse(uri, () => + { + var response = new HttpResponseMessage(HttpStatusCode.NotModified); + if (!string.IsNullOrEmpty(etag)) + { + response.Headers.ETag = new EntityTagHeaderValue(etag); + } + + return response; + }); + } private static string ReadFixture(string filename) { @@ -304,10 +268,4 @@ public sealed class CertFrConnectorTests : IAsyncLifetime private static string Normalize(string value) => value.Replace("\r\n", "\n", StringComparison.Ordinal); - public Task InitializeAsync() => Task.CompletedTask; - - public async Task DisposeAsync() - { - await _fixture.Client.DropDatabaseAsync(_fixture.Database.DatabaseNamespace.DatabaseName); - } -} +} diff --git a/src/Concelier/__Tests/StellaOps.Concelier.Connector.CertIn.Tests/CertIn/CertInConnectorTests.cs b/src/Concelier/__Tests/StellaOps.Concelier.Connector.CertIn.Tests/CertIn/CertInConnectorTests.cs index 4e0541a31..e7f4123b8 100644 --- a/src/Concelier/__Tests/StellaOps.Concelier.Connector.CertIn.Tests/CertIn/CertInConnectorTests.cs +++ b/src/Concelier/__Tests/StellaOps.Concelier.Connector.CertIn.Tests/CertIn/CertInConnectorTests.cs @@ -13,32 +13,33 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; using Microsoft.Extensions.Time.Testing; -using MongoDB.Bson; +using StellaOps.Concelier.Bson; using StellaOps.Concelier.Models; using StellaOps.Concelier.Connector.CertIn; using StellaOps.Concelier.Connector.CertIn.Configuration; using StellaOps.Concelier.Connector.CertIn.Internal; using StellaOps.Concelier.Connector.Common; -using StellaOps.Concelier.Connector.Common.Fetch; -using StellaOps.Concelier.Connector.Common.Http; -using StellaOps.Concelier.Connector.Common.Testing; -using StellaOps.Concelier.Storage.Mongo; -using StellaOps.Concelier.Storage.Mongo.Advisories; -using StellaOps.Concelier.Storage.Mongo; -using StellaOps.Concelier.Storage.Mongo; -using StellaOps.Concelier.Testing; +using StellaOps.Concelier.Connector.Common.Fetch; +using StellaOps.Concelier.Connector.Common.Http; +using StellaOps.Concelier.Connector.Common.Testing; +using StellaOps.Concelier.Storage; +using StellaOps.Concelier.Storage.Advisories; +using StellaOps.Concelier.Storage.Postgres; +using StellaOps.Concelier.Storage; +using StellaOps.Concelier.Storage; +using StellaOps.Concelier.Testing; namespace StellaOps.Concelier.Connector.CertIn.Tests; -[Collection("mongo-fixture")] +[Collection(ConcelierFixtureCollection.Name)] public sealed class CertInConnectorTests : IAsyncLifetime { - private readonly MongoIntegrationFixture _fixture; + private readonly ConcelierPostgresFixture _fixture; private readonly FakeTimeProvider _timeProvider; private readonly CannedHttpMessageHandler _handler; private ServiceProvider? _serviceProvider; - public CertInConnectorTests(MongoIntegrationFixture fixture) + public CertInConnectorTests(ConcelierPostgresFixture fixture) { _fixture = fixture; _timeProvider = new FakeTimeProvider(new DateTimeOffset(2024, 4, 20, 0, 0, 0, TimeSpan.Zero)); @@ -275,24 +276,24 @@ public sealed class CertInConnectorTests : IAsyncLifetime await ResetDatabaseAsync(); return; } - - await _fixture.Client.DropDatabaseAsync(_fixture.Database.DatabaseNamespace.DatabaseName); - - var services = new ServiceCollection(); - services.AddLogging(builder => builder.AddProvider(NullLoggerProvider.Instance)); - services.AddSingleton(_timeProvider); - services.AddSingleton(_handler); - - services.AddMongoStorage(options => - { - options.ConnectionString = _fixture.Runner.ConnectionString; - options.DatabaseName = _fixture.Database.DatabaseNamespace.DatabaseName; - options.CommandTimeout = TimeSpan.FromSeconds(5); - }); - - services.AddSourceCommon(); - services.AddCertInConnector(opts => - { + + await _fixture.TruncateAllTablesAsync(); + + var services = new ServiceCollection(); + services.AddLogging(builder => builder.AddProvider(NullLoggerProvider.Instance)); + services.AddSingleton(_timeProvider); + services.AddSingleton(_handler); + + services.AddConcelierPostgresStorage(options => + { + options.ConnectionString = _fixture.ConnectionString; + options.SchemaName = _fixture.SchemaName; + options.CommandTimeoutSeconds = 5; + }); + + services.AddSourceCommon(); + services.AddCertInConnector(opts => + { opts.AlertsEndpoint = template.AlertsEndpoint; opts.WindowSize = template.WindowSize; opts.WindowOverlap = template.WindowOverlap; @@ -306,15 +307,13 @@ public sealed class CertInConnectorTests : IAsyncLifetime { builder.PrimaryHandler = _handler; }); - }); - - _serviceProvider = services.BuildServiceProvider(); - var bootstrapper = _serviceProvider.GetRequiredService(); - await bootstrapper.InitializeAsync(CancellationToken.None); - } - - private Task ResetDatabaseAsync() - => _fixture.Client.DropDatabaseAsync(_fixture.Database.DatabaseNamespace.DatabaseName); + }); + + _serviceProvider = services.BuildServiceProvider(); + } + + private Task ResetDatabaseAsync() + => _fixture.TruncateAllTablesAsync(); private static string ReadFixture(string filename) => File.ReadAllText(ResolveFixturePath(filename)); diff --git a/src/Concelier/__Tests/StellaOps.Concelier.Connector.Common.Tests/Common/SourceFetchServiceGuardTests.cs b/src/Concelier/__Tests/StellaOps.Concelier.Connector.Common.Tests/Common/SourceFetchServiceGuardTests.cs index b80487931..30a69d8c5 100644 --- a/src/Concelier/__Tests/StellaOps.Concelier.Connector.Common.Tests/Common/SourceFetchServiceGuardTests.cs +++ b/src/Concelier/__Tests/StellaOps.Concelier.Connector.Common.Tests/Common/SourceFetchServiceGuardTests.cs @@ -5,7 +5,7 @@ using System.Text; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; using Mongo2Go; -using MongoDB.Bson; +using StellaOps.Concelier.Bson; using MongoDB.Driver; using StellaOps.Aoc; using StellaOps.Concelier.Connector.Common.Fetch; @@ -13,8 +13,8 @@ using StellaOps.Concelier.Connector.Common.Http; using StellaOps.Concelier.Core.Aoc; using StellaOps.Concelier.Core.Linksets; using StellaOps.Concelier.RawModels; -using StellaOps.Concelier.Storage.Mongo; -using StellaOps.Concelier.Storage.Mongo; +using StellaOps.Concelier.Storage; +using StellaOps.Concelier.Storage; using StellaOps.Cryptography; namespace StellaOps.Concelier.Connector.Common.Tests; diff --git a/src/Concelier/__Tests/StellaOps.Concelier.Connector.Common.Tests/Common/SourceStateSeedProcessorTests.cs b/src/Concelier/__Tests/StellaOps.Concelier.Connector.Common.Tests/Common/SourceStateSeedProcessorTests.cs index b9bca08e7..837664d44 100644 --- a/src/Concelier/__Tests/StellaOps.Concelier.Connector.Common.Tests/Common/SourceStateSeedProcessorTests.cs +++ b/src/Concelier/__Tests/StellaOps.Concelier.Connector.Common.Tests/Common/SourceStateSeedProcessorTests.cs @@ -2,15 +2,15 @@ using System.Collections.Generic; using System.Linq; using System.Text; using Mongo2Go; -using MongoDB.Bson; +using StellaOps.Concelier.Bson; using MongoDB.Driver; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Time.Testing; using StellaOps.Concelier.Connector.Common; using StellaOps.Concelier.Connector.Common.Fetch; using StellaOps.Concelier.Connector.Common.State; -using StellaOps.Concelier.Storage.Mongo; -using StellaOps.Concelier.Storage.Mongo; +using StellaOps.Concelier.Storage; +using StellaOps.Concelier.Storage; using StellaOps.Cryptography; namespace StellaOps.Concelier.Connector.Common.Tests; diff --git a/src/Concelier/__Tests/StellaOps.Concelier.Connector.Common.Tests/Common/TimeWindowCursorPlannerTests.cs b/src/Concelier/__Tests/StellaOps.Concelier.Connector.Common.Tests/Common/TimeWindowCursorPlannerTests.cs index 27b43e83c..b6e551cc7 100644 --- a/src/Concelier/__Tests/StellaOps.Concelier.Connector.Common.Tests/Common/TimeWindowCursorPlannerTests.cs +++ b/src/Concelier/__Tests/StellaOps.Concelier.Connector.Common.Tests/Common/TimeWindowCursorPlannerTests.cs @@ -1,4 +1,4 @@ -using MongoDB.Bson; +using StellaOps.Concelier.Bson; using StellaOps.Concelier.Connector.Common.Cursors; namespace StellaOps.Concelier.Connector.Common.Tests; diff --git a/src/Concelier/__Tests/StellaOps.Concelier.Connector.Cve.Tests/Cve/CveConnectorTests.cs b/src/Concelier/__Tests/StellaOps.Concelier.Connector.Cve.Tests/Cve/CveConnectorTests.cs index 3662ed4a7..f2b23e4fd 100644 --- a/src/Concelier/__Tests/StellaOps.Concelier.Connector.Cve.Tests/Cve/CveConnectorTests.cs +++ b/src/Concelier/__Tests/StellaOps.Concelier.Connector.Cve.Tests/Cve/CveConnectorTests.cs @@ -7,7 +7,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; -using MongoDB.Bson; +using StellaOps.Concelier.Bson; using MongoDB.Driver; using StellaOps.Concelier.Models; using StellaOps.Concelier.Connector.Common.Fetch; @@ -15,22 +15,22 @@ using StellaOps.Concelier.Connector.Common.Testing; using StellaOps.Concelier.Connector.Cve.Configuration; using StellaOps.Concelier.Connector.Cve.Internal; using StellaOps.Concelier.Testing; -using StellaOps.Concelier.Storage.Mongo; -using StellaOps.Concelier.Storage.Mongo.Advisories; -using StellaOps.Concelier.Storage.Mongo; -using StellaOps.Concelier.Storage.Mongo; +using StellaOps.Concelier.Storage; +using StellaOps.Concelier.Storage.Advisories; +using StellaOps.Concelier.Storage; +using StellaOps.Concelier.Storage; using Xunit.Abstractions; namespace StellaOps.Concelier.Connector.Cve.Tests; -[Collection("mongo-fixture")] +[Collection(ConcelierFixtureCollection.Name)] public sealed class CveConnectorTests : IAsyncLifetime { - private readonly MongoIntegrationFixture _fixture; + private readonly ConcelierPostgresFixture _fixture; private readonly ITestOutputHelper _output; private ConnectorTestHarness? _harness; - public CveConnectorTests(MongoIntegrationFixture fixture, ITestOutputHelper output) + public CveConnectorTests(ConcelierPostgresFixture fixture, ITestOutputHelper output) { _fixture = fixture; _output = output; diff --git a/src/Concelier/__Tests/StellaOps.Concelier.Connector.Distro.Debian.Tests/DebianConnectorTests.cs b/src/Concelier/__Tests/StellaOps.Concelier.Connector.Distro.Debian.Tests/DebianConnectorTests.cs index 13e9da484..405a20938 100644 --- a/src/Concelier/__Tests/StellaOps.Concelier.Connector.Distro.Debian.Tests/DebianConnectorTests.cs +++ b/src/Concelier/__Tests/StellaOps.Concelier.Connector.Distro.Debian.Tests/DebianConnectorTests.cs @@ -10,40 +10,40 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Http; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Abstractions; -using Microsoft.Extensions.Options; -using Microsoft.Extensions.Time.Testing; -using MongoDB.Driver; -using StellaOps.Concelier.Models; -using StellaOps.Concelier.Connector.Common; -using StellaOps.Concelier.Connector.Common.Http; -using StellaOps.Concelier.Connector.Common.Testing; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Time.Testing; +using StellaOps.Concelier.Storage.Postgres; +using StellaOps.Concelier.Models; +using StellaOps.Concelier.Connector.Common; +using StellaOps.Concelier.Connector.Common.Http; +using StellaOps.Concelier.Connector.Common.Testing; using StellaOps.Concelier.Connector.Distro.Debian.Configuration; -using StellaOps.Concelier.Storage.Mongo; -using StellaOps.Concelier.Storage.Mongo.Advisories; -using StellaOps.Concelier.Storage.Mongo; -using StellaOps.Concelier.Storage.Mongo; +using StellaOps.Concelier.Storage; +using StellaOps.Concelier.Storage.Advisories; +using StellaOps.Concelier.Storage; +using StellaOps.Concelier.Storage; using StellaOps.Concelier.Testing; using Xunit; using Xunit.Abstractions; namespace StellaOps.Concelier.Connector.Distro.Debian.Tests; -[Collection("mongo-fixture")] +[Collection(ConcelierFixtureCollection.Name)] public sealed class DebianConnectorTests : IAsyncLifetime { private static readonly Uri ListUri = new("https://salsa.debian.org/security-tracker-team/security-tracker/-/raw/master/data/DSA/list"); private static readonly Uri DetailResolved = new("https://security-tracker.debian.org/tracker/DSA-2024-123"); private static readonly Uri DetailOpen = new("https://security-tracker.debian.org/tracker/DSA-2024-124"); - private readonly MongoIntegrationFixture _fixture; + private readonly ConcelierPostgresFixture _fixture; private readonly FakeTimeProvider _timeProvider; private readonly CannedHttpMessageHandler _handler; private readonly Dictionary> _fallbackFactories = new(); private readonly ITestOutputHelper _output; - public DebianConnectorTests(MongoIntegrationFixture fixture, ITestOutputHelper output) + public DebianConnectorTests(ConcelierPostgresFixture fixture, ITestOutputHelper output) { _fixture = fixture; _handler = new CannedHttpMessageHandler(); @@ -103,7 +103,7 @@ public sealed class DebianConnectorTests : IAsyncLifetime Assert.NotNull(openRange.Primitives); Assert.NotNull(openRange.Primitives!.Evr); - // Ensure data persisted through Mongo round-trip. + // Ensure data persisted through storage round-trip. var found = await advisoryStore.FindAsync("DSA-2024-123", CancellationToken.None); Assert.NotNull(found); var persistedRange = Assert.Single(found!.AffectedPackages, pkg => pkg.Platform == "bookworm").VersionRanges.Single(); @@ -125,23 +125,23 @@ public sealed class DebianConnectorTests : IAsyncLifetime Assert.Equal(2, refreshed.Count); } - private async Task BuildServiceProviderAsync() - { - await _fixture.Client.DropDatabaseAsync(_fixture.Database.DatabaseNamespace.DatabaseName, CancellationToken.None); - _handler.Clear(); - _fallbackFactories.Clear(); + private async Task BuildServiceProviderAsync() + { + await _fixture.TruncateAllTablesAsync(CancellationToken.None); + _handler.Clear(); + _fallbackFactories.Clear(); var services = new ServiceCollection(); services.AddLogging(builder => builder.AddProvider(new TestOutputLoggerProvider(_output))); services.AddSingleton(_timeProvider); services.AddSingleton(_handler); - services.AddMongoStorage(options => - { - options.ConnectionString = _fixture.Runner.ConnectionString; - options.DatabaseName = _fixture.Database.DatabaseNamespace.DatabaseName; - options.CommandTimeout = TimeSpan.FromSeconds(5); - }); + services.AddConcelierPostgresStorage(options => + { + options.ConnectionString = _fixture.ConnectionString; + options.SchemaName = _fixture.SchemaName; + options.CommandTimeoutSeconds = 5; + }); services.AddSourceCommon(); services.AddDebianConnector(options => @@ -160,11 +160,8 @@ public sealed class DebianConnectorTests : IAsyncLifetime }); }); - var provider = services.BuildServiceProvider(); - var bootstrapper = provider.GetRequiredService(); - await bootstrapper.InitializeAsync(CancellationToken.None); - return provider; - } + return services.BuildServiceProvider(); + } private void SeedInitialResponses() { diff --git a/src/Concelier/__Tests/StellaOps.Concelier.Connector.Distro.Debian.Tests/DebianMapperTests.cs b/src/Concelier/__Tests/StellaOps.Concelier.Connector.Distro.Debian.Tests/DebianMapperTests.cs index e29a488a0..82942a9bd 100644 --- a/src/Concelier/__Tests/StellaOps.Concelier.Connector.Distro.Debian.Tests/DebianMapperTests.cs +++ b/src/Concelier/__Tests/StellaOps.Concelier.Connector.Distro.Debian.Tests/DebianMapperTests.cs @@ -3,7 +3,7 @@ using Xunit; using StellaOps.Concelier.Models; using StellaOps.Concelier.Connector.Distro.Debian; using StellaOps.Concelier.Connector.Distro.Debian.Internal; -using StellaOps.Concelier.Storage.Mongo; +using StellaOps.Concelier.Storage; namespace StellaOps.Concelier.Connector.Distro.Debian.Tests; diff --git a/src/Concelier/__Tests/StellaOps.Concelier.Connector.Distro.RedHat.Tests/RedHat/RedHatConnectorHarnessTests.cs b/src/Concelier/__Tests/StellaOps.Concelier.Connector.Distro.RedHat.Tests/RedHat/RedHatConnectorHarnessTests.cs index 746867964..c94c033b6 100644 --- a/src/Concelier/__Tests/StellaOps.Concelier.Connector.Distro.RedHat.Tests/RedHat/RedHatConnectorHarnessTests.cs +++ b/src/Concelier/__Tests/StellaOps.Concelier.Connector.Distro.RedHat.Tests/RedHat/RedHatConnectorHarnessTests.cs @@ -2,23 +2,23 @@ using System; using System.IO; using System.Linq; using Microsoft.Extensions.DependencyInjection; -using MongoDB.Bson; +using StellaOps.Concelier.Bson; using StellaOps.Concelier.Connector.Common.Testing; using StellaOps.Concelier.Connector.Distro.RedHat; using StellaOps.Concelier.Connector.Distro.RedHat.Configuration; -using StellaOps.Concelier.Storage.Mongo; -using StellaOps.Concelier.Storage.Mongo.Advisories; +using StellaOps.Concelier.Storage; +using StellaOps.Concelier.Storage.Advisories; using StellaOps.Concelier.Testing; using StellaOps.Concelier.Testing; namespace StellaOps.Concelier.Connector.Distro.RedHat.Tests; -[Collection("mongo-fixture")] +[Collection(ConcelierFixtureCollection.Name)] public sealed class RedHatConnectorHarnessTests : IAsyncLifetime { private readonly ConnectorTestHarness _harness; - public RedHatConnectorHarnessTests(MongoIntegrationFixture fixture) + public RedHatConnectorHarnessTests(ConcelierPostgresFixture fixture) { _harness = new ConnectorTestHarness(fixture, new DateTimeOffset(2025, 10, 5, 0, 0, 0, TimeSpan.Zero), RedHatOptions.HttpClientName); } diff --git a/src/Concelier/__Tests/StellaOps.Concelier.Connector.Distro.RedHat.Tests/RedHat/RedHatConnectorTests.cs b/src/Concelier/__Tests/StellaOps.Concelier.Connector.Distro.RedHat.Tests/RedHat/RedHatConnectorTests.cs index cd633fdc3..650cf2ea2 100644 --- a/src/Concelier/__Tests/StellaOps.Concelier.Connector.Distro.RedHat.Tests/RedHat/RedHatConnectorTests.cs +++ b/src/Concelier/__Tests/StellaOps.Concelier.Connector.Distro.RedHat.Tests/RedHat/RedHatConnectorTests.cs @@ -13,7 +13,7 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; using Microsoft.Extensions.Time.Testing; -using MongoDB.Bson; +using StellaOps.Concelier.Bson; using StellaOps.Concelier.Connector.Common; using StellaOps.Concelier.Core.Jobs; using StellaOps.Concelier.Connector.Common.Fetch; @@ -23,10 +23,11 @@ using StellaOps.Concelier.Connector.Distro.RedHat; using StellaOps.Concelier.Connector.Distro.RedHat.Configuration; using StellaOps.Concelier.Connector.Distro.RedHat.Internal; using StellaOps.Concelier.Models; -using StellaOps.Concelier.Storage.Mongo; -using StellaOps.Concelier.Storage.Mongo.Advisories; -using StellaOps.Concelier.Storage.Mongo; -using StellaOps.Concelier.Storage.Mongo; +using StellaOps.Concelier.Storage; +using StellaOps.Concelier.Storage.Advisories; +using StellaOps.Concelier.Storage; +using StellaOps.Concelier.Storage; +using StellaOps.Concelier.Storage.Postgres; using StellaOps.Concelier.Testing; using StellaOps.Plugin; using Xunit; @@ -34,10 +35,10 @@ using Xunit.Abstractions; namespace StellaOps.Concelier.Connector.Distro.RedHat.Tests; -[Collection("mongo-fixture")] +[Collection(ConcelierFixtureCollection.Name)] public sealed class RedHatConnectorTests : IAsyncLifetime { - private readonly MongoIntegrationFixture _fixture; + private readonly ConcelierPostgresFixture _fixture; private readonly FakeTimeProvider _timeProvider; private readonly DateTimeOffset _initialNow; private readonly CannedHttpMessageHandler _handler; @@ -45,7 +46,7 @@ public sealed class RedHatConnectorTests : IAsyncLifetime private ServiceProvider? _serviceProvider; private const bool ForceUpdateGoldens = false; - public RedHatConnectorTests(MongoIntegrationFixture fixture, ITestOutputHelper output) + public RedHatConnectorTests(ConcelierPostgresFixture fixture, ITestOutputHelper output) { _fixture = fixture; _initialNow = new DateTimeOffset(2025, 10, 5, 0, 0, 0, TimeSpan.Zero); @@ -544,11 +545,11 @@ public sealed class RedHatConnectorTests : IAsyncLifetime services.AddSingleton(_timeProvider); services.AddSingleton(handler); - services.AddMongoStorage(storageOptions => + services.AddConcelierPostgresStorage(storageOptions => { - storageOptions.ConnectionString = _fixture.Runner.ConnectionString; - storageOptions.DatabaseName = _fixture.Database.DatabaseNamespace.DatabaseName; - storageOptions.CommandTimeout = TimeSpan.FromSeconds(5); + storageOptions.ConnectionString = _fixture.ConnectionString; + storageOptions.SchemaName = _fixture.SchemaName; + storageOptions.CommandTimeoutSeconds = 5; }); services.AddSourceCommon(); @@ -584,10 +585,7 @@ public sealed class RedHatConnectorTests : IAsyncLifetime }); }); - var provider = services.BuildServiceProvider(); - var bootstrapper = provider.GetRequiredService(); - await bootstrapper.InitializeAsync(CancellationToken.None); - return provider; + return services.BuildServiceProvider(); } private Task ResetDatabaseAsync() @@ -611,7 +609,7 @@ public sealed class RedHatConnectorTests : IAsyncLifetime _serviceProvider = null; } - await _fixture.Client.DropDatabaseAsync(_fixture.Database.DatabaseNamespace.DatabaseName); + await _fixture.TruncateAllTablesAsync(); _handler.Clear(); _timeProvider.SetUtcNow(_initialNow); } diff --git a/src/Concelier/__Tests/StellaOps.Concelier.Connector.Distro.Suse.Tests/SuseConnectorTests.cs b/src/Concelier/__Tests/StellaOps.Concelier.Connector.Distro.Suse.Tests/SuseConnectorTests.cs index 9c991621d..8036a950d 100644 --- a/src/Concelier/__Tests/StellaOps.Concelier.Connector.Distro.Suse.Tests/SuseConnectorTests.cs +++ b/src/Concelier/__Tests/StellaOps.Concelier.Connector.Distro.Suse.Tests/SuseConnectorTests.cs @@ -4,64 +4,53 @@ using System.Linq; using System.Net; using System.Net.Http; using System.Net.Http.Headers; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Http; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Abstractions; -using Microsoft.Extensions.Time.Testing; -using StellaOps.Concelier.Models; -using StellaOps.Concelier.Connector.Common; -using StellaOps.Concelier.Connector.Common.Http; -using StellaOps.Concelier.Connector.Common.Testing; -using StellaOps.Concelier.Connector.Distro.Suse; -using StellaOps.Concelier.Connector.Distro.Suse.Configuration; -using StellaOps.Concelier.Storage.Mongo; -using StellaOps.Concelier.Storage.Mongo.Advisories; -using StellaOps.Concelier.Storage.Mongo; -using StellaOps.Concelier.Storage.Mongo; -using StellaOps.Concelier.Testing; -using Xunit; -using Xunit.Abstractions; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using StellaOps.Concelier.Models; +using StellaOps.Concelier.Connector.Common; +using StellaOps.Concelier.Connector.Common.Testing; +using StellaOps.Concelier.Connector.Distro.Suse; +using StellaOps.Concelier.Connector.Distro.Suse.Configuration; +using StellaOps.Concelier.Storage; +using StellaOps.Concelier.Storage.Advisories; +using StellaOps.Concelier.Testing; +using Xunit; +using Xunit.Abstractions; namespace StellaOps.Concelier.Connector.Distro.Suse.Tests; -[Collection("mongo-fixture")] -public sealed class SuseConnectorTests : IAsyncLifetime -{ +[Collection(ConcelierFixtureCollection.Name)] +public sealed class SuseConnectorTests +{ private static readonly Uri ChangesUri = new("https://ftp.suse.com/pub/projects/security/csaf/changes.csv"); private static readonly Uri AdvisoryResolvedUri = new("https://ftp.suse.com/pub/projects/security/csaf/suse-su-2025_0001-1.json"); - private static readonly Uri AdvisoryOpenUri = new("https://ftp.suse.com/pub/projects/security/csaf/suse-su-2025_0002-1.json"); + private static readonly Uri AdvisoryOpenUri = new("https://ftp.suse.com/pub/projects/security/csaf/suse-su-2025_0002-1.json"); + + private readonly ConcelierPostgresFixture _fixture; + + public SuseConnectorTests(ConcelierPostgresFixture fixture, ITestOutputHelper output) + { + _fixture = fixture; + } - private readonly MongoIntegrationFixture _fixture; - private readonly FakeTimeProvider _timeProvider; - private readonly CannedHttpMessageHandler _handler; - - public SuseConnectorTests(MongoIntegrationFixture fixture, ITestOutputHelper output) - { - _fixture = fixture; - _timeProvider = new FakeTimeProvider(new DateTimeOffset(2025, 1, 22, 0, 0, 0, TimeSpan.Zero)); - _handler = new CannedHttpMessageHandler(); - } - - [Fact] - public async Task FetchParseMap_ProcessesResolvedAndOpenNotices() - { - await using var provider = await BuildServiceProviderAsync(); - - SeedInitialResponses(); - - var connector = provider.GetRequiredService(); - await connector.FetchAsync(provider, CancellationToken.None); - _timeProvider.Advance(TimeSpan.FromMinutes(1)); - await connector.ParseAsync(provider, CancellationToken.None); - await connector.MapAsync(provider, CancellationToken.None); - - var advisoryStore = provider.GetRequiredService(); - var advisories = await advisoryStore.GetRecentAsync(10, CancellationToken.None); - Assert.Equal(2, advisories.Count); + [Fact] + public async Task FetchParseMap_ProcessesResolvedAndOpenNotices() + { + await using var harness = await BuildHarnessAsync(); + + SeedInitialResponses(harness.Handler); + + var connector = harness.ServiceProvider.GetRequiredService(); + await connector.FetchAsync(harness.ServiceProvider, CancellationToken.None); + harness.TimeProvider.Advance(TimeSpan.FromMinutes(1)); + await connector.ParseAsync(harness.ServiceProvider, CancellationToken.None); + await connector.MapAsync(harness.ServiceProvider, CancellationToken.None); + + var advisoryStore = harness.ServiceProvider.GetRequiredService(); + var advisories = await advisoryStore.GetRecentAsync(10, CancellationToken.None); + Assert.Equal(2, advisories.Count); var resolved = advisories.Single(a => a.AdvisoryKey == "SUSE-SU-2025:0001-1"); var resolvedPackage = Assert.Single(resolved.AffectedPackages); @@ -71,77 +60,54 @@ public sealed class SuseConnectorTests : IAsyncLifetime Assert.NotNull(resolvedRange.Primitives!.Nevra?.Fixed); var open = advisories.Single(a => a.AdvisoryKey == "SUSE-SU-2025:0002-1"); - var openPackage = Assert.Single(open.AffectedPackages); - Assert.Equal(AffectedPackageStatusCatalog.UnderInvestigation, openPackage.Statuses.Single().Status); - - SeedNotModifiedResponses(); - - await connector.FetchAsync(provider, CancellationToken.None); - _timeProvider.Advance(TimeSpan.FromMinutes(1)); - await connector.ParseAsync(provider, CancellationToken.None); - await connector.MapAsync(provider, CancellationToken.None); - - advisories = await advisoryStore.GetRecentAsync(10, CancellationToken.None); - Assert.Equal(2, advisories.Count); - _handler.AssertNoPendingResponses(); - } - - private async Task BuildServiceProviderAsync() - { - await _fixture.Client.DropDatabaseAsync(_fixture.Database.DatabaseNamespace.DatabaseName); - _handler.Clear(); - - var services = new ServiceCollection(); - services.AddLogging(builder => builder.AddProvider(NullLoggerProvider.Instance)); - services.AddSingleton(_timeProvider); - services.AddSingleton(_handler); - - services.AddMongoStorage(options => - { - options.ConnectionString = _fixture.Runner.ConnectionString; - options.DatabaseName = _fixture.Database.DatabaseNamespace.DatabaseName; - options.CommandTimeout = TimeSpan.FromSeconds(5); - }); - - services.AddSourceCommon(); - services.AddSuseConnector(options => - { - options.ChangesEndpoint = ChangesUri; - options.AdvisoryBaseUri = new Uri("https://ftp.suse.com/pub/projects/security/csaf/"); - options.MaxAdvisoriesPerFetch = 5; - options.RequestDelay = TimeSpan.Zero; - }); - - services.Configure(SuseOptions.HttpClientName, builderOptions => - { - builderOptions.HttpMessageHandlerBuilderActions.Add(builder => - { - builder.PrimaryHandler = _handler; - }); - }); - - var provider = services.BuildServiceProvider(); - var bootstrapper = provider.GetRequiredService(); - await bootstrapper.InitializeAsync(CancellationToken.None); - return provider; - } - - private void SeedInitialResponses() - { - _handler.AddResponse(ChangesUri, () => BuildResponse(HttpStatusCode.OK, "suse-changes.csv", "\"changes-v1\"")); - _handler.AddResponse(AdvisoryResolvedUri, () => BuildResponse(HttpStatusCode.OK, "suse-su-2025_0001-1.json", "\"adv-1\"")); - _handler.AddResponse(AdvisoryOpenUri, () => BuildResponse(HttpStatusCode.OK, "suse-su-2025_0002-1.json", "\"adv-2\"")); - } - - private void SeedNotModifiedResponses() - { - _handler.AddResponse(ChangesUri, () => BuildResponse(HttpStatusCode.NotModified, "suse-changes.csv", "\"changes-v1\"")); - } - - private HttpResponseMessage BuildResponse(HttpStatusCode statusCode, string fixture, string etag) - { - var response = new HttpResponseMessage(statusCode); - if (statusCode == HttpStatusCode.OK) + var openPackage = Assert.Single(open.AffectedPackages); + Assert.Equal(AffectedPackageStatusCatalog.UnderInvestigation, openPackage.Statuses.Single().Status); + + SeedNotModifiedResponses(harness.Handler); + + await connector.FetchAsync(harness.ServiceProvider, CancellationToken.None); + harness.TimeProvider.Advance(TimeSpan.FromMinutes(1)); + await connector.ParseAsync(harness.ServiceProvider, CancellationToken.None); + await connector.MapAsync(harness.ServiceProvider, CancellationToken.None); + + advisories = await advisoryStore.GetRecentAsync(10, CancellationToken.None); + Assert.Equal(2, advisories.Count); + harness.Handler.AssertNoPendingResponses(); + } + + private async Task BuildHarnessAsync() + { + var initialTime = new DateTimeOffset(2025, 1, 22, 0, 0, 0, TimeSpan.Zero); + var harness = new ConnectorTestHarness(_fixture, initialTime, SuseOptions.HttpClientName); + await harness.EnsureServiceProviderAsync(services => + { + services.AddSuseConnector(options => + { + options.ChangesEndpoint = ChangesUri; + options.AdvisoryBaseUri = new Uri("https://ftp.suse.com/pub/projects/security/csaf/"); + options.MaxAdvisoriesPerFetch = 5; + options.RequestDelay = TimeSpan.Zero; + }); + }); + return harness; + } + + private static void SeedInitialResponses(CannedHttpMessageHandler handler) + { + handler.AddResponse(ChangesUri, () => BuildResponse(HttpStatusCode.OK, "suse-changes.csv", "\"changes-v1\"")); + handler.AddResponse(AdvisoryResolvedUri, () => BuildResponse(HttpStatusCode.OK, "suse-su-2025_0001-1.json", "\"adv-1\"")); + handler.AddResponse(AdvisoryOpenUri, () => BuildResponse(HttpStatusCode.OK, "suse-su-2025_0002-1.json", "\"adv-2\"")); + } + + private static void SeedNotModifiedResponses(CannedHttpMessageHandler handler) + { + handler.AddResponse(ChangesUri, () => BuildResponse(HttpStatusCode.NotModified, "suse-changes.csv", "\"changes-v1\"")); + } + + private static HttpResponseMessage BuildResponse(HttpStatusCode statusCode, string fixture, string etag) + { + var response = new HttpResponseMessage(statusCode); + if (statusCode == HttpStatusCode.OK) { var contentType = fixture.EndsWith(".csv", StringComparison.OrdinalIgnoreCase) ? "text/csv" : "application/json"; response.Content = new StringContent(ReadFixture(Path.Combine("Source", "Distro", "Suse", "Fixtures", fixture)), Encoding.UTF8, contentType); @@ -158,11 +124,7 @@ public sealed class SuseConnectorTests : IAsyncLifetime { throw new FileNotFoundException($"Fixture '{relativePath}' not found.", path); } - - return File.ReadAllText(path); - } - - public Task InitializeAsync() => Task.CompletedTask; - - public Task DisposeAsync() => Task.CompletedTask; -} + + return File.ReadAllText(path); + } +} diff --git a/src/Concelier/__Tests/StellaOps.Concelier.Connector.Distro.Suse.Tests/SuseMapperTests.cs b/src/Concelier/__Tests/StellaOps.Concelier.Connector.Distro.Suse.Tests/SuseMapperTests.cs index 106c2b2df..dae3a944d 100644 --- a/src/Concelier/__Tests/StellaOps.Concelier.Connector.Distro.Suse.Tests/SuseMapperTests.cs +++ b/src/Concelier/__Tests/StellaOps.Concelier.Connector.Distro.Suse.Tests/SuseMapperTests.cs @@ -1,12 +1,12 @@ using System; using System.Collections.Generic; using System.IO; -using MongoDB.Bson; +using StellaOps.Concelier.Bson; using StellaOps.Concelier.Models; using StellaOps.Concelier.Connector.Common; using StellaOps.Concelier.Connector.Distro.Suse; using StellaOps.Concelier.Connector.Distro.Suse.Internal; -using StellaOps.Concelier.Storage.Mongo; +using StellaOps.Concelier.Storage; using Xunit; namespace StellaOps.Concelier.Connector.Distro.Suse.Tests; diff --git a/src/Concelier/__Tests/StellaOps.Concelier.Connector.Distro.Ubuntu.Tests/UbuntuConnectorTests.cs b/src/Concelier/__Tests/StellaOps.Concelier.Connector.Distro.Ubuntu.Tests/UbuntuConnectorTests.cs index 7c29b1227..b0e4ce903 100644 --- a/src/Concelier/__Tests/StellaOps.Concelier.Connector.Distro.Ubuntu.Tests/UbuntuConnectorTests.cs +++ b/src/Concelier/__Tests/StellaOps.Concelier.Connector.Distro.Ubuntu.Tests/UbuntuConnectorTests.cs @@ -4,61 +4,52 @@ using System.Linq; using System.Net; using System.Net.Http; using System.Net.Http.Headers; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Http; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Abstractions; -using Microsoft.Extensions.Time.Testing; -using StellaOps.Concelier.Models; -using StellaOps.Concelier.Connector.Common; -using StellaOps.Concelier.Connector.Common.Http; -using StellaOps.Concelier.Connector.Common.Testing; -using StellaOps.Concelier.Connector.Distro.Ubuntu; -using StellaOps.Concelier.Connector.Distro.Ubuntu.Configuration; -using StellaOps.Concelier.Storage.Mongo; -using StellaOps.Concelier.Storage.Mongo.Advisories; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using StellaOps.Concelier.Models; +using StellaOps.Concelier.Connector.Common; +using StellaOps.Concelier.Connector.Common.Testing; +using StellaOps.Concelier.Connector.Distro.Ubuntu; +using StellaOps.Concelier.Connector.Distro.Ubuntu.Configuration; +using StellaOps.Concelier.Storage; +using StellaOps.Concelier.Storage.Advisories; using StellaOps.Concelier.Testing; using StellaOps.Cryptography.DependencyInjection; -using Xunit; +using Xunit; namespace StellaOps.Concelier.Connector.Distro.Ubuntu.Tests; -[Collection("mongo-fixture")] -public sealed class UbuntuConnectorTests : IAsyncLifetime -{ +[Collection(ConcelierFixtureCollection.Name)] +public sealed class UbuntuConnectorTests +{ private static readonly Uri IndexPage0Uri = new("https://ubuntu.com/security/notices.json?offset=0&limit=1"); - private static readonly Uri IndexPage1Uri = new("https://ubuntu.com/security/notices.json?offset=1&limit=1"); - - private readonly MongoIntegrationFixture _fixture; - private readonly FakeTimeProvider _timeProvider; - private readonly CannedHttpMessageHandler _handler; - - public UbuntuConnectorTests(MongoIntegrationFixture fixture) - { - _fixture = fixture; - _timeProvider = new FakeTimeProvider(new DateTimeOffset(2025, 1, 25, 0, 0, 0, TimeSpan.Zero)); - _handler = new CannedHttpMessageHandler(); - } - - [Fact] - public async Task FetchParseMap_GeneratesEvrRangePrimitives() - { - await using var provider = await BuildServiceProviderAsync(); - - SeedInitialResponses(); - - var connector = provider.GetRequiredService(); - await connector.FetchAsync(provider, CancellationToken.None); - _timeProvider.Advance(TimeSpan.FromMinutes(1)); - await connector.ParseAsync(provider, CancellationToken.None); - await connector.MapAsync(provider, CancellationToken.None); - - var advisoryStore = provider.GetRequiredService(); - var advisories = await advisoryStore.GetRecentAsync(10, CancellationToken.None); - Assert.Equal(2, advisories.Count); + private static readonly Uri IndexPage1Uri = new("https://ubuntu.com/security/notices.json?offset=1&limit=1"); + + private readonly ConcelierPostgresFixture _fixture; + + public UbuntuConnectorTests(ConcelierPostgresFixture fixture) + { + _fixture = fixture; + } + + [Fact] + public async Task FetchParseMap_GeneratesEvrRangePrimitives() + { + await using var harness = await BuildHarnessAsync(); + + SeedInitialResponses(harness.Handler); + + var connector = harness.ServiceProvider.GetRequiredService(); + await connector.FetchAsync(harness.ServiceProvider, CancellationToken.None); + harness.TimeProvider.Advance(TimeSpan.FromMinutes(1)); + await connector.ParseAsync(harness.ServiceProvider, CancellationToken.None); + await connector.MapAsync(harness.ServiceProvider, CancellationToken.None); + + var advisoryStore = harness.ServiceProvider.GetRequiredService(); + var advisories = await advisoryStore.GetRecentAsync(10, CancellationToken.None); + Assert.Equal(2, advisories.Count); var kernelNotice = advisories.Single(a => a.AdvisoryKey == "USN-9001-1"); var noblePackage = Assert.Single(kernelNotice.AffectedPackages, pkg => pkg.Platform == "noble"); @@ -73,95 +64,72 @@ public sealed class UbuntuConnectorTests : IAsyncLifetime Assert.Equal(range.Primitives.Evr!.Fixed!.ToCanonicalString(), normalizedRule.Max); Assert.Equal("ubuntu:noble", normalizedRule.Notes); - SeedNotModifiedResponses(); - - await connector.FetchAsync(provider, CancellationToken.None); - _timeProvider.Advance(TimeSpan.FromMinutes(1)); - await connector.ParseAsync(provider, CancellationToken.None); - await connector.MapAsync(provider, CancellationToken.None); - - advisories = await advisoryStore.GetRecentAsync(10, CancellationToken.None); - Assert.Equal(2, advisories.Count); - _handler.AssertNoPendingResponses(); - } - - private async Task BuildServiceProviderAsync() - { - await _fixture.Client.DropDatabaseAsync(_fixture.Database.DatabaseNamespace.DatabaseName); - _handler.Clear(); - - var services = new ServiceCollection(); - services.AddLogging(builder => builder.AddProvider(NullLoggerProvider.Instance)); - services.AddSingleton(_timeProvider); - services.AddSingleton(_handler); - - services.AddMongoStorage(options => + SeedNotModifiedResponses(harness.Handler); + + await connector.FetchAsync(harness.ServiceProvider, CancellationToken.None); + harness.TimeProvider.Advance(TimeSpan.FromMinutes(1)); + await connector.ParseAsync(harness.ServiceProvider, CancellationToken.None); + await connector.MapAsync(harness.ServiceProvider, CancellationToken.None); + + advisories = await advisoryStore.GetRecentAsync(10, CancellationToken.None); + Assert.Equal(2, advisories.Count); + harness.Handler.AssertNoPendingResponses(); + } + + private async Task BuildHarnessAsync() + { + var initialTime = new DateTimeOffset(2025, 1, 25, 0, 0, 0, TimeSpan.Zero); + var harness = new ConnectorTestHarness(_fixture, initialTime, UbuntuOptions.HttpClientName); + await harness.EnsureServiceProviderAsync(services => { - options.ConnectionString = _fixture.Runner.ConnectionString; - options.DatabaseName = _fixture.Database.DatabaseNamespace.DatabaseName; - options.CommandTimeout = TimeSpan.FromSeconds(5); + services.AddStellaOpsCrypto(); + services.AddUbuntuConnector(options => + { + options.NoticesEndpoint = new Uri("https://ubuntu.com/security/notices.json"); + options.NoticeDetailBaseUri = new Uri("https://ubuntu.com/security/"); + options.MaxNoticesPerFetch = 2; + options.IndexPageSize = 1; + }); + }); + return harness; + } + + private static void SeedInitialResponses(CannedHttpMessageHandler handler) + { + handler.AddResponse(IndexPage0Uri, () => + { + var response = new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(ReadFixture("Fixtures/ubuntu-notices-page0.json"), Encoding.UTF8, "application/json") + }; + response.Headers.ETag = new EntityTagHeaderValue("\"index-page0-v1\""); + return response; }); - services.AddSourceCommon(); - services.AddStellaOpsCrypto(); - services.AddUbuntuConnector(options => + handler.AddResponse(IndexPage1Uri, () => { - options.NoticesEndpoint = new Uri("https://ubuntu.com/security/notices.json"); - options.NoticeDetailBaseUri = new Uri("https://ubuntu.com/security/"); - options.MaxNoticesPerFetch = 2; - options.IndexPageSize = 1; - }); - - services.Configure(UbuntuOptions.HttpClientName, builderOptions => - { - builderOptions.HttpMessageHandlerBuilderActions.Add(builder => - { - builder.PrimaryHandler = _handler; - }); - }); - - var provider = services.BuildServiceProvider(); - var bootstrapper = provider.GetRequiredService(); - await bootstrapper.InitializeAsync(CancellationToken.None); - return provider; - } - - private void SeedInitialResponses() - { - _handler.AddResponse(IndexPage0Uri, () => - { - var response = new HttpResponseMessage(HttpStatusCode.OK) - { - Content = new StringContent(ReadFixture("Fixtures/ubuntu-notices-page0.json"), Encoding.UTF8, "application/json") - }; - response.Headers.ETag = new EntityTagHeaderValue("\"index-page0-v1\""); - return response; - }); - - _handler.AddResponse(IndexPage1Uri, () => - { - var response = new HttpResponseMessage(HttpStatusCode.OK) - { - Content = new StringContent(ReadFixture("Fixtures/ubuntu-notices-page1.json"), Encoding.UTF8, "application/json") + var response = new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(ReadFixture("Fixtures/ubuntu-notices-page1.json"), Encoding.UTF8, "application/json") }; response.Headers.ETag = new EntityTagHeaderValue("\"index-page1-v1\""); - return response; - }); - } - - private void SeedNotModifiedResponses() - { - _handler.AddResponse(IndexPage0Uri, () => - { - var response = new HttpResponseMessage(HttpStatusCode.NotModified); - response.Headers.ETag = new EntityTagHeaderValue("\"index-page0-v1\""); - return response; - }); + return response; + }); + } + + private static void SeedNotModifiedResponses(CannedHttpMessageHandler handler) + { + handler.AddResponse(IndexPage0Uri, () => + { + var response = new HttpResponseMessage(HttpStatusCode.NotModified); + response.Headers.ETag = new EntityTagHeaderValue("\"index-page0-v1\""); + return response; + }); // Page 1 remains cached; the connector should skip fetching it when page 0 is unchanged. } - - private static string ReadFixture(string relativePath) + + private static string ReadFixture(string relativePath) { var path = Path.Combine(AppContext.BaseDirectory, relativePath.Replace('/', Path.DirectorySeparatorChar)); if (!File.Exists(path)) @@ -169,10 +137,6 @@ public sealed class UbuntuConnectorTests : IAsyncLifetime throw new FileNotFoundException($"Fixture '{relativePath}' not found.", path); } - return File.ReadAllText(path); - } - - public Task InitializeAsync() => Task.CompletedTask; - - public Task DisposeAsync() => Task.CompletedTask; -} + return File.ReadAllText(path); + } +} diff --git a/src/Concelier/__Tests/StellaOps.Concelier.Connector.Ghsa.Tests/Ghsa/GhsaConflictFixtureTests.cs b/src/Concelier/__Tests/StellaOps.Concelier.Connector.Ghsa.Tests/Ghsa/GhsaConflictFixtureTests.cs index 8274733c1..795a36372 100644 --- a/src/Concelier/__Tests/StellaOps.Concelier.Connector.Ghsa.Tests/Ghsa/GhsaConflictFixtureTests.cs +++ b/src/Concelier/__Tests/StellaOps.Concelier.Connector.Ghsa.Tests/Ghsa/GhsaConflictFixtureTests.cs @@ -1,6 +1,6 @@ using StellaOps.Concelier.Models; using StellaOps.Concelier.Connector.Ghsa.Internal; -using StellaOps.Concelier.Storage.Mongo; +using StellaOps.Concelier.Storage; namespace StellaOps.Concelier.Connector.Ghsa.Tests; diff --git a/src/Concelier/__Tests/StellaOps.Concelier.Connector.Ghsa.Tests/Ghsa/GhsaConnectorTests.cs b/src/Concelier/__Tests/StellaOps.Concelier.Connector.Ghsa.Tests/Ghsa/GhsaConnectorTests.cs index 511094db3..3f2baf0b5 100644 --- a/src/Concelier/__Tests/StellaOps.Concelier.Connector.Ghsa.Tests/Ghsa/GhsaConnectorTests.cs +++ b/src/Concelier/__Tests/StellaOps.Concelier.Connector.Ghsa.Tests/Ghsa/GhsaConnectorTests.cs @@ -1,7 +1,7 @@ using System.Net; using System.Net.Http; using System.Text; -using MongoDB.Bson; +using StellaOps.Concelier.Bson; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; @@ -10,18 +10,18 @@ using StellaOps.Concelier.Connector.Common.Testing; using StellaOps.Concelier.Connector.Ghsa.Configuration; using StellaOps.Concelier.Connector.Ghsa.Internal; using StellaOps.Concelier.Testing; -using StellaOps.Concelier.Storage.Mongo; -using StellaOps.Concelier.Storage.Mongo.Advisories; +using StellaOps.Concelier.Storage; +using StellaOps.Concelier.Storage.Advisories; namespace StellaOps.Concelier.Connector.Ghsa.Tests; -[Collection("mongo-fixture")] +[Collection(ConcelierFixtureCollection.Name)] public sealed class GhsaConnectorTests : IAsyncLifetime { - private readonly MongoIntegrationFixture _fixture; + private readonly ConcelierPostgresFixture _fixture; private ConnectorTestHarness? _harness; - public GhsaConnectorTests(MongoIntegrationFixture fixture) + public GhsaConnectorTests(ConcelierPostgresFixture fixture) { _fixture = fixture; } diff --git a/src/Concelier/__Tests/StellaOps.Concelier.Connector.Ghsa.Tests/Ghsa/GhsaMapperTests.cs b/src/Concelier/__Tests/StellaOps.Concelier.Connector.Ghsa.Tests/Ghsa/GhsaMapperTests.cs index f23606b3a..df00cca94 100644 --- a/src/Concelier/__Tests/StellaOps.Concelier.Connector.Ghsa.Tests/Ghsa/GhsaMapperTests.cs +++ b/src/Concelier/__Tests/StellaOps.Concelier.Connector.Ghsa.Tests/Ghsa/GhsaMapperTests.cs @@ -1,5 +1,5 @@ using StellaOps.Concelier.Connector.Ghsa.Internal; -using StellaOps.Concelier.Storage.Mongo; +using StellaOps.Concelier.Storage; namespace StellaOps.Concelier.Connector.Ghsa.Tests; diff --git a/src/Concelier/__Tests/StellaOps.Concelier.Connector.Ics.Cisa.Tests/IcsCisaConnectorTests.cs b/src/Concelier/__Tests/StellaOps.Concelier.Connector.Ics.Cisa.Tests/IcsCisaConnectorTests.cs index 06dd6694b..3bf815e84 100644 --- a/src/Concelier/__Tests/StellaOps.Concelier.Connector.Ics.Cisa.Tests/IcsCisaConnectorTests.cs +++ b/src/Concelier/__Tests/StellaOps.Concelier.Connector.Ics.Cisa.Tests/IcsCisaConnectorTests.cs @@ -3,50 +3,45 @@ using System.IO; using System.Linq; using System.Net; using System.Net.Http; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Http; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Abstractions; -using StellaOps.Concelier.Connector.Common.Testing; -using StellaOps.Concelier.Connector.Common.Http; -using StellaOps.Concelier.Connector.Ics.Cisa; -using StellaOps.Concelier.Connector.Ics.Cisa.Configuration; -using StellaOps.Concelier.Storage.Mongo; -using StellaOps.Concelier.Storage.Mongo.Advisories; -using StellaOps.Concelier.Testing; -using Xunit; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using StellaOps.Concelier.Connector.Common.Testing; +using StellaOps.Concelier.Connector.Ics.Cisa; +using StellaOps.Concelier.Connector.Ics.Cisa.Configuration; +using StellaOps.Concelier.Storage; +using StellaOps.Concelier.Storage.Advisories; +using StellaOps.Concelier.Testing; +using Xunit; namespace StellaOps.Concelier.Connector.Ics.Cisa.Tests; -[Collection("mongo-fixture")] -public sealed class IcsCisaConnectorTests : IAsyncLifetime -{ - private readonly MongoIntegrationFixture _fixture; - private readonly CannedHttpMessageHandler _handler = new(); +[Collection(ConcelierFixtureCollection.Name)] +public sealed class IcsCisaConnectorTests +{ + private readonly ConcelierPostgresFixture _fixture; + + public IcsCisaConnectorTests(ConcelierPostgresFixture fixture) + { + _fixture = fixture ?? throw new ArgumentNullException(nameof(fixture)); + } - public IcsCisaConnectorTests(MongoIntegrationFixture fixture) - { - _fixture = fixture ?? throw new ArgumentNullException(nameof(fixture)); - } - - [Fact] - public async Task FetchParseMap_EndToEnd_ProducesCanonicalAdvisories() - { - await using var provider = await BuildServiceProviderAsync(); - RegisterResponses(); - - var connector = provider.GetRequiredService(); - await connector.FetchAsync(provider, CancellationToken.None); - await connector.ParseAsync(provider, CancellationToken.None); - await connector.MapAsync(provider, CancellationToken.None); - - _handler.AssertNoPendingResponses(); - - var advisoryStore = provider.GetRequiredService(); - var advisories = await advisoryStore.GetRecentAsync(10, CancellationToken.None); + [Fact] + public async Task FetchParseMap_EndToEnd_ProducesCanonicalAdvisories() + { + await using var harness = await BuildHarnessAsync(); + RegisterResponses(harness.Handler); + + var connector = harness.ServiceProvider.GetRequiredService(); + await connector.FetchAsync(harness.ServiceProvider, CancellationToken.None); + await connector.ParseAsync(harness.ServiceProvider, CancellationToken.None); + await connector.MapAsync(harness.ServiceProvider, CancellationToken.None); + + harness.Handler.AssertNoPendingResponses(); + + var advisoryStore = harness.ServiceProvider.GetRequiredService(); + var advisories = await advisoryStore.GetRecentAsync(10, CancellationToken.None); Assert.Equal(2, advisories.Count); @@ -78,78 +73,48 @@ public sealed class IcsCisaConnectorTests : IAsyncLifetime Assert.Contains(icsma.References, reference => reference.Url == "https://www.cisa.gov/sites/default/files/2025-10/ICSMA-25-045-01_Supplement.pdf"); var infusionPackage = Assert.Single(icsma.AffectedPackages, package => string.Equals(package.Identifier, "InfusionManager", StringComparison.OrdinalIgnoreCase)); var infusionRange = Assert.Single(infusionPackage.VersionRanges); - Assert.Equal("2.1", infusionRange.RangeExpression); - } - - private async Task BuildServiceProviderAsync() - { - await _fixture.Client.DropDatabaseAsync(_fixture.Database.DatabaseNamespace.DatabaseName); - _handler.Clear(); - - var services = new ServiceCollection(); - services.AddLogging(builder => builder.AddProvider(NullLoggerProvider.Instance)); - services.AddSingleton(_handler); - - services.AddMongoStorage(options => - { - options.ConnectionString = _fixture.Runner.ConnectionString; - options.DatabaseName = _fixture.Database.DatabaseNamespace.DatabaseName; - options.CommandTimeout = TimeSpan.FromSeconds(5); - }); - - services.AddSourceCommon(); - services.AddIcsCisaConnector(options => - { - options.GovDeliveryCode = "TESTCODE"; - options.TopicsEndpoint = new Uri("https://feed.test/topics.rss", UriKind.Absolute); - options.TopicIds.Clear(); - options.TopicIds.Add("USDHSCISA_TEST"); - options.RequestDelay = TimeSpan.Zero; - options.DetailBaseUri = new Uri("https://www.cisa.gov/", UriKind.Absolute); - options.AdditionalHosts.Add("files.cisa.gov"); - }); - - services.Configure(IcsCisaOptions.HttpClientName, builder => - { - builder.HttpMessageHandlerBuilderActions.Add(handlerBuilder => - { - handlerBuilder.PrimaryHandler = _handler; - }); - }); - - var provider = services.BuildServiceProvider(); - var bootstrapper = provider.GetRequiredService(); - await bootstrapper.InitializeAsync(CancellationToken.None); - return provider; - } - - private void RegisterResponses() - { - var feedUri = new Uri("https://feed.test/topics.rss?code=TESTCODE&format=xml&topic_id=USDHSCISA_TEST", UriKind.Absolute); - _handler.AddResponse(feedUri, () => CreateTextResponse("IcsCisa/Fixtures/sample-feed.xml", "application/rss+xml")); - - var icsaDetail = new Uri("https://www.cisa.gov/news-events/ics-advisories/icsa-25-123-01", UriKind.Absolute); - _handler.AddResponse(icsaDetail, () => CreateTextResponse("IcsCisa/Fixtures/icsa-25-123-01.html", "text/html")); - - var icsmaDetail = new Uri("https://www.cisa.gov/news-events/ics-medical-advisories/icsma-25-045-01", UriKind.Absolute); - _handler.AddResponse(icsmaDetail, () => CreateTextResponse("IcsCisa/Fixtures/icsma-25-045-01.html", "text/html")); - } + Assert.Equal("2.1", infusionRange.RangeExpression); + } + + private async Task BuildHarnessAsync() + { + var initialTime = new DateTimeOffset(2025, 10, 12, 0, 0, 0, TimeSpan.Zero); + var harness = new ConnectorTestHarness(_fixture, initialTime, IcsCisaOptions.HttpClientName); + await harness.EnsureServiceProviderAsync(services => + { + services.AddIcsCisaConnector(options => + { + options.GovDeliveryCode = "TESTCODE"; + options.TopicsEndpoint = new Uri("https://feed.test/topics.rss", UriKind.Absolute); + options.TopicIds.Clear(); + options.TopicIds.Add("USDHSCISA_TEST"); + options.RequestDelay = TimeSpan.Zero; + options.DetailBaseUri = new Uri("https://www.cisa.gov/", UriKind.Absolute); + options.AdditionalHosts.Add("files.cisa.gov"); + }); + }); + return harness; + } + + private static void RegisterResponses(CannedHttpMessageHandler handler) + { + var feedUri = new Uri("https://feed.test/topics.rss?code=TESTCODE&format=xml&topic_id=USDHSCISA_TEST", UriKind.Absolute); + handler.AddResponse(feedUri, () => CreateTextResponse("IcsCisa/Fixtures/sample-feed.xml", "application/rss+xml")); + + var icsaDetail = new Uri("https://www.cisa.gov/news-events/ics-advisories/icsa-25-123-01", UriKind.Absolute); + handler.AddResponse(icsaDetail, () => CreateTextResponse("IcsCisa/Fixtures/icsa-25-123-01.html", "text/html")); + + var icsmaDetail = new Uri("https://www.cisa.gov/news-events/ics-medical-advisories/icsma-25-045-01", UriKind.Absolute); + handler.AddResponse(icsmaDetail, () => CreateTextResponse("IcsCisa/Fixtures/icsma-25-045-01.html", "text/html")); + } private static HttpResponseMessage CreateTextResponse(string relativePath, string contentType) { var fullPath = Path.Combine(AppContext.BaseDirectory, relativePath); var content = File.ReadAllText(fullPath); - return new HttpResponseMessage(HttpStatusCode.OK) - { - Content = new StringContent(content, Encoding.UTF8, contentType), - }; - } - - public Task InitializeAsync() => Task.CompletedTask; - - public Task DisposeAsync() - { - _handler.Clear(); - return Task.CompletedTask; - } -} + return new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(content, Encoding.UTF8, contentType), + }; + } +} diff --git a/src/Concelier/__Tests/StellaOps.Concelier.Connector.Ics.Kaspersky.Tests/Kaspersky/KasperskyConnectorTests.cs b/src/Concelier/__Tests/StellaOps.Concelier.Connector.Ics.Kaspersky.Tests/Kaspersky/KasperskyConnectorTests.cs index 4d1c78526..38babf4bb 100644 --- a/src/Concelier/__Tests/StellaOps.Concelier.Connector.Ics.Kaspersky.Tests/Kaspersky/KasperskyConnectorTests.cs +++ b/src/Concelier/__Tests/StellaOps.Concelier.Connector.Ics.Kaspersky.Tests/Kaspersky/KasperskyConnectorTests.cs @@ -11,7 +11,7 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; using Microsoft.Extensions.Time.Testing; -using MongoDB.Bson; +using StellaOps.Concelier.Bson; using StellaOps.Concelier.Models; using StellaOps.Concelier.Connector.Common.Fetch; using StellaOps.Concelier.Connector.Common.Http; @@ -19,23 +19,24 @@ using StellaOps.Concelier.Connector.Common.Testing; using StellaOps.Concelier.Connector.Common; using StellaOps.Concelier.Connector.Ics.Kaspersky; using StellaOps.Concelier.Connector.Ics.Kaspersky.Configuration; -using StellaOps.Concelier.Storage.Mongo; -using StellaOps.Concelier.Storage.Mongo.Advisories; -using StellaOps.Concelier.Storage.Mongo; -using StellaOps.Concelier.Storage.Mongo; -using StellaOps.Concelier.Testing; +using StellaOps.Concelier.Storage; +using StellaOps.Concelier.Storage.Advisories; +using StellaOps.Concelier.Storage; +using StellaOps.Concelier.Storage; +using StellaOps.Concelier.Storage.Postgres; +using StellaOps.Concelier.Testing; namespace StellaOps.Concelier.Connector.Ics.Kaspersky.Tests; -[Collection("mongo-fixture")] +[Collection(ConcelierFixtureCollection.Name)] public sealed class KasperskyConnectorTests : IAsyncLifetime { - private readonly MongoIntegrationFixture _fixture; + private readonly ConcelierPostgresFixture _fixture; private readonly FakeTimeProvider _timeProvider; private readonly CannedHttpMessageHandler _handler; private ServiceProvider? _serviceProvider; - public KasperskyConnectorTests(MongoIntegrationFixture fixture) + public KasperskyConnectorTests(ConcelierPostgresFixture fixture) { _fixture = fixture; _timeProvider = new FakeTimeProvider(new DateTimeOffset(2024, 10, 20, 0, 0, 0, TimeSpan.Zero)); @@ -274,19 +275,19 @@ public sealed class KasperskyConnectorTests : IAsyncLifetime return; } - await _fixture.Client.DropDatabaseAsync(_fixture.Database.DatabaseNamespace.DatabaseName); + await _fixture.TruncateAllTablesAsync(); var services = new ServiceCollection(); services.AddLogging(builder => builder.AddProvider(NullLoggerProvider.Instance)); services.AddSingleton(_timeProvider); services.AddSingleton(_handler); - services.AddMongoStorage(options => - { - options.ConnectionString = _fixture.Runner.ConnectionString; - options.DatabaseName = _fixture.Database.DatabaseNamespace.DatabaseName; - options.CommandTimeout = TimeSpan.FromSeconds(5); - }); + services.AddConcelierPostgresStorage(options => + { + options.ConnectionString = _fixture.ConnectionString; + options.SchemaName = _fixture.SchemaName; + options.CommandTimeoutSeconds = 5; + }); services.AddSourceCommon(); services.AddKasperskyIcsConnector(opts => @@ -307,12 +308,10 @@ public sealed class KasperskyConnectorTests : IAsyncLifetime }); _serviceProvider = services.BuildServiceProvider(); - var bootstrapper = _serviceProvider.GetRequiredService(); - await bootstrapper.InitializeAsync(CancellationToken.None); - } - - private Task ResetDatabaseAsync() - => _fixture.Client.DropDatabaseAsync(_fixture.Database.DatabaseNamespace.DatabaseName); + } + + private Task ResetDatabaseAsync() + => _fixture.TruncateAllTablesAsync(); private static string ReadFixture(string filename) { diff --git a/src/Concelier/__Tests/StellaOps.Concelier.Connector.Jvn.Tests/Jvn/JvnConnectorTests.cs b/src/Concelier/__Tests/StellaOps.Concelier.Connector.Jvn.Tests/Jvn/JvnConnectorTests.cs index 9ce2239c6..a4ec7b4f6 100644 --- a/src/Concelier/__Tests/StellaOps.Concelier.Connector.Jvn.Tests/Jvn/JvnConnectorTests.cs +++ b/src/Concelier/__Tests/StellaOps.Concelier.Connector.Jvn.Tests/Jvn/JvnConnectorTests.cs @@ -6,41 +6,41 @@ using System.Net; using System.Net.Http; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Http; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Abstractions; -using Microsoft.Extensions.Options; -using Microsoft.Extensions.Time.Testing; -using MongoDB.Bson; -using MongoDB.Driver; -using StellaOps.Concelier.Models; -using StellaOps.Concelier.Connector.Common.Fetch; -using StellaOps.Concelier.Connector.Common.Http; -using StellaOps.Concelier.Connector.Common.Testing; -using StellaOps.Concelier.Connector.Common; -using StellaOps.Concelier.Connector.Jvn; -using StellaOps.Concelier.Connector.Jvn.Configuration; -using StellaOps.Concelier.Storage.Mongo; -using StellaOps.Concelier.Storage.Mongo.Advisories; -using StellaOps.Concelier.Storage.Mongo; -using StellaOps.Concelier.Storage.Mongo; -using StellaOps.Concelier.Storage.Mongo.JpFlags; -using Xunit.Abstractions; -using StellaOps.Concelier.Testing; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Time.Testing; +using StellaOps.Concelier.Bson; +using StellaOps.Concelier.Models; +using StellaOps.Concelier.Connector.Common.Fetch; +using StellaOps.Concelier.Connector.Common.Http; +using StellaOps.Concelier.Connector.Common.Testing; +using StellaOps.Concelier.Connector.Common; +using StellaOps.Concelier.Connector.Jvn; +using StellaOps.Concelier.Connector.Jvn.Configuration; +using StellaOps.Concelier.Storage; +using StellaOps.Concelier.Storage.Advisories; +using StellaOps.Concelier.Storage; +using StellaOps.Concelier.Storage; +using StellaOps.Concelier.Storage.JpFlags; +using StellaOps.Concelier.Storage.Postgres; +using Xunit.Abstractions; +using StellaOps.Concelier.Testing; namespace StellaOps.Concelier.Connector.Jvn.Tests; -[Collection("mongo-fixture")] +[Collection(ConcelierFixtureCollection.Name)] public sealed class JvnConnectorTests : IAsyncLifetime { private const string VulnId = "JVNDB-2024-123456"; - private readonly MongoIntegrationFixture _fixture; + private readonly ConcelierPostgresFixture _fixture; private readonly ITestOutputHelper _output; private readonly FakeTimeProvider _timeProvider; private readonly CannedHttpMessageHandler _handler; private ServiceProvider? _serviceProvider; - public JvnConnectorTests(MongoIntegrationFixture fixture, ITestOutputHelper output) + public JvnConnectorTests(ConcelierPostgresFixture fixture, ITestOutputHelper output) { _fixture = fixture; _output = output; @@ -77,66 +77,28 @@ public sealed class JvnConnectorTests : IAsyncLifetime var stateAfterFetch = await provider.GetRequiredService() .TryGetAsync(JvnConnectorPlugin.SourceName, CancellationToken.None); - if (stateAfterFetch?.Cursor is not null) - { - _output.WriteLine($"Fetch state cursor: {stateAfterFetch.Cursor.ToJson()}"); - } - - var rawDocuments = await _fixture.Database - .GetCollection("document") - .Find(Builders.Filter.Empty) - .ToListAsync(CancellationToken.None); - _output.WriteLine($"Fixture document count: {rawDocuments.Count}"); - - _timeProvider.Advance(TimeSpan.FromMinutes(1)); - await connector.ParseAsync(provider, CancellationToken.None); + if (stateAfterFetch?.Cursor is not null) + { + _output.WriteLine($"Fetch state cursor: {stateAfterFetch.Cursor.ToJson()}"); + } + + _timeProvider.Advance(TimeSpan.FromMinutes(1)); + await connector.ParseAsync(provider, CancellationToken.None); var stateAfterParse = await provider.GetRequiredService() .TryGetAsync(JvnConnectorPlugin.SourceName, CancellationToken.None); _output.WriteLine($"Parse state failure reason: {stateAfterParse?.LastFailureReason ?? ""}"); - if (stateAfterParse?.Cursor is not null) - { - _output.WriteLine($"Parse state cursor: {stateAfterParse.Cursor.ToJson()}"); - } - - var dtoCollection = provider.GetRequiredService() - .GetCollection("dto"); - var dtoDocs = await dtoCollection.Find(FilterDefinition.Empty).ToListAsync(CancellationToken.None); - _output.WriteLine($"DTO document count: {dtoDocs.Count}"); - - var documentsAfterParse = await _fixture.Database - .GetCollection("document") - .Find(Builders.Filter.Empty) - .ToListAsync(CancellationToken.None); - _output.WriteLine($"Document statuses after parse: {string.Join(",", documentsAfterParse.Select(d => d.GetValue("status", BsonValue.Create("")).AsString))}"); - - await connector.MapAsync(provider, CancellationToken.None); - - var rawAdvisories = await _fixture.Database - .GetCollection("advisory") - .Find(Builders.Filter.Empty) - .ToListAsync(CancellationToken.None); - _output.WriteLine($"Fixture advisory count: {rawAdvisories.Count}"); - Assert.NotEmpty(rawAdvisories); - - var providerDatabase = provider.GetRequiredService(); - var providerCount = await providerDatabase - .GetCollection("advisory") - .CountDocumentsAsync(FilterDefinition.Empty, cancellationToken: CancellationToken.None); - _output.WriteLine($"Provider advisory count: {providerCount}"); - Assert.True(providerCount > 0, $"Provider DB advisory count was {providerCount}"); - - var typedDocs = await providerDatabase - .GetCollection("advisory") - .Find(FilterDefinition.Empty) - .ToListAsync(CancellationToken.None); - _output.WriteLine($"Typed advisory docs: {typedDocs.Count}"); - Assert.NotEmpty(typedDocs); - - var advisoryStore = provider.GetRequiredService(); - var singleAdvisory = await advisoryStore.FindAsync(VulnId, CancellationToken.None); - Assert.NotNull(singleAdvisory); - _output.WriteLine($"singleAdvisory null? {singleAdvisory is null}"); + if (stateAfterParse?.Cursor is not null) + { + _output.WriteLine($"Parse state cursor: {stateAfterParse.Cursor.ToJson()}"); + } + + await connector.MapAsync(provider, CancellationToken.None); + + var advisoryStore = provider.GetRequiredService(); + var singleAdvisory = await advisoryStore.FindAsync(VulnId, CancellationToken.None); + Assert.NotNull(singleAdvisory); + _output.WriteLine($"singleAdvisory null? {singleAdvisory is null}"); var canonical = SnapshotSerializer.ToSnapshot(singleAdvisory!).Replace("\r\n", "\n"); var expected = ReadFixture("expected-advisory.json").Replace("\r\n", "\n"); @@ -174,19 +136,19 @@ public sealed class JvnConnectorTests : IAsyncLifetime return; } - await _fixture.Client.DropDatabaseAsync(_fixture.Database.DatabaseNamespace.DatabaseName); + await _fixture.TruncateAllTablesAsync(); var services = new ServiceCollection(); services.AddLogging(builder => builder.AddProvider(NullLoggerProvider.Instance)); services.AddSingleton(_timeProvider); services.AddSingleton(_handler); - services.AddMongoStorage(options => - { - options.ConnectionString = _fixture.Runner.ConnectionString; - options.DatabaseName = _fixture.Database.DatabaseNamespace.DatabaseName; - options.CommandTimeout = TimeSpan.FromSeconds(5); - }); + services.AddConcelierPostgresStorage(options => + { + options.ConnectionString = _fixture.ConnectionString; + options.SchemaName = _fixture.SchemaName; + options.CommandTimeoutSeconds = 5; + }); services.AddSourceCommon(); services.AddJvnConnector(opts => @@ -205,14 +167,12 @@ public sealed class JvnConnectorTests : IAsyncLifetime builder.PrimaryHandler = _handler; }); }); - - _serviceProvider = services.BuildServiceProvider(); - var bootstrapper = _serviceProvider.GetRequiredService(); - await bootstrapper.InitializeAsync(CancellationToken.None); - } - - private Task ResetDatabaseAsync() - => _fixture.Client.DropDatabaseAsync(_fixture.Database.DatabaseNamespace.DatabaseName); + + _serviceProvider = services.BuildServiceProvider(); + } + + private Task ResetDatabaseAsync() + => _fixture.TruncateAllTablesAsync(); private static Uri BuildOverviewUri(JvnOptions options, DateTimeOffset windowStart, DateTimeOffset windowEnd, int startItem) { diff --git a/src/Concelier/__Tests/StellaOps.Concelier.Connector.Kev.Tests/Kev/KevConnectorTests.cs b/src/Concelier/__Tests/StellaOps.Concelier.Connector.Kev.Tests/Kev/KevConnectorTests.cs index 253620853..1e2482fea 100644 --- a/src/Concelier/__Tests/StellaOps.Concelier.Connector.Kev.Tests/Kev/KevConnectorTests.cs +++ b/src/Concelier/__Tests/StellaOps.Concelier.Connector.Kev.Tests/Kev/KevConnectorTests.cs @@ -2,37 +2,37 @@ using System.Net; using System.Net.Http; using System.Net.Http.Headers; using System.Text; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Http; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Abstractions; -using Microsoft.Extensions.Time.Testing; -using MongoDB.Bson; -using StellaOps.Concelier.Models; -using StellaOps.Concelier.Connector.Common; -using StellaOps.Concelier.Connector.Common.Http; -using StellaOps.Concelier.Connector.Common.Testing; -using StellaOps.Concelier.Connector.Kev; -using StellaOps.Concelier.Connector.Kev.Configuration; -using StellaOps.Concelier.Storage.Mongo; -using StellaOps.Concelier.Storage.Mongo.Advisories; -using StellaOps.Concelier.Storage.Mongo; -using StellaOps.Concelier.Testing; -using Xunit; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Http; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Time.Testing; +using StellaOps.Concelier.Bson; +using StellaOps.Concelier.Models; +using StellaOps.Concelier.Connector.Common; +using StellaOps.Concelier.Connector.Common.Http; +using StellaOps.Concelier.Connector.Common.Testing; +using StellaOps.Concelier.Connector.Kev; +using StellaOps.Concelier.Connector.Kev.Configuration; +using StellaOps.Concelier.Storage; +using StellaOps.Concelier.Storage.Advisories; +using StellaOps.Concelier.Storage.Postgres; +using StellaOps.Concelier.Testing; +using Xunit; namespace StellaOps.Concelier.Connector.Kev.Tests; -[Collection("mongo-fixture")] +[Collection(ConcelierFixtureCollection.Name)] public sealed class KevConnectorTests : IAsyncLifetime { private static readonly Uri FeedUri = new("https://www.cisa.gov/sites/default/files/feeds/known_exploited_vulnerabilities.json"); private const string CatalogEtag = "\"kev-2025-10-09\""; - private readonly MongoIntegrationFixture _fixture; + private readonly ConcelierPostgresFixture _fixture; private readonly FakeTimeProvider _timeProvider; private readonly CannedHttpMessageHandler _handler; - public KevConnectorTests(MongoIntegrationFixture fixture) + public KevConnectorTests(ConcelierPostgresFixture fixture) { _fixture = fixture; _timeProvider = new FakeTimeProvider(new DateTimeOffset(2025, 10, 10, 0, 0, 0, TimeSpan.Zero)); @@ -77,39 +77,36 @@ public sealed class KevConnectorTests : IAsyncLifetime Assert.True(IsEmptyArray(state.Cursor, "pendingMappings")); } - private async Task BuildServiceProviderAsync() - { - await _fixture.Client.DropDatabaseAsync(_fixture.Database.DatabaseNamespace.DatabaseName); - _handler.Clear(); - - var services = new ServiceCollection(); - services.AddLogging(builder => builder.AddProvider(NullLoggerProvider.Instance)); - services.AddSingleton(_timeProvider); - - services.AddMongoStorage(options => - { - options.ConnectionString = _fixture.Runner.ConnectionString; - options.DatabaseName = _fixture.Database.DatabaseNamespace.DatabaseName; - options.CommandTimeout = TimeSpan.FromSeconds(5); - }); - - services.AddSourceCommon(); - services.AddKevConnector(options => - { + private async Task BuildServiceProviderAsync() + { + await _fixture.TruncateAllTablesAsync(CancellationToken.None); + _handler.Clear(); + + var services = new ServiceCollection(); + services.AddLogging(builder => builder.AddProvider(NullLoggerProvider.Instance)); + services.AddSingleton(_timeProvider); + + services.AddConcelierPostgresStorage(options => + { + options.ConnectionString = _fixture.ConnectionString; + options.SchemaName = _fixture.SchemaName; + options.CommandTimeoutSeconds = 5; + }); + + services.AddSourceCommon(); + services.AddKevConnector(options => + { options.FeedUri = FeedUri; options.RequestTimeout = TimeSpan.FromSeconds(10); }); services.Configure(KevOptions.HttpClientName, builderOptions => { - builderOptions.HttpMessageHandlerBuilderActions.Add(builder => builder.PrimaryHandler = _handler); - }); - - var provider = services.BuildServiceProvider(); - var bootstrapper = provider.GetRequiredService(); - await bootstrapper.InitializeAsync(CancellationToken.None); - return provider; - } + builderOptions.HttpMessageHandlerBuilderActions.Add(builder => builder.PrimaryHandler = _handler); + }); + + return services.BuildServiceProvider(); + } private void SeedCatalogResponse() { @@ -211,8 +208,8 @@ public sealed class KevConnectorTests : IAsyncLifetime public Task InitializeAsync() => Task.CompletedTask; - public async Task DisposeAsync() - { - await _fixture.Client.DropDatabaseAsync(_fixture.Database.DatabaseNamespace.DatabaseName); - } -} + public async Task DisposeAsync() + { + await _fixture.TruncateAllTablesAsync(CancellationToken.None); + } +} diff --git a/src/Concelier/__Tests/StellaOps.Concelier.Connector.Kisa.Tests/KisaConnectorTests.cs b/src/Concelier/__Tests/StellaOps.Concelier.Connector.Kisa.Tests/KisaConnectorTests.cs index f4a0454dc..71ea2d7fa 100644 --- a/src/Concelier/__Tests/StellaOps.Concelier.Connector.Kisa.Tests/KisaConnectorTests.cs +++ b/src/Concelier/__Tests/StellaOps.Concelier.Connector.Kisa.Tests/KisaConnectorTests.cs @@ -14,34 +14,35 @@ using Microsoft.Extensions.Http; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; -using MongoDB.Bson; +using StellaOps.Concelier.Bson; using StellaOps.Concelier.Connector.Common; using StellaOps.Concelier.Connector.Common.Http; using StellaOps.Concelier.Connector.Common.Testing; using StellaOps.Concelier.Connector.Kisa.Configuration; using StellaOps.Concelier.Connector.Kisa.Internal; using StellaOps.Concelier.Models; -using StellaOps.Concelier.Storage.Mongo; -using StellaOps.Concelier.Storage.Mongo.Advisories; -using StellaOps.Concelier.Storage.Mongo; -using StellaOps.Concelier.Storage.Mongo; +using StellaOps.Concelier.Storage; +using StellaOps.Concelier.Storage.Advisories; +using StellaOps.Concelier.Storage; +using StellaOps.Concelier.Storage; +using StellaOps.Concelier.Storage.Postgres; using StellaOps.Concelier.Testing; using Xunit; using Xunit.Abstractions; namespace StellaOps.Concelier.Connector.Kisa.Tests; -[Collection("mongo-fixture")] +[Collection(ConcelierFixtureCollection.Name)] public sealed class KisaConnectorTests : IAsyncLifetime { private static readonly Uri FeedUri = new("https://test.local/rss/securityInfo.do"); private static readonly Uri DetailPageUri = new("https://test.local/detailDos.do?IDX=5868"); - private readonly MongoIntegrationFixture _fixture; + private readonly ConcelierPostgresFixture _fixture; private readonly CannedHttpMessageHandler _handler; private readonly ITestOutputHelper _output; - public KisaConnectorTests(MongoIntegrationFixture fixture, ITestOutputHelper output) + public KisaConnectorTests(ConcelierPostgresFixture fixture, ITestOutputHelper output) { _fixture = fixture; _handler = new CannedHttpMessageHandler(); @@ -373,18 +374,18 @@ public sealed class KisaConnectorTests : IAsyncLifetime private async Task BuildServiceProviderAsync() { - await _fixture.Client.DropDatabaseAsync(_fixture.Database.DatabaseNamespace.DatabaseName); + await _fixture.TruncateAllTablesAsync(); _handler.Clear(); var services = new ServiceCollection(); services.AddLogging(builder => builder.AddProvider(NullLoggerProvider.Instance)); services.AddSingleton(_handler); - services.AddMongoStorage(options => + services.AddConcelierPostgresStorage(options => { - options.ConnectionString = _fixture.Runner.ConnectionString; - options.DatabaseName = _fixture.Database.DatabaseNamespace.DatabaseName; - options.CommandTimeout = TimeSpan.FromSeconds(5); + options.ConnectionString = _fixture.ConnectionString; + options.SchemaName = _fixture.SchemaName; + options.CommandTimeoutSeconds = 5; }); services.AddSourceCommon(); @@ -406,10 +407,7 @@ public sealed class KisaConnectorTests : IAsyncLifetime }); }); - var provider = services.BuildServiceProvider(); - var bootstrapper = provider.GetRequiredService(); - await bootstrapper.InitializeAsync(CancellationToken.None); - return provider; + return services.BuildServiceProvider(); } private void SeedResponses(string? versionOverride = null) diff --git a/src/Concelier/__Tests/StellaOps.Concelier.Connector.Nvd.Tests/Nvd/NvdConflictFixtureTests.cs b/src/Concelier/__Tests/StellaOps.Concelier.Connector.Nvd.Tests/Nvd/NvdConflictFixtureTests.cs index 38fe96829..416053ef2 100644 --- a/src/Concelier/__Tests/StellaOps.Concelier.Connector.Nvd.Tests/Nvd/NvdConflictFixtureTests.cs +++ b/src/Concelier/__Tests/StellaOps.Concelier.Connector.Nvd.Tests/Nvd/NvdConflictFixtureTests.cs @@ -1,7 +1,7 @@ using System.Text.Json; using StellaOps.Concelier.Models; using StellaOps.Concelier.Connector.Nvd.Internal; -using StellaOps.Concelier.Storage.Mongo; +using StellaOps.Concelier.Storage; namespace StellaOps.Concelier.Connector.Nvd.Tests; diff --git a/src/Concelier/__Tests/StellaOps.Concelier.Connector.Nvd.Tests/Nvd/NvdConnectorHarnessTests.cs b/src/Concelier/__Tests/StellaOps.Concelier.Connector.Nvd.Tests/Nvd/NvdConnectorHarnessTests.cs index a7bec5f7e..0198cc6c3 100644 --- a/src/Concelier/__Tests/StellaOps.Concelier.Connector.Nvd.Tests/Nvd/NvdConnectorHarnessTests.cs +++ b/src/Concelier/__Tests/StellaOps.Concelier.Connector.Nvd.Tests/Nvd/NvdConnectorHarnessTests.cs @@ -4,25 +4,25 @@ using System.Globalization; using System.IO; using System.Linq; using Microsoft.Extensions.DependencyInjection; -using MongoDB.Bson; +using StellaOps.Concelier.Bson; using StellaOps.Concelier.Connector.Common.Testing; using StellaOps.Concelier.Connector.Nvd; using StellaOps.Concelier.Connector.Nvd.Configuration; -using StellaOps.Concelier.Storage.Mongo; -using StellaOps.Concelier.Storage.Mongo.Advisories; -using StellaOps.Concelier.Storage.Mongo; +using StellaOps.Concelier.Storage; +using StellaOps.Concelier.Storage.Advisories; +using StellaOps.Concelier.Storage; using StellaOps.Concelier.Testing; using StellaOps.Concelier.Testing; using System.Net; namespace StellaOps.Concelier.Connector.Nvd.Tests; -[Collection("mongo-fixture")] +[Collection(ConcelierFixtureCollection.Name)] public sealed class NvdConnectorHarnessTests : IAsyncLifetime { private readonly ConnectorTestHarness _harness; - public NvdConnectorHarnessTests(MongoIntegrationFixture fixture) + public NvdConnectorHarnessTests(ConcelierPostgresFixture fixture) { _harness = new ConnectorTestHarness(fixture, new DateTimeOffset(2024, 1, 2, 12, 0, 0, TimeSpan.Zero), NvdOptions.HttpClientName); } diff --git a/src/Concelier/__Tests/StellaOps.Concelier.Connector.Nvd.Tests/Nvd/NvdConnectorTests.cs b/src/Concelier/__Tests/StellaOps.Concelier.Connector.Nvd.Tests/Nvd/NvdConnectorTests.cs index 2959921e7..b2da9660e 100644 --- a/src/Concelier/__Tests/StellaOps.Concelier.Connector.Nvd.Tests/Nvd/NvdConnectorTests.cs +++ b/src/Concelier/__Tests/StellaOps.Concelier.Connector.Nvd.Tests/Nvd/NvdConnectorTests.cs @@ -11,7 +11,7 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; using Microsoft.Extensions.Time.Testing; -using MongoDB.Bson; +using StellaOps.Concelier.Bson; using StellaOps.Concelier.Models; using StellaOps.Concelier.Connector.Common.Fetch; using StellaOps.Concelier.Connector.Common.Http; @@ -20,25 +20,26 @@ using StellaOps.Concelier.Connector.Common; using StellaOps.Concelier.Connector.Nvd; using StellaOps.Concelier.Connector.Nvd.Configuration; using StellaOps.Concelier.Connector.Nvd.Internal; -using StellaOps.Concelier.Storage.Mongo; -using StellaOps.Concelier.Storage.Mongo.Advisories; -using StellaOps.Concelier.Storage.Mongo; -using StellaOps.Concelier.Storage.Mongo; -using StellaOps.Concelier.Storage.Mongo.ChangeHistory; -using StellaOps.Concelier.Testing; +using StellaOps.Concelier.Storage; +using StellaOps.Concelier.Storage.Advisories; +using StellaOps.Concelier.Storage; +using StellaOps.Concelier.Storage; +using StellaOps.Concelier.Storage.ChangeHistory; +using StellaOps.Concelier.Storage.Postgres; +using StellaOps.Concelier.Testing; namespace StellaOps.Concelier.Connector.Nvd.Tests; -[Collection("mongo-fixture")] +[Collection(ConcelierFixtureCollection.Name)] public sealed class NvdConnectorTests : IAsyncLifetime { - private readonly MongoIntegrationFixture _fixture; + private readonly ConcelierPostgresFixture _fixture; private FakeTimeProvider _timeProvider; private readonly DateTimeOffset _initialNow; private readonly CannedHttpMessageHandler _handler; private ServiceProvider? _serviceProvider; - public NvdConnectorTests(MongoIntegrationFixture fixture) + public NvdConnectorTests(ConcelierPostgresFixture fixture) { _fixture = fixture; _initialNow = new DateTimeOffset(2024, 1, 2, 12, 0, 0, TimeSpan.Zero); @@ -511,12 +512,12 @@ public sealed class NvdConnectorTests : IAsyncLifetime services.AddSingleton(_timeProvider); services.AddSingleton(handler); - services.AddMongoStorage(storageOptions => - { - storageOptions.ConnectionString = _fixture.Runner.ConnectionString; - storageOptions.DatabaseName = _fixture.Database.DatabaseNamespace.DatabaseName; - storageOptions.CommandTimeout = TimeSpan.FromSeconds(5); - }); + services.AddConcelierPostgresStorage(storageOptions => + { + storageOptions.ConnectionString = _fixture.ConnectionString; + storageOptions.SchemaName = _fixture.SchemaName; + storageOptions.CommandTimeoutSeconds = 5; + }); services.AddSourceCommon(); services.AddNvdConnector(configure: opts => @@ -535,11 +536,8 @@ public sealed class NvdConnectorTests : IAsyncLifetime }); }); - var provider = services.BuildServiceProvider(); - var bootstrapper = provider.GetRequiredService(); - await bootstrapper.InitializeAsync(CancellationToken.None); - return provider; - } + return services.BuildServiceProvider(); + } private async Task ResetDatabaseInternalAsync() { @@ -557,10 +555,10 @@ public sealed class NvdConnectorTests : IAsyncLifetime _serviceProvider = null; } - await _fixture.Client.DropDatabaseAsync(_fixture.Database.DatabaseNamespace.DatabaseName); - _handler.Clear(); - _timeProvider = new FakeTimeProvider(_initialNow); - } + await _fixture.TruncateAllTablesAsync(); + _handler.Clear(); + _timeProvider = new FakeTimeProvider(_initialNow); + } private sealed class MetricCollector : IDisposable { diff --git a/src/Concelier/__Tests/StellaOps.Concelier.Connector.Osv.Tests/Osv/OsvConflictFixtureTests.cs b/src/Concelier/__Tests/StellaOps.Concelier.Connector.Osv.Tests/Osv/OsvConflictFixtureTests.cs index 9ad75b49d..983340e9c 100644 --- a/src/Concelier/__Tests/StellaOps.Concelier.Connector.Osv.Tests/Osv/OsvConflictFixtureTests.cs +++ b/src/Concelier/__Tests/StellaOps.Concelier.Connector.Osv.Tests/Osv/OsvConflictFixtureTests.cs @@ -1,9 +1,9 @@ using System.Text.Json; -using MongoDB.Bson; +using StellaOps.Concelier.Bson; using StellaOps.Concelier.Models; using StellaOps.Concelier.Connector.Osv.Internal; -using StellaOps.Concelier.Storage.Mongo; -using StellaOps.Concelier.Storage.Mongo; +using StellaOps.Concelier.Storage; +using StellaOps.Concelier.Storage; namespace StellaOps.Concelier.Connector.Osv.Tests; diff --git a/src/Concelier/__Tests/StellaOps.Concelier.Connector.Osv.Tests/Osv/OsvGhsaParityRegressionTests.cs b/src/Concelier/__Tests/StellaOps.Concelier.Connector.Osv.Tests/Osv/OsvGhsaParityRegressionTests.cs index 56a125a0a..6cc2edb2b 100644 --- a/src/Concelier/__Tests/StellaOps.Concelier.Connector.Osv.Tests/Osv/OsvGhsaParityRegressionTests.cs +++ b/src/Concelier/__Tests/StellaOps.Concelier.Connector.Osv.Tests/Osv/OsvGhsaParityRegressionTests.cs @@ -8,13 +8,13 @@ using System.Net.Http; using System.Net.Http.Headers; using System.Text.Json; using System.Text.RegularExpressions; -using MongoDB.Bson; +using StellaOps.Concelier.Bson; using StellaOps.Concelier.Models; using StellaOps.Concelier.Connector.Common; using StellaOps.Concelier.Connector.Osv; using StellaOps.Concelier.Connector.Osv.Internal; -using StellaOps.Concelier.Storage.Mongo; -using StellaOps.Concelier.Storage.Mongo; +using StellaOps.Concelier.Storage; +using StellaOps.Concelier.Storage; using StellaOps.Cryptography; using Xunit; diff --git a/src/Concelier/__Tests/StellaOps.Concelier.Connector.Osv.Tests/Osv/OsvMapperTests.cs b/src/Concelier/__Tests/StellaOps.Concelier.Connector.Osv.Tests/Osv/OsvMapperTests.cs index 0ccbac5bf..0bdc31770 100644 --- a/src/Concelier/__Tests/StellaOps.Concelier.Connector.Osv.Tests/Osv/OsvMapperTests.cs +++ b/src/Concelier/__Tests/StellaOps.Concelier.Connector.Osv.Tests/Osv/OsvMapperTests.cs @@ -3,14 +3,14 @@ using System.Collections.Generic; using System.Text.Json; using System.Text.Json.Serialization; using System.Reflection; -using MongoDB.Bson; +using StellaOps.Concelier.Bson; using StellaOps.Concelier.Models; using StellaOps.Concelier.Connector.Common; using StellaOps.Concelier.Connector.Osv; using StellaOps.Concelier.Connector.Osv.Internal; using StellaOps.Concelier.Normalization.Identifiers; -using StellaOps.Concelier.Storage.Mongo; -using StellaOps.Concelier.Storage.Mongo; +using StellaOps.Concelier.Storage; +using StellaOps.Concelier.Storage; using Xunit; namespace StellaOps.Concelier.Connector.Osv.Tests; diff --git a/src/Concelier/__Tests/StellaOps.Concelier.Connector.Osv.Tests/Osv/OsvSnapshotTests.cs b/src/Concelier/__Tests/StellaOps.Concelier.Connector.Osv.Tests/Osv/OsvSnapshotTests.cs index 4fe854e4c..2a8723023 100644 --- a/src/Concelier/__Tests/StellaOps.Concelier.Connector.Osv.Tests/Osv/OsvSnapshotTests.cs +++ b/src/Concelier/__Tests/StellaOps.Concelier.Connector.Osv.Tests/Osv/OsvSnapshotTests.cs @@ -2,12 +2,12 @@ using System; using System.Collections.Generic; using System.IO; using System.Text.Json; -using MongoDB.Bson; +using StellaOps.Concelier.Bson; using StellaOps.Concelier.Models; using StellaOps.Concelier.Connector.Osv; using StellaOps.Concelier.Connector.Osv.Internal; -using StellaOps.Concelier.Storage.Mongo; -using StellaOps.Concelier.Storage.Mongo; +using StellaOps.Concelier.Storage; +using StellaOps.Concelier.Storage; using StellaOps.Concelier.Connector.Common; using Xunit; using Xunit.Abstractions; diff --git a/src/Concelier/__Tests/StellaOps.Concelier.Connector.Ru.Bdu.Tests/RuBduConnectorSnapshotTests.cs b/src/Concelier/__Tests/StellaOps.Concelier.Connector.Ru.Bdu.Tests/RuBduConnectorSnapshotTests.cs index c4a959f3b..dd77e0758 100644 --- a/src/Concelier/__Tests/StellaOps.Concelier.Connector.Ru.Bdu.Tests/RuBduConnectorSnapshotTests.cs +++ b/src/Concelier/__Tests/StellaOps.Concelier.Connector.Ru.Bdu.Tests/RuBduConnectorSnapshotTests.cs @@ -13,18 +13,16 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Http; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; -using MongoDB.Bson; -using MongoDB.Bson.Serialization; -using MongoDB.Driver; +using StellaOps.Concelier.Bson; using StellaOps.Concelier.Models; using StellaOps.Concelier.Connector.Common.Testing; using StellaOps.Concelier.Connector.Ru.Bdu; using StellaOps.Concelier.Connector.Ru.Bdu.Configuration; using StellaOps.Concelier.Connector.Ru.Bdu.Internal; -using StellaOps.Concelier.Storage.Mongo; -using StellaOps.Concelier.Storage.Mongo.Advisories; -using StellaOps.Concelier.Storage.Mongo; -using StellaOps.Concelier.Storage.Mongo; +using StellaOps.Concelier.Storage; +using StellaOps.Concelier.Storage.Advisories; +using StellaOps.Concelier.Storage; +using StellaOps.Concelier.Storage; using StellaOps.Concelier.Testing; using StellaOps.Cryptography.DependencyInjection; using Xunit; @@ -32,16 +30,16 @@ using Xunit.Sdk; namespace StellaOps.Concelier.Connector.Ru.Bdu.Tests; -[Collection("mongo-fixture")] +[Collection(ConcelierFixtureCollection.Name)] public sealed class RuBduConnectorSnapshotTests : IAsyncLifetime { private const string UpdateFixturesVariable = "UPDATE_BDU_FIXTURES"; private static readonly Uri ArchiveUri = new("https://bdu.fstec.ru/files/documents/vulxml.zip"); - private readonly MongoIntegrationFixture _fixture; + private readonly ConcelierPostgresFixture _fixture; private ConnectorTestHarness? _harness; - public RuBduConnectorSnapshotTests(MongoIntegrationFixture fixture) + public RuBduConnectorSnapshotTests(ConcelierPostgresFixture fixture) { _fixture = fixture; } @@ -65,10 +63,6 @@ public sealed class RuBduConnectorSnapshotTests : IAsyncLifetime await connector.ParseAsync(harness.ServiceProvider, CancellationToken.None); await connector.MapAsync(harness.ServiceProvider, CancellationToken.None); - var documentsCollection = _fixture.Database.GetCollection(MongoStorageDefaults.Collections.Document); - var documentCount = await documentsCollection.CountDocumentsAsync(Builders.Filter.Empty); - Assert.True(documentCount > 0, "Expected persisted documents after map stage"); - var documentsSnapshot = await BuildDocumentsSnapshotAsync(harness.ServiceProvider, expectedDocumentIds); WriteOrAssertSnapshot(documentsSnapshot, "ru-bdu-documents.snapshot.json"); @@ -122,7 +116,7 @@ public sealed class RuBduConnectorSnapshotTests : IAsyncLifetime options.DataArchivePath = "files/documents/vulxml.zip"; options.MaxVulnerabilitiesPerFetch = 25; options.RequestTimeout = TimeSpan.FromSeconds(30); - var cacheRoot = Path.Combine(Path.GetTempPath(), "stellaops-tests", _fixture.Database.DatabaseNamespace.DatabaseName, "ru-bdu"); + var cacheRoot = Path.Combine(Path.GetTempPath(), "stellaops-tests", _fixture.SchemaName, "ru-bdu"); Directory.CreateDirectory(cacheRoot); options.CacheDirectory = cacheRoot; }); @@ -160,15 +154,7 @@ public sealed class RuBduConnectorSnapshotTests : IAsyncLifetime var record = await documentStore.FindAsync(documentId, CancellationToken.None); if (record is null) { - var existing = await _fixture.Database - .GetCollection("documents") - .Find(Builders.Filter.Empty) - .Project(Builders.Projection.Include("Uri")) - .ToListAsync(CancellationToken.None); - var uris = existing - .Select(document => document.GetValue("Uri", BsonValue.Create(string.Empty)).AsString) - .ToArray(); - throw new XunitException($"Document id not found: {documentId}. Known URIs: {string.Join(", ", uris)}"); + throw new XunitException($"Document id not found: {documentId}"); } records.Add(new diff --git a/src/Concelier/__Tests/StellaOps.Concelier.Connector.Ru.Bdu.Tests/RuBduMapperTests.cs b/src/Concelier/__Tests/StellaOps.Concelier.Connector.Ru.Bdu.Tests/RuBduMapperTests.cs index e920ebe8e..68cbd58f2 100644 --- a/src/Concelier/__Tests/StellaOps.Concelier.Connector.Ru.Bdu.Tests/RuBduMapperTests.cs +++ b/src/Concelier/__Tests/StellaOps.Concelier.Connector.Ru.Bdu.Tests/RuBduMapperTests.cs @@ -1,9 +1,9 @@ using System.Collections.Immutable; -using MongoDB.Bson; +using StellaOps.Concelier.Bson; using StellaOps.Concelier.Connector.Common; using StellaOps.Concelier.Models; using StellaOps.Concelier.Connector.Ru.Bdu.Internal; -using StellaOps.Concelier.Storage.Mongo; +using StellaOps.Concelier.Storage; using Xunit; namespace StellaOps.Concelier.Connector.Ru.Bdu.Tests; diff --git a/src/Concelier/__Tests/StellaOps.Concelier.Connector.Ru.Nkcki.Tests/RuNkckiConnectorTests.cs b/src/Concelier/__Tests/StellaOps.Concelier.Connector.Ru.Nkcki.Tests/RuNkckiConnectorTests.cs index e107205db..eb764f4b2 100644 --- a/src/Concelier/__Tests/StellaOps.Concelier.Connector.Ru.Nkcki.Tests/RuNkckiConnectorTests.cs +++ b/src/Concelier/__Tests/StellaOps.Concelier.Connector.Ru.Nkcki.Tests/RuNkckiConnectorTests.cs @@ -13,24 +13,24 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; using Microsoft.Extensions.Time.Testing; -using MongoDB.Bson; +using StellaOps.Concelier.Bson; using StellaOps.Concelier.Connector.Common; using StellaOps.Concelier.Connector.Common.Http; using StellaOps.Concelier.Connector.Common.Testing; using StellaOps.Concelier.Connector.Ru.Nkcki; using StellaOps.Concelier.Connector.Ru.Nkcki.Configuration; -using StellaOps.Concelier.Storage.Mongo; -using StellaOps.Concelier.Storage.Mongo.Advisories; -using StellaOps.Concelier.Storage.Mongo; -using StellaOps.Concelier.Testing; -using StellaOps.Concelier.Models; -using MongoDB.Driver; -using StellaOps.Cryptography.DependencyInjection; -using Xunit; +using StellaOps.Concelier.Storage; +using StellaOps.Concelier.Storage.Advisories; +using StellaOps.Concelier.Storage; +using StellaOps.Concelier.Testing; +using StellaOps.Concelier.Models; +using StellaOps.Concelier.Storage.Postgres; +using StellaOps.Cryptography.DependencyInjection; +using Xunit; namespace StellaOps.Concelier.Connector.Ru.Nkcki.Tests; -[Collection("mongo-fixture")] +[Collection(ConcelierFixtureCollection.Name)] public sealed class RuNkckiConnectorTests : IAsyncLifetime { private static readonly Uri ListingUri = new("https://cert.gov.ru/materialy/uyazvimosti/"); @@ -38,11 +38,11 @@ public sealed class RuNkckiConnectorTests : IAsyncLifetime private static readonly Uri BulletinUri = new("https://cert.gov.ru/materialy/uyazvimosti/bulletin-sample.json.zip"); private static readonly Uri LegacyBulletinUri = new("https://cert.gov.ru/materialy/uyazvimosti/bulletin-legacy.json.zip"); - private readonly MongoIntegrationFixture _fixture; + private readonly ConcelierPostgresFixture _fixture; private readonly FakeTimeProvider _timeProvider; private readonly CannedHttpMessageHandler _handler; - public RuNkckiConnectorTests(MongoIntegrationFixture fixture) + public RuNkckiConnectorTests(ConcelierPostgresFixture fixture) { _fixture = fixture; _timeProvider = new FakeTimeProvider(new DateTimeOffset(2025, 10, 12, 0, 0, 0, TimeSpan.Zero)); @@ -114,22 +114,22 @@ public sealed class RuNkckiConnectorTests : IAsyncLifetime _handler.AssertNoPendingResponses(); } - private async Task BuildServiceProviderAsync() - { - await _fixture.Client.DropDatabaseAsync(_fixture.Database.DatabaseNamespace.DatabaseName); - - _handler.Clear(); + private async Task BuildServiceProviderAsync() + { + await _fixture.TruncateAllTablesAsync(); + + _handler.Clear(); var services = new ServiceCollection(); services.AddLogging(builder => builder.AddProvider(NullLoggerProvider.Instance)); services.AddSingleton(_timeProvider); - services.AddMongoStorage(options => - { - options.ConnectionString = _fixture.Runner.ConnectionString; - options.DatabaseName = _fixture.Database.DatabaseNamespace.DatabaseName; - options.CommandTimeout = TimeSpan.FromSeconds(5); - }); + services.AddConcelierPostgresStorage(options => + { + options.ConnectionString = _fixture.ConnectionString; + options.SchemaName = _fixture.SchemaName; + options.CommandTimeoutSeconds = 5; + }); services.AddStellaOpsCrypto(); services.AddSourceCommon(); @@ -137,26 +137,23 @@ public sealed class RuNkckiConnectorTests : IAsyncLifetime { options.BaseAddress = new Uri("https://cert.gov.ru/"); options.ListingPath = "/materialy/uyazvimosti/"; - options.MaxBulletinsPerFetch = 2; - options.MaxListingPagesPerFetch = 2; - options.MaxVulnerabilitiesPerFetch = 50; - options.ListingCacheDuration = TimeSpan.Zero; - var cacheRoot = Path.Combine(Path.GetTempPath(), "stellaops-tests", _fixture.Database.DatabaseNamespace.DatabaseName); - Directory.CreateDirectory(cacheRoot); - options.CacheDirectory = Path.Combine(cacheRoot, "ru-nkcki"); - options.RequestDelay = TimeSpan.Zero; - }); + options.MaxBulletinsPerFetch = 2; + options.MaxListingPagesPerFetch = 2; + options.MaxVulnerabilitiesPerFetch = 50; + options.ListingCacheDuration = TimeSpan.Zero; + var cacheRoot = Path.Combine(Path.GetTempPath(), "stellaops-tests", _fixture.SchemaName); + Directory.CreateDirectory(cacheRoot); + options.CacheDirectory = Path.Combine(cacheRoot, "ru-nkcki"); + options.RequestDelay = TimeSpan.Zero; + }); services.Configure(RuNkckiOptions.HttpClientName, builderOptions => - { - builderOptions.HttpMessageHandlerBuilderActions.Add(builder => builder.PrimaryHandler = _handler); - }); - - var provider = services.BuildServiceProvider(); - var bootstrapper = provider.GetRequiredService(); - await bootstrapper.InitializeAsync(CancellationToken.None); - return provider; - } + { + builderOptions.HttpMessageHandlerBuilderActions.Add(builder => builder.PrimaryHandler = _handler); + }); + + return services.BuildServiceProvider(); + } private void SeedListingAndBulletin() { @@ -286,8 +283,8 @@ public sealed class RuNkckiConnectorTests : IAsyncLifetime throw new InvalidOperationException("Unable to locate project root for Ru.Nkcki tests."); } - public Task InitializeAsync() => Task.CompletedTask; - - public async Task DisposeAsync() - => await _fixture.Client.DropDatabaseAsync(_fixture.Database.DatabaseNamespace.DatabaseName); -} + public Task InitializeAsync() => Task.CompletedTask; + + public async Task DisposeAsync() + => await _fixture.TruncateAllTablesAsync(); +} diff --git a/src/Concelier/__Tests/StellaOps.Concelier.Connector.Ru.Nkcki.Tests/RuNkckiMapperTests.cs b/src/Concelier/__Tests/StellaOps.Concelier.Connector.Ru.Nkcki.Tests/RuNkckiMapperTests.cs index 9755a6de0..5534062f4 100644 --- a/src/Concelier/__Tests/StellaOps.Concelier.Connector.Ru.Nkcki.Tests/RuNkckiMapperTests.cs +++ b/src/Concelier/__Tests/StellaOps.Concelier.Connector.Ru.Nkcki.Tests/RuNkckiMapperTests.cs @@ -1,9 +1,9 @@ using System.Collections.Immutable; -using MongoDB.Bson; +using StellaOps.Concelier.Bson; using StellaOps.Concelier.Connector.Common; using StellaOps.Concelier.Models; using StellaOps.Concelier.Connector.Ru.Nkcki.Internal; -using StellaOps.Concelier.Storage.Mongo; +using StellaOps.Concelier.Storage; using Xunit; using System.Reflection; diff --git a/src/Concelier/__Tests/StellaOps.Concelier.Connector.StellaOpsMirror.Tests/StellaOpsMirrorConnectorTests.cs b/src/Concelier/__Tests/StellaOps.Concelier.Connector.StellaOpsMirror.Tests/StellaOpsMirrorConnectorTests.cs index 3c5fa86a1..34e9eb68b 100644 --- a/src/Concelier/__Tests/StellaOps.Concelier.Connector.StellaOpsMirror.Tests/StellaOpsMirrorConnectorTests.cs +++ b/src/Concelier/__Tests/StellaOps.Concelier.Connector.StellaOpsMirror.Tests/StellaOpsMirrorConnectorTests.cs @@ -12,16 +12,17 @@ using Microsoft.Extensions.Http; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; -using MongoDB.Bson; +using StellaOps.Concelier.Bson; using StellaOps.Concelier.Connector.Common; using StellaOps.Concelier.Connector.Common.Fetch; using StellaOps.Concelier.Connector.Common.Testing; using StellaOps.Concelier.Connector.StellaOpsMirror.Internal; using StellaOps.Concelier.Connector.StellaOpsMirror.Settings; -using StellaOps.Concelier.Storage.Mongo; -using StellaOps.Concelier.Storage.Mongo.Advisories; -using StellaOps.Concelier.Storage.Mongo; -using StellaOps.Concelier.Storage.Mongo; +using StellaOps.Concelier.Storage; +using StellaOps.Concelier.Storage.Advisories; +using StellaOps.Concelier.Storage; +using StellaOps.Concelier.Storage; +using StellaOps.Concelier.Storage.Postgres; using StellaOps.Concelier.Testing; using StellaOps.Cryptography; using StellaOps.Cryptography.DependencyInjection; @@ -30,13 +31,13 @@ using Xunit; namespace StellaOps.Concelier.Connector.StellaOpsMirror.Tests; -[Collection("mongo-fixture")] +[Collection(ConcelierFixtureCollection.Name)] public sealed class StellaOpsMirrorConnectorTests : IAsyncLifetime { - private readonly MongoIntegrationFixture _fixture; + private readonly ConcelierPostgresFixture _fixture; private readonly CannedHttpMessageHandler _handler; - public StellaOpsMirrorConnectorTests(MongoIntegrationFixture fixture) + public StellaOpsMirrorConnectorTests(ConcelierPostgresFixture fixture) { _fixture = fixture; _handler = new CannedHttpMessageHandler(); @@ -273,7 +274,7 @@ public sealed class StellaOpsMirrorConnectorTests : IAsyncLifetime private async Task BuildServiceProviderAsync(Action? configureOptions = null) { - await _fixture.Client.DropDatabaseAsync(_fixture.Database.DatabaseNamespace.DatabaseName); + await _fixture.TruncateAllTablesAsync(); _handler.Clear(); var services = new ServiceCollection(); @@ -281,11 +282,11 @@ public sealed class StellaOpsMirrorConnectorTests : IAsyncLifetime services.AddSingleton(_handler); services.AddSingleton(TimeProvider.System); - services.AddMongoStorage(options => + services.AddConcelierPostgresStorage(options => { - options.ConnectionString = _fixture.Runner.ConnectionString; - options.DatabaseName = _fixture.Database.DatabaseNamespace.DatabaseName; - options.CommandTimeout = TimeSpan.FromSeconds(5); + options.ConnectionString = _fixture.ConnectionString; + options.SchemaName = _fixture.SchemaName; + options.CommandTimeoutSeconds = 5; }); services.AddStellaOpsCrypto(); @@ -315,10 +316,7 @@ public sealed class StellaOpsMirrorConnectorTests : IAsyncLifetime }); }); - var provider = services.BuildServiceProvider(); - var bootstrapper = provider.GetRequiredService(); - await bootstrapper.InitializeAsync(CancellationToken.None); - return provider; + return services.BuildServiceProvider(); } private void SeedResponses(string indexJson, string manifestContent, string bundleContent, string? signature) diff --git a/src/Concelier/__Tests/StellaOps.Concelier.Connector.Vndr.Adobe.Tests/Adobe/AdobeConnectorFetchTests.cs b/src/Concelier/__Tests/StellaOps.Concelier.Connector.Vndr.Adobe.Tests/Adobe/AdobeConnectorFetchTests.cs index 4a4ff139b..7e10efa6a 100644 --- a/src/Concelier/__Tests/StellaOps.Concelier.Connector.Vndr.Adobe.Tests/Adobe/AdobeConnectorFetchTests.cs +++ b/src/Concelier/__Tests/StellaOps.Concelier.Connector.Vndr.Adobe.Tests/Adobe/AdobeConnectorFetchTests.cs @@ -11,33 +11,33 @@ using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Http; using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Abstractions; -using Microsoft.Extensions.Options; -using Microsoft.Extensions.Time.Testing; -using MongoDB.Bson; -using MongoDB.Driver; -using StellaOps.Concelier.Models; -using StellaOps.Concelier.Connector.Common; -using StellaOps.Concelier.Connector.Common.Http; -using StellaOps.Concelier.Connector.Common.Testing; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Time.Testing; +using StellaOps.Concelier.Bson; +using StellaOps.Concelier.Models; +using StellaOps.Concelier.Connector.Common; +using StellaOps.Concelier.Connector.Common.Http; +using StellaOps.Concelier.Connector.Common.Testing; using StellaOps.Concelier.Connector.Vndr.Adobe; using StellaOps.Concelier.Connector.Vndr.Adobe.Configuration; -using StellaOps.Concelier.Storage.Mongo; -using StellaOps.Concelier.Storage.Mongo.Advisories; -using StellaOps.Concelier.Storage.Mongo; -using StellaOps.Concelier.Storage.Mongo; -using StellaOps.Concelier.Storage.Mongo.PsirtFlags; -using StellaOps.Concelier.Testing; +using StellaOps.Concelier.Storage; +using StellaOps.Concelier.Storage.Advisories; +using StellaOps.Concelier.Storage; +using StellaOps.Concelier.Storage; +using StellaOps.Concelier.Storage.PsirtFlags; +using StellaOps.Concelier.Storage.Postgres; +using StellaOps.Concelier.Testing; namespace StellaOps.Concelier.Connector.Vndr.Adobe.Tests; -[Collection("mongo-fixture")] +[Collection(ConcelierFixtureCollection.Name)] public sealed class AdobeConnectorFetchTests : IAsyncLifetime { - private readonly MongoIntegrationFixture _fixture; + private readonly ConcelierPostgresFixture _fixture; private readonly FakeTimeProvider _timeProvider; - public AdobeConnectorFetchTests(MongoIntegrationFixture fixture) + public AdobeConnectorFetchTests(ConcelierPostgresFixture fixture) { _fixture = fixture; _timeProvider = new FakeTimeProvider(new DateTimeOffset(2025, 9, 10, 0, 0, 0, TimeSpan.Zero)); @@ -259,12 +259,10 @@ public sealed class AdobeConnectorFetchTests : IAsyncLifetime Assert.Equal(normalizedExpected, normalizedSnapshot); - var flagsCollection = _fixture.Database.GetCollection("psirt_flags"); - var rawFlags = await flagsCollection.Find(Builders.Filter.Empty).ToListAsync(); - Assert.NotEmpty(rawFlags); - - var flagRecord = rawFlags.Single(doc => doc["_id"].AsString == "APSB25-87"); - Assert.Equal("Adobe", flagRecord["vendor"].AsString); + var psirtStore = provider.GetRequiredService(); + var flagRecord = await psirtStore.FindAsync("APSB25-87", CancellationToken.None); + Assert.NotNull(flagRecord); + Assert.Equal("Adobe", flagRecord!.Vendor); } [Fact] @@ -321,21 +319,21 @@ public sealed class AdobeConnectorFetchTests : IAsyncLifetime Assert.True(state.Cursor.TryGetValue("pendingMappings", out var pendingMap) && pendingMap.AsBsonArray.Count == 0); } - private async Task BuildServiceProviderAsync(CannedHttpMessageHandler handler) - { - await _fixture.Client.DropDatabaseAsync(_fixture.Database.DatabaseNamespace.DatabaseName); + private async Task BuildServiceProviderAsync(CannedHttpMessageHandler handler) + { + await _fixture.TruncateAllTablesAsync(); var services = new ServiceCollection(); services.AddLogging(builder => builder.AddProvider(NullLoggerProvider.Instance)); services.AddSingleton(_timeProvider); services.AddSingleton(handler); - services.AddMongoStorage(options => - { - options.ConnectionString = _fixture.Runner.ConnectionString; - options.DatabaseName = _fixture.Database.DatabaseNamespace.DatabaseName; - options.CommandTimeout = TimeSpan.FromSeconds(5); - }); + services.AddConcelierPostgresStorage(options => + { + options.ConnectionString = _fixture.ConnectionString; + options.SchemaName = _fixture.SchemaName; + options.CommandTimeoutSeconds = 5; + }); services.AddSourceCommon(); services.AddAdobeConnector(opts => @@ -353,11 +351,8 @@ public sealed class AdobeConnectorFetchTests : IAsyncLifetime }); }); - var provider = services.BuildServiceProvider(); - var bootstrapper = provider.GetRequiredService(); - await bootstrapper.InitializeAsync(CancellationToken.None); - return provider; - } + return services.BuildServiceProvider(); + } private static void SeedIndex(CannedHttpMessageHandler handler) { diff --git a/src/Concelier/__Tests/StellaOps.Concelier.Connector.Vndr.Apple.Tests/Apple/AppleConnectorTests.cs b/src/Concelier/__Tests/StellaOps.Concelier.Connector.Vndr.Apple.Tests/Apple/AppleConnectorTests.cs index f288e6dfa..b1c26dea7 100644 --- a/src/Concelier/__Tests/StellaOps.Concelier.Connector.Vndr.Apple.Tests/Apple/AppleConnectorTests.cs +++ b/src/Concelier/__Tests/StellaOps.Concelier.Connector.Vndr.Apple.Tests/Apple/AppleConnectorTests.cs @@ -15,24 +15,25 @@ using StellaOps.Concelier.Connector.Common.Fetch; using StellaOps.Concelier.Connector.Common.Testing; using StellaOps.Concelier.Connector.Common.Packages; using StellaOps.Concelier.Connector.Vndr.Apple; -using StellaOps.Concelier.Storage.Mongo; -using StellaOps.Concelier.Storage.Mongo.Advisories; -using StellaOps.Concelier.Storage.Mongo.PsirtFlags; -using StellaOps.Concelier.Testing; -using Xunit; +using StellaOps.Concelier.Storage; +using StellaOps.Concelier.Storage.Advisories; +using StellaOps.Concelier.Storage.PsirtFlags; +using StellaOps.Concelier.Storage.Postgres; +using StellaOps.Concelier.Testing; +using Xunit; namespace StellaOps.Concelier.Connector.Vndr.Apple.Tests; -[Collection("mongo-fixture")] +[Collection(ConcelierFixtureCollection.Name)] public sealed class AppleConnectorTests : IAsyncLifetime { private static readonly Uri IndexUri = new("https://support.example.com/index.json"); private static readonly Uri DetailBaseUri = new("https://support.example.com/en-us/"); - private readonly MongoIntegrationFixture _fixture; + private readonly ConcelierPostgresFixture _fixture; private readonly FakeTimeProvider _timeProvider; - public AppleConnectorTests(MongoIntegrationFixture fixture) + public AppleConnectorTests(ConcelierPostgresFixture fixture) { _fixture = fixture; _timeProvider = new FakeTimeProvider(new DateTimeOffset(2025, 10, 10, 0, 0, 0, TimeSpan.Zero)); @@ -178,21 +179,21 @@ public sealed class AppleConnectorTests : IAsyncLifetime public Task DisposeAsync() => Task.CompletedTask; - private async Task BuildServiceProviderAsync(CannedHttpMessageHandler handler) - { - await _fixture.Client.DropDatabaseAsync(_fixture.Database.DatabaseNamespace.DatabaseName); + private async Task BuildServiceProviderAsync(CannedHttpMessageHandler handler) + { + await _fixture.TruncateAllTablesAsync(); var services = new ServiceCollection(); services.AddLogging(builder => builder.AddProvider(NullLoggerProvider.Instance)); services.AddSingleton(_timeProvider); services.AddSingleton(handler); - services.AddMongoStorage(options => - { - options.ConnectionString = _fixture.Runner.ConnectionString; - options.DatabaseName = _fixture.Database.DatabaseNamespace.DatabaseName; - options.CommandTimeout = TimeSpan.FromSeconds(5); - }); + services.AddConcelierPostgresStorage(options => + { + options.ConnectionString = _fixture.ConnectionString; + options.SchemaName = _fixture.SchemaName; + options.CommandTimeoutSeconds = 5; + }); services.AddSourceCommon(); services.AddAppleConnector(opts => @@ -213,11 +214,8 @@ public sealed class AppleConnectorTests : IAsyncLifetime }); }); - var provider = services.BuildServiceProvider(); - var bootstrapper = provider.GetRequiredService(); - await bootstrapper.InitializeAsync(CancellationToken.None); - return provider; - } + return services.BuildServiceProvider(); + } private static void SeedIndex(CannedHttpMessageHandler handler) { diff --git a/src/Concelier/__Tests/StellaOps.Concelier.Connector.Vndr.Chromium.Tests/Chromium/ChromiumConnectorTests.cs b/src/Concelier/__Tests/StellaOps.Concelier.Connector.Vndr.Chromium.Tests/Chromium/ChromiumConnectorTests.cs index e7c6d1ae6..4de047a14 100644 --- a/src/Concelier/__Tests/StellaOps.Concelier.Connector.Vndr.Chromium.Tests/Chromium/ChromiumConnectorTests.cs +++ b/src/Concelier/__Tests/StellaOps.Concelier.Connector.Vndr.Chromium.Tests/Chromium/ChromiumConnectorTests.cs @@ -5,34 +5,34 @@ using System.Linq; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Http; using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Abstractions; -using Microsoft.Extensions.Options; -using Microsoft.Extensions.Time.Testing; -using MongoDB.Bson; -using MongoDB.Driver; -using StellaOps.Concelier.Models; -using StellaOps.Concelier.Connector.Common.Http; -using StellaOps.Concelier.Connector.Common.Json; -using StellaOps.Concelier.Connector.Common.Testing; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Time.Testing; +using StellaOps.Concelier.Bson; +using StellaOps.Concelier.Storage.Postgres; +using StellaOps.Concelier.Models; +using StellaOps.Concelier.Connector.Common.Http; +using StellaOps.Concelier.Connector.Common.Json; +using StellaOps.Concelier.Connector.Common.Testing; using StellaOps.Concelier.Connector.Common; using StellaOps.Concelier.Connector.Vndr.Chromium; using StellaOps.Concelier.Connector.Vndr.Chromium.Configuration; -using StellaOps.Concelier.Storage.Mongo; -using StellaOps.Concelier.Storage.Mongo.Advisories; -using StellaOps.Concelier.Storage.Mongo; -using StellaOps.Concelier.Storage.Mongo.PsirtFlags; +using StellaOps.Concelier.Storage; +using StellaOps.Concelier.Storage.Advisories; +using StellaOps.Concelier.Storage; +using StellaOps.Concelier.Storage.PsirtFlags; using StellaOps.Concelier.Testing; namespace StellaOps.Concelier.Connector.Vndr.Chromium.Tests; -[Collection("mongo-fixture")] +[Collection(ConcelierFixtureCollection.Name)] public sealed class ChromiumConnectorTests : IAsyncLifetime { - private readonly MongoIntegrationFixture _fixture; + private readonly ConcelierPostgresFixture _fixture; private readonly FakeTimeProvider _timeProvider; private readonly List _allocatedDatabases = new(); - public ChromiumConnectorTests(MongoIntegrationFixture fixture) + public ChromiumConnectorTests(ConcelierPostgresFixture fixture) { _fixture = fixture; _timeProvider = new FakeTimeProvider(new DateTimeOffset(2024, 9, 10, 18, 0, 0, TimeSpan.Zero)); @@ -263,19 +263,19 @@ public sealed class ChromiumConnectorTests : IAsyncLifetime } } - private async Task BuildServiceProviderAsync(CannedHttpMessageHandler handler, string databaseName) - { - var services = new ServiceCollection(); - services.AddLogging(builder => builder.AddProvider(NullLoggerProvider.Instance)); - services.AddSingleton(_timeProvider); - services.AddSingleton(handler); - - services.AddMongoStorage(options => - { - options.ConnectionString = _fixture.Runner.ConnectionString; - options.DatabaseName = databaseName; - options.CommandTimeout = TimeSpan.FromSeconds(5); - }); + private async Task BuildServiceProviderAsync(CannedHttpMessageHandler handler, string databaseName) + { + var services = new ServiceCollection(); + services.AddLogging(builder => builder.AddProvider(NullLoggerProvider.Instance)); + services.AddSingleton(_timeProvider); + services.AddSingleton(handler); + + services.AddConcelierPostgresStorage(options => + { + options.ConnectionString = _fixture.ConnectionString; + options.SchemaName = _fixture.SchemaName; + options.CommandTimeoutSeconds = 5; + }); services.AddSourceCommon(); services.AddChromiumConnector(opts => @@ -295,13 +295,8 @@ public sealed class ChromiumConnectorTests : IAsyncLifetime }); }); - var provider = services.BuildServiceProvider(); - - var bootstrapper = provider.GetRequiredService(); - await bootstrapper.InitializeAsync(CancellationToken.None); - - return provider; - } + return services.BuildServiceProvider(); + } private string AllocateDatabaseName() { @@ -310,16 +305,10 @@ public sealed class ChromiumConnectorTests : IAsyncLifetime return name; } - private async Task DropDatabaseAsync(string databaseName) - { - try - { - await _fixture.Client.DropDatabaseAsync(databaseName); - } - catch (MongoCommandException ex) when (ex.CodeName == "NamespaceNotFound") - { - } - } + private async Task DropDatabaseAsync(string databaseName) + { + await _fixture.TruncateAllTablesAsync(); + } private static void SeedHttpFixtures(CannedHttpMessageHandler handler) { diff --git a/src/Concelier/__Tests/StellaOps.Concelier.Connector.Vndr.Cisco.Tests/CiscoMapperTests.cs b/src/Concelier/__Tests/StellaOps.Concelier.Connector.Vndr.Cisco.Tests/CiscoMapperTests.cs index dab66f282..03a7f42bc 100644 --- a/src/Concelier/__Tests/StellaOps.Concelier.Connector.Vndr.Cisco.Tests/CiscoMapperTests.cs +++ b/src/Concelier/__Tests/StellaOps.Concelier.Connector.Vndr.Cisco.Tests/CiscoMapperTests.cs @@ -2,13 +2,13 @@ using System; using System.Collections.Generic; using System.Linq; using FluentAssertions; -using MongoDB.Bson; +using StellaOps.Concelier.Bson; using StellaOps.Concelier.Models; using StellaOps.Concelier.Connector.Common; using StellaOps.Concelier.Connector.Vndr.Cisco; using StellaOps.Concelier.Connector.Vndr.Cisco.Internal; -using StellaOps.Concelier.Storage.Mongo; -using StellaOps.Concelier.Storage.Mongo; +using StellaOps.Concelier.Storage; +using StellaOps.Concelier.Storage; using Xunit; namespace StellaOps.Concelier.Connector.Vndr.Cisco.Tests; diff --git a/src/Concelier/__Tests/StellaOps.Concelier.Connector.Vndr.Msrc.Tests/MsrcConnectorTests.cs b/src/Concelier/__Tests/StellaOps.Concelier.Connector.Vndr.Msrc.Tests/MsrcConnectorTests.cs index b3d308df3..813df2879 100644 --- a/src/Concelier/__Tests/StellaOps.Concelier.Connector.Vndr.Msrc.Tests/MsrcConnectorTests.cs +++ b/src/Concelier/__Tests/StellaOps.Concelier.Connector.Vndr.Msrc.Tests/MsrcConnectorTests.cs @@ -10,33 +10,34 @@ using Microsoft.Extensions.Http; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; -using MongoDB.Bson; +using StellaOps.Concelier.Bson; using StellaOps.Concelier.Connector.Common.Fetch; using StellaOps.Concelier.Connector.Common; using StellaOps.Concelier.Connector.Common.Testing; using StellaOps.Concelier.Connector.Vndr.Msrc.Configuration; using StellaOps.Concelier.Connector.Vndr.Msrc.Internal; -using StellaOps.Concelier.Storage.Mongo; -using StellaOps.Concelier.Storage.Mongo.Advisories; -using StellaOps.Concelier.Storage.Mongo; -using StellaOps.Concelier.Storage.Mongo; -using StellaOps.Concelier.Testing; -using Xunit; -using StellaOps.Concelier.Connector.Common.Http; +using StellaOps.Concelier.Storage; +using StellaOps.Concelier.Storage.Advisories; +using StellaOps.Concelier.Storage; +using StellaOps.Concelier.Storage; +using StellaOps.Concelier.Storage.Postgres; +using StellaOps.Concelier.Testing; +using Xunit; +using StellaOps.Concelier.Connector.Common.Http; namespace StellaOps.Concelier.Connector.Vndr.Msrc.Tests; -[Collection("mongo-fixture")] +[Collection(ConcelierFixtureCollection.Name)] public sealed class MsrcConnectorTests : IAsyncLifetime { private static readonly Uri TokenUri = new("https://login.microsoftonline.com/11111111-1111-1111-1111-111111111111/oauth2/v2.0/token"); private static readonly Uri SummaryUri = new("https://api.msrc.microsoft.com/sug/v2.0/vulnerabilities"); private static readonly Uri DetailUri = new("https://api.msrc.microsoft.com/sug/v2.0/vulnerability/ADV123456"); - private readonly MongoIntegrationFixture _fixture; + private readonly ConcelierPostgresFixture _fixture; private readonly CannedHttpMessageHandler _handler; - public MsrcConnectorTests(MongoIntegrationFixture fixture) + public MsrcConnectorTests(ConcelierPostgresFixture fixture) { _fixture = fixture; _handler = new CannedHttpMessageHandler(); @@ -80,22 +81,22 @@ public sealed class MsrcConnectorTests : IAsyncLifetime cvrfDocument!.Status.Should().Be(DocumentStatuses.Mapped); } - private async Task BuildServiceProviderAsync() - { - await _fixture.Client.DropDatabaseAsync(_fixture.Database.DatabaseNamespace.DatabaseName); - _handler.Clear(); + private async Task BuildServiceProviderAsync() + { + await _fixture.TruncateAllTablesAsync(); + _handler.Clear(); var services = new ServiceCollection(); services.AddLogging(builder => builder.AddProvider(NullLoggerProvider.Instance)); services.AddSingleton(_handler); services.AddSingleton(TimeProvider.System); - services.AddMongoStorage(options => - { - options.ConnectionString = _fixture.Runner.ConnectionString; - options.DatabaseName = _fixture.Database.DatabaseNamespace.DatabaseName; - options.CommandTimeout = TimeSpan.FromSeconds(5); - }); + services.AddConcelierPostgresStorage(options => + { + options.ConnectionString = _fixture.ConnectionString; + options.SchemaName = _fixture.SchemaName; + options.CommandTimeoutSeconds = 5; + }); services.AddSourceCommon(); services.AddMsrcConnector(options => @@ -126,11 +127,8 @@ public sealed class MsrcConnectorTests : IAsyncLifetime }); }); - var provider = services.BuildServiceProvider(); - var bootstrapper = provider.GetRequiredService(); - await bootstrapper.InitializeAsync(CancellationToken.None); - return provider; - } + return services.BuildServiceProvider(); + } private void SeedResponses() { diff --git a/src/Concelier/__Tests/StellaOps.Concelier.Connector.Vndr.Oracle.Tests/Oracle/OracleConnectorTests.cs b/src/Concelier/__Tests/StellaOps.Concelier.Connector.Vndr.Oracle.Tests/Oracle/OracleConnectorTests.cs index 67364cf71..a06f61af4 100644 --- a/src/Concelier/__Tests/StellaOps.Concelier.Connector.Vndr.Oracle.Tests/Oracle/OracleConnectorTests.cs +++ b/src/Concelier/__Tests/StellaOps.Concelier.Connector.Vndr.Oracle.Tests/Oracle/OracleConnectorTests.cs @@ -10,32 +10,33 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Http; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Abstractions; -using Microsoft.Extensions.Options; -using Microsoft.Extensions.Time.Testing; -using MongoDB.Bson; -using MongoDB.Driver; -using StellaOps.Concelier.Models; -using StellaOps.Concelier.Connector.Common; -using StellaOps.Concelier.Connector.Common.Http; -using StellaOps.Concelier.Connector.Common.Testing; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Time.Testing; +using StellaOps.Concelier.Bson; +using StellaOps.Concelier.Models; +using StellaOps.Concelier.Connector.Common; +using StellaOps.Concelier.Connector.Common.Http; +using StellaOps.Concelier.Connector.Common.Testing; using StellaOps.Concelier.Connector.Vndr.Oracle; using StellaOps.Concelier.Connector.Vndr.Oracle.Configuration; using StellaOps.Concelier.Connector.Vndr.Oracle.Internal; -using StellaOps.Concelier.Storage.Mongo; -using StellaOps.Concelier.Storage.Mongo.Advisories; -using StellaOps.Concelier.Storage.Mongo; -using StellaOps.Concelier.Storage.Mongo; -using StellaOps.Concelier.Testing; -using Xunit.Abstractions; +using StellaOps.Concelier.Storage; +using StellaOps.Concelier.Storage.Advisories; +using StellaOps.Concelier.Storage; +using StellaOps.Concelier.Storage; +using StellaOps.Concelier.Storage.Postgres; +using StellaOps.Concelier.Storage.PsirtFlags; +using StellaOps.Concelier.Testing; +using Xunit.Abstractions; namespace StellaOps.Concelier.Connector.Vndr.Oracle.Tests; -[Collection("mongo-fixture")] +[Collection(ConcelierFixtureCollection.Name)] public sealed class OracleConnectorTests : IAsyncLifetime { - private readonly MongoIntegrationFixture _fixture; + private readonly ConcelierPostgresFixture _fixture; private readonly FakeTimeProvider _timeProvider; private readonly CannedHttpMessageHandler _handler; private readonly ITestOutputHelper _output; @@ -44,7 +45,7 @@ public sealed class OracleConnectorTests : IAsyncLifetime private static readonly Uri AdvisoryTwo = new("https://www.oracle.com/security-alerts/cpuapr2024-02.html"); private static readonly Uri CalendarUri = new("https://www.oracle.com/security-alerts/cpuapr2024.html"); - public OracleConnectorTests(MongoIntegrationFixture fixture, ITestOutputHelper output) + public OracleConnectorTests(ConcelierPostgresFixture fixture, ITestOutputHelper output) { _fixture = fixture; _timeProvider = new FakeTimeProvider(new DateTimeOffset(2024, 4, 18, 0, 0, 0, TimeSpan.Zero)); @@ -105,11 +106,19 @@ public sealed class OracleConnectorTests : IAsyncLifetime Assert.Equal(normalizedExpected, normalizedSnapshot); - var psirtCollection = _fixture.Database.GetCollection(MongoStorageDefaults.Collections.PsirtFlags); - var flags = await psirtCollection.Find(Builders.Filter.Empty).ToListAsync(); - _output.WriteLine("Psirt flags: " + string.Join(", ", flags.Select(doc => doc.GetValue("_id", BsonValue.Create("")).ToString()))); - Assert.Equal(2, flags.Count); - Assert.All(flags, doc => Assert.Equal("Oracle", doc["vendor"].AsString)); + var psirtStore = provider.GetRequiredService(); + var flags = new List(); + foreach (var advisory in advisories) + { + var flag = await psirtStore.FindAsync(advisory.AdvisoryKey, CancellationToken.None); + if (flag is not null) + { + flags.Add(flag); + } + } + + Assert.Equal(2, flags.Count); + Assert.All(flags, flag => Assert.Equal("Oracle", flag.Vendor)); } [Fact] @@ -149,9 +158,11 @@ public sealed class OracleConnectorTests : IAsyncLifetime Assert.NotNull(second); Assert.Equal(DocumentStatuses.Mapped, second!.Status); - var dtoCollection = _fixture.Database.GetCollection(MongoStorageDefaults.Collections.Dto); - var dtoCount = await dtoCollection.CountDocumentsAsync(Builders.Filter.Empty); - Assert.Equal(2, dtoCount); + var dtoStore = provider.GetRequiredService(); + var dto1 = await dtoStore.FindByDocumentIdAsync(first!.Id, CancellationToken.None); + Assert.NotNull(dto1); + var dto2 = await dtoStore.FindByDocumentIdAsync(second!.Id, CancellationToken.None); + Assert.NotNull(dto2); } [Fact] @@ -209,18 +220,10 @@ public sealed class OracleConnectorTests : IAsyncLifetime Assert.NotNull(invalidDocument); _output.WriteLine($"Invalid document status: {invalidDocument!.Status}"); - var rawDoc = await _fixture.Database.GetCollection(MongoStorageDefaults.Collections.Document) - .Find(Builders.Filter.Eq("uri", AdvisoryOne.ToString())) - .FirstOrDefaultAsync(); - if (rawDoc is not null) - { - _output.WriteLine("Raw document: " + rawDoc.ToJson()); - } - - var dtoStore = provider.GetRequiredService(); - var invalidDto = await dtoStore.FindByDocumentIdAsync(invalidDocument.Id, CancellationToken.None); - if (invalidDto is not null) - { + var dtoStore = provider.GetRequiredService(); + var invalidDto = await dtoStore.FindByDocumentIdAsync(invalidDocument.Id, CancellationToken.None); + if (invalidDto is not null) + { _output.WriteLine("Validation unexpectedly succeeded. DTO: " + invalidDto.Payload.ToJson()); } Assert.Equal(DocumentStatuses.Failed, invalidDocument.Status); @@ -234,12 +237,15 @@ public sealed class OracleConnectorTests : IAsyncLifetime await connector.MapAsync(provider, CancellationToken.None); var advisories = await provider.GetRequiredService().GetRecentAsync(10, CancellationToken.None); - Assert.Single(advisories); - Assert.Equal("oracle/cpuapr2024-02-html", advisories[0].AdvisoryKey); - - var psirtCollection = _fixture.Database.GetCollection(MongoStorageDefaults.Collections.PsirtFlags); - var flagCount = await psirtCollection.CountDocumentsAsync(Builders.Filter.Empty); - Assert.Equal(1, flagCount); + Assert.Single(advisories); + Assert.Equal("oracle/cpuapr2024-02-html", advisories[0].AdvisoryKey); + + var psirtStore = provider.GetRequiredService(); + var validFlag = await psirtStore.FindAsync(advisories[0].AdvisoryKey, CancellationToken.None); + Assert.NotNull(validFlag); + + var missingFlag = await psirtStore.FindAsync("oracle/cpuapr2024-01-html", CancellationToken.None); + Assert.Null(missingFlag); var stateRepository = provider.GetRequiredService(); var state = await stateRepository.TryGetAsync(VndrOracleConnectorPlugin.SourceName, CancellationToken.None); @@ -249,22 +255,22 @@ public sealed class OracleConnectorTests : IAsyncLifetime Assert.Empty(cursor.PendingMappings); } - private async Task BuildServiceProviderAsync() - { - await _fixture.Client.DropDatabaseAsync(_fixture.Database.DatabaseNamespace.DatabaseName); - _handler.Clear(); + private async Task BuildServiceProviderAsync() + { + await _fixture.TruncateAllTablesAsync(); + _handler.Clear(); var services = new ServiceCollection(); services.AddLogging(builder => builder.AddProvider(NullLoggerProvider.Instance)); services.AddSingleton(_timeProvider); services.AddSingleton(_handler); - services.AddMongoStorage(options => - { - options.ConnectionString = _fixture.Runner.ConnectionString; - options.DatabaseName = _fixture.Database.DatabaseNamespace.DatabaseName; - options.CommandTimeout = TimeSpan.FromSeconds(5); - }); + services.AddConcelierPostgresStorage(options => + { + options.ConnectionString = _fixture.ConnectionString; + options.SchemaName = _fixture.SchemaName; + options.CommandTimeoutSeconds = 5; + }); services.AddSourceCommon(); services.AddOracleConnector(opts => @@ -281,11 +287,8 @@ public sealed class OracleConnectorTests : IAsyncLifetime }); }); - var provider = services.BuildServiceProvider(); - var bootstrapper = provider.GetRequiredService(); - await bootstrapper.InitializeAsync(CancellationToken.None); - return provider; - } + return services.BuildServiceProvider(); + } private void SeedDetails() { diff --git a/src/Concelier/__Tests/StellaOps.Concelier.Connector.Vndr.Vmware.Tests/Vmware/VmwareConnectorTests.cs b/src/Concelier/__Tests/StellaOps.Concelier.Connector.Vndr.Vmware.Tests/Vmware/VmwareConnectorTests.cs index 2ae6969f9..7fa1616fa 100644 --- a/src/Concelier/__Tests/StellaOps.Concelier.Connector.Vndr.Vmware.Tests/Vmware/VmwareConnectorTests.cs +++ b/src/Concelier/__Tests/StellaOps.Concelier.Connector.Vndr.Vmware.Tests/Vmware/VmwareConnectorTests.cs @@ -9,32 +9,33 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Http; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Abstractions; -using Microsoft.Extensions.Options; -using Microsoft.Extensions.Time.Testing; -using MongoDB.Bson; -using MongoDB.Driver; -using StellaOps.Concelier.Models; -using StellaOps.Concelier.Connector.Common; -using StellaOps.Concelier.Connector.Common.Http; -using StellaOps.Concelier.Connector.Common.Testing; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Time.Testing; +using StellaOps.Concelier.Bson; +using StellaOps.Concelier.Models; +using StellaOps.Concelier.Connector.Common; +using StellaOps.Concelier.Connector.Common.Http; +using StellaOps.Concelier.Connector.Common.Testing; using StellaOps.Concelier.Connector.Vndr.Vmware; using StellaOps.Concelier.Connector.Vndr.Vmware.Configuration; using StellaOps.Concelier.Connector.Vndr.Vmware.Internal; -using StellaOps.Concelier.Storage.Mongo; -using StellaOps.Concelier.Storage.Mongo.Advisories; -using StellaOps.Concelier.Storage.Mongo; -using StellaOps.Concelier.Storage.Mongo; -using StellaOps.Concelier.Testing; -using Xunit.Abstractions; +using StellaOps.Concelier.Storage; +using StellaOps.Concelier.Storage.Advisories; +using StellaOps.Concelier.Storage; +using StellaOps.Concelier.Storage; +using StellaOps.Concelier.Storage.Postgres; +using StellaOps.Concelier.Storage.PsirtFlags; +using StellaOps.Concelier.Testing; +using Xunit.Abstractions; namespace StellaOps.Concelier.Connector.Vndr.Vmware.Tests.Vmware; -[Collection("mongo-fixture")] +[Collection(ConcelierFixtureCollection.Name)] public sealed class VmwareConnectorTests : IAsyncLifetime { - private readonly MongoIntegrationFixture _fixture; + private readonly ConcelierPostgresFixture _fixture; private readonly FakeTimeProvider _timeProvider; private readonly CannedHttpMessageHandler _handler; private readonly ITestOutputHelper _output; @@ -44,7 +45,7 @@ public sealed class VmwareConnectorTests : IAsyncLifetime private static readonly Uri DetailTwo = new("https://vmware.example/api/vmsa/VMSA-2024-0002.json"); private static readonly Uri DetailThree = new("https://vmware.example/api/vmsa/VMSA-2024-0003.json"); - public VmwareConnectorTests(MongoIntegrationFixture fixture, ITestOutputHelper output) + public VmwareConnectorTests(ConcelierPostgresFixture fixture, ITestOutputHelper output) { _fixture = fixture; _timeProvider = new FakeTimeProvider(new DateTimeOffset(2024, 4, 5, 0, 0, 0, TimeSpan.Zero)); @@ -81,11 +82,19 @@ public sealed class VmwareConnectorTests : IAsyncLifetime Assert.Equal(expected, snapshot); - var psirtCollection = _fixture.Database.GetCollection(MongoStorageDefaults.Collections.PsirtFlags); - var psirtFlags = await psirtCollection.Find(Builders.Filter.Empty).ToListAsync(); - _output.WriteLine("PSIRT flags after initial map: " + string.Join(", ", psirtFlags.Select(flag => flag.GetValue("_id", BsonValue.Create("")).ToString()))); - Assert.Equal(2, psirtFlags.Count); - Assert.All(psirtFlags, doc => Assert.Equal("VMware", doc["vendor"].AsString)); + var psirtStore = provider.GetRequiredService(); + var psirtFlags = new List(); + foreach (var advisory in ordered) + { + var flag = await psirtStore.FindAsync(advisory.AdvisoryKey, CancellationToken.None); + if (flag is not null) + { + psirtFlags.Add(flag); + } + } + + Assert.Equal(2, psirtFlags.Count); + Assert.All(psirtFlags, flag => Assert.Equal("VMware", flag.Vendor)); var stateRepository = provider.GetRequiredService(); var state = await stateRepository.TryGetAsync(VmwareConnectorPlugin.SourceName, CancellationToken.None); @@ -149,22 +158,22 @@ public sealed class VmwareConnectorTests : IAsyncLifetime return Task.CompletedTask; } - private async Task BuildServiceProviderAsync() - { - await _fixture.Client.DropDatabaseAsync(_fixture.Database.DatabaseNamespace.DatabaseName); - _handler.Clear(); + private async Task BuildServiceProviderAsync() + { + await _fixture.TruncateAllTablesAsync(); + _handler.Clear(); var services = new ServiceCollection(); services.AddLogging(builder => builder.AddProvider(NullLoggerProvider.Instance)); services.AddSingleton(_timeProvider); services.AddSingleton(_handler); - services.AddMongoStorage(options => - { - options.ConnectionString = _fixture.Runner.ConnectionString; - options.DatabaseName = _fixture.Database.DatabaseNamespace.DatabaseName; - options.CommandTimeout = TimeSpan.FromSeconds(5); - }); + services.AddConcelierPostgresStorage(options => + { + options.ConnectionString = _fixture.ConnectionString; + options.SchemaName = _fixture.SchemaName; + options.CommandTimeoutSeconds = 5; + }); services.AddSourceCommon(); services.AddVmwareConnector(opts => @@ -181,11 +190,8 @@ public sealed class VmwareConnectorTests : IAsyncLifetime builderOptions.HttpMessageHandlerBuilderActions.Add(builder => builder.PrimaryHandler = _handler); }); - var provider = services.BuildServiceProvider(); - var bootstrapper = provider.GetRequiredService(); - await bootstrapper.InitializeAsync(CancellationToken.None); - return provider; - } + return services.BuildServiceProvider(); + } private void SeedInitialResponses() { diff --git a/src/Concelier/__Tests/StellaOps.Concelier.Connector.Vndr.Vmware.Tests/Vmware/VmwareMapperTests.cs b/src/Concelier/__Tests/StellaOps.Concelier.Connector.Vndr.Vmware.Tests/Vmware/VmwareMapperTests.cs index 9850ecebb..73c90e751 100644 --- a/src/Concelier/__Tests/StellaOps.Concelier.Connector.Vndr.Vmware.Tests/Vmware/VmwareMapperTests.cs +++ b/src/Concelier/__Tests/StellaOps.Concelier.Connector.Vndr.Vmware.Tests/Vmware/VmwareMapperTests.cs @@ -1,13 +1,13 @@ using System; using System.Collections.Generic; using System.Text.Json; -using MongoDB.Bson; +using StellaOps.Concelier.Bson; using StellaOps.Concelier.Models; using StellaOps.Concelier.Connector.Common; using StellaOps.Concelier.Connector.Vndr.Vmware; using StellaOps.Concelier.Connector.Vndr.Vmware.Internal; -using StellaOps.Concelier.Storage.Mongo; -using StellaOps.Concelier.Storage.Mongo; +using StellaOps.Concelier.Storage; +using StellaOps.Concelier.Storage; using Xunit; namespace StellaOps.Concelier.Connector.Vndr.Vmware.Tests; diff --git a/src/Concelier/__Tests/StellaOps.Concelier.Exporter.Json.Tests/JsonExporterDependencyInjectionRoutineTests.cs b/src/Concelier/__Tests/StellaOps.Concelier.Exporter.Json.Tests/JsonExporterDependencyInjectionRoutineTests.cs index 5cf548621..defb46ad1 100644 --- a/src/Concelier/__Tests/StellaOps.Concelier.Exporter.Json.Tests/JsonExporterDependencyInjectionRoutineTests.cs +++ b/src/Concelier/__Tests/StellaOps.Concelier.Exporter.Json.Tests/JsonExporterDependencyInjectionRoutineTests.cs @@ -9,8 +9,8 @@ using Microsoft.Extensions.Options; using StellaOps.Concelier.Core.Jobs; using StellaOps.Concelier.Core.Events; using StellaOps.Concelier.Exporter.Json; -using StellaOps.Concelier.Storage.Mongo.Advisories; -using StellaOps.Concelier.Storage.Mongo.Exporting; +using StellaOps.Concelier.Storage.Advisories; +using StellaOps.Concelier.Storage.Exporting; using StellaOps.Concelier.Models; using StellaOps.Cryptography; using StellaOps.Cryptography.DependencyInjection; diff --git a/src/Concelier/__Tests/StellaOps.Concelier.Exporter.Json.Tests/JsonFeedExporterTests.cs b/src/Concelier/__Tests/StellaOps.Concelier.Exporter.Json.Tests/JsonFeedExporterTests.cs index 2903d724c..1058189d8 100644 --- a/src/Concelier/__Tests/StellaOps.Concelier.Exporter.Json.Tests/JsonFeedExporterTests.cs +++ b/src/Concelier/__Tests/StellaOps.Concelier.Exporter.Json.Tests/JsonFeedExporterTests.cs @@ -16,8 +16,8 @@ using Microsoft.Extensions.Options; using StellaOps.Concelier.Core.Events; using StellaOps.Concelier.Exporter.Json; using StellaOps.Concelier.Models; -using StellaOps.Concelier.Storage.Mongo.Advisories; -using StellaOps.Concelier.Storage.Mongo.Exporting; +using StellaOps.Concelier.Storage.Advisories; +using StellaOps.Concelier.Storage.Exporting; using StellaOps.Provenance.Mongo; using StellaOps.Cryptography; using StellaOps.Cryptography.DependencyInjection; diff --git a/src/Concelier/__Tests/StellaOps.Concelier.Exporter.TrivyDb.Tests/TrivyDbExportPlannerTests.cs b/src/Concelier/__Tests/StellaOps.Concelier.Exporter.TrivyDb.Tests/TrivyDbExportPlannerTests.cs index 9dc782475..55a3783ad 100644 --- a/src/Concelier/__Tests/StellaOps.Concelier.Exporter.TrivyDb.Tests/TrivyDbExportPlannerTests.cs +++ b/src/Concelier/__Tests/StellaOps.Concelier.Exporter.TrivyDb.Tests/TrivyDbExportPlannerTests.cs @@ -1,6 +1,6 @@ using System; using StellaOps.Concelier.Exporter.TrivyDb; -using StellaOps.Concelier.Storage.Mongo.Exporting; +using StellaOps.Concelier.Storage.Exporting; namespace StellaOps.Concelier.Exporter.TrivyDb.Tests; diff --git a/src/Concelier/__Tests/StellaOps.Concelier.Exporter.TrivyDb.Tests/TrivyDbFeedExporterTests.cs b/src/Concelier/__Tests/StellaOps.Concelier.Exporter.TrivyDb.Tests/TrivyDbFeedExporterTests.cs index 58b37ba21..3280fc74c 100644 --- a/src/Concelier/__Tests/StellaOps.Concelier.Exporter.TrivyDb.Tests/TrivyDbFeedExporterTests.cs +++ b/src/Concelier/__Tests/StellaOps.Concelier.Exporter.TrivyDb.Tests/TrivyDbFeedExporterTests.cs @@ -14,8 +14,8 @@ using Microsoft.Extensions.Options; using StellaOps.Concelier.Exporter.Json; using StellaOps.Concelier.Exporter.TrivyDb; using StellaOps.Concelier.Models; -using StellaOps.Concelier.Storage.Mongo.Advisories; -using StellaOps.Concelier.Storage.Mongo.Exporting; +using StellaOps.Concelier.Storage.Advisories; +using StellaOps.Concelier.Storage.Exporting; namespace StellaOps.Concelier.Exporter.TrivyDb.Tests; diff --git a/src/Concelier/__Tests/StellaOps.Concelier.Exporter.TrivyDb.Tests/TrivyDbOciWriterTests.cs b/src/Concelier/__Tests/StellaOps.Concelier.Exporter.TrivyDb.Tests/TrivyDbOciWriterTests.cs index 0d7eb2540..4f51bb9ba 100644 --- a/src/Concelier/__Tests/StellaOps.Concelier.Exporter.TrivyDb.Tests/TrivyDbOciWriterTests.cs +++ b/src/Concelier/__Tests/StellaOps.Concelier.Exporter.TrivyDb.Tests/TrivyDbOciWriterTests.cs @@ -7,7 +7,7 @@ using System.Text; using System.Text.Json; using System.Threading; using System.Threading.Tasks; -using StellaOps.Concelier.Storage.Mongo.Exporting; +using StellaOps.Concelier.Storage.Exporting; using Xunit; namespace StellaOps.Concelier.Exporter.TrivyDb.Tests; diff --git a/src/Concelier/__Tests/StellaOps.Concelier.Merge.Tests/AdvisoryMergeServiceTests.cs b/src/Concelier/__Tests/StellaOps.Concelier.Merge.Tests/AdvisoryMergeServiceTests.cs index 400c2973c..b65df0468 100644 --- a/src/Concelier/__Tests/StellaOps.Concelier.Merge.Tests/AdvisoryMergeServiceTests.cs +++ b/src/Concelier/__Tests/StellaOps.Concelier.Merge.Tests/AdvisoryMergeServiceTests.cs @@ -8,9 +8,9 @@ using StellaOps.Concelier.Core; using StellaOps.Concelier.Core.Events; using StellaOps.Concelier.Merge.Services; using StellaOps.Concelier.Models; -using StellaOps.Concelier.Storage.Mongo.Advisories; -using StellaOps.Concelier.Storage.Mongo.Aliases; -using StellaOps.Concelier.Storage.Mongo.MergeEvents; +using StellaOps.Concelier.Storage.Advisories; +using StellaOps.Concelier.Storage.Aliases; +using StellaOps.Concelier.Storage.MergeEvents; using StellaOps.Provenance.Mongo; namespace StellaOps.Concelier.Merge.Tests; diff --git a/src/Concelier/__Tests/StellaOps.Concelier.Merge.Tests/AliasGraphResolverTests.cs b/src/Concelier/__Tests/StellaOps.Concelier.Merge.Tests/AliasGraphResolverTests.cs index 1b3b6a7ce..c92127e43 100644 --- a/src/Concelier/__Tests/StellaOps.Concelier.Merge.Tests/AliasGraphResolverTests.cs +++ b/src/Concelier/__Tests/StellaOps.Concelier.Merge.Tests/AliasGraphResolverTests.cs @@ -1,34 +1,20 @@ -using System; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.Logging.Abstractions; -using MongoDB.Driver; -using StellaOps.Concelier.Merge.Services; -using StellaOps.Concelier.Storage.Mongo; -using StellaOps.Concelier.Storage.Mongo.Aliases; -using StellaOps.Concelier.Testing; - -namespace StellaOps.Concelier.Merge.Tests; - -[Collection("mongo-fixture")] -public sealed class AliasGraphResolverTests : IClassFixture -{ - private readonly MongoIntegrationFixture _fixture; - - public AliasGraphResolverTests(MongoIntegrationFixture fixture) - { - _fixture = fixture; - } - - [Fact] - public async Task ResolveAsync_ReturnsCollisions_WhenAliasesOverlap() - { - await DropAliasCollectionAsync(); - - var aliasStore = new AliasStore(_fixture.Database, NullLogger.Instance); - var resolver = new AliasGraphResolver(aliasStore); - - var timestamp = DateTimeOffset.UtcNow; +using System; +using System.Threading; +using System.Threading.Tasks; +using StellaOps.Concelier.Merge.Services; +using StellaOps.Concelier.Storage.Aliases; + +namespace StellaOps.Concelier.Merge.Tests; + +public sealed class AliasGraphResolverTests +{ + [Fact] + public async Task ResolveAsync_ReturnsCollisions_WhenAliasesOverlap() + { + var aliasStore = new AliasStore(); + var resolver = new AliasGraphResolver(aliasStore); + + var timestamp = DateTimeOffset.UtcNow; await aliasStore.ReplaceAsync( "ADV-1", new[] { new AliasEntry("CVE", "CVE-2025-2000"), new AliasEntry(AliasStoreConstants.PrimaryScheme, "ADV-1") }, @@ -50,13 +36,12 @@ public sealed class AliasGraphResolverTests : IClassFixture.Instance); - var resolver = new AliasGraphResolver(aliasStore); + + [Fact] + public async Task BuildComponentAsync_TracesConnectedAdvisories() + { + var aliasStore = new AliasStore(); + var resolver = new AliasGraphResolver(aliasStore); var timestamp = DateTimeOffset.UtcNow; await aliasStore.ReplaceAsync( @@ -83,28 +68,15 @@ public sealed class AliasGraphResolverTests : IClassFixture record.Scheme == "OSV" && record.Value == "OSV-2025-1"); - } - - private async Task DropAliasCollectionAsync() - { - try - { - await _fixture.Database.DropCollectionAsync(MongoStorageDefaults.Collections.Alias); - } - catch (MongoDB.Driver.MongoCommandException ex) when (ex.CodeName == "NamespaceNotFound" || ex.Message.Contains("ns not found", StringComparison.OrdinalIgnoreCase)) - { - } - } - - [Fact] - public async Task BuildComponentAsync_LinksOsvAndGhsaAliases() - { - await DropAliasCollectionAsync(); - - var aliasStore = new AliasStore(_fixture.Database, NullLogger.Instance); - var resolver = new AliasGraphResolver(aliasStore); - var timestamp = DateTimeOffset.UtcNow; + Assert.Contains(component.AliasMap["ADV-B"], record => record.Scheme == "OSV" && record.Value == "OSV-2025-1"); + } + + [Fact] + public async Task BuildComponentAsync_LinksOsvAndGhsaAliases() + { + var aliasStore = new AliasStore(); + var resolver = new AliasGraphResolver(aliasStore); + var timestamp = DateTimeOffset.UtcNow; await aliasStore.ReplaceAsync( "ADV-OSV", diff --git a/src/Concelier/__Tests/StellaOps.Concelier.Merge.Tests/MergeEventWriterTests.cs b/src/Concelier/__Tests/StellaOps.Concelier.Merge.Tests/MergeEventWriterTests.cs index 4577a2626..d30d13c1d 100644 --- a/src/Concelier/__Tests/StellaOps.Concelier.Merge.Tests/MergeEventWriterTests.cs +++ b/src/Concelier/__Tests/StellaOps.Concelier.Merge.Tests/MergeEventWriterTests.cs @@ -2,7 +2,7 @@ using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Time.Testing; using StellaOps.Concelier.Merge.Services; using StellaOps.Concelier.Models; -using StellaOps.Concelier.Storage.Mongo.MergeEvents; +using StellaOps.Concelier.Storage.MergeEvents; namespace StellaOps.Concelier.Merge.Tests; diff --git a/src/Concelier/__Tests/StellaOps.Concelier.Merge.Tests/MergePrecedenceIntegrationTests.cs b/src/Concelier/__Tests/StellaOps.Concelier.Merge.Tests/MergePrecedenceIntegrationTests.cs index 57b940829..2171cb1d9 100644 --- a/src/Concelier/__Tests/StellaOps.Concelier.Merge.Tests/MergePrecedenceIntegrationTests.cs +++ b/src/Concelier/__Tests/StellaOps.Concelier.Merge.Tests/MergePrecedenceIntegrationTests.cs @@ -1,30 +1,20 @@ using System; using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.Logging.Abstractions; -using Microsoft.Extensions.Time.Testing; -using MongoDB.Driver; -using StellaOps.Concelier.Merge.Services; -using StellaOps.Concelier.Models; -using StellaOps.Concelier.Storage.Mongo; -using StellaOps.Concelier.Storage.Mongo.MergeEvents; -using StellaOps.Concelier.Testing; - -namespace StellaOps.Concelier.Merge.Tests; - -[Collection("mongo-fixture")] -public sealed class MergePrecedenceIntegrationTests : IAsyncLifetime -{ - private readonly MongoIntegrationFixture _fixture; - private MergeEventStore? _mergeEventStore; - private MergeEventWriter? _mergeEventWriter; - private AdvisoryPrecedenceMerger? _merger; - private FakeTimeProvider? _timeProvider; - - public MergePrecedenceIntegrationTests(MongoIntegrationFixture fixture) - { - _fixture = fixture; - } +using System.Threading.Tasks; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Time.Testing; +using StellaOps.Concelier.Merge.Services; +using StellaOps.Concelier.Models; +using StellaOps.Concelier.Storage.MergeEvents; + +namespace StellaOps.Concelier.Merge.Tests; + +public sealed class MergePrecedenceIntegrationTests : IAsyncLifetime +{ + private MergeEventStore? _mergeEventStore; + private MergeEventWriter? _mergeEventWriter; + private AdvisoryPrecedenceMerger? _merger; + private FakeTimeProvider? _timeProvider; [Fact] public async Task MergePipeline_PsirtOverridesNvd_AndKevOnlyTogglesExploitKnown() @@ -82,29 +72,28 @@ public sealed class MergePrecedenceIntegrationTests : IAsyncLifetime AutoAdvanceAmount = TimeSpan.Zero, }; _merger = new AdvisoryPrecedenceMerger(new AffectedPackagePrecedenceResolver(), _timeProvider); - _mergeEventStore = new MergeEventStore(_fixture.Database, NullLogger.Instance); - _mergeEventWriter = new MergeEventWriter(_mergeEventStore, new CanonicalHashCalculator(), _timeProvider, NullLogger.Instance); - await DropMergeCollectionAsync(); - } + _mergeEventStore = new MergeEventStore(); + _mergeEventWriter = new MergeEventWriter(_mergeEventStore, new CanonicalHashCalculator(), _timeProvider, NullLogger.Instance); + } public Task DisposeAsync() => Task.CompletedTask; - private async Task EnsureInitializedAsync() - { - if (_mergeEventWriter is null) - { - await InitializeAsync(); - } - } + private async Task EnsureInitializedAsync() + { + if (_mergeEventWriter is null) + { + await InitializeAsync(); + } + } - private async Task DropMergeCollectionAsync() + private Task DropMergeCollectionAsync() { - try - { - await _fixture.Database.DropCollectionAsync(MongoStorageDefaults.Collections.MergeEvent); - } - catch (MongoCommandException ex) when (ex.CodeName == "NamespaceNotFound" || ex.Message.Contains("ns not found", StringComparison.OrdinalIgnoreCase)) - { + return Task.CompletedTask; + // { + // await _fixture.Database.DropCollectionAsync(MongoStorageDefaults.Collections.MergeEvent); + // } + // catch (MongoCommandException ex) when (ex.CodeName == "NamespaceNotFound" || ex.Message.Contains("ns not found", StringComparison.OrdinalIgnoreCase)) + // { // Collection has not been created yet – safe to ignore. } } diff --git a/src/Concelier/__Tests/StellaOps.Concelier.Storage.Postgres.Tests/AdvisoryConversionServiceTests.cs b/src/Concelier/__Tests/StellaOps.Concelier.Storage.Postgres.Tests/AdvisoryConversionServiceTests.cs index bdf3bd0bd..9ee2c99de 100644 --- a/src/Concelier/__Tests/StellaOps.Concelier.Storage.Postgres.Tests/AdvisoryConversionServiceTests.cs +++ b/src/Concelier/__Tests/StellaOps.Concelier.Storage.Postgres.Tests/AdvisoryConversionServiceTests.cs @@ -1,8 +1,8 @@ using FluentAssertions; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; -using MongoDB.Bson; -using StellaOps.Concelier.Storage.Mongo.Advisories; +using StellaOps.Concelier.Bson; +using StellaOps.Concelier.Storage.Advisories; using StellaOps.Concelier.Storage.Postgres; using StellaOps.Concelier.Storage.Postgres.Converters; using StellaOps.Concelier.Storage.Postgres.Repositories; diff --git a/src/Concelier/__Tests/StellaOps.Concelier.Storage.Postgres.Tests/AdvisoryConverterTests.cs b/src/Concelier/__Tests/StellaOps.Concelier.Storage.Postgres.Tests/AdvisoryConverterTests.cs index d38f7d5c1..8484db687 100644 --- a/src/Concelier/__Tests/StellaOps.Concelier.Storage.Postgres.Tests/AdvisoryConverterTests.cs +++ b/src/Concelier/__Tests/StellaOps.Concelier.Storage.Postgres.Tests/AdvisoryConverterTests.cs @@ -1,6 +1,6 @@ using FluentAssertions; -using MongoDB.Bson; -using StellaOps.Concelier.Storage.Mongo.Advisories; +using StellaOps.Concelier.Bson; +using StellaOps.Concelier.Storage.Advisories; using StellaOps.Concelier.Storage.Postgres.Converters; using Xunit; diff --git a/src/Concelier/__Tests/StellaOps.Concelier.Storage.Postgres.Tests/DualImportParityTests.cs b/src/Concelier/__Tests/StellaOps.Concelier.Storage.Postgres.Tests/DualImportParityTests.cs index 61fba295c..f43640100 100644 --- a/src/Concelier/__Tests/StellaOps.Concelier.Storage.Postgres.Tests/DualImportParityTests.cs +++ b/src/Concelier/__Tests/StellaOps.Concelier.Storage.Postgres.Tests/DualImportParityTests.cs @@ -1,9 +1,9 @@ using FluentAssertions; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; -using MongoDB.Bson; +using StellaOps.Concelier.Bson; using MongoDB.Driver; -using StellaOps.Concelier.Storage.Mongo.Advisories; +using StellaOps.Concelier.Storage.Advisories; using StellaOps.Concelier.Storage.Postgres; using StellaOps.Concelier.Storage.Postgres.Converters; using StellaOps.Concelier.Storage.Postgres.Converters.Importers; diff --git a/src/Concelier/__Tests/StellaOps.Concelier.Storage.Postgres.Tests/GhsaImporterMongoTests.cs b/src/Concelier/__Tests/StellaOps.Concelier.Storage.Postgres.Tests/GhsaImporterMongoTests.cs index 6a4ef0ea0..cffbcc7e5 100644 --- a/src/Concelier/__Tests/StellaOps.Concelier.Storage.Postgres.Tests/GhsaImporterMongoTests.cs +++ b/src/Concelier/__Tests/StellaOps.Concelier.Storage.Postgres.Tests/GhsaImporterMongoTests.cs @@ -1,9 +1,9 @@ using FluentAssertions; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; -using MongoDB.Bson; +using StellaOps.Concelier.Bson; using MongoDB.Driver; -using StellaOps.Concelier.Storage.Mongo.Advisories; +using StellaOps.Concelier.Storage.Advisories; using StellaOps.Concelier.Storage.Postgres; using StellaOps.Concelier.Storage.Postgres.Converters; using StellaOps.Concelier.Storage.Postgres.Converters.Importers; diff --git a/src/Concelier/__Tests/StellaOps.Concelier.Storage.Postgres.Tests/NvdImporterMongoTests.cs b/src/Concelier/__Tests/StellaOps.Concelier.Storage.Postgres.Tests/NvdImporterMongoTests.cs index c14c4f094..9ec4e10c7 100644 --- a/src/Concelier/__Tests/StellaOps.Concelier.Storage.Postgres.Tests/NvdImporterMongoTests.cs +++ b/src/Concelier/__Tests/StellaOps.Concelier.Storage.Postgres.Tests/NvdImporterMongoTests.cs @@ -1,9 +1,9 @@ using FluentAssertions; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; -using MongoDB.Bson; +using StellaOps.Concelier.Bson; using MongoDB.Driver; -using StellaOps.Concelier.Storage.Mongo.Advisories; +using StellaOps.Concelier.Storage.Advisories; using StellaOps.Concelier.Storage.Postgres; using StellaOps.Concelier.Storage.Postgres.Converters; using StellaOps.Concelier.Storage.Postgres.Converters.Importers; diff --git a/src/Concelier/__Tests/StellaOps.Concelier.Storage.Postgres.Tests/NvdImporterTests.cs b/src/Concelier/__Tests/StellaOps.Concelier.Storage.Postgres.Tests/NvdImporterTests.cs index 4cbf49476..3788bb9e8 100644 --- a/src/Concelier/__Tests/StellaOps.Concelier.Storage.Postgres.Tests/NvdImporterTests.cs +++ b/src/Concelier/__Tests/StellaOps.Concelier.Storage.Postgres.Tests/NvdImporterTests.cs @@ -1,10 +1,10 @@ using FluentAssertions; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; -using MongoDB.Bson; +using StellaOps.Concelier.Bson; using MongoDB.Driver; using MongoDB.Driver.Linq; -using StellaOps.Concelier.Storage.Mongo.Advisories; +using StellaOps.Concelier.Storage.Advisories; using StellaOps.Concelier.Storage.Postgres; using StellaOps.Concelier.Storage.Postgres.Converters; using StellaOps.Concelier.Storage.Postgres.Converters.Importers; diff --git a/src/Concelier/__Tests/StellaOps.Concelier.Storage.Postgres.Tests/OsvImporterMongoTests.cs b/src/Concelier/__Tests/StellaOps.Concelier.Storage.Postgres.Tests/OsvImporterMongoTests.cs index eb0bca8f4..443fb2c00 100644 --- a/src/Concelier/__Tests/StellaOps.Concelier.Storage.Postgres.Tests/OsvImporterMongoTests.cs +++ b/src/Concelier/__Tests/StellaOps.Concelier.Storage.Postgres.Tests/OsvImporterMongoTests.cs @@ -1,9 +1,9 @@ using FluentAssertions; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; -using MongoDB.Bson; +using StellaOps.Concelier.Bson; using MongoDB.Driver; -using StellaOps.Concelier.Storage.Mongo.Advisories; +using StellaOps.Concelier.Storage.Advisories; using StellaOps.Concelier.Storage.Postgres; using StellaOps.Concelier.Storage.Postgres.Converters; using StellaOps.Concelier.Storage.Postgres.Converters.Importers; diff --git a/src/Concelier/__Tests/StellaOps.Concelier.Storage.Postgres.Tests/Parity/DualBackendFixture.cs b/src/Concelier/__Tests/StellaOps.Concelier.Storage.Postgres.Tests/Parity/DualBackendFixture.cs index 617adbb19..38275f706 100644 --- a/src/Concelier/__Tests/StellaOps.Concelier.Storage.Postgres.Tests/Parity/DualBackendFixture.cs +++ b/src/Concelier/__Tests/StellaOps.Concelier.Storage.Postgres.Tests/Parity/DualBackendFixture.cs @@ -2,9 +2,9 @@ using System.Reflection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; -using StellaOps.Concelier.Storage.Mongo; -using StellaOps.Concelier.Storage.Mongo.Advisories; -using StellaOps.Concelier.Storage.Mongo.Aliases; +using StellaOps.Concelier.Storage; +using StellaOps.Concelier.Storage.Advisories; +using StellaOps.Concelier.Storage.Aliases; using StellaOps.Concelier.Storage.Postgres; using StellaOps.Concelier.Storage.Postgres.Advisories; using StellaOps.Concelier.Storage.Postgres.Repositories; @@ -26,7 +26,7 @@ namespace StellaOps.Concelier.Storage.Postgres.Tests.Parity; /// public sealed class DualBackendFixture : IAsyncLifetime { - private MongoIntegrationFixture? _mongoFixture; + private ConcelierPostgresFixture? _mongoFixture; private PostgreSqlContainer? _postgresContainer; private PostgresFixture? _postgresFixture; @@ -53,7 +53,7 @@ public sealed class DualBackendFixture : IAsyncLifetime /// /// Gets the MongoDB integration fixture for test cleanup. /// - public MongoIntegrationFixture MongoFixture => _mongoFixture + public ConcelierPostgresFixture MongoFixture => _mongoFixture ?? throw new InvalidOperationException("MongoDB fixture not initialized"); /// @@ -71,7 +71,7 @@ public sealed class DualBackendFixture : IAsyncLifetime public async Task InitializeAsync() { // Initialize MongoDB - _mongoFixture = new MongoIntegrationFixture(); + _mongoFixture = new ConcelierPostgresFixture(); await _mongoFixture.InitializeAsync(); var mongoOptions = Options.Create(new MongoStorageOptions()); diff --git a/src/Concelier/__Tests/StellaOps.Concelier.Storage.Postgres.Tests/ParityRunnerTests.cs b/src/Concelier/__Tests/StellaOps.Concelier.Storage.Postgres.Tests/ParityRunnerTests.cs index 73dfd3184..93e16246c 100644 --- a/src/Concelier/__Tests/StellaOps.Concelier.Storage.Postgres.Tests/ParityRunnerTests.cs +++ b/src/Concelier/__Tests/StellaOps.Concelier.Storage.Postgres.Tests/ParityRunnerTests.cs @@ -1,9 +1,9 @@ using FluentAssertions; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; -using MongoDB.Bson; +using StellaOps.Concelier.Bson; using MongoDB.Driver; -using StellaOps.Concelier.Storage.Mongo.Advisories; +using StellaOps.Concelier.Storage.Advisories; using StellaOps.Concelier.Storage.Postgres; using StellaOps.Concelier.Storage.Postgres.Converters; using StellaOps.Concelier.Storage.Postgres.Converters.Importers; diff --git a/src/Concelier/__Tests/StellaOps.Concelier.WebService.Tests/Fixtures/AdvisoryChunkSeedData.cs b/src/Concelier/__Tests/StellaOps.Concelier.WebService.Tests/Fixtures/AdvisoryChunkSeedData.cs index 8198f3235..bb6254085 100644 --- a/src/Concelier/__Tests/StellaOps.Concelier.WebService.Tests/Fixtures/AdvisoryChunkSeedData.cs +++ b/src/Concelier/__Tests/StellaOps.Concelier.WebService.Tests/Fixtures/AdvisoryChunkSeedData.cs @@ -1,6 +1,6 @@ using System.Collections.Immutable; using System.Text.Json; -using MongoDB.Bson.Serialization.Attributes; +using StellaOps.Concelier.Bson.Serialization.Attributes; using StellaOps.Concelier.RawModels; namespace StellaOps.Concelier.WebService.Tests.Fixtures; diff --git a/src/Concelier/__Tests/StellaOps.Concelier.WebService.Tests/LinksetTestFixtures.cs b/src/Concelier/__Tests/StellaOps.Concelier.WebService.Tests/LinksetTestFixtures.cs index 267ea9ca6..6b3613242 100644 --- a/src/Concelier/__Tests/StellaOps.Concelier.WebService.Tests/LinksetTestFixtures.cs +++ b/src/Concelier/__Tests/StellaOps.Concelier.WebService.Tests/LinksetTestFixtures.cs @@ -1,6 +1,6 @@ using System; using System.Collections.Generic; -using MongoDB.Bson.Serialization.Attributes; +using StellaOps.Concelier.Bson.Serialization.Attributes; namespace StellaOps.Concelier.WebService.Tests; diff --git a/src/Concelier/__Tests/StellaOps.Concelier.WebService.Tests/OrchestratorEndpointsTests.cs b/src/Concelier/__Tests/StellaOps.Concelier.WebService.Tests/OrchestratorEndpointsTests.cs index 556ce126a..a0bafa8d6 100644 --- a/src/Concelier/__Tests/StellaOps.Concelier.WebService.Tests/OrchestratorEndpointsTests.cs +++ b/src/Concelier/__Tests/StellaOps.Concelier.WebService.Tests/OrchestratorEndpointsTests.cs @@ -10,7 +10,7 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Options; -using StellaOps.Concelier.Storage.Mongo; +using StellaOps.Concelier.Storage; using StellaOps.Concelier.Core.Orchestration; using StellaOps.Concelier.WebService; using StellaOps.Concelier.WebService.Options; diff --git a/src/Concelier/__Tests/StellaOps.Concelier.WebService.Tests/WebServiceEndpointsTests.cs b/src/Concelier/__Tests/StellaOps.Concelier.WebService.Tests/WebServiceEndpointsTests.cs index 928672b6c..1f2c751f9 100644 --- a/src/Concelier/__Tests/StellaOps.Concelier.WebService.Tests/WebServiceEndpointsTests.cs +++ b/src/Concelier/__Tests/StellaOps.Concelier.WebService.Tests/WebServiceEndpointsTests.cs @@ -24,8 +24,8 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Mongo2Go; -using MongoDB.Bson; -using MongoDB.Bson.IO; +using StellaOps.Concelier.Bson; +using StellaOps.Concelier.Bson.IO; using MongoDB.Driver; using StellaOps.Concelier.Core.Attestation; using static StellaOps.Concelier.WebService.Program; @@ -33,10 +33,10 @@ using StellaOps.Concelier.Core.Events; using StellaOps.Concelier.Core.Jobs; using StellaOps.Concelier.Models; using StellaOps.Concelier.Merge.Services; -using StellaOps.Concelier.Storage.Mongo; -using StellaOps.Concelier.Storage.Mongo.Advisories; -using StellaOps.Concelier.Storage.Mongo.Observations; -using StellaOps.Concelier.Storage.Mongo.Linksets; +using StellaOps.Concelier.Storage; +using StellaOps.Concelier.Storage.Advisories; +using StellaOps.Concelier.Storage.Observations; +using StellaOps.Concelier.Storage.Linksets; using StellaOps.Concelier.Core.Raw; using StellaOps.Concelier.WebService.Jobs; using StellaOps.Concelier.WebService.Options; diff --git a/src/Directory.Build.props b/src/Directory.Build.props index e8271023e..d4a2e5013 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -32,35 +32,23 @@ false runtime - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + diff --git a/src/Web/StellaOps.Web/TASKS.md b/src/Web/StellaOps.Web/TASKS.md index 6beaa1a7a..79fc473ba 100644 --- a/src/Web/StellaOps.Web/TASKS.md +++ b/src/Web/StellaOps.Web/TASKS.md @@ -4,10 +4,21 @@ | --- | --- | --- | | WEB-AOC-19-002 | DONE (2025-11-30) | Added provenance builder, checksum utilities, and DSSE/CMS signature verification helpers with unit tests. | | WEB-AOC-19-003 | DONE (2025-11-30) | Added client-side guard validator (forbidden/derived/unknown fields, provenance/signature checks) with unit fixtures. | +| WEB-AIAI-31-001 | DONE (2025-12-12) | Advisory AI gateway contract + samples shipped (`docs/api/gateway/advisory-ai.md`); web SDK client added (`src/app/core/api/advisory-ai.client.ts`). | +| WEB-AIAI-31-002 | DONE (2025-12-12) | SSE job event streaming implemented + unit spec (`src/app/core/api/advisory-ai.client.spec.ts`). | +| WEB-AIAI-31-003 | DONE (2025-12-12) | Telemetry headers + prompt hash support; documented guardrail surface for audit visibility. | | WEB-CONSOLE-23-002 | DONE (2025-12-04) | console/status polling + run stream client/store/UI shipped; samples verified in `docs/api/console/samples/`. | | WEB-CONSOLE-23-003 | DONE (2025-12-07) | Exports client/store/service + models shipped; targeted Karma specs green locally with CHROME_BIN override (`node ./node_modules/@angular/cli/bin/ng.js test --watch=false --browsers=ChromeHeadless --include console-export specs`). Backend manifest/limits v0.4 published; awaiting final Policy/DevOps sign-off but UI/client slice complete. | | WEB-RISK-66-001 | BLOCKED (2025-12-03) | Same implementation landed; npm ci hangs so Angular tests can’t run; waiting on stable install environment and gateway endpoints to validate. | -| WEB-EXC-25-001 | BLOCKED (2025-12-06) | Pending exception schema + policy scopes/audit rules; cannot wire CRUD until contracts land. | +| WEB-EXC-25-001 | DONE (2025-12-12) | Exception contract + sample updated (`docs/api/console/exception-schema.md`); `ExceptionApiHttpClient` enforces scopes + trace/tenant headers with unit spec. | +| WEB-EXC-25-002 | DONE (2025-12-12) | Contract + samples in `docs/api/gateway/policy-exceptions.md`; client + unit spec in `src/Web/StellaOps.Web/src/app/core/api/policy-exceptions.client.ts`. | +| WEB-EXC-25-003 | DONE (2025-12-12) | Contract + samples in `docs/api/gateway/exception-events.md`; client + unit spec in `src/Web/StellaOps.Web/src/app/core/api/exception-events.client.ts`. | +| WEB-LNM-21-001 | DONE (2025-12-12) | Contract + samples in `docs/api/gateway/advisories.md`; client + unit spec in `src/Web/StellaOps.Web/src/app/core/api/advisories.client.ts`. | +| WEB-LNM-21-002 | DONE (2025-12-12) | Contract + samples in `docs/api/gateway/vex-evidence.md`; client + unit spec in `src/Web/StellaOps.Web/src/app/core/api/vex-evidence.client.ts`. | +| WEB-LNM-21-003 | DONE (2025-12-12) | Contract + sample in `docs/api/gateway/policy-evidence.md`; composition client + deterministic mock + unit spec in `src/Web/StellaOps.Web/src/app/core/api/policy-evidence.client.ts`. | +| WEB-ORCH-32-001 | DONE (2025-12-12) | Contract + sample in `docs/api/gateway/orchestrator.md`; web SDK client + deterministic mock + unit spec in `src/Web/StellaOps.Web/src/app/core/api/orchestrator.client.ts`. | +| WEB-ORCH-33-001 | DONE (2025-12-12) | Orchestrator control SDK shipped (`src/app/core/api/orchestrator-control.*`) with unit spec; gateway contract + samples updated in `docs/api/gateway/orchestrator.md`. | +| WEB-ORCH-34-001 | DONE (2025-12-12) | Quota/metrics surfaces documented + sampled (`docs/api/gateway/orchestrator.md` + samples) and covered by `OrchestratorControlHttpClient` unit spec. | | WEB-TEN-47-CONTRACT | DONE (2025-12-01) | Gateway tenant auth/ABAC contract doc v1.0 published (`docs/api/gateway/tenant-auth.md`). | | WEB-VULN-29-LEDGER-DOC | DONE (2025-12-01) | Findings Ledger proxy contract doc v1.0 with idempotency + retries (`docs/api/gateway/findings-ledger-proxy.md`). | | WEB-RISK-68-NOTIFY-DOC | DONE (2025-12-01) | Notifications severity transition event schema v1.0 published (`docs/api/gateway/notifications-severity.md`). | @@ -25,3 +36,8 @@ | UI-POLICY-23-006 | DONE (2025-12-06) | Explain view route `/policy-studio/packs/:packId/explain/:runId` with trace + JSON/PDF export (uses offline-safe jsPDF shim). | | UI-POLICY-23-001 | DONE (2025-12-05) | Workspace route `/policy-studio/packs` with pack list + quick actions; cached pack store with offline fallback. | | CVSS-UI-190-011 | DONE (2025-12-07) | Added CVSS receipt viewer route (/cvss/receipts/:receiptId) with score badge, tabbed sections, stub client, and unit spec in src/Web/StellaOps.Web. | +| UI-POLICY-27-001 | DONE (2025-12-12) | Policy Studio RBAC guards + nav gating aligned to `policy:author/review/approve/operate/audit/simulate`; auth fixtures/e2e aligned; `ng test` + `playwright test` green. | +| UI-SIG-26-001 | DONE (2025-12-12) | Vulnerability Explorer reachability column/filter/tooltips with deterministic stub data; hooks Why drawer. | +| UI-SIG-26-002 | DONE (2025-12-12) | Reachability Why drawer with deterministic call paths/timeline/evidence (MockSignalsClient). | +| UI-SIG-26-003 | DONE (2025-12-12) | SBOM Graph reachability halo overlay + time slider + legend (deterministic overlay state). | +| UI-SIG-26-004 | DONE (2025-12-12) | Reachability Center view (coverage/missing/stale) using deterministic fixture rows; swap to upstream datasets when published. | diff --git a/src/Web/StellaOps.Web/playwright.config.ts b/src/Web/StellaOps.Web/playwright.config.ts index 369053e2c..e606e4cde 100644 --- a/src/Web/StellaOps.Web/playwright.config.ts +++ b/src/Web/StellaOps.Web/playwright.config.ts @@ -1,17 +1,25 @@ -import { defineConfig } from '@playwright/test'; - -const port = process.env.PLAYWRIGHT_PORT - ? Number.parseInt(process.env.PLAYWRIGHT_PORT, 10) - : 4400; - -export default defineConfig({ - testDir: 'tests/e2e', - timeout: 30_000, - retries: process.env.CI ? 1 : 0, - use: { - baseURL: process.env.PLAYWRIGHT_BASE_URL ?? `http://127.0.0.1:${port}`, - trace: 'retain-on-failure', - }, +import { defineConfig } from '@playwright/test'; + +// Prefer the same offline-friendly Chromium resolution as Karma/unit tests. +// Falls back to Playwright-managed installs when not available. +// eslint-disable-next-line @typescript-eslint/no-var-requires +const { resolveChromeBinary } = require('./scripts/chrome-path'); + +const port = process.env.PLAYWRIGHT_PORT + ? Number.parseInt(process.env.PLAYWRIGHT_PORT, 10) + : 4400; + +const chromiumExecutable = resolveChromeBinary(__dirname) as string | null; + +export default defineConfig({ + testDir: 'tests/e2e', + timeout: 30_000, + retries: process.env.CI ? 1 : 0, + use: { + baseURL: process.env.PLAYWRIGHT_BASE_URL ?? `http://127.0.0.1:${port}`, + trace: 'retain-on-failure', + ...(chromiumExecutable ? { launchOptions: { executablePath: chromiumExecutable } } : {}), + }, webServer: { command: 'npm run serve:test', reuseExistingServer: !process.env.CI, diff --git a/src/Web/StellaOps.Web/src/app/app.component.html b/src/Web/StellaOps.Web/src/app/app.component.html index c3d0d4efa..9e682fc1c 100644 --- a/src/Web/StellaOps.Web/src/app/app.component.html +++ b/src/Web/StellaOps.Web/src/app/app.component.html @@ -27,39 +27,48 @@ Risk + + Vulnerabilities + + + SBOM Graph + + + Reachability Center +
diff --git a/src/Web/StellaOps.Web/src/app/features/console/console-status.component.html b/src/Web/StellaOps.Web/src/app/features/console/console-status.component.html index eece9f1ae..506c1e6e2 100644 --- a/src/Web/StellaOps.Web/src/app/features/console/console-status.component.html +++ b/src/Web/StellaOps.Web/src/app/features/console/console-status.component.html @@ -39,7 +39,7 @@

Run Stream

diff --git a/src/Web/StellaOps.Web/src/app/features/exceptions/exception-draft-inline.component.html b/src/Web/StellaOps.Web/src/app/features/exceptions/exception-draft-inline.component.html index f72c27122..acc5e84c7 100644 --- a/src/Web/StellaOps.Web/src/app/features/exceptions/exception-draft-inline.component.html +++ b/src/Web/StellaOps.Web/src/app/features/exceptions/exception-draft-inline.component.html @@ -20,27 +20,27 @@
Vulnerabilities:
- - {{ vulnId }} - - - +{{ context.vulnIds.length - 5 }} more - -
-
+ + {{ vulnId }} + + + +{{ (context.vulnIds?.length ?? 0) - 5 }} more + +
+
Components:
- - {{ purl | slice:-40 }} - - - +{{ context.componentPurls.length - 3 }} more - -
-
+ + {{ purl | slice:-40 }} + + + +{{ (context.componentPurls?.length ?? 0) - 3 }} more + + +
diff --git a/src/Web/StellaOps.Web/src/app/features/exceptions/exception-draft-inline.component.ts b/src/Web/StellaOps.Web/src/app/features/exceptions/exception-draft-inline.component.ts index fb8cfa93f..584217a1e 100644 --- a/src/Web/StellaOps.Web/src/app/features/exceptions/exception-draft-inline.component.ts +++ b/src/Web/StellaOps.Web/src/app/features/exceptions/exception-draft-inline.component.ts @@ -17,16 +17,15 @@ import { } from '@angular/forms'; import { firstValueFrom } from 'rxjs'; -import { - EXCEPTION_API, - ExceptionApi, - MockExceptionApiService, -} from '../../core/api/exception.client'; -import { - Exception, - ExceptionScope, - ExceptionSeverity, -} from '../../core/api/exception.models'; +import { + EXCEPTION_API, + ExceptionApi, +} from '../../core/api/exception.client'; +import { + Exception, + ExceptionSeverity, + ExceptionScopeType, +} from '../../core/api/exception.contract.models'; export interface ExceptionDraftContext { readonly vulnIds?: readonly string[]; @@ -57,14 +56,11 @@ const SEVERITY_OPTIONS: readonly { value: ExceptionSeverity; label: string }[] = selector: 'app-exception-draft-inline', standalone: true, imports: [CommonModule, ReactiveFormsModule], - templateUrl: './exception-draft-inline.component.html', - styleUrls: ['./exception-draft-inline.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush, - providers: [ - { provide: EXCEPTION_API, useClass: MockExceptionApiService }, - ], -}) -export class ExceptionDraftInlineComponent implements OnInit { + templateUrl: './exception-draft-inline.component.html', + styleUrls: ['./exception-draft-inline.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class ExceptionDraftInlineComponent implements OnInit { private readonly api = inject(EXCEPTION_API); private readonly formBuilder = inject(NonNullableFormBuilder); @@ -92,12 +88,12 @@ export class ExceptionDraftInlineComponent implements OnInit { timeboxDays: this.formBuilder.control(30), }); - readonly scopeType = computed(() => { - if (this.context?.componentPurls?.length) return 'component'; - if (this.context?.assetIds?.length) return 'asset'; - if (this.context?.tenantId) return 'tenant'; - return 'global'; - }); + readonly scopeType = computed(() => { + if (this.context?.componentPurls?.length) return 'component'; + if (this.context?.assetIds?.length) return 'asset'; + if (this.context?.tenantId) return 'tenant'; + return 'global'; + }); readonly scopeSummary = computed(() => { const ctx = this.context; diff --git a/src/Web/StellaOps.Web/src/app/features/graph/graph-canvas.component.ts b/src/Web/StellaOps.Web/src/app/features/graph/graph-canvas.component.ts index 564b68175..4bf1a62e8 100644 --- a/src/Web/StellaOps.Web/src/app/features/graph/graph-canvas.component.ts +++ b/src/Web/StellaOps.Web/src/app/features/graph/graph-canvas.component.ts @@ -16,6 +16,8 @@ import { signal, } from '@angular/core'; +import type { GraphOverlayState, ReachabilityOverlayData } from './graph-overlays.component'; + export interface CanvasNode { readonly id: string; readonly type: 'asset' | 'component' | 'vulnerability'; @@ -229,7 +231,7 @@ const VIEWPORT_PADDING = 100; @for (node of visibleNodes(); track node.id) { + @if (getReachabilityData(node.id); as reach) { + + {{ reach.status }} ({{ (reach.confidence * 100).toFixed(0) }}%) · {{ reach.observedAt }} + + } + (); @Output() canvasClicked = new EventEmitter(); @@ -1153,6 +1178,22 @@ export class GraphCanvasComponent implements OnChanges, AfterViewInit, OnDestroy } } + getReachabilityData(nodeId: string): ReachabilityOverlayData | null { + if (!this.overlayState) return null; + return this.overlayState.reachability.get(nodeId) ?? null; + } + + getReachabilityHaloStroke(status: ReachabilityOverlayData['status']): string { + switch (status) { + case 'reachable': + return '#22c55e'; + case 'unreachable': + return '#94a3b8'; + default: + return '#f59e0b'; + } + } + getMinimapNodeColor(node: LayoutNode): string { switch (node.type) { case 'asset': return '#3b82f6'; diff --git a/src/Web/StellaOps.Web/src/app/features/graph/graph-explorer.component.html b/src/Web/StellaOps.Web/src/app/features/graph/graph-explorer.component.html index 867506767..33209a35a 100644 --- a/src/Web/StellaOps.Web/src/app/features/graph/graph-explorer.component.html +++ b/src/Web/StellaOps.Web/src/app/features/graph/graph-explorer.component.html @@ -108,14 +108,15 @@
- -
+ +
; evidence: Map; license: Map; exposure: Map; + reachability: Map; } // Mock overlay data generators +function stableHash(input: string): number { + let hash = 2166136261; + for (let i = 0; i < input.length; i++) { + hash ^= input.charCodeAt(i); + hash = Math.imul(hash, 16777619); + } + return hash >>> 0; +} + +function fraction(hash: number): number { + return (hash % 1000) / 999; +} + function generateMockPolicyData(nodeIds: string[]): Map { const data = new Map(); const statuses: PolicyOverlayData['policyStatus'][] = ['pass', 'warn', 'block', 'unknown']; for (const nodeId of nodeIds) { - const status = statuses[Math.floor(Math.random() * statuses.length)]; + const hash = stableHash(`policy:${nodeId}`); + const status = statuses[hash % statuses.length]; + const policyNumber = hash % 100; data.set(nodeId, { nodeId, policyStatus: status, - policyName: status === 'pass' ? undefined : `policy-${Math.floor(Math.random() * 100)}`, + policyName: status === 'pass' ? undefined : `policy-${policyNumber}`, violations: status === 'block' ? ['Vulnerable dependency detected', 'Missing attestation'] : undefined, gateBlocked: status === 'block', }); @@ -83,12 +106,13 @@ function generateMockEvidenceData(nodeIds: string[]): Map 0.3; + const hash = stableHash(`evidence:${nodeId}`); + const hasEvidence = (hash % 10) >= 3; data.set(nodeId, { nodeId, hasEvidence, - evidenceType: hasEvidence ? types[Math.floor(Math.random() * types.length)] : undefined, - confidence: hasEvidence ? Math.floor(Math.random() * 40) + 60 : undefined, + evidenceType: hasEvidence ? types[hash % types.length] : undefined, + confidence: hasEvidence ? Math.round(60 + fraction(hash) * 40) : undefined, sources: hasEvidence ? ['scanner', 'registry'] : undefined, }); } @@ -98,13 +122,13 @@ function generateMockEvidenceData(nodeIds: string[]): Map { const data = new Map(); const licenses = ['MIT', 'Apache-2.0', 'GPL-3.0', 'BSD-3-Clause', 'LGPL-2.1', 'Proprietary']; - const families: LicenseOverlayData['licenseFamily'][] = ['permissive', 'copyleft', 'proprietary', 'unknown']; for (const nodeId of nodeIds) { - const license = licenses[Math.floor(Math.random() * licenses.length)]; + const hash = stableHash(`license:${nodeId}`); + const license = licenses[hash % licenses.length]; const family = license.includes('GPL') ? 'copyleft' : license === 'Proprietary' ? 'proprietary' : 'permissive'; - const compatible = family !== 'copyleft' || Math.random() > 0.5; + const compatible = family !== 'copyleft' || (hash % 2) === 0; data.set(nodeId, { nodeId, @@ -122,19 +146,60 @@ function generateMockExposureData(nodeIds: string[]): Map { + const data = new Map(); + const snapshotDays: Record = { current: 0, '1d': 1, '7d': 7, '30d': 30 }; + const days = snapshotDays[snapshot] ?? 0; + const base = Date.parse('2025-12-12T00:00:00Z'); + const observedAt = new Date(base - days * 24 * 60 * 60 * 1000).toISOString(); + + for (const nodeId of nodeIds) { + const normalized = nodeId.toLowerCase(); + let status: ReachabilityOverlayData['status'] = 'unknown'; + let confidence = 0.0; + + if (normalized.includes('log4j') || normalized.includes('log4shell')) { + status = 'unreachable'; + confidence = 0.95; + } else if ( + normalized.includes('curl') || + normalized.includes('nghttp2') || + normalized.includes('golang') || + normalized.includes('jwt') || + normalized.includes('jsonwebtoken') + ) { + status = 'reachable'; + confidence = 0.88; + } else if (normalized.includes('spring')) { + status = 'reachable'; + confidence = 0.6; + } + + data.set(nodeId, { + nodeId, + status, + confidence, + observedAt, + }); + } + + return data; +} + @Component({ selector: 'app-graph-overlays', standalone: true, @@ -263,6 +328,26 @@ function generateMockExposureData(nodeIds: string[]): Map
} + + @if (isOverlayEnabled('reachability')) { +
+

Reachability

+
+
+ + Reachable +
+
+ + Unreachable +
+
+ + Unknown +
+
+
+ } } @@ -363,6 +448,29 @@ function generateMockExposureData(nodeIds: string[]): Map } + + @if (isOverlayEnabled('reachability') && getReachabilityData(selectedNodeId)) { +
+
+ + Reachability + + {{ getReachabilityData(selectedNodeId)!.status }} + +
+
+ Confidence: {{ (getReachabilityData(selectedNodeId)!.confidence * 100).toFixed(0) }}% +
+
+ Observed: {{ getReachabilityData(selectedNodeId)!.observedAt }} +
+
+ } } @@ -419,15 +527,27 @@ function generateMockExposureData(nodeIds: string[]): Map ⏱️ Time Travel - @if (timeTravelEnabled()) { + @if (timeTravelEnabled() || isOverlayEnabled('reachability')) {
+
+ + {{ snapshotLabel() }} +
- + @if (timeTravelEnabled()) { + + }
} @@ -614,6 +736,10 @@ function generateMockExposureData(nodeIds: string[]): Map('shortest'); readonly timeTravelEnabled = signal(false); readonly selectedSnapshot = signal('current'); + private readonly snapshotOrder = ['current', '1d', '7d', '30d'] as const; + + readonly snapshotIndex = computed(() => { + const idx = this.snapshotOrder.indexOf(this.selectedSnapshot() as (typeof this.snapshotOrder)[number]); + return idx === -1 ? 0 : idx; + }); + + readonly snapshotLabel = computed(() => { + switch (this.selectedSnapshot()) { + case '1d': + return '1 day ago'; + case '7d': + return '7 days ago'; + case '30d': + return '30 days ago'; + default: + return 'Current'; + } + }); // Computed readonly hasActiveOverlays = computed(() => @@ -910,6 +1073,7 @@ export class GraphOverlaysComponent implements OnChanges { ngOnChanges(changes: SimpleChanges): void { if (changes['nodeIds']) { this.regenerateOverlayData(); + this.overlayStateChange.emit(this.overlayState()); } } @@ -920,9 +1084,11 @@ export class GraphOverlaysComponent implements OnChanges { ); this.overlayConfigs.set(updated); - // Regenerate data for newly enabled overlay - if (updated.find(c => c.type === type)?.enabled) { + const enabled = updated.find(c => c.type === type)?.enabled ?? false; + if (enabled) { this.regenerateOverlayDataForType(type); + } else { + this.clearOverlayDataForType(type); } this.overlayStateChange.emit(this.overlayState()); @@ -967,6 +1133,16 @@ export class GraphOverlaysComponent implements OnChanges { enabled: this.timeTravelEnabled(), snapshot, }); + + if (this.isOverlayEnabled('reachability')) { + this.regenerateOverlayDataForType('reachability'); + this.overlayStateChange.emit(this.overlayState()); + } + } + + setSnapshotByIndex(index: number): void { + const clamped = Math.max(0, Math.min(index, this.snapshotOrder.length - 1)); + this.setSnapshot(this.snapshotOrder[clamped]); } showDiff(): void { @@ -990,13 +1166,21 @@ export class GraphOverlaysComponent implements OnChanges { return this.overlayState().exposure.get(nodeId); } + getReachabilityData(nodeId: string): ReachabilityOverlayData | undefined { + return this.overlayState().reachability.get(nodeId); + } + private regenerateOverlayData(): void { const nodeIds = this.nodeIds; + const enabled = new Set(this.overlayConfigs().filter((c) => c.enabled).map((c) => c.type)); const state: GraphOverlayState = { - policy: generateMockPolicyData(nodeIds), - evidence: generateMockEvidenceData(nodeIds), - license: generateMockLicenseData(nodeIds), - exposure: generateMockExposureData(nodeIds), + policy: enabled.has('policy') ? generateMockPolicyData(nodeIds) : new Map(), + evidence: enabled.has('evidence') ? generateMockEvidenceData(nodeIds) : new Map(), + license: enabled.has('license') ? generateMockLicenseData(nodeIds) : new Map(), + exposure: enabled.has('exposure') ? generateMockExposureData(nodeIds) : new Map(), + reachability: enabled.has('reachability') + ? generateMockReachabilityData(nodeIds, this.selectedSnapshot()) + : new Map(), }; this.overlayState.set(state); } @@ -1018,6 +1202,31 @@ export class GraphOverlaysComponent implements OnChanges { case 'exposure': this.overlayState.set({ ...current, exposure: generateMockExposureData(nodeIds) }); break; + case 'reachability': + this.overlayState.set({ ...current, reachability: generateMockReachabilityData(nodeIds, this.selectedSnapshot()) }); + break; + } + } + + private clearOverlayDataForType(type: OverlayType): void { + const current = this.overlayState(); + + switch (type) { + case 'policy': + this.overlayState.set({ ...current, policy: new Map() }); + break; + case 'evidence': + this.overlayState.set({ ...current, evidence: new Map() }); + break; + case 'license': + this.overlayState.set({ ...current, license: new Map() }); + break; + case 'exposure': + this.overlayState.set({ ...current, exposure: new Map() }); + break; + case 'reachability': + this.overlayState.set({ ...current, reachability: new Map() }); + break; } } } diff --git a/src/Web/StellaOps.Web/src/app/features/policy-studio/workspace/policy-workspace.component.spec.ts b/src/Web/StellaOps.Web/src/app/features/policy-studio/workspace/policy-workspace.component.spec.ts index aca889505..926eda70e 100644 --- a/src/Web/StellaOps.Web/src/app/features/policy-studio/workspace/policy-workspace.component.spec.ts +++ b/src/Web/StellaOps.Web/src/app/features/policy-studio/workspace/policy-workspace.component.spec.ts @@ -11,6 +11,7 @@ describe('PolicyWorkspaceComponent', () => { let fixture: ComponentFixture; let component: PolicyWorkspaceComponent; let store: jasmine.SpyObj; + let authStub: AuthService; beforeEach(async () => { store = jasmine.createSpyObj('PolicyPackStore', ['getPacks']); @@ -43,11 +44,19 @@ describe('PolicyWorkspaceComponent', () => { ]) ); + authStub = { + canViewPolicies: () => true, + canAuthorPolicies: () => true, + canSimulatePolicies: () => true, + canReviewPolicies: () => true, + canApprovePolicies: () => false, + } as AuthService; + await TestBed.configureTestingModule({ imports: [CommonModule, RouterLink, PolicyWorkspaceComponent], providers: [ { provide: PolicyPackStore, useValue: store }, - { provide: AUTH_SERVICE, useValue: { canViewPolicies: () => true, canAuthorPolicies: () => true, canSimulatePolicies: () => true, canReviewPolicies: () => true } as AuthService }, + { provide: AUTH_SERVICE, useValue: authStub }, { provide: ActivatedRoute, useValue: { @@ -67,4 +76,20 @@ describe('PolicyWorkspaceComponent', () => { expect(component['packs'][0].id).toBe('pack-b'); expect(component['packs'][1].id).toBe('pack-a'); })); + + it('enables approvals when approve scope present', fakeAsync(() => { + authStub.canAuthorPolicies = () => false; + authStub.canSimulatePolicies = () => false; + authStub.canReviewPolicies = () => false; + authStub.canApprovePolicies = () => true; + + const localFixture = TestBed.createComponent(PolicyWorkspaceComponent); + localFixture.detectChanges(); + tick(); + + const el = localFixture.nativeElement as HTMLElement; + const approvals = Array.from(el.querySelectorAll('a')).find((a) => a.textContent?.trim() === 'Approvals'); + expect(approvals).toBeTruthy(); + expect(approvals!.classList.contains('action-disabled')).toBeFalse(); + })); }); diff --git a/src/Web/StellaOps.Web/src/app/features/policy-studio/workspace/policy-workspace.component.ts b/src/Web/StellaOps.Web/src/app/features/policy-studio/workspace/policy-workspace.component.ts index 14a2bf885..65856f431 100644 --- a/src/Web/StellaOps.Web/src/app/features/policy-studio/workspace/policy-workspace.component.ts +++ b/src/Web/StellaOps.Web/src/app/features/policy-studio/workspace/policy-workspace.component.ts @@ -62,9 +62,9 @@ import { PolicyPackStore } from '../services/policy-pack.store'; Approvals @@ -136,6 +136,7 @@ export class PolicyWorkspaceComponent { protected canApprove = false; protected canOperate = false; protected canAudit = false; + protected canReviewOrApprove = false; protected canView = false; protected scopeHint = ''; protected refreshing = false; @@ -171,6 +172,7 @@ export class PolicyWorkspaceComponent { this.canReview = this.auth.canReviewPolicies?.() ?? false; this.canView = this.auth.canViewPolicies?.() ?? false; this.canApprove = this.auth.canApprovePolicies?.() ?? false; + this.canReviewOrApprove = this.canReview || this.canApprove; this.canOperate = this.auth.canOperatePolicies?.() ?? false; this.canAudit = this.auth.canAuditPolicies?.() ?? false; const missing: string[] = []; diff --git a/src/Web/StellaOps.Web/src/app/features/reachability/reachability-center.component.spec.ts b/src/Web/StellaOps.Web/src/app/features/reachability/reachability-center.component.spec.ts new file mode 100644 index 000000000..04d976700 --- /dev/null +++ b/src/Web/StellaOps.Web/src/app/features/reachability/reachability-center.component.spec.ts @@ -0,0 +1,29 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ReachabilityCenterComponent } from './reachability-center.component'; + +describe('ReachabilityCenterComponent', () => { + let fixture: ComponentFixture; + let component: ReachabilityCenterComponent; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ReachabilityCenterComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(ReachabilityCenterComponent); + component = fixture.componentInstance; + }); + + it('computes summary counts deterministically', () => { + expect(component.okCount()).toBe(1); + expect(component.staleCount()).toBe(1); + expect(component.missingCount()).toBe(1); + }); + + it('filters rows by status', () => { + component.setStatusFilter('stale'); + expect(component.filteredRows().map((r) => r.assetId)).toEqual(['asset-api-prod']); + }); +}); + diff --git a/src/Web/StellaOps.Web/src/app/features/reachability/reachability-center.component.ts b/src/Web/StellaOps.Web/src/app/features/reachability/reachability-center.component.ts new file mode 100644 index 000000000..5f0a147d9 --- /dev/null +++ b/src/Web/StellaOps.Web/src/app/features/reachability/reachability-center.component.ts @@ -0,0 +1,329 @@ +import { CommonModule } from '@angular/common'; +import { ChangeDetectionStrategy, Component, computed, signal } from '@angular/core'; + +type CoverageStatus = 'ok' | 'stale' | 'missing'; + +interface ReachabilityCoverageRow { + readonly assetId: string; + readonly coveragePercent: number; + readonly sensorsOnline: number; + readonly sensorsExpected: number; + readonly lastFactAt: string | null; + readonly status: CoverageStatus; +} + +const FIXTURE_ROWS: ReachabilityCoverageRow[] = [ + { + assetId: 'asset-api-prod', + coveragePercent: 75, + sensorsOnline: 2, + sensorsExpected: 3, + lastFactAt: '2025-12-01T06:10:00Z', + status: 'stale', + }, + { + assetId: 'asset-web-prod', + coveragePercent: 92, + sensorsOnline: 3, + sensorsExpected: 3, + lastFactAt: '2025-12-11T09:20:00Z', + status: 'ok', + }, + { + assetId: 'asset-worker-prod', + coveragePercent: 40, + sensorsOnline: 0, + sensorsExpected: 2, + lastFactAt: null, + status: 'missing', + }, +]; + +@Component({ + selector: 'app-reachability-center', + standalone: true, + imports: [CommonModule], + changeDetection: ChangeDetectionStrategy.OnPush, + template: ` +
+
+
+

Signals · Reachability

+

Reachability Center

+

+ Coverage-first view: what we observe, what is missing, and what is stale. +

+
+ +
+ +
+
+
{{ okCount() }}
+
Healthy assets
+
+
+
{{ staleCount() }}
+
Stale facts
+
+
+
{{ missingCount() }}
+
Missing sensors
+
+
+ +
+ + + + +
+ +
+ + + + + + + + + + + + @for (row of filteredRows(); track row.assetId) { + + + + + + + + } + +
AssetCoverageSensorsLast factStatus
{{ row.assetId }}{{ row.coveragePercent }}%{{ row.sensorsOnline }}/{{ row.sensorsExpected }}{{ row.lastFactAt ?? '—' }} + + {{ row.status }} + +
+
+
+ `, + styles: [ + ` + :host { + display: block; + min-height: 100vh; + background: #0b1224; + color: #e5e7eb; + } + + .reachability { + max-width: 1100px; + margin: 0 auto; + padding: 1.5rem; + display: grid; + gap: 1rem; + } + + .reachability__header { + display: flex; + justify-content: space-between; + gap: 1rem; + align-items: flex-start; + } + + .reachability__eyebrow { + margin: 0; + color: #22d3ee; + text-transform: uppercase; + letter-spacing: 0.06em; + font-size: 0.8rem; + } + + h1 { + margin: 0.25rem 0 0; + font-size: 1.5rem; + } + + .reachability__subtitle { + margin: 0.25rem 0 0; + color: #94a3b8; + } + + .btn { + border: 1px solid #334155; + background: transparent; + color: #e5e7eb; + border-radius: 10px; + padding: 0.5rem 0.8rem; + cursor: pointer; + } + + .reachability__summary { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 0.75rem; + } + + .summary-card { + border: 1px solid #1f2937; + background: #0f172a; + border-radius: 14px; + padding: 0.9rem 1rem; + display: grid; + gap: 0.25rem; + } + + .summary-card__value { + font-size: 1.5rem; + font-weight: 700; + } + + .summary-card__label { + color: #94a3b8; + font-size: 0.85rem; + } + + .summary-card--warn .summary-card__value { + color: #f59e0b; + } + + .summary-card--danger .summary-card__value { + color: #ef4444; + } + + .reachability__filters { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; + } + + .pill { + border: 1px solid #334155; + background: #0f172a; + color: #e5e7eb; + border-radius: 999px; + padding: 0.35rem 0.75rem; + cursor: pointer; + } + + .pill--active { + border-color: #22d3ee; + color: #22d3ee; + } + + .reachability__table { + border: 1px solid #1f2937; + background: #0f172a; + border-radius: 14px; + overflow: hidden; + } + + table { + width: 100%; + border-collapse: collapse; + } + + th, + td { + padding: 0.75rem 0.9rem; + border-bottom: 1px solid #1f2937; + text-align: left; + font-size: 0.9rem; + } + + th { + color: #94a3b8; + font-size: 0.75rem; + text-transform: uppercase; + letter-spacing: 0.06em; + } + + code { + font-family: ui-monospace, monospace; + color: #e2e8f0; + } + + .status { + display: inline-flex; + padding: 0.2rem 0.55rem; + border-radius: 999px; + font-size: 0.75rem; + border: 1px solid #334155; + text-transform: uppercase; + letter-spacing: 0.04em; + } + + .status--ok { + border-color: #14532d; + color: #22c55e; + } + + .status--stale { + border-color: #92400e; + color: #f59e0b; + } + + .status--missing { + border-color: #991b1b; + color: #ef4444; + } + `, + ], +}) +export class ReachabilityCenterComponent { + readonly statusFilter = signal('all'); + + readonly rows = signal( + [...FIXTURE_ROWS].sort((a, b) => a.assetId.localeCompare(b.assetId)) + ); + + readonly filteredRows = computed(() => { + const status = this.statusFilter(); + const rows = this.rows(); + if (status === 'all') return rows; + return rows.filter((r) => r.status === status); + }); + + readonly okCount = computed(() => this.rows().filter((r) => r.status === 'ok').length); + readonly staleCount = computed(() => this.rows().filter((r) => r.status === 'stale').length); + readonly missingCount = computed(() => this.rows().filter((r) => r.status === 'missing').length); + + setStatusFilter(status: CoverageStatus | 'all'): void { + this.statusFilter.set(status); + } + + reset(): void { + this.statusFilter.set('all'); + } +} + diff --git a/src/Web/StellaOps.Web/src/app/features/reachability/reachability-why-drawer.component.spec.ts b/src/Web/StellaOps.Web/src/app/features/reachability/reachability-why-drawer.component.spec.ts new file mode 100644 index 000000000..58b173efc --- /dev/null +++ b/src/Web/StellaOps.Web/src/app/features/reachability/reachability-why-drawer.component.spec.ts @@ -0,0 +1,87 @@ +import { ComponentFixture, TestBed, fakeAsync, flushMicrotasks } from '@angular/core/testing'; +import { of } from 'rxjs'; + +import { MockSignalsClient } from '../../core/api/signals.client'; +import { ReachabilityWhyDrawerComponent } from './reachability-why-drawer.component'; + +describe('ReachabilityWhyDrawerComponent', () => { + let fixture: ComponentFixture; + let signals: jasmine.SpyObj; + + beforeEach(async () => { + signals = jasmine.createSpyObj('MockSignalsClient', ['getFacts', 'getCallGraphs']); + + signals.getFacts.and.returnValue( + of({ + tenantId: 'tenant-default', + facts: [ + { + id: 'fact-1', + type: 'reachability', + assetId: 'asset-1', + component: 'pkg:npm/jsonwebtoken@9.0.2', + status: 'reachable', + confidence: 0.88, + observedAt: '2025-12-05T10:10:00Z', + signalsVersion: 'signals-2025.310.1', + evidenceTraceIds: ['trace-abc'], + }, + ], + pagination: { nextPageToken: null }, + etag: '"etag-1"', + traceId: 'trace-req-1', + }) + ); + + signals.getCallGraphs.and.returnValue( + of({ + tenantId: 'tenant-default', + assetId: 'asset-1', + paths: [ + { + id: 'path-1', + source: 'api-gateway', + target: 'jwt-auth-service', + hops: [ + { service: 'api-gateway', endpoint: '/login', timestamp: '2025-12-05T10:00:00Z' }, + { service: 'jwt-auth-service', endpoint: '/verify', timestamp: '2025-12-05T10:00:01Z' }, + ], + evidence: { traceId: 'trace-abc', spanCount: 2, score: 0.92 }, + lastObserved: '2025-12-05T10:00:01Z', + }, + ], + pagination: { nextPageToken: null }, + etag: '"etag-2"', + traceId: 'trace-req-2', + }) + ); + + await TestBed.configureTestingModule({ + imports: [ReachabilityWhyDrawerComponent], + providers: [{ provide: MockSignalsClient, useValue: signals }], + }).compileComponents(); + + fixture = TestBed.createComponent(ReachabilityWhyDrawerComponent); + }); + + it('loads evidence and renders trace ids', fakeAsync(() => { + fixture.componentRef.setInput('open', true); + fixture.componentRef.setInput('status', 'reachable'); + fixture.componentRef.setInput('confidence', 0.88); + fixture.componentRef.setInput('component', 'pkg:npm/jsonwebtoken@9.0.2'); + fixture.componentRef.setInput('assetId', 'asset-1'); + fixture.detectChanges(); + + flushMicrotasks(); + fixture.detectChanges(); + + expect(signals.getFacts).toHaveBeenCalled(); + expect(signals.getCallGraphs).toHaveBeenCalled(); + + const el = fixture.nativeElement as HTMLElement; + expect(el.textContent).toContain('Call paths'); + expect(el.textContent).toContain('api-gateway'); + expect(el.textContent).toContain('trace-abc'); + })); +}); + diff --git a/src/Web/StellaOps.Web/src/app/features/reachability/reachability-why-drawer.component.ts b/src/Web/StellaOps.Web/src/app/features/reachability/reachability-why-drawer.component.ts new file mode 100644 index 000000000..50a6efc13 --- /dev/null +++ b/src/Web/StellaOps.Web/src/app/features/reachability/reachability-why-drawer.component.ts @@ -0,0 +1,423 @@ +import { CommonModule } from '@angular/common'; +import { + ChangeDetectionStrategy, + Component, + EventEmitter, + Output, + computed, + effect, + inject, + input, + signal, +} from '@angular/core'; +import { firstValueFrom } from 'rxjs'; + +import { MockSignalsClient, type CallGraphPath } from '../../core/api/signals.client'; + +type ReachabilityStatus = 'reachable' | 'unreachable' | 'unknown'; + +@Component({ + selector: 'app-reachability-why-drawer', + standalone: true, + imports: [CommonModule], + changeDetection: ChangeDetectionStrategy.OnPush, + template: ` + @if (open()) { +
+
+ +
+ } + `, + styles: [ + ` + .why-drawer { + position: fixed; + inset: 0; + z-index: 210; + display: grid; + grid-template-columns: 1fr min(520px, 100%); + } + + .why-drawer__backdrop { + background: rgba(15, 23, 42, 0.65); + backdrop-filter: blur(2px); + } + + .why-drawer__panel { + background: #0b1224; + color: #e5e7eb; + border-left: 1px solid #1f2937; + display: grid; + grid-template-rows: auto 1fr; + overflow: hidden; + } + + .why-drawer__header { + padding: 1rem 1.25rem; + border-bottom: 1px solid #1f2937; + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 1rem; + } + + .why-drawer__eyebrow { + margin: 0; + font-size: 0.75rem; + color: #22d3ee; + text-transform: uppercase; + letter-spacing: 0.06em; + } + + .why-drawer__title { + margin: 0.25rem 0 0; + font-size: 1.25rem; + } + + .why-drawer__confidence { + margin-left: 0.5rem; + font-size: 0.85rem; + color: #94a3b8; + } + + .why-drawer__subtitle { + margin: 0.25rem 0 0; + font-size: 0.75rem; + color: #94a3b8; + word-break: break-all; + } + + .why-drawer__close { + background: transparent; + color: #e5e7eb; + border: 1px solid #334155; + border-radius: 8px; + padding: 0.35rem 0.65rem; + cursor: pointer; + } + + .why-drawer__body { + padding: 1rem 1.25rem; + overflow: auto; + } + + .why-drawer__section + .why-drawer__section { + margin-top: 1.25rem; + } + + h3 { + margin: 0 0 0.5rem; + font-size: 0.9rem; + color: #e2e8f0; + } + + .muted { + margin: 0; + color: #94a3b8; + font-size: 0.85rem; + } + + .why-drawer__loading, + .why-drawer__error, + .why-drawer__empty { + padding: 0.75rem 1rem; + border: 1px solid #334155; + border-radius: 10px; + background: #0f172a; + } + + .timeline { + list-style: none; + padding: 0; + margin: 0; + display: grid; + gap: 0.5rem; + } + + .timeline li { + display: grid; + gap: 0.15rem; + padding: 0.6rem 0.75rem; + border: 1px solid #1f2937; + border-radius: 10px; + background: #0f172a; + } + + .timeline__when { + font-size: 0.75rem; + color: #94a3b8; + font-family: ui-monospace, monospace; + } + + .timeline__what { + font-size: 0.875rem; + color: #e5e7eb; + } + + .paths { + display: grid; + gap: 0.75rem; + } + + .path-card { + border: 1px solid #1f2937; + background: #0f172a; + border-radius: 12px; + padding: 0.75rem 0.9rem; + } + + .path-card__header { + display: flex; + justify-content: space-between; + gap: 0.75rem; + align-items: baseline; + margin-bottom: 0.5rem; + } + + .path-card__title { + font-weight: 600; + } + + .path-card__meta { + display: flex; + gap: 0.75rem; + font-size: 0.75rem; + color: #94a3b8; + font-family: ui-monospace, monospace; + } + + .hops { + margin: 0; + padding-left: 1.25rem; + display: grid; + gap: 0.35rem; + color: #e2e8f0; + font-size: 0.85rem; + } + + .hop { + display: grid; + gap: 0.15rem; + } + + .hop__svc { + font-weight: 600; + } + + .hop__ep { + font-family: ui-monospace, monospace; + color: #cbd5e1; + } + + .hop__ts { + font-size: 0.75rem; + color: #94a3b8; + font-family: ui-monospace, monospace; + } + + .evidence { + list-style: none; + padding: 0; + margin: 0; + display: grid; + gap: 0.35rem; + } + + code { + font-family: ui-monospace, monospace; + font-size: 0.8rem; + color: #e2e8f0; + } + `, + ], +}) +export class ReachabilityWhyDrawerComponent { + private readonly signals = inject(MockSignalsClient); + + readonly open = input.required(); + readonly status = input('unknown'); + readonly confidence = input(null); + readonly component = input(null); + readonly assetId = input(null); + + @Output() close = new EventEmitter(); + + readonly loading = signal(false); + readonly error = signal(null); + readonly paths = signal([]); + readonly factObservedAt = signal(null); + readonly lastObserved = signal(null); + readonly evidenceTraceIds = signal([]); + + readonly statusLabel = computed(() => { + switch (this.status()) { + case 'reachable': + return 'Reachable'; + case 'unreachable': + return 'Unreachable'; + default: + return 'Unknown'; + } + }); + + readonly confidenceLabel = computed(() => { + const c = this.confidence(); + if (typeof c !== 'number') return ''; + return `${Math.round(c * 100)}%`; + }); + + readonly timeline = computed(() => { + const items: { key: string; when: string; what: string }[] = []; + const factAt = this.factObservedAt(); + if (factAt) { + items.push({ key: `fact-${factAt}`, when: factAt, what: 'Reachability fact observed' }); + } + const last = this.lastObserved(); + if (last) { + items.push({ key: `call-${last}`, when: last, what: 'Latest call path observation' }); + } + return items; + }); + + constructor() { + effect( + () => { + if (!this.open()) return; + if (!this.component()) return; + void this.refresh(); + }, + { allowSignalWrites: true } + ); + } + + requestClose(): void { + this.close.emit(); + } + + private async refresh(): Promise { + if (!this.open() || !this.component()) return; + + this.loading.set(true); + this.error.set(null); + + try { + const component = this.component(); + if (!component) return; + const [facts, callGraphs] = await Promise.all([ + firstValueFrom( + this.signals.getFacts({ + assetId: this.assetId() ?? undefined, + component, + }) + ), + firstValueFrom( + this.signals.getCallGraphs({ + assetId: this.assetId() ?? undefined, + }) + ), + ]); + + this.paths.set(callGraphs.paths ?? []); + this.lastObserved.set(callGraphs.paths?.[0]?.lastObserved ?? null); + + const fact = facts.facts?.[0] ?? null; + this.factObservedAt.set(fact?.observedAt ?? null); + + const traces = new Set(); + for (const path of callGraphs.paths ?? []) { + if (path?.evidence?.traceId) traces.add(path.evidence.traceId); + } + for (const trace of fact?.evidenceTraceIds ?? []) { + traces.add(trace); + } + this.evidenceTraceIds.set([...traces].sort((a, b) => a.localeCompare(b))); + } catch (err) { + const message = err instanceof Error ? err.message : 'Unable to load reachability evidence.'; + this.error.set(message); + this.paths.set([]); + this.evidenceTraceIds.set([]); + } finally { + this.loading.set(false); + } + } +} diff --git a/src/Web/StellaOps.Web/src/app/features/scans/entropy-panel.component.ts b/src/Web/StellaOps.Web/src/app/features/scans/entropy-panel.component.ts index 96419e36a..76ff4e974 100644 --- a/src/Web/StellaOps.Web/src/app/features/scans/entropy-panel.component.ts +++ b/src/Web/StellaOps.Web/src/app/features/scans/entropy-panel.component.ts @@ -224,13 +224,13 @@ type ViewMode = 'summary' | 'layers' | 'files'; {{ (file.opaqueRatio * 100).toFixed(1) }}% - - -
- @for (window of file.windows; track window.offset) { -
+
+ @for (window of file.windows; track window.offset) { +
} diff --git a/src/Web/StellaOps.Web/src/app/features/scans/scan-detail-page.component.html b/src/Web/StellaOps.Web/src/app/features/scans/scan-detail-page.component.html index 7b785b7a7..863804196 100644 --- a/src/Web/StellaOps.Web/src/app/features/scans/scan-detail-page.component.html +++ b/src/Web/StellaOps.Web/src/app/features/scans/scan-detail-page.component.html @@ -51,29 +51,29 @@

-
-

SBOM Determinism

- @if (scan().determinism) { - - } @else { -

- No determinism evidence available for this scan. -

- } -
+
+

SBOM Determinism

+ @if (scan().determinism) { + + } @else { +

+ No determinism evidence available for this scan. +

+ } +
-
-

Entropy Analysis

- @if (scan().entropy) { - - - - - } @else { -

- No entropy analysis available for this scan. -

- } -
- +
+

Entropy Analysis

+ @if (scan().entropy) { + + + + + } @else { +

+ No entropy analysis available for this scan. +

+ } +
+ diff --git a/src/Web/StellaOps.Web/src/app/features/vulnerabilities/vulnerability-explorer.component.html b/src/Web/StellaOps.Web/src/app/features/vulnerabilities/vulnerability-explorer.component.html index 20e454fd0..92841cb81 100644 --- a/src/Web/StellaOps.Web/src/app/features/vulnerabilities/vulnerability-explorer.component.html +++ b/src/Web/StellaOps.Web/src/app/features/vulnerabilities/vulnerability-explorer.component.html @@ -85,24 +85,38 @@
-
- - -
- -
+ +
+ + +
+ +
-
- Status - - {{ statusLabels[vuln.status] }} - -
- +
+ Status + + {{ statusLabels[vuln.status] }} + +
+
+ Reachability + + {{ getReachabilityLabel(vuln) }} + + +
+
@@ -299,21 +337,30 @@
-
- -
- - -
- + +
+ + + + +
+ diff --git a/src/Web/StellaOps.Web/src/app/features/vulnerabilities/vulnerability-explorer.component.scss b/src/Web/StellaOps.Web/src/app/features/vulnerabilities/vulnerability-explorer.component.scss index 7f0a3d04b..6e29969e7 100644 --- a/src/Web/StellaOps.Web/src/app/features/vulnerabilities/vulnerability-explorer.component.scss +++ b/src/Web/StellaOps.Web/src/app/features/vulnerabilities/vulnerability-explorer.component.scss @@ -413,15 +413,31 @@ color: #92400e; } -.status--excepted { - background: #f3e8ff; - color: #7c3aed; -} - -// Empty State -.empty-state { - display: flex; - flex-direction: column; +.status--excepted { + background: #f3e8ff; + color: #7c3aed; +} + +// Reachability chips +.reachability--reachable { + background: #dcfce7; + color: #166534; +} + +.reachability--unreachable { + background: #f1f5f9; + color: #475569; +} + +.reachability--unknown { + background: #fef3c7; + color: #92400e; +} + +// Empty State +.empty-state { + display: flex; + flex-direction: column; align-items: center; justify-content: center; padding: 3rem; diff --git a/src/Web/StellaOps.Web/src/app/features/vulnerabilities/vulnerability-explorer.component.spec.ts b/src/Web/StellaOps.Web/src/app/features/vulnerabilities/vulnerability-explorer.component.spec.ts new file mode 100644 index 000000000..13a467acb --- /dev/null +++ b/src/Web/StellaOps.Web/src/app/features/vulnerabilities/vulnerability-explorer.component.spec.ts @@ -0,0 +1,83 @@ +import { ComponentFixture, TestBed, fakeAsync, flushMicrotasks } from '@angular/core/testing'; +import { of } from 'rxjs'; + +import { EXCEPTION_API, MockExceptionApiService } from '../../core/api/exception.client'; +import { VULNERABILITY_API, type VulnerabilityApi } from '../../core/api/vulnerability.client'; +import type { Vulnerability, VulnerabilityStats } from '../../core/api/vulnerability.models'; +import { VulnerabilityExplorerComponent } from './vulnerability-explorer.component'; + +describe('VulnerabilityExplorerComponent', () => { + let fixture: ComponentFixture; + let component: VulnerabilityExplorerComponent; + let api: jasmine.SpyObj; + + beforeEach(async () => { + api = jasmine.createSpyObj('VulnerabilityApi', ['listVulnerabilities', 'getStats']); + + const vulns: Vulnerability[] = [ + { + vulnId: 'v-1', + cveId: 'CVE-2024-0001', + title: 'Reachable vuln', + description: '', + severity: 'high', + status: 'open', + affectedComponents: [], + reachabilityStatus: 'reachable', + reachabilityScore: 0.9, + }, + { + vulnId: 'v-2', + cveId: 'CVE-2024-0002', + title: 'Unreachable vuln', + description: '', + severity: 'high', + status: 'open', + affectedComponents: [], + reachabilityStatus: 'unreachable', + reachabilityScore: 0.95, + }, + ]; + + const stats: VulnerabilityStats = { + total: 2, + bySeverity: { critical: 0, high: 2, medium: 0, low: 0, unknown: 0 }, + byStatus: { open: 2, fixed: 0, wont_fix: 0, in_progress: 0, excepted: 0 }, + withExceptions: 0, + criticalOpen: 0, + }; + + api.listVulnerabilities.and.returnValue(of({ items: vulns, total: vulns.length })); + api.getStats.and.returnValue(of(stats)); + + await TestBed.configureTestingModule({ + imports: [VulnerabilityExplorerComponent], + providers: [ + { provide: VULNERABILITY_API, useValue: api }, + { provide: EXCEPTION_API, useClass: MockExceptionApiService }, + ], + }).compileComponents(); + + fixture = TestBed.createComponent(VulnerabilityExplorerComponent); + component = fixture.componentInstance; + }); + + it('requests reachability data from API', fakeAsync(() => { + fixture.detectChanges(); + flushMicrotasks(); + + const options = api.listVulnerabilities.calls.mostRecent().args[0]; + expect(options?.includeReachability).toBeTrue(); + })); + + it('filters by reachability status', fakeAsync(() => { + fixture.detectChanges(); + flushMicrotasks(); + + component.reachabilityFilter.set('reachable'); + expect(component.filteredVulnerabilities().map((v) => v.vulnId)).toEqual(['v-1']); + + component.reachabilityFilter.set('unreachable'); + expect(component.filteredVulnerabilities().map((v) => v.vulnId)).toEqual(['v-2']); + })); +}); diff --git a/src/Web/StellaOps.Web/src/app/features/vulnerabilities/vulnerability-explorer.component.ts b/src/Web/StellaOps.Web/src/app/features/vulnerabilities/vulnerability-explorer.component.ts index 78e7f3287..74576355b 100644 --- a/src/Web/StellaOps.Web/src/app/features/vulnerabilities/vulnerability-explorer.component.ts +++ b/src/Web/StellaOps.Web/src/app/features/vulnerabilities/vulnerability-explorer.component.ts @@ -20,17 +20,19 @@ import { ExceptionDraftContext, ExceptionDraftInlineComponent, } from '../exceptions/exception-draft-inline.component'; -import { - ExceptionBadgeComponent, - ExceptionBadgeData, - ExceptionExplainComponent, - ExceptionExplainData, -} from '../../shared/components'; +import { + ExceptionBadgeComponent, + ExceptionBadgeData, + ExceptionExplainComponent, + ExceptionExplainData, +} from '../../shared/components'; +import { ReachabilityWhyDrawerComponent } from '../reachability/reachability-why-drawer.component'; -type SeverityFilter = VulnerabilitySeverity | 'all'; -type StatusFilter = VulnerabilityStatus | 'all'; -type SortField = 'cveId' | 'severity' | 'cvssScore' | 'publishedAt' | 'status'; -type SortOrder = 'asc' | 'desc'; +type SeverityFilter = VulnerabilitySeverity | 'all'; +type StatusFilter = VulnerabilityStatus | 'all'; +type ReachabilityFilter = 'reachable' | 'unreachable' | 'unknown' | 'all'; +type SortField = 'cveId' | 'severity' | 'cvssScore' | 'publishedAt' | 'status'; +type SortOrder = 'asc' | 'desc'; const SEVERITY_LABELS: Record = { critical: 'Critical', @@ -40,29 +42,35 @@ const SEVERITY_LABELS: Record = { unknown: 'Unknown', }; -const STATUS_LABELS: Record = { - open: 'Open', - fixed: 'Fixed', - wont_fix: "Won't Fix", - in_progress: 'In Progress', - excepted: 'Excepted', -}; - -const SEVERITY_ORDER: Record = { - critical: 0, - high: 1, - medium: 2, +const STATUS_LABELS: Record = { + open: 'Open', + fixed: 'Fixed', + wont_fix: "Won't Fix", + in_progress: 'In Progress', + excepted: 'Excepted', +}; + +const REACHABILITY_LABELS: Record, string> = { + reachable: 'Reachable', + unreachable: 'Unreachable', + unknown: 'Unknown', +}; + +const SEVERITY_ORDER: Record = { + critical: 0, + high: 1, + medium: 2, low: 3, unknown: 4, }; @Component({ - selector: 'app-vulnerability-explorer', - standalone: true, - imports: [CommonModule, ExceptionDraftInlineComponent, ExceptionBadgeComponent, ExceptionExplainComponent], - templateUrl: './vulnerability-explorer.component.html', - styleUrls: ['./vulnerability-explorer.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush, + selector: 'app-vulnerability-explorer', + standalone: true, + imports: [CommonModule, ExceptionDraftInlineComponent, ExceptionBadgeComponent, ExceptionExplainComponent, ReachabilityWhyDrawerComponent], + templateUrl: './vulnerability-explorer.component.html', + styleUrls: ['./vulnerability-explorer.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, providers: [], }) export class VulnerabilityExplorerComponent implements OnInit { @@ -78,45 +86,55 @@ export class VulnerabilityExplorerComponent implements OnInit { readonly stats = signal(null); readonly selectedVulnId = signal(null); - // Filters & sorting - readonly severityFilter = signal('all'); - readonly statusFilter = signal('all'); - readonly searchQuery = signal(''); - readonly sortField = signal('severity'); - readonly sortOrder = signal('asc'); - readonly showExceptedOnly = signal(false); + // Filters & sorting + readonly severityFilter = signal('all'); + readonly statusFilter = signal('all'); + readonly reachabilityFilter = signal('all'); + readonly searchQuery = signal(''); + readonly sortField = signal('severity'); + readonly sortOrder = signal('asc'); + readonly showExceptedOnly = signal(false); // Exception draft state readonly showExceptionDraft = signal(false); readonly selectedForException = signal([]); - // Exception explain state - readonly showExceptionExplain = signal(false); - readonly explainExceptionId = signal(null); - - // Constants for template - readonly severityLabels = SEVERITY_LABELS; - readonly statusLabels = STATUS_LABELS; - readonly allSeverities: VulnerabilitySeverity[] = ['critical', 'high', 'medium', 'low', 'unknown']; - readonly allStatuses: VulnerabilityStatus[] = ['open', 'fixed', 'wont_fix', 'in_progress', 'excepted']; + // Exception explain state + readonly showExceptionExplain = signal(false); + readonly explainExceptionId = signal(null); + + // Why drawer state + readonly showWhyDrawer = signal(false); + + // Constants for template + readonly severityLabels = SEVERITY_LABELS; + readonly statusLabels = STATUS_LABELS; + readonly reachabilityLabels = REACHABILITY_LABELS; + readonly allSeverities: VulnerabilitySeverity[] = ['critical', 'high', 'medium', 'low', 'unknown']; + readonly allStatuses: VulnerabilityStatus[] = ['open', 'fixed', 'wont_fix', 'in_progress', 'excepted']; + readonly allReachability: Exclude[] = ['reachable', 'unknown', 'unreachable']; // Computed: filtered and sorted list - readonly filteredVulnerabilities = computed(() => { - let items = [...this.vulnerabilities()]; - const severity = this.severityFilter(); - const status = this.statusFilter(); - const search = this.searchQuery().toLowerCase(); - const exceptedOnly = this.showExceptedOnly(); + readonly filteredVulnerabilities = computed(() => { + let items = [...this.vulnerabilities()]; + const severity = this.severityFilter(); + const status = this.statusFilter(); + const reachability = this.reachabilityFilter(); + const search = this.searchQuery().toLowerCase(); + const exceptedOnly = this.showExceptedOnly(); if (severity !== 'all') { items = items.filter((v) => v.severity === severity); } - if (status !== 'all') { - items = items.filter((v) => v.status === status); - } - if (exceptedOnly) { - items = items.filter((v) => v.hasException); - } + if (status !== 'all') { + items = items.filter((v) => v.status === status); + } + if (reachability !== 'all') { + items = items.filter((v) => (v.reachabilityStatus ?? 'unknown') === reachability); + } + if (exceptedOnly) { + items = items.filter((v) => v.hasException); + } if (search) { items = items.filter( (v) => @@ -221,10 +239,10 @@ export class VulnerabilityExplorerComponent implements OnInit { this.message.set(null); try { - const [vulnsResponse, statsResponse] = await Promise.all([ - firstValueFrom(this.api.listVulnerabilities()), - firstValueFrom(this.api.getStats()), - ]); + const [vulnsResponse, statsResponse] = await Promise.all([ + firstValueFrom(this.api.listVulnerabilities({ includeReachability: true })), + firstValueFrom(this.api.getStats()), + ]); this.vulnerabilities.set([...vulnsResponse.items]); this.stats.set(statsResponse); @@ -240,14 +258,18 @@ export class VulnerabilityExplorerComponent implements OnInit { this.severityFilter.set(severity); } - setStatusFilter(status: StatusFilter): void { - this.statusFilter.set(status); - } - - onSearchInput(event: Event): void { - const input = event.target as HTMLInputElement; - this.searchQuery.set(input.value); - } + setStatusFilter(status: StatusFilter): void { + this.statusFilter.set(status); + } + + setReachabilityFilter(reachability: ReachabilityFilter): void { + this.reachabilityFilter.set(reachability); + } + + onSearchInput(event: Event): void { + const input = event.target as HTMLInputElement; + this.searchQuery.set(input.value); + } clearSearch(): void { this.searchQuery.set(''); @@ -315,17 +337,17 @@ export class VulnerabilityExplorerComponent implements OnInit { this.showExceptionExplain.set(true); } - closeExplain(): void { - this.showExceptionExplain.set(false); - this.explainExceptionId.set(null); - } + closeExplain(): void { + this.showExceptionExplain.set(false); + this.explainExceptionId.set(null); + } viewExceptionFromExplain(exceptionId: string): void { this.closeExplain(); this.onViewExceptionDetails(exceptionId); } - openFullWizard(): void { + openFullWizard(): void { // In a real app, this would navigate to the Exception Center wizard // For now, just show a message this.showMessage('Opening full wizard... (would navigate to Exception Center)', 'info'); @@ -349,13 +371,47 @@ export class VulnerabilityExplorerComponent implements OnInit { }); } - formatCvss(score: number | undefined): string { - if (score === undefined) return '-'; - return score.toFixed(1); - } - - trackByVuln = (_: number, item: Vulnerability) => item.vulnId; - trackByComponent = (_: number, item: { purl: string }) => item.purl; + formatCvss(score: number | undefined): string { + if (score === undefined) return '-'; + return score.toFixed(1); + } + + openWhyDrawer(): void { + this.showWhyDrawer.set(true); + } + + closeWhyDrawer(): void { + this.showWhyDrawer.set(false); + } + + getReachabilityClass(vuln: Vulnerability): string { + const status = vuln.reachabilityStatus ?? 'unknown'; + return `reachability--${status}`; + } + + getReachabilityLabel(vuln: Vulnerability): string { + const status = vuln.reachabilityStatus ?? 'unknown'; + return REACHABILITY_LABELS[status]; + } + + getReachabilityTooltip(vuln: Vulnerability): string { + const status = vuln.reachabilityStatus ?? 'unknown'; + const score = vuln.reachabilityScore; + const scoreText = + typeof score === 'number' ? ` (confidence ${(score * 100).toFixed(0)}%)` : ''; + + switch (status) { + case 'reachable': + return `Reachable${scoreText}. Signals indicates a call path reaches at least one affected component.`; + case 'unreachable': + return `Unreachable${scoreText}. Signals found no call path to affected components.`; + default: + return `Unknown${scoreText}. No reachability evidence is available for the affected components.`; + } + } + + trackByVuln = (_: number, item: Vulnerability) => item.vulnId; + trackByComponent = (_: number, item: { purl: string }) => item.purl; private sortVulnerabilities(items: Vulnerability[]): Vulnerability[] { const field = this.sortField(); @@ -392,9 +448,9 @@ export class VulnerabilityExplorerComponent implements OnInit { setTimeout(() => this.message.set(null), 5000); } - private toErrorMessage(error: unknown): string { - if (error instanceof Error) return error.message; - if (typeof error === 'string') return error; - return 'Operation failed. Please retry.'; - } -} + private toErrorMessage(error: unknown): string { + if (error instanceof Error) return error.message; + if (typeof error === 'string') return error; + return 'Operation failed. Please retry.'; + } +} diff --git a/src/Web/StellaOps.Web/src/app/shared/components/exception-explain.component.ts b/src/Web/StellaOps.Web/src/app/shared/components/exception-explain.component.ts index df2ee5378..1117ee19b 100644 --- a/src/Web/StellaOps.Web/src/app/shared/components/exception-explain.component.ts +++ b/src/Web/StellaOps.Web/src/app/shared/components/exception-explain.component.ts @@ -79,25 +79,28 @@ export interface ExceptionExplainData { -
-

What does it cover?

-
    -
  • - {{ data.scope.vulnIds.length }} vulnerabilit{{ data.scope.vulnIds.length === 1 ? 'y' : 'ies' }}: - {{ formatList(data.scope.vulnIds) }} -
  • -
  • - {{ data.scope.componentPurls.length }} component{{ data.scope.componentPurls.length === 1 ? '' : 's' }} -
  • -
  • - {{ data.scope.assetIds.length }} asset{{ data.scope.assetIds.length === 1 ? '' : 's' }}: - {{ formatList(data.scope.assetIds) }} -
  • -
  • - Global scope - applies to all matching findings -
  • -
-
+
+

What does it cover?

+
    +
  • + {{ data.scope.vulnIds?.length ?? 0 }} + vulnerabilit{{ (data.scope.vulnIds?.length ?? 0) === 1 ? 'y' : 'ies' }}: + {{ formatList(data.scope.vulnIds ?? []) }} +
  • +
  • + {{ data.scope.componentPurls?.length ?? 0 }} + component{{ (data.scope.componentPurls?.length ?? 0) === 1 ? '' : 's' }} +
  • +
  • + {{ data.scope.assetIds?.length ?? 0 }} + asset{{ (data.scope.assetIds?.length ?? 0) === 1 ? '' : 's' }}: + {{ formatList(data.scope.assetIds ?? []) }} +
  • +
  • + Global scope - applies to all matching findings +
  • +
+
diff --git a/src/Web/StellaOps.Web/src/app/shared/components/policy-pack-selector.component.ts b/src/Web/StellaOps.Web/src/app/shared/components/policy-pack-selector.component.ts index 8c417a46f..c57db40df 100644 --- a/src/Web/StellaOps.Web/src/app/shared/components/policy-pack-selector.component.ts +++ b/src/Web/StellaOps.Web/src/app/shared/components/policy-pack-selector.component.ts @@ -27,7 +27,7 @@ import { PolicyPackSummary } from '../../features/policy-studio/models/policy.mo