diff --git a/api-docs/openapi.json b/api-docs/openapi.json index 3fa552918..8f4ed585e 100644 --- a/api-docs/openapi.json +++ b/api-docs/openapi.json @@ -1,7 +1,7 @@ { "openapi": "3.0.2", "info": { - "version": "2.5.3", + "version": "2.5.4", "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", "contact": { @@ -1190,6 +1190,63 @@ } } }, + "/cve_count": { + "get": { + "tags": [ + "CVE Record" + ], + "summary": "Retrieves the count of all the CVE Records after applying the query parameters as filters (accessible to all users)", + "description": "

Access Control

Endpoint is accessible to all

Expected Behavior

Retrieves the count of all CVE records for all organizations

", + "operationId": "cveGetFilteredCount", + "parameters": [ + { + "$ref": "#/components/parameters/cveState" + } + ], + "responses": { + "200": { + "description": "A count of the total number of filtered CVE records", + "content": { + "application/json": { + "schema": { + "$ref": "../schemas/cve/get-cve-record-count.json" + } + } + } + }, + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "$ref": "../schemas/errors/bad-request.json" + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "$ref": "../schemas/errors/generic.json" + } + } + } + }, + "500": { + "description": "Internal Server Error", + "content": { + "application/json": { + "schema": { + "$ref": "../schemas/errors/generic.json" + } + } + } + } + } + } + }, "/cve_cursor": { "get": { "tags": [ diff --git a/package-lock.json b/package-lock.json index 981474310..8d7454c3b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "cve-services", - "version": "2.5.3", + "version": "2.5.4", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "cve-services", - "version": "2.5.3", + "version": "2.5.4", "license": "(CC0)", "dependencies": { "ajv": "^8.6.2", diff --git a/package.json b/package.json index cbd1617ce..cbb81d10f 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "cve-services", "author": "Automation Working Group", - "version": "2.5.3", + "version": "2.5.4", "license": "(CC0)", "devDependencies": { "@faker-js/faker": "^7.6.0", diff --git a/schemas/cve/get-cve-record-count.json b/schemas/cve/get-cve-record-count.json new file mode 100644 index 000000000..54a01556f --- /dev/null +++ b/schemas/cve/get-cve-record-count.json @@ -0,0 +1,10 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema", + "type": "object", + "properties": { + "totalCount": { + "type": "integer", + "format": "int32" + } + } +} \ No newline at end of file diff --git a/src/controller/cve.controller/cve.controller.js b/src/controller/cve.controller/cve.controller.js index 687c315fb..6f5d9294d 100644 --- a/src/controller/cve.controller/cve.controller.js +++ b/src/controller/cve.controller/cve.controller.js @@ -31,6 +31,17 @@ async function getCve (req, res, next) { } } +// Called by GET /cve +async function getFilteredCveCount (req, res, next) { + try { + req.ctx.query.count_only = '1' + const result = await getFilteredCves(req, res, next) + return result + } catch (err) { + next(err) + } +} + // Called by GET /cve async function getFilteredCves (req, res, next) { const CONSTANTS = getConstants() @@ -916,6 +927,7 @@ module.exports = { CVE_GET_SINGLE: getCve, CVE_GET_FILTERED: getFilteredCves, CVE_GET_FILTERED_CURSOR: getFilteredCvesCursor, + CVE_GET_FILTERED_COUNT: getFilteredCveCount, CVE_SUBMIT: submitCve, CVE_UPDATE_SINGLE: updateCve, CVE_SUBMIT_CNA: submitCna, diff --git a/src/controller/cve.controller/index.js b/src/controller/cve.controller/index.js index 3d6ef4570..c1821492b 100644 --- a/src/controller/cve.controller/index.js +++ b/src/controller/cve.controller/index.js @@ -274,6 +274,58 @@ router.get('/cve', parseGetParams, controller.CVE_GET_FILTERED) +router.get('/cve_count', + /* + #swagger.tags = ['CVE Record'] + #swagger.operationId = 'cveGetFilteredCount' + #swagger.summary = "Retrieves the count of all the CVE Records after applying the query parameters as filters (accessible to all users)" + #swagger.description = " +

Access Control

+

Endpoint is accessible to all

+

Expected Behavior

+

Retrieves the count of all CVE records for all organizations

" + #swagger.parameters['$ref'] = [ + '#/components/parameters/cveState', + ] + #swagger.responses[200] = { + description: 'A count of the total number of filtered CVE records', + content: { + "application/json": { + schema: { $ref: '../schemas/cve/get-cve-record-count.json' } + } + } + } + #swagger.responses[400] = { + description: 'Bad Request', + content: { + "application/json": { + schema: { $ref: '../schemas/errors/bad-request.json' } + } + } + } + #swagger.responses[404] = { + description: 'Not Found', + 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' } + } + } + } + */ + query().custom((query) => { return mw.validateQueryParameterNames(query, ['state']) }), + query(['state']).optional().isString().trim().customSanitizer(val => { return val.toUpperCase() }).isIn(CHOICES).withMessage(errorMsgs.CVE_FILTERED_STATES), + parseError, + parseGetParams, + controller.CVE_GET_FILTERED_COUNT) + router.get('/cve_cursor', /* #swagger.tags = ['CVE Record'] diff --git a/src/controller/org.controller/org.controller.js b/src/controller/org.controller/org.controller.js index d1a094b57..02cc4dc56 100644 --- a/src/controller/org.controller/org.controller.js +++ b/src/controller/org.controller/org.controller.js @@ -395,7 +395,7 @@ async function updateOrg (req, res, next) { // update org let result = await orgRepo.updateByOrgUUID(org.UUID, newOrg) - if (result.n === 0) { + if (result.matchedCount === 0) { logger.info({ uuid: req.ctx.uuid, message: shortName + ' organization could not be updated in MongoDB because it does not exist.' }) return res.status(404).json(error.orgDnePathParam(shortName)) } @@ -450,34 +450,42 @@ async function createUser (req, res, next) { return res.status(400).json(error.userLimitReached()) } - Object.keys(req.ctx.body).forEach(k => { - const key = k.toLowerCase() + const body = req.ctx.body + const keys = Object.keys(body) - if (key === 'username') { - newUser.username = req.ctx.body.username - } else if (key === 'authority') { - if (req.ctx.body.authority.active_roles) { - newUser.authority.active_roles = [...new Set(req.ctx.body.authority.active_roles)] // Removes any duplicate strings from array - } - } else if (key === 'name') { - if (req.ctx.body.name.first) { - newUser.name.first = req.ctx.body.name.first - } - if (req.ctx.body.name.last) { - newUser.name.last = req.ctx.body.name.last - } - if (req.ctx.body.name.middle) { - newUser.name.middle = req.ctx.body.name.middle - } - if (req.ctx.body.name.suffix) { - newUser.name.suffix = req.ctx.body.name.suffix - } - } else if (key === 'org_uuid') { - return res.status(400).json(error.uuidProvided('org')) - } else if (key === 'uuid') { + for (const keyRaw of keys) { + const key = keyRaw.toLowerCase() + + if (key === 'uuid') { return res.status(400).json(error.uuidProvided('user')) } - }) + + if (key === 'org_uuid') { + return res.status(400).json(error.uuidProvided('org')) + } + + const handlers = { + username: () => { + newUser.username = body.username + }, + authority: () => { + if (body.authority?.active_roles) { + newUser.authority.active_roles = [...new Set(body.authority.active_roles)] + } + }, + name: () => { + const name = body.name || {} + if (name.first) newUser.name.first = name.first + if (name.last) newUser.name.last = name.last + if (name.middle) newUser.name.middle = name.middle + if (name.suffix) newUser.name.suffix = name.suffix + } + } + + if (handlers[key]) { + handlers[key]() // execute the appropriate handler + } + } const requesterOrgUUID = await orgRepo.getOrgUUID(requesterShortName) const isSecretariat = await orgRepo.isSecretariatUUID(requesterOrgUUID) @@ -711,7 +719,7 @@ async function updateUser (req, res, next) { newUser.authority.active_roles = duplicateCheckedRoles let result = await userRepo.updateByUserNameAndOrgUUID(username, orgUUID, newUser) - if (result.n === 0) { + if (result.matchedCount === 0) { logger.info({ uuid: req.ctx.uuid, message: 'The user could not be updated because ' + username + ' does not exist for ' + shortName + ' organization.' }) return res.status(404).json(error.userDne(username)) } @@ -786,7 +794,7 @@ async function resetSecret (req, res, next) { const randomKey = cryptoRandomString({ length: getConstants().CRYPTO_RANDOM_STRING_LENGTH }) oldUser.secret = await argon2.hash(randomKey) // store in db const user = await userRepo.updateByUserNameAndOrgUUID(oldUser.username, orgUUID, oldUser) - if (user.n === 0) { + if (user.matchedCount === 0) { logger.info({ uuid: req.ctx.uuid, message: 'The user could not be updated because ' + username + ' does not exist for ' + orgShortName + ' organization.' }) return res.status(404).json(error.userDne(username)) } diff --git a/src/controller/schemas.controller/index.js b/src/controller/schemas.controller/index.js index dba7997ae..fe9cdd2b9 100644 --- a/src/controller/schemas.controller/index.js +++ b/src/controller/schemas.controller/index.js @@ -20,6 +20,7 @@ router.get('/cve/create-cve-record-cna-request.json', controller.getCnaFullSchem router.get('/cve/create-adp-record-adp-request.json', controller.getAdpFullSchema) router.get('/cve/create-cve-record-secretariat-request.json', controller.getCnaSecretariatFullSchema) router.get('/cve/cna-minimum-request.json', controller.getCnaMinSchema) +router.get('/cve/get-cve-record-count.json', controller.getCveCountResponseSchema) // Schemas relating to CVE IDs router.get('/cve-id/create-cve-ids-response.json', controller.getCreateCveIdsResponseSchema) diff --git a/src/controller/schemas.controller/schemas.controller.js b/src/controller/schemas.controller/schemas.controller.js index 1d800a68c..caf97cd28 100644 --- a/src/controller/schemas.controller/schemas.controller.js +++ b/src/controller/schemas.controller/schemas.controller.js @@ -222,6 +222,12 @@ async function getUpdateUserResponseSchema (req, res) { res.status(200) } +async function getCveCountResponseSchema (req, res) { + const cveCountResponseSchema = require('../../../schemas/cve/get-cve-record-count.json') + res.json(cveCountResponseSchema) + res.status(200) +} + module.exports = { getBadRequestSchema: getBadRequestSchema, getCreateCveRecordResponseSchema: getCreateCveRecordResponseSchema, @@ -258,5 +264,6 @@ module.exports = { getCnaFullSchema: getCnaFullSchema, getAdpFullSchema: getAdpFullSchema, getCnaSecretariatFullSchema: getCnaSecretariatFullSchema, - getCnaMinSchema: getCnaMinSchema + getCnaMinSchema: getCnaMinSchema, + getCveCountResponseSchema: getCveCountResponseSchema } diff --git a/src/swagger.js b/src/swagger.js index af5d04700..0aaa44171 100644 --- a/src/swagger.js +++ b/src/swagger.js @@ -18,7 +18,7 @@ const fullCnaContainerRequest = require('../schemas/cve/create-cve-record-cna-re /* eslint-disable no-multi-str */ const doc = { info: { - version: '2.5.3', + version: '2.5.4', 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/README.md b/test/README.md index 797e80d38..ceb4dfc80 100644 --- a/test/README.md +++ b/test/README.md @@ -1,7 +1,7 @@ # CVE-API-Unit-Tests -In order to Run Tests, make sure you configure a DB connection in the config/config.json under the `test` environment. +In order to Run Tests, make sure you configure a DB connection in the config/default.json under the `test` environment. ## Dependencies @@ -14,11 +14,16 @@ This project uses or depends on software from - Mocha https://mochajs.org/ - Chai https://www.chaijs.com/ +In order to pre-populate a new database for testing, run the following command: + +```sh +npm run populate:test +``` In order to run unit tests, use the following command: ```sh -npm run start:test +npm run test ``` ## Notes diff --git a/test/integration-tests/cve/getCveCount.js b/test/integration-tests/cve/getCveCount.js new file mode 100644 index 000000000..db4ba678f --- /dev/null +++ b/test/integration-tests/cve/getCveCount.js @@ -0,0 +1,72 @@ +/* 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') + +describe('Test get /cve_count for CVE records', () => { + context('Positive Tests', () => { + it('Get /cve_count should allow any user to get count', async () => { + await chai.request(app) + .get('/api/cve_count') + .then((res, err) => { + expect(err).to.be.undefined + expect(res).to.have.status(200) + expect(res.body).to.have.property('totalCount').that.is.a('number') + }) + }) + it('Get /cve_count should allow privledged user to get count also', async () => { + await chai.request(app) + .get('/api/cve_count') + .set(constants.headers) + .then((res, err) => { + expect(err).to.be.undefined + expect(res).to.have.status(200) + expect(res.body).to.have.property('totalCount').that.is.a('number') + }) + }) + it('Get /cve_count should return count with valid parameters', async () => { + await chai.request(app) + .get('/api/cve_count?state=PUBLISHED') + .set(constants.headers) + .then((res, err) => { + expect(err).to.be.undefined + expect(res).to.have.status(200) + expect(res.body).to.have.property('totalCount').that.is.a('number') + }) + }) + }) + context('Negative Tests', () => { + it('Get /cve should NOT allow any user to get count', async () => { + await chai.request(app) + .get('/api/cve?count_only=1') + .then((res, err) => { + expect(err).to.be.undefined + expect(res).to.have.status(400) + }) + }) + it('Get /cve_count should fail if it is passed invalid parameters', async () => { + await chai.request(app) + .get('/api/cve_count?time_modified.gt=2022-13-01T00:00:00Z') + .set(constants.headers) + .then((res, err) => { + expect(err).to.be.undefined + expect(res).to.have.status(400) + expect(res.body.message).to.contain('Parameters were invalid') + }) + }) + it('Get /cve_count should fail if it is `state` parameter value is invalid ', async () => { + await chai.request(app) + .get('/api/cve_count?state=SUCESS') + .set(constants.headers) + .then((res, err) => { + expect(err).to.be.undefined + expect(res).to.have.status(400) + expect(res.body.message).to.contain('Parameters were invalid') + }) + }) + }) +})