diff --git a/csaf_2_1/informativeTests.js b/csaf_2_1/informativeTests.js index 7e914506..dbcb450a 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_6, informativeTest_6_3_7, informativeTest_6_3_8, informativeTest_6_3_9, @@ -11,6 +10,7 @@ export { informativeTest_6_3_1 } from './informativeTests/informativeTest_6_3_1. 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_6 } from './informativeTests/informativeTest_6_3_6.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' export { informativeTest_6_3_21_1 } from './informativeTests/informativeTest_6_3_21_1.js' diff --git a/csaf_2_1/informativeTests/informativeTest_6_3_6.js b/csaf_2_1/informativeTests/informativeTest_6_3_6.js new file mode 100644 index 00000000..70c06fd4 --- /dev/null +++ b/csaf_2_1/informativeTests/informativeTest_6_3_6.js @@ -0,0 +1,219 @@ +import { Ajv } from 'ajv/dist/jtd.js' +import testURL from '#lib/informativeTests/shared/testURL.js' +import { walkPath } from '#lib/walkPaths.js' + +const ajv = new Ajv() + +const referenceSchema = /** @type {const} */ ({ + additionalProperties: true, + optionalProperties: { + url: { type: 'string' }, + category: { type: 'string' }, + }, +}) + +const inputSchema = /** @type {const} */ ({ + additionalProperties: true, + optionalProperties: { + document: { + additionalProperties: true, + optionalProperties: { + acknowledgments: { + elements: { + additionalProperties: true, + optionalProperties: { + urls: { + elements: { type: 'string' }, + }, + }, + }, + }, + references: { + elements: referenceSchema, + }, + aggregate_severity: { + additionalProperties: true, + optionalProperties: { + namespace: { type: 'string' }, + }, + }, + distribution: { + additionalProperties: true, + optionalProperties: { + tlp: { + additionalProperties: true, + optionalProperties: { + url: { type: 'string' }, + }, + }, + }, + }, + publisher: { + additionalProperties: true, + optionalProperties: { + namespace: { type: 'string' }, + }, + }, + }, + }, + product_tree: { + additionalProperties: true, + optionalProperties: { + full_product_names: { + elements: { + additionalProperties: true, + optionalProperties: { + product_identification_helper: { + additionalProperties: true, + optionalProperties: { + sbom_urls: { elements: { type: 'string' } }, + x_generic_uris: { + elements: { + additionalProperties: true, + optionalProperties: { + namespace: { type: 'string' }, + uri: { type: 'string' }, + }, + }, + }, + }, + }, + }, + }, + }, + branches: { + elements: { + additionalProperties: true, + properties: {}, + }, + }, + product_paths: { + elements: { + additionalProperties: true, + optionalProperties: { + full_product_name: { + additionalProperties: true, + optionalProperties: { + product_identification_helper: { + additionalProperties: true, + optionalProperties: { + sbom_urls: { elements: { type: 'string' } }, + x_generic_uris: { + elements: { + additionalProperties: true, + optionalProperties: { + namespace: { type: 'string' }, + uri: { type: 'string' }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + vulnerabilities: { + elements: { + additionalProperties: true, + optionalProperties: { + remediations: { + elements: { + additionalProperties: true, + optionalProperties: { + url: { type: 'string' }, + }, + }, + }, + acknowledgments: { + elements: { + additionalProperties: true, + optionalProperties: { + urls: { + elements: { type: 'string' }, + }, + }, + }, + }, + references: { + elements: referenceSchema, + }, + }, + }, + }, + }, +}) + +const validateInput = ajv.compile(inputSchema) +const validateReference = ajv.compile(referenceSchema) + +/** + * CSAF 2.1 Informative Test 6.3.6: + * verifies that all non-self references using URL fields resolve to HTTP 2xx or 3xx. + * + * @param {unknown} doc + */ +export async function informativeTest_6_3_6(doc) { + const ctx = { + infos: /** @type {Array<{ message: string; instancePath: string }>} */ ([]), + } + + if (!validateInput(doc)) { + return ctx + } + + for (const path of [ + '/document/acknowledgments[]/urls[]', + '/document/aggregate_severity/namespace', + '/document/distribution/tlp/url', + '/document/publisher/namespace', + '/product_tree/branches[*]/product/product_identification_helper/sbom_urls[]', + '/product_tree/branches[*]/product/product_identification_helper/x_generic_uris[]/namespace', + '/product_tree/branches[*]/product/product_identification_helper/x_generic_uris[]/uri', + '/product_tree/full_product_names[]/product_identification_helper/sbom_urls[]', + '/product_tree/full_product_names[]/product_identification_helper/x_generic_uris[]/namespace', + '/product_tree/full_product_names[]/product_identification_helper/x_generic_uris[]/uri', + '/product_tree/product_paths[]/full_product_name/product_identification_helper/sbom_urls[]', + '/product_tree/product_paths[]/full_product_name/product_identification_helper/x_generic_uris[]/namespace', + '/product_tree/product_paths[]/full_product_name/product_identification_helper/x_generic_uris[]/uri', + '/vulnerabilities[]/acknowledgments[]/urls[]', + '/vulnerabilities[]/remediations[]/url', + ]) { + await walkPath(doc, path, async (instancePath, value) => { + if (typeof value !== 'string') return + await testURL(value, () => { + ctx.infos.push({ + instancePath, + message: 'use of non-self referencing urls failing to resolve', + }) + }) + }) + } + + for (const path of [ + '/document/references[]', + '/vulnerabilities[]/references[]', + ]) { + await walkPath(doc, path, async (instancePath, value) => { + if ( + !validateReference(value) || + value.category === 'self' || + typeof value.url !== 'string' + ) { + return + } + + await testURL(value.url, () => { + ctx.infos.push({ + instancePath: instancePath + '/url', + message: 'use of non-self referencing urls failing to resolve', + }) + }) + }) + } + + return ctx +} diff --git a/package.json b/package.json index b5baaff3..c4a8f7a1 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,9 @@ "repository": { "url": "https://github.com/secvisogram/csaf-validator-lib" }, + "imports": { + "#*.js": "./*.js" + }, "files": [ "lib", "schemas", diff --git a/tests/csaf_2_1/informativeTest_6_3_6.js b/tests/csaf_2_1/informativeTest_6_3_6.js new file mode 100644 index 00000000..1c1a5e14 --- /dev/null +++ b/tests/csaf_2_1/informativeTest_6_3_6.js @@ -0,0 +1,8 @@ +import { informativeTest_6_3_6 } from '../../csaf_2_1/informativeTests.js' + +describe('informativeTest_6_3_6', function () { + it('returns no infos for invalid input', async function () { + const result = await informativeTest_6_3_6('not-an-object') + assert.equal(result.infos.length, 0) + }) +})