diff --git a/api-docs/openapi.json b/api-docs/openapi.json index 0f88bb0dc..de6ecbf6f 100644 --- a/api-docs/openapi.json +++ b/api-docs/openapi.json @@ -3,10 +3,10 @@ "info": { "version": "2.5.0", "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.1 CVE Record format. Details of the JSON 5.1 schema are located here.
Contact the CVE Services team", + "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.1 CVE Record format. Details of the JSON 5.1 schema are located here.
Contact the CVE Services team", "contact": { "name": "CVE Services Overview", - "url": "https://cveproject.github.io/automation-cve-services#services-overview" + "url": "https://www.cve.org/AllResources/CveServices" } }, "servers": [ @@ -1323,6 +1323,9 @@ }, { "$ref": "#/components/parameters/apiSecretHeader" + }, + { + "$ref": "#/components/parameters/erlCheck" } ], "responses": { @@ -1432,6 +1435,9 @@ }, { "$ref": "#/components/parameters/apiSecretHeader" + }, + { + "$ref": "#/components/parameters/erlCheck" } ], "responses": { @@ -3036,6 +3042,15 @@ "type": "string" } }, + "erlCheck": { + "in": "query", + "name": "erlcheck", + "description": "Enables stricter validation that ensures submitted record meets enrichment data requirements. For a record to be enriched, a CVSS score and a CWE ID must be provided.", + "required": false, + "schema": { + "type": "boolean" + } + }, "batch_type": { "in": "query", "name": "batch_type", diff --git a/src/controller/cve.controller/cve.controller.js b/src/controller/cve.controller/cve.controller.js index d8e62db7a..e112b233a 100644 --- a/src/controller/cve.controller/cve.controller.js +++ b/src/controller/cve.controller/cve.controller.js @@ -4,6 +4,7 @@ const errors = require('./error') const getConstants = require('../../constants').getConstants const error = new errors.CveControllerError() const booleanIsTrue = require('../../utils/utils').booleanIsTrue +const isEnrichedContainer = require('../../utils/utils').isEnrichedContainer const url = process.env.NODE_ENV === 'staging' ? 'https://test.cve.org/' : 'https://cve.org/' // Helper function to create providerMetadata object @@ -458,6 +459,14 @@ async function submitCna (req, res, next) { const orgUuid = await orgRepo.getOrgUUID(req.ctx.org) const userUuid = await userRepo.getUserUUID(req.ctx.user, orgUuid) + // To avoid breaking legacy behavior in the "booleanIsTrue" function, we need to check to make sure that undefined is set to false + let erlCheck + if (typeof req.query.erlcheck === 'undefined') { + erlCheck = false + } else { + erlCheck = booleanIsTrue(req.query.erlcheck) || false + } + // check that cve id exists let result = await cveIdRepo.findOneByCveId(id) if (!result || result.state === CONSTANTS.CVE_STATES.AVAILABLE) { @@ -477,10 +486,15 @@ async function submitCna (req, res, next) { return res.status(403).json(error.cveRecordExists()) } + const cnaContainer = req.ctx.body.cnaContainer + if (erlCheck && !isEnrichedContainer(cnaContainer)) { + // Process the ERL check here + return res.status(403).json(error.erlCheckFailed()) + } + // create full cve record here const owningCna = await orgRepo.findOneByUUID(cveId.owning_cna) const assignerShortName = owningCna.short_name - const cnaContainer = req.ctx.body.cnaContainer const dateUpdated = (new Date()).toISOString() const additionalCveMetadataFields = { assignerShortName: assignerShortName, @@ -541,6 +555,14 @@ async function updateCna (req, res, next) { const orgUuid = await orgRepo.getOrgUUID(req.ctx.org) const userUuid = await userRepo.getUserUUID(req.ctx.user, orgUuid) + // To avoid breaking legacy behavior in the "booleanIsTrue" function, we need to check to make sure that undefined is set to false + let erlCheck + if (typeof req.query.erlcheck === 'undefined') { + erlCheck = false + } else { + erlCheck = booleanIsTrue(req.query.erlcheck) || false + } + // check that cve id exists let result = await cveIdRepo.findOneByCveId(id) if (!result || result.state === CONSTANTS.CVE_STATES.AVAILABLE) { @@ -560,9 +582,14 @@ async function updateCna (req, res, next) { return res.status(403).json(error.cveRecordDne()) } + const cnaContainer = req.ctx.body.cnaContainer + if (erlCheck && !isEnrichedContainer(cnaContainer)) { + // Process the ERL check here + return res.status(403).json(error.erlCheckFailed()) + } + // update cve record here const cveRecord = result.cve - const cnaContainer = req.ctx.body.cnaContainer const dateUpdated = (new Date()).toISOString() cveRecord.cveMetadata.dateUpdated = dateUpdated diff --git a/src/controller/cve.controller/error.js b/src/controller/cve.controller/error.js index a270f0c75..2ccd9b3df 100644 --- a/src/controller/cve.controller/error.js +++ b/src/controller/cve.controller/error.js @@ -111,6 +111,13 @@ class CveControllerError extends idrErr.IDRError { err.message = 'The ADP data does not begin with adpContainer.' return err } + + erlCheckFailed () { + const err = {} + err.error = 'ERL_CHECK_FAILED' + err.message = 'The ERL check failed. The CNA container is not enriched and the ERLCheck flag is set.' + return err + } } module.exports = { diff --git a/src/controller/cve.controller/index.js b/src/controller/cve.controller/index.js index d7ae0f586..3d6ef4570 100644 --- a/src/controller/cve.controller/index.js +++ b/src/controller/cve.controller/index.js @@ -549,7 +549,8 @@ router.post('/cve/:id/cna', #swagger.parameters['$ref'] = [ '#/components/parameters/apiEntityHeader', '#/components/parameters/apiUserHeader', - '#/components/parameters/apiSecretHeader' + '#/components/parameters/apiSecretHeader', + '#/components/parameters/erlCheck' ] #swagger.requestBody = { description: 'CVE data is to be in the JSON 5.1 CVE Record format. Details of the JSON 5.1 schema are \ - located here.
\ + located here.\ Contact the CVE Services team", contact: { name: 'CVE Services Overview', - url: 'https://cveproject.github.io/automation-cve-services#services-overview' + url: 'https://www.cve.org/AllResources/CveServices' } }, @@ -168,6 +168,15 @@ const doc = { type: 'string' } }, + erlCheck: { + in: 'query', + name: 'erlcheck', + description: 'Enables stricter validation that ensures submitted record meets enrichment data requirements. For a record to be enriched, a CVSS score and a CWE ID must be provided.', + required: false, + schema: { + type: 'boolean' + } + }, batch_type: { in: 'query', name: 'batch_type', diff --git a/src/utils/utils.js b/src/utils/utils.js index f6ec5d2a1..68b31aa14 100644 --- a/src/utils/utils.js +++ b/src/utils/utils.js @@ -123,6 +123,7 @@ function reqCtxMapping (req, keyType, keys) { } // Return true if boolean is 0, true, or yes, with any mix of casing +// Please note that this function does NOT evaluate "undefined" as false. - A tired developer who lost way too much time to this. function booleanIsTrue (val) { if ((val.toString() === '1') || (val.toString().toLowerCase() === 'true') || @@ -153,12 +154,22 @@ function toDate (val) { return result } +function isEnrichedContainer (container) { + const hasCvss = container?.metrics?.some(item => 'cvssV4_0' in item || 'cvssV3_1' in item || 'cvssV3_0' in item || 'cvssV2_0' in item) + const hasCwe = container?.problemTypes?.some(pItem => pItem?.descriptions?.some(dItem => 'cweId' in dItem)) + if (!(hasCvss && hasCwe)) { + return false + } + return true +} + module.exports = { isSecretariat, isBulkDownload, isAdmin, isAdminUUID, isSecretariatUUID, + isEnrichedContainer, getOrgUUID, getUserUUID, reqCtxMapping, diff --git a/test/integration-tests/constants.js b/test/integration-tests/constants.js index 87ef53c09..292602f14 100644 --- a/test/integration-tests/constants.js +++ b/test/integration-tests/constants.js @@ -241,6 +241,114 @@ const testAdp2 = { } } +const enrichedCve = { + + cnaContainer: { + affected: [ + { + vendor: 'n/da', + product: 'n/a', + versions: [ + { + version: 'n/a', + status: 'unknown' + } + ] + } + ], + descriptions: [ + { + lang: 'en', + value: "Cross-site scdfgfdgripting (XSS) vulnerability in Revive Adserver before 4.0.1 allows remote authenticated users to inject arbitrary web script or HTML via the user's email address." + } + ], + problemTypes: [ + { + descriptions: [ + { + description: 'n/a', + lang: 'eng', + type: 'text', + cweId: 'CWE-79' + } + ] + } + ], + providerMetadata: { + orgId: '9cbfeea8-dea2-4923-b772-1ab41730e742' + }, + references: [ + { + name: '[oss-security] 20170202 Re: CVE request: multiples vulnerabilities in Revive Adserver', + url: 'http://www.openwall.com/lists/oss-security/2017/02/02/3' + }, + { + name: 'https://www.revive-adserver.com/security/revive-sa-2017-001/', + url: 'https://www.revive-adserver.com/security/revive-sa-2017-001/' + }, + { + name: '95dsf875', + url: 'http://www.securityfocus.com/bid/95875' + } + ], + metrics: [ + { + format: 'CVSS', + scenarios: [ + { + lang: 'en', + value: 'GENERAL' + } + ], + cvssV4_0: { + baseScore: 7.8, + baseSeverity: 'HIGH', + vectorString: 'CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:N/SC:H/SI:L/SA:L', + version: '4.0' + }, + cvssV3_1: { + version: '3.1', + attackVector: 'NETWORK', + attackComplexity: 'LOW', + privilegesRequired: 'NONE', + userInteraction: 'NONE', + scope: 'UNCHANGED', + confidentialityImpact: 'HIGH', + integrityImpact: 'HIGH', + availabilityImpact: 'HIGH', + baseScore: 9.8, + baseSeverity: 'CRITICAL', + vectorString: 'CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H' + } + }, + { + format: 'CVSS', + scenarios: [ + { + lang: 'en', + value: "If the enhanced host protection mode is turned on, this vulnerability can only be exploited to run os commands as user 'nobody'. Privilege escalation is not possible." + } + ], + cvssV3_1: { + version: '3.1', + attackVector: 'NETWORK', + attackComplexity: 'LOW', + privilegesRequired: 'NONE', + userInteraction: 'NONE', + scope: 'UNCHANGED', + confidentialityImpact: 'LOW', + integrityImpact: 'LOW', + availabilityImpact: 'LOW', + baseScore: 7.3, + baseSeverity: 'HIGH', + vectorString: 'CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:L/A:L' + } + } + ] + } + +} + const testOrg = { short_name: 'test_org', @@ -278,6 +386,7 @@ module.exports = { nonSecretariatUserHeadersWithAdp2, testCve, testCveEdited, + enrichedCve, testAdp, testAdp2, testOrg, diff --git a/test/integration-tests/cve-id/getCveIdTest.js b/test/integration-tests/cve-id/getCveIdTest.js index 3209b822e..41dc671e0 100644 --- a/test/integration-tests/cve-id/getCveIdTest.js +++ b/test/integration-tests/cve-id/getCveIdTest.js @@ -12,7 +12,7 @@ const app = require('../../../src/index.js') describe('Testing Get CVE-ID endpoint', () => { // TODO: Update this test to dynamically calculate reserved count. - const RESESRVED_COUNT = 120 + const RESESRVED_COUNT = 122 const YEAR_COUNT = 10 const PUB_YEAR_COUNT = 4 const TIME_WINDOW_COUNT = 40 diff --git a/test/integration-tests/cve/erlCheckPOSTTest.js b/test/integration-tests/cve/erlCheckPOSTTest.js new file mode 100644 index 000000000..32da1e905 --- /dev/null +++ b/test/integration-tests/cve/erlCheckPOSTTest.js @@ -0,0 +1,80 @@ +/* eslint-disable no-unused-expressions */ + +const chai = require('chai') +chai.use(require('chai-http')) + +const expect = chai.expect + +const constants = require('../constants.js') +const app = require('../../../src/index.js') +const helpers = require('../helpers.js') + +const requestLength = 1 +const shortName = 'win_5' +const cveYear = '2023' +const batchType = 'non-sequential' + +describe('Testing POST ERLCheck field', () => { + let cveId + beforeEach(async () => { + cveId = await helpers.cveIdReserveHelper(requestLength, cveYear, shortName, batchType) + }) + context('ERL POST Check Tests', () => { + it('POST CVE that is ERL Checked with correct details', async () => { + await chai.request(app) + .post(`/api/cve/${cveId}/cna?erlcheck=true`) + .set(constants.nonSecretariatUserHeaders) + .send(constants.enrichedCve) + .then((res, err) => { + // Safety Expect + expect(err).to.be.undefined + expect(res).to.have.status(200) + }) + }) + + it('POST CVE that is ERL checked with the incorrect details', async () => { + await chai.request(app) + .post(`/api/cve/${cveId}/cna?erlcheck=true`) + .set(constants.nonSecretariatUserHeaders) + .send(constants.testCve) + .then((res, err) => { + // Safety Expect + expect(res).to.have.status(403) + }) + }) + + it('POST CVE that is ERL is false with correct details', async () => { + await chai.request(app) + .post(`/api/cve/${cveId}/cna?erlcheck=false`) + .set(constants.nonSecretariatUserHeaders) + .send(constants.enrichedCve) + .then((res, err) => { + // Safety Expect + expect(err).to.be.undefined + expect(res).to.have.status(200) + }) + }) + + it('POST CVE that is ERL is false with the incorrect details', async () => { + await chai.request(app) + .post(`/api/cve/${cveId}/cna?erlcheck=false`) + .set(constants.nonSecretariatUserHeaders) + .send(constants.testCve) + .then((res, err) => { + // Safety Expect + expect(res).to.have.status(200) + }) + }) + + it('POST CVE that is ERL is null with the incorrect details', async () => { + await chai.request(app) + .post(`/api/cve/${cveId}/cna?erlcheck=null`) + .set(constants.nonSecretariatUserHeaders) + .send(constants.testCve) + .then((res, err) => { + // Safety Expect + expect(res).to.have.status(400) + }) + }) + }) +}) diff --git a/test/integration-tests/cve/erlCheckPUTTest.js b/test/integration-tests/cve/erlCheckPUTTest.js new file mode 100644 index 000000000..bec8bd1c6 --- /dev/null +++ b/test/integration-tests/cve/erlCheckPUTTest.js @@ -0,0 +1,83 @@ +/* eslint-disable no-unused-expressions */ + +const chai = require('chai') +chai.use(require('chai-http')) + +const expect = chai.expect + +const constants = require('../constants.js') +const app = require('../../../src/index.js') +const helpers = require('../helpers.js') + +const requestLength = 1 +const shortName = 'win_5' +const cveYear = '2023' +const batchType = 'non-sequential' + +describe('Testing PUT ERLCheck field', () => { + let cveId + before(async () => { + cveId = await helpers.cveIdReserveHelper(requestLength, cveYear, shortName, batchType) + await helpers.cveRequestAsCnaHelper(cveId) + console.log('HEre') + }) + context('ERL PUT Check Tests', () => { + it('PUT CVE that is ERL Checked with correct details', async () => { + await chai.request(app) + .put(`/api/cve/${cveId}/cna?erlcheck=true`) + .set(constants.nonSecretariatUserHeaders) + .send(constants.enrichedCve) + .then((res, err) => { + // Safety Expect + expect(err).to.be.undefined + expect(res).to.have.status(200) + }) + }) + + it('PUT CVE that is ERL checked with the incorrect details', async () => { + await chai.request(app) + .put(`/api/cve/${cveId}/cna?erlcheck=true`) + .set(constants.nonSecretariatUserHeaders) + .send(constants.testCve) + .then((res, err) => { + // Safety Expect + expect(res).to.have.status(403) + }) + }) + + it('PUT CVE that is ERL is false with correct details', async () => { + await chai.request(app) + .put(`/api/cve/${cveId}/cna?erlcheck=false`) + .set(constants.nonSecretariatUserHeaders) + .send(constants.enrichedCve) + .then((res, err) => { + // Safety Expect + expect(err).to.be.undefined + expect(res).to.have.status(200) + }) + }) + + it('PUT CVE that is ERL is false with the incorrect details', async () => { + await chai.request(app) + .put(`/api/cve/${cveId}/cna?erlcheck=false`) + .set(constants.nonSecretariatUserHeaders) + .send(constants.testCve) + .then((res, err) => { + // Safety Expect + expect(res).to.have.status(200) + }) + }) + + it('PUT CVE that is ERL is null with correct details', async () => { + await chai.request(app) + .put(`/api/cve/${cveId}/cna?erlcheck=null`) + .set(constants.nonSecretariatUserHeaders) + .send(constants.enrichedCve) + .then((res, err) => { + // Safety Expect + expect(err).to.be.undefined + expect(res).to.have.status(400) + }) + }) + }) +}) diff --git a/test/unit-tests/cve/updateCnaTest.js b/test/unit-tests/cve/updateCnaTest.js index 7b7beda39..b72cb009e 100644 --- a/test/unit-tests/cve/updateCnaTest.js +++ b/test/unit-tests/cve/updateCnaTest.js @@ -108,6 +108,9 @@ describe('updateCna function', () => { body: { cnaContainer: cnaContainerCopy } + }, + query: { + erlcheck: 'false' } } })