diff --git a/README.md b/README.md index 32d77a87..39f11522 100644 --- a/README.md +++ b/README.md @@ -479,6 +479,7 @@ export const recommendedTest_6_2_40: DocumentTest export const recommendedTest_6_2_41: DocumentTest export const recommendedTest_6_2_43: DocumentTest export const recommendedTest_6_2_47: DocumentTest +export const recommendedTest_6_2_48: DocumentTest ``` [(back to top)](#bsi-csaf-validator-lib) diff --git a/csaf_2_1/recommendedTests.js b/csaf_2_1/recommendedTests.js index 594f22a4..c7da1b4f 100644 --- a/csaf_2_1/recommendedTests.js +++ b/csaf_2_1/recommendedTests.js @@ -40,3 +40,4 @@ export { recommendedTest_6_2_40 } from './recommendedTests/recommendedTest_6_2_4 export { recommendedTest_6_2_41 } from './recommendedTests/recommendedTest_6_2_41.js' export { recommendedTest_6_2_43 } from './recommendedTests/recommendedTest_6_2_43.js' export { recommendedTest_6_2_47 } from './recommendedTests/recommendedTest_6_2_47.js' +export { recommendedTest_6_2_48 } from './recommendedTests/recommendedTest_6_2_48.js' diff --git a/csaf_2_1/recommendedTests/recommendedTest_6_2_48.js b/csaf_2_1/recommendedTests/recommendedTest_6_2_48.js new file mode 100644 index 00000000..7986c7aa --- /dev/null +++ b/csaf_2_1/recommendedTests/recommendedTest_6_2_48.js @@ -0,0 +1,108 @@ +import Ajv from 'ajv/dist/jtd.js' + +const ajv = new Ajv() + +const branchSchema = /** @type {const} */ ({ + additionalProperties: true, + optionalProperties: { + category: { type: 'string' }, + name: { type: 'string' }, + branches: { + elements: { + additionalProperties: true, + properties: {}, + }, + }, + }, +}) + +/* + 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: { + product_tree: { + additionalProperties: true, + optionalProperties: { + branches: { + elements: branchSchema, + }, + }, + }, + }, +}) + +const validateInput = ajv.compile(inputSchema) +const validateBranch = ajv.compile(branchSchema) + +/** + * @typedef {import('ajv/dist/core').JTDDataType} Branch + */ + +/** + * This implements the recommended test 6.2.48 of the CSAF 2.1 standard. + * + * @param {any} doc + */ +export function recommendedTest_6_2_48(doc) { + const ctx = { + warnings: + /** @type {Array<{ instancePath: string; message: string }>} */ ([]), + } + + if (!validateInput(doc)) { + return ctx + } + + const branches = doc.product_tree?.branches ?? [] + branches.forEach((branch, index) => { + checkBranch(branch, `/product_tree/branches/${index}`, ctx.warnings) + }) + + return ctx +} + +/** + * Recursively checks a branch and its nested branches. + * + * @param {Branch} branch + * @param {string} basePath + * @param {Array<{ instancePath: string; message: string }>} warnings + */ +function checkBranch(branch, basePath, warnings) { + if (!validateBranch(branch)) return + if (branch.category === 'vendor') { + if ( + branch.name !== undefined && + normalizeBranchName(branch.name) === 'opensource' + ) { + warnings.push({ + instancePath: `${basePath}/name`, + message: + 'Branch with category "vendor" should not have the name "Open Source"', + }) + } + } + + if (Array.isArray(branch.branches)) { + branch.branches.forEach( + (/** @type {Branch} */ childBranch, /** @type {number} */ index) => { + checkBranch(childBranch, `${basePath}/branches/${index}`, warnings) + } + ) + } +} + +/** + * Normalizes a string to be case and white space insensitive. + * + * @param {string} str + * @returns {string} + */ +function normalizeBranchName(str) { + return str.replaceAll(/\s+/g, '').toLowerCase() +} diff --git a/tests/csaf_2_1/oasis.js b/tests/csaf_2_1/oasis.js index 8f223924..6dcd63ed 100644 --- a/tests/csaf_2_1/oasis.js +++ b/tests/csaf_2_1/oasis.js @@ -50,7 +50,6 @@ const excluded = [ '6.2.44', '6.2.45', '6.2.46', - '6.2.48', '6.2.49', '6.2.50.1', '6.2.50.2', diff --git a/tests/csaf_2_1/recommendedTest_6_2_48.js b/tests/csaf_2_1/recommendedTest_6_2_48.js new file mode 100644 index 00000000..ed166ad8 --- /dev/null +++ b/tests/csaf_2_1/recommendedTest_6_2_48.js @@ -0,0 +1,30 @@ +import assert from 'node:assert/strict' +import { recommendedTest_6_2_48 } from '../../csaf_2_1/recommendedTests/recommendedTest_6_2_48.js' + +describe('recommendedTest_6_2_48', function () { + it('only runs on relevant documents', function () { + assert.equal(recommendedTest_6_2_48({}).warnings.length, 0) + }) + + it('does not warn when product_tree has no branches', function () { + assert.equal( + recommendedTest_6_2_48({ product_tree: {} }).warnings.length, + 0 + ) + }) + + it('skips invalid child branches that do not pass schema validation', function () { + const result = recommendedTest_6_2_48({ + product_tree: { + branches: [ + { + category: 'vendor', + name: 'Open Source Company', + branches: [42, null], + }, + ], + }, + }) + assert.equal(result.warnings.length, 0) + }) +})