feat: Add CVSS receipt management endpoints and related functionality
Some checks failed
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Concelier Attestation Tests / attestation-tests (push) Has been cancelled
Docs CI / lint-and-preview (push) Has been cancelled

- 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:
StellaOps Bot
2025-12-07 00:43:14 +02:00
parent 0de92144d2
commit 53889d85e7
67 changed files with 17207 additions and 16293 deletions

View 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`.

View File

@@ -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. |
| 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.) |
| 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`. |
| 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. |
@@ -48,7 +48,7 @@
| --- | --- | --- | --- | --- |
| 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. |
| 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. |
## 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. |
| 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 811). | API/CLI/UI delivery stalled. | AGENTS delivered 2025-12-06 (tasks 1516). 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
| 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 | Created Policy Gateway AGENTS and refreshed Concelier AGENTS for CVSS v4 ingest (tasks 1516 DONE); moved tasks 811 to TODO, set W3 to TODO, mitigated risk R5. | Project Mgmt |
| 2025-12-06 | Added tasks 1516 to create AGENTS for Policy WebService and Concelier; set Wave 2 to DONE; marked Waves 34 BLOCKED until AGENTS exist; captured risk R5. | Project Mgmt |

View File

@@ -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 | 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 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-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 |

View File

@@ -52,7 +52,7 @@
| 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 |
| 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. |
| 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. |
@@ -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 | 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 | PG-T7.1.5b set to DOING; began wiring Postgres document store (DI registration, repository find) to replace Mongo bindings. | Concelier Guild |
## Decisions & Risks
- Cleanup is strictly after all phases complete; do not start T7 tasks until module cutovers are DONE.

View File

@@ -3,7 +3,7 @@
| # | 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. |
| 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. |
| 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. |

View File

@@ -261,7 +261,7 @@ public sealed class AcscConnector : IFeedConnector
_diagnostics.ParseAttempt(feedTag);
if (!document.GridFsId.HasValue)
if (!document.PayloadId.HasValue)
{
_diagnostics.ParseFailure(feedTag, "missingPayload");
_logger.LogWarning("ACSC document {DocumentId} missing GridFS payload (feed={Feed})", document.Id, feedTag);
@@ -274,7 +274,7 @@ public sealed class AcscConnector : IFeedConnector
byte[] rawBytes;
try
{
rawBytes = await _rawDocumentStorage.DownloadAsync(document.GridFsId.Value, cancellationToken).ConfigureAwait(false);
rawBytes = await _rawDocumentStorage.DownloadAsync(document.PayloadId.Value, cancellationToken).ConfigureAwait(false);
}
catch (Exception ex)
{
@@ -556,12 +556,12 @@ public sealed class AcscConnector : IFeedConnector
private async Task<DateTimeOffset?> TryComputeLatestPublishedAsync(DocumentRecord document, CancellationToken cancellationToken)
{
if (!document.GridFsId.HasValue)
if (!document.PayloadId.HasValue)
{
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)
{
return null;

View File

@@ -182,7 +182,7 @@ public sealed class CccsConnector : IFeedConnector
Metadata: metadata,
Etag: null,
LastModified: rawDocument.Modified ?? rawDocument.Published ?? result.LastModifiedUtc,
GridFsId: gridFsId,
PayloadId: gridFsId,
ExpiresAt: null);
var upserted = await _documentStore.UpsertAsync(record, cancellationToken).ConfigureAwait(false);
@@ -262,7 +262,7 @@ public sealed class CccsConnector : IFeedConnector
continue;
}
if (!document.GridFsId.HasValue)
if (!document.PayloadId.HasValue)
{
_diagnostics.ParseFailure();
_logger.LogWarning("CCCS document {DocumentId} missing GridFS payload", documentId);
@@ -276,7 +276,7 @@ public sealed class CccsConnector : IFeedConnector
byte[] payload;
try
{
payload = await _rawDocumentStorage.DownloadAsync(document.GridFsId.Value, cancellationToken).ConfigureAwait(false);
payload = await _rawDocumentStorage.DownloadAsync(document.PayloadId.Value, cancellationToken).ConfigureAwait(false);
}
catch (Exception ex)
{

View File

@@ -245,7 +245,7 @@ public sealed class CertBundConnector : IFeedConnector
continue;
}
if (!document.GridFsId.HasValue)
if (!document.PayloadId.HasValue)
{
await _documentStore.UpdateStatusAsync(document.Id, DocumentStatuses.Failed, cancellationToken).ConfigureAwait(false);
remainingDocuments.Remove(documentId);
@@ -258,7 +258,7 @@ public sealed class CertBundConnector : IFeedConnector
byte[] payload;
try
{
payload = await _rawDocumentStorage.DownloadAsync(document.GridFsId.Value, cancellationToken).ConfigureAwait(false);
payload = await _rawDocumentStorage.DownloadAsync(document.PayloadId.Value, cancellationToken).ConfigureAwait(false);
}
catch (Exception ex)
{

View File

@@ -597,23 +597,23 @@ public sealed class CertCcConnector : IFeedConnector
private async Task<IReadOnlyList<string>> ReadSummaryNotesAsync(DocumentRecord document, CancellationToken cancellationToken)
{
if (!document.GridFsId.HasValue)
if (!document.PayloadId.HasValue)
{
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);
}
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.");
}
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)

View File

@@ -196,7 +196,7 @@ public sealed class CertFrConnector : IFeedConnector
continue;
}
if (!document.GridFsId.HasValue)
if (!document.PayloadId.HasValue)
{
_logger.LogWarning("Cert-FR document {DocumentId} missing GridFS payload", document.Id);
await _documentStore.UpdateStatusAsync(document.Id, DocumentStatuses.Failed, cancellationToken).ConfigureAwait(false);
@@ -222,7 +222,7 @@ public sealed class CertFrConnector : IFeedConnector
CertFrDto dto;
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);
dto = CertFrParser.Parse(html, metadata);
}

View File

@@ -198,7 +198,7 @@ public sealed class CertInConnector : IFeedConnector
continue;
}
if (!document.GridFsId.HasValue)
if (!document.PayloadId.HasValue)
{
_logger.LogWarning("CERT-In document {DocumentId} missing GridFS payload", document.Id);
await _documentStore.UpdateStatusAsync(document.Id, DocumentStatuses.Failed, cancellationToken).ConfigureAwait(false);
@@ -217,7 +217,7 @@ public sealed class CertInConnector : IFeedConnector
byte[] rawBytes;
try
{
rawBytes = await _rawDocumentStorage.DownloadAsync(document.GridFsId.Value, cancellationToken).ConfigureAwait(false);
rawBytes = await _rawDocumentStorage.DownloadAsync(document.PayloadId.Value, cancellationToken).ConfigureAwait(false);
}
catch (Exception ex)
{

View File

@@ -3,6 +3,7 @@ using System.Net.Http;
using System.Net.Security;
using System.Security.Cryptography.X509Certificates;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Options;
using StellaOps.Concelier.Connector.Common.Xml;
using StellaOps.Concelier.Core.Aoc;
@@ -169,7 +170,7 @@ public static class ServiceCollectionExtensions
services.AddSingleton<Fetch.IJitterSource, Fetch.CryptoJitterSource>();
services.AddConcelierAocGuards();
services.AddConcelierLinksetMappers();
services.AddSingleton<IDocumentStore, InMemoryDocumentStore>();
services.TryAddSingleton<IDocumentStore, InMemoryDocumentStore>();
services.AddSingleton<Fetch.RawDocumentStorage>();
services.AddSingleton<Fetch.SourceFetchService>();

View File

@@ -145,7 +145,7 @@ public sealed class SourceStateSeedProcessor
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);
}

View File

@@ -342,7 +342,7 @@ public sealed class CveConnector : IFeedConnector
continue;
}
if (!document.GridFsId.HasValue)
if (!document.PayloadId.HasValue)
{
_diagnostics.ParseFailure();
_logger.LogWarning("CVEs document {DocumentId} missing GridFS content", documentId);
@@ -354,7 +354,7 @@ public sealed class CveConnector : IFeedConnector
byte[] rawBytes;
try
{
rawBytes = await _rawDocumentStorage.DownloadAsync(document.GridFsId.Value, cancellationToken).ConfigureAwait(false);
rawBytes = await _rawDocumentStorage.DownloadAsync(document.PayloadId.Value, cancellationToken).ConfigureAwait(false);
}
catch (Exception ex)
{
@@ -514,7 +514,7 @@ public sealed class CveConnector : IFeedConnector
try
{
if (existing?.GridFsId is ObjectId existingGrid && existingGrid != ObjectId.Empty)
if (existing?.PayloadId is ObjectId existingGrid && existingGrid != ObjectId.Empty)
{
gridId = existingGrid;
}
@@ -547,7 +547,7 @@ public sealed class CveConnector : IFeedConnector
Metadata: metadata,
Etag: null,
LastModified: lastModified,
GridFsId: gridId);
PayloadId: gridId);
await _documentStore.UpsertAsync(document, cancellationToken).ConfigureAwait(false);

View File

@@ -127,7 +127,7 @@ public sealed class DebianConnector : IFeedConnector
{
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);
}
@@ -136,7 +136,7 @@ public sealed class DebianConnector : IFeedConnector
byte[] bytes;
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)
{
@@ -326,7 +326,7 @@ public sealed class DebianConnector : IFeedConnector
continue;
}
if (!document.GridFsId.HasValue)
if (!document.PayloadId.HasValue)
{
_logger.LogWarning("Debian document {DocumentId} missing GridFS payload", document.Id);
await _documentStore.UpdateStatusAsync(document.Id, DocumentStatuses.Failed, cancellationToken).ConfigureAwait(false);
@@ -346,7 +346,7 @@ public sealed class DebianConnector : IFeedConnector
byte[] bytes;
try
{
bytes = await _rawDocumentStorage.DownloadAsync(document.GridFsId.Value, cancellationToken).ConfigureAwait(false);
bytes = await _rawDocumentStorage.DownloadAsync(document.PayloadId.Value, cancellationToken).ConfigureAwait(false);
}
catch (Exception ex)
{

View File

@@ -301,7 +301,7 @@ public sealed class RedHatConnector : IFeedConnector
continue;
}
if (!document.GridFsId.HasValue)
if (!document.PayloadId.HasValue)
{
_logger.LogWarning("Red Hat document {DocumentId} missing GridFS content; skipping", document.Id);
remainingFetch.Remove(documentId);
@@ -309,7 +309,7 @@ public sealed class RedHatConnector : IFeedConnector
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);
var sanitized = JsonSerializer.Serialize(jsonDocument.RootElement);
var payload = BsonDocument.Parse(sanitized);

View File

@@ -125,12 +125,12 @@ public sealed class SuseConnector : IFeedConnector
else if (changesResult.IsSuccess && changesResult.Document is not null)
{
fetchCache[changesKey] = SuseFetchCacheEntry.FromDocument(changesResult.Document);
if (changesResult.Document.GridFsId.HasValue)
if (changesResult.Document.PayloadId.HasValue)
{
byte[] changesBytes;
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)
{
@@ -302,7 +302,7 @@ public sealed class SuseConnector : IFeedConnector
continue;
}
if (!document.GridFsId.HasValue)
if (!document.PayloadId.HasValue)
{
_logger.LogWarning("SUSE document {DocumentId} missing GridFS payload", document.Id);
await _documentStore.UpdateStatusAsync(document.Id, DocumentStatuses.Failed, cancellationToken).ConfigureAwait(false);
@@ -313,7 +313,7 @@ public sealed class SuseConnector : IFeedConnector
byte[] bytes;
try
{
bytes = await _rawDocumentStorage.DownloadAsync(document.GridFsId.Value, cancellationToken).ConfigureAwait(false);
bytes = await _rawDocumentStorage.DownloadAsync(document.PayloadId.Value, cancellationToken).ConfigureAwait(false);
}
catch (Exception ex)
{

View File

@@ -160,7 +160,7 @@ public sealed class UbuntuConnector : IFeedConnector
Metadata: metadata,
Etag: existing?.Etag,
LastModified: existing?.LastModified ?? notice.Published,
GridFsId: null);
PayloadId: null);
await _documentStore.UpsertAsync(record, cancellationToken).ConfigureAwait(false);
@@ -311,12 +311,12 @@ public sealed class UbuntuConnector : IFeedConnector
fetchCache[cacheKey] = cachedEntryForPage;
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;
}
payload = await _rawDocumentStorage.DownloadAsync(existingDocument.GridFsId.Value, cancellationToken).ConfigureAwait(false);
payload = await _rawDocumentStorage.DownloadAsync(existingDocument.PayloadId.Value, cancellationToken).ConfigureAwait(false);
}
else
{
@@ -327,13 +327,13 @@ public sealed class UbuntuConnector : IFeedConnector
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);
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));

View File

@@ -284,7 +284,7 @@ public sealed class GhsaConnector : IFeedConnector
continue;
}
if (!document.GridFsId.HasValue)
if (!document.PayloadId.HasValue)
{
_diagnostics.ParseFailure();
_logger.LogWarning("GHSA document {DocumentId} missing GridFS content", documentId);
@@ -296,7 +296,7 @@ public sealed class GhsaConnector : IFeedConnector
byte[] rawBytes;
try
{
rawBytes = await _rawDocumentStorage.DownloadAsync(document.GridFsId.Value, cancellationToken).ConfigureAwait(false);
rawBytes = await _rawDocumentStorage.DownloadAsync(document.PayloadId.Value, cancellationToken).ConfigureAwait(false);
}
catch (Exception ex)
{

View File

@@ -247,7 +247,7 @@ public sealed class IcsCisaConnector : IFeedConnector
topicId = topicValue;
}
if (!document.GridFsId.HasValue)
if (!document.PayloadId.HasValue)
{
await _documentStore.UpdateStatusAsync(document.Id, DocumentStatuses.Failed, cancellationToken).ConfigureAwait(false);
remainingDocuments.Remove(documentId);
@@ -259,7 +259,7 @@ public sealed class IcsCisaConnector : IFeedConnector
byte[] bytes;
try
{
bytes = await _rawDocumentStorage.DownloadAsync(document.GridFsId.Value, cancellationToken).ConfigureAwait(false);
bytes = await _rawDocumentStorage.DownloadAsync(document.PayloadId.Value, cancellationToken).ConfigureAwait(false);
}
catch (Exception ex)
{

View File

@@ -239,7 +239,7 @@ public sealed class KasperskyConnector : IFeedConnector
continue;
}
if (!document.GridFsId.HasValue)
if (!document.PayloadId.HasValue)
{
_logger.LogWarning("Kaspersky document {DocumentId} missing GridFS content", document.Id);
await _documentStore.UpdateStatusAsync(document.Id, DocumentStatuses.Failed, cancellationToken).ConfigureAwait(false);
@@ -260,7 +260,7 @@ public sealed class KasperskyConnector : IFeedConnector
byte[] rawBytes;
try
{
rawBytes = await _rawDocumentStorage.DownloadAsync(document.GridFsId.Value, cancellationToken).ConfigureAwait(false);
rawBytes = await _rawDocumentStorage.DownloadAsync(document.PayloadId.Value, cancellationToken).ConfigureAwait(false);
}
catch (Exception ex)
{

View File

@@ -189,7 +189,7 @@ public sealed class JvnConnector : IFeedConnector
continue;
}
if (!document.GridFsId.HasValue)
if (!document.PayloadId.HasValue)
{
_logger.LogWarning("JVN document {DocumentId} is missing GridFS content; marking as failed", documentId);
await _documentStore.UpdateStatusAsync(document.Id, DocumentStatuses.Failed, cancellationToken).ConfigureAwait(false);
@@ -200,7 +200,7 @@ public sealed class JvnConnector : IFeedConnector
byte[] rawBytes;
try
{
rawBytes = await _rawDocumentStorage.DownloadAsync(document.GridFsId.Value, cancellationToken).ConfigureAwait(false);
rawBytes = await _rawDocumentStorage.DownloadAsync(document.PayloadId.Value, cancellationToken).ConfigureAwait(false);
}
catch (Exception ex)
{

View File

@@ -183,7 +183,7 @@ public sealed class KevConnector : IFeedConnector
continue;
}
if (!document.GridFsId.HasValue)
if (!document.PayloadId.HasValue)
{
_diagnostics.ParseFailure("missingPayload", cursor.CatalogVersion);
_logger.LogWarning("KEV document {DocumentId} missing GridFS payload", document.Id);
@@ -196,7 +196,7 @@ public sealed class KevConnector : IFeedConnector
byte[] rawBytes;
try
{
rawBytes = await _rawDocumentStorage.DownloadAsync(document.GridFsId.Value, cancellationToken).ConfigureAwait(false);
rawBytes = await _rawDocumentStorage.DownloadAsync(document.PayloadId.Value, cancellationToken).ConfigureAwait(false);
}
catch (Exception ex)
{

View File

@@ -244,7 +244,7 @@ public sealed class KisaConnector : IFeedConnector
}
var category = GetCategory(document);
if (!document.GridFsId.HasValue)
if (!document.PayloadId.HasValue)
{
_diagnostics.ParseFailure(category, "missing-gridfs");
_logger.LogWarning("KISA document {DocumentId} missing GridFS payload", document.Id);
@@ -259,7 +259,7 @@ public sealed class KisaConnector : IFeedConnector
byte[] payload;
try
{
payload = await _rawDocumentStorage.DownloadAsync(document.GridFsId.Value, cancellationToken).ConfigureAwait(false);
payload = await _rawDocumentStorage.DownloadAsync(document.PayloadId.Value, cancellationToken).ConfigureAwait(false);
}
catch (Exception ex)
{

View File

@@ -179,7 +179,7 @@ public sealed class NvdConnector : IFeedConnector
continue;
}
if (!document.GridFsId.HasValue)
if (!document.PayloadId.HasValue)
{
_logger.LogWarning("Document {DocumentId} is missing GridFS content; skipping", documentId);
_diagnostics.ParseFailure();
@@ -188,7 +188,7 @@ public sealed class NvdConnector : IFeedConnector
continue;
}
var rawBytes = await _rawDocumentStorage.DownloadAsync(document.GridFsId.Value, cancellationToken).ConfigureAwait(false);
var rawBytes = await _rawDocumentStorage.DownloadAsync(document.PayloadId.Value, cancellationToken).ConfigureAwait(false);
try
{
using var jsonDocument = JsonDocument.Parse(rawBytes);
@@ -314,7 +314,7 @@ public sealed class NvdConnector : IFeedConnector
DocumentRecord firstDocument,
CancellationToken cancellationToken)
{
if (firstDocument.GridFsId is null)
if (firstDocument.PayloadId is null)
{
return Array.Empty<Guid>();
}
@@ -322,7 +322,7 @@ public sealed class NvdConnector : IFeedConnector
byte[] rawBytes;
try
{
rawBytes = await _rawDocumentStorage.DownloadAsync(firstDocument.GridFsId.Value, cancellationToken).ConfigureAwait(false);
rawBytes = await _rawDocumentStorage.DownloadAsync(firstDocument.PayloadId.Value, cancellationToken).ConfigureAwait(false);
}
catch (Exception ex)
{

View File

@@ -147,7 +147,7 @@ public sealed class OsvConnector : IFeedConnector
continue;
}
if (!document.GridFsId.HasValue)
if (!document.PayloadId.HasValue)
{
_logger.LogWarning("OSV document {DocumentId} missing GridFS content", document.Id);
await _documentStore.UpdateStatusAsync(document.Id, DocumentStatuses.Failed, cancellationToken).ConfigureAwait(false);
@@ -158,7 +158,7 @@ public sealed class OsvConnector : IFeedConnector
byte[] bytes;
try
{
bytes = await _rawDocumentStorage.DownloadAsync(document.GridFsId.Value, cancellationToken).ConfigureAwait(false);
bytes = await _rawDocumentStorage.DownloadAsync(document.PayloadId.Value, cancellationToken).ConfigureAwait(false);
}
catch (Exception ex)
{
@@ -447,7 +447,7 @@ public sealed class OsvConnector : IFeedConnector
Metadata: metadata,
Etag: null,
LastModified: modified,
GridFsId: gridFsId,
PayloadId: gridFsId,
ExpiresAt: null);
var upserted = await _documentStore.UpsertAsync(record, cancellationToken).ConfigureAwait(false);

View File

@@ -221,7 +221,7 @@ public sealed class RuBduConnector : IFeedConnector
continue;
}
if (!document.GridFsId.HasValue)
if (!document.PayloadId.HasValue)
{
_logger.LogWarning("RU-BDU document {DocumentId} missing GridFS payload", documentId);
_diagnostics.ParseFailure();
@@ -234,7 +234,7 @@ public sealed class RuBduConnector : IFeedConnector
byte[] payload;
try
{
payload = await _rawDocumentStorage.DownloadAsync(document.GridFsId.Value, cancellationToken).ConfigureAwait(false);
payload = await _rawDocumentStorage.DownloadAsync(document.PayloadId.Value, cancellationToken).ConfigureAwait(false);
}
catch (Exception ex)
{
@@ -435,7 +435,7 @@ public sealed class RuBduConnector : IFeedConnector
Metadata: metadata,
Etag: null,
LastModified: archiveLastModified ?? dto.IdentifyDate,
GridFsId: gridFsId,
PayloadId: gridFsId,
ExpiresAt: null);
var upserted = await _documentStore.UpsertAsync(record, cancellationToken).ConfigureAwait(false);

View File

@@ -295,7 +295,7 @@ public sealed class RuNkckiConnector : IFeedConnector
continue;
}
if (!document.GridFsId.HasValue)
if (!document.PayloadId.HasValue)
{
_logger.LogWarning("NKCKI document {DocumentId} missing GridFS payload", documentId);
await _documentStore.UpdateStatusAsync(document.Id, DocumentStatuses.Failed, cancellationToken).ConfigureAwait(false);
@@ -307,7 +307,7 @@ public sealed class RuNkckiConnector : IFeedConnector
byte[] payload;
try
{
payload = await _rawDocumentStorage.DownloadAsync(document.GridFsId.Value, cancellationToken).ConfigureAwait(false);
payload = await _rawDocumentStorage.DownloadAsync(document.PayloadId.Value, cancellationToken).ConfigureAwait(false);
}
catch (Exception ex)
{
@@ -641,7 +641,7 @@ public sealed class RuNkckiConnector : IFeedConnector
Metadata: metadata,
Etag: null,
LastModified: lastModified,
GridFsId: gridFsId,
PayloadId: gridFsId,
ExpiresAt: null);
var upserted = await _documentStore.UpsertAsync(record, cancellationToken).ConfigureAwait(false);

View File

@@ -251,7 +251,7 @@ public sealed class StellaOpsMirrorConnector : IFeedConnector
Metadata: metadata,
Etag: null,
LastModified: generatedAt,
GridFsId: gridFsId,
PayloadId: gridFsId,
ExpiresAt: null);
var upserted = await _documentStore.UpsertAsync(record, cancellationToken).ConfigureAwait(false);
@@ -372,7 +372,7 @@ public sealed class StellaOpsMirrorConnector : IFeedConnector
continue;
}
if (!document.GridFsId.HasValue)
if (!document.PayloadId.HasValue)
{
_logger.LogWarning("Mirror bundle document {DocumentId} missing GridFS payload.", documentId);
await _documentStore.UpdateStatusAsync(document.Id, DocumentStatuses.Failed, cancellationToken).ConfigureAwait(false);
@@ -385,7 +385,7 @@ public sealed class StellaOpsMirrorConnector : IFeedConnector
byte[] payload;
try
{
payload = await _rawDocumentStorage.DownloadAsync(document.GridFsId.Value, cancellationToken).ConfigureAwait(false);
payload = await _rawDocumentStorage.DownloadAsync(document.PayloadId.Value, cancellationToken).ConfigureAwait(false);
}
catch (Exception ex)
{

View File

@@ -452,7 +452,7 @@ public sealed class AdobeConnector : IFeedConnector
continue;
}
if (!document.GridFsId.HasValue)
if (!document.PayloadId.HasValue)
{
_logger.LogWarning("Adobe document {DocumentId} missing GridFS payload", document.Id);
await _documentStore.UpdateStatusAsync(document.Id, DocumentStatuses.Failed, cancellationToken).ConfigureAwait(false);
@@ -478,7 +478,7 @@ public sealed class AdobeConnector : IFeedConnector
AdobeBulletinDto dto;
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);
dto = AdobeDetailParser.Parse(html, metadata);
}

View File

@@ -225,7 +225,7 @@ public sealed class AppleConnector : IFeedConnector
continue;
}
if (!document.GridFsId.HasValue)
if (!document.PayloadId.HasValue)
{
_diagnostics.ParseFailure();
_logger.LogWarning("Apple document {DocumentId} missing GridFS payload", document.Id);
@@ -238,7 +238,7 @@ public sealed class AppleConnector : IFeedConnector
AppleDetailDto dto;
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 entry = RehydrateIndexEntry(document);
dto = AppleDetailParser.Parse(html, entry);

View File

@@ -214,7 +214,7 @@ public sealed class ChromiumConnector : IFeedConnector
continue;
}
if (!document.GridFsId.HasValue)
if (!document.PayloadId.HasValue)
{
_logger.LogWarning("Chromium document {DocumentId} missing GridFS payload", document.Id);
await _documentStore.UpdateStatusAsync(document.Id, DocumentStatuses.Failed, cancellationToken).ConfigureAwait(false);
@@ -227,7 +227,7 @@ public sealed class ChromiumConnector : IFeedConnector
try
{
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);
dto = ChromiumParser.Parse(html, metadata);
}

View File

@@ -163,7 +163,7 @@ public sealed class CiscoConnector : IFeedConnector
BuildMetadata(advisory),
Etag: null,
LastModified: advisory.LastUpdated ?? advisory.FirstPublished ?? now,
GridFsId: gridFsId,
PayloadId: gridFsId,
ExpiresAt: null);
var upserted = await _documentStore.UpsertAsync(record, cancellationToken).ConfigureAwait(false);
@@ -259,7 +259,7 @@ public sealed class CiscoConnector : IFeedConnector
continue;
}
if (!document.GridFsId.HasValue)
if (!document.PayloadId.HasValue)
{
_diagnostics.ParseFailure();
_logger.LogWarning("Cisco document {DocumentId} missing GridFS payload", documentId);
@@ -273,7 +273,7 @@ public sealed class CiscoConnector : IFeedConnector
byte[] payload;
try
{
payload = await _rawDocumentStorage.DownloadAsync(document.GridFsId.Value, cancellationToken).ConfigureAwait(false);
payload = await _rawDocumentStorage.DownloadAsync(document.PayloadId.Value, cancellationToken).ConfigureAwait(false);
}
catch (Exception ex)
{

View File

@@ -133,7 +133,7 @@ public sealed class MsrcConnector : IFeedConnector
}
_diagnostics.DetailFetchAttempt();
if (existing?.GridFsId is { } oldGridId)
if (existing?.PayloadId is { } oldGridId)
{
await _rawDocumentStorage.DeleteAsync(oldGridId, cancellationToken).ConfigureAwait(false);
}
@@ -238,7 +238,7 @@ public sealed class MsrcConnector : IFeedConnector
continue;
}
if (!document.GridFsId.HasValue)
if (!document.PayloadId.HasValue)
{
await _documentStore.UpdateStatusAsync(document.Id, DocumentStatuses.Failed, cancellationToken).ConfigureAwait(false);
remainingDocuments.Remove(documentId);
@@ -250,7 +250,7 @@ public sealed class MsrcConnector : IFeedConnector
byte[] payload;
try
{
payload = await _rawDocumentStorage.DownloadAsync(document.GridFsId.Value, cancellationToken).ConfigureAwait(false);
payload = await _rawDocumentStorage.DownloadAsync(document.PayloadId.Value, cancellationToken).ConfigureAwait(false);
}
catch (Exception ex)
{

View File

@@ -185,7 +185,7 @@ public sealed class OracleConnector : IFeedConnector
continue;
}
if (!document.GridFsId.HasValue)
if (!document.PayloadId.HasValue)
{
_logger.LogWarning("Oracle document {DocumentId} missing GridFS payload", document.Id);
await _documentStore.UpdateStatusAsync(document.Id, DocumentStatuses.Failed, cancellationToken).ConfigureAwait(false);
@@ -198,7 +198,7 @@ public sealed class OracleConnector : IFeedConnector
try
{
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);
dto = OracleParser.Parse(html, metadata);
}

View File

@@ -299,7 +299,7 @@ public sealed class VmwareConnector : IFeedConnector
continue;
}
if (!document.GridFsId.HasValue)
if (!document.PayloadId.HasValue)
{
_logger.LogWarning("VMware document {DocumentId} missing GridFS payload", document.Id);
await _documentStore.UpdateStatusAsync(document.Id, DocumentStatuses.Failed, cancellationToken).ConfigureAwait(false);
@@ -311,7 +311,7 @@ public sealed class VmwareConnector : IFeedConnector
byte[] bytes;
try
{
bytes = await _rawDocumentStorage.DownloadAsync(document.GridFsId.Value, cancellationToken).ConfigureAwait(false);
bytes = await _rawDocumentStorage.DownloadAsync(document.PayloadId.Value, cancellationToken).ConfigureAwait(false);
}
catch (Exception ex)
{

View File

@@ -10,6 +10,7 @@
<ItemGroup>
<ProjectReference Include="..\StellaOps.Concelier.Models\StellaOps.Concelier.Models.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.DependencyInjection/StellaOps.DependencyInjection.csproj" />
<ProjectReference Include="../../../__Libraries/StellaOps.Cryptography/StellaOps.Cryptography.csproj" />

View File

@@ -10,6 +10,7 @@
<ItemGroup>
<ProjectReference Include="..\StellaOps.Concelier.Exporter.Json\StellaOps.Concelier.Exporter.Json.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.Plugin/StellaOps.Plugin.csproj" />
</ItemGroup>

View File

@@ -24,6 +24,8 @@ namespace MongoDB.Bson
{
protected readonly object? _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
{
null => BsonType.Null,
@@ -59,12 +61,24 @@ namespace MongoDB.Bson
public class BsonInt64 : BsonValue { public BsonInt64(long 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 BsonNull : BsonValue
{
private BsonNull() : base(null) { }
public static BsonNull Value { get; } = new();
}
public class BsonArray : BsonValue, IEnumerable<BsonValue>
{
private readonly List<BsonValue> _items = new();
public BsonArray() : base(null) { }
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 IEnumerator<BsonValue> GetEnumerator() => _items.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
@@ -93,6 +107,8 @@ namespace MongoDB.Bson
_ => new BsonValue(value)
};
internal static BsonValue WrapExternal(object? value) => Wrap(value);
public BsonValue this[string 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 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();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
@@ -156,7 +173,7 @@ namespace MongoDB.Bson
{
BsonDocument doc => doc._values.ToDictionary(kvp => kvp.Key, kvp => Unwrap(kvp.Value)),
BsonArray array => array.Select(Unwrap).ToArray(),
_ => value._value
_ => value.RawValue
};
}
}

View File

@@ -1,4 +1,5 @@
using System.Collections.Concurrent;
using System.IO;
using StellaOps.Concelier.Models;
namespace StellaOps.Concelier.Storage.Mongo
@@ -33,8 +34,9 @@ namespace StellaOps.Concelier.Storage.Mongo
IReadOnlyDictionary<string, string>? Metadata = null,
string? Etag = null,
DateTimeOffset? LastModified = null,
MongoDB.Bson.ObjectId? GridFsId = null,
DateTimeOffset? ExpiresAt = null);
Guid? PayloadId = null,
DateTimeOffset? ExpiresAt = null,
byte[]? Payload = null);
public interface IDocumentStore
{
@@ -85,7 +87,7 @@ namespace StellaOps.Concelier.Storage.Mongo
Guid DocumentId,
string SourceName,
string Format,
MongoDB.Bson.BsonDocument Payload,
string Payload,
DateTimeOffset CreatedAt);
public interface IDtoStore
@@ -113,40 +115,40 @@ namespace StellaOps.Concelier.Storage.Mongo
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();
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);
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))
{
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 _);
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
{
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);
}
@@ -160,9 +162,9 @@ namespace StellaOps.Concelier.Storage.Mongo
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;
}
@@ -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
{
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)
{
_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)
{
_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,
cursor ?? digest,
digest,
lastDeltaDigest: null,
baseExportId: resetBaseline ? exportId : null,
baseDigest: resetBaseline ? digest : null,
targetRepository,
LastDeltaDigest: null,
BaseExportId: resetBaseline ? exportId : null,
BaseDigest: resetBaseline ? digest : null,
TargetRepository: targetRepository,
manifest,
exporterVersion,
_timeProvider.GetUtcNow());
@@ -307,11 +356,11 @@ namespace StellaOps.Concelier.Storage.Mongo.Exporting
var record = new ExportStateRecord(
id,
cursor ?? deltaDigest,
lastFullDigest: null,
lastDeltaDigest: deltaDigest,
baseExportId: null,
baseDigest: null,
targetRepository: null,
LastFullDigest: null,
LastDeltaDigest: deltaDigest,
BaseExportId: null,
BaseDigest: null,
TargetRepository: null,
manifest,
exporterVersion,
_timeProvider.GetUtcNow());

View File

@@ -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);
}
}

View File

@@ -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);

View File

@@ -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);

View File

@@ -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));
}
}

View File

@@ -4,6 +4,7 @@ using StellaOps.Concelier.Storage.Postgres.Repositories;
using StellaOps.Infrastructure.Postgres;
using StellaOps.Infrastructure.Postgres.Options;
using StellaOps.Concelier.Core.Linksets;
using StellaOps.Concelier.Storage.Mongo;
namespace StellaOps.Concelier.Storage.Postgres;
@@ -38,11 +39,13 @@ public static class ServiceCollectionExtensions
services.AddScoped<IAdvisoryWeaknessRepository, AdvisoryWeaknessRepository>();
services.AddScoped<IKevFlagRepository, KevFlagRepository>();
services.AddScoped<ISourceStateRepository, SourceStateRepository>();
services.AddScoped<IDocumentRepository, DocumentRepository>();
services.AddScoped<IFeedSnapshotRepository, FeedSnapshotRepository>();
services.AddScoped<IAdvisorySnapshotRepository, AdvisorySnapshotRepository>();
services.AddScoped<IMergeEventRepository, MergeEventRepository>();
services.AddScoped<IAdvisoryLinksetStore, AdvisoryLinksetCacheRepository>();
services.AddScoped<IAdvisoryLinksetLookup>(sp => sp.GetRequiredService<IAdvisoryLinksetStore>());
services.AddScoped<IDocumentStore, PostgresDocumentStore>();
return services;
}
@@ -71,11 +74,13 @@ public static class ServiceCollectionExtensions
services.AddScoped<IAdvisoryWeaknessRepository, AdvisoryWeaknessRepository>();
services.AddScoped<IKevFlagRepository, KevFlagRepository>();
services.AddScoped<ISourceStateRepository, SourceStateRepository>();
services.AddScoped<IDocumentRepository, DocumentRepository>();
services.AddScoped<IFeedSnapshotRepository, FeedSnapshotRepository>();
services.AddScoped<IAdvisorySnapshotRepository, AdvisorySnapshotRepository>();
services.AddScoped<IMergeEventRepository, MergeEventRepository>();
services.AddScoped<IAdvisoryLinksetStore, AdvisoryLinksetCacheRepository>();
services.AddScoped<IAdvisoryLinksetLookup>(sp => sp.GetRequiredService<IAdvisoryLinksetStore>());
services.AddScoped<IDocumentStore, PostgresDocumentStore>();
return services;
}

View File

@@ -10,6 +10,11 @@
<RootNamespace>StellaOps.Concelier.Storage.Postgres</RootNamespace>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Dapper" Version="2.1.35" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.0" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Migrations\**\*.sql" LogicalName="%(RecursiveDir)%(Filename)%(Extension)" />
</ItemGroup>
@@ -25,6 +30,7 @@
<ItemGroup>
<ProjectReference Include="..\..\..\__Libraries\StellaOps.Infrastructure.Postgres\StellaOps.Infrastructure.Postgres.csproj" />
<ProjectReference Include="..\StellaOps.Concelier.Core\StellaOps.Concelier.Core.csproj" />
<ProjectReference Include="..\..\..\__Libraries\StellaOps.DependencyInjection\StellaOps.DependencyInjection.csproj" />
</ItemGroup>
</Project>

View File

@@ -28,7 +28,7 @@ public sealed class CccsMapperTests
Metadata: null,
Etag: null,
LastModified: dto.Modified,
GridFsId: null);
PayloadId: null);
var recordedAt = DateTimeOffset.Parse("2025-08-12T00:00:00Z");
var advisory = CccsMapper.Map(dto, document, recordedAt);

View File

@@ -82,7 +82,7 @@ public sealed class CertCcMapperTests
Metadata: null,
Etag: null,
LastModified: PublishedAt,
GridFsId: null);
PayloadId: null);
var dtoRecord = new DtoRecord(
Id: Guid.NewGuid(),

View File

@@ -93,7 +93,7 @@ public sealed class SourceStateSeedProcessorTests : IAsyncLifetime
Assert.Equal(documentId, storedDocument!.Id);
Assert.Equal("application/json", storedDocument.ContentType);
Assert.Equal(DocumentStatuses.PendingParse, storedDocument.Status);
Assert.NotNull(storedDocument.GridFsId);
Assert.NotNull(storedDocument.PayloadId);
Assert.NotNull(storedDocument.Headers);
Assert.Equal("true", storedDocument.Headers!["X-Test"]);
Assert.NotNull(storedDocument.Metadata);
@@ -153,7 +153,7 @@ public sealed class SourceStateSeedProcessorTests : IAsyncLifetime
CancellationToken.None);
Assert.NotNull(existingRecord);
var previousGridId = existingRecord!.GridFsId;
var previousGridId = existingRecord!.PayloadId;
Assert.NotNull(previousGridId);
var filesCollection = _database.GetCollection<BsonDocument>("documents.files");
@@ -189,8 +189,8 @@ public sealed class SourceStateSeedProcessorTests : IAsyncLifetime
Assert.NotNull(refreshedRecord);
Assert.Equal(documentId, refreshedRecord!.Id);
Assert.NotNull(refreshedRecord.GridFsId);
Assert.NotEqual(previousGridId, refreshedRecord.GridFsId);
Assert.NotNull(refreshedRecord.PayloadId);
Assert.NotEqual(previousGridId, refreshedRecord.PayloadId);
var files = await filesCollection.Find(FilterDefinition<BsonDocument>.Empty).ToListAsync();
Assert.Single(files);

View File

@@ -57,7 +57,7 @@ public sealed class DebianMapperTests
Metadata: null,
Etag: null,
LastModified: null,
GridFsId: null);
PayloadId: null);
Advisory advisory = DebianMapper.Map(dto, document, new DateTimeOffset(2024, 9, 1, 2, 0, 0, TimeSpan.Zero));

View File

@@ -498,7 +498,7 @@ public sealed class RedHatConnectorTests : IAsyncLifetime
Metadata: metadata,
Etag: null,
LastModified: fixture.ValidatedAt,
GridFsId: null);
PayloadId: null);
var dto = new DtoRecord(Guid.NewGuid(), document.Id, RedHatConnectorPlugin.SourceName, "redhat.csaf.v2", bson, fixture.ValidatedAt);

View File

@@ -34,7 +34,7 @@ public sealed class SuseMapperTests
},
Etag: "adv-1",
LastModified: DateTimeOffset.UtcNow,
GridFsId: ObjectId.Empty);
PayloadId: ObjectId.Empty);
var mapped = SuseMapper.Map(dto, document, DateTimeOffset.UtcNow);

View File

@@ -22,7 +22,7 @@ public sealed class GhsaConflictFixtureTests
Metadata: null,
Etag: "\"etag-ghsa-conflict\"",
LastModified: new DateTimeOffset(2025, 3, 3, 18, 0, 0, TimeSpan.Zero),
GridFsId: null);
PayloadId: null);
var dto = new GhsaRecordDto
{

View File

@@ -21,7 +21,7 @@ public sealed class GhsaMapperTests
Metadata: null,
Etag: "\"etag-ghsa-fallback\"",
LastModified: recordedAt.AddHours(-3),
GridFsId: null);
PayloadId: null);
var dto = new GhsaRecordDto
{

View File

@@ -82,7 +82,7 @@ public sealed class NvdConflictFixtureTests
Metadata: null,
Etag: "\"etag-nvd-conflict\"",
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 advisory = Assert.Single(advisories);

View File

@@ -91,7 +91,7 @@ public sealed class OsvConflictFixtureTests
Metadata: null,
Etag: "\"etag-osv-conflict\"",
LastModified: new DateTimeOffset(2025, 3, 6, 12, 0, 0, TimeSpan.Zero),
GridFsId: null);
PayloadId: null);
var dtoRecord = new DtoRecord(
Id: Guid.Parse("6f7d5ce7-cb47-40a5-8b41-8ad022b5fd5c"),

View File

@@ -75,11 +75,11 @@ public sealed class StellaOpsMirrorConnectorTests : IAsyncLifetime
Assert.Equal(NormalizeDigest(bundleDigest), bundleDocument.Sha256);
var rawStorage = provider.GetRequiredService<RawDocumentStorage>();
Assert.NotNull(manifestDocument.GridFsId);
Assert.NotNull(bundleDocument.GridFsId);
Assert.NotNull(manifestDocument.PayloadId);
Assert.NotNull(bundleDocument.PayloadId);
var manifestBytes = await rawStorage.DownloadAsync(manifestDocument.GridFsId!.Value, CancellationToken.None);
var bundleBytes = await rawStorage.DownloadAsync(bundleDocument.GridFsId!.Value, CancellationToken.None);
var manifestBytes = await rawStorage.DownloadAsync(manifestDocument.PayloadId!.Value, CancellationToken.None);
var bundleBytes = await rawStorage.DownloadAsync(bundleDocument.PayloadId!.Value, CancellationToken.None);
Assert.Equal(manifestContent, Encoding.UTF8.GetString(manifestBytes));
Assert.Equal(bundleContent, Encoding.UTF8.GetString(bundleBytes));

View File

@@ -52,7 +52,7 @@ public sealed class CiscoMapperTests
Metadata: null,
Etag: null,
LastModified: updated,
GridFsId: null);
PayloadId: null);
var dtoRecord = new DtoRecord(Guid.NewGuid(), document.Id, VndrCiscoConnectorPlugin.SourceName, "cisco.dto.test", new BsonDocument(), updated);

View File

@@ -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);

View File

@@ -1,5 +1,7 @@
using StellaOps.Policy.Gateway.Contracts;
using StellaOps.Policy.Gateway.Infrastructure;
using StellaOps.Policy.Scoring;
using StellaOps.Policy.Scoring.Receipts;
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<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);
}

View File

@@ -12,6 +12,8 @@ using StellaOps.Policy.Gateway.Contracts;
using StellaOps.Policy.Gateway.Infrastructure;
using StellaOps.Policy.Gateway.Options;
using StellaOps.Policy.Gateway.Services;
using StellaOps.Policy.Scoring;
using StellaOps.Policy.Scoring.Receipts;
namespace StellaOps.Policy.Gateway.Clients;
@@ -98,6 +100,61 @@ internal sealed class PolicyEngineClient : IPolicyEngineClient
request,
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>(
HttpMethod method,
string relativeUri,

View File

@@ -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);

View File

@@ -336,6 +336,137 @@ policyPacks.MapPost("/{packId}/revisions/{version:int}:activate", async Task<IRe
StellaOpsScopes.PolicyOperate,
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();
static IAsyncPolicy<HttpResponseMessage> CreateAuthorityRetryPolicy(IServiceProvider provider)

View File

@@ -16,6 +16,7 @@
<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="../../AirGap/StellaOps.AirGap.Policy/StellaOps.AirGap.Policy/StellaOps.AirGap.Policy.csproj" />
<ProjectReference Include="../StellaOps.Policy.Scoring/StellaOps.Policy.Scoring.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Http.Polly" Version="10.0.0" />

View File

@@ -12,7 +12,7 @@ import jsPDF from './jspdf.stub';
imports: [CommonModule],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<section class="expl" aria-busy="{{ loading }}">
<section class="expl" [attr.aria-busy]="loading">
<header class="expl__header" *ngIf="result">
<div>
<p class="expl__eyebrow">Policy Studio · Explain</p>

View File

@@ -9,7 +9,7 @@ import { ActivatedRoute } from '@angular/router';
imports: [CommonModule, ReactiveFormsModule],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<section class="rb" aria-busy="false">
<section class="rb" [attr.aria-busy]="false">
<header class="rb__header">
<div>
<p class="rb__eyebrow">Policy Studio · Rule Builder</p>

View File

@@ -7,7 +7,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="MongoDB.Driver" Version="3.5.0" />
<ProjectReference Include="..\..\Concelier\__Libraries\StellaOps.Concelier.Models\StellaOps.Concelier.Models.csproj" />
</ItemGroup>
</Project>