diff --git a/README.md b/README.md index 6f4f9297..c78133ca 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 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..00290bf7 --- /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 '../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..adb7ff5a --- /dev/null +++ b/tests/csaf_2_1/recommendedTest_6_2_41.js @@ -0,0 +1,90 @@ +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 vulnerabilities without metrics object', function () { + assert.equal( + recommendedTest_6_2_41({ + document: { + tracking: { + revision_history: [{ date: '2024-01-24T10:00:00.000Z' }], + status: 'final', + }, + }, + vulnerabilities: [{}], + }).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 + ) + }) +})