diff --git a/csaf_2_1/informativeTests.js b/csaf_2_1/informativeTests.js index 0ecb389f..e15078a9 100644 --- a/csaf_2_1/informativeTests.js +++ b/csaf_2_1/informativeTests.js @@ -1,6 +1,5 @@ export { informativeTest_6_3_3, - informativeTest_6_3_5, informativeTest_6_3_6, informativeTest_6_3_7, informativeTest_6_3_8, @@ -11,5 +10,6 @@ export { export { informativeTest_6_3_1 } from './informativeTests/informativeTest_6_3_1.js' export { informativeTest_6_3_2 } from './informativeTests/informativeTest_6_3_2.js' export { informativeTest_6_3_4 } from './informativeTests/informativeTest_6_3_4.js' +export { informativeTest_6_3_5 } from './informativeTests/informativeTest_6_3_5.js' export { informativeTest_6_3_12 } from './informativeTests/informativeTest_6_3_12.js' export { informativeTest_6_3_18 } from './informativeTests/informativeTest_6_3_18.js' diff --git a/csaf_2_1/informativeTests/informativeTest_6_3_5.js b/csaf_2_1/informativeTests/informativeTest_6_3_5.js new file mode 100644 index 00000000..c9683eb6 --- /dev/null +++ b/csaf_2_1/informativeTests/informativeTest_6_3_5.js @@ -0,0 +1,24 @@ +import { walkHashes } from '../shared/csafHelpers/walkHashes.js' + +/** + * @param {unknown} doc + * @returns + */ +export function informativeTest_6_3_5(doc) { + const ctx = { + infos: /** @type {Array<{ message: string; instancePath: string }>} */ ([]), + } + + walkHashes(doc, ({ path, hash }) => { + hash.file_hashes.forEach((fileHash, fileHashIndex) => { + if (typeof fileHash.value === 'string' && fileHash.value.length < 64) { + ctx.infos.push({ + instancePath: `${path}/${fileHashIndex}/value`, + message: 'use of short hash', + }) + } + }) + }) + + return ctx +} diff --git a/csaf_2_1/shared/csafHelpers/walkHashes.js b/csaf_2_1/shared/csafHelpers/walkHashes.js index 8591290b..994a9b44 100644 --- a/csaf_2_1/shared/csafHelpers/walkHashes.js +++ b/csaf_2_1/shared/csafHelpers/walkHashes.js @@ -4,9 +4,15 @@ const ajv = new Ajv() const hashSchema = /** @type {const} */ ({ additionalProperties: true, - optionalProperties: { + properties: { file_hashes: { - elements: { additionalProperties: true, properties: {} }, + elements: { + additionalProperties: true, + properties: { + algorithm: { type: 'string' }, + value: { type: 'string' }, + }, + }, }, }, }) @@ -67,10 +73,15 @@ const validateInput = ajv.compile(inputSchema) const validateFullProductName = ajv.compile(fullProductNameSchema) const validateBranch = ajv.compile(branchSchema) const validateProductPath = ajv.compile(productPathSchema) +const validateFileHash = ajv.compile(hashSchema) + +/** + * @typedef {{ file_hashes: Array<{ algorithm: string; value: string }> }} FileHashEntry + */ /** * @param {any} doc - * @param {(params: { path: string; hash: {} }) => void} onHashFound + * @param {(params: { path: string; hash: FileHashEntry }) => void} onHashFound */ export function walkHashes(doc, onHashFound) { if (!validateInput(doc)) { @@ -85,6 +96,7 @@ export function walkHashes(doc, onHashFound) { fullProductName.product_identification_helper?.hashes?.forEach( (hash, hashIndex) => { + if (!validateFileHash(hash)) return onHashFound({ path: `/product_tree/full_product_names/${fullProductNameIndex}/product_identification_helper/hashes/${hashIndex}/file_hashes`, hash, @@ -106,6 +118,7 @@ export function walkHashes(doc, onHashFound) { branch.product?.product_identification_helper?.hashes?.forEach( (hash, hashIndex) => { + if (!validateFileHash(hash)) return onHashFound({ path: `${prefix}${branchIndex}/product/product_identification_helper/hashes/${hashIndex}/file_hashes`, hash, @@ -128,6 +141,7 @@ export function walkHashes(doc, onHashFound) { productPath.full_product_name?.product_identification_helper?.hashes?.forEach( (hash, hashIndex) => { + if (!validateFileHash(hash)) return onHashFound({ path: `/product_tree/product_paths/${productPathIndex}/full_product_name/product_identification_helper/hashes/${hashIndex}/file_hashes`, hash, diff --git a/tests/csaf_2_1/informativeTest_6_3_5.js b/tests/csaf_2_1/informativeTest_6_3_5.js new file mode 100644 index 00000000..7c54377f --- /dev/null +++ b/tests/csaf_2_1/informativeTest_6_3_5.js @@ -0,0 +1,97 @@ +import assert from 'node:assert' +import { informativeTest_6_3_5 } from '../../csaf_2_1/informativeTests.js' + +describe('informativeTest_6_3_5', function () { + it('only runs on relevant documents', function () { + assert.equal(informativeTest_6_3_5({ document: 'mydoc' }).infos.length, 0) + }) + + it('skip invalid hash value', function () { + const result = informativeTest_6_3_5({ + product_tree: { + full_product_names: [ + { + name: 'Product A', + product_id: 'CSAFPID-9080700', + product_identification_helper: { + hashes: [ + { + filename: 'product_a.so', + }, + ], + }, + }, + ], + }, + }) + assert.equal(result.infos.length, 0) + }) + + it('detects short hash in product_paths', function () { + const result = informativeTest_6_3_5({ + product_tree: { + product_paths: [ + { + full_product_name: { + name: 'Product A', + product_id: 'CSAFPID-0001', + product_identification_helper: { + hashes: [ + { + file_hashes: [ + { + algorithm: 'md5', + value: 'd41d8cd98f00b204e9800998ecf8427e', + }, + ], + filename: 'product_b.so', + }, + ], + }, + }, + }, + ], + }, + }) + assert.equal(result.infos.length, 1) + assert.equal( + result.infos[0].instancePath, + '/product_tree/product_paths/0/full_product_name/product_identification_helper/hashes/0/file_hashes/0/value' + ) + }) + + it('detects short hash in branches', function () { + const result = informativeTest_6_3_5({ + product_tree: { + branches: [ + { + category: 'vendor', + name: 'Vendor A', + product: { + name: 'Product A', + product_id: 'CSAFPID-0001', + product_identification_helper: { + hashes: [ + { + file_hashes: [ + { + algorithm: 'md5', + value: 'd41d8cd98f00b204e9800998ecf8427e', + }, + ], + filename: 'product_c.so', + }, + ], + }, + }, + }, + ], + }, + }) + assert.equal(result.infos.length, 1) + assert.equal( + result.infos[0].instancePath, + '/product_tree/branches/0/product/product_identification_helper/hashes/0/file_hashes/0/value' + ) + }) +})