diff --git a/README.md b/README.md index 6f4f9297..549ed961 100644 --- a/README.md +++ b/README.md @@ -315,7 +315,6 @@ The following tests are not yet implemented and therefore missing: - Mandatory Test 6.1.27.13 - Mandatory Test 6.1.27.18 - Mandatory Test 6.1.42 -- Mandatory Test 6.1.44 - Mandatory Test 6.1.46 - Mandatory Test 6.1.47 - Mandatory Test 6.1.48 @@ -433,6 +432,7 @@ export const mandatoryTest_6_1_39: DocumentTest export const mandatoryTest_6_1_40: DocumentTest export const mandatoryTest_6_1_41: DocumentTest export const mandatoryTest_6_1_43: DocumentTest +export const mandatoryTest_6_1_44: DocumentTest export const mandatoryTest_6_1_45: DocumentTest export const mandatoryTest_6_1_52: DocumentTest ``` diff --git a/csaf_2_1/mandatoryTests.js b/csaf_2_1/mandatoryTests.js index d5a7706b..60806caf 100644 --- a/csaf_2_1/mandatoryTests.js +++ b/csaf_2_1/mandatoryTests.js @@ -58,5 +58,6 @@ export { mandatoryTest_6_1_39 } from './mandatoryTests/mandatoryTest_6_1_39.js' export { mandatoryTest_6_1_40 } from './mandatoryTests/mandatoryTest_6_1_40.js' export { mandatoryTest_6_1_41 } from './mandatoryTests/mandatoryTest_6_1_41.js' export { mandatoryTest_6_1_43 } from './mandatoryTests/mandatoryTest_6_1_43.js' +export { mandatoryTest_6_1_44 } from './mandatoryTests/mandatoryTest_6_1_44.js' export { mandatoryTest_6_1_45 } from './mandatoryTests/mandatoryTest_6_1_45.js' export { mandatoryTest_6_1_52 } from './mandatoryTests/mandatoryTest_6_1_52.js' diff --git a/csaf_2_1/mandatoryTests/mandatoryTest_6_1_44.js b/csaf_2_1/mandatoryTests/mandatoryTest_6_1_44.js new file mode 100644 index 00000000..7bce9ef3 --- /dev/null +++ b/csaf_2_1/mandatoryTests/mandatoryTest_6_1_44.js @@ -0,0 +1,206 @@ +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_identification_helper: { + additionalProperties: true, + optionalProperties: { + serial_numbers: { elements: { type: 'string' } }, + }, + }, + }, + }, + }, +}) + +const validateBranch = ajv.compile(branchSchema) + +const fullProductNameSchema = /** @type {const} */ ({ + additionalProperties: true, + optionalProperties: { + product_identification_helper: { + additionalProperties: true, + optionalProperties: { + serial_numbers: { elements: { 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, + }, + relationships: { + 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 + */ + +/** + * Checks if the `stringToCheck` includes more than one unescaped `*` character. A `*` character + * can be escaped by prefixing it with a backslash (`\`). + * + * @param {string} stringToCheck + * @return {boolean} + */ +export function containMultipleUnescapedStars(stringToCheck) { + const regex = /\*/g + return ( + (stringToCheck + .replace(/\\\*/g, '') // remove escaped '*' + .match(regex)?.length ?? 0) > 1 // check if there is more than 1 unescaped '*' + ) +} + +/** + * Validates all given serial numbers and + * check whether they contain multiple unescaped stars + * + * @param {Array | undefined} serialNumbers serial_numbers to check + * @return {Array} indexes of the serial_numbers that invalid + */ +export function checkSerialNumbers(serialNumbers) { + /** @type {Array}*/ + const invalidNumbers = [] + if (serialNumbers) { + for (let i = 0; i < serialNumbers.length; i++) { + const modelNumber = serialNumbers[i] + if (containMultipleUnescapedStars(modelNumber)) { + invalidNumbers.push('' + i) + } + } + } + return invalidNumbers +} + +/** + * For each serial number, it MUST be tested + * that it does not contain multiple unescaped stars. + * + * @param {unknown} doc + */ +export function mandatoryTest_6_1_44(doc) { + /* + The `ctx` variable holds the state that is accumulated during the test run and is + finally returned by the function. + */ + const ctx = { + errors: + /** @type {Array<{ instancePath: string; message: string }>} */ ([]), + isValid: true, + } + + if (!validate(doc)) { + return ctx + } + + doc.product_tree?.branches?.forEach((branch, index) => { + checkBranch(`/product_tree/branches/${index}`, branch) + }) + + doc.product_tree?.full_product_names?.forEach((fullProduceName, index) => { + checkFullProductName( + `/product_tree/full_product_names/${index}`, + fullProduceName + ) + }) + + doc.product_tree?.relationships?.forEach((relationship, index) => { + const fullProductName = relationship.full_product_name + if (fullProductName) { + checkFullProductName( + `/product_tree/relationships/${index}/full_product_name`, + fullProductName + ) + } + }) + + return ctx + + /** + * Check whether the serial numbers contain multiple unescaped stars for a full product name object + * + * @param {string} prefix The instance path prefix of the "full product name". It is + * used to generate error messages. + * @param {FullProductName} fullProductName The "full product name" object. + */ + function checkFullProductName(prefix, fullProductName) { + const invalidNumberIndexes = checkSerialNumbers( + fullProductName.product_identification_helper?.serial_numbers + ) + invalidNumberIndexes.forEach((invalidNumberIndex) => { + ctx.isValid = false + ctx.errors.push({ + instancePath: `${prefix}/product_identification_helper/serial_numbers/${invalidNumberIndex}`, + message: 'Serial number contains multiple unescaped stars', + }) + }) + } + + /** + * Check whether the model numbers contain multiple unescaped stars for the given branch object + * and its branch children. + * + * @param {string} prefix The instance path prefix of the "branch". It is + * used to generate error messages. + * @param {Branch} branch The "branch" object. + */ + function checkBranch(prefix, branch) { + const invalidNumberIndexes = checkSerialNumbers( + branch.product?.product_identification_helper?.serial_numbers + ) + invalidNumberIndexes.forEach((invalidNumberIndex) => { + ctx.isValid = false + ctx.errors.push({ + instancePath: `${prefix}/product/product_identification_helper/serial_numbers/${invalidNumberIndex}`, + message: 'Serial number contains multiple unescaped stars', + }) + }) + branch.branches?.forEach((branch, index) => { + if (validateBranch(branch)) { + checkBranch(`${prefix}/branches/${index}`, branch) + } + }) + } +} diff --git a/tests/csaf_2_1/mandatoryTest_6_1_44.js b/tests/csaf_2_1/mandatoryTest_6_1_44.js new file mode 100644 index 00000000..5116d5fe --- /dev/null +++ b/tests/csaf_2_1/mandatoryTest_6_1_44.js @@ -0,0 +1,9 @@ +import assert from 'node:assert/strict' + +import { mandatoryTest_6_1_44 } from '../../csaf_2_1/mandatoryTests/mandatoryTest_6_1_44.js' + +describe('mandatoryTest_6_1_44', function () { + it('only runs on relevant documents', function () { + assert.equal(mandatoryTest_6_1_44({ product_tree: 'mydoc' }).isValid, true) + }) +}) diff --git a/tests/csaf_2_1/oasis.js b/tests/csaf_2_1/oasis.js index 0e9d2e60..d8bef3a3 100644 --- a/tests/csaf_2_1/oasis.js +++ b/tests/csaf_2_1/oasis.js @@ -21,7 +21,6 @@ const excluded = [ '6.1.27.18', '6.1.37', '6.1.42', - '6.1.44', '6.1.46', '6.1.47', '6.1.48',