diff --git a/csaf_2_1/recommendedTests/recommendedTest_6_2_16.js b/csaf_2_1/recommendedTests/recommendedTest_6_2_16.js index 30db99f7..d78261f6 100644 --- a/csaf_2_1/recommendedTests/recommendedTest_6_2_16.js +++ b/csaf_2_1/recommendedTests/recommendedTest_6_2_16.js @@ -1,8 +1,137 @@ -import { optionalTest_6_2_16 } from '../../optionalTests.js' +import { Ajv } from 'ajv/dist/jtd.js' + +const ajv = new Ajv() + +const inputSchema = /** @type {const} */ ({ + additionalProperties: true, + optionalProperties: { + product_tree: { + additionalProperties: true, + optionalProperties: { + branches: { + elements: { additionalProperties: true, properties: {} }, + }, + full_product_names: { + elements: { additionalProperties: true, properties: {} }, + }, + product_paths: { + elements: { additionalProperties: true, properties: {} }, + }, + }, + }, + }, +}) + +const branchSchema = /** @type {const} */ ({ + additionalProperties: true, + optionalProperties: { + branches: { + elements: { + additionalProperties: true, + properties: {}, + }, + }, + product: { + additionalProperties: true, + optionalProperties: { + product_identification_helper: { + additionalProperties: true, + properties: {}, + }, + }, + }, + }, +}) + +const productPathSchema = /** @type {const} */ ({ + additionalProperties: true, + optionalProperties: { + full_product_name: { + additionalProperties: true, + optionalProperties: { + product_identification_helper: { + additionalProperties: true, + properties: {}, + }, + }, + }, + }, +}) + +const validateInput = ajv.compile(inputSchema) +const validateBranch = ajv.compile(branchSchema) +const validateProductPath = ajv.compile(productPathSchema) /** - * @param {unknown} doc + * @param {any} doc */ export function recommendedTest_6_2_16(doc) { - return optionalTest_6_2_16(doc) + const ctx = { + warnings: + /** @type {Array<{ instancePath: string; message: string }>} */ ([]), + } + + if (!validateInput(doc)) { + return ctx + } + + doc.product_tree?.full_product_names?.forEach( + (fullProductName, fullProductNameIndex) => { + if (!fullProductName.product_identification_helper) { + ctx.warnings.push({ + instancePath: `/product_tree/full_product_names/${fullProductNameIndex}`, + message: 'missing product identification helper', + }) + } + } + ) + + /** + * @param {object} params + * @param {string} params.path + * @param {unknown[]} params.branches + */ + function checkBranches({ path, branches }) { + branches.forEach((branch, branchIndex) => { + if (!validateBranch(branch)) { + return + } + if (branch.product && !branch.product.product_identification_helper) { + ctx.warnings.push({ + instancePath: `${path}/${branchIndex}/product`, + message: 'missing product identification helper', + }) + } + if (Array.isArray(branch.branches)) { + checkBranches({ + path: `${path}/${branchIndex}/branches`, + branches: branch.branches, + }) + } + }) + } + + if (doc.product_tree?.branches) { + checkBranches({ + path: '/product_tree/branches', + branches: doc.product_tree.branches, + }) + } + + doc.product_tree?.product_paths?.forEach((productPath, productPathIndex) => { + if (!validateProductPath(productPath)) { + return + } + if ( + productPath.full_product_name && + !productPath.full_product_name.product_identification_helper + ) { + ctx.warnings.push({ + instancePath: `/product_tree/product_paths/${productPathIndex}/full_product_name`, + message: 'missing product identification helper', + }) + } + }) + + return ctx } diff --git a/tests/csaf_2_1/recommendedTest_6_2_16.js b/tests/csaf_2_1/recommendedTest_6_2_16.js new file mode 100644 index 00000000..78639e7d --- /dev/null +++ b/tests/csaf_2_1/recommendedTest_6_2_16.js @@ -0,0 +1,56 @@ +import assert from 'node:assert' +import { recommendedTest_6_2_16 } from '../../csaf_2_1/recommendedTests.js' + +describe('recommendedTest_6_2_16', function () { + it('only runs on relevant documents', function () { + assert.equal( + recommendedTest_6_2_16({ vulnerabilities: 'mydoc' }).warnings.length, + 0 + ) + }) + + it('returns no warnings for invalid inputSchema (product_tree is not an object)', function () { + const result = recommendedTest_6_2_16({ + product_tree: 'not-an-object', + }) + assert.equal(result.warnings.length, 0) + }) + + it('skips invalid branch entries and does not throw', function () { + const result = recommendedTest_6_2_16({ + product_tree: { + branches: [{ product: 'not-an-object' }], + }, + }) + assert.equal(result.warnings.length, 0) + }) + + it('skips invalid product_paths entries and does not throw', function () { + const result = recommendedTest_6_2_16({ + product_tree: { + product_paths: [{ full_product_name: 'not-an-object' }], + }, + }) + assert.equal(result.warnings.length, 0) + }) + + it('warns when a product_paths entry is missing product_identification_helper', function () { + const result = recommendedTest_6_2_16({ + product_tree: { + product_paths: [ + { + full_product_name: { + name: 'Product A', + product_id: 'CSAFPID-0001', + }, + }, + ], + }, + }) + assert.equal(result.warnings.length, 1) + assert.equal( + result.warnings[0].instancePath, + '/product_tree/product_paths/0/full_product_name' + ) + }) +})