diff --git a/README.md b/README.md index f9a60d1..ac98d80 100644 --- a/README.md +++ b/README.md @@ -331,7 +331,6 @@ The following tests are not yet implemented and therefore missing: - Mandatory Test 6.1.48 - Mandatory Test 6.1.49 - Mandatory Test 6.1.50 -- Mandatory Test 6.1.51 - Mandatory Test 6.1.52 - Mandatory Test 6.1.53 - Mandatory Test 6.1.54 @@ -435,6 +434,7 @@ export const mandatoryTest_6_1_38: DocumentTest export const mandatoryTest_6_1_39: DocumentTest export const mandatoryTest_6_1_40: DocumentTest export const mandatoryTest_6_1_41: DocumentTest +export const mandatoryTest_6_1_51: DocumentTest ``` [(back to top)](#bsi-csaf-validator-lib) diff --git a/csaf_2_1/mandatoryTests.js b/csaf_2_1/mandatoryTests.js index f51d680..4a965d1 100644 --- a/csaf_2_1/mandatoryTests.js +++ b/csaf_2_1/mandatoryTests.js @@ -37,15 +37,16 @@ export { export { mandatoryTest_6_1_1 } from './mandatoryTests/mandatoryTest_6_1_1.js' export { mandatoryTest_6_1_7 } from './mandatoryTests/mandatoryTest_6_1_7.js' export { mandatoryTest_6_1_8 } from './mandatoryTests/mandatoryTest_6_1_8.js' +export { mandatoryTest_6_1_9 } from './mandatoryTests/mandatoryTest_6_1_9.js' export { mandatoryTest_6_1_11 } from './mandatoryTests/mandatoryTest_6_1_11.js' export { mandatoryTest_6_1_13 } from './mandatoryTests/mandatoryTest_6_1_13.js' export { mandatoryTest_6_1_10 } from './mandatoryTests/mandatoryTest_6_1_10.js' export { mandatoryTest_6_1_34 } from './mandatoryTests/mandatoryTest_6_1_34.js' export { mandatoryTest_6_1_35 } from './mandatoryTests/mandatoryTest_6_1_35.js' -export { mandatoryTest_6_1_9 } from './mandatoryTests/mandatoryTest_6_1_9.js' export { mandatoryTest_6_1_36 } from './mandatoryTests/mandatoryTest_6_1_36.js' export { mandatoryTest_6_1_37 } from './mandatoryTests/mandatoryTest_6_1_37.js' export { mandatoryTest_6_1_38 } from './mandatoryTests/mandatoryTests_6_1_38.js' export { mandatoryTest_6_1_39 } from './mandatoryTests/mandatoryTest_6_1_39.js' export { mandatoryTest_6_1_40 } from './mandatoryTests/mandatoryTest_6_1_40.js' export { mandatoryTest_6_1_41 } from './mandatoryTests/mandatoryTest_6_1_41.js' +export { mandatoryTest_6_1_51 } from './mandatoryTests/mandatoryTest_6_1_51.js' diff --git a/csaf_2_1/mandatoryTests/mandatoryTest_6_1_51.js b/csaf_2_1/mandatoryTests/mandatoryTest_6_1_51.js new file mode 100644 index 0000000..def4ce6 --- /dev/null +++ b/csaf_2_1/mandatoryTests/mandatoryTest_6_1_51.js @@ -0,0 +1,122 @@ +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: { + metrics: { + elements: { + additionalProperties: true, + optionalProperties: { + content: { + additionalProperties: true, + optionalProperties: { + epss: { + additionalProperties: true, + optionalProperties: { + timestamp: {type: 'string'}, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, +}) + +const validate = ajv.compile(inputSchema) + +/** + * This implements the mandatory test 6.1.51 of the CSAF 2.1 standard. + * + * @param {any} doc + */ +export function mandatoryTest_6_1_51(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 + } + + const newestRevisionHistoryItem = doc.document.tracking.revision_history + .filter(item => item.date != null) + .slice() + .sort((a, z) => + compareZonedDateTimes( + /** @type {string} */ (z.date), + /** @type {string} */ (a.date) + ) + )[0] + + doc.vulnerabilities?.forEach((vulnerability, vulnerabilityIndex) => { + const metrics = vulnerability.metrics || [] + metrics.forEach((metric, metricIdx) => { + const content = metric.content || {} + const epss = content.epss || {} + if (epss.timestamp) { + if ( + newestRevisionHistoryItem && + compareZonedDateTimes( + /** @type {string} */ (newestRevisionHistoryItem.date), + epss.timestamp + ) < 0 + ) { + ctx.isValid = false + ctx.errors.push({ + instancePath: `/vulnerabilities/${vulnerabilityIndex}/metrics/${metricIdx}/content/epss/timestamp`, + message: `the status is ${status}, but the EPSS 'timestamp' is newer than the newest revision history date`, + }) + } + } + }) + }) + + return ctx +} diff --git a/tests/csaf_2_1/mandatoryTest_6_1_51.js b/tests/csaf_2_1/mandatoryTest_6_1_51.js new file mode 100644 index 0000000..c113f8a --- /dev/null +++ b/tests/csaf_2_1/mandatoryTest_6_1_51.js @@ -0,0 +1,106 @@ +import assert from 'node:assert/strict' +import {mandatoryTest_6_1_51} from '../../csaf_2_1/mandatoryTests/mandatoryTest_6_1_51.js' +import {cvssV31Content} from "./shared/csafDocHelper.js"; + +describe('mandatoryTest_6_1_51', function () { + it('only runs on relevant documents', function () { + assert.equal(mandatoryTest_6_1_51({ document: 'mydoc' }).isValid, true) + }) + + it('skips status draft', function () { + assert.equal( + mandatoryTest_6_1_51({ + document: { + tracking: { + revision_history: [], + status: 'draft', + }, + }, + vulnerabilities: [], + }).isValid, + true + ) + }) + + it('skips empty objects', function () { + assert.equal( + mandatoryTest_6_1_51({ + document: { + tracking: { + revision_history: [ + {}, + { date: '2024-01-24T10:00:00.000Z' } + ], + status: 'final', + }, + }, + vulnerabilities: [ + {}, // should be ignored + { + metrics: [ + {}, // should be ignored + { + content: { + epss: { + timestamp: '2024-01-24T12:34:56.789Z', + } + } + }, + ], + }, + ], + }).isValid, + false + ) + }) + + it('skips empty content object', function () { + assert.equal( + mandatoryTest_6_1_51({ + document: { + tracking: { + revision_history: [{ date: '2024-01-24T10:00:00.000Z' }], + status: 'final', + }, + }, + vulnerabilities: [ + { + metrics: [ + { + content: { + } + }, + ], + }, + ], + }).isValid, + true + ) + }) + + it('skips empty epss object', function () { + assert.equal( + mandatoryTest_6_1_51({ + document: { + tracking: { + revision_history: [{ date: '2024-01-24T10:00:00.000Z' }], + status: 'final', + }, + }, + vulnerabilities: [ + { + metrics: [ + { + content: { + epss: { + } + } + }, + ], + }, + ], + }).isValid, + true + ) + }) +}) diff --git a/tests/csaf_2_1/oasis.js b/tests/csaf_2_1/oasis.js index ed81ff0..c172dca 100644 --- a/tests/csaf_2_1/oasis.js +++ b/tests/csaf_2_1/oasis.js @@ -36,7 +36,6 @@ const excluded = [ '6.1.48', '6.1.49', '6.1.50', - '6.1.51', '6.1.52', '6.1.53', '6.1.54',