diff --git a/SPRINTS.md b/SPRINTS.md
index c14eff2a..b4cfc435 100644
--- a/SPRINTS.md
+++ b/SPRINTS.md
@@ -112,9 +112,9 @@
 | Sprint 5 | Vexer Core Foundations | src/StellaOps.Vexer.Core/TASKS.md | DONE (2025-10-15) | Team Vexer Core & Policy | VEXER-CORE-01-003 | Publish shared connector/exporter/attestation abstractions and deterministic query signature utilities for cache/attestation workflows. |
 | Sprint 5 | Vexer Core Foundations | src/StellaOps.Vexer.Policy/TASKS.md | DONE (2025-10-15) | Team Vexer Policy | VEXER-POLICY-01-001 | Established policy options & snapshot provider covering baseline weights/overrides. |
 | Sprint 5 | Vexer Core Foundations | src/StellaOps.Vexer.Policy/TASKS.md | DONE (2025-10-15) | Team Vexer Policy | VEXER-POLICY-01-002 | Policy evaluator now feeds consensus resolver with immutable snapshots. |
-| Sprint 5 | Vexer Core Foundations | src/StellaOps.Vexer.Policy/TASKS.md | TODO | Team Vexer Policy | VEXER-POLICY-01-003 | Author policy diagnostics, CLI/WebService surfacing, and documentation updates. |
-| Sprint 5 | Vexer Core Foundations | src/StellaOps.Vexer.Policy/TASKS.md | TODO | Team Vexer Policy | VEXER-POLICY-01-004 | Implement YAML/JSON schema validation and deterministic diagnostics for operator bundles. |
-| Sprint 5 | Vexer Core Foundations | src/StellaOps.Vexer.Policy/TASKS.md | TODO | Team Vexer Policy | VEXER-POLICY-01-005 | Add policy change tracking, snapshot digests, and telemetry/logging hooks. |
+| Sprint 5 | Vexer Core Foundations | src/StellaOps.Vexer.Policy/TASKS.md | DONE (2025-10-16) | Team Vexer Policy | VEXER-POLICY-01-003 | Author policy diagnostics, CLI/WebService surfacing, and documentation updates. |
+| Sprint 5 | Vexer Core Foundations | src/StellaOps.Vexer.Policy/TASKS.md | DONE (2025-10-16) | Team Vexer Policy | VEXER-POLICY-01-004 | Implement YAML/JSON schema validation and deterministic diagnostics for operator bundles. |
+| Sprint 5 | Vexer Core Foundations | src/StellaOps.Vexer.Policy/TASKS.md | DONE (2025-10-16) | Team Vexer Policy | VEXER-POLICY-01-005 | Add policy change tracking, snapshot digests, and telemetry/logging hooks. |
 | Sprint 5 | Vexer Core Foundations | src/StellaOps.Vexer.Storage.Mongo/TASKS.md | DONE (2025-10-15) | Team Vexer Storage | VEXER-STORAGE-01-001 | Mongo mapping registry plus raw/export entities and DI extensions in place. |
 | Sprint 5 | Vexer Core Foundations | src/StellaOps.Vexer.Storage.Mongo/TASKS.md | TODO | Team Vexer Storage | VEXER-STORAGE-01-004 | Build provider/consensus/cache class maps and related collections. |
 | Sprint 5 | Vexer Core Foundations | src/StellaOps.Vexer.Export/TASKS.md | DONE (2025-10-15) | Team Vexer Export | VEXER-EXPORT-01-001 | Export engine delivers cache lookup, manifest creation, and policy integration. |
@@ -134,3 +134,17 @@
 | Sprint 6 | Vexer Ingest & Formats | src/StellaOps.Vexer.Connectors.Ubuntu.CSAF/TASKS.md | TODO | Team Vexer Connectors – Ubuntu | VEXER-CONN-UBUNTU-01-001 | Implement Ubuntu CSAF discovery and channel selection for USN ingestion. |
 | Sprint 6 | Vexer Ingest & Formats | src/StellaOps.Vexer.Connectors.OCI.OpenVEX.Attest/TASKS.md | TODO | Team Vexer Connectors – OCI | VEXER-CONN-OCI-01-001 | Wire OCI discovery/auth to fetch OpenVEX attestations for configured images. |
 | Sprint 6 | Vexer Ingest & Formats | src/StellaOps.Cli/TASKS.md | TODO | DevEx/CLI | VEXER-CLI-01-001 | Add `vexer` CLI verbs bridging to WebService with consistent auth and offline UX. |
+| Sprint 7 | Contextual Truth Foundations | src/StellaOps.Vexer.Core/TASKS.md | TODO | Team Vexer Core & Policy | VEXER-CORE-02-001 | Context signal schema prep – extend consensus models with severity/KEV/EPSS fields and update canonical serializers. |
+| Sprint 7 | Contextual Truth Foundations | src/StellaOps.Vexer.Policy/TASKS.md | TODO | Team Vexer Policy | VEXER-POLICY-02-001 | Scoring coefficients & weight ceilings – add α/β options, weight boosts, and validation guidance. |
+| Sprint 7 | Contextual Truth Foundations | src/StellaOps.Vexer.Storage.Mongo/TASKS.md | TODO | Team Vexer Storage | VEXER-STORAGE-02-001 | Statement events & scoring signals – create immutable VEX statement store plus consensus extensions with indexes/migrations. |
+| Sprint 7 | Contextual Truth Foundations | src/StellaOps.Vexer.WebService/TASKS.md | TODO | Team Vexer WebService | VEXER-WEB-01-004 | Resolve API & signed responses – expose `/vexer/resolve`, return signed consensus/score envelopes, document auth. |
+| Sprint 7 | Contextual Truth Foundations | src/StellaOps.Vexer.Attestation/TASKS.md | TODO | Team Vexer Attestation | VEXER-ATTEST-01-002 | Rekor v2 client integration – ship transparency log client with retries and offline queue. |
+| Sprint 7 | Contextual Truth Foundations | src/StellaOps.Vexer.Worker/TASKS.md | TODO | Team Vexer Worker | VEXER-WORKER-01-004 | TTL refresh & stability damper – schedule re-resolve loops and guard against status flapping. |
+| Sprint 7 | Contextual Truth Foundations | src/StellaOps.Vexer.Export/TASKS.md | TODO | Team Vexer Export | VEXER-EXPORT-01-005 | Score & resolve envelope surfaces – include signed consensus/score artifacts in exports. |
+| Sprint 7 | Contextual Truth Foundations | src/StellaOps.Feedser.Core/TASKS.md | TODO | Team Core Engine & Storage Analytics | FEEDCORE-ENGINE-07-001 | Advisory event log & asOf queries – surface immutable statements and replay capability. |
+| Sprint 7 | Contextual Truth Foundations | src/StellaOps.Feedser.Core/TASKS.md | TODO | Team Core Engine & Data Science | FEEDCORE-ENGINE-07-002 | Noise prior computation service – learn false-positive priors and expose deterministic summaries. |
+| Sprint 7 | Contextual Truth Foundations | src/StellaOps.Feedser.Storage.Mongo/TASKS.md | TODO | Team Normalization & Storage Backbone | FEEDSTORAGE-DATA-07-001 | Advisory statement & conflict collections – provision Mongo schema/indexes for event-sourced merge. |
+| Sprint 7 | Contextual Truth Foundations | src/StellaOps.Feedser.Merge/TASKS.md | TODO | BE-Merge | FEEDMERGE-ENGINE-07-001 | Conflict sets & explainers – persist conflict materialization and replay hashes for merge decisions. |
+| Sprint 8 | Mongo strengthening | src/StellaOps.Feedser.Storage.Mongo/TASKS.md | TODO | Team Normalization & Storage Backbone | FEEDSTORAGE-MONGO-08-001 | Causal-consistent Feedser storage sessions
Ensure `AddMongoStorage` registers a scoped session facilitator (causal consistency + majority concerns), update repositories to accept optional session handles, and add integration coverage proving read-your-write and monotonic reads across a replica set/election scenario. |
+| Sprint 8 | Mongo strengthening | src/StellaOps.Authority/TASKS.md | TODO | Authority Core & Storage Guild | AUTHSTORAGE-MONGO-08-001 | Harden Authority Mongo usage
Introduce scoped MongoDB sessions with `writeConcern`/`readConcern` majority defaults, flow the session through stores used in mutations + follow-up reads, and document middleware pattern for web/API & GraphQL layers. |
+| Sprint 8 | Mongo strengthening | src/StellaOps.Vexer.Storage.Mongo/TASKS.md | TODO | Team Vexer Storage | VEXER-STORAGE-MONGO-08-001 | Causal consistency for Vexer repositories
Register Mongo options with majority defaults, push session-aware overloads through raw/export/consensus/cache stores, and extend migration/tests to validate causal reads after writes (including GridFS-backed content) under replica-set failover. |
diff --git a/docs/ARCHITECTURE_VEXER.md b/docs/ARCHITECTURE_VEXER.md
index aca2512e..c591388d 100644
--- a/docs/ARCHITECTURE_VEXER.md
+++ b/docs/ARCHITECTURE_VEXER.md
@@ -26,6 +26,7 @@ MongoDB acts as the canonical store; collections (with logical responsibilities)
 - `vex.consensus` – consensus projections per `(vulnId, productKey)` capturing rollup status, source weights, conflicts, and policy revision.
 - `vex.exports` – export manifests containing artifact digests, cache metadata, and attestation pointers.
 - `vex.cache` – index from `querySignature`/`format` to export digest for fast reuse.
+- `vex.migrations` – tracks applied storage migrations (index bootstrap, future schema updates).
 
 GridFS is used for large raw payloads when necessary, and artifact stores (S3/MinIO/file) hold serialized exports referenced by `vex.exports`.
 
@@ -54,6 +55,7 @@ Policy snapshots are immutable and versioned so consensus records capture the po
 - JSON serialization uses `VexCanonicalJsonSerializer`, enforcing property ordering and camelCase naming for reproducible snapshots and test fixtures.
 - `VexQuerySignature` produces canonical filter/order strings and SHA-256 digests, enabling cache keys shared across services.
 - Export manifests reuse cached artifacts when the same signature/format is requested unless `ForceRefresh` is explicitly set.
+- For scorring multiple sources on same VEX topic use - `VEXER_SCORRING.md`
 
 ## 6. Observability & offline posture
 
@@ -68,5 +70,16 @@ Policy snapshots are immutable and versioned so consensus records capture the po
 - Build WebService endpoints (`/vexer/status`, `/vexer/claims`, `/vexer/exports`) plus CLI verbs mirroring Feedser patterns.
 - Provide CSAF, CycloneDX VEX, and OpenVEX normalizers along with vendor-specific connectors (Red Hat, Cisco, SUSE, MSRC, Oracle, Ubuntu, OCI attestation).
 - Extend policy diagnostics with schema validation, change tracking, and operator-facing diff reports.
+- Mongo bootstrapper runs ordered migrations (`vex.migrations`) to ensure indexes for raw documents, providers, consensus snapshots, exports, and cache entries.
+
+## Appendix A – Policy diagnostics workflow
+
+- `StellaOps.Vexer.Policy` now exposes `IVexPolicyDiagnostics`, producing deterministic diagnostics reports with timestamp, severity counts, active provider overrides, and the full issue list surfaced by `IVexPolicyProvider`.
+- CLI/WebService layers should call `IVexPolicyDiagnostics.GetDiagnostics()` to display operator-friendly summaries (`vexer policy diagnostics` and `/vexer/policy/diagnostics` are the planned entry points).
+- Recommendations in the report guide operators to resolve blocking errors, review warnings, and audit override usage before consensus runs—embed them directly in UX copy instead of re-deriving logic.
+- Export/consensus telemetry should log the diagnostic `Version` alongside `policyRevisionId` so dashboards can correlate policy changes with consensus decisions.
+- Offline installations can persist the diagnostics report (JSON) in the Offline Kit to document policy headroom during audits; the output is deterministic and diff-friendly.
+- Use `VexPolicyBinder` when ingesting operator-supplied YAML/JSON bundles; it normalizes weight/override values, reports deterministic issues, and returns the consensus-ready `VexConsensusPolicyOptions` used by `VexPolicyProvider`.
+- Reload telemetry emits `vex.policy.reloads` (tags: `revision`, `version`, `issues`) whenever a new digest is observed—feed this into dashboards to correlate policy changes with consensus outcomes.
 
 This architecture keeps Vexer aligned with StellaOps' deterministic, offline-operable design while layering VEX-specific consensus and attestation capabilities on top of the Feedser foundations.
diff --git a/docs/VEXER_SCORRING.md b/docs/VEXER_SCORRING.md
new file mode 100644
index 00000000..bf79b85a
--- /dev/null
+++ b/docs/VEXER_SCORRING.md
@@ -0,0 +1,83 @@
+## Status
+
+This document tracks the future-looking risk scoring model for Vexer. The calculation below is not active yet; Sprint 7 work will add the required schema fields, policy controls, and services. Until that ships, Vexer emits consensus statuses without numeric scores.
+
+## Scoring model (target state)
+
+**S = Gate(VEX_status) × W_trust(source) × [Severity_base × (1 + α·KEV + β·EPSS)]**
+
+* **Gate(VEX_status)**: `affected`/`under_investigation` → 1, `not_affected`/`fixed` → 0. A trusted “not affected” or “fixed” still zeroes the score.
+* **W_trust(source)**: normalized policy weight (baseline 0‒1). Policies may opt into >1 boosts for signed vendor feeds once Phase 1 closes.
+* **Severity_base**: canonical numeric severity from Feedser (CVSS or org-defined scale).
+* **KEV flag**: 0/1 boost when CISA Known Exploited Vulnerabilities applies.
+* **EPSS**: probability [0,1]; bounded multiplier.
+* **α, β**: configurable coefficients (default α=0.25, β=0.5) stored in policy.
+
+Safeguards: freeze boosts when product identity is unknown, clamp outputs ≥0, and log every factor in the audit trail.
+
+## Implementation roadmap
+
+| Phase | Scope | Artifacts |
+| --- | --- | --- |
+| **Phase 1 – Schema foundations** | Extend Vexer consensus/claims and Feedser canonical advisories with severity, KEV, EPSS, and expose α/β + weight ceilings in policy. | Sprint 7 tasks `VEXER-CORE-02-001`, `VEXER-POLICY-02-001`, `VEXER-STORAGE-02-001`, `FEEDCORE-ENGINE-07-001`. |
+| **Phase 2 – Deterministic score engine** | Implement a scoring component that executes alongside consensus and persists score envelopes with hashes. | Planned task `VEXER-CORE-02-002` (backlog). |
+| **Phase 3 – Surfacing & enforcement** | Expose scores via WebService/CLI, integrate with Feedser noise priors, and enforce policy-based suppressions. | To be scheduled after Phase 2. |
+
+## Data model (after Phase 1)
+
+```json
+{
+  "vulnerabilityId": "CVE-2025-12345",
+  "product": "pkg:name@version",
+  "consensus": {
+    "status": "affected",
+    "policyRevisionId": "rev-12",
+    "policyDigest": "0D9AEC…"
+  },
+  "signals": {
+    "severity": {"scheme": "CVSS:3.1", "score": 7.5},
+    "kev": true,
+    "epss": 0.40
+  },
+  "policy": {
+    "weight": 1.15,
+    "alpha": 0.25,
+    "beta": 0.5
+  },
+  "score": {
+    "value": 10.8,
+    "generatedAt": "2025-11-05T14:12:30Z",
+    "audit": [
+      "gate:affected",
+      "weight:1.15",
+      "severity:7.5",
+      "kev:1",
+      "epss:0.40"
+    ]
+  }
+}
+```
+
+## Operational guidance
+
+* **Inputs**: Feedser delivers severity/KEV/EPSS via the advisory event log; Vexer connectors load VEX statements. Policy owns trust tiers and coefficients.
+* **Processing**: the scoring engine (Phase 2) runs next to consensus, storing results with deterministic hashes so exports and attestations can reference them.
+* **Consumption**: WebService/CLI will return consensus plus score; scanners may suppress findings only when policy-authorized VEX gating and signed score envelopes agree.
+
+## Pseudocode (Phase 2 preview)
+
+```python
+def risk_score(gate, weight, severity, kev, epss, alpha, beta, freeze_boosts=False):
+    if gate == 0:
+        return 0
+    if freeze_boosts:
+        kev, epss = 0, 0
+    boost = 1 + alpha * kev + beta * epss
+    return max(0, weight * severity * boost)
+```
+
+## FAQ
+
+* **Can operators opt out?** Set α=β=0 or keep weights ≤1.0 via policy.
+* **What about missing signals?** Treat them as zero and log the omission.
+* **When will this ship?** Phase 1 is planned for Sprint 7; later phases depend on connector coverage and attestation delivery.
diff --git a/docs/ops/feedser-ghsa-operations.md b/docs/ops/feedser-ghsa-operations.md
index 04441ec8..fb29fe6c 100644
--- a/docs/ops/feedser-ghsa-operations.md
+++ b/docs/ops/feedser-ghsa-operations.md
@@ -1,6 +1,6 @@
 # Feedser GHSA Connector – Operations Runbook
 
-_Last updated: 2025-10-12_
+_Last updated: 2025-10-16_
 
 ## 1. Overview
 The GitHub Security Advisories (GHSA) connector pulls advisory metadata from the GitHub REST API `/security/advisories` endpoint. GitHub enforces both primary and secondary rate limits, so operators must monitor usage and configure retries to avoid throttling incidents.
@@ -114,3 +114,10 @@ When enabling GHSA the first time, run a staged backfill:
 - Prometheus: `ghsa_ratelimit_remaining_bucket` (from histogram) – use `histogram_quantile(0.99, ...)` to trend capacity.
 - VictoriaMetrics: `LAST_over_time(ghsa_ratelimit_remaining_sum[5m])` for simple last-value graphs.
 - Grafana: stack remaining + used to visualise total limit per resource.
+
+## 8. Canonical metric fallback analytics
+When GitHub omits CVSS vectors/scores, the connector now assigns a deterministic canonical metric id in the form `ghsa:severity/` and publishes it to Merge so severity precedence still resolves against GHSA even without CVSS data.
+
+- Metric: `ghsa.map.canonical_metric_fallbacks` (counter) with tags `severity`, `canonical_metric_id`, `reason=no_cvss`.
+- Monitor the counter alongside Merge parity checks; a sudden spike suggests GitHub is shipping advisories without vectors and warrants cross-checking downstream exporters.
+- Because the canonical id feeds Merge, parity dashboards should overlay this metric to confirm fallback advisories continue to merge ahead of downstream sources when GHSA supplies more recent data.
diff --git a/docs/ops/feedser-osv-operations.md b/docs/ops/feedser-osv-operations.md
new file mode 100644
index 00000000..a00dcac5
--- /dev/null
+++ b/docs/ops/feedser-osv-operations.md
@@ -0,0 +1,24 @@
+# Feedser OSV Connector – Operations Notes
+
+_Last updated: 2025-10-16_
+
+The OSV connector ingests advisories from OSV.dev across OSS ecosystems. This note highlights the additional merge/export expectations introduced with the canonical metric fallback work in Sprint 4.
+
+## 1. Canonical metric fallbacks
+- When OSV omits CVSS vectors (common for CVSS v4-only payloads) the mapper now emits a deterministic canonical metric id in the form `osv:severity/` and normalises the advisory severity to the same ``.
+- Metric: `osv.map.canonical_metric_fallbacks` (counter) with tags `severity`, `canonical_metric_id`, `ecosystem`, `reason=no_cvss`. Watch this alongside merge parity dashboards to catch spikes where OSV publishes severity-only advisories.
+- Merge precedence still prefers GHSA over OSV; the shared severity-based canonical id keeps Merge/export parity deterministic even when only OSV supplies severity data.
+
+## 2. CWE provenance
+- `database_specific.cwe_ids` now populates provenance decision reasons for every mapped weakness. Expect `decisionReason="database_specific.cwe_ids"` on OSV weakness provenance and confirm exporters preserve the value.
+- If OSV ever attaches `database_specific.cwe_notes`, the connector will surface the joined note string in `decisionReason` instead of the default marker.
+
+## 3. Dashboards & alerts
+- Extend existing merge dashboards with the new counter:
+  - Overlay `sum(osv.map.canonical_metric_fallbacks{ecosystem=~".+"})` with Merge severity overrides to confirm fallback advisories are reconciling cleanly.
+  - Alert when the 1-hour sum exceeds 50 for any ecosystem; baseline volume is currently <5 per day (mostly GHSA mirrors emitting CVSS v4 only).
+- Exporters already surface `canonicalMetricId`; no schema change is required, but ORAS/Trivy bundles should be spot-checked after deploying the connector update.
+
+## 4. Runbook updates
+- Fixture parity suites (`osv-ghsa.*`) now assert the fallback id and provenance notes. Regenerate via `dotnet test src/StellaOps.Feedser.Source.Osv.Tests/StellaOps.Feedser.Source.Osv.Tests.csproj`.
+- When investigating merge severity conflicts, include the fallback counter and confirm OSV advisories carry the expected `osv:severity/` id before raising connector bugs.
diff --git a/src/StellaOps.Authority/TASKS.md b/src/StellaOps.Authority/TASKS.md
index dc3bf375..c7d7db2a 100644
--- a/src/StellaOps.Authority/TASKS.md
+++ b/src/StellaOps.Authority/TASKS.md
@@ -19,5 +19,6 @@
 | AUTHCORE-BUILD-OPENIDDICT | DONE (2025-10-14) | Authority Core | SEC2.HOST | Adapt host/audit handlers for OpenIddict 6.4 API surface (no `OpenIddictServerTransaction`) and restore Authority solution build. | ✅ Build `dotnet build src/StellaOps.Authority.sln` succeeds; ✅ Audit correlation + tamper logging verified under new abstractions; ✅ Tests updated. |
 | AUTHCORE-STORAGE-DEVICE-TOKENS | DONE (2025-10-14) | Authority Core, Storage Guild | AUTHCORE-BUILD-OPENIDDICT | Reintroduce `AuthorityTokenDeviceDocument` + projections removed during refactor so storage layer compiles. | ✅ Document type restored with mappings/migrations; ✅ Storage tests cover device artifacts; ✅ Authority solution build green. |
 | AUTHCORE-BOOTSTRAP-INVITES | DONE (2025-10-14) | Authority Core, DevOps | AUTHCORE-STORAGE-DEVICE-TOKENS | Wire bootstrap invite cleanup service against restored document schema and re-enable lifecycle tests. | ✅ `BootstrapInviteCleanupService` passes integration tests; ✅ Operator guide updated if behavior changes; ✅ Build/test matrices green. |
+| AUTHSTORAGE-MONGO-08-001 | TODO | Authority Core & Storage Guild | — | Harden Mongo session usage with causal consistency for mutations and follow-up reads. | • Scoped middleware/service creates `IClientSessionHandle` with causal consistency + majority read/write concerns
• Stores accept optional session parameter and reuse it for write + immediate reads
• GraphQL/HTTP pipelines updated to flow session through post-mutation queries
• Replica-set integration test exercises primary election and verifies read-your-write guarantees |
 
 > Update status columns (TODO / DOING / DONE / BLOCKED) together with code changes. Always run `dotnet test src/StellaOps.Authority.sln` when touching host logic.
diff --git a/src/StellaOps.Feedser.Core/TASKS.md b/src/StellaOps.Feedser.Core/TASKS.md
index fa12c0e0..76724b04 100644
--- a/src/StellaOps.Feedser.Core/TASKS.md
+++ b/src/StellaOps.Feedser.Core/TASKS.md
@@ -16,3 +16,5 @@
 |FEEDCORE-ENGINE-03-002 Field precedence and tie-breaker map|BE-Core|Merge|DONE – field precedence and freshness overrides enforced via `FieldPrecedence` map with tie-breakers and analytics capture. **Reminder:** Storage/Merge owners review precedence overrides when onboarding new feeds to ensure `decisionReason` tagging stays consistent.|
 |Canonical merger parity for description/CWE/canonical metric|BE-Core|Models|DONE (2025-10-15) – merger now populates description/CWEs/canonical metric id with provenance and regression tests cover the new decisions.|
 |Reference normalization & freshness instrumentation cleanup|BE-Core, QA|Models|DONE (2025-10-15) – reference keys normalized, freshness overrides applied to union fields, and new tests assert decision logging.|
+|FEEDCORE-ENGINE-07-001 – Advisory event log & asOf queries|Team Core Engine & Storage Analytics|FEEDSTORAGE-DATA-07-001|TODO – Introduce immutable advisory statement events, expose `asOf` query surface for merge/export pipelines, and document determinism guarantees for replay.|
+|FEEDCORE-ENGINE-07-002 – Noise prior computation service|Team Core Engine & Data Science|FEEDCORE-ENGINE-07-001|TODO – Build rule-based learner capturing false-positive priors per package/env, persist summaries, and expose APIs for Vexer/scan suppressors with reproducible statistics.|
diff --git a/src/StellaOps.Feedser.Merge/TASKS.md b/src/StellaOps.Feedser.Merge/TASKS.md
index b96c2fb3..bf3b3190 100644
--- a/src/StellaOps.Feedser.Merge/TASKS.md
+++ b/src/StellaOps.Feedser.Merge/TASKS.md
@@ -18,3 +18,4 @@
 |Range primitives backlog|BE-Merge|Connector WGs|**DOING** – Coordinate remaining connectors (`Acsc`, `Cccs`, `CertBund`, `CertCc`, `Cve`, `Ghsa`, `Ics.Cisa`, `Kisa`, `Ru.Bdu`, `Ru.Nkcki`, `Vndr.Apple`, `Vndr.Cisco`, `Vndr.Msrc`) to emit canonical RangePrimitives with provenance tags; track progress/fixtures here.
2025-10-11: Storage alignment notes + sample normalized rule JSON now captured in `RANGE_PRIMITIVES_COORDINATION.md` (see “Storage alignment quick reference”).
2025-10-11 18:45Z: GHSA normalized rules landed; OSV connector picked up next for rollout.
2025-10-11 21:10Z: `docs/dev/merge_semver_playbook.md` Section 8 now documents the persisted Mongo projection (SemVer + NEVRA) for connector reviewers.
2025-10-11 21:30Z: Added `docs/dev/normalized_versions_rollout.md` dashboard to centralize connector status and upcoming milestones.
2025-10-11 21:55Z: Merge now emits `feedser.merge.normalized_rules*` counters and unions connector-provided normalized arrays; see new test coverage in `AdvisoryPrecedenceMergerTests.Merge_RecordsNormalizedRuleMetrics`.
2025-10-12 17:05Z: CVE + KEV normalized rule verification complete; OSV parity fixtures revalidated—downstream parity/monitoring tasks may proceed.|
 |Merge pipeline parity for new advisory fields|BE-Merge|Models, Core|DONE (2025-10-15) – merge service now surfaces description/CWE/canonical metric decisions with updated metrics/tests.|
 |Connector coordination for new advisory fields|Connector Leads, BE-Merge|Models, Core|**DONE (2025-10-15)** – GHSA, NVD, and OSV connectors now emit advisory descriptions, CWE weaknesses, and canonical metric ids. Fixtures refreshed (GHSA connector regression suite, `conflict-nvd.canonical.json`, OSV parity snapshots) and completion recorded in coordination log.|
+|FEEDMERGE-ENGINE-07-001 Conflict sets & explainers|BE-Merge|FEEDSTORAGE-DATA-07-001|TODO – Persist conflict sets referencing advisory statements, output rule/explainer payloads with replay hashes, and add integration tests covering deterministic `asOf` evaluations.|
diff --git a/src/StellaOps.Feedser.Source.Ghsa.Tests/Fixtures/conflict-ghsa.canonical.json b/src/StellaOps.Feedser.Source.Ghsa.Tests/Fixtures/conflict-ghsa.canonical.json
index 5a96baa2..dcafd7fa 100644
--- a/src/StellaOps.Feedser.Source.Ghsa.Tests/Fixtures/conflict-ghsa.canonical.json
+++ b/src/StellaOps.Feedser.Source.Ghsa.Tests/Fixtures/conflict-ghsa.canonical.json
@@ -89,7 +89,7 @@
     "CVE-2025-4242",
     "GHSA-qqqq-wwww-eeee"
   ],
-  "canonicalMetricId": null,
+  "canonicalMetricId": "ghsa:severity/high",
   "credits": [
     {
       "displayName": "maintainer-team",
@@ -192,4 +192,4 @@
   "severity": "high",
   "summary": "Container escape in conflict-package",
   "title": "Container escape in conflict-package"
-}
\ No newline at end of file
+}
diff --git a/src/StellaOps.Feedser.Source.Ghsa.Tests/Ghsa/GhsaConflictFixtureTests.cs b/src/StellaOps.Feedser.Source.Ghsa.Tests/Ghsa/GhsaConflictFixtureTests.cs
index 3d455e47..b954bfbf 100644
--- a/src/StellaOps.Feedser.Source.Ghsa.Tests/Ghsa/GhsaConflictFixtureTests.cs
+++ b/src/StellaOps.Feedser.Source.Ghsa.Tests/Ghsa/GhsaConflictFixtureTests.cs
@@ -76,6 +76,8 @@ public sealed class GhsaConflictFixtureTests
         };
 
         var advisory = GhsaMapper.Map(dto, document, recordedAt);
+        Assert.Equal("ghsa:severity/high", advisory.CanonicalMetricId);
+        Assert.True(advisory.CvssMetrics.IsEmpty);
         var snapshot = SnapshotSerializer.ToSnapshot(advisory).Replace("\r\n", "\n").TrimEnd();
 
         var expectedPath = Path.Combine(AppContext.BaseDirectory, "Fixtures", "conflict-ghsa.canonical.json");
diff --git a/src/StellaOps.Feedser.Source.Ghsa.Tests/Ghsa/GhsaMapperTests.cs b/src/StellaOps.Feedser.Source.Ghsa.Tests/Ghsa/GhsaMapperTests.cs
new file mode 100644
index 00000000..f93923fa
--- /dev/null
+++ b/src/StellaOps.Feedser.Source.Ghsa.Tests/Ghsa/GhsaMapperTests.cs
@@ -0,0 +1,53 @@
+using StellaOps.Feedser.Source.Ghsa.Internal;
+using StellaOps.Feedser.Storage.Mongo.Documents;
+
+namespace StellaOps.Feedser.Source.Ghsa.Tests;
+
+public sealed class GhsaMapperTests
+{
+    [Fact]
+    public void Map_WhenCvssVectorMissing_UsesSeverityFallback()
+    {
+        var recordedAt = new DateTimeOffset(2025, 4, 10, 12, 0, 0, TimeSpan.Zero);
+        var document = new DocumentRecord(
+            Id: Guid.Parse("d7814678-3c3e-4e63-98c4-68e2f6d7ba6f"),
+            SourceName: GhsaConnectorPlugin.SourceName,
+            Uri: "https://github.com/advisories/GHSA-fallback-test",
+            FetchedAt: recordedAt.AddHours(-2),
+            Sha256: "sha256-ghsa-fallback-test",
+            Status: "completed",
+            ContentType: "application/json",
+            Headers: null,
+            Metadata: null,
+            Etag: "\"etag-ghsa-fallback\"",
+            LastModified: recordedAt.AddHours(-3),
+            GridFsId: null);
+
+        var dto = new GhsaRecordDto
+        {
+            GhsaId = "GHSA-fallback-test",
+            Summary = "Severity-only GHSA advisory",
+            Description = "GHSA record where GitHub omitted CVSS vector/score.",
+            Severity = null,
+            PublishedAt = recordedAt.AddDays(-3),
+            UpdatedAt = recordedAt.AddDays(-1),
+            Aliases = new[] { "GHSA-fallback-test" },
+            References = Array.Empty(),
+            Affected = Array.Empty(),
+            Credits = Array.Empty(),
+            Cwes = Array.Empty(),
+            Cvss = new GhsaCvssDto
+            {
+                Severity = "CRITICAL",
+                Score = null,
+                VectorString = null,
+            }
+        };
+
+        var advisory = GhsaMapper.Map(dto, document, recordedAt);
+
+        Assert.Equal("critical", advisory.Severity);
+        Assert.Equal("ghsa:severity/critical", advisory.CanonicalMetricId);
+        Assert.True(advisory.CvssMetrics.IsEmpty);
+    }
+}
diff --git a/src/StellaOps.Feedser.Source.Ghsa/GhsaConnector.cs b/src/StellaOps.Feedser.Source.Ghsa/GhsaConnector.cs
index 03bc591c..c5eaaf0e 100644
--- a/src/StellaOps.Feedser.Source.Ghsa/GhsaConnector.cs
+++ b/src/StellaOps.Feedser.Source.Ghsa/GhsaConnector.cs
@@ -381,6 +381,22 @@ public sealed class GhsaConnector : IFeedConnector
 
             var advisory = GhsaMapper.Map(dto, document, dtoRecord.ValidatedAt);
 
+            if (advisory.CvssMetrics.IsEmpty && !string.IsNullOrWhiteSpace(advisory.CanonicalMetricId))
+            {
+                var fallbackSeverity = string.IsNullOrWhiteSpace(advisory.Severity)
+                    ? "unknown"
+                    : advisory.Severity!;
+                _diagnostics.CanonicalMetricFallback(advisory.CanonicalMetricId!, fallbackSeverity);
+                if (_logger.IsEnabled(LogLevel.Debug))
+                {
+                    _logger.LogDebug(
+                        "GHSA {GhsaId} emitted canonical metric fallback {CanonicalMetricId} (severity {Severity})",
+                        advisory.AdvisoryKey,
+                        advisory.CanonicalMetricId,
+                        fallbackSeverity);
+                }
+            }
+
             await _advisoryStore.UpsertAsync(advisory, cancellationToken).ConfigureAwait(false);
             await _documentStore.UpdateStatusAsync(document.Id, DocumentStatuses.Mapped, cancellationToken).ConfigureAwait(false);
             pendingMappings.Remove(documentId);
diff --git a/src/StellaOps.Feedser.Source.Ghsa/Internal/GhsaDiagnostics.cs b/src/StellaOps.Feedser.Source.Ghsa/Internal/GhsaDiagnostics.cs
index d1bb313d..7ded3f38 100644
--- a/src/StellaOps.Feedser.Source.Ghsa/Internal/GhsaDiagnostics.cs
+++ b/src/StellaOps.Feedser.Source.Ghsa/Internal/GhsaDiagnostics.cs
@@ -23,6 +23,7 @@ public sealed class GhsaDiagnostics : IDisposable
     private readonly Histogram _rateLimitHeadroomPct;
     private readonly ObservableGauge _rateLimitHeadroomGauge;
     private readonly Counter _rateLimitExhausted;
+    private readonly Counter _canonicalMetricFallbacks;
     private readonly object _rateLimitLock = new();
     private GhsaRateLimitSnapshot? _lastRateLimitSnapshot;
     private readonly Dictionary<(string Phase, string? Resource), GhsaRateLimitSnapshot> _rateLimitSnapshots = new();
@@ -44,6 +45,7 @@ public sealed class GhsaDiagnostics : IDisposable
         _rateLimitHeadroomPct = _meter.CreateHistogram("ghsa.ratelimit.headroom_pct", unit: "percent");
         _rateLimitHeadroomGauge = _meter.CreateObservableGauge("ghsa.ratelimit.headroom_pct_current", ObserveHeadroom, unit: "percent");
         _rateLimitExhausted = _meter.CreateCounter("ghsa.ratelimit.exhausted", unit: "events");
+        _canonicalMetricFallbacks = _meter.CreateCounter("ghsa.map.canonical_metric_fallbacks", unit: "advisories");
     }
 
     public void FetchAttempt() => _fetchAttempts.Add(1);
@@ -100,6 +102,13 @@ public sealed class GhsaDiagnostics : IDisposable
     internal void RateLimitExhausted(string phase)
         => _rateLimitExhausted.Add(1, new KeyValuePair("phase", phase));
 
+    public void CanonicalMetricFallback(string canonicalMetricId, string severity)
+        => _canonicalMetricFallbacks.Add(
+            1,
+            new KeyValuePair("canonical_metric_id", canonicalMetricId),
+            new KeyValuePair("severity", severity),
+            new KeyValuePair("reason", "no_cvss"));
+
     internal GhsaRateLimitSnapshot? GetLastRateLimitSnapshot()
     {
         lock (_rateLimitLock)
diff --git a/src/StellaOps.Feedser.Source.Ghsa/Internal/GhsaMapper.cs b/src/StellaOps.Feedser.Source.Ghsa/Internal/GhsaMapper.cs
index d82afb7b..330b048a 100644
--- a/src/StellaOps.Feedser.Source.Ghsa/Internal/GhsaMapper.cs
+++ b/src/StellaOps.Feedser.Source.Ghsa/Internal/GhsaMapper.cs
@@ -57,7 +57,19 @@ internal static class GhsaMapper
         var weaknesses = CreateWeaknesses(dto.Cwes, recordedAt);
         var cvssMetrics = CreateCvssMetrics(dto.Cvss, recordedAt, out var cvssSeverity, out var canonicalMetricId);
 
-        var severity = SeverityNormalization.Normalize(dto.Severity) ?? cvssSeverity;
+        var severityHint = SeverityNormalization.Normalize(dto.Severity);
+        var cvssSeverityHint = SeverityNormalization.Normalize(dto.Cvss?.Severity);
+        var severity = severityHint ?? cvssSeverity ?? cvssSeverityHint;
+
+        if (canonicalMetricId is null)
+        {
+            var fallbackSeverity = severityHint ?? cvssSeverityHint ?? cvssSeverity;
+            if (!string.IsNullOrWhiteSpace(fallbackSeverity))
+            {
+                canonicalMetricId = BuildSeverityCanonicalMetricId(fallbackSeverity);
+            }
+        }
+
         var summary = dto.Summary ?? dto.Description;
         var description = Validation.TrimToNull(dto.Description);
 
@@ -81,6 +93,9 @@ internal static class GhsaMapper
             canonicalMetricId: canonicalMetricId);
     }
 
+    private static string BuildSeverityCanonicalMetricId(string severity)
+        => $"{GhsaConnectorPlugin.SourceName}:severity/{severity}";
+
     private static AdvisoryReference? CreateReference(GhsaReferenceDto reference, DateTimeOffset recordedAt)
     {
         if (string.IsNullOrWhiteSpace(reference.Url) || !Validation.LooksLikeHttpUrl(reference.Url))
diff --git a/src/StellaOps.Feedser.Source.Ghsa/TASKS.md b/src/StellaOps.Feedser.Source.Ghsa/TASKS.md
index ffd9b397..da091701 100644
--- a/src/StellaOps.Feedser.Source.Ghsa/TASKS.md
+++ b/src/StellaOps.Feedser.Source.Ghsa/TASKS.md
@@ -16,4 +16,4 @@
 |FEEDCONN-GHSA-02-005 Quota monitoring hardening|BE-Conn-GHSA, Observability|Source.Common metrics|**DONE (2025-10-12)** – Diagnostics expose headroom histograms/gauges, warning logs dedupe below the configured threshold, and the ops runbook gained alerting and mitigation guidance.|
 |FEEDCONN-GHSA-02-006 Scheduler rollout integration|BE-Conn-GHSA, Ops|Job scheduler|**DONE (2025-10-12)** – Dependency routine tests assert cron/timeouts, and the runbook highlights cron overrides plus backoff toggles for staged rollouts.|
 |FEEDCONN-GHSA-04-003 Description/CWE/metric parity rollout|BE-Conn-GHSA|Models, Core|**DONE (2025-10-15)** – Mapper emits advisory description, CWE weaknesses, and canonical CVSS metric id with updated fixtures (`osv-ghsa.osv.json` parity suite) and connector regression covers the new fields. Reported completion to Merge coordination.|
-|FEEDCONN-GHSA-04-004 Canonical metric fallback coverage|BE-Conn-GHSA|Models, Merge|TODO – Ensure canonical metric ids remain populated when GitHub omits CVSS vectors/scores; add fixtures capturing severity-only advisories, document precedence with Merge, and emit analytics to track fallback usage.|
+|FEEDCONN-GHSA-04-004 Canonical metric fallback coverage|BE-Conn-GHSA|Models, Merge|**DONE (2025-10-16)** – Ensure canonical metric ids remain populated when GitHub omits CVSS vectors/scores; add fixtures capturing severity-only advisories, document precedence with Merge, and emit analytics to track fallback usage.
2025-10-16: Mapper now emits `ghsa:severity/` canonical ids when vectors are missing, diagnostics expose `ghsa.map.canonical_metric_fallbacks`, conflict/mapper fixtures updated, and runbook documents Merge precedence. Tests: `dotnet test src/StellaOps.Feedser.Source.Ghsa.Tests/StellaOps.Feedser.Source.Ghsa.Tests.csproj`.|
diff --git a/src/StellaOps.Feedser.Source.Osv.Tests/Fixtures/osv-ghsa.osv.json b/src/StellaOps.Feedser.Source.Osv.Tests/Fixtures/osv-ghsa.osv.json
index 0a0f0bcc..724f6d31 100644
--- a/src/StellaOps.Feedser.Source.Osv.Tests/Fixtures/osv-ghsa.osv.json
+++ b/src/StellaOps.Feedser.Source.Osv.Tests/Fixtures/osv-ghsa.osv.json
@@ -1,1609 +1,1609 @@
-[
-  {
-    "advisoryKey": "GHSA-77vh-xpmg-72qh",
-    "affectedPackages": [
-      {
-        "type": "semver",
-        "identifier": "pkg:golang/github.com/opencontainers/image-spec",
-        "platform": "Go",
-        "versionRanges": [
-          {
-            "fixedVersion": "1.0.2",
-            "introducedVersion": "0",
-            "lastAffectedVersion": null,
-            "primitives": {
-              "evr": null,
-              "hasVendorExtensions": false,
-              "nevra": null,
-              "semVer": {
-                "constraintExpression": null,
-                "exactValue": null,
-                "fixed": "1.0.2",
-                "fixedInclusive": false,
-                "introduced": "0",
-                "introducedInclusive": true,
-                "lastAffected": null,
-                "lastAffectedInclusive": true,
-                "style": "range"
-              },
-              "vendorExtensions": null
-            },
-            "provenance": {
-              "source": "osv",
-              "kind": "range",
-              "value": "pkg:golang/github.com/opencontainers/image-spec",
-              "decisionReason": null,
-              "recordedAt": "2025-10-15T14:48:57.9970795+00:00",
-              "fieldMask": [
-                "affectedpackages[].versionranges[]"
-              ]
-            },
-            "rangeExpression": null,
-            "rangeKind": "semver"
-          }
-        ],
-        "normalizedVersions": [
-          {
-            "scheme": "semver",
-            "type": "range",
-            "min": "0",
-            "minInclusive": true,
-            "max": "1.0.2",
-            "maxInclusive": false,
-            "value": null,
-            "notes": "osv:Go:GHSA-77vh-xpmg-72qh:pkg:golang/github.com/opencontainers/image-spec"
-          }
-        ],
-        "statuses": [],
-        "provenance": [
-          {
-            "source": "osv",
-            "kind": "affected",
-            "value": "pkg:golang/github.com/opencontainers/image-spec",
-            "decisionReason": null,
-            "recordedAt": "2025-10-15T14:48:57.9970795+00:00",
-            "fieldMask": [
-              "affectedpackages[]"
-            ]
-          }
-        ]
-      }
-    ],
-    "aliases": [
-      "CGA-j36r-723f-8c29",
-      "GHSA-77vh-xpmg-72qh"
-    ],
-    "canonicalMetricId": "3.1|CVSS:3.1/AV:N/AC:H/PR:L/UI:R/S:C/C:N/I:L/A:N",
-    "credits": [],
-    "cvssMetrics": [
-      {
-        "baseScore": 3,
-        "baseSeverity": "low",
-        "provenance": {
-          "source": "osv",
-          "kind": "cvss",
-          "value": "CVSS_V3",
-          "decisionReason": null,
-          "recordedAt": "2025-10-15T14:48:57.9970795+00:00",
-          "fieldMask": []
-        },
-        "vector": "CVSS:3.1/AV:N/AC:H/PR:L/UI:R/S:C/C:N/I:L/A:N",
-        "version": "3.1"
-      }
-    ],
-    "cwes": [
-      {
-        "taxonomy": "cwe",
-        "identifier": "CWE-843",
-        "name": null,
-        "uri": "https://cwe.mitre.org/data/definitions/843.html",
-        "provenance": [
-          {
-            "source": "osv",
-            "kind": "weakness",
-            "value": "CWE-843",
-            "decisionReason": null,
-            "recordedAt": "2025-10-15T14:48:57.9970795+00:00",
-            "fieldMask": [
-              "cwes[]"
-            ]
-          }
-        ]
-      }
-    ],
-    "description": "### Impact\nIn the OCI Image Specification version 1.0.1 and prior, manifest and index documents are not self-describing and documents with a single digest could be interpreted as either a manifest or an index.\n\n### Patches\nThe Image Specification will be updated to recommend that both manifest and index documents contain a `mediaType` field to identify the type of document.\nRelease [v1.0.2](https://github.com/opencontainers/image-spec/releases/tag/v1.0.2) includes these updates.\n\n### Workarounds\nSoftware attempting to deserialize an ambiguous document may reject the document if it contains both “manifests” and “layers” fields or “manifests” and “config” fields.\n\n### References\nhttps://github.com/opencontainers/distribution-spec/security/advisories/GHSA-mc8v-mgrf-8f4m\n\n### For more information\nIf you have any questions or comments about this advisory:\n* Open an issue in https://github.com/opencontainers/image-spec\n* Email us at [security@opencontainers.org](mailto:security@opencontainers.org)\n* https://github.com/opencontainers/image-spec/commits/v1.0.2",
-    "exploitKnown": false,
-    "language": "en",
-    "modified": "2021-11-24T19:43:35+00:00",
-    "provenance": [
-      {
-        "source": "osv",
-        "kind": "document",
-        "value": "https://osv.dev/vulnerability/GHSA-77vh-xpmg-72qh",
-        "decisionReason": null,
-        "recordedAt": "2021-11-18T16:02:41+00:00",
-        "fieldMask": [
-          "advisory"
-        ]
-      },
-      {
-        "source": "osv",
-        "kind": "mapping",
-        "value": "GHSA-77vh-xpmg-72qh",
-        "decisionReason": null,
-        "recordedAt": "2025-10-15T14:48:57.9970795+00:00",
-        "fieldMask": [
-          "advisory"
-        ]
-      }
-    ],
-    "published": "2021-11-18T16:02:41+00:00",
-    "references": [
-      {
-        "kind": null,
-        "provenance": {
-          "source": "osv",
-          "kind": "reference",
-          "value": "https://github.com/opencontainers/distribution-spec/security/advisories/GHSA-mc8v-mgrf-8f4m",
-          "decisionReason": null,
-          "recordedAt": "2025-10-15T14:48:57.9970795+00:00",
-          "fieldMask": [
-            "references[]"
-          ]
-        },
-        "sourceTag": "WEB",
-        "summary": null,
-        "url": "https://github.com/opencontainers/distribution-spec/security/advisories/GHSA-mc8v-mgrf-8f4m"
-      },
-      {
-        "kind": null,
-        "provenance": {
-          "source": "osv",
-          "kind": "reference",
-          "value": "https://github.com/opencontainers/image-spec",
-          "decisionReason": null,
-          "recordedAt": "2025-10-15T14:48:57.9970795+00:00",
-          "fieldMask": [
-            "references[]"
-          ]
-        },
-        "sourceTag": "PACKAGE",
-        "summary": null,
-        "url": "https://github.com/opencontainers/image-spec"
-      },
-      {
-        "kind": null,
-        "provenance": {
-          "source": "osv",
-          "kind": "reference",
-          "value": "https://github.com/opencontainers/image-spec/commit/693428a734f5bab1a84bd2f990d92ef1111cd60c",
-          "decisionReason": null,
-          "recordedAt": "2025-10-15T14:48:57.9970795+00:00",
-          "fieldMask": [
-            "references[]"
-          ]
-        },
-        "sourceTag": "WEB",
-        "summary": null,
-        "url": "https://github.com/opencontainers/image-spec/commit/693428a734f5bab1a84bd2f990d92ef1111cd60c"
-      },
-      {
-        "kind": null,
-        "provenance": {
-          "source": "osv",
-          "kind": "reference",
-          "value": "https://github.com/opencontainers/image-spec/releases/tag/v1.0.2",
-          "decisionReason": null,
-          "recordedAt": "2025-10-15T14:48:57.9970795+00:00",
-          "fieldMask": [
-            "references[]"
-          ]
-        },
-        "sourceTag": "WEB",
-        "summary": null,
-        "url": "https://github.com/opencontainers/image-spec/releases/tag/v1.0.2"
-      },
-      {
-        "kind": null,
-        "provenance": {
-          "source": "osv",
-          "kind": "reference",
-          "value": "https://github.com/opencontainers/image-spec/security/advisories/GHSA-77vh-xpmg-72qh",
-          "decisionReason": null,
-          "recordedAt": "2025-10-15T14:48:57.9970795+00:00",
-          "fieldMask": [
-            "references[]"
-          ]
-        },
-        "sourceTag": "WEB",
-        "summary": null,
-        "url": "https://github.com/opencontainers/image-spec/security/advisories/GHSA-77vh-xpmg-72qh"
-      }
-    ],
-    "severity": "low",
-    "summary": "### Impact In the OCI Image Specification version 1.0.1 and prior, manifest and index documents are not self-describing and documents with a single digest could be interpreted as either a manifest or an index. ### Patches The Image Specification will be updated to recommend that both manifest and index documents contain a `mediaType` field to identify the type of document. Release [v1.0.2](https://github.com/opencontainers/image-spec/releases/tag/v1.0.2) includes these updates. ### Workarounds Software attempting to deserialize an ambiguous document may reject the document if it contains both “manifests” and “layers” fields or “manifests” and “config” fields. ### References https://github.com/opencontainers/distribution-spec/security/advisories/GHSA-mc8v-mgrf-8f4m ### For more information If you have any questions or comments about this advisory: * Open an issue in https://github.com/opencontainers/image-spec * Email us at [security@opencontainers.org](mailto:security@opencontainers.org) * https://github.com/opencontainers/image-spec/commits/v1.0.2",
-    "title": "Clarify `mediaType` handling"
-  },
-  {
-    "advisoryKey": "GHSA-7rjr-3q55-vv33",
-    "affectedPackages": [
-      {
-        "type": "semver",
-        "identifier": "pkg:maven/org.apache.logging.log4j/log4j-core",
-        "platform": "Maven",
-        "versionRanges": [
-          {
-            "fixedVersion": "2.16.0",
-            "introducedVersion": "2.13.0",
-            "lastAffectedVersion": null,
-            "primitives": {
-              "evr": null,
-              "hasVendorExtensions": false,
-              "nevra": null,
-              "semVer": {
-                "constraintExpression": null,
-                "exactValue": null,
-                "fixed": "2.16.0",
-                "fixedInclusive": false,
-                "introduced": "2.13.0",
-                "introducedInclusive": true,
-                "lastAffected": null,
-                "lastAffectedInclusive": true,
-                "style": "range"
-              },
-              "vendorExtensions": null
-            },
-            "provenance": {
-              "source": "osv",
-              "kind": "range",
-              "value": "pkg:maven/org.apache.logging.log4j/log4j-core",
-              "decisionReason": null,
-              "recordedAt": "2025-10-15T14:48:57.9980643+00:00",
-              "fieldMask": [
-                "affectedpackages[].versionranges[]"
-              ]
-            },
-            "rangeExpression": null,
-            "rangeKind": "semver"
-          }
-        ],
-        "normalizedVersions": [
-          {
-            "scheme": "semver",
-            "type": "range",
-            "min": "2.13.0",
-            "minInclusive": true,
-            "max": "2.16.0",
-            "maxInclusive": false,
-            "value": null,
-            "notes": "osv:Maven:GHSA-7rjr-3q55-vv33:pkg:maven/org.apache.logging.log4j/log4j-core"
-          }
-        ],
-        "statuses": [],
-        "provenance": [
-          {
-            "source": "osv",
-            "kind": "affected",
-            "value": "pkg:maven/org.apache.logging.log4j/log4j-core",
-            "decisionReason": null,
-            "recordedAt": "2025-10-15T14:48:57.9980643+00:00",
-            "fieldMask": [
-              "affectedpackages[]"
-            ]
-          }
-        ]
-      },
-      {
-        "type": "semver",
-        "identifier": "pkg:maven/org.apache.logging.log4j/log4j-core",
-        "platform": "Maven",
-        "versionRanges": [
-          {
-            "fixedVersion": "2.12.2",
-            "introducedVersion": "0",
-            "lastAffectedVersion": null,
-            "primitives": {
-              "evr": null,
-              "hasVendorExtensions": false,
-              "nevra": null,
-              "semVer": {
-                "constraintExpression": null,
-                "exactValue": null,
-                "fixed": "2.12.2",
-                "fixedInclusive": false,
-                "introduced": "0",
-                "introducedInclusive": true,
-                "lastAffected": null,
-                "lastAffectedInclusive": true,
-                "style": "range"
-              },
-              "vendorExtensions": null
-            },
-            "provenance": {
-              "source": "osv",
-              "kind": "range",
-              "value": "pkg:maven/org.apache.logging.log4j/log4j-core",
-              "decisionReason": null,
-              "recordedAt": "2025-10-15T14:48:57.9980643+00:00",
-              "fieldMask": [
-                "affectedpackages[].versionranges[]"
-              ]
-            },
-            "rangeExpression": null,
-            "rangeKind": "semver"
-          }
-        ],
-        "normalizedVersions": [
-          {
-            "scheme": "semver",
-            "type": "range",
-            "min": "0",
-            "minInclusive": true,
-            "max": "2.12.2",
-            "maxInclusive": false,
-            "value": null,
-            "notes": "osv:Maven:GHSA-7rjr-3q55-vv33:pkg:maven/org.apache.logging.log4j/log4j-core"
-          }
-        ],
-        "statuses": [],
-        "provenance": [
-          {
-            "source": "osv",
-            "kind": "affected",
-            "value": "pkg:maven/org.apache.logging.log4j/log4j-core",
-            "decisionReason": null,
-            "recordedAt": "2025-10-15T14:48:57.9980643+00:00",
-            "fieldMask": [
-              "affectedpackages[]"
-            ]
-          }
-        ]
-      },
-      {
-        "type": "semver",
-        "identifier": "pkg:maven/org.ops4j.pax.logging/pax-logging-log4j2",
-        "platform": "Maven",
-        "versionRanges": [
-          {
-            "fixedVersion": "1.9.2",
-            "introducedVersion": "1.8.0",
-            "lastAffectedVersion": null,
-            "primitives": {
-              "evr": null,
-              "hasVendorExtensions": false,
-              "nevra": null,
-              "semVer": {
-                "constraintExpression": null,
-                "exactValue": null,
-                "fixed": "1.9.2",
-                "fixedInclusive": false,
-                "introduced": "1.8.0",
-                "introducedInclusive": true,
-                "lastAffected": null,
-                "lastAffectedInclusive": true,
-                "style": "range"
-              },
-              "vendorExtensions": null
-            },
-            "provenance": {
-              "source": "osv",
-              "kind": "range",
-              "value": "pkg:maven/org.ops4j.pax.logging/pax-logging-log4j2",
-              "decisionReason": null,
-              "recordedAt": "2025-10-15T14:48:57.9980643+00:00",
-              "fieldMask": [
-                "affectedpackages[].versionranges[]"
-              ]
-            },
-            "rangeExpression": null,
-            "rangeKind": "semver"
-          }
-        ],
-        "normalizedVersions": [
-          {
-            "scheme": "semver",
-            "type": "range",
-            "min": "1.8.0",
-            "minInclusive": true,
-            "max": "1.9.2",
-            "maxInclusive": false,
-            "value": null,
-            "notes": "osv:Maven:GHSA-7rjr-3q55-vv33:pkg:maven/org.ops4j.pax.logging/pax-logging-log4j2"
-          }
-        ],
-        "statuses": [],
-        "provenance": [
-          {
-            "source": "osv",
-            "kind": "affected",
-            "value": "pkg:maven/org.ops4j.pax.logging/pax-logging-log4j2",
-            "decisionReason": null,
-            "recordedAt": "2025-10-15T14:48:57.9980643+00:00",
-            "fieldMask": [
-              "affectedpackages[]"
-            ]
-          }
-        ]
-      },
-      {
-        "type": "semver",
-        "identifier": "pkg:maven/org.ops4j.pax.logging/pax-logging-log4j2",
-        "platform": "Maven",
-        "versionRanges": [
-          {
-            "fixedVersion": "1.10.8",
-            "introducedVersion": "1.10.0",
-            "lastAffectedVersion": null,
-            "primitives": {
-              "evr": null,
-              "hasVendorExtensions": false,
-              "nevra": null,
-              "semVer": {
-                "constraintExpression": null,
-                "exactValue": null,
-                "fixed": "1.10.8",
-                "fixedInclusive": false,
-                "introduced": "1.10.0",
-                "introducedInclusive": true,
-                "lastAffected": null,
-                "lastAffectedInclusive": true,
-                "style": "range"
-              },
-              "vendorExtensions": null
-            },
-            "provenance": {
-              "source": "osv",
-              "kind": "range",
-              "value": "pkg:maven/org.ops4j.pax.logging/pax-logging-log4j2",
-              "decisionReason": null,
-              "recordedAt": "2025-10-15T14:48:57.9980643+00:00",
-              "fieldMask": [
-                "affectedpackages[].versionranges[]"
-              ]
-            },
-            "rangeExpression": null,
-            "rangeKind": "semver"
-          }
-        ],
-        "normalizedVersions": [
-          {
-            "scheme": "semver",
-            "type": "range",
-            "min": "1.10.0",
-            "minInclusive": true,
-            "max": "1.10.8",
-            "maxInclusive": false,
-            "value": null,
-            "notes": "osv:Maven:GHSA-7rjr-3q55-vv33:pkg:maven/org.ops4j.pax.logging/pax-logging-log4j2"
-          }
-        ],
-        "statuses": [],
-        "provenance": [
-          {
-            "source": "osv",
-            "kind": "affected",
-            "value": "pkg:maven/org.ops4j.pax.logging/pax-logging-log4j2",
-            "decisionReason": null,
-            "recordedAt": "2025-10-15T14:48:57.9980643+00:00",
-            "fieldMask": [
-              "affectedpackages[]"
-            ]
-          }
-        ]
-      },
-      {
-        "type": "semver",
-        "identifier": "pkg:maven/org.ops4j.pax.logging/pax-logging-log4j2",
-        "platform": "Maven",
-        "versionRanges": [
-          {
-            "fixedVersion": "1.11.11",
-            "introducedVersion": "1.11.0",
-            "lastAffectedVersion": null,
-            "primitives": {
-              "evr": null,
-              "hasVendorExtensions": false,
-              "nevra": null,
-              "semVer": {
-                "constraintExpression": null,
-                "exactValue": null,
-                "fixed": "1.11.11",
-                "fixedInclusive": false,
-                "introduced": "1.11.0",
-                "introducedInclusive": true,
-                "lastAffected": null,
-                "lastAffectedInclusive": true,
-                "style": "range"
-              },
-              "vendorExtensions": null
-            },
-            "provenance": {
-              "source": "osv",
-              "kind": "range",
-              "value": "pkg:maven/org.ops4j.pax.logging/pax-logging-log4j2",
-              "decisionReason": null,
-              "recordedAt": "2025-10-15T14:48:57.9980643+00:00",
-              "fieldMask": [
-                "affectedpackages[].versionranges[]"
-              ]
-            },
-            "rangeExpression": null,
-            "rangeKind": "semver"
-          }
-        ],
-        "normalizedVersions": [
-          {
-            "scheme": "semver",
-            "type": "range",
-            "min": "1.11.0",
-            "minInclusive": true,
-            "max": "1.11.11",
-            "maxInclusive": false,
-            "value": null,
-            "notes": "osv:Maven:GHSA-7rjr-3q55-vv33:pkg:maven/org.ops4j.pax.logging/pax-logging-log4j2"
-          }
-        ],
-        "statuses": [],
-        "provenance": [
-          {
-            "source": "osv",
-            "kind": "affected",
-            "value": "pkg:maven/org.ops4j.pax.logging/pax-logging-log4j2",
-            "decisionReason": null,
-            "recordedAt": "2025-10-15T14:48:57.9980643+00:00",
-            "fieldMask": [
-              "affectedpackages[]"
-            ]
-          }
-        ]
-      },
-      {
-        "type": "semver",
-        "identifier": "pkg:maven/org.ops4j.pax.logging/pax-logging-log4j2",
-        "platform": "Maven",
-        "versionRanges": [
-          {
-            "fixedVersion": "2.0.12",
-            "introducedVersion": "2.0.0",
-            "lastAffectedVersion": null,
-            "primitives": {
-              "evr": null,
-              "hasVendorExtensions": false,
-              "nevra": null,
-              "semVer": {
-                "constraintExpression": null,
-                "exactValue": null,
-                "fixed": "2.0.12",
-                "fixedInclusive": false,
-                "introduced": "2.0.0",
-                "introducedInclusive": true,
-                "lastAffected": null,
-                "lastAffectedInclusive": true,
-                "style": "range"
-              },
-              "vendorExtensions": null
-            },
-            "provenance": {
-              "source": "osv",
-              "kind": "range",
-              "value": "pkg:maven/org.ops4j.pax.logging/pax-logging-log4j2",
-              "decisionReason": null,
-              "recordedAt": "2025-10-15T14:48:57.9980643+00:00",
-              "fieldMask": [
-                "affectedpackages[].versionranges[]"
-              ]
-            },
-            "rangeExpression": null,
-            "rangeKind": "semver"
-          }
-        ],
-        "normalizedVersions": [
-          {
-            "scheme": "semver",
-            "type": "range",
-            "min": "2.0.0",
-            "minInclusive": true,
-            "max": "2.0.12",
-            "maxInclusive": false,
-            "value": null,
-            "notes": "osv:Maven:GHSA-7rjr-3q55-vv33:pkg:maven/org.ops4j.pax.logging/pax-logging-log4j2"
-          }
-        ],
-        "statuses": [],
-        "provenance": [
-          {
-            "source": "osv",
-            "kind": "affected",
-            "value": "pkg:maven/org.ops4j.pax.logging/pax-logging-log4j2",
-            "decisionReason": null,
-            "recordedAt": "2025-10-15T14:48:57.9980643+00:00",
-            "fieldMask": [
-              "affectedpackages[]"
-            ]
-          }
-        ]
-      }
-    ],
-    "aliases": [
-      "CVE-2021-45046",
-      "GHSA-7rjr-3q55-vv33"
-    ],
-    "canonicalMetricId": "3.1|CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:C/C:H/I:H/A:H",
-    "credits": [],
-    "cvssMetrics": [
-      {
-        "baseScore": 9,
-        "baseSeverity": "critical",
-        "provenance": {
-          "source": "osv",
-          "kind": "cvss",
-          "value": "CVSS_V3",
-          "decisionReason": null,
-          "recordedAt": "2025-10-15T14:48:57.9980643+00:00",
-          "fieldMask": []
-        },
-        "vector": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:C/C:H/I:H/A:H",
-        "version": "3.1"
-      }
-    ],
-    "cwes": [
-      {
-        "taxonomy": "cwe",
-        "identifier": "CWE-502",
-        "name": null,
-        "uri": "https://cwe.mitre.org/data/definitions/502.html",
-        "provenance": [
-          {
-            "source": "osv",
-            "kind": "weakness",
-            "value": "CWE-502",
-            "decisionReason": null,
-            "recordedAt": "2025-10-15T14:48:57.9980643+00:00",
-            "fieldMask": [
-              "cwes[]"
-            ]
-          }
-        ]
-      },
-      {
-        "taxonomy": "cwe",
-        "identifier": "CWE-917",
-        "name": null,
-        "uri": "https://cwe.mitre.org/data/definitions/917.html",
-        "provenance": [
-          {
-            "source": "osv",
-            "kind": "weakness",
-            "value": "CWE-917",
-            "decisionReason": null,
-            "recordedAt": "2025-10-15T14:48:57.9980643+00:00",
-            "fieldMask": [
-              "cwes[]"
-            ]
-          }
-        ]
-      }
-    ],
-    "description": "# Impact\n\nThe fix to address [CVE-2021-44228](https://nvd.nist.gov/vuln/detail/CVE-2021-44228) in Apache Log4j 2.15.0 was incomplete in certain non-default configurations. This could allow attackers with control over Thread Context Map (MDC) input data when the logging configuration uses a non-default Pattern Layout with either a Context Lookup (for example, $${ctx:loginId}) or a Thread Context Map pattern (%X, %mdc, or %MDC) to craft malicious input data using a JNDI Lookup pattern resulting in a remote code execution (RCE) attack. \n\n## Affected packages\nOnly the `org.apache.logging.log4j:log4j-core` package is directly affected by this vulnerability. The `org.apache.logging.log4j:log4j-api` should be kept at the same version as the `org.apache.logging.log4j:log4j-core` package to ensure compatability if in use.\n\n# Mitigation\n\nLog4j 2.16.0 fixes this issue by removing support for message lookup patterns and disabling JNDI functionality by default. This issue can be mitigated in prior releases (< 2.16.0) by removing the JndiLookup class from the classpath (example: zip -q -d log4j-core-*.jar org/apache/logging/log4j/core/lookup/JndiLookup.class).\n\nLog4j 2.15.0 restricts JNDI LDAP lookups to localhost by default. Note that previous mitigations involving configuration such as to set the system property `log4j2.formatMsgNoLookups` to `true` do NOT mitigate this specific vulnerability.",
-    "exploitKnown": false,
-    "language": "en",
-    "modified": "2025-05-09T13:13:16.169374+00:00",
-    "provenance": [
-      {
-        "source": "osv",
-        "kind": "document",
-        "value": "https://osv.dev/vulnerability/GHSA-7rjr-3q55-vv33",
-        "decisionReason": null,
-        "recordedAt": "2021-12-14T18:01:28+00:00",
-        "fieldMask": [
-          "advisory"
-        ]
-      },
-      {
-        "source": "osv",
-        "kind": "mapping",
-        "value": "GHSA-7rjr-3q55-vv33",
-        "decisionReason": null,
-        "recordedAt": "2025-10-15T14:48:57.9980643+00:00",
-        "fieldMask": [
-          "advisory"
-        ]
-      }
-    ],
-    "published": "2021-12-14T18:01:28+00:00",
-    "references": [
-      {
-        "kind": null,
-        "provenance": {
-          "source": "osv",
-          "kind": "reference",
-          "value": "http://www.openwall.com/lists/oss-security/2021/12/14/4",
-          "decisionReason": null,
-          "recordedAt": "2025-10-15T14:48:57.9980643+00:00",
-          "fieldMask": [
-            "references[]"
-          ]
-        },
-        "sourceTag": "WEB",
-        "summary": null,
-        "url": "http://www.openwall.com/lists/oss-security/2021/12/14/4"
-      },
-      {
-        "kind": null,
-        "provenance": {
-          "source": "osv",
-          "kind": "reference",
-          "value": "http://www.openwall.com/lists/oss-security/2021/12/15/3",
-          "decisionReason": null,
-          "recordedAt": "2025-10-15T14:48:57.9980643+00:00",
-          "fieldMask": [
-            "references[]"
-          ]
-        },
-        "sourceTag": "WEB",
-        "summary": null,
-        "url": "http://www.openwall.com/lists/oss-security/2021/12/15/3"
-      },
-      {
-        "kind": null,
-        "provenance": {
-          "source": "osv",
-          "kind": "reference",
-          "value": "http://www.openwall.com/lists/oss-security/2021/12/18/1",
-          "decisionReason": null,
-          "recordedAt": "2025-10-15T14:48:57.9980643+00:00",
-          "fieldMask": [
-            "references[]"
-          ]
-        },
-        "sourceTag": "WEB",
-        "summary": null,
-        "url": "http://www.openwall.com/lists/oss-security/2021/12/18/1"
-      },
-      {
-        "kind": null,
-        "provenance": {
-          "source": "osv",
-          "kind": "reference",
-          "value": "https://cert-portal.siemens.com/productcert/pdf/ssa-397453.pdf",
-          "decisionReason": null,
-          "recordedAt": "2025-10-15T14:48:57.9980643+00:00",
-          "fieldMask": [
-            "references[]"
-          ]
-        },
-        "sourceTag": "WEB",
-        "summary": null,
-        "url": "https://cert-portal.siemens.com/productcert/pdf/ssa-397453.pdf"
-      },
-      {
-        "kind": null,
-        "provenance": {
-          "source": "osv",
-          "kind": "reference",
-          "value": "https://cert-portal.siemens.com/productcert/pdf/ssa-479842.pdf",
-          "decisionReason": null,
-          "recordedAt": "2025-10-15T14:48:57.9980643+00:00",
-          "fieldMask": [
-            "references[]"
-          ]
-        },
-        "sourceTag": "WEB",
-        "summary": null,
-        "url": "https://cert-portal.siemens.com/productcert/pdf/ssa-479842.pdf"
-      },
-      {
-        "kind": null,
-        "provenance": {
-          "source": "osv",
-          "kind": "reference",
-          "value": "https://cert-portal.siemens.com/productcert/pdf/ssa-661247.pdf",
-          "decisionReason": null,
-          "recordedAt": "2025-10-15T14:48:57.9980643+00:00",
-          "fieldMask": [
-            "references[]"
-          ]
-        },
-        "sourceTag": "WEB",
-        "summary": null,
-        "url": "https://cert-portal.siemens.com/productcert/pdf/ssa-661247.pdf"
-      },
-      {
-        "kind": null,
-        "provenance": {
-          "source": "osv",
-          "kind": "reference",
-          "value": "https://cert-portal.siemens.com/productcert/pdf/ssa-714170.pdf",
-          "decisionReason": null,
-          "recordedAt": "2025-10-15T14:48:57.9980643+00:00",
-          "fieldMask": [
-            "references[]"
-          ]
-        },
-        "sourceTag": "WEB",
-        "summary": null,
-        "url": "https://cert-portal.siemens.com/productcert/pdf/ssa-714170.pdf"
-      },
-      {
-        "kind": "advisory",
-        "provenance": {
-          "source": "osv",
-          "kind": "reference",
-          "value": "https://github.com/advisories/GHSA-jfh8-c2jp-5v3q",
-          "decisionReason": null,
-          "recordedAt": "2025-10-15T14:48:57.9980643+00:00",
-          "fieldMask": [
-            "references[]"
-          ]
-        },
-        "sourceTag": "ADVISORY",
-        "summary": null,
-        "url": "https://github.com/advisories/GHSA-jfh8-c2jp-5v3q"
-      },
-      {
-        "kind": null,
-        "provenance": {
-          "source": "osv",
-          "kind": "reference",
-          "value": "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/EOKPQGV24RRBBI4TBZUDQMM4MEH7MXCY",
-          "decisionReason": null,
-          "recordedAt": "2025-10-15T14:48:57.9980643+00:00",
-          "fieldMask": [
-            "references[]"
-          ]
-        },
-        "sourceTag": "WEB",
-        "summary": null,
-        "url": "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/EOKPQGV24RRBBI4TBZUDQMM4MEH7MXCY"
-      },
-      {
-        "kind": null,
-        "provenance": {
-          "source": "osv",
-          "kind": "reference",
-          "value": "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/SIG7FZULMNK2XF6FZRU4VWYDQXNMUGAJ",
-          "decisionReason": null,
-          "recordedAt": "2025-10-15T14:48:57.9980643+00:00",
-          "fieldMask": [
-            "references[]"
-          ]
-        },
-        "sourceTag": "WEB",
-        "summary": null,
-        "url": "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/SIG7FZULMNK2XF6FZRU4VWYDQXNMUGAJ"
-      },
-      {
-        "kind": null,
-        "provenance": {
-          "source": "osv",
-          "kind": "reference",
-          "value": "https://logging.apache.org/log4j/2.x/security.html",
-          "decisionReason": null,
-          "recordedAt": "2025-10-15T14:48:57.9980643+00:00",
-          "fieldMask": [
-            "references[]"
-          ]
-        },
-        "sourceTag": "WEB",
-        "summary": null,
-        "url": "https://logging.apache.org/log4j/2.x/security.html"
-      },
-      {
-        "kind": "advisory",
-        "provenance": {
-          "source": "osv",
-          "kind": "reference",
-          "value": "https://nvd.nist.gov/vuln/detail/CVE-2021-45046",
-          "decisionReason": null,
-          "recordedAt": "2025-10-15T14:48:57.9980643+00:00",
-          "fieldMask": [
-            "references[]"
-          ]
-        },
-        "sourceTag": "ADVISORY",
-        "summary": null,
-        "url": "https://nvd.nist.gov/vuln/detail/CVE-2021-45046"
-      },
-      {
-        "kind": null,
-        "provenance": {
-          "source": "osv",
-          "kind": "reference",
-          "value": "https://psirt.global.sonicwall.com/vuln-detail/SNWLID-2021-0032",
-          "decisionReason": null,
-          "recordedAt": "2025-10-15T14:48:57.9980643+00:00",
-          "fieldMask": [
-            "references[]"
-          ]
-        },
-        "sourceTag": "WEB",
-        "summary": null,
-        "url": "https://psirt.global.sonicwall.com/vuln-detail/SNWLID-2021-0032"
-      },
-      {
-        "kind": null,
-        "provenance": {
-          "source": "osv",
-          "kind": "reference",
-          "value": "https://sec.cloudapps.cisco.com/security/center/content/CiscoSecurityAdvisory/cisco-sa-apache-log4j-qRuKNEbd",
-          "decisionReason": null,
-          "recordedAt": "2025-10-15T14:48:57.9980643+00:00",
-          "fieldMask": [
-            "references[]"
-          ]
-        },
-        "sourceTag": "WEB",
-        "summary": null,
-        "url": "https://sec.cloudapps.cisco.com/security/center/content/CiscoSecurityAdvisory/cisco-sa-apache-log4j-qRuKNEbd"
-      },
-      {
-        "kind": null,
-        "provenance": {
-          "source": "osv",
-          "kind": "reference",
-          "value": "https://security.gentoo.org/glsa/202310-16",
-          "decisionReason": null,
-          "recordedAt": "2025-10-15T14:48:57.9980643+00:00",
-          "fieldMask": [
-            "references[]"
-          ]
-        },
-        "sourceTag": "WEB",
-        "summary": null,
-        "url": "https://security.gentoo.org/glsa/202310-16"
-      },
-      {
-        "kind": null,
-        "provenance": {
-          "source": "osv",
-          "kind": "reference",
-          "value": "https://www.cve.org/CVERecord?id=CVE-2021-44228",
-          "decisionReason": null,
-          "recordedAt": "2025-10-15T14:48:57.9980643+00:00",
-          "fieldMask": [
-            "references[]"
-          ]
-        },
-        "sourceTag": "WEB",
-        "summary": null,
-        "url": "https://www.cve.org/CVERecord?id=CVE-2021-44228"
-      },
-      {
-        "kind": null,
-        "provenance": {
-          "source": "osv",
-          "kind": "reference",
-          "value": "https://www.debian.org/security/2021/dsa-5022",
-          "decisionReason": null,
-          "recordedAt": "2025-10-15T14:48:57.9980643+00:00",
-          "fieldMask": [
-            "references[]"
-          ]
-        },
-        "sourceTag": "WEB",
-        "summary": null,
-        "url": "https://www.debian.org/security/2021/dsa-5022"
-      },
-      {
-        "kind": null,
-        "provenance": {
-          "source": "osv",
-          "kind": "reference",
-          "value": "https://www.intel.com/content/www/us/en/security-center/advisory/intel-sa-00646.html",
-          "decisionReason": null,
-          "recordedAt": "2025-10-15T14:48:57.9980643+00:00",
-          "fieldMask": [
-            "references[]"
-          ]
-        },
-        "sourceTag": "WEB",
-        "summary": null,
-        "url": "https://www.intel.com/content/www/us/en/security-center/advisory/intel-sa-00646.html"
-      },
-      {
-        "kind": null,
-        "provenance": {
-          "source": "osv",
-          "kind": "reference",
-          "value": "https://www.kb.cert.org/vuls/id/930724",
-          "decisionReason": null,
-          "recordedAt": "2025-10-15T14:48:57.9980643+00:00",
-          "fieldMask": [
-            "references[]"
-          ]
-        },
-        "sourceTag": "WEB",
-        "summary": null,
-        "url": "https://www.kb.cert.org/vuls/id/930724"
-      },
-      {
-        "kind": null,
-        "provenance": {
-          "source": "osv",
-          "kind": "reference",
-          "value": "https://www.openwall.com/lists/oss-security/2021/12/14/4",
-          "decisionReason": null,
-          "recordedAt": "2025-10-15T14:48:57.9980643+00:00",
-          "fieldMask": [
-            "references[]"
-          ]
-        },
-        "sourceTag": "WEB",
-        "summary": null,
-        "url": "https://www.openwall.com/lists/oss-security/2021/12/14/4"
-      },
-      {
-        "kind": null,
-        "provenance": {
-          "source": "osv",
-          "kind": "reference",
-          "value": "https://www.oracle.com/security-alerts/alert-cve-2021-44228.html",
-          "decisionReason": null,
-          "recordedAt": "2025-10-15T14:48:57.9980643+00:00",
-          "fieldMask": [
-            "references[]"
-          ]
-        },
-        "sourceTag": "WEB",
-        "summary": null,
-        "url": "https://www.oracle.com/security-alerts/alert-cve-2021-44228.html"
-      },
-      {
-        "kind": null,
-        "provenance": {
-          "source": "osv",
-          "kind": "reference",
-          "value": "https://www.oracle.com/security-alerts/cpuapr2022.html",
-          "decisionReason": null,
-          "recordedAt": "2025-10-15T14:48:57.9980643+00:00",
-          "fieldMask": [
-            "references[]"
-          ]
-        },
-        "sourceTag": "WEB",
-        "summary": null,
-        "url": "https://www.oracle.com/security-alerts/cpuapr2022.html"
-      },
-      {
-        "kind": null,
-        "provenance": {
-          "source": "osv",
-          "kind": "reference",
-          "value": "https://www.oracle.com/security-alerts/cpujan2022.html",
-          "decisionReason": null,
-          "recordedAt": "2025-10-15T14:48:57.9980643+00:00",
-          "fieldMask": [
-            "references[]"
-          ]
-        },
-        "sourceTag": "WEB",
-        "summary": null,
-        "url": "https://www.oracle.com/security-alerts/cpujan2022.html"
-      },
-      {
-        "kind": null,
-        "provenance": {
-          "source": "osv",
-          "kind": "reference",
-          "value": "https://www.oracle.com/security-alerts/cpujul2022.html",
-          "decisionReason": null,
-          "recordedAt": "2025-10-15T14:48:57.9980643+00:00",
-          "fieldMask": [
-            "references[]"
-          ]
-        },
-        "sourceTag": "WEB",
-        "summary": null,
-        "url": "https://www.oracle.com/security-alerts/cpujul2022.html"
-      }
-    ],
-    "severity": "critical",
-    "summary": "# Impact The fix to address [CVE-2021-44228](https://nvd.nist.gov/vuln/detail/CVE-2021-44228) in Apache Log4j 2.15.0 was incomplete in certain non-default configurations. This could allow attackers with control over Thread Context Map (MDC) input data when the logging configuration uses a non-default Pattern Layout with either a Context Lookup (for example, $${ctx:loginId}) or a Thread Context Map pattern (%X, %mdc, or %MDC) to craft malicious input data using a JNDI Lookup pattern resulting in a remote code execution (RCE) attack. ## Affected packages Only the `org.apache.logging.log4j:log4j-core` package is directly affected by this vulnerability. The `org.apache.logging.log4j:log4j-api` should be kept at the same version as the `org.apache.logging.log4j:log4j-core` package to ensure compatability if in use. # Mitigation Log4j 2.16.0 fixes this issue by removing support for message lookup patterns and disabling JNDI functionality by default. This issue can be mitigated in prior releases (< 2.16.0) by removing the JndiLookup class from the classpath (example: zip -q -d log4j-core-*.jar org/apache/logging/log4j/core/lookup/JndiLookup.class). Log4j 2.15.0 restricts JNDI LDAP lookups to localhost by default. Note that previous mitigations involving configuration such as to set the system property `log4j2.formatMsgNoLookups` to `true` do NOT mitigate this specific vulnerability.",
-    "title": "Incomplete fix for Apache Log4j vulnerability"
-  },
-  {
-    "advisoryKey": "GHSA-cjjf-27cc-pvmv",
-    "affectedPackages": [
-      {
-        "type": "semver",
-        "identifier": "pkg:pypi/pyload-ng",
-        "platform": "PyPI",
-        "versionRanges": [
-          {
-            "fixedVersion": "0.5.0b3.dev91",
-            "introducedVersion": "0",
-            "lastAffectedVersion": null,
-            "primitives": {
-              "evr": null,
-              "hasVendorExtensions": false,
-              "nevra": null,
-              "semVer": {
-                "constraintExpression": null,
-                "exactValue": null,
-                "fixed": "0.5.0b3.dev91",
-                "fixedInclusive": false,
-                "introduced": "0",
-                "introducedInclusive": true,
-                "lastAffected": null,
-                "lastAffectedInclusive": true,
-                "style": "range"
-              },
-              "vendorExtensions": null
-            },
-            "provenance": {
-              "source": "osv",
-              "kind": "range",
-              "value": "pkg:pypi/pyload-ng",
-              "decisionReason": null,
-              "recordedAt": "2025-10-15T14:48:57.995174+00:00",
-              "fieldMask": [
-                "affectedpackages[].versionranges[]"
-              ]
-            },
-            "rangeExpression": null,
-            "rangeKind": "semver"
-          }
-        ],
-        "normalizedVersions": [
-          {
-            "scheme": "semver",
-            "type": "range",
-            "min": "0",
-            "minInclusive": true,
-            "max": "0.5.0b3.dev91",
-            "maxInclusive": false,
-            "value": null,
-            "notes": "osv:PyPI:GHSA-cjjf-27cc-pvmv:pkg:pypi/pyload-ng"
-          }
-        ],
-        "statuses": [],
-        "provenance": [
-          {
-            "source": "osv",
-            "kind": "affected",
-            "value": "pkg:pypi/pyload-ng",
-            "decisionReason": null,
-            "recordedAt": "2025-10-15T14:48:57.995174+00:00",
-            "fieldMask": [
-              "affectedpackages[]"
-            ]
-          }
-        ]
-      }
-    ],
-    "aliases": [
-      "CVE-2025-61773",
-      "GHSA-cjjf-27cc-pvmv"
-    ],
-    "canonicalMetricId": "3.1|CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:N",
-    "credits": [],
-    "cvssMetrics": [
-      {
-        "baseScore": 8.1,
-        "baseSeverity": "high",
-        "provenance": {
-          "source": "osv",
-          "kind": "cvss",
-          "value": "CVSS_V3",
-          "decisionReason": null,
-          "recordedAt": "2025-10-15T14:48:57.995174+00:00",
-          "fieldMask": []
-        },
-        "vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:N",
-        "version": "3.1"
-      }
-    ],
-    "cwes": [
-      {
-        "taxonomy": "cwe",
-        "identifier": "CWE-116",
-        "name": null,
-        "uri": "https://cwe.mitre.org/data/definitions/116.html",
-        "provenance": [
-          {
-            "source": "osv",
-            "kind": "weakness",
-            "value": "CWE-116",
-            "decisionReason": null,
-            "recordedAt": "2025-10-15T14:48:57.995174+00:00",
-            "fieldMask": [
-              "cwes[]"
-            ]
-          }
-        ]
-      },
-      {
-        "taxonomy": "cwe",
-        "identifier": "CWE-74",
-        "name": null,
-        "uri": "https://cwe.mitre.org/data/definitions/74.html",
-        "provenance": [
-          {
-            "source": "osv",
-            "kind": "weakness",
-            "value": "CWE-74",
-            "decisionReason": null,
-            "recordedAt": "2025-10-15T14:48:57.995174+00:00",
-            "fieldMask": [
-              "cwes[]"
-            ]
-          }
-        ]
-      },
-      {
-        "taxonomy": "cwe",
-        "identifier": "CWE-79",
-        "name": null,
-        "uri": "https://cwe.mitre.org/data/definitions/79.html",
-        "provenance": [
-          {
-            "source": "osv",
-            "kind": "weakness",
-            "value": "CWE-79",
-            "decisionReason": null,
-            "recordedAt": "2025-10-15T14:48:57.995174+00:00",
-            "fieldMask": [
-              "cwes[]"
-            ]
-          }
-        ]
-      },
-      {
-        "taxonomy": "cwe",
-        "identifier": "CWE-94",
-        "name": null,
-        "uri": "https://cwe.mitre.org/data/definitions/94.html",
-        "provenance": [
-          {
-            "source": "osv",
-            "kind": "weakness",
-            "value": "CWE-94",
-            "decisionReason": null,
-            "recordedAt": "2025-10-15T14:48:57.995174+00:00",
-            "fieldMask": [
-              "cwes[]"
-            ]
-          }
-        ]
-      }
-    ],
-    "description": "### Summary\npyLoad web interface contained insufficient input validation in both the Captcha script endpoint and the Click'N'Load (CNL) Blueprint. This flaw allowed untrusted user input to be processed unsafely, which could be exploited by an attacker to inject arbitrary content into the web UI or manipulate request handling. The vulnerability could lead to client-side code execution (XSS) or other unintended behaviors when a malicious payload is submitted.\n\nuser-supplied parameters from HTTP requests were not adequately validated or sanitized before being passed into the application logic and response generation. This allowed crafted input to alter the expected execution flow.\n CNL (Click'N'Load) blueprint exposed unsafe handling of untrusted parameters in HTTP requests. The application did not consistently enforce input validation or encoding, making it possible for an attacker to craft malicious requests.\n\n### PoC\n\n1. Run a vulnerable version of pyLoad prior to commit [`f9d27f2`](https://github.com/pyload/pyload/pull/4624).\n2. Start the web UI and access the Captcha or CNL endpoints.\n3. Submit a crafted request containing malicious JavaScript payloads in unvalidated parameters (`/flash/addcrypted2?jk=function(){alert(1)}&crypted=12345`).\n4. Observe that the payload is reflected and executed in the client’s browser, demonstrating cross-site scripting (XSS).\n\nExample request:\n\n```http\nGET /flash/addcrypted2?jk=function(){alert(1)}&crypted=12345 HTTP/1.1\nHost: 127.0.0.1:8000\nContent-Type: application/x-www-form-urlencoded\nContent-Length: 107\n```\n\n### Impact\n\nExploiting this vulnerability allows an attacker to inject and execute arbitrary JavaScript within the browser session of a user accessing the pyLoad Web UI. In practice, this means an attacker could impersonate an administrator, steal authentication cookies or tokens, and perform unauthorized actions on behalf of the victim. Because the affected endpoints are part of the core interface, a successful attack undermines the trust and security of the entire application, potentially leading to a full compromise of the management interface and the data it controls. The impact is particularly severe in cases where the Web UI is exposed over a network without additional access restrictions, as it enables remote attackers to directly target users with crafted links or requests that trigger the vulnerability.",
-    "exploitKnown": false,
-    "language": "en",
-    "modified": "2025-10-09T15:59:13.250015+00:00",
-    "provenance": [
-      {
-        "source": "osv",
-        "kind": "document",
-        "value": "https://osv.dev/vulnerability/GHSA-cjjf-27cc-pvmv",
-        "decisionReason": null,
-        "recordedAt": "2025-10-09T15:19:48+00:00",
-        "fieldMask": [
-          "advisory"
-        ]
-      },
-      {
-        "source": "osv",
-        "kind": "mapping",
-        "value": "GHSA-cjjf-27cc-pvmv",
-        "decisionReason": null,
-        "recordedAt": "2025-10-15T14:48:57.995174+00:00",
-        "fieldMask": [
-          "advisory"
-        ]
-      }
-    ],
-    "published": "2025-10-09T15:19:48+00:00",
-    "references": [
-      {
-        "kind": null,
-        "provenance": {
-          "source": "osv",
-          "kind": "reference",
-          "value": "https://github.com/pyload/pyload",
-          "decisionReason": null,
-          "recordedAt": "2025-10-15T14:48:57.995174+00:00",
-          "fieldMask": [
-            "references[]"
-          ]
-        },
-        "sourceTag": "PACKAGE",
-        "summary": null,
-        "url": "https://github.com/pyload/pyload"
-      },
-      {
-        "kind": null,
-        "provenance": {
-          "source": "osv",
-          "kind": "reference",
-          "value": "https://github.com/pyload/pyload/commit/5823327d0b797161c7195a1f660266d30a69f0ca",
-          "decisionReason": null,
-          "recordedAt": "2025-10-15T14:48:57.995174+00:00",
-          "fieldMask": [
-            "references[]"
-          ]
-        },
-        "sourceTag": "WEB",
-        "summary": null,
-        "url": "https://github.com/pyload/pyload/commit/5823327d0b797161c7195a1f660266d30a69f0ca"
-      },
-      {
-        "kind": null,
-        "provenance": {
-          "source": "osv",
-          "kind": "reference",
-          "value": "https://github.com/pyload/pyload/pull/4624",
-          "decisionReason": null,
-          "recordedAt": "2025-10-15T14:48:57.995174+00:00",
-          "fieldMask": [
-            "references[]"
-          ]
-        },
-        "sourceTag": "WEB",
-        "summary": null,
-        "url": "https://github.com/pyload/pyload/pull/4624"
-      },
-      {
-        "kind": null,
-        "provenance": {
-          "source": "osv",
-          "kind": "reference",
-          "value": "https://github.com/pyload/pyload/security/advisories/GHSA-cjjf-27cc-pvmv",
-          "decisionReason": null,
-          "recordedAt": "2025-10-15T14:48:57.995174+00:00",
-          "fieldMask": [
-            "references[]"
-          ]
-        },
-        "sourceTag": "WEB",
-        "summary": null,
-        "url": "https://github.com/pyload/pyload/security/advisories/GHSA-cjjf-27cc-pvmv"
-      }
-    ],
-    "severity": "high",
-    "summary": "### Summary pyLoad web interface contained insufficient input validation in both the Captcha script endpoint and the Click'N'Load (CNL) Blueprint. This flaw allowed untrusted user input to be processed unsafely, which could be exploited by an attacker to inject arbitrary content into the web UI or manipulate request handling. The vulnerability could lead to client-side code execution (XSS) or other unintended behaviors when a malicious payload is submitted. user-supplied parameters from HTTP requests were not adequately validated or sanitized before being passed into the application logic and response generation. This allowed crafted input to alter the expected execution flow. CNL (Click'N'Load) blueprint exposed unsafe handling of untrusted parameters in HTTP requests. The application did not consistently enforce input validation or encoding, making it possible for an attacker to craft malicious requests. ### PoC 1. Run a vulnerable version of pyLoad prior to commit [`f9d27f2`](https://github.com/pyload/pyload/pull/4624). 2. Start the web UI and access the Captcha or CNL endpoints. 3. Submit a crafted request containing malicious JavaScript payloads in unvalidated parameters (`/flash/addcrypted2?jk=function(){alert(1)}&crypted=12345`). 4. Observe that the payload is reflected and executed in the client’s browser, demonstrating cross-site scripting (XSS). Example request: ```http GET /flash/addcrypted2?jk=function(){alert(1)}&crypted=12345 HTTP/1.1 Host: 127.0.0.1:8000 Content-Type: application/x-www-form-urlencoded Content-Length: 107 ``` ### Impact Exploiting this vulnerability allows an attacker to inject and execute arbitrary JavaScript within the browser session of a user accessing the pyLoad Web UI. In practice, this means an attacker could impersonate an administrator, steal authentication cookies or tokens, and perform unauthorized actions on behalf of the victim. Because the affected endpoints are part of the core interface, a successful attack undermines the trust and security of the entire application, potentially leading to a full compromise of the management interface and the data it controls. The impact is particularly severe in cases where the Web UI is exposed over a network without additional access restrictions, as it enables remote attackers to directly target users with crafted links or requests that trigger the vulnerability.",
-    "title": "pyLoad CNL and captcha handlers allow Code Injection via unsanitized parameters"
-  },
-  {
-    "advisoryKey": "GHSA-wv4w-6qv2-qqfg",
-    "affectedPackages": [
-      {
-        "type": "semver",
-        "identifier": "pkg:pypi/social-auth-app-django",
-        "platform": "PyPI",
-        "versionRanges": [
-          {
-            "fixedVersion": "5.6.0",
-            "introducedVersion": "0",
-            "lastAffectedVersion": null,
-            "primitives": {
-              "evr": null,
-              "hasVendorExtensions": false,
-              "nevra": null,
-              "semVer": {
-                "constraintExpression": null,
-                "exactValue": null,
-                "fixed": "5.6.0",
-                "fixedInclusive": false,
-                "introduced": "0",
-                "introducedInclusive": true,
-                "lastAffected": null,
-                "lastAffectedInclusive": true,
-                "style": "range"
-              },
-              "vendorExtensions": null
-            },
-            "provenance": {
-              "source": "osv",
-              "kind": "range",
-              "value": "pkg:pypi/social-auth-app-django",
-              "decisionReason": null,
-              "recordedAt": "2025-10-15T14:48:57.9927932+00:00",
-              "fieldMask": [
-                "affectedpackages[].versionranges[]"
-              ]
-            },
-            "rangeExpression": null,
-            "rangeKind": "semver"
-          }
-        ],
-        "normalizedVersions": [
-          {
-            "scheme": "semver",
-            "type": "range",
-            "min": "0",
-            "minInclusive": true,
-            "max": "5.6.0",
-            "maxInclusive": false,
-            "value": null,
-            "notes": "osv:PyPI:GHSA-wv4w-6qv2-qqfg:pkg:pypi/social-auth-app-django"
-          }
-        ],
-        "statuses": [],
-        "provenance": [
-          {
-            "source": "osv",
-            "kind": "affected",
-            "value": "pkg:pypi/social-auth-app-django",
-            "decisionReason": null,
-            "recordedAt": "2025-10-15T14:48:57.9927932+00:00",
-            "fieldMask": [
-              "affectedpackages[]"
-            ]
-          }
-        ]
-      }
-    ],
-    "aliases": [
-      "CVE-2025-61783",
-      "GHSA-wv4w-6qv2-qqfg"
-    ],
-    "canonicalMetricId": null,
-    "credits": [],
-    "cvssMetrics": [],
-    "cwes": [
-      {
-        "taxonomy": "cwe",
-        "identifier": "CWE-290",
-        "name": null,
-        "uri": "https://cwe.mitre.org/data/definitions/290.html",
-        "provenance": [
-          {
-            "source": "osv",
-            "kind": "weakness",
-            "value": "CWE-290",
-            "decisionReason": null,
-            "recordedAt": "2025-10-15T14:48:57.9927932+00:00",
-            "fieldMask": [
-              "cwes[]"
-            ]
-          }
-        ]
-      }
-    ],
-    "description": "### Impact\n\nUpon authentication, the user could be associated by e-mail even if the `associate_by_email` pipeline was not included. This could lead to account compromise when a third-party authentication service does not validate provided e-mail addresses or doesn't require unique e-mail addresses.\n\n### Patches\n\n* https://github.com/python-social-auth/social-app-django/pull/803\n\n### Workarounds\n\nReview the authentication service policy on e-mail addresses; many will not allow exploiting this vulnerability.",
-    "exploitKnown": false,
-    "language": "en",
-    "modified": "2025-10-09T17:57:29.916841+00:00",
-    "provenance": [
-      {
-        "source": "osv",
-        "kind": "document",
-        "value": "https://osv.dev/vulnerability/GHSA-wv4w-6qv2-qqfg",
-        "decisionReason": null,
-        "recordedAt": "2025-10-09T17:08:05+00:00",
-        "fieldMask": [
-          "advisory"
-        ]
-      },
-      {
-        "source": "osv",
-        "kind": "mapping",
-        "value": "GHSA-wv4w-6qv2-qqfg",
-        "decisionReason": null,
-        "recordedAt": "2025-10-15T14:48:57.9927932+00:00",
-        "fieldMask": [
-          "advisory"
-        ]
-      }
-    ],
-    "published": "2025-10-09T17:08:05+00:00",
-    "references": [
-      {
-        "kind": null,
-        "provenance": {
-          "source": "osv",
-          "kind": "reference",
-          "value": "https://github.com/python-social-auth/social-app-django",
-          "decisionReason": null,
-          "recordedAt": "2025-10-15T14:48:57.9927932+00:00",
-          "fieldMask": [
-            "references[]"
-          ]
-        },
-        "sourceTag": "PACKAGE",
-        "summary": null,
-        "url": "https://github.com/python-social-auth/social-app-django"
-      },
-      {
-        "kind": null,
-        "provenance": {
-          "source": "osv",
-          "kind": "reference",
-          "value": "https://github.com/python-social-auth/social-app-django/commit/10c80e2ebabeccd4e9c84ad0e16e1db74148ed4c",
-          "decisionReason": null,
-          "recordedAt": "2025-10-15T14:48:57.9927932+00:00",
-          "fieldMask": [
-            "references[]"
-          ]
-        },
-        "sourceTag": "WEB",
-        "summary": null,
-        "url": "https://github.com/python-social-auth/social-app-django/commit/10c80e2ebabeccd4e9c84ad0e16e1db74148ed4c"
-      },
-      {
-        "kind": null,
-        "provenance": {
-          "source": "osv",
-          "kind": "reference",
-          "value": "https://github.com/python-social-auth/social-app-django/issues/220",
-          "decisionReason": null,
-          "recordedAt": "2025-10-15T14:48:57.9927932+00:00",
-          "fieldMask": [
-            "references[]"
-          ]
-        },
-        "sourceTag": "WEB",
-        "summary": null,
-        "url": "https://github.com/python-social-auth/social-app-django/issues/220"
-      },
-      {
-        "kind": null,
-        "provenance": {
-          "source": "osv",
-          "kind": "reference",
-          "value": "https://github.com/python-social-auth/social-app-django/issues/231",
-          "decisionReason": null,
-          "recordedAt": "2025-10-15T14:48:57.9927932+00:00",
-          "fieldMask": [
-            "references[]"
-          ]
-        },
-        "sourceTag": "WEB",
-        "summary": null,
-        "url": "https://github.com/python-social-auth/social-app-django/issues/231"
-      },
-      {
-        "kind": null,
-        "provenance": {
-          "source": "osv",
-          "kind": "reference",
-          "value": "https://github.com/python-social-auth/social-app-django/issues/634",
-          "decisionReason": null,
-          "recordedAt": "2025-10-15T14:48:57.9927932+00:00",
-          "fieldMask": [
-            "references[]"
-          ]
-        },
-        "sourceTag": "WEB",
-        "summary": null,
-        "url": "https://github.com/python-social-auth/social-app-django/issues/634"
-      },
-      {
-        "kind": null,
-        "provenance": {
-          "source": "osv",
-          "kind": "reference",
-          "value": "https://github.com/python-social-auth/social-app-django/pull/803",
-          "decisionReason": null,
-          "recordedAt": "2025-10-15T14:48:57.9927932+00:00",
-          "fieldMask": [
-            "references[]"
-          ]
-        },
-        "sourceTag": "WEB",
-        "summary": null,
-        "url": "https://github.com/python-social-auth/social-app-django/pull/803"
-      },
-      {
-        "kind": null,
-        "provenance": {
-          "source": "osv",
-          "kind": "reference",
-          "value": "https://github.com/python-social-auth/social-app-django/security/advisories/GHSA-wv4w-6qv2-qqfg",
-          "decisionReason": null,
-          "recordedAt": "2025-10-15T14:48:57.9927932+00:00",
-          "fieldMask": [
-            "references[]"
-          ]
-        },
-        "sourceTag": "WEB",
-        "summary": null,
-        "url": "https://github.com/python-social-auth/social-app-django/security/advisories/GHSA-wv4w-6qv2-qqfg"
-      }
-    ],
-    "severity": "medium",
-    "summary": "### Impact Upon authentication, the user could be associated by e-mail even if the `associate_by_email` pipeline was not included. This could lead to account compromise when a third-party authentication service does not validate provided e-mail addresses or doesn't require unique e-mail addresses. ### Patches * https://github.com/python-social-auth/social-app-django/pull/803 ### Workarounds Review the authentication service policy on e-mail addresses; many will not allow exploiting this vulnerability.",
-    "title": "Python Social Auth - Django has unsafe account association"
-  }
-]
\ No newline at end of file
+[
+  {
+    "advisoryKey": "GHSA-77vh-xpmg-72qh",
+    "affectedPackages": [
+      {
+        "type": "semver",
+        "identifier": "pkg:golang/github.com/opencontainers/image-spec",
+        "platform": "Go",
+        "versionRanges": [
+          {
+            "fixedVersion": "1.0.2",
+            "introducedVersion": "0",
+            "lastAffectedVersion": null,
+            "primitives": {
+              "evr": null,
+              "hasVendorExtensions": false,
+              "nevra": null,
+              "semVer": {
+                "constraintExpression": null,
+                "exactValue": null,
+                "fixed": "1.0.2",
+                "fixedInclusive": false,
+                "introduced": "0",
+                "introducedInclusive": true,
+                "lastAffected": null,
+                "lastAffectedInclusive": true,
+                "style": "range"
+              },
+              "vendorExtensions": null
+            },
+            "provenance": {
+              "source": "osv",
+              "kind": "range",
+              "value": "pkg:golang/github.com/opencontainers/image-spec",
+              "decisionReason": null,
+              "recordedAt": "2025-10-15T14:48:57.9970795+00:00",
+              "fieldMask": [
+                "affectedpackages[].versionranges[]"
+              ]
+            },
+            "rangeExpression": null,
+            "rangeKind": "semver"
+          }
+        ],
+        "normalizedVersions": [
+          {
+            "scheme": "semver",
+            "type": "range",
+            "min": "0",
+            "minInclusive": true,
+            "max": "1.0.2",
+            "maxInclusive": false,
+            "value": null,
+            "notes": "osv:Go:GHSA-77vh-xpmg-72qh:pkg:golang/github.com/opencontainers/image-spec"
+          }
+        ],
+        "statuses": [],
+        "provenance": [
+          {
+            "source": "osv",
+            "kind": "affected",
+            "value": "pkg:golang/github.com/opencontainers/image-spec",
+            "decisionReason": null,
+            "recordedAt": "2025-10-15T14:48:57.9970795+00:00",
+            "fieldMask": [
+              "affectedpackages[]"
+            ]
+          }
+        ]
+      }
+    ],
+    "aliases": [
+      "CGA-j36r-723f-8c29",
+      "GHSA-77vh-xpmg-72qh"
+    ],
+    "canonicalMetricId": "3.1|CVSS:3.1/AV:N/AC:H/PR:L/UI:R/S:C/C:N/I:L/A:N",
+    "credits": [],
+    "cvssMetrics": [
+      {
+        "baseScore": 3,
+        "baseSeverity": "low",
+        "provenance": {
+          "source": "osv",
+          "kind": "cvss",
+          "value": "CVSS_V3",
+          "decisionReason": null,
+          "recordedAt": "2025-10-15T14:48:57.9970795+00:00",
+          "fieldMask": []
+        },
+        "vector": "CVSS:3.1/AV:N/AC:H/PR:L/UI:R/S:C/C:N/I:L/A:N",
+        "version": "3.1"
+      }
+    ],
+    "cwes": [
+      {
+        "taxonomy": "cwe",
+        "identifier": "CWE-843",
+        "name": null,
+        "uri": "https://cwe.mitre.org/data/definitions/843.html",
+        "provenance": [
+          {
+            "source": "osv",
+            "kind": "weakness",
+            "value": "CWE-843",
+            "decisionReason": "database_specific.cwe_ids",
+            "recordedAt": "2025-10-15T14:48:57.9970795+00:00",
+            "fieldMask": [
+              "cwes[]"
+            ]
+          }
+        ]
+      }
+    ],
+    "description": "### Impact\nIn the OCI Image Specification version 1.0.1 and prior, manifest and index documents are not self-describing and documents with a single digest could be interpreted as either a manifest or an index.\n\n### Patches\nThe Image Specification will be updated to recommend that both manifest and index documents contain a `mediaType` field to identify the type of document.\nRelease [v1.0.2](https://github.com/opencontainers/image-spec/releases/tag/v1.0.2) includes these updates.\n\n### Workarounds\nSoftware attempting to deserialize an ambiguous document may reject the document if it contains both “manifests” and “layers” fields or “manifests” and “config” fields.\n\n### References\nhttps://github.com/opencontainers/distribution-spec/security/advisories/GHSA-mc8v-mgrf-8f4m\n\n### For more information\nIf you have any questions or comments about this advisory:\n* Open an issue in https://github.com/opencontainers/image-spec\n* Email us at [security@opencontainers.org](mailto:security@opencontainers.org)\n* https://github.com/opencontainers/image-spec/commits/v1.0.2",
+    "exploitKnown": false,
+    "language": "en",
+    "modified": "2021-11-24T19:43:35+00:00",
+    "provenance": [
+      {
+        "source": "osv",
+        "kind": "document",
+        "value": "https://osv.dev/vulnerability/GHSA-77vh-xpmg-72qh",
+        "decisionReason": null,
+        "recordedAt": "2021-11-18T16:02:41+00:00",
+        "fieldMask": [
+          "advisory"
+        ]
+      },
+      {
+        "source": "osv",
+        "kind": "mapping",
+        "value": "GHSA-77vh-xpmg-72qh",
+        "decisionReason": null,
+        "recordedAt": "2025-10-15T14:48:57.9970795+00:00",
+        "fieldMask": [
+          "advisory"
+        ]
+      }
+    ],
+    "published": "2021-11-18T16:02:41+00:00",
+    "references": [
+      {
+        "kind": null,
+        "provenance": {
+          "source": "osv",
+          "kind": "reference",
+          "value": "https://github.com/opencontainers/distribution-spec/security/advisories/GHSA-mc8v-mgrf-8f4m",
+          "decisionReason": null,
+          "recordedAt": "2025-10-15T14:48:57.9970795+00:00",
+          "fieldMask": [
+            "references[]"
+          ]
+        },
+        "sourceTag": "WEB",
+        "summary": null,
+        "url": "https://github.com/opencontainers/distribution-spec/security/advisories/GHSA-mc8v-mgrf-8f4m"
+      },
+      {
+        "kind": null,
+        "provenance": {
+          "source": "osv",
+          "kind": "reference",
+          "value": "https://github.com/opencontainers/image-spec",
+          "decisionReason": null,
+          "recordedAt": "2025-10-15T14:48:57.9970795+00:00",
+          "fieldMask": [
+            "references[]"
+          ]
+        },
+        "sourceTag": "PACKAGE",
+        "summary": null,
+        "url": "https://github.com/opencontainers/image-spec"
+      },
+      {
+        "kind": null,
+        "provenance": {
+          "source": "osv",
+          "kind": "reference",
+          "value": "https://github.com/opencontainers/image-spec/commit/693428a734f5bab1a84bd2f990d92ef1111cd60c",
+          "decisionReason": null,
+          "recordedAt": "2025-10-15T14:48:57.9970795+00:00",
+          "fieldMask": [
+            "references[]"
+          ]
+        },
+        "sourceTag": "WEB",
+        "summary": null,
+        "url": "https://github.com/opencontainers/image-spec/commit/693428a734f5bab1a84bd2f990d92ef1111cd60c"
+      },
+      {
+        "kind": null,
+        "provenance": {
+          "source": "osv",
+          "kind": "reference",
+          "value": "https://github.com/opencontainers/image-spec/releases/tag/v1.0.2",
+          "decisionReason": null,
+          "recordedAt": "2025-10-15T14:48:57.9970795+00:00",
+          "fieldMask": [
+            "references[]"
+          ]
+        },
+        "sourceTag": "WEB",
+        "summary": null,
+        "url": "https://github.com/opencontainers/image-spec/releases/tag/v1.0.2"
+      },
+      {
+        "kind": null,
+        "provenance": {
+          "source": "osv",
+          "kind": "reference",
+          "value": "https://github.com/opencontainers/image-spec/security/advisories/GHSA-77vh-xpmg-72qh",
+          "decisionReason": null,
+          "recordedAt": "2025-10-15T14:48:57.9970795+00:00",
+          "fieldMask": [
+            "references[]"
+          ]
+        },
+        "sourceTag": "WEB",
+        "summary": null,
+        "url": "https://github.com/opencontainers/image-spec/security/advisories/GHSA-77vh-xpmg-72qh"
+      }
+    ],
+    "severity": "low",
+    "summary": "### Impact In the OCI Image Specification version 1.0.1 and prior, manifest and index documents are not self-describing and documents with a single digest could be interpreted as either a manifest or an index. ### Patches The Image Specification will be updated to recommend that both manifest and index documents contain a `mediaType` field to identify the type of document. Release [v1.0.2](https://github.com/opencontainers/image-spec/releases/tag/v1.0.2) includes these updates. ### Workarounds Software attempting to deserialize an ambiguous document may reject the document if it contains both “manifests” and “layers” fields or “manifests” and “config” fields. ### References https://github.com/opencontainers/distribution-spec/security/advisories/GHSA-mc8v-mgrf-8f4m ### For more information If you have any questions or comments about this advisory: * Open an issue in https://github.com/opencontainers/image-spec * Email us at [security@opencontainers.org](mailto:security@opencontainers.org) * https://github.com/opencontainers/image-spec/commits/v1.0.2",
+    "title": "Clarify `mediaType` handling"
+  },
+  {
+    "advisoryKey": "GHSA-7rjr-3q55-vv33",
+    "affectedPackages": [
+      {
+        "type": "semver",
+        "identifier": "pkg:maven/org.apache.logging.log4j/log4j-core",
+        "platform": "Maven",
+        "versionRanges": [
+          {
+            "fixedVersion": "2.16.0",
+            "introducedVersion": "2.13.0",
+            "lastAffectedVersion": null,
+            "primitives": {
+              "evr": null,
+              "hasVendorExtensions": false,
+              "nevra": null,
+              "semVer": {
+                "constraintExpression": null,
+                "exactValue": null,
+                "fixed": "2.16.0",
+                "fixedInclusive": false,
+                "introduced": "2.13.0",
+                "introducedInclusive": true,
+                "lastAffected": null,
+                "lastAffectedInclusive": true,
+                "style": "range"
+              },
+              "vendorExtensions": null
+            },
+            "provenance": {
+              "source": "osv",
+              "kind": "range",
+              "value": "pkg:maven/org.apache.logging.log4j/log4j-core",
+              "decisionReason": null,
+              "recordedAt": "2025-10-15T14:48:57.9980643+00:00",
+              "fieldMask": [
+                "affectedpackages[].versionranges[]"
+              ]
+            },
+            "rangeExpression": null,
+            "rangeKind": "semver"
+          }
+        ],
+        "normalizedVersions": [
+          {
+            "scheme": "semver",
+            "type": "range",
+            "min": "2.13.0",
+            "minInclusive": true,
+            "max": "2.16.0",
+            "maxInclusive": false,
+            "value": null,
+            "notes": "osv:Maven:GHSA-7rjr-3q55-vv33:pkg:maven/org.apache.logging.log4j/log4j-core"
+          }
+        ],
+        "statuses": [],
+        "provenance": [
+          {
+            "source": "osv",
+            "kind": "affected",
+            "value": "pkg:maven/org.apache.logging.log4j/log4j-core",
+            "decisionReason": null,
+            "recordedAt": "2025-10-15T14:48:57.9980643+00:00",
+            "fieldMask": [
+              "affectedpackages[]"
+            ]
+          }
+        ]
+      },
+      {
+        "type": "semver",
+        "identifier": "pkg:maven/org.apache.logging.log4j/log4j-core",
+        "platform": "Maven",
+        "versionRanges": [
+          {
+            "fixedVersion": "2.12.2",
+            "introducedVersion": "0",
+            "lastAffectedVersion": null,
+            "primitives": {
+              "evr": null,
+              "hasVendorExtensions": false,
+              "nevra": null,
+              "semVer": {
+                "constraintExpression": null,
+                "exactValue": null,
+                "fixed": "2.12.2",
+                "fixedInclusive": false,
+                "introduced": "0",
+                "introducedInclusive": true,
+                "lastAffected": null,
+                "lastAffectedInclusive": true,
+                "style": "range"
+              },
+              "vendorExtensions": null
+            },
+            "provenance": {
+              "source": "osv",
+              "kind": "range",
+              "value": "pkg:maven/org.apache.logging.log4j/log4j-core",
+              "decisionReason": null,
+              "recordedAt": "2025-10-15T14:48:57.9980643+00:00",
+              "fieldMask": [
+                "affectedpackages[].versionranges[]"
+              ]
+            },
+            "rangeExpression": null,
+            "rangeKind": "semver"
+          }
+        ],
+        "normalizedVersions": [
+          {
+            "scheme": "semver",
+            "type": "range",
+            "min": "0",
+            "minInclusive": true,
+            "max": "2.12.2",
+            "maxInclusive": false,
+            "value": null,
+            "notes": "osv:Maven:GHSA-7rjr-3q55-vv33:pkg:maven/org.apache.logging.log4j/log4j-core"
+          }
+        ],
+        "statuses": [],
+        "provenance": [
+          {
+            "source": "osv",
+            "kind": "affected",
+            "value": "pkg:maven/org.apache.logging.log4j/log4j-core",
+            "decisionReason": null,
+            "recordedAt": "2025-10-15T14:48:57.9980643+00:00",
+            "fieldMask": [
+              "affectedpackages[]"
+            ]
+          }
+        ]
+      },
+      {
+        "type": "semver",
+        "identifier": "pkg:maven/org.ops4j.pax.logging/pax-logging-log4j2",
+        "platform": "Maven",
+        "versionRanges": [
+          {
+            "fixedVersion": "1.9.2",
+            "introducedVersion": "1.8.0",
+            "lastAffectedVersion": null,
+            "primitives": {
+              "evr": null,
+              "hasVendorExtensions": false,
+              "nevra": null,
+              "semVer": {
+                "constraintExpression": null,
+                "exactValue": null,
+                "fixed": "1.9.2",
+                "fixedInclusive": false,
+                "introduced": "1.8.0",
+                "introducedInclusive": true,
+                "lastAffected": null,
+                "lastAffectedInclusive": true,
+                "style": "range"
+              },
+              "vendorExtensions": null
+            },
+            "provenance": {
+              "source": "osv",
+              "kind": "range",
+              "value": "pkg:maven/org.ops4j.pax.logging/pax-logging-log4j2",
+              "decisionReason": null,
+              "recordedAt": "2025-10-15T14:48:57.9980643+00:00",
+              "fieldMask": [
+                "affectedpackages[].versionranges[]"
+              ]
+            },
+            "rangeExpression": null,
+            "rangeKind": "semver"
+          }
+        ],
+        "normalizedVersions": [
+          {
+            "scheme": "semver",
+            "type": "range",
+            "min": "1.8.0",
+            "minInclusive": true,
+            "max": "1.9.2",
+            "maxInclusive": false,
+            "value": null,
+            "notes": "osv:Maven:GHSA-7rjr-3q55-vv33:pkg:maven/org.ops4j.pax.logging/pax-logging-log4j2"
+          }
+        ],
+        "statuses": [],
+        "provenance": [
+          {
+            "source": "osv",
+            "kind": "affected",
+            "value": "pkg:maven/org.ops4j.pax.logging/pax-logging-log4j2",
+            "decisionReason": null,
+            "recordedAt": "2025-10-15T14:48:57.9980643+00:00",
+            "fieldMask": [
+              "affectedpackages[]"
+            ]
+          }
+        ]
+      },
+      {
+        "type": "semver",
+        "identifier": "pkg:maven/org.ops4j.pax.logging/pax-logging-log4j2",
+        "platform": "Maven",
+        "versionRanges": [
+          {
+            "fixedVersion": "1.10.8",
+            "introducedVersion": "1.10.0",
+            "lastAffectedVersion": null,
+            "primitives": {
+              "evr": null,
+              "hasVendorExtensions": false,
+              "nevra": null,
+              "semVer": {
+                "constraintExpression": null,
+                "exactValue": null,
+                "fixed": "1.10.8",
+                "fixedInclusive": false,
+                "introduced": "1.10.0",
+                "introducedInclusive": true,
+                "lastAffected": null,
+                "lastAffectedInclusive": true,
+                "style": "range"
+              },
+              "vendorExtensions": null
+            },
+            "provenance": {
+              "source": "osv",
+              "kind": "range",
+              "value": "pkg:maven/org.ops4j.pax.logging/pax-logging-log4j2",
+              "decisionReason": null,
+              "recordedAt": "2025-10-15T14:48:57.9980643+00:00",
+              "fieldMask": [
+                "affectedpackages[].versionranges[]"
+              ]
+            },
+            "rangeExpression": null,
+            "rangeKind": "semver"
+          }
+        ],
+        "normalizedVersions": [
+          {
+            "scheme": "semver",
+            "type": "range",
+            "min": "1.10.0",
+            "minInclusive": true,
+            "max": "1.10.8",
+            "maxInclusive": false,
+            "value": null,
+            "notes": "osv:Maven:GHSA-7rjr-3q55-vv33:pkg:maven/org.ops4j.pax.logging/pax-logging-log4j2"
+          }
+        ],
+        "statuses": [],
+        "provenance": [
+          {
+            "source": "osv",
+            "kind": "affected",
+            "value": "pkg:maven/org.ops4j.pax.logging/pax-logging-log4j2",
+            "decisionReason": null,
+            "recordedAt": "2025-10-15T14:48:57.9980643+00:00",
+            "fieldMask": [
+              "affectedpackages[]"
+            ]
+          }
+        ]
+      },
+      {
+        "type": "semver",
+        "identifier": "pkg:maven/org.ops4j.pax.logging/pax-logging-log4j2",
+        "platform": "Maven",
+        "versionRanges": [
+          {
+            "fixedVersion": "1.11.11",
+            "introducedVersion": "1.11.0",
+            "lastAffectedVersion": null,
+            "primitives": {
+              "evr": null,
+              "hasVendorExtensions": false,
+              "nevra": null,
+              "semVer": {
+                "constraintExpression": null,
+                "exactValue": null,
+                "fixed": "1.11.11",
+                "fixedInclusive": false,
+                "introduced": "1.11.0",
+                "introducedInclusive": true,
+                "lastAffected": null,
+                "lastAffectedInclusive": true,
+                "style": "range"
+              },
+              "vendorExtensions": null
+            },
+            "provenance": {
+              "source": "osv",
+              "kind": "range",
+              "value": "pkg:maven/org.ops4j.pax.logging/pax-logging-log4j2",
+              "decisionReason": null,
+              "recordedAt": "2025-10-15T14:48:57.9980643+00:00",
+              "fieldMask": [
+                "affectedpackages[].versionranges[]"
+              ]
+            },
+            "rangeExpression": null,
+            "rangeKind": "semver"
+          }
+        ],
+        "normalizedVersions": [
+          {
+            "scheme": "semver",
+            "type": "range",
+            "min": "1.11.0",
+            "minInclusive": true,
+            "max": "1.11.11",
+            "maxInclusive": false,
+            "value": null,
+            "notes": "osv:Maven:GHSA-7rjr-3q55-vv33:pkg:maven/org.ops4j.pax.logging/pax-logging-log4j2"
+          }
+        ],
+        "statuses": [],
+        "provenance": [
+          {
+            "source": "osv",
+            "kind": "affected",
+            "value": "pkg:maven/org.ops4j.pax.logging/pax-logging-log4j2",
+            "decisionReason": null,
+            "recordedAt": "2025-10-15T14:48:57.9980643+00:00",
+            "fieldMask": [
+              "affectedpackages[]"
+            ]
+          }
+        ]
+      },
+      {
+        "type": "semver",
+        "identifier": "pkg:maven/org.ops4j.pax.logging/pax-logging-log4j2",
+        "platform": "Maven",
+        "versionRanges": [
+          {
+            "fixedVersion": "2.0.12",
+            "introducedVersion": "2.0.0",
+            "lastAffectedVersion": null,
+            "primitives": {
+              "evr": null,
+              "hasVendorExtensions": false,
+              "nevra": null,
+              "semVer": {
+                "constraintExpression": null,
+                "exactValue": null,
+                "fixed": "2.0.12",
+                "fixedInclusive": false,
+                "introduced": "2.0.0",
+                "introducedInclusive": true,
+                "lastAffected": null,
+                "lastAffectedInclusive": true,
+                "style": "range"
+              },
+              "vendorExtensions": null
+            },
+            "provenance": {
+              "source": "osv",
+              "kind": "range",
+              "value": "pkg:maven/org.ops4j.pax.logging/pax-logging-log4j2",
+              "decisionReason": null,
+              "recordedAt": "2025-10-15T14:48:57.9980643+00:00",
+              "fieldMask": [
+                "affectedpackages[].versionranges[]"
+              ]
+            },
+            "rangeExpression": null,
+            "rangeKind": "semver"
+          }
+        ],
+        "normalizedVersions": [
+          {
+            "scheme": "semver",
+            "type": "range",
+            "min": "2.0.0",
+            "minInclusive": true,
+            "max": "2.0.12",
+            "maxInclusive": false,
+            "value": null,
+            "notes": "osv:Maven:GHSA-7rjr-3q55-vv33:pkg:maven/org.ops4j.pax.logging/pax-logging-log4j2"
+          }
+        ],
+        "statuses": [],
+        "provenance": [
+          {
+            "source": "osv",
+            "kind": "affected",
+            "value": "pkg:maven/org.ops4j.pax.logging/pax-logging-log4j2",
+            "decisionReason": null,
+            "recordedAt": "2025-10-15T14:48:57.9980643+00:00",
+            "fieldMask": [
+              "affectedpackages[]"
+            ]
+          }
+        ]
+      }
+    ],
+    "aliases": [
+      "CVE-2021-45046",
+      "GHSA-7rjr-3q55-vv33"
+    ],
+    "canonicalMetricId": "3.1|CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:C/C:H/I:H/A:H",
+    "credits": [],
+    "cvssMetrics": [
+      {
+        "baseScore": 9,
+        "baseSeverity": "critical",
+        "provenance": {
+          "source": "osv",
+          "kind": "cvss",
+          "value": "CVSS_V3",
+          "decisionReason": null,
+          "recordedAt": "2025-10-15T14:48:57.9980643+00:00",
+          "fieldMask": []
+        },
+        "vector": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:C/C:H/I:H/A:H",
+        "version": "3.1"
+      }
+    ],
+    "cwes": [
+      {
+        "taxonomy": "cwe",
+        "identifier": "CWE-502",
+        "name": null,
+        "uri": "https://cwe.mitre.org/data/definitions/502.html",
+        "provenance": [
+          {
+            "source": "osv",
+            "kind": "weakness",
+            "value": "CWE-502",
+            "decisionReason": "database_specific.cwe_ids",
+            "recordedAt": "2025-10-15T14:48:57.9980643+00:00",
+            "fieldMask": [
+              "cwes[]"
+            ]
+          }
+        ]
+      },
+      {
+        "taxonomy": "cwe",
+        "identifier": "CWE-917",
+        "name": null,
+        "uri": "https://cwe.mitre.org/data/definitions/917.html",
+        "provenance": [
+          {
+            "source": "osv",
+            "kind": "weakness",
+            "value": "CWE-917",
+            "decisionReason": "database_specific.cwe_ids",
+            "recordedAt": "2025-10-15T14:48:57.9980643+00:00",
+            "fieldMask": [
+              "cwes[]"
+            ]
+          }
+        ]
+      }
+    ],
+    "description": "# Impact\n\nThe fix to address [CVE-2021-44228](https://nvd.nist.gov/vuln/detail/CVE-2021-44228) in Apache Log4j 2.15.0 was incomplete in certain non-default configurations. This could allow attackers with control over Thread Context Map (MDC) input data when the logging configuration uses a non-default Pattern Layout with either a Context Lookup (for example, $${ctx:loginId}) or a Thread Context Map pattern (%X, %mdc, or %MDC) to craft malicious input data using a JNDI Lookup pattern resulting in a remote code execution (RCE) attack. \n\n## Affected packages\nOnly the `org.apache.logging.log4j:log4j-core` package is directly affected by this vulnerability. The `org.apache.logging.log4j:log4j-api` should be kept at the same version as the `org.apache.logging.log4j:log4j-core` package to ensure compatability if in use.\n\n# Mitigation\n\nLog4j 2.16.0 fixes this issue by removing support for message lookup patterns and disabling JNDI functionality by default. This issue can be mitigated in prior releases (< 2.16.0) by removing the JndiLookup class from the classpath (example: zip -q -d log4j-core-*.jar org/apache/logging/log4j/core/lookup/JndiLookup.class).\n\nLog4j 2.15.0 restricts JNDI LDAP lookups to localhost by default. Note that previous mitigations involving configuration such as to set the system property `log4j2.formatMsgNoLookups` to `true` do NOT mitigate this specific vulnerability.",
+    "exploitKnown": false,
+    "language": "en",
+    "modified": "2025-05-09T13:13:16.169374+00:00",
+    "provenance": [
+      {
+        "source": "osv",
+        "kind": "document",
+        "value": "https://osv.dev/vulnerability/GHSA-7rjr-3q55-vv33",
+        "decisionReason": null,
+        "recordedAt": "2021-12-14T18:01:28+00:00",
+        "fieldMask": [
+          "advisory"
+        ]
+      },
+      {
+        "source": "osv",
+        "kind": "mapping",
+        "value": "GHSA-7rjr-3q55-vv33",
+        "decisionReason": null,
+        "recordedAt": "2025-10-15T14:48:57.9980643+00:00",
+        "fieldMask": [
+          "advisory"
+        ]
+      }
+    ],
+    "published": "2021-12-14T18:01:28+00:00",
+    "references": [
+      {
+        "kind": null,
+        "provenance": {
+          "source": "osv",
+          "kind": "reference",
+          "value": "http://www.openwall.com/lists/oss-security/2021/12/14/4",
+          "decisionReason": null,
+          "recordedAt": "2025-10-15T14:48:57.9980643+00:00",
+          "fieldMask": [
+            "references[]"
+          ]
+        },
+        "sourceTag": "WEB",
+        "summary": null,
+        "url": "http://www.openwall.com/lists/oss-security/2021/12/14/4"
+      },
+      {
+        "kind": null,
+        "provenance": {
+          "source": "osv",
+          "kind": "reference",
+          "value": "http://www.openwall.com/lists/oss-security/2021/12/15/3",
+          "decisionReason": null,
+          "recordedAt": "2025-10-15T14:48:57.9980643+00:00",
+          "fieldMask": [
+            "references[]"
+          ]
+        },
+        "sourceTag": "WEB",
+        "summary": null,
+        "url": "http://www.openwall.com/lists/oss-security/2021/12/15/3"
+      },
+      {
+        "kind": null,
+        "provenance": {
+          "source": "osv",
+          "kind": "reference",
+          "value": "http://www.openwall.com/lists/oss-security/2021/12/18/1",
+          "decisionReason": null,
+          "recordedAt": "2025-10-15T14:48:57.9980643+00:00",
+          "fieldMask": [
+            "references[]"
+          ]
+        },
+        "sourceTag": "WEB",
+        "summary": null,
+        "url": "http://www.openwall.com/lists/oss-security/2021/12/18/1"
+      },
+      {
+        "kind": null,
+        "provenance": {
+          "source": "osv",
+          "kind": "reference",
+          "value": "https://cert-portal.siemens.com/productcert/pdf/ssa-397453.pdf",
+          "decisionReason": null,
+          "recordedAt": "2025-10-15T14:48:57.9980643+00:00",
+          "fieldMask": [
+            "references[]"
+          ]
+        },
+        "sourceTag": "WEB",
+        "summary": null,
+        "url": "https://cert-portal.siemens.com/productcert/pdf/ssa-397453.pdf"
+      },
+      {
+        "kind": null,
+        "provenance": {
+          "source": "osv",
+          "kind": "reference",
+          "value": "https://cert-portal.siemens.com/productcert/pdf/ssa-479842.pdf",
+          "decisionReason": null,
+          "recordedAt": "2025-10-15T14:48:57.9980643+00:00",
+          "fieldMask": [
+            "references[]"
+          ]
+        },
+        "sourceTag": "WEB",
+        "summary": null,
+        "url": "https://cert-portal.siemens.com/productcert/pdf/ssa-479842.pdf"
+      },
+      {
+        "kind": null,
+        "provenance": {
+          "source": "osv",
+          "kind": "reference",
+          "value": "https://cert-portal.siemens.com/productcert/pdf/ssa-661247.pdf",
+          "decisionReason": null,
+          "recordedAt": "2025-10-15T14:48:57.9980643+00:00",
+          "fieldMask": [
+            "references[]"
+          ]
+        },
+        "sourceTag": "WEB",
+        "summary": null,
+        "url": "https://cert-portal.siemens.com/productcert/pdf/ssa-661247.pdf"
+      },
+      {
+        "kind": null,
+        "provenance": {
+          "source": "osv",
+          "kind": "reference",
+          "value": "https://cert-portal.siemens.com/productcert/pdf/ssa-714170.pdf",
+          "decisionReason": null,
+          "recordedAt": "2025-10-15T14:48:57.9980643+00:00",
+          "fieldMask": [
+            "references[]"
+          ]
+        },
+        "sourceTag": "WEB",
+        "summary": null,
+        "url": "https://cert-portal.siemens.com/productcert/pdf/ssa-714170.pdf"
+      },
+      {
+        "kind": "advisory",
+        "provenance": {
+          "source": "osv",
+          "kind": "reference",
+          "value": "https://github.com/advisories/GHSA-jfh8-c2jp-5v3q",
+          "decisionReason": null,
+          "recordedAt": "2025-10-15T14:48:57.9980643+00:00",
+          "fieldMask": [
+            "references[]"
+          ]
+        },
+        "sourceTag": "ADVISORY",
+        "summary": null,
+        "url": "https://github.com/advisories/GHSA-jfh8-c2jp-5v3q"
+      },
+      {
+        "kind": null,
+        "provenance": {
+          "source": "osv",
+          "kind": "reference",
+          "value": "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/EOKPQGV24RRBBI4TBZUDQMM4MEH7MXCY",
+          "decisionReason": null,
+          "recordedAt": "2025-10-15T14:48:57.9980643+00:00",
+          "fieldMask": [
+            "references[]"
+          ]
+        },
+        "sourceTag": "WEB",
+        "summary": null,
+        "url": "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/EOKPQGV24RRBBI4TBZUDQMM4MEH7MXCY"
+      },
+      {
+        "kind": null,
+        "provenance": {
+          "source": "osv",
+          "kind": "reference",
+          "value": "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/SIG7FZULMNK2XF6FZRU4VWYDQXNMUGAJ",
+          "decisionReason": null,
+          "recordedAt": "2025-10-15T14:48:57.9980643+00:00",
+          "fieldMask": [
+            "references[]"
+          ]
+        },
+        "sourceTag": "WEB",
+        "summary": null,
+        "url": "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/SIG7FZULMNK2XF6FZRU4VWYDQXNMUGAJ"
+      },
+      {
+        "kind": null,
+        "provenance": {
+          "source": "osv",
+          "kind": "reference",
+          "value": "https://logging.apache.org/log4j/2.x/security.html",
+          "decisionReason": null,
+          "recordedAt": "2025-10-15T14:48:57.9980643+00:00",
+          "fieldMask": [
+            "references[]"
+          ]
+        },
+        "sourceTag": "WEB",
+        "summary": null,
+        "url": "https://logging.apache.org/log4j/2.x/security.html"
+      },
+      {
+        "kind": "advisory",
+        "provenance": {
+          "source": "osv",
+          "kind": "reference",
+          "value": "https://nvd.nist.gov/vuln/detail/CVE-2021-45046",
+          "decisionReason": null,
+          "recordedAt": "2025-10-15T14:48:57.9980643+00:00",
+          "fieldMask": [
+            "references[]"
+          ]
+        },
+        "sourceTag": "ADVISORY",
+        "summary": null,
+        "url": "https://nvd.nist.gov/vuln/detail/CVE-2021-45046"
+      },
+      {
+        "kind": null,
+        "provenance": {
+          "source": "osv",
+          "kind": "reference",
+          "value": "https://psirt.global.sonicwall.com/vuln-detail/SNWLID-2021-0032",
+          "decisionReason": null,
+          "recordedAt": "2025-10-15T14:48:57.9980643+00:00",
+          "fieldMask": [
+            "references[]"
+          ]
+        },
+        "sourceTag": "WEB",
+        "summary": null,
+        "url": "https://psirt.global.sonicwall.com/vuln-detail/SNWLID-2021-0032"
+      },
+      {
+        "kind": null,
+        "provenance": {
+          "source": "osv",
+          "kind": "reference",
+          "value": "https://sec.cloudapps.cisco.com/security/center/content/CiscoSecurityAdvisory/cisco-sa-apache-log4j-qRuKNEbd",
+          "decisionReason": null,
+          "recordedAt": "2025-10-15T14:48:57.9980643+00:00",
+          "fieldMask": [
+            "references[]"
+          ]
+        },
+        "sourceTag": "WEB",
+        "summary": null,
+        "url": "https://sec.cloudapps.cisco.com/security/center/content/CiscoSecurityAdvisory/cisco-sa-apache-log4j-qRuKNEbd"
+      },
+      {
+        "kind": null,
+        "provenance": {
+          "source": "osv",
+          "kind": "reference",
+          "value": "https://security.gentoo.org/glsa/202310-16",
+          "decisionReason": null,
+          "recordedAt": "2025-10-15T14:48:57.9980643+00:00",
+          "fieldMask": [
+            "references[]"
+          ]
+        },
+        "sourceTag": "WEB",
+        "summary": null,
+        "url": "https://security.gentoo.org/glsa/202310-16"
+      },
+      {
+        "kind": null,
+        "provenance": {
+          "source": "osv",
+          "kind": "reference",
+          "value": "https://www.cve.org/CVERecord?id=CVE-2021-44228",
+          "decisionReason": null,
+          "recordedAt": "2025-10-15T14:48:57.9980643+00:00",
+          "fieldMask": [
+            "references[]"
+          ]
+        },
+        "sourceTag": "WEB",
+        "summary": null,
+        "url": "https://www.cve.org/CVERecord?id=CVE-2021-44228"
+      },
+      {
+        "kind": null,
+        "provenance": {
+          "source": "osv",
+          "kind": "reference",
+          "value": "https://www.debian.org/security/2021/dsa-5022",
+          "decisionReason": null,
+          "recordedAt": "2025-10-15T14:48:57.9980643+00:00",
+          "fieldMask": [
+            "references[]"
+          ]
+        },
+        "sourceTag": "WEB",
+        "summary": null,
+        "url": "https://www.debian.org/security/2021/dsa-5022"
+      },
+      {
+        "kind": null,
+        "provenance": {
+          "source": "osv",
+          "kind": "reference",
+          "value": "https://www.intel.com/content/www/us/en/security-center/advisory/intel-sa-00646.html",
+          "decisionReason": null,
+          "recordedAt": "2025-10-15T14:48:57.9980643+00:00",
+          "fieldMask": [
+            "references[]"
+          ]
+        },
+        "sourceTag": "WEB",
+        "summary": null,
+        "url": "https://www.intel.com/content/www/us/en/security-center/advisory/intel-sa-00646.html"
+      },
+      {
+        "kind": null,
+        "provenance": {
+          "source": "osv",
+          "kind": "reference",
+          "value": "https://www.kb.cert.org/vuls/id/930724",
+          "decisionReason": null,
+          "recordedAt": "2025-10-15T14:48:57.9980643+00:00",
+          "fieldMask": [
+            "references[]"
+          ]
+        },
+        "sourceTag": "WEB",
+        "summary": null,
+        "url": "https://www.kb.cert.org/vuls/id/930724"
+      },
+      {
+        "kind": null,
+        "provenance": {
+          "source": "osv",
+          "kind": "reference",
+          "value": "https://www.openwall.com/lists/oss-security/2021/12/14/4",
+          "decisionReason": null,
+          "recordedAt": "2025-10-15T14:48:57.9980643+00:00",
+          "fieldMask": [
+            "references[]"
+          ]
+        },
+        "sourceTag": "WEB",
+        "summary": null,
+        "url": "https://www.openwall.com/lists/oss-security/2021/12/14/4"
+      },
+      {
+        "kind": null,
+        "provenance": {
+          "source": "osv",
+          "kind": "reference",
+          "value": "https://www.oracle.com/security-alerts/alert-cve-2021-44228.html",
+          "decisionReason": null,
+          "recordedAt": "2025-10-15T14:48:57.9980643+00:00",
+          "fieldMask": [
+            "references[]"
+          ]
+        },
+        "sourceTag": "WEB",
+        "summary": null,
+        "url": "https://www.oracle.com/security-alerts/alert-cve-2021-44228.html"
+      },
+      {
+        "kind": null,
+        "provenance": {
+          "source": "osv",
+          "kind": "reference",
+          "value": "https://www.oracle.com/security-alerts/cpuapr2022.html",
+          "decisionReason": null,
+          "recordedAt": "2025-10-15T14:48:57.9980643+00:00",
+          "fieldMask": [
+            "references[]"
+          ]
+        },
+        "sourceTag": "WEB",
+        "summary": null,
+        "url": "https://www.oracle.com/security-alerts/cpuapr2022.html"
+      },
+      {
+        "kind": null,
+        "provenance": {
+          "source": "osv",
+          "kind": "reference",
+          "value": "https://www.oracle.com/security-alerts/cpujan2022.html",
+          "decisionReason": null,
+          "recordedAt": "2025-10-15T14:48:57.9980643+00:00",
+          "fieldMask": [
+            "references[]"
+          ]
+        },
+        "sourceTag": "WEB",
+        "summary": null,
+        "url": "https://www.oracle.com/security-alerts/cpujan2022.html"
+      },
+      {
+        "kind": null,
+        "provenance": {
+          "source": "osv",
+          "kind": "reference",
+          "value": "https://www.oracle.com/security-alerts/cpujul2022.html",
+          "decisionReason": null,
+          "recordedAt": "2025-10-15T14:48:57.9980643+00:00",
+          "fieldMask": [
+            "references[]"
+          ]
+        },
+        "sourceTag": "WEB",
+        "summary": null,
+        "url": "https://www.oracle.com/security-alerts/cpujul2022.html"
+      }
+    ],
+    "severity": "critical",
+    "summary": "# Impact The fix to address [CVE-2021-44228](https://nvd.nist.gov/vuln/detail/CVE-2021-44228) in Apache Log4j 2.15.0 was incomplete in certain non-default configurations. This could allow attackers with control over Thread Context Map (MDC) input data when the logging configuration uses a non-default Pattern Layout with either a Context Lookup (for example, $${ctx:loginId}) or a Thread Context Map pattern (%X, %mdc, or %MDC) to craft malicious input data using a JNDI Lookup pattern resulting in a remote code execution (RCE) attack. ## Affected packages Only the `org.apache.logging.log4j:log4j-core` package is directly affected by this vulnerability. The `org.apache.logging.log4j:log4j-api` should be kept at the same version as the `org.apache.logging.log4j:log4j-core` package to ensure compatability if in use. # Mitigation Log4j 2.16.0 fixes this issue by removing support for message lookup patterns and disabling JNDI functionality by default. This issue can be mitigated in prior releases (< 2.16.0) by removing the JndiLookup class from the classpath (example: zip -q -d log4j-core-*.jar org/apache/logging/log4j/core/lookup/JndiLookup.class). Log4j 2.15.0 restricts JNDI LDAP lookups to localhost by default. Note that previous mitigations involving configuration such as to set the system property `log4j2.formatMsgNoLookups` to `true` do NOT mitigate this specific vulnerability.",
+    "title": "Incomplete fix for Apache Log4j vulnerability"
+  },
+  {
+    "advisoryKey": "GHSA-cjjf-27cc-pvmv",
+    "affectedPackages": [
+      {
+        "type": "semver",
+        "identifier": "pkg:pypi/pyload-ng",
+        "platform": "PyPI",
+        "versionRanges": [
+          {
+            "fixedVersion": "0.5.0b3.dev91",
+            "introducedVersion": "0",
+            "lastAffectedVersion": null,
+            "primitives": {
+              "evr": null,
+              "hasVendorExtensions": false,
+              "nevra": null,
+              "semVer": {
+                "constraintExpression": null,
+                "exactValue": null,
+                "fixed": "0.5.0b3.dev91",
+                "fixedInclusive": false,
+                "introduced": "0",
+                "introducedInclusive": true,
+                "lastAffected": null,
+                "lastAffectedInclusive": true,
+                "style": "range"
+              },
+              "vendorExtensions": null
+            },
+            "provenance": {
+              "source": "osv",
+              "kind": "range",
+              "value": "pkg:pypi/pyload-ng",
+              "decisionReason": null,
+              "recordedAt": "2025-10-15T14:48:57.995174+00:00",
+              "fieldMask": [
+                "affectedpackages[].versionranges[]"
+              ]
+            },
+            "rangeExpression": null,
+            "rangeKind": "semver"
+          }
+        ],
+        "normalizedVersions": [
+          {
+            "scheme": "semver",
+            "type": "range",
+            "min": "0",
+            "minInclusive": true,
+            "max": "0.5.0b3.dev91",
+            "maxInclusive": false,
+            "value": null,
+            "notes": "osv:PyPI:GHSA-cjjf-27cc-pvmv:pkg:pypi/pyload-ng"
+          }
+        ],
+        "statuses": [],
+        "provenance": [
+          {
+            "source": "osv",
+            "kind": "affected",
+            "value": "pkg:pypi/pyload-ng",
+            "decisionReason": null,
+            "recordedAt": "2025-10-15T14:48:57.995174+00:00",
+            "fieldMask": [
+              "affectedpackages[]"
+            ]
+          }
+        ]
+      }
+    ],
+    "aliases": [
+      "CVE-2025-61773",
+      "GHSA-cjjf-27cc-pvmv"
+    ],
+    "canonicalMetricId": "3.1|CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:N",
+    "credits": [],
+    "cvssMetrics": [
+      {
+        "baseScore": 8.1,
+        "baseSeverity": "high",
+        "provenance": {
+          "source": "osv",
+          "kind": "cvss",
+          "value": "CVSS_V3",
+          "decisionReason": null,
+          "recordedAt": "2025-10-15T14:48:57.995174+00:00",
+          "fieldMask": []
+        },
+        "vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:N",
+        "version": "3.1"
+      }
+    ],
+    "cwes": [
+      {
+        "taxonomy": "cwe",
+        "identifier": "CWE-116",
+        "name": null,
+        "uri": "https://cwe.mitre.org/data/definitions/116.html",
+        "provenance": [
+          {
+            "source": "osv",
+            "kind": "weakness",
+            "value": "CWE-116",
+            "decisionReason": "database_specific.cwe_ids",
+            "recordedAt": "2025-10-15T14:48:57.995174+00:00",
+            "fieldMask": [
+              "cwes[]"
+            ]
+          }
+        ]
+      },
+      {
+        "taxonomy": "cwe",
+        "identifier": "CWE-74",
+        "name": null,
+        "uri": "https://cwe.mitre.org/data/definitions/74.html",
+        "provenance": [
+          {
+            "source": "osv",
+            "kind": "weakness",
+            "value": "CWE-74",
+            "decisionReason": "database_specific.cwe_ids",
+            "recordedAt": "2025-10-15T14:48:57.995174+00:00",
+            "fieldMask": [
+              "cwes[]"
+            ]
+          }
+        ]
+      },
+      {
+        "taxonomy": "cwe",
+        "identifier": "CWE-79",
+        "name": null,
+        "uri": "https://cwe.mitre.org/data/definitions/79.html",
+        "provenance": [
+          {
+            "source": "osv",
+            "kind": "weakness",
+            "value": "CWE-79",
+            "decisionReason": "database_specific.cwe_ids",
+            "recordedAt": "2025-10-15T14:48:57.995174+00:00",
+            "fieldMask": [
+              "cwes[]"
+            ]
+          }
+        ]
+      },
+      {
+        "taxonomy": "cwe",
+        "identifier": "CWE-94",
+        "name": null,
+        "uri": "https://cwe.mitre.org/data/definitions/94.html",
+        "provenance": [
+          {
+            "source": "osv",
+            "kind": "weakness",
+            "value": "CWE-94",
+            "decisionReason": "database_specific.cwe_ids",
+            "recordedAt": "2025-10-15T14:48:57.995174+00:00",
+            "fieldMask": [
+              "cwes[]"
+            ]
+          }
+        ]
+      }
+    ],
+    "description": "### Summary\npyLoad web interface contained insufficient input validation in both the Captcha script endpoint and the Click'N'Load (CNL) Blueprint. This flaw allowed untrusted user input to be processed unsafely, which could be exploited by an attacker to inject arbitrary content into the web UI or manipulate request handling. The vulnerability could lead to client-side code execution (XSS) or other unintended behaviors when a malicious payload is submitted.\n\nuser-supplied parameters from HTTP requests were not adequately validated or sanitized before being passed into the application logic and response generation. This allowed crafted input to alter the expected execution flow.\n CNL (Click'N'Load) blueprint exposed unsafe handling of untrusted parameters in HTTP requests. The application did not consistently enforce input validation or encoding, making it possible for an attacker to craft malicious requests.\n\n### PoC\n\n1. Run a vulnerable version of pyLoad prior to commit [`f9d27f2`](https://github.com/pyload/pyload/pull/4624).\n2. Start the web UI and access the Captcha or CNL endpoints.\n3. Submit a crafted request containing malicious JavaScript payloads in unvalidated parameters (`/flash/addcrypted2?jk=function(){alert(1)}&crypted=12345`).\n4. Observe that the payload is reflected and executed in the client’s browser, demonstrating cross-site scripting (XSS).\n\nExample request:\n\n```http\nGET /flash/addcrypted2?jk=function(){alert(1)}&crypted=12345 HTTP/1.1\nHost: 127.0.0.1:8000\nContent-Type: application/x-www-form-urlencoded\nContent-Length: 107\n```\n\n### Impact\n\nExploiting this vulnerability allows an attacker to inject and execute arbitrary JavaScript within the browser session of a user accessing the pyLoad Web UI. In practice, this means an attacker could impersonate an administrator, steal authentication cookies or tokens, and perform unauthorized actions on behalf of the victim. Because the affected endpoints are part of the core interface, a successful attack undermines the trust and security of the entire application, potentially leading to a full compromise of the management interface and the data it controls. The impact is particularly severe in cases where the Web UI is exposed over a network without additional access restrictions, as it enables remote attackers to directly target users with crafted links or requests that trigger the vulnerability.",
+    "exploitKnown": false,
+    "language": "en",
+    "modified": "2025-10-09T15:59:13.250015+00:00",
+    "provenance": [
+      {
+        "source": "osv",
+        "kind": "document",
+        "value": "https://osv.dev/vulnerability/GHSA-cjjf-27cc-pvmv",
+        "decisionReason": null,
+        "recordedAt": "2025-10-09T15:19:48+00:00",
+        "fieldMask": [
+          "advisory"
+        ]
+      },
+      {
+        "source": "osv",
+        "kind": "mapping",
+        "value": "GHSA-cjjf-27cc-pvmv",
+        "decisionReason": null,
+        "recordedAt": "2025-10-15T14:48:57.995174+00:00",
+        "fieldMask": [
+          "advisory"
+        ]
+      }
+    ],
+    "published": "2025-10-09T15:19:48+00:00",
+    "references": [
+      {
+        "kind": null,
+        "provenance": {
+          "source": "osv",
+          "kind": "reference",
+          "value": "https://github.com/pyload/pyload",
+          "decisionReason": null,
+          "recordedAt": "2025-10-15T14:48:57.995174+00:00",
+          "fieldMask": [
+            "references[]"
+          ]
+        },
+        "sourceTag": "PACKAGE",
+        "summary": null,
+        "url": "https://github.com/pyload/pyload"
+      },
+      {
+        "kind": null,
+        "provenance": {
+          "source": "osv",
+          "kind": "reference",
+          "value": "https://github.com/pyload/pyload/commit/5823327d0b797161c7195a1f660266d30a69f0ca",
+          "decisionReason": null,
+          "recordedAt": "2025-10-15T14:48:57.995174+00:00",
+          "fieldMask": [
+            "references[]"
+          ]
+        },
+        "sourceTag": "WEB",
+        "summary": null,
+        "url": "https://github.com/pyload/pyload/commit/5823327d0b797161c7195a1f660266d30a69f0ca"
+      },
+      {
+        "kind": null,
+        "provenance": {
+          "source": "osv",
+          "kind": "reference",
+          "value": "https://github.com/pyload/pyload/pull/4624",
+          "decisionReason": null,
+          "recordedAt": "2025-10-15T14:48:57.995174+00:00",
+          "fieldMask": [
+            "references[]"
+          ]
+        },
+        "sourceTag": "WEB",
+        "summary": null,
+        "url": "https://github.com/pyload/pyload/pull/4624"
+      },
+      {
+        "kind": null,
+        "provenance": {
+          "source": "osv",
+          "kind": "reference",
+          "value": "https://github.com/pyload/pyload/security/advisories/GHSA-cjjf-27cc-pvmv",
+          "decisionReason": null,
+          "recordedAt": "2025-10-15T14:48:57.995174+00:00",
+          "fieldMask": [
+            "references[]"
+          ]
+        },
+        "sourceTag": "WEB",
+        "summary": null,
+        "url": "https://github.com/pyload/pyload/security/advisories/GHSA-cjjf-27cc-pvmv"
+      }
+    ],
+    "severity": "high",
+    "summary": "### Summary pyLoad web interface contained insufficient input validation in both the Captcha script endpoint and the Click'N'Load (CNL) Blueprint. This flaw allowed untrusted user input to be processed unsafely, which could be exploited by an attacker to inject arbitrary content into the web UI or manipulate request handling. The vulnerability could lead to client-side code execution (XSS) or other unintended behaviors when a malicious payload is submitted. user-supplied parameters from HTTP requests were not adequately validated or sanitized before being passed into the application logic and response generation. This allowed crafted input to alter the expected execution flow. CNL (Click'N'Load) blueprint exposed unsafe handling of untrusted parameters in HTTP requests. The application did not consistently enforce input validation or encoding, making it possible for an attacker to craft malicious requests. ### PoC 1. Run a vulnerable version of pyLoad prior to commit [`f9d27f2`](https://github.com/pyload/pyload/pull/4624). 2. Start the web UI and access the Captcha or CNL endpoints. 3. Submit a crafted request containing malicious JavaScript payloads in unvalidated parameters (`/flash/addcrypted2?jk=function(){alert(1)}&crypted=12345`). 4. Observe that the payload is reflected and executed in the client’s browser, demonstrating cross-site scripting (XSS). Example request: ```http GET /flash/addcrypted2?jk=function(){alert(1)}&crypted=12345 HTTP/1.1 Host: 127.0.0.1:8000 Content-Type: application/x-www-form-urlencoded Content-Length: 107 ``` ### Impact Exploiting this vulnerability allows an attacker to inject and execute arbitrary JavaScript within the browser session of a user accessing the pyLoad Web UI. In practice, this means an attacker could impersonate an administrator, steal authentication cookies or tokens, and perform unauthorized actions on behalf of the victim. Because the affected endpoints are part of the core interface, a successful attack undermines the trust and security of the entire application, potentially leading to a full compromise of the management interface and the data it controls. The impact is particularly severe in cases where the Web UI is exposed over a network without additional access restrictions, as it enables remote attackers to directly target users with crafted links or requests that trigger the vulnerability.",
+    "title": "pyLoad CNL and captcha handlers allow Code Injection via unsanitized parameters"
+  },
+  {
+    "advisoryKey": "GHSA-wv4w-6qv2-qqfg",
+    "affectedPackages": [
+      {
+        "type": "semver",
+        "identifier": "pkg:pypi/social-auth-app-django",
+        "platform": "PyPI",
+        "versionRanges": [
+          {
+            "fixedVersion": "5.6.0",
+            "introducedVersion": "0",
+            "lastAffectedVersion": null,
+            "primitives": {
+              "evr": null,
+              "hasVendorExtensions": false,
+              "nevra": null,
+              "semVer": {
+                "constraintExpression": null,
+                "exactValue": null,
+                "fixed": "5.6.0",
+                "fixedInclusive": false,
+                "introduced": "0",
+                "introducedInclusive": true,
+                "lastAffected": null,
+                "lastAffectedInclusive": true,
+                "style": "range"
+              },
+              "vendorExtensions": null
+            },
+            "provenance": {
+              "source": "osv",
+              "kind": "range",
+              "value": "pkg:pypi/social-auth-app-django",
+              "decisionReason": null,
+              "recordedAt": "2025-10-15T14:48:57.9927932+00:00",
+              "fieldMask": [
+                "affectedpackages[].versionranges[]"
+              ]
+            },
+            "rangeExpression": null,
+            "rangeKind": "semver"
+          }
+        ],
+        "normalizedVersions": [
+          {
+            "scheme": "semver",
+            "type": "range",
+            "min": "0",
+            "minInclusive": true,
+            "max": "5.6.0",
+            "maxInclusive": false,
+            "value": null,
+            "notes": "osv:PyPI:GHSA-wv4w-6qv2-qqfg:pkg:pypi/social-auth-app-django"
+          }
+        ],
+        "statuses": [],
+        "provenance": [
+          {
+            "source": "osv",
+            "kind": "affected",
+            "value": "pkg:pypi/social-auth-app-django",
+            "decisionReason": null,
+            "recordedAt": "2025-10-15T14:48:57.9927932+00:00",
+            "fieldMask": [
+              "affectedpackages[]"
+            ]
+          }
+        ]
+      }
+    ],
+    "aliases": [
+      "CVE-2025-61783",
+      "GHSA-wv4w-6qv2-qqfg"
+    ],
+    "canonicalMetricId": "osv:severity/medium",
+    "credits": [],
+    "cvssMetrics": [],
+    "cwes": [
+      {
+        "taxonomy": "cwe",
+        "identifier": "CWE-290",
+        "name": null,
+        "uri": "https://cwe.mitre.org/data/definitions/290.html",
+        "provenance": [
+          {
+            "source": "osv",
+            "kind": "weakness",
+            "value": "CWE-290",
+            "decisionReason": "database_specific.cwe_ids",
+            "recordedAt": "2025-10-15T14:48:57.9927932+00:00",
+            "fieldMask": [
+              "cwes[]"
+            ]
+          }
+        ]
+      }
+    ],
+    "description": "### Impact\n\nUpon authentication, the user could be associated by e-mail even if the `associate_by_email` pipeline was not included. This could lead to account compromise when a third-party authentication service does not validate provided e-mail addresses or doesn't require unique e-mail addresses.\n\n### Patches\n\n* https://github.com/python-social-auth/social-app-django/pull/803\n\n### Workarounds\n\nReview the authentication service policy on e-mail addresses; many will not allow exploiting this vulnerability.",
+    "exploitKnown": false,
+    "language": "en",
+    "modified": "2025-10-09T17:57:29.916841+00:00",
+    "provenance": [
+      {
+        "source": "osv",
+        "kind": "document",
+        "value": "https://osv.dev/vulnerability/GHSA-wv4w-6qv2-qqfg",
+        "decisionReason": null,
+        "recordedAt": "2025-10-09T17:08:05+00:00",
+        "fieldMask": [
+          "advisory"
+        ]
+      },
+      {
+        "source": "osv",
+        "kind": "mapping",
+        "value": "GHSA-wv4w-6qv2-qqfg",
+        "decisionReason": null,
+        "recordedAt": "2025-10-15T14:48:57.9927932+00:00",
+        "fieldMask": [
+          "advisory"
+        ]
+      }
+    ],
+    "published": "2025-10-09T17:08:05+00:00",
+    "references": [
+      {
+        "kind": null,
+        "provenance": {
+          "source": "osv",
+          "kind": "reference",
+          "value": "https://github.com/python-social-auth/social-app-django",
+          "decisionReason": null,
+          "recordedAt": "2025-10-15T14:48:57.9927932+00:00",
+          "fieldMask": [
+            "references[]"
+          ]
+        },
+        "sourceTag": "PACKAGE",
+        "summary": null,
+        "url": "https://github.com/python-social-auth/social-app-django"
+      },
+      {
+        "kind": null,
+        "provenance": {
+          "source": "osv",
+          "kind": "reference",
+          "value": "https://github.com/python-social-auth/social-app-django/commit/10c80e2ebabeccd4e9c84ad0e16e1db74148ed4c",
+          "decisionReason": null,
+          "recordedAt": "2025-10-15T14:48:57.9927932+00:00",
+          "fieldMask": [
+            "references[]"
+          ]
+        },
+        "sourceTag": "WEB",
+        "summary": null,
+        "url": "https://github.com/python-social-auth/social-app-django/commit/10c80e2ebabeccd4e9c84ad0e16e1db74148ed4c"
+      },
+      {
+        "kind": null,
+        "provenance": {
+          "source": "osv",
+          "kind": "reference",
+          "value": "https://github.com/python-social-auth/social-app-django/issues/220",
+          "decisionReason": null,
+          "recordedAt": "2025-10-15T14:48:57.9927932+00:00",
+          "fieldMask": [
+            "references[]"
+          ]
+        },
+        "sourceTag": "WEB",
+        "summary": null,
+        "url": "https://github.com/python-social-auth/social-app-django/issues/220"
+      },
+      {
+        "kind": null,
+        "provenance": {
+          "source": "osv",
+          "kind": "reference",
+          "value": "https://github.com/python-social-auth/social-app-django/issues/231",
+          "decisionReason": null,
+          "recordedAt": "2025-10-15T14:48:57.9927932+00:00",
+          "fieldMask": [
+            "references[]"
+          ]
+        },
+        "sourceTag": "WEB",
+        "summary": null,
+        "url": "https://github.com/python-social-auth/social-app-django/issues/231"
+      },
+      {
+        "kind": null,
+        "provenance": {
+          "source": "osv",
+          "kind": "reference",
+          "value": "https://github.com/python-social-auth/social-app-django/issues/634",
+          "decisionReason": null,
+          "recordedAt": "2025-10-15T14:48:57.9927932+00:00",
+          "fieldMask": [
+            "references[]"
+          ]
+        },
+        "sourceTag": "WEB",
+        "summary": null,
+        "url": "https://github.com/python-social-auth/social-app-django/issues/634"
+      },
+      {
+        "kind": null,
+        "provenance": {
+          "source": "osv",
+          "kind": "reference",
+          "value": "https://github.com/python-social-auth/social-app-django/pull/803",
+          "decisionReason": null,
+          "recordedAt": "2025-10-15T14:48:57.9927932+00:00",
+          "fieldMask": [
+            "references[]"
+          ]
+        },
+        "sourceTag": "WEB",
+        "summary": null,
+        "url": "https://github.com/python-social-auth/social-app-django/pull/803"
+      },
+      {
+        "kind": null,
+        "provenance": {
+          "source": "osv",
+          "kind": "reference",
+          "value": "https://github.com/python-social-auth/social-app-django/security/advisories/GHSA-wv4w-6qv2-qqfg",
+          "decisionReason": null,
+          "recordedAt": "2025-10-15T14:48:57.9927932+00:00",
+          "fieldMask": [
+            "references[]"
+          ]
+        },
+        "sourceTag": "WEB",
+        "summary": null,
+        "url": "https://github.com/python-social-auth/social-app-django/security/advisories/GHSA-wv4w-6qv2-qqfg"
+      }
+    ],
+    "severity": "medium",
+    "summary": "### Impact Upon authentication, the user could be associated by e-mail even if the `associate_by_email` pipeline was not included. This could lead to account compromise when a third-party authentication service does not validate provided e-mail addresses or doesn't require unique e-mail addresses. ### Patches * https://github.com/python-social-auth/social-app-django/pull/803 ### Workarounds Review the authentication service policy on e-mail addresses; many will not allow exploiting this vulnerability.",
+    "title": "Python Social Auth - Django has unsafe account association"
+  }
+]
diff --git a/src/StellaOps.Feedser.Source.Osv.Tests/Osv/OsvMapperTests.cs b/src/StellaOps.Feedser.Source.Osv.Tests/Osv/OsvMapperTests.cs
index 87df844e..e479579b 100644
--- a/src/StellaOps.Feedser.Source.Osv.Tests/Osv/OsvMapperTests.cs
+++ b/src/StellaOps.Feedser.Source.Osv.Tests/Osv/OsvMapperTests.cs
@@ -124,6 +124,46 @@ public sealed class OsvMapperTests
         Assert.Equal("3.1", advisory.CvssMetrics[0].Version);
     }
 
+    [Fact]
+    public void Map_AssignsSeverityFallbackWhenCvssVectorUnsupported()
+    {
+        using var databaseSpecificJson = JsonDocument.Parse("""
+        {
+            "severity": "MODERATE",
+            "cwe_ids": ["CWE-290"]
+        }
+        """);
+
+        var dto = new OsvVulnerabilityDto
+        {
+            Id = "OSV-CVSS4",
+            Summary = "Severity-only advisory",
+            Details = "OSV entry that lacks a parsable CVSS vector.",
+            Published = DateTimeOffset.UtcNow.AddDays(-10),
+            Modified = DateTimeOffset.UtcNow.AddDays(-5),
+            DatabaseSpecific = databaseSpecificJson.RootElement,
+            Severity = new[]
+            {
+                new OsvSeverityDto
+                {
+                    Type = "CVSS_V4",
+                    Score = "CVSS:4.0/AV:N/AC:H/AT:N/PR:N/UI:N/VC:L/VI:L/VA:N/SC:N/SI:N/SA:N"
+                }
+            }
+        };
+
+        var (document, dtoRecord) = CreateDocumentAndDtoRecord(dto, "PyPI");
+        var advisory = OsvMapper.Map(dto, document, dtoRecord, "PyPI");
+
+        Assert.True(advisory.CvssMetrics.IsEmpty);
+        Assert.Equal("medium", advisory.Severity);
+        Assert.Equal("osv:severity/medium", advisory.CanonicalMetricId);
+
+        var weakness = Assert.Single(advisory.Cwes);
+        var provenance = Assert.Single(weakness.Provenance);
+        Assert.Equal("database_specific.cwe_ids", provenance.DecisionReason);
+    }
+
     [Theory]
     [InlineData("Go", "github.com/example/project", "pkg:golang/github.com/example/project")]
     [InlineData("PyPI", "social_auth_app_django", "pkg:pypi/social-auth-app-django")]
diff --git a/src/StellaOps.Feedser.Source.Osv/Internal/OsvDiagnostics.cs b/src/StellaOps.Feedser.Source.Osv/Internal/OsvDiagnostics.cs
new file mode 100644
index 00000000..28bfa958
--- /dev/null
+++ b/src/StellaOps.Feedser.Source.Osv/Internal/OsvDiagnostics.cs
@@ -0,0 +1,36 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics.Metrics;
+
+namespace StellaOps.Feedser.Source.Osv.Internal;
+
+/// 
+/// Connector-specific diagnostics for OSV mapping.
+/// 
+public sealed class OsvDiagnostics : IDisposable
+{
+    private const string MeterName = "StellaOps.Feedser.Source.Osv";
+    private const string MeterVersion = "1.0.0";
+
+    private readonly Meter _meter;
+    private readonly Counter _canonicalMetricFallbacks;
+
+    public OsvDiagnostics()
+    {
+        _meter = new Meter(MeterName, MeterVersion);
+        _canonicalMetricFallbacks = _meter.CreateCounter("osv.map.canonical_metric_fallbacks", unit: "advisories");
+    }
+
+    public void CanonicalMetricFallback(string canonicalMetricId, string severity, string? ecosystem)
+        => _canonicalMetricFallbacks.Add(
+            1,
+            new KeyValuePair("canonical_metric_id", canonicalMetricId),
+            new KeyValuePair("severity", severity),
+            new KeyValuePair("ecosystem", string.IsNullOrWhiteSpace(ecosystem) ? "unknown" : ecosystem),
+            new KeyValuePair("reason", "no_cvss"));
+
+    public void Dispose()
+    {
+        _meter.Dispose();
+    }
+}
diff --git a/src/StellaOps.Feedser.Source.Osv/Internal/OsvMapper.cs b/src/StellaOps.Feedser.Source.Osv/Internal/OsvMapper.cs
index 4e53a712..88625b0d 100644
--- a/src/StellaOps.Feedser.Source.Osv/Internal/OsvMapper.cs
+++ b/src/StellaOps.Feedser.Source.Osv/Internal/OsvMapper.cs
@@ -68,11 +68,22 @@ internal static class OsvMapper
         var credits = BuildCredits(dto, recordedAt);
         var affectedPackages = BuildAffectedPackages(dto, ecosystem, recordedAt);
         var cvssMetrics = BuildCvssMetrics(dto, recordedAt, out var severity);
+        var databaseSpecificSeverity = ExtractDatabaseSpecificSeverity(dto.DatabaseSpecific);
+        if (severity is null)
+        {
+            severity = databaseSpecificSeverity;
+        }
+
         var weaknesses = BuildWeaknesses(dto, recordedAt);
         var canonicalMetricId = cvssMetrics.Count > 0
             ? $"{cvssMetrics[0].Version}|{cvssMetrics[0].Vector}"
             : null;
 
+        if (canonicalMetricId is null && !string.IsNullOrWhiteSpace(severity))
+        {
+            canonicalMetricId = BuildSeverityCanonicalMetricId(severity);
+        }
+
         var normalizedDescription = DescriptionNormalizer.Normalize(new[]
         {
             new LocalizedText(dto.Details, "en"),
@@ -106,7 +117,10 @@ internal static class OsvMapper
             descriptionText,
             weaknesses,
             canonicalMetricId);
-    }
+    }
+
+    private static string BuildSeverityCanonicalMetricId(string severity)
+        => $"{OsvConnectorPlugin.SourceName}:severity/{severity}";
 
     private static IEnumerable BuildAliases(OsvVulnerabilityDto dto)
     {
@@ -509,7 +523,8 @@ internal static class OsvMapper
                 "weakness",
                 identifier,
                 recordedAt,
-                new[] { ProvenanceFieldMasks.Weaknesses });
+                new[] { ProvenanceFieldMasks.Weaknesses },
+                decisionReason: GetCweDecisionReason(dto.DatabaseSpecific, identifier));
 
             var provenanceArray = ImmutableArray.Create(provenance);
             list.Add(new AdvisoryWeakness(
@@ -550,6 +565,78 @@ internal static class OsvMapper
         return digits.Length == 0 ? null : $"https://cwe.mitre.org/data/definitions/{digits}.html";
     }
 
+    private static string? ExtractDatabaseSpecificSeverity(JsonElement databaseSpecific)
+    {
+        if (databaseSpecific.ValueKind != JsonValueKind.Object)
+        {
+            return null;
+        }
+
+        if (!databaseSpecific.TryGetProperty("severity", out var severityElement))
+        {
+            return null;
+        }
+
+        if (severityElement.ValueKind == JsonValueKind.String)
+        {
+            var severity = severityElement.GetString();
+            return SeverityNormalization.Normalize(severity);
+        }
+
+        return null;
+    }
+
+    private static string? GetCweDecisionReason(JsonElement databaseSpecific, string identifier)
+    {
+        if (databaseSpecific.ValueKind != JsonValueKind.Object)
+        {
+            return null;
+        }
+
+        var hasCweIds = databaseSpecific.TryGetProperty("cwe_ids", out _);
+        string? notes = null;
+
+        if (databaseSpecific.TryGetProperty("cwe_notes", out var notesElement))
+        {
+            notes = NormalizeCweNotes(notesElement);
+        }
+
+        if (!string.IsNullOrWhiteSpace(notes))
+        {
+            return notes;
+        }
+
+        return hasCweIds ? "database_specific.cwe_ids" : null;
+    }
+
+    private static string? NormalizeCweNotes(JsonElement notesElement)
+    {
+        if (notesElement.ValueKind == JsonValueKind.String)
+        {
+            return Validation.TrimToNull(notesElement.GetString());
+        }
+
+        if (notesElement.ValueKind != JsonValueKind.Array)
+        {
+            return null;
+        }
+
+        var buffer = new List();
+        foreach (var item in notesElement.EnumerateArray())
+        {
+            if (item.ValueKind == JsonValueKind.String)
+            {
+                var value = Validation.TrimToNull(item.GetString());
+                if (!string.IsNullOrEmpty(value))
+                {
+                    buffer.Add(value);
+                }
+            }
+        }
+
+        return buffer.Count == 0 ? null : string.Join(" | ", buffer);
+    }
+
     private static IReadOnlyList BuildCvssMetrics(OsvVulnerabilityDto dto, DateTimeOffset recordedAt, out string? severity)
     {
         severity = null;
diff --git a/src/StellaOps.Feedser.Source.Osv/OsvConnector.cs b/src/StellaOps.Feedser.Source.Osv/OsvConnector.cs
index d15e3e54..7690b135 100644
--- a/src/StellaOps.Feedser.Source.Osv/OsvConnector.cs
+++ b/src/StellaOps.Feedser.Source.Osv/OsvConnector.cs
@@ -14,8 +14,7 @@ using Microsoft.Extensions.Logging;
 using Microsoft.Extensions.Options;
 using MongoDB.Bson;
 using MongoDB.Bson.IO;
-using StellaOps.Feedser.Models;
-using StellaOps.Feedser.Models;
+using StellaOps.Feedser.Models;
 using StellaOps.Feedser.Source.Common;
 using StellaOps.Feedser.Source.Common.Fetch;
 using StellaOps.Feedser.Source.Osv.Configuration;
@@ -39,35 +38,38 @@ public sealed class OsvConnector : IFeedConnector
     private readonly IHttpClientFactory _httpClientFactory;
     private readonly RawDocumentStorage _rawDocumentStorage;
     private readonly IDocumentStore _documentStore;
-    private readonly IDtoStore _dtoStore;
-    private readonly IAdvisoryStore _advisoryStore;
-    private readonly ISourceStateRepository _stateRepository;
-    private readonly OsvOptions _options;
-    private readonly TimeProvider _timeProvider;
-    private readonly ILogger _logger;
+    private readonly IDtoStore _dtoStore;
+    private readonly IAdvisoryStore _advisoryStore;
+    private readonly ISourceStateRepository _stateRepository;
+    private readonly OsvOptions _options;
+    private readonly TimeProvider _timeProvider;
+    private readonly ILogger _logger;
+    private readonly OsvDiagnostics _diagnostics;
 
     public OsvConnector(
         IHttpClientFactory httpClientFactory,
         RawDocumentStorage rawDocumentStorage,
-        IDocumentStore documentStore,
-        IDtoStore dtoStore,
-        IAdvisoryStore advisoryStore,
-        ISourceStateRepository stateRepository,
-        IOptions options,
-        TimeProvider? timeProvider,
-        ILogger logger)
-    {
-        _httpClientFactory = httpClientFactory ?? throw new ArgumentNullException(nameof(httpClientFactory));
-        _rawDocumentStorage = rawDocumentStorage ?? throw new ArgumentNullException(nameof(rawDocumentStorage));
-        _documentStore = documentStore ?? throw new ArgumentNullException(nameof(documentStore));
-        _dtoStore = dtoStore ?? throw new ArgumentNullException(nameof(dtoStore));
-        _advisoryStore = advisoryStore ?? throw new ArgumentNullException(nameof(advisoryStore));
-        _stateRepository = stateRepository ?? throw new ArgumentNullException(nameof(stateRepository));
-        _options = (options ?? throw new ArgumentNullException(nameof(options))).Value ?? throw new ArgumentNullException(nameof(options));
-        _options.Validate();
-        _timeProvider = timeProvider ?? TimeProvider.System;
-        _logger = logger ?? throw new ArgumentNullException(nameof(logger));
-    }
+        IDocumentStore documentStore,
+        IDtoStore dtoStore,
+        IAdvisoryStore advisoryStore,
+        ISourceStateRepository stateRepository,
+        IOptions options,
+        OsvDiagnostics diagnostics,
+        TimeProvider? timeProvider,
+        ILogger logger)
+    {
+        _httpClientFactory = httpClientFactory ?? throw new ArgumentNullException(nameof(httpClientFactory));
+        _rawDocumentStorage = rawDocumentStorage ?? throw new ArgumentNullException(nameof(rawDocumentStorage));
+        _documentStore = documentStore ?? throw new ArgumentNullException(nameof(documentStore));
+        _dtoStore = dtoStore ?? throw new ArgumentNullException(nameof(dtoStore));
+        _advisoryStore = advisoryStore ?? throw new ArgumentNullException(nameof(advisoryStore));
+        _stateRepository = stateRepository ?? throw new ArgumentNullException(nameof(stateRepository));
+        _options = (options ?? throw new ArgumentNullException(nameof(options))).Value ?? throw new ArgumentNullException(nameof(options));
+        _diagnostics = diagnostics ?? throw new ArgumentNullException(nameof(diagnostics));
+        _options.Validate();
+        _timeProvider = timeProvider ?? TimeProvider.System;
+        _logger = logger ?? throw new ArgumentNullException(nameof(logger));
+    }
 
     public string SourceName => OsvConnectorPlugin.SourceName;
 
@@ -259,16 +261,31 @@ public sealed class OsvConnector : IFeedConnector
                 continue;
             }
 
-            var ecosystem = document.Metadata is not null && document.Metadata.TryGetValue("osv.ecosystem", out var ecosystemValue)
-                ? ecosystemValue
-                : "unknown";
-
-            var advisory = OsvMapper.Map(osvDto, document, dto, ecosystem);
-            await _advisoryStore.UpsertAsync(advisory, cancellationToken).ConfigureAwait(false);
-            await _documentStore.UpdateStatusAsync(document.Id, DocumentStatuses.Mapped, cancellationToken).ConfigureAwait(false);
-
-            pendingMappings.Remove(documentId);
-        }
+            var ecosystem = document.Metadata is not null && document.Metadata.TryGetValue("osv.ecosystem", out var ecosystemValue)
+                ? ecosystemValue
+                : "unknown";
+
+            var advisory = OsvMapper.Map(osvDto, document, dto, ecosystem);
+            if (advisory.CvssMetrics.IsEmpty && !string.IsNullOrWhiteSpace(advisory.CanonicalMetricId))
+            {
+                var fallbackSeverity = string.IsNullOrWhiteSpace(advisory.Severity) ? "unknown" : advisory.Severity!;
+                _diagnostics.CanonicalMetricFallback(advisory.CanonicalMetricId!, fallbackSeverity, ecosystem);
+                if (_logger.IsEnabled(LogLevel.Debug))
+                {
+                    _logger.LogDebug(
+                        "OSV {OsvId} emitted canonical metric fallback {CanonicalMetricId} (severity {Severity}, ecosystem {Ecosystem})",
+                        advisory.AdvisoryKey,
+                        advisory.CanonicalMetricId,
+                        fallbackSeverity,
+                        ecosystem);
+                }
+            }
+
+            await _advisoryStore.UpsertAsync(advisory, cancellationToken).ConfigureAwait(false);
+            await _documentStore.UpdateStatusAsync(document.Id, DocumentStatuses.Mapped, cancellationToken).ConfigureAwait(false);
+
+            pendingMappings.Remove(documentId);
+        }
 
         var updatedCursor = cursor.WithPendingMappings(pendingMappings);
         await UpdateCursorAsync(updatedCursor, cancellationToken).ConfigureAwait(false);
diff --git a/src/StellaOps.Feedser.Source.Osv/OsvServiceCollectionExtensions.cs b/src/StellaOps.Feedser.Source.Osv/OsvServiceCollectionExtensions.cs
index 269bdb5a..34740db5 100644
--- a/src/StellaOps.Feedser.Source.Osv/OsvServiceCollectionExtensions.cs
+++ b/src/StellaOps.Feedser.Source.Osv/OsvServiceCollectionExtensions.cs
@@ -2,7 +2,8 @@ using System;
 using Microsoft.Extensions.DependencyInjection;
 using Microsoft.Extensions.Options;
 using StellaOps.Feedser.Source.Common.Http;
-using StellaOps.Feedser.Source.Osv.Configuration;
+using StellaOps.Feedser.Source.Osv.Configuration;
+using StellaOps.Feedser.Source.Osv.Internal;
 
 namespace StellaOps.Feedser.Source.Osv;
 
@@ -17,21 +18,22 @@ public static class OsvServiceCollectionExtensions
             .Configure(configure)
             .PostConfigure(static opts => opts.Validate());
 
-        services.AddSourceHttpClient(OsvOptions.HttpClientName, (sp, clientOptions) =>
-        {
-            var options = sp.GetRequiredService>().Value;
-            clientOptions.BaseAddress = options.BaseUri;
-            clientOptions.Timeout = options.HttpTimeout;
+        services.AddSourceHttpClient(OsvOptions.HttpClientName, (sp, clientOptions) =>
+        {
+            var options = sp.GetRequiredService>().Value;
+            clientOptions.BaseAddress = options.BaseUri;
+            clientOptions.Timeout = options.HttpTimeout;
             clientOptions.UserAgent = "StellaOps.Feedser.OSV/1.0";
             clientOptions.AllowedHosts.Clear();
             clientOptions.AllowedHosts.Add(options.BaseUri.Host);
-            clientOptions.DefaultRequestHeaders["Accept"] = "application/zip";
-        });
-
-        services.AddTransient();
-        services.AddTransient();
-        services.AddTransient();
-        services.AddTransient();
-        return services;
+            clientOptions.DefaultRequestHeaders["Accept"] = "application/zip";
+        });
+
+        services.AddSingleton();
+        services.AddTransient();
+        services.AddTransient();
+        services.AddTransient();
+        services.AddTransient();
+        return services;
     }
 }
diff --git a/src/StellaOps.Feedser.Source.Osv/TASKS.md b/src/StellaOps.Feedser.Source.Osv/TASKS.md
index de8ec6f9..1d35c28a 100644
--- a/src/StellaOps.Feedser.Source.Osv/TASKS.md
+++ b/src/StellaOps.Feedser.Source.Osv/TASKS.md
@@ -17,4 +17,4 @@
 |FEEDCONN-OSV-04-003 Parity fixture refresh|QA, BE-Conn-OSV|Normalized versions rollout, GHSA parity tests|**DONE (2025-10-12)** – Parity fixtures include normalizedVersions notes (`osv:::`); regression math rerun via `dotnet test src/StellaOps.Feedser.Source.Osv.Tests` and docs flagged for workflow sync.|
 |FEEDCONN-OSV-04-002 Conflict regression fixtures|BE-Conn-OSV, QA|Merge `FEEDMERGE-ENGINE-04-001`|**DONE (2025-10-12)** – Added `conflict-osv.canonical.json` + regression asserting SemVer range + CVSS medium severity; dataset matches GHSA/NVD fixtures for merge tests. Validation: `dotnet test src/StellaOps.Feedser.Source.Osv.Tests/StellaOps.Feedser.Source.Osv.Tests.csproj --filter OsvConflictFixtureTests`.|
 |FEEDCONN-OSV-04-004 Description/CWE/metric parity rollout|BE-Conn-OSV|Models, Core|**DONE (2025-10-15)** – OSV mapper writes advisory descriptions, `database_specific.cwe_ids` weaknesses, and canonical CVSS metric id. Parity fixtures (`osv-ghsa.*`, `osv-npm.snapshot.json`, `osv-pypi.snapshot.json`) refreshed and status communicated to Merge coordination.|
-|FEEDCONN-OSV-04-005 Canonical metric fallbacks & CWE notes|BE-Conn-OSV|Models, Merge|TODO – Add fallback logic and metrics for advisories lacking CVSS vectors, enrich CWE provenance notes, and document merge/export expectations; refresh parity fixtures accordingly.|
+|FEEDCONN-OSV-04-005 Canonical metric fallbacks & CWE notes|BE-Conn-OSV|Models, Merge|**DONE (2025-10-16)** – Add fallback logic and metrics for advisories lacking CVSS vectors, enrich CWE provenance notes, and document merge/export expectations; refresh parity fixtures accordingly.
2025-10-16: Mapper now emits `osv:severity/` canonical ids for severity-only advisories, weakness provenance carries `database_specific.cwe_ids`, diagnostics expose `osv.map.canonical_metric_fallbacks`, parity fixtures regenerated, and ops notes added in `docs/ops/feedser-osv-operations.md`. Tests: `dotnet test src/StellaOps.Feedser.Source.Osv.Tests/StellaOps.Feedser.Source.Osv.Tests.csproj`.|
diff --git a/src/StellaOps.Feedser.Storage.Mongo/TASKS.md b/src/StellaOps.Feedser.Storage.Mongo/TASKS.md
index d7a9c309..dddad01f 100644
--- a/src/StellaOps.Feedser.Storage.Mongo/TASKS.md
+++ b/src/StellaOps.Feedser.Storage.Mongo/TASKS.md
@@ -20,3 +20,5 @@
 |FEEDSTORAGE-DATA-02-002 Provenance decision persistence|BE-Storage|Models `FEEDMODELS-SCHEMA-01-002`|**DONE (2025-10-12)** – Normalized documents carry decision reasons/source/timestamps with regression coverage verifying SemVer notes + provenance fallbacks.|
 |FEEDSTORAGE-DATA-02-003 Normalized versions index creation|BE-Storage|Normalization, Mongo bootstrapper|**DONE (2025-10-12)** – Bootstrapper seeds `normalizedVersions.*` indexes when SemVer style is enabled; docs/tests confirm index presence.|
 |FEEDSTORAGE-DATA-04-001 Advisory payload parity (description/CWEs/canonical metric)|BE-Storage|Models, Core|DONE (2025-10-15) – Mongo payloads round-trip new advisory fields; serializer/tests updated, no migration required beyond optional backfill.|
+|FEEDSTORAGE-MONGO-08-001 Causal-consistent session plumbing|BE-Storage|Feedser Core DI|TODO – Introduce scoped MongoDB session provider enabling causal consistency + majority read/write concerns in `AddMongoStorage`; flow optional `IClientSessionHandle` through job/advisory/source state/document stores; add integration test simulating primary election to prove read-your-write + monotonic reads.|
+|FEEDSTORAGE-DATA-07-001 Advisory statement & conflict collections|Team Normalization & Storage Backbone|FEEDMERGE-ENGINE-07-001|TODO – Create `advisory_statements` (immutable) and `advisory_conflicts` collections, define `asOf`/`vulnerabilityKey` indexes, and document migration/rollback steps for event-sourced merge.|
diff --git a/src/StellaOps.Vexer.ArtifactStores.S3.Tests/S3ArtifactClientTests.cs b/src/StellaOps.Vexer.ArtifactStores.S3.Tests/S3ArtifactClientTests.cs
new file mode 100644
index 00000000..16f9061b
--- /dev/null
+++ b/src/StellaOps.Vexer.ArtifactStores.S3.Tests/S3ArtifactClientTests.cs
@@ -0,0 +1,38 @@
+using Amazon.S3;
+using Amazon.S3.Model;
+using Moq;
+using StellaOps.Vexer.ArtifactStores.S3;
+using StellaOps.Vexer.Export;
+
+namespace StellaOps.Vexer.ArtifactStores.S3.Tests;
+
+public sealed class S3ArtifactClientTests
+{
+    [Fact]
+    public async Task ObjectExistsAsync_ReturnsTrue_WhenMetadataSucceeds()
+    {
+        var mock = new Mock();
+        mock.Setup(x => x.GetObjectMetadataAsync("bucket", "key", default)).ReturnsAsync(new GetObjectMetadataResponse
+        {
+            HttpStatusCode = System.Net.HttpStatusCode.OK,
+        });
+
+        var client = new S3ArtifactClient(mock.Object, Microsoft.Extensions.Logging.Abstractions.NullLogger.Instance);
+        var exists = await client.ObjectExistsAsync("bucket", "key", default);
+        Assert.True(exists);
+    }
+
+    [Fact]
+    public async Task PutObjectAsync_MapsMetadata()
+    {
+        var mock = new Mock();
+        mock.Setup(x => x.PutObjectAsync(It.IsAny(), default))
+            .ReturnsAsync(new PutObjectResponse());
+
+        var client = new S3ArtifactClient(mock.Object, Microsoft.Extensions.Logging.Abstractions.NullLogger.Instance);
+        using var stream = new MemoryStream(new byte[] { 1, 2, 3 });
+        await client.PutObjectAsync("bucket", "key", stream, new Dictionary { ["a"] = "b" }, default);
+
+        mock.Verify(x => x.PutObjectAsync(It.Is(r => r.Metadata["a"] == "b"), default), Times.Once);
+    }
+}
diff --git a/src/StellaOps.Vexer.ArtifactStores.S3.Tests/StellaOps.Vexer.ArtifactStores.S3.Tests.csproj b/src/StellaOps.Vexer.ArtifactStores.S3.Tests/StellaOps.Vexer.ArtifactStores.S3.Tests.csproj
new file mode 100644
index 00000000..43bbc0f4
--- /dev/null
+++ b/src/StellaOps.Vexer.ArtifactStores.S3.Tests/StellaOps.Vexer.ArtifactStores.S3.Tests.csproj
@@ -0,0 +1,15 @@
+
+  
+    net10.0
+    preview
+    enable
+    enable
+    true
+  
+  
+    
+  
+  
+    
+  
+
diff --git a/src/StellaOps.Vexer.ArtifactStores.S3/Extensions/ServiceCollectionExtensions.cs b/src/StellaOps.Vexer.ArtifactStores.S3/Extensions/ServiceCollectionExtensions.cs
new file mode 100644
index 00000000..483956b4
--- /dev/null
+++ b/src/StellaOps.Vexer.ArtifactStores.S3/Extensions/ServiceCollectionExtensions.cs
@@ -0,0 +1,38 @@
+using Amazon;
+using Amazon.Runtime;
+using Amazon.S3;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Options;
+using StellaOps.Vexer.Export;
+
+namespace StellaOps.Vexer.ArtifactStores.S3.Extensions;
+
+public static class ServiceCollectionExtensions
+{
+    public static IServiceCollection AddVexS3ArtifactClient(this IServiceCollection services, Action configure)
+    {
+        ArgumentNullException.ThrowIfNull(configure);
+
+        services.Configure(configure);
+        services.AddSingleton(CreateS3Client);
+        services.AddSingleton();
+        return services;
+    }
+
+    private static IAmazonS3 CreateS3Client(IServiceProvider provider)
+    {
+        var options = provider.GetRequiredService>().Value;
+        var config = new AmazonS3Config
+        {
+            RegionEndpoint = RegionEndpoint.GetBySystemName(options.Region),
+            ForcePathStyle = options.ForcePathStyle,
+        };
+
+        if (!string.IsNullOrWhiteSpace(options.ServiceUrl))
+        {
+            config.ServiceURL = options.ServiceUrl;
+        }
+
+        return new AmazonS3Client(config);
+    }
+}
diff --git a/src/StellaOps.Vexer.ArtifactStores.S3/S3ArtifactClient.cs b/src/StellaOps.Vexer.ArtifactStores.S3/S3ArtifactClient.cs
new file mode 100644
index 00000000..96494fd2
--- /dev/null
+++ b/src/StellaOps.Vexer.ArtifactStores.S3/S3ArtifactClient.cs
@@ -0,0 +1,85 @@
+using Amazon.S3;
+using Amazon.S3.Model;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+using StellaOps.Vexer.Export;
+
+namespace StellaOps.Vexer.ArtifactStores.S3;
+
+public sealed class S3ArtifactClientOptions
+{
+    public string Region { get; set; } = "us-east-1";
+
+    public string? ServiceUrl { get; set; }
+        = null;
+
+    public bool ForcePathStyle { get; set; }
+        = true;
+}
+
+public sealed class S3ArtifactClient : IS3ArtifactClient
+{
+    private readonly IAmazonS3 _s3;
+    private readonly ILogger _logger;
+
+    public S3ArtifactClient(IAmazonS3 s3, ILogger logger)
+    {
+        _s3 = s3 ?? throw new ArgumentNullException(nameof(s3));
+        _logger = logger ?? throw new ArgumentNullException(nameof(logger));
+    }
+
+    public async Task ObjectExistsAsync(string bucketName, string key, CancellationToken cancellationToken)
+    {
+        try
+        {
+            var metadata = await _s3.GetObjectMetadataAsync(bucketName, key, cancellationToken).ConfigureAwait(false);
+            return metadata.HttpStatusCode == System.Net.HttpStatusCode.OK;
+        }
+        catch (AmazonS3Exception ex) when (ex.StatusCode == System.Net.HttpStatusCode.NotFound)
+        {
+            return false;
+        }
+    }
+
+    public async Task PutObjectAsync(string bucketName, string key, Stream content, IDictionary metadata, CancellationToken cancellationToken)
+    {
+        var request = new PutObjectRequest
+        {
+            BucketName = bucketName,
+            Key = key,
+            InputStream = content,
+            AutoCloseStream = false,
+        };
+
+        foreach (var kvp in metadata)
+        {
+            request.Metadata[kvp.Key] = kvp.Value;
+        }
+
+        await _s3.PutObjectAsync(request, cancellationToken).ConfigureAwait(false);
+        _logger.LogDebug("Uploaded object {Bucket}/{Key}", bucketName, key);
+    }
+
+    public async Task GetObjectAsync(string bucketName, string key, CancellationToken cancellationToken)
+    {
+        try
+        {
+            var response = await _s3.GetObjectAsync(bucketName, key, cancellationToken).ConfigureAwait(false);
+            var buffer = new MemoryStream();
+            await response.ResponseStream.CopyToAsync(buffer, cancellationToken).ConfigureAwait(false);
+            buffer.Position = 0;
+            return buffer;
+        }
+        catch (AmazonS3Exception ex) when (ex.StatusCode == System.Net.HttpStatusCode.NotFound)
+        {
+            _logger.LogDebug("Object {Bucket}/{Key} not found", bucketName, key);
+            return null;
+        }
+    }
+
+    public async Task DeleteObjectAsync(string bucketName, string key, CancellationToken cancellationToken)
+    {
+        await _s3.DeleteObjectAsync(bucketName, key, cancellationToken).ConfigureAwait(false);
+        _logger.LogDebug("Deleted object {Bucket}/{Key}", bucketName, key);
+    }
+}
diff --git a/src/StellaOps.Vexer.ArtifactStores.S3/StellaOps.Vexer.ArtifactStores.S3.csproj b/src/StellaOps.Vexer.ArtifactStores.S3/StellaOps.Vexer.ArtifactStores.S3.csproj
new file mode 100644
index 00000000..507c79e3
--- /dev/null
+++ b/src/StellaOps.Vexer.ArtifactStores.S3/StellaOps.Vexer.ArtifactStores.S3.csproj
@@ -0,0 +1,17 @@
+
+  
+    net10.0
+    preview
+    enable
+    enable
+    true
+  
+  
+    
+    
+    
+  
+  
+    
+  
+
diff --git a/src/StellaOps.Vexer.Attestation.Tests/StellaOps.Vexer.Attestation.Tests.csproj b/src/StellaOps.Vexer.Attestation.Tests/StellaOps.Vexer.Attestation.Tests.csproj
new file mode 100644
index 00000000..6f4e0b68
--- /dev/null
+++ b/src/StellaOps.Vexer.Attestation.Tests/StellaOps.Vexer.Attestation.Tests.csproj
@@ -0,0 +1,13 @@
+
+  
+    net10.0
+    preview
+    enable
+    enable
+    true
+  
+  
+    
+    
+  
+
diff --git a/src/StellaOps.Vexer.Attestation.Tests/VexAttestationClientTests.cs b/src/StellaOps.Vexer.Attestation.Tests/VexAttestationClientTests.cs
new file mode 100644
index 00000000..1a27252b
--- /dev/null
+++ b/src/StellaOps.Vexer.Attestation.Tests/VexAttestationClientTests.cs
@@ -0,0 +1,81 @@
+using System.Collections.Immutable;
+using Microsoft.Extensions.Logging.Abstractions;
+using Microsoft.Extensions.Options;
+using StellaOps.Vexer.Attestation.Dsse;
+using StellaOps.Vexer.Attestation.Signing;
+using StellaOps.Vexer.Attestation.Transparency;
+using StellaOps.Vexer.Core;
+
+namespace StellaOps.Vexer.Attestation.Tests;
+
+public sealed class VexAttestationClientTests
+{
+    [Fact]
+    public async Task SignAsync_ReturnsEnvelopeDigestAndDiagnostics()
+    {
+        var signer = new FakeSigner();
+        var builder = new VexDsseBuilder(signer, NullLogger.Instance);
+        var options = Options.Create(new VexAttestationClientOptions());
+        var client = new VexAttestationClient(builder, options, NullLogger.Instance);
+
+        var request = new VexAttestationRequest(
+            ExportId: "exports/456",
+            QuerySignature: new VexQuerySignature("filters"),
+            Artifact: new VexContentAddress("sha256", "deadbeef"),
+            Format: VexExportFormat.Json,
+            CreatedAt: DateTimeOffset.UtcNow,
+            SourceProviders: ImmutableArray.Create("vendor"),
+            Metadata: ImmutableDictionary.Empty);
+
+        var response = await client.SignAsync(request, CancellationToken.None);
+
+        Assert.NotNull(response.Attestation);
+        Assert.NotNull(response.Attestation.EnvelopeDigest);
+        Assert.True(response.Diagnostics.ContainsKey("envelope"));
+    }
+
+    [Fact]
+    public async Task SignAsync_SubmitsToTransparencyLog_WhenConfigured()
+    {
+        var signer = new FakeSigner();
+        var builder = new VexDsseBuilder(signer, NullLogger.Instance);
+        var options = Options.Create(new VexAttestationClientOptions());
+        var transparency = new FakeTransparencyLogClient();
+        var client = new VexAttestationClient(builder, options, NullLogger.Instance, transparencyLogClient: transparency);
+
+        var request = new VexAttestationRequest(
+            ExportId: "exports/789",
+            QuerySignature: new VexQuerySignature("filters"),
+            Artifact: new VexContentAddress("sha256", "deadbeef"),
+            Format: VexExportFormat.Json,
+            CreatedAt: DateTimeOffset.UtcNow,
+            SourceProviders: ImmutableArray.Create("vendor"),
+            Metadata: ImmutableDictionary.Empty);
+
+        var response = await client.SignAsync(request, CancellationToken.None);
+
+        Assert.NotNull(response.Attestation.Rekor);
+        Assert.True(response.Diagnostics.ContainsKey("rekorLocation"));
+        Assert.True(transparency.SubmitCalled);
+    }
+
+    private sealed class FakeSigner : IVexSigner
+    {
+        public ValueTask SignAsync(ReadOnlyMemory payload, CancellationToken cancellationToken)
+            => ValueTask.FromResult(new VexSignedPayload("signature", "key"));
+    }
+
+    private sealed class FakeTransparencyLogClient : ITransparencyLogClient
+    {
+        public bool SubmitCalled { get; private set; }
+
+        public ValueTask SubmitAsync(DsseEnvelope envelope, CancellationToken cancellationToken)
+        {
+            SubmitCalled = true;
+            return ValueTask.FromResult(new TransparencyLogEntry(Guid.NewGuid().ToString(), "https://rekor.example/entries/123", "23", null));
+        }
+
+        public ValueTask VerifyAsync(string entryLocation, CancellationToken cancellationToken)
+            => ValueTask.FromResult(true);
+    }
+}
diff --git a/src/StellaOps.Vexer.Attestation.Tests/VexDsseBuilderTests.cs b/src/StellaOps.Vexer.Attestation.Tests/VexDsseBuilderTests.cs
new file mode 100644
index 00000000..408d5dfd
--- /dev/null
+++ b/src/StellaOps.Vexer.Attestation.Tests/VexDsseBuilderTests.cs
@@ -0,0 +1,52 @@
+using System.Collections.Immutable;
+using Microsoft.Extensions.Logging.Abstractions;
+using StellaOps.Vexer.Attestation.Dsse;
+using StellaOps.Vexer.Attestation.Models;
+using StellaOps.Vexer.Attestation.Signing;
+using StellaOps.Vexer.Core;
+
+namespace StellaOps.Vexer.Attestation.Tests;
+
+public sealed class VexDsseBuilderTests
+{
+    [Fact]
+    public async Task CreateEnvelopeAsync_ProducesDeterministicPayload()
+    {
+        var signer = new FakeSigner("signature-value", "key-1");
+        var builder = new VexDsseBuilder(signer, NullLogger.Instance);
+
+        var request = new VexAttestationRequest(
+            ExportId: "exports/123",
+            QuerySignature: new VexQuerySignature("filters"),
+            Artifact: new VexContentAddress("sha256", "deadbeef"),
+            Format: VexExportFormat.Json,
+            CreatedAt: DateTimeOffset.UtcNow,
+            SourceProviders: ImmutableArray.Create("vendor"),
+            Metadata: ImmutableDictionary.Empty);
+
+        var envelope = await builder.CreateEnvelopeAsync(request, request.Metadata, CancellationToken.None);
+
+        Assert.Equal("application/vnd.in-toto+json", envelope.PayloadType);
+        Assert.Single(envelope.Signatures);
+        Assert.Equal("signature-value", envelope.Signatures[0].Signature);
+        Assert.Equal("key-1", envelope.Signatures[0].KeyId);
+
+        var digest = VexDsseBuilder.ComputeEnvelopeDigest(envelope);
+        Assert.StartsWith("sha256:", digest);
+    }
+
+    private sealed class FakeSigner : IVexSigner
+    {
+        private readonly string _signature;
+        private readonly string _keyId;
+
+        public FakeSigner(string signature, string keyId)
+        {
+            _signature = signature;
+            _keyId = keyId;
+        }
+
+        public ValueTask SignAsync(ReadOnlyMemory payload, CancellationToken cancellationToken)
+            => ValueTask.FromResult(new VexSignedPayload(_signature, _keyId));
+    }
+}
diff --git a/src/StellaOps.Vexer.Attestation/Dsse/DsseEnvelope.cs b/src/StellaOps.Vexer.Attestation/Dsse/DsseEnvelope.cs
new file mode 100644
index 00000000..65b7f93c
--- /dev/null
+++ b/src/StellaOps.Vexer.Attestation/Dsse/DsseEnvelope.cs
@@ -0,0 +1,13 @@
+using System.Collections.Generic;
+using System.Text.Json.Serialization;
+
+namespace StellaOps.Vexer.Attestation.Dsse;
+
+public sealed record DsseEnvelope(
+    [property: JsonPropertyName("payload")] string Payload,
+    [property: JsonPropertyName("payloadType")] string PayloadType,
+    [property: JsonPropertyName("signatures")] IReadOnlyList Signatures);
+
+public sealed record DsseSignature(
+    [property: JsonPropertyName("sig")] string Signature,
+    [property: JsonPropertyName("keyid")] string? KeyId);
diff --git a/src/StellaOps.Vexer.Attestation/Dsse/VexDsseBuilder.cs b/src/StellaOps.Vexer.Attestation/Dsse/VexDsseBuilder.cs
new file mode 100644
index 00000000..64e846c0
--- /dev/null
+++ b/src/StellaOps.Vexer.Attestation/Dsse/VexDsseBuilder.cs
@@ -0,0 +1,83 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Security.Cryptography;
+using System.Text;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Logging;
+using StellaOps.Vexer.Attestation.Models;
+using StellaOps.Vexer.Attestation.Signing;
+using StellaOps.Vexer.Core;
+
+namespace StellaOps.Vexer.Attestation.Dsse;
+
+public sealed class VexDsseBuilder
+{
+    private const string PayloadType = "application/vnd.in-toto+json";
+
+    private readonly IVexSigner _signer;
+    private readonly ILogger _logger;
+    private readonly JsonSerializerOptions _serializerOptions;
+
+    public VexDsseBuilder(IVexSigner signer, ILogger logger)
+    {
+        _signer = signer ?? throw new ArgumentNullException(nameof(signer));
+        _logger = logger ?? throw new ArgumentNullException(nameof(logger));
+        _serializerOptions = new JsonSerializerOptions
+        {
+            PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
+            DefaultIgnoreCondition = JsonIgnoreCondition.Never,
+            WriteIndented = false,
+        };
+        _serializerOptions.Converters.Add(new JsonStringEnumConverter(JsonNamingPolicy.CamelCase));
+    }
+
+    public async ValueTask CreateEnvelopeAsync(
+        VexAttestationRequest request,
+        IReadOnlyDictionary? metadata,
+        CancellationToken cancellationToken)
+    {
+        ArgumentNullException.ThrowIfNull(request);
+
+        var predicate = VexAttestationPredicate.FromRequest(request, metadata);
+        var subject = new VexInTotoSubject(
+            request.ExportId,
+            new Dictionary(StringComparer.Ordinal)
+            {
+                { request.Artifact.Algorithm.ToLowerInvariant(), request.Artifact.Digest }
+            });
+
+        var statement = new VexInTotoStatement(
+            VexInTotoStatement.InTotoType,
+            "https://stella-ops.org/attestations/vex-export",
+            new[] { subject },
+            predicate);
+
+        var payloadBytes = JsonSerializer.SerializeToUtf8Bytes(statement, _serializerOptions);
+        var signatureResult = await _signer.SignAsync(payloadBytes, cancellationToken).ConfigureAwait(false);
+
+        var envelope = new DsseEnvelope(
+            Convert.ToBase64String(payloadBytes),
+            PayloadType,
+            new[] { new DsseSignature(signatureResult.Signature, signatureResult.KeyId) });
+
+        _logger.LogDebug("DSSE envelope created for export {ExportId}", request.ExportId);
+        return envelope;
+    }
+
+    public static string ComputeEnvelopeDigest(DsseEnvelope envelope)
+    {
+        ArgumentNullException.ThrowIfNull(envelope);
+        var envelopeJson = JsonSerializer.Serialize(envelope, new JsonSerializerOptions
+        {
+            PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
+            DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
+        });
+        var bytes = Encoding.UTF8.GetBytes(envelopeJson);
+        var hash = SHA256.HashData(bytes);
+        return "sha256:" + Convert.ToHexString(hash).ToLowerInvariant();
+    }
+}
diff --git a/src/StellaOps.Vexer.Attestation/Extensions/ServiceCollectionExtensions.cs b/src/StellaOps.Vexer.Attestation/Extensions/ServiceCollectionExtensions.cs
new file mode 100644
index 00000000..ac48256c
--- /dev/null
+++ b/src/StellaOps.Vexer.Attestation/Extensions/ServiceCollectionExtensions.cs
@@ -0,0 +1,24 @@
+using Microsoft.Extensions.DependencyInjection;
+using StellaOps.Vexer.Attestation.Dsse;
+using StellaOps.Vexer.Attestation.Transparency;
+using StellaOps.Vexer.Core;
+
+namespace StellaOps.Vexer.Attestation.Extensions;
+
+public static class VexAttestationServiceCollectionExtensions
+{
+    public static IServiceCollection AddVexAttestation(this IServiceCollection services)
+    {
+        services.AddSingleton();
+        services.AddSingleton();
+        return services;
+    }
+
+    public static IServiceCollection AddVexRekorClient(this IServiceCollection services, Action configure)
+    {
+        ArgumentNullException.ThrowIfNull(configure);
+        services.Configure(configure);
+        services.AddHttpClient();
+        return services;
+    }
+}
diff --git a/src/StellaOps.Vexer.Attestation/Models/VexAttestationPredicate.cs b/src/StellaOps.Vexer.Attestation/Models/VexAttestationPredicate.cs
new file mode 100644
index 00000000..c879b069
--- /dev/null
+++ b/src/StellaOps.Vexer.Attestation/Models/VexAttestationPredicate.cs
@@ -0,0 +1,44 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using System.Text.Json.Serialization;
+using StellaOps.Vexer.Core;
+
+namespace StellaOps.Vexer.Attestation.Models;
+
+public sealed record VexAttestationPredicate(
+    string ExportId,
+    string QuerySignature,
+    string ArtifactAlgorithm,
+    string ArtifactDigest,
+    VexExportFormat Format,
+    DateTimeOffset CreatedAt,
+    IReadOnlyList SourceProviders,
+    IReadOnlyDictionary Metadata)
+{
+    public static VexAttestationPredicate FromRequest(
+        VexAttestationRequest request,
+        IReadOnlyDictionary? metadata = null)
+        => new(
+            request.ExportId,
+            request.QuerySignature.Value,
+            request.Artifact.Algorithm,
+            request.Artifact.Digest,
+            request.Format,
+            request.CreatedAt,
+            request.SourceProviders,
+            metadata is null ? ImmutableDictionary.Empty : metadata.ToImmutableDictionary(StringComparer.Ordinal));
+}
+
+public sealed record VexInTotoSubject(
+    string Name,
+    IReadOnlyDictionary Digest);
+
+public sealed record VexInTotoStatement(
+    [property: JsonPropertyName("_type")] string Type,
+    string PredicateType,
+    IReadOnlyList Subject,
+    VexAttestationPredicate Predicate)
+{
+    public static readonly string InTotoType = "https://in-toto.io/Statement/v0.1";
+}
diff --git a/src/StellaOps.Vexer.Attestation/Signing/IVexSigner.cs b/src/StellaOps.Vexer.Attestation/Signing/IVexSigner.cs
new file mode 100644
index 00000000..d2371425
--- /dev/null
+++ b/src/StellaOps.Vexer.Attestation/Signing/IVexSigner.cs
@@ -0,0 +1,12 @@
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace StellaOps.Vexer.Attestation.Signing;
+
+public sealed record VexSignedPayload(string Signature, string? KeyId);
+
+public interface IVexSigner
+{
+    ValueTask SignAsync(ReadOnlyMemory payload, CancellationToken cancellationToken);
+}
diff --git a/src/StellaOps.Vexer.Attestation/StellaOps.Vexer.Attestation.csproj b/src/StellaOps.Vexer.Attestation/StellaOps.Vexer.Attestation.csproj
new file mode 100644
index 00000000..8874903c
--- /dev/null
+++ b/src/StellaOps.Vexer.Attestation/StellaOps.Vexer.Attestation.csproj
@@ -0,0 +1,17 @@
+
+  
+    net10.0
+    preview
+    enable
+    enable
+    true
+  
+  
+    
+    
+    
+  
+  
+    
+  
+
diff --git a/src/StellaOps.Vexer.Attestation/TASKS.md b/src/StellaOps.Vexer.Attestation/TASKS.md
index 3bfb0e8a..7f318efd 100644
--- a/src/StellaOps.Vexer.Attestation/TASKS.md
+++ b/src/StellaOps.Vexer.Attestation/TASKS.md
@@ -2,6 +2,6 @@ If you are working on this file you need to read docs/ARCHITECTURE_VEXER.md and
 # TASKS
 | Task | Owner(s) | Depends on | Notes |
 |---|---|---|---|
-|VEXER-ATTEST-01-001 – In-toto predicate & DSSE builder|Team Vexer Attestation|VEXER-CORE-01-001|TODO – Implement export attestation predicates and DSSE envelope builder with deterministic hashing and signer abstraction.|
-|VEXER-ATTEST-01-002 – Rekor v2 client integration|Team Vexer Attestation|VEXER-ATTEST-01-001|TODO – Provide `ITransparencyLogClient` with submit/verify operations, retries, and offline queue fallback matching architecture guidance.|
+|VEXER-ATTEST-01-001 – In-toto predicate & DSSE builder|Team Vexer Attestation|VEXER-CORE-01-001|**DONE (2025-10-16)** – Added deterministic in-toto predicate/statement models, DSSE envelope builder wired to signer abstraction, and attestation client producing metadata + diagnostics.|
+|VEXER-ATTEST-01-002 – Rekor v2 client integration|Team Vexer Attestation|VEXER-ATTEST-01-001|**DONE (2025-10-16)** – Implemented Rekor HTTP client with retry/backoff, transparency log abstraction, DI helpers, and attestation client integration capturing Rekor metadata + diagnostics.|
 |VEXER-ATTEST-01-003 – Verification suite & observability|Team Vexer Attestation|VEXER-ATTEST-01-002|TODO – Add verification helpers for Worker/WebService, metrics/logging hooks, and negative-path regression tests.|
diff --git a/src/StellaOps.Vexer.Attestation/Transparency/ITransparencyLogClient.cs b/src/StellaOps.Vexer.Attestation/Transparency/ITransparencyLogClient.cs
new file mode 100644
index 00000000..de87adc9
--- /dev/null
+++ b/src/StellaOps.Vexer.Attestation/Transparency/ITransparencyLogClient.cs
@@ -0,0 +1,14 @@
+using System.Threading;
+using System.Threading.Tasks;
+using StellaOps.Vexer.Attestation.Dsse;
+
+namespace StellaOps.Vexer.Attestation.Transparency;
+
+public sealed record TransparencyLogEntry(string Id, string Location, string? LogIndex, string? InclusionProofUrl);
+
+public interface ITransparencyLogClient
+{
+    ValueTask SubmitAsync(DsseEnvelope envelope, CancellationToken cancellationToken);
+
+    ValueTask VerifyAsync(string entryLocation, CancellationToken cancellationToken);
+}
diff --git a/src/StellaOps.Vexer.Attestation/Transparency/RekorHttpClient.cs b/src/StellaOps.Vexer.Attestation/Transparency/RekorHttpClient.cs
new file mode 100644
index 00000000..e851f82a
--- /dev/null
+++ b/src/StellaOps.Vexer.Attestation/Transparency/RekorHttpClient.cs
@@ -0,0 +1,91 @@
+using System.Net.Http.Json;
+using System.Text.Json;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+using StellaOps.Vexer.Attestation.Dsse;
+
+namespace StellaOps.Vexer.Attestation.Transparency;
+
+internal sealed class RekorHttpClient : ITransparencyLogClient
+{
+    private readonly HttpClient _httpClient;
+    private readonly RekorHttpClientOptions _options;
+    private readonly ILogger _logger;
+
+    public RekorHttpClient(HttpClient httpClient, IOptions options, ILogger logger)
+    {
+        _httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
+        ArgumentNullException.ThrowIfNull(options);
+        _options = options.Value;
+        _logger = logger ?? throw new ArgumentNullException(nameof(logger));
+
+        if (!string.IsNullOrWhiteSpace(_options.BaseAddress))
+        {
+            _httpClient.BaseAddress = new Uri(_options.BaseAddress, UriKind.Absolute);
+        }
+
+        if (!string.IsNullOrWhiteSpace(_options.ApiKey))
+        {
+            _httpClient.DefaultRequestHeaders.Add("Authorization", _options.ApiKey);
+        }
+    }
+
+    public async ValueTask SubmitAsync(DsseEnvelope envelope, CancellationToken cancellationToken)
+    {
+        ArgumentNullException.ThrowIfNull(envelope);
+        var payload = JsonSerializer.Serialize(envelope);
+        using var content = new StringContent(payload);
+        content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/json");
+
+        HttpResponseMessage? response = null;
+        for (var attempt = 0; attempt < _options.RetryCount; attempt++)
+        {
+            response = await _httpClient.PostAsync("/api/v2/log/entries", content, cancellationToken).ConfigureAwait(false);
+            if (response.IsSuccessStatusCode)
+            {
+                break;
+            }
+
+            _logger.LogWarning("Rekor submission failed with status {Status}; attempt {Attempt}", response.StatusCode, attempt + 1);
+            if (attempt + 1 < _options.RetryCount)
+            {
+                await Task.Delay(_options.RetryDelay, cancellationToken).ConfigureAwait(false);
+            }
+        }
+
+        if (response is null || !response.IsSuccessStatusCode)
+        {
+            throw new HttpRequestException($"Failed to submit attestation to Rekor ({response?.StatusCode}).");
+        }
+
+        var entryLocation = response.Headers.Location?.ToString() ?? string.Empty;
+        var body = await response.Content.ReadFromJsonAsync(cancellationToken: cancellationToken).ConfigureAwait(false);
+        var entry = ParseEntryLocation(entryLocation, body);
+        _logger.LogInformation("Rekor entry recorded at {Location}", entry.Location);
+        return entry;
+    }
+
+    public async ValueTask VerifyAsync(string entryLocation, CancellationToken cancellationToken)
+    {
+        if (string.IsNullOrWhiteSpace(entryLocation))
+        {
+            return false;
+        }
+
+        var response = await _httpClient.GetAsync(entryLocation, cancellationToken).ConfigureAwait(false);
+        return response.IsSuccessStatusCode;
+    }
+
+    private static TransparencyLogEntry ParseEntryLocation(string location, JsonElement body)
+    {
+        var id = body.TryGetProperty("uuid", out var uuid) ? uuid.GetString() ?? string.Empty : Guid.NewGuid().ToString();
+        var logIndex = body.TryGetProperty("logIndex", out var logIndexElement) ? logIndexElement.GetString() : null;
+        string? inclusionProof = null;
+        if (body.TryGetProperty("verification", out var verification) && verification.TryGetProperty("inclusionProof", out var inclusion))
+        {
+            inclusionProof = inclusion.GetProperty("logIndex").GetRawText();
+        }
+
+        return new TransparencyLogEntry(id, location, logIndex, inclusionProof);
+    }
+}
diff --git a/src/StellaOps.Vexer.Attestation/Transparency/RekorHttpClientOptions.cs b/src/StellaOps.Vexer.Attestation/Transparency/RekorHttpClientOptions.cs
new file mode 100644
index 00000000..1565e260
--- /dev/null
+++ b/src/StellaOps.Vexer.Attestation/Transparency/RekorHttpClientOptions.cs
@@ -0,0 +1,13 @@
+namespace StellaOps.Vexer.Attestation.Transparency;
+
+public sealed class RekorHttpClientOptions
+{
+    public string BaseAddress { get; set; } = "https://rekor.sigstore.dev";
+
+    public string? ApiKey { get; set; }
+        = null;
+
+    public int RetryCount { get; set; } = 3;
+
+    public TimeSpan RetryDelay { get; set; } = TimeSpan.FromSeconds(2);
+}
diff --git a/src/StellaOps.Vexer.Attestation/VexAttestationClient.cs b/src/StellaOps.Vexer.Attestation/VexAttestationClient.cs
new file mode 100644
index 00000000..94f6f214
--- /dev/null
+++ b/src/StellaOps.Vexer.Attestation/VexAttestationClient.cs
@@ -0,0 +1,108 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using System.Text.Json;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+using StellaOps.Vexer.Attestation.Dsse;
+using StellaOps.Vexer.Attestation.Models;
+using StellaOps.Vexer.Attestation.Signing;
+using StellaOps.Vexer.Attestation.Transparency;
+using StellaOps.Vexer.Core;
+
+namespace StellaOps.Vexer.Attestation;
+
+public sealed class VexAttestationClientOptions
+{
+    public IReadOnlyDictionary DefaultMetadata { get; set; } = ImmutableDictionary.Empty;
+}
+
+public sealed class VexAttestationClient : IVexAttestationClient
+{
+    private readonly VexDsseBuilder _builder;
+    private readonly ILogger _logger;
+    private readonly TimeProvider _timeProvider;
+    private readonly IReadOnlyDictionary _defaultMetadata;
+    private readonly ITransparencyLogClient? _transparencyLogClient;
+
+    public VexAttestationClient(
+        VexDsseBuilder builder,
+        IOptions options,
+        ILogger logger,
+        TimeProvider? timeProvider = null,
+        ITransparencyLogClient? transparencyLogClient = null)
+    {
+        _builder = builder ?? throw new ArgumentNullException(nameof(builder));
+        ArgumentNullException.ThrowIfNull(options);
+        _logger = logger ?? throw new ArgumentNullException(nameof(logger));
+        _timeProvider = timeProvider ?? TimeProvider.System;
+        _defaultMetadata = options.Value.DefaultMetadata;
+        _transparencyLogClient = transparencyLogClient;
+    }
+
+    public async ValueTask SignAsync(VexAttestationRequest request, CancellationToken cancellationToken)
+    {
+        ArgumentNullException.ThrowIfNull(request);
+        var mergedMetadata = MergeMetadata(request.Metadata, _defaultMetadata);
+
+        var envelope = await _builder.CreateEnvelopeAsync(request, mergedMetadata, cancellationToken).ConfigureAwait(false);
+        var envelopeDigest = VexDsseBuilder.ComputeEnvelopeDigest(envelope);
+        var signedAt = _timeProvider.GetUtcNow();
+
+        var diagnosticsBuilder = ImmutableDictionary.Empty
+            .Add("envelope", JsonSerializer.Serialize(envelope))
+            .Add("predicateType", "https://stella-ops.org/attestations/vex-export");
+
+        VexRekorReference? rekorReference = null;
+        if (_transparencyLogClient is not null)
+        {
+            try
+            {
+                var entry = await _transparencyLogClient.SubmitAsync(envelope, cancellationToken).ConfigureAwait(false);
+                rekorReference = new VexRekorReference("0.2", entry.Location, entry.LogIndex, entry.InclusionProofUrl is not null ? new Uri(entry.InclusionProofUrl) : null);
+                diagnosticsBuilder = diagnosticsBuilder.Add("rekorLocation", entry.Location);
+            }
+            catch (Exception ex)
+            {
+                _logger.LogError(ex, "Failed to submit attestation to Rekor transparency log");
+                throw;
+            }
+        }
+
+        var metadata = new VexAttestationMetadata(
+            predicateType: "https://stella-ops.org/attestations/vex-export",
+            rekor: rekorReference,
+            envelopeDigest: envelopeDigest,
+            signedAt: signedAt);
+
+        _logger.LogInformation("Generated DSSE envelope for export {ExportId} ({Digest})", request.ExportId, envelopeDigest);
+
+        return new VexAttestationResponse(metadata, diagnosticsBuilder);
+    }
+
+    public ValueTask VerifyAsync(VexAttestationRequest request, CancellationToken cancellationToken)
+    {
+        // Placeholder until verification flow is implemented in VEXER-ATTEST-01-003.
+        return ValueTask.FromResult(new VexAttestationVerification(true, ImmutableDictionary.Empty));
+    }
+
+    private static IReadOnlyDictionary MergeMetadata(
+        IReadOnlyDictionary requestMetadata,
+        IReadOnlyDictionary defaults)
+    {
+        if (defaults.Count == 0)
+        {
+            return requestMetadata;
+        }
+
+        var merged = new Dictionary(defaults, StringComparer.Ordinal);
+        foreach (var kvp in requestMetadata)
+        {
+            merged[kvp.Key] = kvp.Value;
+        }
+
+        return merged.ToImmutableDictionary(StringComparer.Ordinal);
+    }
+}
diff --git a/src/StellaOps.Vexer.Core.Tests/StellaOps.Vexer.Core.Tests.csproj b/src/StellaOps.Vexer.Core.Tests/StellaOps.Vexer.Core.Tests.csproj
index 9fed3419..1d4ebbf1 100644
--- a/src/StellaOps.Vexer.Core.Tests/StellaOps.Vexer.Core.Tests.csproj
+++ b/src/StellaOps.Vexer.Core.Tests/StellaOps.Vexer.Core.Tests.csproj
@@ -8,5 +8,6 @@
   
   
     
+    
   
 
diff --git a/src/StellaOps.Vexer.Core.Tests/VexPolicyBinderTests.cs b/src/StellaOps.Vexer.Core.Tests/VexPolicyBinderTests.cs
new file mode 100644
index 00000000..9f48ffa5
--- /dev/null
+++ b/src/StellaOps.Vexer.Core.Tests/VexPolicyBinderTests.cs
@@ -0,0 +1,83 @@
+using System;
+using System.IO;
+using System.Text;
+using StellaOps.Vexer.Policy;
+
+namespace StellaOps.Vexer.Core.Tests;
+
+public sealed class VexPolicyBinderTests
+{
+    private const string JsonPolicy = """
+    {
+      "version": "custom/v2",
+      "weights": {
+        "vendor": 0.95,
+        "distro": 0.85
+      },
+      "providerOverrides": {
+        "provider.example": 0.5
+      }
+    }
+    """;
+
+    private const string YamlPolicy = """
+    version: custom/v3
+    weights:
+      vendor: 0.8
+      distro: 0.7
+      platform: 0.6
+    providerOverrides:
+      provider-a: 0.4
+      provider-b: 0.3
+    """;
+
+    [Fact]
+    public void Bind_Json_ReturnsNormalizedOptions()
+    {
+        var result = VexPolicyBinder.Bind(JsonPolicy, VexPolicyDocumentFormat.Json);
+
+        Assert.True(result.Success);
+        Assert.NotNull(result.Options);
+        Assert.NotNull(result.NormalizedOptions);
+        Assert.Equal("custom/v2", result.Options!.Version);
+        Assert.Equal("custom/v2", result.NormalizedOptions!.Version);
+        Assert.Empty(result.Issues);
+    }
+
+    [Fact]
+    public void Bind_Yaml_ReturnsOverridesAndWarningsSorted()
+    {
+        var result = VexPolicyBinder.Bind(YamlPolicy, VexPolicyDocumentFormat.Yaml);
+
+        Assert.True(result.Success);
+        Assert.NotNull(result.NormalizedOptions);
+        var overrides = result.NormalizedOptions!.ProviderOverrides;
+        Assert.Equal(2, overrides.Count);
+        Assert.Equal(0.4, overrides["provider-a"]);
+        Assert.Equal(0.3, overrides["provider-b"]);
+        Assert.Empty(result.Issues);
+    }
+
+    [Fact]
+    public void Bind_InvalidJson_ReturnsError()
+    {
+        const string invalidJson = "{ \"weights\": { \"vendor\": \"not-a-number\" }";
+
+        var result = VexPolicyBinder.Bind(invalidJson, VexPolicyDocumentFormat.Json);
+
+        Assert.False(result.Success);
+        var issue = Assert.Single(result.Issues);
+        Assert.Equal(VexPolicyIssueSeverity.Error, issue.Severity);
+        Assert.StartsWith("policy.parse.json", issue.Code, StringComparison.Ordinal);
+    }
+
+    [Fact]
+    public void Bind_Stream_SupportsEncoding()
+    {
+        using var stream = new MemoryStream(Encoding.UTF8.GetBytes(JsonPolicy));
+        var result = VexPolicyBinder.Bind(stream, VexPolicyDocumentFormat.Json);
+
+        Assert.True(result.Success);
+        Assert.NotNull(result.Options);
+    }
+}
diff --git a/src/StellaOps.Vexer.Core.Tests/VexPolicyDiagnosticsTests.cs b/src/StellaOps.Vexer.Core.Tests/VexPolicyDiagnosticsTests.cs
new file mode 100644
index 00000000..06971d3d
--- /dev/null
+++ b/src/StellaOps.Vexer.Core.Tests/VexPolicyDiagnosticsTests.cs
@@ -0,0 +1,169 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using System.Linq;
+using Microsoft.Extensions.Logging.Abstractions;
+using Microsoft.Extensions.Options;
+using Microsoft.Extensions.Time.Testing;
+using StellaOps.Vexer.Core;
+using StellaOps.Vexer.Policy;
+using System.Diagnostics.Metrics;
+
+namespace StellaOps.Vexer.Core.Tests;
+
+public class VexPolicyDiagnosticsTests
+{
+    [Fact]
+    public void GetDiagnostics_ReportsCountsRecommendationsAndOverrides()
+    {
+        var overrides = new[]
+        {
+            new KeyValuePair("provider-a", 0.8),
+            new KeyValuePair("provider-b", 0.6),
+        };
+
+        var snapshot = new VexPolicySnapshot(
+            "custom/v1",
+            new VexConsensusPolicyOptions(
+                version: "custom/v1",
+                providerOverrides: overrides),
+            new BaselineVexConsensusPolicy(),
+            ImmutableArray.Create(
+                new VexPolicyIssue("sample.error", "Blocking issue.", VexPolicyIssueSeverity.Error),
+                new VexPolicyIssue("sample.warning", "Non-blocking issue.", VexPolicyIssueSeverity.Warning)),
+            "rev-test",
+            "ABCDEF");
+
+        var fakeProvider = new FakePolicyProvider(snapshot);
+        var fakeTime = new FakeTimeProvider(new DateTimeOffset(2025, 10, 16, 17, 0, 0, TimeSpan.Zero));
+        var diagnostics = new VexPolicyDiagnostics(fakeProvider, fakeTime);
+
+        var report = diagnostics.GetDiagnostics();
+
+        Assert.Equal("custom/v1", report.Version);
+        Assert.Equal("rev-test", report.RevisionId);
+        Assert.Equal("ABCDEF", report.Digest);
+        Assert.Equal(1, report.ErrorCount);
+        Assert.Equal(1, report.WarningCount);
+        Assert.Equal(fakeTime.GetUtcNow(), report.GeneratedAt);
+        Assert.Collection(report.Issues,
+            issue => Assert.Equal("sample.error", issue.Code),
+            issue => Assert.Equal("sample.warning", issue.Code));
+        Assert.Equal(new[] { "provider-a", "provider-b" }, report.ActiveOverrides.Keys.OrderBy(static key => key, StringComparer.Ordinal));
+        Assert.Contains(report.Recommendations, message => message.Contains("Resolve policy errors", StringComparison.OrdinalIgnoreCase));
+        Assert.Contains(report.Recommendations, message => message.Contains("provider-a", StringComparison.OrdinalIgnoreCase));
+        Assert.Contains(report.Recommendations, message => message.Contains("docs/ARCHITECTURE_VEXER.md", StringComparison.OrdinalIgnoreCase));
+    }
+
+    [Fact]
+    public void GetDiagnostics_WhenNoIssues_StillReturnsDefaultRecommendation()
+    {
+        var fakeProvider = new FakePolicyProvider(VexPolicySnapshot.Default);
+        var fakeTime = new FakeTimeProvider(new DateTimeOffset(2025, 10, 16, 17, 0, 0, TimeSpan.Zero));
+        var diagnostics = new VexPolicyDiagnostics(fakeProvider, fakeTime);
+
+        var report = diagnostics.GetDiagnostics();
+
+        Assert.Equal(0, report.ErrorCount);
+        Assert.Equal(0, report.WarningCount);
+        Assert.Empty(report.ActiveOverrides);
+        Assert.Single(report.Recommendations);
+    }
+
+    [Fact]
+    public void PolicyProvider_ComputesRevisionAndDigest_AndEmitsTelemetry()
+    {
+        using var listener = new MeterListener();
+        var reloadMeasurements = 0;
+        string? lastRevision = null;
+        listener.InstrumentPublished += (instrument, _) =>
+        {
+            if (instrument.Meter.Name == "StellaOps.Vexer.Policy" &&
+                instrument.Name == "vex.policy.reloads")
+            {
+                listener.EnableMeasurementEvents(instrument);
+            }
+        };
+
+        listener.SetMeasurementEventCallback((instrument, measurement, tags, state) =>
+        {
+            reloadMeasurements++;
+            foreach (var tag in tags)
+            {
+                if (tag.Key is "revision" && tag.Value is string revision)
+                {
+                    lastRevision = revision;
+                    break;
+                }
+            }
+        });
+
+        listener.Start();
+
+        var optionsMonitor = new MutableOptionsMonitor(new VexPolicyOptions());
+        var provider = new VexPolicyProvider(optionsMonitor, NullLogger.Instance);
+
+        var snapshot1 = provider.GetSnapshot();
+        Assert.Equal("rev-1", snapshot1.RevisionId);
+        Assert.False(string.IsNullOrWhiteSpace(snapshot1.Digest));
+
+        var snapshot2 = provider.GetSnapshot();
+        Assert.Equal("rev-1", snapshot2.RevisionId);
+        Assert.Equal(snapshot1.Digest, snapshot2.Digest);
+
+        optionsMonitor.Update(new VexPolicyOptions
+        {
+            ProviderOverrides = new Dictionary
+            {
+                ["provider-a"] = 0.4
+            }
+        });
+
+        var snapshot3 = provider.GetSnapshot();
+        Assert.Equal("rev-2", snapshot3.RevisionId);
+        Assert.NotEqual(snapshot1.Digest, snapshot3.Digest);
+
+        listener.Dispose();
+
+        Assert.True(reloadMeasurements >= 2);
+        Assert.Equal("rev-2", lastRevision);
+    }
+
+    private sealed class FakePolicyProvider : IVexPolicyProvider
+    {
+        private readonly VexPolicySnapshot _snapshot;
+
+        public FakePolicyProvider(VexPolicySnapshot snapshot)
+        {
+            _snapshot = snapshot;
+        }
+
+        public VexPolicySnapshot GetSnapshot() => _snapshot;
+    }
+
+    private sealed class MutableOptionsMonitor : IOptionsMonitor
+    {
+        private T _value;
+
+        public MutableOptionsMonitor(T value)
+        {
+            _value = value;
+        }
+
+        public T CurrentValue => _value;
+
+        public T Get(string? name) => _value;
+
+        public void Update(T newValue) => _value = newValue;
+
+        public IDisposable OnChange(Action listener) => NullDisposable.Instance;
+
+        private sealed class NullDisposable : IDisposable
+        {
+            public static readonly NullDisposable Instance = new();
+            public void Dispose()
+            {
+            }
+        }
+    }
+}
diff --git a/src/StellaOps.Vexer.Core/TASKS.md b/src/StellaOps.Vexer.Core/TASKS.md
index 8dc56f9f..4ed131d7 100644
--- a/src/StellaOps.Vexer.Core/TASKS.md
+++ b/src/StellaOps.Vexer.Core/TASKS.md
@@ -5,3 +5,5 @@ If you are working on this file you need to read docs/ARCHITECTURE_VEXER.md and
 |VEXER-CORE-01-001 – Canonical VEX domain records|Team Vexer Core & Policy|docs/ARCHITECTURE_VEXER.md|DONE (2025-10-15) – Introduced `VexClaim`, `VexConsensus`, provider metadata, export manifest records, and deterministic JSON serialization with tests covering canonical ordering and query signatures.|
 |VEXER-CORE-01-002 – Trust-weighted consensus resolver|Team Vexer Core & Policy|VEXER-CORE-01-001|DONE (2025-10-15) – Added consensus resolver, baseline policy (tier weights + justification gate), telemetry output, and tests covering acceptance, conflict ties, and determinism.|
 |VEXER-CORE-01-003 – Shared contracts & query signatures|Team Vexer Core & Policy|VEXER-CORE-01-001|DONE (2025-10-15) – Published connector/normalizer/exporter/attestation abstractions and expanded deterministic `VexQuerySignature`/hash utilities with test coverage.|
+|VEXER-CORE-02-001 – Context signal schema prep|Team Vexer Core & Policy|VEXER-POLICY-02-001|TODO – Extend `VexClaim`/`VexConsensus` with optional severity/KEV/EPSS payloads, update canonical serializer/hashes, and coordinate migration notes with Storage.|
+|VEXER-CORE-02-002 – Deterministic risk scoring engine|Team Vexer Core & Policy|VEXER-CORE-02-001, VEXER-POLICY-02-001|BACKLOG – Introduce the scoring calculator invoked by consensus, persist score envelopes with audit trails, and add regression fixtures covering gate/boost behaviour before enabling exports.|
diff --git a/src/StellaOps.Vexer.Core/VexAttestationAbstractions.cs b/src/StellaOps.Vexer.Core/VexAttestationAbstractions.cs
index e524784b..8c8a8fe0 100644
--- a/src/StellaOps.Vexer.Core/VexAttestationAbstractions.cs
+++ b/src/StellaOps.Vexer.Core/VexAttestationAbstractions.cs
@@ -13,10 +13,12 @@ public interface IVexAttestationClient
 }
 
 public sealed record VexAttestationRequest(
+    string ExportId,
     VexQuerySignature QuerySignature,
     VexContentAddress Artifact,
     VexExportFormat Format,
     DateTimeOffset CreatedAt,
+    ImmutableArray SourceProviders,
     ImmutableDictionary Metadata);
 
 public sealed record VexAttestationResponse(
diff --git a/src/StellaOps.Vexer.Core/VexCacheEntry.cs b/src/StellaOps.Vexer.Core/VexCacheEntry.cs
new file mode 100644
index 00000000..3fec0666
--- /dev/null
+++ b/src/StellaOps.Vexer.Core/VexCacheEntry.cs
@@ -0,0 +1,56 @@
+using System;
+
+namespace StellaOps.Vexer.Core;
+
+/// 
+/// Cached export artifact metadata allowing reuse of previously generated manifests.
+/// 
+public sealed class VexCacheEntry
+{
+    public VexCacheEntry(
+        VexQuerySignature querySignature,
+        VexExportFormat format,
+        VexContentAddress artifact,
+        DateTimeOffset createdAt,
+        long sizeBytes,
+        string? manifestId = null,
+        string? gridFsObjectId = null,
+        DateTimeOffset? expiresAt = null)
+    {
+        QuerySignature = querySignature ?? throw new ArgumentNullException(nameof(querySignature));
+        Artifact = artifact ?? throw new ArgumentNullException(nameof(artifact));
+        Format = format;
+        CreatedAt = createdAt;
+        SizeBytes = sizeBytes >= 0
+            ? sizeBytes
+            : throw new ArgumentOutOfRangeException(nameof(sizeBytes), sizeBytes, "Size must be non-negative.");
+        ManifestId = Normalize(manifestId);
+        GridFsObjectId = Normalize(gridFsObjectId);
+
+        if (expiresAt.HasValue && expiresAt.Value < createdAt)
+        {
+            throw new ArgumentOutOfRangeException(nameof(expiresAt), expiresAt, "Expiration cannot be before creation.");
+        }
+
+        ExpiresAt = expiresAt;
+    }
+
+    public VexQuerySignature QuerySignature { get; }
+
+    public VexExportFormat Format { get; }
+
+    public VexContentAddress Artifact { get; }
+
+    public DateTimeOffset CreatedAt { get; }
+
+    public long SizeBytes { get; }
+
+    public string? ManifestId { get; }
+
+    public string? GridFsObjectId { get; }
+
+    public DateTimeOffset? ExpiresAt { get; }
+
+    private static string? Normalize(string? value)
+        => string.IsNullOrWhiteSpace(value) ? null : value.Trim();
+}
diff --git a/src/StellaOps.Vexer.Core/VexConsensus.cs b/src/StellaOps.Vexer.Core/VexConsensus.cs
index 222d33e4..a6217ed1 100644
--- a/src/StellaOps.Vexer.Core/VexConsensus.cs
+++ b/src/StellaOps.Vexer.Core/VexConsensus.cs
@@ -13,7 +13,9 @@ public sealed record VexConsensus
         IEnumerable sources,
         IEnumerable? conflicts = null,
         string? policyVersion = null,
-        string? summary = null)
+        string? summary = null,
+        string? policyRevisionId = null,
+        string? policyDigest = null)
     {
         if (string.IsNullOrWhiteSpace(vulnerabilityId))
         {
@@ -28,6 +30,8 @@ public sealed record VexConsensus
         Conflicts = NormalizeConflicts(conflicts);
         PolicyVersion = string.IsNullOrWhiteSpace(policyVersion) ? null : policyVersion.Trim();
         Summary = string.IsNullOrWhiteSpace(summary) ? null : summary.Trim();
+        PolicyRevisionId = string.IsNullOrWhiteSpace(policyRevisionId) ? null : policyRevisionId.Trim();
+        PolicyDigest = string.IsNullOrWhiteSpace(policyDigest) ? null : policyDigest.Trim();
     }
 
     public string VulnerabilityId { get; }
@@ -46,6 +50,10 @@ public sealed record VexConsensus
 
     public string? Summary { get; }
 
+    public string? PolicyRevisionId { get; }
+
+    public string? PolicyDigest { get; }
+
     private static ImmutableArray NormalizeSources(IEnumerable sources)
     {
         if (sources is null)
diff --git a/src/StellaOps.Vexer.Core/VexConsensusResolver.cs b/src/StellaOps.Vexer.Core/VexConsensusResolver.cs
index d9bd7f18..71e8f7ca 100644
--- a/src/StellaOps.Vexer.Core/VexConsensusResolver.cs
+++ b/src/StellaOps.Vexer.Core/VexConsensusResolver.cs
@@ -106,7 +106,9 @@ public sealed class VexConsensusResolver
             acceptedSources,
             AttachConflictDetails(conflicts, acceptedSources, consensusStatus, conflictKeys),
             _policy.Version,
-            summary);
+            summary,
+            request.PolicyRevisionId,
+            request.PolicyDigest);
 
         return new VexConsensusResolution(consensus, decisions.ToImmutable());
     }
@@ -272,7 +274,9 @@ public sealed record VexConsensusRequest(
     VexProduct Product,
     IReadOnlyList Claims,
     IReadOnlyDictionary Providers,
-    DateTimeOffset CalculatedAt);
+    DateTimeOffset CalculatedAt,
+    string? PolicyRevisionId = null,
+    string? PolicyDigest = null);
 
 public sealed record VexConsensusResolution(
     VexConsensus Consensus,
diff --git a/src/StellaOps.Vexer.Export.Tests/ExportEngineTests.cs b/src/StellaOps.Vexer.Export.Tests/ExportEngineTests.cs
index 511eaf0b..fc917b06 100644
--- a/src/StellaOps.Vexer.Export.Tests/ExportEngineTests.cs
+++ b/src/StellaOps.Vexer.Export.Tests/ExportEngineTests.cs
@@ -38,6 +38,56 @@ public sealed class ExportEngineTests
         Assert.Equal(manifest.ExportId, cached.ExportId);
     }
 
+    [Fact]
+    public async Task ExportAsync_ForceRefreshInvalidatesCacheEntry()
+    {
+        var store = new InMemoryExportStore();
+        var evaluator = new StaticPolicyEvaluator("baseline/v1");
+        var dataSource = new InMemoryExportDataSource();
+        var exporter = new DummyExporter(VexExportFormat.Json);
+        var cacheIndex = new RecordingCacheIndex();
+        var engine = new VexExportEngine(store, evaluator, dataSource, new[] { exporter }, NullLogger.Instance, cacheIndex);
+
+        var query = VexQuery.Create(new[] { new VexQueryFilter("vulnId", "CVE-2025-0001") });
+        var initialContext = new VexExportRequestContext(query, VexExportFormat.Json, DateTimeOffset.UtcNow);
+        _ = await engine.ExportAsync(initialContext, CancellationToken.None);
+
+        var refreshContext = new VexExportRequestContext(query, VexExportFormat.Json, DateTimeOffset.UtcNow.AddMinutes(1), ForceRefresh: true);
+        var refreshed = await engine.ExportAsync(refreshContext, CancellationToken.None);
+
+        Assert.False(refreshed.FromCache);
+        var signature = VexQuerySignature.FromQuery(refreshContext.Query);
+        Assert.True(cacheIndex.RemoveCalls.TryGetValue((signature.Value, refreshContext.Format), out var removed));
+        Assert.True(removed);
+    }
+
+    [Fact]
+    public async Task ExportAsync_WritesArtifactsToAllStores()
+    {
+        var store = new InMemoryExportStore();
+        var evaluator = new StaticPolicyEvaluator("baseline/v1");
+        var dataSource = new InMemoryExportDataSource();
+        var exporter = new DummyExporter(VexExportFormat.Json);
+        var recorder1 = new RecordingArtifactStore();
+        var recorder2 = new RecordingArtifactStore();
+        var engine = new VexExportEngine(
+            store,
+            evaluator,
+            dataSource,
+            new[] { exporter },
+            NullLogger.Instance,
+            cacheIndex: null,
+            artifactStores: new[] { recorder1, recorder2 });
+
+        var query = VexQuery.Create(new[] { new VexQueryFilter("vulnId", "CVE-2025-0001") });
+        var context = new VexExportRequestContext(query, VexExportFormat.Json, DateTimeOffset.UtcNow);
+
+        await engine.ExportAsync(context, CancellationToken.None);
+
+        Assert.Equal(1, recorder1.SaveCount);
+        Assert.Equal(1, recorder2.SaveCount);
+    }
+
     private sealed class InMemoryExportStore : IVexExportStore
     {
         private readonly Dictionary _store = new(StringComparer.Ordinal);
@@ -60,6 +110,40 @@ public sealed class ExportEngineTests
             => FormattableString.Invariant($"{signature}|{format}");
     }
 
+    private sealed class RecordingCacheIndex : IVexCacheIndex
+    {
+        public Dictionary<(string Signature, VexExportFormat Format), bool> RemoveCalls { get; } = new();
+
+        public ValueTask FindAsync(VexQuerySignature signature, VexExportFormat format, CancellationToken cancellationToken)
+            => ValueTask.FromResult(null);
+
+        public ValueTask SaveAsync(VexCacheEntry entry, CancellationToken cancellationToken)
+            => ValueTask.CompletedTask;
+
+        public ValueTask RemoveAsync(VexQuerySignature signature, VexExportFormat format, CancellationToken cancellationToken)
+        {
+            RemoveCalls[(signature.Value, format)] = true;
+            return ValueTask.CompletedTask;
+        }
+    }
+
+    private sealed class RecordingArtifactStore : IVexArtifactStore
+    {
+        public int SaveCount { get; private set; }
+
+        public ValueTask SaveAsync(VexExportArtifact artifact, CancellationToken cancellationToken)
+        {
+            SaveCount++;
+            return ValueTask.FromResult(new VexStoredArtifact(artifact.ContentAddress, "memory", artifact.Content.Length, artifact.Metadata));
+        }
+
+        public ValueTask DeleteAsync(VexContentAddress contentAddress, CancellationToken cancellationToken)
+            => ValueTask.CompletedTask;
+
+        public ValueTask OpenReadAsync(VexContentAddress contentAddress, CancellationToken cancellationToken)
+            => ValueTask.FromResult(null);
+    }
+
     private sealed class StaticPolicyEvaluator : IVexPolicyEvaluator
     {
         public StaticPolicyEvaluator(string version)
diff --git a/src/StellaOps.Vexer.Export.Tests/FileSystemArtifactStoreTests.cs b/src/StellaOps.Vexer.Export.Tests/FileSystemArtifactStoreTests.cs
new file mode 100644
index 00000000..b7cc8522
--- /dev/null
+++ b/src/StellaOps.Vexer.Export.Tests/FileSystemArtifactStoreTests.cs
@@ -0,0 +1,33 @@
+using System.Collections.Immutable;
+using Microsoft.Extensions.Logging.Abstractions;
+using Microsoft.Extensions.Options;
+using StellaOps.Vexer.Core;
+using StellaOps.Vexer.Export;
+using System.IO.Abstractions.TestingHelpers;
+
+namespace StellaOps.Vexer.Export.Tests;
+
+public sealed class FileSystemArtifactStoreTests
+{
+    [Fact]
+    public async Task SaveAsync_WritesArtifactToDisk()
+    {
+        var fs = new MockFileSystem();
+        var options = Options.Create(new FileSystemArtifactStoreOptions { RootPath = "/exports" });
+        var store = new FileSystemArtifactStore(options, NullLogger.Instance, fs);
+
+        var content = new byte[] { 1, 2, 3 };
+        var artifact = new VexExportArtifact(
+            new VexContentAddress("sha256", "deadbeef"),
+            VexExportFormat.Json,
+            content,
+            ImmutableDictionary.Empty);
+
+        var stored = await store.SaveAsync(artifact, CancellationToken.None);
+
+        Assert.Equal(artifact.Content.Length, stored.SizeBytes);
+        var filePath = fs.Path.Combine(options.Value.RootPath, stored.Location);
+        Assert.True(fs.FileExists(filePath));
+        Assert.Equal(content, fs.File.ReadAllBytes(filePath));
+    }
+}
diff --git a/src/StellaOps.Vexer.Export.Tests/OfflineBundleArtifactStoreTests.cs b/src/StellaOps.Vexer.Export.Tests/OfflineBundleArtifactStoreTests.cs
new file mode 100644
index 00000000..7d3abf04
--- /dev/null
+++ b/src/StellaOps.Vexer.Export.Tests/OfflineBundleArtifactStoreTests.cs
@@ -0,0 +1,59 @@
+using System.Collections.Immutable;
+using System.IO.Abstractions.TestingHelpers;
+using System.Linq;
+using System.Text.Json;
+using Microsoft.Extensions.Logging.Abstractions;
+using Microsoft.Extensions.Options;
+using StellaOps.Vexer.Core;
+using StellaOps.Vexer.Export;
+
+namespace StellaOps.Vexer.Export.Tests;
+
+public sealed class OfflineBundleArtifactStoreTests
+{
+    [Fact]
+    public async Task SaveAsync_WritesArtifactAndManifest()
+    {
+        var fs = new MockFileSystem();
+        var options = Options.Create(new OfflineBundleArtifactStoreOptions { RootPath = "/offline" });
+        var store = new OfflineBundleArtifactStore(options, NullLogger.Instance, fs);
+
+        var content = new byte[] { 1, 2, 3 };
+        var digest = "sha256:" + Convert.ToHexString(System.Security.Cryptography.SHA256.HashData(content)).ToLowerInvariant();
+        var artifact = new VexExportArtifact(
+            new VexContentAddress("sha256", digest.Split(':')[1]),
+            VexExportFormat.Json,
+            content,
+            ImmutableDictionary.Empty);
+
+        var stored = await store.SaveAsync(artifact, CancellationToken.None);
+
+        var artifactPath = fs.Path.Combine(options.Value.RootPath, stored.Location);
+        Assert.True(fs.FileExists(artifactPath));
+
+        var manifestPath = fs.Path.Combine(options.Value.RootPath, options.Value.ManifestFileName);
+        Assert.True(fs.FileExists(manifestPath));
+        await using var manifestStream = fs.File.OpenRead(manifestPath);
+        using var document = await JsonDocument.ParseAsync(manifestStream);
+        var artifacts = document.RootElement.GetProperty("artifacts");
+        Assert.True(artifacts.GetArrayLength() >= 1);
+        var first = artifacts.EnumerateArray().First();
+        Assert.Equal(digest, first.GetProperty("digest").GetString());
+    }
+
+    [Fact]
+    public async Task SaveAsync_ThrowsOnDigestMismatch()
+    {
+        var fs = new MockFileSystem();
+        var options = Options.Create(new OfflineBundleArtifactStoreOptions { RootPath = "/offline" });
+        var store = new OfflineBundleArtifactStore(options, NullLogger.Instance, fs);
+
+        var artifact = new VexExportArtifact(
+            new VexContentAddress("sha256", "deadbeef"),
+            VexExportFormat.Json,
+            new byte[] { 0x01, 0x02 },
+            ImmutableDictionary.Empty);
+
+        await Assert.ThrowsAsync(() => store.SaveAsync(artifact, CancellationToken.None).AsTask());
+    }
+}
diff --git a/src/StellaOps.Vexer.Export.Tests/S3ArtifactStoreTests.cs b/src/StellaOps.Vexer.Export.Tests/S3ArtifactStoreTests.cs
new file mode 100644
index 00000000..64c3b482
--- /dev/null
+++ b/src/StellaOps.Vexer.Export.Tests/S3ArtifactStoreTests.cs
@@ -0,0 +1,95 @@
+using System.Collections.Concurrent;
+using System.Collections.Immutable;
+using Microsoft.Extensions.Logging.Abstractions;
+using Microsoft.Extensions.Options;
+using StellaOps.Vexer.Core;
+using StellaOps.Vexer.Export;
+
+namespace StellaOps.Vexer.Export.Tests;
+
+public sealed class S3ArtifactStoreTests
+{
+    [Fact]
+    public async Task SaveAsync_UploadsContentWithMetadata()
+    {
+        var client = new FakeS3Client();
+        var options = Options.Create(new S3ArtifactStoreOptions { BucketName = "exports", Prefix = "vex" });
+        var store = new S3ArtifactStore(client, options, NullLogger.Instance);
+
+        var content = new byte[] { 1, 2, 3, 4 };
+        var artifact = new VexExportArtifact(
+            new VexContentAddress("sha256", "deadbeef"),
+            VexExportFormat.Json,
+            content,
+            ImmutableDictionary.Empty);
+
+        await store.SaveAsync(artifact, CancellationToken.None);
+
+        Assert.True(client.PutCalls.TryGetValue("exports", out var bucketEntries));
+        Assert.NotNull(bucketEntries);
+        var entry = bucketEntries!.Single();
+        Assert.Equal("vex/json/deadbeef.json", entry.Key);
+        Assert.Equal(content, entry.Content);
+        Assert.Equal("sha256:deadbeef", entry.Metadata["vex-digest"]);
+    }
+
+    [Fact]
+    public async Task OpenReadAsync_ReturnsStoredContent()
+    {
+        var client = new FakeS3Client();
+        var options = Options.Create(new S3ArtifactStoreOptions { BucketName = "exports", Prefix = "vex" });
+        var store = new S3ArtifactStore(client, options, NullLogger.Instance);
+
+        var address = new VexContentAddress("sha256", "cafebabe");
+        client.SeedObject("exports", "vex/json/cafebabe.json", new byte[] { 9, 9, 9 });
+
+        var stream = await store.OpenReadAsync(address, CancellationToken.None);
+        Assert.NotNull(stream);
+        using var ms = new MemoryStream();
+        await stream!.CopyToAsync(ms);
+        Assert.Equal(new byte[] { 9, 9, 9 }, ms.ToArray());
+    }
+
+    private sealed class FakeS3Client : IS3ArtifactClient
+    {
+        public ConcurrentDictionary> PutCalls { get; } = new(StringComparer.Ordinal);
+        private readonly ConcurrentDictionary<(string Bucket, string Key), byte[]> _storage = new();
+
+        public void SeedObject(string bucket, string key, byte[] content)
+        {
+            PutCalls.GetOrAdd(bucket, _ => new List()).Add(new S3Entry(key, content, new Dictionary()));
+            _storage[(bucket, key)] = content;
+        }
+
+        public Task ObjectExistsAsync(string bucketName, string key, CancellationToken cancellationToken)
+            => Task.FromResult(_storage.ContainsKey((bucketName, key)));
+
+        public Task PutObjectAsync(string bucketName, string key, Stream content, IDictionary metadata, CancellationToken cancellationToken)
+        {
+            using var ms = new MemoryStream();
+            content.CopyTo(ms);
+            var bytes = ms.ToArray();
+            PutCalls.GetOrAdd(bucketName, _ => new List()).Add(new S3Entry(key, bytes, new Dictionary(metadata)));
+            _storage[(bucketName, key)] = bytes;
+            return Task.CompletedTask;
+        }
+
+        public Task GetObjectAsync(string bucketName, string key, CancellationToken cancellationToken)
+        {
+            if (_storage.TryGetValue((bucketName, key), out var bytes))
+            {
+                return Task.FromResult(new MemoryStream(bytes, writable: false));
+            }
+
+            return Task.FromResult(null);
+        }
+
+        public Task DeleteObjectAsync(string bucketName, string key, CancellationToken cancellationToken)
+        {
+            _storage.TryRemove((bucketName, key), out _);
+            return Task.CompletedTask;
+        }
+
+        public readonly record struct S3Entry(string Key, byte[] Content, IDictionary Metadata);
+    }
+}
diff --git a/src/StellaOps.Vexer.Export.Tests/StellaOps.Vexer.Export.Tests.csproj b/src/StellaOps.Vexer.Export.Tests/StellaOps.Vexer.Export.Tests.csproj
index 354ae572..3a38978f 100644
--- a/src/StellaOps.Vexer.Export.Tests/StellaOps.Vexer.Export.Tests.csproj
+++ b/src/StellaOps.Vexer.Export.Tests/StellaOps.Vexer.Export.Tests.csproj
@@ -6,6 +6,9 @@
     enable
     true
   
+  
+    
+  
   
     
   
diff --git a/src/StellaOps.Vexer.Export.Tests/VexExportCacheServiceTests.cs b/src/StellaOps.Vexer.Export.Tests/VexExportCacheServiceTests.cs
new file mode 100644
index 00000000..cbaa7efe
--- /dev/null
+++ b/src/StellaOps.Vexer.Export.Tests/VexExportCacheServiceTests.cs
@@ -0,0 +1,81 @@
+using Microsoft.Extensions.Logging.Abstractions;
+using StellaOps.Vexer.Core;
+using StellaOps.Vexer.Export;
+using StellaOps.Vexer.Storage.Mongo;
+
+namespace StellaOps.Vexer.Export.Tests;
+
+public sealed class VexExportCacheServiceTests
+{
+    [Fact]
+    public async Task InvalidateAsync_RemovesEntry()
+    {
+        var cacheIndex = new RecordingIndex();
+        var maintenance = new StubMaintenance();
+        var service = new VexExportCacheService(cacheIndex, maintenance, NullLogger.Instance);
+
+        var signature = new VexQuerySignature("format=json|provider=vendor");
+        await service.InvalidateAsync(signature, VexExportFormat.Json, CancellationToken.None);
+
+        Assert.Equal(signature.Value, cacheIndex.LastSignature?.Value);
+        Assert.Equal(VexExportFormat.Json, cacheIndex.LastFormat);
+        Assert.Equal(1, cacheIndex.RemoveCalls);
+    }
+
+    [Fact]
+    public async Task PruneExpiredAsync_ReturnsCount()
+    {
+        var cacheIndex = new RecordingIndex();
+        var maintenance = new StubMaintenance { ExpiredCount = 3 };
+        var service = new VexExportCacheService(cacheIndex, maintenance, NullLogger.Instance);
+
+        var removed = await service.PruneExpiredAsync(DateTimeOffset.UtcNow, CancellationToken.None);
+
+        Assert.Equal(3, removed);
+    }
+
+    [Fact]
+    public async Task PruneDanglingAsync_ReturnsCount()
+    {
+        var cacheIndex = new RecordingIndex();
+        var maintenance = new StubMaintenance { DanglingCount = 2 };
+        var service = new VexExportCacheService(cacheIndex, maintenance, NullLogger.Instance);
+
+        var removed = await service.PruneDanglingAsync(CancellationToken.None);
+
+        Assert.Equal(2, removed);
+    }
+
+    private sealed class RecordingIndex : IVexCacheIndex
+    {
+        public VexQuerySignature? LastSignature { get; private set; }
+        public VexExportFormat LastFormat { get; private set; }
+        public int RemoveCalls { get; private set; }
+
+        public ValueTask FindAsync(VexQuerySignature signature, VexExportFormat format, CancellationToken cancellationToken)
+            => ValueTask.FromResult(null);
+
+        public ValueTask SaveAsync(VexCacheEntry entry, CancellationToken cancellationToken)
+            => ValueTask.CompletedTask;
+
+        public ValueTask RemoveAsync(VexQuerySignature signature, VexExportFormat format, CancellationToken cancellationToken)
+        {
+            LastSignature = signature;
+            LastFormat = format;
+            RemoveCalls++;
+            return ValueTask.CompletedTask;
+        }
+    }
+
+    private sealed class StubMaintenance : IVexCacheMaintenance
+    {
+        public int ExpiredCount { get; set; }
+        public int DanglingCount { get; set; }
+
+        public ValueTask RemoveExpiredAsync(DateTimeOffset asOf, CancellationToken cancellationToken)
+            => ValueTask.FromResult(ExpiredCount);
+
+        public ValueTask RemoveMissingManifestReferencesAsync(CancellationToken cancellationToken)
+            => ValueTask.FromResult(DanglingCount);
+    }
+}
diff --git a/src/StellaOps.Vexer.Export/ExportEngine.cs b/src/StellaOps.Vexer.Export/ExportEngine.cs
index 49571650..3ea66ee6 100644
--- a/src/StellaOps.Vexer.Export/ExportEngine.cs
+++ b/src/StellaOps.Vexer.Export/ExportEngine.cs
@@ -37,18 +37,24 @@ public sealed class VexExportEngine : IExportEngine
     private readonly IVexExportDataSource _dataSource;
     private readonly IReadOnlyDictionary _exporters;
     private readonly ILogger _logger;
+    private readonly IVexCacheIndex? _cacheIndex;
+    private readonly IReadOnlyList _artifactStores;
 
     public VexExportEngine(
         IVexExportStore exportStore,
         IVexPolicyEvaluator policyEvaluator,
         IVexExportDataSource dataSource,
         IEnumerable exporters,
-        ILogger logger)
+        ILogger logger,
+        IVexCacheIndex? cacheIndex = null,
+        IEnumerable? artifactStores = null)
     {
         _exportStore = exportStore ?? throw new ArgumentNullException(nameof(exportStore));
         _policyEvaluator = policyEvaluator ?? throw new ArgumentNullException(nameof(policyEvaluator));
         _dataSource = dataSource ?? throw new ArgumentNullException(nameof(dataSource));
         _logger = logger ?? throw new ArgumentNullException(nameof(logger));
+        _cacheIndex = cacheIndex;
+        _artifactStores = artifactStores?.ToArray() ?? Array.Empty();
 
         if (exporters is null)
         {
@@ -69,9 +75,25 @@ public sealed class VexExportEngine : IExportEngine
             if (cached is not null)
             {
                 _logger.LogInformation("Reusing cached export for {Signature} ({Format})", signature.Value, context.Format);
-                return cached with { FromCache = true };
+                return new VexExportManifest(
+                    cached.ExportId,
+                    cached.QuerySignature,
+                    cached.Format,
+                    cached.CreatedAt,
+                    cached.Artifact,
+                    cached.ClaimCount,
+                    cached.SourceProviders,
+                    fromCache: true,
+                    cached.ConsensusRevision,
+                    cached.Attestation,
+                    cached.SizeBytes);
             }
         }
+        else if (_cacheIndex is not null)
+        {
+            await _cacheIndex.RemoveAsync(signature, context.Format, cancellationToken).ConfigureAwait(false);
+            _logger.LogInformation("Force refresh requested; invalidated cache entry for {Signature} ({Format})", signature.Value, context.Format);
+        }
 
         var dataset = await _dataSource.FetchAsync(context.Query, cancellationToken).ConfigureAwait(false);
         var exporter = ResolveExporter(context.Format);
@@ -87,6 +109,31 @@ public sealed class VexExportEngine : IExportEngine
         await using var buffer = new MemoryStream();
         var result = await exporter.SerializeAsync(exportRequest, buffer, cancellationToken).ConfigureAwait(false);
 
+        if (_artifactStores.Count > 0)
+        {
+            var writtenBytes = buffer.ToArray();
+            try
+            {
+                var artifact = new VexExportArtifact(
+                    result.Digest,
+                    context.Format,
+                    writtenBytes,
+                    result.Metadata);
+
+                foreach (var store in _artifactStores)
+                {
+                    await store.SaveAsync(artifact, cancellationToken).ConfigureAwait(false);
+                }
+
+                _logger.LogInformation("Stored export artifact {Digest} via {StoreCount} store(s)", result.Digest.ToUri(), _artifactStores.Count);
+            }
+            catch (Exception ex)
+            {
+                _logger.LogError(ex, "Failed to store export artifact {Digest}", result.Digest.ToUri());
+                throw;
+            }
+        }
+
         var exportId = FormattableString.Invariant($"exports/{context.RequestedAt:yyyyMMddTHHmmssfffZ}/{digest.Digest}");
         var manifest = new VexExportManifest(
             exportId,
@@ -123,6 +170,7 @@ public static class VexExportServiceCollectionExtensions
     public static IServiceCollection AddVexExportEngine(this IServiceCollection services)
     {
         services.AddSingleton();
+        services.AddVexExportCacheServices();
         return services;
     }
 }
diff --git a/src/StellaOps.Vexer.Export/FileSystemArtifactStore.cs b/src/StellaOps.Vexer.Export/FileSystemArtifactStore.cs
new file mode 100644
index 00000000..9aa2f2a8
--- /dev/null
+++ b/src/StellaOps.Vexer.Export/FileSystemArtifactStore.cs
@@ -0,0 +1,159 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.IO.Abstractions;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+using StellaOps.Vexer.Core;
+
+namespace StellaOps.Vexer.Export;
+
+public sealed class FileSystemArtifactStoreOptions
+{
+    public string RootPath { get; set; } = ".";
+
+    public bool OverwriteExisting { get; set; } = false;
+}
+
+public sealed class FileSystemArtifactStore : IVexArtifactStore
+{
+    private readonly IFileSystem _fileSystem;
+    private readonly FileSystemArtifactStoreOptions _options;
+    private readonly ILogger _logger;
+
+    public FileSystemArtifactStore(
+        IOptions options,
+        ILogger logger,
+        IFileSystem? fileSystem = null)
+    {
+        ArgumentNullException.ThrowIfNull(options);
+        _options = options.Value;
+        _logger = logger ?? throw new ArgumentNullException(nameof(logger));
+        _fileSystem = fileSystem ?? new FileSystem();
+
+        if (string.IsNullOrWhiteSpace(_options.RootPath))
+        {
+            throw new ArgumentException("RootPath must be provided for FileSystemArtifactStore.", nameof(options));
+        }
+
+        var root = _fileSystem.Path.GetFullPath(_options.RootPath);
+        _fileSystem.Directory.CreateDirectory(root);
+        _options.RootPath = root;
+    }
+
+    public async ValueTask SaveAsync(VexExportArtifact artifact, CancellationToken cancellationToken)
+    {
+        ArgumentNullException.ThrowIfNull(artifact);
+
+        var relativePath = BuildArtifactPath(artifact.ContentAddress, artifact.Format);
+        var destination = _fileSystem.Path.Combine(_options.RootPath, relativePath);
+        var directory = _fileSystem.Path.GetDirectoryName(destination);
+        if (!string.IsNullOrEmpty(directory))
+        {
+            _fileSystem.Directory.CreateDirectory(directory);
+        }
+
+        if (_fileSystem.File.Exists(destination) && !_options.OverwriteExisting)
+        {
+            _logger.LogInformation("Artifact {Digest} already exists at {Path}; skipping write.", artifact.ContentAddress.ToUri(), destination);
+        }
+        else
+        {
+            await using var stream = _fileSystem.File.Create(destination);
+            await stream.WriteAsync(artifact.Content, cancellationToken).ConfigureAwait(false);
+        }
+
+        var location = destination.Replace(_options.RootPath, string.Empty).TrimStart(_fileSystem.Path.DirectorySeparatorChar, _fileSystem.Path.AltDirectorySeparatorChar);
+
+        return new VexStoredArtifact(
+            artifact.ContentAddress,
+            location,
+            artifact.Content.Length,
+            artifact.Metadata);
+    }
+
+    public ValueTask DeleteAsync(VexContentAddress contentAddress, CancellationToken cancellationToken)
+    {
+        var path = MaterializePath(contentAddress);
+        if (path is not null && _fileSystem.File.Exists(path))
+        {
+            _fileSystem.File.Delete(path);
+        }
+
+        return ValueTask.CompletedTask;
+    }
+
+    public ValueTask OpenReadAsync(VexContentAddress contentAddress, CancellationToken cancellationToken)
+    {
+        var path = MaterializePath(contentAddress);
+        if (path is null || !_fileSystem.File.Exists(path))
+        {
+            return ValueTask.FromResult(null);
+        }
+
+        Stream stream = _fileSystem.File.OpenRead(path);
+        return ValueTask.FromResult(stream);
+    }
+
+    private static string BuildArtifactPath(VexContentAddress address, VexExportFormat format)
+    {
+        var formatSegment = format.ToString().ToLowerInvariant();
+        var safeDigest = address.Digest.Replace(':', '_');
+        var extension = GetExtension(format);
+        return Path.Combine(formatSegment, safeDigest + extension);
+    }
+
+    private string? MaterializePath(VexContentAddress address)
+    {
+        ArgumentNullException.ThrowIfNull(address);
+        var sanitized = address.Digest.Replace(':', '_');
+
+        foreach (VexExportFormat format in Enum.GetValues(typeof(VexExportFormat)))
+        {
+            var candidate = _fileSystem.Path.Combine(_options.RootPath, format.ToString().ToLowerInvariant(), sanitized + GetExtension(format));
+            if (_fileSystem.File.Exists(candidate))
+            {
+                return candidate;
+            }
+        }
+
+        // fallback: direct root search with common extensions
+        foreach (var extension in new[] { ".json", ".jsonl" })
+        {
+            var candidate = _fileSystem.Path.Combine(_options.RootPath, sanitized + extension);
+            if (_fileSystem.File.Exists(candidate))
+            {
+                return candidate;
+            }
+        }
+
+        return null;
+    }
+
+    private static string GetExtension(VexExportFormat format)
+        => format switch
+        {
+            VexExportFormat.Json => ".json",
+            VexExportFormat.JsonLines => ".jsonl",
+            VexExportFormat.OpenVex => ".json",
+            VexExportFormat.Csaf => ".json",
+            _ => ".bin",
+        };
+}
+
+public static class FileSystemArtifactStoreServiceCollectionExtensions
+{
+    public static IServiceCollection AddVexFileSystemArtifactStore(this IServiceCollection services, Action? configure = null)
+    {
+        if (configure is not null)
+        {
+            services.Configure(configure);
+        }
+
+        services.AddSingleton();
+        return services;
+    }
+}
diff --git a/src/StellaOps.Vexer.Export/IVexArtifactStore.cs b/src/StellaOps.Vexer.Export/IVexArtifactStore.cs
new file mode 100644
index 00000000..5de445ac
--- /dev/null
+++ b/src/StellaOps.Vexer.Export/IVexArtifactStore.cs
@@ -0,0 +1,28 @@
+using System.Collections.Generic;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+using StellaOps.Vexer.Core;
+
+namespace StellaOps.Vexer.Export;
+
+public sealed record VexExportArtifact(
+    VexContentAddress ContentAddress,
+    VexExportFormat Format,
+    ReadOnlyMemory Content,
+    IReadOnlyDictionary Metadata);
+
+public sealed record VexStoredArtifact(
+    VexContentAddress ContentAddress,
+    string Location,
+    long SizeBytes,
+    IReadOnlyDictionary Metadata);
+
+public interface IVexArtifactStore
+{
+    ValueTask SaveAsync(VexExportArtifact artifact, CancellationToken cancellationToken);
+
+    ValueTask DeleteAsync(VexContentAddress contentAddress, CancellationToken cancellationToken);
+
+    ValueTask OpenReadAsync(VexContentAddress contentAddress, CancellationToken cancellationToken);
+}
diff --git a/src/StellaOps.Vexer.Export/OfflineBundleArtifactStore.cs b/src/StellaOps.Vexer.Export/OfflineBundleArtifactStore.cs
new file mode 100644
index 00000000..ba5e771e
--- /dev/null
+++ b/src/StellaOps.Vexer.Export/OfflineBundleArtifactStore.cs
@@ -0,0 +1,243 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using System.IO;
+using System.IO.Abstractions;
+using System.IO.Compression;
+using System.Security.Cryptography;
+using System.Text.Json;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+using StellaOps.Vexer.Core;
+
+namespace StellaOps.Vexer.Export;
+
+public sealed class OfflineBundleArtifactStoreOptions
+{
+    public string RootPath { get; set; } = ".";
+
+    public string ArtifactsFolder { get; set; } = "artifacts";
+
+    public string BundlesFolder { get; set; } = "bundles";
+
+    public string ManifestFileName { get; set; } = "offline-manifest.json";
+}
+
+public sealed class OfflineBundleArtifactStore : IVexArtifactStore
+{
+    private readonly IFileSystem _fileSystem;
+    private readonly OfflineBundleArtifactStoreOptions _options;
+    private readonly ILogger _logger;
+    private readonly JsonSerializerOptions _serializerOptions = new()
+    {
+        PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
+        WriteIndented = true,
+    };
+
+    public OfflineBundleArtifactStore(
+        IOptions options,
+        ILogger logger,
+        IFileSystem? fileSystem = null)
+    {
+        ArgumentNullException.ThrowIfNull(options);
+        _options = options.Value;
+        _logger = logger ?? throw new ArgumentNullException(nameof(logger));
+        _fileSystem = fileSystem ?? new FileSystem();
+
+        if (string.IsNullOrWhiteSpace(_options.RootPath))
+        {
+            throw new ArgumentException("RootPath must be provided for OfflineBundleArtifactStore.", nameof(options));
+        }
+
+        var root = _fileSystem.Path.GetFullPath(_options.RootPath);
+        _fileSystem.Directory.CreateDirectory(root);
+        _options.RootPath = root;
+        _fileSystem.Directory.CreateDirectory(GetArtifactsRoot());
+        _fileSystem.Directory.CreateDirectory(GetBundlesRoot());
+    }
+
+    public async ValueTask SaveAsync(VexExportArtifact artifact, CancellationToken cancellationToken)
+    {
+        ArgumentNullException.ThrowIfNull(artifact);
+        EnforceDigestMatch(artifact);
+
+        var artifactRelativePath = BuildArtifactRelativePath(artifact);
+        var artifactFullPath = _fileSystem.Path.Combine(_options.RootPath, artifactRelativePath);
+        var artifactDirectory = _fileSystem.Path.GetDirectoryName(artifactFullPath);
+        if (!string.IsNullOrEmpty(artifactDirectory))
+        {
+            _fileSystem.Directory.CreateDirectory(artifactDirectory);
+        }
+
+        await using (var stream = _fileSystem.File.Create(artifactFullPath))
+        {
+            await stream.WriteAsync(artifact.Content, cancellationToken).ConfigureAwait(false);
+        }
+
+        WriteOfflineBundle(artifactRelativePath, artifact, cancellationToken);
+        await UpdateManifestAsync(artifactRelativePath, artifact, cancellationToken).ConfigureAwait(false);
+
+        _logger.LogInformation("Stored offline artifact {Digest} at {Path}", artifact.ContentAddress.ToUri(), artifactRelativePath);
+
+        return new VexStoredArtifact(
+            artifact.ContentAddress,
+            artifactRelativePath,
+            artifact.Content.Length,
+            artifact.Metadata);
+    }
+
+    public ValueTask DeleteAsync(VexContentAddress contentAddress, CancellationToken cancellationToken)
+    {
+        ArgumentNullException.ThrowIfNull(contentAddress);
+        var sanitized = contentAddress.Digest.Replace(':', '_');
+        var artifactsRoot = GetArtifactsRoot();
+
+        foreach (VexExportFormat format in Enum.GetValues(typeof(VexExportFormat)))
+        {
+            var extension = GetExtension(format);
+            var path = _fileSystem.Path.Combine(artifactsRoot, format.ToString().ToLowerInvariant(), sanitized + extension);
+            if (_fileSystem.File.Exists(path))
+            {
+                _fileSystem.File.Delete(path);
+            }
+
+            var bundlePath = _fileSystem.Path.Combine(GetBundlesRoot(), sanitized + ".zip");
+            if (_fileSystem.File.Exists(bundlePath))
+            {
+                _fileSystem.File.Delete(bundlePath);
+            }
+        }
+
+        return ValueTask.CompletedTask;
+    }
+
+    public ValueTask OpenReadAsync(VexContentAddress contentAddress, CancellationToken cancellationToken)
+    {
+        ArgumentNullException.ThrowIfNull(contentAddress);
+        var artifactsRoot = GetArtifactsRoot();
+        var sanitized = contentAddress.Digest.Replace(':', '_');
+
+        foreach (VexExportFormat format in Enum.GetValues(typeof(VexExportFormat)))
+        {
+            var candidate = _fileSystem.Path.Combine(artifactsRoot, format.ToString().ToLowerInvariant(), sanitized + GetExtension(format));
+            if (_fileSystem.File.Exists(candidate))
+            {
+                return ValueTask.FromResult(_fileSystem.File.OpenRead(candidate));
+            }
+        }
+
+        return ValueTask.FromResult(null);
+    }
+
+    private void EnforceDigestMatch(VexExportArtifact artifact)
+    {
+        if (!artifact.ContentAddress.Algorithm.Equals("sha256", StringComparison.OrdinalIgnoreCase))
+        {
+            return;
+        }
+
+        using var sha = SHA256.Create();
+        var computed = "sha256:" + Convert.ToHexString(sha.ComputeHash(artifact.Content.ToArray())).ToLowerInvariant();
+        if (!string.Equals(computed, artifact.ContentAddress.ToUri(), StringComparison.OrdinalIgnoreCase))
+        {
+            throw new InvalidOperationException($"Artifact content digest mismatch. Expected {artifact.ContentAddress.ToUri()} but computed {computed}.");
+        }
+    }
+
+    private string BuildArtifactRelativePath(VexExportArtifact artifact)
+    {
+        var sanitized = artifact.ContentAddress.Digest.Replace(':', '_');
+        var folder = _fileSystem.Path.Combine(_options.ArtifactsFolder, artifact.Format.ToString().ToLowerInvariant());
+        return _fileSystem.Path.Combine(folder, sanitized + GetExtension(artifact.Format));
+    }
+
+    private void WriteOfflineBundle(string artifactRelativePath, VexExportArtifact artifact, CancellationToken cancellationToken)
+    {
+        var zipPath = _fileSystem.Path.Combine(GetBundlesRoot(), artifact.ContentAddress.Digest.Replace(':', '_') + ".zip");
+        using var zipStream = _fileSystem.File.Create(zipPath);
+        using var archive = new ZipArchive(zipStream, ZipArchiveMode.Create);
+        var entry = archive.CreateEntry(artifactRelativePath, CompressionLevel.Optimal);
+        using (var entryStream = entry.Open())
+        {
+            entryStream.Write(artifact.Content.Span);
+        }
+
+        // embed metadata file
+        var metadataEntry = archive.CreateEntry("metadata.json", CompressionLevel.Optimal);
+        using var metadataStream = new StreamWriter(metadataEntry.Open());
+        var metadata = new Dictionary
+        {
+            ["digest"] = artifact.ContentAddress.ToUri(),
+            ["format"] = artifact.Format.ToString().ToLowerInvariant(),
+            ["sizeBytes"] = artifact.Content.Length,
+            ["metadata"] = artifact.Metadata,
+        };
+        metadataStream.Write(JsonSerializer.Serialize(metadata, _serializerOptions));
+    }
+
+    private async Task UpdateManifestAsync(string artifactRelativePath, VexExportArtifact artifact, CancellationToken cancellationToken)
+    {
+        var manifestPath = _fileSystem.Path.Combine(_options.RootPath, _options.ManifestFileName);
+        var records = new List();
+
+        if (_fileSystem.File.Exists(manifestPath))
+        {
+            await using var existingStream = _fileSystem.File.OpenRead(manifestPath);
+            var existing = await JsonSerializer.DeserializeAsync(existingStream, _serializerOptions, cancellationToken).ConfigureAwait(false);
+            if (existing is not null)
+            {
+                records.AddRange(existing.Artifacts);
+            }
+        }
+
+        records.RemoveAll(x => string.Equals(x.Digest, artifact.ContentAddress.ToUri(), StringComparison.OrdinalIgnoreCase));
+        records.Add(new ManifestEntry(
+            artifact.ContentAddress.ToUri(),
+            artifact.Format.ToString().ToLowerInvariant(),
+            artifactRelativePath.Replace("\\", "/"),
+            artifact.Content.Length,
+            artifact.Metadata));
+
+        records.Sort(static (a, b) => string.CompareOrdinal(a.Digest, b.Digest));
+
+        var doc = new ManifestDocument(records.ToImmutableArray());
+
+        await using var stream = _fileSystem.File.Create(manifestPath);
+        await JsonSerializer.SerializeAsync(stream, doc, _serializerOptions, cancellationToken).ConfigureAwait(false);
+    }
+
+    private string GetArtifactsRoot() => _fileSystem.Path.Combine(_options.RootPath, _options.ArtifactsFolder);
+
+    private string GetBundlesRoot() => _fileSystem.Path.Combine(_options.RootPath, _options.BundlesFolder);
+
+    private static string GetExtension(VexExportFormat format)
+        => format switch
+        {
+            VexExportFormat.Json => ".json",
+            VexExportFormat.JsonLines => ".jsonl",
+            VexExportFormat.OpenVex => ".json",
+            VexExportFormat.Csaf => ".json",
+            _ => ".bin",
+        };
+
+    private sealed record ManifestDocument(ImmutableArray Artifacts);
+
+    private sealed record ManifestEntry(string Digest, string Format, string Path, long SizeBytes, IReadOnlyDictionary Metadata);
+}
+
+public static class OfflineBundleArtifactStoreServiceCollectionExtensions
+{
+    public static IServiceCollection AddVexOfflineBundleArtifactStore(this IServiceCollection services, Action? configure = null)
+    {
+        if (configure is not null)
+        {
+            services.Configure(configure);
+        }
+
+        services.AddSingleton();
+        return services;
+    }
+}
diff --git a/src/StellaOps.Vexer.Export/Properties/AssemblyInfo.cs b/src/StellaOps.Vexer.Export/Properties/AssemblyInfo.cs
new file mode 100644
index 00000000..d7a3cbff
--- /dev/null
+++ b/src/StellaOps.Vexer.Export/Properties/AssemblyInfo.cs
@@ -0,0 +1,3 @@
+using System.Runtime.CompilerServices;
+
+[assembly: InternalsVisibleTo("StellaOps.Vexer.Export.Tests")]
diff --git a/src/StellaOps.Vexer.Export/S3ArtifactStore.cs b/src/StellaOps.Vexer.Export/S3ArtifactStore.cs
new file mode 100644
index 00000000..9cd99f89
--- /dev/null
+++ b/src/StellaOps.Vexer.Export/S3ArtifactStore.cs
@@ -0,0 +1,181 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+using StellaOps.Vexer.Core;
+
+namespace StellaOps.Vexer.Export;
+
+public sealed class S3ArtifactStoreOptions
+{
+    public string BucketName { get; set; } = string.Empty;
+
+    public string? Prefix { get; set; }
+        = null;
+
+    public bool OverwriteExisting { get; set; }
+        = true;
+}
+
+public interface IS3ArtifactClient
+{
+    Task ObjectExistsAsync(string bucketName, string key, CancellationToken cancellationToken);
+
+    Task PutObjectAsync(string bucketName, string key, Stream content, IDictionary metadata, CancellationToken cancellationToken);
+
+    Task GetObjectAsync(string bucketName, string key, CancellationToken cancellationToken);
+
+    Task DeleteObjectAsync(string bucketName, string key, CancellationToken cancellationToken);
+}
+
+public sealed class S3ArtifactStore : IVexArtifactStore
+{
+    private readonly IS3ArtifactClient _client;
+    private readonly S3ArtifactStoreOptions _options;
+    private readonly ILogger _logger;
+
+    public S3ArtifactStore(
+        IS3ArtifactClient client,
+        IOptions options,
+        ILogger logger)
+    {
+        _client = client ?? throw new ArgumentNullException(nameof(client));
+        ArgumentNullException.ThrowIfNull(options);
+        _options = options.Value;
+        _logger = logger ?? throw new ArgumentNullException(nameof(logger));
+
+        if (string.IsNullOrWhiteSpace(_options.BucketName))
+        {
+            throw new ArgumentException("BucketName must be provided for S3ArtifactStore.", nameof(options));
+        }
+    }
+
+    public async ValueTask SaveAsync(VexExportArtifact artifact, CancellationToken cancellationToken)
+    {
+        ArgumentNullException.ThrowIfNull(artifact);
+        var key = BuildObjectKey(artifact.ContentAddress, artifact.Format);
+
+        if (!_options.OverwriteExisting)
+        {
+            var exists = await _client.ObjectExistsAsync(_options.BucketName, key, cancellationToken).ConfigureAwait(false);
+            if (exists)
+            {
+                _logger.LogInformation("S3 object {Bucket}/{Key} already exists; skipping upload.", _options.BucketName, key);
+                return new VexStoredArtifact(artifact.ContentAddress, key, artifact.Content.Length, artifact.Metadata);
+            }
+        }
+
+        using var contentStream = new MemoryStream(artifact.Content.ToArray());
+        await _client.PutObjectAsync(
+            _options.BucketName,
+            key,
+            contentStream,
+            BuildObjectMetadata(artifact),
+            cancellationToken).ConfigureAwait(false);
+
+        _logger.LogInformation("Uploaded export artifact {Digest} to {Bucket}/{Key}", artifact.ContentAddress.ToUri(), _options.BucketName, key);
+
+        return new VexStoredArtifact(
+            artifact.ContentAddress,
+            key,
+            artifact.Content.Length,
+            artifact.Metadata);
+    }
+
+    public async ValueTask DeleteAsync(VexContentAddress contentAddress, CancellationToken cancellationToken)
+    {
+        ArgumentNullException.ThrowIfNull(contentAddress);
+        foreach (var key in BuildCandidateKeys(contentAddress))
+        {
+            await _client.DeleteObjectAsync(_options.BucketName, key, cancellationToken).ConfigureAwait(false);
+        }
+        _logger.LogInformation("Deleted export artifact {Digest} from {Bucket}", contentAddress.ToUri(), _options.BucketName);
+    }
+
+    public async ValueTask OpenReadAsync(VexContentAddress contentAddress, CancellationToken cancellationToken)
+    {
+        ArgumentNullException.ThrowIfNull(contentAddress);
+        foreach (var key in BuildCandidateKeys(contentAddress))
+        {
+            var stream = await _client.GetObjectAsync(_options.BucketName, key, cancellationToken).ConfigureAwait(false);
+            if (stream is not null)
+            {
+                return stream;
+            }
+        }
+
+        return null;
+    }
+
+    private string BuildObjectKey(VexContentAddress address, VexExportFormat format)
+    {
+        var sanitizedDigest = address.Digest.Replace(':', '_');
+        var prefix = string.IsNullOrWhiteSpace(_options.Prefix) ? string.Empty : _options.Prefix.TrimEnd('/') + "/";
+        var formatSegment = format.ToString().ToLowerInvariant();
+        return $"{prefix}{formatSegment}/{sanitizedDigest}{GetExtension(format)}";
+    }
+
+    private IEnumerable BuildCandidateKeys(VexContentAddress address)
+    {
+        foreach (VexExportFormat format in Enum.GetValues(typeof(VexExportFormat)))
+        {
+            yield return BuildObjectKey(address, format);
+        }
+
+        if (!string.IsNullOrWhiteSpace(_options.Prefix))
+        {
+            yield return $"{_options.Prefix.TrimEnd('/')}/{address.Digest.Replace(':', '_')}";
+        }
+
+        yield return address.Digest.Replace(':', '_');
+    }
+
+    private static IDictionary BuildObjectMetadata(VexExportArtifact artifact)
+    {
+        var metadata = new Dictionary(StringComparer.OrdinalIgnoreCase)
+        {
+            ["vex-format"] = artifact.Format.ToString().ToLowerInvariant(),
+            ["vex-digest"] = artifact.ContentAddress.ToUri(),
+            ["content-type"] = artifact.Format switch
+            {
+                VexExportFormat.Json => "application/json",
+                VexExportFormat.JsonLines => "application/json",
+                VexExportFormat.OpenVex => "application/vnd.openvex+json",
+                VexExportFormat.Csaf => "application/json",
+                _ => "application/octet-stream",
+            },
+        };
+
+        foreach (var kvp in artifact.Metadata)
+        {
+            metadata[$"meta-{kvp.Key}"] = kvp.Value;
+        }
+
+        return metadata;
+    }
+
+    private static string GetExtension(VexExportFormat format)
+        => format switch
+        {
+            VexExportFormat.Json => ".json",
+            VexExportFormat.JsonLines => ".jsonl",
+            VexExportFormat.OpenVex => ".json",
+            VexExportFormat.Csaf => ".json",
+            _ => ".bin",
+        };
+}
+
+public static class S3ArtifactStoreServiceCollectionExtensions
+{
+    public static IServiceCollection AddVexS3ArtifactStore(this IServiceCollection services, Action configure)
+    {
+        ArgumentNullException.ThrowIfNull(configure);
+        services.Configure(configure);
+        services.AddSingleton();
+        return services;
+    }
+}
diff --git a/src/StellaOps.Vexer.Export/StellaOps.Vexer.Export.csproj b/src/StellaOps.Vexer.Export/StellaOps.Vexer.Export.csproj
index ae3c90d4..dc199446 100644
--- a/src/StellaOps.Vexer.Export/StellaOps.Vexer.Export.csproj
+++ b/src/StellaOps.Vexer.Export/StellaOps.Vexer.Export.csproj
@@ -9,6 +9,7 @@
   
     
     
+    
   
   
     
diff --git a/src/StellaOps.Vexer.Export/TASKS.md b/src/StellaOps.Vexer.Export/TASKS.md
index 8faab11b..f229d90d 100644
--- a/src/StellaOps.Vexer.Export/TASKS.md
+++ b/src/StellaOps.Vexer.Export/TASKS.md
@@ -3,6 +3,7 @@ If you are working on this file you need to read docs/ARCHITECTURE_VEXER.md and
 | Task | Owner(s) | Depends on | Notes |
 |---|---|---|---|
 |VEXER-EXPORT-01-001 – Export engine orchestration|Team Vexer Export|VEXER-CORE-01-003|DONE (2025-10-15) – Export engine scaffolding with cache lookup, data source hooks, and deterministic manifest emission.|
-|VEXER-EXPORT-01-002 – Cache index & eviction hooks|Team Vexer Export|VEXER-EXPORT-01-001, VEXER-STORAGE-01-003|TODO – Wire cache lookup/write path against `vex.cache` collection and add GC utilities for Worker to prune stale entries deterministically.|
-|VEXER-EXPORT-01-003 – Artifact store adapters|Team Vexer Export|VEXER-EXPORT-01-001|TODO – Provide pluggable storage adapters (filesystem, S3/MinIO) with offline bundle packaging and hash verification.|
+|VEXER-EXPORT-01-002 – Cache index & eviction hooks|Team Vexer Export|VEXER-EXPORT-01-001, VEXER-STORAGE-01-003|**DONE (2025-10-16)** – Export engine now invalidates cache entries on force refresh, cache services expose prune/invalidate APIs, and storage maintenance trims expired/dangling records with Mongo2Go coverage.|
+|VEXER-EXPORT-01-003 – Artifact store adapters|Team Vexer Export|VEXER-EXPORT-01-001|**DONE (2025-10-16)** – Implemented multi-store pipeline with filesystem, S3-compatible, and offline bundle adapters (hash verification + manifest/zip output) plus unit coverage and DI hooks.|
 |VEXER-EXPORT-01-004 – Attestation handoff integration|Team Vexer Export|VEXER-EXPORT-01-001, VEXER-ATTEST-01-001|TODO – Connect export engine to attestation client, persist Rekor metadata, and reuse cached attestations.|
+|VEXER-EXPORT-01-005 – Score & resolve envelope surfaces|Team Vexer Export|VEXER-EXPORT-01-004, VEXER-CORE-02-001|TODO – Emit consensus+score envelopes in export manifests, include policy/scoring digests, and update offline bundle/ORAS layouts to carry signed VEX responses.|
diff --git a/src/StellaOps.Vexer.Export/VexExportCacheService.cs b/src/StellaOps.Vexer.Export/VexExportCacheService.cs
new file mode 100644
index 00000000..6cdda3f9
--- /dev/null
+++ b/src/StellaOps.Vexer.Export/VexExportCacheService.cs
@@ -0,0 +1,54 @@
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+using StellaOps.Vexer.Core;
+using StellaOps.Vexer.Storage.Mongo;
+
+namespace StellaOps.Vexer.Export;
+
+public interface IVexExportCacheService
+{
+    ValueTask InvalidateAsync(VexQuerySignature signature, VexExportFormat format, CancellationToken cancellationToken);
+
+    ValueTask PruneExpiredAsync(DateTimeOffset asOf, CancellationToken cancellationToken);
+
+    ValueTask PruneDanglingAsync(CancellationToken cancellationToken);
+}
+
+internal sealed class VexExportCacheService : IVexExportCacheService
+{
+    private readonly IVexCacheIndex _cacheIndex;
+    private readonly IVexCacheMaintenance _maintenance;
+    private readonly ILogger _logger;
+
+    public VexExportCacheService(
+        IVexCacheIndex cacheIndex,
+        IVexCacheMaintenance maintenance,
+        ILogger logger)
+    {
+        _cacheIndex = cacheIndex ?? throw new ArgumentNullException(nameof(cacheIndex));
+        _maintenance = maintenance ?? throw new ArgumentNullException(nameof(maintenance));
+        _logger = logger ?? throw new ArgumentNullException(nameof(logger));
+    }
+
+    public async ValueTask InvalidateAsync(VexQuerySignature signature, VexExportFormat format, CancellationToken cancellationToken)
+    {
+        ArgumentNullException.ThrowIfNull(signature);
+        await _cacheIndex.RemoveAsync(signature, format, cancellationToken).ConfigureAwait(false);
+        _logger.LogInformation("Invalidated export cache entry {Signature} ({Format})", signature.Value, format);
+    }
+
+    public ValueTask PruneExpiredAsync(DateTimeOffset asOf, CancellationToken cancellationToken)
+        => _maintenance.RemoveExpiredAsync(asOf, cancellationToken);
+
+    public ValueTask PruneDanglingAsync(CancellationToken cancellationToken)
+        => _maintenance.RemoveMissingManifestReferencesAsync(cancellationToken);
+}
+
+public static class VexExportCacheServiceCollectionExtensions
+{
+    public static IServiceCollection AddVexExportCacheServices(this IServiceCollection services)
+    {
+        services.AddSingleton();
+        return services;
+    }
+}
diff --git a/src/StellaOps.Vexer.Policy/IVexPolicyProvider.cs b/src/StellaOps.Vexer.Policy/IVexPolicyProvider.cs
index b591aec5..b0a675f8 100644
--- a/src/StellaOps.Vexer.Policy/IVexPolicyProvider.cs
+++ b/src/StellaOps.Vexer.Policy/IVexPolicyProvider.cs
@@ -1,3 +1,4 @@
+using System.Collections.Generic;
 using System.Collections.Immutable;
 using System.Globalization;
 using Microsoft.Extensions.DependencyInjection;
@@ -27,13 +28,17 @@ public sealed record VexPolicySnapshot(
     string Version,
     VexConsensusPolicyOptions ConsensusOptions,
     IVexConsensusPolicy ConsensusPolicy,
-    ImmutableArray Issues)
+    ImmutableArray Issues,
+    string RevisionId,
+    string Digest)
 {
     public static readonly VexPolicySnapshot Default = new(
         VexConsensusPolicyOptions.BaselineVersion,
         new VexConsensusPolicyOptions(),
         new BaselineVexConsensusPolicy(),
-        ImmutableArray.Empty);
+        ImmutableArray.Empty,
+        "rev-0",
+        string.Empty);
 }
 
 public sealed record VexPolicyIssue(
@@ -51,6 +56,10 @@ public sealed class VexPolicyProvider : IVexPolicyProvider
 {
     private readonly IOptionsMonitor _options;
     private readonly ILogger _logger;
+    private readonly object _sync = new();
+    private long _revisionCounter;
+    private string? _currentRevisionId;
+    private string? _currentDigest;
 
     public VexPolicyProvider(
         IOptionsMonitor options,
@@ -68,36 +77,48 @@ public sealed class VexPolicyProvider : IVexPolicyProvider
 
     private VexPolicySnapshot BuildSnapshot(VexPolicyOptions options)
     {
-        var issues = ImmutableArray.CreateBuilder();
+        var normalization = VexPolicyProcessing.Normalize(options);
+        var digest = VexPolicyDigest.Compute(normalization.ConsensusOptions);
+        string revisionId;
+        bool isNewRevision;
 
-        if (!TryNormalizeWeights(options.Weights, out var weightOptions, issues))
+        lock (_sync)
         {
-            issues.Add(new VexPolicyIssue(
-                "weights.invalid",
-                "Weight configuration is invalid; falling back to defaults.",
-                VexPolicyIssueSeverity.Warning));
-            weightOptions = new VexConsensusPolicyOptions();
+            if (!string.Equals(_currentDigest, digest, StringComparison.Ordinal))
+            {
+                _revisionCounter++;
+                revisionId = $"rev-{_revisionCounter}";
+                _currentDigest = digest;
+                _currentRevisionId = revisionId;
+                isNewRevision = true;
+            }
+            else
+            {
+                revisionId = _currentRevisionId ?? "rev-0";
+                isNewRevision = false;
+            }
         }
 
-        var overrides = NormalizeOverrides(options.ProviderOverrides, issues);
-
-        var consensusOptions = new VexConsensusPolicyOptions(
-            options.Version ?? VexConsensusPolicyOptions.BaselineVersion,
-            weightOptions.VendorWeight,
-            weightOptions.DistroWeight,
-            weightOptions.PlatformWeight,
-            weightOptions.HubWeight,
-            weightOptions.AttestationWeight,
-            overrides);
-
-        var policy = new BaselineVexConsensusPolicy(consensusOptions);
+        var policy = new BaselineVexConsensusPolicy(normalization.ConsensusOptions);
         var snapshot = new VexPolicySnapshot(
-            consensusOptions.Version,
-            consensusOptions,
+            normalization.ConsensusOptions.Version,
+            normalization.ConsensusOptions,
             policy,
-            issues.ToImmutable());
+            normalization.Issues,
+            revisionId,
+            digest);
 
-        if (snapshot.Issues.Length > 0)
+        if (isNewRevision)
+        {
+            _logger.LogInformation(
+                "Policy snapshot updated: revision {RevisionId}, version {Version}, digest {Digest}, issues {IssueCount}",
+                snapshot.RevisionId,
+                snapshot.Version,
+                snapshot.Digest,
+                snapshot.Issues.Length);
+            VexPolicyTelemetry.RecordReload(snapshot.RevisionId, snapshot.Version, snapshot.Issues.Length);
+        }
+        else if (snapshot.Issues.Length > 0)
         {
             foreach (var issue in snapshot.Issues)
             {
@@ -107,93 +128,6 @@ public sealed class VexPolicyProvider : IVexPolicyProvider
 
         return snapshot;
     }
-
-    private static bool TryNormalizeWeights(
-        VexPolicyWeightOptions options,
-        out VexConsensusPolicyOptions normalized,
-        ImmutableArray.Builder issues)
-    {
-        var hasAny = options is not null &&
-                     (options.Vendor.HasValue || options.Distro.HasValue ||
-                      options.Platform.HasValue || options.Hub.HasValue || options.Attestation.HasValue);
-
-        if (!hasAny)
-        {
-            normalized = new VexConsensusPolicyOptions();
-            return true;
-        }
-
-        var vendor = Clamp(options.Vendor, nameof(options.Vendor), issues);
-        var distro = Clamp(options.Distro, nameof(options.Distro), issues);
-        var platform = Clamp(options.Platform, nameof(options.Platform), issues);
-        var hub = Clamp(options.Hub, nameof(options.Hub), issues);
-        var attestation = Clamp(options.Attestation, nameof(options.Attestation), issues);
-
-        normalized = new VexConsensusPolicyOptions(
-            VexConsensusPolicyOptions.BaselineVersion,
-            vendor ?? 1.0,
-            distro ?? 0.9,
-            platform ?? 0.7,
-            hub ?? 0.5,
-            attestation ?? 0.6);
-        return true;
-    }
-
-    private static double? Clamp(double? value, string fieldName, ImmutableArray.Builder issues)
-    {
-        if (value is null)
-        {
-            return null;
-        }
-
-        if (double.IsNaN(value.Value) || double.IsInfinity(value.Value))
-        {
-            issues.Add(new VexPolicyIssue(
-                $"weights.{fieldName}.invalid",
-                $"{fieldName} must be a finite number.",
-                VexPolicyIssueSeverity.Warning));
-            return null;
-        }
-
-        if (value.Value < 0 || value.Value > 1)
-        {
-            issues.Add(new VexPolicyIssue(
-                $"weights.{fieldName}.range",
-                $"{fieldName} must be between 0 and 1; value {value.Value.ToString(CultureInfo.InvariantCulture)} was clamped.",
-                VexPolicyIssueSeverity.Warning));
-            return Math.Clamp(value.Value, 0, 1);
-        }
-
-        return value.Value;
-    }
-
-    private static ImmutableDictionary NormalizeOverrides(
-        IDictionary? overrides,
-        ImmutableArray.Builder issues)
-    {
-        if (overrides is null || overrides.Count == 0)
-        {
-            return ImmutableDictionary.Empty;
-        }
-
-        var builder = ImmutableDictionary.CreateBuilder(StringComparer.Ordinal);
-        foreach (var kvp in overrides)
-        {
-            if (string.IsNullOrWhiteSpace(kvp.Key))
-            {
-                issues.Add(new VexPolicyIssue(
-                    "overrides.key.missing",
-                    "Encountered provider override with empty key; ignoring entry.",
-                    VexPolicyIssueSeverity.Warning));
-                continue;
-            }
-
-            var weight = Clamp(kvp.Value, $"overrides.{kvp.Key}", issues) ?? kvp.Value;
-            builder[kvp.Key.Trim()] = weight;
-        }
-
-        return builder.ToImmutable();
-    }
 }
 
 public sealed class VexPolicyEvaluator : IVexPolicyEvaluator
diff --git a/src/StellaOps.Vexer.Policy/StellaOps.Vexer.Policy.csproj b/src/StellaOps.Vexer.Policy/StellaOps.Vexer.Policy.csproj
index 2d2c59e1..95980268 100644
--- a/src/StellaOps.Vexer.Policy/StellaOps.Vexer.Policy.csproj
+++ b/src/StellaOps.Vexer.Policy/StellaOps.Vexer.Policy.csproj
@@ -9,6 +9,7 @@
   
     
     
+    
   
   
     
diff --git a/src/StellaOps.Vexer.Policy/TASKS.md b/src/StellaOps.Vexer.Policy/TASKS.md
index 06af2572..6fd59797 100644
--- a/src/StellaOps.Vexer.Policy/TASKS.md
+++ b/src/StellaOps.Vexer.Policy/TASKS.md
@@ -4,6 +4,8 @@ If you are working on this file you need to read docs/ARCHITECTURE_VEXER.md and
 |---|---|---|---|
 |VEXER-POLICY-01-001 – Policy schema & binding|Team Vexer Policy|VEXER-CORE-01-001|DONE (2025-10-15) – Established `VexPolicyOptions`, options binding, and snapshot provider covering baseline weights/overrides.|
 |VEXER-POLICY-01-002 – Policy evaluator service|Team Vexer Policy|VEXER-POLICY-01-001|DONE (2025-10-15) – `VexPolicyEvaluator` exposes immutable snapshots to consensus and normalizes rejection reasons.|
-|VEXER-POLICY-01-003 – Operator diagnostics & docs|Team Vexer Policy|VEXER-POLICY-01-001|TODO – Surface structured diagnostics (CLI/WebService) and author policy upgrade guidance in docs/ARCHITECTURE_VEXER.md appendix.|
-|VEXER-POLICY-01-004 – Policy schema validation & YAML binding|Team Vexer Policy|VEXER-POLICY-01-001|TODO – Add strongly-typed YAML/JSON binding, schema validation, and deterministic diagnostics for operator-supplied policy bundles.|
-|VEXER-POLICY-01-005 – Policy change tracking & telemetry|Team Vexer Policy|VEXER-POLICY-01-002|TODO – Emit revision history, expose snapshot digests via CLI/WebService, and add structured logging/metrics for policy reloads.|
+|VEXER-POLICY-01-003 – Operator diagnostics & docs|Team Vexer Policy|VEXER-POLICY-01-001|**DONE (2025-10-16)** – Surface structured diagnostics (CLI/WebService) and author policy upgrade guidance in docs/ARCHITECTURE_VEXER.md appendix.
2025-10-16: Added `IVexPolicyDiagnostics`/`VexPolicyDiagnosticsReport`, sorted issue ordering, recommendations, and appendix guidance. Tests: `dotnet test src/StellaOps.Vexer.Core.Tests/StellaOps.Vexer.Core.Tests.csproj`.|
+|VEXER-POLICY-01-004 – Policy schema validation & YAML binding|Team Vexer Policy|VEXER-POLICY-01-001|**DONE (2025-10-16)** – Added strongly-typed YAML/JSON binding, schema validation, and deterministic diagnostics for operator-supplied policy bundles.|
+|VEXER-POLICY-01-005 – Policy change tracking & telemetry|Team Vexer Policy|VEXER-POLICY-01-002|**DONE (2025-10-16)** – Emit revision history, expose snapshot digests via CLI/WebService, and add structured logging/metrics for policy reloads.
2025-10-16: `VexPolicySnapshot` now carries revision/digest, provider logs reloads, `vex.policy.reloads` metric emitted, binder/diagnostics expose digest metadata. Tests: `dotnet test src/StellaOps.Vexer.Core.Tests/StellaOps.Vexer.Core.Tests.csproj`.|
+|VEXER-POLICY-02-001 – Scoring coefficients & weight ceilings|Team Vexer Policy|VEXER-POLICY-01-004|TODO – Extend `VexPolicyOptions` with α/β boosters and optional >1.0 weight ceilings, validate ranges, and document operator guidance in `docs/ARCHITECTURE_VEXER.md`/`docs/VEXER_SCORRING.md`.|
+|VEXER-POLICY-02-002 – Diagnostics for scoring signals|Team Vexer Policy|VEXER-POLICY-02-001|BACKLOG – Update diagnostics reports to surface missing severity/KEV/EPSS mappings, coefficient overrides, and provide actionable recommendations for policy tuning.|
diff --git a/src/StellaOps.Vexer.Policy/VexPolicyBinder.cs b/src/StellaOps.Vexer.Policy/VexPolicyBinder.cs
new file mode 100644
index 00000000..5e09de5b
--- /dev/null
+++ b/src/StellaOps.Vexer.Policy/VexPolicyBinder.cs
@@ -0,0 +1,94 @@
+using System.Collections.Immutable;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Text.Json;
+using StellaOps.Vexer.Core;
+using YamlDotNet.Serialization;
+using YamlDotNet.Serialization.NamingConventions;
+
+namespace StellaOps.Vexer.Policy;
+
+public enum VexPolicyDocumentFormat
+{
+    Json,
+    Yaml,
+}
+
+public sealed record VexPolicyBindingResult(
+    bool Success,
+    VexPolicyOptions? Options,
+    VexConsensusPolicyOptions? NormalizedOptions,
+    ImmutableArray Issues);
+
+public static class VexPolicyBinder
+{
+    public static VexPolicyBindingResult Bind(string content, VexPolicyDocumentFormat format)
+    {
+        if (string.IsNullOrWhiteSpace(content))
+        {
+            return Failure("policy.empty", "Policy document is empty.");
+        }
+
+        try
+        {
+            var options = Parse(content, format);
+            return Normalize(options);
+        }
+        catch (JsonException ex)
+        {
+            return Failure("policy.parse.json", $"Failed to parse JSON policy document: {ex.Message}");
+        }
+        catch (YamlDotNet.Core.YamlException ex)
+        {
+            return Failure("policy.parse.yaml", $"Failed to parse YAML policy document: {ex.Message}");
+        }
+    }
+
+    public static VexPolicyBindingResult Bind(Stream stream, VexPolicyDocumentFormat format, Encoding? encoding = null)
+    {
+        if (stream is null)
+        {
+            throw new ArgumentNullException(nameof(stream));
+        }
+
+        encoding ??= Encoding.UTF8;
+        using var reader = new StreamReader(stream, encoding, detectEncodingFromByteOrderMarks: true, leaveOpen: true);
+        var content = reader.ReadToEnd();
+        return Bind(content, format);
+    }
+
+    private static VexPolicyBindingResult Normalize(VexPolicyOptions options)
+    {
+        var normalization = VexPolicyProcessing.Normalize(options);
+        var hasErrors = normalization.Issues.Any(static issue => issue.Severity == VexPolicyIssueSeverity.Error);
+        return new VexPolicyBindingResult(!hasErrors, options, normalization.ConsensusOptions, normalization.Issues);
+    }
+
+    private static VexPolicyBindingResult Failure(string code, string message)
+    {
+        var issue = new VexPolicyIssue(code, message, VexPolicyIssueSeverity.Error);
+        return new VexPolicyBindingResult(false, null, null, ImmutableArray.Create(issue));
+    }
+
+    private static VexPolicyOptions Parse(string content, VexPolicyDocumentFormat format)
+    {
+        return format switch
+        {
+            VexPolicyDocumentFormat.Json => JsonSerializer.Deserialize(content, new JsonSerializerOptions
+            {
+                PropertyNameCaseInsensitive = true,
+                ReadCommentHandling = JsonCommentHandling.Skip,
+                AllowTrailingCommas = true,
+            }) ?? new VexPolicyOptions(),
+            VexPolicyDocumentFormat.Yaml => BuildYamlDeserializer().Deserialize(content) ?? new VexPolicyOptions(),
+            _ => throw new ArgumentOutOfRangeException(nameof(format), format, "Unsupported policy document format."),
+        };
+    }
+
+    private static IDeserializer BuildYamlDeserializer()
+        => new DeserializerBuilder()
+            .WithNamingConvention(CamelCaseNamingConvention.Instance)
+            .IgnoreUnmatchedProperties()
+            .Build();
+}
diff --git a/src/StellaOps.Vexer.Policy/VexPolicyDiagnostics.cs b/src/StellaOps.Vexer.Policy/VexPolicyDiagnostics.cs
new file mode 100644
index 00000000..da4ce4b8
--- /dev/null
+++ b/src/StellaOps.Vexer.Policy/VexPolicyDiagnostics.cs
@@ -0,0 +1,87 @@
+using System;
+using System.Collections.Immutable;
+using System.Linq;
+
+namespace StellaOps.Vexer.Policy;
+
+public interface IVexPolicyDiagnostics
+{
+    VexPolicyDiagnosticsReport GetDiagnostics();
+}
+
+public sealed record VexPolicyDiagnosticsReport(
+    string Version,
+    string RevisionId,
+    string Digest,
+    int ErrorCount,
+    int WarningCount,
+    DateTimeOffset GeneratedAt,
+    ImmutableArray Issues,
+    ImmutableArray Recommendations,
+    ImmutableDictionary ActiveOverrides);
+
+public sealed class VexPolicyDiagnostics : IVexPolicyDiagnostics
+{
+    private readonly IVexPolicyProvider _policyProvider;
+    private readonly TimeProvider _timeProvider;
+
+    public VexPolicyDiagnostics(
+        IVexPolicyProvider policyProvider,
+        TimeProvider? timeProvider = null)
+    {
+        _policyProvider = policyProvider ?? throw new ArgumentNullException(nameof(policyProvider));
+        _timeProvider = timeProvider ?? TimeProvider.System;
+    }
+
+    public VexPolicyDiagnosticsReport GetDiagnostics()
+    {
+        var snapshot = _policyProvider.GetSnapshot();
+        var issues = snapshot.Issues;
+
+        var errorCount = issues.Count(static issue => issue.Severity == VexPolicyIssueSeverity.Error);
+        var warningCount = issues.Count(static issue => issue.Severity == VexPolicyIssueSeverity.Warning);
+        var overrides = snapshot.ConsensusOptions.ProviderOverrides
+            .OrderBy(static pair => pair.Key, StringComparer.Ordinal)
+            .ToImmutableDictionary();
+
+        var recommendations = BuildRecommendations(errorCount, warningCount, overrides);
+
+        return new VexPolicyDiagnosticsReport(
+            snapshot.Version,
+            snapshot.RevisionId,
+            snapshot.Digest,
+            errorCount,
+            warningCount,
+            _timeProvider.GetUtcNow(),
+            issues,
+            recommendations,
+            overrides);
+    }
+
+    private static ImmutableArray BuildRecommendations(
+        int errorCount,
+        int warningCount,
+        ImmutableDictionary overrides)
+    {
+        var messages = ImmutableArray.CreateBuilder