Skip to content

Commit bf8c537

Browse files
authored
Merge pull request #1566 from CVEProject/5.2.0-Purl-Test
Updating Production to CVE-Services v2.6.0
2 parents bca4ef8 + c269dc4 commit bf8c537

28 files changed

+501
-140
lines changed

api-docs/openapi.json

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
{
22
"openapi": "3.0.2",
33
"info": {
4-
"version": "2.5.4",
4+
"version": "2.6.0",
55
"title": "CVE Services API",
6-
"description": "The CVE Services API supports automation tooling for the CVE Program. Credentials are required for most service endpoints. Representatives of <a href='https://www.cve.org/ProgramOrganization/CNAs'>CVE Numbering Authorities (CNAs)</a> should use one of the methods below to obtain credentials: <ul><li>If your organization already has an Organizational Administrator (OA) account for the CVE Services, ask your admin for credentials</li> <li>Contact your Root (<a href='https://www.cve.org/PartnerInformation/ListofPartners/partner/Google'>Google</a>, <a href='https://www.cve.org/PartnerInformation/ListofPartners/partner/INCIBE'>INCIBE</a>, <a href='https://www.cve.org/PartnerInformation/ListofPartners/partner/jpcert'>JPCERT/CC</a>, or <a href='https://www.cve.org/PartnerInformation/ListofPartners/partner/redhat'>Red Hat</a>) or Top-Level Root (<a href='https://www.cve.org/PartnerInformation/ListofPartners/partner/icscert'>CISA ICS</a> or <a href='https://www.cve.org/PartnerInformation/ListofPartners/partner/mitre'>MITRE</a>) to request credentials </ul> <p>CVE data is to be in the JSON 5.1 CVE Record format. Details of the JSON 5.1 schema are located <a href='https://github.com/CVEProject/cve-schema/tree/v5.1.1-rc2/schema' target='_blank'>here</a>.</p> <a href='https://cveform.mitre.org/' class='link' target='_blank'>Contact the CVE Services team</a>",
6+
"description": "The CVE Services API supports automation tooling for the CVE Program. Credentials are required for most service endpoints. Representatives of <a href='https://www.cve.org/ProgramOrganization/CNAs'>CVE Numbering Authorities (CNAs)</a> should use one of the methods below to obtain credentials: <ul><li>If your organization already has an Organizational Administrator (OA) account for the CVE Services, ask your admin for credentials</li> <li>Contact your Root (<a href='https://www.cve.org/PartnerInformation/ListofPartners/partner/Google'>Google</a>, <a href='https://www.cve.org/PartnerInformation/ListofPartners/partner/INCIBE'>INCIBE</a>, <a href='https://www.cve.org/PartnerInformation/ListofPartners/partner/jpcert'>JPCERT/CC</a>, or <a href='https://www.cve.org/PartnerInformation/ListofPartners/partner/redhat'>Red Hat</a>) or Top-Level Root (<a href='https://www.cve.org/PartnerInformation/ListofPartners/partner/icscert'>CISA ICS</a> or <a href='https://www.cve.org/PartnerInformation/ListofPartners/partner/mitre'>MITRE</a>) to request credentials </ul> <p>CVE data is to be in the JSON 5.2 CVE Record format. Details of the JSON 5.2 schema are located <a href='https://github.com/CVEProject/cve-schema/releases/tag/v5.2.0' target='_blank'>here</a>.</p> <a href='https://cveform.mitre.org/' class='link' target='_blank'>Contact the CVE Services team</a>",
77
"contact": {
88
"name": "CVE Services Overview",
99
"url": "https://www.cve.org/AllResources/CveServices"
@@ -2974,7 +2974,6 @@
29742974
"summary": "Checks that the system is running (accessible to all users)",
29752975
"description": " <h2>Access Control</h2> <p>Endpoint is accessible to all</p> <h2>Expected Behavior</h2> <p>Returns a 200 response code when CVE Services are running</p>",
29762976
"operationId": "healthCheck",
2977-
"parameters": [],
29782977
"responses": {
29792978
"200": {
29802979
"description": "Returns a 200 response code"
@@ -5048,4 +5047,4 @@
50485047
}
50495048
}
50505049
}
5051-
}
5050+
}

package-lock.json

Lines changed: 14 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "cve-services",
33
"author": "Automation Working Group",
4-
"version": "2.5.4",
4+
"version": "2.6.0",
55
"license": "(CC0)",
66
"devDependencies": {
77
"@faker-js/faker": "^7.6.0",
@@ -27,10 +27,10 @@
2727
"standard": "^16.0.3"
2828
},
2929
"dependencies": {
30-
"bson": "^6.10.1",
3130
"ajv": "^8.6.2",
3231
"ajv-formats": "^2.1.1",
3332
"argon2": "^0.41.1",
33+
"bson": "^6.10.1",
3434
"config": "^3.3.6",
3535
"cors": "^2.8.5",
3636
"crypto-random-string": "^3.3.1",
@@ -50,6 +50,7 @@
5050
"mongoose-aggregate-paginate-v2": "1.0.6",
5151
"morgan": "^1.9.1",
5252
"node-dev": "^7.4.3",
53+
"packageurl-js": "^2.0.1",
5354
"prompt-sync": "^4.2.0",
5455
"replace-in-file": "6.3.5",
5556
"replace-json-property": "^1.8.0",
@@ -60,7 +61,11 @@
6061
"winston": "^3.2.1",
6162
"yamljs": "^0.3.0"
6263
},
63-
"overrides": { "mongo-cursor-pagination": { "bson": "^6.10.1" } },
64+
"overrides": {
65+
"mongo-cursor-pagination": {
66+
"bson": "^6.10.1"
67+
}
68+
},
6469
"apidoc": {
6570
"name": "CVE-Services",
6671
"version": "0.0.0",
@@ -103,4 +108,4 @@
103108
"test:coverage-html": "NODE_ENV=test nyc --reporter=html mocha src/* --recursive --exit || true",
104109
"test:scripts": "NODE_ENV=development node-dev src/scripts/templateScript.js"
105110
}
106-
}
111+
}

src/constants/index.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
const fs = require('fs')
2-
const cveSchemaV5 = JSON.parse(fs.readFileSync('src/middleware/schemas/CVE_JSON_5.1.1_bundled.json'))
2+
const cveSchemaV5 = JSON.parse(fs.readFileSync('src/middleware/schemas/CVE_JSON_5.2.0_bundled.json'))
33

44
/**
55
* Return default values.
@@ -16,7 +16,7 @@ function getConstants () {
1616
* @lends defaults
1717
*/
1818
const defaults = {
19-
SCHEMA_VERSION: '5.1',
19+
SCHEMA_VERSION: '5.2',
2020
MONGOOSE_VALIDATION: {
2121
Org_policies_id_quota_min: 0,
2222
Org_policies_id_quota_min_message: 'Org.policies.id_quota cannot be a negative number.',

src/controller/cve.controller/cve.middleware.js

Lines changed: 100 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1+
const { PackageURL } = require('packageurl-js')
12
const { body, validationResult } = require('express-validator')
23
const errors = require('./error')
34
const error = new errors.CveControllerError()
45
const utils = require('../../utils/utils')
56
const fs = require('fs')
6-
const RejectedSchema = JSON.parse(fs.readFileSync('src/middleware/schemas/5.1.1_rejected_cna_container.json'))
7-
const cnaContainerSchema = JSON.parse(fs.readFileSync('src/middleware/schemas/5.1.1_published_cna_container.json'))
7+
const RejectedSchema = JSON.parse(fs.readFileSync('src/middleware/schemas/5.2.0_rejected_cna_container.json'))
8+
const cnaContainerSchema = JSON.parse(fs.readFileSync('src/middleware/schemas/5.2.0_published_cna_container.json'))
89
const logger = require('../../middleware/logger')
910
const Ajv = require('ajv')
1011
const addFormats = require('ajv-formats')
@@ -184,6 +185,100 @@ function validateCveAdpContainerJsonSchema (req, res, next) {
184185
next()
185186
}
186187

188+
// PURL validator middleware
189+
function validatePURL (pURLIndex) {
190+
return body(pURLIndex).optional({ nullable: true }).bail().custom((affected) => {
191+
const result = purlValidateHelper(affected)
192+
return result
193+
})
194+
}
195+
196+
function purlValidateHelper (affected) {
197+
for (const affObj of affected) {
198+
const purlStr = affObj.packageURL
199+
200+
// If no PURL string provided, skip validation and continue through loop
201+
if (!purlStr) {
202+
continue
203+
}
204+
let purlObj
205+
let parsedPurlArray
206+
// Passes first validation check if PackageURL can build a PURL from the string
207+
try {
208+
// PURL class from PackageURL
209+
purlObj = PackageURL.fromString(purlStr)
210+
211+
// PURL string broken up by component and store in array. Used for additional validation
212+
parsedPurlArray = PackageURL.parseString(purlStr)
213+
} catch (e) {
214+
throw new Error(e.message + ': "' + purlStr + '"')
215+
}
216+
217+
// PURL's with versions are not allowed
218+
if (purlObj.version !== undefined) {
219+
throw new Error('The PURL version component is currently not supported by the CVE schema: ' + purlStr)
220+
}
221+
222+
// Handle qualifier cases
223+
if (purlObj.qualifiers !== undefined) {
224+
// Check for versions within qualifiers
225+
if (Object.keys(purlObj.qualifiers).includes('vers')) {
226+
throw new Error('PURL versions are currently not supported by the CVE schema: ' + purlStr)
227+
}
228+
}
229+
230+
if (parsedPurlArray[4] !== undefined) {
231+
// Check for qualifier with key but no value
232+
if ((Array.from(parsedPurlArray[4].values()).includes(''))) {
233+
throw new Error('Qualifier keys must have a value: ' + purlStr)
234+
}
235+
}
236+
237+
// PackageURL does not properly prevent encoded ':', so check for that here
238+
const encColon = /%3a/i
239+
if (encColon.test(purlStr)) {
240+
throw new Error('Percent-encoded colons are not allowed in a PURL: ' + purlStr)
241+
}
242+
243+
// PackageURL does not properly account for certain Subpath situations
244+
// so adding additional validation to account for them
245+
// Handles PURLs that include a # but no subpath
246+
if (purlObj.subpath === undefined && purlStr.includes('#')) {
247+
throw new Error('Subpaths cannot be empty or contain only a /: ' + purlStr)
248+
}
249+
250+
if (purlObj.subpath !== undefined) {
251+
// Checks if any subpaths contain invalid characters
252+
// Subpaths cannot be '.' or '..'
253+
const parsedSubpaths = subpathHelper(parsedPurlArray[5])
254+
255+
if (parsedSubpaths.includes('..') || parsedSubpaths.includes('.')) {
256+
throw new Error('Subpaths cannot be "." or "..": ' + purlStr)
257+
}
258+
259+
if (parsedSubpaths.includes('')) {
260+
throw new Error('Subpaths cannot be empty or contain only a /: ' + purlStr)
261+
}
262+
}
263+
}
264+
return true
265+
}
266+
267+
// Parses subpaths into array, stripping leading and trailing '/'
268+
function subpathHelper (subpathStr) {
269+
// remove leading and trailing '/'s
270+
if (subpathStr[0] === '/') {
271+
subpathStr = subpathStr.slice(1)
272+
}
273+
274+
if (subpathStr[subpathStr.length - 1] === '/') {
275+
subpathStr = subpathStr.slice(0, subpathStr.length - 1)
276+
}
277+
278+
const parsedSubpaths = subpathStr.split('/')
279+
return parsedSubpaths
280+
}
281+
187282
module.exports = {
188283
parseGetParams,
189284
parsePostParams,
@@ -195,5 +290,7 @@ module.exports = {
195290
validateDescription,
196291
validateRejectBody,
197292
validateDatePublic,
198-
datePublicHelper
293+
datePublicHelper,
294+
validatePURL,
295+
purlValidateHelper
199296
}

src/controller/cve.controller/index.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ const mw = require('../../middleware/middleware')
44
const errorMsgs = require('../../middleware/errorMessages')
55
const controller = require('./cve.controller')
66
const { body, param, query } = require('express-validator')
7-
const { parseGetParams, parsePostParams, parseError, validateCveCnaContainerJsonSchema, validateCveAdpContainerJsonSchema, validateRejectBody, validateUniqueEnglishEntry, validateDescription, validateDatePublic } = require('./cve.middleware')
7+
const { parseGetParams, parsePostParams, parseError, validateCveCnaContainerJsonSchema, validateCveAdpContainerJsonSchema, validateRejectBody, validateUniqueEnglishEntry, validateDescription, validateDatePublic, validatePURL } = require('./cve.middleware')
88
const getConstants = require('../../constants').getConstants
99
const CONSTANTS = getConstants()
1010
const CHOICES = [CONSTANTS.CVE_STATES.REJECTED, CONSTANTS.CVE_STATES.PUBLISHED]
@@ -495,6 +495,7 @@ router.post('/cve/:id',
495495
validateUniqueEnglishEntry(['containers.cna.descriptions', 'containers.cna.rejectedReasons']),
496496
validateDescription(['containers.cna.rejectedReasons', 'containers.cna.descriptions', 'containers.cna.problemTypes[0].descriptions']),
497497
validateDatePublic(['containers.cna.datePublic']),
498+
validatePURL(['containers.cna.affected']),
498499
param(['id']).isString().matches(CONSTANTS.CVE_ID_REGEX),
499500
parseError,
500501
parsePostParams,
@@ -581,6 +582,7 @@ router.put('/cve/:id',
581582
validateUniqueEnglishEntry(['containers.cna.descriptions', 'containers.cna.rejectedReasons']),
582583
validateDescription(['containers.cna.rejectedReasons', 'containers.cna.descriptions', 'containers.cna.problemTypes[0].descriptions']),
583584
validateDatePublic(['containers.cna.datePublic']),
585+
validatePURL(['containers.cna.affected']),
584586
param(['id']).isString().matches(CONSTANTS.CVE_ID_REGEX),
585587
parseError,
586588
parsePostParams,
@@ -679,6 +681,7 @@ router.post('/cve/:id/cna',
679681
validateUniqueEnglishEntry('cnaContainer.descriptions'),
680682
validateDescription(['cnaContainer.descriptions', 'cnaContainer.problemTypes[0].descriptions']),
681683
validateDatePublic(['cnaContainer.datePublic']),
684+
validatePURL(['cnaContainer.affected']),
682685
param(['id']).isString().matches(CONSTANTS.CVE_ID_REGEX),
683686
parseError,
684687
parsePostParams,
@@ -779,6 +782,7 @@ router.put('/cve/:id/cna',
779782
validateUniqueEnglishEntry('cnaContainer.descriptions'),
780783
validateDescription(['cnaContainer.descriptions', 'cnaContainer.problemTypes[0].descriptions']),
781784
validateDatePublic(['cnaContainer.datePublic']),
785+
validatePURL(['cnaContainer.affected']),
782786
param(['id']).isString().matches(CONSTANTS.CVE_ID_REGEX),
783787
parseError,
784788
parsePostParams,
@@ -1049,6 +1053,7 @@ router.put('/cve/:id/adp',
10491053
mw.onlyAdps,
10501054
mw.trimJSONWhitespace,
10511055
validateCveAdpContainerJsonSchema,
1056+
validatePURL(['adpContainer.affected']),
10521057
param(['id']).isString().matches(CONSTANTS.CVE_ID_REGEX),
10531058
parseError,
10541059
parsePostParams,

src/middleware/middleware.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
const getConstants = require('../constants').getConstants
22
const fs = require('fs')
3-
const cveSchemaV5 = JSON.parse(fs.readFileSync('src/middleware/schemas/CVE_JSON_5.1.1_bundled.json'))
3+
const cveSchemaV5 = JSON.parse(fs.readFileSync('src/middleware/schemas/CVE_JSON_5.2.0_bundled.json'))
44
const argon2 = require('argon2')
55
const logger = require('./logger')
66
const Ajv = require('ajv')

0 commit comments

Comments
 (0)