Repository: CVEProject/cve-services (js)

Issue:
Trim leading and trailing whitespace from cve record fields
## Summary

When a record is submitted to be created through Cve-services, leading and trailing whitespace from field values should be trimmed before the record is validated and created. 

**Definition of Done**
- Create middleware that removes leading and trailing whitespace from values
- Ensure submitted cve json has whitespace in values trimmed
- Ensure records are still validated correctly
- Created unit tests for middleware

Pull request description:
Resolves #1125 trim leading and trailing whitespace from JSON values
Closes Issue #1125

# Summary
Added middleware on CVE related endpoints that trims leading and trailing whitespace from JSON values

# Important Changes
`middleware.js`
- Created `trimJSONWhitespace` function.

`cve.controller/index.js`
- Added `trimJSONWhitespace` on POST/PUT endpoints

# Testing

Steps to manually test updated functionality, if possible
- [ ] 1) Run unit-tests
- [ ] 2) Test manually by submitting JSON with leading/trailing whitespace



Patch:
diff --git a/src/controller/cve.controller/index.js b/src/controller/cve.controller/index.js
index 307d0bff..22fd7b18 100644
--- a/src/controller/cve.controller/index.js
+++ b/src/controller/cve.controller/index.js
@@ -9,7 +9,6 @@ const getConstants = require('../../constants').getConstants
 const CONSTANTS = getConstants()
 const CHOICES = [CONSTANTS.CVE_STATES.REJECTED, CONSTANTS.CVE_STATES.PUBLISHED]
 const toDate = require('../../utils/utils').toDate
-
 router.get('/cve/:id',
   /*
   #swagger.tags = ['CVE Record']
@@ -438,6 +437,7 @@ router.post('/cve/:id',
   */
   mw.validateUser,
   mw.onlySecretariat,
+  mw.trimJSONWhitespace,
   mw.validateCveJsonSchema,
   // the lang key to check depends on the state, so pass both
   validateUniqueEnglishEntry(['containers.cna.descriptions', 'containers.cna.rejectedReasons']),
@@ -522,6 +522,7 @@ router.put('/cve/:id',
   */
   mw.validateUser,
   mw.onlySecretariat,
+  mw.trimJSONWhitespace,
   mw.validateCveJsonSchema,
   // the lang key to check depends on the state, so pass both
   validateUniqueEnglishEntry(['containers.cna.descriptions', 'containers.cna.rejectedReasons']),
@@ -608,6 +609,7 @@ router.post('/cve/:id/cna',
   */
   mw.validateUser,
   mw.onlyCnas,
+  mw.trimJSONWhitespace,
   validateCveCnaContainerJsonSchema,
   validateUniqueEnglishEntry('cnaContainer.descriptions'),
   validateDescription(['cnaContainer.descriptions', 'cnaContainer.problemTypes[0].descriptions']),
@@ -694,6 +696,7 @@ router.put('/cve/:id/cna',
   */
   mw.validateUser,
   mw.onlyCnas,
+  mw.trimJSONWhitespace,
   validateCveCnaContainerJsonSchema,
   validateUniqueEnglishEntry('cnaContainer.descriptions'),
   validateDescription(['cnaContainer.descriptions', 'cnaContainer.problemTypes[0].descriptions']),
@@ -780,6 +783,7 @@ router.post('/cve/:id/reject',
   */
   mw.validateUser,
   mw.onlyCnas,
+  mw.trimJSONWhitespace,
   validateRejectBody,
   validateUniqueEnglishEntry(['cnaContainer.rejectedReasons']),
   validateDescription(['cnaContainer.rejectedReasons']),
@@ -867,6 +871,7 @@ router.put('/cve/:id/reject',
   */
   mw.validateUser,
   mw.onlyCnas,
+  mw.trimJSONWhitespace,
   validateRejectBody,
   validateUniqueEnglishEntry('cnaContainer.rejectedReasons'),
   validateDescription(['cnaContainer.rejectedReasons']),
@@ -954,6 +959,7 @@ router.put('/cve/:id/adp',
   */
   mw.validateUser,
   mw.onlyAdps,
+  mw.trimJSONWhitespace,
   validateCveAdpContainerJsonSchema,
   param(['id']).isString().matches(CONSTANTS.CVE_ID_REGEX),
   parseError,
diff --git a/src/middleware/middleware.js b/src/middleware/middleware.js
index 2d2f6867..ed497219 100644
--- a/src/middleware/middleware.js
+++ b/src/middleware/middleware.js
@@ -310,6 +310,8 @@ async function cnaMustOwnID (req, res, next) {
 
 function validateCveJsonSchema (req, res, next) {
   const CONSTANTS = getConstants()
+
+  // Trim any leading/trailing whitespace from values
   const cve = req.body
   let cveState = cve.cveMetadata
 
@@ -374,6 +376,31 @@ function validateJsonSyntax (err, req, res, next) {
   }
 }
 
+// Middleware function to trim trailing and leading whitespace from JSON field values
+function trimJSONWhitespace (req, res, next) {
+  try {
+    const queue = [req.body] // A queue to track objects for processing
+
+    while (queue.length > 0) {
+      const currentObject = queue.shift()
+
+      for (const key in currentObject) {
+        const value = currentObject[key]
+
+        if (typeof value === 'string') {
+          currentObject[key] = value.trim()
+        } else if (typeof value === 'object') {
+          queue.push(value) // Add nested objects to the queue
+        }
+      }
+    }
+  } catch (err) {
+    next(err)
+  }
+
+  next()
+}
+
 function errorHandler (err, req, res, next) {
   logger.error(JSON.stringify({ error: err.stack }))
   return res.status(500).json(error.serviceNotAvailable())
@@ -471,5 +498,6 @@ module.exports = {
   rateLimiter: limiter,
   isFlatStringArray,
   toUpperCaseArray,
-  containsNoInvalidCharacters
+  containsNoInvalidCharacters,
+  trimJSONWhitespace
 }


Test patch:
diff --git a/test/unit-tests/middleware/trimJSONWhitespaceTest.js b/test/unit-tests/middleware/trimJSONWhitespaceTest.js
new file mode 100644
index 00000000..900c53c8
--- /dev/null
+++ b/test/unit-tests/middleware/trimJSONWhitespaceTest.js
@@ -0,0 +1,67 @@
+const chai = require('chai')
+const expect = chai.expect
+const sinon = require('sinon')
+
+const { trimJSONWhitespace } = require('../../../src/middleware/middleware')
+
+describe('Testing trimJSONWhitespace middleware', () => {
+  let status, json, res, next
+  beforeEach(() => {
+    status = sinon.stub()
+    json = sinon.spy()
+    res = { json, status }
+    next = sinon.spy()
+    status.returns(res)
+  })
+
+  it('Should successfully trim leading/trailing whitespace for a simple JSON object', async () => {
+    const req = {
+      body: {
+        field1: '     this has whitespace   ',
+        field2: 'trailing whitespace only    '
+      }
+    }
+    trimJSONWhitespace(req, res, next)
+    expect(req.body).to.be.an('object')
+    expect(req.body).to.deep.equal({ field1: 'this has whitespace', field2: 'trailing whitespace only' })
+  })
+
+  it('Should successfully trim leading/trailing whitespace for a nested JSON object', async () => {
+    const req = {
+      body: {
+        field1: {
+          nestedObj: {
+            name: '   Test Name   ',
+            secondObj: {
+              test: '   second value   '
+            }
+          }
+        }
+      }
+    }
+    trimJSONWhitespace(req, res, next)
+    expect(req.body).to.be.an('object')
+    expect(req.body).to.deep.equal({
+      field1: {
+        nestedObj: {
+          name: 'Test Name',
+          secondObj: {
+            test: 'second value'
+          }
+        }
+      }
+    })
+  })
+
+  it('Should ignore non-string and non-object values', async () => {
+    const req = {
+      body: {
+        test: 'Test Name',
+        numberTest: 25
+      }
+    }
+    trimJSONWhitespace(req, res, next)
+    expect(req.body).to.be.an('object')
+    expect(req.body).to.deep.equal({ test: 'Test Name', numberTest: 25 })
+  })
+})