Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion schemas/registry-org/BaseOrg.json
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@
"properties": {
"cve_website_update_date": {
"type": "string",
"format": "date-time"
"format": "date"
},
"cve_website_update_needed": {
"type": "boolean"
Expand Down
2 changes: 1 addition & 1 deletion schemas/registry-org/create-registry-org-request.json
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@
"properties": {
"cve_website_update_date": {
"type": "string",
"format": "date-time"
"format": "date"
},
"cve_website_update_needed": {
"type": "boolean"
Expand Down
2 changes: 1 addition & 1 deletion schemas/registry-org/get-registry-org-response.json
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@
"properties": {
"cve_website_update_date": {
"type": "string",
"format": "date-time"
"format": "date"
},
"cve_website_update_needed": {
"type": "boolean"
Expand Down
2 changes: 1 addition & 1 deletion schemas/registry-org/list-registry-orgs-response.json
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@
"properties": {
"cve_website_update_date": {
"type": "string",
"format": "date-time"
"format": "date"
},
"cve_website_update_needed": {
"type": "boolean"
Expand Down
2 changes: 1 addition & 1 deletion schemas/registry-org/update-registry-org-request.json
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,7 @@
"properties": {
"cve_website_update_date": {
"type": "string",
"format": "date-time"
"format": "date"
},
"cve_website_update_needed": {
"type": "boolean"
Expand Down
10 changes: 9 additions & 1 deletion src/model/baseorg.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
const mongoose = require('mongoose')
const aggregatePaginate = require('mongoose-aggregate-paginate-v2')
const MongoPaging = require('mongo-cursor-pagination')
const { isValidDateOnlyString, normalizeDateOnlyInput } = require('../utils/dateOnly')

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

Expand Down Expand Up @@ -29,7 +30,14 @@ const schema = {
partner_number: String,
partner_country: String,
program_data: {
cve_website_update_date: Date,
cve_website_update_date: {
type: String,
set: normalizeDateOnlyInput,
validate: {
validator: value => value == null || isValidDateOnlyString(value),
message: 'cve_website_update_date must be a date in YYYY-MM-DD format.'
}
},
cve_website_update_needed: Boolean,
partner_active_date: String,
partner_inactive_date: String,
Expand Down
8 changes: 8 additions & 0 deletions src/repositories/baseOrgRepository.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ const {
createAuditLogEntry,
handleAuthorityModelChange
} = require('./baseOrgRepositoryHelpers')
const { normalizeOrgCveWebsiteUpdateDate } = require('../utils/dateOnly')

/**
* @function setAggregateOrgObj
Expand Down Expand Up @@ -111,6 +112,7 @@ function getOrgProjection (isSecretariat = false) {
function filterOrg (orgObj, isSecretariat = false) {
const CONSTANTS = getConstants()
const _ = require('lodash')
normalizeOrgCveWebsiteUpdateDate(orgObj, { output: true })
let fieldsToOmit = [...CONSTANTS.ORG_EXCLUDED_FIELDS]
if (!isSecretariat) {
fieldsToOmit = [...fieldsToOmit, ...CONSTANTS.ORG_RESTRICTED_FIELDS]
Expand Down Expand Up @@ -399,6 +401,7 @@ class BaseOrgRepository extends BaseRepository {
if (org.reports_to === null) {
delete org.reports_to
}
normalizeOrgCveWebsiteUpdateDate(org, { output: true })
})
}

Expand Down Expand Up @@ -475,6 +478,7 @@ class BaseOrgRepository extends BaseRepository {
}
}

normalizeOrgCveWebsiteUpdateDate(result, { output: true })
return deepRemoveEmpty(result)
}

Expand Down Expand Up @@ -913,6 +917,8 @@ class BaseOrgRepository extends BaseRepository {
const originalPlain = orgObjectOriginal.toObject ? orgObjectOriginal.toObject() : orgObjectOriginal
const originalSerialized = JSON.parse(JSON.stringify(originalPlain))
const updatedSerialized = JSON.parse(JSON.stringify(orgObjectUpdated))
normalizeOrgCveWebsiteUpdateDate(originalSerialized, { output: true })
normalizeOrgCveWebsiteUpdateDate(updatedSerialized)

// Filter the list to find only fields that have changed
const changedFields = _.filter(jointApprovalFields, field => {
Expand Down Expand Up @@ -1034,6 +1040,8 @@ class BaseOrgRepository extends BaseRepository {
* @returns {object} The validation result object.
*/
validateOrg (org) {
normalizeOrgCveWebsiteUpdateDate(org)

if (!org.authority || (Array.isArray(org.authority) && org.authority.length === 0)) {
return { isValid: false, errors: [{ instancePath: '/authority', message: 'authority is required' }] }
}
Expand Down
68 changes: 68 additions & 0 deletions src/utils/dateOnly.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
const ISO_DATE_PREFIX = /^(\d{4})-(\d{2})-(\d{2})(?:$|T)/
const ISO_DATE_ONLY = /^(\d{4})-(\d{2})-(\d{2})$/
const MONGOOSE_DATE_STRING = /^[A-Z][a-z]{2} [A-Z][a-z]{2} \d{2} \d{4} .* GMT[+-]\d{4}/

function isValidDateOnlyParts (year, month, day) {
const yearNumber = Number(year)
const monthNumber = Number(month)
const dayNumber = Number(day)
const date = new Date(Date.UTC(yearNumber, monthNumber - 1, dayNumber))

return (
date.getUTCFullYear() === yearNumber &&
date.getUTCMonth() === monthNumber - 1 &&
date.getUTCDate() === dayNumber
)
}

function isValidDateOnlyString (value) {
if (typeof value !== 'string') return false
const match = value.match(ISO_DATE_ONLY)
if (!match) return false
return isValidDateOnlyParts(match[1], match[2], match[3])
}

function normalizeDateOnlyInput (value) {
if (value instanceof Date) {
if (isNaN(value)) return value
return value.toISOString().split('T')[0]
}

if (typeof value !== 'string') return value

const match = value.match(ISO_DATE_PREFIX)
if (!match) return value
if (!isValidDateOnlyParts(match[1], match[2], match[3])) return value

return `${match[1]}-${match[2]}-${match[3]}`
}

function normalizeDateOnlyOutput (value) {
const normalizedValue = normalizeDateOnlyInput(value)
if (normalizedValue !== value) return normalizedValue

if (typeof value === 'string' && MONGOOSE_DATE_STRING.test(value)) {
const date = new Date(value)
if (!isNaN(date)) return date.toISOString().split('T')[0]
}

return value
}

function normalizeOrgCveWebsiteUpdateDate (org, options = {}) {
if (!org || typeof org !== 'object') return org
if (!org.program_data || typeof org.program_data !== 'object') return org
if (!Object.prototype.hasOwnProperty.call(org.program_data, 'cve_website_update_date')) return org

const normalize = options.output ? normalizeDateOnlyOutput : normalizeDateOnlyInput
org.program_data.cve_website_update_date = normalize(org.program_data.cve_website_update_date)

return org
}

module.exports = {
isValidDateOnlyString,
normalizeDateOnlyInput,
normalizeDateOnlyOutput,
normalizeOrgCveWebsiteUpdateDate
}
56 changes: 55 additions & 1 deletion test/integration-tests/registry-org/registryOrgCRUDTest.js
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,36 @@ describe('Testing /registryOrg endpoints', () => {
expect(res.body.created.vulnerability_advisory_location_for_web_scraping).to.deep.equal(['https://example.com/scraping'])
})
})
it('Creates a new registry org with a date-only CVE website update date', async () => {
const cveWebsiteUpdateDate = '2024-01-15'
const orgWithWebsiteUpdateDate = {
...testRegistryOrg,
short_name: 'registry_org_test_web_date',
program_data: {
status: 'active',
cve_website_update_date: cveWebsiteUpdateDate
}
}

await chai.request(app)
.post('/api/registry/org')
.set(secretariatHeaders)
.send(orgWithWebsiteUpdateDate)
.then((res, err) => {
expect(err).to.be.undefined
expect(res).to.have.status(200)
expect(res.body.created).to.haveOwnProperty('program_data')
expect(res.body.created.program_data.cve_website_update_date).to.equal(cveWebsiteUpdateDate)
})

await chai.request(app)
.get('/api/registry/org/registry_org_test_web_date')
.set(secretariatHeaders)
.then((res) => {
expect(res).to.have.status(200)
expect(res.body.program_data.cve_website_update_date).to.equal(cveWebsiteUpdateDate)
})
})
})
context('Negative Tests', () => {
it('Fails to create a new registry organization with an existing short name', async () => {
Expand Down Expand Up @@ -149,6 +179,27 @@ describe('Testing /registryOrg endpoints', () => {
expect(res.body.message).to.equal('Parameters were invalid')
})
})
it('Fails to create a new registry organization with an ambiguous CVE website update date', async () => {
await chai.request(app)
.post('/api/registry/org')
.set(secretariatHeaders)
.send({
...testRegistryOrg,
short_name: 'test_create_ambiguous_date',
program_data: {
status: 'active',
cve_website_update_date: '01/15/2024'
}
})
.then((res) => {
expect(res).to.have.status(400)
expect(res.body.message).to.equal('Parameters were invalid')

const dateError = res.body.errors.find(error => error.instancePath === '/program_data/cve_website_update_date')
expect(dateError).to.not.be.undefined
expect(dateError.message).to.equal('must match format "date"')
})
})
it('Fails to create a new registry organization with reports_to manually provided', async () => {
await chai.request(app)
.post('/api/registry/org')
Expand Down Expand Up @@ -363,6 +414,7 @@ describe('Testing /registryOrg endpoints', () => {
})
it('Allows Secretariat to update program_data', async () => {
const partnerActiveDate = '2024-01-15'
const cveWebsiteUpdateDate = '2024-04-10T18:30:00.000Z'
await chai.request(app)
.put('/api/registry/org/registry_org_test')
.set(secretariatHeaders)
Expand All @@ -372,7 +424,8 @@ describe('Testing /registryOrg endpoints', () => {
vulnerability_advisory_location_for_web_scraping: ['https://example.com/scraping'],
program_data: {
status: 'active',
partner_active_date: partnerActiveDate
partner_active_date: partnerActiveDate,
cve_website_update_date: cveWebsiteUpdateDate
}
})
.then((res, err) => {
Expand All @@ -382,6 +435,7 @@ describe('Testing /registryOrg endpoints', () => {
expect(res.body.updated.program_data.status).to.equal('active')
expect(res.body.updated.program_data).to.haveOwnProperty('partner_active_date')
expect(res.body.updated.program_data.partner_active_date).to.equal(partnerActiveDate)
expect(res.body.updated.program_data.cve_website_update_date).to.equal('2024-04-10')
expect(res.body.updated.program_data).to.not.haveOwnProperty('advisory_location_require_credentials')
expect(res.body.updated.program_data).to.not.haveOwnProperty('vulnerability_advisory_location_for_web_scraping')
expect(res.body.updated.advisory_location_require_credentials).to.be.true
Expand Down
Loading