From edad0805ef557c00396e670492151795f0b442f5 Mon Sep 17 00:00:00 2001 From: bendo-eXX Date: Thu, 26 Jun 2025 14:15:02 +0200 Subject: [PATCH 1/2] feat: add mandatory test 6.1.53 --- README.md | 2 +- csaf_2_1/mandatoryTests.js | 1 + .../mandatoryTests/mandatoryTest_6_1_53.js | 101 ++++++++++++++++++ tests/csaf_2_1/mandatoryTest_6_1_53.js | 73 +++++++++++++ tests/csaf_2_1/oasis.js | 1 - 5 files changed, 176 insertions(+), 2 deletions(-) create mode 100644 csaf_2_1/mandatoryTests/mandatoryTest_6_1_53.js create mode 100644 tests/csaf_2_1/mandatoryTest_6_1_53.js diff --git a/README.md b/README.md index 6f4f9297..91792594 100644 --- a/README.md +++ b/README.md @@ -322,7 +322,6 @@ The following tests are not yet implemented and therefore missing: - Mandatory Test 6.1.49 - Mandatory Test 6.1.50 - Mandatory Test 6.1.51 -- Mandatory Test 6.1.53 - Mandatory Test 6.1.54 - Mandatory Test 6.1.55 @@ -435,6 +434,7 @@ export const mandatoryTest_6_1_41: DocumentTest export const mandatoryTest_6_1_43: DocumentTest export const mandatoryTest_6_1_45: DocumentTest export const mandatoryTest_6_1_52: DocumentTest +export const mandatoryTest_6_1_53: DocumentTest ``` [(back to top)](#bsi-csaf-validator-lib) diff --git a/csaf_2_1/mandatoryTests.js b/csaf_2_1/mandatoryTests.js index d5a7706b..d418a3fc 100644 --- a/csaf_2_1/mandatoryTests.js +++ b/csaf_2_1/mandatoryTests.js @@ -60,3 +60,4 @@ export { mandatoryTest_6_1_41 } from './mandatoryTests/mandatoryTest_6_1_41.js' export { mandatoryTest_6_1_43 } from './mandatoryTests/mandatoryTest_6_1_43.js' export { mandatoryTest_6_1_45 } from './mandatoryTests/mandatoryTest_6_1_45.js' export { mandatoryTest_6_1_52 } from './mandatoryTests/mandatoryTest_6_1_52.js' +export { mandatoryTest_6_1_53 } from './mandatoryTests/mandatoryTest_6_1_53.js' diff --git a/csaf_2_1/mandatoryTests/mandatoryTest_6_1_53.js b/csaf_2_1/mandatoryTests/mandatoryTest_6_1_53.js new file mode 100644 index 00000000..42abe181 --- /dev/null +++ b/csaf_2_1/mandatoryTests/mandatoryTest_6_1_53.js @@ -0,0 +1,101 @@ +import Ajv from 'ajv/dist/jtd.js' +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: { + revision_history: { + elements: { + additionalProperties: true, + optionalProperties: { + date: { type: 'string' }, + }, + }, + }, + status: { type: 'string' }, + }, + }, + }, + }, + vulnerabilities: { + elements: { + additionalProperties: true, + optionalProperties: { + first_known_exploitation_dates: { + elements: { + additionalProperties: true, + optionalProperties: { + date: { type: 'string' }, + exploitation_date: { type: 'string' }, + }, + }, + }, + }, + }, + }, + }, +}) + +const validate = ajv.compile(inputSchema) + +/** + * This implements the mandatory test 6.1.53 of the CSAF 2.1 standard. + * + * @param {any} doc + */ +export function mandatoryTest_6_1_53(doc) { + /* + The `ctx` variable holds the state that is accumulated during the test ran and is + finally returned by the function. + */ + const ctx = { + errors: + /** @type {Array<{ instancePath: string; message: string }>} */ ([]), + isValid: true, + } + + if (!validate(doc)) { + return ctx + } + const status = doc.document.tracking.status + if (status !== 'final' && status !== 'interim') { + return ctx + } + + doc.vulnerabilities?.forEach((vulnerability, vulnerabilityIndex) => { + const exploitDate = vulnerability.first_known_exploitation_dates || [] + exploitDate.forEach((exploit, exploitIdx) => { + const date = exploit.date + const exploitationDate = exploit.exploitation_date + + if ( + compareZonedDateTimes( + /** @type {string} */ (date), + /** @type {string} */ (exploitationDate) + ) < 0 + ) { + ctx.isValid = false + ctx.errors.push({ + instancePath: `/vulnerabilities/${vulnerabilityIndex}/first_known_exploitation_dates/${exploitIdx}`, + message: `the status is ${status}, but the "exploitation_date" are newer than the "date"`, + }) + } + }) + }) + + return ctx +} diff --git a/tests/csaf_2_1/mandatoryTest_6_1_53.js b/tests/csaf_2_1/mandatoryTest_6_1_53.js new file mode 100644 index 00000000..31f3d7c3 --- /dev/null +++ b/tests/csaf_2_1/mandatoryTest_6_1_53.js @@ -0,0 +1,73 @@ +import assert from 'node:assert/strict' +import { mandatoryTest_6_1_53 } from '../../csaf_2_1/mandatoryTests/mandatoryTest_6_1_53.js' + +describe('mandatoryTest_6_1_53', function () { + it('only runs on relevant documents', function () { + assert.equal(mandatoryTest_6_1_53({ document: 'mydoc' }).isValid, true) + }) + + it('skips status draft', function () { + assert.equal( + mandatoryTest_6_1_53({ + document: { + tracking: { + revision_history: [], + status: 'draft', + }, + }, + vulnerabilities: [], + }).isValid, + true + ) + }) + + it('skips empty vulnerability', function () { + assert.equal( + mandatoryTest_6_1_53({ + document: { + tracking: { + revision_history: [], + status: 'final', + }, + }, + vulnerabilities: [ + {}, // should be ignored + { + first_known_exploitation_dates: [ + { + date: '2024-01-24T12:34:56.789Z', + exploitation_date: '2024-01-24T13:00:00.000Z', + }, + ], + }, + ], + }).isValid, + false + ) + }) + + it('skips empty first_known_exploitation_date', function () { + assert.equal( + mandatoryTest_6_1_53({ + document: { + tracking: { + revision_history: [], + status: 'final', + }, + }, + vulnerabilities: [ + { + first_known_exploitation_dates: [ + {}, // should be ignored + { + date: '2024-01-24T12:34:56.789Z', + exploitation_date: '2024-01-24T13:00:00.000Z', + }, + ], + }, + ], + }).isValid, + false + ) + }) +}) diff --git a/tests/csaf_2_1/oasis.js b/tests/csaf_2_1/oasis.js index 0e9d2e60..1ee840e3 100644 --- a/tests/csaf_2_1/oasis.js +++ b/tests/csaf_2_1/oasis.js @@ -28,7 +28,6 @@ const excluded = [ '6.1.49', '6.1.50', '6.1.51', - '6.1.53', '6.1.54', '6.1.55', '6.1.56', From f381750ed53f0e67c6238821617e0707a51cb961 Mon Sep 17 00:00:00 2001 From: bendo-eXX Date: Fri, 12 Sep 2025 12:02:57 +0200 Subject: [PATCH 2/2] fix(CSAF2.1): remove status --- csaf_2_1/mandatoryTests/mandatoryTest_6_1_53.js | 9 ++------- tests/csaf_2_1/mandatoryTest_6_1_53.js | 17 ----------------- 2 files changed, 2 insertions(+), 24 deletions(-) diff --git a/csaf_2_1/mandatoryTests/mandatoryTest_6_1_53.js b/csaf_2_1/mandatoryTests/mandatoryTest_6_1_53.js index 42abe181..55b01d99 100644 --- a/csaf_2_1/mandatoryTests/mandatoryTest_6_1_53.js +++ b/csaf_2_1/mandatoryTests/mandatoryTest_6_1_53.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() @@ -26,7 +26,6 @@ const inputSchema = /** @type {const} */ ({ }, }, }, - status: { type: 'string' }, }, }, }, @@ -71,10 +70,6 @@ export function mandatoryTest_6_1_53(doc) { if (!validate(doc)) { return ctx } - const status = doc.document.tracking.status - if (status !== 'final' && status !== 'interim') { - return ctx - } doc.vulnerabilities?.forEach((vulnerability, vulnerabilityIndex) => { const exploitDate = vulnerability.first_known_exploitation_dates || [] @@ -91,7 +86,7 @@ export function mandatoryTest_6_1_53(doc) { ctx.isValid = false ctx.errors.push({ instancePath: `/vulnerabilities/${vulnerabilityIndex}/first_known_exploitation_dates/${exploitIdx}`, - message: `the status is ${status}, but the "exploitation_date" are newer than the "date"`, + message: 'the "exploitation_date" is newer than the "date"', }) } }) diff --git a/tests/csaf_2_1/mandatoryTest_6_1_53.js b/tests/csaf_2_1/mandatoryTest_6_1_53.js index 31f3d7c3..2202efd2 100644 --- a/tests/csaf_2_1/mandatoryTest_6_1_53.js +++ b/tests/csaf_2_1/mandatoryTest_6_1_53.js @@ -6,28 +6,12 @@ describe('mandatoryTest_6_1_53', function () { assert.equal(mandatoryTest_6_1_53({ document: 'mydoc' }).isValid, true) }) - it('skips status draft', function () { - assert.equal( - mandatoryTest_6_1_53({ - document: { - tracking: { - revision_history: [], - status: 'draft', - }, - }, - vulnerabilities: [], - }).isValid, - true - ) - }) - it('skips empty vulnerability', function () { assert.equal( mandatoryTest_6_1_53({ document: { tracking: { revision_history: [], - status: 'final', }, }, vulnerabilities: [ @@ -52,7 +36,6 @@ describe('mandatoryTest_6_1_53', function () { document: { tracking: { revision_history: [], - status: 'final', }, }, vulnerabilities: [