From 4b5d58023de724027f9df749adfaeaf3da170bd1 Mon Sep 17 00:00:00 2001 From: chirschenberger Date: Thu, 8 May 2025 15:55:41 +0200 Subject: [PATCH 01/10] feat: add optional test 6.2.24 --- csaf_2_1/optionalTests.js | 23 +++ .../recommendedTests/optionalTest_6_2_24.js | 131 ++++++++++++++++++ tests/csaf_2_1/oasis.js | 1 - 3 files changed, 154 insertions(+), 1 deletion(-) create mode 100644 csaf_2_1/optionalTests.js create mode 100644 csaf_2_1/recommendedTests/optionalTest_6_2_24.js diff --git a/csaf_2_1/optionalTests.js b/csaf_2_1/optionalTests.js new file mode 100644 index 000000000..02dedd2ea --- /dev/null +++ b/csaf_2_1/optionalTests.js @@ -0,0 +1,23 @@ +export { + optionalTest_6_2_1, + optionalTest_6_2_2, + optionalTest_6_2_4, + optionalTest_6_2_5, + optionalTest_6_2_6, + optionalTest_6_2_7, + optionalTest_6_2_8, + optionalTest_6_2_9, + optionalTest_6_2_10, + optionalTest_6_2_11, + optionalTest_6_2_12, + optionalTest_6_2_13, + optionalTest_6_2_14, + optionalTest_6_2_15, + optionalTest_6_2_16, + optionalTest_6_2_17, + optionalTest_6_2_18, + optionalTest_6_2_19, + optionalTest_6_2_20, +} from '../optionalTests.js' +export { optionalTest_6_2_24 } from './optionalTests/optionalTest_6_2_24.js' +export { optionalTest_6_2_3 } from './optionalTests/optionalTest_6_2_3.js' diff --git a/csaf_2_1/recommendedTests/optionalTest_6_2_24.js b/csaf_2_1/recommendedTests/optionalTest_6_2_24.js new file mode 100644 index 000000000..3688576d5 --- /dev/null +++ b/csaf_2_1/recommendedTests/optionalTest_6_2_24.js @@ -0,0 +1,131 @@ +import Ajv from 'ajv/dist/jtd.js' +import { cwecMap } from '../../lib/cwec.js' +import { LocalDate, ZoneId } from '@js-joda/core' +import { compareZonedDateTimes } from '../../lib/shared/dateHelper.js' + +const ajv = new Ajv() + +/* + This is the jtd schema that needs to match the input document so that the + test is activated. If this schema doesn't match it normally means that the input + document does not validate against the csaf json schema or optional fields that + the test checks are not present. + */ +const inputSchema = /** @type {const} */ ({ + additionalProperties: true, + properties: { + document: { + additionalProperties: true, + properties: { + tracking: { + additionalProperties: true, + properties: { + current_release_date: { type: 'timestamp' }, + }, + }, + }, + }, + vulnerabilities: { + elements: { + additionalProperties: true, + properties: { + cwes: { + elements: { + additionalProperties: true, + properties: {}, + }, + }, + }, + }, + }, + }, +}) + +const validateInput = ajv.compile(inputSchema) + +const cweSchema = /** @type {const} */ ({ + additionalProperties: true, + properties: { + id: { type: 'string' }, + version: { type: 'string' }, + name: { type: 'string' }, + }, +}) + +const validateCWE = ajv.compile(cweSchema) + +/** + * This implements the optional test 6.2.24 of the CSAF 2.1 standard. + * + * @param {any} doc + */ +export async function optionalTest_6_2_24(doc) { + /** @type {Array<{ message: string; instancePath: string }>} */ + const warnings = [] + const context = { warnings } + + if (!validateInput(doc)) { + return context + } + + const currentReleaseDate = doc.document.tracking.current_release_date + const cwecArray = [] + for (const key of cwecMap.keys()) { + const cwec = cwecMap.get(key) + if (cwec) { + const date = (await cwec()).default.date + cwecArray.push([key, date]) + } + } + + for (let i = 0; i < doc.vulnerabilities.length; ++i) { + const vulnerability = doc.vulnerabilities[i] + for (let j = 0; j < vulnerability.cwes.length; ++j) { + const cwe = vulnerability.cwes.at(j) + if (validateCWE(cwe)) { + const cwec = cwecMap.get(cwe.version) + if (!cwec) { + context.warnings.push({ + instancePath: `/vulnerabilities/${i}/cwes/${j}/version`, + message: 'no such cwe version is recognized', + }) + continue + } + const date = (await cwec()).default.date + const localDate = LocalDate.parse(date) + const localDateTime = localDate.atStartOfDay() + const zonedDateTime = localDateTime.atZone(ZoneId.of('UTC')) + const entry = (await cwec()).default.weaknesses.find( + (w) => w.id === cwe.id + ) + if (!entry) { + context.warnings.push({ + instancePath: `/vulnerabilities/${i}/cwes/${j}/id`, + message: `no weakness with this id is recognized in CWE ${cwe.version}`, + }) + continue + } + if (compareZonedDateTimes(currentReleaseDate, zonedDateTime) < 0) { + context.warnings.push({ + instancePath: `/vulnerabilities/${i}/cwes/${j}/id`, + message: `at release date ${currentReleaseDate} the used version ${cwe.version} was not published yet`, + }) + } else { + const idx = cwecArray.findIndex((row) => row[0] === cwe.version) + for (let i = idx - 1; i >= 0; i--) { + const localDate = LocalDate.parse(cwecArray[i][1]) + const localDateTime = localDate.atStartOfDay() + const zonedDateTime = localDateTime.atZone(ZoneId.of('UTC')) + if (compareZonedDateTimes(currentReleaseDate, zonedDateTime) > 0) { + context.warnings.push({ + instancePath: `/vulnerabilities/${i}/cwes/${j}/id`, + message: `the used version ${cwe.version} was not the newest one available at release date ${currentReleaseDate}`, + }) + } + } + } + } + } + } + return context +} diff --git a/tests/csaf_2_1/oasis.js b/tests/csaf_2_1/oasis.js index 31ce644c1..0018eb661 100644 --- a/tests/csaf_2_1/oasis.js +++ b/tests/csaf_2_1/oasis.js @@ -45,7 +45,6 @@ const excluded = [ '6.2.20', '6.2.21', '6.2.23', - '6.2.24', '6.2.25', '6.2.26', '6.2.30', From c299e7802b3b5e862d0fad0f35d0d3c1bfd519b4 Mon Sep 17 00:00:00 2001 From: chirschenberger Date: Fri, 9 May 2025 13:42:25 +0200 Subject: [PATCH 02/10] feat: add test for validating inputSchema of optional test 6.2.24 --- tests/csaf_2_1/optionalTest_6_2_24.js | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 tests/csaf_2_1/optionalTest_6_2_24.js diff --git a/tests/csaf_2_1/optionalTest_6_2_24.js b/tests/csaf_2_1/optionalTest_6_2_24.js new file mode 100644 index 000000000..aa32b3dca --- /dev/null +++ b/tests/csaf_2_1/optionalTest_6_2_24.js @@ -0,0 +1,11 @@ +import assert from 'node:assert' +import { optionalTest_6_2_24 } from '../../csaf_2_1/optionalTests.js' + +describe('optionalTest_6_2_24', function () { + it('only runs on relevant documents', async function () { + assert.equal( + (await optionalTest_6_2_24({ vulnerabilities: 'mydoc' })).warnings.length, + 0 + ) + }) +}) From 2607e207995f6d472c479e90611763e9c2b0f9ad Mon Sep 17 00:00:00 2001 From: chirschenberger Date: Mon, 4 Aug 2025 15:22:54 +0200 Subject: [PATCH 03/10] fix: fix wrong typedef in cwe-catalogue --- lib/cwec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/cwec.js b/lib/cwec.js index a8ef801c4..9b75d9d18 100644 --- a/lib/cwec.js +++ b/lib/cwec.js @@ -8,7 +8,7 @@ /** * @typedef {object} CweCatalogue - * @property {string} [date] + * @property {string} date * @property {CweWeakness[]} weaknesses */ From dcb7fe2eda148a8696ea0e397fea935263f5c648 Mon Sep 17 00:00:00 2001 From: chirschenberger Date: Mon, 4 Aug 2025 15:24:35 +0200 Subject: [PATCH 04/10] feat: adapt recommended test 6.2.24 --- README.md | 2 +- csaf_2_1/optionalTests.js | 23 --- csaf_2_1/recommendedTests.js | 1 + .../recommendedTests/optionalTest_6_2_24.js | 131 ------------------ .../recommendedTest_6_2_24.js | 128 +++++++++++++++++ lib/shared/dateHelper.js | 17 ++- tests/csaf_2_1/optionalTest_6_2_24.js | 11 -- tests/csaf_2_1/recommendedTest_6_2_24.js | 12 ++ 8 files changed, 158 insertions(+), 167 deletions(-) delete mode 100644 csaf_2_1/optionalTests.js delete mode 100644 csaf_2_1/recommendedTests/optionalTest_6_2_24.js create mode 100644 csaf_2_1/recommendedTests/recommendedTest_6_2_24.js delete mode 100644 tests/csaf_2_1/optionalTest_6_2_24.js create mode 100644 tests/csaf_2_1/recommendedTest_6_2_24.js diff --git a/README.md b/README.md index ebb45f8df..511ce568f 100644 --- a/README.md +++ b/README.md @@ -343,7 +343,6 @@ The following tests are not yet implemented and therefore missing: - Recommended Test 6.2.19 - Recommended Test 6.2.20 - Recommended Test 6.2.21 -- Recommended Test 6.2.23 - Recommended Test 6.2.24 - Recommended Test 6.2.25 - Recommended Test 6.2.26 @@ -462,6 +461,7 @@ export const recommendedTest_6_2_16: DocumentTest export const recommendedTest_6_2_17: DocumentTest export const recommendedTest_6_2_18: DocumentTest export const recommendedTest_6_2_22: DocumentTest +export const recommendedTest_6_2_23: DocumentTest ``` [(back to top)](#bsi-csaf-validator-lib) diff --git a/csaf_2_1/optionalTests.js b/csaf_2_1/optionalTests.js deleted file mode 100644 index 02dedd2ea..000000000 --- a/csaf_2_1/optionalTests.js +++ /dev/null @@ -1,23 +0,0 @@ -export { - optionalTest_6_2_1, - optionalTest_6_2_2, - optionalTest_6_2_4, - optionalTest_6_2_5, - optionalTest_6_2_6, - optionalTest_6_2_7, - optionalTest_6_2_8, - optionalTest_6_2_9, - optionalTest_6_2_10, - optionalTest_6_2_11, - optionalTest_6_2_12, - optionalTest_6_2_13, - optionalTest_6_2_14, - optionalTest_6_2_15, - optionalTest_6_2_16, - optionalTest_6_2_17, - optionalTest_6_2_18, - optionalTest_6_2_19, - optionalTest_6_2_20, -} from '../optionalTests.js' -export { optionalTest_6_2_24 } from './optionalTests/optionalTest_6_2_24.js' -export { optionalTest_6_2_3 } from './optionalTests/optionalTest_6_2_3.js' diff --git a/csaf_2_1/recommendedTests.js b/csaf_2_1/recommendedTests.js index aeb25c42d..e0acb432f 100644 --- a/csaf_2_1/recommendedTests.js +++ b/csaf_2_1/recommendedTests.js @@ -27,6 +27,7 @@ export { recommendedTest_6_2_18 } from './recommendedTests/recommendedTest_6_2_1 export { recommendedTest_6_2_19 } from './recommendedTests/recommendedTest_6_2_19.js' export { recommendedTest_6_2_20 } from './recommendedTests/recommendedTest_6_2_20.js' export { recommendedTest_6_2_22 } from './recommendedTests/recommendedTest_6_2_22.js' +export { recommendedTest_6_2_24 } from './recommendedTests/recommendedTest_6_2_24.js' export { recommendedTest_6_2_27 } from './recommendedTests/recommendedTest_6_2_27.js' export { recommendedTest_6_2_28 } from './recommendedTests/recommendedTest_6_2_28.js' export { recommendedTest_6_2_29 } from './recommendedTests/recommendedTest_6_2_29.js' diff --git a/csaf_2_1/recommendedTests/optionalTest_6_2_24.js b/csaf_2_1/recommendedTests/optionalTest_6_2_24.js deleted file mode 100644 index 3688576d5..000000000 --- a/csaf_2_1/recommendedTests/optionalTest_6_2_24.js +++ /dev/null @@ -1,131 +0,0 @@ -import Ajv from 'ajv/dist/jtd.js' -import { cwecMap } from '../../lib/cwec.js' -import { LocalDate, ZoneId } from '@js-joda/core' -import { compareZonedDateTimes } from '../../lib/shared/dateHelper.js' - -const ajv = new Ajv() - -/* - This is the jtd schema that needs to match the input document so that the - test is activated. If this schema doesn't match it normally means that the input - document does not validate against the csaf json schema or optional fields that - the test checks are not present. - */ -const inputSchema = /** @type {const} */ ({ - additionalProperties: true, - properties: { - document: { - additionalProperties: true, - properties: { - tracking: { - additionalProperties: true, - properties: { - current_release_date: { type: 'timestamp' }, - }, - }, - }, - }, - vulnerabilities: { - elements: { - additionalProperties: true, - properties: { - cwes: { - elements: { - additionalProperties: true, - properties: {}, - }, - }, - }, - }, - }, - }, -}) - -const validateInput = ajv.compile(inputSchema) - -const cweSchema = /** @type {const} */ ({ - additionalProperties: true, - properties: { - id: { type: 'string' }, - version: { type: 'string' }, - name: { type: 'string' }, - }, -}) - -const validateCWE = ajv.compile(cweSchema) - -/** - * This implements the optional test 6.2.24 of the CSAF 2.1 standard. - * - * @param {any} doc - */ -export async function optionalTest_6_2_24(doc) { - /** @type {Array<{ message: string; instancePath: string }>} */ - const warnings = [] - const context = { warnings } - - if (!validateInput(doc)) { - return context - } - - const currentReleaseDate = doc.document.tracking.current_release_date - const cwecArray = [] - for (const key of cwecMap.keys()) { - const cwec = cwecMap.get(key) - if (cwec) { - const date = (await cwec()).default.date - cwecArray.push([key, date]) - } - } - - for (let i = 0; i < doc.vulnerabilities.length; ++i) { - const vulnerability = doc.vulnerabilities[i] - for (let j = 0; j < vulnerability.cwes.length; ++j) { - const cwe = vulnerability.cwes.at(j) - if (validateCWE(cwe)) { - const cwec = cwecMap.get(cwe.version) - if (!cwec) { - context.warnings.push({ - instancePath: `/vulnerabilities/${i}/cwes/${j}/version`, - message: 'no such cwe version is recognized', - }) - continue - } - const date = (await cwec()).default.date - const localDate = LocalDate.parse(date) - const localDateTime = localDate.atStartOfDay() - const zonedDateTime = localDateTime.atZone(ZoneId.of('UTC')) - const entry = (await cwec()).default.weaknesses.find( - (w) => w.id === cwe.id - ) - if (!entry) { - context.warnings.push({ - instancePath: `/vulnerabilities/${i}/cwes/${j}/id`, - message: `no weakness with this id is recognized in CWE ${cwe.version}`, - }) - continue - } - if (compareZonedDateTimes(currentReleaseDate, zonedDateTime) < 0) { - context.warnings.push({ - instancePath: `/vulnerabilities/${i}/cwes/${j}/id`, - message: `at release date ${currentReleaseDate} the used version ${cwe.version} was not published yet`, - }) - } else { - const idx = cwecArray.findIndex((row) => row[0] === cwe.version) - for (let i = idx - 1; i >= 0; i--) { - const localDate = LocalDate.parse(cwecArray[i][1]) - const localDateTime = localDate.atStartOfDay() - const zonedDateTime = localDateTime.atZone(ZoneId.of('UTC')) - if (compareZonedDateTimes(currentReleaseDate, zonedDateTime) > 0) { - context.warnings.push({ - instancePath: `/vulnerabilities/${i}/cwes/${j}/id`, - message: `the used version ${cwe.version} was not the newest one available at release date ${currentReleaseDate}`, - }) - } - } - } - } - } - } - return context -} diff --git a/csaf_2_1/recommendedTests/recommendedTest_6_2_24.js b/csaf_2_1/recommendedTests/recommendedTest_6_2_24.js new file mode 100644 index 000000000..9216fcac6 --- /dev/null +++ b/csaf_2_1/recommendedTests/recommendedTest_6_2_24.js @@ -0,0 +1,128 @@ +import Ajv from 'ajv/dist/jtd.js' +import { cwecMap } from '../../lib/cwec.js' +import { LocalDate, ZoneId } from '@js-joda/core' +import { + compareZonedDateTimes, + localDateToTimeZonedDate, +} from '../../lib/shared/dateHelper.js' + +const ajv = new Ajv() + +/* + This is the jtd schema that needs to match the input document so that the + test is activated. If this schema doesn't match it normally means that the input + document does not validate against the csaf json schema or optional fields that + the test checks are not present. + */ +const inputSchema = /** @type {const} */ ({ + additionalProperties: true, + properties: { + document: { + additionalProperties: true, + properties: { + tracking: { + additionalProperties: true, + properties: { + current_release_date: { type: 'string' }, + }, + }, + }, + }, + vulnerabilities: { + elements: { + additionalProperties: true, + optionalProperties: { + cwes: { + elements: { + additionalProperties: true, + properties: {}, + }, + }, + }, + }, + }, + }, +}) + +const validateInput = ajv.compile(inputSchema) + +const cweSchema = /** @type {const} */ ({ + additionalProperties: true, + properties: { + id: { type: 'string' }, + version: { type: 'string' }, + name: { type: 'string' }, + }, +}) + +const validateCWE = ajv.compile(cweSchema) + +/** + * This implements the recommended test 6.2.24 of the CSAF 2.1 standard. + * + * @param {any} doc + */ +export async function recommendedTest_6_2_24(doc) { + /** @type {Array<{ message: string; instancePath: string }>} */ + const warnings = [] + const context = { warnings } + + if (!validateInput(doc)) { + return context + } + + const currentReleaseDate = doc.document.tracking.current_release_date + const cwecArray = [] + for (const key of cwecMap.keys()) { + const cwec = cwecMap.get(key) + if (cwec) { + const date = (await cwec()).default.date + cwecArray.push([key, localDateToTimeZonedDate(date)]) + } + } + /** Make sure the array is sorted ascending by date hence this is crucial later */ + cwecArray.sort((a, b) => compareZonedDateTimes(b[1], a[1])) + + for (let i = 0; i < doc.vulnerabilities.length; ++i) { + const vulnerability = doc.vulnerabilities[i] + if (vulnerability.cwes) { + for (let j = 0; j < vulnerability.cwes.length; ++j) { + const cwe = vulnerability.cwes.at(j) + if (validateCWE(cwe)) { + const idx = cwecArray.findIndex((row) => row[0] === cwe.version) + const zonedCweReleaseDate = cwecArray[idx][1] + // Case 1: Check if the used cwe version was already published + // at the current release date of the CSAF document + if ( + compareZonedDateTimes(currentReleaseDate, zonedCweReleaseDate) < 0 + ) { + context.warnings.push({ + instancePath: `/vulnerabilities/${i}/cwes/${j}/id`, + message: `at release date ${currentReleaseDate} the used version ${cwe.version} was not published yet`, + }) + // Case 2: Check if a newer cwe version than the used one had already been published + // at the current release date of the CSAF document + } else { + const idx = cwecArray.findIndex((row) => row[0] === cwe.version) + for (let i = idx - 1; i >= 0; i--) { + const zonedCweReleaseDate = cwecArray[i][1] + + if ( + compareZonedDateTimes(currentReleaseDate, zonedCweReleaseDate) > + 0 + ) { + context.warnings.push({ + instancePath: `/vulnerabilities/${i}/cwes/${j}/id`, + message: + `the used cwe version ${cwe.version} ` + + `was not the newest one available at release date ${currentReleaseDate}`, + }) + } + } + } + } + } + } + } + return context +} diff --git a/lib/shared/dateHelper.js b/lib/shared/dateHelper.js index bde935122..1ce8d8ead 100644 --- a/lib/shared/dateHelper.js +++ b/lib/shared/dateHelper.js @@ -1,4 +1,4 @@ -import { Duration, ZonedDateTime } from '@js-joda/core' +import { Duration, LocalDate, ZonedDateTime, ZoneId } from '@js-joda/core' /** * compare ZonedDateTimes from js-joda @@ -31,3 +31,18 @@ export const compareZonedDateTimes = (a, b) => { return 0 } } + +/** + * Convert a local date in string representation, i.e a date accurate to the day like 'yyyy-mm-yy' to a time zoned day + * returns a negative number if a is less than b, positive if a is greater than b, and zero if they are equal. + * This function also returns 0 if one of the given values could not be parsed. + * + * @param {string} localDateString + * @returns the time zoned representation of the given local date + * + */ +export const localDateToTimeZonedDate = (localDateString) => { + const localDate = LocalDate.parse(localDateString) + const localDateTime = localDate.atStartOfDay() + return localDateTime.atZone(ZoneId.of('UTC')) +} diff --git a/tests/csaf_2_1/optionalTest_6_2_24.js b/tests/csaf_2_1/optionalTest_6_2_24.js deleted file mode 100644 index aa32b3dca..000000000 --- a/tests/csaf_2_1/optionalTest_6_2_24.js +++ /dev/null @@ -1,11 +0,0 @@ -import assert from 'node:assert' -import { optionalTest_6_2_24 } from '../../csaf_2_1/optionalTests.js' - -describe('optionalTest_6_2_24', function () { - it('only runs on relevant documents', async function () { - assert.equal( - (await optionalTest_6_2_24({ vulnerabilities: 'mydoc' })).warnings.length, - 0 - ) - }) -}) diff --git a/tests/csaf_2_1/recommendedTest_6_2_24.js b/tests/csaf_2_1/recommendedTest_6_2_24.js new file mode 100644 index 000000000..3d2b6d0ef --- /dev/null +++ b/tests/csaf_2_1/recommendedTest_6_2_24.js @@ -0,0 +1,12 @@ +import assert from 'node:assert' +import { recommendedTest_6_2_24 } from '../../csaf_2_1/recommendedTests.js' + +describe('recommendedTest_6_2_24', function () { + it('only runs on relevant documents', async function () { + assert.equal( + (await recommendedTest_6_2_24({ vulnerabilities: 'mydoc' })).warnings + .length, + 0 + ) + }) +}) From 6fc05bb415e69370483ff2fb54c75dde0b92ef98 Mon Sep 17 00:00:00 2001 From: chirschenberger Date: Mon, 4 Aug 2025 15:30:23 +0200 Subject: [PATCH 05/10] feat: add test on empty objects --- tests/csaf_2_1/recommendedTest_6_2_24.js | 26 ++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/tests/csaf_2_1/recommendedTest_6_2_24.js b/tests/csaf_2_1/recommendedTest_6_2_24.js index 3d2b6d0ef..37b9eb559 100644 --- a/tests/csaf_2_1/recommendedTest_6_2_24.js +++ b/tests/csaf_2_1/recommendedTest_6_2_24.js @@ -9,4 +9,30 @@ describe('recommendedTest_6_2_24', function () { 0 ) }) + it('skips empty objects', async function () { + assert.equal( + ( + await recommendedTest_6_2_24({ + document: { + tracking: { + current_release_date: '2024-01-21T10:00:00.000Z', + }, + }, + vulnerabilities: [ + { + cwes: [ + { + id: 'CWE-256', + name: 'Plaintext Storage of a Password', + version: '4.12', + }, + ], + }, + {}, // should be ignored + ], + }) + ).warnings.length, + 1 + ) + }) }) From cb486ce0c5689261f240533cc15c1aef5fe1c372 Mon Sep 17 00:00:00 2001 From: bendo-eXX Date: Fri, 29 May 2026 16:06:05 +0200 Subject: [PATCH 06/10] feat(CSAF2.1): fix import --- csaf_2_1/recommendedTests/recommendedTest_6_2_24.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/csaf_2_1/recommendedTests/recommendedTest_6_2_24.js b/csaf_2_1/recommendedTests/recommendedTest_6_2_24.js index 9216fcac6..cee495949 100644 --- a/csaf_2_1/recommendedTests/recommendedTest_6_2_24.js +++ b/csaf_2_1/recommendedTests/recommendedTest_6_2_24.js @@ -1,6 +1,5 @@ -import Ajv from 'ajv/dist/jtd.js' +import { Ajv } from 'ajv' import { cwecMap } from '../../lib/cwec.js' -import { LocalDate, ZoneId } from '@js-joda/core' import { compareZonedDateTimes, localDateToTimeZonedDate, From 9580d5bb9592654e034740cd0e01ef0c6705b53a Mon Sep 17 00:00:00 2001 From: bendo-eXX Date: Fri, 29 May 2026 16:08:24 +0200 Subject: [PATCH 07/10] feat(CSAF2.1): fix import --- csaf_2_1/recommendedTests/recommendedTest_6_2_24.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/csaf_2_1/recommendedTests/recommendedTest_6_2_24.js b/csaf_2_1/recommendedTests/recommendedTest_6_2_24.js index cee495949..acde6796c 100644 --- a/csaf_2_1/recommendedTests/recommendedTest_6_2_24.js +++ b/csaf_2_1/recommendedTests/recommendedTest_6_2_24.js @@ -1,4 +1,4 @@ -import { Ajv } from 'ajv' +import { Ajv } from 'ajv/dist/jtd.js' import { cwecMap } from '../../lib/cwec.js' import { compareZonedDateTimes, From 0a349d3f52b169f6950210b1a90b238f4e5c4b07 Mon Sep 17 00:00:00 2001 From: bendo-eXX Date: Fri, 29 May 2026 16:38:48 +0200 Subject: [PATCH 08/10] feat(CSAF2.1): add test for unknown version --- .../recommendedTest_6_2_24.js | 15 ++++++----- lib/shared/dateHelper.js | 8 +++--- tests/csaf_2_1/recommendedTest_6_2_24.js | 27 +++++++++++++++++++ 3 files changed, 39 insertions(+), 11 deletions(-) diff --git a/csaf_2_1/recommendedTests/recommendedTest_6_2_24.js b/csaf_2_1/recommendedTests/recommendedTest_6_2_24.js index acde6796c..a34d23aa5 100644 --- a/csaf_2_1/recommendedTests/recommendedTest_6_2_24.js +++ b/csaf_2_1/recommendedTests/recommendedTest_6_2_24.js @@ -79,7 +79,7 @@ export async function recommendedTest_6_2_24(doc) { cwecArray.push([key, localDateToTimeZonedDate(date)]) } } - /** Make sure the array is sorted ascending by date hence this is crucial later */ + /** Make sure the array is sorted descending by date (newest first) hence this is crucial later */ cwecArray.sort((a, b) => compareZonedDateTimes(b[1], a[1])) for (let i = 0; i < doc.vulnerabilities.length; ++i) { @@ -89,6 +89,7 @@ export async function recommendedTest_6_2_24(doc) { const cwe = vulnerability.cwes.at(j) if (validateCWE(cwe)) { const idx = cwecArray.findIndex((row) => row[0] === cwe.version) + if (idx === -1) continue const zonedCweReleaseDate = cwecArray[idx][1] // Case 1: Check if the used cwe version was already published // at the current release date of the CSAF document @@ -102,13 +103,14 @@ export async function recommendedTest_6_2_24(doc) { // Case 2: Check if a newer cwe version than the used one had already been published // at the current release date of the CSAF document } else { - const idx = cwecArray.findIndex((row) => row[0] === cwe.version) - for (let i = idx - 1; i >= 0; i--) { - const zonedCweReleaseDate = cwecArray[i][1] + for (let k = idx - 1; k >= 0; k--) { + const zonedCweReleaseDateNewer = cwecArray[k][1] if ( - compareZonedDateTimes(currentReleaseDate, zonedCweReleaseDate) > - 0 + compareZonedDateTimes( + currentReleaseDate, + zonedCweReleaseDateNewer + ) > 0 ) { context.warnings.push({ instancePath: `/vulnerabilities/${i}/cwes/${j}/id`, @@ -116,6 +118,7 @@ export async function recommendedTest_6_2_24(doc) { `the used cwe version ${cwe.version} ` + `was not the newest one available at release date ${currentReleaseDate}`, }) + break } } } diff --git a/lib/shared/dateHelper.js b/lib/shared/dateHelper.js index 1ce8d8ead..6f6ec037b 100644 --- a/lib/shared/dateHelper.js +++ b/lib/shared/dateHelper.js @@ -27,18 +27,16 @@ export const compareZonedDateTimes = (a, b) => { } else { return -1 } - } catch (e) { + } catch { return 0 } } /** - * Convert a local date in string representation, i.e a date accurate to the day like 'yyyy-mm-yy' to a time zoned day - * returns a negative number if a is less than b, positive if a is greater than b, and zero if they are equal. - * This function also returns 0 if one of the given values could not be parsed. + * Convert a local date in string representation, i.e a date accurate to the day like 'yyyy-mm-dd' to a time zoned day. * * @param {string} localDateString - * @returns the time zoned representation of the given local date + * @returns the UTC-zoned representation of the given local date * */ export const localDateToTimeZonedDate = (localDateString) => { diff --git a/tests/csaf_2_1/recommendedTest_6_2_24.js b/tests/csaf_2_1/recommendedTest_6_2_24.js index 37b9eb559..738110b2b 100644 --- a/tests/csaf_2_1/recommendedTest_6_2_24.js +++ b/tests/csaf_2_1/recommendedTest_6_2_24.js @@ -9,6 +9,33 @@ describe('recommendedTest_6_2_24', function () { 0 ) }) + + it('skips cwe entries with an unknown version (not in cwecMap)', async function () { + assert.equal( + ( + await recommendedTest_6_2_24({ + document: { + tracking: { + current_release_date: '2024-01-21T10:00:00.000Z', + }, + }, + vulnerabilities: [ + { + cwes: [ + { + id: 'CWE-256', + name: 'Plaintext Storage of a Password', + version: '99.99', + }, + ], + }, + ], + }) + ).warnings.length, + 0 + ) + }) + it('skips empty objects', async function () { assert.equal( ( From 989c603d663476e0782806cb28658d253c2fabeb Mon Sep 17 00:00:00 2001 From: bendo-eXX Date: Mon, 20 Jul 2026 16:42:45 +0200 Subject: [PATCH 09/10] feat(CSAF2.1): change dateHelper --- csaf_2_1/dateHelper.js | 16 ++++++++++++++++ .../recommendedTests/recommendedTest_6_2_24.js | 2 +- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/csaf_2_1/dateHelper.js b/csaf_2_1/dateHelper.js index 037d1dd05..69f7acc17 100644 --- a/csaf_2_1/dateHelper.js +++ b/csaf_2_1/dateHelper.js @@ -17,6 +17,22 @@ export const compareZonedDateTimes = (a, b) => { } } +/** + * Convert a local date in string representation, i.e. a date accurate to the + * day like 'yyyy-mm-dd', to a zoned instant string at the start of that day + * in UTC. This is needed because `compareZonedDateTimes` relies on + * `Temporal.Instant.from`, which cannot parse plain (offset-less) dates. + * + * @param {string} localDateString + * @returns {string} the UTC instant representing the start of the given local date + */ +export const localDateToTimeZonedDate = (localDateString) => { + return Temporal.PlainDate.from(localDateString) + .toZonedDateTime('UTC') + .toInstant() + .toString() +} + /** * This regex validates a date against RFC 3339 section 5.6. * See: https://datatracker.ietf.org/doc/html/rfc3339#section-5.6 diff --git a/csaf_2_1/recommendedTests/recommendedTest_6_2_24.js b/csaf_2_1/recommendedTests/recommendedTest_6_2_24.js index a34d23aa5..ebee81f26 100644 --- a/csaf_2_1/recommendedTests/recommendedTest_6_2_24.js +++ b/csaf_2_1/recommendedTests/recommendedTest_6_2_24.js @@ -3,7 +3,7 @@ import { cwecMap } from '../../lib/cwec.js' import { compareZonedDateTimes, localDateToTimeZonedDate, -} from '../../lib/shared/dateHelper.js' +} from '../dateHelper.js' const ajv = new Ajv() From 139a6a86169677832143a3a89bf4fb03df0a6f09 Mon Sep 17 00:00:00 2001 From: bendo-eXX Date: Thu, 15 Oct 2026 12:26:16 +0200 Subject: [PATCH 10/10] feat(csaf2.1): change unit test to vitest --- .../recommendedTest_6_2_24.js | 38 +++++++++----- tests/csaf_2_1/recommendedTest_6_2_24.js | 50 ++++++++++++++----- 2 files changed, 64 insertions(+), 24 deletions(-) diff --git a/csaf_2_1/recommendedTests/recommendedTest_6_2_24.js b/csaf_2_1/recommendedTests/recommendedTest_6_2_24.js index ebee81f26..20d4d6d7f 100644 --- a/csaf_2_1/recommendedTests/recommendedTest_6_2_24.js +++ b/csaf_2_1/recommendedTests/recommendedTest_6_2_24.js @@ -56,6 +56,29 @@ const cweSchema = /** @type {const} */ ({ const validateCWE = ajv.compile(cweSchema) +/** @type {Promise> | undefined} */ +let sortedCwecReleases + +/** + * Loads the release dates of all known CWE versions once (loading all catalogues is expensive) + * and returns them sorted descending by date (newest first). Versions with the same date + * keep the order of the `cwecMap`, which lists newer versions first. + * + * @returns {Promise>} + */ +function getSortedCwecReleases() { + sortedCwecReleases ??= (async () => { + /** @type {Array<[string, string]>} */ + const releases = [] + for (const [version, load] of cwecMap) { + const date = (await load()).default.date + releases.push([version, localDateToTimeZonedDate(date)]) + } + return releases.sort((a, b) => compareZonedDateTimes(b[1], a[1])) + })() + return sortedCwecReleases +} + /** * This implements the recommended test 6.2.24 of the CSAF 2.1 standard. * @@ -71,16 +94,7 @@ export async function recommendedTest_6_2_24(doc) { } const currentReleaseDate = doc.document.tracking.current_release_date - const cwecArray = [] - for (const key of cwecMap.keys()) { - const cwec = cwecMap.get(key) - if (cwec) { - const date = (await cwec()).default.date - cwecArray.push([key, localDateToTimeZonedDate(date)]) - } - } - /** Make sure the array is sorted descending by date (newest first) hence this is crucial later */ - cwecArray.sort((a, b) => compareZonedDateTimes(b[1], a[1])) + const cwecArray = await getSortedCwecReleases() for (let i = 0; i < doc.vulnerabilities.length; ++i) { const vulnerability = doc.vulnerabilities[i] @@ -97,7 +111,7 @@ export async function recommendedTest_6_2_24(doc) { compareZonedDateTimes(currentReleaseDate, zonedCweReleaseDate) < 0 ) { context.warnings.push({ - instancePath: `/vulnerabilities/${i}/cwes/${j}/id`, + instancePath: `/vulnerabilities/${i}/cwes/${j}/version`, message: `at release date ${currentReleaseDate} the used version ${cwe.version} was not published yet`, }) // Case 2: Check if a newer cwe version than the used one had already been published @@ -113,7 +127,7 @@ export async function recommendedTest_6_2_24(doc) { ) > 0 ) { context.warnings.push({ - instancePath: `/vulnerabilities/${i}/cwes/${j}/id`, + instancePath: `/vulnerabilities/${i}/cwes/${j}/version`, message: `the used cwe version ${cwe.version} ` + `was not the newest one available at release date ${currentReleaseDate}`, diff --git a/tests/csaf_2_1/recommendedTest_6_2_24.js b/tests/csaf_2_1/recommendedTest_6_2_24.js index 738110b2b..263aea75e 100644 --- a/tests/csaf_2_1/recommendedTest_6_2_24.js +++ b/tests/csaf_2_1/recommendedTest_6_2_24.js @@ -1,17 +1,15 @@ -import assert from 'node:assert' import { recommendedTest_6_2_24 } from '../../csaf_2_1/recommendedTests.js' describe('recommendedTest_6_2_24', function () { it('only runs on relevant documents', async function () { - assert.equal( + expect( (await recommendedTest_6_2_24({ vulnerabilities: 'mydoc' })).warnings - .length, - 0 - ) + .length + ).toBe(0) }) it('skips cwe entries with an unknown version (not in cwecMap)', async function () { - assert.equal( + expect( ( await recommendedTest_6_2_24({ document: { @@ -31,13 +29,12 @@ describe('recommendedTest_6_2_24', function () { }, ], }) - ).warnings.length, - 0 - ) + ).warnings.length + ).toBe(0) }) it('skips empty objects', async function () { - assert.equal( + expect( ( await recommendedTest_6_2_24({ document: { @@ -58,8 +55,37 @@ describe('recommendedTest_6_2_24', function () { {}, // should be ignored ], }) - ).warnings.length, - 1 + ).warnings.length + ).toBe(1) + }) + + it('skips cwe entries not matching the cwe schema but checks valid ones', async function () { + const result = await recommendedTest_6_2_24({ + document: { + tracking: { current_release_date: '2024-01-21T10:00:00.000Z' }, + }, + vulnerabilities: [ + { + cwes: [ + { id: 'CWE-256', name: 'Plaintext Storage of a Password' }, // version missing + { + id: 'CWE-256', + name: 'Plaintext Storage of a Password', + version: 4.12, + }, // wrong type + 'CWE-256', // not an object + { + id: 'CWE-256', + name: 'Plaintext Storage of a Password', + version: '4.12', + }, + ], + }, + ], + }) + + expect(result.warnings[0].instancePath).to.equal( + '/vulnerabilities/0/cwes/3/version' ) }) })