From 69e90830d9d635d433ffd954314d687b32bd9ce0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Burgd=C3=B6rfer?= Date: Wed, 26 Feb 2025 16:42:28 +0100 Subject: [PATCH] feat: add mandatory test 6.1.36 --- csaf_2_1/mandatoryTests.js | 1 + .../mandatoryTests/mandatoryTest_6_1_36.js | 221 ++++++++++++++++++ tests/csaf_2_1/oasis.js | 1 - 3 files changed, 222 insertions(+), 1 deletion(-) create mode 100644 csaf_2_1/mandatoryTests/mandatoryTest_6_1_36.js diff --git a/csaf_2_1/mandatoryTests.js b/csaf_2_1/mandatoryTests.js index 9d16e7d8..5ee51ef7 100644 --- a/csaf_2_1/mandatoryTests.js +++ b/csaf_2_1/mandatoryTests.js @@ -40,6 +40,7 @@ export { mandatoryTest_6_1_11 } from './mandatoryTests/mandatoryTest_6_1_11.js' export { mandatoryTest_6_1_13 } from './mandatoryTests/mandatoryTest_6_1_13.js' export { mandatoryTest_6_1_34 } from './mandatoryTests/mandatoryTest_6_1_34.js' export { mandatoryTest_6_1_35 } from './mandatoryTests/mandatoryTest_6_1_35.js' +export { mandatoryTest_6_1_36 } from './mandatoryTests/mandatoryTest_6_1_36.js' export { mandatoryTest_6_1_37 } from './mandatoryTests/mandatoryTest_6_1_37.js' export { mandatoryTest_6_1_38 } from './mandatoryTests/mandatoryTests_6_1_38.js' export { mandatoryTest_6_1_39 } from './mandatoryTests/mandatoryTest_6_1_39.js' diff --git a/csaf_2_1/mandatoryTests/mandatoryTest_6_1_36.js b/csaf_2_1/mandatoryTests/mandatoryTest_6_1_36.js new file mode 100644 index 00000000..2bc0a558 --- /dev/null +++ b/csaf_2_1/mandatoryTests/mandatoryTest_6_1_36.js @@ -0,0 +1,221 @@ +import Ajv from 'ajv/dist/jtd.js' + +const ajv = new Ajv() + +/** + * @typedef {'workaround' + * | 'mitigation' + * | 'vendor_fix' + * | 'optional_patch' + * | 'none_available' + * | 'fix_planned' + * | 'no_fix_planned'} Category + */ + +/** + * @typedef {'first_affected' + * | 'known_affected' + * | 'last_affected' + * | 'known_not_affected' + * | 'first_fixed' + * | 'fixed' + * | 'under_investigation'} ProductStatus + */ + +/* + The spec groups the product statuses in groups. This grouping is + expressed in this object. + */ +const productStatus = /** + * @type {const} + * @satisfies {Record} + */ ({ + affected: ['first_affected', 'known_affected', 'last_affected'], + notAffected: ['known_not_affected'], + fixed: ['first_fixed', 'fixed'], + underInvestigation: ['under_investigation'], +}) + +/** + * This map holds prohibited category / product status combinations. + * See https://github.com/oasis-tcs/csaf/blob/master/csaf_2.1/prose/share/csaf-v2.1-draft.md#324131-vulnerabilities-property---remediations---category- + * + * @type {Map>} + */ +const prohibitionRuleMap = new Map( + /** @satisfies {Array<[Category, ProductStatus[]]>} */ ([ + ['workaround', [...productStatus.notAffected, ...productStatus.fixed]], + ['mitigation', [...productStatus.notAffected, ...productStatus.fixed]], + ['vendor_fix', [...productStatus.notAffected, ...productStatus.fixed]], + ['optional_patch', [...productStatus.affected]], + ['none_available', [...productStatus.notAffected, ...productStatus.fixed]], + ['fix_planned', [...productStatus.fixed]], + ['no_fix_planned', [...productStatus.fixed]], + ]).map((e) => [e[0], new Set(e[1])]) +) + +const remediationSchema = /** @type {const} */ ({ + additionalProperties: true, + optionalProperties: { + group_ids: { + elements: { + type: 'string', + }, + }, + product_ids: { + elements: { + type: 'string', + }, + }, + category: { 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: { + product_groups: { + elements: { + additionalProperties: true, + optionalProperties: { + group_id: { type: 'string' }, + product_ids: { + elements: { + type: 'string', + }, + }, + }, + }, + }, + }, + }, + }, + properties: { + vulnerabilities: { + elements: { + additionalProperties: true, + optionalProperties: { + remediations: { + elements: remediationSchema, + }, + product_status: { + additionalProperties: true, + optionalProperties: { + first_affected: { elements: { type: 'string' } }, + known_affected: { elements: { type: 'string' } }, + last_affected: { elements: { type: 'string' } }, + known_not_affected: { elements: { type: 'string' } }, + first_fixed: { elements: { type: 'string' } }, + fixed: { elements: { type: 'string' } }, + under_investigation: { elements: { type: 'string' } }, + }, + }, + }, + }, + }, + }, +}) + +const validate = ajv.compile(inputSchema) + +/** + * This implements the mandatory test 6.1.36 of the CSAF 2.1 standard. + * + * @param {any} doc + */ +export function mandatoryTest_6_1_36(doc) { + /* + The `ctx` variable holds the state that is accumulated during the test ran and is + finally returned by the function. + */ + const ctx = { + /** @type {Array<{ instancePath: string; message: string }>} */ + errors: [], + isValid: true, + } + + if (!validate(doc)) { + return ctx + } + + for (const [ + vulnerabilityIndex, + vulnerability, + ] of doc.vulnerabilities.entries()) { + vulnerability.remediations?.forEach((remediation, remediationIndex) => { + const category = remediation.category + if (!category) return + + /** + * This map holds the discovered product ids for the remediation and maps them to + * the set of corresponding product status names. Later we can check this map to + * find out if there are any contradicting remediations. + * + * @type {Map>} + */ + const productToProductStatusNamesMap = new Map() + + /** + * This function adds all product status names for the given product id to the + * `productMap`. If the product does not yet exist in the map, it is added. + * + * @param {string} id + */ + const collectProductStatusNames = (id) => { + const productStatusNames = + /* + To speed things up we first check if the product status names where already + collected and do not search again. The product names are always for a + product in the same vulnerability. + */ + productToProductStatusNamesMap.get(id) ?? + new Set( + /** @type {string[]} */ ( + Object.entries(vulnerability.product_status ?? {}) + .filter((e) => + Array.isArray(e[1]) ? e[1].includes(id) : false + ) + .map((e) => e[0]) + ) + ) + productToProductStatusNamesMap.set(id, productStatusNames) + } + + remediation.product_ids?.forEach(collectProductStatusNames) + + remediation.group_ids?.forEach((id) => { + const group = doc.product_tree?.product_groups?.find( + (g) => g.group_id === id + ) + if (!group) return + group.product_ids?.forEach(collectProductStatusNames) + }) + + for (const [ + productId, + productStatusNames, + ] of productToProductStatusNamesMap) { + for (const productStatus of productStatusNames) { + if (prohibitionRuleMap.get(category)?.has(productStatus)) { + ctx.errors.push({ + instancePath: `/vulnerabilities/${vulnerabilityIndex}/remediations/${remediationIndex}`, + message: `contradicting remediation product status combination for product id "${productId}": ${category}, ${productStatus}`, + }) + ctx.isValid = false + } + } + } + }) + } + + return ctx +} diff --git a/tests/csaf_2_1/oasis.js b/tests/csaf_2_1/oasis.js index ec6df017..82963eb5 100644 --- a/tests/csaf_2_1/oasis.js +++ b/tests/csaf_2_1/oasis.js @@ -15,7 +15,6 @@ const excluded = [ '6.1.10', '6.1.14', '6.1.16', - '6.1.36', '6.1.42', '6.1.43', '6.1.44',