From 17a62b1719175308062a42540075ea3c735cbd16 Mon Sep 17 00:00:00 2001 From: bendo-eXX Date: Wed, 29 Apr 2026 11:11:21 +0200 Subject: [PATCH 1/4] fix(CSAF2.1): update test 6.2.8 to the new csaf 2.1 schema --- .../recommendedTests/recommendedTest_6_2_8.js | 7 +- .../shared/checkForUnsafeHashAlgorithms.js | 58 ++++++++ csaf_2_1/shared/csafHelpers/walkHashes.js | 138 ++++++++++++++++++ tests/csaf_2_1/recommendedTest_6_2_8.js | 95 ++++++++++++ 4 files changed, 295 insertions(+), 3 deletions(-) create mode 100644 csaf_2_1/recommendedTests/shared/checkForUnsafeHashAlgorithms.js create mode 100644 csaf_2_1/shared/csafHelpers/walkHashes.js create mode 100644 tests/csaf_2_1/recommendedTest_6_2_8.js diff --git a/csaf_2_1/recommendedTests/recommendedTest_6_2_8.js b/csaf_2_1/recommendedTests/recommendedTest_6_2_8.js index b4d10e86..97d91777 100644 --- a/csaf_2_1/recommendedTests/recommendedTest_6_2_8.js +++ b/csaf_2_1/recommendedTests/recommendedTest_6_2_8.js @@ -1,8 +1,9 @@ -import { optionalTest_6_2_8 } from '../../optionalTests.js' +import checkForUnsafeHashAlgorithms from './shared/checkForUnsafeHashAlgorithms.js' /** - * @param {unknown} doc + * This implements the recommended test 6.2.8 of the CSAF 2.1 standard. + * @param {any} doc */ export function recommendedTest_6_2_8(doc) { - return optionalTest_6_2_8(doc) + return checkForUnsafeHashAlgorithms(doc, 'md5') } diff --git a/csaf_2_1/recommendedTests/shared/checkForUnsafeHashAlgorithms.js b/csaf_2_1/recommendedTests/shared/checkForUnsafeHashAlgorithms.js new file mode 100644 index 00000000..b8d23516 --- /dev/null +++ b/csaf_2_1/recommendedTests/shared/checkForUnsafeHashAlgorithms.js @@ -0,0 +1,58 @@ +import Ajv from 'ajv/dist/jtd.js' +import { walkHashes } from '../../shared/csafHelpers/walkHashes.js' + +const ajv = new Ajv() + +const hashSchema = /** @type {const} */ ({ + additionalProperties: true, + properties: { + file_hashes: { + elements: { + additionalProperties: true, + properties: {}, + }, + }, + }, +}) + +const validateHash = ajv.compile(hashSchema) + +/** + * @param {any} doc + * @param {string} hashName + */ +export default function checkForUnsafeHashAlgorithms(doc, hashName) { + const ctx = { + warnings: + /** @type {Array<{ instancePath: string; message: string }>} */ ([]), + } + + walkHashes(doc, ({ path, hash }) => { + if (!validateHash(hash)) return + const hashSet = getHashAlgorithmSet(hash) + if (hashSet.has(hashName) && hashSet.size === 1) { + ctx.warnings.push({ + instancePath: path, + message: `use of ${hashName} as the only hash algorithm`, + }) + } + }) + + return ctx +} + +/** + * + * @param {{ file_hashes: Array<{ algorithm?: unknown }> }} hash + * @returns + */ +function getHashAlgorithmSet(hash) { + return new Set( + hash.file_hashes + .map((h) => h.algorithm) + .filter( + /** @returns {v is string} */ + (v) => typeof v === 'string' + ) + ) +} diff --git a/csaf_2_1/shared/csafHelpers/walkHashes.js b/csaf_2_1/shared/csafHelpers/walkHashes.js new file mode 100644 index 00000000..bb0c69e7 --- /dev/null +++ b/csaf_2_1/shared/csafHelpers/walkHashes.js @@ -0,0 +1,138 @@ +import Ajv from 'ajv/dist/jtd.js' + +const ajv = new Ajv() + +const hashSchema = /** @type {const} */ ({ + additionalProperties: true, + optionalProperties: { + file_hashes: { + elements: { additionalProperties: true, properties: {} }, + }, + }, +}) + +const fullProductNameSchema = /** @type {const} */ ({ + additionalProperties: true, + optionalProperties: { + product_identification_helper: { + additionalProperties: true, + optionalProperties: { + hashes: { elements: hashSchema }, + }, + }, + }, +}) + +const branchSchema = /** @type {const} */ ({ + additionalProperties: true, + optionalProperties: { + branches: { + elements: { + additionalProperties: true, + properties: {}, + }, + }, + product: fullProductNameSchema, + }, +}) + +const productPathSchema = /** @type {const} */ ({ + additionalProperties: true, + optionalProperties: { + full_product_name: fullProductNameSchema, + }, +}) + +const inputSchema = /** @type {const} */ ({ + additionalProperties: true, + optionalProperties: { + product_tree: { + additionalProperties: true, + optionalProperties: { + branches: { + elements: branchSchema, + }, + full_product_names: { + elements: fullProductNameSchema, + }, + product_paths: { + elements: productPathSchema, + }, + }, + }, + }, +}) + +const validateInput = ajv.compile(inputSchema) +const validateFullProductName = ajv.compile(fullProductNameSchema) +const validateBranch = ajv.compile(branchSchema) +const validateProductPath = ajv.compile(productPathSchema) + +/** + * @param {any} doc + * @param {(params: { path: string; hash: {} }) => void} onHashFound + */ +export function walkHashes(doc, onHashFound) { + if (!validateInput(doc)) { + return + } + + doc.product_tree?.full_product_names?.forEach( + (fullProductName, fullProductNameIndex) => { + if (!validateFullProductName(fullProductName)) { + return + } + + fullProductName.product_identification_helper?.hashes?.forEach( + (hash, hashIndex) => { + onHashFound({ + path: `/product_tree/full_product_names/${fullProductNameIndex}/product_identification_helper/hashes/${hashIndex}`, + hash, + }) + } + ) + } + ) + + /** + * @param {string} prefix + * @param {unknown[]} branches + */ + const checkBranches = (prefix, branches) => { + branches.forEach((branch, branchIndex) => { + if (!validateBranch(branch)) { + return + } + + branch.product?.product_identification_helper?.hashes?.forEach( + (hash, hashIndex) => { + onHashFound({ + path: `${prefix}${branchIndex}/product/product_identification_helper/hashes/${hashIndex}`, + hash, + }) + } + ) + checkBranches( + `${prefix}${branchIndex}/branches/`, + Array.isArray(branch.branches) ? branch.branches : [] + ) + }) + } + + checkBranches('/product_tree/branches/', doc.product_tree?.branches ?? []) + + doc.product_tree?.product_paths?.forEach((productPath, productPathIndex) => { + if (!validateProductPath(productPath)) { + return + } + + productPath.full_product_name?.product_identification_helper?.hashes?.forEach( + (hash, hashIndex) => { + onHashFound({ + path: `/product_tree/product_paths/${productPathIndex}/full_product_name/product_identification_helper/hashes/${hashIndex}`, + hash, + }) + } + ) + }) +} diff --git a/tests/csaf_2_1/recommendedTest_6_2_8.js b/tests/csaf_2_1/recommendedTest_6_2_8.js new file mode 100644 index 00000000..8f8130cc --- /dev/null +++ b/tests/csaf_2_1/recommendedTest_6_2_8.js @@ -0,0 +1,95 @@ +import assert from 'node:assert' +import { recommendedTest_6_2_8 } from '../../csaf_2_1/recommendedTests.js' + +/** Helper: build a hash entry with the given algorithms */ +function makeHash(/** @type {string[]} */ algorithms) { + return { + file_hashes: algorithms.map((alg) => ({ algorithm: alg, value: 'aabbcc' })), + filename: 'product.so', + } +} + +describe('recommendedTest_6_2_8', function () { + it('only runs on relevant documents', function () { + assert.equal( + recommendedTest_6_2_8({ vulnerabilities: 'mydoc' }).warnings.length, + 0 + ) + }) + + it('warns when md5 is the only algorithm in branches', function () { + const doc = { + product_tree: { + branches: [ + { + category: 'vendor', + name: 'Vendor A', + branches: [ + { + category: 'product_name', + name: 'Product A', + product: { + product_id: 'CSAFPID-0001', + name: 'Vendor A Product A', + product_identification_helper: { + hashes: [makeHash(['md5', 'sha256'])], + }, + }, + }, + { + category: 'product_name', + name: 'Product B', + product: { + product_id: 'CSAFPID-0002', + name: 'Vendor A Product B', + product_identification_helper: { + hashes: [makeHash(['md5'])], + }, + }, + }, + ], + }, + ], + }, + } + const result = recommendedTest_6_2_8(doc) + assert.equal(result.warnings.length, 1) + assert.equal( + result.warnings[0].instancePath, + '/product_tree/branches/0/branches/1/product/product_identification_helper/hashes/0' + ) + }) + + it('warns when md5 is the only algorithm in product_paths', function () { + const doc = { + product_tree: { + product_paths: [ + { + full_product_name: { + name: 'Product A', + product_id: 'CSAFPID-0001', + product_identification_helper: { + hashes: [makeHash(['md5', 'sha256'])], + }, + }, + }, + { + full_product_name: { + name: 'Product B', + product_id: 'CSAFPID-0002', + product_identification_helper: { + hashes: [makeHash(['md5'])], + }, + }, + }, + ], + }, + } + const result = recommendedTest_6_2_8(doc) + assert.equal(result.warnings.length, 1) + assert.equal( + result.warnings[0].instancePath, + '/product_tree/product_paths/1/full_product_name/product_identification_helper/hashes/0' + ) + }) +}) From 8868f2ae33371783e2be329a222c23fa54446161 Mon Sep 17 00:00:00 2001 From: bendo-eXX Date: Wed, 29 Apr 2026 11:41:48 +0200 Subject: [PATCH 2/4] fix(CSAF2.1): extends instancePath to /file_hashes --- csaf_2_1/shared/csafHelpers/walkHashes.js | 6 +++--- tests/csaf_2_1/recommendedTest_6_2_8.js | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/csaf_2_1/shared/csafHelpers/walkHashes.js b/csaf_2_1/shared/csafHelpers/walkHashes.js index bb0c69e7..314f6a15 100644 --- a/csaf_2_1/shared/csafHelpers/walkHashes.js +++ b/csaf_2_1/shared/csafHelpers/walkHashes.js @@ -86,7 +86,7 @@ export function walkHashes(doc, onHashFound) { fullProductName.product_identification_helper?.hashes?.forEach( (hash, hashIndex) => { onHashFound({ - path: `/product_tree/full_product_names/${fullProductNameIndex}/product_identification_helper/hashes/${hashIndex}`, + path: `/product_tree/full_product_names/${fullProductNameIndex}/product_identification_helper/hashes/${hashIndex}/file_hashes`, hash, }) } @@ -107,7 +107,7 @@ export function walkHashes(doc, onHashFound) { branch.product?.product_identification_helper?.hashes?.forEach( (hash, hashIndex) => { onHashFound({ - path: `${prefix}${branchIndex}/product/product_identification_helper/hashes/${hashIndex}`, + path: `${prefix}${branchIndex}/product/product_identification_helper/hashes/${hashIndex}/file_hashes`, hash, }) } @@ -129,7 +129,7 @@ export function walkHashes(doc, onHashFound) { productPath.full_product_name?.product_identification_helper?.hashes?.forEach( (hash, hashIndex) => { onHashFound({ - path: `/product_tree/product_paths/${productPathIndex}/full_product_name/product_identification_helper/hashes/${hashIndex}`, + path: `/product_tree/product_paths/${productPathIndex}/full_product_name/product_identification_helper/hashes/${hashIndex}/file_hashes`, hash, }) } diff --git a/tests/csaf_2_1/recommendedTest_6_2_8.js b/tests/csaf_2_1/recommendedTest_6_2_8.js index 8f8130cc..5a67c1f6 100644 --- a/tests/csaf_2_1/recommendedTest_6_2_8.js +++ b/tests/csaf_2_1/recommendedTest_6_2_8.js @@ -56,7 +56,7 @@ describe('recommendedTest_6_2_8', function () { assert.equal(result.warnings.length, 1) assert.equal( result.warnings[0].instancePath, - '/product_tree/branches/0/branches/1/product/product_identification_helper/hashes/0' + '/product_tree/branches/0/branches/1/product/product_identification_helper/hashes/0/file_hashes' ) }) @@ -89,7 +89,7 @@ describe('recommendedTest_6_2_8', function () { assert.equal(result.warnings.length, 1) assert.equal( result.warnings[0].instancePath, - '/product_tree/product_paths/1/full_product_name/product_identification_helper/hashes/0' + '/product_tree/product_paths/1/full_product_name/product_identification_helper/hashes/0/file_hashes' ) }) }) From 29a04061ca41d17e52d0139eb186c6dad4ceb1f9 Mon Sep 17 00:00:00 2001 From: bendo-eXX Date: Thu, 30 Apr 2026 09:51:44 +0200 Subject: [PATCH 3/4] fix(CSAF2.1): update import --- .../recommendedTests/shared/checkForUnsafeHashAlgorithms.js | 2 +- csaf_2_1/shared/csafHelpers/walkHashes.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/csaf_2_1/recommendedTests/shared/checkForUnsafeHashAlgorithms.js b/csaf_2_1/recommendedTests/shared/checkForUnsafeHashAlgorithms.js index b8d23516..3b157233 100644 --- a/csaf_2_1/recommendedTests/shared/checkForUnsafeHashAlgorithms.js +++ b/csaf_2_1/recommendedTests/shared/checkForUnsafeHashAlgorithms.js @@ -1,4 +1,4 @@ -import Ajv from 'ajv/dist/jtd.js' +import { Ajv } from 'ajv/dist/jtd.js' import { walkHashes } from '../../shared/csafHelpers/walkHashes.js' const ajv = new Ajv() diff --git a/csaf_2_1/shared/csafHelpers/walkHashes.js b/csaf_2_1/shared/csafHelpers/walkHashes.js index 314f6a15..8591290b 100644 --- a/csaf_2_1/shared/csafHelpers/walkHashes.js +++ b/csaf_2_1/shared/csafHelpers/walkHashes.js @@ -1,4 +1,4 @@ -import Ajv from 'ajv/dist/jtd.js' +import { Ajv } from 'ajv/dist/jtd.js' const ajv = new Ajv() From e6e25f26eb57ef6c072d7c82c9d9c19dd804efc6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Burgd=C3=B6rfer?= Date: Thu, 30 Apr 2026 11:09:38 +0200 Subject: [PATCH 4/4] chore: tighten types --- csaf_2_1/recommendedTests/recommendedTest_6_2_8.js | 2 +- .../recommendedTests/shared/checkForUnsafeHashAlgorithms.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/csaf_2_1/recommendedTests/recommendedTest_6_2_8.js b/csaf_2_1/recommendedTests/recommendedTest_6_2_8.js index 97d91777..4e2c3da9 100644 --- a/csaf_2_1/recommendedTests/recommendedTest_6_2_8.js +++ b/csaf_2_1/recommendedTests/recommendedTest_6_2_8.js @@ -2,7 +2,7 @@ import checkForUnsafeHashAlgorithms from './shared/checkForUnsafeHashAlgorithms. /** * This implements the recommended test 6.2.8 of the CSAF 2.1 standard. - * @param {any} doc + * @param {unknown} doc */ export function recommendedTest_6_2_8(doc) { return checkForUnsafeHashAlgorithms(doc, 'md5') diff --git a/csaf_2_1/recommendedTests/shared/checkForUnsafeHashAlgorithms.js b/csaf_2_1/recommendedTests/shared/checkForUnsafeHashAlgorithms.js index 3b157233..5d484950 100644 --- a/csaf_2_1/recommendedTests/shared/checkForUnsafeHashAlgorithms.js +++ b/csaf_2_1/recommendedTests/shared/checkForUnsafeHashAlgorithms.js @@ -18,7 +18,7 @@ const hashSchema = /** @type {const} */ ({ const validateHash = ajv.compile(hashSchema) /** - * @param {any} doc + * @param {unknown} doc * @param {string} hashName */ export default function checkForUnsafeHashAlgorithms(doc, hashName) {