Skip to content

Commit e5e1d8f

Browse files
committed
Merge branch 'dev' of https://github.com/CVEProject/cve-services into adp_ambig_dates
2 parents 4c4f5eb + 933e022 commit e5e1d8f

25 files changed

Lines changed: 584 additions & 247 deletions

api-docs/openapi.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"openapi": "3.0.2",
33
"info": {
4-
"version": "ur-v0.3.0",
4+
"version": "2.6.1",
55
"title": "CVE Services API",
66
"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": {
@@ -5078,7 +5078,7 @@
50785078
}
50795079
}
50805080
},
5081-
"/review/org/{uuid}": {
5081+
"/review/{uuid}": {
50825082
"put": {
50835083
"tags": [
50845084
"Review Object"
@@ -5181,7 +5181,7 @@
51815181
}
51825182
}
51835183
},
5184-
"/review/org/{uuid}/approve": {
5184+
"/review/{uuid}/approve": {
51855185
"put": {
51865186
"tags": [
51875187
"Review Object"
@@ -5285,7 +5285,7 @@
52855285
}
52865286
}
52875287
},
5288-
"/review/org/{uuid}/reject": {
5288+
"/review/{uuid}/reject": {
52895289
"put": {
52905290
"tags": [
52915291
"Review Object"

package-lock.json

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

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "cve-services",
33
"author": "Automation Working Group",
4-
"version": "ur-v0.3.0",
4+
"version": "2.6.1",
55
"license": "(CC0)",
66
"devDependencies": {
77
"@faker-js/faker": "^7.6.0",

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

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,32 @@ function datePublicHelper (datePublic) {
178178
return currentDate > datePublicWithGracePeriod
179179
}
180180

181+
/**
182+
* Checks that timeline.time fields are valid datetime objects.
183+
* This accounts for invalid timezone offsets that aren't handled by the schema.
184+
*
185+
* @param {String} dateIndex
186+
* @returns true
187+
* @throws Error
188+
*/
189+
function validateTimelineDates (dateIndex) {
190+
// Check if datePublic is a future date
191+
return body(dateIndex).isArray().withMessage('Time must be a date string').optional({ nullable: true }).bail().custom((timelineArray) => {
192+
for (const timelineObj of timelineArray) {
193+
const value = new Date(timelineObj.time)
194+
if (!validateTimelineHelper(value)) {
195+
throw new Error(`Invalid date string: ${timelineObj.time} `)
196+
}
197+
}
198+
199+
return true
200+
})
201+
}
202+
203+
function validateTimelineHelper (value) {
204+
return value instanceof Date && !isNaN(value)
205+
}
206+
181207
// Organizations in the ADP pilot are generating JSON programatically, and thus
182208
// informing them about the result of the final validation (against the full
183209
// CVE Record schema) is currently sufficient.
@@ -290,6 +316,7 @@ module.exports = {
290316
validateDescription,
291317
validateRejectBody,
292318
validateDatePublic,
319+
validateTimelineDates,
293320
datePublicHelper,
294321
validatePURL,
295322
purlValidateHelper

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, validatePURL } = require('./cve.middleware')
7+
const { parseGetParams, parsePostParams, parseError, validateCveCnaContainerJsonSchema, validateCveAdpContainerJsonSchema, validateRejectBody, validateUniqueEnglishEntry, validateDescription, validateDatePublic, validateTimelineDates, 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]
@@ -499,6 +499,7 @@ router.post('/cve/:id',
499499
validateUniqueEnglishEntry(['containers.cna.descriptions', 'containers.cna.rejectedReasons']),
500500
validateDescription(['containers.cna.rejectedReasons', 'containers.cna.descriptions', 'containers.cna.problemTypes[0].descriptions']),
501501
validateDatePublic(['containers.cna.datePublic']),
502+
validateTimelineDates(['containers.cna.timeline']),
502503
validatePURL(['containers.cna.affected']),
503504
param(['id']).isString().matches(CONSTANTS.CVE_ID_REGEX),
504505
parseError,
@@ -586,6 +587,7 @@ router.put('/cve/:id',
586587
validateUniqueEnglishEntry(['containers.cna.descriptions', 'containers.cna.rejectedReasons']),
587588
validateDescription(['containers.cna.rejectedReasons', 'containers.cna.descriptions', 'containers.cna.problemTypes[0].descriptions']),
588589
validateDatePublic(['containers.cna.datePublic']),
590+
validateTimelineDates(['containers.cna.timeline']),
589591
validatePURL(['containers.cna.affected']),
590592
param(['id']).isString().matches(CONSTANTS.CVE_ID_REGEX),
591593
parseError,
@@ -685,6 +687,7 @@ router.post('/cve/:id/cna',
685687
validateUniqueEnglishEntry('cnaContainer.descriptions'),
686688
validateDescription(['cnaContainer.descriptions', 'cnaContainer.problemTypes[0].descriptions']),
687689
validateDatePublic(['cnaContainer.datePublic']),
690+
validateTimelineDates(['cnaContainer.timeline']),
688691
validatePURL(['cnaContainer.affected']),
689692
param(['id']).isString().matches(CONSTANTS.CVE_ID_REGEX),
690693
parseError,
@@ -786,6 +789,7 @@ router.put('/cve/:id/cna',
786789
validateUniqueEnglishEntry('cnaContainer.descriptions'),
787790
validateDescription(['cnaContainer.descriptions', 'cnaContainer.problemTypes[0].descriptions']),
788791
validateDatePublic(['cnaContainer.datePublic']),
792+
validateTimelineDates(['cnaContainer.timeline']),
789793
validatePURL(['cnaContainer.affected']),
790794
param(['id']).isString().matches(CONSTANTS.CVE_ID_REGEX),
791795
parseError,
@@ -1058,6 +1062,7 @@ router.put('/cve/:id/adp',
10581062
mw.trimJSONWhitespace,
10591063
validateCveAdpContainerJsonSchema,
10601064
validatePURL(['adpContainer.affected']),
1065+
validateTimelineDates(['adpContainer.timeline']),
10611066
param(['id']).isString().matches(CONSTANTS.CVE_ID_REGEX),
10621067
parseError,
10631068
parsePostParams,

src/controller/org.controller/org.controller.js

Lines changed: 8 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,8 @@ const validateUUID = require('uuid').validate
1717
*/
1818
async function getOrgs (req, res, next) {
1919
try {
20-
const session = await mongoose.startSession()
2120
const repo = req.ctx.repositories.getBaseOrgRepository()
2221
const CONSTANTS = getConstants()
23-
let returnValue
2422

2523
// temporary measure to allow tests to work after fixing #920
2624
// tests required changing the global limit to force pagination
@@ -32,11 +30,7 @@ async function getOrgs (req, res, next) {
3230
options.sort = { short_name: 'asc' }
3331
options.page = req.ctx.query.page ? parseInt(req.ctx.query.page) : CONSTANTS.PAGINATOR_PAGE // if 'page' query parameter is not defined, set 'page' to the default page value
3432

35-
try {
36-
returnValue = await repo.getAllOrgs({ ...options, session }, true)
37-
} finally {
38-
await session.endSession()
39-
}
33+
const returnValue = await repo.getAllOrgs({ ...options }, true)
4034

4135
logger.info({ uuid: req.ctx.uuid, message: 'The orgs were sent to the user.' })
4236
return res.status(200).json(returnValue)
@@ -58,7 +52,6 @@ async function getOrgs (req, res, next) {
5852
*/
5953
async function getOrg (req, res, next) {
6054
try {
61-
const session = await mongoose.startSession()
6255
const repo = req.ctx.repositories.getBaseOrgRepository()
6356
const requesterOrgShortName = req.ctx.org
6457
const identifier = req.ctx.params.identifier
@@ -67,26 +60,21 @@ async function getOrg (req, res, next) {
6760
let returnValue
6861

6962
try {
70-
session.startTransaction()
71-
const requesterOrg = await repo.findOneByShortName(requesterOrgShortName, { session }, returnLegacyFormat)
63+
const requesterOrg = await repo.findOneByShortName(requesterOrgShortName, {}, returnLegacyFormat)
7264
const requesterOrgIdentifier = identifierIsUUID ? requesterOrg.UUID : requesterOrgShortName
73-
const isSecretariat = await repo.isSecretariat(requesterOrg, { session }, returnLegacyFormat)
65+
const isSecretariat = await repo.isSecretariat(requesterOrg, {}, returnLegacyFormat)
7466

7567
if (requesterOrgIdentifier !== identifier && !isSecretariat) {
7668
logger.info({ uuid: req.ctx.uuid, message: identifier + ' organization can only be viewed by the users of the same organization or the Secretariat.' })
7769
return res.status(403).json(error.notSameOrgOrSecretariat())
7870
}
7971

80-
returnValue = await repo.getOrg(identifier, identifierIsUUID, { session }, returnLegacyFormat)
72+
returnValue = await repo.getOrg(identifier, identifierIsUUID, {}, returnLegacyFormat)
8173
} catch (error) {
82-
await session.abortTransaction()
8374
// Handle the specific error thrown by BaseOrgRepository.createOrg
8475
if (error.message && error.message.includes('Unknown Org type requested')) {
8576
return res.status(400).json({ message: error.message })
8677
}
87-
throw error
88-
} finally {
89-
await session.endSession()
9078
}
9179
if (!returnValue) { // an empty result can only happen if the requestor is the Secretariat
9280
logger.info({ uuid: req.ctx.uuid, message: identifier + ' organization does not exist.' })
@@ -187,13 +175,13 @@ async function getUser (req, res, next) {
187175
return res.status(404).json(error.userDne(username))
188176
}
189177

190-
const rawResult = result
178+
const rawResult = result.toObject()
191179

192180
delete rawResult._id
193181
delete rawResult.__v
194182
delete rawResult.secret
195183

196-
logger.info({ uuid: req.ctx.uuid, message: username + ' was sent to the user.', user: result })
184+
logger.info({ uuid: req.ctx.uuid, message: username + ' was sent to the user.', user: rawResult })
197185
return res.status(200).json(rawResult)
198186
} catch (err) {
199187
next(err)
@@ -202,7 +190,7 @@ async function getUser (req, res, next) {
202190

203191
/**
204192
* Get details on ID quota for an org with the specified org shortname.
205-
* Called by GET /api/registry/org/{shortname}/id_quota, GET /api/org/{shortname}/id_quota
193+
* Called by GET /api/registry/org/{shortname}/hard_quota, GET /api/org/{shortname}/id_quota
206194
*
207195
* @param {Object} req - The request object
208196
* @param {Object} res - The response object
@@ -336,7 +324,7 @@ async function updateOrg (req, res, next) {
336324
const shortNameUrlParameter = req.ctx.params.shortname
337325
const orgRepository = req.ctx.repositories.getBaseOrgRepository()
338326

339-
const session = await mongoose.startSession()
327+
const session = await mongoose.startSession({ causalConsistency: false })
340328
let responseMessage
341329
// Get the query parameters as JSON
342330
// These are validated by the middleware in org/index.js

src/controller/registry-org.controller/registry-org.controller.js

Lines changed: 17 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,6 @@ async function getAllOrgs (req, res, next) {
7171
*/
7272
async function getOrg (req, res, next) {
7373
try {
74-
const session = await mongoose.startSession()
7574
const repo = req.ctx.repositories.getBaseOrgRepository()
7675
const conversationRepo = req.ctx.repositories.getConversationRepository()
7776
// User passed in parameter to filter for
@@ -81,32 +80,28 @@ async function getOrg (req, res, next) {
8180
let returnValue
8281

8382
try {
84-
session.startTransaction()
85-
const requesterOrg = await repo.findOneByShortName(requesterOrgShortName, { session })
83+
const requesterOrg = await repo.findOneByShortName(requesterOrgShortName)
8684
const requesterOrgIdentifier = identifierIsUUID ? requesterOrg.UUID : requesterOrgShortName
87-
const isSecretariat = await repo.isSecretariat(requesterOrg, { session })
85+
const isSecretariat = await repo.isSecretariat(requesterOrg)
8886

8987
if (requesterOrgIdentifier !== identifier && !isSecretariat) {
9088
logger.info({ uuid: req.ctx.uuid, message: identifier + ' organization can only be viewed by the users of the same organization or the Secretariat.' })
9189
return res.status(403).json(error.notSameOrgOrSecretariat())
9290
}
9391

94-
returnValue = await repo.getOrg(identifier, identifierIsUUID, { session })
92+
returnValue = await repo.getOrg(identifier, identifierIsUUID)
9593

9694
if (returnValue) {
9795
// fetch conversation
98-
const conversation = await conversationRepo.getAllByTargetUUID(returnValue.UUID, isSecretariat, { session })
96+
const conversation = await conversationRepo.getAllByTargetUUID(returnValue.UUID, isSecretariat)
9997
returnValue.conversation = conversation?.length ? _.map(conversation, c => _.omit(c, ['__v', '_id', 'UUID', 'previous_conversation_uuid', 'next_conversation_uuid', 'target_uuid', 'visibility'])) : undefined
10098
}
10199
} catch (error) {
102-
await session.abortTransaction()
103100
// Handle the specific error thrown by BaseOrgRepository.createOrg
104101
if (error.message && error.message.includes('Unknown Org type requested')) {
105102
return res.status(400).json({ message: error.message })
106103
}
107104
throw error
108-
} finally {
109-
await session.endSession()
110105
}
111106
if (!returnValue) { // an empty result can only happen if the requestor is the Secretariat
112107
logger.info({ uuid: req.ctx.uuid, message: identifier + ' organization does not exist.' })
@@ -134,7 +129,7 @@ async function getOrg (req, res, next) {
134129
*/
135130
async function createOrg (req, res, next) {
136131
try {
137-
const session = await mongoose.startSession()
132+
const session = await mongoose.startSession({ causalConsistency: false })
138133
const repo = req.ctx.repositories.getBaseOrgRepository()
139134
const body = req.ctx.body
140135
const isSecretariat = await repo.isSecretariatByShortName(req.ctx.org, { session })
@@ -232,7 +227,7 @@ async function createOrg (req, res, next) {
232227
*/
233228
async function updateOrg (req, res, next) {
234229
try {
235-
const session = await mongoose.startSession()
230+
const session = await mongoose.startSession({ causalConsistency: false })
236231
const shortName = req.ctx.params.shortname
237232
const repo = req.ctx.repositories.getBaseOrgRepository()
238233
const userRepo = req.ctx.repositories.getBaseUserRepository()
@@ -318,15 +313,18 @@ async function updateOrg (req, res, next) {
318313
}
319314
}
320315

316+
// Update Org full will cause a write to the Conversations collection, to avoid a read-after-write issue, we need to get the previous conversation data first
317+
const previousConversation = await conversationRepo.getAllByTargetUUID(await repo.getOrgUUID(shortName, { session }), isSecretariat, { session }) || []
318+
321319
updatedOrg = await repo.updateOrgFull(shortName, req.ctx.body, { session }, false, requestingUser.UUID, isAdmin, isSecretariat)
322320
jointApprovalRequired = _.get(updatedOrg, 'joint_approval_required', false)
323321
_.unset(updatedOrg, 'joint_approval_required')
324-
325-
await session.commitTransaction()
326-
session.startTransaction()
327-
// Checking for existing Conversations
328-
const existingConversations = await conversationRepo.getAllByTargetUUID(updatedOrg.UUID, isSecretariat, { session }) || []
329-
updatedOrg.conversation = existingConversations.map(c => _.omit(c, ['__v', '_id', 'previous_conversation_uuid', 'next_conversation_uuid']))
322+
// append previous conversations to any conversations that are in the org already
323+
const currentConversations = Array.isArray(updatedOrg?.conversation) ? updatedOrg.conversation : []
324+
const prevConversations = Array.isArray(previousConversation) ? previousConversation : []
325+
if (updatedOrg) {
326+
updatedOrg.conversation = [...currentConversations, ...prevConversations].map(c => _.omit(c, ['__v', '_id', 'previous_conversation_uuid', 'next_conversation_uuid']))
327+
}
330328

331329
await session.commitTransaction()
332330
} catch (updateErr) {
@@ -387,7 +385,7 @@ async function updateOrg (req, res, next) {
387385
*/
388386
async function deleteOrg (req, res, next) {
389387
try {
390-
const session = await mongoose.startSession()
388+
const session = await mongoose.startSession({ causalConsistency: false })
391389
const repo = req.ctx.repositories.getBaseOrgRepository()
392390
const shortName = req.ctx.params.identifier
393391

@@ -500,7 +498,7 @@ async function getUsers (req, res, next) {
500498
* Called by POST /api/registryOrg/:shortname/user
501499
*/
502500
async function createUserByOrg (req, res, next) {
503-
const session = await mongoose.startSession()
501+
const session = await mongoose.startSession({ causalConsistency: false })
504502
try {
505503
const body = req.ctx.body
506504
const userRepo = req.ctx.repositories.getBaseUserRepository()

0 commit comments

Comments
 (0)