diff --git a/README.md b/README.md index f9a60d1b..52726dd9 100644 --- a/README.md +++ b/README.md @@ -345,11 +345,7 @@ The following tests are not yet implemented and therefore missing: - Recommeded Test 6.2.24 - Recommeded Test 6.2.25 - Recommeded Test 6.2.26 -- Recommeded Test 6.2.27 -- Recommeded Test 6.2.28 -- Recommeded Test 6.2.29 - Recommeded Test 6.2.30 -- Recommeded Test 6.2.31 - Recommeded Test 6.2.32 - Recommeded Test 6.2.33 - Recommeded Test 6.2.34 @@ -463,6 +459,10 @@ export const recommendedTest_6_2_18: DocumentTest export const recommendedTest_6_2_19: DocumentTest export const recommendedTest_6_2_20: DocumentTest export const recommendedTest_6_2_22: DocumentTest +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_31: DocumentTest ``` [(back to top)](#bsi-csaf-validator-lib) diff --git a/csaf_2_1/recommendedTests.js b/csaf_2_1/recommendedTests.js index 641832bb..04bd48ef 100644 --- a/csaf_2_1/recommendedTests.js +++ b/csaf_2_1/recommendedTests.js @@ -30,4 +30,5 @@ export { recommendedTest_6_2_22 } 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/recommendedTest_6_2_31.js b/csaf_2_1/recommendedTests/recommendedTest_6_2_31.js new file mode 100644 index 00000000..10db17d0 --- /dev/null +++ b/csaf_2_1/recommendedTests/recommendedTest_6_2_31.js @@ -0,0 +1,211 @@ +import Ajv from 'ajv/dist/jtd.js' + +const ajv = new Ajv() + +/** + * @typedef {Object} FullProductName + * @property {string} name + * @property {string} product_id + * @property {ProductIdentificationHelper} product_identification_helper + */ + +/** + * @typedef {Object} Branch + * @property {Array} branches + * @property {FullProductName} product + */ + +/** + * @typedef {Object} ProductIdentificationHelper + * @property {string[]=} serial_numbers + * @property {string[]=} model_numbers + */ + +/** + * @typedef {Object} Relationship + * @property {string} product_reference + * @property {string} relates_to_product_reference + */ + +const productIdentificationHelperSchema = { + additionalProperties: true, + optionalProperties: { + serial_numbers: { + elements: { type: 'string' }, + }, + model_numbers: { + elements: { type: 'string' }, + }, + }, +}; + +const relationshipSchema = { + elements: { + additionalProperties: true, + properties: { + 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, + properties: { + product_id: { type: 'string' }, + }, + optionalProperties: { + product_identification_helper: productIdentificationHelperSchema, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + full_product_names: { + elements: { + additionalProperties: true, + properties: { + product_id: { type: 'string' }, + }, + optionalProperties: { + 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 recommendedTest_6_2_31(doc) { + const ctx = { + warnings: + /** @type {Array<{ instancePath: string; message: string }>} */ ([]), + } + + if (!validateInput(doc)) { + return ctx + } + + 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(safeDoc.product_tree?.branches ?? [], relationships, ctx) + + checkFullProductNames(safeDoc.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 {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 + ) { + 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 {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') { + 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 {Array} relationships + * @param {string} productId + * @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 ed81ff09..5e0ca546 100644 --- a/tests/csaf_2_1/oasis.js +++ b/tests/csaf_2_1/oasis.js @@ -50,7 +50,6 @@ const excluded = [ '6.2.25', '6.2.26', '6.2.30', - '6.2.31', '6.2.32', '6.2.33', '6.2.34', 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 + ) + }) +})