From b74986c40c9374db3fcaacd90837857c5c3e81bb Mon Sep 17 00:00:00 2001 From: chirschenberger Date: Thu, 15 May 2025 16:02:35 +0200 Subject: [PATCH 1/4] feat: add optionalTest 6.2.33 --- csaf_2_1/optionalTests.js | 0 .../recommendedTests/optionalTest_6_2_33.js | 82 +++++++++++++++++++ tests/csaf_2_1/oasis.js | 1 - tests/csaf_2_1/optionalTest_6_2_33.js | 11 +++ 4 files changed, 93 insertions(+), 1 deletion(-) create mode 100644 csaf_2_1/optionalTests.js create mode 100644 csaf_2_1/recommendedTests/optionalTest_6_2_33.js create mode 100644 tests/csaf_2_1/optionalTest_6_2_33.js diff --git a/csaf_2_1/optionalTests.js b/csaf_2_1/optionalTests.js new file mode 100644 index 00000000..e69de29b diff --git a/csaf_2_1/recommendedTests/optionalTest_6_2_33.js b/csaf_2_1/recommendedTests/optionalTest_6_2_33.js new file mode 100644 index 00000000..730801b4 --- /dev/null +++ b/csaf_2_1/recommendedTests/optionalTest_6_2_33.js @@ -0,0 +1,82 @@ +import Ajv from 'ajv/dist/jtd.js' +import { ZonedDateTime, ZoneId } from '@js-joda/core' +import { compareZonedDateTimes } from '../../lib/shared/dateHelper.js' + +const ajv = new Ajv() + +const inputSchema = /** @type {const} */ ({ + additionalProperties: true, + properties: { + document: { + additionalProperties: true, + properties: { + tracking: { + additionalProperties: true, + properties: { + revision_history: { + elements: { + additionalProperties: true, + properties: { + date: { type: 'string' }, + }, + }, + }, + }, + }, + }, + }, + vulnerabilities: { + elements: { + additionalProperties: true, + properties: { + disclosure_date: { type: 'string' }, + }, + }, + }, + }, +}) + +const validate = ajv.compile(inputSchema) + +/** + * This implements the optional test 6.2.33 of the CSAF 2.1 standard. + * + * @param {any} doc + */ +export function optionalTest_6_2_33(doc) { + /** @type {Array<{ message: string; instancePath: string }>} */ + const warnings = [] + const context = { warnings } + + if (!validate(doc)) { + return context + } + const currentTimestampUtc = ZonedDateTime.now(ZoneId.UTC) + for (let i = 0; i < doc.vulnerabilities.length; ++i) { + const disclosureDate = doc.vulnerabilities[i].disclosure_date + // check if the disclosure date is in the past + if (compareZonedDateTimes(disclosureDate, currentTimestampUtc) < 1) { + const revisionHistory = doc.document.tracking.revision_history + // sort the revision history (ascending) so we don't need to loop through it + // to find the date of its newest item + revisionHistory.sort( + (a, b) => new Date(a.date).valueOf() - new Date(b.date).valueOf() + ) + // compare the disclosure date with the date of the newest item in the revision history + if ( + compareZonedDateTimes( + disclosureDate, + revisionHistory[revisionHistory.length - 1].date + ) > 0 + ) { + context.warnings.push({ + message: + 'The disclosure_date is in the past but newer than the date of the newest item in the revision_history.', + instancePath: `/vulnerabilities/${i}/disclosure_date`, + }) + } + } + } + + return context +} diff --git a/tests/csaf_2_1/oasis.js b/tests/csaf_2_1/oasis.js index 6caa005c..b303bf66 100644 --- a/tests/csaf_2_1/oasis.js +++ b/tests/csaf_2_1/oasis.js @@ -36,7 +36,6 @@ const excluded = [ '6.2.26', '6.2.31', '6.2.32', - '6.2.33', '6.2.34', '6.2.35', '6.2.36', diff --git a/tests/csaf_2_1/optionalTest_6_2_33.js b/tests/csaf_2_1/optionalTest_6_2_33.js new file mode 100644 index 00000000..0c75d387 --- /dev/null +++ b/tests/csaf_2_1/optionalTest_6_2_33.js @@ -0,0 +1,11 @@ +import assert from 'node:assert' +import { optionalTest_6_2_33 } from '../../csaf_2_1/optionalTests.js' + +describe('optionalTest_6_2_33', function () { + it('only runs on relevant documents', function () { + assert.equal( + optionalTest_6_2_33({ vulnerabilities: 'mydoc' }).warnings.length, + 0 + ) + }) +}) From 63ed62add05bf7289f58a791532fa4b1490d258e Mon Sep 17 00:00:00 2001 From: chirschenberger Date: Fri, 16 May 2025 12:47:58 +0200 Subject: [PATCH 2/4] fix: use built-in js instead of js-joda to compare dates --- .../recommendedTests/optionalTest_6_2_33.js | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/csaf_2_1/recommendedTests/optionalTest_6_2_33.js b/csaf_2_1/recommendedTests/optionalTest_6_2_33.js index 730801b4..f24e614c 100644 --- a/csaf_2_1/recommendedTests/optionalTest_6_2_33.js +++ b/csaf_2_1/recommendedTests/optionalTest_6_2_33.js @@ -1,6 +1,4 @@ import Ajv from 'ajv/dist/jtd.js' -import { ZonedDateTime, ZoneId } from '@js-joda/core' -import { compareZonedDateTimes } from '../../lib/shared/dateHelper.js' const ajv = new Ajv() @@ -51,23 +49,25 @@ export function optionalTest_6_2_33(doc) { if (!validate(doc)) { return context } - const currentTimestampUtc = ZonedDateTime.now(ZoneId.UTC) + + // current date in UTC + const currentTimestampUtc = new Date() + for (let i = 0; i < doc.vulnerabilities.length; ++i) { - const disclosureDate = doc.vulnerabilities[i].disclosure_date + const disclosureDate = new Date(doc.vulnerabilities[i].disclosure_date) // check if the disclosure date is in the past - if (compareZonedDateTimes(disclosureDate, currentTimestampUtc) < 1) { + if (currentTimestampUtc.getTime() - disclosureDate.getTime() > 0) { const revisionHistory = doc.document.tracking.revision_history // sort the revision history (ascending) so we don't need to loop through it // to find the date of its newest item revisionHistory.sort( - (a, b) => new Date(a.date).valueOf() - new Date(b.date).valueOf() + (a, b) => new Date(a.date).getTime() - new Date(b.date).getTime() ) // compare the disclosure date with the date of the newest item in the revision history if ( - compareZonedDateTimes( - disclosureDate, - revisionHistory[revisionHistory.length - 1].date - ) > 0 + disclosureDate.getTime() - + new Date(revisionHistory[revisionHistory.length - 1].date).getTime() > + 0 ) { context.warnings.push({ message: From c2ffe0ac66f6579648cbfcf0529f7e328d8e3afd Mon Sep 17 00:00:00 2001 From: chirschenberger Date: Thu, 7 Aug 2025 17:34:58 +0200 Subject: [PATCH 3/4] feat: adapt recommended test 6.2.33 --- README.md | 2 +- csaf_2_1/optionalTests.js | 0 csaf_2_1/recommendedTests.js | 1 + ...st_6_2_33.js => recommendedTest_6_2_33.js} | 54 ++++++++++++------- tests/csaf_2_1/optionalTest_6_2_33.js | 11 ---- tests/csaf_2_1/recommendedTest_6_2_33.js | 36 +++++++++++++ 6 files changed, 73 insertions(+), 31 deletions(-) delete mode 100644 csaf_2_1/optionalTests.js rename csaf_2_1/recommendedTests/{optionalTest_6_2_33.js => recommendedTest_6_2_33.js} (51%) delete mode 100644 tests/csaf_2_1/optionalTest_6_2_33.js create mode 100644 tests/csaf_2_1/recommendedTest_6_2_33.js diff --git a/README.md b/README.md index 5753a4a1..1554ce78 100644 --- a/README.md +++ b/README.md @@ -332,7 +332,6 @@ The following tests are not yet implemented and therefore missing: - Recommended Test 6.2.26 - Recommended Test 6.2.31 - Recommended Test 6.2.32 -- Recommended Test 6.2.33 - Recommended Test 6.2.34 - Recommended Test 6.2.35 - Recommended Test 6.2.36 @@ -460,6 +459,7 @@ export const recommendedTest_6_2_27: DocumentTest export const recommendedTest_6_2_28: DocumentTest export const recommendedTest_6_2_29: DocumentTest export const recommendedTest_6_2_30: DocumentTest +export const recommendedTest_6_2_33: DocumentTest export const recommendedTest_6_2_43: DocumentTest ``` diff --git a/csaf_2_1/optionalTests.js b/csaf_2_1/optionalTests.js deleted file mode 100644 index e69de29b..00000000 diff --git a/csaf_2_1/recommendedTests.js b/csaf_2_1/recommendedTests.js index d90a5174..8f5791e2 100644 --- a/csaf_2_1/recommendedTests.js +++ b/csaf_2_1/recommendedTests.js @@ -33,5 +33,6 @@ export { recommendedTest_6_2_27 } from './recommendedTests/recommendedTest_6_2_2 export { recommendedTest_6_2_28 } from './recommendedTests/recommendedTest_6_2_28.js' export { recommendedTest_6_2_29 } from './recommendedTests/recommendedTest_6_2_29.js' export { recommendedTest_6_2_30 } from './recommendedTests/recommendedTest_6_2_30.js' +export { recommendedTest_6_2_33 } from './recommendedTests/recommendedTest_6_2_33.js' export { recommendedTest_6_2_38 } from './recommendedTests/recommendedTest_6_2_38.js' export { recommendedTest_6_2_43 } from './recommendedTests/recommendedTest_6_2_43.js' diff --git a/csaf_2_1/recommendedTests/optionalTest_6_2_33.js b/csaf_2_1/recommendedTests/recommendedTest_6_2_33.js similarity index 51% rename from csaf_2_1/recommendedTests/optionalTest_6_2_33.js rename to csaf_2_1/recommendedTests/recommendedTest_6_2_33.js index f24e614c..8bc033d9 100644 --- a/csaf_2_1/recommendedTests/optionalTest_6_2_33.js +++ b/csaf_2_1/recommendedTests/recommendedTest_6_2_33.js @@ -1,4 +1,6 @@ import Ajv from 'ajv/dist/jtd.js' +import { ZonedDateTime, ZoneId } from '@js-joda/core' +import { compareZonedDateTimes } from '../../lib/shared/dateHelper.js' const ajv = new Ajv() @@ -14,7 +16,7 @@ const inputSchema = /** @type {const} */ ({ revision_history: { elements: { additionalProperties: true, - properties: { + optionalProperties: { date: { type: 'string' }, }, }, @@ -26,7 +28,7 @@ const inputSchema = /** @type {const} */ ({ vulnerabilities: { elements: { additionalProperties: true, - properties: { + optionalProperties: { disclosure_date: { type: 'string' }, }, }, @@ -37,11 +39,11 @@ const inputSchema = /** @type {const} */ ({ const validate = ajv.compile(inputSchema) /** - * This implements the optional test 6.2.33 of the CSAF 2.1 standard. + * This implements the recommended test 6.2.33 of the CSAF 2.1 standard. * * @param {any} doc */ -export function optionalTest_6_2_33(doc) { +export function recommendedTest_6_2_33(doc) { /** @type {Array<{ message: string; instancePath: string }>} */ const warnings = [] const context = { warnings } @@ -49,29 +51,43 @@ export function optionalTest_6_2_33(doc) { if (!validate(doc)) { return context } - - // current date in UTC - const currentTimestampUtc = new Date() - + const currentTimestampUtc = ZonedDateTime.now(ZoneId.UTC) for (let i = 0; i < doc.vulnerabilities.length; ++i) { - const disclosureDate = new Date(doc.vulnerabilities[i].disclosure_date) + const disclosureDate = doc.vulnerabilities[i].disclosure_date // check if the disclosure date is in the past - if (currentTimestampUtc.getTime() - disclosureDate.getTime() > 0) { + if ( + disclosureDate && + compareZonedDateTimes(disclosureDate, currentTimestampUtc) < 1 + ) { const revisionHistory = doc.document.tracking.revision_history - // sort the revision history (ascending) so we don't need to loop through it + // sort the revision history (ascending) so one don't need to loop through it // to find the date of its newest item - revisionHistory.sort( - (a, b) => new Date(a.date).getTime() - new Date(b.date).getTime() - ) - // compare the disclosure date with the date of the newest item in the revision history + revisionHistory.sort((a, b) => { + // if both dates are undefined, consider them equal + if (!a.date && !b.date) { + return 0 + } + // move undefined items to the beginning of the array + if (!a.date) { + return -1 + } + if (!b.date) { + return 1 + } + + // if both dates are not undefined, compare them + return compareZonedDateTimes(a.date, b.date) + }) + // compare the disclosure date with the date of the newest item of the revision history + const newestItemInRevisionHistory = + revisionHistory[revisionHistory.length - 1].date if ( - disclosureDate.getTime() - - new Date(revisionHistory[revisionHistory.length - 1].date).getTime() > - 0 + newestItemInRevisionHistory && + compareZonedDateTimes(disclosureDate, newestItemInRevisionHistory) > 0 ) { context.warnings.push({ message: - 'The disclosure_date is in the past but newer than the date of the newest item in the revision_history.', + 'The disclosure_date is in the past but newer than the date of the newest item of the revision_history.', instancePath: `/vulnerabilities/${i}/disclosure_date`, }) } diff --git a/tests/csaf_2_1/optionalTest_6_2_33.js b/tests/csaf_2_1/optionalTest_6_2_33.js deleted file mode 100644 index 0c75d387..00000000 --- a/tests/csaf_2_1/optionalTest_6_2_33.js +++ /dev/null @@ -1,11 +0,0 @@ -import assert from 'node:assert' -import { optionalTest_6_2_33 } from '../../csaf_2_1/optionalTests.js' - -describe('optionalTest_6_2_33', function () { - it('only runs on relevant documents', function () { - assert.equal( - optionalTest_6_2_33({ vulnerabilities: 'mydoc' }).warnings.length, - 0 - ) - }) -}) diff --git a/tests/csaf_2_1/recommendedTest_6_2_33.js b/tests/csaf_2_1/recommendedTest_6_2_33.js new file mode 100644 index 00000000..c93f9184 --- /dev/null +++ b/tests/csaf_2_1/recommendedTest_6_2_33.js @@ -0,0 +1,36 @@ +import assert from 'node:assert' +import { recommendedTest_6_2_33 } from '../../csaf_2_1/recommendedTests.js' + +describe('recommendedTest_6_2_33', function () { + it('only runs on relevant documents', function () { + assert.equal( + recommendedTest_6_2_33({ vulnerabilities: 'mydoc' }).warnings.length, + 0 + ) + }) + it('skips empty objects', function () { + assert.equal( + recommendedTest_6_2_33({ + document: { + tracking: { + revision_history: [ + { + date: '2024-01-22T10:00:00.000Z', + number: '1', + summary: 'Initial version.', + }, + {}, // should be ignored + ], + }, + }, + vulnerabilities: [ + { + disclosure_date: '2024-02-24T10:00:00.000Z', + }, + {}, // should be ignored + ], + }).warnings.length, + 1 + ) + }) +}) From 82e9bc42fc674497bcaab9fd419a3c4f3c4b6fe3 Mon Sep 17 00:00:00 2001 From: chirschenberger Date: Mon, 24 Nov 2025 17:24:26 +0100 Subject: [PATCH 4/4] feat: use temporal to compare dates --- csaf_2_1/recommendedTests/recommendedTest_6_2_33.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/csaf_2_1/recommendedTests/recommendedTest_6_2_33.js b/csaf_2_1/recommendedTests/recommendedTest_6_2_33.js index 8bc033d9..7214540f 100644 --- a/csaf_2_1/recommendedTests/recommendedTest_6_2_33.js +++ b/csaf_2_1/recommendedTests/recommendedTest_6_2_33.js @@ -1,6 +1,6 @@ import Ajv from 'ajv/dist/jtd.js' -import { ZonedDateTime, ZoneId } from '@js-joda/core' -import { compareZonedDateTimes } from '../../lib/shared/dateHelper.js' +import { compareZonedDateTimes } from '../dateHelper.js' +import { Temporal } from 'temporal-polyfill' const ajv = new Ajv() @@ -51,7 +51,7 @@ export function recommendedTest_6_2_33(doc) { if (!validate(doc)) { return context } - const currentTimestampUtc = ZonedDateTime.now(ZoneId.UTC) + const currentTimestampUtc = Temporal.Now.zonedDateTimeISO('UTC').toString() for (let i = 0; i < doc.vulnerabilities.length; ++i) { const disclosureDate = doc.vulnerabilities[i].disclosure_date // check if the disclosure date is in the past