diff --git a/csaf_2_1/mandatoryTests.js b/csaf_2_1/mandatoryTests.js index 8ae82bc4..0a1ed735 100644 --- a/csaf_2_1/mandatoryTests.js +++ b/csaf_2_1/mandatoryTests.js @@ -1,5 +1,4 @@ export { - mandatoryTest_6_1_2, mandatoryTest_6_1_3, mandatoryTest_6_1_4, mandatoryTest_6_1_5, @@ -35,6 +34,7 @@ export { mandatoryTest_6_1_33, } from '../mandatoryTests.js' export { mandatoryTest_6_1_1 } from './mandatoryTests/mandatoryTest_6_1_1.js' +export { mandatoryTest_6_1_2 } from './mandatoryTests/mandatoryTest_6_1_2.js' export { mandatoryTest_6_1_6 } from './mandatoryTests/mandatoryTest_6_1_6.js' export { mandatoryTest_6_1_7 } from './mandatoryTests/mandatoryTest_6_1_7.js' export { mandatoryTest_6_1_8 } from './mandatoryTests/mandatoryTest_6_1_8.js' diff --git a/csaf_2_1/mandatoryTests/mandatoryTest_6_1_2.js b/csaf_2_1/mandatoryTests/mandatoryTest_6_1_2.js new file mode 100644 index 00000000..c6adedb3 --- /dev/null +++ b/csaf_2_1/mandatoryTests/mandatoryTest_6_1_2.js @@ -0,0 +1,160 @@ +import Ajv from 'ajv/dist/jtd.js' + +const ajv = new Ajv() + +const branchSchema = /** @type {const} */ ({ + additionalProperties: true, + optionalProperties: { + branches: { + elements: { + additionalProperties: true, + properties: {}, + }, + }, + product: { + additionalProperties: true, + optionalProperties: { + product_id: { type: 'string' }, + }, + }, + }, +}) + +const validateBranch = ajv.compile(branchSchema) + +const fullProductNameSchema = /** @type {const} */ ({ + additionalProperties: true, + optionalProperties: { + product_id: { type: 'string' }, + }, +}) + +/* + 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, + optionalProperties: { + product_tree: { + additionalProperties: true, + optionalProperties: { + branches: { + elements: branchSchema, + }, + full_product_names: { + elements: fullProductNameSchema, + }, + product_paths: { + elements: { + additionalProperties: true, + optionalProperties: { + full_product_name: fullProductNameSchema, + }, + }, + }, + }, + }, + }, +}) + +const validate = ajv.compile(inputSchema) + +/** + * @typedef {import('ajv/dist/core').JTDDataType} Branch + * @typedef {import('ajv/dist/core').JTDDataType} FullProductName + */ + +/** + * This implements the mandatory test 6.1.2 of the CSAF 2.1 standard. + * + * @param {unknown} doc + */ +export function mandatoryTest_6_1_2(doc) { + /* + The `ctx` variable holds the state that is accumulated during the test ran and is + finally returned by the function. + */ + const ctx = { + errors: + /** @type {Array<{ instancePath: string; message: string }>} */ ([]), + isValid: true, + } + + if (!validate(doc)) { + return ctx + } + + /** @type {Map} */ + const seenProductIds = new Map() + + doc.product_tree?.branches?.forEach((branch, index) => { + checkBranch(`/product_tree/branches/${index}`, branch) + }) + + doc.product_tree?.full_product_names?.forEach((fullProductName, index) => { + checkFullProductName( + `/product_tree/full_product_names/${index}`, + fullProductName + ) + }) + + doc.product_tree?.product_paths?.forEach((productPath, index) => { + const fullProductName = productPath.full_product_name + if (!fullProductName) return + checkFullProductName( + `/product_tree/product_paths/${index}/full_product_name`, + fullProductName + ) + }) + + return ctx + + /** + * Checks whether the given product_id was already defined and registers an error if so. + * + * @param {string} instancePath The instance path of the product_id to check. + * @param {string} productId The product_id value to check. + */ + function checkProductId(instancePath, productId) { + if (seenProductIds.has(productId)) { + ctx.isValid = false + ctx.errors.push({ + instancePath, + message: 'duplicate definition of product_id', + }) + } else { + seenProductIds.set(productId, instancePath) + } + } + + /** + * Checks the product_id in the given "full product name". + * + * @param {string} prefix The instance path prefix of the "full product name". + * @param {FullProductName} fullProductName The "full product name" object. + */ + function checkFullProductName(prefix, fullProductName) { + if (fullProductName.product_id) { + checkProductId(`${prefix}/product_id`, fullProductName.product_id) + } + } + + /** + * Checks the product_id in the given branch object and its branch children. + * + * @param {string} prefix The instance path prefix of the "branch". + * @param {Branch} branch The "branch" object. + */ + function checkBranch(prefix, branch) { + if (branch.product?.product_id) { + checkProductId(`${prefix}/product/product_id`, branch.product.product_id) + } + branch.branches?.forEach((child, index) => { + if (!validateBranch(child)) return + checkBranch(`${prefix}/branches/${index}`, child) + }) + } +} diff --git a/tests/csaf_2_1/mandatoryTest_6_1_2.js b/tests/csaf_2_1/mandatoryTest_6_1_2.js new file mode 100644 index 00000000..617de372 --- /dev/null +++ b/tests/csaf_2_1/mandatoryTest_6_1_2.js @@ -0,0 +1,65 @@ +import assert from 'node:assert' +import { mandatoryTest_6_1_2 } from '../../csaf_2_1/mandatoryTests/mandatoryTest_6_1_2.js' + +describe('mandatory test 6.1.2', function () { + describe('valid examples', function () { + it('only runs on relevant documents', function () { + assert.equal(mandatoryTest_6_1_2({ product_tree: 'mydoc' }).isValid, true) + }) + }) + it('skips invalid full product names', function () { + assert.equal( + mandatoryTest_6_1_2({ + product_tree: { + full_product_names: [ + { + product_id: { invalid: true }, + }, + ], + }, + }).isValid, + true + ) + }) + + it('validates branches and skips invalid ones', function () { + assert.equal( + mandatoryTest_6_1_2({ + product_tree: { + branches: [ + { + product: { + product_id: 'CSAFPID-9080700', + }, + branches: [ + { + product: 'CSAFPID-9080701', + }, + { + branches: [{}], + }, + ], + }, + ], + }, + }).isValid, + true + ) + }) + + it('validates product_paths and skips invalid ones', function () { + assert.equal( + mandatoryTest_6_1_2({ + product_tree: { + product_paths: [ + { + full_product_name: {}, + }, + {}, + ], + }, + }).isValid, + true + ) + }) +})