feat: Add CVSS receipt management endpoints and related functionality
- Introduced new API endpoints for creating, retrieving, amending, and listing CVSS receipts. - Updated IPolicyEngineClient interface to include methods for CVSS receipt operations. - Implemented PolicyEngineClient to handle CVSS receipt requests. - Enhanced Program.cs to map new CVSS receipt routes with appropriate authorization. - Added necessary models and contracts for CVSS receipt requests and responses. - Integrated Postgres document store for managing CVSS receipts and related data. - Updated database schema with new migrations for source documents and payload storage. - Refactored existing components to support new CVSS functionality.
This commit is contained in:
16
deploy/helm/stellaops/README-mock.md
Normal file
16
deploy/helm/stellaops/README-mock.md
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
# Mock Overlay (Dev Only)
|
||||||
|
|
||||||
|
Purpose: let deployment tasks progress with placeholder digests until real releases land.
|
||||||
|
|
||||||
|
Use:
|
||||||
|
```bash
|
||||||
|
helm template mock ./deploy/helm/stellaops -f deploy/helm/stellaops/values-mock.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
Contents:
|
||||||
|
- Mock deployments for orchestrator, policy-registry, packs-registry, task-runner, VEX Lens, issuer-directory, findings-ledger, vuln-explorer-api.
|
||||||
|
- Image pins pulled from `deploy/releases/2025.09-mock-dev.yaml`.
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
- Annotated with `stellaops.dev/mock: "true"` to discourage production use.
|
||||||
|
- Swap to real values once official digests publish; keep mock overlay gated behind `mock.enabled`.
|
||||||
@@ -34,7 +34,7 @@
|
|||||||
| 6 | CVSS-DSSE-190-006 | DONE (2025-11-28) | Depends on 190-005; uses Attestor primitives. | Policy Guild · Attestor Guild (`src/Policy/StellaOps.Policy.Scoring`, `src/Attestor/StellaOps.Attestor.Envelope`) | Attach DSSE attestations to score receipts: create `stella.ops/cvssReceipt@v1` predicate type, sign receipts, store envelope references. |
|
| 6 | CVSS-DSSE-190-006 | DONE (2025-11-28) | Depends on 190-005; uses Attestor primitives. | Policy Guild · Attestor Guild (`src/Policy/StellaOps.Policy.Scoring`, `src/Attestor/StellaOps.Attestor.Envelope`) | Attach DSSE attestations to score receipts: create `stella.ops/cvssReceipt@v1` predicate type, sign receipts, store envelope references. |
|
||||||
| 7 | CVSS-HISTORY-190-007 | DONE (2025-11-28) | Depends on 190-005. | Policy Guild (`src/Policy/StellaOps.Policy.Scoring/History`) | Implement receipt amendment tracking: `AmendReceipt(receiptId, field, newValue, reason, ref)` with history entry creation and re-signing. |
|
| 7 | CVSS-HISTORY-190-007 | DONE (2025-11-28) | Depends on 190-005. | Policy Guild (`src/Policy/StellaOps.Policy.Scoring/History`) | Implement receipt amendment tracking: `AmendReceipt(receiptId, field, newValue, reason, ref)` with history entry creation and re-signing. |
|
||||||
| 8 | CVSS-CONCELIER-190-008 | DONE (2025-12-06) | Depends on 190-001; Concelier AGENTS updated 2025-12-06. | Concelier Guild · Policy Guild (`src/Concelier/__Libraries/StellaOps.Concelier.Core`) | Ingest vendor-provided CVSS v4.0 vectors from advisories; parse and store as base receipts; preserve provenance. (Implemented CVSS priority ordering in Advisory → Postgres conversion so v4 vectors are primary and provenance-preserved.) |
|
| 8 | CVSS-CONCELIER-190-008 | DONE (2025-12-06) | Depends on 190-001; Concelier AGENTS updated 2025-12-06. | Concelier Guild · Policy Guild (`src/Concelier/__Libraries/StellaOps.Concelier.Core`) | Ingest vendor-provided CVSS v4.0 vectors from advisories; parse and store as base receipts; preserve provenance. (Implemented CVSS priority ordering in Advisory → Postgres conversion so v4 vectors are primary and provenance-preserved.) |
|
||||||
| 9 | CVSS-API-190-009 | BLOCKED (2025-12-06) | Depends on 190-005, 190-007; missing Policy Engine CVSS receipt endpoints to proxy. | Policy Guild (`src/Policy/StellaOps.Policy.Gateway`) | REST/gRPC APIs: `POST /cvss/receipts`, `GET /cvss/receipts/{id}`, `PUT /cvss/receipts/{id}/amend`, `GET /cvss/receipts/{id}/history`, `GET /cvss/policies`. |
|
| 9 | CVSS-API-190-009 | DONE (2025-12-06) | Depends on 190-005, 190-007; Policy Engine + Gateway CVSS endpoints shipped. | Policy Guild (`src/Policy/StellaOps.Policy.Gateway`) | REST APIs delivered: `POST /cvss/receipts`, `GET /cvss/receipts/{id}`, `PUT /cvss/receipts/{id}/amend`, `GET /cvss/receipts/{id}/history`, `GET /cvss/policies`. |
|
||||||
| 10 | CVSS-CLI-190-010 | TODO | Depends on 190-009 (API readiness). | CLI Guild (`src/Cli/StellaOps.Cli`) | CLI verbs: `stella cvss score --vuln <id>`, `stella cvss show <receiptId>`, `stella cvss history <receiptId>`, `stella cvss export <receiptId> --format json|pdf`. |
|
| 10 | CVSS-CLI-190-010 | TODO | Depends on 190-009 (API readiness). | CLI Guild (`src/Cli/StellaOps.Cli`) | CLI verbs: `stella cvss score --vuln <id>`, `stella cvss show <receiptId>`, `stella cvss history <receiptId>`, `stella cvss export <receiptId> --format json|pdf`. |
|
||||||
| 11 | CVSS-UI-190-011 | TODO | Depends on 190-009 (API readiness). | UI Guild (`src/UI/StellaOps.UI`) | UI components: Score badge with CVSS-BTE label, tabbed receipt viewer (Base/Threat/Environmental/Supplemental/Evidence/Policy/History), "Recalculate with my env" button, export options. |
|
| 11 | CVSS-UI-190-011 | TODO | Depends on 190-009 (API readiness). | UI Guild (`src/UI/StellaOps.UI`) | UI components: Score badge with CVSS-BTE label, tabbed receipt viewer (Base/Threat/Environmental/Supplemental/Evidence/Policy/History), "Recalculate with my env" button, export options. |
|
||||||
| 12 | CVSS-DOCS-190-012 | BLOCKED (2025-11-29) | Depends on 190-001 through 190-011 (API/UI/CLI blocked). | Docs Guild (`docs/modules/policy/cvss-v4.md`, `docs/09_API_CLI_REFERENCE.md`) | Document CVSS v4.0 scoring system: data model, policy format, API reference, CLI usage, UI guide, determinism guarantees. |
|
| 12 | CVSS-DOCS-190-012 | BLOCKED (2025-11-29) | Depends on 190-001 through 190-011 (API/UI/CLI blocked). | Docs Guild (`docs/modules/policy/cvss-v4.md`, `docs/09_API_CLI_REFERENCE.md`) | Document CVSS v4.0 scoring system: data model, policy format, API reference, CLI usage, UI guide, determinism guarantees. |
|
||||||
@@ -48,7 +48,7 @@
|
|||||||
| --- | --- | --- | --- | --- |
|
| --- | --- | --- | --- | --- |
|
||||||
| W1 Foundation | Policy Guild | None | DONE (2025-11-28) | Tasks 1-4: Data model, engine, tests, policy loader. |
|
| W1 Foundation | Policy Guild | None | DONE (2025-11-28) | Tasks 1-4: Data model, engine, tests, policy loader. |
|
||||||
| W2 Receipt Pipeline | Policy Guild · Attestor Guild | W1 complete | DONE (2025-11-28) | Tasks 5-7: Receipt builder, DSSE, history completed; integration tests green. |
|
| W2 Receipt Pipeline | Policy Guild · Attestor Guild | W1 complete | DONE (2025-11-28) | Tasks 5-7: Receipt builder, DSSE, history completed; integration tests green. |
|
||||||
| W3 Integration | Concelier · Policy · CLI · UI Guilds | W2 complete; AGENTS delivered 2025-12-06 | BLOCKED (2025-12-06) | CVSS-API-190-009 blocked: Policy Engine lacks CVSS receipt endpoints to proxy; CLI/UI depend on it. |
|
| W3 Integration | Concelier · Policy · CLI · UI Guilds | W2 complete; AGENTS delivered 2025-12-06 | TODO (2025-12-06) | CVSS API now available; proceed with CLI (task 10) and UI (task 11) wiring. |
|
||||||
| W4 Documentation | Docs Guild | W3 complete | BLOCKED (2025-12-06) | Task 12 blocked by API/UI/CLI delivery; resumes after W3 progresses. |
|
| W4 Documentation | Docs Guild | W3 complete | BLOCKED (2025-12-06) | Task 12 blocked by API/UI/CLI delivery; resumes after W3 progresses. |
|
||||||
|
|
||||||
## Interlocks
|
## Interlocks
|
||||||
@@ -75,11 +75,12 @@
|
|||||||
| R3 | Receipt storage grows large with evidence links. | Storage costs; query performance. | Implement evidence reference deduplication; use CAS URIs; Platform Guild. |
|
| R3 | Receipt storage grows large with evidence links. | Storage costs; query performance. | Implement evidence reference deduplication; use CAS URIs; Platform Guild. |
|
||||||
| R4 | CVSS parser/ruleset changes ungoverned (CVM9). | Score drift, audit gaps. | Version parsers/rulesets; DSSE-sign releases; log scorer version in receipts; dual-review changes. |
|
| R4 | CVSS parser/ruleset changes ungoverned (CVM9). | Score drift, audit gaps. | Version parsers/rulesets; DSSE-sign releases; log scorer version in receipts; dual-review changes. |
|
||||||
| R5 | Missing AGENTS for Policy WebService and Concelier ingestion block integration (tasks 8–11). | API/CLI/UI delivery stalled. | AGENTS delivered 2025-12-06 (tasks 15–16). Risk mitigated; monitor API contract approvals. |
|
| R5 | Missing AGENTS for Policy WebService and Concelier ingestion block integration (tasks 8–11). | API/CLI/UI delivery stalled. | AGENTS delivered 2025-12-06 (tasks 15–16). Risk mitigated; monitor API contract approvals. |
|
||||||
| R6 | Policy Engine lacks CVSS receipt endpoints; gateway proxy cannot be implemented yet. | API/CLI/UI tasks remain blocked. | Policy Guild to add receipt API surface in Policy Engine; re-run gateway wiring once available. |
|
| R6 | Policy Engine lacks CVSS receipt endpoints; gateway proxy cannot be implemented yet. | API/CLI/UI tasks remain blocked. | **Mitigated 2025-12-06:** CVSS receipt endpoints implemented in Policy Engine and Gateway; unblock CLI/UI. |
|
||||||
|
|
||||||
## Execution Log
|
## Execution Log
|
||||||
| Date (UTC) | Update | Owner |
|
| Date (UTC) | Update | Owner |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
|
| 2025-12-06 | CVSS-API-190-009 DONE: added Policy Engine CVSS receipt endpoints and Gateway proxies (`/api/cvss/receipts`, history, amend, policies); W3 unblocked; risk R6 mitigated. | Implementer |
|
||||||
| 2025-12-06 | CVSS-CONCELIER-190-008 DONE: prioritized CVSS v4.0 vectors as primary in advisory→Postgres conversion; provenance preserved; enables Policy receipt ingestion. CVSS-API-190-009 set BLOCKED pending Policy Engine CVSS receipt endpoints (risk R6). | Implementer |
|
| 2025-12-06 | CVSS-CONCELIER-190-008 DONE: prioritized CVSS v4.0 vectors as primary in advisory→Postgres conversion; provenance preserved; enables Policy receipt ingestion. CVSS-API-190-009 set BLOCKED pending Policy Engine CVSS receipt endpoints (risk R6). | Implementer |
|
||||||
| 2025-12-06 | Created Policy Gateway AGENTS and refreshed Concelier AGENTS for CVSS v4 ingest (tasks 15–16 DONE); moved tasks 8–11 to TODO, set W3 to TODO, mitigated risk R5. | Project Mgmt |
|
| 2025-12-06 | Created Policy Gateway AGENTS and refreshed Concelier AGENTS for CVSS v4 ingest (tasks 15–16 DONE); moved tasks 8–11 to TODO, set W3 to TODO, mitigated risk R5. | Project Mgmt |
|
||||||
| 2025-12-06 | Added tasks 15–16 to create AGENTS for Policy WebService and Concelier; set Wave 2 to DONE; marked Waves 3–4 BLOCKED until AGENTS exist; captured risk R5. | Project Mgmt |
|
| 2025-12-06 | Added tasks 15–16 to create AGENTS for Policy WebService and Concelier; set Wave 2 to DONE; marked Waves 3–4 BLOCKED until AGENTS exist; captured risk R5. | Project Mgmt |
|
||||||
|
|||||||
@@ -39,6 +39,7 @@
|
|||||||
| 2025-12-06 | CI workflow `.gitea/workflows/mock-dev-release.yml` now packages mock manifest + downloads JSON into `mock-dev-release.tgz` for dev pipelines. | Deployment Guild |
|
| 2025-12-06 | CI workflow `.gitea/workflows/mock-dev-release.yml` now packages mock manifest + downloads JSON into `mock-dev-release.tgz` for dev pipelines. | Deployment Guild |
|
||||||
| 2025-12-06 | Mock Compose overlay (`deploy/compose/docker-compose.mock.yaml`) documented for dev-only configs using placeholder digests; production pins remain pending. | Deployment Guild |
|
| 2025-12-06 | Mock Compose overlay (`deploy/compose/docker-compose.mock.yaml`) documented for dev-only configs using placeholder digests; production pins remain pending. | Deployment Guild |
|
||||||
| 2025-12-06 | Added production guard `.gitea/workflows/release-manifest-verify.yml` to fail CI if stable/airgap manifests or downloads JSON omit required components. | Deployment Guild |
|
| 2025-12-06 | Added production guard `.gitea/workflows/release-manifest-verify.yml` to fail CI if stable/airgap manifests or downloads JSON omit required components. | Deployment Guild |
|
||||||
|
| 2025-12-06 | Added Helm mock overlays (`orchestrator/policy/packs/vex/vuln` under `deploy/helm/stellaops/templates/*-mock.yaml`) and `values-mock.yaml`; mock dev release workflow now renders `helm template` with mock values for dev packaging. | Deployment Guild |
|
||||||
| 2025-12-05 | HELM-45-003 DONE: added HPA template with per-service overrides, PDB support, Prometheus scrape annotations hook, and production defaults (prod enabled, airgap prometheus on but HPA off). | Deployment Guild |
|
| 2025-12-05 | HELM-45-003 DONE: added HPA template with per-service overrides, PDB support, Prometheus scrape annotations hook, and production defaults (prod enabled, airgap prometheus on but HPA off). | Deployment Guild |
|
||||||
| 2025-12-05 | HELM-45-002 DONE: added ingress/TLS toggles, NetworkPolicy defaults, pod security contexts, and ExternalSecret scaffold (prod enabled, airgap off); documented via values changes and templates (`core.yaml`, `networkpolicy.yaml`, `ingress.yaml`, `externalsecrets.yaml`). | Deployment Guild |
|
| 2025-12-05 | HELM-45-002 DONE: added ingress/TLS toggles, NetworkPolicy defaults, pod security contexts, and ExternalSecret scaffold (prod enabled, airgap off); documented via values changes and templates (`core.yaml`, `networkpolicy.yaml`, `ingress.yaml`, `externalsecrets.yaml`). | Deployment Guild |
|
||||||
| 2025-12-05 | HELM-45-001 DONE: added migration job scaffolding and toggle to Helm chart (`deploy/helm/stellaops/templates/migrations.yaml`, values defaults), kept digest pins, and published install guide (`deploy/helm/stellaops/INSTALL.md`). | Deployment Guild |
|
| 2025-12-05 | HELM-45-001 DONE: added migration job scaffolding and toggle to Helm chart (`deploy/helm/stellaops/templates/migrations.yaml`, values defaults), kept digest pins, and published install guide (`deploy/helm/stellaops/INSTALL.md`). | Deployment Guild |
|
||||||
|
|||||||
@@ -52,7 +52,7 @@
|
|||||||
| 9 | PG-T7.1.9 | TODO | Depends on PG-T7.1.8 | Infrastructure Guild | Remove MongoDB configuration options |
|
| 9 | PG-T7.1.9 | TODO | Depends on PG-T7.1.8 | Infrastructure Guild | Remove MongoDB configuration options |
|
||||||
| 10 | PG-T7.1.10 | TODO | Depends on PG-T7.1.9 | Infrastructure Guild | Run full build to verify no broken references |
|
| 10 | PG-T7.1.10 | TODO | Depends on PG-T7.1.9 | Infrastructure Guild | Run full build to verify no broken references |
|
||||||
| 14 | PG-T7.1.5a | DOING | Concelier Guild | Concelier: replace Mongo deps with Postgres equivalents; remove MongoDB packages; compat layer added. |
|
| 14 | PG-T7.1.5a | DOING | Concelier Guild | Concelier: replace Mongo deps with Postgres equivalents; remove MongoDB packages; compat layer added. |
|
||||||
| 15 | PG-T7.1.5b | TODO | Concelier Guild | Build Postgres document/raw storage + state repositories and wire DI. |
|
| 15 | PG-T7.1.5b | DOING | Concelier Guild | Build Postgres document/raw storage + state repositories and wire DI. |
|
||||||
| 16 | PG-T7.1.5c | TODO | Concelier Guild | Refactor connectors/exporters/tests to Postgres storage; delete Storage.Mongo code. |
|
| 16 | PG-T7.1.5c | TODO | Concelier Guild | Refactor connectors/exporters/tests to Postgres storage; delete Storage.Mongo code. |
|
||||||
| 17 | PG-T7.1.5d | TODO | Concelier Guild | Add migrations for document/state/export tables; include in air-gap kit. |
|
| 17 | PG-T7.1.5d | TODO | Concelier Guild | Add migrations for document/state/export tables; include in air-gap kit. |
|
||||||
| 18 | PG-T7.1.5e | TODO | Concelier Guild | Postgres-only Concelier build/tests green; remove Mongo artefacts and update docs. |
|
| 18 | PG-T7.1.5e | TODO | Concelier Guild | Postgres-only Concelier build/tests green; remove Mongo artefacts and update docs. |
|
||||||
@@ -122,6 +122,7 @@
|
|||||||
| 2025-12-06 | Attempted Scheduler Postgres tests; restore/build fails because `StellaOps.Concelier.Storage.Mongo` project is absent and Concelier connectors reference it. Need phased Concelier plan/shim to unblock test/build runs. | Scheduler Guild |
|
| 2025-12-06 | Attempted Scheduler Postgres tests; restore/build fails because `StellaOps.Concelier.Storage.Mongo` project is absent and Concelier connectors reference it. Need phased Concelier plan/shim to unblock test/build runs. | Scheduler Guild |
|
||||||
| 2025-12-06 | Began Concelier Mongo compatibility shim: added `FindAsync` to in-memory `IDocumentStore` in Postgres compat layer to unblock connector compile; full Mongo removal still pending. | Infrastructure Guild |
|
| 2025-12-06 | Began Concelier Mongo compatibility shim: added `FindAsync` to in-memory `IDocumentStore` in Postgres compat layer to unblock connector compile; full Mongo removal still pending. | Infrastructure Guild |
|
||||||
| 2025-12-06 | Added lightweight `StellaOps.Concelier.Storage.Mongo` in-memory stub (advisory/dto/document/state/export stores) to unblock Concelier connector build while Postgres rewiring continues; no Mongo driver/runtime. | Infrastructure Guild |
|
| 2025-12-06 | Added lightweight `StellaOps.Concelier.Storage.Mongo` in-memory stub (advisory/dto/document/state/export stores) to unblock Concelier connector build while Postgres rewiring continues; no Mongo driver/runtime. | Infrastructure Guild |
|
||||||
|
| 2025-12-06 | PG-T7.1.5b set to DOING; began wiring Postgres document store (DI registration, repository find) to replace Mongo bindings. | Concelier Guild |
|
||||||
|
|
||||||
## Decisions & Risks
|
## Decisions & Risks
|
||||||
- Cleanup is strictly after all phases complete; do not start T7 tasks until module cutovers are DONE.
|
- Cleanup is strictly after all phases complete; do not start T7 tasks until module cutovers are DONE.
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
| # | Task ID | Status | Owner | Notes |
|
| # | Task ID | Status | Owner | Notes |
|
||||||
|---|---|---|---|---|
|
|---|---|---|---|---|
|
||||||
| 1 | PG-T7.1.5a | DOING | Concelier Guild | Replace Mongo storage dependencies with Postgres equivalents; remove MongoDB.Driver/Bson packages from Concelier projects. |
|
| 1 | PG-T7.1.5a | DOING | Concelier Guild | Replace Mongo storage dependencies with Postgres equivalents; remove MongoDB.Driver/Bson packages from Concelier projects. |
|
||||||
| 2 | PG-T7.1.5b | TODO | Concelier Guild | Implement Postgres document/raw storage (bytea/LargeObject) + state repos to satisfy connector fetch/store paths. |
|
| 2 | PG-T7.1.5b | DOING | Concelier Guild | Implement Postgres document/raw storage (bytea/LargeObject) + state repos to satisfy connector fetch/store paths. |
|
||||||
| 3 | PG-T7.1.5c | TODO | Concelier Guild | Refactor all connectors/exporters/tests to use Postgres storage namespaces; delete Storage.Mongo code/tests. |
|
| 3 | PG-T7.1.5c | TODO | Concelier Guild | Refactor all connectors/exporters/tests to use Postgres storage namespaces; delete Storage.Mongo code/tests. |
|
||||||
| 4 | PG-T7.1.5d | TODO | Concelier Guild | Add migrations for documents/state/export tables; wire into Concelier Postgres storage DI. |
|
| 4 | PG-T7.1.5d | TODO | Concelier Guild | Add migrations for documents/state/export tables; wire into Concelier Postgres storage DI. |
|
||||||
| 5 | PG-T7.1.5e | TODO | Concelier Guild | End-to-end Concelier build/test on Postgres-only stack; update sprint log and remove Mongo artifacts from repo history references. |
|
| 5 | PG-T7.1.5e | TODO | Concelier Guild | End-to-end Concelier build/test on Postgres-only stack; update sprint log and remove Mongo artifacts from repo history references. |
|
||||||
|
|||||||
@@ -261,7 +261,7 @@ public sealed class AcscConnector : IFeedConnector
|
|||||||
|
|
||||||
_diagnostics.ParseAttempt(feedTag);
|
_diagnostics.ParseAttempt(feedTag);
|
||||||
|
|
||||||
if (!document.GridFsId.HasValue)
|
if (!document.PayloadId.HasValue)
|
||||||
{
|
{
|
||||||
_diagnostics.ParseFailure(feedTag, "missingPayload");
|
_diagnostics.ParseFailure(feedTag, "missingPayload");
|
||||||
_logger.LogWarning("ACSC document {DocumentId} missing GridFS payload (feed={Feed})", document.Id, feedTag);
|
_logger.LogWarning("ACSC document {DocumentId} missing GridFS payload (feed={Feed})", document.Id, feedTag);
|
||||||
@@ -274,7 +274,7 @@ public sealed class AcscConnector : IFeedConnector
|
|||||||
byte[] rawBytes;
|
byte[] rawBytes;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
rawBytes = await _rawDocumentStorage.DownloadAsync(document.GridFsId.Value, cancellationToken).ConfigureAwait(false);
|
rawBytes = await _rawDocumentStorage.DownloadAsync(document.PayloadId.Value, cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -556,12 +556,12 @@ public sealed class AcscConnector : IFeedConnector
|
|||||||
|
|
||||||
private async Task<DateTimeOffset?> TryComputeLatestPublishedAsync(DocumentRecord document, CancellationToken cancellationToken)
|
private async Task<DateTimeOffset?> TryComputeLatestPublishedAsync(DocumentRecord document, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
if (!document.GridFsId.HasValue)
|
if (!document.PayloadId.HasValue)
|
||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
var rawBytes = await _rawDocumentStorage.DownloadAsync(document.GridFsId.Value, cancellationToken).ConfigureAwait(false);
|
var rawBytes = await _rawDocumentStorage.DownloadAsync(document.PayloadId.Value, cancellationToken).ConfigureAwait(false);
|
||||||
if (rawBytes.Length == 0)
|
if (rawBytes.Length == 0)
|
||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -182,7 +182,7 @@ public sealed class CccsConnector : IFeedConnector
|
|||||||
Metadata: metadata,
|
Metadata: metadata,
|
||||||
Etag: null,
|
Etag: null,
|
||||||
LastModified: rawDocument.Modified ?? rawDocument.Published ?? result.LastModifiedUtc,
|
LastModified: rawDocument.Modified ?? rawDocument.Published ?? result.LastModifiedUtc,
|
||||||
GridFsId: gridFsId,
|
PayloadId: gridFsId,
|
||||||
ExpiresAt: null);
|
ExpiresAt: null);
|
||||||
|
|
||||||
var upserted = await _documentStore.UpsertAsync(record, cancellationToken).ConfigureAwait(false);
|
var upserted = await _documentStore.UpsertAsync(record, cancellationToken).ConfigureAwait(false);
|
||||||
@@ -262,7 +262,7 @@ public sealed class CccsConnector : IFeedConnector
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!document.GridFsId.HasValue)
|
if (!document.PayloadId.HasValue)
|
||||||
{
|
{
|
||||||
_diagnostics.ParseFailure();
|
_diagnostics.ParseFailure();
|
||||||
_logger.LogWarning("CCCS document {DocumentId} missing GridFS payload", documentId);
|
_logger.LogWarning("CCCS document {DocumentId} missing GridFS payload", documentId);
|
||||||
@@ -276,7 +276,7 @@ public sealed class CccsConnector : IFeedConnector
|
|||||||
byte[] payload;
|
byte[] payload;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
payload = await _rawDocumentStorage.DownloadAsync(document.GridFsId.Value, cancellationToken).ConfigureAwait(false);
|
payload = await _rawDocumentStorage.DownloadAsync(document.PayloadId.Value, cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -245,7 +245,7 @@ public sealed class CertBundConnector : IFeedConnector
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!document.GridFsId.HasValue)
|
if (!document.PayloadId.HasValue)
|
||||||
{
|
{
|
||||||
await _documentStore.UpdateStatusAsync(document.Id, DocumentStatuses.Failed, cancellationToken).ConfigureAwait(false);
|
await _documentStore.UpdateStatusAsync(document.Id, DocumentStatuses.Failed, cancellationToken).ConfigureAwait(false);
|
||||||
remainingDocuments.Remove(documentId);
|
remainingDocuments.Remove(documentId);
|
||||||
@@ -258,7 +258,7 @@ public sealed class CertBundConnector : IFeedConnector
|
|||||||
byte[] payload;
|
byte[] payload;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
payload = await _rawDocumentStorage.DownloadAsync(document.GridFsId.Value, cancellationToken).ConfigureAwait(false);
|
payload = await _rawDocumentStorage.DownloadAsync(document.PayloadId.Value, cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -597,23 +597,23 @@ public sealed class CertCcConnector : IFeedConnector
|
|||||||
|
|
||||||
private async Task<IReadOnlyList<string>> ReadSummaryNotesAsync(DocumentRecord document, CancellationToken cancellationToken)
|
private async Task<IReadOnlyList<string>> ReadSummaryNotesAsync(DocumentRecord document, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
if (!document.GridFsId.HasValue)
|
if (!document.PayloadId.HasValue)
|
||||||
{
|
{
|
||||||
return Array.Empty<string>();
|
return Array.Empty<string>();
|
||||||
}
|
}
|
||||||
|
|
||||||
var payload = await _rawDocumentStorage.DownloadAsync(document.GridFsId.Value, cancellationToken).ConfigureAwait(false);
|
var payload = await _rawDocumentStorage.DownloadAsync(document.PayloadId.Value, cancellationToken).ConfigureAwait(false);
|
||||||
return CertCcSummaryParser.ParseNotes(payload);
|
return CertCcSummaryParser.ParseNotes(payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<byte[]> DownloadDocumentAsync(DocumentRecord document, CancellationToken cancellationToken)
|
private async Task<byte[]> DownloadDocumentAsync(DocumentRecord document, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
if (!document.GridFsId.HasValue)
|
if (!document.PayloadId.HasValue)
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException($"Document {document.Id} has no GridFS payload.");
|
throw new InvalidOperationException($"Document {document.Id} has no GridFS payload.");
|
||||||
}
|
}
|
||||||
|
|
||||||
return await _rawDocumentStorage.DownloadAsync(document.GridFsId.Value, cancellationToken).ConfigureAwait(false);
|
return await _rawDocumentStorage.DownloadAsync(document.PayloadId.Value, cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Uri BuildDetailUri(string noteId, string endpoint)
|
private Uri BuildDetailUri(string noteId, string endpoint)
|
||||||
|
|||||||
@@ -196,7 +196,7 @@ public sealed class CertFrConnector : IFeedConnector
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!document.GridFsId.HasValue)
|
if (!document.PayloadId.HasValue)
|
||||||
{
|
{
|
||||||
_logger.LogWarning("Cert-FR document {DocumentId} missing GridFS payload", document.Id);
|
_logger.LogWarning("Cert-FR document {DocumentId} missing GridFS payload", document.Id);
|
||||||
await _documentStore.UpdateStatusAsync(document.Id, DocumentStatuses.Failed, cancellationToken).ConfigureAwait(false);
|
await _documentStore.UpdateStatusAsync(document.Id, DocumentStatuses.Failed, cancellationToken).ConfigureAwait(false);
|
||||||
@@ -222,7 +222,7 @@ public sealed class CertFrConnector : IFeedConnector
|
|||||||
CertFrDto dto;
|
CertFrDto dto;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var content = await _rawDocumentStorage.DownloadAsync(document.GridFsId.Value, cancellationToken).ConfigureAwait(false);
|
var content = await _rawDocumentStorage.DownloadAsync(document.PayloadId.Value, cancellationToken).ConfigureAwait(false);
|
||||||
var html = System.Text.Encoding.UTF8.GetString(content);
|
var html = System.Text.Encoding.UTF8.GetString(content);
|
||||||
dto = CertFrParser.Parse(html, metadata);
|
dto = CertFrParser.Parse(html, metadata);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -198,7 +198,7 @@ public sealed class CertInConnector : IFeedConnector
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!document.GridFsId.HasValue)
|
if (!document.PayloadId.HasValue)
|
||||||
{
|
{
|
||||||
_logger.LogWarning("CERT-In document {DocumentId} missing GridFS payload", document.Id);
|
_logger.LogWarning("CERT-In document {DocumentId} missing GridFS payload", document.Id);
|
||||||
await _documentStore.UpdateStatusAsync(document.Id, DocumentStatuses.Failed, cancellationToken).ConfigureAwait(false);
|
await _documentStore.UpdateStatusAsync(document.Id, DocumentStatuses.Failed, cancellationToken).ConfigureAwait(false);
|
||||||
@@ -217,7 +217,7 @@ public sealed class CertInConnector : IFeedConnector
|
|||||||
byte[] rawBytes;
|
byte[] rawBytes;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
rawBytes = await _rawDocumentStorage.DownloadAsync(document.GridFsId.Value, cancellationToken).ConfigureAwait(false);
|
rawBytes = await _rawDocumentStorage.DownloadAsync(document.PayloadId.Value, cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ using System.Net.Http;
|
|||||||
using System.Net.Security;
|
using System.Net.Security;
|
||||||
using System.Security.Cryptography.X509Certificates;
|
using System.Security.Cryptography.X509Certificates;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using StellaOps.Concelier.Connector.Common.Xml;
|
using StellaOps.Concelier.Connector.Common.Xml;
|
||||||
using StellaOps.Concelier.Core.Aoc;
|
using StellaOps.Concelier.Core.Aoc;
|
||||||
@@ -169,7 +170,7 @@ public static class ServiceCollectionExtensions
|
|||||||
services.AddSingleton<Fetch.IJitterSource, Fetch.CryptoJitterSource>();
|
services.AddSingleton<Fetch.IJitterSource, Fetch.CryptoJitterSource>();
|
||||||
services.AddConcelierAocGuards();
|
services.AddConcelierAocGuards();
|
||||||
services.AddConcelierLinksetMappers();
|
services.AddConcelierLinksetMappers();
|
||||||
services.AddSingleton<IDocumentStore, InMemoryDocumentStore>();
|
services.TryAddSingleton<IDocumentStore, InMemoryDocumentStore>();
|
||||||
services.AddSingleton<Fetch.RawDocumentStorage>();
|
services.AddSingleton<Fetch.RawDocumentStorage>();
|
||||||
services.AddSingleton<Fetch.SourceFetchService>();
|
services.AddSingleton<Fetch.SourceFetchService>();
|
||||||
|
|
||||||
|
|||||||
@@ -145,7 +145,7 @@ public sealed class SourceStateSeedProcessor
|
|||||||
|
|
||||||
var existing = await _documentStore.FindBySourceAndUriAsync(source, document.Uri, cancellationToken).ConfigureAwait(false);
|
var existing = await _documentStore.FindBySourceAndUriAsync(source, document.Uri, cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
if (existing?.GridFsId is { } oldGridId)
|
if (existing?.PayloadId is { } oldGridId)
|
||||||
{
|
{
|
||||||
await _rawDocumentStorage.DeleteAsync(oldGridId, cancellationToken).ConfigureAwait(false);
|
await _rawDocumentStorage.DeleteAsync(oldGridId, cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -342,7 +342,7 @@ public sealed class CveConnector : IFeedConnector
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!document.GridFsId.HasValue)
|
if (!document.PayloadId.HasValue)
|
||||||
{
|
{
|
||||||
_diagnostics.ParseFailure();
|
_diagnostics.ParseFailure();
|
||||||
_logger.LogWarning("CVEs document {DocumentId} missing GridFS content", documentId);
|
_logger.LogWarning("CVEs document {DocumentId} missing GridFS content", documentId);
|
||||||
@@ -354,7 +354,7 @@ public sealed class CveConnector : IFeedConnector
|
|||||||
byte[] rawBytes;
|
byte[] rawBytes;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
rawBytes = await _rawDocumentStorage.DownloadAsync(document.GridFsId.Value, cancellationToken).ConfigureAwait(false);
|
rawBytes = await _rawDocumentStorage.DownloadAsync(document.PayloadId.Value, cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -514,7 +514,7 @@ public sealed class CveConnector : IFeedConnector
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (existing?.GridFsId is ObjectId existingGrid && existingGrid != ObjectId.Empty)
|
if (existing?.PayloadId is ObjectId existingGrid && existingGrid != ObjectId.Empty)
|
||||||
{
|
{
|
||||||
gridId = existingGrid;
|
gridId = existingGrid;
|
||||||
}
|
}
|
||||||
@@ -547,7 +547,7 @@ public sealed class CveConnector : IFeedConnector
|
|||||||
Metadata: metadata,
|
Metadata: metadata,
|
||||||
Etag: null,
|
Etag: null,
|
||||||
LastModified: lastModified,
|
LastModified: lastModified,
|
||||||
GridFsId: gridId);
|
PayloadId: gridId);
|
||||||
|
|
||||||
await _documentStore.UpsertAsync(document, cancellationToken).ConfigureAwait(false);
|
await _documentStore.UpsertAsync(document, cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
|
|||||||
@@ -127,7 +127,7 @@ public sealed class DebianConnector : IFeedConnector
|
|||||||
{
|
{
|
||||||
fetchCache[listKey] = DebianFetchCacheEntry.FromDocument(listResult.Document);
|
fetchCache[listKey] = DebianFetchCacheEntry.FromDocument(listResult.Document);
|
||||||
|
|
||||||
if (!listResult.Document.GridFsId.HasValue)
|
if (!listResult.Document.PayloadId.HasValue)
|
||||||
{
|
{
|
||||||
_logger.LogWarning("Debian list document {DocumentId} missing GridFS payload", listResult.Document.Id);
|
_logger.LogWarning("Debian list document {DocumentId} missing GridFS payload", listResult.Document.Id);
|
||||||
}
|
}
|
||||||
@@ -136,7 +136,7 @@ public sealed class DebianConnector : IFeedConnector
|
|||||||
byte[] bytes;
|
byte[] bytes;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
bytes = await _rawDocumentStorage.DownloadAsync(listResult.Document.GridFsId.Value, cancellationToken).ConfigureAwait(false);
|
bytes = await _rawDocumentStorage.DownloadAsync(listResult.Document.PayloadId.Value, cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -326,7 +326,7 @@ public sealed class DebianConnector : IFeedConnector
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!document.GridFsId.HasValue)
|
if (!document.PayloadId.HasValue)
|
||||||
{
|
{
|
||||||
_logger.LogWarning("Debian document {DocumentId} missing GridFS payload", document.Id);
|
_logger.LogWarning("Debian document {DocumentId} missing GridFS payload", document.Id);
|
||||||
await _documentStore.UpdateStatusAsync(document.Id, DocumentStatuses.Failed, cancellationToken).ConfigureAwait(false);
|
await _documentStore.UpdateStatusAsync(document.Id, DocumentStatuses.Failed, cancellationToken).ConfigureAwait(false);
|
||||||
@@ -346,7 +346,7 @@ public sealed class DebianConnector : IFeedConnector
|
|||||||
byte[] bytes;
|
byte[] bytes;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
bytes = await _rawDocumentStorage.DownloadAsync(document.GridFsId.Value, cancellationToken).ConfigureAwait(false);
|
bytes = await _rawDocumentStorage.DownloadAsync(document.PayloadId.Value, cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -301,7 +301,7 @@ public sealed class RedHatConnector : IFeedConnector
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!document.GridFsId.HasValue)
|
if (!document.PayloadId.HasValue)
|
||||||
{
|
{
|
||||||
_logger.LogWarning("Red Hat document {DocumentId} missing GridFS content; skipping", document.Id);
|
_logger.LogWarning("Red Hat document {DocumentId} missing GridFS content; skipping", document.Id);
|
||||||
remainingFetch.Remove(documentId);
|
remainingFetch.Remove(documentId);
|
||||||
@@ -309,7 +309,7 @@ public sealed class RedHatConnector : IFeedConnector
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
var rawBytes = await _rawDocumentStorage.DownloadAsync(document.GridFsId.Value, cancellationToken).ConfigureAwait(false);
|
var rawBytes = await _rawDocumentStorage.DownloadAsync(document.PayloadId.Value, cancellationToken).ConfigureAwait(false);
|
||||||
using var jsonDocument = JsonDocument.Parse(rawBytes);
|
using var jsonDocument = JsonDocument.Parse(rawBytes);
|
||||||
var sanitized = JsonSerializer.Serialize(jsonDocument.RootElement);
|
var sanitized = JsonSerializer.Serialize(jsonDocument.RootElement);
|
||||||
var payload = BsonDocument.Parse(sanitized);
|
var payload = BsonDocument.Parse(sanitized);
|
||||||
|
|||||||
@@ -125,12 +125,12 @@ public sealed class SuseConnector : IFeedConnector
|
|||||||
else if (changesResult.IsSuccess && changesResult.Document is not null)
|
else if (changesResult.IsSuccess && changesResult.Document is not null)
|
||||||
{
|
{
|
||||||
fetchCache[changesKey] = SuseFetchCacheEntry.FromDocument(changesResult.Document);
|
fetchCache[changesKey] = SuseFetchCacheEntry.FromDocument(changesResult.Document);
|
||||||
if (changesResult.Document.GridFsId.HasValue)
|
if (changesResult.Document.PayloadId.HasValue)
|
||||||
{
|
{
|
||||||
byte[] changesBytes;
|
byte[] changesBytes;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
changesBytes = await _rawDocumentStorage.DownloadAsync(changesResult.Document.GridFsId.Value, cancellationToken).ConfigureAwait(false);
|
changesBytes = await _rawDocumentStorage.DownloadAsync(changesResult.Document.PayloadId.Value, cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -302,7 +302,7 @@ public sealed class SuseConnector : IFeedConnector
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!document.GridFsId.HasValue)
|
if (!document.PayloadId.HasValue)
|
||||||
{
|
{
|
||||||
_logger.LogWarning("SUSE document {DocumentId} missing GridFS payload", document.Id);
|
_logger.LogWarning("SUSE document {DocumentId} missing GridFS payload", document.Id);
|
||||||
await _documentStore.UpdateStatusAsync(document.Id, DocumentStatuses.Failed, cancellationToken).ConfigureAwait(false);
|
await _documentStore.UpdateStatusAsync(document.Id, DocumentStatuses.Failed, cancellationToken).ConfigureAwait(false);
|
||||||
@@ -313,7 +313,7 @@ public sealed class SuseConnector : IFeedConnector
|
|||||||
byte[] bytes;
|
byte[] bytes;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
bytes = await _rawDocumentStorage.DownloadAsync(document.GridFsId.Value, cancellationToken).ConfigureAwait(false);
|
bytes = await _rawDocumentStorage.DownloadAsync(document.PayloadId.Value, cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -160,7 +160,7 @@ public sealed class UbuntuConnector : IFeedConnector
|
|||||||
Metadata: metadata,
|
Metadata: metadata,
|
||||||
Etag: existing?.Etag,
|
Etag: existing?.Etag,
|
||||||
LastModified: existing?.LastModified ?? notice.Published,
|
LastModified: existing?.LastModified ?? notice.Published,
|
||||||
GridFsId: null);
|
PayloadId: null);
|
||||||
|
|
||||||
await _documentStore.UpsertAsync(record, cancellationToken).ConfigureAwait(false);
|
await _documentStore.UpsertAsync(record, cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
@@ -311,12 +311,12 @@ public sealed class UbuntuConnector : IFeedConnector
|
|||||||
fetchCache[cacheKey] = cachedEntryForPage;
|
fetchCache[cacheKey] = cachedEntryForPage;
|
||||||
|
|
||||||
var existingDocument = await _documentStore.FindBySourceAndUriAsync(SourceName, cacheKey, cancellationToken).ConfigureAwait(false);
|
var existingDocument = await _documentStore.FindBySourceAndUriAsync(SourceName, cacheKey, cancellationToken).ConfigureAwait(false);
|
||||||
if (existingDocument is null || !existingDocument.GridFsId.HasValue)
|
if (existingDocument is null || !existingDocument.PayloadId.HasValue)
|
||||||
{
|
{
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
payload = await _rawDocumentStorage.DownloadAsync(existingDocument.GridFsId.Value, cancellationToken).ConfigureAwait(false);
|
payload = await _rawDocumentStorage.DownloadAsync(existingDocument.PayloadId.Value, cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -327,13 +327,13 @@ public sealed class UbuntuConnector : IFeedConnector
|
|||||||
|
|
||||||
fetchCache[cacheKey] = UbuntuFetchCacheEntry.FromDocument(fetchResult.Document);
|
fetchCache[cacheKey] = UbuntuFetchCacheEntry.FromDocument(fetchResult.Document);
|
||||||
|
|
||||||
if (!fetchResult.Document.GridFsId.HasValue)
|
if (!fetchResult.Document.PayloadId.HasValue)
|
||||||
{
|
{
|
||||||
_logger.LogWarning("Ubuntu index document {DocumentId} missing GridFS payload", fetchResult.Document.Id);
|
_logger.LogWarning("Ubuntu index document {DocumentId} missing GridFS payload", fetchResult.Document.Id);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
payload = await _rawDocumentStorage.DownloadAsync(fetchResult.Document.GridFsId.Value, cancellationToken).ConfigureAwait(false);
|
payload = await _rawDocumentStorage.DownloadAsync(fetchResult.Document.PayloadId.Value, cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
var page = UbuntuNoticeParser.ParseIndex(Encoding.UTF8.GetString(payload));
|
var page = UbuntuNoticeParser.ParseIndex(Encoding.UTF8.GetString(payload));
|
||||||
|
|||||||
@@ -284,7 +284,7 @@ public sealed class GhsaConnector : IFeedConnector
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!document.GridFsId.HasValue)
|
if (!document.PayloadId.HasValue)
|
||||||
{
|
{
|
||||||
_diagnostics.ParseFailure();
|
_diagnostics.ParseFailure();
|
||||||
_logger.LogWarning("GHSA document {DocumentId} missing GridFS content", documentId);
|
_logger.LogWarning("GHSA document {DocumentId} missing GridFS content", documentId);
|
||||||
@@ -296,7 +296,7 @@ public sealed class GhsaConnector : IFeedConnector
|
|||||||
byte[] rawBytes;
|
byte[] rawBytes;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
rawBytes = await _rawDocumentStorage.DownloadAsync(document.GridFsId.Value, cancellationToken).ConfigureAwait(false);
|
rawBytes = await _rawDocumentStorage.DownloadAsync(document.PayloadId.Value, cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -247,7 +247,7 @@ public sealed class IcsCisaConnector : IFeedConnector
|
|||||||
topicId = topicValue;
|
topicId = topicValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!document.GridFsId.HasValue)
|
if (!document.PayloadId.HasValue)
|
||||||
{
|
{
|
||||||
await _documentStore.UpdateStatusAsync(document.Id, DocumentStatuses.Failed, cancellationToken).ConfigureAwait(false);
|
await _documentStore.UpdateStatusAsync(document.Id, DocumentStatuses.Failed, cancellationToken).ConfigureAwait(false);
|
||||||
remainingDocuments.Remove(documentId);
|
remainingDocuments.Remove(documentId);
|
||||||
@@ -259,7 +259,7 @@ public sealed class IcsCisaConnector : IFeedConnector
|
|||||||
byte[] bytes;
|
byte[] bytes;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
bytes = await _rawDocumentStorage.DownloadAsync(document.GridFsId.Value, cancellationToken).ConfigureAwait(false);
|
bytes = await _rawDocumentStorage.DownloadAsync(document.PayloadId.Value, cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -239,7 +239,7 @@ public sealed class KasperskyConnector : IFeedConnector
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!document.GridFsId.HasValue)
|
if (!document.PayloadId.HasValue)
|
||||||
{
|
{
|
||||||
_logger.LogWarning("Kaspersky document {DocumentId} missing GridFS content", document.Id);
|
_logger.LogWarning("Kaspersky document {DocumentId} missing GridFS content", document.Id);
|
||||||
await _documentStore.UpdateStatusAsync(document.Id, DocumentStatuses.Failed, cancellationToken).ConfigureAwait(false);
|
await _documentStore.UpdateStatusAsync(document.Id, DocumentStatuses.Failed, cancellationToken).ConfigureAwait(false);
|
||||||
@@ -260,7 +260,7 @@ public sealed class KasperskyConnector : IFeedConnector
|
|||||||
byte[] rawBytes;
|
byte[] rawBytes;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
rawBytes = await _rawDocumentStorage.DownloadAsync(document.GridFsId.Value, cancellationToken).ConfigureAwait(false);
|
rawBytes = await _rawDocumentStorage.DownloadAsync(document.PayloadId.Value, cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -189,7 +189,7 @@ public sealed class JvnConnector : IFeedConnector
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!document.GridFsId.HasValue)
|
if (!document.PayloadId.HasValue)
|
||||||
{
|
{
|
||||||
_logger.LogWarning("JVN document {DocumentId} is missing GridFS content; marking as failed", documentId);
|
_logger.LogWarning("JVN document {DocumentId} is missing GridFS content; marking as failed", documentId);
|
||||||
await _documentStore.UpdateStatusAsync(document.Id, DocumentStatuses.Failed, cancellationToken).ConfigureAwait(false);
|
await _documentStore.UpdateStatusAsync(document.Id, DocumentStatuses.Failed, cancellationToken).ConfigureAwait(false);
|
||||||
@@ -200,7 +200,7 @@ public sealed class JvnConnector : IFeedConnector
|
|||||||
byte[] rawBytes;
|
byte[] rawBytes;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
rawBytes = await _rawDocumentStorage.DownloadAsync(document.GridFsId.Value, cancellationToken).ConfigureAwait(false);
|
rawBytes = await _rawDocumentStorage.DownloadAsync(document.PayloadId.Value, cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -183,7 +183,7 @@ public sealed class KevConnector : IFeedConnector
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!document.GridFsId.HasValue)
|
if (!document.PayloadId.HasValue)
|
||||||
{
|
{
|
||||||
_diagnostics.ParseFailure("missingPayload", cursor.CatalogVersion);
|
_diagnostics.ParseFailure("missingPayload", cursor.CatalogVersion);
|
||||||
_logger.LogWarning("KEV document {DocumentId} missing GridFS payload", document.Id);
|
_logger.LogWarning("KEV document {DocumentId} missing GridFS payload", document.Id);
|
||||||
@@ -196,7 +196,7 @@ public sealed class KevConnector : IFeedConnector
|
|||||||
byte[] rawBytes;
|
byte[] rawBytes;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
rawBytes = await _rawDocumentStorage.DownloadAsync(document.GridFsId.Value, cancellationToken).ConfigureAwait(false);
|
rawBytes = await _rawDocumentStorage.DownloadAsync(document.PayloadId.Value, cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -244,7 +244,7 @@ public sealed class KisaConnector : IFeedConnector
|
|||||||
}
|
}
|
||||||
|
|
||||||
var category = GetCategory(document);
|
var category = GetCategory(document);
|
||||||
if (!document.GridFsId.HasValue)
|
if (!document.PayloadId.HasValue)
|
||||||
{
|
{
|
||||||
_diagnostics.ParseFailure(category, "missing-gridfs");
|
_diagnostics.ParseFailure(category, "missing-gridfs");
|
||||||
_logger.LogWarning("KISA document {DocumentId} missing GridFS payload", document.Id);
|
_logger.LogWarning("KISA document {DocumentId} missing GridFS payload", document.Id);
|
||||||
@@ -259,7 +259,7 @@ public sealed class KisaConnector : IFeedConnector
|
|||||||
byte[] payload;
|
byte[] payload;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
payload = await _rawDocumentStorage.DownloadAsync(document.GridFsId.Value, cancellationToken).ConfigureAwait(false);
|
payload = await _rawDocumentStorage.DownloadAsync(document.PayloadId.Value, cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -179,7 +179,7 @@ public sealed class NvdConnector : IFeedConnector
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!document.GridFsId.HasValue)
|
if (!document.PayloadId.HasValue)
|
||||||
{
|
{
|
||||||
_logger.LogWarning("Document {DocumentId} is missing GridFS content; skipping", documentId);
|
_logger.LogWarning("Document {DocumentId} is missing GridFS content; skipping", documentId);
|
||||||
_diagnostics.ParseFailure();
|
_diagnostics.ParseFailure();
|
||||||
@@ -188,7 +188,7 @@ public sealed class NvdConnector : IFeedConnector
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
var rawBytes = await _rawDocumentStorage.DownloadAsync(document.GridFsId.Value, cancellationToken).ConfigureAwait(false);
|
var rawBytes = await _rawDocumentStorage.DownloadAsync(document.PayloadId.Value, cancellationToken).ConfigureAwait(false);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
using var jsonDocument = JsonDocument.Parse(rawBytes);
|
using var jsonDocument = JsonDocument.Parse(rawBytes);
|
||||||
@@ -314,7 +314,7 @@ public sealed class NvdConnector : IFeedConnector
|
|||||||
DocumentRecord firstDocument,
|
DocumentRecord firstDocument,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
if (firstDocument.GridFsId is null)
|
if (firstDocument.PayloadId is null)
|
||||||
{
|
{
|
||||||
return Array.Empty<Guid>();
|
return Array.Empty<Guid>();
|
||||||
}
|
}
|
||||||
@@ -322,7 +322,7 @@ public sealed class NvdConnector : IFeedConnector
|
|||||||
byte[] rawBytes;
|
byte[] rawBytes;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
rawBytes = await _rawDocumentStorage.DownloadAsync(firstDocument.GridFsId.Value, cancellationToken).ConfigureAwait(false);
|
rawBytes = await _rawDocumentStorage.DownloadAsync(firstDocument.PayloadId.Value, cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -147,7 +147,7 @@ public sealed class OsvConnector : IFeedConnector
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!document.GridFsId.HasValue)
|
if (!document.PayloadId.HasValue)
|
||||||
{
|
{
|
||||||
_logger.LogWarning("OSV document {DocumentId} missing GridFS content", document.Id);
|
_logger.LogWarning("OSV document {DocumentId} missing GridFS content", document.Id);
|
||||||
await _documentStore.UpdateStatusAsync(document.Id, DocumentStatuses.Failed, cancellationToken).ConfigureAwait(false);
|
await _documentStore.UpdateStatusAsync(document.Id, DocumentStatuses.Failed, cancellationToken).ConfigureAwait(false);
|
||||||
@@ -158,7 +158,7 @@ public sealed class OsvConnector : IFeedConnector
|
|||||||
byte[] bytes;
|
byte[] bytes;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
bytes = await _rawDocumentStorage.DownloadAsync(document.GridFsId.Value, cancellationToken).ConfigureAwait(false);
|
bytes = await _rawDocumentStorage.DownloadAsync(document.PayloadId.Value, cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -447,7 +447,7 @@ public sealed class OsvConnector : IFeedConnector
|
|||||||
Metadata: metadata,
|
Metadata: metadata,
|
||||||
Etag: null,
|
Etag: null,
|
||||||
LastModified: modified,
|
LastModified: modified,
|
||||||
GridFsId: gridFsId,
|
PayloadId: gridFsId,
|
||||||
ExpiresAt: null);
|
ExpiresAt: null);
|
||||||
|
|
||||||
var upserted = await _documentStore.UpsertAsync(record, cancellationToken).ConfigureAwait(false);
|
var upserted = await _documentStore.UpsertAsync(record, cancellationToken).ConfigureAwait(false);
|
||||||
|
|||||||
@@ -221,7 +221,7 @@ public sealed class RuBduConnector : IFeedConnector
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!document.GridFsId.HasValue)
|
if (!document.PayloadId.HasValue)
|
||||||
{
|
{
|
||||||
_logger.LogWarning("RU-BDU document {DocumentId} missing GridFS payload", documentId);
|
_logger.LogWarning("RU-BDU document {DocumentId} missing GridFS payload", documentId);
|
||||||
_diagnostics.ParseFailure();
|
_diagnostics.ParseFailure();
|
||||||
@@ -234,7 +234,7 @@ public sealed class RuBduConnector : IFeedConnector
|
|||||||
byte[] payload;
|
byte[] payload;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
payload = await _rawDocumentStorage.DownloadAsync(document.GridFsId.Value, cancellationToken).ConfigureAwait(false);
|
payload = await _rawDocumentStorage.DownloadAsync(document.PayloadId.Value, cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -435,7 +435,7 @@ public sealed class RuBduConnector : IFeedConnector
|
|||||||
Metadata: metadata,
|
Metadata: metadata,
|
||||||
Etag: null,
|
Etag: null,
|
||||||
LastModified: archiveLastModified ?? dto.IdentifyDate,
|
LastModified: archiveLastModified ?? dto.IdentifyDate,
|
||||||
GridFsId: gridFsId,
|
PayloadId: gridFsId,
|
||||||
ExpiresAt: null);
|
ExpiresAt: null);
|
||||||
|
|
||||||
var upserted = await _documentStore.UpsertAsync(record, cancellationToken).ConfigureAwait(false);
|
var upserted = await _documentStore.UpsertAsync(record, cancellationToken).ConfigureAwait(false);
|
||||||
|
|||||||
@@ -295,7 +295,7 @@ public sealed class RuNkckiConnector : IFeedConnector
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!document.GridFsId.HasValue)
|
if (!document.PayloadId.HasValue)
|
||||||
{
|
{
|
||||||
_logger.LogWarning("NKCKI document {DocumentId} missing GridFS payload", documentId);
|
_logger.LogWarning("NKCKI document {DocumentId} missing GridFS payload", documentId);
|
||||||
await _documentStore.UpdateStatusAsync(document.Id, DocumentStatuses.Failed, cancellationToken).ConfigureAwait(false);
|
await _documentStore.UpdateStatusAsync(document.Id, DocumentStatuses.Failed, cancellationToken).ConfigureAwait(false);
|
||||||
@@ -307,7 +307,7 @@ public sealed class RuNkckiConnector : IFeedConnector
|
|||||||
byte[] payload;
|
byte[] payload;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
payload = await _rawDocumentStorage.DownloadAsync(document.GridFsId.Value, cancellationToken).ConfigureAwait(false);
|
payload = await _rawDocumentStorage.DownloadAsync(document.PayloadId.Value, cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -641,7 +641,7 @@ public sealed class RuNkckiConnector : IFeedConnector
|
|||||||
Metadata: metadata,
|
Metadata: metadata,
|
||||||
Etag: null,
|
Etag: null,
|
||||||
LastModified: lastModified,
|
LastModified: lastModified,
|
||||||
GridFsId: gridFsId,
|
PayloadId: gridFsId,
|
||||||
ExpiresAt: null);
|
ExpiresAt: null);
|
||||||
|
|
||||||
var upserted = await _documentStore.UpsertAsync(record, cancellationToken).ConfigureAwait(false);
|
var upserted = await _documentStore.UpsertAsync(record, cancellationToken).ConfigureAwait(false);
|
||||||
|
|||||||
@@ -251,7 +251,7 @@ public sealed class StellaOpsMirrorConnector : IFeedConnector
|
|||||||
Metadata: metadata,
|
Metadata: metadata,
|
||||||
Etag: null,
|
Etag: null,
|
||||||
LastModified: generatedAt,
|
LastModified: generatedAt,
|
||||||
GridFsId: gridFsId,
|
PayloadId: gridFsId,
|
||||||
ExpiresAt: null);
|
ExpiresAt: null);
|
||||||
|
|
||||||
var upserted = await _documentStore.UpsertAsync(record, cancellationToken).ConfigureAwait(false);
|
var upserted = await _documentStore.UpsertAsync(record, cancellationToken).ConfigureAwait(false);
|
||||||
@@ -372,7 +372,7 @@ public sealed class StellaOpsMirrorConnector : IFeedConnector
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!document.GridFsId.HasValue)
|
if (!document.PayloadId.HasValue)
|
||||||
{
|
{
|
||||||
_logger.LogWarning("Mirror bundle document {DocumentId} missing GridFS payload.", documentId);
|
_logger.LogWarning("Mirror bundle document {DocumentId} missing GridFS payload.", documentId);
|
||||||
await _documentStore.UpdateStatusAsync(document.Id, DocumentStatuses.Failed, cancellationToken).ConfigureAwait(false);
|
await _documentStore.UpdateStatusAsync(document.Id, DocumentStatuses.Failed, cancellationToken).ConfigureAwait(false);
|
||||||
@@ -385,7 +385,7 @@ public sealed class StellaOpsMirrorConnector : IFeedConnector
|
|||||||
byte[] payload;
|
byte[] payload;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
payload = await _rawDocumentStorage.DownloadAsync(document.GridFsId.Value, cancellationToken).ConfigureAwait(false);
|
payload = await _rawDocumentStorage.DownloadAsync(document.PayloadId.Value, cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -452,7 +452,7 @@ public sealed class AdobeConnector : IFeedConnector
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!document.GridFsId.HasValue)
|
if (!document.PayloadId.HasValue)
|
||||||
{
|
{
|
||||||
_logger.LogWarning("Adobe document {DocumentId} missing GridFS payload", document.Id);
|
_logger.LogWarning("Adobe document {DocumentId} missing GridFS payload", document.Id);
|
||||||
await _documentStore.UpdateStatusAsync(document.Id, DocumentStatuses.Failed, cancellationToken).ConfigureAwait(false);
|
await _documentStore.UpdateStatusAsync(document.Id, DocumentStatuses.Failed, cancellationToken).ConfigureAwait(false);
|
||||||
@@ -478,7 +478,7 @@ public sealed class AdobeConnector : IFeedConnector
|
|||||||
AdobeBulletinDto dto;
|
AdobeBulletinDto dto;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var bytes = await _rawDocumentStorage.DownloadAsync(document.GridFsId.Value, cancellationToken).ConfigureAwait(false);
|
var bytes = await _rawDocumentStorage.DownloadAsync(document.PayloadId.Value, cancellationToken).ConfigureAwait(false);
|
||||||
var html = Encoding.UTF8.GetString(bytes);
|
var html = Encoding.UTF8.GetString(bytes);
|
||||||
dto = AdobeDetailParser.Parse(html, metadata);
|
dto = AdobeDetailParser.Parse(html, metadata);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -225,7 +225,7 @@ public sealed class AppleConnector : IFeedConnector
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!document.GridFsId.HasValue)
|
if (!document.PayloadId.HasValue)
|
||||||
{
|
{
|
||||||
_diagnostics.ParseFailure();
|
_diagnostics.ParseFailure();
|
||||||
_logger.LogWarning("Apple document {DocumentId} missing GridFS payload", document.Id);
|
_logger.LogWarning("Apple document {DocumentId} missing GridFS payload", document.Id);
|
||||||
@@ -238,7 +238,7 @@ public sealed class AppleConnector : IFeedConnector
|
|||||||
AppleDetailDto dto;
|
AppleDetailDto dto;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var content = await _rawDocumentStorage.DownloadAsync(document.GridFsId.Value, cancellationToken).ConfigureAwait(false);
|
var content = await _rawDocumentStorage.DownloadAsync(document.PayloadId.Value, cancellationToken).ConfigureAwait(false);
|
||||||
var html = System.Text.Encoding.UTF8.GetString(content);
|
var html = System.Text.Encoding.UTF8.GetString(content);
|
||||||
var entry = RehydrateIndexEntry(document);
|
var entry = RehydrateIndexEntry(document);
|
||||||
dto = AppleDetailParser.Parse(html, entry);
|
dto = AppleDetailParser.Parse(html, entry);
|
||||||
|
|||||||
@@ -214,7 +214,7 @@ public sealed class ChromiumConnector : IFeedConnector
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!document.GridFsId.HasValue)
|
if (!document.PayloadId.HasValue)
|
||||||
{
|
{
|
||||||
_logger.LogWarning("Chromium document {DocumentId} missing GridFS payload", document.Id);
|
_logger.LogWarning("Chromium document {DocumentId} missing GridFS payload", document.Id);
|
||||||
await _documentStore.UpdateStatusAsync(document.Id, DocumentStatuses.Failed, cancellationToken).ConfigureAwait(false);
|
await _documentStore.UpdateStatusAsync(document.Id, DocumentStatuses.Failed, cancellationToken).ConfigureAwait(false);
|
||||||
@@ -227,7 +227,7 @@ public sealed class ChromiumConnector : IFeedConnector
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
var metadata = ChromiumDocumentMetadata.FromDocument(document);
|
var metadata = ChromiumDocumentMetadata.FromDocument(document);
|
||||||
var content = await _rawDocumentStorage.DownloadAsync(document.GridFsId.Value, cancellationToken).ConfigureAwait(false);
|
var content = await _rawDocumentStorage.DownloadAsync(document.PayloadId.Value, cancellationToken).ConfigureAwait(false);
|
||||||
var html = Encoding.UTF8.GetString(content);
|
var html = Encoding.UTF8.GetString(content);
|
||||||
dto = ChromiumParser.Parse(html, metadata);
|
dto = ChromiumParser.Parse(html, metadata);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -163,7 +163,7 @@ public sealed class CiscoConnector : IFeedConnector
|
|||||||
BuildMetadata(advisory),
|
BuildMetadata(advisory),
|
||||||
Etag: null,
|
Etag: null,
|
||||||
LastModified: advisory.LastUpdated ?? advisory.FirstPublished ?? now,
|
LastModified: advisory.LastUpdated ?? advisory.FirstPublished ?? now,
|
||||||
GridFsId: gridFsId,
|
PayloadId: gridFsId,
|
||||||
ExpiresAt: null);
|
ExpiresAt: null);
|
||||||
|
|
||||||
var upserted = await _documentStore.UpsertAsync(record, cancellationToken).ConfigureAwait(false);
|
var upserted = await _documentStore.UpsertAsync(record, cancellationToken).ConfigureAwait(false);
|
||||||
@@ -259,7 +259,7 @@ public sealed class CiscoConnector : IFeedConnector
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!document.GridFsId.HasValue)
|
if (!document.PayloadId.HasValue)
|
||||||
{
|
{
|
||||||
_diagnostics.ParseFailure();
|
_diagnostics.ParseFailure();
|
||||||
_logger.LogWarning("Cisco document {DocumentId} missing GridFS payload", documentId);
|
_logger.LogWarning("Cisco document {DocumentId} missing GridFS payload", documentId);
|
||||||
@@ -273,7 +273,7 @@ public sealed class CiscoConnector : IFeedConnector
|
|||||||
byte[] payload;
|
byte[] payload;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
payload = await _rawDocumentStorage.DownloadAsync(document.GridFsId.Value, cancellationToken).ConfigureAwait(false);
|
payload = await _rawDocumentStorage.DownloadAsync(document.PayloadId.Value, cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -133,7 +133,7 @@ public sealed class MsrcConnector : IFeedConnector
|
|||||||
}
|
}
|
||||||
|
|
||||||
_diagnostics.DetailFetchAttempt();
|
_diagnostics.DetailFetchAttempt();
|
||||||
if (existing?.GridFsId is { } oldGridId)
|
if (existing?.PayloadId is { } oldGridId)
|
||||||
{
|
{
|
||||||
await _rawDocumentStorage.DeleteAsync(oldGridId, cancellationToken).ConfigureAwait(false);
|
await _rawDocumentStorage.DeleteAsync(oldGridId, cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
@@ -238,7 +238,7 @@ public sealed class MsrcConnector : IFeedConnector
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!document.GridFsId.HasValue)
|
if (!document.PayloadId.HasValue)
|
||||||
{
|
{
|
||||||
await _documentStore.UpdateStatusAsync(document.Id, DocumentStatuses.Failed, cancellationToken).ConfigureAwait(false);
|
await _documentStore.UpdateStatusAsync(document.Id, DocumentStatuses.Failed, cancellationToken).ConfigureAwait(false);
|
||||||
remainingDocuments.Remove(documentId);
|
remainingDocuments.Remove(documentId);
|
||||||
@@ -250,7 +250,7 @@ public sealed class MsrcConnector : IFeedConnector
|
|||||||
byte[] payload;
|
byte[] payload;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
payload = await _rawDocumentStorage.DownloadAsync(document.GridFsId.Value, cancellationToken).ConfigureAwait(false);
|
payload = await _rawDocumentStorage.DownloadAsync(document.PayloadId.Value, cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -185,7 +185,7 @@ public sealed class OracleConnector : IFeedConnector
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!document.GridFsId.HasValue)
|
if (!document.PayloadId.HasValue)
|
||||||
{
|
{
|
||||||
_logger.LogWarning("Oracle document {DocumentId} missing GridFS payload", document.Id);
|
_logger.LogWarning("Oracle document {DocumentId} missing GridFS payload", document.Id);
|
||||||
await _documentStore.UpdateStatusAsync(document.Id, DocumentStatuses.Failed, cancellationToken).ConfigureAwait(false);
|
await _documentStore.UpdateStatusAsync(document.Id, DocumentStatuses.Failed, cancellationToken).ConfigureAwait(false);
|
||||||
@@ -198,7 +198,7 @@ public sealed class OracleConnector : IFeedConnector
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
var metadata = OracleDocumentMetadata.FromDocument(document);
|
var metadata = OracleDocumentMetadata.FromDocument(document);
|
||||||
var content = await _rawDocumentStorage.DownloadAsync(document.GridFsId.Value, cancellationToken).ConfigureAwait(false);
|
var content = await _rawDocumentStorage.DownloadAsync(document.PayloadId.Value, cancellationToken).ConfigureAwait(false);
|
||||||
var html = System.Text.Encoding.UTF8.GetString(content);
|
var html = System.Text.Encoding.UTF8.GetString(content);
|
||||||
dto = OracleParser.Parse(html, metadata);
|
dto = OracleParser.Parse(html, metadata);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -299,7 +299,7 @@ public sealed class VmwareConnector : IFeedConnector
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!document.GridFsId.HasValue)
|
if (!document.PayloadId.HasValue)
|
||||||
{
|
{
|
||||||
_logger.LogWarning("VMware document {DocumentId} missing GridFS payload", document.Id);
|
_logger.LogWarning("VMware document {DocumentId} missing GridFS payload", document.Id);
|
||||||
await _documentStore.UpdateStatusAsync(document.Id, DocumentStatuses.Failed, cancellationToken).ConfigureAwait(false);
|
await _documentStore.UpdateStatusAsync(document.Id, DocumentStatuses.Failed, cancellationToken).ConfigureAwait(false);
|
||||||
@@ -311,7 +311,7 @@ public sealed class VmwareConnector : IFeedConnector
|
|||||||
byte[] bytes;
|
byte[] bytes;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
bytes = await _rawDocumentStorage.DownloadAsync(document.GridFsId.Value, cancellationToken).ConfigureAwait(false);
|
bytes = await _rawDocumentStorage.DownloadAsync(document.PayloadId.Value, cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\StellaOps.Concelier.Models\StellaOps.Concelier.Models.csproj" />
|
<ProjectReference Include="..\StellaOps.Concelier.Models\StellaOps.Concelier.Models.csproj" />
|
||||||
<ProjectReference Include="..\StellaOps.Concelier.Normalization\StellaOps.Concelier.Normalization.csproj" />
|
<ProjectReference Include="..\StellaOps.Concelier.Normalization\StellaOps.Concelier.Normalization.csproj" />
|
||||||
|
<ProjectReference Include="..\StellaOps.Concelier.Core\StellaOps.Concelier.Core.csproj" />
|
||||||
<ProjectReference Include="../../../__Libraries/StellaOps.Plugin/StellaOps.Plugin.csproj" />
|
<ProjectReference Include="../../../__Libraries/StellaOps.Plugin/StellaOps.Plugin.csproj" />
|
||||||
<ProjectReference Include="../../../__Libraries/StellaOps.DependencyInjection/StellaOps.DependencyInjection.csproj" />
|
<ProjectReference Include="../../../__Libraries/StellaOps.DependencyInjection/StellaOps.DependencyInjection.csproj" />
|
||||||
<ProjectReference Include="../../../__Libraries/StellaOps.Cryptography/StellaOps.Cryptography.csproj" />
|
<ProjectReference Include="../../../__Libraries/StellaOps.Cryptography/StellaOps.Cryptography.csproj" />
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\StellaOps.Concelier.Exporter.Json\StellaOps.Concelier.Exporter.Json.csproj" />
|
<ProjectReference Include="..\StellaOps.Concelier.Exporter.Json\StellaOps.Concelier.Exporter.Json.csproj" />
|
||||||
<ProjectReference Include="..\StellaOps.Concelier.Models\StellaOps.Concelier.Models.csproj" />
|
<ProjectReference Include="..\StellaOps.Concelier.Models\StellaOps.Concelier.Models.csproj" />
|
||||||
|
<ProjectReference Include="..\StellaOps.Concelier.Core\StellaOps.Concelier.Core.csproj" />
|
||||||
<ProjectReference Include="../../../__Libraries/StellaOps.DependencyInjection/StellaOps.DependencyInjection.csproj" />
|
<ProjectReference Include="../../../__Libraries/StellaOps.DependencyInjection/StellaOps.DependencyInjection.csproj" />
|
||||||
<ProjectReference Include="../../../__Libraries/StellaOps.Plugin/StellaOps.Plugin.csproj" />
|
<ProjectReference Include="../../../__Libraries/StellaOps.Plugin/StellaOps.Plugin.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ namespace MongoDB.Bson
|
|||||||
{
|
{
|
||||||
protected readonly object? _value;
|
protected readonly object? _value;
|
||||||
public BsonValue(object? value) => _value = value;
|
public BsonValue(object? value) => _value = value;
|
||||||
|
internal object? RawValue => _value;
|
||||||
|
public static BsonValue Create(object? value) => BsonDocument.WrapExternal(value);
|
||||||
public virtual BsonType BsonType => _value switch
|
public virtual BsonType BsonType => _value switch
|
||||||
{
|
{
|
||||||
null => BsonType.Null,
|
null => BsonType.Null,
|
||||||
@@ -59,12 +61,24 @@ namespace MongoDB.Bson
|
|||||||
public class BsonInt64 : BsonValue { public BsonInt64(long value) : base(value) { } }
|
public class BsonInt64 : BsonValue { public BsonInt64(long value) : base(value) { } }
|
||||||
public class BsonDouble : BsonValue { public BsonDouble(double value) : base(value) { } }
|
public class BsonDouble : BsonValue { public BsonDouble(double value) : base(value) { } }
|
||||||
public class BsonDateTime : BsonValue { public BsonDateTime(DateTime value) : base(value) { } }
|
public class BsonDateTime : BsonValue { public BsonDateTime(DateTime value) : base(value) { } }
|
||||||
|
public class BsonNull : BsonValue
|
||||||
|
{
|
||||||
|
private BsonNull() : base(null) { }
|
||||||
|
public static BsonNull Value { get; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
public class BsonArray : BsonValue, IEnumerable<BsonValue>
|
public class BsonArray : BsonValue, IEnumerable<BsonValue>
|
||||||
{
|
{
|
||||||
private readonly List<BsonValue> _items = new();
|
private readonly List<BsonValue> _items = new();
|
||||||
public BsonArray() : base(null) { }
|
public BsonArray() : base(null) { }
|
||||||
public BsonArray(IEnumerable<BsonValue> values) : this() => _items.AddRange(values);
|
public BsonArray(IEnumerable<BsonValue> values) : this() => _items.AddRange(values);
|
||||||
|
public BsonArray(IEnumerable<object?> values) : this()
|
||||||
|
{
|
||||||
|
foreach (var value in values)
|
||||||
|
{
|
||||||
|
_items.Add(BsonDocument.WrapExternal(value));
|
||||||
|
}
|
||||||
|
}
|
||||||
public void Add(BsonValue value) => _items.Add(value);
|
public void Add(BsonValue value) => _items.Add(value);
|
||||||
public IEnumerator<BsonValue> GetEnumerator() => _items.GetEnumerator();
|
public IEnumerator<BsonValue> GetEnumerator() => _items.GetEnumerator();
|
||||||
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
|
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
|
||||||
@@ -93,6 +107,8 @@ namespace MongoDB.Bson
|
|||||||
_ => new BsonValue(value)
|
_ => new BsonValue(value)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
internal static BsonValue WrapExternal(object? value) => Wrap(value);
|
||||||
|
|
||||||
public BsonValue this[string key]
|
public BsonValue this[string key]
|
||||||
{
|
{
|
||||||
get => _values[key];
|
get => _values[key];
|
||||||
@@ -104,6 +120,7 @@ namespace MongoDB.Bson
|
|||||||
public bool TryGetValue(string key, out BsonValue value) => _values.TryGetValue(key, out value!);
|
public bool TryGetValue(string key, out BsonValue value) => _values.TryGetValue(key, out value!);
|
||||||
|
|
||||||
public void Add(string key, BsonValue value) => _values[key] = value;
|
public void Add(string key, BsonValue value) => _values[key] = value;
|
||||||
|
public void Add(string key, object? value) => _values[key] = Wrap(value);
|
||||||
|
|
||||||
public IEnumerator<KeyValuePair<string, BsonValue>> GetEnumerator() => _values.GetEnumerator();
|
public IEnumerator<KeyValuePair<string, BsonValue>> GetEnumerator() => _values.GetEnumerator();
|
||||||
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
|
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
|
||||||
@@ -156,7 +173,7 @@ namespace MongoDB.Bson
|
|||||||
{
|
{
|
||||||
BsonDocument doc => doc._values.ToDictionary(kvp => kvp.Key, kvp => Unwrap(kvp.Value)),
|
BsonDocument doc => doc._values.ToDictionary(kvp => kvp.Key, kvp => Unwrap(kvp.Value)),
|
||||||
BsonArray array => array.Select(Unwrap).ToArray(),
|
BsonArray array => array.Select(Unwrap).ToArray(),
|
||||||
_ => value._value
|
_ => value.RawValue
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
|
using System.IO;
|
||||||
using StellaOps.Concelier.Models;
|
using StellaOps.Concelier.Models;
|
||||||
|
|
||||||
namespace StellaOps.Concelier.Storage.Mongo
|
namespace StellaOps.Concelier.Storage.Mongo
|
||||||
@@ -33,8 +34,9 @@ namespace StellaOps.Concelier.Storage.Mongo
|
|||||||
IReadOnlyDictionary<string, string>? Metadata = null,
|
IReadOnlyDictionary<string, string>? Metadata = null,
|
||||||
string? Etag = null,
|
string? Etag = null,
|
||||||
DateTimeOffset? LastModified = null,
|
DateTimeOffset? LastModified = null,
|
||||||
MongoDB.Bson.ObjectId? GridFsId = null,
|
Guid? PayloadId = null,
|
||||||
DateTimeOffset? ExpiresAt = null);
|
DateTimeOffset? ExpiresAt = null,
|
||||||
|
byte[]? Payload = null);
|
||||||
|
|
||||||
public interface IDocumentStore
|
public interface IDocumentStore
|
||||||
{
|
{
|
||||||
@@ -85,7 +87,7 @@ namespace StellaOps.Concelier.Storage.Mongo
|
|||||||
Guid DocumentId,
|
Guid DocumentId,
|
||||||
string SourceName,
|
string SourceName,
|
||||||
string Format,
|
string Format,
|
||||||
MongoDB.Bson.BsonDocument Payload,
|
string Payload,
|
||||||
DateTimeOffset CreatedAt);
|
DateTimeOffset CreatedAt);
|
||||||
|
|
||||||
public interface IDtoStore
|
public interface IDtoStore
|
||||||
@@ -113,40 +115,40 @@ namespace StellaOps.Concelier.Storage.Mongo
|
|||||||
|
|
||||||
public sealed class RawDocumentStorage
|
public sealed class RawDocumentStorage
|
||||||
{
|
{
|
||||||
private readonly ConcurrentDictionary<MongoDB.Bson.ObjectId, byte[]> _blobs = new();
|
private readonly ConcurrentDictionary<Guid, byte[]> _blobs = new();
|
||||||
|
|
||||||
public Task<MongoDB.Bson.ObjectId> UploadAsync(string sourceName, string uri, byte[] content, string? contentType, DateTimeOffset? expiresAt, CancellationToken cancellationToken)
|
public Task<Guid> UploadAsync(string sourceName, string uri, byte[] content, string? contentType, DateTimeOffset? expiresAt, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var id = MongoDB.Bson.ObjectId.GenerateNewId();
|
var id = Guid.NewGuid();
|
||||||
_blobs[id] = content.ToArray();
|
_blobs[id] = content.ToArray();
|
||||||
return Task.FromResult(id);
|
return Task.FromResult(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<MongoDB.Bson.ObjectId> UploadAsync(string sourceName, string uri, byte[] content, string? contentType, CancellationToken cancellationToken)
|
public Task<Guid> UploadAsync(string sourceName, string uri, byte[] content, string? contentType, CancellationToken cancellationToken)
|
||||||
=> UploadAsync(sourceName, uri, content, contentType, null, cancellationToken);
|
=> UploadAsync(sourceName, uri, content, contentType, null, cancellationToken);
|
||||||
|
|
||||||
public Task<byte[]> DownloadAsync(MongoDB.Bson.ObjectId id, CancellationToken cancellationToken)
|
public Task<byte[]> DownloadAsync(Guid id, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
if (_blobs.TryGetValue(id, out var bytes))
|
if (_blobs.TryGetValue(id, out var bytes))
|
||||||
{
|
{
|
||||||
return Task.FromResult(bytes);
|
return Task.FromResult(bytes);
|
||||||
}
|
}
|
||||||
throw new MongoDB.Driver.GridFSFileNotFoundException($"Blob {id} not found.");
|
throw new FileNotFoundException($"Blob {id} not found.");
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task DeleteAsync(MongoDB.Bson.ObjectId id, CancellationToken cancellationToken)
|
public Task DeleteAsync(Guid id, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
_blobs.TryRemove(id, out _);
|
_blobs.TryRemove(id, out _);
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed record SourceStateRecord(string SourceName, MongoDB.Bson.BsonDocument? Cursor, DateTimeOffset UpdatedAt);
|
public sealed record SourceStateRecord(string SourceName, string? CursorJson, DateTimeOffset UpdatedAt);
|
||||||
|
|
||||||
public interface ISourceStateRepository
|
public interface ISourceStateRepository
|
||||||
{
|
{
|
||||||
Task<SourceStateRecord?> TryGetAsync(string sourceName, CancellationToken cancellationToken);
|
Task<SourceStateRecord?> TryGetAsync(string sourceName, CancellationToken cancellationToken);
|
||||||
Task UpdateCursorAsync(string sourceName, MongoDB.Bson.BsonDocument cursor, DateTimeOffset completedAt, CancellationToken cancellationToken);
|
Task UpdateCursorAsync(string sourceName, string cursorJson, DateTimeOffset completedAt, CancellationToken cancellationToken);
|
||||||
Task MarkFailureAsync(string sourceName, DateTimeOffset now, TimeSpan backoff, string reason, CancellationToken cancellationToken);
|
Task MarkFailureAsync(string sourceName, DateTimeOffset now, TimeSpan backoff, string reason, CancellationToken cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -160,9 +162,9 @@ namespace StellaOps.Concelier.Storage.Mongo
|
|||||||
return Task.FromResult<SourceStateRecord?>(record);
|
return Task.FromResult<SourceStateRecord?>(record);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task UpdateCursorAsync(string sourceName, MongoDB.Bson.BsonDocument cursor, DateTimeOffset completedAt, CancellationToken cancellationToken)
|
public Task UpdateCursorAsync(string sourceName, string cursorJson, DateTimeOffset completedAt, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
_states[sourceName] = new SourceStateRecord(sourceName, cursor.DeepClone(), completedAt);
|
_states[sourceName] = new SourceStateRecord(sourceName, cursorJson, completedAt);
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -174,6 +176,53 @@ namespace StellaOps.Concelier.Storage.Mongo
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
namespace StellaOps.Concelier.Storage.Mongo.Advisories
|
||||||
|
{
|
||||||
|
public interface IAdvisoryStore
|
||||||
|
{
|
||||||
|
Task UpsertAsync(Advisory advisory, CancellationToken cancellationToken);
|
||||||
|
Task<Advisory?> FindAsync(string advisoryKey, CancellationToken cancellationToken);
|
||||||
|
Task<IReadOnlyList<Advisory>> GetRecentAsync(int limit, CancellationToken cancellationToken);
|
||||||
|
IAsyncEnumerable<Advisory> StreamAsync(CancellationToken cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class InMemoryAdvisoryStore : IAdvisoryStore
|
||||||
|
{
|
||||||
|
private readonly ConcurrentDictionary<string, Advisory> _advisories = new(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
public Task UpsertAsync(Advisory advisory, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
_advisories[advisory.AdvisoryKey] = advisory;
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<Advisory?> FindAsync(string advisoryKey, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
_advisories.TryGetValue(advisoryKey, out var advisory);
|
||||||
|
return Task.FromResult<Advisory?>(advisory);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<IReadOnlyList<Advisory>> GetRecentAsync(int limit, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var result = _advisories.Values
|
||||||
|
.OrderByDescending(a => a.Modified ?? a.Published ?? DateTimeOffset.MinValue)
|
||||||
|
.Take(limit)
|
||||||
|
.ToArray();
|
||||||
|
return Task.FromResult<IReadOnlyList<Advisory>>(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async IAsyncEnumerable<Advisory> StreamAsync([System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
foreach (var advisory in _advisories.Values.OrderBy(a => a.AdvisoryKey, StringComparer.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
yield return advisory;
|
||||||
|
await Task.Yield();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
namespace StellaOps.Concelier.Storage.Mongo.Aliases
|
namespace StellaOps.Concelier.Storage.Mongo.Aliases
|
||||||
{
|
{
|
||||||
public sealed record AliasRecord(string AdvisoryKey, string Scheme, string Value);
|
public sealed record AliasRecord(string AdvisoryKey, string Scheme, string Value);
|
||||||
@@ -192,13 +241,13 @@ namespace StellaOps.Concelier.Storage.Mongo.Aliases
|
|||||||
public Task<IReadOnlyList<AliasRecord>> GetByAdvisoryAsync(string advisoryKey, CancellationToken cancellationToken)
|
public Task<IReadOnlyList<AliasRecord>> GetByAdvisoryAsync(string advisoryKey, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
_byAdvisory.TryGetValue(advisoryKey, out var records);
|
_byAdvisory.TryGetValue(advisoryKey, out var records);
|
||||||
return Task.FromResult<IReadOnlyList<AliasRecord>>(records ?? Array.Empty<AliasRecord>());
|
return Task.FromResult<IReadOnlyList<AliasRecord>>(records ?? (IReadOnlyList<AliasRecord>)Array.Empty<AliasRecord>());
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<IReadOnlyList<AliasRecord>> GetByAliasAsync(string scheme, string value, CancellationToken cancellationToken)
|
public Task<IReadOnlyList<AliasRecord>> GetByAliasAsync(string scheme, string value, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
_byAlias.TryGetValue((scheme, value), out var records);
|
_byAlias.TryGetValue((scheme, value), out var records);
|
||||||
return Task.FromResult<IReadOnlyList<AliasRecord>>(records ?? Array.Empty<AliasRecord>());
|
return Task.FromResult<IReadOnlyList<AliasRecord>>(records ?? (IReadOnlyList<AliasRecord>)Array.Empty<AliasRecord>());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -286,10 +335,10 @@ namespace StellaOps.Concelier.Storage.Mongo.Exporting
|
|||||||
id,
|
id,
|
||||||
cursor ?? digest,
|
cursor ?? digest,
|
||||||
digest,
|
digest,
|
||||||
lastDeltaDigest: null,
|
LastDeltaDigest: null,
|
||||||
baseExportId: resetBaseline ? exportId : null,
|
BaseExportId: resetBaseline ? exportId : null,
|
||||||
baseDigest: resetBaseline ? digest : null,
|
BaseDigest: resetBaseline ? digest : null,
|
||||||
targetRepository,
|
TargetRepository: targetRepository,
|
||||||
manifest,
|
manifest,
|
||||||
exporterVersion,
|
exporterVersion,
|
||||||
_timeProvider.GetUtcNow());
|
_timeProvider.GetUtcNow());
|
||||||
@@ -307,11 +356,11 @@ namespace StellaOps.Concelier.Storage.Mongo.Exporting
|
|||||||
var record = new ExportStateRecord(
|
var record = new ExportStateRecord(
|
||||||
id,
|
id,
|
||||||
cursor ?? deltaDigest,
|
cursor ?? deltaDigest,
|
||||||
lastFullDigest: null,
|
LastFullDigest: null,
|
||||||
lastDeltaDigest: deltaDigest,
|
LastDeltaDigest: deltaDigest,
|
||||||
baseExportId: null,
|
BaseExportId: null,
|
||||||
baseDigest: null,
|
BaseDigest: null,
|
||||||
targetRepository: null,
|
TargetRepository: null,
|
||||||
manifest,
|
manifest,
|
||||||
exporterVersion,
|
exporterVersion,
|
||||||
_timeProvider.GetUtcNow());
|
_timeProvider.GetUtcNow());
|
||||||
|
|||||||
@@ -0,0 +1,88 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
using StellaOps.Concelier.Storage.Mongo;
|
||||||
|
using StellaOps.Concelier.Storage.Postgres.Models;
|
||||||
|
using StellaOps.Concelier.Storage.Postgres.Repositories;
|
||||||
|
|
||||||
|
namespace StellaOps.Concelier.Storage.Postgres;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Postgres-backed implementation that satisfies the legacy IDocumentStore contract.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class PostgresDocumentStore : IDocumentStore
|
||||||
|
{
|
||||||
|
private readonly IDocumentRepository _repository;
|
||||||
|
private readonly ISourceRepository _sourceRepository;
|
||||||
|
private readonly JsonSerializerOptions _json = new(JsonSerializerDefaults.Web);
|
||||||
|
|
||||||
|
public PostgresDocumentStore(IDocumentRepository repository, ISourceRepository sourceRepository)
|
||||||
|
{
|
||||||
|
_repository = repository ?? throw new ArgumentNullException(nameof(repository));
|
||||||
|
_sourceRepository = sourceRepository ?? throw new ArgumentNullException(nameof(sourceRepository));
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<DocumentRecord?> FindAsync(Guid id, CancellationToken cancellationToken, MongoDB.Driver.IClientSessionHandle? session = null)
|
||||||
|
{
|
||||||
|
var row = await _repository.FindAsync(id, cancellationToken).ConfigureAwait(false);
|
||||||
|
return row is null ? null : Map(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<DocumentRecord?> FindBySourceAndUriAsync(string sourceName, string uri, CancellationToken cancellationToken, MongoDB.Driver.IClientSessionHandle? session = null)
|
||||||
|
{
|
||||||
|
var row = await _repository.FindBySourceAndUriAsync(sourceName, uri, cancellationToken).ConfigureAwait(false);
|
||||||
|
return row is null ? null : Map(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<DocumentRecord> UpsertAsync(DocumentRecord record, CancellationToken cancellationToken, MongoDB.Driver.IClientSessionHandle? session = null)
|
||||||
|
{
|
||||||
|
// Ensure source exists
|
||||||
|
var source = await _sourceRepository.GetByNameAsync(record.SourceName, cancellationToken).ConfigureAwait(false)
|
||||||
|
?? throw new InvalidOperationException($"Source '{record.SourceName}' not provisioned.");
|
||||||
|
|
||||||
|
var entity = new DocumentRecordEntity(
|
||||||
|
Id: record.Id == Guid.Empty ? Guid.NewGuid() : record.Id,
|
||||||
|
SourceId: source.Id,
|
||||||
|
SourceName: record.SourceName,
|
||||||
|
Uri: record.Uri,
|
||||||
|
Sha256: record.Sha256,
|
||||||
|
Status: record.Status,
|
||||||
|
ContentType: record.ContentType,
|
||||||
|
HeadersJson: record.Headers is null ? null : JsonSerializer.Serialize(record.Headers, _json),
|
||||||
|
MetadataJson: record.Metadata is null ? null : JsonSerializer.Serialize(record.Metadata, _json),
|
||||||
|
Etag: record.Etag,
|
||||||
|
LastModified: record.LastModified,
|
||||||
|
Payload: Array.Empty<byte>(), // payload handled via RawDocumentStorage; keep pointer zero-length here
|
||||||
|
CreatedAt: record.CreatedAt,
|
||||||
|
UpdatedAt: DateTimeOffset.UtcNow,
|
||||||
|
ExpiresAt: record.ExpiresAt);
|
||||||
|
|
||||||
|
var saved = await _repository.UpsertAsync(entity, cancellationToken).ConfigureAwait(false);
|
||||||
|
return Map(saved);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task UpdateStatusAsync(Guid id, string status, CancellationToken cancellationToken, MongoDB.Driver.IClientSessionHandle? session = null)
|
||||||
|
{
|
||||||
|
await _repository.UpdateStatusAsync(id, status, cancellationToken).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private DocumentRecord Map(DocumentRecordEntity row)
|
||||||
|
{
|
||||||
|
return new DocumentRecord(
|
||||||
|
row.Id,
|
||||||
|
row.SourceName,
|
||||||
|
row.Uri,
|
||||||
|
row.CreatedAt,
|
||||||
|
row.Sha256,
|
||||||
|
row.Status,
|
||||||
|
row.ContentType,
|
||||||
|
row.HeadersJson is null
|
||||||
|
? null
|
||||||
|
: JsonSerializer.Deserialize<Dictionary<string, string>>(row.HeadersJson, _json),
|
||||||
|
row.MetadataJson is null
|
||||||
|
? null
|
||||||
|
: JsonSerializer.Deserialize<Dictionary<string, string>>(row.MetadataJson, _json),
|
||||||
|
row.Etag,
|
||||||
|
row.LastModified,
|
||||||
|
PayloadId: null,
|
||||||
|
ExpiresAt: row.ExpiresAt);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
-- Concelier Postgres Migration 004: Source documents and payload storage (Mongo replacement)
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS concelier.source_documents (
|
||||||
|
id UUID NOT NULL,
|
||||||
|
source_id UUID NOT NULL,
|
||||||
|
source_name TEXT NOT NULL,
|
||||||
|
uri TEXT NOT NULL,
|
||||||
|
sha256 TEXT NOT NULL,
|
||||||
|
status TEXT NOT NULL,
|
||||||
|
content_type TEXT,
|
||||||
|
headers_json JSONB,
|
||||||
|
metadata_json JSONB,
|
||||||
|
etag TEXT,
|
||||||
|
last_modified TIMESTAMPTZ,
|
||||||
|
payload BYTEA NOT NULL,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
expires_at TIMESTAMPTZ,
|
||||||
|
CONSTRAINT pk_source_documents PRIMARY KEY (source_name, uri)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_source_documents_source_id ON concelier.source_documents(source_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_source_documents_status ON concelier.source_documents(status);
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
namespace StellaOps.Concelier.Storage.Postgres.Models;
|
||||||
|
|
||||||
|
public sealed record DocumentRecordEntity(
|
||||||
|
Guid Id,
|
||||||
|
Guid SourceId,
|
||||||
|
string SourceName,
|
||||||
|
string Uri,
|
||||||
|
string Sha256,
|
||||||
|
string Status,
|
||||||
|
string? ContentType,
|
||||||
|
string? HeadersJson,
|
||||||
|
string? MetadataJson,
|
||||||
|
string? Etag,
|
||||||
|
DateTimeOffset? LastModified,
|
||||||
|
byte[] Payload,
|
||||||
|
DateTimeOffset CreatedAt,
|
||||||
|
DateTimeOffset UpdatedAt,
|
||||||
|
DateTimeOffset? ExpiresAt);
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
using Dapper;
|
||||||
|
using StellaOps.Concelier.Storage.Postgres.Models;
|
||||||
|
using StellaOps.Infrastructure.Postgres;
|
||||||
|
using StellaOps.Infrastructure.Postgres.Connections;
|
||||||
|
|
||||||
|
namespace StellaOps.Concelier.Storage.Postgres.Repositories;
|
||||||
|
|
||||||
|
public interface IDocumentRepository
|
||||||
|
{
|
||||||
|
Task<DocumentRecordEntity?> FindAsync(Guid id, CancellationToken cancellationToken);
|
||||||
|
Task<DocumentRecordEntity?> FindBySourceAndUriAsync(string sourceName, string uri, CancellationToken cancellationToken);
|
||||||
|
Task<DocumentRecordEntity> UpsertAsync(DocumentRecordEntity record, CancellationToken cancellationToken);
|
||||||
|
Task UpdateStatusAsync(Guid id, string status, CancellationToken cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class DocumentRepository : RepositoryBase<ConcelierDataSource>, IDocumentRepository
|
||||||
|
{
|
||||||
|
private readonly JsonSerializerOptions _json = new(JsonSerializerDefaults.Web);
|
||||||
|
|
||||||
|
public DocumentRepository(ConcelierDataSource dataSource, ILogger<DocumentRepository> logger)
|
||||||
|
: base(dataSource, logger)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<DocumentRecordEntity?> FindAsync(Guid id, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
const string sql = """
|
||||||
|
SELECT * FROM concelier.source_documents
|
||||||
|
WHERE id = @Id
|
||||||
|
LIMIT 1;
|
||||||
|
""";
|
||||||
|
|
||||||
|
await using var conn = await DataSource.OpenSystemConnectionAsync(cancellationToken);
|
||||||
|
var row = await conn.QuerySingleOrDefaultAsync(sql, new { Id = id });
|
||||||
|
return row is null ? null : Map(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<DocumentRecordEntity?> FindBySourceAndUriAsync(string sourceName, string uri, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
const string sql = """
|
||||||
|
SELECT * FROM concelier.source_documents
|
||||||
|
WHERE source_name = @SourceName AND uri = @Uri
|
||||||
|
LIMIT 1;
|
||||||
|
""";
|
||||||
|
await using var conn = await DataSource.OpenSystemConnectionAsync(cancellationToken);
|
||||||
|
var row = await conn.QuerySingleOrDefaultAsync(sql, new { SourceName = sourceName, Uri = uri });
|
||||||
|
return row is null ? null : Map(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<DocumentRecordEntity> UpsertAsync(DocumentRecordEntity record, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
const string sql = """
|
||||||
|
INSERT INTO concelier.source_documents (
|
||||||
|
id, source_id, source_name, uri, sha256, status, content_type,
|
||||||
|
headers_json, metadata_json, etag, last_modified, payload, created_at, updated_at, expires_at)
|
||||||
|
VALUES (
|
||||||
|
@Id, @SourceId, @SourceName, @Uri, @Sha256, @Status, @ContentType,
|
||||||
|
@HeadersJson, @MetadataJson, @Etag, @LastModified, @Payload, @CreatedAt, @UpdatedAt, @ExpiresAt)
|
||||||
|
ON CONFLICT (source_name, uri) DO UPDATE SET
|
||||||
|
sha256 = EXCLUDED.sha256,
|
||||||
|
status = EXCLUDED.status,
|
||||||
|
content_type = EXCLUDED.content_type,
|
||||||
|
headers_json = EXCLUDED.headers_json,
|
||||||
|
metadata_json = EXCLUDED.metadata_json,
|
||||||
|
etag = EXCLUDED.etag,
|
||||||
|
last_modified = EXCLUDED.last_modified,
|
||||||
|
payload = EXCLUDED.payload,
|
||||||
|
updated_at = EXCLUDED.updated_at,
|
||||||
|
expires_at = EXCLUDED.expires_at
|
||||||
|
RETURNING *;
|
||||||
|
""";
|
||||||
|
await using var conn = await DataSource.OpenSystemConnectionAsync(cancellationToken);
|
||||||
|
var row = await conn.QuerySingleAsync(sql, new
|
||||||
|
{
|
||||||
|
record.Id,
|
||||||
|
record.SourceId,
|
||||||
|
record.SourceName,
|
||||||
|
record.Uri,
|
||||||
|
record.Sha256,
|
||||||
|
record.Status,
|
||||||
|
record.ContentType,
|
||||||
|
record.HeadersJson,
|
||||||
|
record.MetadataJson,
|
||||||
|
record.Etag,
|
||||||
|
record.LastModified,
|
||||||
|
record.Payload,
|
||||||
|
record.CreatedAt,
|
||||||
|
record.UpdatedAt,
|
||||||
|
record.ExpiresAt
|
||||||
|
});
|
||||||
|
return Map(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task UpdateStatusAsync(Guid id, string status, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
const string sql = """
|
||||||
|
UPDATE concelier.source_documents
|
||||||
|
SET status = @Status, updated_at = NOW()
|
||||||
|
WHERE id = @Id;
|
||||||
|
""";
|
||||||
|
await using var conn = await DataSource.OpenSystemConnectionAsync(cancellationToken);
|
||||||
|
await conn.ExecuteAsync(sql, new { Id = id, Status = status });
|
||||||
|
}
|
||||||
|
|
||||||
|
private DocumentRecordEntity Map(dynamic row)
|
||||||
|
{
|
||||||
|
return new DocumentRecordEntity(
|
||||||
|
row.id,
|
||||||
|
row.source_id,
|
||||||
|
row.source_name,
|
||||||
|
row.uri,
|
||||||
|
row.sha256,
|
||||||
|
row.status,
|
||||||
|
(string?)row.content_type,
|
||||||
|
(string?)row.headers_json,
|
||||||
|
(string?)row.metadata_json,
|
||||||
|
(string?)row.etag,
|
||||||
|
(DateTimeOffset?)row.last_modified,
|
||||||
|
(byte[])row.payload,
|
||||||
|
DateTime.SpecifyKind(row.created_at, DateTimeKind.Utc),
|
||||||
|
DateTime.SpecifyKind(row.updated_at, DateTimeKind.Utc),
|
||||||
|
row.expires_at is null ? null : DateTime.SpecifyKind(row.expires_at, DateTimeKind.Utc));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ using StellaOps.Concelier.Storage.Postgres.Repositories;
|
|||||||
using StellaOps.Infrastructure.Postgres;
|
using StellaOps.Infrastructure.Postgres;
|
||||||
using StellaOps.Infrastructure.Postgres.Options;
|
using StellaOps.Infrastructure.Postgres.Options;
|
||||||
using StellaOps.Concelier.Core.Linksets;
|
using StellaOps.Concelier.Core.Linksets;
|
||||||
|
using StellaOps.Concelier.Storage.Mongo;
|
||||||
|
|
||||||
namespace StellaOps.Concelier.Storage.Postgres;
|
namespace StellaOps.Concelier.Storage.Postgres;
|
||||||
|
|
||||||
@@ -38,11 +39,13 @@ public static class ServiceCollectionExtensions
|
|||||||
services.AddScoped<IAdvisoryWeaknessRepository, AdvisoryWeaknessRepository>();
|
services.AddScoped<IAdvisoryWeaknessRepository, AdvisoryWeaknessRepository>();
|
||||||
services.AddScoped<IKevFlagRepository, KevFlagRepository>();
|
services.AddScoped<IKevFlagRepository, KevFlagRepository>();
|
||||||
services.AddScoped<ISourceStateRepository, SourceStateRepository>();
|
services.AddScoped<ISourceStateRepository, SourceStateRepository>();
|
||||||
|
services.AddScoped<IDocumentRepository, DocumentRepository>();
|
||||||
services.AddScoped<IFeedSnapshotRepository, FeedSnapshotRepository>();
|
services.AddScoped<IFeedSnapshotRepository, FeedSnapshotRepository>();
|
||||||
services.AddScoped<IAdvisorySnapshotRepository, AdvisorySnapshotRepository>();
|
services.AddScoped<IAdvisorySnapshotRepository, AdvisorySnapshotRepository>();
|
||||||
services.AddScoped<IMergeEventRepository, MergeEventRepository>();
|
services.AddScoped<IMergeEventRepository, MergeEventRepository>();
|
||||||
services.AddScoped<IAdvisoryLinksetStore, AdvisoryLinksetCacheRepository>();
|
services.AddScoped<IAdvisoryLinksetStore, AdvisoryLinksetCacheRepository>();
|
||||||
services.AddScoped<IAdvisoryLinksetLookup>(sp => sp.GetRequiredService<IAdvisoryLinksetStore>());
|
services.AddScoped<IAdvisoryLinksetLookup>(sp => sp.GetRequiredService<IAdvisoryLinksetStore>());
|
||||||
|
services.AddScoped<IDocumentStore, PostgresDocumentStore>();
|
||||||
|
|
||||||
return services;
|
return services;
|
||||||
}
|
}
|
||||||
@@ -71,11 +74,13 @@ public static class ServiceCollectionExtensions
|
|||||||
services.AddScoped<IAdvisoryWeaknessRepository, AdvisoryWeaknessRepository>();
|
services.AddScoped<IAdvisoryWeaknessRepository, AdvisoryWeaknessRepository>();
|
||||||
services.AddScoped<IKevFlagRepository, KevFlagRepository>();
|
services.AddScoped<IKevFlagRepository, KevFlagRepository>();
|
||||||
services.AddScoped<ISourceStateRepository, SourceStateRepository>();
|
services.AddScoped<ISourceStateRepository, SourceStateRepository>();
|
||||||
|
services.AddScoped<IDocumentRepository, DocumentRepository>();
|
||||||
services.AddScoped<IFeedSnapshotRepository, FeedSnapshotRepository>();
|
services.AddScoped<IFeedSnapshotRepository, FeedSnapshotRepository>();
|
||||||
services.AddScoped<IAdvisorySnapshotRepository, AdvisorySnapshotRepository>();
|
services.AddScoped<IAdvisorySnapshotRepository, AdvisorySnapshotRepository>();
|
||||||
services.AddScoped<IMergeEventRepository, MergeEventRepository>();
|
services.AddScoped<IMergeEventRepository, MergeEventRepository>();
|
||||||
services.AddScoped<IAdvisoryLinksetStore, AdvisoryLinksetCacheRepository>();
|
services.AddScoped<IAdvisoryLinksetStore, AdvisoryLinksetCacheRepository>();
|
||||||
services.AddScoped<IAdvisoryLinksetLookup>(sp => sp.GetRequiredService<IAdvisoryLinksetStore>());
|
services.AddScoped<IAdvisoryLinksetLookup>(sp => sp.GetRequiredService<IAdvisoryLinksetStore>());
|
||||||
|
services.AddScoped<IDocumentStore, PostgresDocumentStore>();
|
||||||
|
|
||||||
return services;
|
return services;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,11 @@
|
|||||||
<RootNamespace>StellaOps.Concelier.Storage.Postgres</RootNamespace>
|
<RootNamespace>StellaOps.Concelier.Storage.Postgres</RootNamespace>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Dapper" Version="2.1.35" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.0" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<EmbeddedResource Include="Migrations\**\*.sql" LogicalName="%(RecursiveDir)%(Filename)%(Extension)" />
|
<EmbeddedResource Include="Migrations\**\*.sql" LogicalName="%(RecursiveDir)%(Filename)%(Extension)" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
@@ -25,6 +30,7 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\..\..\__Libraries\StellaOps.Infrastructure.Postgres\StellaOps.Infrastructure.Postgres.csproj" />
|
<ProjectReference Include="..\..\..\__Libraries\StellaOps.Infrastructure.Postgres\StellaOps.Infrastructure.Postgres.csproj" />
|
||||||
<ProjectReference Include="..\StellaOps.Concelier.Core\StellaOps.Concelier.Core.csproj" />
|
<ProjectReference Include="..\StellaOps.Concelier.Core\StellaOps.Concelier.Core.csproj" />
|
||||||
|
<ProjectReference Include="..\..\..\__Libraries\StellaOps.DependencyInjection\StellaOps.DependencyInjection.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ public sealed class CccsMapperTests
|
|||||||
Metadata: null,
|
Metadata: null,
|
||||||
Etag: null,
|
Etag: null,
|
||||||
LastModified: dto.Modified,
|
LastModified: dto.Modified,
|
||||||
GridFsId: null);
|
PayloadId: null);
|
||||||
|
|
||||||
var recordedAt = DateTimeOffset.Parse("2025-08-12T00:00:00Z");
|
var recordedAt = DateTimeOffset.Parse("2025-08-12T00:00:00Z");
|
||||||
var advisory = CccsMapper.Map(dto, document, recordedAt);
|
var advisory = CccsMapper.Map(dto, document, recordedAt);
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ public sealed class CertCcMapperTests
|
|||||||
Metadata: null,
|
Metadata: null,
|
||||||
Etag: null,
|
Etag: null,
|
||||||
LastModified: PublishedAt,
|
LastModified: PublishedAt,
|
||||||
GridFsId: null);
|
PayloadId: null);
|
||||||
|
|
||||||
var dtoRecord = new DtoRecord(
|
var dtoRecord = new DtoRecord(
|
||||||
Id: Guid.NewGuid(),
|
Id: Guid.NewGuid(),
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ public sealed class SourceStateSeedProcessorTests : IAsyncLifetime
|
|||||||
Assert.Equal(documentId, storedDocument!.Id);
|
Assert.Equal(documentId, storedDocument!.Id);
|
||||||
Assert.Equal("application/json", storedDocument.ContentType);
|
Assert.Equal("application/json", storedDocument.ContentType);
|
||||||
Assert.Equal(DocumentStatuses.PendingParse, storedDocument.Status);
|
Assert.Equal(DocumentStatuses.PendingParse, storedDocument.Status);
|
||||||
Assert.NotNull(storedDocument.GridFsId);
|
Assert.NotNull(storedDocument.PayloadId);
|
||||||
Assert.NotNull(storedDocument.Headers);
|
Assert.NotNull(storedDocument.Headers);
|
||||||
Assert.Equal("true", storedDocument.Headers!["X-Test"]);
|
Assert.Equal("true", storedDocument.Headers!["X-Test"]);
|
||||||
Assert.NotNull(storedDocument.Metadata);
|
Assert.NotNull(storedDocument.Metadata);
|
||||||
@@ -153,7 +153,7 @@ public sealed class SourceStateSeedProcessorTests : IAsyncLifetime
|
|||||||
CancellationToken.None);
|
CancellationToken.None);
|
||||||
|
|
||||||
Assert.NotNull(existingRecord);
|
Assert.NotNull(existingRecord);
|
||||||
var previousGridId = existingRecord!.GridFsId;
|
var previousGridId = existingRecord!.PayloadId;
|
||||||
Assert.NotNull(previousGridId);
|
Assert.NotNull(previousGridId);
|
||||||
|
|
||||||
var filesCollection = _database.GetCollection<BsonDocument>("documents.files");
|
var filesCollection = _database.GetCollection<BsonDocument>("documents.files");
|
||||||
@@ -189,8 +189,8 @@ public sealed class SourceStateSeedProcessorTests : IAsyncLifetime
|
|||||||
|
|
||||||
Assert.NotNull(refreshedRecord);
|
Assert.NotNull(refreshedRecord);
|
||||||
Assert.Equal(documentId, refreshedRecord!.Id);
|
Assert.Equal(documentId, refreshedRecord!.Id);
|
||||||
Assert.NotNull(refreshedRecord.GridFsId);
|
Assert.NotNull(refreshedRecord.PayloadId);
|
||||||
Assert.NotEqual(previousGridId, refreshedRecord.GridFsId);
|
Assert.NotEqual(previousGridId, refreshedRecord.PayloadId);
|
||||||
|
|
||||||
var files = await filesCollection.Find(FilterDefinition<BsonDocument>.Empty).ToListAsync();
|
var files = await filesCollection.Find(FilterDefinition<BsonDocument>.Empty).ToListAsync();
|
||||||
Assert.Single(files);
|
Assert.Single(files);
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ public sealed class DebianMapperTests
|
|||||||
Metadata: null,
|
Metadata: null,
|
||||||
Etag: null,
|
Etag: null,
|
||||||
LastModified: null,
|
LastModified: null,
|
||||||
GridFsId: null);
|
PayloadId: null);
|
||||||
|
|
||||||
Advisory advisory = DebianMapper.Map(dto, document, new DateTimeOffset(2024, 9, 1, 2, 0, 0, TimeSpan.Zero));
|
Advisory advisory = DebianMapper.Map(dto, document, new DateTimeOffset(2024, 9, 1, 2, 0, 0, TimeSpan.Zero));
|
||||||
|
|
||||||
|
|||||||
@@ -498,7 +498,7 @@ public sealed class RedHatConnectorTests : IAsyncLifetime
|
|||||||
Metadata: metadata,
|
Metadata: metadata,
|
||||||
Etag: null,
|
Etag: null,
|
||||||
LastModified: fixture.ValidatedAt,
|
LastModified: fixture.ValidatedAt,
|
||||||
GridFsId: null);
|
PayloadId: null);
|
||||||
|
|
||||||
var dto = new DtoRecord(Guid.NewGuid(), document.Id, RedHatConnectorPlugin.SourceName, "redhat.csaf.v2", bson, fixture.ValidatedAt);
|
var dto = new DtoRecord(Guid.NewGuid(), document.Id, RedHatConnectorPlugin.SourceName, "redhat.csaf.v2", bson, fixture.ValidatedAt);
|
||||||
|
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ public sealed class SuseMapperTests
|
|||||||
},
|
},
|
||||||
Etag: "adv-1",
|
Etag: "adv-1",
|
||||||
LastModified: DateTimeOffset.UtcNow,
|
LastModified: DateTimeOffset.UtcNow,
|
||||||
GridFsId: ObjectId.Empty);
|
PayloadId: ObjectId.Empty);
|
||||||
|
|
||||||
var mapped = SuseMapper.Map(dto, document, DateTimeOffset.UtcNow);
|
var mapped = SuseMapper.Map(dto, document, DateTimeOffset.UtcNow);
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ public sealed class GhsaConflictFixtureTests
|
|||||||
Metadata: null,
|
Metadata: null,
|
||||||
Etag: "\"etag-ghsa-conflict\"",
|
Etag: "\"etag-ghsa-conflict\"",
|
||||||
LastModified: new DateTimeOffset(2025, 3, 3, 18, 0, 0, TimeSpan.Zero),
|
LastModified: new DateTimeOffset(2025, 3, 3, 18, 0, 0, TimeSpan.Zero),
|
||||||
GridFsId: null);
|
PayloadId: null);
|
||||||
|
|
||||||
var dto = new GhsaRecordDto
|
var dto = new GhsaRecordDto
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ public sealed class GhsaMapperTests
|
|||||||
Metadata: null,
|
Metadata: null,
|
||||||
Etag: "\"etag-ghsa-fallback\"",
|
Etag: "\"etag-ghsa-fallback\"",
|
||||||
LastModified: recordedAt.AddHours(-3),
|
LastModified: recordedAt.AddHours(-3),
|
||||||
GridFsId: null);
|
PayloadId: null);
|
||||||
|
|
||||||
var dto = new GhsaRecordDto
|
var dto = new GhsaRecordDto
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ public sealed class NvdConflictFixtureTests
|
|||||||
Metadata: null,
|
Metadata: null,
|
||||||
Etag: "\"etag-nvd-conflict\"",
|
Etag: "\"etag-nvd-conflict\"",
|
||||||
LastModified: new DateTimeOffset(2025, 3, 3, 9, 45, 0, TimeSpan.Zero),
|
LastModified: new DateTimeOffset(2025, 3, 3, 9, 45, 0, TimeSpan.Zero),
|
||||||
GridFsId: null);
|
PayloadId: null);
|
||||||
|
|
||||||
var advisories = NvdMapper.Map(document, sourceDocument, new DateTimeOffset(2025, 3, 4, 2, 0, 0, TimeSpan.Zero));
|
var advisories = NvdMapper.Map(document, sourceDocument, new DateTimeOffset(2025, 3, 4, 2, 0, 0, TimeSpan.Zero));
|
||||||
var advisory = Assert.Single(advisories);
|
var advisory = Assert.Single(advisories);
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ public sealed class OsvConflictFixtureTests
|
|||||||
Metadata: null,
|
Metadata: null,
|
||||||
Etag: "\"etag-osv-conflict\"",
|
Etag: "\"etag-osv-conflict\"",
|
||||||
LastModified: new DateTimeOffset(2025, 3, 6, 12, 0, 0, TimeSpan.Zero),
|
LastModified: new DateTimeOffset(2025, 3, 6, 12, 0, 0, TimeSpan.Zero),
|
||||||
GridFsId: null);
|
PayloadId: null);
|
||||||
|
|
||||||
var dtoRecord = new DtoRecord(
|
var dtoRecord = new DtoRecord(
|
||||||
Id: Guid.Parse("6f7d5ce7-cb47-40a5-8b41-8ad022b5fd5c"),
|
Id: Guid.Parse("6f7d5ce7-cb47-40a5-8b41-8ad022b5fd5c"),
|
||||||
|
|||||||
@@ -75,11 +75,11 @@ public sealed class StellaOpsMirrorConnectorTests : IAsyncLifetime
|
|||||||
Assert.Equal(NormalizeDigest(bundleDigest), bundleDocument.Sha256);
|
Assert.Equal(NormalizeDigest(bundleDigest), bundleDocument.Sha256);
|
||||||
|
|
||||||
var rawStorage = provider.GetRequiredService<RawDocumentStorage>();
|
var rawStorage = provider.GetRequiredService<RawDocumentStorage>();
|
||||||
Assert.NotNull(manifestDocument.GridFsId);
|
Assert.NotNull(manifestDocument.PayloadId);
|
||||||
Assert.NotNull(bundleDocument.GridFsId);
|
Assert.NotNull(bundleDocument.PayloadId);
|
||||||
|
|
||||||
var manifestBytes = await rawStorage.DownloadAsync(manifestDocument.GridFsId!.Value, CancellationToken.None);
|
var manifestBytes = await rawStorage.DownloadAsync(manifestDocument.PayloadId!.Value, CancellationToken.None);
|
||||||
var bundleBytes = await rawStorage.DownloadAsync(bundleDocument.GridFsId!.Value, CancellationToken.None);
|
var bundleBytes = await rawStorage.DownloadAsync(bundleDocument.PayloadId!.Value, CancellationToken.None);
|
||||||
Assert.Equal(manifestContent, Encoding.UTF8.GetString(manifestBytes));
|
Assert.Equal(manifestContent, Encoding.UTF8.GetString(manifestBytes));
|
||||||
Assert.Equal(bundleContent, Encoding.UTF8.GetString(bundleBytes));
|
Assert.Equal(bundleContent, Encoding.UTF8.GetString(bundleBytes));
|
||||||
|
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ public sealed class CiscoMapperTests
|
|||||||
Metadata: null,
|
Metadata: null,
|
||||||
Etag: null,
|
Etag: null,
|
||||||
LastModified: updated,
|
LastModified: updated,
|
||||||
GridFsId: null);
|
PayloadId: null);
|
||||||
|
|
||||||
var dtoRecord = new DtoRecord(Guid.NewGuid(), document.Id, VndrCiscoConnectorPlugin.SourceName, "cisco.dto.test", new BsonDocument(), updated);
|
var dtoRecord = new DtoRecord(Guid.NewGuid(), document.Id, VndrCiscoConnectorPlugin.SourceName, "cisco.dto.test", new BsonDocument(), updated);
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,327 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.Immutable;
|
||||||
|
using Microsoft.AspNetCore.Http.HttpResults;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using StellaOps.Auth.Abstractions;
|
||||||
|
using StellaOps.Attestor.Envelope;
|
||||||
|
using StellaOps.Policy.Engine.Services;
|
||||||
|
using StellaOps.Policy.Scoring;
|
||||||
|
using StellaOps.Policy.Scoring.Engine;
|
||||||
|
using StellaOps.Policy.Scoring.Receipts;
|
||||||
|
|
||||||
|
namespace StellaOps.Policy.Engine.Endpoints;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Minimal API surface for CVSS v4.0 score receipts (create, read, amend, history).
|
||||||
|
/// </summary>
|
||||||
|
internal static class CvssReceiptEndpoints
|
||||||
|
{
|
||||||
|
public static IEndpointRouteBuilder MapCvssReceipts(this IEndpointRouteBuilder endpoints)
|
||||||
|
{
|
||||||
|
var group = endpoints.MapGroup("/api/cvss")
|
||||||
|
.RequireAuthorization()
|
||||||
|
.WithTags("CVSS Receipts");
|
||||||
|
|
||||||
|
group.MapPost("/receipts", CreateReceipt)
|
||||||
|
.WithName("CreateCvssReceipt")
|
||||||
|
.WithSummary("Create a CVSS v4.0 receipt with deterministic hashing and optional DSSE attestation.")
|
||||||
|
.Produces<CvssScoreReceipt>(StatusCodes.Status201Created)
|
||||||
|
.Produces<ProblemHttpResult>(StatusCodes.Status400BadRequest)
|
||||||
|
.Produces<ProblemHttpResult>(StatusCodes.Status401Unauthorized);
|
||||||
|
|
||||||
|
group.MapGet("/receipts/{receiptId}", GetReceipt)
|
||||||
|
.WithName("GetCvssReceipt")
|
||||||
|
.WithSummary("Retrieve a CVSS v4.0 receipt by ID.")
|
||||||
|
.Produces<CvssScoreReceipt>(StatusCodes.Status200OK)
|
||||||
|
.Produces<ProblemHttpResult>(StatusCodes.Status404NotFound);
|
||||||
|
|
||||||
|
group.MapPut("/receipts/{receiptId}/amend", AmendReceipt)
|
||||||
|
.WithName("AmendCvssReceipt")
|
||||||
|
.WithSummary("Append an amendment entry to a CVSS receipt history and optionally re-sign.")
|
||||||
|
.Produces<CvssScoreReceipt>(StatusCodes.Status200OK)
|
||||||
|
.Produces<ProblemHttpResult>(StatusCodes.Status400BadRequest)
|
||||||
|
.Produces<ProblemHttpResult>(StatusCodes.Status404NotFound);
|
||||||
|
|
||||||
|
group.MapGet("/receipts/{receiptId}/history", GetReceiptHistory)
|
||||||
|
.WithName("GetCvssReceiptHistory")
|
||||||
|
.WithSummary("Return the ordered amendment history for a CVSS receipt.")
|
||||||
|
.Produces<IReadOnlyList<ReceiptHistoryEntry>>(StatusCodes.Status200OK)
|
||||||
|
.Produces<ProblemHttpResult>(StatusCodes.Status404NotFound);
|
||||||
|
|
||||||
|
group.MapGet("/policies", ListPolicies)
|
||||||
|
.WithName("ListCvssPolicies")
|
||||||
|
.WithSummary("List available CVSS policies configured on this host.")
|
||||||
|
.Produces<IReadOnlyList<CvssPolicy>>(StatusCodes.Status200OK);
|
||||||
|
|
||||||
|
return endpoints;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<IResult> CreateReceipt(
|
||||||
|
HttpContext context,
|
||||||
|
[FromBody] CreateCvssReceiptRequest request,
|
||||||
|
IReceiptBuilder receiptBuilder,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var scopeResult = ScopeAuthorization.RequireScope(context, StellaOpsScopes.PolicyRun);
|
||||||
|
if (scopeResult is not null)
|
||||||
|
{
|
||||||
|
return scopeResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request is null)
|
||||||
|
{
|
||||||
|
return Results.BadRequest(new ProblemDetails
|
||||||
|
{
|
||||||
|
Title = "Request body required.",
|
||||||
|
Status = StatusCodes.Status400BadRequest
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.Policy is null || string.IsNullOrWhiteSpace(request.Policy.Hash))
|
||||||
|
{
|
||||||
|
return Results.BadRequest(new ProblemDetails
|
||||||
|
{
|
||||||
|
Title = "Policy hash required",
|
||||||
|
Detail = "CvssPolicy with a deterministic hash must be supplied.",
|
||||||
|
Status = StatusCodes.Status400BadRequest
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var tenantId = ResolveTenantId(context);
|
||||||
|
if (string.IsNullOrWhiteSpace(tenantId))
|
||||||
|
{
|
||||||
|
return Results.BadRequest(new ProblemDetails
|
||||||
|
{
|
||||||
|
Title = "Tenant required",
|
||||||
|
Detail = "Specify tenant via X-Tenant-Id header or tenant_id claim.",
|
||||||
|
Status = StatusCodes.Status400BadRequest
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var actor = ResolveActorId(context) ?? request.CreatedBy ?? "system";
|
||||||
|
var createdAt = request.CreatedAt ?? DateTimeOffset.UtcNow;
|
||||||
|
|
||||||
|
var createRequest = new CreateReceiptRequest
|
||||||
|
{
|
||||||
|
TenantId = tenantId,
|
||||||
|
VulnerabilityId = request.VulnerabilityId,
|
||||||
|
CreatedBy = actor,
|
||||||
|
CreatedAt = createdAt,
|
||||||
|
Policy = request.Policy,
|
||||||
|
BaseMetrics = request.BaseMetrics,
|
||||||
|
ThreatMetrics = request.ThreatMetrics,
|
||||||
|
EnvironmentalMetrics = request.EnvironmentalMetrics ?? request.Policy.DefaultEnvironmentalMetrics,
|
||||||
|
SupplementalMetrics = request.SupplementalMetrics,
|
||||||
|
Evidence = request.Evidence?.ToImmutableList() ?? ImmutableList<CvssEvidenceItem>.Empty,
|
||||||
|
SigningKey = request.SigningKey
|
||||||
|
};
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var receipt = await receiptBuilder.CreateAsync(createRequest, cancellationToken).ConfigureAwait(false);
|
||||||
|
return Results.Created($"/api/cvss/receipts/{receipt.ReceiptId}", receipt);
|
||||||
|
}
|
||||||
|
catch (Exception ex) when (ex is InvalidOperationException or ArgumentException)
|
||||||
|
{
|
||||||
|
return Results.BadRequest(new ProblemDetails
|
||||||
|
{
|
||||||
|
Title = "Failed to create CVSS receipt",
|
||||||
|
Detail = ex.Message,
|
||||||
|
Status = StatusCodes.Status400BadRequest
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<IResult> GetReceipt(
|
||||||
|
HttpContext context,
|
||||||
|
[FromRoute] string receiptId,
|
||||||
|
IReceiptRepository repository,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var scopeResult = ScopeAuthorization.RequireScope(context, StellaOpsScopes.FindingsRead);
|
||||||
|
if (scopeResult is not null)
|
||||||
|
{
|
||||||
|
return scopeResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
var tenantId = ResolveTenantId(context);
|
||||||
|
if (string.IsNullOrWhiteSpace(tenantId))
|
||||||
|
{
|
||||||
|
return Results.BadRequest(new ProblemDetails
|
||||||
|
{
|
||||||
|
Title = "Tenant required",
|
||||||
|
Detail = "Specify tenant via X-Tenant-Id header or tenant_id claim.",
|
||||||
|
Status = StatusCodes.Status400BadRequest
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var receipt = await repository.GetAsync(tenantId, receiptId, cancellationToken).ConfigureAwait(false);
|
||||||
|
if (receipt is null)
|
||||||
|
{
|
||||||
|
return Results.NotFound(new ProblemDetails
|
||||||
|
{
|
||||||
|
Title = "Receipt not found",
|
||||||
|
Detail = $"CVSS receipt '{receiptId}' was not found.",
|
||||||
|
Status = StatusCodes.Status404NotFound
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return Results.Ok(receipt);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<IResult> AmendReceipt(
|
||||||
|
HttpContext context,
|
||||||
|
[FromRoute] string receiptId,
|
||||||
|
[FromBody] AmendCvssReceiptRequest request,
|
||||||
|
IReceiptHistoryService historyService,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var scopeResult = ScopeAuthorization.RequireScope(context, StellaOpsScopes.PolicyRun);
|
||||||
|
if (scopeResult is not null)
|
||||||
|
{
|
||||||
|
return scopeResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request is null)
|
||||||
|
{
|
||||||
|
return Results.BadRequest(new ProblemDetails
|
||||||
|
{
|
||||||
|
Title = "Request body required.",
|
||||||
|
Status = StatusCodes.Status400BadRequest
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var tenantId = ResolveTenantId(context);
|
||||||
|
if (string.IsNullOrWhiteSpace(tenantId))
|
||||||
|
{
|
||||||
|
return Results.BadRequest(new ProblemDetails
|
||||||
|
{
|
||||||
|
Title = "Tenant required",
|
||||||
|
Detail = "Specify tenant via X-Tenant-Id header or tenant_id claim.",
|
||||||
|
Status = StatusCodes.Status400BadRequest
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var actor = ResolveActorId(context) ?? request.Actor ?? "system";
|
||||||
|
|
||||||
|
var amend = new AmendReceiptRequest
|
||||||
|
{
|
||||||
|
ReceiptId = receiptId,
|
||||||
|
TenantId = tenantId,
|
||||||
|
Actor = actor,
|
||||||
|
Field = request.Field,
|
||||||
|
PreviousValue = request.PreviousValue,
|
||||||
|
NewValue = request.NewValue,
|
||||||
|
Reason = request.Reason,
|
||||||
|
ReferenceUri = request.ReferenceUri,
|
||||||
|
SigningKey = request.SigningKey
|
||||||
|
};
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var amended = await historyService.AmendAsync(amend, cancellationToken).ConfigureAwait(false);
|
||||||
|
return Results.Ok(amended);
|
||||||
|
}
|
||||||
|
catch (InvalidOperationException ex)
|
||||||
|
{
|
||||||
|
return Results.NotFound(new ProblemDetails
|
||||||
|
{
|
||||||
|
Title = "Receipt not found",
|
||||||
|
Detail = ex.Message,
|
||||||
|
Status = StatusCodes.Status404NotFound
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (Exception ex) when (ex is ArgumentException)
|
||||||
|
{
|
||||||
|
return Results.BadRequest(new ProblemDetails
|
||||||
|
{
|
||||||
|
Title = "Failed to amend receipt",
|
||||||
|
Detail = ex.Message,
|
||||||
|
Status = StatusCodes.Status400BadRequest
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<IResult> GetReceiptHistory(
|
||||||
|
HttpContext context,
|
||||||
|
[FromRoute] string receiptId,
|
||||||
|
IReceiptRepository repository,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var scopeResult = ScopeAuthorization.RequireScope(context, StellaOpsScopes.FindingsRead);
|
||||||
|
if (scopeResult is not null)
|
||||||
|
{
|
||||||
|
return scopeResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
var tenantId = ResolveTenantId(context);
|
||||||
|
if (string.IsNullOrWhiteSpace(tenantId))
|
||||||
|
{
|
||||||
|
return Results.BadRequest(new ProblemDetails
|
||||||
|
{
|
||||||
|
Title = "Tenant required",
|
||||||
|
Detail = "Specify tenant via X-Tenant-Id header or tenant_id claim.",
|
||||||
|
Status = StatusCodes.Status400BadRequest
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var receipt = await repository.GetAsync(tenantId, receiptId, cancellationToken).ConfigureAwait(false);
|
||||||
|
if (receipt is null)
|
||||||
|
{
|
||||||
|
return Results.NotFound(new ProblemDetails
|
||||||
|
{
|
||||||
|
Title = "Receipt not found",
|
||||||
|
Detail = $"CVSS receipt '{receiptId}' was not found.",
|
||||||
|
Status = StatusCodes.Status404NotFound
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var orderedHistory = receipt.History
|
||||||
|
.OrderBy(h => h.Timestamp)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
return Results.Ok(orderedHistory);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IResult ListPolicies()
|
||||||
|
=> Results.Ok(Array.Empty<CvssPolicy>());
|
||||||
|
|
||||||
|
private static string? ResolveTenantId(HttpContext context)
|
||||||
|
{
|
||||||
|
if (context.Request.Headers.TryGetValue("X-Tenant-Id", out var tenantHeader) &&
|
||||||
|
!string.IsNullOrWhiteSpace(tenantHeader))
|
||||||
|
{
|
||||||
|
return tenantHeader.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
return context.User?.FindFirst("tenant_id")?.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string? ResolveActorId(HttpContext context)
|
||||||
|
{
|
||||||
|
var user = context.User;
|
||||||
|
return user?.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value
|
||||||
|
?? user?.FindFirst("sub")?.Value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed record CreateCvssReceiptRequest(
|
||||||
|
string VulnerabilityId,
|
||||||
|
CvssPolicy Policy,
|
||||||
|
CvssBaseMetrics BaseMetrics,
|
||||||
|
CvssThreatMetrics? ThreatMetrics,
|
||||||
|
CvssEnvironmentalMetrics? EnvironmentalMetrics,
|
||||||
|
CvssSupplementalMetrics? SupplementalMetrics,
|
||||||
|
IReadOnlyList<CvssEvidenceItem>? Evidence,
|
||||||
|
EnvelopeKey? SigningKey,
|
||||||
|
string? CreatedBy,
|
||||||
|
DateTimeOffset? CreatedAt);
|
||||||
|
|
||||||
|
internal sealed record AmendCvssReceiptRequest(
|
||||||
|
string Field,
|
||||||
|
string? PreviousValue,
|
||||||
|
string? NewValue,
|
||||||
|
string Reason,
|
||||||
|
string? ReferenceUri,
|
||||||
|
EnvelopeKey? SigningKey,
|
||||||
|
string? Actor);
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
using StellaOps.Policy.Gateway.Contracts;
|
using StellaOps.Policy.Gateway.Contracts;
|
||||||
using StellaOps.Policy.Gateway.Infrastructure;
|
using StellaOps.Policy.Gateway.Infrastructure;
|
||||||
|
using StellaOps.Policy.Scoring;
|
||||||
|
using StellaOps.Policy.Scoring.Receipts;
|
||||||
|
|
||||||
namespace StellaOps.Policy.Gateway.Clients;
|
namespace StellaOps.Policy.Gateway.Clients;
|
||||||
|
|
||||||
@@ -12,4 +14,14 @@ internal interface IPolicyEngineClient
|
|||||||
Task<PolicyEngineResponse<PolicyRevisionDto>> CreatePolicyRevisionAsync(GatewayForwardingContext? forwardingContext, string packId, CreatePolicyRevisionRequest request, CancellationToken cancellationToken);
|
Task<PolicyEngineResponse<PolicyRevisionDto>> CreatePolicyRevisionAsync(GatewayForwardingContext? forwardingContext, string packId, CreatePolicyRevisionRequest request, CancellationToken cancellationToken);
|
||||||
|
|
||||||
Task<PolicyEngineResponse<PolicyRevisionActivationDto>> ActivatePolicyRevisionAsync(GatewayForwardingContext? forwardingContext, string packId, int version, ActivatePolicyRevisionRequest request, CancellationToken cancellationToken);
|
Task<PolicyEngineResponse<PolicyRevisionActivationDto>> ActivatePolicyRevisionAsync(GatewayForwardingContext? forwardingContext, string packId, int version, ActivatePolicyRevisionRequest request, CancellationToken cancellationToken);
|
||||||
|
|
||||||
|
Task<PolicyEngineResponse<CvssScoreReceipt>> CreateCvssReceiptAsync(GatewayForwardingContext? forwardingContext, CreateCvssReceiptRequest request, CancellationToken cancellationToken);
|
||||||
|
|
||||||
|
Task<PolicyEngineResponse<CvssScoreReceipt>> GetCvssReceiptAsync(GatewayForwardingContext? forwardingContext, string receiptId, CancellationToken cancellationToken);
|
||||||
|
|
||||||
|
Task<PolicyEngineResponse<CvssScoreReceipt>> AmendCvssReceiptAsync(GatewayForwardingContext? forwardingContext, string receiptId, AmendCvssReceiptRequest request, CancellationToken cancellationToken);
|
||||||
|
|
||||||
|
Task<PolicyEngineResponse<IReadOnlyList<ReceiptHistoryEntry>>> GetCvssReceiptHistoryAsync(GatewayForwardingContext? forwardingContext, string receiptId, CancellationToken cancellationToken);
|
||||||
|
|
||||||
|
Task<PolicyEngineResponse<IReadOnlyList<CvssPolicy>>> ListCvssPoliciesAsync(GatewayForwardingContext? forwardingContext, CancellationToken cancellationToken);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ using StellaOps.Policy.Gateway.Contracts;
|
|||||||
using StellaOps.Policy.Gateway.Infrastructure;
|
using StellaOps.Policy.Gateway.Infrastructure;
|
||||||
using StellaOps.Policy.Gateway.Options;
|
using StellaOps.Policy.Gateway.Options;
|
||||||
using StellaOps.Policy.Gateway.Services;
|
using StellaOps.Policy.Gateway.Services;
|
||||||
|
using StellaOps.Policy.Scoring;
|
||||||
|
using StellaOps.Policy.Scoring.Receipts;
|
||||||
|
|
||||||
namespace StellaOps.Policy.Gateway.Clients;
|
namespace StellaOps.Policy.Gateway.Clients;
|
||||||
|
|
||||||
@@ -98,6 +100,61 @@ internal sealed class PolicyEngineClient : IPolicyEngineClient
|
|||||||
request,
|
request,
|
||||||
cancellationToken);
|
cancellationToken);
|
||||||
|
|
||||||
|
public Task<PolicyEngineResponse<CvssScoreReceipt>> CreateCvssReceiptAsync(
|
||||||
|
GatewayForwardingContext? forwardingContext,
|
||||||
|
CreateCvssReceiptRequest request,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
=> SendAsync<CvssScoreReceipt>(
|
||||||
|
HttpMethod.Post,
|
||||||
|
"api/cvss/receipts",
|
||||||
|
forwardingContext,
|
||||||
|
request,
|
||||||
|
cancellationToken);
|
||||||
|
|
||||||
|
public Task<PolicyEngineResponse<CvssScoreReceipt>> GetCvssReceiptAsync(
|
||||||
|
GatewayForwardingContext? forwardingContext,
|
||||||
|
string receiptId,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
=> SendAsync<CvssScoreReceipt>(
|
||||||
|
HttpMethod.Get,
|
||||||
|
$"api/cvss/receipts/{Uri.EscapeDataString(receiptId)}",
|
||||||
|
forwardingContext,
|
||||||
|
content: null,
|
||||||
|
cancellationToken);
|
||||||
|
|
||||||
|
public Task<PolicyEngineResponse<CvssScoreReceipt>> AmendCvssReceiptAsync(
|
||||||
|
GatewayForwardingContext? forwardingContext,
|
||||||
|
string receiptId,
|
||||||
|
AmendCvssReceiptRequest request,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
=> SendAsync<CvssScoreReceipt>(
|
||||||
|
HttpMethod.Put,
|
||||||
|
$"api/cvss/receipts/{Uri.EscapeDataString(receiptId)}/amend",
|
||||||
|
forwardingContext,
|
||||||
|
request,
|
||||||
|
cancellationToken);
|
||||||
|
|
||||||
|
public Task<PolicyEngineResponse<IReadOnlyList<ReceiptHistoryEntry>>> GetCvssReceiptHistoryAsync(
|
||||||
|
GatewayForwardingContext? forwardingContext,
|
||||||
|
string receiptId,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
=> SendAsync<IReadOnlyList<ReceiptHistoryEntry>>(
|
||||||
|
HttpMethod.Get,
|
||||||
|
$"api/cvss/receipts/{Uri.EscapeDataString(receiptId)}/history",
|
||||||
|
forwardingContext,
|
||||||
|
content: null,
|
||||||
|
cancellationToken);
|
||||||
|
|
||||||
|
public Task<PolicyEngineResponse<IReadOnlyList<CvssPolicy>>> ListCvssPoliciesAsync(
|
||||||
|
GatewayForwardingContext? forwardingContext,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
=> SendAsync<IReadOnlyList<CvssPolicy>>(
|
||||||
|
HttpMethod.Get,
|
||||||
|
"api/cvss/policies",
|
||||||
|
forwardingContext,
|
||||||
|
content: null,
|
||||||
|
cancellationToken);
|
||||||
|
|
||||||
private async Task<PolicyEngineResponse<TSuccess>> SendAsync<TSuccess>(
|
private async Task<PolicyEngineResponse<TSuccess>> SendAsync<TSuccess>(
|
||||||
HttpMethod method,
|
HttpMethod method,
|
||||||
string relativeUri,
|
string relativeUri,
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using StellaOps.Attestor.Envelope;
|
||||||
|
using StellaOps.Policy.Scoring;
|
||||||
|
using StellaOps.Policy.Scoring.Receipts;
|
||||||
|
|
||||||
|
namespace StellaOps.Policy.Gateway.Contracts;
|
||||||
|
|
||||||
|
public sealed record CreateCvssReceiptRequest(
|
||||||
|
[Required] string VulnerabilityId,
|
||||||
|
[Required] CvssPolicy Policy,
|
||||||
|
[Required] CvssBaseMetrics BaseMetrics,
|
||||||
|
CvssThreatMetrics? ThreatMetrics,
|
||||||
|
CvssEnvironmentalMetrics? EnvironmentalMetrics,
|
||||||
|
CvssSupplementalMetrics? SupplementalMetrics,
|
||||||
|
IReadOnlyList<CvssEvidenceItem>? Evidence,
|
||||||
|
EnvelopeKey? SigningKey,
|
||||||
|
string? CreatedBy,
|
||||||
|
DateTimeOffset? CreatedAt);
|
||||||
|
|
||||||
|
public sealed record AmendCvssReceiptRequest(
|
||||||
|
[Required] string Field,
|
||||||
|
string? PreviousValue,
|
||||||
|
string? NewValue,
|
||||||
|
[Required] string Reason,
|
||||||
|
string? ReferenceUri,
|
||||||
|
EnvelopeKey? SigningKey,
|
||||||
|
string? Actor);
|
||||||
|
|
||||||
|
public sealed record CvssReceiptHistoryResponse(
|
||||||
|
string ReceiptId,
|
||||||
|
IReadOnlyList<ReceiptHistoryEntry> History);
|
||||||
@@ -336,6 +336,137 @@ policyPacks.MapPost("/{packId}/revisions/{version:int}:activate", async Task<IRe
|
|||||||
StellaOpsScopes.PolicyOperate,
|
StellaOpsScopes.PolicyOperate,
|
||||||
StellaOpsScopes.PolicyActivate));
|
StellaOpsScopes.PolicyActivate));
|
||||||
|
|
||||||
|
var cvss = app.MapGroup("/api/cvss")
|
||||||
|
.WithTags("CVSS Receipts");
|
||||||
|
|
||||||
|
cvss.MapPost("/receipts", async Task<IResult>(
|
||||||
|
HttpContext context,
|
||||||
|
CreateCvssReceiptRequest request,
|
||||||
|
IPolicyEngineClient client,
|
||||||
|
PolicyEngineTokenProvider tokenProvider,
|
||||||
|
CancellationToken cancellationToken) =>
|
||||||
|
{
|
||||||
|
if (request is null)
|
||||||
|
{
|
||||||
|
return Results.BadRequest(new ProblemDetails
|
||||||
|
{
|
||||||
|
Title = "Request body required.",
|
||||||
|
Status = StatusCodes.Status400BadRequest
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
GatewayForwardingContext? forwardingContext = null;
|
||||||
|
if (GatewayForwardingContext.TryCreate(context, out var callerContext))
|
||||||
|
{
|
||||||
|
forwardingContext = callerContext;
|
||||||
|
}
|
||||||
|
else if (!tokenProvider.IsEnabled)
|
||||||
|
{
|
||||||
|
return Results.Unauthorized();
|
||||||
|
}
|
||||||
|
|
||||||
|
var response = await client.CreateCvssReceiptAsync(forwardingContext, request, cancellationToken).ConfigureAwait(false);
|
||||||
|
return response.ToMinimalResult();
|
||||||
|
})
|
||||||
|
.RequireAuthorization(policy => policy.RequireStellaOpsScopes(StellaOpsScopes.PolicyRun));
|
||||||
|
|
||||||
|
cvss.MapGet("/receipts/{receiptId}", async Task<IResult>(
|
||||||
|
HttpContext context,
|
||||||
|
string receiptId,
|
||||||
|
IPolicyEngineClient client,
|
||||||
|
PolicyEngineTokenProvider tokenProvider,
|
||||||
|
CancellationToken cancellationToken) =>
|
||||||
|
{
|
||||||
|
GatewayForwardingContext? forwardingContext = null;
|
||||||
|
if (GatewayForwardingContext.TryCreate(context, out var callerContext))
|
||||||
|
{
|
||||||
|
forwardingContext = callerContext;
|
||||||
|
}
|
||||||
|
else if (!tokenProvider.IsEnabled)
|
||||||
|
{
|
||||||
|
return Results.Unauthorized();
|
||||||
|
}
|
||||||
|
|
||||||
|
var response = await client.GetCvssReceiptAsync(forwardingContext, receiptId, cancellationToken).ConfigureAwait(false);
|
||||||
|
return response.ToMinimalResult();
|
||||||
|
})
|
||||||
|
.RequireAuthorization(policy => policy.RequireStellaOpsScopes(StellaOpsScopes.FindingsRead));
|
||||||
|
|
||||||
|
cvss.MapPut("/receipts/{receiptId}/amend", async Task<IResult>(
|
||||||
|
HttpContext context,
|
||||||
|
string receiptId,
|
||||||
|
AmendCvssReceiptRequest request,
|
||||||
|
IPolicyEngineClient client,
|
||||||
|
PolicyEngineTokenProvider tokenProvider,
|
||||||
|
CancellationToken cancellationToken) =>
|
||||||
|
{
|
||||||
|
if (request is null)
|
||||||
|
{
|
||||||
|
return Results.BadRequest(new ProblemDetails
|
||||||
|
{
|
||||||
|
Title = "Request body required.",
|
||||||
|
Status = StatusCodes.Status400BadRequest
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
GatewayForwardingContext? forwardingContext = null;
|
||||||
|
if (GatewayForwardingContext.TryCreate(context, out var callerContext))
|
||||||
|
{
|
||||||
|
forwardingContext = callerContext;
|
||||||
|
}
|
||||||
|
else if (!tokenProvider.IsEnabled)
|
||||||
|
{
|
||||||
|
return Results.Unauthorized();
|
||||||
|
}
|
||||||
|
|
||||||
|
var response = await client.AmendCvssReceiptAsync(forwardingContext, receiptId, request, cancellationToken).ConfigureAwait(false);
|
||||||
|
return response.ToMinimalResult();
|
||||||
|
})
|
||||||
|
.RequireAuthorization(policy => policy.RequireStellaOpsScopes(StellaOpsScopes.PolicyRun));
|
||||||
|
|
||||||
|
cvss.MapGet("/receipts/{receiptId}/history", async Task<IResult>(
|
||||||
|
HttpContext context,
|
||||||
|
string receiptId,
|
||||||
|
IPolicyEngineClient client,
|
||||||
|
PolicyEngineTokenProvider tokenProvider,
|
||||||
|
CancellationToken cancellationToken) =>
|
||||||
|
{
|
||||||
|
GatewayForwardingContext? forwardingContext = null;
|
||||||
|
if (GatewayForwardingContext.TryCreate(context, out var callerContext))
|
||||||
|
{
|
||||||
|
forwardingContext = callerContext;
|
||||||
|
}
|
||||||
|
else if (!tokenProvider.IsEnabled)
|
||||||
|
{
|
||||||
|
return Results.Unauthorized();
|
||||||
|
}
|
||||||
|
|
||||||
|
var response = await client.GetCvssReceiptHistoryAsync(forwardingContext, receiptId, cancellationToken).ConfigureAwait(false);
|
||||||
|
return response.ToMinimalResult();
|
||||||
|
})
|
||||||
|
.RequireAuthorization(policy => policy.RequireStellaOpsScopes(StellaOpsScopes.FindingsRead));
|
||||||
|
|
||||||
|
cvss.MapGet("/policies", async Task<IResult>(
|
||||||
|
HttpContext context,
|
||||||
|
IPolicyEngineClient client,
|
||||||
|
PolicyEngineTokenProvider tokenProvider,
|
||||||
|
CancellationToken cancellationToken) =>
|
||||||
|
{
|
||||||
|
GatewayForwardingContext? forwardingContext = null;
|
||||||
|
if (GatewayForwardingContext.TryCreate(context, out var callerContext))
|
||||||
|
{
|
||||||
|
forwardingContext = callerContext;
|
||||||
|
}
|
||||||
|
else if (!tokenProvider.IsEnabled)
|
||||||
|
{
|
||||||
|
return Results.Unauthorized();
|
||||||
|
}
|
||||||
|
|
||||||
|
var response = await client.ListCvssPoliciesAsync(forwardingContext, cancellationToken).ConfigureAwait(false);
|
||||||
|
return response.ToMinimalResult();
|
||||||
|
})
|
||||||
|
.RequireAuthorization(policy => policy.RequireStellaOpsScopes(StellaOpsScopes.FindingsRead));
|
||||||
|
|
||||||
app.Run();
|
app.Run();
|
||||||
|
|
||||||
static IAsyncPolicy<HttpResponseMessage> CreateAuthorityRetryPolicy(IServiceProvider provider)
|
static IAsyncPolicy<HttpResponseMessage> CreateAuthorityRetryPolicy(IServiceProvider provider)
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
<ProjectReference Include="../../Authority/StellaOps.Authority/StellaOps.Auth.Client/StellaOps.Auth.Client.csproj" />
|
<ProjectReference Include="../../Authority/StellaOps.Authority/StellaOps.Auth.Client/StellaOps.Auth.Client.csproj" />
|
||||||
<ProjectReference Include="../../Authority/StellaOps.Authority/StellaOps.Auth.ServerIntegration/StellaOps.Auth.ServerIntegration.csproj" />
|
<ProjectReference Include="../../Authority/StellaOps.Authority/StellaOps.Auth.ServerIntegration/StellaOps.Auth.ServerIntegration.csproj" />
|
||||||
<ProjectReference Include="../../AirGap/StellaOps.AirGap.Policy/StellaOps.AirGap.Policy/StellaOps.AirGap.Policy.csproj" />
|
<ProjectReference Include="../../AirGap/StellaOps.AirGap.Policy/StellaOps.AirGap.Policy/StellaOps.AirGap.Policy.csproj" />
|
||||||
|
<ProjectReference Include="../StellaOps.Policy.Scoring/StellaOps.Policy.Scoring.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.Extensions.Http.Polly" Version="10.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Http.Polly" Version="10.0.0" />
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import jsPDF from './jspdf.stub';
|
|||||||
imports: [CommonModule],
|
imports: [CommonModule],
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
template: `
|
template: `
|
||||||
<section class="expl" aria-busy="{{ loading }}">
|
<section class="expl" [attr.aria-busy]="loading">
|
||||||
<header class="expl__header" *ngIf="result">
|
<header class="expl__header" *ngIf="result">
|
||||||
<div>
|
<div>
|
||||||
<p class="expl__eyebrow">Policy Studio · Explain</p>
|
<p class="expl__eyebrow">Policy Studio · Explain</p>
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { ActivatedRoute } from '@angular/router';
|
|||||||
imports: [CommonModule, ReactiveFormsModule],
|
imports: [CommonModule, ReactiveFormsModule],
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
template: `
|
template: `
|
||||||
<section class="rb" aria-busy="false">
|
<section class="rb" [attr.aria-busy]="false">
|
||||||
<header class="rb__header">
|
<header class="rb__header">
|
||||||
<div>
|
<div>
|
||||||
<p class="rb__eyebrow">Policy Studio · Rule Builder</p>
|
<p class="rb__eyebrow">Policy Studio · Rule Builder</p>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="MongoDB.Driver" Version="3.5.0" />
|
<ProjectReference Include="..\..\Concelier\__Libraries\StellaOps.Concelier.Models\StellaOps.Concelier.Models.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
Reference in New Issue
Block a user