From 09c6f99b452c02bcbcad7a976f4a2ff4ed5b0ea9 Mon Sep 17 00:00:00 2001 From: bendo-eXX Date: Tue, 21 Jul 2026 13:23:48 +0200 Subject: [PATCH] feat(csaf2.1): add recommendedTest_6_2_36.js --- README.md | 2 +- csaf_2_1/recommendedTests.js | 1 + .../recommendedTest_6_2_36.js | 108 ++++++++++++++++++ csaf_2_1/shared/ssvcNamespaces.js | 18 +++ tests/csaf_2_1/oasis.js | 1 - tests/csaf_2_1/recommendedTest_6_2_36.js | 11 ++ 6 files changed, 139 insertions(+), 2 deletions(-) create mode 100644 csaf_2_1/recommendedTests/recommendedTest_6_2_36.js create mode 100644 csaf_2_1/shared/ssvcNamespaces.js create mode 100644 tests/csaf_2_1/recommendedTest_6_2_36.js diff --git a/README.md b/README.md index 2910de45..66054ca3 100644 --- a/README.md +++ b/README.md @@ -336,7 +336,6 @@ The following tests are not yet implemented and therefore missing: - Recommended Test 6.2.33 - Recommended Test 6.2.34 - Recommended Test 6.2.35 -- Recommended Test 6.2.36 - Recommended Test 6.2.37 - Recommended Test 6.2.38 - Recommended Test 6.2.39.1 @@ -495,6 +494,7 @@ export const recommendedTest_6_2_27: DocumentTest export const recommendedTest_6_2_28: DocumentTest export const recommendedTest_6_2_29: DocumentTest export const recommendedTest_6_2_30: DocumentTest +export const recommendedTest_6_2_36: DocumentTest export const recommendedTest_6_2_39_2: DocumentTest export const recommendedTest_6_2_39_3: DocumentTest export const recommendedTest_6_2_39_4: DocumentTest diff --git a/csaf_2_1/recommendedTests.js b/csaf_2_1/recommendedTests.js index 46213ccb..badfb223 100644 --- a/csaf_2_1/recommendedTests.js +++ b/csaf_2_1/recommendedTests.js @@ -33,6 +33,7 @@ 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_30 } from './recommendedTests/recommendedTest_6_2_30.js' +export { recommendedTest_6_2_36 } from './recommendedTests/recommendedTest_6_2_36.js' export { recommendedTest_6_2_38 } from './recommendedTests/recommendedTest_6_2_38.js' export { recommendedTest_6_2_39_2 } from './recommendedTests/recommendedTest_6_2_39_2.js' export { recommendedTest_6_2_39_3 } from './recommendedTests/recommendedTest_6_2_39_3.js' diff --git a/csaf_2_1/recommendedTests/recommendedTest_6_2_36.js b/csaf_2_1/recommendedTests/recommendedTest_6_2_36.js new file mode 100644 index 00000000..0b94c2a8 --- /dev/null +++ b/csaf_2_1/recommendedTests/recommendedTest_6_2_36.js @@ -0,0 +1,108 @@ +import { Ajv } from 'ajv/dist/jtd.js' +import { hasSsvcNamespaceExtension } from '../shared/ssvcNamespaces.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: { + distribution: { + additionalProperties: true, + properties: { + tlp: { + additionalProperties: true, + properties: { + label: { type: 'string' }, + }, + }, + }, + }, + }, + }, + }, + optionalProperties: { + 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 optional test 6.2.36 of the CSAF 2.1 standard. + * @param {any} doc + */ +export function recommendedTest_6_2_36(doc) { + const ctx = { + warnings: + /** @type {Array<{ instancePath: string; message: string }>} */ ([]), + } + + if (!validateInput(doc)) { + return ctx + } + + if (doc.document.distribution.tlp.label !== 'CLEAR') { + return ctx + } + + doc.vulnerabilities?.forEach((vulnerability, vulnerabilityIndex) => { + vulnerability.metrics?.forEach((metric, metricIndex) => { + metric.content?.ssvc_v2?.selections?.forEach( + (selection, selectionIndex) => { + const namespace = selection.namespace + if ( + typeof namespace === 'string' && + hasSsvcNamespaceExtension(namespace) + ) { + ctx.warnings.push({ + instancePath: `/vulnerabilities/${vulnerabilityIndex}/metrics/${metricIndex}/content/ssvc_v2/selections/${selectionIndex}/namespace`, + message: + 'the SSVC decision point namespace must not use an extension in TLP:CLEAR documents', + }) + } + } + ) + }) + }) + + return ctx +} diff --git a/csaf_2_1/shared/ssvcNamespaces.js b/csaf_2_1/shared/ssvcNamespaces.js new file mode 100644 index 00000000..88cb9584 --- /dev/null +++ b/csaf_2_1/shared/ssvcNamespaces.js @@ -0,0 +1,18 @@ +/** + * SSVC Namespace Specification: https://certcc.github.io/SSVC/reference/code/namespaces/ + */ + +/** + * Returns true if the given full SSVC namespace string uses an extension. + * + * Extensions are always separated from the base namespace (which may itself + * have an optional `#`-fragment) by a `/` character. Neither the base + * namespace nor its fragment may contain a `/`, so the presence of a `/` + * unambiguously indicates that an extension segment follows. + * + * @param {string} namespace - full namespace string + * @returns {boolean} + */ +export function hasSsvcNamespaceExtension(namespace) { + return namespace.includes('/') +} diff --git a/tests/csaf_2_1/oasis.js b/tests/csaf_2_1/oasis.js index f6b8d926..b659b1c3 100644 --- a/tests/csaf_2_1/oasis.js +++ b/tests/csaf_2_1/oasis.js @@ -35,7 +35,6 @@ const excluded = [ '6.2.33', '6.2.34', '6.2.35', - '6.2.36', '6.2.37', '6.2.39.1', '6.2.39.5', diff --git a/tests/csaf_2_1/recommendedTest_6_2_36.js b/tests/csaf_2_1/recommendedTest_6_2_36.js new file mode 100644 index 00000000..e42a1736 --- /dev/null +++ b/tests/csaf_2_1/recommendedTest_6_2_36.js @@ -0,0 +1,11 @@ +import assert from 'node:assert' +import { recommendedTest_6_2_36 } from '../../csaf_2_1/recommendedTests.js' + +describe('recommendedTest_6_2_36', function () { + it('only runs on relevant documents', function () { + assert.equal( + recommendedTest_6_2_36({ vulnerabilities: 'mydoc' }).warnings.length, + 0 + ) + }) +})