From bdcb9b23f6a3904c233354f1f49189dfcec1d957 Mon Sep 17 00:00:00 2001 From: bendo-eXX Date: Thu, 18 Jun 2026 09:12:00 +0200 Subject: [PATCH 1/3] feat(CSAF2.1): add mandatoryTest_6_1_4.js --- csaf_2_1/mandatoryTests.js | 2 +- .../mandatoryTests/mandatoryTest_6_1_4.js | 285 ++++++++++++++++++ .../mandatoryTests/shared/docProductUtils.js | 52 +++- tests/csaf_2_1/mandatoryTest_6_1_4.js | 8 + tests/csaf_2_1/oasis.js | 1 - 5 files changed, 345 insertions(+), 3 deletions(-) create mode 100644 csaf_2_1/mandatoryTests/mandatoryTest_6_1_4.js create mode 100644 tests/csaf_2_1/mandatoryTest_6_1_4.js 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..64c9be4f --- /dev/null +++ b/csaf_2_1/mandatoryTests/mandatoryTest_6_1_4.js @@ -0,0 +1,285 @@ +import { Ajv } from 'ajv/dist/jtd.js' +import { + collectGroupIdsFromProductTree, + findMissingDefinitions, +} from './shared/docProductUtils.js' + +const ajv = new Ajv() + +const groupIdsSchema = /** @type {const} */ ({ + additionalProperties: true, + optionalProperties: { + group_ids: { elements: { type: 'string' } }, + }, +}) + +const vulnerabilitySchema = /** @type {const} */ ({ + additionalProperties: true, + optionalProperties: { + first_known_exploitation_dates: { elements: groupIdsSchema }, + flags: { elements: groupIdsSchema }, + ids: { elements: groupIdsSchema }, + involvements: { elements: groupIdsSchema }, + notes: { elements: groupIdsSchema }, + remediations: { elements: groupIdsSchema }, + threats: { elements: groupIdsSchema }, + }, +}) + +/* + 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: { + document: { + additionalProperties: true, + optionalProperties: { + notes: { elements: groupIdsSchema }, + }, + }, + vulnerabilities: { elements: vulnerabilitySchema }, + }, +}) + +const validateInput = ajv.compile(inputSchema) + +/** + * @typedef {import('ajv/dist/core.js').JTDDataType} Vulnerability + * @typedef {import('ajv/dist/core.js').JTDDataType} InputSchema + * @typedef {{id: string, instancePath: string}} GroupId + */ + +/** + * This implements the mandatory test 6.1.4 of the CSAF 2.1 standard. + * + * @param {unknown} doc + */ +export function mandatoryTest_6_1_4(doc) { + const ctx = { + errors: + /** @type {Array<{ instancePath: string; message: string }>} */ ([]), + isValid: true, + } + + if (!validateInput(doc)) { + return ctx + } + + const groupIds = collectGroupIdsFromProductTree(/** @type {any} */ (doc)) + const groupIdRefs = 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 +} + +/** + * This method collects references to group ids and corresponding instancePaths in the given document. + * @param {InputSchema} doc + * @returns {GroupId[]} + */ +function collectGroupIds(doc) { + const entries = /** @type {GroupId[]} */ ([]) + + doc.document?.notes?.forEach((documentNote, documentNoteIndex) => { + collectRefsInGroupIds( + documentNote.group_ids, + `/document/notes/${documentNoteIndex}/group_ids`, + entries + ) + }) + + doc.vulnerabilities?.forEach((vulnerability, vulnerabilityIndex) => { + collectGroupRefsInFirstKnownExploitationDates( + `/vulnerabilities/${vulnerabilityIndex}/first_known_exploitation_dates`, + vulnerability, + entries + ) + collectGroupRefsInFlags( + `/vulnerabilities/${vulnerabilityIndex}/flags`, + vulnerability, + entries + ) + collectGroupRefsInIds( + `/vulnerabilities/${vulnerabilityIndex}/ids`, + vulnerability, + entries + ) + collectGroupRefsInInvolvements( + `/vulnerabilities/${vulnerabilityIndex}/involvements`, + vulnerability, + entries + ) + collectGroupRefsInNotes( + `/vulnerabilities/${vulnerabilityIndex}/notes`, + vulnerability, + entries + ) + collectGroupRefsInRemediations( + `/vulnerabilities/${vulnerabilityIndex}/remediations`, + vulnerability, + entries + ) + collectGroupRefsInThreats( + `/vulnerabilities/${vulnerabilityIndex}/threats`, + vulnerability, + entries + ) + }) + + return entries +} + +/** + * @param {string[] | undefined} groupIds + * @param {string} instancePath + * @param {GroupId[]} entries + */ +const collectRefsInGroupIds = (groupIds, instancePath, entries) => { + groupIds?.forEach((groupId, groupIdIndex) => { + if (groupId) { + entries.push({ + id: groupId, + instancePath: `${instancePath}/${groupIdIndex}`, + }) + } + }) +} + +/** + * @param {string} instancePath + * @param {Vulnerability} vulnerability + * @param {GroupId[]} entries + */ +const collectGroupRefsInFirstKnownExploitationDates = ( + instancePath, + vulnerability, + entries +) => { + vulnerability.first_known_exploitation_dates?.forEach( + (firstKnownExploitationDate, firstKnownExploitationDateIndex) => { + collectRefsInGroupIds( + firstKnownExploitationDate.group_ids, + `${instancePath}/${firstKnownExploitationDateIndex}/group_ids`, + entries + ) + } + ) +} + +/** + * @param {string} instancePath + * @param {Vulnerability} vulnerability + * @param {GroupId[]} entries + */ +const collectGroupRefsInFlags = (instancePath, vulnerability, entries) => { + vulnerability.flags?.forEach((flag, flagIndex) => { + collectRefsInGroupIds( + flag.group_ids, + `${instancePath}/${flagIndex}/group_ids`, + entries + ) + }) +} + +/** + * @param {string} instancePath + * @param {Vulnerability} vulnerability + * @param {GroupId[]} entries + */ +const collectGroupRefsInIds = (instancePath, vulnerability, entries) => { + vulnerability.ids?.forEach((id, idIndex) => { + collectRefsInGroupIds( + id.group_ids, + `${instancePath}/${idIndex}/group_ids`, + entries + ) + }) +} + +/** + * @param {string} instancePath + * @param {Vulnerability} vulnerability + * @param {GroupId[]} entries + */ +const collectGroupRefsInInvolvements = ( + instancePath, + vulnerability, + entries +) => { + vulnerability.involvements?.forEach((involvement, involvementIndex) => { + collectRefsInGroupIds( + involvement.group_ids, + `${instancePath}/${involvementIndex}/group_ids`, + entries + ) + }) +} + +/** + * @param {string} instancePath + * @param {Vulnerability} vulnerability + * @param {GroupId[]} entries + */ +const collectGroupRefsInNotes = (instancePath, vulnerability, entries) => { + vulnerability.notes?.forEach((note, noteIndex) => { + collectRefsInGroupIds( + note.group_ids, + `${instancePath}/${noteIndex}/group_ids`, + entries + ) + }) +} + +/** + * @param {string} instancePath + * @param {Vulnerability} vulnerability + * @param {GroupId[]} entries + */ +const collectGroupRefsInRemediations = ( + instancePath, + vulnerability, + entries +) => { + vulnerability.remediations?.forEach((remediation, remediationIndex) => { + collectRefsInGroupIds( + remediation.group_ids, + `${instancePath}/${remediationIndex}/group_ids`, + entries + ) + }) +} + +/** + * @param {string} instancePath + * @param {Vulnerability} vulnerability + * @param {GroupId[]} entries + */ +const collectGroupRefsInThreats = (instancePath, vulnerability, entries) => { + vulnerability.threats?.forEach((threat, threatIndex) => { + collectRefsInGroupIds( + threat.group_ids, + `${instancePath}/${threatIndex}/group_ids`, + entries + ) + }) +} diff --git a/csaf_2_1/mandatoryTests/shared/docProductUtils.js b/csaf_2_1/mandatoryTests/shared/docProductUtils.js index b73e7947..c3811ea2 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,45 @@ 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 +} + +/** + * Returns all references that don't have a matching definition by 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/mandatoryTest_6_1_4.js b/tests/csaf_2_1/mandatoryTest_6_1_4.js new file mode 100644 index 00000000..a61a7aea --- /dev/null +++ b/tests/csaf_2_1/mandatoryTest_6_1_4.js @@ -0,0 +1,8 @@ +import assert from 'node:assert' +import { mandatoryTest_6_1_4 } from '../../csaf_2_1/mandatoryTests/mandatoryTest_6_1_4.js' + +describe('mandatoryTest_6_1_4', function () { + it('only runs on relevant documents', function () { + assert.equal(mandatoryTest_6_1_4({ document: 'mydoc' }).isValid, true) + }) +}) 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', ]) From ec45bc3999b6c92badfa591f47af58d739c0ccf2 Mon Sep 17 00:00:00 2001 From: bendo-eXX Date: Thu, 18 Jun 2026 11:48:37 +0200 Subject: [PATCH 2/3] feat(CSAF2.1): change to walkPaths --- .../mandatoryTests/mandatoryTest_6_1_4.js | 259 ++---------------- tests/csaf_2_1/mandatoryTest_6_1_4.js | 8 - 2 files changed, 23 insertions(+), 244 deletions(-) delete mode 100644 tests/csaf_2_1/mandatoryTest_6_1_4.js diff --git a/csaf_2_1/mandatoryTests/mandatoryTest_6_1_4.js b/csaf_2_1/mandatoryTests/mandatoryTest_6_1_4.js index 64c9be4f..9e2dc073 100644 --- a/csaf_2_1/mandatoryTests/mandatoryTest_6_1_4.js +++ b/csaf_2_1/mandatoryTests/mandatoryTest_6_1_4.js @@ -1,55 +1,10 @@ -import { Ajv } from 'ajv/dist/jtd.js' +import { walkPath } from '../../lib/walkPaths.js' import { collectGroupIdsFromProductTree, findMissingDefinitions, } from './shared/docProductUtils.js' -const ajv = new Ajv() - -const groupIdsSchema = /** @type {const} */ ({ - additionalProperties: true, - optionalProperties: { - group_ids: { elements: { type: 'string' } }, - }, -}) - -const vulnerabilitySchema = /** @type {const} */ ({ - additionalProperties: true, - optionalProperties: { - first_known_exploitation_dates: { elements: groupIdsSchema }, - flags: { elements: groupIdsSchema }, - ids: { elements: groupIdsSchema }, - involvements: { elements: groupIdsSchema }, - notes: { elements: groupIdsSchema }, - remediations: { elements: groupIdsSchema }, - threats: { elements: groupIdsSchema }, - }, -}) - -/* - 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: { - document: { - additionalProperties: true, - optionalProperties: { - notes: { elements: groupIdsSchema }, - }, - }, - vulnerabilities: { elements: vulnerabilitySchema }, - }, -}) - -const validateInput = ajv.compile(inputSchema) - /** - * @typedef {import('ajv/dist/core.js').JTDDataType} Vulnerability - * @typedef {import('ajv/dist/core.js').JTDDataType} InputSchema * @typedef {{id: string, instancePath: string}} GroupId */ @@ -58,19 +13,15 @@ const validateInput = ajv.compile(inputSchema) * * @param {unknown} doc */ -export function mandatoryTest_6_1_4(doc) { +export async function mandatoryTest_6_1_4(doc) { const ctx = { errors: /** @type {Array<{ instancePath: string; message: string }>} */ ([]), isValid: true, } - if (!validateInput(doc)) { - return ctx - } - const groupIds = collectGroupIdsFromProductTree(/** @type {any} */ (doc)) - const groupIdRefs = collectGroupIds(doc) + const groupIdRefs = await collectGroupIds(doc) const missingGroupDefinitions = /** @type {{id: string, instancePath: string}[]} */ ( findMissingDefinitions(groupIds, groupIdRefs) @@ -93,193 +44,29 @@ export function mandatoryTest_6_1_4(doc) { } /** - * This method collects references to group ids and corresponding instancePaths in the given document. - * @param {InputSchema} doc - * @returns {GroupId[]} + * Collects all group_id references and their instancePaths from the document. + * @param {unknown} doc + * @returns {Promise} */ -function collectGroupIds(doc) { +async function collectGroupIds(doc) { const entries = /** @type {GroupId[]} */ ([]) - doc.document?.notes?.forEach((documentNote, documentNoteIndex) => { - collectRefsInGroupIds( - documentNote.group_ids, - `/document/notes/${documentNoteIndex}/group_ids`, - entries - ) - }) - - doc.vulnerabilities?.forEach((vulnerability, vulnerabilityIndex) => { - collectGroupRefsInFirstKnownExploitationDates( - `/vulnerabilities/${vulnerabilityIndex}/first_known_exploitation_dates`, - vulnerability, - entries - ) - collectGroupRefsInFlags( - `/vulnerabilities/${vulnerabilityIndex}/flags`, - vulnerability, - entries - ) - collectGroupRefsInIds( - `/vulnerabilities/${vulnerabilityIndex}/ids`, - vulnerability, - entries - ) - collectGroupRefsInInvolvements( - `/vulnerabilities/${vulnerabilityIndex}/involvements`, - vulnerability, - entries - ) - collectGroupRefsInNotes( - `/vulnerabilities/${vulnerabilityIndex}/notes`, - vulnerability, - entries - ) - collectGroupRefsInRemediations( - `/vulnerabilities/${vulnerabilityIndex}/remediations`, - vulnerability, - entries - ) - collectGroupRefsInThreats( - `/vulnerabilities/${vulnerabilityIndex}/threats`, - vulnerability, - entries - ) - }) + 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 } - -/** - * @param {string[] | undefined} groupIds - * @param {string} instancePath - * @param {GroupId[]} entries - */ -const collectRefsInGroupIds = (groupIds, instancePath, entries) => { - groupIds?.forEach((groupId, groupIdIndex) => { - if (groupId) { - entries.push({ - id: groupId, - instancePath: `${instancePath}/${groupIdIndex}`, - }) - } - }) -} - -/** - * @param {string} instancePath - * @param {Vulnerability} vulnerability - * @param {GroupId[]} entries - */ -const collectGroupRefsInFirstKnownExploitationDates = ( - instancePath, - vulnerability, - entries -) => { - vulnerability.first_known_exploitation_dates?.forEach( - (firstKnownExploitationDate, firstKnownExploitationDateIndex) => { - collectRefsInGroupIds( - firstKnownExploitationDate.group_ids, - `${instancePath}/${firstKnownExploitationDateIndex}/group_ids`, - entries - ) - } - ) -} - -/** - * @param {string} instancePath - * @param {Vulnerability} vulnerability - * @param {GroupId[]} entries - */ -const collectGroupRefsInFlags = (instancePath, vulnerability, entries) => { - vulnerability.flags?.forEach((flag, flagIndex) => { - collectRefsInGroupIds( - flag.group_ids, - `${instancePath}/${flagIndex}/group_ids`, - entries - ) - }) -} - -/** - * @param {string} instancePath - * @param {Vulnerability} vulnerability - * @param {GroupId[]} entries - */ -const collectGroupRefsInIds = (instancePath, vulnerability, entries) => { - vulnerability.ids?.forEach((id, idIndex) => { - collectRefsInGroupIds( - id.group_ids, - `${instancePath}/${idIndex}/group_ids`, - entries - ) - }) -} - -/** - * @param {string} instancePath - * @param {Vulnerability} vulnerability - * @param {GroupId[]} entries - */ -const collectGroupRefsInInvolvements = ( - instancePath, - vulnerability, - entries -) => { - vulnerability.involvements?.forEach((involvement, involvementIndex) => { - collectRefsInGroupIds( - involvement.group_ids, - `${instancePath}/${involvementIndex}/group_ids`, - entries - ) - }) -} - -/** - * @param {string} instancePath - * @param {Vulnerability} vulnerability - * @param {GroupId[]} entries - */ -const collectGroupRefsInNotes = (instancePath, vulnerability, entries) => { - vulnerability.notes?.forEach((note, noteIndex) => { - collectRefsInGroupIds( - note.group_ids, - `${instancePath}/${noteIndex}/group_ids`, - entries - ) - }) -} - -/** - * @param {string} instancePath - * @param {Vulnerability} vulnerability - * @param {GroupId[]} entries - */ -const collectGroupRefsInRemediations = ( - instancePath, - vulnerability, - entries -) => { - vulnerability.remediations?.forEach((remediation, remediationIndex) => { - collectRefsInGroupIds( - remediation.group_ids, - `${instancePath}/${remediationIndex}/group_ids`, - entries - ) - }) -} - -/** - * @param {string} instancePath - * @param {Vulnerability} vulnerability - * @param {GroupId[]} entries - */ -const collectGroupRefsInThreats = (instancePath, vulnerability, entries) => { - vulnerability.threats?.forEach((threat, threatIndex) => { - collectRefsInGroupIds( - threat.group_ids, - `${instancePath}/${threatIndex}/group_ids`, - entries - ) - }) -} diff --git a/tests/csaf_2_1/mandatoryTest_6_1_4.js b/tests/csaf_2_1/mandatoryTest_6_1_4.js deleted file mode 100644 index a61a7aea..00000000 --- a/tests/csaf_2_1/mandatoryTest_6_1_4.js +++ /dev/null @@ -1,8 +0,0 @@ -import assert from 'node:assert' -import { mandatoryTest_6_1_4 } from '../../csaf_2_1/mandatoryTests/mandatoryTest_6_1_4.js' - -describe('mandatoryTest_6_1_4', function () { - it('only runs on relevant documents', function () { - assert.equal(mandatoryTest_6_1_4({ document: 'mydoc' }).isValid, true) - }) -}) From 2f41fa872064331b9faff05a06b4df8362d23006 Mon Sep 17 00:00:00 2001 From: bendo-eXX Date: Fri, 19 Jun 2026 08:43:01 +0200 Subject: [PATCH 3/3] feat(CSAF2.1): change function description --- csaf_2_1/mandatoryTests/shared/docProductUtils.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/csaf_2_1/mandatoryTests/shared/docProductUtils.js b/csaf_2_1/mandatoryTests/shared/docProductUtils.js index c3811ea2..7e9e776e 100644 --- a/csaf_2_1/mandatoryTests/shared/docProductUtils.js +++ b/csaf_2_1/mandatoryTests/shared/docProductUtils.js @@ -146,7 +146,8 @@ export const collectGroupIdsFromProductTree = (doc) => { } /** - * Returns all references that don't have a matching definition by id. + * 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}[]}