Skip to content

Commit 833844f

Browse files
authored
Merge pull request #1834 from CVEProject/dr_1831
Resolves issue 1831, updates `program_data.cve_website_update_date` to behave as a date-only field.
2 parents 3970669 + 90d031c commit 833844f

9 files changed

Lines changed: 145 additions & 7 deletions

File tree

schemas/registry-org/BaseOrg.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,7 @@
167167
"properties": {
168168
"cve_website_update_date": {
169169
"type": "string",
170-
"format": "date-time"
170+
"format": "date"
171171
},
172172
"cve_website_update_needed": {
173173
"type": "boolean"

schemas/registry-org/create-registry-org-request.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@
166166
"properties": {
167167
"cve_website_update_date": {
168168
"type": "string",
169-
"format": "date-time"
169+
"format": "date"
170170
},
171171
"cve_website_update_needed": {
172172
"type": "boolean"

schemas/registry-org/get-registry-org-response.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@
129129
"properties": {
130130
"cve_website_update_date": {
131131
"type": "string",
132-
"format": "date-time"
132+
"format": "date"
133133
},
134134
"cve_website_update_needed": {
135135
"type": "boolean"

schemas/registry-org/list-registry-orgs-response.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,7 @@
158158
"properties": {
159159
"cve_website_update_date": {
160160
"type": "string",
161-
"format": "date-time"
161+
"format": "date"
162162
},
163163
"cve_website_update_needed": {
164164
"type": "boolean"

schemas/registry-org/update-registry-org-request.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,7 @@
182182
"properties": {
183183
"cve_website_update_date": {
184184
"type": "string",
185-
"format": "date-time"
185+
"format": "date"
186186
},
187187
"cve_website_update_needed": {
188188
"type": "boolean"

src/model/baseorg.js

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
const mongoose = require('mongoose')
22
const aggregatePaginate = require('mongoose-aggregate-paginate-v2')
33
const MongoPaging = require('mongo-cursor-pagination')
4+
const { isValidDateOnlyString, normalizeDateOnlyInput } = require('../utils/dateOnly')
45

56
const toUndefined = value => (value === '' ? undefined : value)
67

@@ -29,7 +30,14 @@ const schema = {
2930
partner_number: String,
3031
partner_country: String,
3132
program_data: {
32-
cve_website_update_date: Date,
33+
cve_website_update_date: {
34+
type: String,
35+
set: normalizeDateOnlyInput,
36+
validate: {
37+
validator: value => value == null || isValidDateOnlyString(value),
38+
message: 'cve_website_update_date must be a date in YYYY-MM-DD format.'
39+
}
40+
},
3341
cve_website_update_needed: Boolean,
3442
partner_active_date: String,
3543
partner_inactive_date: String,

src/repositories/baseOrgRepository.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ const {
1616
createAuditLogEntry,
1717
handleAuthorityModelChange
1818
} = require('./baseOrgRepositoryHelpers')
19+
const { normalizeOrgCveWebsiteUpdateDate } = require('../utils/dateOnly')
1920

2021
/**
2122
* @function setAggregateOrgObj
@@ -111,6 +112,7 @@ function getOrgProjection (isSecretariat = false) {
111112
function filterOrg (orgObj, isSecretariat = false) {
112113
const CONSTANTS = getConstants()
113114
const _ = require('lodash')
115+
normalizeOrgCveWebsiteUpdateDate(orgObj, { output: true })
114116
let fieldsToOmit = [...CONSTANTS.ORG_EXCLUDED_FIELDS]
115117
if (!isSecretariat) {
116118
fieldsToOmit = [...fieldsToOmit, ...CONSTANTS.ORG_RESTRICTED_FIELDS]
@@ -399,6 +401,7 @@ class BaseOrgRepository extends BaseRepository {
399401
if (org.reports_to === null) {
400402
delete org.reports_to
401403
}
404+
normalizeOrgCveWebsiteUpdateDate(org, { output: true })
402405
})
403406
}
404407

@@ -475,6 +478,7 @@ class BaseOrgRepository extends BaseRepository {
475478
}
476479
}
477480

481+
normalizeOrgCveWebsiteUpdateDate(result, { output: true })
478482
return deepRemoveEmpty(result)
479483
}
480484

@@ -913,6 +917,8 @@ class BaseOrgRepository extends BaseRepository {
913917
const originalPlain = orgObjectOriginal.toObject ? orgObjectOriginal.toObject() : orgObjectOriginal
914918
const originalSerialized = JSON.parse(JSON.stringify(originalPlain))
915919
const updatedSerialized = JSON.parse(JSON.stringify(orgObjectUpdated))
920+
normalizeOrgCveWebsiteUpdateDate(originalSerialized, { output: true })
921+
normalizeOrgCveWebsiteUpdateDate(updatedSerialized)
916922

917923
// Filter the list to find only fields that have changed
918924
const changedFields = _.filter(jointApprovalFields, field => {
@@ -1034,6 +1040,8 @@ class BaseOrgRepository extends BaseRepository {
10341040
* @returns {object} The validation result object.
10351041
*/
10361042
validateOrg (org) {
1043+
normalizeOrgCveWebsiteUpdateDate(org)
1044+
10371045
if (!org.authority || (Array.isArray(org.authority) && org.authority.length === 0)) {
10381046
return { isValid: false, errors: [{ instancePath: '/authority', message: 'authority is required' }] }
10391047
}

src/utils/dateOnly.js

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
const ISO_DATE_PREFIX = /^(\d{4})-(\d{2})-(\d{2})(?:$|T)/
2+
const ISO_DATE_ONLY = /^(\d{4})-(\d{2})-(\d{2})$/
3+
const MONGOOSE_DATE_STRING = /^[A-Z][a-z]{2} [A-Z][a-z]{2} \d{2} \d{4} .* GMT[+-]\d{4}/
4+
5+
function isValidDateOnlyParts (year, month, day) {
6+
const yearNumber = Number(year)
7+
const monthNumber = Number(month)
8+
const dayNumber = Number(day)
9+
const date = new Date(Date.UTC(yearNumber, monthNumber - 1, dayNumber))
10+
11+
return (
12+
date.getUTCFullYear() === yearNumber &&
13+
date.getUTCMonth() === monthNumber - 1 &&
14+
date.getUTCDate() === dayNumber
15+
)
16+
}
17+
18+
function isValidDateOnlyString (value) {
19+
if (typeof value !== 'string') return false
20+
const match = value.match(ISO_DATE_ONLY)
21+
if (!match) return false
22+
return isValidDateOnlyParts(match[1], match[2], match[3])
23+
}
24+
25+
function normalizeDateOnlyInput (value) {
26+
if (value instanceof Date) {
27+
if (isNaN(value)) return value
28+
return value.toISOString().split('T')[0]
29+
}
30+
31+
if (typeof value !== 'string') return value
32+
33+
const match = value.match(ISO_DATE_PREFIX)
34+
if (!match) return value
35+
if (!isValidDateOnlyParts(match[1], match[2], match[3])) return value
36+
37+
return `${match[1]}-${match[2]}-${match[3]}`
38+
}
39+
40+
function normalizeDateOnlyOutput (value) {
41+
const normalizedValue = normalizeDateOnlyInput(value)
42+
if (normalizedValue !== value) return normalizedValue
43+
44+
if (typeof value === 'string' && MONGOOSE_DATE_STRING.test(value)) {
45+
const date = new Date(value)
46+
if (!isNaN(date)) return date.toISOString().split('T')[0]
47+
}
48+
49+
return value
50+
}
51+
52+
function normalizeOrgCveWebsiteUpdateDate (org, options = {}) {
53+
if (!org || typeof org !== 'object') return org
54+
if (!org.program_data || typeof org.program_data !== 'object') return org
55+
if (!Object.prototype.hasOwnProperty.call(org.program_data, 'cve_website_update_date')) return org
56+
57+
const normalize = options.output ? normalizeDateOnlyOutput : normalizeDateOnlyInput
58+
org.program_data.cve_website_update_date = normalize(org.program_data.cve_website_update_date)
59+
60+
return org
61+
}
62+
63+
module.exports = {
64+
isValidDateOnlyString,
65+
normalizeDateOnlyInput,
66+
normalizeDateOnlyOutput,
67+
normalizeOrgCveWebsiteUpdateDate
68+
}

test/integration-tests/registry-org/registryOrgCRUDTest.js

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,36 @@ describe('Testing /registryOrg endpoints', () => {
109109
expect(res.body.created.vulnerability_advisory_location_for_web_scraping).to.deep.equal(['https://example.com/scraping'])
110110
})
111111
})
112+
it('Creates a new registry org with a date-only CVE website update date', async () => {
113+
const cveWebsiteUpdateDate = '2024-01-15'
114+
const orgWithWebsiteUpdateDate = {
115+
...testRegistryOrg,
116+
short_name: 'registry_org_test_web_date',
117+
program_data: {
118+
status: 'active',
119+
cve_website_update_date: cveWebsiteUpdateDate
120+
}
121+
}
122+
123+
await chai.request(app)
124+
.post('/api/registry/org')
125+
.set(secretariatHeaders)
126+
.send(orgWithWebsiteUpdateDate)
127+
.then((res, err) => {
128+
expect(err).to.be.undefined
129+
expect(res).to.have.status(200)
130+
expect(res.body.created).to.haveOwnProperty('program_data')
131+
expect(res.body.created.program_data.cve_website_update_date).to.equal(cveWebsiteUpdateDate)
132+
})
133+
134+
await chai.request(app)
135+
.get('/api/registry/org/registry_org_test_web_date')
136+
.set(secretariatHeaders)
137+
.then((res) => {
138+
expect(res).to.have.status(200)
139+
expect(res.body.program_data.cve_website_update_date).to.equal(cveWebsiteUpdateDate)
140+
})
141+
})
112142
})
113143
context('Negative Tests', () => {
114144
it('Fails to create a new registry organization with an existing short name', async () => {
@@ -149,6 +179,27 @@ describe('Testing /registryOrg endpoints', () => {
149179
expect(res.body.message).to.equal('Parameters were invalid')
150180
})
151181
})
182+
it('Fails to create a new registry organization with an ambiguous CVE website update date', async () => {
183+
await chai.request(app)
184+
.post('/api/registry/org')
185+
.set(secretariatHeaders)
186+
.send({
187+
...testRegistryOrg,
188+
short_name: 'test_create_ambiguous_date',
189+
program_data: {
190+
status: 'active',
191+
cve_website_update_date: '01/15/2024'
192+
}
193+
})
194+
.then((res) => {
195+
expect(res).to.have.status(400)
196+
expect(res.body.message).to.equal('Parameters were invalid')
197+
198+
const dateError = res.body.errors.find(error => error.instancePath === '/program_data/cve_website_update_date')
199+
expect(dateError).to.not.be.undefined
200+
expect(dateError.message).to.equal('must match format "date"')
201+
})
202+
})
152203
it('Fails to create a new registry organization with reports_to manually provided', async () => {
153204
await chai.request(app)
154205
.post('/api/registry/org')
@@ -363,6 +414,7 @@ describe('Testing /registryOrg endpoints', () => {
363414
})
364415
it('Allows Secretariat to update program_data', async () => {
365416
const partnerActiveDate = '2024-01-15'
417+
const cveWebsiteUpdateDate = '2024-04-10T18:30:00.000Z'
366418
await chai.request(app)
367419
.put('/api/registry/org/registry_org_test')
368420
.set(secretariatHeaders)
@@ -372,7 +424,8 @@ describe('Testing /registryOrg endpoints', () => {
372424
vulnerability_advisory_location_for_web_scraping: ['https://example.com/scraping'],
373425
program_data: {
374426
status: 'active',
375-
partner_active_date: partnerActiveDate
427+
partner_active_date: partnerActiveDate,
428+
cve_website_update_date: cveWebsiteUpdateDate
376429
}
377430
})
378431
.then((res, err) => {
@@ -382,6 +435,7 @@ describe('Testing /registryOrg endpoints', () => {
382435
expect(res.body.updated.program_data.status).to.equal('active')
383436
expect(res.body.updated.program_data).to.haveOwnProperty('partner_active_date')
384437
expect(res.body.updated.program_data.partner_active_date).to.equal(partnerActiveDate)
438+
expect(res.body.updated.program_data.cve_website_update_date).to.equal('2024-04-10')
385439
expect(res.body.updated.program_data).to.not.haveOwnProperty('advisory_location_require_credentials')
386440
expect(res.body.updated.program_data).to.not.haveOwnProperty('vulnerability_advisory_location_for_web_scraping')
387441
expect(res.body.updated.advisory_location_require_credentials).to.be.true

0 commit comments

Comments
 (0)