From fecb05cbd0b1ae0758c129401345ac3cd11a4523 Mon Sep 17 00:00:00 2001 From: Leo Williamson Date: Tue, 18 Aug 2026 15:13:05 -0400 Subject: [PATCH 1/5] 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 2/5] 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 3/5] 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 b99fe5d3057988596706bb70a723fa4a5ea3264c Mon Sep 17 00:00:00 2001 From: David T Rocca Date: Wed, 2 Sep 2026 14:44:53 -0400 Subject: [PATCH 4/5] 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 30820b0d7b2eb12c990bfeb2c758227b565e4470 Mon Sep 17 00:00:00 2001 From: David T Rocca Date: Tue, 8 Sep 2026 11:08:51 -0400 Subject: [PATCH 5/5] 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',