Skip to content

Commit 007d7b9

Browse files
authored
Merge branch 'dev' into dr-1278
2 parents beb8459 + c646956 commit 007d7b9

File tree

13 files changed

+430
-16
lines changed

13 files changed

+430
-16
lines changed

api-docs/openapi.json

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,10 @@
33
"info": {
44
"version": "2.5.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/5.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.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>",
77
"contact": {
88
"name": "CVE Services Overview",
9-
"url": "https://cveproject.github.io/automation-cve-services#services-overview"
9+
"url": "https://www.cve.org/AllResources/CveServices"
1010
}
1111
},
1212
"servers": [
@@ -1323,6 +1323,9 @@
13231323
},
13241324
{
13251325
"$ref": "#/components/parameters/apiSecretHeader"
1326+
},
1327+
{
1328+
"$ref": "#/components/parameters/erlCheck"
13261329
}
13271330
],
13281331
"responses": {
@@ -1432,6 +1435,9 @@
14321435
},
14331436
{
14341437
"$ref": "#/components/parameters/apiSecretHeader"
1438+
},
1439+
{
1440+
"$ref": "#/components/parameters/erlCheck"
14351441
}
14361442
],
14371443
"responses": {
@@ -3036,6 +3042,15 @@
30363042
"type": "string"
30373043
}
30383044
},
3045+
"erlCheck": {
3046+
"in": "query",
3047+
"name": "erlcheck",
3048+
"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.",
3049+
"required": false,
3050+
"schema": {
3051+
"type": "boolean"
3052+
}
3053+
},
30393054
"batch_type": {
30403055
"in": "query",
30413056
"name": "batch_type",

src/constants/index.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,9 @@ function getConstants () {
100100
CVE_ID_PATTERN: cveSchemaV5.definitions.cveId.pattern,
101101
// Ajv's pattern validation uses the "u" (unicode) flag:
102102
// https://ajv.js.org/json-schema.html#pattern
103-
CVE_ID_REGEX: new RegExp(cveSchemaV5.definitions.cveId.pattern, 'u')
103+
CVE_ID_REGEX: new RegExp(cveSchemaV5.definitions.cveId.pattern, 'u'),
104+
DATE_FIELDS: ['cveMetadata.datePublished', 'cveMetadata.dateUpdated', 'cveMetadata.dateReserved', 'providerMetadata.dateUpdated', 'datePublic', 'dateAssigned'
105+
]
104106
}
105107

106108
return defaults

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

Lines changed: 36 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ const errors = require('./error')
44
const getConstants = require('../../constants').getConstants
55
const error = new errors.CveControllerError()
66
const booleanIsTrue = require('../../utils/utils').booleanIsTrue
7+
const convertDatesToISO = require('../../utils/utils').convertDatesToISO
8+
const isEnrichedContainer = require('../../utils/utils').isEnrichedContainer
79
const url = process.env.NODE_ENV === 'staging' ? 'https://test.cve.org/' : 'https://cve.org/'
810

911
// Helper function to create providerMetadata object
@@ -331,7 +333,7 @@ async function submitCve (req, res, next) {
331333
const CONSTANTS = getConstants()
332334

333335
try {
334-
const newCve = new Cve({ cve: req.ctx.body })
336+
const newCve = new Cve({ cve: convertDatesToISO(req.ctx.body, CONSTANTS.DATE_FIELDS) })
335337
const id = req.ctx.params.id
336338
const cveId = newCve.cve.cveMetadata.cveId
337339
const state = newCve.cve.cveMetadata.state
@@ -395,7 +397,8 @@ async function updateCve (req, res, next) {
395397
const CONSTANTS = getConstants()
396398

397399
try {
398-
const newCve = new Cve({ cve: req.ctx.body })
400+
// All CVE fields are stored in UTC format, we need to check and convert dates to ISO before storing in the database.
401+
const newCve = new Cve({ cve: convertDatesToISO(req.ctx.body, CONSTANTS.DATE_FIELDS) })
399402
const cveId = req.ctx.params.id
400403
const cveRepo = req.ctx.repositories.getCveRepository()
401404
const cveIdRepo = req.ctx.repositories.getCveIdRepository()
@@ -465,6 +468,14 @@ async function submitCna (req, res, next) {
465468
const orgUuid = await orgRepo.getOrgUUID(req.ctx.org)
466469
const userUuid = await userRepo.getUserUUID(req.ctx.user, orgUuid)
467470

471+
// To avoid breaking legacy behavior in the "booleanIsTrue" function, we need to check to make sure that undefined is set to false
472+
let erlCheck
473+
if (typeof req.query.erlcheck === 'undefined') {
474+
erlCheck = false
475+
} else {
476+
erlCheck = booleanIsTrue(req.query.erlcheck) || false
477+
}
478+
468479
// check that cve id exists
469480
let result = await cveIdRepo.findOneByCveId(id)
470481
if (!result || result.state === CONSTANTS.CVE_STATES.AVAILABLE) {
@@ -484,10 +495,15 @@ async function submitCna (req, res, next) {
484495
return res.status(403).json(error.cveRecordExists())
485496
}
486497

498+
const cnaContainer = convertDatesToISO(req.ctx.body.cnaContainer, CONSTANTS.DATE_FIELDS)
499+
if (erlCheck && !isEnrichedContainer(cnaContainer)) {
500+
// Process the ERL check here
501+
return res.status(403).json(error.erlCheckFailed())
502+
}
503+
487504
// create full cve record here
488505
const owningCna = await orgRepo.findOneByUUID(cveId.owning_cna)
489506
const assignerShortName = owningCna.short_name
490-
const cnaContainer = req.ctx.body.cnaContainer
491507
const dateUpdated = (new Date()).toISOString()
492508
const additionalCveMetadataFields = {
493509
assignerShortName: assignerShortName,
@@ -548,6 +564,14 @@ async function updateCna (req, res, next) {
548564
const orgUuid = await orgRepo.getOrgUUID(req.ctx.org)
549565
const userUuid = await userRepo.getUserUUID(req.ctx.user, orgUuid)
550566

567+
// To avoid breaking legacy behavior in the "booleanIsTrue" function, we need to check to make sure that undefined is set to false
568+
let erlCheck
569+
if (typeof req.query.erlcheck === 'undefined') {
570+
erlCheck = false
571+
} else {
572+
erlCheck = booleanIsTrue(req.query.erlcheck) || false
573+
}
574+
551575
// check that cve id exists
552576
let result = await cveIdRepo.findOneByCveId(id)
553577
if (!result || result.state === CONSTANTS.CVE_STATES.AVAILABLE) {
@@ -567,9 +591,14 @@ async function updateCna (req, res, next) {
567591
return res.status(403).json(error.cveRecordDne())
568592
}
569593

594+
const cnaContainer = convertDatesToISO(req.ctx.body.cnaContainer, CONSTANTS.DATE_FIELDS)
595+
if (erlCheck && !isEnrichedContainer(cnaContainer)) {
596+
// Process the ERL check here
597+
return res.status(403).json(error.erlCheckFailed())
598+
}
599+
570600
// update cve record here
571601
const cveRecord = result.cve
572-
const cnaContainer = req.ctx.body.cnaContainer
573602
const dateUpdated = (new Date()).toISOString()
574603
cveRecord.cveMetadata.dateUpdated = dateUpdated
575604

@@ -664,7 +693,7 @@ async function rejectCVE (req, res, next) {
664693

665694
const providerMetadata = createProviderMetadata(providerOrgObj.UUID, req.ctx.org, (new Date()).toISOString())
666695
const rejectedCve = Cve.newRejectedCve(cveIdObj, req.ctx.body, owningCnaShortName, providerMetadata)
667-
const newCveObj = new Cve({ cve: rejectedCve })
696+
const newCveObj = new Cve({ cve: convertDatesToISO(rejectedCve, CONSTANTS.DATE_FIELDS) })
668697

669698
result = Cve.validateCveRecord(newCveObj.cve)
670699
if (!result.isValid) {
@@ -739,7 +768,7 @@ async function rejectExistingCve (req, res, next) {
739768
const oldCveRecord = result
740769
// update CVE record to rejected
741770
const updatedRecord = Cve.updateCveToRejected(id, providerMetadata, result.cve, req.ctx.body)
742-
const updatedCve = new Cve({ cve: updatedRecord })
771+
const updatedCve = new Cve({ cve: convertDatesToISO(updatedRecord, CONSTANTS.DATE_FIELDS) })
743772

744773
result = Cve.validateCveRecord(updatedCve.cve)
745774
if (!result.isValid) {
@@ -851,7 +880,7 @@ async function insertAdp (req, res, next) {
851880
cveRecord.containers.adp.push(adpContainer)
852881
}
853882

854-
const cveModel = new Cve({ cve: cveRecord })
883+
const cveModel = new Cve({ cve: convertDatesToISO(cveRecord, CONSTANTS.DATE_FIELDS) })
855884
result = Cve.validateCveRecord(cveModel.cve)
856885
if (!result.isValid) {
857886
logger.error(JSON.stringify({ uuid: req.ctx.uuid, message: 'CVE JSON schema validation FAILED.' }))

src/controller/cve.controller/error.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,13 @@ class CveControllerError extends idrErr.IDRError {
111111
err.message = 'The ADP data does not begin with adpContainer.'
112112
return err
113113
}
114+
115+
erlCheckFailed () {
116+
const err = {}
117+
err.error = 'ERL_CHECK_FAILED'
118+
err.message = 'The ERL check failed. The CNA container is not enriched and the ERLCheck flag is set.'
119+
return err
120+
}
114121
}
115122

116123
module.exports = {

src/controller/cve.controller/index.js

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -549,7 +549,8 @@ router.post('/cve/:id/cna',
549549
#swagger.parameters['$ref'] = [
550550
'#/components/parameters/apiEntityHeader',
551551
'#/components/parameters/apiUserHeader',
552-
'#/components/parameters/apiSecretHeader'
552+
'#/components/parameters/apiSecretHeader',
553+
'#/components/parameters/erlCheck'
553554
]
554555
#swagger.requestBody = {
555556
description: '<h3>Notes:</h3>
@@ -620,6 +621,8 @@ router.post('/cve/:id/cna',
620621
mw.validateUser,
621622
mw.onlyCnas,
622623
mw.trimJSONWhitespace,
624+
query().custom((query) => { return mw.validateQueryParameterNames(query, ['erlcheck']) }),
625+
query(['erlcheck']).optional().isBoolean({ loose: true }).withMessage(errorMsgs.ERLCHECK),
623626
validateCveCnaContainerJsonSchema,
624627
validateUniqueEnglishEntry('cnaContainer.descriptions'),
625628
validateDescription(['cnaContainer.descriptions', 'cnaContainer.problemTypes[0].descriptions']),
@@ -645,7 +648,8 @@ router.put('/cve/:id/cna',
645648
#swagger.parameters['$ref'] = [
646649
'#/components/parameters/apiEntityHeader',
647650
'#/components/parameters/apiUserHeader',
648-
'#/components/parameters/apiSecretHeader'
651+
'#/components/parameters/apiSecretHeader',
652+
'#/components/parameters/erlCheck'
649653
]
650654
#swagger.requestBody = {
651655
description: '<h3>Notes:</h3>
@@ -717,6 +721,8 @@ router.put('/cve/:id/cna',
717721
mw.validateUser,
718722
mw.onlyCnas,
719723
mw.trimJSONWhitespace,
724+
query().custom((query) => { return mw.validateQueryParameterNames(query, ['erlcheck']) }),
725+
query(['erlcheck']).optional().isBoolean({ loose: true }).withMessage(errorMsgs.ERLCHECK),
720726
validateCveCnaContainerJsonSchema,
721727
validateUniqueEnglishEntry('cnaContainer.descriptions'),
722728
validateDescription(['cnaContainer.descriptions', 'cnaContainer.problemTypes[0].descriptions']),

src/middleware/errorMessages.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ module.exports = {
1010
COUNT_ONLY: 'Invalid count_only value. Value should be 1, true, or yes to indicate true, or 0, false, or no to indicate false',
1111
TIMESTAMP_FORMAT: "Bad date, or invalid timestamp format: valid format is yyyy-MM-ddTHH:mm:ss or yyyy-MM-ddTHH:mm:ssZZ:ZZ (to use '+' in timezone offset, encode as '%2B). ZZ:ZZ (if used) must be between 00:00 and 23:59.",
1212
CNA_MODIFIED: 'Invalid cna_modified value. Value should be 1, true, or yes to indicate true, or 0, false, or no to indicate false',
13+
ERLCHECK: 'Invalid erlcheck value. Value should be 1, true, or yes to indicate true, or 0, false, or no to indicate false',
1314
FIRSTNAME_LENGTH: 'Invalid name.first. Name must be between 1 and 100 characters in length.',
1415
LASTNAME_LENGTH: 'Invalid name.last. Name must be between 1 and 100 characters in length.',
1516
MIDDLENAME_LENGTH: 'Invalid name.middle. Name must be between 1 and 100 characters in length.',

src/swagger.js

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,11 @@ const doc = {
3434
or <a href='https://www.cve.org/PartnerInformation/ListofPartners/partner/mitre'>MITRE</a>) to request credentials \
3535
</ul> \
3636
<p>CVE data is to be in the JSON 5.1 CVE Record format. Details of the JSON 5.1 schema are \
37-
located <a href='https://github.com/CVEProject/cve-schema/tree/5.1.1-rc2/schema' target='_blank'>here</a>.</p>\
37+
located <a href='https://github.com/CVEProject/cve-schema/tree/v5.1.1-rc2/schema' target='_blank'>here</a>.</p>\
3838
<a href='https://cveform.mitre.org/' class='link' target='_blank'>Contact the CVE Services team</a>",
3939
contact: {
4040
name: 'CVE Services Overview',
41-
url: 'https://cveproject.github.io/automation-cve-services#services-overview'
41+
url: 'https://www.cve.org/AllResources/CveServices'
4242

4343
}
4444
},
@@ -168,6 +168,15 @@ const doc = {
168168
type: 'string'
169169
}
170170
},
171+
erlCheck: {
172+
in: 'query',
173+
name: 'erlcheck',
174+
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.',
175+
required: false,
176+
schema: {
177+
type: 'boolean'
178+
}
179+
},
171180
batch_type: {
172181
in: 'query',
173182
name: 'batch_type',

src/utils/utils.js

Lines changed: 71 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
const Org = require('../model/org')
22
const User = require('../model/user')
33
const getConstants = require('../constants').getConstants
4+
const _ = require('lodash')
45
const { DateTime } = require('luxon')
56

67
async function getOrgUUID (shortName) {
@@ -123,6 +124,7 @@ function reqCtxMapping (req, keyType, keys) {
123124
}
124125

125126
// Return true if boolean is 0, true, or yes, with any mix of casing
127+
// Please note that this function does NOT evaluate "undefined" as false. - A tired developer who lost way too much time to this.
126128
function booleanIsTrue (val) {
127129
if ((val.toString() === '1') ||
128130
(val.toString().toLowerCase() === 'true') ||
@@ -153,15 +155,83 @@ function toDate (val) {
153155
return result
154156
}
155157

158+
// Covert Dates to ISO format
159+
function convertDatesToISO (obj, dateKeys) {
160+
// Helper function to check if a value is a valid date
161+
function isValidDate (value) {
162+
return value instanceof Date && !isNaN(value)
163+
}
164+
165+
// Helper function to check if a string is a valid date
166+
function isStringDate (value) {
167+
const date = new Date(value)
168+
return !isNaN(date.getTime())
169+
}
170+
171+
function updateDateValue (objectToUpdate, key, value) {
172+
if (isValidDate(value)) {
173+
_.set(objectToUpdate, key, value.toISOString())
174+
} else if (typeof value === 'string' && isStringDate(value)) {
175+
_.set(objectToUpdate, key, new Date(value).toISOString())
176+
}
177+
}
178+
179+
// For the top layer object
180+
for (const key of dateKeys) {
181+
if (_.has(obj, key)) {
182+
const value = _.get(obj, key)
183+
updateDateValue(obj, key, value)
184+
}
185+
}
186+
187+
// For the ADP(s)
188+
if (_.has(obj, 'containers.adp')) {
189+
// Use lodash for each to loop over array and check for date keys
190+
_.each(obj.containers.adp, (adp) => {
191+
for (const key of dateKeys) {
192+
if (_.has(adp, key)) {
193+
const value = _.get(adp, key)
194+
updateDateValue(adp, key, value)
195+
}
196+
}
197+
})
198+
}
199+
// For the CNAs
200+
201+
if (_.has(obj, 'containers.cna')) {
202+
// Use lodash to check the containers.cna object for date keys
203+
for (const key of dateKeys) {
204+
if (_.has(obj.containers.cna, key)) {
205+
const value = _.get(obj.containers.cna, key)
206+
updateDateValue(obj.containers.cna, key, value)
207+
}
208+
}
209+
}
210+
211+
return obj
212+
}
213+
214+
function isEnrichedContainer (container) {
215+
const hasCvss = container?.metrics?.some(item => 'cvssV4_0' in item || 'cvssV3_1' in item || 'cvssV3_0' in item || 'cvssV2_0' in item)
216+
const hasCwe = container?.problemTypes?.some(pItem => pItem?.descriptions?.some(dItem => 'cweId' in dItem))
217+
if (!(hasCvss && hasCwe)) {
218+
return false
219+
}
220+
return true
221+
}
222+
156223
module.exports = {
157224
isSecretariat,
158225
isBulkDownload,
159226
isAdmin,
160227
isAdminUUID,
161228
isSecretariatUUID,
229+
isEnrichedContainer,
162230
getOrgUUID,
163231
getUserUUID,
164232
reqCtxMapping,
165233
booleanIsTrue,
166-
toDate
234+
toDate,
235+
convertDatesToISO
236+
167237
}

0 commit comments

Comments
 (0)