diff --git a/csaf_2_1/mandatoryTests.js b/csaf_2_1/mandatoryTests.js index 60b53fd3..bf9715a3 100644 --- a/csaf_2_1/mandatoryTests.js +++ b/csaf_2_1/mandatoryTests.js @@ -1,6 +1,5 @@ export { mandatoryTest_6_1_3, - mandatoryTest_6_1_4, mandatoryTest_6_1_5, mandatoryTest_6_1_12, mandatoryTest_6_1_14, @@ -32,6 +31,7 @@ export { } 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_4 } from './mandatoryTests/mandatoryTest_6_1_4.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_4.js b/csaf_2_1/mandatoryTests/mandatoryTest_6_1_4.js new file mode 100644 index 00000000..9e2dc073 --- /dev/null +++ b/csaf_2_1/mandatoryTests/mandatoryTest_6_1_4.js @@ -0,0 +1,72 @@ +import { walkPath } from '../../lib/walkPaths.js' +import { + collectGroupIdsFromProductTree, + findMissingDefinitions, +} from './shared/docProductUtils.js' + +/** + * @typedef {{id: string, instancePath: string}} GroupId + */ + +/** + * This implements the mandatory test 6.1.4 of the CSAF 2.1 standard. + * + * @param {unknown} doc + */ +export async function mandatoryTest_6_1_4(doc) { + const ctx = { + errors: + /** @type {Array<{ instancePath: string; message: string }>} */ ([]), + isValid: true, + } + + const groupIds = collectGroupIdsFromProductTree(/** @type {any} */ (doc)) + const groupIdRefs = await collectGroupIds(doc) + const missingGroupDefinitions = + /** @type {{id: string, instancePath: string}[]} */ ( + findMissingDefinitions(groupIds, groupIdRefs) + ) + if (missingGroupDefinitions.length > 0) { + ctx.isValid = false + missingGroupDefinitions.forEach( + ( + /** @type {{id: string, instancePath: string}} */ missingGroupDefinition + ) => { + ctx.errors.push({ + message: 'definition of group id missing', + instancePath: missingGroupDefinition.instancePath, + }) + } + ) + } + + return ctx +} + +/** + * Collects all group_id references and their instancePaths from the document. + * @param {unknown} doc + * @returns {Promise} + */ +async function collectGroupIds(doc) { + const entries = /** @type {GroupId[]} */ ([]) + + for (const path of [ + '/document/notes[]/group_ids[]', + '/vulnerabilities[]/first_known_exploitation_dates[]/group_ids[]', + '/vulnerabilities[]/flags[]/group_ids[]', + '/vulnerabilities[]/ids[]/group_ids[]', + '/vulnerabilities[]/involvements[]/group_ids[]', + '/vulnerabilities[]/notes[]/group_ids[]', + '/vulnerabilities[]/remediations[]/group_ids[]', + '/vulnerabilities[]/threats[]/group_ids[]', + ]) { + await walkPath(doc, path, async (instancePath, value) => { + if (typeof value === 'string' && value) { + entries.push({ id: value, instancePath }) + } + }) + } + + return entries +} diff --git a/csaf_2_1/mandatoryTests/shared/docProductUtils.js b/csaf_2_1/mandatoryTests/shared/docProductUtils.js index b73e7947..7e9e776e 100644 --- a/csaf_2_1/mandatoryTests/shared/docProductUtils.js +++ b/csaf_2_1/mandatoryTests/shared/docProductUtils.js @@ -30,11 +30,20 @@ const productPathEntrySchema = /** @type {const} */ ({ }, }) +const productGroupSchema = /** @type {const} */ ({ + additionalProperties: true, + optionalProperties: { + group_id: { type: 'string' }, + summary: { type: 'string' }, + }, +}) + const productTreeSchema = /** @type {const} */ ({ additionalProperties: true, optionalProperties: { full_product_names: { elements: fullProductNameSchema }, product_paths: { elements: productPathEntrySchema }, + product_groups: { elements: productGroupSchema }, branches: { elements: branchSchema }, }, }) @@ -51,12 +60,14 @@ const inputSchema = /** @type {const} */ ({ * @typedef {import('ajv/dist/core.js').JTDDataType} Branch * @typedef {import('ajv/dist/core.js').JTDDataType} FullProductName * @typedef {import('ajv/dist/core.js').JTDDataType} ProductPathEntry + * @typedef {import('ajv/dist/core.js').JTDDataType} ProductGroup */ const validateDoc = ajv.compile(inputSchema) /** - * This method collects definitions of product ids and corresponding names and instancePaths in the given document and returns a result object. + * This method collects definitions of product ids and corresponding names and + * instancePaths in the given document and returns a result object. * @param {InputSchema} doc * @returns {{id: string, name: string, instancePath: string}[]} */ @@ -105,6 +116,46 @@ export const collectProductIdsFromFullProductPath = (doc) => { return entries } +/** + * This method collects definitions of group ids and corresponding names and + * instancePaths in the given document and returns a result object. + * @param {InputSchema} doc + * @returns {{id: string, name: string, instancePath: string}[]} + */ +export const collectGroupIdsFromProductTree = (doc) => { + const entries = + /** @type {{id: string, name: string, instancePath: string}[]} */ ([]) + + if (!validateDoc(doc)) { + return entries + } + + doc.product_tree?.product_groups?.forEach( + (productGroup, productGroupIndex) => { + if (productGroup.group_id) { + entries.push({ + id: productGroup.group_id, + name: productGroup.summary ?? '', + instancePath: `/product_tree/product_groups/${productGroupIndex}/group_id`, + }) + } + } + ) + + return entries +} + +/** + * Filters a list of references down to those that have no matching definition. + * A reference is considered unresolved if no entry in `entries` shares the same `id`. + * @param {{id: string}[]} entries + * @param {{id: string, instancePath: string}[]} refs + * @returns {{id: string, instancePath: string}[]} + */ +export const findMissingDefinitions = (entries, refs) => { + return refs.filter((ref) => !entries.some((e) => e.id === ref.id)) +} + /** * @param {Branch[]} branches * @param {{id: string, name: string, instancePath: string}[]} entries diff --git a/tests/csaf_2_1/oasis.js b/tests/csaf_2_1/oasis.js index 51e4d5bc..669f3de2 100644 --- a/tests/csaf_2_1/oasis.js +++ b/tests/csaf_2_1/oasis.js @@ -87,7 +87,6 @@ const skippedTests = new Set([ 'mandatory/oasis_csaf_tc-csaf_2_1-2024-6-1-03-01.json', 'mandatory/oasis_csaf_tc-csaf_2_1-2024-6-1-03-02.json', 'mandatory/oasis_csaf_tc-csaf_2_1-2024-6-1-27-08-02.json', - 'mandatory/oasis_csaf_tc-csaf_2_1-2024-6-1-04-03.json', 'recommended/oasis_csaf_tc-csaf_2_1-2024-6-2-38-13.json', 'recommended/oasis_csaf_tc-csaf_2_1-2024-6-2-38-02.json', ])