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:

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 })