From fecb05cbd0b1ae0758c129401345ac3cd11a4523 Mon Sep 17 00:00:00 2001 From: Leo Williamson Date: Tue, 18 Aug 2026 15:13:05 -0400 Subject: [PATCH 01/27] Add public active CNA list endpoint --- .../list-active-cnas-response.json | 21 +++++++++++++++ src/controller/registry.controller/index.js | 26 +++++++++++++++++++ .../org.registry.controller.js | 10 +++++++ .../registry-org/activeCnaListTest.js | 19 ++++++++++++++ test/unit-tests/org/activeCnaListTest.js | 13 ++++++++++ 5 files changed, 89 insertions(+) create mode 100644 schemas/registry-org/list-active-cnas-response.json create mode 100644 test/integration-tests/registry-org/activeCnaListTest.js create mode 100644 test/unit-tests/org/activeCnaListTest.js diff --git a/schemas/registry-org/list-active-cnas-response.json b/schemas/registry-org/list-active-cnas-response.json new file mode 100644 index 000000000..27b6b392c --- /dev/null +++ b/schemas/registry-org/list-active-cnas-response.json @@ -0,0 +1,21 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "array", + "items": { + "type": "object", + "required": ["shortName", "cnaID", "organizationName", "scope", "contact", "disclosurePolicy", "securityAdvisories", "resources", "CNA", "country"], + "properties": { + "shortName": { "type": "string" }, + "cnaID": { "type": "string" }, + "organizationName": { "type": "string" }, + "scope": { "type": "string" }, + "contact": { "type": "array" }, + "disclosurePolicy": { "type": "array" }, + "securityAdvisories": { "type": "object" }, + "resources": { "type": "array" }, + "CNA": { "type": "object" }, + "country": { "type": "string" } + }, + "additionalProperties": false + } +} diff --git a/src/controller/registry.controller/index.js b/src/controller/registry.controller/index.js index 2735f8cdf..f6f4fcea1 100644 --- a/src/controller/registry.controller/index.js +++ b/src/controller/registry.controller/index.js @@ -92,6 +92,32 @@ router.get('/registry/org', registryOrgController.ALL_ORGS ) +router.get('/registry/org/cnas', + /* + #swagger.tags = ['Registry Organization'] + #swagger.operationId = 'registryOrgActiveCnas' + #swagger.summary = 'Lists active CNAs in the CVE.org public format' + #swagger.description = 'This public endpoint returns the canonical CVE.org active CNA list.' + #swagger.responses[200] = { + description: 'Returns active CNAs in the CVE.org public format', + content: { + 'application/json': { + schema: { $ref: '../schemas/registry-org/list-active-cnas-response.json' } + } + } + } + #swagger.responses[500] = { + description: 'Internal Server Error', + content: { + 'application/json': { + schema: { $ref: '../schemas/errors/generic.json' } + } + } + } + */ + registryOrgController.ACTIVE_CNAS +) + router.get('/registry/org/:shortname/users', /* #swagger.tags = ['Registry User'] diff --git a/src/controller/registry.controller/org.registry.controller.js b/src/controller/registry.controller/org.registry.controller.js index 56ff62871..881fe1b37 100644 --- a/src/controller/registry.controller/org.registry.controller.js +++ b/src/controller/registry.controller/org.registry.controller.js @@ -1,6 +1,7 @@ /** Registry organization route handlers. */ const mongoose = require('mongoose') const logger = require('../../middleware/logger') +const activeCnaList = require('../../scripts/CNAlist.json') const { getConstants } = require('../../constants') const _ = require('lodash') const errors = require('./org.error') @@ -162,6 +163,14 @@ async function getAllOrgs (req, res, next) { * @description This endpoint is accessible to Secretariat only. It retrieves information about the specified registry organization. * Called by GET /api/registry/org/:identifier */ +async function getActiveCnas (req, res, next) { + try { + return res.status(200).json(activeCnaList) + } catch (err) { + next(err) + } +} + async function getOrg (req, res, next) { try { const repo = req.ctx.repositories.getBaseOrgRepository() @@ -1005,6 +1014,7 @@ async function editConversationForOrg (req, res, next) { module.exports = { ALL_ORGS: getAllOrgs, + ACTIVE_CNAS: getActiveCnas, SINGLE_ORG: getOrg, CREATE_ORG: createOrg, UPDATE_ORG: updateOrg, diff --git a/test/integration-tests/registry-org/activeCnaListTest.js b/test/integration-tests/registry-org/activeCnaListTest.js new file mode 100644 index 000000000..2365c903f --- /dev/null +++ b/test/integration-tests/registry-org/activeCnaListTest.js @@ -0,0 +1,19 @@ +/* eslint-disable no-unused-expressions */ +const chai = require('chai') +const crypto = require('crypto') +const expect = chai.expect +chai.use(require('chai-http')) + +const app = require('../../../src/index.js') +const activeCnaList = require('../../../src/scripts/CNAlist.json') + +describe('Public active CNA list', () => { + it('returns the canonical active CNA list without authentication', async () => { + const res = await chai.request(app).get('/api/registry/org/cnas') + + expect(res).to.have.status(200) + const expectedHash = crypto.createHash('sha256').update(JSON.stringify(activeCnaList)).digest('hex') + const responseHash = crypto.createHash('sha256').update(res.text).digest('hex') + expect(responseHash).to.equal(expectedHash) + }) +}) diff --git a/test/unit-tests/org/activeCnaListTest.js b/test/unit-tests/org/activeCnaListTest.js new file mode 100644 index 000000000..f0c033a26 --- /dev/null +++ b/test/unit-tests/org/activeCnaListTest.js @@ -0,0 +1,13 @@ +const { expect } = require('chai') +const sinon = require('sinon') +const { ACTIVE_CNAS } = require('../../../src/controller/registry.controller/org.registry.controller') +const activeCnaList = require('../../../src/scripts/CNAlist.json') + +describe('Active CNA list', () => { + it('returns the canonical CVE.org active CNA list without authentication', async () => { + const res = { status: sinon.stub().returnsThis(), json: sinon.stub() } + await ACTIVE_CNAS({}, res, sinon.stub()) + expect(res.status.calledWith(200)).to.equal(true) + expect(res.json.calledOnceWith(activeCnaList)).to.equal(true) + }) +}) From a587da99b77f0898efea0833acff5806da3ca602 Mon Sep 17 00:00:00 2001 From: Leo Williamson Date: Wed, 2 Sep 2026 10:21:11 -0400 Subject: [PATCH 02/27] Refactor public CNA endpoint to use registry database --- .../list-active-cnas-response.json | 87 ++++++++++- src/controller/registry.controller/index.js | 2 +- .../org.registry.controller.js | 74 +++++++++- src/repositories/baseOrgRepository.js | 82 +++++++++++ .../registry-org/activeCnaListTest.js | 96 +++++++++++- test/unit-tests/org/activeCnaListTest.js | 138 +++++++++++++++++- 6 files changed, 455 insertions(+), 24 deletions(-) diff --git a/schemas/registry-org/list-active-cnas-response.json b/schemas/registry-org/list-active-cnas-response.json index 27b6b392c..ffd862fff 100644 --- a/schemas/registry-org/list-active-cnas-response.json +++ b/schemas/registry-org/list-active-cnas-response.json @@ -1,5 +1,63 @@ { "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "link": { + "type": "object", + "required": ["label", "url"], + "properties": { + "label": { "type": "string" }, + "url": { "type": "string" } + }, + "additionalProperties": false + }, + "policyLink": { + "type": "object", + "required": ["label", "language", "url"], + "properties": { + "label": { "type": "string" }, + "language": { "type": "string" }, + "url": { "type": "string" } + }, + "additionalProperties": false + }, + "email": { + "type": "object", + "required": ["label", "emailAddr"], + "properties": { + "label": { "type": "string" }, + "emailAddr": { "type": "string" } + }, + "additionalProperties": false + }, + "contact": { + "type": "object", + "required": ["email", "contact", "form"], + "properties": { + "email": { "type": "array", "items": { "$ref": "#/definitions/email" } }, + "contact": { "type": "array", "items": { "$ref": "#/definitions/link" } }, + "form": { "type": "array", "items": { "$ref": "#/definitions/link" } } + }, + "additionalProperties": false + }, + "orgReference": { + "type": "object", + "required": ["shortName", "organizationName"], + "properties": { + "shortName": { "type": "string" }, + "organizationName": { "type": "string" } + }, + "additionalProperties": false + }, + "role": { + "type": "object", + "required": ["helpText", "role"], + "properties": { + "helpText": { "type": "string" }, + "role": { "type": "string" } + }, + "additionalProperties": false + } + }, "type": "array", "items": { "type": "object", @@ -9,11 +67,30 @@ "cnaID": { "type": "string" }, "organizationName": { "type": "string" }, "scope": { "type": "string" }, - "contact": { "type": "array" }, - "disclosurePolicy": { "type": "array" }, - "securityAdvisories": { "type": "object" }, - "resources": { "type": "array" }, - "CNA": { "type": "object" }, + "contact": { "type": "array", "items": { "$ref": "#/definitions/contact" } }, + "disclosurePolicy": { "type": "array", "items": { "$ref": "#/definitions/policyLink" } }, + "securityAdvisories": { + "type": "object", + "required": ["alerts", "advisories"], + "properties": { + "alerts": { "type": "array", "items": { "$ref": "#/definitions/link" } }, + "advisories": { "type": "array", "items": { "$ref": "#/definitions/link" } } + }, + "additionalProperties": false + }, + "resources": { "type": "array", "items": { "$ref": "#/definitions/link" } }, + "CNA": { + "type": "object", + "required": ["isRoot", "root", "type", "TLR", "roles"], + "properties": { + "isRoot": { "type": "boolean" }, + "root": { "$ref": "#/definitions/orgReference" }, + "type": { "type": "array", "items": { "type": "string" } }, + "TLR": { "$ref": "#/definitions/orgReference" }, + "roles": { "type": "array", "items": { "$ref": "#/definitions/role" } } + }, + "additionalProperties": false + }, "country": { "type": "string" } }, "additionalProperties": false diff --git a/src/controller/registry.controller/index.js b/src/controller/registry.controller/index.js index f6f4fcea1..41f2accd8 100644 --- a/src/controller/registry.controller/index.js +++ b/src/controller/registry.controller/index.js @@ -97,7 +97,7 @@ router.get('/registry/org/cnas', #swagger.tags = ['Registry Organization'] #swagger.operationId = 'registryOrgActiveCnas' #swagger.summary = 'Lists active CNAs in the CVE.org public format' - #swagger.description = 'This public endpoint returns the canonical CVE.org active CNA list.' + #swagger.description = 'This public endpoint builds the active CNA list from registry organization data.' #swagger.responses[200] = { description: 'Returns active CNAs in the CVE.org public format', content: { diff --git a/src/controller/registry.controller/org.registry.controller.js b/src/controller/registry.controller/org.registry.controller.js index 881fe1b37..4b2059951 100644 --- a/src/controller/registry.controller/org.registry.controller.js +++ b/src/controller/registry.controller/org.registry.controller.js @@ -1,7 +1,6 @@ /** Registry organization route handlers. */ const mongoose = require('mongoose') const logger = require('../../middleware/logger') -const activeCnaList = require('../../scripts/CNAlist.json') const { getConstants } = require('../../constants') const _ = require('lodash') const errors = require('./org.error') @@ -151,26 +150,86 @@ async function getAllOrgs (req, res, next) { } } +function asUrlEntries (urls, label) { + return (urls || []).filter(Boolean).map(url => ({ label, url: url.trim() })) +} + +function asPublicOrgReference (org) { + return org + ? { shortName: org.short_name || 'n/a', organizationName: org.long_name || 'n/a' } + : { shortName: 'n/a', organizationName: 'n/a' } +} + +function mapAuthorityRoles (authority, isRoot, isTopLevelRoot) { + const roles = [] + if (isRoot) roles.push({ helpText: '', role: isTopLevelRoot ? 'Top-Level Root' : 'Root' }) + if (authority.includes('CNA')) roles.push({ helpText: '', role: isRoot ? 'CNA-LR' : 'CNA' }) + if (authority.includes('ADP')) roles.push({ helpText: '', role: 'ADP' }) + if (authority.includes('SECRETARIAT')) roles.push({ helpText: '', role: 'Secretariat' }) + return roles.length ? roles : [{ helpText: '', role: 'CNA' }] +} + +function mapActiveCnaToPublicFormat (org) { + const emails = (org.contact_info?.emails || []).filter(Boolean).map(emailAddr => ({ label: 'Email', emailAddr })) + const contacts = asUrlEntries(org.contact_info?.websites, 'Website') + const authority = org.authority || [] + const isRoot = org.__t === 'RootOrg' || authority.includes('ROOT') + const isTopLevelRoot = isRoot && String(org.top_level_root).toLowerCase() === 'true' + + return { + shortName: org.short_name || '', + cnaID: org.partner_number || '', + organizationName: org.long_name || '', + scope: org.charter_or_scope || '', + contact: [{ email: emails, contact: contacts, form: [] }], + disclosurePolicy: asUrlEntries((org.disclosure_policy || '').split(';'), 'Policy').map(policy => ({ ...policy, language: '' })), + securityAdvisories: { alerts: [], advisories: asUrlEntries(org.advisory_locations, 'Advisories') }, + resources: [], + CNA: { + isRoot, + root: isRoot ? asPublicOrgReference() : asPublicOrgReference(org._root), + type: org.partner_role_type || [], + TLR: isTopLevelRoot ? asPublicOrgReference() : asPublicOrgReference(org._tlr), + roles: mapAuthorityRoles(authority, isRoot, isTopLevelRoot) + }, + country: org.partner_country || '' + } +} + /** - * Retrieves information about a specific registry organization. + * Retrieves active CNA partners in the public CVE.org response format. * * @async - * @function getOrg - * @param {object} req - The Express request object, containing the organization identifier in `req.ctx.params.identifier`. + * @function getActiveCnas + * @param {object} req - The Express request object. * @param {object} res - The Express response object. * @param {function} next - The next middleware function. * @returns {Promise} - A promise that resolves when the response is sent. - * @description This endpoint is accessible to Secretariat only. It retrieves information about the specified registry organization. - * Called by GET /api/registry/org/:identifier + * @description This endpoint is public and reads active CNA data from the registry database. + * Called by GET /api/registry/org/cnas */ async function getActiveCnas (req, res, next) { try { - return res.status(200).json(activeCnaList) + const repo = req.ctx.repositories.getBaseOrgRepository() + const activeCnas = await repo.getActiveCnas() + return res.status(200).json(activeCnas.map(mapActiveCnaToPublicFormat)) } catch (err) { next(err) } } +/** + * Retrieves information about a specific registry organization. + * + * @async + * @function getOrg + * @param {object} req - The Express request object, containing the organization identifier in `req.ctx.params.identifier`. + * @param {object} res - The Express response object. + * @param {function} next - The next middleware function. + * @returns {Promise} - A promise that resolves when the response is sent. + * @description This endpoint is accessible to Secretariat only. It retrieves information about the specified registry organization. + * Called by GET /api/registry/org/:identifier + */ async function getOrg (req, res, next) { try { const repo = req.ctx.repositories.getBaseOrgRepository() @@ -1015,6 +1074,7 @@ async function editConversationForOrg (req, res, next) { module.exports = { ALL_ORGS: getAllOrgs, ACTIVE_CNAS: getActiveCnas, + mapActiveCnaToPublicFormat, SINGLE_ORG: getOrg, CREATE_ORG: createOrg, UPDATE_ORG: updateOrg, diff --git a/src/repositories/baseOrgRepository.js b/src/repositories/baseOrgRepository.js index 8585ef2aa..ad2f41e6a 100644 --- a/src/repositories/baseOrgRepository.js +++ b/src/repositories/baseOrgRepository.js @@ -25,6 +25,47 @@ function exactCaseInsensitiveRegex (value) { return new RegExp(`^${_.escapeRegExp(String(value))}$`, 'i') } +function normalizeOrgReference (value) { + if (typeof value !== 'string') return null + const normalized = value.trim().toLowerCase().replace(/\s+tlr$/, '').replace(/[^a-z0-9]+/g, '') + return normalized || null +} + +function addOrgReferences (orgByReference, org) { + const references = [org.short_name, org.long_name, ...(org.aliases || [])] + references.forEach(reference => { + const normalized = normalizeOrgReference(reference) + if (normalized && !orgByReference.has(normalized)) orgByReference.set(normalized, org) + }) +} + +function isRootOrg (org) { + return org?.__t === 'RootOrg' || org?.authority?.includes('ROOT') +} + +function decorateCnaRelationships (activeOrgs, rootOrgs) { + const orgByReference = new Map() + ;[...activeOrgs, ...rootOrgs].forEach(org => addOrgReferences(orgByReference, org)) + + return activeOrgs.map(org => { + const root = rootOrgs.find(candidate => candidate.UUID !== org.UUID && candidate.oversees?.includes(org.UUID)) + const tlrReference = org.top_level_root || root?.top_level_root + let tlr + + if (String(tlrReference).toLowerCase() === 'true') { + tlr = isRootOrg(org) ? org : root + } else if (!['false', 'n/a'].includes(String(tlrReference).toLowerCase())) { + tlr = orgByReference.get(normalizeOrgReference(tlrReference)) + } + + return { + ...org, + _root: root, + _tlr: tlr + } + }) +} + function isResponseExtensionField (key) { return key.startsWith('_') && !INTERNAL_UNDERSCORE_FIELDS.includes(key) } @@ -562,6 +603,47 @@ class BaseOrgRepository extends BaseRepository { return data } + /** + * Retrieves active CNA organizations for the unauthenticated public partner list. + * + * The status comparison supports normalized lowercase and legacy title-case data. + * + * @returns {Promise} Active CNA organization documents. + */ + async getActiveCnas () { + const projection = { + _id: false, + __t: true, + UUID: true, + short_name: true, + long_name: true, + aliases: true, + partner_number: true, + charter_or_scope: true, + disclosure_policy: true, + advisory_locations: true, + contact_info: true, + partner_role_type: true, + partner_country: true, + top_level_root: true, + authority: true, + oversees: true + } + const activeCnaQuery = BaseOrgModel.find({ + authority: { $in: ['CNA', 'ROOT'] }, + 'program_data.status': { $in: ['active', 'Active'] } + }) + .select(projection) + .sort({ short_name: 1 }) + .lean() + const rootOrgQuery = RootOrgModel.find({}) + .select(projection) + .lean() + const [activeOrgs, rootOrgs] = await Promise.all([activeCnaQuery, rootOrgQuery]) + + return decorateCnaRelationships(activeOrgs, rootOrgs) + } + /** * @async * @function getOrgObject diff --git a/test/integration-tests/registry-org/activeCnaListTest.js b/test/integration-tests/registry-org/activeCnaListTest.js index 2365c903f..c82489bb2 100644 --- a/test/integration-tests/registry-org/activeCnaListTest.js +++ b/test/integration-tests/registry-org/activeCnaListTest.js @@ -1,19 +1,103 @@ /* eslint-disable no-unused-expressions */ const chai = require('chai') -const crypto = require('crypto') const expect = chai.expect chai.use(require('chai-http')) +const { v4: uuidv4 } = require('uuid') const app = require('../../../src/index.js') -const activeCnaList = require('../../../src/scripts/CNAlist.json') +const BaseOrg = require('../../../src/model/baseorg') + +const runId = uuidv4() +const uuids = { + tlr: uuidv4(), + root: uuidv4(), + child: uuidv4(), + inactive: uuidv4() +} +const shortNames = { + tlr: `public-tlr-${runId}`, + root: `public-root-${runId}`, + child: `public-cna-${runId}`, + inactive: `public-inactive-${runId}` +} describe('Public active CNA list', () => { - it('returns the canonical active CNA list without authentication', async () => { + before(async () => { + await BaseOrg.collection.insertMany([{ + __t: 'RootOrg', + UUID: uuids.tlr, + short_name: shortNames.tlr, + long_name: 'Public Test TLR', + authority: ['ROOT'], + top_level_root: 'true', + oversees: [uuids.root], + program_data: { status: 'active' } + }, { + __t: 'RootOrg', + UUID: uuids.root, + short_name: shortNames.root, + long_name: 'Public Test Root', + authority: ['ROOT'], + top_level_root: `${shortNames.tlr} TLR`, + oversees: [uuids.child], + program_data: { status: 'active' } + }, { + __t: 'CNAOrg', + UUID: uuids.child, + short_name: shortNames.child, + long_name: 'Public Test CNA', + authority: ['CNA'], + top_level_root: `${shortNames.tlr} TLR`, + partner_number: 'CNA-TEST-0001', + partner_role_type: ['Vendor'], + partner_country: 'USA', + charter_or_scope: 'Public endpoint integration test.', + contact_info: { emails: ['security@example.test'], websites: ['https://example.test/contact'] }, + disclosure_policy: 'https://example.test/policy', + advisory_locations: ['https://example.test/advisories'], + program_data: { status: 'active' } + }, { + __t: 'CNAOrg', + UUID: uuids.inactive, + short_name: shortNames.inactive, + long_name: 'Inactive Public Test CNA', + authority: ['CNA'], + program_data: { status: 'inactive' } + }]) + }) + + after(async () => { + await BaseOrg.collection.deleteMany({ UUID: { $in: Object.values(uuids) } }) + }) + + it('returns database-backed active CNAs without authentication and excludes inactive CNAs', async () => { + const res = await chai.request(app).get('/api/registry/org/cnas') + + expect(res).to.have.status(200) + const child = res.body.find(org => org.shortName === shortNames.child) + expect(res.body.some(org => org.shortName === shortNames.inactive)).to.equal(false) + expect(child).to.include({ + cnaID: 'CNA-TEST-0001', + organizationName: 'Public Test CNA', + country: 'USA' + }) + }) + + it('resolves root and top-level-root relationships from database records', async () => { const res = await chai.request(app).get('/api/registry/org/cnas') expect(res).to.have.status(200) - const expectedHash = crypto.createHash('sha256').update(JSON.stringify(activeCnaList)).digest('hex') - const responseHash = crypto.createHash('sha256').update(res.text).digest('hex') - expect(responseHash).to.equal(expectedHash) + const child = res.body.find(org => org.shortName === shortNames.child) + const root = res.body.find(org => org.shortName === shortNames.root) + expect(child.CNA).to.deep.include({ + isRoot: false, + root: { shortName: shortNames.root, organizationName: 'Public Test Root' }, + TLR: { shortName: shortNames.tlr, organizationName: 'Public Test TLR' } + }) + expect(root.CNA).to.deep.include({ + isRoot: true, + root: { shortName: 'n/a', organizationName: 'n/a' }, + TLR: { shortName: shortNames.tlr, organizationName: 'Public Test TLR' } + }) }) }) diff --git a/test/unit-tests/org/activeCnaListTest.js b/test/unit-tests/org/activeCnaListTest.js index f0c033a26..3dee3ef28 100644 --- a/test/unit-tests/org/activeCnaListTest.js +++ b/test/unit-tests/org/activeCnaListTest.js @@ -1,13 +1,141 @@ const { expect } = require('chai') const sinon = require('sinon') -const { ACTIVE_CNAS } = require('../../../src/controller/registry.controller/org.registry.controller') -const activeCnaList = require('../../../src/scripts/CNAlist.json') +const { + ACTIVE_CNAS, + mapActiveCnaToPublicFormat +} = require('../../../src/controller/registry.controller/org.registry.controller') +const BaseOrgRepository = require('../../../src/repositories/baseOrgRepository') +const BaseOrgModel = require('../../../src/model/baseorg') +const RootOrgModel = require('../../../src/model/rootorg') describe('Active CNA list', () => { - it('returns the canonical CVE.org active CNA list without authentication', async () => { + afterEach(() => sinon.restore()) + + it('returns active CNAs from the database in the public partner-list format', async () => { const res = { status: sinon.stub().returnsThis(), json: sinon.stub() } - await ACTIVE_CNAS({}, res, sinon.stub()) + const activeCnas = [{ + UUID: 'child-uuid', + short_name: 'example', + partner_number: 'CNA-2026-0001', + long_name: 'Example Organization', + charter_or_scope: 'Example products.', + disclosure_policy: 'https://example.test/policy', + advisory_locations: ['https://example.test/advisories'], + contact_info: { emails: ['security@example.test'], websites: ['https://example.test/contact'] }, + partner_role_type: ['Vendor'], + partner_country: 'USA', + top_level_root: 'MITRE TLR', + authority: ['CNA'], + _root: { short_name: 'example-root', long_name: 'Example Root' }, + _tlr: { short_name: 'mitre', long_name: 'MITRE Corporation' } + }] + const repo = { getActiveCnas: sinon.stub().resolves(activeCnas) } + const req = { ctx: { repositories: { getBaseOrgRepository: () => repo } } } + + await ACTIVE_CNAS(req, res, sinon.stub()) + + expect(repo.getActiveCnas.calledOnce).to.equal(true) expect(res.status.calledWith(200)).to.equal(true) - expect(res.json.calledOnceWith(activeCnaList)).to.equal(true) + expect(res.json.firstCall.args[0]).to.deep.equal([{ + shortName: 'example', + cnaID: 'CNA-2026-0001', + organizationName: 'Example Organization', + scope: 'Example products.', + contact: [{ + email: [{ label: 'Email', emailAddr: 'security@example.test' }], + contact: [{ label: 'Website', url: 'https://example.test/contact' }], + form: [] + }], + disclosurePolicy: [{ label: 'Policy', url: 'https://example.test/policy', language: '' }], + securityAdvisories: { alerts: [], advisories: [{ label: 'Advisories', url: 'https://example.test/advisories' }] }, + resources: [], + CNA: { + isRoot: false, + root: { shortName: 'example-root', organizationName: 'Example Root' }, + type: ['Vendor'], + TLR: { shortName: 'mitre', organizationName: 'MITRE Corporation' }, + roles: [{ helpText: '', role: 'CNA' }] + }, + country: 'USA' + }]) + }) + + it('derives root roles and resolves a root CNA top-level root', () => { + const result = mapActiveCnaToPublicFormat({ + __t: 'RootOrg', + authority: ['ROOT', 'CNA'], + short_name: 'icscert', + long_name: 'ICS-CERT', + top_level_root: 'CISA TLR', + _tlr: { short_name: 'CISA', long_name: 'Cybersecurity and Infrastructure Security Agency (CISA)' } + }) + + expect(result.CNA).to.deep.equal({ + isRoot: true, + root: { shortName: 'n/a', organizationName: 'n/a' }, + type: [], + TLR: { + shortName: 'CISA', + organizationName: 'Cybersecurity and Infrastructure Security Agency (CISA)' + }, + roles: [ + { helpText: '', role: 'Root' }, + { helpText: '', role: 'CNA-LR' } + ] + }) + }) + + it('queries active CNA/root organizations and decorates their hierarchy', async () => { + const activeRecords = [{ + UUID: 'child-uuid', + short_name: 'example', + top_level_root: 'MITRE TLR' + }, { + UUID: 'tlr-uuid', + short_name: 'mitre', + long_name: 'MITRE Corporation', + __t: 'RootOrg', + authority: ['ROOT'], + top_level_root: 'true' + }] + const rootRecords = [{ + UUID: 'root-uuid', + short_name: 'example-root', + long_name: 'Example Root', + oversees: ['child-uuid'], + top_level_root: 'MITRE TLR' + }, { + UUID: 'tlr-uuid', + short_name: 'mitre', + long_name: 'MITRE Corporation', + oversees: [], + top_level_root: 'true' + }] + const activeQuery = { + select: sinon.stub(), + sort: sinon.stub(), + lean: sinon.stub().resolves(activeRecords) + } + activeQuery.select.returns(activeQuery) + activeQuery.sort.returns(activeQuery) + const rootQuery = { + select: sinon.stub(), + lean: sinon.stub().resolves(rootRecords) + } + rootQuery.select.returns(rootQuery) + sinon.stub(BaseOrgModel, 'find').returns(activeQuery) + sinon.stub(RootOrgModel, 'find').returns(rootQuery) + + const result = await new BaseOrgRepository().getActiveCnas() + + expect(BaseOrgModel.find.firstCall.args[0]).to.deep.equal({ + authority: { $in: ['CNA', 'ROOT'] }, + 'program_data.status': { $in: ['active', 'Active'] } + }) + expect(activeQuery.sort.calledOnceWith({ short_name: 1 })).to.equal(true) + expect(result[0]._root).to.equal(rootRecords[0]) + expect(result[0]._tlr).to.equal(activeRecords[1]) + expect(result[1]._root).to.equal(undefined) + expect(result[1]._tlr).to.equal(activeRecords[1]) }) }) From 7dbaee60c5c4260c9232d9d9f53bdbf29e232bb1 Mon Sep 17 00:00:00 2001 From: Leo Williamson Date: Wed, 2 Sep 2026 10:47:52 -0400 Subject: [PATCH 03/27] Fix public CNA hierarchy resolution --- src/repositories/baseOrgRepository.js | 36 +++++++++++-------- .../registry-org/activeCnaListTest.js | 8 ++--- test/unit-tests/org/activeCnaListTest.js | 30 +++++++++++----- 3 files changed, 48 insertions(+), 26 deletions(-) diff --git a/src/repositories/baseOrgRepository.js b/src/repositories/baseOrgRepository.js index ad2f41e6a..42c4cd3b0 100644 --- a/src/repositories/baseOrgRepository.js +++ b/src/repositories/baseOrgRepository.js @@ -31,6 +31,11 @@ function normalizeOrgReference (value) { return normalized || null } +function isUsableTlrReference (value) { + const normalized = String(value || '').trim().toLowerCase() + return normalized !== '' && normalized !== 'false' && normalized !== 'n/a' +} + function addOrgReferences (orgByReference, org) { const references = [org.short_name, org.long_name, ...(org.aliases || [])] references.forEach(reference => { @@ -39,22 +44,20 @@ function addOrgReferences (orgByReference, org) { }) } -function isRootOrg (org) { - return org?.__t === 'RootOrg' || org?.authority?.includes('ROOT') -} - -function decorateCnaRelationships (activeOrgs, rootOrgs) { +function decorateCnaRelationships (activeOrgs, hierarchyOrgs) { const orgByReference = new Map() - ;[...activeOrgs, ...rootOrgs].forEach(org => addOrgReferences(orgByReference, org)) + ;[...activeOrgs, ...hierarchyOrgs].forEach(org => addOrgReferences(orgByReference, org)) return activeOrgs.map(org => { - const root = rootOrgs.find(candidate => candidate.UUID !== org.UUID && candidate.oversees?.includes(org.UUID)) - const tlrReference = org.top_level_root || root?.top_level_root + const root = hierarchyOrgs.find(candidate => candidate.UUID !== org.UUID && candidate.oversees?.includes(org.UUID)) + const hasOwnTlrReference = isUsableTlrReference(org.top_level_root) + const tlrReference = hasOwnTlrReference ? org.top_level_root : root?.top_level_root + const normalizedTlrReference = String(tlrReference || '').trim().toLowerCase() let tlr - if (String(tlrReference).toLowerCase() === 'true') { - tlr = isRootOrg(org) ? org : root - } else if (!['false', 'n/a'].includes(String(tlrReference).toLowerCase())) { + if (normalizedTlrReference === 'true') { + tlr = hasOwnTlrReference ? org : root + } else if (isUsableTlrReference(tlrReference)) { tlr = orgByReference.get(normalizeOrgReference(tlrReference)) } @@ -636,12 +639,17 @@ class BaseOrgRepository extends BaseRepository { .select(projection) .sort({ short_name: 1 }) .lean() - const rootOrgQuery = RootOrgModel.find({}) + const hierarchyOrgQuery = BaseOrgModel.find({ + $or: [ + { authority: 'ROOT' }, + { 'oversees.0': { $exists: true } } + ] + }) .select(projection) .lean() - const [activeOrgs, rootOrgs] = await Promise.all([activeCnaQuery, rootOrgQuery]) + const [activeOrgs, hierarchyOrgs] = await Promise.all([activeCnaQuery, hierarchyOrgQuery]) - return decorateCnaRelationships(activeOrgs, rootOrgs) + return decorateCnaRelationships(activeOrgs, hierarchyOrgs) } /** diff --git a/test/integration-tests/registry-org/activeCnaListTest.js b/test/integration-tests/registry-org/activeCnaListTest.js index c82489bb2..9be63f27d 100644 --- a/test/integration-tests/registry-org/activeCnaListTest.js +++ b/test/integration-tests/registry-org/activeCnaListTest.js @@ -33,12 +33,12 @@ describe('Public active CNA list', () => { oversees: [uuids.root], program_data: { status: 'active' } }, { - __t: 'RootOrg', + __t: 'CNAOrg', UUID: uuids.root, short_name: shortNames.root, long_name: 'Public Test Root', - authority: ['ROOT'], - top_level_root: `${shortNames.tlr} TLR`, + authority: ['CNA', 'ROOT'], + top_level_root: 'false', oversees: [uuids.child], program_data: { status: 'active' } }, { @@ -83,7 +83,7 @@ describe('Public active CNA list', () => { }) }) - it('resolves root and top-level-root relationships from database records', async () => { + it('resolves CNA-discriminator roots and inherits sentinel top-level-root relationships', async () => { const res = await chai.request(app).get('/api/registry/org/cnas') expect(res).to.have.status(200) diff --git a/test/unit-tests/org/activeCnaListTest.js b/test/unit-tests/org/activeCnaListTest.js index 3dee3ef28..941e0018c 100644 --- a/test/unit-tests/org/activeCnaListTest.js +++ b/test/unit-tests/org/activeCnaListTest.js @@ -6,7 +6,6 @@ const { } = require('../../../src/controller/registry.controller/org.registry.controller') const BaseOrgRepository = require('../../../src/repositories/baseOrgRepository') const BaseOrgModel = require('../../../src/model/baseorg') -const RootOrgModel = require('../../../src/model/rootorg') describe('Active CNA list', () => { afterEach(() => sinon.restore()) @@ -89,7 +88,11 @@ describe('Active CNA list', () => { const activeRecords = [{ UUID: 'child-uuid', short_name: 'example', - top_level_root: 'MITRE TLR' + top_level_root: 'false' + }, { + UUID: 'child-na-uuid', + short_name: 'example-na', + top_level_root: ' N/A ' }, { UUID: 'tlr-uuid', short_name: 'mitre', @@ -102,7 +105,9 @@ describe('Active CNA list', () => { UUID: 'root-uuid', short_name: 'example-root', long_name: 'Example Root', - oversees: ['child-uuid'], + __t: 'CNAOrg', + authority: ['CNA', 'ROOT'], + oversees: ['child-uuid', 'child-na-uuid'], top_level_root: 'MITRE TLR' }, { UUID: 'tlr-uuid', @@ -123,8 +128,9 @@ describe('Active CNA list', () => { lean: sinon.stub().resolves(rootRecords) } rootQuery.select.returns(rootQuery) - sinon.stub(BaseOrgModel, 'find').returns(activeQuery) - sinon.stub(RootOrgModel, 'find').returns(rootQuery) + const baseOrgFindStub = sinon.stub(BaseOrgModel, 'find') + baseOrgFindStub.onFirstCall().returns(activeQuery) + baseOrgFindStub.onSecondCall().returns(rootQuery) const result = await new BaseOrgRepository().getActiveCnas() @@ -132,10 +138,18 @@ describe('Active CNA list', () => { authority: { $in: ['CNA', 'ROOT'] }, 'program_data.status': { $in: ['active', 'Active'] } }) + expect(BaseOrgModel.find.secondCall.args[0]).to.deep.equal({ + $or: [ + { authority: 'ROOT' }, + { 'oversees.0': { $exists: true } } + ] + }) expect(activeQuery.sort.calledOnceWith({ short_name: 1 })).to.equal(true) expect(result[0]._root).to.equal(rootRecords[0]) - expect(result[0]._tlr).to.equal(activeRecords[1]) - expect(result[1]._root).to.equal(undefined) - expect(result[1]._tlr).to.equal(activeRecords[1]) + expect(result[0]._tlr).to.equal(activeRecords[2]) + expect(result[1]._root).to.equal(rootRecords[0]) + expect(result[1]._tlr).to.equal(activeRecords[2]) + expect(result[2]._root).to.equal(undefined) + expect(result[2]._tlr).to.equal(activeRecords[2]) }) }) From c50ffd4261644b3d27ddd9746c895de1a98654ad Mon Sep 17 00:00:00 2001 From: david-rocca Date: Thu, 27 Aug 2026 10:11:34 -0400 Subject: [PATCH 04/27] removing unused python file that has existed since the beginning of time --- CveRecords5.0Upload.py | 45 ------------------------------------------ 1 file changed, 45 deletions(-) delete mode 100644 CveRecords5.0Upload.py diff --git a/CveRecords5.0Upload.py b/CveRecords5.0Upload.py deleted file mode 100644 index 094c1a221..000000000 --- a/CveRecords5.0Upload.py +++ /dev/null @@ -1,45 +0,0 @@ -# Upload 5.0 records -# Global variables should be set appropiately, and a directory of json 5.0 files must be given -import sys -import getopt -import os.path -import json -import requests - -RSUS_URL = '' -CVE_API_USER = '' -CVE_API_KEY = '' -CVE_API_ORG = '' - -def main(argv): - inputPath = '' - try: - opts, args = getopt.getopt(argv, "hi:", ["ifile="]) - except getopt.GetopError: - print ('USAGE python cve4to5up.py -i Date: Wed, 29 Jul 2026 14:10:40 -0400 Subject: [PATCH 05/27] Document legacy organization responses --- api-docs/openapi.json | 20 +++--------------- src/controller/org.controller/index.js | 14 ++----------- test/unit-tests/org/openapiOrgResponseTest.js | 21 +++++++++++++++++++ 3 files changed, 26 insertions(+), 29 deletions(-) create mode 100644 test/unit-tests/org/openapiOrgResponseTest.js diff --git a/api-docs/openapi.json b/api-docs/openapi.json index 3031f2cf1..a0ac560b4 100644 --- a/api-docs/openapi.json +++ b/api-docs/openapi.json @@ -1921,14 +1921,7 @@ "content": { "application/json": { "schema": { - "oneOf": [ - { - "$ref": "../schemas/org/list-orgs-response.json" - }, - { - "$ref": "../schemas/registry-org/list-registry-orgs-response.json" - } - ] + "$ref": "../schemas/org/list-orgs-response.json" } } } @@ -2009,14 +2002,7 @@ "content": { "application/json": { "schema": { - "oneOf": [ - { - "$ref": "../schemas/org/create-org-response.json" - }, - { - "$ref": "../schemas/registry-org/create-registry-org-response.json" - } - ] + "$ref": "../schemas/org/create-org-response.json" } } } @@ -8134,4 +8120,4 @@ } } } -} \ No newline at end of file +} diff --git a/src/controller/org.controller/index.js b/src/controller/org.controller/index.js index 52e4c4c77..c2c353917 100644 --- a/src/controller/org.controller/index.js +++ b/src/controller/org.controller/index.js @@ -30,12 +30,7 @@ router.get('/org', description: 'Returns information about all organizations, along with pagination fields if results span multiple pages of data', content: { "application/json": { - schema: { - oneOf: [ - { $ref: '../schemas/org/list-orgs-response.json' }, - { $ref: '../schemas/registry-org/list-registry-orgs-response.json' } - ] - } + schema: { $ref: '../schemas/org/list-orgs-response.json' } } } } @@ -118,12 +113,7 @@ router.post( description: 'Returns information about the organization created', content: { "application/json": { - schema: { - oneOf: [ - { $ref: '../schemas/org/create-org-response.json' }, - { $ref: '../schemas/registry-org/create-registry-org-response.json' } - ] - } + schema: { $ref: '../schemas/org/create-org-response.json' } } } } diff --git a/test/unit-tests/org/openapiOrgResponseTest.js b/test/unit-tests/org/openapiOrgResponseTest.js new file mode 100644 index 000000000..63be2b86a --- /dev/null +++ b/test/unit-tests/org/openapiOrgResponseTest.js @@ -0,0 +1,21 @@ +const chai = require('chai') +const expect = chai.expect +const openapi = require('../../../api-docs/openapi.json') + +describe('Organization OpenAPI response schemas', () => { + it('documents GET /org with only the legacy organization list response', () => { + const schema = openapi.paths['/org'].get.responses['200'].content['application/json'].schema + + expect(schema).to.deep.equal({ + $ref: '../schemas/org/list-orgs-response.json' + }) + }) + + it('documents POST /org with only the legacy organization create response', () => { + const schema = openapi.paths['/org'].post.responses['200'].content['application/json'].schema + + expect(schema).to.deep.equal({ + $ref: '../schemas/org/create-org-response.json' + }) + }) +}) From 4c81ac575720de1108b6fac5608dad0145382668 Mon Sep 17 00:00:00 2001 From: Leo Williamson Date: Tue, 25 Aug 2026 12:25:05 -0400 Subject: [PATCH 06/27] Optimize read-only Mongoose queries with lean --- .../cve-id.controller/cve-id.controller.js | 12 ++-- src/repositories/auditRepository.js | 11 ++- src/repositories/baseOrgRepository.js | 22 +++--- src/repositories/baseRepository.js | 9 +-- src/repositories/baseUserRepository.js | 12 ++-- src/repositories/conversationRepository.js | 6 +- src/repositories/cveIdRepository.js | 2 +- src/repositories/cveRepository.js | 2 +- src/repositories/glossaryRepository.js | 4 +- src/repositories/orgRepository.js | 10 ++- src/repositories/reviewObjectRepository.js | 12 ++-- src/repositories/userRepository.js | 4 ++ .../middleware/authenticatedContextTest.js | 4 +- .../conversationRepositoryTest.js | 24 +++---- test/unit-tests/cve-id/cveIdGetAllTest.js | 8 +-- test/unit-tests/org/baseOrgRepositoryTest.js | 4 +- .../repository/baseRepositoryLeanTest.js | 69 +++++++++++++++++++ 17 files changed, 146 insertions(+), 69 deletions(-) create mode 100644 test/unit-tests/repository/baseRepositoryLeanTest.js diff --git a/src/controller/cve-id.controller/cve-id.controller.js b/src/controller/cve-id.controller/cve-id.controller.js index e33b86411..2289b0362 100644 --- a/src/controller/cve-id.controller/cve-id.controller.js +++ b/src/controller/cve-id.controller/cve-id.controller.js @@ -40,8 +40,8 @@ async function getFilteredCveId (req, res, next) { // Create map of orgUUID to shortnames and users to simplify aggregation later // Only project the fields needed for the maps to avoid fetching full documents - const orgs = await orgRepo.getAllOrgs({}, { UUID: 1, short_name: 1, _id: 0 }) - const users = await userRepo.getAllUsers({}, { UUID: 1, username: 1, org_UUID: 1, _id: 0 }) + const orgs = await orgRepo.getCveIdMapOrgs() + const users = await userRepo.getCveIdMapUsers() const orgMap = {} const userMap = {} @@ -694,7 +694,7 @@ async function nonSequentialReservation (year, amount, shortName, orgShortName, } } - available = await cveIdRepo.find({ cve_year: year, state: 'AVAILABLE' }, { limit: availableLimit }) // get available ids + available = await cveIdRepo.find({ cve_year: year, state: 'AVAILABLE' }, { limit: availableLimit, lean: true }) // get available ids // Case 1: Not enough IDs in the 'AVAILABLE' pool if (available.length < availableLimit) { @@ -708,7 +708,7 @@ async function nonSequentialReservation (year, amount, shortName, orgShortName, } await allocateAvailableCveIds(result.ids, year, req) // Pool was incremented. Create 'AVAILABLE' cve ids. - available = await cveIdRepo.find({ cve_year: year, state: 'AVAILABLE' }, { limit: availableLimit }) // get available ids + available = await cveIdRepo.find({ cve_year: year, state: 'AVAILABLE' }, { limit: availableLimit, lean: true }) // get available ids } // Case 2: Enough IDs in the 'AVAILABLE' pool @@ -734,7 +734,7 @@ async function nonSequentialReservation (year, amount, shortName, orgShortName, available.splice(index, 1) // remove reserved cve id from the 'AVAILABLE' pool counter++ } else { - available = await cveIdRepo.find({ cve_year: year, state: 'AVAILABLE' }, { limit: availableLimit }) // get available ids + available = await cveIdRepo.find({ cve_year: year, state: 'AVAILABLE' }, { limit: availableLimit, lean: true }) // get available ids availableLimit = Math.max(3 * (amount - counter), CONSTANTS.DEFAULT_AVAILABLE_POOL) // recalculate the available limit since some ids might have been reserved // Case 1: Not enough IDs in the 'AVAILABLE' pool @@ -750,7 +750,7 @@ async function nonSequentialReservation (year, amount, shortName, orgShortName, } await allocateAvailableCveIds(result.ids, year, req) // Pool was incremented. Create 'AVAILABLE' cve ids. - available = await cveIdRepo.find({ cve_year: year, state: 'AVAILABLE' }, { limit: availableLimit }) // get available ids + available = await cveIdRepo.find({ cve_year: year, state: 'AVAILABLE' }, { limit: availableLimit, lean: true }) // get available ids } } } diff --git a/src/repositories/auditRepository.js b/src/repositories/auditRepository.js index 5ba66f691..e6b24e361 100644 --- a/src/repositories/auditRepository.js +++ b/src/repositories/auditRepository.js @@ -80,7 +80,7 @@ class AuditRepository extends BaseRepository { return null } const query = { target_uuid: org.UUID } - return this.collection.findOne(query, null, options) + return this.collection.findOne(query, null, options).lean() } /** @@ -88,7 +88,7 @@ class AuditRepository extends BaseRepository { */ async findOneByTargetUUID (targetUUID, options = {}) { const query = { target_uuid: targetUUID } - const auditObject = await Audit.findOne(query, null, options) + const auditObject = await Audit.findOne(query, null, options).lean() return auditObject } @@ -97,22 +97,21 @@ class AuditRepository extends BaseRepository { */ async findOneByUUID (auditUUID, options = {}) { const query = { uuid: auditUUID } - return this.collection.findOne(query, null, options) + return this.collection.findOne(query, null, options).lean() } /** * Find all audit documents */ async findAllAuditDocuments (options = {}) { - const audits = await Audit.find({}, null, options) - return audits.map(audit => audit.toObject()) + return Audit.find({}, null, options).lean() } /** * Get the last X changes for a target UUID */ async getLastXChanges (targetUUID, numberOfChanges, options = {}) { - const audit = await Audit.findOne({ target_uuid: targetUUID }, null, options) + const audit = await Audit.findOne({ target_uuid: targetUUID }, null, options).lean() if (!audit || !audit.history || audit.history.length === 0) { return [] } diff --git a/src/repositories/baseOrgRepository.js b/src/repositories/baseOrgRepository.js index 8585ef2aa..eef337974 100644 --- a/src/repositories/baseOrgRepository.js +++ b/src/repositories/baseOrgRepository.js @@ -197,7 +197,8 @@ class BaseOrgRepository extends BaseRepository { const OrgRepository = require('./orgRepository') const legacyOrgRepo = new OrgRepository() if (returnLegacyFormat) return await legacyOrgRepo.findOneByShortName(shortName, options, projection) - const data = await BaseOrgModel.findOne({ short_name: shortName }, projection, options) + const query = BaseOrgModel.findOne({ short_name: shortName }, projection, options) + const data = await (options.lean ? query.lean() : query) return data } @@ -215,7 +216,8 @@ class BaseOrgRepository extends BaseRepository { const OrgRepository = require('./orgRepository') const legacyOrgRepo = new OrgRepository() if (returnLegacyFormat) return await legacyOrgRepo.findOneByUUID(UUID, options, projection) - return await BaseOrgModel.findOne({ UUID: UUID }, projection, options) + const query = BaseOrgModel.findOne({ UUID: UUID }, projection, options) + return await (options.lean ? query.lean() : query) } /** @@ -257,7 +259,7 @@ class BaseOrgRepository extends BaseRepository { { users: { $in: userUUIDs } }, { _id: 0, UUID: 1, short_name: 1, users: 1 }, options - ) + ).lean() } /** @@ -311,7 +313,7 @@ class BaseOrgRepository extends BaseRepository { async orgExists (shortName, options = {}, returnLegacyFormat = false) { if (!shortName) return false const query = { short_name: exactCaseInsensitiveRegex(shortName) } - const exists = await BaseOrgModel.findOne(query, null, options) + const exists = await BaseOrgModel.findOne(query, { _id: 1 }, options).lean() if (exists) { return true } @@ -349,7 +351,7 @@ class BaseOrgRepository extends BaseRepository { ] } - const collisionOrg = await BaseOrgModel.findOne(query, 'short_name long_name aliases', options) + const collisionOrg = await BaseOrgModel.findOne(query, 'short_name long_name aliases', options).lean() if (collisionOrg) { // Determine which string collided for better error reporting for (const str of searchStrings) { @@ -594,10 +596,10 @@ class BaseOrgRepository extends BaseRepository { const { deepRemoveEmpty } = require('../utils/utils') const projection = getOrgProjection(isSecretariat) const data = identifierIsUUID - ? await this.findOneByUUID(identifier, options, returnLegacyFormat, projection) - : await this.findOneByShortName(identifier, options, returnLegacyFormat, projection) + ? await this.findOneByUUID(identifier, { ...options, lean: true }, returnLegacyFormat, projection) + : await this.findOneByShortName(identifier, { ...options, lean: true }, returnLegacyFormat, projection) if (!data) return null - const result = data.toObject() + const result = data const parentOrg = await BaseOrgModel.findOne({ oversees: result.UUID }).select('UUID').lean() if (parentOrg) { @@ -1268,7 +1270,7 @@ class BaseOrgRepository extends BaseRepository { * @returns {Promise} True if the organization is a Secretariat, false otherwise. */ async isSecretariatByShortName (shortname, options = {}, isLegacyObject = false) { - const org = await BaseOrgModel.findOne({ short_name: shortname }, null, options) + const org = await BaseOrgModel.findOne({ short_name: shortname }, 'authority', options).lean() if (org.authority.includes('SECRETARIAT')) { return true } @@ -1297,7 +1299,7 @@ class BaseOrgRepository extends BaseRepository { * @returns {Promise} True if the organization is a Bulk Download provider, false otherwise. */ async isBulkDownloadByShortname (orgShortname, options = {}, isLegacyObject = false) { - const org = await BaseOrgModel.findOne({ short_name: orgShortname }, null, options) + const org = await BaseOrgModel.findOne({ short_name: orgShortname }, 'authority', options).lean() if (org.authority.includes('BULK_DOWNLOAD')) { return true } diff --git a/src/repositories/baseRepository.js b/src/repositories/baseRepository.js index 9ba588f54..bb65f1931 100644 --- a/src/repositories/baseRepository.js +++ b/src/repositories/baseRepository.js @@ -45,13 +45,10 @@ class BaseRepository { if (count) { return results.countDocuments().exec() - } else if (lean) { - return results.lean().exec() - } else if (limit) { - return results.limit(limit).exec() - } else { - return results.exec() } + if (limit) results.limit(limit) + if (lean) results.lean() + return results.exec() } async findOne (query = {}) { diff --git a/src/repositories/baseUserRepository.js b/src/repositories/baseUserRepository.js index ae84f9764..32ee9001e 100644 --- a/src/repositories/baseUserRepository.js +++ b/src/repositories/baseUserRepository.js @@ -87,7 +87,7 @@ class BaseUserRepository extends BaseRepository { * @returns {Promise} True if the organization has the user, false otherwise. */ async orgHasUserByUUID (orgShortName, uuid, options = {}, isLegacyObject = false) { - const org = await BaseOrgModel.findOne({ short_name: orgShortName }, null, options) + const org = await BaseOrgModel.findOne({ short_name: orgShortName }, 'users', options).lean() if (!org || !Array.isArray(org.users)) { return false } @@ -108,13 +108,13 @@ class BaseUserRepository extends BaseRepository { */ async orgHasUser (orgShortName, username, options = {}, isLegacyObject = false) { // 1. Find the org - const org = await BaseOrgModel.findOne({ short_name: orgShortName }, null, options) + const org = await BaseOrgModel.findOne({ short_name: orgShortName }, 'users', options).lean() if (!org || !Array.isArray(org.users)) { return false } // 2. Check if a user with this username exists in the org - const user = await BaseUser.findOne({ username, UUID: { $in: org.users } }, null, options) + const user = await BaseUser.findOne({ username, UUID: { $in: org.users } }, { _id: 1 }, options).lean() return !!user } @@ -213,7 +213,7 @@ class BaseUserRepository extends BaseRepository { { UUID: { $in: uuids } }, { _id: 0, UUID: 1, username: 1, name: 1 }, options - ) + ).lean() } /** @@ -231,7 +231,7 @@ class BaseUserRepository extends BaseRepository { return false } - const org = await BaseOrgModel.findOne({ UUID: orgUUID }, null, options).select('admins users') + const org = await BaseOrgModel.findOne({ UUID: orgUUID }, null, options).select('admins users').lean() if (!org) { return false } @@ -326,7 +326,7 @@ class BaseUserRepository extends BaseRepository { * @returns {Promise} An array of user UUIDs. */ async findUsersByOrgShortname (shortName, options = {}) { - const org = await BaseOrgModel.findOne({ short_name: shortName }, null, options) + const org = await BaseOrgModel.findOne({ short_name: shortName }, 'users', options).lean() return org.users } diff --git a/src/repositories/conversationRepository.js b/src/repositories/conversationRepository.js index 7835990b6..82c902733 100644 --- a/src/repositories/conversationRepository.js +++ b/src/repositories/conversationRepository.js @@ -59,8 +59,8 @@ class ConversationRepository extends BaseRepository { posted_at: 1, UUID: 1 } - }) - return conversations.map(convo => convo.toObject()).filter(conv => isSecretariat || conv.visibility === 'public').map(conv => { + }).lean() + return conversations.filter(conv => isSecretariat || conv.visibility === 'public').map(conv => { normalizeConversationAuthorName(conv) if (!isSecretariat && conv.author_role === 'Secretariat') { delete conv.author_id @@ -77,7 +77,7 @@ class ConversationRepository extends BaseRepository { posted_at: 1, UUID: 1 } - }).skip(index).limit(1) + }).skip(index).limit(1).lean() return conversation[0] } diff --git a/src/repositories/cveIdRepository.js b/src/repositories/cveIdRepository.js index 797dcae23..8db3b5072 100644 --- a/src/repositories/cveIdRepository.js +++ b/src/repositories/cveIdRepository.js @@ -7,7 +7,7 @@ class CveIdRepository extends BaseRepository { } async findOneByCveId (id) { - return this.collection.findOne().byCveId(id) + return this.collection.findOne().byCveId(id).lean() } async updateByCveId (id, cveIdObj, options = {}) { diff --git a/src/repositories/cveRepository.js b/src/repositories/cveRepository.js index 34a1451fb..7b48f79ea 100644 --- a/src/repositories/cveRepository.js +++ b/src/repositories/cveRepository.js @@ -7,7 +7,7 @@ class CveRepository extends BaseRepository { } async findOneByCveId (id) { - const results = this.collection.findOne().byCveId(id) + const results = this.collection.findOne().byCveId(id).lean() return results } diff --git a/src/repositories/glossaryRepository.js b/src/repositories/glossaryRepository.js index 3ff74abe4..e69bfc0e9 100644 --- a/src/repositories/glossaryRepository.js +++ b/src/repositories/glossaryRepository.js @@ -7,11 +7,11 @@ class GlossaryRepository extends BaseRepository { } async getAll () { - return this.collection.find({}, { _id: 0, __v: 0, createdAt: 0, updatedAt: 0 }).exec() + return this.collection.find({}, { _id: 0, __v: 0, createdAt: 0, updatedAt: 0 }).lean().exec() } async findOneByServicesShortName (servicesShortName) { - return this.collection.findOne({ services_short_name: servicesShortName }, { _id: 0, __v: 0, createdAt: 0, updatedAt: 0 }).exec() + return this.collection.findOne({ services_short_name: servicesShortName }, { _id: 0, __v: 0, createdAt: 0, updatedAt: 0 }).lean().exec() } async updateByServicesShortName (servicesShortName, newGlossaryData) { diff --git a/src/repositories/orgRepository.js b/src/repositories/orgRepository.js index 48f47ee93..00666a893 100644 --- a/src/repositories/orgRepository.js +++ b/src/repositories/orgRepository.js @@ -9,11 +9,13 @@ class OrgRepository extends BaseRepository { async findOneByShortName (shortName, options = {}, projection = {}) { const query = { short_name: shortName } - return this.collection.findOne(query, projection, options) + const result = this.collection.findOne(query, projection, options) + return options.lean ? result.lean() : result } async findOneByUUID (UUID, options = {}, projection = {}) { - return this.collection.findOne({ UUID: UUID }, projection, options) + const result = this.collection.findOne({ UUID: UUID }, projection, options) + return options.lean ? result.lean() : result } async getOrgUUID (shortName, options = {}) { @@ -51,6 +53,10 @@ class OrgRepository extends BaseRepository { return this.collection.find({}, projection, options) } + async getCveIdMapOrgs (options = {}) { + return this.collection.find({}, { _id: 0, UUID: 1, short_name: 1 }, options).lean() + } + async deleteOneByShortName (shortName, options = {}) { return this.collection.deleteOne({ short_name: shortName }, options) } diff --git a/src/repositories/reviewObjectRepository.js b/src/repositories/reviewObjectRepository.js index 921c2d5b5..5fab8a1f4 100644 --- a/src/repositories/reviewObjectRepository.js +++ b/src/repositories/reviewObjectRepository.js @@ -41,9 +41,9 @@ class ReviewObjectRepository extends BaseRepository { const conversationRepository = new ConversationRepository() let reviewObject const query = pending ? { uuid: UUID, status: 'pending' } : { uuid: UUID } - const reviewObjectRaw = await ReviewObjectModel.findOne(query, { _id: 0, __v: 0 }, options) + const reviewObjectRaw = await ReviewObjectModel.findOne(query, { _id: 0, __v: 0 }, options).lean() if (reviewObjectRaw) { - reviewObject = reviewObjectRaw.toObject() + reviewObject = reviewObjectRaw const conversations = await conversationRepository.getAllByTargetUUID(reviewObject.target_object_uuid, isSecretariat, options) reviewObject.conversation = conversations?.length ? conversations : undefined reviewObject.new_review_data = filterReviewOrgData(reviewObject.new_review_data, isSecretariat) @@ -112,9 +112,9 @@ class ReviewObjectRepository extends BaseRepository { ...options, sort: { created: -1 } } - ) + ).lean() if (reviewObjectRaw) { - reviewObject = reviewObjectRaw.toObject() + reviewObject = reviewObjectRaw const conversations = await conversationRepository.getAllByTargetUUID(org.UUID, isSecretariat, options) reviewObject.conversation = conversations?.length ? conversations : undefined reviewObject.new_review_data = filterReviewOrgData(reviewObject.new_review_data, isSecretariat) @@ -142,9 +142,9 @@ class ReviewObjectRepository extends BaseRepository { ...options, sort: { created: -1 } } - ) + ).lean() if (reviewObjectRaw) { - reviewObject = reviewObjectRaw.toObject() + reviewObject = reviewObjectRaw const conversations = await conversationRepository.getAllByTargetUUID(org.UUID, isSecretariat, options) reviewObject.conversation = conversations?.length ? conversations : undefined reviewObject.new_review_data = filterReviewOrgData(reviewObject.new_review_data, isSecretariat) diff --git a/src/repositories/userRepository.js b/src/repositories/userRepository.js index 346710878..74afa451b 100644 --- a/src/repositories/userRepository.js +++ b/src/repositories/userRepository.js @@ -64,6 +64,10 @@ class UserRepository extends BaseRepository { async getAllUsers (options = {}, projection = {}) { return this.collection.find({}, projection, options) } + + async getCveIdMapUsers (options = {}) { + return this.collection.find({}, { _id: 0, UUID: 1, username: 1, org_UUID: 1 }, options).lean() + } } module.exports = UserRepository diff --git a/test/integration-tests/middleware/authenticatedContextTest.js b/test/integration-tests/middleware/authenticatedContextTest.js index 6ad751346..3cf908765 100644 --- a/test/integration-tests/middleware/authenticatedContextTest.js +++ b/test/integration-tests/middleware/authenticatedContextTest.js @@ -155,13 +155,13 @@ describe('Authenticated request context middleware integration', () => { return false } - async getAllOrgs () { + async getCveIdMapOrgs () { return [legacyAuthenticatedOrg] } } class LegacyUserRepo { - async getAllUsers () { + async getCveIdMapUsers () { return [] } } diff --git a/test/unit-tests/conversation/conversationRepositoryTest.js b/test/unit-tests/conversation/conversationRepositoryTest.js index 5865edeed..d8cab9b19 100644 --- a/test/unit-tests/conversation/conversationRepositoryTest.js +++ b/test/unit-tests/conversation/conversationRepositoryTest.js @@ -60,9 +60,9 @@ describe('Testing Conversation Repository', () => { }) it('normalizes stored Secretariat author names when conversations are returned to Secretariat', async () => { - sinon.stub(ConversationModel, 'find').resolves([ - { - toObject: () => ({ + sinon.stub(ConversationModel, 'find').returns({ + lean: sinon.stub().resolves([ + { UUID: 'conversation-uuid', target_uuid: 'target-uuid', author_id: 'secretariat-user-uuid', @@ -70,9 +70,9 @@ describe('Testing Conversation Repository', () => { author_role: 'Secretariat', visibility: 'public', body: 'Existing Secretariat comment' - }) - } - ]) + } + ]) + }) const repo = new ConversationRepository() const result = await repo.getAllByTargetUUID('target-uuid', true) @@ -106,9 +106,9 @@ describe('Testing Conversation Repository', () => { }) it('continues stripping Secretariat author fields when conversations are returned to non-Secretariat', async () => { - sinon.stub(ConversationModel, 'find').resolves([ - { - toObject: () => ({ + sinon.stub(ConversationModel, 'find').returns({ + lean: sinon.stub().resolves([ + { UUID: 'conversation-uuid', target_uuid: 'target-uuid', author_id: 'secretariat-user-uuid', @@ -116,9 +116,9 @@ describe('Testing Conversation Repository', () => { author_role: 'Secretariat', visibility: 'public', body: 'Existing Secretariat comment' - }) - } - ]) + } + ]) + }) const repo = new ConversationRepository() const result = await repo.getAllByTargetUUID('target-uuid', false) diff --git a/test/unit-tests/cve-id/cveIdGetAllTest.js b/test/unit-tests/cve-id/cveIdGetAllTest.js index 2946f871c..bd7228bbf 100644 --- a/test/unit-tests/cve-id/cveIdGetAllTest.js +++ b/test/unit-tests/cve-id/cveIdGetAllTest.js @@ -108,10 +108,10 @@ describe('Testing getFilteredCveId function', () => { sandbox.stub(orgRepo, 'getOrgUUID').returns(stubOrg.UUID) sandbox.stub(orgRepo, 'isSecretariat').returns(true) sandbox.stub(orgRepo, 'isBulkDownload').returns(false) - sandbox.stub(orgRepo, 'getAllOrgs').returns([stubOrg, stubOrg2]) + sandbox.stub(orgRepo, 'getCveIdMapOrgs').returns([stubOrg, stubOrg2]) sandbox.stub(userRepo, 'getUserUUID').returns(stubUser.UUID) - sandbox.stub(userRepo, 'getAllUsers').returns([stubUser]) + sandbox.stub(userRepo, 'getCveIdMapUsers').returns([stubUser]) sandbox.spy(cveIdController, 'CVEID_GET_FILTER') @@ -145,8 +145,8 @@ describe('Testing getFilteredCveId function', () => { it('Should request only the fields needed to build the org and user maps', async () => { await cveIdController.CVEID_GET_FILTER(req, res, next) - expect(orgRepo.getAllOrgs.calledOnceWith({}, { UUID: 1, short_name: 1, _id: 0 })).to.equal(true) - expect(userRepo.getAllUsers.calledOnceWith({}, { UUID: 1, username: 1, org_UUID: 1, _id: 0 })).to.equal(true) + expect(orgRepo.getCveIdMapOrgs.calledOnceWithExactly()).to.equal(true) + expect(userRepo.getCveIdMapUsers.calledOnceWithExactly()).to.equal(true) }) it('Should swap UUIDs for names in Cve-ids', async () => { diff --git a/test/unit-tests/org/baseOrgRepositoryTest.js b/test/unit-tests/org/baseOrgRepositoryTest.js index cad1297d7..f14244ac0 100644 --- a/test/unit-tests/org/baseOrgRepositoryTest.js +++ b/test/unit-tests/org/baseOrgRepositoryTest.js @@ -11,7 +11,7 @@ describe('Testing BaseOrgRepository', () => { }) it('Checks org existence without using $expr', async () => { - const findOne = sinon.stub(BaseOrgModel, 'findOne').resolves(null) + const findOne = sinon.stub(BaseOrgModel, 'findOne').returns({ lean: sinon.stub().resolves(null) }) const repo = new BaseOrgRepository() const options = { session: 'session' } @@ -28,7 +28,7 @@ describe('Testing BaseOrgRepository', () => { }) it('Checks alias collisions without using $expr', async () => { - const findOne = sinon.stub(BaseOrgModel, 'findOne').resolves(null) + const findOne = sinon.stub(BaseOrgModel, 'findOne').returns({ lean: sinon.stub().resolves(null) }) const repo = new BaseOrgRepository() const collision = await repo.checkAliasCollisions( diff --git a/test/unit-tests/repository/baseRepositoryLeanTest.js b/test/unit-tests/repository/baseRepositoryLeanTest.js new file mode 100644 index 000000000..8fd0d95a2 --- /dev/null +++ b/test/unit-tests/repository/baseRepositoryLeanTest.js @@ -0,0 +1,69 @@ +const { expect } = require('chai') +const sinon = require('sinon') + +const BaseRepository = require('../../../src/repositories/baseRepository') + +function createQuery (result) { + return { + limit: sinon.stub().returnsThis(), + lean: sinon.stub().returnsThis(), + exec: sinon.stub().resolves(result), + countDocuments: sinon.stub() + } +} + +describe('BaseRepository.find', () => { + it('applies both limit and lean before executing a collection query', async () => { + const query = createQuery([{ cve_id: 'CVE-2026-1' }]) + const model = { find: sinon.stub().returns(query) } + const repository = new BaseRepository(model) + + const result = await repository.find( + { state: 'AVAILABLE' }, + { limit: 10, lean: true } + ) + + expect(result).to.deep.equal([{ cve_id: 'CVE-2026-1' }]) + expect(model.find.calledOnceWithExactly({ state: 'AVAILABLE' })).to.equal(true) + expect(query.limit.calledOnceWithExactly(10)).to.equal(true) + expect(query.lean.calledOnce).to.equal(true) + expect(query.exec.calledOnce).to.equal(true) + }) + + it('applies lean to single-document queries', async () => { + const query = createQuery({ cve_id: 'CVE-2026-1' }) + const model = { findOne: sinon.stub().returns(query) } + const repository = new BaseRepository(model) + + const result = await repository.find( + { cve_id: 'CVE-2026-1' }, + { multiple: false, lean: true } + ) + + expect(result).to.deep.equal({ cve_id: 'CVE-2026-1' }) + expect(model.findOne.calledOnceWithExactly({ cve_id: 'CVE-2026-1' })).to.equal(true) + expect(query.limit.called).to.equal(false) + expect(query.lean.calledOnce).to.equal(true) + expect(query.exec.calledOnce).to.equal(true) + }) + + it('uses the count query without applying document query modifiers', async () => { + const countExec = sinon.stub().resolves(3) + const query = createQuery([]) + query.countDocuments.returns({ exec: countExec }) + const model = { find: sinon.stub().returns(query) } + const repository = new BaseRepository(model) + + const result = await repository.find( + { state: 'AVAILABLE' }, + { count: true, limit: 10, lean: true } + ) + + expect(result).to.equal(3) + expect(query.countDocuments.calledOnce).to.equal(true) + expect(countExec.calledOnce).to.equal(true) + expect(query.limit.called).to.equal(false) + expect(query.lean.called).to.equal(false) + expect(query.exec.called).to.equal(false) + }) +}) From 5181e48d4ce74a177ab5740ea3ea6a5b6aaa9227 Mon Sep 17 00:00:00 2001 From: Leo Williamson Date: Tue, 25 Aug 2026 13:07:35 -0400 Subject: [PATCH 07/27] Add lean query regression coverage --- .../cve-id/reserveCveIdTest.js | 32 ++ .../repository/leanRepositoryQueriesTest.js | 277 ++++++++++++++++++ 2 files changed, 309 insertions(+) create mode 100644 test/unit-tests/repository/leanRepositoryQueriesTest.js diff --git a/test/integration-tests/cve-id/reserveCveIdTest.js b/test/integration-tests/cve-id/reserveCveIdTest.js index 42e8f58d6..16db34b0e 100644 --- a/test/integration-tests/cve-id/reserveCveIdTest.js +++ b/test/integration-tests/cve-id/reserveCveIdTest.js @@ -2,17 +2,49 @@ const chai = require('chai') chai.use(require('chai-http')) +const sinon = require('sinon') const expect = chai.expect const constants = require('../constants.js') const app = require('../../../src/index.js') +const BaseRepository = require('../../../src/repositories/baseRepository.js') const requestLength = 10 describe('Testing Reserve CVE-ID Endpoints', () => { // beforeEach(() => { }) context('Positive Tests', () => { + it('Reserves an available CVE-ID through the endpoint when the pool query uses limit and lean', async () => { + const findSpy = sinon.spy(BaseRepository.prototype, 'find') + let availablePoolCall + + try { + const res = await chai.request(app) + .post('/api/cve-id?amount=1&cve_year=2023&short_name=mitre&batch_type=non-sequential') + .set(constants.headers) + + availablePoolCall = findSpy.getCalls().find(call => ( + call.args[0]?.cve_year === '2023' && + call.args[0]?.state === 'AVAILABLE' + )) + + expect(res).to.have.status(200) + expect(res.body.cve_ids).to.have.length(1) + expect(res.body.cve_ids[0]).to.include({ + cve_year: '2023', + state: 'RESERVED' + }) + expect(res.body.cve_ids[0]).to.have.property('cve_id') + } finally { + findSpy.restore() + } + + expect(availablePoolCall).to.not.be.undefined + expect(availablePoolCall.args[1]).to.include({ lean: true }) + expect(availablePoolCall.args[1].limit).to.be.a('number').and.to.be.greaterThan(0) + }) + it('Should return 200 and have correct number of cve-id results for a successful non-squential cve-id reservation ', (done) => { chai.request(app) .post(`/api/cve-id?amount=${requestLength}&cve_year=2023&short_name=mitre&batch_type=non-sequential`) diff --git a/test/unit-tests/repository/leanRepositoryQueriesTest.js b/test/unit-tests/repository/leanRepositoryQueriesTest.js new file mode 100644 index 000000000..70aa33094 --- /dev/null +++ b/test/unit-tests/repository/leanRepositoryQueriesTest.js @@ -0,0 +1,277 @@ +const { expect } = require('chai') +const sinon = require('sinon') + +const Audit = require('../../../src/model/audit') +const BaseOrg = require('../../../src/model/baseorg') +const BaseUser = require('../../../src/model/baseuser') +const Conversation = require('../../../src/model/conversation') +const Cve = require('../../../src/model/cve') +const CveId = require('../../../src/model/cve-id') +const Glossary = require('../../../src/model/glossary') +const Org = require('../../../src/model/org') +const ReviewObject = require('../../../src/model/reviewobject') +const User = require('../../../src/model/user') + +const AuditRepository = require('../../../src/repositories/auditRepository') +const BaseOrgRepository = require('../../../src/repositories/baseOrgRepository') +const BaseUserRepository = require('../../../src/repositories/baseUserRepository') +const ConversationRepository = require('../../../src/repositories/conversationRepository') +const CveIdRepository = require('../../../src/repositories/cveIdRepository') +const CveRepository = require('../../../src/repositories/cveRepository') +const GlossaryRepository = require('../../../src/repositories/glossaryRepository') +const OrgRepository = require('../../../src/repositories/orgRepository') +const ReviewObjectRepository = require('../../../src/repositories/reviewObjectRepository') +const UserRepository = require('../../../src/repositories/userRepository') + +function leanQuery (result) { + return { + lean: sinon.stub().resolves(result) + } +} + +function selectLeanQuery (result) { + const query = leanQuery(result) + query.select = sinon.stub().returns(query) + return query +} + +function paginatedLeanQuery (result) { + const query = leanQuery(result) + query.skip = sinon.stub().returns(query) + query.limit = sinon.stub().returns(query) + return query +} + +describe('Lean repository queries', () => { + afterEach(() => { + sinon.restore() + }) + + it('returns plain CVE and CVE-ID lookups through their custom query helpers', async () => { + const cveQuery = leanQuery({ cveMetadata: { cveId: 'CVE-2026-1' } }) + cveQuery.byCveId = sinon.stub().returns(cveQuery) + const cveIdQuery = leanQuery({ cve_id: 'CVE-2026-1' }) + cveIdQuery.byCveId = sinon.stub().returns(cveIdQuery) + sinon.stub(Cve, 'findOne').returns(cveQuery) + sinon.stub(CveId, 'findOne').returns(cveIdQuery) + + await new CveRepository().findOneByCveId('CVE-2026-1') + await new CveIdRepository().findOneByCveId('CVE-2026-1') + + expect(cveQuery.byCveId.calledOnceWithExactly('CVE-2026-1')).to.equal(true) + expect(cveQuery.lean.calledOnce).to.equal(true) + expect(cveIdQuery.byCveId.calledOnceWithExactly('CVE-2026-1')).to.equal(true) + expect(cveIdQuery.lean.calledOnce).to.equal(true) + }) + + it('uses projected lean queries to load the legacy CVE-ID maps', async () => { + const orgQuery = leanQuery([]) + const userQuery = leanQuery([]) + sinon.stub(Org, 'find').returns(orgQuery) + sinon.stub(User, 'find').returns(userQuery) + + await new OrgRepository().getCveIdMapOrgs() + await new UserRepository().getCveIdMapUsers() + + expect(orgQuery.lean.calledOnce).to.equal(true) + expect(userQuery.lean.calledOnce).to.equal(true) + }) + + it('supports opt-in lean legacy organization lookups', async () => { + const shortNameQuery = leanQuery({ UUID: 'org-uuid' }) + const uuidQuery = leanQuery({ UUID: 'org-uuid' }) + const findOne = sinon.stub(Org, 'findOne') + findOne.onCall(0).returns(shortNameQuery) + findOne.onCall(1).returns(uuidQuery) + const repository = new OrgRepository() + + await repository.findOneByShortName('example', { lean: true }) + await repository.findOneByUUID('org-uuid', { lean: true }) + + expect(shortNameQuery.lean.calledOnce).to.equal(true) + expect(uuidQuery.lean.calledOnce).to.equal(true) + }) + + it('supports opt-in lean base organization lookups without changing default lookup behavior', async () => { + const shortNameQuery = leanQuery({ UUID: 'org-uuid' }) + const uuidQuery = leanQuery({ UUID: 'org-uuid' }) + const findOne = sinon.stub(BaseOrg, 'findOne') + findOne.onCall(0).returns(shortNameQuery) + findOne.onCall(1).returns(uuidQuery) + const repository = new BaseOrgRepository() + + await repository.findOneByShortName('example', { lean: true }) + await repository.findOneByUUID('org-uuid', { lean: true }) + + expect(shortNameQuery.lean.calledOnce).to.equal(true) + expect(uuidQuery.lean.calledOnce).to.equal(true) + }) + + it('uses lean for base organization map, existence, collision, and role checks', async () => { + const organizationsQuery = leanQuery([]) + const existsQuery = leanQuery(null) + const collisionQuery = leanQuery(null) + const secretariatQuery = leanQuery({ authority: ['SECRETARIAT'] }) + const bulkDownloadQuery = leanQuery({ authority: ['BULK_DOWNLOAD'] }) + sinon.stub(BaseOrg, 'find').returns(organizationsQuery) + const findOne = sinon.stub(BaseOrg, 'findOne') + findOne.onCall(0).returns(existsQuery) + findOne.onCall(1).returns(collisionQuery) + findOne.onCall(2).returns(secretariatQuery) + findOne.onCall(3).returns(bulkDownloadQuery) + const repository = new BaseOrgRepository() + + await repository.findOrgsByUserUUIDs(['user-uuid']) + await repository.orgExists('example') + await repository.checkAliasCollisions('example', 'Example', ['example-alias']) + await repository.isSecretariatByShortName('secretariat') + await repository.isBulkDownloadByShortname('bulk-download') + + expect(organizationsQuery.lean.calledOnce).to.equal(true) + expect(existsQuery.lean.calledOnce).to.equal(true) + expect(collisionQuery.lean.calledOnce).to.equal(true) + expect(secretariatQuery.lean.calledOnce).to.equal(true) + expect(bulkDownloadQuery.lean.calledOnce).to.equal(true) + }) + + it('requests a lean organization before constructing a registry organization response', async () => { + const parentQuery = selectLeanQuery(null) + sinon.stub(BaseOrg, 'findOne').returns(parentQuery) + const repository = new BaseOrgRepository() + const lookup = sinon.stub(repository, 'findOneByShortName').resolves({ + UUID: 'org-uuid', + short_name: 'example', + authority: ['CNA'] + }) + + await repository.getOrg('example') + + expect(lookup.calledOnce).to.equal(true) + expect(lookup.firstCall.args[1]).to.include({ lean: true }) + expect(parentQuery.lean.calledOnce).to.equal(true) + }) + + it('uses lean projected organization and user queries for base user membership checks', async () => { + const byUuidQuery = leanQuery({ users: ['user-uuid'] }) + const byUsernameOrgQuery = leanQuery({ users: ['user-uuid'] }) + const userQuery = leanQuery({ _id: 'user-id' }) + const findOne = sinon.stub(BaseOrg, 'findOne') + findOne.onCall(0).returns(byUuidQuery) + findOne.onCall(1).returns(byUsernameOrgQuery) + sinon.stub(BaseUser, 'findOne').returns(userQuery) + const repository = new BaseUserRepository() + + expect(await repository.orgHasUserByUUID('example', 'user-uuid')).to.equal(true) + expect(await repository.orgHasUser('example', 'user')).to.equal(true) + + expect(byUuidQuery.lean.calledOnce).to.equal(true) + expect(byUsernameOrgQuery.lean.calledOnce).to.equal(true) + expect(userQuery.lean.calledOnce).to.equal(true) + }) + + it('uses lean user and organization queries for base user maps and role checks', async () => { + const usersQuery = leanQuery([]) + const adminOrgQuery = selectLeanQuery({ admins: ['user-uuid'], users: [] }) + const shortNameOrgQuery = leanQuery({ users: ['user-uuid'] }) + sinon.stub(BaseUser, 'find').returns(usersQuery) + const findOne = sinon.stub(BaseOrg, 'findOne') + findOne.onCall(0).returns(adminOrgQuery) + findOne.onCall(1).returns(shortNameOrgQuery) + const repository = new BaseUserRepository() + + await repository.findUsersByUUIDs(['user-uuid']) + expect(await repository.isUserAdminOfOrgUUID('user-uuid', 'org-uuid')).to.equal(true) + expect(await repository.findUsersByOrgShortname('example')).to.deep.equal(['user-uuid']) + + expect(usersQuery.lean.calledOnce).to.equal(true) + expect(adminOrgQuery.lean.calledOnce).to.equal(true) + expect(shortNameOrgQuery.lean.calledOnce).to.equal(true) + }) + + it('uses lean conversation queries for both target lists and indexed authorization lookups', async () => { + const conversationsQuery = leanQuery([]) + const indexedQuery = paginatedLeanQuery([{ UUID: 'conversation-uuid' }]) + const find = sinon.stub(Conversation, 'find') + find.onCall(0).returns(conversationsQuery) + find.onCall(1).returns(indexedQuery) + const repository = new ConversationRepository() + + await repository.getAllByTargetUUID('target-uuid', true) + expect(await repository.findByTargetUUIDAndIndex('target-uuid', 2)).to.deep.equal({ UUID: 'conversation-uuid' }) + + expect(conversationsQuery.lean.calledOnce).to.equal(true) + expect(indexedQuery.skip.calledOnceWithExactly(2)).to.equal(true) + expect(indexedQuery.limit.calledOnceWithExactly(1)).to.equal(true) + expect(indexedQuery.lean.calledOnce).to.equal(true) + }) + + it('uses lean review object queries before attaching conversation response data', async () => { + const byUuidQuery = leanQuery({ uuid: 'review-uuid', target_object_uuid: 'org-uuid', new_review_data: {} }) + const byShortNameQuery = leanQuery({ uuid: 'review-uuid', target_object_uuid: 'org-uuid', new_review_data: {} }) + const byOrgUuidQuery = leanQuery({ uuid: 'review-uuid', target_object_uuid: 'org-uuid', new_review_data: {} }) + const findOne = sinon.stub(ReviewObject, 'findOne') + findOne.onCall(0).returns(byUuidQuery) + findOne.onCall(1).returns(byShortNameQuery) + findOne.onCall(2).returns(byOrgUuidQuery) + sinon.stub(BaseOrgRepository.prototype, 'findOneByShortName').resolves({ UUID: 'org-uuid' }) + sinon.stub(BaseOrgRepository.prototype, 'findOneByUUID').resolves({ UUID: 'org-uuid' }) + sinon.stub(ConversationRepository.prototype, 'getAllByTargetUUID').resolves([]) + const repository = new ReviewObjectRepository() + + await repository.findOneByUUIDWithConversation('review-uuid', true) + await repository.getOrgReviewObjectByOrgShortname('example', true) + await repository.getOrgReviewObjectByOrgUUID('org-uuid', true) + + expect(byUuidQuery.lean.calledOnce).to.equal(true) + expect(byShortNameQuery.lean.calledOnce).to.equal(true) + expect(byOrgUuidQuery.lean.calledOnce).to.equal(true) + }) + + it('uses lean audit queries before returning audit data or sorting history', async () => { + const byShortNameQuery = leanQuery({ target_uuid: 'org-uuid' }) + const byTargetQuery = leanQuery({ target_uuid: 'org-uuid' }) + const byUuidQuery = leanQuery({ uuid: 'audit-uuid' }) + const allQuery = leanQuery([]) + const historyQuery = leanQuery({ history: [] }) + const findOne = sinon.stub(Audit, 'findOne') + findOne.onCall(0).returns(byShortNameQuery) + findOne.onCall(1).returns(byTargetQuery) + findOne.onCall(2).returns(byUuidQuery) + findOne.onCall(3).returns(historyQuery) + sinon.stub(Audit, 'find').returns(allQuery) + sinon.stub(BaseOrgRepository.prototype, 'findOneByShortName').resolves({ UUID: 'org-uuid' }) + const repository = new AuditRepository() + + await repository.findOneByOrgShortname('example') + await repository.findOneByTargetUUID('org-uuid') + await repository.findOneByUUID('audit-uuid') + await repository.findAllAuditDocuments() + await repository.getLastXChanges('org-uuid', 1) + + expect(byShortNameQuery.lean.calledOnce).to.equal(true) + expect(byTargetQuery.lean.calledOnce).to.equal(true) + expect(byUuidQuery.lean.calledOnce).to.equal(true) + expect(allQuery.lean.calledOnce).to.equal(true) + expect(historyQuery.lean.calledOnce).to.equal(true) + }) + + it('uses lean glossary queries for list and lookup responses', async () => { + const allQuery = leanQuery([]) + allQuery.exec = sinon.stub().resolves([]) + allQuery.lean.returns(allQuery) + const byShortNameQuery = leanQuery({ services_short_name: 'example' }) + byShortNameQuery.exec = sinon.stub().resolves({ services_short_name: 'example' }) + byShortNameQuery.lean.returns(byShortNameQuery) + const find = sinon.stub(Glossary, 'find').returns(allQuery) + const findOne = sinon.stub(Glossary, 'findOne').returns(byShortNameQuery) + const repository = new GlossaryRepository() + + await repository.getAll() + await repository.findOneByServicesShortName('example') + + expect(find.calledOnce).to.equal(true) + expect(findOne.calledOnce).to.equal(true) + expect(allQuery.lean.calledOnce).to.equal(true) + expect(byShortNameQuery.lean.calledOnce).to.equal(true) + }) +}) From 933a0d2a4afe66e7d67e3406969a9efb1a105944 Mon Sep 17 00:00:00 2001 From: Leo Williamson Date: Tue, 25 Aug 2026 13:45:04 -0400 Subject: [PATCH 08/27] Preserve sparse org behavior with lean queries --- src/repositories/baseOrgRepository.js | 45 ++++++++++++---- src/repositories/baseUserRepository.js | 2 +- .../repository/leanRepositoryQueriesTest.js | 54 +++++++++++++++++++ 3 files changed, 91 insertions(+), 10 deletions(-) diff --git a/src/repositories/baseOrgRepository.js b/src/repositories/baseOrgRepository.js index eef337974..d7db6c983 100644 --- a/src/repositories/baseOrgRepository.js +++ b/src/repositories/baseOrgRepository.js @@ -150,6 +150,39 @@ function getOrgProjection (isSecretariat = false) { return projection } +function applyLeanOrgDefaults (org) { + const defaultArrayFields = [ + 'aliases', + 'authority', + 'users', + 'admins', + 'private_contacts', + 'advisory_locations' + ] + + defaultArrayFields.forEach(field => { + if (!Array.isArray(org[field])) { + org[field] = [] + } + }) + + if (!org.contact_info || typeof org.contact_info !== 'object') { + org.contact_info = {} + } + if (!Array.isArray(org.contact_info.websites)) { + org.contact_info.websites = [] + } + if (!Array.isArray(org.contact_info.emails)) { + org.contact_info.emails = [] + } + + if (['CNAOrg', 'SecretariatOrg', 'RootOrg'].includes(org.kind) && !Array.isArray(org.oversees)) { + org.oversees = [] + } + + return org +} + function filterOrg (orgObj, isSecretariat = false, applyResponseMask = false, fieldsToPreserve = []) { const CONSTANTS = getConstants() const _ = require('lodash') @@ -599,7 +632,7 @@ class BaseOrgRepository extends BaseRepository { ? await this.findOneByUUID(identifier, { ...options, lean: true }, returnLegacyFormat, projection) : await this.findOneByShortName(identifier, { ...options, lean: true }, returnLegacyFormat, projection) if (!data) return null - const result = data + const result = applyLeanOrgDefaults(data) const parentOrg = await BaseOrgModel.findOne({ oversees: result.UUID }).select('UUID').lean() if (parentOrg) { @@ -1271,10 +1304,7 @@ class BaseOrgRepository extends BaseRepository { */ async isSecretariatByShortName (shortname, options = {}, isLegacyObject = false) { const org = await BaseOrgModel.findOne({ short_name: shortname }, 'authority', options).lean() - if (org.authority.includes('SECRETARIAT')) { - return true - } - return false + return Array.isArray(org?.authority) && org.authority.includes('SECRETARIAT') } /** @@ -1300,10 +1330,7 @@ class BaseOrgRepository extends BaseRepository { */ async isBulkDownloadByShortname (orgShortname, options = {}, isLegacyObject = false) { const org = await BaseOrgModel.findOne({ short_name: orgShortname }, 'authority', options).lean() - if (org.authority.includes('BULK_DOWNLOAD')) { - return true - } - return false + return Array.isArray(org?.authority) && org.authority.includes('BULK_DOWNLOAD') } /** diff --git a/src/repositories/baseUserRepository.js b/src/repositories/baseUserRepository.js index 32ee9001e..4160a8bbc 100644 --- a/src/repositories/baseUserRepository.js +++ b/src/repositories/baseUserRepository.js @@ -327,7 +327,7 @@ class BaseUserRepository extends BaseRepository { */ async findUsersByOrgShortname (shortName, options = {}) { const org = await BaseOrgModel.findOne({ short_name: shortName }, 'users', options).lean() - return org.users + return Array.isArray(org?.users) ? org.users : [] } /** diff --git a/test/unit-tests/repository/leanRepositoryQueriesTest.js b/test/unit-tests/repository/leanRepositoryQueriesTest.js index 70aa33094..95bd39b03 100644 --- a/test/unit-tests/repository/leanRepositoryQueriesTest.js +++ b/test/unit-tests/repository/leanRepositoryQueriesTest.js @@ -134,6 +134,26 @@ describe('Lean repository queries', () => { expect(bulkDownloadQuery.lean.calledOnce).to.equal(true) }) + it('treats a missing authority array as not Secretariat', async () => { + const secretariatQuery = leanQuery({}) + sinon.stub(BaseOrg, 'findOne').returns(secretariatQuery) + const repository = new BaseOrgRepository() + + expect(await repository.isSecretariatByShortName('sparse-org')).to.equal(false) + + expect(secretariatQuery.lean.calledOnce).to.equal(true) + }) + + it('treats a missing authority array as not Bulk Download', async () => { + const bulkDownloadQuery = leanQuery({}) + sinon.stub(BaseOrg, 'findOne').returns(bulkDownloadQuery) + const repository = new BaseOrgRepository() + + expect(await repository.isBulkDownloadByShortname('sparse-org')).to.equal(false) + + expect(bulkDownloadQuery.lean.calledOnce).to.equal(true) + }) + it('requests a lean organization before constructing a registry organization response', async () => { const parentQuery = selectLeanQuery(null) sinon.stub(BaseOrg, 'findOne').returns(parentQuery) @@ -151,6 +171,31 @@ describe('Lean repository queries', () => { expect(parentQuery.lean.calledOnce).to.equal(true) }) + it('preserves hydrated empty-array defaults in sparse registry organization responses', async () => { + const sparseOrganization = { + UUID: 'org-uuid', + short_name: 'sparse-org', + kind: 'CNAOrg', + authority: ['CNA'] + } + const organizationQuery = leanQuery(sparseOrganization) + const parentQuery = selectLeanQuery(null) + const findOne = sinon.stub(BaseOrg, 'findOne') + findOne.onCall(0).returns(organizationQuery) + findOne.onCall(1).returns(parentQuery) + const repository = new BaseOrgRepository() + + const result = await repository.getOrg('sparse-org', false, {}, false, true) + + expect(result.aliases).to.deep.equal([]) + expect(result.users).to.deep.equal([]) + expect(result.admins).to.deep.equal([]) + expect(result.private_contacts).to.deep.equal([]) + expect(result.advisory_locations).to.deep.equal([]) + expect(result.oversees).to.deep.equal([]) + expect(result.contact_info).to.deep.equal({ websites: [], emails: [] }) + }) + it('uses lean projected organization and user queries for base user membership checks', async () => { const byUuidQuery = leanQuery({ users: ['user-uuid'] }) const byUsernameOrgQuery = leanQuery({ users: ['user-uuid'] }) @@ -188,6 +233,15 @@ describe('Lean repository queries', () => { expect(shortNameOrgQuery.lean.calledOnce).to.equal(true) }) + it('returns an empty user list when a sparse organization has no users field', async () => { + const organizationQuery = leanQuery({ UUID: 'org-uuid' }) + sinon.stub(BaseOrg, 'findOne').returns(organizationQuery) + const repository = new BaseUserRepository() + + expect(await repository.findUsersByOrgShortname('sparse-org')).to.deep.equal([]) + expect(organizationQuery.lean.calledOnce).to.equal(true) + }) + it('uses lean conversation queries for both target lists and indexed authorization lookups', async () => { const conversationsQuery = leanQuery([]) const indexedQuery = paginatedLeanQuery([{ UUID: 'conversation-uuid' }]) From eea3d2f085a354cba31658e526781304b687ddd5 Mon Sep 17 00:00:00 2001 From: Leo Williamson Date: Wed, 26 Aug 2026 11:04:58 -0400 Subject: [PATCH 09/27] Preserve legacy and registry org shapes with lean --- src/repositories/baseOrgRepository.js | 18 +++-- .../org/leanSparseOrgResponseTest.js | 69 +++++++++++++++++++ .../repository/leanRepositoryQueriesTest.js | 40 ++++++++++- 3 files changed, 120 insertions(+), 7 deletions(-) create mode 100644 test/integration-tests/org/leanSparseOrgResponseTest.js diff --git a/src/repositories/baseOrgRepository.js b/src/repositories/baseOrgRepository.js index d7db6c983..573075d37 100644 --- a/src/repositories/baseOrgRepository.js +++ b/src/repositories/baseOrgRepository.js @@ -150,7 +150,7 @@ function getOrgProjection (isSecretariat = false) { return projection } -function applyLeanOrgDefaults (org) { +function applyLeanRegistryOrgDefaults (org) { const defaultArrayFields = [ 'aliases', 'authority', @@ -176,10 +176,16 @@ function applyLeanOrgDefaults (org) { org.contact_info.emails = [] } - if (['CNAOrg', 'SecretariatOrg', 'RootOrg'].includes(org.kind) && !Array.isArray(org.oversees)) { - org.oversees = [] - } + return org +} +function applyLeanLegacyOrgDefaults (org) { + if (!org.authority || typeof org.authority !== 'object' || Array.isArray(org.authority)) { + org.authority = {} + } + if (!Array.isArray(org.authority.active_roles)) { + org.authority.active_roles = [] + } return org } @@ -632,7 +638,9 @@ class BaseOrgRepository extends BaseRepository { ? await this.findOneByUUID(identifier, { ...options, lean: true }, returnLegacyFormat, projection) : await this.findOneByShortName(identifier, { ...options, lean: true }, returnLegacyFormat, projection) if (!data) return null - const result = applyLeanOrgDefaults(data) + const result = returnLegacyFormat + ? applyLeanLegacyOrgDefaults(data) + : applyLeanRegistryOrgDefaults(data) const parentOrg = await BaseOrgModel.findOne({ oversees: result.UUID }).select('UUID').lean() if (parentOrg) { diff --git a/test/integration-tests/org/leanSparseOrgResponseTest.js b/test/integration-tests/org/leanSparseOrgResponseTest.js new file mode 100644 index 000000000..c478e2197 --- /dev/null +++ b/test/integration-tests/org/leanSparseOrgResponseTest.js @@ -0,0 +1,69 @@ +/* eslint-disable no-unused-expressions */ + +const chai = require('chai') +chai.use(require('chai-http')) +const expect = chai.expect + +const app = require('../../../src/index') +const BaseOrg = require('../../../src/model/baseorg') +const Org = require('../../../src/model/org') +const constants = require('../constants') + +const sparseOrgUUID = 'b88b6a9c-1a8b-4e5d-a1d8-6a8c48b98c4d' +const sparseOrgShortName = 'lean_sparse_org' + +describe('Lean sparse organization response compatibility', () => { + before(async () => { + await BaseOrg.deleteMany({ UUID: sparseOrgUUID }) + await Org.deleteMany({ UUID: sparseOrgUUID }) + + await BaseOrg.collection.insertOne({ + UUID: sparseOrgUUID, + short_name: sparseOrgShortName, + long_name: 'Lean Sparse Organization', + authority: ['CNA'], + __t: 'CNAOrg' + }) + + await Org.collection.insertOne({ + UUID: sparseOrgUUID, + short_name: sparseOrgShortName, + name: 'Lean Sparse Organization', + authority: { active_roles: ['CNA'] }, + policies: { id_quota: 100 } + }) + }) + + after(async () => { + await BaseOrg.deleteMany({ UUID: sparseOrgUUID }) + await Org.deleteMany({ UUID: sparseOrgUUID }) + }) + + it('preserves registry empty-array defaults for a sparse organization', async () => { + const res = await chai.request(app) + .get(`/api/registry/org/${sparseOrgShortName}`) + .set(constants.headers) + + expect(res).to.have.status(200) + expect(res.body.authority).to.deep.equal(['CNA']) + expect(res.body.aliases).to.deep.equal([]) + expect(res.body.users).to.deep.equal([]) + expect(res.body.admins).to.deep.equal([]) + expect(res.body.private_contacts).to.deep.equal([]) + expect(res.body.advisory_locations).to.deep.equal([]) + expect(res.body.contact_info).to.deep.equal({ websites: [], emails: [] }) + }) + + it('preserves legacy authority without injecting registry fields', async () => { + const res = await chai.request(app) + .get(`/api/org/${sparseOrgShortName}`) + .set(constants.headers) + + expect(res).to.have.status(200) + expect(res.body.authority).to.deep.equal({ active_roles: ['CNA'] }) + expect(res.body).to.not.have.property('aliases') + expect(res.body).to.not.have.property('users') + expect(res.body).to.not.have.property('admins') + expect(res.body).to.not.have.property('contact_info') + }) +}) diff --git a/test/unit-tests/repository/leanRepositoryQueriesTest.js b/test/unit-tests/repository/leanRepositoryQueriesTest.js index 95bd39b03..582716cb2 100644 --- a/test/unit-tests/repository/leanRepositoryQueriesTest.js +++ b/test/unit-tests/repository/leanRepositoryQueriesTest.js @@ -175,7 +175,6 @@ describe('Lean repository queries', () => { const sparseOrganization = { UUID: 'org-uuid', short_name: 'sparse-org', - kind: 'CNAOrg', authority: ['CNA'] } const organizationQuery = leanQuery(sparseOrganization) @@ -192,10 +191,47 @@ describe('Lean repository queries', () => { expect(result.admins).to.deep.equal([]) expect(result.private_contacts).to.deep.equal([]) expect(result.advisory_locations).to.deep.equal([]) - expect(result.oversees).to.deep.equal([]) + expect(result).to.not.have.property('oversees') expect(result.contact_info).to.deep.equal({ websites: [], emails: [] }) }) + it('preserves legacy authority objects without injecting registry defaults', async () => { + const legacyOrganizationQuery = leanQuery({ + UUID: 'legacy-org-uuid', + short_name: 'legacy-org', + authority: { active_roles: ['CNA'] } + }) + const parentQuery = selectLeanQuery(null) + sinon.stub(Org, 'findOne').returns(legacyOrganizationQuery) + sinon.stub(BaseOrg, 'findOne').returns(parentQuery) + const repository = new BaseOrgRepository() + + const result = await repository.getOrg('legacy-org', false, {}, true) + + expect(result.authority).to.deep.equal({ active_roles: ['CNA'] }) + expect(result).to.not.have.property('aliases') + expect(result).to.not.have.property('users') + expect(result).to.not.have.property('admins') + expect(result).to.not.have.property('contact_info') + expect(legacyOrganizationQuery.lean.calledOnce).to.equal(true) + }) + + it('preserves the hydrated legacy authority default for sparse organizations', async () => { + const legacyOrganizationQuery = leanQuery({ + UUID: 'legacy-org-uuid', + short_name: 'sparse-legacy-org' + }) + const parentQuery = selectLeanQuery(null) + sinon.stub(Org, 'findOne').returns(legacyOrganizationQuery) + sinon.stub(BaseOrg, 'findOne').returns(parentQuery) + const repository = new BaseOrgRepository() + + const result = await repository.getOrg('sparse-legacy-org', false, {}, true) + + expect(result.authority).to.deep.equal({ active_roles: [] }) + expect(legacyOrganizationQuery.lean.calledOnce).to.equal(true) + }) + it('uses lean projected organization and user queries for base user membership checks', async () => { const byUuidQuery = leanQuery({ users: ['user-uuid'] }) const byUsernameOrgQuery = leanQuery({ users: ['user-uuid'] }) From b99fe5d3057988596706bb70a723fa4a5ea3264c Mon Sep 17 00:00:00 2001 From: David T Rocca Date: Wed, 2 Sep 2026 14:44:53 -0400 Subject: [PATCH 10/27] minor changes --- api-docs/openapi.json | 32 +++++++++++++++++++ src/controller/registry.controller/index.js | 3 ++ src/scripts/migrate.js | 3 ++ .../registry-org/activeCnaListTest.js | 12 +++++++ 4 files changed, 50 insertions(+) diff --git a/api-docs/openapi.json b/api-docs/openapi.json index 84fe59bcf..b9d195b81 100644 --- a/api-docs/openapi.json +++ b/api-docs/openapi.json @@ -3199,6 +3199,38 @@ } } }, + "/registry/org/cnas": { + "get": { + "tags": [ + "Registry Organization" + ], + "summary": "Lists active CNAs in the CVE.org public format", + "description": "This public endpoint builds the active CNA list from registry organization data.", + "operationId": "registryOrgActiveCnas", + "responses": { + "200": { + "description": "Returns active CNAs in the CVE.org public format", + "content": { + "application/json": { + "schema": { + "$ref": "../schemas/registry-org/list-active-cnas-response.json" + } + } + } + }, + "500": { + "description": "Internal Server Error", + "content": { + "application/json": { + "schema": { + "$ref": "../schemas/errors/generic.json" + } + } + } + } + } + } + }, "/registry/org/{shortname}/users": { "get": { "tags": [ diff --git a/src/controller/registry.controller/index.js b/src/controller/registry.controller/index.js index 41f2accd8..057015acb 100644 --- a/src/controller/registry.controller/index.js +++ b/src/controller/registry.controller/index.js @@ -115,6 +115,9 @@ router.get('/registry/org/cnas', } } */ + mw.useRegistry(), + mw.validateUser, + mw.onlySecretariat, registryOrgController.ACTIVE_CNAS ) diff --git a/src/scripts/migrate.js b/src/scripts/migrate.js index 1bc3d8eb9..1f0331098 100644 --- a/src/scripts/migrate.js +++ b/src/scripts/migrate.js @@ -215,6 +215,9 @@ async function orgHelper (db) { websites: site ? [site] : [], phone: null }, + program_data: { + status: 'active' + }, inUse: doc.inUse, created: doc.time.created, last_updated: doc.time.modified diff --git a/test/integration-tests/registry-org/activeCnaListTest.js b/test/integration-tests/registry-org/activeCnaListTest.js index 9be63f27d..0af6f598c 100644 --- a/test/integration-tests/registry-org/activeCnaListTest.js +++ b/test/integration-tests/registry-org/activeCnaListTest.js @@ -70,6 +70,18 @@ describe('Public active CNA list', () => { await BaseOrg.collection.deleteMany({ UUID: { $in: Object.values(uuids) } }) }) + it('returns a CNA created by the standard populate and migration flow', async () => { + const migratedCna = await BaseOrg.findOne({ short_name: 'window_1' }).lean() + + expect(migratedCna).to.not.equal(null) + expect(migratedCna.program_data.status).to.equal('active') + + const res = await chai.request(app).get('/api/registry/org/cnas') + + expect(res).to.have.status(200) + expect(res.body.some(org => org.shortName === migratedCna.short_name)).to.equal(true) + }) + it('returns database-backed active CNAs without authentication and excludes inactive CNAs', async () => { const res = await chai.request(app).get('/api/registry/org/cnas') From b2a0aaee5246f70efba14bbef87ed0506b24560d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 2 Sep 2026 23:33:10 +0000 Subject: [PATCH 11/27] Bump fast-uri from 3.1.5 to 3.1.7 Bumps [fast-uri](https://github.com/fastify/fast-uri) from 3.1.5 to 3.1.7. - [Release notes](https://github.com/fastify/fast-uri/releases) - [Commits](https://github.com/fastify/fast-uri/compare/v3.1.5...v3.1.7) --- updated-dependencies: - dependency-name: fast-uri dependency-version: 3.1.7 dependency-type: indirect ... Signed-off-by: dependabot[bot] --- package-lock.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 18cd66a37..b093c8edc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "cve-services", - "version": "2.8.4", + "version": "2.8.5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "cve-services", - "version": "2.8.4", + "version": "2.8.5", "license": "(CC0)", "dependencies": { "ajv": "^8.6.2", @@ -3454,9 +3454,9 @@ "license": "MIT" }, "node_modules/fast-uri": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.5.tgz", - "integrity": "sha512-gHwA1O9LDIcKunMKhObS/HimwtehO1nPUECKAu5TpKgaO19fcWEl4bliWe1jWxVFvIXztJjjQ4L8XQ1EU9f7Jw==", + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.7.tgz", + "integrity": "sha512-dOvZVzjdZdz7phd9v6jCbwxrBW3fK6n8Rc0CtdmM4bumzMnxywBYhuph6J819RRw/ku+rLbelwfMunktuzVVHg==", "funding": [ { "type": "github", From efec8b700081a89bbacd1503b9a3706687ed3d70 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 3 Sep 2026 00:33:36 +0000 Subject: [PATCH 12/27] Bump browserslist from 4.28.2 to 4.28.8 Bumps [browserslist](https://github.com/browserslist/browserslist) from 4.28.2 to 4.28.8. - [Release notes](https://github.com/browserslist/browserslist/releases) - [Changelog](https://github.com/browserslist/browserslist/blob/main/CHANGELOG.md) - [Commits](https://github.com/browserslist/browserslist/compare/4.28.2...4.28.8) --- updated-dependencies: - dependency-name: browserslist dependency-version: 4.28.8 dependency-type: indirect ... Signed-off-by: dependabot[bot] --- package-lock.json | 50 +++++++++++++++++++++++------------------------ 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/package-lock.json b/package-lock.json index 18cd66a37..1845b7ba9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "cve-services", - "version": "2.8.4", + "version": "2.8.5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "cve-services", - "version": "2.8.4", + "version": "2.8.5", "license": "(CC0)", "dependencies": { "ajv": "^8.6.2", @@ -1433,9 +1433,9 @@ } }, "node_modules/baseline-browser-mapping": { - "version": "2.10.37", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.37.tgz", - "integrity": "sha512-girxaJ7WZssDOFhzCGZTDKoTa1gk6A1TbflaYTpykLJ4UU9Fz9kx1aREM8JCuoVHbL8X8T/mJg7w2oYSq72Oig==", + "version": "2.11.20", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.20.tgz", + "integrity": "sha512-H0ulySigv6icDJ1F7SjtdCD6PrhTpdYCmP0CactWy1+ekh0AFd0o1Wn5T8b+hnTmdBx19u9yhL6wvCylXMY7zw==", "dev": true, "license": "Apache-2.0", "bin": { @@ -1546,9 +1546,9 @@ "license": "ISC" }, "node_modules/browserslist": { - "version": "4.28.2", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", - "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "version": "4.28.8", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.8.tgz", + "integrity": "sha512-V2NpofLblG64mfOtSgDhOJESZEGogzDMBv/q+W6oc4LXWP/q75eOXoOaaOu1EOadB9U4Bwx/e0yzbvwKH8zalA==", "dev": true, "funding": [ { @@ -1566,11 +1566,11 @@ ], "license": "MIT", "dependencies": { - "baseline-browser-mapping": "^2.10.12", - "caniuse-lite": "^1.0.30001782", - "electron-to-chromium": "^1.5.328", - "node-releases": "^2.0.36", - "update-browserslist-db": "^1.2.3" + "baseline-browser-mapping": "^2.11.12", + "caniuse-lite": "^1.0.30001809", + "electron-to-chromium": "^1.5.402", + "node-releases": "^2.0.53", + "update-browserslist-db": "^1.3.0" }, "bin": { "browserslist": "cli.js" @@ -1694,9 +1694,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001799", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001799.tgz", - "integrity": "sha512-hG1bReV+OUU+MOqK4t/ZWI0tZOyz3rqS9XuhOUz1cIcbwBKjOyJEJuw9ER5JuNyqxNk8u/JUVbGibBOL1yrjFw==", + "version": "1.0.30001810", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001810.tgz", + "integrity": "sha512-TITQPUkaz+aVk5GL6NhOdwk1aEaNTSDPsGFWrTuhKGtjTF70jL/Oht2W4c6rXUe5fu7Ie19VIahAXHIIiWWNeg==", "dev": true, "funding": [ { @@ -2505,9 +2505,9 @@ "license": "MIT" }, "node_modules/electron-to-chromium": { - "version": "1.5.375", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.375.tgz", - "integrity": "sha512-ZWP5eB4BVPW/ZYo9252hQZHZ5XavtsTgpbhcmMmRwymavC5AsLWQWBPaKMeNd2LW0KGby5HPXvj7+sr4ta5j/Q==", + "version": "1.5.420", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.420.tgz", + "integrity": "sha512-2yD6XreGusOfNV+dUcvipJEXc3n/n7fgr7996aszTG+YY5E4mqM4tOq/3uhP129cazL9YHbVWSpc79ePotWtPA==", "dev": true, "license": "ISC" }, @@ -6103,9 +6103,9 @@ } }, "node_modules/node-releases": { - "version": "2.0.47", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.47.tgz", - "integrity": "sha512-Uzmd6LXpouKo8EUK68IjH4+E01w/hXyV3R3g/geCJo+rXLNfh1xucB+LOzYEOQPSiUK3h/xZf0cQGcSsmyL2Og==", + "version": "2.0.54", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.54.tgz", + "integrity": "sha512-YHs7BmmcsdAI5Ozuf8JZo6PT0mv2GIWC9vMfvUC3dp65M8hn7Ux8CPL+2oBI7juNuj9d0ndhTcznq2ODBps9cQ==", "dev": true, "license": "MIT", "engines": { @@ -9569,9 +9569,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", - "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.2.tgz", + "integrity": "sha512-UQ+MSxlhRm1bzjhU+DcuXfjFO1FzNtqhK5+9Yvlp90ItDLk5vT932A0rFu619nf7RVS+Y/VeaUW1jaRDqZ8VJw==", "dev": true, "funding": [ { From 30820b0d7b2eb12c990bfeb2c758227b565e4470 Mon Sep 17 00:00:00 2001 From: David T Rocca Date: Tue, 8 Sep 2026 11:08:51 -0400 Subject: [PATCH 13/27] Fixing tests --- api-docs/openapi.json | 49 +++++++++++++-- src/controller/registry.controller/index.js | 35 ++++++++++- .../org.registry.controller.js | 4 +- src/repositories/baseOrgRepository.js | 2 +- test/integration-tests/cve-id/getCveIdTest.js | 7 ++- .../registry-org/activeCnaListTest.js | 62 +++++++++++++------ test/unit-tests/org/activeCnaListTest.js | 2 +- 7 files changed, 129 insertions(+), 32 deletions(-) diff --git a/api-docs/openapi.json b/api-docs/openapi.json index b9d195b81..d4c33d370 100644 --- a/api-docs/openapi.json +++ b/api-docs/openapi.json @@ -11,7 +11,7 @@ }, "servers": [ { - "url": "https://cveawg-dev.mitre.org/api" + "url": "urlplaceholder" } ], "paths": { @@ -3204,12 +3204,23 @@ "tags": [ "Registry Organization" ], - "summary": "Lists active CNAs in the CVE.org public format", - "description": "This public endpoint builds the active CNA list from registry organization data.", + "summary": "Lists active CNAs in the CVE.org partner-list format (Secretariat only)", + "description": "

Access Control

User must belong to an organization with the Secretariat role.

Expected Behavior

Secretariat: Retrieves the active CNA list built from registry organization data in the CVE.org partner-list format.

", "operationId": "registryOrgActiveCnas", + "parameters": [ + { + "$ref": "#/components/parameters/apiEntityHeader" + }, + { + "$ref": "#/components/parameters/apiUserHeader" + }, + { + "$ref": "#/components/parameters/apiSecretHeader" + } + ], "responses": { "200": { - "description": "Returns active CNAs in the CVE.org public format", + "description": "Returns active CNAs in the CVE.org partner-list format", "content": { "application/json": { "schema": { @@ -3218,6 +3229,36 @@ } } }, + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "$ref": "../schemas/errors/bad-request.json" + } + } + } + }, + "401": { + "description": "Not Authenticated", + "content": { + "application/json": { + "schema": { + "$ref": "../schemas/errors/generic.json" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "../schemas/errors/generic.json" + } + } + } + }, "500": { "description": "Internal Server Error", "content": { diff --git a/src/controller/registry.controller/index.js b/src/controller/registry.controller/index.js index 057015acb..6a8eb9832 100644 --- a/src/controller/registry.controller/index.js +++ b/src/controller/registry.controller/index.js @@ -96,16 +96,45 @@ router.get('/registry/org/cnas', /* #swagger.tags = ['Registry Organization'] #swagger.operationId = 'registryOrgActiveCnas' - #swagger.summary = 'Lists active CNAs in the CVE.org public format' - #swagger.description = 'This public endpoint builds the active CNA list from registry organization data.' + #swagger.summary = 'Lists active CNAs in the CVE.org partner-list format (Secretariat only)' + #swagger.description = '

Access Control

User must belong to an organization with the Secretariat role.

Expected Behavior

Secretariat: Retrieves the active CNA list built from registry organization data in the CVE.org partner-list format.

' + #swagger.parameters['$ref'] = [ + '#/components/parameters/apiEntityHeader', + '#/components/parameters/apiUserHeader', + '#/components/parameters/apiSecretHeader' + ] #swagger.responses[200] = { - description: 'Returns active CNAs in the CVE.org public format', + description: 'Returns active CNAs in the CVE.org partner-list format', content: { 'application/json': { schema: { $ref: '../schemas/registry-org/list-active-cnas-response.json' } } } } + #swagger.responses[400] = { + description: 'Bad Request', + content: { + 'application/json': { + schema: { $ref: '../schemas/errors/bad-request.json' } + } + } + } + #swagger.responses[401] = { + description: 'Not Authenticated', + content: { + 'application/json': { + schema: { $ref: '../schemas/errors/generic.json' } + } + } + } + #swagger.responses[403] = { + description: 'Forbidden', + content: { + 'application/json': { + schema: { $ref: '../schemas/errors/generic.json' } + } + } + } #swagger.responses[500] = { description: 'Internal Server Error', content: { diff --git a/src/controller/registry.controller/org.registry.controller.js b/src/controller/registry.controller/org.registry.controller.js index 4b2059951..2d54e9356 100644 --- a/src/controller/registry.controller/org.registry.controller.js +++ b/src/controller/registry.controller/org.registry.controller.js @@ -197,7 +197,7 @@ function mapActiveCnaToPublicFormat (org) { } /** - * Retrieves active CNA partners in the public CVE.org response format. + * Retrieves active CNA partners in the CVE.org partner-list response format. * * @async * @function getActiveCnas @@ -205,7 +205,7 @@ function mapActiveCnaToPublicFormat (org) { * @param {object} res - The Express response object. * @param {function} next - The next middleware function. * @returns {Promise} - A promise that resolves when the response is sent. - * @description This endpoint is public and reads active CNA data from the registry database. + * @description This endpoint is restricted to Secretariat users and reads active CNA data from the registry database. * Called by GET /api/registry/org/cnas */ async function getActiveCnas (req, res, next) { diff --git a/src/repositories/baseOrgRepository.js b/src/repositories/baseOrgRepository.js index 42c4cd3b0..9f8150276 100644 --- a/src/repositories/baseOrgRepository.js +++ b/src/repositories/baseOrgRepository.js @@ -607,7 +607,7 @@ class BaseOrgRepository extends BaseRepository { } /** - * Retrieves active CNA organizations for the unauthenticated public partner list. + * Retrieves active CNA organizations for the CVE.org partner-list response. * * The status comparison supports normalized lowercase and legacy title-case data. * diff --git a/test/integration-tests/cve-id/getCveIdTest.js b/test/integration-tests/cve-id/getCveIdTest.js index 053b9b84f..636b92765 100644 --- a/test/integration-tests/cve-id/getCveIdTest.js +++ b/test/integration-tests/cve-id/getCveIdTest.js @@ -9,10 +9,9 @@ const expect = chai.expect const constants = require('../constants.js') const helpers = require('../helpers.js') const app = require('../../../src/index.js') +const CveId = require('../../../src/model/cve-id') describe('Testing Get CVE-ID endpoint', () => { - // TODO: Update this test to dynamically calculate reserved count. - const RESESRVED_COUNT = 124 const YEAR_COUNT = 10 const PUB_YEAR_COUNT = 4 const TIME_WINDOW_COUNT = 40 @@ -59,6 +58,8 @@ describe('Testing Get CVE-ID endpoint', () => { }) }) it('Get all CVE-IDs in the RESERVED state', async () => { + const reservedCount = await CveId.countDocuments({ state: 'RESERVED' }) + await chai.request(app) .get('/api/cve-id?state=RESERVED') .set(constants.headers) @@ -66,7 +67,7 @@ describe('Testing Get CVE-ID endpoint', () => { expect(err).to.be.undefined expect(res).to.have.status(200) expect(_.every(res.body.cve_ids, { state: 'RESERVED' })).to.be.true - expect(res.body.cve_ids).to.have.length(RESESRVED_COUNT) + expect(res.body.cve_ids).to.have.length(reservedCount) }) }) it('Get all CVE-IDs in the PUBLISHED state', async () => { diff --git a/test/integration-tests/registry-org/activeCnaListTest.js b/test/integration-tests/registry-org/activeCnaListTest.js index 0af6f598c..cdc671729 100644 --- a/test/integration-tests/registry-org/activeCnaListTest.js +++ b/test/integration-tests/registry-org/activeCnaListTest.js @@ -4,6 +4,7 @@ const expect = chai.expect chai.use(require('chai-http')) const { v4: uuidv4 } = require('uuid') +const constants = require('../constants.js') const app = require('../../../src/index.js') const BaseOrg = require('../../../src/model/baseorg') @@ -15,19 +16,19 @@ const uuids = { inactive: uuidv4() } const shortNames = { - tlr: `public-tlr-${runId}`, - root: `public-root-${runId}`, - child: `public-cna-${runId}`, - inactive: `public-inactive-${runId}` + tlr: `secretariat-tlr-${runId}`, + root: `secretariat-root-${runId}`, + child: `secretariat-cna-${runId}`, + inactive: `secretariat-inactive-${runId}` } -describe('Public active CNA list', () => { +describe('Secretariat active CNA list', () => { before(async () => { await BaseOrg.collection.insertMany([{ __t: 'RootOrg', UUID: uuids.tlr, short_name: shortNames.tlr, - long_name: 'Public Test TLR', + long_name: 'Secretariat Test TLR', authority: ['ROOT'], top_level_root: 'true', oversees: [uuids.root], @@ -36,7 +37,7 @@ describe('Public active CNA list', () => { __t: 'CNAOrg', UUID: uuids.root, short_name: shortNames.root, - long_name: 'Public Test Root', + long_name: 'Secretariat Test Root', authority: ['CNA', 'ROOT'], top_level_root: 'false', oversees: [uuids.child], @@ -45,13 +46,13 @@ describe('Public active CNA list', () => { __t: 'CNAOrg', UUID: uuids.child, short_name: shortNames.child, - long_name: 'Public Test CNA', + long_name: 'Secretariat Test CNA', authority: ['CNA'], top_level_root: `${shortNames.tlr} TLR`, partner_number: 'CNA-TEST-0001', partner_role_type: ['Vendor'], partner_country: 'USA', - charter_or_scope: 'Public endpoint integration test.', + charter_or_scope: 'Secretariat-only endpoint integration test.', contact_info: { emails: ['security@example.test'], websites: ['https://example.test/contact'] }, disclosure_policy: 'https://example.test/policy', advisory_locations: ['https://example.test/advisories'], @@ -60,7 +61,7 @@ describe('Public active CNA list', () => { __t: 'CNAOrg', UUID: uuids.inactive, short_name: shortNames.inactive, - long_name: 'Inactive Public Test CNA', + long_name: 'Inactive Secretariat Test CNA', authority: ['CNA'], program_data: { status: 'inactive' } }]) @@ -76,40 +77,65 @@ describe('Public active CNA list', () => { expect(migratedCna).to.not.equal(null) expect(migratedCna.program_data.status).to.equal('active') - const res = await chai.request(app).get('/api/registry/org/cnas') + const res = await chai.request(app) + .get('/api/registry/org/cnas') + .set(constants.headers) expect(res).to.have.status(200) expect(res.body.some(org => org.shortName === migratedCna.short_name)).to.equal(true) }) - it('returns database-backed active CNAs without authentication and excludes inactive CNAs', async () => { - const res = await chai.request(app).get('/api/registry/org/cnas') + it('returns database-backed active CNAs to Secretariat and excludes inactive CNAs', async () => { + const res = await chai.request(app) + .get('/api/registry/org/cnas') + .set(constants.headers) expect(res).to.have.status(200) const child = res.body.find(org => org.shortName === shortNames.child) expect(res.body.some(org => org.shortName === shortNames.inactive)).to.equal(false) expect(child).to.include({ cnaID: 'CNA-TEST-0001', - organizationName: 'Public Test CNA', + organizationName: 'Secretariat Test CNA', country: 'USA' }) }) it('resolves CNA-discriminator roots and inherits sentinel top-level-root relationships', async () => { - const res = await chai.request(app).get('/api/registry/org/cnas') + const res = await chai.request(app) + .get('/api/registry/org/cnas') + .set(constants.headers) expect(res).to.have.status(200) const child = res.body.find(org => org.shortName === shortNames.child) const root = res.body.find(org => org.shortName === shortNames.root) expect(child.CNA).to.deep.include({ isRoot: false, - root: { shortName: shortNames.root, organizationName: 'Public Test Root' }, - TLR: { shortName: shortNames.tlr, organizationName: 'Public Test TLR' } + root: { shortName: shortNames.root, organizationName: 'Secretariat Test Root' }, + TLR: { shortName: shortNames.tlr, organizationName: 'Secretariat Test TLR' } }) expect(root.CNA).to.deep.include({ isRoot: true, root: { shortName: 'n/a', organizationName: 'n/a' }, - TLR: { shortName: shortNames.tlr, organizationName: 'Public Test TLR' } + TLR: { shortName: shortNames.tlr, organizationName: 'Secretariat Test TLR' } }) }) + + it('requires authentication', async () => { + const res = await chai.request(app).get('/api/registry/org/cnas') + + expect(res).to.have.status(400) + expect(res.body).to.deep.equal({ + error: 'BAD_REQUEST', + message: 'CVE-API-ORG header field required.' + }) + }) + + it('rejects authenticated non-Secretariat users', async () => { + const res = await chai.request(app) + .get('/api/registry/org/cnas') + .set(constants.nonSecretariatUserHeaders) + + expect(res).to.have.status(403) + expect(res.body.error).to.equal('SECRETARIAT_ONLY') + }) }) diff --git a/test/unit-tests/org/activeCnaListTest.js b/test/unit-tests/org/activeCnaListTest.js index 941e0018c..7d8917c9b 100644 --- a/test/unit-tests/org/activeCnaListTest.js +++ b/test/unit-tests/org/activeCnaListTest.js @@ -10,7 +10,7 @@ const BaseOrgModel = require('../../../src/model/baseorg') describe('Active CNA list', () => { afterEach(() => sinon.restore()) - it('returns active CNAs from the database in the public partner-list format', async () => { + it('returns active CNAs from the database in the CVE.org partner-list format', async () => { const res = { status: sinon.stub().returnsThis(), json: sinon.stub() } const activeCnas = [{ UUID: 'child-uuid', From 13dcc8331d5cb181145fc7d3c1b77640ce644c06 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 2 Sep 2026 17:29:27 +0000 Subject: [PATCH 14/27] Bump @faker-js/faker from 7.6.0 to 10.5.0 Bumps [@faker-js/faker](https://github.com/faker-js/faker) from 7.6.0 to 10.5.0. - [Release notes](https://github.com/faker-js/faker/releases) - [Changelog](https://github.com/faker-js/faker/blob/next/CHANGELOG.md) - [Commits](https://github.com/faker-js/faker/compare/v7.6.0...v10.5.0) --- updated-dependencies: - dependency-name: "@faker-js/faker" dependency-version: 10.5.0 dependency-type: direct:development ... Signed-off-by: dependabot[bot] --- package-lock.json | 18 ++++++++++++------ package.json | 2 +- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/package-lock.json b/package-lock.json index 950fb7687..806d1c1e5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -44,7 +44,7 @@ "yamljs": "^0.3.0" }, "devDependencies": { - "@faker-js/faker": "^7.6.0", + "@faker-js/faker": "^10.5.0", "chai": "^4.2.0", "chai-arrays": "^2.0.0", "chai-http": "^4.3.0", @@ -496,14 +496,20 @@ } }, "node_modules/@faker-js/faker": { - "version": "7.6.0", - "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-7.6.0.tgz", - "integrity": "sha512-XK6BTq1NDMo9Xqw/YkYyGjSsg44fbNwYRx7QK2CuoQgyy+f1rrTDHoExVM5PsyXCtfl2vs2vVJ0MN0yN6LppRw==", + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-10.5.0.tgz", + "integrity": "sha512-bsxD8WLS5lIj7aaoCx1YJkktqYj5vlBUE6HWzu2Q51ksrGJ0H737ECCKlFU7Yf8Br45z9t99frBp/J7kzbMPAg==", "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/fakerjs" + } + ], "license": "MIT", "engines": { - "node": ">=14.0.0", - "npm": ">=6.0.0" + "node": "^20.19.0 || ^22.13.0 || ^23.5.0 || >=24.0.0", + "npm": ">=10" } }, "node_modules/@humanwhocodes/config-array": { diff --git a/package.json b/package.json index b63447db2..4381afffb 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "version": "2.8.5", "license": "(CC0)", "devDependencies": { - "@faker-js/faker": "^7.6.0", + "@faker-js/faker": "^10.5.0", "chai": "^4.2.0", "chai-arrays": "^2.0.0", "chai-http": "^4.3.0", From d2c3cb86b54d93eba96b0dfcb8d0d289799c6aae Mon Sep 17 00:00:00 2001 From: David T Rocca Date: Tue, 8 Sep 2026 12:27:13 -0400 Subject: [PATCH 15/27] Fixing faker tests --- .../integration-tests/org/postOrgUsersTest.js | 6 ++-- .../org/regularUsersTestRegistry.js | 32 +++++++++---------- test/unit-tests/cve-id/cveIdGetAllTest.js | 6 ++-- test/unit-tests/cve/insertAdpTest.js | 4 +-- test/unit-tests/cve/updateCnaTest.js | 4 +-- test/unit-tests/middleware/onlyAdpsTest.js | 6 ++-- .../middleware/onlyOrgWithPartnerRoleTest.js | 10 +++--- test/unit-tests/org/orgCreateADPTest.js | 2 +- test/unit-tests/org/orgCreateTest.js | 16 +++++----- test/unit-tests/org/orgGetSingleTest.js | 2 +- test/unit-tests/user/userCreateTest.js | 4 +-- test/unit-tests/user/userGetSingleTest.js | 10 +++--- test/unit-tests/user/userResetSecretTest.js | 14 ++++---- 13 files changed, 58 insertions(+), 58 deletions(-) diff --git a/test/integration-tests/org/postOrgUsersTest.js b/test/integration-tests/org/postOrgUsersTest.js index bc2f1fda7..41ca3e21b 100644 --- a/test/integration-tests/org/postOrgUsersTest.js +++ b/test/integration-tests/org/postOrgUsersTest.js @@ -337,13 +337,13 @@ describe('Testing user post endpoint', () => { this.timeout(70000) let counter = await User.where({ org_UUID: orgUuid }).countDocuments().exec() do { - const firstName = faker.name.firstName() - const lastName = faker.name.lastName() + const firstName = faker.person.firstName() + const lastName = faker.person.lastName() await chai.request(app) .post('/api/org/win_5/user') .set({ ...constants.headers, ...shortName }) .send({ - username: faker.internet.userName({ firstName: firstName, lastName: lastName }) + ' ' + counter, + username: faker.internet.username({ firstName: firstName, lastName: lastName }) + ' ' + counter, name: { first: firstName, last: lastName diff --git a/test/integration-tests/org/regularUsersTestRegistry.js b/test/integration-tests/org/regularUsersTestRegistry.js index 252122f48..c794bfc9c 100644 --- a/test/integration-tests/org/regularUsersTestRegistry.js +++ b/test/integration-tests/org/regularUsersTestRegistry.js @@ -64,7 +64,7 @@ describe('Testing regular user permissions for /api/registry/org/ endpoints with /* Negative Tests */ context('Negative Test', () => { it('regular user cannot update their username', async () => { - const newUsername = faker.datatype.uuid() + const newUsername = faker.string.uuid() const org = constants.nonSecretariatUserHeaders['CVE-API-ORG'] const user = constants.nonSecretariatUserHeaders['CVE-API-USER'] @@ -87,7 +87,7 @@ describe('Testing regular user permissions for /api/registry/org/ endpoints with }) }) it('regular user cannot update information of another user of the same organization', async () => { - const newUsername = faker.datatype.uuid() + const newUsername = faker.string.uuid() const org = constants.nonSecretariatUserHeaders['CVE-API-ORG'] const user2 = constants.nonSecretariatUserHeaders2['CVE-API-USER'] @@ -132,7 +132,7 @@ describe('Testing regular user permissions for /api/registry/org/ endpoints with it('regular users cannot update organization', async () => { const org1 = constants.nonSecretariatUserHeaders['CVE-API-ORG'] const user = constants.nonSecretariatUserHeaders['CVE-API-USER'] - const org2 = faker.datatype.uuid().slice(0, MAX_SHORTNAME_LENGTH) + const org2 = faker.string.uuid().slice(0, MAX_SHORTNAME_LENGTH) let previousBody await chai.request(app).get(`/api/registry/org/${org1}/user/${user}`) @@ -188,7 +188,7 @@ describe('Testing regular user permissions for /api/registry/org/ endpoints with }) it('regular users cannot use grant-role missing users to enumerate another organization', async () => { const org = constants.nonSecretariatUserHeaders3['CVE-API-ORG'] - const user = faker.datatype.uuid() + const user = faker.string.uuid() await chai.request(app) .post(`/api/registry/org/${org}/user/${user}/grant-role`) .set(constants.nonSecretariatUserHeaders) @@ -216,7 +216,7 @@ describe('Testing regular user permissions for /api/registry/org/ endpoints with }) it('regular users cannot use revoke-role missing users to enumerate another organization', async () => { const org = constants.nonSecretariatUserHeaders3['CVE-API-ORG'] - const user = faker.datatype.uuid() + const user = faker.string.uuid() await chai.request(app) .post(`/api/registry/org/${org}/user/${user}/revoke-role`) .set(constants.nonSecretariatUserHeaders) @@ -229,7 +229,7 @@ describe('Testing regular user permissions for /api/registry/org/ endpoints with }) }) it("regular user cannot update a user from an org that doesn't exist", async () => { - const org = faker.datatype.uuid().slice(0, MAX_SHORTNAME_LENGTH) + const org = faker.string.uuid().slice(0, MAX_SHORTNAME_LENGTH) const user = constants.nonSecretariatUserHeaders['CVE-API-USER'] await chai.request(app) .put(`/api/registry/org/${org}/user/${user}`) @@ -243,7 +243,7 @@ describe('Testing regular user permissions for /api/registry/org/ endpoints with }) it("regular user cannot update a user that doesn't exist ", async () => { const org = constants.nonSecretariatUserHeaders['CVE-API-ORG'] - const user = faker.datatype.uuid() + const user = faker.string.uuid() await chai.request(app) .put(`/api/registry/org/${org}/user/${user}`) .set(constants.nonSecretariatUserHeaders) @@ -268,7 +268,7 @@ describe('Testing regular user permissions for /api/registry/org/ endpoints with }) }) it("regular user cannot reset the secret of a user from an org that doesn't exist", async () => { - const org = faker.datatype.uuid().slice(0, MAX_SHORTNAME_LENGTH) + const org = faker.string.uuid().slice(0, MAX_SHORTNAME_LENGTH) const user = constants.nonSecretariatUserHeaders['CVE-API-USER'] await chai.request(app) .put(`/api/registry/org/${org}/user/${user}/reset_secret`) @@ -282,7 +282,7 @@ describe('Testing regular user permissions for /api/registry/org/ endpoints with }) it("regular user cannot reset the secret of a user that doesn't exist", async () => { const org = constants.nonSecretariatUserHeaders['CVE-API-ORG'] - const user = faker.datatype.uuid() + const user = faker.string.uuid() await chai.request(app) .put(`/api/registry/org/${org}/user/${user}/reset_secret`) .set(constants.nonSecretariatUserHeaders) @@ -295,7 +295,7 @@ describe('Testing regular user permissions for /api/registry/org/ endpoints with }) it('regular users cannot use reset-secret missing users to enumerate another organization', async () => { const org = constants.nonSecretariatUserHeaders3['CVE-API-ORG'] - const user = faker.datatype.uuid() + const user = faker.string.uuid() await chai.request(app) .put(`/api/registry/org/${org}/user/${user}/reset_secret`) .set(constants.nonSecretariatUserHeaders) @@ -326,7 +326,7 @@ describe('Testing regular user permissions for /api/registry/org/ endpoints with /* Negative Tests */ context('Negative Test', () => { it('regular user cannot create another user', async () => { - const newUsername = faker.datatype.uuid() + const newUsername = faker.string.uuid() const org = constants.nonSecretariatUserHeaders['CVE-API-ORG'] await chai.request(app) .post(`/api/registry/org/${org}/user`) @@ -374,7 +374,7 @@ describe('Testing regular user permissions for /api/registry/org/ endpoints with /* Negative Tests */ context('Negative Test', () => { it("regular users cannot view users of an organization that doesn't exist", async () => { - const org = faker.datatype.uuid().slice(0, MAX_SHORTNAME_LENGTH) + const org = faker.string.uuid().slice(0, MAX_SHORTNAME_LENGTH) await chai.request(app) .get(`/api/registry/org/${org}/users`) .set(constants.nonSecretariatUserHeaders) @@ -412,7 +412,7 @@ describe('Testing regular user permissions for /api/registry/org/ endpoints with }) it('regular users cannot use missing users to enumerate another organization', async () => { const org = constants.nonSecretariatUserHeaders3['CVE-API-ORG'] - const user = faker.datatype.uuid() + const user = faker.string.uuid() await chai.request(app) .get(`/api/registry/org/${org}/user/${user}`) .set(constants.nonSecretariatUserHeaders) @@ -425,7 +425,7 @@ describe('Testing regular user permissions for /api/registry/org/ endpoints with }) it("regular user cannot view user that doesn't exist", async () => { const org = constants.nonSecretariatUserHeaders['CVE-API-ORG'] - const user = faker.datatype.uuid() + const user = faker.string.uuid() await chai.request(app) .get(`/api/registry/org/${org}/user/${user}`) .set(constants.nonSecretariatUserHeaders) @@ -456,7 +456,7 @@ describe('Testing regular user permissions for /api/registry/org/ endpoints with }) }) it('regular user cannot update an organization', async () => { - const org = faker.datatype.uuid().slice(0, MAX_SHORTNAME_LENGTH) + const org = faker.string.uuid().slice(0, MAX_SHORTNAME_LENGTH) await chai.request(app) .put(`/api/registry/org/${org}`) .set(constants.nonSecretariatUserHeaders) @@ -519,7 +519,7 @@ describe('Testing regular user permissions for /api/registry/org/ endpoints with /* Negative Tests */ context('Negative Test', () => { it("regular users cannot view an organization they don't belong to", async () => { - const org = faker.datatype.uuid().slice(0, MAX_SHORTNAME_LENGTH) + const org = faker.string.uuid().slice(0, MAX_SHORTNAME_LENGTH) await chai.request(app) .get(`/api/registry/org/${org}`) .set(constants.nonSecretariatUserHeaders) diff --git a/test/unit-tests/cve-id/cveIdGetAllTest.js b/test/unit-tests/cve-id/cveIdGetAllTest.js index bd7228bbf..ec08e28bf 100644 --- a/test/unit-tests/cve-id/cveIdGetAllTest.js +++ b/test/unit-tests/cve-id/cveIdGetAllTest.js @@ -9,9 +9,9 @@ const OrgRepository = require('../../../src/repositories/orgRepository.js') const CveIdRepository = require('../../../src/repositories/cveIdRepository.js') const UserRepository = require('../../../src/repositories/userRepository.js') -const orgUUID = faker.datatype.uuid() -const orgUUID2 = faker.datatype.uuid() -const userUUID = faker.datatype.uuid() +const orgUUID = faker.string.uuid() +const orgUUID2 = faker.string.uuid() +const userUUID = faker.string.uuid() const stubOrg = { short_name: 'testOrg', diff --git a/test/unit-tests/cve/insertAdpTest.js b/test/unit-tests/cve/insertAdpTest.js index fd654a247..53818cc4d 100644 --- a/test/unit-tests/cve/insertAdpTest.js +++ b/test/unit-tests/cve/insertAdpTest.js @@ -16,7 +16,7 @@ const CveIdRepository = require('../../../src/repositories/cveIdRepository.js') const CveRepository = require('../../../src/repositories/cveRepository.js') const UserRepository = require('../../../src/repositories/userRepository.js') -const adpUUID = faker.datatype.uuid() +const adpUUID = faker.string.uuid() const stubAdpOrg = { short_name: 'adpOrg', @@ -32,7 +32,7 @@ const stubAdpOrg = { const stubAdpUser = { username: 'testAdpUser', org_UUID: adpUUID, - UUID: faker.datatype.uuid() + UUID: faker.string.uuid() } const stubCveId = { diff --git a/test/unit-tests/cve/updateCnaTest.js b/test/unit-tests/cve/updateCnaTest.js index b72cb009e..3a09be6e8 100644 --- a/test/unit-tests/cve/updateCnaTest.js +++ b/test/unit-tests/cve/updateCnaTest.js @@ -12,7 +12,7 @@ const error = new errors.CveControllerError() const constants = require('../../../src/constants').getConstants() const Cve = require('../../../src/model/cve.js') -const cnaUUID = faker.datatype.uuid() +const cnaUUID = faker.string.uuid() const stubCnaOrg = { short_name: 'CnaOrg', @@ -28,7 +28,7 @@ const stubCnaOrg = { const stubCnaUser = { username: 'testCnaUser', org_UUID: cnaUUID, - UUID: faker.datatype.uuid() + UUID: faker.string.uuid() } const stubCveId = { diff --git a/test/unit-tests/middleware/onlyAdpsTest.js b/test/unit-tests/middleware/onlyAdpsTest.js index 8b2cd30d4..6f3d585b6 100644 --- a/test/unit-tests/middleware/onlyAdpsTest.js +++ b/test/unit-tests/middleware/onlyAdpsTest.js @@ -13,7 +13,7 @@ const error = new errors.MiddlewareError() const stubAdpOrg = { short_name: 'adpOrg', name: 'test_adp', - UUID: faker.datatype.uuid(), + UUID: faker.string.uuid(), authority: { active_roles: [ 'ADP' @@ -24,7 +24,7 @@ const stubAdpOrg = { const stubCnaOrg = { short_name: 'cnaOrg', name: 'test_cna', - UUID: faker.datatype.uuid(), + UUID: faker.string.uuid(), authority: { active_roles: [ 'CNA' @@ -35,7 +35,7 @@ const stubCnaOrg = { const stubSecretariat = { short_name: 'secOrg', name: 'test_sec', - UUID: faker.datatype.uuid(), + UUID: faker.string.uuid(), authority: { active_roles: [ 'SECRETARIAT' diff --git a/test/unit-tests/middleware/onlyOrgWithPartnerRoleTest.js b/test/unit-tests/middleware/onlyOrgWithPartnerRoleTest.js index 34013b54c..5428a48b0 100644 --- a/test/unit-tests/middleware/onlyOrgWithPartnerRoleTest.js +++ b/test/unit-tests/middleware/onlyOrgWithPartnerRoleTest.js @@ -15,7 +15,7 @@ const error = new errors.MiddlewareError() const stubAdpOrg = { short_name: 'adpOrg', name: 'test_adp', - UUID: faker.datatype.uuid(), + UUID: faker.string.uuid(), authority: { active_roles: [ 'ADP' @@ -26,7 +26,7 @@ const stubAdpOrg = { const stubCnaOrg = { short_name: 'cnaOrg', name: 'test_cna', - UUID: faker.datatype.uuid(), + UUID: faker.string.uuid(), authority: { active_roles: [ 'CNA' @@ -37,7 +37,7 @@ const stubCnaOrg = { const stubBulkDownloadOrg = { short_name: 'bdOrg', name: 'test_bd', - UUID: faker.datatype.uuid(), + UUID: faker.string.uuid(), authority: { active_roles: [ 'BULK_DOWNLOAD' @@ -48,7 +48,7 @@ const stubBulkDownloadOrg = { const stubOrgNoRole = { short_name: 'NoRole', name: 'test_org', - UUID: faker.datatype.uuid(), + UUID: faker.string.uuid(), authority: { active_roles: [] } @@ -57,7 +57,7 @@ const stubOrgNoRole = { const stubSecretariat = { short_name: 'secOrg', name: 'test_sec', - UUID: faker.datatype.uuid(), + UUID: faker.string.uuid(), authority: { active_roles: [ 'SECRETARIAT' diff --git a/test/unit-tests/org/orgCreateADPTest.js b/test/unit-tests/org/orgCreateADPTest.js index e982cd20d..61c6f6d7f 100644 --- a/test/unit-tests/org/orgCreateADPTest.js +++ b/test/unit-tests/org/orgCreateADPTest.js @@ -87,7 +87,7 @@ describe('Testing creating orgs with the ADP role', () => { it('Should return newly created org with id_quota of 0 and ADP role', async () => { const req = { ctx: { - uuid: faker.datatype.uuid(), + uuid: faker.string.uuid(), repositories: { getBaseOrgRepository, getBaseUserRepository diff --git a/test/unit-tests/org/orgCreateTest.js b/test/unit-tests/org/orgCreateTest.js index a2d28ff7a..e8ec547f3 100644 --- a/test/unit-tests/org/orgCreateTest.js +++ b/test/unit-tests/org/orgCreateTest.js @@ -149,7 +149,7 @@ describe('Testing the ORG_CREATE_SINGLE controller', () => { it('Should fail if a UUID is provided in the request body', async () => { const req = { ctx: { - uuid: faker.datatype.uuid(), + uuid: faker.string.uuid(), repositories: { getOrgRepository, getBaseOrgRepository, getUserRepository, getBaseUserRepository }, body: orgFixtures.existentOrg // This fixture includes a UUID } @@ -175,7 +175,7 @@ describe('Testing the ORG_CREATE_SINGLE controller', () => { const req = { ctx: { - uuid: faker.datatype.uuid(), + uuid: faker.string.uuid(), repositories: { getOrgRepository, getBaseOrgRepository, getUserRepository, getBaseUserRepository }, body: testOrgPayload } @@ -218,7 +218,7 @@ describe('Testing the ORG_CREATE_SINGLE controller', () => { const req = { ctx: { - uuid: faker.datatype.uuid(), + uuid: faker.string.uuid(), org: 'test_secretariat_org', user: 'test_secretariat_user', repositories: { getOrgRepository, getUserRepository, getBaseOrgRepository, getBaseUserRepository }, @@ -245,7 +245,7 @@ describe('Testing the ORG_CREATE_SINGLE controller', () => { const req = { ctx: { - uuid: faker.datatype.uuid(), + uuid: faker.string.uuid(), org: 'test_secretariat_org', user: 'test_secretariat_user', repositories: { getOrgRepository, getUserRepository, getBaseOrgRepository, getBaseUserRepository }, @@ -274,7 +274,7 @@ describe('Testing the ORG_CREATE_SINGLE controller', () => { const req = { ctx: { - uuid: faker.datatype.uuid(), + uuid: faker.string.uuid(), org: 'test_secretariat_org', user: 'test_secretariat_user', repositories: { getOrgRepository, getUserRepository, getBaseOrgRepository, getBaseUserRepository }, @@ -306,7 +306,7 @@ describe('Testing the ORG_CREATE_SINGLE controller', () => { const req = { ctx: { - uuid: faker.datatype.uuid(), + uuid: faker.string.uuid(), org: 'test_secretariat_org', user: 'test_secretariat_user', repositories: { getOrgRepository, getUserRepository, getBaseOrgRepository, getBaseUserRepository }, @@ -335,7 +335,7 @@ describe('Testing the ORG_CREATE_SINGLE controller', () => { const req = { ctx: { - uuid: faker.datatype.uuid(), + uuid: faker.string.uuid(), org: 'test_secretariat_org', user: 'test_secretariat_user', repositories: { getOrgRepository, getUserRepository, getBaseOrgRepository, getBaseUserRepository }, @@ -360,7 +360,7 @@ describe('Testing the ORG_CREATE_SINGLE controller', () => { const req = { ctx: { - uuid: faker.datatype.uuid(), + uuid: faker.string.uuid(), org: 'test_secretariat_org', user: 'test_secretariat_user', repositories: { getOrgRepository, getUserRepository, getBaseOrgRepository, getBaseUserRepository }, diff --git a/test/unit-tests/org/orgGetSingleTest.js b/test/unit-tests/org/orgGetSingleTest.js index e3e8ead08..8aa7296f5 100644 --- a/test/unit-tests/org/orgGetSingleTest.js +++ b/test/unit-tests/org/orgGetSingleTest.js @@ -66,7 +66,7 @@ describe('Testing the GET /org/:identifier endpoint in Org Controller', () => { req = { ctx: { org: orgFixtures.secretariatOrg.short_name, - uuid: faker.datatype.uuid(), + uuid: faker.string.uuid(), params: { identifier: orgFixtures.targetOrg.short_name }, diff --git a/test/unit-tests/user/userCreateTest.js b/test/unit-tests/user/userCreateTest.js index 281539f94..b51052c0f 100644 --- a/test/unit-tests/user/userCreateTest.js +++ b/test/unit-tests/user/userCreateTest.js @@ -14,8 +14,8 @@ const BaseUserRepository = require('../../../src/repositories/baseUserRepository const BaseUser = require('../../../src/model/baseuser.js') const UserRepository = require('../../../src/repositories/userRepository.js') -const stubOrgUUID = faker.datatype.uuid() -const stubUserUUID = faker.datatype.uuid() +const stubOrgUUID = faker.string.uuid() +const stubUserUUID = faker.string.uuid() const stubOrg = { short_name: 'stubOrg', diff --git a/test/unit-tests/user/userGetSingleTest.js b/test/unit-tests/user/userGetSingleTest.js index 88a289dc8..de63ef496 100644 --- a/test/unit-tests/user/userGetSingleTest.js +++ b/test/unit-tests/user/userGetSingleTest.js @@ -50,7 +50,7 @@ describe('Testing the GET /org/:shortname/user/:username endpoint in Org Control const req = { ctx: { - uuid: faker.datatype.uuid(), + uuid: faker.string.uuid(), org: userFixtures.secretariatHeader['CVE-API-ORG'], params: { shortname: userFixtures.nonExistentOrg.short_name, @@ -78,7 +78,7 @@ describe('Testing the GET /org/:shortname/user/:username endpoint in Org Control const req = { ctx: { - uuid: faker.datatype.uuid(), + uuid: faker.string.uuid(), org: userFixtures.secretariatHeader['CVE-API-ORG'], params: { shortname: userFixtures.existentOrg.short_name, @@ -105,7 +105,7 @@ describe('Testing the GET /org/:shortname/user/:username endpoint in Org Control const req = { ctx: { - uuid: faker.datatype.uuid(), + uuid: faker.string.uuid(), org: userFixtures.orgHeader.short_name, params: { shortname: userFixtures.owningOrg.short_name, @@ -146,7 +146,7 @@ describe('Testing the GET /org/:shortname/user/:username endpoint in Org Control const req = { ctx: { - uuid: faker.datatype.uuid(), + uuid: faker.string.uuid(), org: userFixtures.secretariatHeader['CVE-API-ORG'], params: { shortname: userFixtures.existentOrg.short_name, @@ -189,7 +189,7 @@ describe('Testing the GET /org/:shortname/user/:username endpoint in Org Control const req = { ctx: { authenticated: true, - uuid: faker.datatype.uuid(), + uuid: faker.string.uuid(), org: userFixtures.owningOrg.short_name, orgUUID: userFixtures.owningOrg.UUID, userUUID: userFixtures.existentUserDummy.UUID, diff --git a/test/unit-tests/user/userResetSecretTest.js b/test/unit-tests/user/userResetSecretTest.js index a0e500bb8..e3952cf86 100644 --- a/test/unit-tests/user/userResetSecretTest.js +++ b/test/unit-tests/user/userResetSecretTest.js @@ -81,7 +81,7 @@ describe('Testing the PUT /org/:shortname/user/:username/reset_secret endpoint', const req = { ctx: { - uuid: faker.datatype.uuid(), + uuid: faker.string.uuid(), org: 'secretariat_org', user: 'secretariat_user', repositories: { getOrgRepository, getUserRepository, getBaseUserRepository, getBaseOrgRepository }, @@ -111,7 +111,7 @@ describe('Testing the PUT /org/:shortname/user/:username/reset_secret endpoint', const req = { ctx: { - uuid: faker.datatype.uuid(), + uuid: faker.string.uuid(), org: 'secretariat_org', user: 'secretariat_user', repositories: { getOrgRepository, getUserRepository, getBaseUserRepository, getBaseOrgRepository }, @@ -143,7 +143,7 @@ describe('Testing the PUT /org/:shortname/user/:username/reset_secret endpoint', const req = { ctx: { - uuid: faker.datatype.uuid(), + uuid: faker.string.uuid(), org: userFixtures.owningOrg.short_name, user: 'some_user', repositories: { getOrgRepository, getUserRepository, getBaseUserRepository, getBaseOrgRepository }, @@ -179,7 +179,7 @@ describe('Testing the PUT /org/:shortname/user/:username/reset_secret endpoint', const req = { ctx: { - uuid: faker.datatype.uuid(), + uuid: faker.string.uuid(), org: userFixtures.existentOrgDummy.short_name, orgUUID: userFixtures.existentOrgDummy.UUID, user: userFixtures.userA.username, @@ -220,7 +220,7 @@ describe('Testing the PUT /org/:shortname/user/:username/reset_secret endpoint', const req = { ctx: { - uuid: faker.datatype.uuid(), + uuid: faker.string.uuid(), org: userFixtures.existentOrgDummy.short_name, orgUUID: userFixtures.existentOrgDummy.UUID, user: userFixtures.userA.username, @@ -254,7 +254,7 @@ describe('Testing the PUT /org/:shortname/user/:username/reset_secret endpoint', const req = { ctx: { - uuid: faker.datatype.uuid(), + uuid: faker.string.uuid(), org: 'secretariat_org', user: 'secretariat_user', repositories: { getBaseOrgRepository, getBaseUserRepository }, @@ -284,7 +284,7 @@ describe('Testing the PUT /org/:shortname/user/:username/reset_secret endpoint', const req = { ctx: { - uuid: faker.datatype.uuid(), + uuid: faker.string.uuid(), org: userFixtures.existentOrgDummy.short_name, orgUUID: userFixtures.existentOrgDummy.UUID, user: userFixtures.userD.username, From 859a0cb36ee144304fad846f603f0272c6eba267 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 3 Sep 2026 00:33:28 +0000 Subject: [PATCH 16/27] Bump qs and express Bumps [qs](https://github.com/ljharb/qs) to 6.16.0 and updates ancestor dependency [express](https://github.com/expressjs/express). These dependencies need to be updated together. Updates `qs` from 6.15.2 to 6.16.0 - [Changelog](https://github.com/ljharb/qs/blob/main/CHANGELOG.md) - [Commits](https://github.com/ljharb/qs/compare/v6.15.2...v6.16.0) Updates `express` from 4.22.2 to 5.2.1 - [Release notes](https://github.com/expressjs/express/releases) - [Changelog](https://github.com/expressjs/express/blob/master/History.md) - [Commits](https://github.com/expressjs/express/compare/v4.22.2...v5.2.1) --- updated-dependencies: - dependency-name: qs dependency-version: 6.16.0 dependency-type: indirect - dependency-name: express dependency-version: 5.2.1 dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- package-lock.json | 593 +++++++++++++++++++++++++++------------------- package.json | 2 +- 2 files changed, 352 insertions(+), 243 deletions(-) diff --git a/package-lock.json b/package-lock.json index 806d1c1e5..9f73e8329 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,7 +17,7 @@ "cors": "^2.8.5", "crypto-random-string": "^3.3.1", "dotenv": "^5.0.1", - "express": "^4.22.2", + "express": "^5.2.1", "express-jsonschema": "^1.1.6", "express-rate-limit": "^6.5.2", "express-validator": "^6.14.2", @@ -1015,18 +1015,43 @@ "license": "MIT" }, "node_modules/accepts": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", - "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", "license": "MIT", "dependencies": { - "mime-types": "~2.1.34", - "negotiator": "0.6.3" + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" }, "engines": { "node": ">= 0.6" } }, + "node_modules/accepts/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/accepts/node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/acorn": { "version": "8.16.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", @@ -1226,12 +1251,6 @@ "node": ">=8" } }, - "node_modules/array-flatten": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", - "license": "MIT" - }, "node_modules/array-includes": { "version": "3.1.9", "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz", @@ -1483,44 +1502,42 @@ } }, "node_modules/body-parser": { - "version": "1.20.6", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.6.tgz", - "integrity": "sha512-p5tAzS57i5MV9fZFDj9LeIiTZEufbSe2eDozP+ElheSUq1m74CRq1jI4mYNDdVs9vQztXFLuk/Gd6BWTdwRJ5g==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz", + "integrity": "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==", "license": "MIT", "dependencies": { - "bytes": "~3.1.2", - "content-type": "~1.0.5", - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "~1.2.0", - "http-errors": "~2.0.1", - "iconv-lite": "~0.4.24", - "on-finished": "~2.4.1", - "qs": "~6.15.1", - "raw-body": "~2.5.3", - "type-is": "~1.6.18", - "unpipe": "~1.0.0" + "bytes": "^3.1.2", + "content-type": "^2.0.0", + "debug": "^4.4.3", + "http-errors": "^2.0.1", + "iconv-lite": "^0.7.2", + "on-finished": "^2.4.1", + "qs": "^6.15.2", + "raw-body": "^3.0.2", + "type-is": "^2.1.0" }, "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/body-parser/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "node_modules/body-parser/node_modules/content-type": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz", + "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==", "license": "MIT", - "dependencies": { - "ms": "2.0.0" + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/body-parser/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "license": "MIT" - }, "node_modules/brace-expansion": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz", @@ -2054,15 +2071,16 @@ } }, "node_modules/content-disposition": { - "version": "0.5.4", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", - "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", + "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==", "license": "MIT", - "dependencies": { - "safe-buffer": "5.2.1" - }, "engines": { - "node": ">= 0.6" + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/content-type": { @@ -2091,10 +2109,13 @@ } }, "node_modules/cookie-signature": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", - "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", - "license": "MIT" + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } }, "node_modules/cookiejar": { "version": "2.1.4", @@ -2418,16 +2439,6 @@ "dev": true, "license": "MIT" }, - "node_modules/destroy": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", - "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", - "license": "MIT", - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, "node_modules/detect-file": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/detect-file/-/detect-file-1.0.0.tgz", @@ -3336,45 +3347,42 @@ } }, "node_modules/express": { - "version": "4.22.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.22.2.tgz", - "integrity": "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q==", - "license": "MIT", - "dependencies": { - "accepts": "~1.3.8", - "array-flatten": "1.1.1", - "body-parser": "~1.20.5", - "content-disposition": "~0.5.4", - "content-type": "~1.0.4", - "cookie": "~0.7.1", - "cookie-signature": "~1.0.6", - "debug": "2.6.9", - "depd": "2.0.0", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "finalhandler": "~1.3.1", - "fresh": "~0.5.2", - "http-errors": "~2.0.0", - "merge-descriptors": "1.0.3", - "methods": "~1.1.2", - "on-finished": "~2.4.1", - "parseurl": "~1.3.3", - "path-to-regexp": "~0.1.12", - "proxy-addr": "~2.0.7", - "qs": "~6.15.1", - "range-parser": "~1.2.1", - "safe-buffer": "5.2.1", - "send": "~0.19.0", - "serve-static": "~1.16.2", - "setprototypeof": "1.2.0", - "statuses": "~2.0.1", - "type-is": "~1.6.18", - "utils-merge": "1.0.1", - "vary": "~1.1.2" + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" }, "engines": { - "node": ">= 0.10.0" + "node": ">= 18" }, "funding": { "type": "opencollective", @@ -3417,20 +3425,30 @@ "node": ">= 8.0.0" } }, - "node_modules/express/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "node_modules/express/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", "license": "MIT", - "dependencies": { - "ms": "2.0.0" + "engines": { + "node": ">= 0.6" } }, - "node_modules/express/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "license": "MIT" + "node_modules/express/node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } }, "node_modules/fast-deep-equal": { "version": "3.1.3", @@ -3527,38 +3545,26 @@ } }, "node_modules/finalhandler": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", - "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", "license": "MIT", "dependencies": { - "debug": "2.6.9", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "on-finished": "~2.4.1", - "parseurl": "~1.3.3", - "statuses": "~2.0.2", - "unpipe": "~1.0.0" + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" }, "engines": { - "node": ">= 0.8" - } - }, - "node_modules/finalhandler/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "license": "MIT", - "dependencies": { - "ms": "2.0.0" + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/finalhandler/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "license": "MIT" - }, "node_modules/find-cache-dir": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz", @@ -3731,12 +3737,12 @@ } }, "node_modules/fresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", "license": "MIT", "engines": { - "node": ">= 0.6" + "node": ">= 0.8" } }, "node_modules/fromentries": { @@ -4299,15 +4305,19 @@ } }, "node_modules/iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz", + "integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==", "license": "MIT", "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" + "safer-buffer": ">= 2.1.2 < 3.0.0" }, "engines": { "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/ignore": { @@ -4748,6 +4758,12 @@ "node": ">=8" } }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, "node_modules/is-regex": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", @@ -5517,12 +5533,16 @@ } }, "node_modules/media-typer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.1.tgz", + "integrity": "sha512-yz3xRaG20c6/BOzvYoDaGtPmGscs7YivItZEEqe6GbwNfHuxu9YNmvnEkMzKldAGY4/80pRcQRZSEnhquk9XuQ==", "license": "MIT", "engines": { - "node": ">= 0.6" + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/memory-pager": { @@ -5532,10 +5552,13 @@ "license": "MIT" }, "node_modules/merge-descriptors": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", - "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", "license": "MIT", + "engines": { + "node": ">=18" + }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } @@ -5544,6 +5567,7 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -5563,22 +5587,11 @@ "node": ">=8.6" } }, - "node_modules/mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", - "license": "MIT", - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/mime-db": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -5588,6 +5601,7 @@ "version": "2.1.35", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, "license": "MIT", "dependencies": { "mime-db": "1.52.0" @@ -5962,12 +5976,32 @@ "license": "MIT" }, "node_modules/negotiator": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.1.0.tgz", + "integrity": "sha512-NMPBRMJgiQHjbd8phG3Vebdx4kZ1H121rbl5IkMqeOsahptB9BKo/d7oJ3zTXqTgagn2bWlNSXkh0QUGM31RYg==", + "license": "MIT", + "dependencies": { + "content-type": "^2.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/negotiator/node_modules/content-type": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz", + "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==", "license": "MIT", "engines": { - "node": ">= 0.6" + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/nise": { @@ -6763,10 +6797,14 @@ "license": "MIT" }, "node_modules/path-to-regexp": { - "version": "0.1.13", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz", - "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==", - "license": "MIT" + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", + "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } }, "node_modules/path-type": { "version": "4.0.0", @@ -7206,12 +7244,13 @@ } }, "node_modules/qs": { - "version": "6.15.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz", - "integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==", + "version": "6.16.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.16.0.tgz", + "integrity": "sha512-h6fhOIaRrID2CbEY2fqs+7t+UXZo+MLAnU5gRIq85uFtdiUPCdsApMlHhXogKVM4HM2DVbIjGNTTYH2OcmP1vA==", "license": "BSD-3-Clause", "dependencies": { - "side-channel": "^1.1.0" + "es-define-property": "^1.0.1", + "side-channel": "^1.1.1" }, "engines": { "node": ">=0.6" @@ -7249,27 +7288,31 @@ "license": "MIT" }, "node_modules/range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.3.0.tgz", + "integrity": "sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw==", "license": "MIT", "engines": { "node": ">= 0.6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/raw-body": { - "version": "2.5.3", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", - "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", "license": "MIT", "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", - "iconv-lite": "~0.4.24", + "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" }, "engines": { - "node": ">= 0.8" + "node": ">= 0.10" } }, "node_modules/react-is": { @@ -7807,6 +7850,22 @@ "node": "*" } }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -7941,43 +8000,55 @@ "license": "MIT" }, "node_modules/send": { - "version": "0.19.2", - "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", - "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", "license": "MIT", "dependencies": { - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "fresh": "~0.5.2", - "http-errors": "~2.0.1", - "mime": "1.6.0", - "ms": "2.1.3", - "on-finished": "~2.4.1", - "range-parser": "~1.2.1", - "statuses": "~2.0.2" + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" }, "engines": { - "node": ">= 0.8.0" + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/send/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "node_modules/send/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", "license": "MIT", - "dependencies": { - "ms": "2.0.0" + "engines": { + "node": ">= 0.6" } }, - "node_modules/send/node_modules/debug/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "license": "MIT" + "node_modules/send/node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } }, "node_modules/serialize-javascript": { "version": "7.0.5", @@ -7990,18 +8061,22 @@ } }, "node_modules/serve-static": { - "version": "1.16.3", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", - "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", "license": "MIT", "dependencies": { - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "parseurl": "~1.3.3", - "send": "~0.19.1" + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" }, "engines": { - "node": ">= 0.8.0" + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/set-blocking": { @@ -8096,14 +8171,14 @@ "license": "MIT" }, "node_modules/side-channel": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", - "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", "license": "MIT", "dependencies": { "es-errors": "^1.3.0", - "object-inspect": "^1.13.3", - "side-channel-list": "^1.0.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" }, @@ -8115,13 +8190,13 @@ } }, "node_modules/side-channel-list": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", - "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", "license": "MIT", "dependencies": { "es-errors": "^1.3.0", - "object-inspect": "^1.13.3" + "object-inspect": "^1.13.4" }, "engines": { "node": ">= 0.4" @@ -9424,18 +9499,61 @@ } }, "node_modules/type-is": { - "version": "1.6.18", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", - "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz", + "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==", "license": "MIT", "dependencies": { - "media-typer": "0.3.0", - "mime-types": "~2.1.24" + "content-type": "^2.0.0", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/type-is/node_modules/content-type": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz", + "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==", + "license": "MIT", + "engines": { + "node": ">=18" }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/type-is/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", "engines": { "node": ">= 0.6" } }, + "node_modules/type-is/node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/typed-array-buffer": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", @@ -9621,15 +9739,6 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "license": "MIT" }, - "node_modules/utils-merge": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", - "license": "MIT", - "engines": { - "node": ">= 0.4.0" - } - }, "node_modules/uuid": { "version": "14.0.0", "resolved": "https://registry.npmjs.org/uuid/-/uuid-14.0.0.tgz", diff --git a/package.json b/package.json index 4381afffb..f7553ff9a 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,7 @@ "cors": "^2.8.5", "crypto-random-string": "^3.3.1", "dotenv": "^5.0.1", - "express": "^4.22.2", + "express": "^5.2.1", "express-jsonschema": "^1.1.6", "express-rate-limit": "^6.5.2", "express-validator": "^6.14.2", From 3850b1515fdcc53148a51e26998ebd3c8c777385 Mon Sep 17 00:00:00 2001 From: David T Rocca Date: Tue, 8 Sep 2026 13:37:34 -0400 Subject: [PATCH 17/27] Fixing version upgrades --- .../cve-id.controller/cve-id.middleware.js | 2 +- .../cve.controller/cve.middleware.js | 2 +- .../org.controller/org.middleware.js | 2 +- src/index.js | 3 + src/utils/utils.js | 24 +++++++ .../middleware/validatedQueryContextTest.js | 62 +++++++++++++++++++ 6 files changed, 92 insertions(+), 3 deletions(-) create mode 100644 test/integration-tests/middleware/validatedQueryContextTest.js diff --git a/src/controller/cve-id.controller/cve-id.middleware.js b/src/controller/cve-id.controller/cve-id.middleware.js index e81545bb8..ee6247f7f 100644 --- a/src/controller/cve-id.controller/cve-id.middleware.js +++ b/src/controller/cve-id.controller/cve-id.middleware.js @@ -4,7 +4,7 @@ const error = new errors.CveIdControllerError() const utils = require('../../utils/utils') function parseGetParams (req, res, next) { - utils.reqCtxMapping(req, 'query', ['page', 'state', 'cve_id_year', 'time_reserved.lt', 'time_reserved.gt', 'time_modified.lt', 'time_modified.gt']) + utils.reqCtxValidatedQueryMapping(req, ['page', 'state', 'cve_id_year', 'time_reserved.lt', 'time_reserved.gt', 'time_modified.lt', 'time_modified.gt']) utils.reqCtxMapping(req, 'params', ['id']) next() } diff --git a/src/controller/cve.controller/cve.middleware.js b/src/controller/cve.controller/cve.middleware.js index 576bdad3f..ea645e666 100644 --- a/src/controller/cve.controller/cve.middleware.js +++ b/src/controller/cve.controller/cve.middleware.js @@ -21,7 +21,7 @@ function parsePostParams (req, res, next) { } function parseGetParams (req, res, next) { - utils.reqCtxMapping(req, 'query', ['page', 'time_modified.lt', 'time_modified.gt', 'time_created.lt', 'time_created.gt', 'state', 'count_only', 'assigner_short_name', 'assigner', 'cna_modified', 'adp_short_name', 'next_page', 'previous_page', 'limit']) + utils.reqCtxValidatedQueryMapping(req, ['page', 'time_modified.lt', 'time_modified.gt', 'time_created.lt', 'time_created.gt', 'state', 'count_only', 'assigner_short_name', 'assigner', 'cna_modified', 'adp_short_name', 'next_page', 'previous_page', 'limit']) utils.reqCtxMapping(req, 'params', ['id']) next() } diff --git a/src/controller/org.controller/org.middleware.js b/src/controller/org.controller/org.middleware.js index fdff50735..bcd7c3b05 100644 --- a/src/controller/org.controller/org.middleware.js +++ b/src/controller/org.controller/org.middleware.js @@ -149,7 +149,7 @@ function parsePutParams (req, res, next) { ...QUERY_PARAMETERS.registryOnly, ...QUERY_PARAMETERS.userParams ] - utils.reqCtxMapping(req, 'query', allQueryParams) + utils.reqCtxValidatedQueryMapping(req, allQueryParams) utils.reqCtxMapping(req, 'params', ['shortname', 'username', 'identifier']) next() } diff --git a/src/index.js b/src/index.js index fa8ddf843..c58a3fdbf 100644 --- a/src/index.js +++ b/src/index.js @@ -3,6 +3,9 @@ const cors = require('cors') const config = require('config') const express = require('express') const app = express() +// Express 5 defaults to the simple query parser. Keep the qs-based parser used +// by Express 4 so existing bracket/nested query parameters retain their API. +app.set('query parser', 'extended') const helmet = require('helmet') const mongoose = require('mongoose') const morgan = require('morgan') diff --git a/src/utils/utils.js b/src/utils/utils.js index c1c66cc71..fdc130942 100644 --- a/src/utils/utils.js +++ b/src/utils/utils.js @@ -8,6 +8,7 @@ const getConstants = require('../constants').getConstants const _ = require('lodash') const { DateTime } = require('luxon') const BaseOrgRepository = require('../repositories/baseOrgRepository') +const { matchedData } = require('express-validator') async function getOrgUUID (shortName, useRegistry = false, options = {}) { const ModelToQuery = useRegistry ? BaseOrg : Org @@ -177,6 +178,28 @@ function reqCtxMapping (req, keyType, keys) { } } +// Express 5 exposes req.query as a getter which reparses the URL on every +// access. express-validator sanitizers therefore cannot persist changes on +// req.query. Copy the validator context instead, while retaining the flat +// dotted keys expected by the legacy controllers. +function reqCtxValidatedQueryMapping (req, keys) { + if (!('query' in req.ctx)) { + req.ctx.query = {} + } + + const validatedQuery = matchedData(req, { + locations: ['query'], + includeOptionals: true + }) + + keys.forEach(key => { + const value = _.get(validatedQuery, key) + if (value !== undefined) { + req.ctx.query[key] = value + } + }) +} + // Return true if boolean is 0, true, or yes, with any mix of casing // Please note that this function does NOT evaluate "undefined" as false. - A tired developer who lost way too much time to this. function booleanIsTrue (val) { @@ -334,6 +357,7 @@ module.exports = { getUserUUID, getUserFullName, reqCtxMapping, + reqCtxValidatedQueryMapping, booleanIsTrue, toDate, convertDatesToISO diff --git a/test/integration-tests/middleware/validatedQueryContextTest.js b/test/integration-tests/middleware/validatedQueryContextTest.js new file mode 100644 index 000000000..a96b52702 --- /dev/null +++ b/test/integration-tests/middleware/validatedQueryContextTest.js @@ -0,0 +1,62 @@ +const chai = require('chai') +chai.use(require('chai-http')) +const expect = chai.expect +const express = require('express') +const { query } = require('express-validator') + +const utils = require('../../../src/utils/utils') +const toDate = require('../../../src/utils/utils').toDate + +describe('Validated query request context', () => { + it('retains a sanitized date when Express 5 reparses req.query', async () => { + const app = express() + app.set('query parser', 'extended') + app.use((req, res, next) => { + req.ctx = {} + next() + }) + app.get('/date', + query('time_modified.gt').customSanitizer(toDate), + (req, res) => { + utils.reqCtxValidatedQueryMapping(req, ['time_modified.gt']) + return res.status(200).json({ + isDate: req.ctx.query['time_modified.gt'] instanceof Date, + value: req.ctx.query['time_modified.gt'].toISOString() + }) + }) + + const response = await chai.request(app) + .get('/date?time_modified.gt=2022-01-01T00:00:00Z') + + expect(response).to.have.status(200) + expect(response.body).to.deep.equal({ + isDate: true, + value: '2022-01-01T00:00:00.000Z' + }) + }) + + it('retains validator-produced role arrays for organization updates', async () => { + const app = express() + app.set('query parser', 'extended') + app.use((req, res, next) => { + req.ctx = {} + next() + }) + app.put('/roles', + query('active_roles.add').toArray(), + query('active_roles.remove').toArray(), + (req, res) => { + utils.reqCtxValidatedQueryMapping(req, ['active_roles.add', 'active_roles.remove']) + return res.status(200).json(req.ctx.query) + }) + + const response = await chai.request(app) + .put('/roles?active_roles.add=ADMIN&active_roles.remove=CNA') + + expect(response).to.have.status(200) + expect(response.body).to.deep.equal({ + 'active_roles.add': ['ADMIN'], + 'active_roles.remove': ['CNA'] + }) + }) +}) From b7e45a61df482a081f692db983aa34c89b400b04 Mon Sep 17 00:00:00 2001 From: David T Rocca Date: Tue, 8 Sep 2026 13:53:18 -0400 Subject: [PATCH 18/27] More fixes --- src/utils/utils.js | 5 ++++- .../middleware/validatedQueryContextTest.js | 18 ++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/src/utils/utils.js b/src/utils/utils.js index fdc130942..718432bc1 100644 --- a/src/utils/utils.js +++ b/src/utils/utils.js @@ -193,7 +193,10 @@ function reqCtxValidatedQueryMapping (req, keys) { }) keys.forEach(key => { - const value = _.get(validatedQuery, key) + // Direct controller callers (including unit tests) do not always run the + // route validation chains. Retain their allowed raw query value when no + // validator context exists for the key. + const value = _.get(validatedQuery, key) ?? req.query[key] if (value !== undefined) { req.ctx.query[key] = value } diff --git a/test/integration-tests/middleware/validatedQueryContextTest.js b/test/integration-tests/middleware/validatedQueryContextTest.js index a96b52702..c818fe68d 100644 --- a/test/integration-tests/middleware/validatedQueryContextTest.js +++ b/test/integration-tests/middleware/validatedQueryContextTest.js @@ -59,4 +59,22 @@ describe('Validated query request context', () => { 'active_roles.remove': ['CNA'] }) }) + + it('retains allowed raw query values when no route validators ran', async () => { + const app = express() + app.use((req, res, next) => { + req.ctx = {} + next() + }) + app.put('/direct-controller', (req, res) => { + utils.reqCtxValidatedQueryMapping(req, ['new_short_name']) + return res.status(200).json(req.ctx.query) + }) + + const response = await chai.request(app) + .put('/direct-controller?new_short_name=renamed-org') + + expect(response).to.have.status(200) + expect(response.body).to.deep.equal({ new_short_name: 'renamed-org' }) + }) }) From aa95b8eeb3926b59b34aff92180aa655c177d40e Mon Sep 17 00:00:00 2001 From: James Dalphond Date: Wed, 2 Sep 2026 09:46:06 -0400 Subject: [PATCH 19/27] 2) Review Object Controller Integration Tests --- api-docs/openapi.json | 2 +- schemas/registry-org/BaseOrg.json | 3 --- schemas/registry-org/CNAOrg.json | 3 --- schemas/registry-org/RootOrg.json | 3 --- .../create-registry-org-request.json | 3 --- .../create-registry-org-response.json | 3 --- .../get-registry-org-response.json | 3 --- .../list-registry-orgs-response.json | 3 --- .../update-registry-org-request.json | 3 --- .../update-registry-org-response.json | 3 --- .../org.controller/org.middleware.js | 1 - src/controller/registry.controller/index.js | 1 - src/model/baseorg.js | 3 +-- src/repositories/baseOrgRepository.js | 1 - src/scripts/migrate.js | 4 +--- ...-services-registry.postman_collection.json | 6 +++--- src/scripts/test_data/testData.js | 16 --------------- test/integration-tests/constants.js | 9 +++------ .../registry-org/verifyDeepRemoveEmpty.js | 20 +++++++++++++++++-- 19 files changed, 27 insertions(+), 63 deletions(-) diff --git a/api-docs/openapi.json b/api-docs/openapi.json index d5034c968..834e86fda 100644 --- a/api-docs/openapi.json +++ b/api-docs/openapi.json @@ -3906,7 +3906,7 @@ "Registry Organization" ], "summary": "Updates information about the organization specified by short name (accessible to Secretariat or same-organization Admin)", - "description": "

Access Control

User must belong to an organization with the Secretariat role or be an Admin of the requested organization.

With Joint Approval required for the following fields:

Expected Behavior

This endpoint expects a full organization object in the request body.

Secretariat: Updates any organization's information

Organization Admin: Requests changes to its organization's information

  • short_name
  • long_name
  • authority
  • aliases
  • oversees
  • top_level_root
  • charter_or_scope
  • product_list
  • disclosure_policy
  • contact_info.websites
  • contact_info.emails
  • contact_info.phone
  • partner_role_type
  • partner_country
  • advisory_locations
  • advisory_location_require_credentials
  • vulnerability_advisory_location_for_web_scraping
  • industry
  • tl_root_start_date
  • is_cna_discussion_list
", + "description": "

Access Control

User must belong to an organization with the Secretariat role or be an Admin of the requested organization.

With Joint Approval required for the following fields:

Expected Behavior

This endpoint expects a full organization object in the request body.

Secretariat: Updates any organization's information

Organization Admin: Requests changes to its organization's information

  • short_name
  • long_name
  • authority
  • aliases
  • oversees
  • top_level_root
  • charter_or_scope
  • product_list
  • disclosure_policy
  • contact_info.websites
  • contact_info.emails
  • partner_role_type
  • partner_country
  • advisory_locations
  • advisory_location_require_credentials
  • vulnerability_advisory_location_for_web_scraping
  • industry
  • tl_root_start_date
  • is_cna_discussion_list
", "operationId": "registryOrgUpdateSingle", "parameters": [ { diff --git a/schemas/registry-org/BaseOrg.json b/schemas/registry-org/BaseOrg.json index aab61eba1..3b2801d62 100644 --- a/schemas/registry-org/BaseOrg.json +++ b/schemas/registry-org/BaseOrg.json @@ -157,9 +157,6 @@ "format": "email" }, "uniqueItems": true - }, - "phone": { - "type": "string" } }, "additionalProperties": false diff --git a/schemas/registry-org/CNAOrg.json b/schemas/registry-org/CNAOrg.json index 8e32aa932..5f0033637 100644 --- a/schemas/registry-org/CNAOrg.json +++ b/schemas/registry-org/CNAOrg.json @@ -60,9 +60,6 @@ "format": "email" }, "uniqueItems": true - }, - "phone": { - "type": "string" } }, "additionalProperties": false diff --git a/schemas/registry-org/RootOrg.json b/schemas/registry-org/RootOrg.json index a60691ee5..8f1b48cd7 100644 --- a/schemas/registry-org/RootOrg.json +++ b/schemas/registry-org/RootOrg.json @@ -60,9 +60,6 @@ "format": "email" }, "uniqueItems": true - }, - "phone": { - "type": "string" } }, "additionalProperties": false diff --git a/schemas/registry-org/create-registry-org-request.json b/schemas/registry-org/create-registry-org-request.json index 2e583abc4..4fdb7f4bd 100644 --- a/schemas/registry-org/create-registry-org-request.json +++ b/schemas/registry-org/create-registry-org-request.json @@ -108,9 +108,6 @@ "format": "email" }, "uniqueItems": true - }, - "phone": { - "type": "string" } }, "additionalProperties": false diff --git a/schemas/registry-org/create-registry-org-response.json b/schemas/registry-org/create-registry-org-response.json index 5161b509d..609298060 100644 --- a/schemas/registry-org/create-registry-org-response.json +++ b/schemas/registry-org/create-registry-org-response.json @@ -145,9 +145,6 @@ "format": "email" }, "uniqueItems": true - }, - "phone": { - "type": "string" } }, "additionalProperties": false diff --git a/schemas/registry-org/get-registry-org-response.json b/schemas/registry-org/get-registry-org-response.json index a1ba1fc1a..83c1077f3 100644 --- a/schemas/registry-org/get-registry-org-response.json +++ b/schemas/registry-org/get-registry-org-response.json @@ -115,9 +115,6 @@ }, "uniqueItems": true }, - "phone": { - "type": "string" - }, "additional_contacts": { "type": "array", "items": { diff --git a/schemas/registry-org/list-registry-orgs-response.json b/schemas/registry-org/list-registry-orgs-response.json index 40bc74add..ce5c5ba43 100644 --- a/schemas/registry-org/list-registry-orgs-response.json +++ b/schemas/registry-org/list-registry-orgs-response.json @@ -143,9 +143,6 @@ "format": "email" }, "uniqueItems": true - }, - "phone": { - "type": "string" } }, "additionalProperties": false diff --git a/schemas/registry-org/update-registry-org-request.json b/schemas/registry-org/update-registry-org-request.json index 60cbe400a..71ee3f92a 100644 --- a/schemas/registry-org/update-registry-org-request.json +++ b/schemas/registry-org/update-registry-org-request.json @@ -124,9 +124,6 @@ "format": "email" }, "uniqueItems": true - }, - "phone": { - "type": "string" } }, "additionalProperties": false diff --git a/schemas/registry-org/update-registry-org-response.json b/schemas/registry-org/update-registry-org-response.json index 387ef9f87..eb6a10f75 100644 --- a/schemas/registry-org/update-registry-org-response.json +++ b/schemas/registry-org/update-registry-org-response.json @@ -134,9 +134,6 @@ "format": "email" }, "uniqueItems": true - }, - "phone": { - "type": "string" } }, "additionalProperties": false diff --git a/src/controller/org.controller/org.middleware.js b/src/controller/org.controller/org.middleware.js index bcd7c3b05..78b06147f 100644 --- a/src/controller/org.controller/org.middleware.js +++ b/src/controller/org.controller/org.middleware.js @@ -110,7 +110,6 @@ const QUERY_PARAMETERS = { 'contact_info', 'contact_info.websites', 'contact_info.emails', - 'contact_info.phone', '', '', 'partner_role_type', diff --git a/src/controller/registry.controller/index.js b/src/controller/registry.controller/index.js index 2b4997694..5efc707a0 100644 --- a/src/controller/registry.controller/index.js +++ b/src/controller/registry.controller/index.js @@ -622,7 +622,6 @@ router.put('/registry/org/:shortname',
  • disclosure_policy
  • contact_info.websites
  • contact_info.emails
  • -
  • contact_info.phone
  • partner_role_type
  • partner_country
  • advisory_locations
  • diff --git a/src/model/baseorg.js b/src/model/baseorg.js index ee690878c..0b5fa666e 100644 --- a/src/model/baseorg.js +++ b/src/model/baseorg.js @@ -17,8 +17,7 @@ const schema = { admins: [String], contact_info: { websites: [String], - emails: [String], - phone: String + emails: [String] }, private_contacts: [{ _id: false, diff --git a/src/repositories/baseOrgRepository.js b/src/repositories/baseOrgRepository.js index 8be1a9307..076bc38dc 100644 --- a/src/repositories/baseOrgRepository.js +++ b/src/repositories/baseOrgRepository.js @@ -1004,7 +1004,6 @@ class BaseOrgRepository extends BaseRepository { * @param {string} [incomingParameters.reports_to] - The short name of the organization this org reports to. (Registry only) * @param {string[]} [incomingParameters.contact_info.websites] - The organization's website URLs. (Registry only) * @param {string[]} [incomingParameters.contact_info.emails] - The organization's email addresses. (Registry only) - * @param {string} [incomingParameters.contact_info.phone] - The organization's phone number. (Registry only) * @param {string} [incomingParameters.cna_role_type] - (Registry only) * @param {string} [incomingParameters.cna_country] - (Registry only) * @param {string[]} [incomingParameters.advisory_locations] - (Registry only) diff --git a/src/scripts/migrate.js b/src/scripts/migrate.js index 1f0331098..c643ac7b9 100644 --- a/src/scripts/migrate.js +++ b/src/scripts/migrate.js @@ -109,7 +109,6 @@ async function addCVEBoard (db) { id_quota: null, private_contacts: [], contact_info: { - phone: null, emails: [], websites: [] }, @@ -212,8 +211,7 @@ async function orgHelper (db) { private_contacts: [], // don't have now contact_info: { emails: email ? [email] : [], - websites: site ? [site] : [], - phone: null + websites: site ? [site] : [] }, program_data: { status: 'active' diff --git a/src/scripts/test_data/postman/cve-services-registry.postman_collection.json b/src/scripts/test_data/postman/cve-services-registry.postman_collection.json index e79e30e80..9c858d708 100644 --- a/src/scripts/test_data/postman/cve-services-registry.postman_collection.json +++ b/src/scripts/test_data/postman/cve-services-registry.postman_collection.json @@ -109,7 +109,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"authority\": [\"CNA\"],\n \"long_name\": \"{{registryOrgLongName}}\",\n \"short_name\": \"{{registryOrgShortName}}\",\n \"partner_number\": \"CNA-POSTMAN-REGISTRY-{{registryRunId}}\",\n \"top_level_root\": \"MITRE TLR\",\n \"aliases\": [\"Postman Registry CNA {{registryRunId}}\"],\n \"partner_role_type\": [\"Vendor\"],\n \"partner_country\": \"United States\",\n \"industry\": \"Information Technology\",\n \"id_quota\": 500,\n \"advisory_locations\": [\"https://postman-registry.example/advisories\"],\n \"advisory_location_require_credentials\": false,\n \"vulnerability_advisory_location_for_web_scraping\": [\"https://postman-registry.example/advisories\"],\n \"is_cna_discussion_list\": true,\n \"contact_info\": {\n \"phone\": \"+1-555-210-3344\",\n \"emails\": [\"security@postman-registry.example\"],\n \"websites\": [\"https://postman-registry.example\"]\n },\n \"private_contacts\": [\n {\n \"phone\": \"+1-555-210-3345\",\n \"poc_email\": \"{{registryAdminUsername}}\"\n }\n ],\n \"program_data\": {\n \"status\": \"active\",\n \"partner_active_date\": \"2015-03-15\",\n \"cve_website_update_needed\": false,\n \"cve_website_update_date\": \"2024-11-01\"\n },\n \"charter_or_scope\": \"https://postman-registry.example/charter\",\n \"disclosure_policy\": \"https://postman-registry.example/disclosure\",\n \"product_list\": \"https://postman-registry.example/products\"\n}" + "raw": "{\n \"authority\": [\"CNA\"],\n \"long_name\": \"{{registryOrgLongName}}\",\n \"short_name\": \"{{registryOrgShortName}}\",\n \"partner_number\": \"CNA-POSTMAN-REGISTRY-{{registryRunId}}\",\n \"top_level_root\": \"MITRE TLR\",\n \"aliases\": [\"Postman Registry CNA {{registryRunId}}\"],\n \"partner_role_type\": [\"Vendor\"],\n \"partner_country\": \"United States\",\n \"industry\": \"Information Technology\",\n \"id_quota\": 500,\n \"advisory_locations\": [\"https://postman-registry.example/advisories\"],\n \"advisory_location_require_credentials\": false,\n \"vulnerability_advisory_location_for_web_scraping\": [\"https://postman-registry.example/advisories\"],\n \"is_cna_discussion_list\": true,\n \"contact_info\": {\n \"emails\": [\"security@postman-registry.example\"],\n \"websites\": [\"https://postman-registry.example\"]\n },\n \"private_contacts\": [\n {\n \"phone\": \"+1-555-210-3345\",\n \"poc_email\": \"{{registryAdminUsername}}\"\n }\n ],\n \"program_data\": {\n \"status\": \"active\",\n \"partner_active_date\": \"2015-03-15\",\n \"cve_website_update_needed\": false,\n \"cve_website_update_date\": \"2024-11-01\"\n },\n \"charter_or_scope\": \"https://postman-registry.example/charter\",\n \"disclosure_policy\": \"https://postman-registry.example/disclosure\",\n \"product_list\": \"https://postman-registry.example/products\"\n}" }, "url": { "raw": "{{baseUrl}}/api/registry/org", @@ -500,7 +500,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"authority\": [\"CNA\"],\n \"long_name\": \"{{registryOrgLongName}}\",\n \"short_name\": \"{{registryOrgShortName}}\",\n \"aliases\": [\"Postman Registry CNA {{registryRunId}}\"],\n \"partner_role_type\": [\"Vendor\"],\n \"partner_country\": \"United States\",\n \"industry\": \"Information Technology\",\n \"id_quota\": 500,\n \"advisory_locations\": [\"https://postman-registry.example/advisories\"],\n \"advisory_location_require_credentials\": false,\n \"vulnerability_advisory_location_for_web_scraping\": [\"https://postman-registry.example/advisories\"],\n \"is_cna_discussion_list\": true,\n \"contact_info\": {\n \"phone\": \"+1-555-210-3344\",\n \"emails\": [\"security@postman-registry.example\"],\n \"websites\": [\"https://postman-registry.example\"]\n },\n \"private_contacts\": [\n {\n \"phone\": \"+1-555-210-3345\",\n \"poc_email\": \"{{registryAdminUsername}}\"\n }\n ],\n \"charter_or_scope\": \"https://postman-registry.example/charter\",\n \"disclosure_policy\": \"https://postman-registry.example/disclosure\",\n \"product_list\": \"https://postman-registry.example/products\",\n \"test\": \"additional key not in schema\"\n}" + "raw": "{\n \"authority\": [\"CNA\"],\n \"long_name\": \"{{registryOrgLongName}}\",\n \"short_name\": \"{{registryOrgShortName}}\",\n \"aliases\": [\"Postman Registry CNA {{registryRunId}}\"],\n \"partner_role_type\": [\"Vendor\"],\n \"partner_country\": \"United States\",\n \"industry\": \"Information Technology\",\n \"id_quota\": 500,\n \"advisory_locations\": [\"https://postman-registry.example/advisories\"],\n \"advisory_location_require_credentials\": false,\n \"vulnerability_advisory_location_for_web_scraping\": [\"https://postman-registry.example/advisories\"],\n \"is_cna_discussion_list\": true,\n \"contact_info\": {\n \"emails\": [\"security@postman-registry.example\"],\n \"websites\": [\"https://postman-registry.example\"]\n },\n \"private_contacts\": [\n {\n \"phone\": \"+1-555-210-3345\",\n \"poc_email\": \"{{registryAdminUsername}}\"\n }\n ],\n \"charter_or_scope\": \"https://postman-registry.example/charter\",\n \"disclosure_policy\": \"https://postman-registry.example/disclosure\",\n \"product_list\": \"https://postman-registry.example/products\",\n \"test\": \"additional key not in schema\"\n}" }, "url": { "raw": "{{baseUrl}}/api/registry/org/{{registryOrgShortName}}", @@ -654,7 +654,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"authority\": [\"CNA\"],\n \"long_name\": \"{{registryOrgLongName}}\",\n \"short_name\": \"{{registryOrgShortName}}\",\n \"aliases\": [\"Postman Registry CNA {{registryRunId}}\"],\n \"partner_role_type\": [\"Vendor\"],\n \"partner_country\": \"United States\",\n \"industry\": \"Information Technology\",\n \"id_quota\": 500,\n \"advisory_locations\": [\"https://postman-registry.example/advisories\"],\n \"advisory_location_require_credentials\": false,\n \"vulnerability_advisory_location_for_web_scraping\": [\"https://postman-registry.example/advisories\"],\n \"is_cna_discussion_list\": true,\n \"contact_info\": {\n \"phone\": \"+1-555-210-3344\",\n \"emails\": [\"security@postman-registry.example\"],\n \"websites\": [\"{{registryAdminUpdateWebsite}}\"]\n },\n \"private_contacts\": [\n {\n \"phone\": \"+1-555-210-3345\",\n \"poc_email\": \"{{registryAdminUsername}}\"\n }\n ],\n \"charter_or_scope\": \"https://postman-registry.example/charter\",\n \"disclosure_policy\": \"https://postman-registry.example/disclosure\",\n \"product_list\": \"https://postman-registry.example/products\"\n}" + "raw": "{\n \"authority\": [\"CNA\"],\n \"long_name\": \"{{registryOrgLongName}}\",\n \"short_name\": \"{{registryOrgShortName}}\",\n \"aliases\": [\"Postman Registry CNA {{registryRunId}}\"],\n \"partner_role_type\": [\"Vendor\"],\n \"partner_country\": \"United States\",\n \"industry\": \"Information Technology\",\n \"id_quota\": 500,\n \"advisory_locations\": [\"https://postman-registry.example/advisories\"],\n \"advisory_location_require_credentials\": false,\n \"vulnerability_advisory_location_for_web_scraping\": [\"https://postman-registry.example/advisories\"],\n \"is_cna_discussion_list\": true,\n \"contact_info\": {\n \"emails\": [\"security@postman-registry.example\"],\n \"websites\": [\"{{registryAdminUpdateWebsite}}\"]\n },\n \"private_contacts\": [\n {\n \"phone\": \"+1-555-210-3345\",\n \"poc_email\": \"{{registryAdminUsername}}\"\n }\n ],\n \"charter_or_scope\": \"https://postman-registry.example/charter\",\n \"disclosure_policy\": \"https://postman-registry.example/disclosure\",\n \"product_list\": \"https://postman-registry.example/products\"\n}" }, "url": { "raw": "{{baseUrl}}/api/registry/org/{{registryOrgShortName}}", diff --git a/src/scripts/test_data/testData.js b/src/scripts/test_data/testData.js index ee8d7bb5a..42718e02f 100644 --- a/src/scripts/test_data/testData.js +++ b/src/scripts/test_data/testData.js @@ -385,7 +385,6 @@ export const orgs = [ ], is_cna_discussion_list: true, contact_info: { - phone: '+1-555-210-3344', emails: [ 'jholloway@acmesecurity.example.com', 'security@acmesecurity.example.com' @@ -425,7 +424,6 @@ export const orgs = [ ], is_cna_discussion_list: false, contact_info: { - phone: '+49-30-555-2211', emails: ['kbauer@cetil.example.de'], websites: ['https://cetil.example.de'] }, @@ -463,7 +461,6 @@ export const orgs = [ ], is_cna_discussion_list: false, contact_info: { - phone: '+82-2-5555-1122', emails: ['jhpark@kvic.example.kr'], websites: ['https://kvic.example.kr'] }, @@ -528,7 +525,6 @@ export const orgs = [ ], is_cna_discussion_list: true, contact_info: { - phone: '+41-44-555-7766', emails: ['hmuller@spsg.example.ch'], websites: ['https://spsg.example.ch'] }, @@ -563,7 +559,6 @@ export const orgs = [ ], is_cna_discussion_list: true, contact_info: { - phone: '+33-1-5555-9988', emails: ['sleclerc@fncap.example.fr'], websites: ['https://fncap.example.fr'] }, @@ -590,7 +585,6 @@ export const orgs = [ id_quota: 250, is_cna_discussion_list: true, contact_info: { - phone: '+34-91-555-9900', emails: ['cdelgado@ivcc.example.es'], websites: ['https://ivcc.example.es'] }, @@ -621,7 +615,6 @@ export const orgs = [ ], is_cna_discussion_list: false, contact_info: { - phone: '+44-20-5555-1234', emails: ['jwhitfield@ukhssl.example.co.uk'], websites: ['https://ukhssl.example.co.uk'] }, @@ -654,7 +647,6 @@ export const orgs = [ advisory_location_require_credentials: false, is_cna_discussion_list: false, contact_info: { - phone: '+48-22-555-4321', emails: ['twieczorek@eebbn.example.pl'], websites: ['https://eebbn.example.pl'] }, @@ -683,7 +675,6 @@ export const orgs = [ advisory_location_require_credentials: false, is_cna_discussion_list: true, contact_info: { - phone: '+55-11-5555-3344', emails: ['lferreira@bcoc.example.br'], websites: ['https://bcoc.example.br'] }, @@ -716,7 +707,6 @@ export const orgs = [ advisory_location_require_credentials: false, is_cna_discussion_list: true, contact_info: { - phone: '+1-202-555-0100', emails: ['mcollins@gvsa.example.org'], websites: ['https://gvsa.example.org'] }, @@ -749,7 +739,6 @@ export const orgs = [ advisory_location_require_credentials: false, is_cna_discussion_list: false, contact_info: { - phone: '+47-21-555-678', emails: ['elindgren@ncdi.example.no'], websites: ['https://ncdi.example.no'] }, @@ -798,7 +787,6 @@ export const orgs = [ advisory_location_require_credentials: false, is_cna_discussion_list: false, contact_info: { - phone: '+91-11-5555-2233', emails: ['rkrishnamurthy@iscru.example.in'], websites: ['https://iscru.example.in'] }, @@ -847,7 +835,6 @@ export const orgs = [ id_quota: 200, is_cna_discussion_list: false, contact_info: { - phone: '+65-6555-8877', emails: ['wltan@saca.example.sg'], websites: ['https://saca.example.sg'] }, @@ -878,7 +865,6 @@ export const orgs = [ advisory_location_require_credentials: false, is_cna_discussion_list: false, contact_info: { - phone: '+972-3-555-7890', emails: ['yshapiro@mevrc.example.il'], websites: ['https://mevrc.example.il'] }, @@ -927,7 +913,6 @@ export const orgs = [ advisory_location_require_credentials: false, is_cna_discussion_list: false, contact_info: { - phone: '+27-11-555-6677', emails: ['adiallo@acte.example.za'], websites: ['https://acte.example.za'] }, @@ -954,7 +939,6 @@ export const orgs = [ advisory_location_require_credentials: false, is_cna_discussion_list: false, contact_info: { - phone: '+57-1-555-4422', emails: ['imoreno@acra.example.co'], websites: ['https://acra.example.co'] }, diff --git a/test/integration-tests/constants.js b/test/integration-tests/constants.js index 0711aedd1..5a13a295a 100644 --- a/test/integration-tests/constants.js +++ b/test/integration-tests/constants.js @@ -382,8 +382,7 @@ const testRegistryOrg = { long_name: 'Test Registry Organization', contact_info: { websites: ['https://test.org'], - emails: ['dave@test.org'], - phone: '555-1234' + emails: ['dave@test.org'] }, private_contacts: [{ poc: 'Dave Private', @@ -399,8 +398,7 @@ const testRegistryOrg2 = { long_name: 'Test Registry Organization2', contact_info: { websites: ['https://test.org'], - emails: ['dave@test.org'], - phone: '555-1234' + emails: ['dave@test.org'] }, authority: ['CNA'], id_quota: 100000 @@ -425,8 +423,7 @@ const existingRegistryOrg = { long_name: 'Test Registry Organization', contact_info: { websites: ['https://test.org'], - emails: ['dave@test.org'], - phone: '555-1234' + emails: ['dave@test.org'] }, authority: ['CNA'], id_quota: 100000 diff --git a/test/integration-tests/registry-org/verifyDeepRemoveEmpty.js b/test/integration-tests/registry-org/verifyDeepRemoveEmpty.js index 6884a6784..d7a37cb8a 100644 --- a/test/integration-tests/registry-org/verifyDeepRemoveEmpty.js +++ b/test/integration-tests/registry-org/verifyDeepRemoveEmpty.js @@ -14,7 +14,7 @@ const testNullRemovalOrg = { authority: ['CNA'], id_quota: 1000, contact_info: { - phone: null // Should be removed + websites: null // Should be removed } } @@ -43,7 +43,6 @@ describe('Testing Deep Remove Empty in Create Org', () => { // Ideally if contact_info becomes empty, deepRemoveEmpty might remove the whole object if it recurses well. // Let's check what happened. if (createdOrg.contact_info) { - expect(createdOrg.contact_info).to.not.have.property('phone') expect(createdOrg.contact_info).to.have.property('websites').that.is.an('array') expect(createdOrg.contact_info).to.have.property('emails').that.is.an('array') } else { @@ -53,6 +52,23 @@ describe('Testing Deep Remove Empty in Create Org', () => { }) }) + it('Rejects a public organization phone number', async () => { + const organizationWithPublicPhone = { + ...testNullRemovalOrg, + short_name: 'test_public_contact_phone', + contact_info: { + phone: '555-1234' + } + } + + const res = await chai.request(app) + .post('/api/registry/org') + .set(secretariatHeaders) + .send(organizationWithPublicPhone) + + expect(res).to.have.status(400) + }) + after(async () => { // Cleanup: Delete the created org await chai.request(app) From 8d0ee7478fcc2de0549dce09dec4d0d9f40b7228 Mon Sep 17 00:00:00 2001 From: James Dalphond Date: Tue, 21 Jul 2026 12:44:24 -0400 Subject: [PATCH 20/27] Add migrate-mongo setup and dev populate reset flow - Add migrate-mongo config and initial collection/index migration - Update populate to reset local dev/test DBs, run migrations, and seed Org/BaseOrg/User/BaseUser/Glossary - Update scripts and docs for the new migrate-then-populate workflow --- README.md | 13 +- docker/README.md | 33 +- migrate-mongo-config.js | 77 ++++ ...60720-01-create-application-collections.js | 89 +++++ package-lock.json | 66 ++++ package.json | 28 +- src/scripts/populate.js | 354 ++++++++++++------ src/scripts/test_data/postman/README.md | 2 +- src/utils/data.js | 9 +- 9 files changed, 511 insertions(+), 160 deletions(-) create mode 100644 migrate-mongo-config.js create mode 100644 migrations/20260720-01-create-application-collections.js diff --git a/README.md b/README.md index 06976cece..419559ad8 100644 --- a/README.md +++ b/README.md @@ -89,14 +89,23 @@ Download MongoDB Compass (MongoDB GUI) - https://www.mongodb.com/download-center/compass -Create a `cve_dev` database in Compass. The collections will be automatically created when the API starts storing documents. +Create a `cve_dev` database in Compass. -You can populate the database with test data using: +You can reset the local development database, run pending database migrations, and populate Org, BaseOrg, User, BaseUser, and Glossary seed data using: ```sh npm run populate:dev ``` +If the database only needs versioned data migrations without a local data reset, check the migration status and then apply pending migrations: + +```sh +npm run db:migrate:status +npm run db:migrate:dev +``` + +Migrations are managed by `migrate-mongo` using `migrate-mongo-config.js`. The applied migration records are stored in the `migrations_changelog` collection. Existing `migrate:*` scripts are legacy data sync scripts and are separate from these versioned database migrations. + 3. Start the node application In order to start a dev environment: diff --git a/docker/README.md b/docker/README.md index 37f875d88..9e02e66ad 100644 --- a/docker/README.md +++ b/docker/README.md @@ -76,11 +76,11 @@ If you do not require special configuration to access the internet, you can safe ### Pre-load Data -Populate mongoDB with test data included in `datadump/pre-population/` +Populate MongoDB with local development seed data included in `datadump/pre-population/`. This resets the local development database, runs pending database migrations, and populates Org, BaseOrg, User, BaseUser, and Glossary. -Run the command below using `populate:dev` or `populate:int` depending on your environment: +Run the command below for development environments: ``` -docker-compose exec cveawg npm run populate:int +docker-compose exec cveawg npm run populate:dev ``` You should see the following: @@ -88,22 +88,21 @@ You should see the following: > cve-services@0.0.3 populate:dev /app > NODE_ENV=development node-dev src/scripts/populate.js -2022-06-07 19:58:32 [info]: "Using NODE_ENV 'development' and app environment 'development'" -2022-06-07 19:58:32 [info]: "Using dbName = cve_dev" -2022-06-07 19:58:32 [info]: "Will try to connect to database cve_dev at docdb:27017" 2022-06-07 19:58:32 [info]: "Successfully connected to database!" -Are you sure you wish to pre-populate the database for the development environment? Doing so will drop the Cve, Cve-Id-Range, Cve-Id, User, Org collection(s) in the cve_dev database. (y/n) y +Are you sure you wish to pre-populate the database for the development environment? Doing so will drop and rebuild the database, run migrations, and populate the Org, BaseOrg, User, BaseUser, Glossary collection(s) in the cve_dev database. (y/n) y +2022-06-07 19:58:37 [info]: "Dropping cve_dev database before running migrations..." +2022-06-07 19:58:37 [info]: "Successfully dropped cve_dev database." 2022-06-07 19:58:37 [info]: "Populating Org collection..." 2022-06-07 19:58:37 [info]: "Org populated!" 2022-06-07 19:58:37 [info]: "Populating User collection..." 2022-06-07 19:58:38 [info]: "User populated!" -2022-06-07 19:58:38 [info]: "Populating Cve-Id-Range collection..." -2022-06-07 19:58:38 [info]: "Populating Cve collection..." -2022-06-07 19:58:38 [info]: "Populating Cve-Id collection..." -2022-06-07 19:58:38 [info]: "Cve-Id-Range populated!" -2022-06-07 19:58:38 [info]: "Cve populated!" -2022-06-07 19:58:39 [info]: "Cve-Id populated!" +2022-06-07 19:58:38 [info]: "Populating BaseOrg collection..." +2022-06-07 19:58:38 [info]: "BaseOrg populated!" +2022-06-07 19:58:38 [info]: "Populating BaseUser collection..." +2022-06-07 19:58:38 [info]: "BaseUser populated!" +2022-06-07 19:58:38 [info]: "Populating Glossary collection..." +2022-06-07 19:58:39 [info]: "Glossary populated!" 2022-06-07 19:58:39 [info]: "Successfully populated the database!" ``` @@ -112,14 +111,6 @@ Are you sure you wish to pre-populate the database for the development environme The API token key is generated or stored differently depending on the value of the `NODE_ENV` environment variable. -#### Integration - -For `integration` Node environments, the API key will be generate and saved to the `user-secret.txt` file when the database is populated. - -Display the key with: - -`docker-compose exec cveawg grep admin2 user-secret.txt` - #### Development In `development` environments, the API is the value of the `LOCAL_KEY` variable in the `.docker-env` file. diff --git a/migrate-mongo-config.js b/migrate-mongo-config.js new file mode 100644 index 000000000..cb979e5ba --- /dev/null +++ b/migrate-mongo-config.js @@ -0,0 +1,77 @@ +require('dotenv').config() + +const config = require('config') + +const appEnv = process.env.NODE_ENV || 'development' + +function getConfiguredValue (key) { + const configPath = `${appEnv}.${key}` + return config.has(configPath) ? config.get(configPath) : null +} + +function getMongoConnectionString () { + if (process.env.MONGO_CONN_STRING) { + return process.env.MONGO_CONN_STRING + } + + const dbUser = process.env.MONGO_USER || getConfiguredValue('username') + const dbPassword = process.env.MONGO_PASSWORD || getConfiguredValue('password') + const dbHost = process.env.MONGO_HOST || getConfiguredValue('host') + const dbPort = process.env.MONGO_PORT || getConfiguredValue('port') + const dbName = getMongoDatabaseName() + const dbLoginPrepend = dbUser && dbPassword ? `${dbUser}:${dbPassword}@` : '' + + if (process.env.useAWS) { + return `mongodb://${dbLoginPrepend}${dbHost}:${dbPort}/${dbName}?replicaSet=rs0&readPreference=secondaryPreferred&retryWrites=false` + } + + return `mongodb://${dbLoginPrepend}${dbHost}:${dbPort}/${dbName}` +} + +function getDatabaseNameFromUri () { + if (!process.env.MONGO_CONN_STRING) { + return null + } + + try { + const uri = new URL(process.env.MONGO_CONN_STRING) + const databaseName = uri.pathname.replace(/^\//, '') + return databaseName || null + } catch (err) { + return null + } +} + +function getMongoDatabaseName () { + return process.env.MONGO_DB_NAME || getDatabaseNameFromUri() || getConfiguredValue('database') +} + +function getMongoConnectionOptions () { + if (process.env.useAWS) { + return { + authMechanism: 'SCRAM-SHA-1', + tls: false + } + } + + return { + tls: false + } +} + +const migrationConfig = { + mongodb: { + url: getMongoConnectionString(), + databaseName: getMongoDatabaseName(), + options: getMongoConnectionOptions() + }, + migrationsDir: 'migrations', + changelogCollectionName: 'migrations_changelog', + lockCollectionName: 'migrations_changelog_lock', + lockTtl: 0, + migrationFileExtension: '.js', + useFileHash: false, + moduleSystem: 'commonjs' +} + +module.exports = migrationConfig diff --git a/migrations/20260720-01-create-application-collections.js b/migrations/20260720-01-create-application-collections.js new file mode 100644 index 000000000..b4706329f --- /dev/null +++ b/migrations/20260720-01-create-application-collections.js @@ -0,0 +1,89 @@ +const COLLECTION_NAMES = [ + 'Audit', + 'BaseOrg', + 'BaseUser', + 'Conversation', + 'Cve', + 'Cve-Id', + 'Cve-Id-Range', + 'Glossary', + 'Org', + 'ReviewObject', + 'User' +] + +const INDEXES = [ + { collectionName: 'Audit', key: { uuid: 1 } }, + { collectionName: 'Conversation', key: { target_uuid: 1 } }, + { collectionName: 'Conversation', key: { previous_conversation_uuid: 1 } }, + { collectionName: 'Conversation', key: { next_conversation_uuid: 1 } }, + { collectionName: 'Conversation', key: { author_id: 1 } }, + { collectionName: 'Conversation', key: { posted_at: 1 } }, + { collectionName: 'Cve', key: { 'cve.cveMetadata.cveId': 1 } }, + { collectionName: 'Cve', key: { 'cve.cveMetadata.dateUpdated': 1 } }, + { collectionName: 'Cve', key: { 'cve.containers.cna.providerMetadata.dateUpdated': 1 } }, + { collectionName: 'Cve', key: { 'time.modified': 1 } }, + { collectionName: 'Cve', key: { 'time.created': 1 } }, + { collectionName: 'Cve-Id', key: { cve_id: 1 } }, + { collectionName: 'Cve-Id', key: { owning_cna: 1, state: 1 } }, + { collectionName: 'Cve-Id', key: { reserved: 1 } }, + { collectionName: 'Glossary', key: { services_short_name: 1 }, options: { unique: true } }, + { collectionName: 'Org', key: { UUID: 1 } }, + { collectionName: 'Org', key: { 'authority.active_roles': 1 } }, + { collectionName: 'ReviewObject', key: { target_object_uuid: 1, status: 1, created: -1 } }, + { collectionName: 'User', key: { UUID: 1 } } +] + +async function collectionExists (db, collectionName) { + const collections = await db.listCollections({ name: collectionName }).toArray() + return collections.length > 0 +} + +async function createCollectionIfMissing (db, collectionName) { + if (await collectionExists(db, collectionName)) { + return + } + + try { + await db.createCollection(collectionName) + } catch (err) { + if (err.code === 48 || err.codeName === 'NamespaceExists') { + return + } + throw err + } +} + +function keysMatch (actual, expected) { + const actualKeys = Object.keys(actual) + const expectedKeys = Object.keys(expected) + + return actualKeys.length === expectedKeys.length && + expectedKeys.every((key, index) => key === actualKeys[index] && actual[key] === expected[key]) +} + +async function createIndexIfMissing (db, { collectionName, key, options = {} }) { + const existingIndexes = await db.collection(collectionName).indexes() + const hasIndex = existingIndexes.some(index => keysMatch(index.key, key)) + + if (!hasIndex) { + await db.collection(collectionName).createIndex(key, options) + } +} + +module.exports = { + async up (db, client) { + for (const collectionName of COLLECTION_NAMES) { + await createCollectionIfMissing(db, collectionName) + } + + for (const index of INDEXES) { + await createIndexIfMissing(db, index) + } + }, + + async down (db, client) { + // Intentionally no-op. Dropping application collections is too destructive + // for a rollback in shared or production-like environments. + } +} diff --git a/package-lock.json b/package-lock.json index 9f73e8329..b0cc2810f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,6 +27,7 @@ "kleur": "^4.1.4", "lodash": "^4.18.1", "luxon": "^3.4.4", + "migrate-mongo": "^14.0.7", "mongo-cursor-pagination": "^8.1.3", "mongoose": "^8.9.5", "mongoose-aggregate-paginate-v2": "1.0.6", @@ -1921,6 +1922,31 @@ "node": ">=6" } }, + "node_modules/cli-table3": { + "version": "0.6.5", + "resolved": "https://artifacts.mitre.org:443/artifactory/api/npm/node-npm/cli-table3/-/cli-table3-0.6.5.tgz", + "integrity": "sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==", + "license": "MIT", + "dependencies": { + "string-width": "^4.2.0" + }, + "engines": { + "node": "10.* || >= 12.*" + }, + "optionalDependencies": { + "@colors/colors": "1.5.0" + } + }, + "node_modules/cli-table3/node_modules/@colors/colors": { + "version": "1.5.0", + "resolved": "https://artifacts.mitre.org:443/artifactory/api/npm/node-npm/@colors/colors/-/colors-1.5.0.tgz", + "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.1.90" + } + }, "node_modules/cliui": { "version": "7.0.4", "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", @@ -5587,6 +5613,46 @@ "node": ">=8.6" } }, + "node_modules/migrate-mongo": { + "version": "14.0.7", + "resolved": "https://registry.npmjs.org/migrate-mongo/-/migrate-mongo-14.0.7.tgz", + "integrity": "sha512-+p7XfJDNaXPTHeo7v/ldYmVLMy8xYda0KMXSqkMUzlVndS39rMGBQHfyLrdmOKMjgciWyWpjekXzRQuj1B7HqA==", + "license": "MIT", + "dependencies": { + "cli-table3": "^0.6.5", + "commander": "^14.0.2" + }, + "bin": { + "migrate-mongo": "bin/migrate-mongo.js" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "mongodb": "^4.4.1 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/migrate-mongo/node_modules/commander": { + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz", + "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==", + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/mime-db": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", diff --git a/package.json b/package.json index f7553ff9a..7b734cca9 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,7 @@ "kleur": "^4.1.4", "lodash": "^4.18.1", "luxon": "^3.4.4", + "migrate-mongo": "^14.0.7", "mongo-cursor-pagination": "^8.1.3", "mongoose": "^8.9.5", "mongoose-aggregate-paginate-v2": "1.0.6", @@ -80,18 +81,19 @@ "lint:src": "node node_modules/eslint/bin/eslint.js src/ --fix", "lint:test": "node node_modules/eslint/bin/eslint.js test/ --fix", "lint:test-utils": "node node_modules/eslint/bin/eslint.js test-utils/ --fix", - "populate:dev": "NODE_ENV=development node-dev src/scripts/populate.js", "migrate:dev": "NODE_ENV=development MONGO_CONN_STRING=mongodb://localhost:27017 MONGO_DB_NAME=cve_dev node-dev src/scripts/migrate.js", "migrate:dev:monday": "NODE_ENV=development MONGO_CONN_STRING=mongodb://localhost:27017 MONGO_DB_NAME=cve_dev node-dev src/scripts/MondayMigrate.js", "migrate:test-black-box": "NODE_ENV=development MONGO_CONN_STRING=mongodb://docdb:27017 MONGO_DB_NAME=cve_dev node-dev src/scripts/migrate.js", "migrate:test": "NODE_ENV=test MONGO_CONN_STRING=mongodb://localhost:27017 MONGO_DB_NAME=cve_test node-dev src/scripts/migrate.js", - "populate:stage": "NODE_ENV=staging node src/scripts/populate.js", - "populate:int": "NODE_ENV=integration node src/scripts/populate.js", - "populate:prd": "NODE_ENV=production node src/scripts/populate.js", - "populate-cve:dev": "NODE_ENV=development node-dev src/scripts/populate-cve.js", - "populate-cve:stage": "NODE_ENV=staging node src/scripts/populate-cve.js", - "populate-cve:int": "NODE_ENV=integration node src/scripts/populate-cve.js", - "populate-cve:prd": "NODE_ENV=production node src/scripts/populate-cve.js", + "db:migrate": "migrate-mongo up -f migrate-mongo-config.js", + "db:migrate:dev": "NODE_ENV=development migrate-mongo up -f migrate-mongo-config.js", + "db:migrate:test": "NODE_ENV=test migrate-mongo up -f migrate-mongo-config.js", + "db:migrate:stage": "NODE_ENV=staging migrate-mongo up -f migrate-mongo-config.js", + "db:migrate:int": "NODE_ENV=integration migrate-mongo up -f migrate-mongo-config.js", + "db:migrate:prd": "NODE_ENV=production migrate-mongo up -f migrate-mongo-config.js", + "db:migrate:down": "migrate-mongo down -f migrate-mongo-config.js", + "db:migrate:status": "migrate-mongo status -f migrate-mongo-config.js", + "db:migrate:create": "migrate-mongo create -f migrate-mongo-config.js", "generate": "NODE_ENV=test node src/scripts/test_data/generate.js", "reset-keys": "NODE_ENV=test node src/scripts/test_data/reset_keys.js", "start:dev": "node src/swagger.js && TZ=utc NODE_ENV=development node src/scripts/updateOpenapiHost.js && TZ=utc NODE_ENV=development node-dev src/index.js", @@ -103,11 +105,13 @@ "start:adptest": "node src/swagger.js && NODE_ENV=production node src/scripts/updateOpenapiHost.js && NODE_ENV=production node src/index.js", "swagger-autogen": "node src/swagger.js", "test": "NODE_ENV=test mocha --recursive --exit || true", - "test:integration": "NODE_ENV=test node-dev src/scripts/populate.js y; NODE_ENV=test MONGO_CONN_STRING=mongodb://docdb:27017 MONGO_DB_NAME=cve_test node-dev src/scripts/migrate.js; NODE_ENV=test mocha test/integration-tests --recursive --exit", - "test:integration:replicas": "NODE_ENV=test MONGO_CONN_STRING='mongodb://localhost:27017,localhost:27018,localhost:27019/cve_test?replicaSet=rs0&readPreference=secondaryPreferred&retryWrites=false' node-dev src/scripts/populate.js y; NODE_ENV=test MONGO_CONN_STRING='mongodb://localhost:27017,localhost:27018,localhost:27019/cve_test?replicaSet=rs0&readPreference=secondaryPreferred&retryWrites=false' MONGO_DB_NAME=cve_test node-dev src/scripts/migrate.js; NODE_ENV=test MONGO_CONN_STRING='mongodb://localhost:27017,localhost:27018,localhost:27019/cve_test?replicaSet=rs0&readPreference=secondaryPreferred&retryWrites=false' node src/scripts/runMocha.js test/integration-tests --recursive --exit", + "test:integration": "NODE_ENV=test node-dev src/scripts/populate.js y; NODE_ENV=test mocha test/integration-tests --recursive --exit", + "test:integration:replicas": "NODE_ENV=test MONGO_CONN_STRING='mongodb://localhost:27017,localhost:27018,localhost:27019/cve_test?replicaSet=rs0&readPreference=secondaryPreferred&retryWrites=false' node-dev src/scripts/populate.js y; NODE_ENV=test MONGO_CONN_STRING='mongodb://localhost:27017,localhost:27018,localhost:27019/cve_test?replicaSet=rs0&readPreference=secondaryPreferred&retryWrites=false' node src/scripts/runMocha.js test/integration-tests --recursive --exit", "test:unit-tests": "NODE_ENV=test mocha test/unit-tests --recursive --exit || true", "test:coverage": "NODE_ENV=test nyc --reporter=text mocha src/* --recursive --exit || true", "test:coverage-html": "NODE_ENV=test nyc --reporter=html mocha src/* --recursive --exit || true", - "test:scripts": "NODE_ENV=development node-dev src/scripts/templateScript.js" + "test:scripts": "NODE_ENV=development node-dev src/scripts/templateScript.js", + "populate:dev": "NODE_ENV=development node-dev src/scripts/populate.js", + "populate:test": "NODE_ENV=test node-dev src/scripts/populate.js" } -} \ No newline at end of file +} diff --git a/src/scripts/populate.js b/src/scripts/populate.js index a51d3046a..d1fefc543 100644 --- a/src/scripts/populate.js +++ b/src/scripts/populate.js @@ -6,49 +6,232 @@ const express = require('express') const app = express() const mongoose = require('mongoose') +const _ = require('lodash') const dataUtils = require('../utils/data') -const dbUtils = require('../utils/db') const errors = require('../utils/error') const logger = require('../middleware/logger') -const CveIdRange = require('../model/cve-id-range') -const CveId = require('../model/cve-id') -const Cve = require('../model/cve') const Org = require('../model/org') const User = require('../model/user') const BaseOrg = require('../model/baseorg') const BaseUser = require('../model/baseuser') -const ReviewObject = require('../model/reviewobject') -const Conversation = require('../model/conversation') -const Audit = require('../model/audit') const Glossary = require('../model/glossary') +const migrationConfig = require('../../migrate-mongo-config') +const cnaList = require('./CNAlist.json') const error = new errors.IDRError() +const appEnv = process.env.NODE_ENV || 'development' +const localPopulateEnvironments = new Set(['development', 'test']) const populateTheseCollections = { - Cve: Cve, - 'Cve-Id-Range': CveIdRange, - 'Cve-Id': CveId, - User: User, Org: Org, BaseOrg: BaseOrg, + User: User, BaseUser: BaseUser, - ReviewObject: ReviewObject, - Conversation: Conversation, - Audit: Audit, Glossary: Glossary } -const indexesToCreate = { - Cve: [ - { 'cve.cveMetadata.cveId': 1 }, - { 'cve.cveMetadata.dateUpdated': 1 }, - { 'cve.containers.cna.providerMetadata.dateUpdated': 1 } - ], - 'Cve-Id': [{ cve_id: 1 }, { owning_cna: 1, state: 1 }, { reserved: 1 }], - User: [{ UUID: 1 }], - Org: [{ UUID: 1 }, { 'authority.active_roles': 1 }], - Glossary: [{ services_short_name: 1 }] +function getMongooseConnectionOptions () { + return { + ...migrationConfig.mongodb.options, + dbName: migrationConfig.mongodb.databaseName, + autoIndex: false + } +} + +async function runDatabaseMigrations () { + const migrateMongo = (await import('migrate-mongo')).default + migrateMongo.config.set(migrationConfig) + const { db, client } = await migrateMongo.database.connect() + + try { + const migrated = await migrateMongo.up(db, client) + if (migrated.length === 0) { + logger.info('No pending database migrations to run.') + } else { + logger.info(`Successfully ran database migrations: ${migrated.join(', ')}`) + } + } finally { + await client.close() + } +} + +function isEmptySeedValue (value) { + return value === null || + value === undefined || + (_.isArray(value) && value.length === 0) || + (_.isPlainObject(value) && _.isEmpty(value)) +} + +function deepRemoveEmptySeedValues (obj) { + if (_.isArray(obj)) { + return obj + .map(value => deepRemoveEmptySeedValues(value)) + .filter(value => !isEmptySeedValue(value)) + } else if (_.isPlainObject(obj)) { + return _.transform(obj, (result, value, key) => { + const cleaned = deepRemoveEmptySeedValues(value) + if (!isEmptySeedValue(cleaned)) { + result[key] = cleaned + } + }) + } + return obj +} + +function getCnaListEntry (shortName) { + return cnaList.find(cna => cna.shortName === shortName) +} + +function getOrgType (org) { + const roles = org.authority?.active_roles || [] + const shortName = org.short_name || '' + if (roles.includes('SECRETARIAT') || shortName.toLowerCase().includes('mitre')) { + return 'SecretariatOrg' + } else if (roles.includes('ADP')) { + return 'ADPOrg' + } else if (roles.includes('BULK_DOWNLOAD')) { + return 'BulkDownloadOrg' + } else if (roles.includes('ROOT')) { + return 'RootOrg' + } + return 'CNAOrg' +} + +function buildBaseOrgDocument (org, allUsers) { + const currentCNA = getCnaListEntry(org.short_name) + const orgUsers = [] + const admins = [] + + allUsers.forEach(user => { + if (user.org_UUID === org.UUID) { + orgUsers.push(user.UUID) + if (user.authority?.active_roles?.includes('ADMIN')) { + admins.push(user.UUID) + } + } + }) + + let rootTlr = false + let charterScope = null + let disclosure = null + let email = null + let site = null + + if (currentCNA) { + rootTlr = Object.prototype.hasOwnProperty.call(currentCNA, 'CNA') ? currentCNA.CNA.isRoot : false + charterScope = Object.prototype.hasOwnProperty.call(currentCNA, 'scope') ? currentCNA.scope : null + disclosure = (currentCNA.disclosurePolicy || []) + .map(policy => policy?.url) + .filter(url => url) + .join(';') + + const firstContact = currentCNA.contact?.[0] + email = firstContact?.email?.length > 0 ? firstContact.email[0].emailAddr : null + site = firstContact?.contact?.length > 0 ? firstContact.contact[0].url : null + } + + return deepRemoveEmptySeedValues({ + UUID: org.UUID, + __t: getOrgType(org), + long_name: org.name, + short_name: org.short_name, + aliases: [], + authority: org.authority?.active_roles || [], + reports_to: null, + oversees: [], + top_level_root: rootTlr ? 'true' : 'false', + users: orgUsers, + charter_or_scope: charterScope, + disclosure_policy: disclosure, + product_list: null, + id_quota: org.policies?.id_quota, + admins: admins, + private_contacts: [], + contact_info: { + emails: email ? [email] : [], + websites: site ? [site] : [], + phone: null + }, + in_use: org.inUse, + created: org.time?.created, + last_updated: org.time?.modified + }) +} + +function buildBaseUserDocument (user) { + const roles = user.authority?.active_roles || [] + const isActive = user.active !== false && String(user.active).toLowerCase() !== 'false' + + return deepRemoveEmptySeedValues({ + UUID: user.UUID, + username: user.username, + org_UUID: user.org_UUID, + secret: user.secret, + role: roles.includes('ADMIN') ? 'ADMIN' : '', + name: { + first: user.name?.first, + middle: user.name?.middle, + last: user.name?.last, + suffix: user.name?.suffix + }, + status: isActive ? 'active' : 'inactive', + created: user.time?.created, + created_by: 'system', + last_updated: user.time?.modified, + last_active: null + }) +} + +async function populateBaseOrgCollection () { + logger.info('Populating BaseOrg collection...') + const allUsers = await db.db.collection('User').find().toArray() + const allOrgs = await db.db.collection('Org').find().toArray() + const baseOrgs = allOrgs.map(org => buildBaseOrgDocument(org, allUsers)) + + if (baseOrgs.length > 0) { + await db.db.collection('BaseOrg').insertMany(baseOrgs) + } + logger.info('BaseOrg populated!') +} + +async function populateBaseUserCollection () { + logger.info('Populating BaseUser collection...') + const allUsers = await db.db.collection('User').find().toArray() + const baseUsers = allUsers.map(buildBaseUserDocument) + + if (baseUsers.length > 0) { + await db.db.collection('BaseUser').insertMany(baseUsers) + } + logger.info('BaseUser populated!') +} + +async function resetDatabaseAndRunMigrations () { + logger.info(`Dropping ${migrationConfig.mongodb.databaseName} database before running migrations...`) + await db.db.dropDatabase() + logger.info(`Successfully dropped ${migrationConfig.mongodb.databaseName} database.`) + await runDatabaseMigrations() +} + +async function populateSeedCollections () { + await dataUtils.populateCollection( + './datadump/pre-population/orgs.json', + Org, dataUtils.newOrgTransform + ) + + const hash = await dataUtils.preprocessUserSecrets() + await dataUtils.populateCollection( + './datadump/pre-population/users.json', + User, dataUtils.newUserTransform, hash + ) + + await populateBaseOrgCollection() + await populateBaseUserCollection() + + await dataUtils.populateCollection( + './datadump/pre-population/glossary.json', + Glossary + ) } // Body Parser Middleware @@ -57,113 +240,44 @@ app.use(express.urlencoded({ extended: false })) // Allows us to handle url enco // Make mongoose connection available globally global.mongoose = mongoose -// Connect to MongoDB database -const dbConnectionStr = dbUtils.getMongoConnectionString() -mongoose.connect(dbConnectionStr, { - useNewUrlParser: true, - useUnifiedTopology: false, - autoIndex: false -}) - -console.log('About to test connection') const db = mongoose.connection db.on('error', () => { console.error.bind(console, 'Connection Error: Something went wrong!') logger.error(error.connectionError()) }) +// Connect to MongoDB database +if (!localPopulateEnvironments.has(appEnv)) { + logger.error(`populate.js is only allowed in local development/test environments. Refusing to run for NODE_ENV '${appEnv}'.`) + process.exitCode = 1 +} else { + console.log('About to test connection') + mongoose.connect(migrationConfig.mongodb.url, getMongooseConnectionOptions()) +} + db.once('open', async () => { logger.info('Successfully connected to database!') - let userInput - if (process.argv.length > 2 && process.argv.slice(2)[0] === 'y') { - userInput = process.argv.slice(2)[0] - } else { - // script runner (currently) needs to agree to an action that drops collections - userInput = dataUtils.getUserPopulateInput(Object.keys(populateTheseCollections)) - } - - // drops and re-populates collections - if (userInput.toLowerCase() === 'y') { - const collections = await db.db.listCollections().toArray() - - for (const collection of collections) { - if (!collection.name.startsWith('system.')) { - logger.info(`Dropping ${collection.name} collection !!!`) - await db.dropCollection(collection.name) - } - } - - // Org - await dataUtils.populateCollection( - './datadump/pre-population/orgs.json', - Org, dataUtils.newOrgTransform - ) - - // User, depends on Org - const hash = await dataUtils.preprocessUserSecrets() - await dataUtils.populateCollection( - './datadump/pre-population/users.json', - User, dataUtils.newUserTransform, hash - ) - - const populatePromises = [] - - // CVE ID Range - populatePromises.push(dataUtils.populateCollection( - './datadump/pre-population/cve-ids-range.json', - CveIdRange - )) - - // CVE - if (process.env.NODE_ENV === 'development' || process.env.NODE_ENV === 'test') { - populatePromises.push(dataUtils.populateCollection( - './datadump/pre-population/cves.json', - Cve, dataUtils.newCveTransform - )) + try { + let userInput + if (process.argv.length > 2 && process.argv.slice(2)[0] === 'y') { + userInput = process.argv.slice(2)[0] + } else { + userInput = dataUtils.getUserPopulateInput( + Object.keys(populateTheseCollections), + `drop and rebuild the database, run migrations, and populate the ${Object.keys(populateTheseCollections).join(', ')} collection(s)` + ) } - // CVE ID, depends on User and Org - populatePromises.push(dataUtils.populateCollection( - './datadump/pre-population/cve-ids.json', - CveId, dataUtils.newCveIdTransform - )) - - // Glossary - populatePromises.push(dataUtils.populateCollection( - './datadump/pre-population/glossary.json', - Glossary - )) - - // don't close database connection until all remaining populate - // promises are resolved - Promise.all(populatePromises).then(async function () { + if (userInput.toLowerCase() === 'y') { + await resetDatabaseAndRunMigrations() + await populateSeedCollections() logger.info('Successfully populated the database!') - - const indexPromises = [] - Object.keys(indexesToCreate).forEach(col => { - indexesToCreate[col].forEach(index => { - indexPromises.push(db.collections[col].createIndex(index)) - }) - }) - - try { - await Promise.all(indexPromises) - logger.info('Successfully created indexes!') - - // Explicitly create collections for models that are not pre-populated but require transactions. - // Implicit collection creation inside Mongo transactions acquires heavy locks and leads to LockTimeout. - await Audit.createCollection() - await ReviewObject.createCollection() - await Conversation.createCollection() - await Glossary.createCollection() - } catch (err) { - logger.error('Error creating indexes:', err) - } finally { - mongoose.connection.close() - } - }) - } else { - mongoose.connection.close() + } + } catch (err) { + logger.error('Error populating database:', err) + process.exitCode = 1 + } finally { + await mongoose.connection.close() } }) diff --git a/src/scripts/test_data/postman/README.md b/src/scripts/test_data/postman/README.md index b9613310e..fdccd0ed7 100644 --- a/src/scripts/test_data/postman/README.md +++ b/src/scripts/test_data/postman/README.md @@ -10,7 +10,7 @@ This folder contains a Postman collection and environment for exercising the reg The default environment assumes a local development database and API created with: ```sh -npm run populate:dev; npm run migrate:dev; npm run dev +npm run populate:dev; npm run dev ``` If you are targeting a different database or API host, update the imported environment values before running the collection. diff --git a/src/utils/data.js b/src/utils/data.js index 529ab4c7d..df99656cf 100644 --- a/src/utils/data.js +++ b/src/utils/data.js @@ -109,12 +109,13 @@ async function newCveTransform (cve) { return cve } -function getUserPopulateInput (collectionNames) { - const appEnv = process.env.NODE_ENV - const dbName = config.get(`${appEnv}.database`) +function getUserPopulateInput (collectionNames, actionDescription = null) { + const appEnv = process.env.NODE_ENV || 'development' + const dbName = process.env.MONGO_DB_NAME || config.get(`${appEnv}.database`) + const destructiveAction = actionDescription || `drop the ${collectionNames.join(', ')} collection(s)` const promptString = ( `Are you sure you wish to pre-populate the database for the ${appEnv} environment? ` + - `Doing so will drop the ${collectionNames.join(', ')} collection(s) ` + + `Doing so will ${destructiveAction} ` + `in the ${dbName} database. (y/n) ` ) From 3b0e9981ade94505a45e03a8c04e66db419f19eb Mon Sep 17 00:00:00 2001 From: James Dalphond Date: Wed, 2 Sep 2026 10:34:29 -0400 Subject: [PATCH 21/27] Add migration to remove public organization phone data --- ...20260902-01-remove-public-contact-phone.js | 20 +++++++++++++++++++ src/scripts/populate.js | 3 +-- 2 files changed, 21 insertions(+), 2 deletions(-) create mode 100644 migrations/20260902-01-remove-public-contact-phone.js diff --git a/migrations/20260902-01-remove-public-contact-phone.js b/migrations/20260902-01-remove-public-contact-phone.js new file mode 100644 index 000000000..c2a426c63 --- /dev/null +++ b/migrations/20260902-01-remove-public-contact-phone.js @@ -0,0 +1,20 @@ +const PUBLIC_PHONE_PATH = 'contact_info.phone' +const REVIEW_PHONE_PATH = 'new_review_data.contact_info.phone' + +module.exports = { + async up (db) { + await db.collection('BaseOrg').updateMany( + { [PUBLIC_PHONE_PATH]: { $exists: true } }, + { $unset: { [PUBLIC_PHONE_PATH]: '' } } + ) + + await db.collection('ReviewObject').updateMany( + { [REVIEW_PHONE_PATH]: { $exists: true } }, + { $unset: { [REVIEW_PHONE_PATH]: '' } } + ) + }, + + async down () { + // Public phone values are intentionally removed and cannot be restored. + } +} diff --git a/src/scripts/populate.js b/src/scripts/populate.js index d1fefc543..a6251e7d6 100644 --- a/src/scripts/populate.js +++ b/src/scripts/populate.js @@ -150,8 +150,7 @@ function buildBaseOrgDocument (org, allUsers) { private_contacts: [], contact_info: { emails: email ? [email] : [], - websites: site ? [site] : [], - phone: null + websites: site ? [site] : [] }, in_use: org.inUse, created: org.time?.created, From 833993f061c5c156061b929692b5e0f74ce8cbd3 Mon Sep 17 00:00:00 2001 From: James Dalphond Date: Wed, 2 Sep 2026 11:13:22 -0400 Subject: [PATCH 22/27] remove artifactory in package-lock --- package-lock.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index b0cc2810f..645d32213 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1924,7 +1924,7 @@ }, "node_modules/cli-table3": { "version": "0.6.5", - "resolved": "https://artifacts.mitre.org:443/artifactory/api/npm/node-npm/cli-table3/-/cli-table3-0.6.5.tgz", + "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.5.tgz", "integrity": "sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==", "license": "MIT", "dependencies": { @@ -1939,7 +1939,7 @@ }, "node_modules/cli-table3/node_modules/@colors/colors": { "version": "1.5.0", - "resolved": "https://artifacts.mitre.org:443/artifactory/api/npm/node-npm/@colors/colors/-/colors-1.5.0.tgz", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", "license": "MIT", "optional": true, From 696961faddac13a2918c161281fde67a69a02847 Mon Sep 17 00:00:00 2001 From: James Dalphond Date: Wed, 2 Sep 2026 13:37:31 -0400 Subject: [PATCH 23/27] fix: run database migrations before integration tests - rebuild the test database and apply all migrations before seeding - restore all integration-test fixtures - stop the test suite when population or migration fails - correct BaseUser seed documents - improve local MongoDB replica compatibility --- README.md | 2 +- docker/README.md | 6 +- docker/docker-compose.mongo-cluster.yml | 29 +++++--- package.json | 8 +-- src/scripts/populate.js | 93 ++++++++++++++++--------- 5 files changed, 88 insertions(+), 50 deletions(-) diff --git a/README.md b/README.md index 419559ad8..ae6909a7c 100644 --- a/README.md +++ b/README.md @@ -91,7 +91,7 @@ Download MongoDB Compass (MongoDB GUI) Create a `cve_dev` database in Compass. -You can reset the local development database, run pending database migrations, and populate Org, BaseOrg, User, BaseUser, and Glossary seed data using: +You can reset the local development database, run pending database migrations, and populate Cve, Cve-Id-Range, Cve-Id, Org, BaseOrg, User, BaseUser, and Glossary seed data using: ```sh npm run populate:dev diff --git a/docker/README.md b/docker/README.md index 9e02e66ad..310e181d1 100644 --- a/docker/README.md +++ b/docker/README.md @@ -76,7 +76,7 @@ If you do not require special configuration to access the internet, you can safe ### Pre-load Data -Populate MongoDB with local development seed data included in `datadump/pre-population/`. This resets the local development database, runs pending database migrations, and populates Org, BaseOrg, User, BaseUser, and Glossary. +Populate MongoDB with local development seed data included in `datadump/pre-population/`. This resets the local development database, runs pending database migrations, and populates Cve, Cve-Id-Range, Cve-Id, Org, BaseOrg, User, BaseUser, and Glossary. Run the command below for development environments: ``` @@ -180,7 +180,7 @@ docker compose -f docker-compose.mongo-cluster.yml up -d docdb docdb-read-1 docd Use this connection string from the host, including MongoDB Compass: ```text -mongodb://localhost:27017,localhost:27018,localhost:27019/cve_test?replicaSet=rs0&readPreference=secondaryPreferred&retryWrites=false +mongodb://127.0.0.1:27017,127.0.0.1:27018,127.0.0.1:27019/cve_test?replicaSet=rs0&readPreference=secondaryPreferred&retryWrites=false ``` To run the same local replica topology with Mongo 8, override the image: @@ -190,6 +190,8 @@ cd docker/ MONGO_IMAGE=mongo:8.0 docker compose -f docker-compose.mongo-cluster.yml up -d --force-recreate docdb docdb-read-1 docdb-read-2 mongo-init ``` +The Mongo-only compose file overrides `GLIBC_TUNABLES` for compatibility with Docker Desktop Linux kernels 6.19 through 7.0.13. MongoDB's default TCMalloc configuration refuses to start on those kernel versions. The override can be removed after all development environments use kernel 7.0.14 or newer. + If you already created the local volumes with Mongo 5, Mongo 8 may fail to start against those files. For a fresh Mongo 8 local cluster, remove the Mongo-only volumes first. This deletes local Mongo data for this compose file: ```bash diff --git a/docker/docker-compose.mongo-cluster.yml b/docker/docker-compose.mongo-cluster.yml index 60e925b7e..0458e3970 100644 --- a/docker/docker-compose.mongo-cluster.yml +++ b/docker/docker-compose.mongo-cluster.yml @@ -2,15 +2,18 @@ services: docdb: image: ${MONGO_IMAGE:-mongo:5.0} container_name: mongo + environment: + # MongoDB's default rseq setting is incompatible with Docker kernels 6.19 through 7.0.13. + GLIBC_TUNABLES: glibc.pthread.rseq=1 ports: - - "27017:27017" - - "27018:27018" - - "27019:27019" + - "127.0.0.1:27017:27017" + - "127.0.0.1:27018:27018" + - "127.0.0.1:27019:27019" volumes: - docdb-host-data:/data/db command: ["mongod", "--replSet", "rs0", "--bind_ip_all", "--port", "27017"] healthcheck: - test: ["CMD-SHELL", "mongosh --quiet --port 27017 --eval 'db.adminCommand({ ping: 1 }).ok' || exit 1"] + test: ["CMD-SHELL", "mongosh --quiet --host 127.0.0.1 --port 27017 --eval 'db.adminCommand({ ping: 1 }).ok' || exit 1"] interval: 10s timeout: 10s retries: 12 @@ -19,6 +22,8 @@ services: docdb-read-1: image: ${MONGO_IMAGE:-mongo:5.0} container_name: mongo-read-1 + environment: + GLIBC_TUNABLES: glibc.pthread.rseq=1 network_mode: "service:docdb" depends_on: docdb: @@ -27,7 +32,7 @@ services: - docdb-host-read-1-data:/data/db command: ["mongod", "--replSet", "rs0", "--bind_ip_all", "--port", "27018"] healthcheck: - test: ["CMD-SHELL", "mongosh --quiet --port 27018 --eval 'db.adminCommand({ ping: 1 }).ok' || exit 1"] + test: ["CMD-SHELL", "mongosh --quiet --host 127.0.0.1 --port 27018 --eval 'db.adminCommand({ ping: 1 }).ok' || exit 1"] interval: 10s timeout: 10s retries: 12 @@ -36,6 +41,8 @@ services: docdb-read-2: image: ${MONGO_IMAGE:-mongo:5.0} container_name: mongo-read-2 + environment: + GLIBC_TUNABLES: glibc.pthread.rseq=1 network_mode: "service:docdb" depends_on: docdb: @@ -44,7 +51,7 @@ services: - docdb-host-read-2-data:/data/db command: ["mongod", "--replSet", "rs0", "--bind_ip_all", "--port", "27019"] healthcheck: - test: ["CMD-SHELL", "mongosh --quiet --port 27019 --eval 'db.adminCommand({ ping: 1 }).ok' || exit 1"] + test: ["CMD-SHELL", "mongosh --quiet --host 127.0.0.1 --port 27019 --eval 'db.adminCommand({ ping: 1 }).ok' || exit 1"] interval: 10s timeout: 10s retries: 12 @@ -62,13 +69,13 @@ services: condition: service_healthy command: > sh -c " - mongosh --host localhost --port 27017 --eval ' + mongosh --host 127.0.0.1 --port 27017 --eval ' const desiredConfig = { _id: \"rs0\", members: [ - { _id: 0, host: \"localhost:27017\", priority: 2 }, - { _id: 1, host: \"localhost:27018\", priority: 1 }, - { _id: 2, host: \"localhost:27019\", priority: 1 } + { _id: 0, host: \"127.0.0.1:27017\", priority: 2 }, + { _id: 1, host: \"127.0.0.1:27018\", priority: 1 }, + { _id: 2, host: \"127.0.0.1:27019\", priority: 1 } ] }; @@ -86,7 +93,7 @@ services: rs.status(); const currentConfig = rs.conf(); if (configMatches(currentConfig)) { - print(\"Replica set already initialized with the expected localhost members.\"); + print(\"Replica set already initialized with the expected IPv4 members.\"); } else { print(\"Updating replica set members...\"); rs.reconfig({ ...desiredConfig, version: currentConfig.version + 1 }); diff --git a/package.json b/package.json index 7b734cca9..1d5072b73 100644 --- a/package.json +++ b/package.json @@ -105,13 +105,13 @@ "start:adptest": "node src/swagger.js && NODE_ENV=production node src/scripts/updateOpenapiHost.js && NODE_ENV=production node src/index.js", "swagger-autogen": "node src/swagger.js", "test": "NODE_ENV=test mocha --recursive --exit || true", - "test:integration": "NODE_ENV=test node-dev src/scripts/populate.js y; NODE_ENV=test mocha test/integration-tests --recursive --exit", - "test:integration:replicas": "NODE_ENV=test MONGO_CONN_STRING='mongodb://localhost:27017,localhost:27018,localhost:27019/cve_test?replicaSet=rs0&readPreference=secondaryPreferred&retryWrites=false' node-dev src/scripts/populate.js y; NODE_ENV=test MONGO_CONN_STRING='mongodb://localhost:27017,localhost:27018,localhost:27019/cve_test?replicaSet=rs0&readPreference=secondaryPreferred&retryWrites=false' node src/scripts/runMocha.js test/integration-tests --recursive --exit", + "test:integration": "NODE_ENV=test node src/scripts/populate.js y && NODE_ENV=test mocha test/integration-tests --recursive --exit", + "test:integration:replicas": "NODE_ENV=test MONGO_CONN_STRING='mongodb://127.0.0.1:27017,127.0.0.1:27018,127.0.0.1:27019/cve_test?replicaSet=rs0&readPreference=secondaryPreferred&retryWrites=false' node src/scripts/populate.js y && NODE_ENV=test MONGO_CONN_STRING='mongodb://127.0.0.1:27017,127.0.0.1:27018,127.0.0.1:27019/cve_test?replicaSet=rs0&readPreference=secondaryPreferred&retryWrites=false' node src/scripts/runMocha.js test/integration-tests --recursive --exit", "test:unit-tests": "NODE_ENV=test mocha test/unit-tests --recursive --exit || true", "test:coverage": "NODE_ENV=test nyc --reporter=text mocha src/* --recursive --exit || true", "test:coverage-html": "NODE_ENV=test nyc --reporter=html mocha src/* --recursive --exit || true", "test:scripts": "NODE_ENV=development node-dev src/scripts/templateScript.js", - "populate:dev": "NODE_ENV=development node-dev src/scripts/populate.js", - "populate:test": "NODE_ENV=test node-dev src/scripts/populate.js" + "populate:dev": "NODE_ENV=development node src/scripts/populate.js", + "populate:test": "NODE_ENV=test node src/scripts/populate.js" } } diff --git a/src/scripts/populate.js b/src/scripts/populate.js index a6251e7d6..fd69a07cd 100644 --- a/src/scripts/populate.js +++ b/src/scripts/populate.js @@ -11,6 +11,9 @@ const _ = require('lodash') const dataUtils = require('../utils/data') const errors = require('../utils/error') const logger = require('../middleware/logger') +const CveIdRange = require('../model/cve-id-range') +const CveId = require('../model/cve-id') +const Cve = require('../model/cve') const Org = require('../model/org') const User = require('../model/user') const BaseOrg = require('../model/baseorg') @@ -24,6 +27,9 @@ const error = new errors.IDRError() const appEnv = process.env.NODE_ENV || 'development' const localPopulateEnvironments = new Set(['development', 'test']) const populateTheseCollections = { + Cve: Cve, + 'Cve-Id-Range': CveIdRange, + 'Cve-Id': CveId, Org: Org, BaseOrg: BaseOrg, User: User, @@ -35,7 +41,8 @@ function getMongooseConnectionOptions () { return { ...migrationConfig.mongodb.options, dbName: migrationConfig.mongodb.databaseName, - autoIndex: false + autoIndex: false, + serverSelectionTimeoutMS: 5000 } } @@ -165,7 +172,6 @@ function buildBaseUserDocument (user) { return deepRemoveEmptySeedValues({ UUID: user.UUID, username: user.username, - org_UUID: user.org_UUID, secret: user.secret, role: roles.includes('ADMIN') ? 'ADMIN' : '', name: { @@ -227,6 +233,21 @@ async function populateSeedCollections () { await populateBaseOrgCollection() await populateBaseUserCollection() + await dataUtils.populateCollection( + './datadump/pre-population/cve-ids-range.json', + CveIdRange + ) + + await dataUtils.populateCollection( + './datadump/pre-population/cves.json', + Cve, dataUtils.newCveTransform + ) + + await dataUtils.populateCollection( + './datadump/pre-population/cve-ids.json', + CveId, dataUtils.newCveIdTransform + ) + await dataUtils.populateCollection( './datadump/pre-population/glossary.json', Glossary @@ -240,43 +261,51 @@ app.use(express.urlencoded({ extended: false })) // Allows us to handle url enco global.mongoose = mongoose const db = mongoose.connection -db.on('error', () => { - console.error.bind(console, 'Connection Error: Something went wrong!') +db.on('error', (err) => { + console.error('Connection Error: Something went wrong!', err) logger.error(error.connectionError()) }) -// Connect to MongoDB database -if (!localPopulateEnvironments.has(appEnv)) { - logger.error(`populate.js is only allowed in local development/test environments. Refusing to run for NODE_ENV '${appEnv}'.`) - process.exitCode = 1 -} else { - console.log('About to test connection') - mongoose.connect(migrationConfig.mongodb.url, getMongooseConnectionOptions()) +async function populateDatabase () { + let userInput + if (process.argv.length > 2 && process.argv.slice(2)[0] === 'y') { + userInput = process.argv.slice(2)[0] + } else { + userInput = dataUtils.getUserPopulateInput( + Object.keys(populateTheseCollections), + `drop and rebuild the database, run migrations, and populate the ${Object.keys(populateTheseCollections).join(', ')} collection(s)` + ) + } + + if (userInput.toLowerCase() === 'y') { + await resetDatabaseAndRunMigrations() + await populateSeedCollections() + logger.info('Successfully populated the database!') + console.log('Successfully populated the database!') + } } -db.once('open', async () => { - logger.info('Successfully connected to database!') +async function main () { + if (!localPopulateEnvironments.has(appEnv)) { + throw new Error(`populate.js is only allowed in local development/test environments. Refusing to run for NODE_ENV '${appEnv}'.`) + } - try { - let userInput - if (process.argv.length > 2 && process.argv.slice(2)[0] === 'y') { - userInput = process.argv.slice(2)[0] - } else { - userInput = dataUtils.getUserPopulateInput( - Object.keys(populateTheseCollections), - `drop and rebuild the database, run migrations, and populate the ${Object.keys(populateTheseCollections).join(', ')} collection(s)` - ) - } + console.log('About to connect to MongoDB') - if (userInput.toLowerCase() === 'y') { - await resetDatabaseAndRunMigrations() - await populateSeedCollections() - logger.info('Successfully populated the database!') - } - } catch (err) { - logger.error('Error populating database:', err) - process.exitCode = 1 + try { + await mongoose.connect(migrationConfig.mongodb.url, getMongooseConnectionOptions()) + logger.info('Successfully connected to database!') + console.log('Successfully connected to database!') + await populateDatabase() } finally { - await mongoose.connection.close() + if (mongoose.connection.readyState !== 0) { + await mongoose.connection.close() + } } +} + +main().catch((err) => { + console.error('Unable to populate MongoDB:', err) + logger.error(`Unable to populate MongoDB: ${err.stack || err.message}`) + process.exitCode = 1 }) From 1054e6864e31c86035d45914ab88c94d71cf8346 Mon Sep 17 00:00:00 2001 From: James Dalphond Date: Thu, 3 Sep 2026 14:08:20 -0400 Subject: [PATCH 24/27] WIP: add disabled registry organization flag --- api-docs/openapi.json | 4 +-- datadump/pre-population/orgs.json | 7 +---- docker/README.md | 5 ++-- .../20260903-01-set-registry-org-disabled.js | 29 +++++++++++++++++++ schemas/registry-org/BaseOrg.json | 5 ++++ schemas/registry-org/CNAOrg.json | 3 ++ schemas/registry-org/RootOrg.json | 3 ++ .../create-registry-org-request.json | 5 ++++ .../create-registry-org-response.json | 4 +++ .../get-registry-org-response.json | 4 +++ .../list-registry-orgs-response.json | 4 +++ .../update-registry-org-request.json | 4 +++ .../update-registry-org-response.json | 4 +++ src/constants/index.js | 1 + src/model/baseorg.js | 1 + src/scripts/populate.js | 10 +++---- .../registry-org/registryOrgCRUDTest.js | 19 ++++++++++++ .../registryOrgDisabledMigrationTest.js | 29 +++++++++++++++++++ 18 files changed, 126 insertions(+), 15 deletions(-) create mode 100644 migrations/20260903-01-set-registry-org-disabled.js create mode 100644 test/integration-tests/registry-org/registryOrgDisabledMigrationTest.js diff --git a/api-docs/openapi.json b/api-docs/openapi.json index 834e86fda..6813469f1 100644 --- a/api-docs/openapi.json +++ b/api-docs/openapi.json @@ -3906,7 +3906,7 @@ "Registry Organization" ], "summary": "Updates information about the organization specified by short name (accessible to Secretariat or same-organization Admin)", - "description": "

    Access Control

    User must belong to an organization with the Secretariat role or be an Admin of the requested organization.

    With Joint Approval required for the following fields:

    Expected Behavior

    This endpoint expects a full organization object in the request body.

    Secretariat: Updates any organization's information

    Organization Admin: Requests changes to its organization's information

    • short_name
    • long_name
    • authority
    • aliases
    • oversees
    • top_level_root
    • charter_or_scope
    • product_list
    • disclosure_policy
    • contact_info.websites
    • contact_info.emails
    • partner_role_type
    • partner_country
    • advisory_locations
    • advisory_location_require_credentials
    • vulnerability_advisory_location_for_web_scraping
    • industry
    • tl_root_start_date
    • is_cna_discussion_list
    ", + "description": "

    Access Control

    User must belong to an organization with the Secretariat role or be an Admin of the requested organization.

    With Joint Approval required for the following fields:

    Expected Behavior

    This endpoint expects a full organization object in the request body.

    Secretariat: Updates any organization's information

    Organization Admin: Requests changes to its organization's information

    • short_name
    • long_name
    • authority
    • aliases
    • oversees
    • top_level_root
    • is_top_level_root
    • is_last_resort
    • charter_or_scope
    • product_list
    • disclosure_policy
    • contact_info.websites
    • contact_info.emails
    • partner_role_type
    • partner_country
    • advisory_locations
    • advisory_location_require_credentials
    • vulnerability_advisory_location_for_web_scraping
    • industry
    • tl_root_start_date
    • is_cna_discussion_list
    ", "operationId": "registryOrgUpdateSingle", "parameters": [ { @@ -8193,4 +8193,4 @@ } } } -} +} \ No newline at end of file diff --git a/datadump/pre-population/orgs.json b/datadump/pre-population/orgs.json index fcee2199c..b85d8f4ea 100644 --- a/datadump/pre-population/orgs.json +++ b/datadump/pre-population/orgs.json @@ -323,11 +323,6 @@ } }, { - "authority": { - "active_roles": [ - "CNA" - ] - }, "name": "Chase, May and Jones", "short_name": "sister_20", "time": { @@ -338,4 +333,4 @@ "id_quota": 1408 } } -] \ No newline at end of file +] diff --git a/docker/README.md b/docker/README.md index 310e181d1..e56229d02 100644 --- a/docker/README.md +++ b/docker/README.md @@ -89,9 +89,9 @@ You should see the following: > NODE_ENV=development node-dev src/scripts/populate.js 2022-06-07 19:58:32 [info]: "Successfully connected to database!" -Are you sure you wish to pre-populate the database for the development environment? Doing so will drop and rebuild the database, run migrations, and populate the Org, BaseOrg, User, BaseUser, Glossary collection(s) in the cve_dev database. (y/n) y +Are you sure you wish to pre-populate the database for the development environment? Doing so will drop and rebuild the database, populate the Org, BaseOrg, User, BaseUser, Glossary collection(s), and run migrations in the cve_dev database. (y/n) y -2022-06-07 19:58:37 [info]: "Dropping cve_dev database before running migrations..." +2022-06-07 19:58:37 [info]: "Dropping cve_dev database before population..." 2022-06-07 19:58:37 [info]: "Successfully dropped cve_dev database." 2022-06-07 19:58:37 [info]: "Populating Org collection..." 2022-06-07 19:58:37 [info]: "Org populated!" @@ -103,6 +103,7 @@ Are you sure you wish to pre-populate the database for the development environme 2022-06-07 19:58:38 [info]: "BaseUser populated!" 2022-06-07 19:58:38 [info]: "Populating Glossary collection..." 2022-06-07 19:58:39 [info]: "Glossary populated!" +2022-06-07 19:58:39 [info]: "Successfully ran database migrations." 2022-06-07 19:58:39 [info]: "Successfully populated the database!" ``` diff --git a/migrations/20260903-01-set-registry-org-disabled.js b/migrations/20260903-01-set-registry-org-disabled.js new file mode 100644 index 000000000..a51f28984 --- /dev/null +++ b/migrations/20260903-01-set-registry-org-disabled.js @@ -0,0 +1,29 @@ +const ACTIVE_ROLE_PATH = 'authority.active_roles.0' + +module.exports = { + async up (db) { + const baseOrgCollection = db.collection('BaseOrg') + const orgCollection = db.collection('Org') + + await baseOrgCollection.updateMany( + {}, + { $set: { disabled: true } } + ) + + const enabledOrgUUIDs = (await orgCollection.distinct( + 'UUID', + { [ACTIVE_ROLE_PATH]: { $exists: true } } + )).filter(UUID => typeof UUID === 'string' && UUID.length > 0) + + if (enabledOrgUUIDs.length > 0) { + await baseOrgCollection.updateMany( + { UUID: { $in: enabledOrgUUIDs } }, + { $set: { disabled: false } } + ) + } + }, + + async down () { + // Existing migration values cannot be distinguished from later Secretariat updates. + } +} diff --git a/schemas/registry-org/BaseOrg.json b/schemas/registry-org/BaseOrg.json index 3b2801d62..01956a1ef 100644 --- a/schemas/registry-org/BaseOrg.json +++ b/schemas/registry-org/BaseOrg.json @@ -77,6 +77,11 @@ "long_name": { "$ref": "#/definitions/longName" }, + "disabled": { + "type": "boolean", + "default": true, + "description": "Indicates whether the organization is disabled." + }, "new_short_name": { "$ref": "#/definitions/shortName" }, diff --git a/schemas/registry-org/CNAOrg.json b/schemas/registry-org/CNAOrg.json index 5f0033637..7c366abec 100644 --- a/schemas/registry-org/CNAOrg.json +++ b/schemas/registry-org/CNAOrg.json @@ -15,6 +15,9 @@ "long_name": { "$ref": "/BaseOrg#/definitions/longName" }, + "disabled": { + "$ref": "/BaseOrg#/properties/disabled" + }, "new_short_name": { "description": "Used to rename an organization's short name during an update.", "type": "string", diff --git a/schemas/registry-org/RootOrg.json b/schemas/registry-org/RootOrg.json index 8f1b48cd7..4f03b97d5 100644 --- a/schemas/registry-org/RootOrg.json +++ b/schemas/registry-org/RootOrg.json @@ -15,6 +15,9 @@ "long_name": { "$ref": "/BaseOrg#/definitions/longName" }, + "disabled": { + "$ref": "/BaseOrg#/properties/disabled" + }, "new_short_name": { "description": "Used to rename an organization's short name during an update.", "type": "string", diff --git a/schemas/registry-org/create-registry-org-request.json b/schemas/registry-org/create-registry-org-request.json index 4fdb7f4bd..9278be8fa 100644 --- a/schemas/registry-org/create-registry-org-request.json +++ b/schemas/registry-org/create-registry-org-request.json @@ -9,6 +9,11 @@ "type": "string", "description": "Full name of the organization" }, + "disabled": { + "type": "boolean", + "default": true, + "description": "Indicates whether the organization is disabled. This field can only be modified by the Secretariat." + }, "short_name": { "type": "string", "description": "Short name or acronym of the organization" diff --git a/schemas/registry-org/create-registry-org-response.json b/schemas/registry-org/create-registry-org-response.json index 609298060..115c2a7ca 100644 --- a/schemas/registry-org/create-registry-org-response.json +++ b/schemas/registry-org/create-registry-org-response.json @@ -20,6 +20,10 @@ "type": "string", "description": "Full name of the organization" }, + "disabled": { + "type": "boolean", + "description": "Indicates whether the organization is disabled" + }, "short_name": { "type": "string", "description": "Short name or acronym of the organization" diff --git a/schemas/registry-org/get-registry-org-response.json b/schemas/registry-org/get-registry-org-response.json index 83c1077f3..ae40b965c 100644 --- a/schemas/registry-org/get-registry-org-response.json +++ b/schemas/registry-org/get-registry-org-response.json @@ -17,6 +17,10 @@ "type": "string", "description": "Full name of the organization" }, + "disabled": { + "type": "boolean", + "description": "Indicates whether the organization is disabled" + }, "aliases": { "type": "array", "items": { diff --git a/schemas/registry-org/list-registry-orgs-response.json b/schemas/registry-org/list-registry-orgs-response.json index ce5c5ba43..84f3f4b80 100644 --- a/schemas/registry-org/list-registry-orgs-response.json +++ b/schemas/registry-org/list-registry-orgs-response.json @@ -46,6 +46,10 @@ "type": "string", "description": "Full name of the organization" }, + "disabled": { + "type": "boolean", + "description": "Indicates whether the organization is disabled" + }, "aliases": { "type": "array", "items": { diff --git a/schemas/registry-org/update-registry-org-request.json b/schemas/registry-org/update-registry-org-request.json index 71ee3f92a..80d4eed99 100644 --- a/schemas/registry-org/update-registry-org-request.json +++ b/schemas/registry-org/update-registry-org-request.json @@ -9,6 +9,10 @@ "type": "string", "description": "Full name of the organization" }, + "disabled": { + "type": "boolean", + "description": "Indicates whether the organization is disabled. This field can only be modified by the Secretariat." + }, "short_name": { "type": "string", "description": "Short name or acronym of the organization" diff --git a/schemas/registry-org/update-registry-org-response.json b/schemas/registry-org/update-registry-org-response.json index eb6a10f75..a8d0e39e8 100644 --- a/schemas/registry-org/update-registry-org-response.json +++ b/schemas/registry-org/update-registry-org-response.json @@ -20,6 +20,10 @@ "type": "string", "description": "Full name of the organization" }, + "disabled": { + "type": "boolean", + "description": "Indicates whether the organization is disabled" + }, "short_name": { "type": "string", "description": "Short name or acronym of the organization" diff --git a/src/constants/index.js b/src/constants/index.js index 034b570dc..756611e8e 100644 --- a/src/constants/index.js +++ b/src/constants/index.js @@ -49,6 +49,7 @@ function getConstants () { ORG_EXCLUDED_FIELDS: ['__t', '__v', '_id', 'inUse', 'in_use'], ORG_RESTRICTED_FIELDS: ['program_data'], SECRETARIAT_ONLY_FIELDS: [ + 'disabled', 'partner_number', 'program_data', 'program_data.cve_website_update_date', diff --git a/src/model/baseorg.js b/src/model/baseorg.js index 0b5fa666e..0a720f8f3 100644 --- a/src/model/baseorg.js +++ b/src/model/baseorg.js @@ -10,6 +10,7 @@ const schema = { UUID: String, long_name: String, short_name: String, + disabled: { type: Boolean, default: true }, aliases: [String], authority: [String], top_level_root: String, diff --git a/src/scripts/populate.js b/src/scripts/populate.js index fd69a07cd..8a71d552a 100644 --- a/src/scripts/populate.js +++ b/src/scripts/populate.js @@ -211,11 +211,10 @@ async function populateBaseUserCollection () { logger.info('BaseUser populated!') } -async function resetDatabaseAndRunMigrations () { - logger.info(`Dropping ${migrationConfig.mongodb.databaseName} database before running migrations...`) +async function resetDatabase () { + logger.info(`Dropping ${migrationConfig.mongodb.databaseName} database before population...`) await db.db.dropDatabase() logger.info(`Successfully dropped ${migrationConfig.mongodb.databaseName} database.`) - await runDatabaseMigrations() } async function populateSeedCollections () { @@ -273,13 +272,14 @@ async function populateDatabase () { } else { userInput = dataUtils.getUserPopulateInput( Object.keys(populateTheseCollections), - `drop and rebuild the database, run migrations, and populate the ${Object.keys(populateTheseCollections).join(', ')} collection(s)` + `drop and rebuild the database, populate the ${Object.keys(populateTheseCollections).join(', ')} collection(s), and run migrations` ) } if (userInput.toLowerCase() === 'y') { - await resetDatabaseAndRunMigrations() + await resetDatabase() await populateSeedCollections() + await runDatabaseMigrations() logger.info('Successfully populated the database!') console.log('Successfully populated the database!') } diff --git a/test/integration-tests/registry-org/registryOrgCRUDTest.js b/test/integration-tests/registry-org/registryOrgCRUDTest.js index c8d6b21db..37a599bfe 100644 --- a/test/integration-tests/registry-org/registryOrgCRUDTest.js +++ b/test/integration-tests/registry-org/registryOrgCRUDTest.js @@ -65,6 +65,9 @@ describe('Testing /registry/org endpoints', () => { expect(res.body.created).to.haveOwnProperty('authority') expect(res.body.created.authority).to.deep.equal(['CNA']) + expect(res.body.created).to.haveOwnProperty('disabled') + expect(res.body.created.disabled).to.equal(true) + expect(res.body.created).to.haveOwnProperty('id_quota') expect(res.body.created.id_quota).to.equal(testRegistryOrg.id_quota) @@ -676,6 +679,7 @@ describe('Testing /registry/org endpoints', () => { partner_number: 'Updated Partner Number', partner_country: 'UK', advisory_locations: ['https://example.com/updated_advisories'], + disabled: false, is_last_resort: false }) .then((res, err) => { @@ -699,6 +703,10 @@ describe('Testing /registry/org endpoints', () => { expect(res.body.updated).to.haveOwnProperty('authority') expect(res.body.updated.authority).to.deep.equal(['CNA']) + expect(res.body.updated).to.haveOwnProperty('disabled') + expect(res.body.updated.disabled).to.equal(false) + createdOrg.disabled = false + expect(res.body.updated).to.haveOwnProperty('id_quota') expect(res.body.updated.id_quota).to.equal(createdOrg.id_quota) @@ -723,6 +731,7 @@ describe('Testing /registry/org endpoints', () => { .then((res) => { expect(res).to.have.status(200) expect(res.body.is_last_resort).to.equal(false) + expect(res.body.disabled).to.equal(false) }) }) it('Allows Secretariat to update program_data', async () => { @@ -1135,6 +1144,16 @@ describe('Testing /registry/org endpoints', () => { expect(res.body.message).to.equal('The following fields can only be modified by the Secretariat: program_data, program_data.status.') }) }) + it('Fails to allow an admin to change the disabled flag', async () => { + await chai.request(app) + .put('/api/registry/org/win_5') + .set(constants.nonSecretariatUserHeaders2) + .send({ disabled: true }) + .then((res) => { + expect(res).to.have.status(403) + expect(res.body.message).to.equal('The following fields can only be modified by the Secretariat: disabled.') + }) + }) it('Fails to update a registry organization providing an erroneous key not found in the schema', async () => { await chai.request(app) .put('/api/registry/org/registry_org_test') diff --git a/test/integration-tests/registry-org/registryOrgDisabledMigrationTest.js b/test/integration-tests/registry-org/registryOrgDisabledMigrationTest.js new file mode 100644 index 000000000..75d9c5c4d --- /dev/null +++ b/test/integration-tests/registry-org/registryOrgDisabledMigrationTest.js @@ -0,0 +1,29 @@ +/* eslint-disable no-unused-expressions */ +const chai = require('chai') +const expect = chai.expect +chai.use(require('chai-http')) + +const constants = require('../constants.js') +const app = require('../../../src/index.js') + +const secretariatHeaders = { ...constants.headers, 'content-type': 'application/json' } + +describe('Registry organization disabled migration', () => { + it('enables a seeded registry organization whose legacy organization has an active authority', async () => { + const res = await chai.request(app) + .get('/api/registry/org/interesting_19') + .set(secretariatHeaders) + + expect(res).to.have.status(200) + expect(res.body.disabled).to.equal(false) + }) + + it('disables the seeded registry organization whose legacy organization has no authority', async () => { + const res = await chai.request(app) + .get('/api/registry/org/sister_20') + .set(secretariatHeaders) + + expect(res).to.have.status(200) + expect(res.body.disabled).to.equal(true) + }) +}) From 6160c96bc853181a2781f22748c5810594cf4e97 Mon Sep 17 00:00:00 2001 From: James Dalphond Date: Thu, 3 Sep 2026 15:17:25 -0400 Subject: [PATCH 25/27] fix: preserve disabled flag when omitted from org updates --- src/repositories/baseOrgRepositoryHelpers.js | 4 +++- .../conversation/editConversationTest.js | 14 ++++++++++++++ .../registry-org/registryOrgCRUDTest.js | 19 +++++++++++++++++++ .../registry-org/rootOrgTest.js | 3 +++ 4 files changed, 39 insertions(+), 1 deletion(-) diff --git a/src/repositories/baseOrgRepositoryHelpers.js b/src/repositories/baseOrgRepositoryHelpers.js index 0f9ae264e..3556fd770 100644 --- a/src/repositories/baseOrgRepositoryHelpers.js +++ b/src/repositories/baseOrgRepositoryHelpers.js @@ -123,7 +123,9 @@ async function processJointApprovalAndMerge (registryOrg, legacyOrg, registryObj const protectedFields = ['_id', 'UUID', '__v', '__t', 'created', 'last_updated', 'createdAt', 'updatedAt', 'users', 'admins', 'inUse', 'in_use'] let registryProtectedFields = [...protectedFields] if (!isSecretariat) { - registryProtectedFields = [...registryProtectedFields, ...getConstants().ORG_RESTRICTED_FIELDS] + // disabled is publicly visible but can only be supplied by the Secretariat. + // Preserve its stored value when a non-Secretariat full update omits it. + registryProtectedFields = [...registryProtectedFields, ...getConstants().ORG_RESTRICTED_FIELDS, 'disabled'] } let updatedRegistryOrg = null diff --git a/test/integration-tests/conversation/editConversationTest.js b/test/integration-tests/conversation/editConversationTest.js index 9697a2173..5ce64bf8d 100644 --- a/test/integration-tests/conversation/editConversationTest.js +++ b/test/integration-tests/conversation/editConversationTest.js @@ -16,6 +16,7 @@ const orgAdminHeaders = { describe('Testing Conversation edit by index endpoint', () => { let org + let disabled before(async () => { await chai @@ -26,6 +27,7 @@ describe('Testing Conversation edit by index endpoint', () => { expect(err).to.be.undefined expect(res).to.have.status(200) org = res.body + disabled = org.disabled delete org.created delete org.last_updated delete org.admins @@ -33,6 +35,7 @@ describe('Testing Conversation edit by index endpoint', () => { delete org.top_level_root delete org.oversees delete org.program_data + delete org.disabled }) await chai @@ -86,6 +89,17 @@ describe('Testing Conversation edit by index endpoint', () => { }) context('Positive Tests', () => { + it('Preserves disabled when omitted from an org admin update', async () => { + await chai.request(app) + .get('/api/registry/org/activity_6') + .set(constants.headers) + .then((res, err) => { + expect(err).to.be.undefined + expect(res).to.have.status(200) + expect(res.body.disabled).to.equal(disabled) + }) + }) + it('Should update own conversation as org admin', async () => { await chai.request(app) .put('/api/registry/org/activity_6/conversation/0') diff --git a/test/integration-tests/registry-org/registryOrgCRUDTest.js b/test/integration-tests/registry-org/registryOrgCRUDTest.js index 37a599bfe..3dfb2e3f9 100644 --- a/test/integration-tests/registry-org/registryOrgCRUDTest.js +++ b/test/integration-tests/registry-org/registryOrgCRUDTest.js @@ -1154,6 +1154,25 @@ describe('Testing /registry/org endpoints', () => { expect(res.body.message).to.equal('The following fields can only be modified by the Secretariat: disabled.') }) }) + it('Fails to allow an admin to supply the unchanged disabled flag', async () => { + let disabled + await chai.request(app) + .get('/api/registry/org/win_5') + .set(secretariatHeaders) + .then((res) => { + expect(res).to.have.status(200) + disabled = res.body.disabled + }) + + await chai.request(app) + .put('/api/registry/org/win_5') + .set(constants.nonSecretariatUserHeaders2) + .send({ disabled }) + .then((res) => { + expect(res).to.have.status(403) + expect(res.body.message).to.equal('The following fields can only be modified by the Secretariat: disabled.') + }) + }) it('Fails to update a registry organization providing an erroneous key not found in the schema', async () => { await chai.request(app) .put('/api/registry/org/registry_org_test') diff --git a/test/integration-tests/registry-org/rootOrgTest.js b/test/integration-tests/registry-org/rootOrgTest.js index e61254eaf..25d98d495 100644 --- a/test/integration-tests/registry-org/rootOrgTest.js +++ b/test/integration-tests/registry-org/rootOrgTest.js @@ -221,6 +221,9 @@ describe('Testing ROOT Organization Type', () => { context('ROOT admin permissions', () => { before(async () => { + // Non-Secretariat callers must omit Secretariat-only fields from PUT payloads. + delete createdOrg.disabled + // Create a Root Admin user await chai.request(app) .post(`/api/registry/org/${testRootOrg.short_name}/user`) From 87feb7253d209647ec088f2daf3550b5a9487ea2 Mon Sep 17 00:00:00 2001 From: James Dalphond Date: Tue, 8 Sep 2026 14:15:49 -0400 Subject: [PATCH 26/27] optional migrations on populate (for testing) --- README.md | 7 +++++++ src/scripts/populate.js | 14 ++++++++++---- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index ae6909a7c..86faf0f38 100644 --- a/README.md +++ b/README.md @@ -97,6 +97,13 @@ You can reset the local development database, run pending database migrations, a npm run populate:dev ``` +To populate without automatically applying versioned migrations, use the following command, then run the migrations manually when ready: + +```sh +npm run populate:dev -- --skip-migrations +npm run db:migrate:dev +``` + If the database only needs versioned data migrations without a local data reset, check the migration status and then apply pending migrations: ```sh diff --git a/src/scripts/populate.js b/src/scripts/populate.js index 8a71d552a..b4b7a94f5 100644 --- a/src/scripts/populate.js +++ b/src/scripts/populate.js @@ -26,6 +26,8 @@ const error = new errors.IDRError() const appEnv = process.env.NODE_ENV || 'development' const localPopulateEnvironments = new Set(['development', 'test']) +const populateArguments = process.argv.slice(2) +const skipMigrations = populateArguments.includes('--skip-migrations') const populateTheseCollections = { Cve: Cve, 'Cve-Id-Range': CveIdRange, @@ -267,19 +269,23 @@ db.on('error', (err) => { async function populateDatabase () { let userInput - if (process.argv.length > 2 && process.argv.slice(2)[0] === 'y') { - userInput = process.argv.slice(2)[0] + if (populateArguments.includes('y')) { + userInput = 'y' } else { userInput = dataUtils.getUserPopulateInput( Object.keys(populateTheseCollections), - `drop and rebuild the database, populate the ${Object.keys(populateTheseCollections).join(', ')} collection(s), and run migrations` + `drop and rebuild the database, populate the ${Object.keys(populateTheseCollections).join(', ')} collection(s)${skipMigrations ? '' : ', and run migrations'}` ) } if (userInput.toLowerCase() === 'y') { await resetDatabase() await populateSeedCollections() - await runDatabaseMigrations() + if (skipMigrations) { + logger.info('Database migrations were skipped. Run npm run db:migrate:dev to apply them manually.') + } else { + await runDatabaseMigrations() + } logger.info('Successfully populated the database!') console.log('Successfully populated the database!') } From 605c2a07d9a7381f08505d816a6c0b5465864d00 Mon Sep 17 00:00:00 2001 From: James Dalphond Date: Tue, 8 Sep 2026 14:26:53 -0400 Subject: [PATCH 27/27] missing info in seed data --- src/scripts/populate.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/scripts/populate.js b/src/scripts/populate.js index b4b7a94f5..b39034332 100644 --- a/src/scripts/populate.js +++ b/src/scripts/populate.js @@ -156,6 +156,9 @@ function buildBaseOrgDocument (org, allUsers) { product_list: null, id_quota: org.policies?.id_quota, admins: admins, + program_data: { + status: 'active' + }, private_contacts: [], contact_info: { emails: email ? [email] : [],