diff --git a/README.md b/README.md index 1bedef0c..de2cd012 100644 --- a/README.md +++ b/README.md @@ -313,7 +313,6 @@ The following tests are not yet implemented and therefore missing: - Mandatory Test 6.1.26 - Mandatory Test 6.1.27.13 -- Mandatory Test 6.1.47 - Mandatory Test 6.1.48 - Mandatory Test 6.1.49 - Mandatory Test 6.1.50 @@ -459,6 +458,7 @@ export const mandatoryTest_6_1_43: DocumentTest export const mandatoryTest_6_1_44: DocumentTest export const mandatoryTest_6_1_45: DocumentTest export const mandatoryTest_6_1_46: DocumentTest +export const mandatoryTest_6_1_47: DocumentTest export const mandatoryTest_6_1_51: DocumentTest export const mandatoryTest_6_1_52: DocumentTest export const mandatoryTest_6_1_53: DocumentTest diff --git a/csaf_2_1/mandatoryTests.js b/csaf_2_1/mandatoryTests.js index d26ae836..139c5e10 100644 --- a/csaf_2_1/mandatoryTests.js +++ b/csaf_2_1/mandatoryTests.js @@ -63,6 +63,7 @@ export { mandatoryTest_6_1_43 } from './mandatoryTests/mandatoryTest_6_1_43.js' export { mandatoryTest_6_1_44 } from './mandatoryTests/mandatoryTest_6_1_44.js' export { mandatoryTest_6_1_45 } from './mandatoryTests/mandatoryTest_6_1_45.js' export { mandatoryTest_6_1_46 } from './mandatoryTests/mandatoryTest_6_1_46.js' +export { mandatoryTest_6_1_47 } from './mandatoryTests/mandatoryTest_6_1_47.js' export { mandatoryTest_6_1_51 } from './mandatoryTests/mandatoryTest_6_1_51.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_47.js b/csaf_2_1/mandatoryTests/mandatoryTest_6_1_47.js new file mode 100644 index 00000000..72be33f3 --- /dev/null +++ b/csaf_2_1/mandatoryTests/mandatoryTest_6_1_47.js @@ -0,0 +1,111 @@ +import { Ajv } from 'ajv/dist/jtd.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, + optionalProperties: { + tracking: { + additionalProperties: true, + optionalProperties: { + id: { type: 'string' }, + }, + }, + }, + }, + vulnerabilities: { + elements: { + additionalProperties: true, + optionalProperties: { + cve: { type: 'string' }, + ids: { + elements: { + additionalProperties: true, + optionalProperties: { + text: { type: 'string' }, + }, + }, + }, + metrics: { + elements: { + additionalProperties: true, + optionalProperties: { + content: { + additionalProperties: true, + optionalProperties: { + ssvc_v2: { + additionalProperties: true, + optionalProperties: { + target_ids: { elements: { type: 'string' } }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, +}) + +const validateInput = ajv.compile(inputSchema) + +/** + * This implements the mandatory test 6.1.47 of the CSAF 2.1 standard. + * + * @param {any} doc + */ +export function mandatoryTest_6_1_47(doc) { + const ctx = { + errors: + /** @type {Array<{ instancePath: string; message: string }>} */ ([]), + isValid: true, + } + + if (!validateInput(doc)) { + return ctx + } + + doc.vulnerabilities.forEach((vulnerability, vulnerabilityIndex) => { + vulnerability.metrics?.forEach((metric, metricIndex) => { + if (metric.content?.ssvc_v2) { + metric.content.ssvc_v2.target_ids?.forEach((ssvcId, ssvcIdIndex) => { + if (ssvcId === doc.document.tracking?.id) { + if (doc.vulnerabilities.length > 1) { + ctx.isValid = false + ctx.errors.push({ + instancePath: `/vulnerabilities/${vulnerabilityIndex}/metrics/${metricIndex}/content/ssvc_v2/target_ids/${ssvcIdIndex}`, + message: + `the ssvc id equals the "document/tracking/id" ` + + `but the csaf document has multiple vulnerabilities`, + }) + } + } + const idTexts = vulnerability.ids?.map((id) => id.text) + if (ssvcId !== vulnerability.cve && !idTexts?.includes(ssvcId)) { + ctx.isValid = false + ctx.errors.push({ + instancePath: `/vulnerabilities/${vulnerabilityIndex}/metrics/${metricIndex}/content/ssvc_v2/target_ids/${ssvcIdIndex}`, + message: + `the ssvc id does neither match the "cve" ` + + `nor it matches the "text" of any item in the "ids" array`, + }) + } + }) + } + }) + }) + + return ctx +} diff --git a/tests/csaf_2_1/mandatoryTest_6_1_47.js b/tests/csaf_2_1/mandatoryTest_6_1_47.js new file mode 100644 index 00000000..e5e87c3f --- /dev/null +++ b/tests/csaf_2_1/mandatoryTest_6_1_47.js @@ -0,0 +1,111 @@ +import assert from 'node:assert/strict' +import { mandatoryTest_6_1_47 } from '../../csaf_2_1/mandatoryTests/mandatoryTest_6_1_47.js' +import { expect } from 'chai' + +const failingInputSchemaTestWithEmptyVulnerability6_1_47 = { + document: {}, + vulnerabilities: [ + {}, // even this vulnerability is empty, the test should run + { + cve: 'CVE-1900-0001', + metrics: [ + { + content: { + ssvc_v2: { + schemaVersion: '2.0.0', + selections: [ + { + key: 'E', + namespace: 'ssvc', + values: [ + { + key: 'N', + }, + ], + version: '1.1.0', + }, + ], + target_ids: ['CVE-1900-0002'], + timestamp: '2024-01-24T10:00:00.000Z', + }, + }, + products: ['CSAFPID-9080700'], + }, + ], + }, + ], +} + +describe('mandatoryTest_6_1_47', function () { + it('only runs on relevant documents', function () { + assert.equal(mandatoryTest_6_1_47({ document: 'mydoc' }).isValid, true) + }) + it('test input schema with empty json object in vulnerabilities', async function () { + const result = mandatoryTest_6_1_47( + failingInputSchemaTestWithEmptyVulnerability6_1_47 + ) + expect(result.errors.length).to.eq(1) + }) + it('emits duplicate errors for one target_id when tracking.id rule and cve/ids rule both fire', async () => { + const doc = { + document: { + category: 'csaf_vex', + csaf_version: '2.1', + lang: 'en-US', + title: 'double-error reproduction', + tracking: { + id: 'TRACK-1', + status: 'final', + version: '1', + initial_release_date: '2024-01-01T00:00:00.000Z', + current_release_date: '2024-01-01T00:00:00.000Z', + revision_history: [ + { + number: '1', + date: '2024-01-01T00:00:00.000Z', + summary: 'initial', + }, + ], + }, + publisher: { + category: 'vendor', + name: 'ACME', + namespace: 'https://example.com', + }, + }, + vulnerabilities: [ + { + cve: 'CVE-2024-0001', + metrics: [ + { + content: { + ssvc_v2: { + // equals tracking.id + target_ids: ['TRACK-1'], + }, + }, + }, + ], + }, + { + cve: 'CVE-2024-0002', + metrics: [], + }, + ], + } + + const result = mandatoryTest_6_1_47(doc) + + // Current behavior: two errors for same location with different messages + expect(result.isValid).eq(false) + expect(result.errors.length).eq(2) + + const path = '/vulnerabilities/0/metrics/0/content/ssvc_v2/target_ids/0' + expect(result.errors[0].instancePath).eq(path) + expect(result.errors[0].message.startsWith('the ssvc id equals the ')).to.be + .true + expect(result.errors[1].instancePath).eq(path) + expect(result.errors[1].message.startsWith('the ssvc id does neither ')).to + .be.true + }) +}) diff --git a/tests/csaf_2_1/oasis.js b/tests/csaf_2_1/oasis.js index 1058d0e3..5b616af8 100644 --- a/tests/csaf_2_1/oasis.js +++ b/tests/csaf_2_1/oasis.js @@ -16,7 +16,6 @@ const excluded = [ '6.1.27.11', '6.1.27.13', '6.1.37', - '6.1.47', '6.1.48', '6.1.49', '6.1.50',