From aaa0ee3cde54f6a641bda2b53aeeac890da7898f Mon Sep 17 00:00:00 2001 From: Benjamin Date: Mon, 28 Apr 2025 15:46:27 +0200 Subject: [PATCH 01/12] feat(CSAF2.1): #196 add optionalTest_6_2_31.js --- csaf_2_1/optionalTests.js | 0 .../recommendedTests/optionalTest_6_2_31.js | 181 ++++++++++++++++++ tests/csaf_2_1/oasis.js | 1 - 3 files changed, 181 insertions(+), 1 deletion(-) create mode 100644 csaf_2_1/optionalTests.js create mode 100644 csaf_2_1/recommendedTests/optionalTest_6_2_31.js diff --git a/csaf_2_1/optionalTests.js b/csaf_2_1/optionalTests.js new file mode 100644 index 00000000..e69de29b diff --git a/csaf_2_1/recommendedTests/optionalTest_6_2_31.js b/csaf_2_1/recommendedTests/optionalTest_6_2_31.js new file mode 100644 index 00000000..c17b11ab --- /dev/null +++ b/csaf_2_1/recommendedTests/optionalTest_6_2_31.js @@ -0,0 +1,181 @@ +import Ajv from 'ajv/dist/jtd.js' + +const ajv = new Ajv() + +const productIdentificationHelperSchema = { + additionalProperties: true, + optionalProperties: { + serial_numbers: { + elements: { type: 'string' }, + }, + model_numbers: { + elements: { type: 'string' }, + }, + }, +}; + +const relationshipSchema = { + elements: { + additionalProperties: true, + optionalProperties: { + product_reference: { type: 'string' }, + relates_to_product_reference: { type: 'string' }, + }, + }, +}; + +const inputSchema = /** @type {const} */ ({ + additionalProperties: true, + properties: { + product_tree: { + additionalProperties: true, + optionalProperties: { + branches: { + elements: { + additionalProperties: true, + optionalProperties: { + branches: { + elements: { + additionalProperties: true, + optionalProperties: { + branches: { + elements: { + additionalProperties: true, + optionalProperties: { + product: { + additionalProperties: true, + optionalProperties: { + product_id: { type: 'string' }, + product_identification_helper: productIdentificationHelperSchema, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + full_product_names: { + elements: { + additionalProperties: true, + optionalProperties: { + product_id: { type: 'string' }, + product_identification_helper: productIdentificationHelperSchema, + }, + }, + }, + relationships: relationshipSchema, + }, + }, + }, +}) + +const validateInput = ajv.compile(inputSchema) + + +/** + * This implements the optional test 6.2.31 of the CSAF 2.1 standard. + * @param {any} doc + */ +export function optionalTest_6_2_31(doc) { + const ctx = { + warnings: + /** @type {Array<{ instancePath: string; message: string }>} */ ([]), + } + + if (!validateInput(doc)) { + return ctx + } + + const relationships = Array.isArray(doc.product_tree?.relationships) + ? doc.product_tree.relationships + : [] + + // Start the recursive check from the root branches + checkBranches(doc.product_tree?.branches ?? [], relationships, ctx) + + checkFullProductNames(doc.product_tree?.full_product_names ?? [], relationships, ctx) + + return ctx +} + +/** + * Check full_product_names for serial_numbers or model_numbers + * @param {Array} full_product_names + * @param {any} relationships + * @param {{ warnings: Array<{ instancePath: string; message: string }> }} ctx + */ +function checkFullProductNames(full_product_names, relationships, ctx) { + full_product_names.forEach((fullProductName, index) => { + if ( + fullProductName?.product_id && + fullProductName?.product_identification_helper + ) { + const { serial_numbers, model_numbers } = + fullProductName.product_identification_helper + + if ( + (serial_numbers?.length || model_numbers?.length) && + !hasRelationship(relationships, fullProductName.product_id) + ) { + ctx.warnings.push({ + instancePath: `/product_tree/full_product_names/${index}`, + message: `missing relationship: Product with serial or model number must be referenced.`, + }) + } + } + }) +} + + +/** + * Recursive function to check branches for products with serial_numbers or model_numbers + * but no corresponding relationship. + * @param {Array} branches - The current level of branches to process. + * @param {any} relationships - The relationships array to check against. + * @param {{ warnings: Array<{ instancePath: string; message: string }> }} ctx - The context to store warnings. + * @param {string} [path='/product_tree/branches'] - The current JSON path. + */ +function checkBranches(branches, relationships, ctx, path = '/product_tree/branches') { + branches?.forEach((branch, branchIndex) => { + const currentPath = `${path}/${branchIndex}` + const product = branch.product; + + if (product?.product_id && product?.product_identification_helper) { + const { serial_numbers, model_numbers } = + product.product_identification_helper + + if ( + (serial_numbers?.length || model_numbers?.length) && + !hasRelationship(relationships, product.product_id) + ) { + ctx.warnings.push({ + instancePath: `${currentPath}/product`, + message: `missing relationship: Product with serial or model number must be referenced.`, + }) + } + } + + // Recursively check nested branches + if (Array.isArray(branch.branches)) { + checkBranches(branch.branches, relationships, ctx, `${currentPath}/branches`) + } + }) +} + +/** + * Helper function to check if a product_id exists in relationships + * @param relationships {Array<{ product_reference: string; relates_to_product_reference: string }>} + * @param productId {string} + * @returns {boolean} + */ +function hasRelationship(relationships, productId) { + return relationships.some( + (rel) => + rel.product_reference === productId || + rel.relates_to_product_reference === productId + ) +} diff --git a/tests/csaf_2_1/oasis.js b/tests/csaf_2_1/oasis.js index 32fdcaf9..5ab01dd8 100644 --- a/tests/csaf_2_1/oasis.js +++ b/tests/csaf_2_1/oasis.js @@ -38,7 +38,6 @@ const excluded = [ '6.2.25', '6.2.26', '6.2.30', - '6.2.31', '6.2.32', '6.2.33', '6.2.34', From 908ab0dc7bcca5b8f6649a33409e429d2b991fa8 Mon Sep 17 00:00:00 2001 From: bendo-eXX Date: Fri, 13 Jun 2025 16:41:59 +0200 Subject: [PATCH 02/12] feat: rename Test to recommendedTest_6_2_30.js --- csaf_2_1/optionalTests.js | 0 csaf_2_1/recommendedTests.js | 1 + ...st_6_2_31.js => recommendedTest_6_2_31.js} | 23 +++++++++++-------- 3 files changed, 15 insertions(+), 9 deletions(-) delete mode 100644 csaf_2_1/optionalTests.js rename csaf_2_1/recommendedTests/{optionalTest_6_2_31.js => recommendedTest_6_2_31.js} (89%) diff --git a/csaf_2_1/optionalTests.js b/csaf_2_1/optionalTests.js deleted file mode 100644 index e69de29b..00000000 diff --git a/csaf_2_1/recommendedTests.js b/csaf_2_1/recommendedTests.js index a39c6673..591cd116 100644 --- a/csaf_2_1/recommendedTests.js +++ b/csaf_2_1/recommendedTests.js @@ -31,4 +31,5 @@ export { recommendedTest_6_2_23 } from './recommendedTests/recommendedTest_6_2_2 export { recommendedTest_6_2_27 } from './recommendedTests/recommendedTest_6_2_27.js' 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_31 } from './recommendedTests/recommendedTest_6_2_31.js' export { recommendedTest_6_2_38 } from './recommendedTests/recommendedTest_6_2_38.js' diff --git a/csaf_2_1/recommendedTests/optionalTest_6_2_31.js b/csaf_2_1/recommendedTests/recommendedTest_6_2_31.js similarity index 89% rename from csaf_2_1/recommendedTests/optionalTest_6_2_31.js rename to csaf_2_1/recommendedTests/recommendedTest_6_2_31.js index c17b11ab..08093bc5 100644 --- a/csaf_2_1/recommendedTests/optionalTest_6_2_31.js +++ b/csaf_2_1/recommendedTests/recommendedTest_6_2_31.js @@ -17,7 +17,7 @@ const productIdentificationHelperSchema = { const relationshipSchema = { elements: { additionalProperties: true, - optionalProperties: { + properties: { product_reference: { type: 'string' }, relates_to_product_reference: { type: 'string' }, }, @@ -44,8 +44,10 @@ const inputSchema = /** @type {const} */ ({ optionalProperties: { product: { additionalProperties: true, - optionalProperties: { + properties: { product_id: { type: 'string' }, + }, + optionalProperties: { product_identification_helper: productIdentificationHelperSchema, }, }, @@ -61,8 +63,10 @@ const inputSchema = /** @type {const} */ ({ full_product_names: { elements: { additionalProperties: true, - optionalProperties: { + properties: { product_id: { type: 'string' }, + }, + optionalProperties: { product_identification_helper: productIdentificationHelperSchema, }, }, @@ -80,7 +84,7 @@ const validateInput = ajv.compile(inputSchema) * This implements the optional test 6.2.31 of the CSAF 2.1 standard. * @param {any} doc */ -export function optionalTest_6_2_31(doc) { +export function recommendedTest_6_2_31(doc) { const ctx = { warnings: /** @type {Array<{ instancePath: string; message: string }>} */ ([]), @@ -90,14 +94,15 @@ export function optionalTest_6_2_31(doc) { return ctx } - const relationships = Array.isArray(doc.product_tree?.relationships) - ? doc.product_tree.relationships + const safeDoc = /** @type {any} */ (doc); + const relationships = Array.isArray(safeDoc.product_tree?.relationships) + ? safeDoc.product_tree.relationships : [] // Start the recursive check from the root branches - checkBranches(doc.product_tree?.branches ?? [], relationships, ctx) + checkBranches(safeDoc.product_tree?.branches ?? [], relationships, ctx) - checkFullProductNames(doc.product_tree?.full_product_names ?? [], relationships, ctx) + checkFullProductNames(safeDoc.product_tree?.full_product_names ?? [], relationships, ctx) return ctx } @@ -135,7 +140,7 @@ function checkFullProductNames(full_product_names, relationships, ctx) { * Recursive function to check branches for products with serial_numbers or model_numbers * but no corresponding relationship. * @param {Array} branches - The current level of branches to process. - * @param {any} relationships - The relationships array to check against. + * @param {Array} relationships - The relationships array to check against. * @param {{ warnings: Array<{ instancePath: string; message: string }> }} ctx - The context to store warnings. * @param {string} [path='/product_tree/branches'] - The current JSON path. */ From 1178c2c59e8c2e0da9e883d9f61e52f1254cb1cd Mon Sep 17 00:00:00 2001 From: bendo-eXX Date: Thu, 26 Jun 2025 16:58:26 +0200 Subject: [PATCH 03/12] feat: remove unknown doc typ --- README.md | 2 - .../recommendedTest_6_2_31.js | 167 ++++++++++-------- tests/csaf_2_1/recommendedTest_6_2_31.js | 11 ++ 3 files changed, 104 insertions(+), 76 deletions(-) create mode 100644 tests/csaf_2_1/recommendedTest_6_2_31.js diff --git a/README.md b/README.md index ed6bd63f..a2f285a6 100644 --- a/README.md +++ b/README.md @@ -340,7 +340,6 @@ The following tests are not yet implemented and therefore missing: - Recommended Test 6.2.28 - Recommended Test 6.2.29 - Recommended Test 6.2.30 -- Recommended Test 6.2.31 - Recommended Test 6.2.32 - Recommended Test 6.2.33 - Recommended Test 6.2.34 @@ -460,7 +459,6 @@ export const recommendedTest_6_2_16: DocumentTest export const recommendedTest_6_2_17: DocumentTest export const recommendedTest_6_2_18: DocumentTest export const recommendedTest_6_2_22: DocumentTest -export const recommendedTest_6_2_23: DocumentTest ``` [(back to top)](#bsi-csaf-validator-lib) diff --git a/csaf_2_1/recommendedTests/recommendedTest_6_2_31.js b/csaf_2_1/recommendedTests/recommendedTest_6_2_31.js index 08093bc5..8f4a9b89 100644 --- a/csaf_2_1/recommendedTests/recommendedTest_6_2_31.js +++ b/csaf_2_1/recommendedTests/recommendedTest_6_2_31.js @@ -2,7 +2,39 @@ import Ajv from 'ajv/dist/jtd.js' const ajv = new Ajv() -const productIdentificationHelperSchema = { +/** + * @typedef {object} ProductIdentificationHelper + * @property {string[]} [serial_numbers] + * @property {string[]} [model_numbers] + */ + +/** + * @typedef {object} Product + * @property {string} product_id + * @property {ProductIdentificationHelper} [product_identification_helper] + */ + +/** + * @typedef {object} Branch + * @property {Product} [product] + * @property {Branch[]} [branches] + */ + +/** + * @typedef {Object} Relationship + * @property {string} product_reference + * @property {string} relates_to_product_reference + */ + +const relationshipSchema = /** @type {const} */ ({ + additionalProperties: true, + properties: { + product_reference: { type: 'string' }, + relates_to_product_reference: { type: 'string' }, + }, +}) + +const productIdentificationHelperSchema = /** @type {const} */ ({ additionalProperties: true, optionalProperties: { serial_numbers: { @@ -12,17 +44,24 @@ const productIdentificationHelperSchema = { elements: { type: 'string' }, }, }, -}; - -const relationshipSchema = { - elements: { - additionalProperties: true, - properties: { - product_reference: { type: 'string' }, - relates_to_product_reference: { type: 'string' }, - }, +}) + +const productSchema = /** @type {const} */ ({ + additionalProperties: true, + properties: { + product_id: { type: 'string' }, }, -}; + optionalProperties: { + product_identification_helper: productIdentificationHelperSchema, + }, +}) + +const branchSchema = /** @type {const} */ ({ + additionalProperties: true, + optionalProperties: { + product: productSchema, + }, +}) const inputSchema = /** @type {const} */ ({ additionalProperties: true, @@ -31,47 +70,14 @@ const inputSchema = /** @type {const} */ ({ additionalProperties: true, optionalProperties: { branches: { - elements: { - additionalProperties: true, - optionalProperties: { - branches: { - elements: { - additionalProperties: true, - optionalProperties: { - branches: { - elements: { - additionalProperties: true, - optionalProperties: { - product: { - additionalProperties: true, - properties: { - product_id: { type: 'string' }, - }, - optionalProperties: { - product_identification_helper: productIdentificationHelperSchema, - }, - }, - }, - }, - }, - }, - }, - }, - }, - }, + elements: branchSchema, }, full_product_names: { - elements: { - additionalProperties: true, - properties: { - product_id: { type: 'string' }, - }, - optionalProperties: { - product_identification_helper: productIdentificationHelperSchema, - }, - }, + elements: productSchema, + }, + relationships: { + elements: relationshipSchema, }, - relationships: relationshipSchema, }, }, }, @@ -79,7 +85,6 @@ const inputSchema = /** @type {const} */ ({ const validateInput = ajv.compile(inputSchema) - /** * This implements the optional test 6.2.31 of the CSAF 2.1 standard. * @param {any} doc @@ -94,64 +99,72 @@ export function recommendedTest_6_2_31(doc) { return ctx } - const safeDoc = /** @type {any} */ (doc); - const relationships = Array.isArray(safeDoc.product_tree?.relationships) - ? safeDoc.product_tree.relationships + const relationships = Array.isArray(doc.product_tree?.relationships) + ? doc.product_tree.relationships : [] // Start the recursive check from the root branches - checkBranches(safeDoc.product_tree?.branches ?? [], relationships, ctx) + checkBranches(doc.product_tree?.branches ?? [], relationships, ctx) - checkFullProductNames(safeDoc.product_tree?.full_product_names ?? [], relationships, ctx) + checkFullProductNames( + doc.product_tree?.full_product_names ?? [], + relationships, + ctx + ) return ctx } /** * Check full_product_names for serial_numbers or model_numbers - * @param {Array} full_product_names - * @param {any} relationships + * @param {Product[]} full_product_names + * @param {Array} relationships * @param {{ warnings: Array<{ instancePath: string; message: string }> }} ctx */ function checkFullProductNames(full_product_names, relationships, ctx) { full_product_names.forEach((fullProductName, index) => { if ( - fullProductName?.product_id && - fullProductName?.product_identification_helper + fullProductName?.product_id && + fullProductName?.product_identification_helper ) { const { serial_numbers, model_numbers } = - fullProductName.product_identification_helper + fullProductName.product_identification_helper if ( - (serial_numbers?.length || model_numbers?.length) && - !hasRelationship(relationships, fullProductName.product_id) + (serial_numbers?.length || model_numbers?.length) && + !hasRelationship(relationships, fullProductName.product_id) ) { ctx.warnings.push({ instancePath: `/product_tree/full_product_names/${index}`, - message: `missing relationship: Product with serial or model number must be referenced.`, + message: + 'missing relationship: Product with serial or model number must be referenced.', }) } } }) } - /** * Recursive function to check branches for products with serial_numbers or model_numbers * but no corresponding relationship. - * @param {Array} branches - The current level of branches to process. - * @param {Array} relationships - The relationships array to check against. + * @param {Array} branches - The current level of branches to process. + * @param {Array} relationships - The relationships array to check against. * @param {{ warnings: Array<{ instancePath: string; message: string }> }} ctx - The context to store warnings. * @param {string} [path='/product_tree/branches'] - The current JSON path. */ -function checkBranches(branches, relationships, ctx, path = '/product_tree/branches') { +function checkBranches( + branches, + relationships, + ctx, + path = '/product_tree/branches' +) { branches?.forEach((branch, branchIndex) => { const currentPath = `${path}/${branchIndex}` - const product = branch.product; + const product = branch.product - if (product?.product_id && product?.product_identification_helper) { + if (product?.product_id && product.product_identification_helper) { const { serial_numbers, model_numbers } = - product.product_identification_helper + product.product_identification_helper if ( (serial_numbers?.length || model_numbers?.length) && @@ -159,22 +172,28 @@ function checkBranches(branches, relationships, ctx, path = '/product_tree/branc ) { ctx.warnings.push({ instancePath: `${currentPath}/product`, - message: `missing relationship: Product with serial or model number must be referenced.`, + message: + 'missing relationship: Product with serial or model number must be referenced', }) } } // Recursively check nested branches if (Array.isArray(branch.branches)) { - checkBranches(branch.branches, relationships, ctx, `${currentPath}/branches`) + checkBranches( + branch.branches, + relationships, + ctx, + `${currentPath}/branches` + ) } }) } /** * Helper function to check if a product_id exists in relationships - * @param relationships {Array<{ product_reference: string; relates_to_product_reference: string }>} - * @param productId {string} + * @param {Array} relationships + * @param {string} productId * @returns {boolean} */ function hasRelationship(relationships, productId) { diff --git a/tests/csaf_2_1/recommendedTest_6_2_31.js b/tests/csaf_2_1/recommendedTest_6_2_31.js new file mode 100644 index 00000000..8f2833f0 --- /dev/null +++ b/tests/csaf_2_1/recommendedTest_6_2_31.js @@ -0,0 +1,11 @@ +import assert from 'node:assert' +import { recommendedTest_6_2_31 } from '../../csaf_2_1/recommendedTests.js' + +describe('recommendedTest_6_2_31', function () { + it('only runs on relevant documents', function () { + assert.equal( + recommendedTest_6_2_31({ vulnerabilities: 'mydoc' }).warnings.length, + 0 + ) + }) +}) From a0d00a88fc4fdebb10cbccd0617ae97f41e43e03 Mon Sep 17 00:00:00 2001 From: bendo-eXX Date: Wed, 16 Jul 2025 08:53:53 +0200 Subject: [PATCH 04/12] refactor(CSAF2.1): refactor typedef --- .../recommendedTest_6_2_31.js | 33 +++++-------------- 1 file changed, 9 insertions(+), 24 deletions(-) diff --git a/csaf_2_1/recommendedTests/recommendedTest_6_2_31.js b/csaf_2_1/recommendedTests/recommendedTest_6_2_31.js index 8f4a9b89..b807c145 100644 --- a/csaf_2_1/recommendedTests/recommendedTest_6_2_31.js +++ b/csaf_2_1/recommendedTests/recommendedTest_6_2_31.js @@ -2,30 +2,6 @@ import Ajv from 'ajv/dist/jtd.js' const ajv = new Ajv() -/** - * @typedef {object} ProductIdentificationHelper - * @property {string[]} [serial_numbers] - * @property {string[]} [model_numbers] - */ - -/** - * @typedef {object} Product - * @property {string} product_id - * @property {ProductIdentificationHelper} [product_identification_helper] - */ - -/** - * @typedef {object} Branch - * @property {Product} [product] - * @property {Branch[]} [branches] - */ - -/** - * @typedef {Object} Relationship - * @property {string} product_reference - * @property {string} relates_to_product_reference - */ - const relationshipSchema = /** @type {const} */ ({ additionalProperties: true, properties: { @@ -85,6 +61,15 @@ const inputSchema = /** @type {const} */ ({ const validateInput = ajv.compile(inputSchema) +/** + * @typedef {import('ajv/dist/core').JTDDataType} Relationship + * @typedef {import('ajv/dist/core').JTDDataType} ProductIdentificationHelper + * @typedef {import('ajv/dist/core').JTDDataType} Product + * @typedef {import('ajv/dist/core').JTDDataType} FullProductName + * @typedef {import('ajv/dist/core').JTDDataType} Branch + * + */ + /** * This implements the optional test 6.2.31 of the CSAF 2.1 standard. * @param {any} doc From 8558d6b5fd8cd9216b2b9db78a8fddf2ea62411f Mon Sep 17 00:00:00 2001 From: bendo-eXX Date: Tue, 29 Jul 2025 16:18:24 +0200 Subject: [PATCH 05/12] feat(CSAF2.1): added tests to recommendedTest_6_2_31.js --- .../recommendedTest_6_2_31.js | 6 +- tests/csaf_2_1/recommendedTest_6_2_31.js | 92 +++++++++++++++++++ 2 files changed, 94 insertions(+), 4 deletions(-) diff --git a/csaf_2_1/recommendedTests/recommendedTest_6_2_31.js b/csaf_2_1/recommendedTests/recommendedTest_6_2_31.js index b807c145..25ae2d88 100644 --- a/csaf_2_1/recommendedTests/recommendedTest_6_2_31.js +++ b/csaf_2_1/recommendedTests/recommendedTest_6_2_31.js @@ -4,7 +4,7 @@ const ajv = new Ajv() const relationshipSchema = /** @type {const} */ ({ additionalProperties: true, - properties: { + optionalProperties: { product_reference: { type: 'string' }, relates_to_product_reference: { type: 'string' }, }, @@ -24,10 +24,8 @@ const productIdentificationHelperSchema = /** @type {const} */ ({ const productSchema = /** @type {const} */ ({ additionalProperties: true, - properties: { - product_id: { type: 'string' }, - }, optionalProperties: { + product_id: { type: 'string' }, product_identification_helper: productIdentificationHelperSchema, }, }) diff --git a/tests/csaf_2_1/recommendedTest_6_2_31.js b/tests/csaf_2_1/recommendedTest_6_2_31.js index 8f2833f0..9bd1ab57 100644 --- a/tests/csaf_2_1/recommendedTest_6_2_31.js +++ b/tests/csaf_2_1/recommendedTest_6_2_31.js @@ -8,4 +8,96 @@ describe('recommendedTest_6_2_31', function () { 0 ) }) + + it('test input schema without branches', function () { + assert.equal( + recommendedTest_6_2_31({ + document: {}, + product_tree: { + full_product_names: [ + { + name: 'Example Company Controller A 1.0', + product_id: 'CSAFPID-908070601', + product_identification_helper: { + serial_numbers: ['143-D-354'], + }, + }, + ], + }, + }).warnings.length, + 1 + ) + }) + + it('test input schema without full_product_names', function () { + assert.equal( + recommendedTest_6_2_31({ + document: {}, + product_tree: { + full_product_names: [], + branches: [ + { + category: 'product_version', + name: '1.0', + product: { + name: 'Example Company Controller A 1.0', + product_id: 'CSAFPID-908070601', + product_identification_helper: { + serial_numbers: ['143-D-354'], + }, + }, + }, + ], + }, + }).warnings.length, + 1 + ) + }) + + it('test branches without serial_numbers or model_numbers', function () { + assert.equal( + recommendedTest_6_2_31({ + document: {}, + product_tree: { + full_product_names: [], + branches: [ + { + category: 'product_version', + name: '1.0', + product: { + name: 'Example Company Controller A 1.0', + product_id: 'CSAFPID-908070601', + product_identification_helper: { + serial_numbers: [], + model_numbers: [], + }, + }, + }, + ], + }, + }).warnings.length, + 0 + ) + }) + + it('test full_product_names without serial_numbers or model_numbers', function () { + assert.equal( + recommendedTest_6_2_31({ + document: {}, + product_tree: { + full_product_names: [ + { + name: 'Example Company Controller A 1.0', + product_id: 'CSAFPID-908070601', + product_identification_helper: { + serial_numbers: [], + model_numbers: [], + }, + }, + ], + }, + }).warnings.length, + 0 + ) + }) }) From 1d38f95f5b5eae9bd3b3af1385e4306092863879 Mon Sep 17 00:00:00 2001 From: bendo-eXX Date: Wed, 20 Aug 2025 16:05:11 +0200 Subject: [PATCH 06/12] feat(CSAF2.1): change massage Co-authored-by: tschmidtb51 <65305130+tschmidtb51@users.noreply.github.com> --- csaf_2_1/recommendedTests/recommendedTest_6_2_31.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/csaf_2_1/recommendedTests/recommendedTest_6_2_31.js b/csaf_2_1/recommendedTests/recommendedTest_6_2_31.js index 25ae2d88..d8f23365 100644 --- a/csaf_2_1/recommendedTests/recommendedTest_6_2_31.js +++ b/csaf_2_1/recommendedTests/recommendedTest_6_2_31.js @@ -120,7 +120,7 @@ function checkFullProductNames(full_product_names, relationships, ctx) { ctx.warnings.push({ instancePath: `/product_tree/full_product_names/${index}`, message: - 'missing relationship: Product with serial or model number must be referenced.', + 'missing relationship: Product with serial number or model number must be referenced.', }) } } From a749d1a4f40478bb98686a712c70e298e9a75e3d Mon Sep 17 00:00:00 2001 From: bendo-eXX Date: Wed, 20 Aug 2025 16:06:01 +0200 Subject: [PATCH 07/12] feat(CSAF2.1): change massage Co-authored-by: tschmidtb51 <65305130+tschmidtb51@users.noreply.github.com> --- csaf_2_1/recommendedTests/recommendedTest_6_2_31.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/csaf_2_1/recommendedTests/recommendedTest_6_2_31.js b/csaf_2_1/recommendedTests/recommendedTest_6_2_31.js index d8f23365..832d930a 100644 --- a/csaf_2_1/recommendedTests/recommendedTest_6_2_31.js +++ b/csaf_2_1/recommendedTests/recommendedTest_6_2_31.js @@ -156,7 +156,7 @@ function checkBranches( ctx.warnings.push({ instancePath: `${currentPath}/product`, message: - 'missing relationship: Product with serial or model number must be referenced', + 'missing relationship: Product with serial number or model number must be referenced', }) } } From d81820303b57c706ce5fb8bb93c2aded2fe44796 Mon Sep 17 00:00:00 2001 From: bendo-eXX Date: Tue, 26 Aug 2025 13:15:49 +0200 Subject: [PATCH 08/12] feat(CSAF2.1): change branch schema --- .../recommendedTest_6_2_31.js | 49 ++++-- tests/csaf_2_1/recommendedTest_6_2_31.js | 149 ++++++++++++++++++ 2 files changed, 182 insertions(+), 16 deletions(-) diff --git a/csaf_2_1/recommendedTests/recommendedTest_6_2_31.js b/csaf_2_1/recommendedTests/recommendedTest_6_2_31.js index 832d930a..f610926b 100644 --- a/csaf_2_1/recommendedTests/recommendedTest_6_2_31.js +++ b/csaf_2_1/recommendedTests/recommendedTest_6_2_31.js @@ -34,6 +34,12 @@ const branchSchema = /** @type {const} */ ({ additionalProperties: true, optionalProperties: { product: productSchema, + branches: { + elements: { + additionalProperties: true, + properties: {}, + }, + }, }, }) @@ -58,19 +64,17 @@ const inputSchema = /** @type {const} */ ({ }) const validateInput = ajv.compile(inputSchema) +const validateBranch = ajv.compile(branchSchema) /** * @typedef {import('ajv/dist/core').JTDDataType} Relationship - * @typedef {import('ajv/dist/core').JTDDataType} ProductIdentificationHelper - * @typedef {import('ajv/dist/core').JTDDataType} Product * @typedef {import('ajv/dist/core').JTDDataType} FullProductName * @typedef {import('ajv/dist/core').JTDDataType} Branch - * */ /** * This implements the optional test 6.2.31 of the CSAF 2.1 standard. - * @param {any} doc + * @param {unknown} doc */ export function recommendedTest_6_2_31(doc) { const ctx = { @@ -100,8 +104,8 @@ export function recommendedTest_6_2_31(doc) { /** * Check full_product_names for serial_numbers or model_numbers - * @param {Product[]} full_product_names - * @param {Array} relationships + * @param {FullProductName[]} full_product_names + * @param {Relationship[]} relationships * @param {{ warnings: Array<{ instancePath: string; message: string }> }} ctx */ function checkFullProductNames(full_product_names, relationships, ctx) { @@ -115,7 +119,7 @@ function checkFullProductNames(full_product_names, relationships, ctx) { if ( (serial_numbers?.length || model_numbers?.length) && - !hasRelationship(relationships, fullProductName.product_id) + !checkRelationship(relationships, fullProductName.product_id) ) { ctx.warnings.push({ instancePath: `/product_tree/full_product_names/${index}`, @@ -130,8 +134,8 @@ function checkFullProductNames(full_product_names, relationships, ctx) { /** * Recursive function to check branches for products with serial_numbers or model_numbers * but no corresponding relationship. - * @param {Array} branches - The current level of branches to process. - * @param {Array} relationships - The relationships array to check against. + * @param {Branch[]} branches - The current level of branches to process. + * @param {Relationship[]} relationships - The relationships array to check against. * @param {{ warnings: Array<{ instancePath: string; message: string }> }} ctx - The context to store warnings. * @param {string} [path='/product_tree/branches'] - The current JSON path. */ @@ -142,6 +146,9 @@ function checkBranches( path = '/product_tree/branches' ) { branches?.forEach((branch, branchIndex) => { + // Skip invalid branches + if (!validateBranch(branch)) return + const currentPath = `${path}/${branchIndex}` const product = branch.product @@ -151,7 +158,7 @@ function checkBranches( if ( (serial_numbers?.length || model_numbers?.length) && - !hasRelationship(relationships, product.product_id) + !checkRelationship(relationships, product.product_id) ) { ctx.warnings.push({ instancePath: `${currentPath}/product`, @@ -174,15 +181,25 @@ function checkBranches( } /** - * Helper function to check if a product_id exists in relationships - * @param {Array} relationships + * Check if there is a valid relationship for the given productId. + * @param {Relationship[]} relationships * @param {string} productId * @returns {boolean} */ -function hasRelationship(relationships, productId) { - return relationships.some( - (rel) => +function checkRelationship(relationships, productId) { + return relationships.some((rel) => { + if (!rel.product_reference || !rel.relates_to_product_reference) { + return false + } + + // Check for self-referencing relationships + if (rel.product_reference === rel.relates_to_product_reference) { + return false + } + // Check if the productId matches either reference + return ( rel.product_reference === productId || rel.relates_to_product_reference === productId - ) + ) + }) } diff --git a/tests/csaf_2_1/recommendedTest_6_2_31.js b/tests/csaf_2_1/recommendedTest_6_2_31.js index 9bd1ab57..172b7618 100644 --- a/tests/csaf_2_1/recommendedTest_6_2_31.js +++ b/tests/csaf_2_1/recommendedTest_6_2_31.js @@ -100,4 +100,153 @@ describe('recommendedTest_6_2_31', function () { 0 ) }) + + it('test nested branches 5 levels deep', function () { + assert.equal( + recommendedTest_6_2_31({ + document: {}, + product_tree: { + branches: [ + { + category: 'vendor', + name: 'Example Company', + branches: [ + { + category: 'product_family', + name: 'Controller Series', + branches: [ + { + category: 'product_name', + name: 'Controller A', + branches: [ + { + category: 'product_version', + name: '1.0', + branches: [ + { + category: 'architecture', + name: 'x86', + product: { + name: 'Example Company Controller A 1.0 x86', + product_id: 'CSAFPID-908070601', + product_identification_helper: { + serial_numbers: ['143-D-354'], + }, + }, + }, + ], + }, + ], + }, + ], + }, + ], + }, + ], + }, + }).warnings.length, + 1 + ) + }) + + it('test relationship with the same ID for product_reference and relates_to_product_reference', function () { + assert.equal( + recommendedTest_6_2_31({ + document: {}, + product_tree: { + full_product_names: [ + { + name: 'Example Company Controller A 1.0', + product_id: 'CSAFPID-908070601', + product_identification_helper: { + serial_numbers: ['143-D-354'], + }, + }, + { + name: 'Microsoft Windows', + product_id: 'CSAFPID-908070602', + }, + ], + relationships: [ + { + product_reference: 'CSAFPID-908070602', + relates_to_product_reference: 'CSAFPID-908070602', + }, + ], + }, + }).warnings.length, + 1 + ) + }) + + it('test relationship with product_reference only', function () { + assert.equal( + recommendedTest_6_2_31({ + document: {}, + product_tree: { + branches: [ + { + category: 'product_version', + name: '1.0', + product: { + name: 'Example Company Controller A 1.0', + product_id: 'CSAFPID-908070601', + product_identification_helper: { + model_numbers: ['CA-1000'], + }, + }, + }, + ], + relationships: [ + { + product_reference: 'CSAFPID-908070601', + }, + ], + }, + }).warnings.length, + 1 + ) + }) + + it('skips invalid branches and processes valid ones', function () { + assert.equal( + recommendedTest_6_2_31({ + document: {}, + product_tree: { + branches: [ + { + category: 'vendor', + name: 'Example Company', + product: { + product_id: 'CSAFPID-908070601', + product_identification_helper: { + serial_numbers: ['143-D-354'], + }, + }, + branches: [ + { + product: 'invalid', + }, + { + branches: [{}], + }, + { + category: 'product_version', + name: '1.0', + product: { + name: 'Example Company Controller A 1.0', + product_id: 'CSAFPID-908070602', + product_identification_helper: { + model_numbers: ['CA-1000'], + }, + }, + }, + ], + }, + ], + }, + }).warnings.length, + 2 + ) + }) }) From 2dd8ae2524739d67cffcd4cf3c7f4176728b0bd3 Mon Sep 17 00:00:00 2001 From: bendo-eXX Date: Fri, 27 Feb 2026 21:08:50 +0100 Subject: [PATCH 09/12] feat: add check for relationships/full_product_name --- .../recommendedTest_6_2_31.js | 86 +++++++++++++------ tests/csaf_2_1/recommendedTest_6_2_31.js | 72 ++++++++++++++++ 2 files changed, 132 insertions(+), 26 deletions(-) diff --git a/csaf_2_1/recommendedTests/recommendedTest_6_2_31.js b/csaf_2_1/recommendedTests/recommendedTest_6_2_31.js index f610926b..93215778 100644 --- a/csaf_2_1/recommendedTests/recommendedTest_6_2_31.js +++ b/csaf_2_1/recommendedTests/recommendedTest_6_2_31.js @@ -2,14 +2,6 @@ import Ajv from 'ajv/dist/jtd.js' const ajv = new Ajv() -const relationshipSchema = /** @type {const} */ ({ - additionalProperties: true, - optionalProperties: { - product_reference: { type: 'string' }, - relates_to_product_reference: { type: 'string' }, - }, -}) - const productIdentificationHelperSchema = /** @type {const} */ ({ additionalProperties: true, optionalProperties: { @@ -30,6 +22,15 @@ const productSchema = /** @type {const} */ ({ }, }) +const relationshipSchema = /** @type {const} */ ({ + additionalProperties: true, + optionalProperties: { + full_product_name: productSchema, + product_reference: { type: 'string' }, + relates_to_product_reference: { type: 'string' }, + }, +}) + const branchSchema = /** @type {const} */ ({ additionalProperties: true, optionalProperties: { @@ -37,6 +38,9 @@ const branchSchema = /** @type {const} */ ({ branches: { elements: { additionalProperties: true, + // AJV's JTD does not support recursive schemas. + // Nested branches are validated at runtime in checkBranches() by calling + // validateBranch() on each child branch individually during the recursive traversal. properties: {}, }, }, @@ -67,9 +71,9 @@ const validateInput = ajv.compile(inputSchema) const validateBranch = ajv.compile(branchSchema) /** - * @typedef {import('ajv/dist/core').JTDDataType} Relationship - * @typedef {import('ajv/dist/core').JTDDataType} FullProductName * @typedef {import('ajv/dist/core').JTDDataType} Branch + * @typedef {import('ajv/dist/core').JTDDataType} FullProductName + * @typedef {import('ajv/dist/core').JTDDataType} Relationship */ /** @@ -99,6 +103,17 @@ export function recommendedTest_6_2_31(doc) { ctx ) + relationships.forEach((rel, index) => { + if (rel?.full_product_name) { + checkFullProductNames( + [rel.full_product_name], + relationships, + ctx, + `/product_tree/relationships/${index}/full_product_name` + ) + } + }) + return ctx } @@ -107,13 +122,24 @@ export function recommendedTest_6_2_31(doc) { * @param {FullProductName[]} full_product_names * @param {Relationship[]} relationships * @param {{ warnings: Array<{ instancePath: string; message: string }> }} ctx + * @param {string} [basePath='/product_tree/full_product_names'] - The base JSON path for warnings */ -function checkFullProductNames(full_product_names, relationships, ctx) { +function checkFullProductNames( + full_product_names, + relationships, + ctx, + basePath = '/product_tree/full_product_names' +) { full_product_names.forEach((fullProductName, index) => { - if ( - fullProductName?.product_id && - fullProductName?.product_identification_helper - ) { + if (fullProductName?.product_identification_helper) { + if (!fullProductName.product_id) { + ctx.warnings.push({ + instancePath: `${basePath}/${index}`, + message: + 'missing product_id: full product name cannot be referenced without a product id.', + }) + return + } const { serial_numbers, model_numbers } = fullProductName.product_identification_helper @@ -122,9 +148,9 @@ function checkFullProductNames(full_product_names, relationships, ctx) { !checkRelationship(relationships, fullProductName.product_id) ) { ctx.warnings.push({ - instancePath: `/product_tree/full_product_names/${index}`, + instancePath: `${basePath}/${index}`, message: - 'missing relationship: Product with serial number or model number must be referenced.', + 'missing relationship: product with serial number or model number should be referenced.', }) } } @@ -152,19 +178,27 @@ function checkBranches( const currentPath = `${path}/${branchIndex}` const product = branch.product - if (product?.product_id && product.product_identification_helper) { - const { serial_numbers, model_numbers } = - product.product_identification_helper - - if ( - (serial_numbers?.length || model_numbers?.length) && - !checkRelationship(relationships, product.product_id) - ) { + if (product) { + if (!product?.product_id) { ctx.warnings.push({ instancePath: `${currentPath}/product`, message: - 'missing relationship: Product with serial number or model number must be referenced', + 'missing product_id: product cannot be referenced without a product id.', }) + } else if (product.product_identification_helper) { + const { serial_numbers, model_numbers } = + product.product_identification_helper + + if ( + (serial_numbers?.length || model_numbers?.length) && + !checkRelationship(relationships, product.product_id) + ) { + ctx.warnings.push({ + instancePath: `${currentPath}/product`, + message: + 'missing relationship: product with serial number or model number should be referenced.', + }) + } } } diff --git a/tests/csaf_2_1/recommendedTest_6_2_31.js b/tests/csaf_2_1/recommendedTest_6_2_31.js index 172b7618..b339ad05 100644 --- a/tests/csaf_2_1/recommendedTest_6_2_31.js +++ b/tests/csaf_2_1/recommendedTest_6_2_31.js @@ -249,4 +249,76 @@ describe('recommendedTest_6_2_31', function () { 2 ) }) + + it('warns when branch product has no product_id', function () { + const result = recommendedTest_6_2_31({ + document: {}, + product_tree: { + branches: [ + { + category: 'product_version', + name: '1.0', + product: { + name: 'Example Company Controller A 1.0', + product_identification_helper: { + serial_numbers: ['143-D-354'], + }, + }, + }, + ], + }, + }) + assert.equal(result.warnings.length, 1) + assert.equal( + result.warnings[0].instancePath, + '/product_tree/branches/0/product' + ) + }) + + it('warns when full_product_names entry has no product_id', function () { + const result = recommendedTest_6_2_31({ + document: {}, + product_tree: { + full_product_names: [ + { + name: 'Example Company Controller A 1.0', + product_identification_helper: { + serial_numbers: ['143-D-354'], + }, + }, + ], + }, + }) + assert.equal(result.warnings.length, 1) + assert.equal( + result.warnings[0].instancePath, + '/product_tree/full_product_names/0' + ) + }) + + it('warns when relationship full_product_name has no product_id', function () { + const result = recommendedTest_6_2_31({ + document: {}, + product_tree: { + full_product_names: [], + relationships: [ + { + product_reference: 'CSAFPID-908070601', + relates_to_product_reference: 'CSAFPID-908070602', + full_product_name: { + name: 'Example Company Controller A 1.0 on Windows', + product_identification_helper: { + serial_numbers: ['143-D-354'], + }, + }, + }, + ], + }, + }) + assert.equal(result.warnings.length, 1) + assert.equal( + result.warnings[0].instancePath, + '/product_tree/relationships/0/full_product_name/0' + ) + }) }) From f7ba9993637215c0613ac92c6b36e641c77ab1bc Mon Sep 17 00:00:00 2001 From: bendo-eXX Date: Wed, 10 Jun 2026 11:29:56 +0200 Subject: [PATCH 10/12] feat(CSAF2.1): update test 6.2.31 to the new csaf 2.1 schema --- .../recommendedTest_6_2_31.js | 143 +++++----- tests/csaf_2_1/recommendedTest_6_2_31.js | 268 ++++++------------ 2 files changed, 156 insertions(+), 255 deletions(-) diff --git a/csaf_2_1/recommendedTests/recommendedTest_6_2_31.js b/csaf_2_1/recommendedTests/recommendedTest_6_2_31.js index 93215778..0bb3b4c4 100644 --- a/csaf_2_1/recommendedTests/recommendedTest_6_2_31.js +++ b/csaf_2_1/recommendedTests/recommendedTest_6_2_31.js @@ -1,4 +1,4 @@ -import Ajv from 'ajv/dist/jtd.js' +import { Ajv } from 'ajv/dist/jtd.js' const ajv = new Ajv() @@ -22,12 +22,21 @@ const productSchema = /** @type {const} */ ({ }, }) -const relationshipSchema = /** @type {const} */ ({ +const subpathSchema = /** @type {const} */ ({ additionalProperties: true, optionalProperties: { + next_product_reference: { type: 'string' }, + }, +}) + +const productPathSchema = /** @type {const} */ ({ + additionalProperties: true, + optionalProperties: { + beginning_product_reference: { type: 'string' }, full_product_name: productSchema, - product_reference: { type: 'string' }, - relates_to_product_reference: { type: 'string' }, + subpaths: { + elements: subpathSchema, + }, }, }) @@ -59,8 +68,8 @@ const inputSchema = /** @type {const} */ ({ full_product_names: { elements: productSchema, }, - relationships: { - elements: relationshipSchema, + product_paths: { + elements: productPathSchema, }, }, }, @@ -71,13 +80,13 @@ const validateInput = ajv.compile(inputSchema) const validateBranch = ajv.compile(branchSchema) /** - * @typedef {import('ajv/dist/core').JTDDataType} Branch - * @typedef {import('ajv/dist/core').JTDDataType} FullProductName - * @typedef {import('ajv/dist/core').JTDDataType} Relationship + * @typedef {import('ajv/dist/core.js').JTDDataType} Branch + * @typedef {import('ajv/dist/core.js').JTDDataType} FullProductName + * @typedef {import('ajv/dist/core.js').JTDDataType} ProductPath */ /** - * This implements the optional test 6.2.31 of the CSAF 2.1 standard. + * This implements the recommended test 6.2.31 of the CSAF 2.1 standard. * @param {unknown} doc */ export function recommendedTest_6_2_31(doc) { @@ -90,27 +99,33 @@ export function recommendedTest_6_2_31(doc) { return ctx } - const relationships = Array.isArray(doc.product_tree?.relationships) - ? doc.product_tree.relationships + /** @type {ProductPath[]} */ + const productPaths = Array.isArray(doc.product_tree?.product_paths) + ? doc.product_tree.product_paths : [] // Start the recursive check from the root branches - checkBranches(doc.product_tree?.branches ?? [], relationships, ctx) + checkBranches(doc.product_tree?.branches ?? [], productPaths, ctx) checkFullProductNames( doc.product_tree?.full_product_names ?? [], - relationships, + productPaths, ctx ) - relationships.forEach((rel, index) => { - if (rel?.full_product_name) { - checkFullProductNames( - [rel.full_product_name], - relationships, - ctx, - `/product_tree/relationships/${index}/full_product_name` - ) + productPaths.forEach((pp, index) => { + const fpn = pp?.full_product_name + if (!fpn?.product_identification_helper || !fpn.product_id) return + const { serial_numbers, model_numbers } = fpn.product_identification_helper + if ( + (serial_numbers?.length || model_numbers?.length) && + !checkProductPath(productPaths, fpn.product_id) + ) { + ctx.warnings.push({ + instancePath: `/product_tree/product_paths/${index}/full_product_name`, + message: + 'missing product path: product with serial number or model number should be referenced in a product path.', + }) } }) @@ -120,54 +135,45 @@ export function recommendedTest_6_2_31(doc) { /** * Check full_product_names for serial_numbers or model_numbers * @param {FullProductName[]} full_product_names - * @param {Relationship[]} relationships + * @param {ProductPath[]} productPaths * @param {{ warnings: Array<{ instancePath: string; message: string }> }} ctx * @param {string} [basePath='/product_tree/full_product_names'] - The base JSON path for warnings */ function checkFullProductNames( full_product_names, - relationships, + productPaths, ctx, basePath = '/product_tree/full_product_names' ) { full_product_names.forEach((fullProductName, index) => { - if (fullProductName?.product_identification_helper) { - if (!fullProductName.product_id) { - ctx.warnings.push({ - instancePath: `${basePath}/${index}`, - message: - 'missing product_id: full product name cannot be referenced without a product id.', - }) - return - } - const { serial_numbers, model_numbers } = - fullProductName.product_identification_helper - - if ( - (serial_numbers?.length || model_numbers?.length) && - !checkRelationship(relationships, fullProductName.product_id) - ) { - ctx.warnings.push({ - instancePath: `${basePath}/${index}`, - message: - 'missing relationship: product with serial number or model number should be referenced.', - }) - } + if (!fullProductName?.product_identification_helper || !fullProductName.product_id) return + const { serial_numbers, model_numbers } = + fullProductName.product_identification_helper + + if ( + (serial_numbers?.length || model_numbers?.length) && + !checkProductPath(productPaths, fullProductName.product_id) + ) { + ctx.warnings.push({ + instancePath: `${basePath}/${index}`, + message: + 'missing product path: product with serial number or model number should be referenced in a product path.', + }) } }) } /** * Recursive function to check branches for products with serial_numbers or model_numbers - * but no corresponding relationship. + * but no corresponding product path. * @param {Branch[]} branches - The current level of branches to process. - * @param {Relationship[]} relationships - The relationships array to check against. + * @param {ProductPath[]} productPaths - The product paths array to check against. * @param {{ warnings: Array<{ instancePath: string; message: string }> }} ctx - The context to store warnings. * @param {string} [path='/product_tree/branches'] - The current JSON path. */ function checkBranches( branches, - relationships, + productPaths, ctx, path = '/product_tree/branches' ) { @@ -179,24 +185,18 @@ function checkBranches( const product = branch.product if (product) { - if (!product?.product_id) { - ctx.warnings.push({ - instancePath: `${currentPath}/product`, - message: - 'missing product_id: product cannot be referenced without a product id.', - }) - } else if (product.product_identification_helper) { + if (product.product_id && product.product_identification_helper) { const { serial_numbers, model_numbers } = product.product_identification_helper if ( (serial_numbers?.length || model_numbers?.length) && - !checkRelationship(relationships, product.product_id) + !checkProductPath(productPaths, product.product_id) ) { ctx.warnings.push({ instancePath: `${currentPath}/product`, message: - 'missing relationship: product with serial number or model number should be referenced.', + 'missing product path: product with serial number or model number should be referenced in a product path.', }) } } @@ -206,7 +206,7 @@ function checkBranches( if (Array.isArray(branch.branches)) { checkBranches( branch.branches, - relationships, + productPaths, ctx, `${currentPath}/branches` ) @@ -215,25 +215,24 @@ function checkBranches( } /** - * Check if there is a valid relationship for the given productId. - * @param {Relationship[]} relationships + * Check if there is a valid product path referencing the given productId. + * A product path is valid if it has at least one subpath and the productId + * matches either the beginning_product_reference or any subpaths[].next_product_reference. + * @param {ProductPath[]} productPaths * @param {string} productId * @returns {boolean} */ -function checkRelationship(relationships, productId) { - return relationships.some((rel) => { - if (!rel.product_reference || !rel.relates_to_product_reference) { +function checkProductPath(productPaths, productId) { + return productPaths.some((pp) => { + const subpaths = pp.subpaths + if (!Array.isArray(subpaths) || subpaths.length === 0) { return false } - // Check for self-referencing relationships - if (rel.product_reference === rel.relates_to_product_reference) { - return false + if (pp.beginning_product_reference === productId) { + return true } - // Check if the productId matches either reference - return ( - rel.product_reference === productId || - rel.relates_to_product_reference === productId - ) + + return subpaths.some((sp) => sp.next_product_reference === productId) }) } diff --git a/tests/csaf_2_1/recommendedTest_6_2_31.js b/tests/csaf_2_1/recommendedTest_6_2_31.js index b339ad05..a8b64e75 100644 --- a/tests/csaf_2_1/recommendedTest_6_2_31.js +++ b/tests/csaf_2_1/recommendedTest_6_2_31.js @@ -4,7 +4,7 @@ import { recommendedTest_6_2_31 } from '../../csaf_2_1/recommendedTests.js' describe('recommendedTest_6_2_31', function () { it('only runs on relevant documents', function () { assert.equal( - recommendedTest_6_2_31({ vulnerabilities: 'mydoc' }).warnings.length, + recommendedTest_6_2_31({ product_tree: 'mydoc' }).warnings.length, 0 ) }) @@ -54,154 +54,28 @@ describe('recommendedTest_6_2_31', function () { ) }) - it('test branches without serial_numbers or model_numbers', function () { - assert.equal( - recommendedTest_6_2_31({ - document: {}, - product_tree: { - full_product_names: [], - branches: [ - { - category: 'product_version', - name: '1.0', - product: { - name: 'Example Company Controller A 1.0', - product_id: 'CSAFPID-908070601', - product_identification_helper: { - serial_numbers: [], - model_numbers: [], - }, - }, - }, - ], - }, - }).warnings.length, - 0 - ) - }) - - it('test full_product_names without serial_numbers or model_numbers', function () { + it('test product_path with no subpaths does not count as valid reference', function () { assert.equal( recommendedTest_6_2_31({ document: {}, product_tree: { full_product_names: [ { - name: 'Example Company Controller A 1.0', - product_id: 'CSAFPID-908070601', - product_identification_helper: { - serial_numbers: [], - model_numbers: [], - }, - }, - ], - }, - }).warnings.length, - 0 - ) - }) - - it('test nested branches 5 levels deep', function () { - assert.equal( - recommendedTest_6_2_31({ - document: {}, - product_tree: { - branches: [ - { - category: 'vendor', - name: 'Example Company', - branches: [ - { - category: 'product_family', - name: 'Controller Series', - branches: [ - { - category: 'product_name', - name: 'Controller A', - branches: [ - { - category: 'product_version', - name: '1.0', - branches: [ - { - category: 'architecture', - name: 'x86', - product: { - name: 'Example Company Controller A 1.0 x86', - product_id: 'CSAFPID-908070601', - product_identification_helper: { - serial_numbers: ['143-D-354'], - }, - }, - }, - ], - }, - ], - }, - ], - }, - ], - }, - ], - }, - }).warnings.length, - 1 - ) - }) - - it('test relationship with the same ID for product_reference and relates_to_product_reference', function () { - assert.equal( - recommendedTest_6_2_31({ - document: {}, - product_tree: { - full_product_names: [ - { - name: 'Example Company Controller A 1.0', product_id: 'CSAFPID-908070601', product_identification_helper: { serial_numbers: ['143-D-354'], }, }, - { - name: 'Microsoft Windows', - product_id: 'CSAFPID-908070602', - }, ], - relationships: [ - { - product_reference: 'CSAFPID-908070602', - relates_to_product_reference: 'CSAFPID-908070602', - }, - ], - }, - }).warnings.length, - 1 - ) - }) - - it('test relationship with product_reference only', function () { - assert.equal( - recommendedTest_6_2_31({ - document: {}, - product_tree: { - branches: [ + product_paths: [ { - category: 'product_version', - name: '1.0', - product: { - name: 'Example Company Controller A 1.0', - product_id: 'CSAFPID-908070601', - product_identification_helper: { - model_numbers: ['CA-1000'], - }, + beginning_product_reference: 'CSAFPID-908070601', + subpaths: [], + full_product_name: { + product_id: 'CSAFPID-908070603', }, }, ], - relationships: [ - { - product_reference: 'CSAFPID-908070601', - }, - ], }, }).warnings.length, 1 @@ -215,14 +89,6 @@ describe('recommendedTest_6_2_31', function () { product_tree: { branches: [ { - category: 'vendor', - name: 'Example Company', - product: { - product_id: 'CSAFPID-908070601', - product_identification_helper: { - serial_numbers: ['143-D-354'], - }, - }, branches: [ { product: 'invalid', @@ -231,10 +97,7 @@ describe('recommendedTest_6_2_31', function () { branches: [{}], }, { - category: 'product_version', - name: '1.0', product: { - name: 'Example Company Controller A 1.0', product_id: 'CSAFPID-908070602', product_identification_helper: { model_numbers: ['CA-1000'], @@ -246,42 +109,34 @@ describe('recommendedTest_6_2_31', function () { ], }, }).warnings.length, - 2 + 1 ) }) - it('warns when branch product has no product_id', function () { - const result = recommendedTest_6_2_31({ - document: {}, - product_tree: { - branches: [ - { - category: 'product_version', - name: '1.0', - product: { - name: 'Example Company Controller A 1.0', - product_identification_helper: { - serial_numbers: ['143-D-354'], - }, - }, - }, - ], - }, - }) - assert.equal(result.warnings.length, 1) - assert.equal( - result.warnings[0].instancePath, - '/product_tree/branches/0/product' - ) - }) + // it('skips branch product without product_id', function () { + // const result = recommendedTest_6_2_31({ + // document: {}, + // product_tree: { + // branches: [ + // { + // product: { + // product_identification_helper: { + // serial_numbers: ['143-D-354'], + // }, + // }, + // }, + // ], + // }, + // }) + // assert.equal(result.warnings.length, 0) + // }) - it('warns when full_product_names entry has no product_id', function () { + it('skips full_product_names entry without product_id', function () { const result = recommendedTest_6_2_31({ document: {}, product_tree: { full_product_names: [ { - name: 'Example Company Controller A 1.0', product_identification_helper: { serial_numbers: ['143-D-354'], }, @@ -289,24 +144,19 @@ describe('recommendedTest_6_2_31', function () { ], }, }) - assert.equal(result.warnings.length, 1) - assert.equal( - result.warnings[0].instancePath, - '/product_tree/full_product_names/0' - ) + assert.equal(result.warnings.length, 0) }) - it('warns when relationship full_product_name has no product_id', function () { + it('warns with correct instancePath for product_paths[*].full_product_name', function () { const result = recommendedTest_6_2_31({ document: {}, product_tree: { - full_product_names: [], - relationships: [ + product_paths: [ { - product_reference: 'CSAFPID-908070601', - relates_to_product_reference: 'CSAFPID-908070602', + beginning_product_reference: 'CSAFPID-908070601', + subpaths: [{ next_product_reference: 'CSAFPID-908070602' }], full_product_name: { - name: 'Example Company Controller A 1.0 on Windows', + product_id: 'CSAFPID-908070603', product_identification_helper: { serial_numbers: ['143-D-354'], }, @@ -318,7 +168,59 @@ describe('recommendedTest_6_2_31', function () { assert.equal(result.warnings.length, 1) assert.equal( result.warnings[0].instancePath, - '/product_tree/relationships/0/full_product_name/0' + '/product_tree/product_paths/0/full_product_name' + ) + }) + + it('no warning when product_paths full_product_name has empty serial and model numbers', function () { + assert.equal( + recommendedTest_6_2_31({ + document: {}, + product_tree: { + product_paths: [ + { + beginning_product_reference: 'CSAFPID-908070601', + subpaths: [{ next_product_reference: 'CSAFPID-908070602' }], + full_product_name: { + product_id: 'CSAFPID-908070603', + product_identification_helper: { + serial_numbers: [], + model_numbers: [], + }, + }, + }, + ], + }, + }).warnings.length, + 0 + ) + }) + + it('no warning when product is referenced as next_product_reference in a subpath', function () { + assert.equal( + recommendedTest_6_2_31({ + document: {}, + product_tree: { + full_product_names: [ + { + product_id: 'CSAFPID-908070601', + product_identification_helper: { + model_numbers: ['CA-1000'], + }, + }, + ], + product_paths: [ + { + beginning_product_reference: 'CSAFPID-908070601', + subpaths: [{ next_product_reference: 'CSAFPID-908070602' }], + full_product_name: { + product_id: 'CSAFPID-908070603', + }, + }, + ], + }, + }).warnings.length, + 0 ) }) }) From 439c6e10f9f643e7ef227d2651f2238e73c3147c Mon Sep 17 00:00:00 2001 From: bendo-eXX Date: Wed, 10 Jun 2026 13:11:36 +0200 Subject: [PATCH 11/12] feat(CSAF2.1): fix code style issue --- .../recommendedTests/recommendedTest_6_2_31.js | 18 ++++++++---------- tests/csaf_2_1/recommendedTest_6_2_31.js | 18 ------------------ 2 files changed, 8 insertions(+), 28 deletions(-) diff --git a/csaf_2_1/recommendedTests/recommendedTest_6_2_31.js b/csaf_2_1/recommendedTests/recommendedTest_6_2_31.js index 0bb3b4c4..4da8962a 100644 --- a/csaf_2_1/recommendedTests/recommendedTest_6_2_31.js +++ b/csaf_2_1/recommendedTests/recommendedTest_6_2_31.js @@ -58,7 +58,7 @@ const branchSchema = /** @type {const} */ ({ const inputSchema = /** @type {const} */ ({ additionalProperties: true, - properties: { + optionalProperties: { product_tree: { additionalProperties: true, optionalProperties: { @@ -137,16 +137,14 @@ export function recommendedTest_6_2_31(doc) { * @param {FullProductName[]} full_product_names * @param {ProductPath[]} productPaths * @param {{ warnings: Array<{ instancePath: string; message: string }> }} ctx - * @param {string} [basePath='/product_tree/full_product_names'] - The base JSON path for warnings */ -function checkFullProductNames( - full_product_names, - productPaths, - ctx, - basePath = '/product_tree/full_product_names' -) { +function checkFullProductNames(full_product_names, productPaths, ctx) { full_product_names.forEach((fullProductName, index) => { - if (!fullProductName?.product_identification_helper || !fullProductName.product_id) return + if ( + !fullProductName?.product_identification_helper || + !fullProductName.product_id + ) + return const { serial_numbers, model_numbers } = fullProductName.product_identification_helper @@ -155,7 +153,7 @@ function checkFullProductNames( !checkProductPath(productPaths, fullProductName.product_id) ) { ctx.warnings.push({ - instancePath: `${basePath}/${index}`, + instancePath: `/product_tree/full_product_names/${index}`, message: 'missing product path: product with serial number or model number should be referenced in a product path.', }) diff --git a/tests/csaf_2_1/recommendedTest_6_2_31.js b/tests/csaf_2_1/recommendedTest_6_2_31.js index a8b64e75..4f187092 100644 --- a/tests/csaf_2_1/recommendedTest_6_2_31.js +++ b/tests/csaf_2_1/recommendedTest_6_2_31.js @@ -113,24 +113,6 @@ describe('recommendedTest_6_2_31', function () { ) }) - // it('skips branch product without product_id', function () { - // const result = recommendedTest_6_2_31({ - // document: {}, - // product_tree: { - // branches: [ - // { - // product: { - // product_identification_helper: { - // serial_numbers: ['143-D-354'], - // }, - // }, - // }, - // ], - // }, - // }) - // assert.equal(result.warnings.length, 0) - // }) - it('skips full_product_names entry without product_id', function () { const result = recommendedTest_6_2_31({ document: {}, From 06bdffcbe15f9fdc598697a1a5cd640f8d71e8ff Mon Sep 17 00:00:00 2001 From: bendo-eXX Date: Wed, 29 Jul 2026 13:48:49 +0200 Subject: [PATCH 12/12] feat(CSAF2.1): change test description --- tests/csaf_2_1/recommendedTest_6_2_31.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/csaf_2_1/recommendedTest_6_2_31.js b/tests/csaf_2_1/recommendedTest_6_2_31.js index 4f187092..c0cc889e 100644 --- a/tests/csaf_2_1/recommendedTest_6_2_31.js +++ b/tests/csaf_2_1/recommendedTest_6_2_31.js @@ -178,7 +178,7 @@ describe('recommendedTest_6_2_31', function () { ) }) - it('no warning when product is referenced as next_product_reference in a subpath', function () { + it('no warning when product is referenced as beginning_product_reference', function () { assert.equal( recommendedTest_6_2_31({ document: {},