Repository: CVEProject/cve-services
Pull request #339 (2021-03-16 16:19:26)

Issue 330
#330 Allow only secretariats to update CVE records.


Patch:
diff --git a/docs/openapi.yml b/docs/openapi.yml
index 13e8809f..97ee3c63 100644
--- a/docs/openapi.yml
+++ b/docs/openapi.yml
@@ -314,3 +314,29 @@ paths:
           description: Not Found
         500:
           description: Internal Server Error
+    put:
+      summary: Updates a CVE ID record by ID, only allowed by the Secretariat
+      parameters:
+        - "$ref": "#/components/parameters/api-entity-header"
+        - "$ref": "#/components/parameters/api-secret-header"
+        - "$ref": "#/components/parameters/api-user-header"
+        - "$ref": "#/components/parameters/cve-id-path"
+      responses:
+        200:
+          description: Returns the cve record updated.
+          content:
+            application/json:
+              schema: 
+                oneOf:
+                  $ref: 'https://github.com/CVEProject/cve-schema/blob/master/schema/v4.0/CVE_JSON_4.0_min_public.schema'
+                  $ref: 'https://github.com/CVEProject/cve-schema/blob/master/schema/v4.0/CVE_JSON_4.0_min_reject.schema'
+        400:
+          description: Bad Request
+        401:
+          description: Not Authenticated
+        403:
+          description: Forbidden
+        404:
+          description: Not Found
+        500:
+          description: Internal Server Error
diff --git a/src/controller/cve.controller/cve.controller.js b/src/controller/cve.controller/cve.controller.js
index 343f1c5c..6328a531 100644
--- a/src/controller/cve.controller/cve.controller.js
+++ b/src/controller/cve.controller/cve.controller.js
@@ -70,35 +70,63 @@ async function submitCve (req, res, next) {
 }
 
 // Updates a CVE if one exists for the specified CVE ID
-async function updateCve (req, res) {
-  const cve = req.body
-  const cveId = req.params.id
+async function updateCve (req, res, next) {
+  try {
+    const cve = req.ctx.body
+    const newCve = new Cve({ cve: cve })
+    const cveId = req.ctx.params.id
+    const cveRepo = req.ctx.repositories.getCveRepository()
+    const cveIdRepo = req.ctx.repositories.getCveIdRepository()
+    const orgRepo = req.ctx.repositories.getOrgRepository()
 
-  if (cveId !== cve.CVE_data_meta.ID) {
-    return res.status(400).json({ message: cveId + ' parameter does not match ' + cve.CVE_data_meta.ID + ' in the JSON body.' })
-  }
+    // cve id in body must match id in URL param
+    if (cveId !== cve.CVE_data_meta.ID) {
+      return res.status(400).json(error.cveIdMismatch())
+    }
+
+    // check that cve does not have status 'RESERVED'
+    if (newCve.cve.CVE_data_meta.STATE === CONSTANTS.CVE_STATES.RESERVED) {
+      return res.status(400).json(error.cveUpdateUnsupportedState(CONSTANTS.CVE_STATES.RESERVED))
+    }
+
+    // update the cve id in MongoDB
+    let result = await cveIdRepo.updateByCveId(cveId, { state: newCve.cve.CVE_data_meta.STATE })
+
+    // check if cve id was found and updated
+    if (result.n === 0) {
+      logger.info(cveId + ' does not exist.')
+      return res.status(403).json(error.cveDne())
+    }
+
+    // update CVE record if it exists
+    result = await cveRepo.updateByCveId(cveId, newCve)
+
+    // check if CVE record was found and updated
+    if (result.n === 0) {
+      logger.info(cveId + ' does not exist.')
+      return res.status(403).json(error.cveRecordDne())
+    }
 
-  const newCve = new Cve({ cve: cve })
-
-  // Find and Update CVE in MongoDB
-  Cve.findOneAndUpdate()
-    .byCveId(cveId)
-    .updateOne(newCve)
-    .exec((err, result) => {
-      if (err) {
-        logger.warn('An error ocurred in MongoDB. ' + cveId + ' was not updated in the database.')
-        logger.error(err.stack)
-        return res.status(500).send(CONSTANTS.SERVER_RESPONSES.Internal_Server_Error)
-      }
-
-      if (result.n === 0) {
-        logger.info(cveId + ' does not exist.')
-        return res.status(404).json({ message: cveId + ' does not exist.' })
-      }
-
-      logger.info(cveId + ' has been updated.')
-      return res.status(200).json({ message: cveId + ' has been updated.' })
-    })
+    const responseMessage = {
+      message: cveId + ' record was successfully updated.',
+      updated: newCve.cve
+    }
+
+    const payload = {
+      action: 'update_cve_record',
+      change: cveId + ' record was successfully updated.',
+      req_UUID: req.ctx.uuid,
+      org_UUID: await orgRepo.getOrgUUID(req.ctx.org),
+      cve: cveId
+    }
+
+    const userRepo = req.ctx.repositories.getUserRepository()
+    payload.user_UUID = await userRepo.getUserUUID(req.ctx.user, payload.org_UUID)
+    logger.info(JSON.stringify(payload))
+    return res.status(200).json(responseMessage)
+  } catch (err) {
+    next(err)
+  }
 }
 
 module.exports = {
diff --git a/src/controller/cve.controller/error.js b/src/controller/cve.controller/error.js
index d2ff5cfe..eb33e8c2 100644
--- a/src/controller/cve.controller/error.js
+++ b/src/controller/cve.controller/error.js
@@ -15,6 +15,13 @@ class CveControllerError extends idrErr.IDRError {
     return err
   }
 
+  cveRecordDne () { // cve
+    const err = {}
+    err.error = 'CVE_RECORD_DNE'
+    err.message = 'The cve record for the cve id does not exist.'
+    return err
+  }
+
   cveDne () { // cve
     const err = {}
     err.error = 'CVEID_DNE'
@@ -28,6 +35,13 @@ class CveControllerError extends idrErr.IDRError {
     err.message = `Cannot create cve record in state ${state}.`
     return err
   }
+
+  cveUpdateUnsupportedState (state) { // cve
+    const err = {}
+    err.error = 'CVE_UNSUPPORTED_RECORD_STATE'
+    err.message = `Cannot update cve record in state ${state}.`
+    return err
+  }
 }
 
 module.exports = {
diff --git a/src/controller/cve.controller/index.js b/src/controller/cve.controller/index.js
index ee8eafdd..4cb15053 100644
--- a/src/controller/cve.controller/index.js
+++ b/src/controller/cve.controller/index.js
@@ -16,6 +16,13 @@ router.post('/cve/:id',
 
 // router.get('/cve', mw.validateUser, controller.CVE_LIST_ALL)
 // router.get('/cve/:id', mw.validateUser, controller.CVE_SINGLE)
-// router.post('/cve/:id', mw.validateUser, mw.validateCveJsonSchema, controller.CVE_UPDATE_SINGLE)
+router.put('/cve/:id',
+  mw.onlySecretariat,
+  mw.validateUser,
+  mw.validateCveJsonSchema,
+  param(['id']).isString().matches(/^CVE-[0-9]{4}-[0-9]{4,}$/i),
+  parseError,
+  parsePostParams,
+  controller.CVE_UPDATE_SINGLE)
 
 module.exports = router


Test patch:
diff --git a/test/unit-tests/cve/cveTest.js b/test/unit-tests/cve/cveTest.js
index 14ac1bfe..cdd761a2 100644
--- a/test/unit-tests/cve/cveTest.js
+++ b/test/unit-tests/cve/cveTest.js
@@ -128,7 +128,7 @@ server.route('/cve-create-record-positive-tests/:id')
     next()
   }, cveParams.parsePostParams, cveController.CVE_SUBMIT)
 
-describe('Testing the /cve/:id endpoint in Cve Controller', () => {
+describe('Testing the POST /cve/:id endpoint in Cve Controller', () => {
   context('Negative Tests', () => {
     it('should return 400 bad request because the cve id in the parameter does not match the cve id in the JSON body', (done) => {
       chai.request(server)
@@ -285,4 +285,320 @@ describe('Testing the /cve/:id endpoint in Cve Controller', () => {
         })
     })
   })
+
+  context('Update CVE PUT Endpoint', () => {
+    it('Update CVE record when requestor is secretariat', (done) => {
+      class OrgRepo {
+        async getOrgUUID () {
+          return null
+        }
+      }
+      class CveRepo {
+        async updateByCveId (cveId, newCve) {
+          expect(cveId).to.equal(cveIdPublic4)
+          expect(newCve).to.have.nested.property('cve.CVE_data_meta.STATE').and.to.equal(CONSTANTS.CVE_STATES.PUBLIC)
+          return { n: 1 }
+        }
+      }
+      class CveIdRepo {
+        async updateByCveId (cveId, newCve) {
+          expect(cveId).to.equal(cveIdPublic4)
+          expect(newCve).to.have.property('state')
+          return { n: 1 }
+        }
+      }
+      class UserRepo {
+        async getUserUUID () {
+          return null
+        }
+      }
+      server.route('/cve-update-record/:id')
+        .put((req, res, next) => {
+          const factory = {
+            getCveIdRepository: () => { return new CveIdRepo() },
+            getCveRepository: () => { return new CveRepo() },
+            getOrgRepository: () => { return new OrgRepo() },
+            getUserRepository: () => { return new UserRepo() }
+          }
+          req.ctx.repositories = factory
+          next()
+        }, cveParams.parsePostParams, cveController.CVE_UPDATE_SINGLE)
+
+      chai.request(server)
+        .put(`/cve-update-record/${cveIdPublic4}`)
+        .set(cveFixtures.secretariatHeader)
+        .send(cvePublicPass4)
+        .end((err, res) => {
+          if (err) {
+            done(err)
+          }
+
+          expect(res).to.have.status(200)
+          expect(res).to.have.property('body').and.to.be.a('object')
+          expect(res.body).to.have.property('updated').and.to.be.a('object')
+          expect(res.body.updated).to.have.nested.property('CVE_data_meta.ID').and.to.equal(cveIdPublic4)
+          expect(res.body.updated).to.have.nested.property('CVE_data_meta.STATE').and.to.equal(CONSTANTS.CVE_STATES.PUBLIC)
+          done()
+        })
+    })
+
+    it('Update CVE record when requestor is secretariat and valid REJECT JSON', (done) => {
+      class OrgRepo {
+        async getOrgUUID () {
+          return null
+        }
+      }
+      class CveRepo {
+        async updateByCveId (cveId, newCve) {
+          return { n: 1 }
+        }
+      }
+      class CveIdRepo {
+        async updateByCveId (cveId, newCve) {
+          return { n: 1 }
+        }
+      }
+      class UserRepo {
+        async getUserUUID () {
+          return null
+        }
+      }
+      server.route('/cve-update-record-reject/:id')
+        .put((req, res, next) => {
+          const factory = {
+            getCveIdRepository: () => { return new CveIdRepo() },
+            getCveRepository: () => { return new CveRepo() },
+            getOrgRepository: () => { return new OrgRepo() },
+            getUserRepository: () => { return new UserRepo() }
+          }
+          req.ctx.repositories = factory
+          next()
+        }, cveParams.parsePostParams, cveController.CVE_UPDATE_SINGLE)
+
+      chai.request(server)
+        .put(`/cve-update-record-reject/${cveIdReject4}`)
+        .set(cveFixtures.secretariatHeader)
+        .send(cveRejectPass4)
+        .end((err, res) => {
+          if (err) {
+            done(err)
+          }
+
+          expect(res).to.have.status(200)
+          expect(res).to.have.property('body').and.to.be.a('object')
+          expect(res.body).to.have.property('updated').and.to.be.a('object')
+          expect(res.body.updated).to.have.nested.property('CVE_data_meta.ID').and.to.equal(cveIdReject4)
+          expect(res.body.updated).to.have.nested.property('CVE_data_meta.STATE').and.to.equal(CONSTANTS.CVE_STATES.REJECT)
+          done()
+        })
+    })
+
+    it('CVE record not updated when cve id doesnt match record', (done) => {
+      class OrgRepo {
+        async getOrgUUID () {
+          return null
+        }
+      }
+      class CveRepo {
+        async updateByCveId (cveId, newCve) {
+          return null
+        }
+      }
+      class CveIdRepo {
+        async updateByCveId (cveId, newCve) {
+          return null
+        }
+      }
+      class UserRepo {
+        async getUserUUID () {
+          return null
+        }
+      }
+      server.route('/cve-not-updated-id-mismatch/:id')
+        .put((req, res, next) => {
+          const factory = {
+            getCveIdRepository: () => { return new CveIdRepo() },
+            getCveRepository: () => { return new CveRepo() },
+            getOrgRepository: () => { return new OrgRepo() },
+            getUserRepository: () => { return new UserRepo() }
+          }
+          req.ctx.repositories = factory
+          next()
+        }, cveParams.parsePostParams, cveController.CVE_UPDATE_SINGLE)
+
+      chai.request(server)
+        .put(`/cve-not-updated-id-mismatch/${nonExistentCveId}`)
+        .set(cveFixtures.secretariatHeader)
+        .send(cvePublicPass4)
+        .end((err, res) => {
+          if (err) {
+            done(err)
+          }
+
+          expect(res).to.have.status(400)
+          expect(res).to.have.property('body').and.to.be.a('object')
+          const errObj = error.cveIdMismatch()
+          expect(res.body.error).to.equal(errObj.error)
+          expect(res.body.message).to.equal(errObj.message)
+          done()
+        })
+    })
+
+    it('CVE record not updated when cve record doesnt exist', (done) => {
+      const nonExistentCve = JSON.parse(JSON.stringify(cvePublicPass4))
+      nonExistentCve.CVE_data_meta.ID = nonExistentCveId
+      class OrgRepo {
+        async getOrgUUID () {
+          return null
+        }
+      }
+      class CveRepo {
+        async updateByCveId (cveId, newCve) {
+          return { n: 0 }
+        }
+      }
+      class CveIdRepo {
+        async updateByCveId (cveId, newCve) {
+          return { n: 1 }
+        }
+      }
+      class UserRepo {
+        async getUserUUID () {
+          return null
+        }
+      }
+      server.route('/cve-not-updated-cve-dne/:id')
+        .put((req, res, next) => {
+          const factory = {
+            getCveIdRepository: () => { return new CveIdRepo() },
+            getCveRepository: () => { return new CveRepo() },
+            getOrgRepository: () => { return new OrgRepo() },
+            getUserRepository: () => { return new UserRepo() }
+          }
+          req.ctx.repositories = factory
+          next()
+        }, cveParams.parsePostParams, cveController.CVE_UPDATE_SINGLE)
+
+      chai.request(server)
+        .put(`/cve-not-updated-cve-dne/${nonExistentCveId}`)
+        .set(cveFixtures.secretariatHeader)
+        .send(nonExistentCve)
+        .end((err, res) => {
+          if (err) {
+            done(err)
+          }
+
+          expect(res).to.have.status(403)
+          expect(res).to.have.property('body').and.to.be.a('object')
+          const errObj = error.cveRecordDne()
+          expect(res.body.error).to.equal(errObj.error)
+          expect(res.body.message).to.equal(errObj.message)
+          done()
+        })
+    })
+
+    it('CVE record not updated when cve id doesnt exist', (done) => {
+      const nonExistentCve = JSON.parse(JSON.stringify(cvePublicPass4))
+      nonExistentCve.CVE_data_meta.ID = nonExistentCveId
+      class OrgRepo {
+        async getOrgUUID () {
+          return null
+        }
+      }
+      class CveRepo {
+        async updateByCveId (cveId, newCve) {
+          return null
+        }
+      }
+      class CveIdRepo {
+        async updateByCveId (cveId, newCve) {
+          return { n: 0 }
+        }
+      }
+      class UserRepo {
+        async getUserUUID () {
+          return null
+        }
+      }
+      server.route('/cve-not-updated-cve-id-dne/:id')
+        .put((req, res, next) => {
+          const factory = {
+            getCveIdRepository: () => { return new CveIdRepo() },
+            getCveRepository: () => { return new CveRepo() },
+            getOrgRepository: () => { return new OrgRepo() },
+            getUserRepository: () => { return new UserRepo() }
+          }
+          req.ctx.repositories = factory
+          next()
+        }, cveParams.parsePostParams, cveController.CVE_UPDATE_SINGLE)
+
+      chai.request(server)
+        .put(`/cve-not-updated-cve-id-dne/${nonExistentCveId}`)
+        .set(cveFixtures.secretariatHeader)
+        .send(nonExistentCve)
+        .end((err, res) => {
+          if (err) {
+            done(err)
+          }
+
+          expect(res).to.have.status(403)
+          expect(res).to.have.property('body').and.to.be.a('object')
+          const errObj = error.cveDne()
+          expect(res.body.error).to.equal(errObj.error)
+          expect(res.body.message).to.equal(errObj.message)
+          done()
+        })
+    })
+
+    it('CVE record not updated when cve record updated to RESERVED state', (done) => {
+      class OrgRepo {
+        async getOrgUUID () {
+          return null
+        }
+      }
+      class CveRepo {
+        async updateByCveId (cveId, newCve) {
+          return null
+        }
+      }
+      class CveIdRepo {
+        async updateByCveId (cveId, newCve) {
+          return null
+        }
+      }
+      class UserRepo {
+        async getUserUUID () {
+          return null
+        }
+      }
+      server.route('/cve-not-updated-reserved-state/:id')
+        .put((req, res, next) => {
+          const factory = {
+            getCveIdRepository: () => { return new CveIdRepo() },
+            getCveRepository: () => { return new CveRepo() },
+            getOrgRepository: () => { return new OrgRepo() },
+            getUserRepository: () => { return new UserRepo() }
+          }
+          req.ctx.repositories = factory
+          next()
+        }, cveParams.parsePostParams, cveController.CVE_UPDATE_SINGLE)
+
+      chai.request(server)
+        .put(`/cve-not-updated-reserved-state/${cveIdReserved4}`)
+        .set(cveFixtures.secretariatHeader)
+        .send(cveReservedPass4)
+        .end((err, res) => {
+          if (err) {
+            done(err)
+          }
+
+          expect(res).to.have.status(400)
+          expect(res).to.have.property('body').and.to.be.a('object')
+          const errObj = error.cveUpdateUnsupportedState(CONSTANTS.CVE_STATES.RESERVED)
+          expect(res.body.error).to.equal(errObj.error)
+          expect(res.body.message).to.equal(errObj.message)
+          done()
+        })
+    })
+  })
 })