diff --git a/api-docs/openapi.json b/api-docs/openapi.json
index 84fe59bcf..3031f2cf1 100644
--- a/api-docs/openapi.json
+++ b/api-docs/openapi.json
@@ -1,7 +1,7 @@
{
"openapi": "3.0.2",
"info": {
- "version": "2.8.4",
+ "version": "2.8.5",
"title": "CVE Services API",
"description": "The CVE Services API supports automation tooling for the CVE Program. Credentials are required for most service endpoints. Representatives of CVE Numbering Authorities (CNAs) should use one of the methods below to obtain credentials:
- If your organization already has an Organizational Administrator (OA) account for the CVE Services, ask your admin for credentials
- Contact your Root (Google, INCIBE, JPCERT/CC, or Red Hat) or Top-Level Root (CISA ICS or MITRE) to request credentials
CVE data is to be in the JSON 5.2 CVE Record format. Details of the JSON 5.2 schema are located here.
Contact the CVE Services team",
"contact": {
diff --git a/package.json b/package.json
index 00409c63f..b63447db2 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"name": "cve-services",
"author": "Automation Working Group",
- "version": "2.8.4",
+ "version": "2.8.5",
"license": "(CC0)",
"devDependencies": {
"@faker-js/faker": "^7.6.0",
diff --git a/schemas/registry-org/CNAOrg.json b/schemas/registry-org/CNAOrg.json
index c852f1e79..8e32aa932 100644
--- a/schemas/registry-org/CNAOrg.json
+++ b/schemas/registry-org/CNAOrg.json
@@ -89,6 +89,9 @@
"minimum": 0,
"maximum": 100000
},
+ "is_last_resort": {
+ "type": "boolean"
+ },
"oversees": {
"type": "array",
"uniqueItems": true,
diff --git a/schemas/registry-org/RootOrg.json b/schemas/registry-org/RootOrg.json
index a6e8a9d7e..a60691ee5 100644
--- a/schemas/registry-org/RootOrg.json
+++ b/schemas/registry-org/RootOrg.json
@@ -107,6 +107,15 @@
},
"top_level_root": {
"$ref": "/BaseOrg#/properties/top_level_root"
+ },
+ "is_top_level_root": {
+ "type": "boolean"
+ },
+ "charter_or_scope": {
+ "type": "string"
+ },
+ "disclosure_policy": {
+ "type": "string"
}
},
"required": [
diff --git a/schemas/registry-org/create-registry-org-request.json b/schemas/registry-org/create-registry-org-request.json
index b1917cf18..2e583abc4 100644
--- a/schemas/registry-org/create-registry-org-request.json
+++ b/schemas/registry-org/create-registry-org-request.json
@@ -44,6 +44,14 @@
"type": "string",
"description": "Indicates if the organization is a root or top-level root"
},
+ "is_top_level_root": {
+ "type": "boolean",
+ "description": "Indicates whether a ROOT organization is top-level"
+ },
+ "is_last_resort": {
+ "type": "boolean",
+ "description": "Indicates whether a CNA organization is a CNA of last resort."
+ },
"users": {
"type": "array",
"items": {
diff --git a/schemas/registry-org/create-registry-org-response.json b/schemas/registry-org/create-registry-org-response.json
index 2071119a1..5161b509d 100644
--- a/schemas/registry-org/create-registry-org-response.json
+++ b/schemas/registry-org/create-registry-org-response.json
@@ -77,6 +77,14 @@
"type": "string",
"description": "Indicates if the organization is a root or top-level root"
},
+ "is_top_level_root": {
+ "type": "boolean",
+ "description": "Indicates whether a ROOT organization is top-level"
+ },
+ "is_last_resort": {
+ "type": "boolean",
+ "description": "Indicates whether a CNA organization is a CNA of last resort"
+ },
"users": {
"type": "array",
"items": {
diff --git a/schemas/registry-org/get-registry-org-response.json b/schemas/registry-org/get-registry-org-response.json
index 86fc75326..a1ba1fc1a 100644
--- a/schemas/registry-org/get-registry-org-response.json
+++ b/schemas/registry-org/get-registry-org-response.json
@@ -41,6 +41,14 @@
"type": "string",
"description": "Indicates if the organization is a root or top-level root"
},
+ "is_top_level_root": {
+ "type": "boolean",
+ "description": "Indicates whether a ROOT organization is top-level"
+ },
+ "is_last_resort": {
+ "type": "boolean",
+ "description": "Indicates whether a CNA organization is a CNA of last resort"
+ },
"reports_to": {
"type": [
"string",
diff --git a/schemas/registry-org/list-registry-orgs-response.json b/schemas/registry-org/list-registry-orgs-response.json
index e4956e545..40bc74add 100644
--- a/schemas/registry-org/list-registry-orgs-response.json
+++ b/schemas/registry-org/list-registry-orgs-response.json
@@ -70,6 +70,14 @@
"type": "string",
"description": "Indicates if the organization is a root or top-level root"
},
+ "is_top_level_root": {
+ "type": "boolean",
+ "description": "Indicates whether a ROOT organization is top-level"
+ },
+ "is_last_resort": {
+ "type": "boolean",
+ "description": "Indicates whether a CNA organization is a CNA of last resort"
+ },
"reports_to": {
"type": [
"string",
diff --git a/schemas/registry-org/update-registry-org-request.json b/schemas/registry-org/update-registry-org-request.json
index f388a40f7..60cbe400a 100644
--- a/schemas/registry-org/update-registry-org-request.json
+++ b/schemas/registry-org/update-registry-org-request.json
@@ -60,6 +60,14 @@
"type": "string",
"description": "Indicates if the organization is a root or top-level root"
},
+ "is_top_level_root": {
+ "type": "boolean",
+ "description": "Indicates whether a ROOT organization is top-level"
+ },
+ "is_last_resort": {
+ "type": "boolean",
+ "description": "Indicates whether a CNA organization is a CNA of last resort"
+ },
"users": {
"type": "array",
"items": {
diff --git a/schemas/registry-org/update-registry-org-response.json b/schemas/registry-org/update-registry-org-response.json
index 4662e4872..387ef9f87 100644
--- a/schemas/registry-org/update-registry-org-response.json
+++ b/schemas/registry-org/update-registry-org-response.json
@@ -66,6 +66,14 @@
"type": "string",
"description": "Indicates if the organization is a root or top-level root"
},
+ "is_top_level_root": {
+ "type": "boolean",
+ "description": "Indicates whether a ROOT organization is top-level"
+ },
+ "is_last_resort": {
+ "type": "boolean",
+ "description": "Indicates whether a CNA organization is a CNA of last resort"
+ },
"users": {
"type": "array",
"items": {
diff --git a/src/constants/index.js b/src/constants/index.js
index 4a6c1d38b..034b570dc 100644
--- a/src/constants/index.js
+++ b/src/constants/index.js
@@ -44,7 +44,7 @@ function getConstants () {
USER_ROLES: [
'ADMIN'
],
- JOINT_APPROVAL_FIELDS: ['short_name', 'long_name', 'authority', 'aliases', 'oversees', 'top_level_root', 'charter_or_scope', 'product_list', 'disclosure_policy', 'partner_role_type', 'partner_number', 'program_data.cve_website_update_date', 'program_data.cve_website_update_needed', 'program_data.status', 'advisory_locations', 'advisory_location_require_credentials', 'vulnerability_advisory_location_for_web_scraping', 'tl_root_start_date', 'is_cna_discussion_list', 'id_quota'],
+ JOINT_APPROVAL_FIELDS: ['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', 'partner_role_type', 'partner_number', 'partner_country', 'program_data.cve_website_update_date', 'program_data.cve_website_update_needed', 'program_data.status', 'advisory_locations', 'advisory_location_require_credentials', 'vulnerability_advisory_location_for_web_scraping', 'industry', 'tl_root_start_date', 'is_cna_discussion_list', 'id_quota'],
JOINT_APPROVAL_FIELDS_LEGACY: ['short_name', 'name', 'authority.active_roles', 'policies.id_quota'],
ORG_EXCLUDED_FIELDS: ['__t', '__v', '_id', 'inUse', 'in_use'],
ORG_RESTRICTED_FIELDS: ['program_data'],
diff --git a/src/controller/registry.controller/index.js b/src/controller/registry.controller/index.js
index 2735f8cdf..7350a564d 100644
--- a/src/controller/registry.controller/index.js
+++ b/src/controller/registry.controller/index.js
@@ -557,6 +557,8 @@ router.put('/registry/org/:shortname',
aliases
oversees
top_level_root
+ is_top_level_root
+ is_last_resort
charter_or_scope
product_list
disclosure_policy
diff --git a/src/model/cnaorg.js b/src/model/cnaorg.js
index 2e7519c1e..20a7e083a 100644
--- a/src/model/cnaorg.js
+++ b/src/model/cnaorg.js
@@ -14,6 +14,7 @@ const validate = ajv.compile(CnaOrgSchema)
const schema = {
oversees: [String],
id_quota: Number,
+ is_last_resort: Boolean,
charter_or_scope: String,
disclosure_policy: String,
product_list: String
diff --git a/src/model/rootorg.js b/src/model/rootorg.js
index 1517ec65c..ad4dbeea9 100644
--- a/src/model/rootorg.js
+++ b/src/model/rootorg.js
@@ -12,7 +12,10 @@ ajv.addSchema(BaseOrgSchema)
const validate = ajv.compile(RootOrgSchema)
const schema = {
- oversees: [String]
+ oversees: [String],
+ is_top_level_root: Boolean,
+ charter_or_scope: String,
+ disclosure_policy: String
}
const options = { discriminatorKey: 'kind' }
diff --git a/src/swagger.js b/src/swagger.js
index 5edc22173..fd775f5cd 100644
--- a/src/swagger.js
+++ b/src/swagger.js
@@ -21,7 +21,7 @@ const fullCnaContainerRequest = require('../schemas/cve/create-cve-record-cna-re
/* eslint-disable no-multi-str */
const doc = {
info: {
- version: '2.8.4',
+ version: '2.8.5',
title: 'CVE Services API',
description: "The CVE Services API supports automation tooling for the CVE Program. Credentials are \
required for most service endpoints. Representatives of \
diff --git a/test/integration-tests/registry-org/registryOrgCRUDTest.js b/test/integration-tests/registry-org/registryOrgCRUDTest.js
index 8b0cf5320..c8d6b21db 100644
--- a/test/integration-tests/registry-org/registryOrgCRUDTest.js
+++ b/test/integration-tests/registry-org/registryOrgCRUDTest.js
@@ -19,7 +19,8 @@ const testRegistryOrg = {
partner_number: 'Initial Partner Number',
partner_country: 'US',
advisory_locations: ['https://example.com/advisories'],
- charter_or_scope: 'This is a normal string, not a URI'
+ charter_or_scope: 'This is a normal string, not a URI',
+ is_last_resort: true
}
let createdOrg
@@ -82,6 +83,9 @@ describe('Testing /registry/org endpoints', () => {
expect(res.body.created).to.haveOwnProperty('charter_or_scope')
expect(res.body.created.charter_or_scope).to.equal(testRegistryOrg.charter_or_scope)
+ expect(res.body.created).to.haveOwnProperty('is_last_resort')
+ expect(res.body.created.is_last_resort).to.equal(true)
+
expect(res.body.created).to.haveOwnProperty('program_data')
expect(res.body.created.program_data.status).to.equal('inactive')
expect(res.body.created.program_data).to.haveOwnProperty('partner_inactive_date')
@@ -671,7 +675,8 @@ describe('Testing /registry/org endpoints', () => {
partner_role_type: ['Researcher', 'Vendor'],
partner_number: 'Updated Partner Number',
partner_country: 'UK',
- advisory_locations: ['https://example.com/updated_advisories']
+ advisory_locations: ['https://example.com/updated_advisories'],
+ is_last_resort: false
})
.then((res, err) => {
expect(err).to.be.undefined
@@ -708,6 +713,16 @@ describe('Testing /registry/org endpoints', () => {
expect(res.body.updated).to.haveOwnProperty('advisory_locations')
expect(res.body.updated.advisory_locations).to.deep.equal(['https://example.com/updated_advisories'])
+ expect(res.body.updated.is_last_resort).to.equal(false)
+ })
+ })
+ it('Retrieves the CNA last-resort flag', async () => {
+ await chai.request(app)
+ .get('/api/registry/org/registry_org_test')
+ .set(secretariatHeaders)
+ .then((res) => {
+ expect(res).to.have.status(200)
+ expect(res.body.is_last_resort).to.equal(false)
})
})
it('Allows Secretariat to update program_data', async () => {
diff --git a/test/integration-tests/registry-org/registryOrgWithJointReviewTest.js b/test/integration-tests/registry-org/registryOrgWithJointReviewTest.js
index 28cd37220..6ff18ccdd 100644
--- a/test/integration-tests/registry-org/registryOrgWithJointReviewTest.js
+++ b/test/integration-tests/registry-org/registryOrgWithJointReviewTest.js
@@ -175,7 +175,13 @@ describe('Testing Joint approval', () => {
await chai.request(app)
.put('/api/registry/org/non_secretariat_org')
.set(nonAdminHeaders)
- .send({ ...testRegistryOrgForReview, short_name: 'new_non_secretariat_org', contact_info: { websites: ['https://www.example.com'] } })
+ .send({
+ ...testRegistryOrgForReview,
+ short_name: 'new_non_secretariat_org',
+ contact_info: { websites: ['https://www.example.com'] },
+ partner_country: 'US',
+ industry: 'Technology'
+ })
.then((res) => {
expect(res).to.have.status(200)
expect(res.body.message).to.contain('organization was successfully updated, but joint approval is required for some fields.')
@@ -192,6 +198,9 @@ describe('Testing Joint approval', () => {
expect(res.body).to.have.property('status', 'pending')
expect(res.body.target_object_uuid).to.equal(orgUUID)
expect(res.body.new_review_data.short_name).to.equal('new_non_secretariat_org')
+ expect(res.body.new_review_data.contact_info.websites).to.deep.equal(['https://www.example.com'])
+ expect(res.body.new_review_data.partner_country).to.equal('US')
+ expect(res.body.new_review_data.industry).to.equal('Technology')
reviewUUID = res.body.uuid
})
})
@@ -203,7 +212,9 @@ describe('Testing Joint approval', () => {
expect(err).to.be.undefined
expect(res).to.have.status(200)
expect(res.body.short_name).to.equal('non_secretariat_org')
- expect(res.body.contact_info.websites[0]).to.equal('https://www.example.com')
+ expect((res.body.contact_info && res.body.contact_info.websites) || []).to.not.include('https://www.example.com')
+ expect(res.body).to.not.haveOwnProperty('partner_country')
+ expect(res.body).to.not.haveOwnProperty('industry')
})
})
it('Secretariat can approve the ORG review with body parameter', async function () {
@@ -306,7 +317,7 @@ describe('Testing Joint approval', () => {
expect(err).to.be.undefined
expect(res).to.have.status(200)
expect(res.body.short_name).to.equal('non_with_comments')
- expect(res.body.contact_info.websites[0]).to.equal('https://www.example.com')
+ expect((res.body.contact_info && res.body.contact_info.websites) || []).to.not.include('https://www.example.com')
})
})
it('Secretariat leaves a public comment on the org review', async () => {
@@ -380,6 +391,7 @@ describe('Testing Joint approval', () => {
expect(res).to.have.status(200)
expect(res.body.short_name).to.equal('new_non_with_comments')
expect(res.body.id_quota).to.equal(1000)
+ expect(res.body.contact_info.websites[0]).to.equal('https://www.example.com')
})
})
})
@@ -500,7 +512,7 @@ describe('Testing Joint approval', () => {
expect(res.body.message).to.contain('organization was successfully updated, but joint approval is required for some fields.')
expect(res.body.updated.UUID).to.equal(orgUUID)
expect(res.body.updated.short_name).to.equal(testRegistryOrgForNewShortNameReview.short_name)
- expect(res.body.updated.contact_info.websites[0]).to.equal('https://www.example.com/new-short-name')
+ expect((res.body.updated.contact_info && res.body.updated.contact_info.websites) || []).to.not.include('https://www.example.com/new-short-name')
})
})
it('Check to see if the new_short_name review was created', async () => {
diff --git a/test/integration-tests/registry-org/rootOrgTest.js b/test/integration-tests/registry-org/rootOrgTest.js
index 18c4bd7e1..e61254eaf 100644
--- a/test/integration-tests/registry-org/rootOrgTest.js
+++ b/test/integration-tests/registry-org/rootOrgTest.js
@@ -13,7 +13,10 @@ let rootAdminHeaders
const testRootOrg = {
short_name: 'root_org_test_4',
long_name: 'Root Org Test',
- authority: ['ROOT']
+ authority: ['ROOT'],
+ is_top_level_root: true,
+ charter_or_scope: 'Coordinates CVE Numbering Authorities.',
+ disclosure_policy: 'Discloses vulnerabilities after coordinated remediation.'
}
let createdOrg
let reportingOrg
@@ -31,6 +34,9 @@ describe('Testing ROOT Organization Type', () => {
expect(res).to.have.status(200)
expect(res.body.message).to.equal(testRootOrg.short_name + ' organization was successfully created.')
expect(res.body.created.authority).to.deep.equal(['ROOT'])
+ expect(res.body.created.is_top_level_root).to.equal(true)
+ expect(res.body.created.charter_or_scope).to.equal(testRootOrg.charter_or_scope)
+ expect(res.body.created.disclosure_policy).to.equal(testRootOrg.disclosure_policy)
createdOrg = res.body.created
delete createdOrg.created
delete createdOrg.last_updated
@@ -55,6 +61,34 @@ describe('Testing ROOT Organization Type', () => {
expect(res.body.message).to.equal('Parameters were invalid')
})
})
+
+ it('Secretariat can update and retrieve ROOT-specific fields', async () => {
+ await chai.request(app)
+ .put(`/api/registry/org/${testRootOrg.short_name}`)
+ .set(secretariatHeaders)
+ .send({
+ ...createdOrg,
+ is_top_level_root: false,
+ charter_or_scope: 'Updated ROOT charter.',
+ disclosure_policy: 'Updated ROOT disclosure policy.'
+ })
+ .then((res) => {
+ expect(res).to.have.status(200)
+ expect(res.body.updated.is_top_level_root).to.equal(false)
+ expect(res.body.updated.charter_or_scope).to.equal('Updated ROOT charter.')
+ expect(res.body.updated.disclosure_policy).to.equal('Updated ROOT disclosure policy.')
+ })
+
+ await chai.request(app)
+ .get(`/api/registry/org/${testRootOrg.short_name}`)
+ .set(secretariatHeaders)
+ .then((res) => {
+ expect(res).to.have.status(200)
+ expect(res.body.is_top_level_root).to.equal(false)
+ expect(res.body.charter_or_scope).to.equal('Updated ROOT charter.')
+ expect(res.body.disclosure_policy).to.equal('Updated ROOT disclosure policy.')
+ })
+ })
})
context('Managing reports-to relationships', () => {
@@ -211,11 +245,13 @@ describe('Testing ROOT Organization Type', () => {
})
it('ROOT admin can update their own org', async () => {
+ const rootAdminPayload = { ...createdOrg }
+ delete rootAdminPayload.is_top_level_root
await chai.request(app)
.put(`/api/registry/org/${testRootOrg.short_name}`)
.set(rootAdminHeaders)
.send({
- ...createdOrg,
+ ...rootAdminPayload,
long_name: 'Updated Root Org Test'
})
.then((res) => {
@@ -238,12 +274,38 @@ describe('Testing ROOT Organization Type', () => {
})
})
- it('ROOT admin cannot set oversees through the standard PUT endpoint', async () => {
+ it('ROOT admin can request a change to is_top_level_root through joint approval', async () => {
await chai.request(app)
.put(`/api/registry/org/${testRootOrg.short_name}`)
.set(rootAdminHeaders)
.send({
...createdOrg,
+ is_top_level_root: true
+ })
+ .then((res) => {
+ expect(res).to.have.status(200)
+ expect(res.body.message).to.include('joint approval is required')
+ expect(res.body.updated.is_top_level_root).to.equal(false)
+ })
+
+ return chai.request(app)
+ .get(`/api/review/org/${testRootOrg.short_name}`)
+ .set(secretariatHeaders)
+ .then((res) => {
+ expect(res).to.have.status(200)
+ expect(res.body.status).to.equal('pending')
+ expect(res.body).to.have.nested.property('new_review_data.is_top_level_root', true)
+ })
+ })
+
+ it('ROOT admin cannot set oversees through the standard PUT endpoint', async () => {
+ const rootAdminPayload = { ...createdOrg }
+ delete rootAdminPayload.is_top_level_root
+ await chai.request(app)
+ .put(`/api/registry/org/${testRootOrg.short_name}`)
+ .set(rootAdminHeaders)
+ .send({
+ ...rootAdminPayload,
oversees: [reportingOrg.UUID]
})
.then((res) => {
diff --git a/test/integration-tests/review-object/reviewObjectTest.js b/test/integration-tests/review-object/reviewObjectTest.js
index d9fbe661f..a22410f31 100644
--- a/test/integration-tests/review-object/reviewObjectTest.js
+++ b/test/integration-tests/review-object/reviewObjectTest.js
@@ -215,6 +215,7 @@ describe('Review Object Controller Integration Tests', () => {
updateData.long_name = 'Approve Test Organization'
updateData.authority = ['CNA']
updateData.id_quota = 1000
+ updateData.is_last_resort = true
updateData.contact_info = { websites: ['https://www.example.com'] }
const res = await chai
.request(app)
@@ -222,7 +223,7 @@ describe('Review Object Controller Integration Tests', () => {
.set({ ...constants.nonSecretariatUserHeaders2 })
.send(updateData)
expect(res).to.have.status(200)
- expect(res.body.updated.contact_info.websites[0]).to.equal('https://www.example.com')
+ expect((res.body.updated.contact_info && res.body.updated.contact_info.websites) || []).to.not.include('https://www.example.com')
const reviewRes = await chai
.request(app)
@@ -232,6 +233,7 @@ describe('Review Object Controller Integration Tests', () => {
expect(reviewRes.body).to.have.property('uuid')
expect(reviewRes.body.status).to.equal('pending')
expect(reviewRes.body).to.have.nested.property('new_review_data.long_name', 'Approve Test Organization')
+ expect(reviewRes.body).to.have.nested.property('new_review_data.is_last_resort', true)
expect(reviewRes.body.new_review_data.contact_info.websites[0]).to.equal('https://www.example.com')
approveTestReviewUUID = reviewRes.body.uuid
})