From ebf10d3e64b074da81be0be8b531ddfed87326ef Mon Sep 17 00:00:00 2001 From: bendo-eXX Date: Tue, 15 Jul 2025 17:15:49 +0200 Subject: [PATCH 1/2] feat(CSAF2.1): add recommendedTest_6_2_41.js --- README.md | 2 - csaf_2_1/recommendedTests.js | 1 + .../recommendedTest_6_2_41.js | 132 ++++++++++++++++++ tests/csaf_2_1/oasis.js | 1 - tests/csaf_2_1/recommendedTest_6_2_41.js | 96 +++++++++++++ 5 files changed, 229 insertions(+), 3 deletions(-) create mode 100644 csaf_2_1/recommendedTests/recommendedTest_6_2_41.js create mode 100644 tests/csaf_2_1/recommendedTest_6_2_41.js diff --git a/README.md b/README.md index 6f4f9297..072a9936 100644 --- a/README.md +++ b/README.md @@ -349,7 +349,6 @@ The following tests are not yet implemented and therefore missing: - Recommended Test 6.2.38 - Recommended Test 6.2.39 - Recommended Test 6.2.40 -- Recommended Test 6.2.41 - Recommended Test 6.2.42 - Recommended Test 6.2.43 - Recommended Test 6.2.44 @@ -460,7 +459,6 @@ 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/recommendedTests.js b/csaf_2_1/recommendedTests.js index a39c6673..e1bd188a 100644 --- a/csaf_2_1/recommendedTests.js +++ b/csaf_2_1/recommendedTests.js @@ -32,3 +32,4 @@ 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_38 } from './recommendedTests/recommendedTest_6_2_38.js' +export { recommendedTest_6_2_41 } from './recommendedTests/recommendedTest_6_2_41.js' diff --git a/csaf_2_1/recommendedTests/recommendedTest_6_2_41.js b/csaf_2_1/recommendedTests/recommendedTest_6_2_41.js new file mode 100644 index 00000000..3b564b9b --- /dev/null +++ b/csaf_2_1/recommendedTests/recommendedTest_6_2_41.js @@ -0,0 +1,132 @@ +import Ajv from 'ajv/dist/jtd.js' +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, + optionalProperties: { + date: { type: 'string' }, + }, + }, + }, + status: { type: 'string' }, + }, + }, + }, + }, + vulnerabilities: { + elements: { + additionalProperties: true, + optionalProperties: { + metrics: { + elements: { + additionalProperties: true, + optionalProperties: { + content: { + additionalProperties: true, + optionalProperties: { + epss: { + additionalProperties: true, + optionalProperties: { + timestamp: { type: 'string' }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, +}) + +const validate = ajv.compile(inputSchema) + +/** + * This implements the recommended test 6.2.41 of the CSAF 2.1 standard. + * + /** + * @param {any} doc + */ +export function recommendedTest_6_2_41(doc) { + /** @type {Array<{ message: string; instancePath: string }>} */ + const warnings = [] + const context = { warnings } + + if (!validate(doc)) { + return context + } + + const status = doc.document.tracking.status + if (status !== 'final' && status !== 'interim') { + return context + } + + const newestRevisionHistoryItem = doc.document.tracking.revision_history + .filter((item) => item.date != null) + .sort((a, z) => + compareZonedDateTimes( + /** @type {string} */ (z.date), + /** @type {string} */ (a.date) + ) + )[0] + + if (!newestRevisionHistoryItem || !newestRevisionHistoryItem.date) { + return context + } + + doc.vulnerabilities?.forEach((vulnerability, vulnerabilityIndex) => { + const metrics = vulnerability.metrics || [] + const newestEpss = metrics + .map((m) => m.content?.epss) + .filter((item) => item?.timestamp != null) + .sort((a, z) => { + if (!a || !z) return 0 + return compareZonedDateTimes( + /** @type {string} */ (z.timestamp), + /** @type {string} */ (a.timestamp) + ) + })[0] + + if ( + !newestEpss || + !newestEpss.timestamp || + !newestRevisionHistoryItem || + !newestRevisionHistoryItem.date + ) { + return context + } + + const revisionDateObj = new Date(newestRevisionHistoryItem.date) + const epssDateObj = new Date(newestEpss.timestamp) + + // difference in milliseconds + const diffInMs = revisionDateObj.getTime() - epssDateObj.getTime() + // 15 days in milliseconds + const fifteenDaysMs = 15 * 24 * 60 * 60 * 1000 + + if (diffInMs > fifteenDaysMs) { + context.warnings.push({ + instancePath: `/vulnerabilities/${vulnerabilityIndex}/metrics/content/epss/timestamp`, + message: + `the status is ${status}, but the EPSS "timestamp:" ${newestEpss.timestamp} is more than 15 days ` + + `older than the newest "revision history date:" ${newestRevisionHistoryItem.date}`, + }) + } + }) + + return context +} diff --git a/tests/csaf_2_1/oasis.js b/tests/csaf_2_1/oasis.js index 0e9d2e60..e533265d 100644 --- a/tests/csaf_2_1/oasis.js +++ b/tests/csaf_2_1/oasis.js @@ -52,7 +52,6 @@ const excluded = [ '6.2.39.3', '6.2.39.4', '6.2.40', - '6.2.41', '6.2.42', '6.2.43', '6.2.44', diff --git a/tests/csaf_2_1/recommendedTest_6_2_41.js b/tests/csaf_2_1/recommendedTest_6_2_41.js new file mode 100644 index 00000000..b7d3471d --- /dev/null +++ b/tests/csaf_2_1/recommendedTest_6_2_41.js @@ -0,0 +1,96 @@ +import assert from 'node:assert' +import { recommendedTest_6_2_41 } from '../../csaf_2_1/recommendedTests.js' + +describe('recommendedTest_6_2_41', function () { + it('only runs on relevant documents', function () { + assert.equal( + recommendedTest_6_2_41({ vulnerabilities: 'mydoc' }).warnings.length, + 0 + ) + }) + + it('skips status draft', function () { + assert.equal( + recommendedTest_6_2_41({ + document: { + tracking: { + revision_history: [], + status: 'draft', + }, + }, + vulnerabilities: [], + }).warnings.length, + 0 + ) + }) + + it('skips empty revision_history object', function () { + assert.equal( + recommendedTest_6_2_41({ + document: { + tracking: { + revision_history: [ + {}, // should be ignored + ], + status: 'final', + }, + }, + vulnerabilities: [], + }).warnings.length, + 0 + ) + }) + + it('skips empty metrics object', function () { + assert.equal( + recommendedTest_6_2_41({ + document: { + tracking: { + revision_history: [{ date: '2024-01-24T10:00:00.000Z' }], + status: 'final', + }, + }, + vulnerabilities: [ + { + metrics: [ + {}, // should be ignored + ], + }, + ], + }).warnings.length, + 0 + ) + }) + + it('skips empty epss object', function () { + assert.equal( + recommendedTest_6_2_41({ + document: { + tracking: { + revision_history: [{ date: '2024-01-24T10:00:00.000Z' }], + status: 'final', + }, + }, + vulnerabilities: [ + { + metrics: [ + { + content: { + epss: {}, // should be ignored + }, + }, + { + content: { + epss: { + timestamp: '2024-01-01T10:00:00.000Z', + }, + }, + }, + ], + }, + ], + }).warnings.length, + 1 + ) + }) +}) From f39e839d6c219929d13764091e84e5fe4936a8b7 Mon Sep 17 00:00:00 2001 From: bendo-eXX Date: Wed, 3 Sep 2025 12:14:51 +0200 Subject: [PATCH 2/2] feat(CSAF2.1): change compareZonedDateTimes import --- README.md | 1 + csaf_2_1/recommendedTests/recommendedTest_6_2_41.js | 2 +- tests/csaf_2_1/recommendedTest_6_2_41.js | 10 ++-------- 3 files changed, 4 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 072a9936..c78133ca 100644 --- a/README.md +++ b/README.md @@ -459,6 +459,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/recommendedTests/recommendedTest_6_2_41.js b/csaf_2_1/recommendedTests/recommendedTest_6_2_41.js index 3b564b9b..00290bf7 100644 --- a/csaf_2_1/recommendedTests/recommendedTest_6_2_41.js +++ b/csaf_2_1/recommendedTests/recommendedTest_6_2_41.js @@ -1,5 +1,5 @@ import Ajv from 'ajv/dist/jtd.js' -import { compareZonedDateTimes } from '../../lib/shared/dateHelper.js' +import { compareZonedDateTimes } from '../dateHelper.js' const ajv = new Ajv() diff --git a/tests/csaf_2_1/recommendedTest_6_2_41.js b/tests/csaf_2_1/recommendedTest_6_2_41.js index b7d3471d..adb7ff5a 100644 --- a/tests/csaf_2_1/recommendedTest_6_2_41.js +++ b/tests/csaf_2_1/recommendedTest_6_2_41.js @@ -41,7 +41,7 @@ describe('recommendedTest_6_2_41', function () { ) }) - it('skips empty metrics object', function () { + it('Skips vulnerabilities without metrics object', function () { assert.equal( recommendedTest_6_2_41({ document: { @@ -50,13 +50,7 @@ describe('recommendedTest_6_2_41', function () { status: 'final', }, }, - vulnerabilities: [ - { - metrics: [ - {}, // should be ignored - ], - }, - ], + vulnerabilities: [{}], }).warnings.length, 0 )