Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions csaf_2_1/recommendedTests/recommendedTest_6_2_8.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { optionalTest_6_2_8 } from '../../optionalTests.js'
import checkForUnsafeHashAlgorithms from './shared/checkForUnsafeHashAlgorithms.js'

/**
* This implements the recommended test 6.2.8 of the CSAF 2.1 standard.
* @param {unknown} doc
*/
export function recommendedTest_6_2_8(doc) {
return optionalTest_6_2_8(doc)
return checkForUnsafeHashAlgorithms(doc, 'md5')
}
58 changes: 58 additions & 0 deletions csaf_2_1/recommendedTests/shared/checkForUnsafeHashAlgorithms.js
Original file line number Diff line number Diff line change
@@ -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 {unknown} 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'
)
)
}
138 changes: 138 additions & 0 deletions csaf_2_1/shared/csafHelpers/walkHashes.js
Original file line number Diff line number Diff line change
@@ -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)) {
Comment thread
domachine marked this conversation as resolved.
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}/file_hashes`,
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}/file_hashes`,
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}/file_hashes`,
hash,
})
}
)
})
}
95 changes: 95 additions & 0 deletions tests/csaf_2_1/recommendedTest_6_2_8.js
Original file line number Diff line number Diff line change
@@ -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/file_hashes'
)
})

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/file_hashes'
)
})
})
Loading