diff --git a/README.md b/README.md index 647244ed..21d4a533 100644 --- a/README.md +++ b/README.md @@ -375,7 +375,6 @@ The following tests are not yet implemented and therefore missing: **Informative Tests** - Informative Test 6.3.13 -- Informative Test 6.3.14 - Informative Test 6.3.16 - Informative Test 6.3.17 - Informative Test 6.3.19.1 @@ -557,6 +556,7 @@ export const informativeTest_6_3_9: DocumentTest export const informativeTest_6_3_10: DocumentTest export const informativeTest_6_3_11: DocumentTest export const informativeTest_6_3_12: DocumentTest +export const informativeTest_6_3_14: DocumentTest export const informativeTest_6_3_15: DocumentTest export const informativeTest_6_3_18: DocumentTest export const informativeTest_6_3_20: DocumentTest diff --git a/csaf_2_1/informativeTests.js b/csaf_2_1/informativeTests.js index 6ac5cfb0..a4816656 100644 --- a/csaf_2_1/informativeTests.js +++ b/csaf_2_1/informativeTests.js @@ -12,6 +12,7 @@ export { informativeTest_6_3_5 } from './informativeTests/informativeTest_6_3_5. export { informativeTest_6_3_6 } from './informativeTests/informativeTest_6_3_6.js' export { informativeTest_6_3_11 } from './informativeTests/informativeTest_6_3_11.js' export { informativeTest_6_3_12 } from './informativeTests/informativeTest_6_3_12.js' +export { informativeTest_6_3_14 } from './informativeTests/informativeTest_6_3_14.js' export { informativeTest_6_3_15 } from './informativeTests/informativeTest_6_3_15.js' export { informativeTest_6_3_18 } from './informativeTests/informativeTest_6_3_18.js' export { informativeTest_6_3_20 } from './informativeTests/informativeTest_6_3_20.js' diff --git a/csaf_2_1/informativeTests/informativeTest_6_3_14.js b/csaf_2_1/informativeTests/informativeTest_6_3_14.js new file mode 100644 index 00000000..ceba8789 --- /dev/null +++ b/csaf_2_1/informativeTests/informativeTest_6_3_14.js @@ -0,0 +1,103 @@ +import { Ajv } from 'ajv/dist/jtd.js' +import { isRegisteredSsvcNamespace } from '#csaf_2_1/shared/ssvcNamespaces.js' + +const ajv = new Ajv() + +const inputSchema = /** @type {const} */ ({ + additionalProperties: true, + properties: { + document: { + additionalProperties: true, + optionalProperties: { + distribution: { + additionalProperties: true, + optionalProperties: { + tlp: { + additionalProperties: true, + optionalProperties: { + label: { type: 'string' }, + }, + }, + }, + }, + }, + }, + vulnerabilities: { + elements: { + additionalProperties: true, + optionalProperties: { + metrics: { + elements: { + additionalProperties: true, + optionalProperties: { + content: { + additionalProperties: true, + optionalProperties: { + ssvc_v2: { + additionalProperties: true, + optionalProperties: { + selections: { + elements: { + additionalProperties: true, + optionalProperties: { + namespace: { type: 'string' }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, +}) + +const validateInput = ajv.compile(inputSchema) + +/** + * This implements the informative test 6.3.14 of the CSAF 2.1 standard. + * + * For each decision point, it SHALL be tested that the namespace is not an + * unregistered one if the document is not labeled TLP:CLEAR. + * + * @param {unknown} doc + * @returns {{ infos: Array<{ message: string; instancePath: string }> }} + */ +export function informativeTest_6_3_14(doc) { + const ctx = { + infos: /** @type {Array<{ message: string; instancePath: string }>} */ ([]), + } + + if (!validateInput(doc)) { + return ctx + } + + if (doc.document?.distribution?.tlp?.label === 'CLEAR') { + return ctx + } + + const vulnerabilities = doc.vulnerabilities + + vulnerabilities.forEach((vulnerability, vulnerabilityIndex) => { + vulnerability.metrics?.forEach((metric, metricIndex) => { + const selections = metric.content?.ssvc_v2?.selections + if (!selections) return + selections.forEach((selection, selectionIndex) => { + if (!selection.namespace) return + if (!isRegisteredSsvcNamespace(selection.namespace)) { + ctx.infos.push({ + instancePath: `/vulnerabilities/${vulnerabilityIndex}/metrics/${metricIndex}/content/ssvc_v2/selections/${selectionIndex}/namespace`, + message: `SSVC decision point namespace "${selection.namespace}" is unregistered and the document is not labeled TLP:CLEAR`, + }) + } + }) + }) + }) + + return ctx +} diff --git a/tests/csaf_2_1/informativeTest_6_3_14.js b/tests/csaf_2_1/informativeTest_6_3_14.js new file mode 100644 index 00000000..4217b276 --- /dev/null +++ b/tests/csaf_2_1/informativeTest_6_3_14.js @@ -0,0 +1,117 @@ +import { informativeTest_6_3_14 } from '../../csaf_2_1/informativeTests.js' + +describe('informativeTest_6_3_14', function () { + it('only runs on relevant documents', function () { + expect(informativeTest_6_3_14({ document: 'mydoc' }).infos.length).toBe(0) + }) + + it('skips metrics without selections', function () { + expect( + informativeTest_6_3_14({ + document: { + distribution: { + tlp: { + label: 'GREEN', + }, + }, + }, + vulnerabilities: [ + {}, + { + cve: 'CVE-1900-0001', + metrics: [ + { + content: {}, + products: ['CSAFPID-9080700'], + }, + ], + }, + ], + }).infos.length + ).toBe(0) + }) + + it('does not report unregistered namespaces when the document is labeled TLP:CLEAR', function () { + expect( + informativeTest_6_3_14({ + document: { + distribution: { + tlp: { + label: 'CLEAR', + }, + }, + }, + vulnerabilities: [ + { + cve: 'CVE-1900-0001', + metrics: [ + { + content: { + ssvc_v2: { + id: 'CVE-1900-0001', + schemaVersion: '1-0-1', + selections: [ + { + name: 'Technical Impact', + namespace: 'acme', + values: ['Total'], + version: '1.0.0', + }, + ], + timestamp: '2024-01-24T10:00:00.000Z', + }, + }, + products: ['CSAFPID-9080700'], + }, + ], + }, + ], + }).infos.length + ).toBe(0) + }) + + it('skips selections without namespace and reports the unregistered ones', function () { + expect( + informativeTest_6_3_14({ + document: { + distribution: { + tlp: { + label: 'GREEN', + }, + }, + }, + vulnerabilities: [ + {}, + { + cve: 'CVE-1900-0001', + metrics: [ + { + content: { + ssvc_v2: { + id: 'CVE-1900-0001', + schemaVersion: '1-0-1', + selections: [ + { + name: 'Technical Impact', + namespace: 'acme', + values: ['Total'], + version: '1.0.0', + }, + { + name: 'Technical Impact', + values: ['Total'], + version: '1.0.0', + }, + ], + timestamp: '2024-01-24T10:00:00.000Z', + }, + }, + products: ['CSAFPID-9080700'], + }, + ], + }, + ], + }).infos.length + ).toBe(1) + }) +}) diff --git a/tests/csaf_2_1/oasis.js b/tests/csaf_2_1/oasis.js index a65b2ea6..f2bb61dd 100644 --- a/tests/csaf_2_1/oasis.js +++ b/tests/csaf_2_1/oasis.js @@ -30,7 +30,6 @@ const excluded = [ '6.2.54.4', '6.3.12', '6.3.13', - '6.3.14', '6.3.16', '6.3.17', '6.3.19.1',