diff --git a/src/controller/cve-id.controller/cve-id.middleware.js b/src/controller/cve-id.controller/cve-id.middleware.js index 7c157f416..dead9984c 100644 --- a/src/controller/cve-id.controller/cve-id.middleware.js +++ b/src/controller/cve-id.controller/cve-id.middleware.js @@ -17,10 +17,11 @@ function parsePostParams (req, res, next) { // Sanitizer for dates function toDate (val) { - let value = val.match(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}$/) + let value = val.match(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(|Z|((-|\+|\s)\d{2}:\d{2}))$/) let result if (value) { - result = new Date(`${value[0]}.000+00:00`) + value[0] = value[0].replace(' ', '+') // Re-add literal '+' which was stripped + result = new Date(value[0]) } else { value = val.match(/^\d{4}-\d{2}-\d{2}$/) if (value) { diff --git a/src/controller/cve-id.controller/index.js b/src/controller/cve-id.controller/index.js index 1cd9fc4f9..ff7cc5ed1 100644 --- a/src/controller/cve-id.controller/index.js +++ b/src/controller/cve-id.controller/index.js @@ -37,8 +37,8 @@ router.get('/cve-id/:id', parseGetParams, controller.CVEID_GET_SINGLE) router.put('/cve-id/:id', - mw.onlyCnas, mw.validateUser, + mw.onlyCnas, param(['id']).isString().matches(/^CVE-[0-9]{4}-[0-9]{4,}$/, 'i'), query(['state']).optional().isString().trim().escape().customSanitizer(val => { return val.toUpperCase() }).isIn(CHOICES), query(['org']).optional().isString().trim().escape(), diff --git a/src/controller/cve.controller/cve.middleware.js b/src/controller/cve.controller/cve.middleware.js index dacacde55..03ead98a2 100644 --- a/src/controller/cve.controller/cve.middleware.js +++ b/src/controller/cve.controller/cve.middleware.js @@ -27,10 +27,11 @@ function parseGetParams (req, res, next) { // Sanitizer for dates function toDate (val) { - let value = val.match(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}$/) + let value = val.match(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(|Z|((-|\+|\s)\d{2}:\d{2}))$/) let result if (value) { - result = new Date(`${value[0]}.000+00:00`) + value[0] = value[0].replace(' ', '+') // Re-add literal '+' which was stripped + result = new Date(value[0]) } else { value = val.match(/^\d{4}-\d{2}-\d{2}$/) if (value) { @@ -52,19 +53,19 @@ function parseError (req, res, next) { next() } -function onlyOneEnglishDescription (arr) { - const arrayLength = arr.length - let numEnglishFound = 0 // for checking how many times an english field shows up - for (var i = 0; i < arrayLength; i++) { - if (arr[i].lang === 'en') { - numEnglishFound += 1 - } - - if (numEnglishFound > 1) { // return error if more than 1 english description is found - return null +function uniqueEnglishDescription (rejectedReasonsArr) { + const langArray = rejectedReasonsArr.map(function (reason) { return reason.lang.toLowerCase() })// create arr of lowercase lang values + const foundValues = new Set() // set to hold languages found + // loop through the lang array and find duplicates + for (var i = 0; i < langArray.length; i++) { + if (langArray[i].startsWith('en')) { // check case only if lang starts with "en" + if (foundValues.has(langArray[i])) { + return false // duplicate found so return false + } + foundValues.add(langArray[i]) // add each unique value to set } } - return numEnglishFound + return true // if no duplicate found, then all lang values were unique } function validateRejectBody (req, res, next) { @@ -108,6 +109,6 @@ module.exports = { parseError, toDate, validateCveCnaContainerJsonSchema, - onlyOneEnglishDescription, + uniqueEnglishDescription, validateRejectBody } diff --git a/src/controller/cve.controller/index.js b/src/controller/cve.controller/index.js index cc0779660..15267d049 100644 --- a/src/controller/cve.controller/index.js +++ b/src/controller/cve.controller/index.js @@ -3,7 +3,7 @@ const router = express.Router() const mw = require('../../middleware/middleware') const controller = require('./cve.controller') const { body, param, query } = require('express-validator') -const { parseGetParams, parsePostParams, parseError, toDate, validateCveCnaContainerJsonSchema, validateRejectBody, onlyOneEnglishDescription } = require('./cve.middleware') +const { parseGetParams, parsePostParams, parseError, toDate, validateCveCnaContainerJsonSchema, validateRejectBody, uniqueEnglishDescription } = require('./cve.middleware') const CONSTANTS = require('../../constants') const CHOICES = [CONSTANTS.CVE_STATES.REJECTED, CONSTANTS.CVE_STATES.PUBLISHED] @@ -56,8 +56,8 @@ router.put('/cve/:id', controller.CVE_UPDATE_SINGLE) router.post('/cve/:id/cna', - mw.onlyCnas, mw.validateUser, + mw.onlyCnas, validateCveCnaContainerJsonSchema, param(['id']).isString().matches(/^CVE-[0-9]{4}-[0-9]{4,}$/i), parseError, @@ -66,8 +66,8 @@ router.post('/cve/:id/cna', controller.CVE_SUBMIT_CNA) router.put('/cve/:id/cna', - mw.onlyCnas, mw.validateUser, + mw.onlyCnas, validateCveCnaContainerJsonSchema, param(['id']).isString().matches(/^CVE-[0-9]{4}-[0-9]{4,}$/i), parseError, @@ -76,12 +76,12 @@ router.put('/cve/:id/cna', controller.CVE_UPDATE_CNA) router.post('/cve/:id/reject', - mw.onlyCnas, mw.validateUser, + mw.onlyCnas, validateRejectBody, param(['id']).isString().matches(/^CVE-[0-9]{4}-[0-9]{4,}$/i), body(['cnaContainer.rejectedReasons']).isArray().custom((arr) => { - if (onlyOneEnglishDescription(arr) !== 1) { + if (!uniqueEnglishDescription(arr)) { throw new Error(400, 'Bad request, more than one English description found') } return true @@ -93,12 +93,12 @@ router.post('/cve/:id/reject', controller.CVE_REJECT_RECORD) router.put('/cve/:id/reject', - mw.onlyCnas, mw.validateUser, + mw.onlyCnas, validateRejectBody, param(['id']).isString().matches(/^CVE-[0-9]{4}-[0-9]{4,}$/i), body(['cnaContainer.rejectedReasons']).isArray().custom((arr) => { - if (onlyOneEnglishDescription(arr) !== 1) { + if (!uniqueEnglishDescription(arr)) { throw new Error(400, 'Bad request, more than one English description found') } return true diff --git a/src/middleware/error.js b/src/middleware/error.js index 4e7ea779c..6648e11b8 100644 --- a/src/middleware/error.js +++ b/src/middleware/error.js @@ -28,6 +28,13 @@ class MiddlewareError extends idrErr.IDRError { return err } + cnaDoesNotExist (shortname) { // mw + const err = {} + err.error = 'CNA_DOES_NOT_EXIST' + err.message = `The '${shortname}' organization designated by the shortname parameter does not exist.` + return err + } + orgDoesNotOwnId (org, id) { const err = { error: 'ORG_DOES_NOT_OWN_ID', @@ -54,7 +61,14 @@ class MiddlewareError extends idrErr.IDRError { return err } - recordTooLarge () { + genericBadRequest (errors) { // mw + const err = {} + err.error = 'BAD_REQUEST' + err.message = errors + return err + } + + recordTooLarge () { // mw const err = {} err.error = 'RECORD_TOO_LARGE' err.message = 'Records must be less than 16MB.' diff --git a/src/middleware/middleware.js b/src/middleware/middleware.js index 5070a981a..82646c7d9 100644 --- a/src/middleware/middleware.js +++ b/src/middleware/middleware.js @@ -129,7 +129,10 @@ async function onlyCnas (req, res, next) { try { const org = await orgRepo.findOneByShortName(shortName) // org exists - if (org.authority.active_roles.includes(CONSTANTS.AUTH_ROLE_ENUM.SECRETARIAT)) { + if (org === null) { + logger.info({ uuid: req.ctx.uuid, message: shortName + ' is NOT a ' + CONSTANTS.AUTH_ROLE_ENUM.CNA }) + return res.status(404).json(error.cnaDoesNotExist(shortName)) + } else if (org.authority.active_roles.includes(CONSTANTS.AUTH_ROLE_ENUM.SECRETARIAT)) { logger.info({ uuid: req.ctx.uuid, message: org.short_name + ' is a ' + CONSTANTS.AUTH_ROLE_ENUM.SECRETARIAT + ' so until Root organizations are implemented this role is allowed.' }) next() } else if (org.authority.active_roles.includes(CONSTANTS.AUTH_ROLE_ENUM.CNA)) { // the org is a CNA @@ -207,14 +210,18 @@ function validateCveJsonSchema (req, res, next) { function validateJsonSyntax (err, req, res, next) { if (err.status && err.message) { - if (err.message.includes('Unexpected token')) { + if (err.message.includes('request entity too large')) { + console.warn('Request failed validation because entity too large') + console.info((JSON.stringify(err))) + return res.status(413).json(error.recordTooLarge(errors)) + } else if (err.status === 400) { console.warn('Request failed validation because JSON syntax is incorrect') console.info((JSON.stringify(err))) return res.status(400).json(error.invalidJsonSyntax(err.message)) - } else if (err.message.includes('request entity too large')) { - console.warn('Request failed validation because entity too large') + } else { + console.warn('Request failed') console.info((JSON.stringify(err))) - return res.status(413).json(error.recordTooLarge(errors)) + return res.status(400).json(error.genericBadRequest(err.message)) } } else { next(err) diff --git a/test-http/src/test/cve_id_tests/cve_id_as_org_admin.py b/test-http/src/test/cve_id_tests/cve_id_as_org_admin.py index dc5a359e2..06d091b62 100644 --- a/test-http/src/test/cve_id_tests/cve_id_as_org_admin.py +++ b/test-http/src/test/cve_id_tests/cve_id_as_org_admin.py @@ -317,10 +317,12 @@ def test_get_cve_id_by_time_modified(org_admin_headers): n_ids = 10 time.sleep(1) t_before = dt.datetime.now().strftime('%Y-%m-%dT%H:%M:%S') + t_before_alt = dt.datetime.now().strftime('%Y-%m-%dT%H:%M:%S+00:00') time.sleep(1) res_ids = get_reserve_cve_ids(n_ids, utils.CURRENT_YEAR, org_admin_headers['CVE-API-ORG']) time.sleep(1) t_after = dt.datetime.now().strftime('%Y-%m-%dT%H:%M:%S') + t_after_alt = dt.datetime.now().strftime('%Y-%m-%dT%H:%M:%S+00:00') res_get_ids = requests.get( f'{env.AWG_BASE_URL}{CVE_ID_URL}', @@ -330,8 +332,19 @@ def test_get_cve_id_by_time_modified(org_admin_headers): 'time_modified.gt': t_before } ) + # Test alternate timezone format + res_get_ids_alt = requests.get( + f'{env.AWG_BASE_URL}{CVE_ID_URL}', + headers=utils.BASE_HEADERS, + params={ + 'time_modified.lt': t_after_alt, + 'time_modified.gt': t_before_alt + } + ) ok_response_contains(res_get_ids, f'CVE-{utils.CURRENT_YEAR}-') assert len(json.loads(res_get_ids.content.decode())['cve_ids']) == n_ids + ok_response_contains(res_get_ids_alt, f'CVE-{utils.CURRENT_YEAR}-') + assert len(json.loads(res_get_ids_alt.content.decode())['cve_ids']) == n_ids def test_get_cve_id_with_params(org_admin_headers): @@ -470,4 +483,4 @@ def get_reserve_cve_ids( 'cve_year': f'{year}', 'short_name': cna_short_name } - ) \ No newline at end of file + ) diff --git a/test-http/src/test/cve_tests/cve.py b/test-http/src/test/cve_tests/cve.py index 766e65b54..c0f02d5b1 100644 --- a/test-http/src/test/cve_tests/cve.py +++ b/test-http/src/test/cve_tests/cve.py @@ -36,12 +36,14 @@ def test_get_cve_by_time_modified(): time.sleep(1) post_cve('CVE-2021-0005_published', 'CVE-2021-0005') t_before = dt.datetime.now().strftime('%Y-%m-%dT%H:%M:%S') + t_before_alt = dt.datetime.now().strftime('%Y-%m-%dT%H:%M:%S+00:00') time.sleep(1) update_cve('CVE-2021-0004_published', 'CVE-2021-0004') time.sleep(1) update_cve('CVE-2021-0005_published', 'CVE-2021-0005') time.sleep(1) t_after = dt.datetime.now().strftime('%Y-%m-%dT%H:%M:%S') + t_after_alt = dt.datetime.now().strftime('%Y-%m-%dT%H:%M:%S+00:00') res = requests.get( f'{env.AWG_BASE_URL}{CVE_URL}/', @@ -51,8 +53,19 @@ def test_get_cve_by_time_modified(): 'time_modified.gt': t_before } ) + # Alternat timezone format + res_alt = requests.get( + f'{env.AWG_BASE_URL}{CVE_URL}/', + headers=utils.BASE_HEADERS, + params={ + 'time_modified.lt': t_after_alt, + 'time_modified.gt': t_before_alt + } + ) assert res.status_code == utils.HTTP_OK assert len(json.loads(res.content.decode())['cveRecords']) >= 2 + assert res_alt.status_code == utils.HTTP_OK + assert len(json.loads(res_alt.content.decode())['cveRecords']) >= 2 def test_get_cve_by_count_only_true(): @@ -350,8 +363,8 @@ def test_submit_record_rejection_id_dne(): assert res.status_code == 403 -def test_submit_record_rejection_multiple_english_descriptions(): - """ submit a reject request with descriptions array that has more than one english description """ +def test_submit_record_rejection_multiple_different_english_values(): + """ submit a reject request with descriptions array that has multiple different English values (ex: "en" and "en-Ca") """ res = requests.post( f'{env.AWG_BASE_URL}/api/cve-id/', headers=utils.BASE_HEADERS, @@ -362,14 +375,58 @@ def test_submit_record_rejection_multiple_english_descriptions(): } ) id_num = json.loads(res.content.decode())['cve_ids'][0]['cve_id'] # obtain id number - with open('./src/test/cve_tests/cve_record_fixtures/rejectBodyMultipleEngDescriptions.json') as json_file: + with open('./src/test/cve_tests/cve_record_fixtures/rejectBodyMultipleDiffEngValues.json') as json_file: data = json.load(json_file) res = requests.post( f'{env.AWG_BASE_URL}{CVE_URL}/{id_num}/reject', headers=utils.BASE_HEADERS, json=data ) - assert res.status_code == 400 + assert res.status_code == 200 # lang values are unique + + +def test_submit_record_rejection_multiple_non_English_values(): + """ submit a reject request with descriptions array that has multiple non English values (ex: "fr" and "fr") """ + res = requests.post( + f'{env.AWG_BASE_URL}/api/cve-id/', + headers=utils.BASE_HEADERS, + params={ + 'amount': 1, + 'cve_year': 2000, + 'short_name': 'mitre' + } + ) + id_num = json.loads(res.content.decode())['cve_ids'][0]['cve_id'] # obtain id number + with open('./src/test/cve_tests/cve_record_fixtures/rejectBodyMultipleNonEngValues.json') as json_file: + data = json.load(json_file) + res = requests.post( + f'{env.AWG_BASE_URL}{CVE_URL}/{id_num}/reject', + headers=utils.BASE_HEADERS, + json=data + ) + assert res.status_code == 200 # lang values are unique + + +def test_submit_record_rejection_multiple_same_English_values(): + """ submit a reject request with descriptions array that has multiple same English values (ex: "en-Gb" and "en-Gb") """ + res = requests.post( + f'{env.AWG_BASE_URL}/api/cve-id/', + headers=utils.BASE_HEADERS, + params={ + 'amount': 1, + 'cve_year': 2000, + 'short_name': 'mitre' + } + ) + id_num = json.loads(res.content.decode())['cve_ids'][0]['cve_id'] # obtain id number + with open('./src/test/cve_tests/cve_record_fixtures/rejectBodyMultipleSameEngValues.json') as json_file: + data = json.load(json_file) + res = requests.post( + f'{env.AWG_BASE_URL}{CVE_URL}/{id_num}/reject', + headers=utils.BASE_HEADERS, + json=data + ) + assert res.status_code == 400 # lang values are not unique #### PUT /cve/:id #### diff --git a/test-http/src/test/cve_tests/cve_record_fixtures/rejectBodyMultipleEngDescriptions.json b/test-http/src/test/cve_tests/cve_record_fixtures/rejectBodyMultipleDiffEngValues.json similarity index 69% rename from test-http/src/test/cve_tests/cve_record_fixtures/rejectBodyMultipleEngDescriptions.json rename to test-http/src/test/cve_tests/cve_record_fixtures/rejectBodyMultipleDiffEngValues.json index 73e7bdb18..1700eff8c 100644 --- a/test-http/src/test/cve_tests/cve_record_fixtures/rejectBodyMultipleEngDescriptions.json +++ b/test-http/src/test/cve_tests/cve_record_fixtures/rejectBodyMultipleDiffEngValues.json @@ -16,18 +16,7 @@ ] }, { - "lang": "en", - "value": "I professional site herself recently behavior. Situation institution meeting recognize successful.", - "supportingMedia": [ - { - "type": "test/markdown", - "base64": false, - "value": "*this* _is_ supporting media in ~markdown~" - } - ] - }, - { - "lang": "en", + "lang": "en-Ca", "value": "I professional site herself recently behavior. Situation institution meeting recognize successful.", "supportingMedia": [ { diff --git a/test-http/src/test/cve_tests/cve_record_fixtures/rejectBodyMultipleNonEngValues.json b/test-http/src/test/cve_tests/cve_record_fixtures/rejectBodyMultipleNonEngValues.json new file mode 100644 index 000000000..10a90e1f9 --- /dev/null +++ b/test-http/src/test/cve_tests/cve_record_fixtures/rejectBodyMultipleNonEngValues.json @@ -0,0 +1,32 @@ +{ + "cnaContainer": { + "providerMetadata": { + "orgId" : "f972b356-145d-4b2e-9a5c-b114d0982a3b" + }, + "rejectedReasons": [ + { + "lang": "fr", + "value": "I professional site herself recently behavior. Situation institution meeting recognize successful.", + "supportingMedia": [ + { + "type": "test/markdown", + "base64": false, + "value": "*this* _is_ supporting media in ~markdown~" + } + ] + }, + { + "lang": "fr", + "value": "I professional site herself recently behavior. Situation institution meeting recognize successful.", + "supportingMedia": [ + { + "type": "test/markdown", + "base64": false, + "value": "*this* _is_ supporting media in ~markdown~" + } + ] + } + ], + "replacedBy": ["CVE-1999-0006"] + } +} \ No newline at end of file diff --git a/test-http/src/test/cve_tests/cve_record_fixtures/rejectBodyMultipleSameEngValues.json b/test-http/src/test/cve_tests/cve_record_fixtures/rejectBodyMultipleSameEngValues.json new file mode 100644 index 000000000..e1c1974a6 --- /dev/null +++ b/test-http/src/test/cve_tests/cve_record_fixtures/rejectBodyMultipleSameEngValues.json @@ -0,0 +1,32 @@ +{ + "cnaContainer": { + "providerMetadata": { + "orgId" : "f972b356-145d-4b2e-9a5c-b114d0982a3b" + }, + "rejectedReasons": [ + { + "lang": "en-Gb", + "value": "I professional site herself recently behavior. Situation institution meeting recognize successful.", + "supportingMedia": [ + { + "type": "test/markdown", + "base64": false, + "value": "*this* _is_ supporting media in ~markdown~" + } + ] + }, + { + "lang": "en-Gb", + "value": "I professional site herself recently behavior. Situation institution meeting recognize successful.", + "supportingMedia": [ + { + "type": "test/markdown", + "base64": false, + "value": "*this* _is_ supporting media in ~markdown~" + } + ] + } + ], + "replacedBy": ["CVE-1999-0006"] + } +} \ No newline at end of file diff --git a/test/unit-tests/cve/cveRecordRejectionTest.js b/test/unit-tests/cve/cveRecordRejectionTest.js index 07c5ee1a7..29068c77c 100644 --- a/test/unit-tests/cve/cveRecordRejectionTest.js +++ b/test/unit-tests/cve/cveRecordRejectionTest.js @@ -17,7 +17,7 @@ const cveController = require('../../../src/controller/cve.controller/cve.contro const cveParams = require('../../../src/controller/cve.controller/cve.middleware') const rejectedBody = require('../../../test-http/src/test/cve_tests/cve_record_fixtures/rejectBody.json') -const multipleEngDescriptions = require('../../../test-http/src/test/cve_tests/cve_record_fixtures/rejectBodyMultipleEngDescriptions.json') +const multipleEngDescriptions = require('../../../test-http/src/test/cve_tests/cve_record_fixtures/rejectBodyMultipleSameEngValues.json') const nonExistentId = 'CVE-1800-0001' const cveIdReserved = 'CVE-2019-1421' diff --git a/test/unit-tests/middleware/onlyCnasMiddlewareTest.js b/test/unit-tests/middleware/onlyCnasMiddlewareTest.js index 1a15273c1..8a4542f0e 100644 --- a/test/unit-tests/middleware/onlyCnasMiddlewareTest.js +++ b/test/unit-tests/middleware/onlyCnasMiddlewareTest.js @@ -160,5 +160,41 @@ describe('Test only CNA middleware', () => { done() }) }) + + it('Requester organization shortname is not valid', function (done) { + class OrgOnlyCnasOrgNull { + async findOneByShortName () { + return null + } + } + + app.route('/only-cnas-org-equals-null') + .post((req, res, next) => { + const factory = { + getOrgRepository: () => { return new OrgOnlyCnasOrgNull() } + } + req.ctx.repositories = factory + next() + }, middleware.onlyCnas, (req, res) => { + return res.status(200).json({ message: 'Success! You have reached the target endpoint.' }) + }) + + chai.request(app) + .post('/only-cnas-org-equals-null') + .set(mwCnaFixtures.notCnaHeaders) + .send() + .end((err, res) => { + if (err) { + done(err) + } + + expect(res).to.have.status(404) + expect(res).to.have.property('body').and.to.be.a('object') + const errObj = error.cnaDoesNotExist(mwCnaFixtures.notCnaHeaders['CVE-API-ORG']) + expect(res.body.error).to.equal(errObj.error) + expect(res.body.message).to.equal(errObj.message) + done() + }) + }) }) })