diff --git a/api-docs/openapi.json b/api-docs/openapi.json
index 84fe59bcf..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": {
@@ -3199,6 +3199,79 @@
}
}
},
+ "/registry/org/cnas": {
+ "get": {
+ "tags": [
+ "Registry Organization"
+ ],
+ "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 partner-list format",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "../schemas/registry-org/list-active-cnas-response.json"
+ }
+ }
+ }
+ },
+ "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": {
+ "application/json": {
+ "schema": {
+ "$ref": "../schemas/errors/generic.json"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
"/registry/org/{shortname}/users": {
"get": {
"tags": [
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..ffd862fff
--- /dev/null
+++ b/schemas/registry-org/list-active-cnas-response.json
@@ -0,0 +1,98 @@
+{
+ "$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",
+ "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", "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 2735f8cdf..6a8eb9832 100644
--- a/src/controller/registry.controller/index.js
+++ b/src/controller/registry.controller/index.js
@@ -92,6 +92,64 @@ 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 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 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: {
+ 'application/json': {
+ schema: { $ref: '../schemas/errors/generic.json' }
+ }
+ }
+ }
+ */
+ mw.useRegistry(),
+ mw.validateUser,
+ mw.onlySecretariat,
+ 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..2d54e9356 100644
--- a/src/controller/registry.controller/org.registry.controller.js
+++ b/src/controller/registry.controller/org.registry.controller.js
@@ -150,6 +150,74 @@ 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 active CNA partners in the CVE.org partner-list response format.
+ *
+ * @async
+ * @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 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) {
+ try {
+ 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.
*
@@ -1005,6 +1073,8 @@ 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..9f8150276 100644
--- a/src/repositories/baseOrgRepository.js
+++ b/src/repositories/baseOrgRepository.js
@@ -25,6 +25,50 @@ 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 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 => {
+ const normalized = normalizeOrgReference(reference)
+ if (normalized && !orgByReference.has(normalized)) orgByReference.set(normalized, org)
+ })
+}
+
+function decorateCnaRelationships (activeOrgs, hierarchyOrgs) {
+ const orgByReference = new Map()
+ ;[...activeOrgs, ...hierarchyOrgs].forEach(org => addOrgReferences(orgByReference, org))
+
+ return activeOrgs.map(org => {
+ 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 (normalizedTlrReference === 'true') {
+ tlr = hasOwnTlrReference ? org : root
+ } else if (isUsableTlrReference(tlrReference)) {
+ tlr = orgByReference.get(normalizeOrgReference(tlrReference))
+ }
+
+ return {
+ ...org,
+ _root: root,
+ _tlr: tlr
+ }
+ })
+}
+
function isResponseExtensionField (key) {
return key.startsWith('_') && !INTERNAL_UNDERSCORE_FIELDS.includes(key)
}
@@ -562,6 +606,52 @@ class BaseOrgRepository extends BaseRepository {
return data
}
+ /**
+ * Retrieves active CNA organizations for the CVE.org partner-list response.
+ *
+ * The status comparison supports normalized lowercase and legacy title-case data.
+ *
+ * @returns {Promise