diff --git a/csaf_2_1/recommendedTests/recommendedTest_6_2_53.js b/csaf_2_1/recommendedTests/recommendedTest_6_2_53.js new file mode 100644 index 00000000..046a5ed7 --- /dev/null +++ b/csaf_2_1/recommendedTests/recommendedTest_6_2_53.js @@ -0,0 +1,85 @@ +import { Ajv } from 'ajv/dist/jtd.js' +import { entries } from '../../rvisc.js' + +const ajv = new Ajv() + +/** @type {Array<{ system_name: string; text_pattern: RegExp }>} */ +const registeredIdSystems = entries.map( + (/** @type {{ system_name: string; text_pattern: string }} */ entry) => ({ + system_name: entry.system_name, + text_pattern: new RegExp(entry.text_pattern), + }) +) + +/* + 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, + + properties: { + vulnerabilities: { + elements: { + additionalProperties: true, + optionalProperties: { + ids: { + elements: { + additionalProperties: true, + optionalProperties: { + system_name: { type: 'string' }, + text: { type: 'string' }, + }, + }, + }, + }, + }, + }, + }, +}) + +const validate = ajv.compile(inputSchema) + +/** @typedef {import('ajv/dist/jtd.js').JTDDataType} InputSchema */ +/** @typedef {InputSchema['vulnerabilities'][number]} Vulnerability */ + +/** + * This implements the recommended test 6.2.53 of the CSAF 2.1 standard. + * + * @param {unknown} doc + */ +export function recommendedTest_6_2_53(doc) { + const ctx = { + warnings: + /** @type {Array<{ instancePath: string; message: string }>} */ ([]), + } + + if (!validate(doc)) { + return ctx + } + + /** @type {Array} */ + const vulnerabilities = doc.vulnerabilities + vulnerabilities.forEach((vulnerability, vulnIndex) => { + const ids = vulnerability.ids ?? [] + ids.forEach((id, idIndex) => { + if (id.system_name === undefined || id.text === undefined) return + + const registeredSystem = registeredIdSystems.find( + (entry) => entry.system_name === id.system_name + ) + if (!registeredSystem) return + + if (!registeredSystem.text_pattern.test(id.text)) { + ctx.warnings.push({ + instancePath: `/vulnerabilities/${vulnIndex}/ids/${idIndex}/text`, + message: `the text does not match the text_pattern of the registered ID system "${id.system_name}"`, + }) + } + }) + }) + + return ctx +} diff --git a/package.json b/package.json index fb931d45..bbc482c6 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "mandatoryTests.js", "optionalTests.js", "README.md", + "rvisc.js", "schemaTests.js", "strip.js", "validate.js", @@ -100,6 +101,10 @@ "types": "./build/lib/shared/cwec.d.ts", "import": "./lib/shared/cwec.js" }, + "./rvisc.js": { + "types": "./build/rvisc.d.ts", + "import": "./rvisc.js" + }, "./lib/shared/cvss2.js": { "types": "./build/lib/shared/cvss2.d.ts", "import": "./lib/shared/cvss2.js" diff --git a/rvisc.js b/rvisc.js new file mode 100644 index 00000000..cb992c1e --- /dev/null +++ b/rvisc.js @@ -0,0 +1,32 @@ +const rvisc = { + $schema: + 'https://raw.githubusercontent.com/oasis-tcs/csaf/master/registry/id/schema/registry.schema.json', + entries: [ + { + common_name: 'OASIS Open CSAF TC GitHub Issues', + example_identifiers: ['#1217'], + published: '2026-01-28T17:45:00Z', + summary: + 'Contains identifiers for issues in the official standard repository of the OASIS Open CSAF TC.', + system_name: 'https://github.com/oasis-tcs/csaf', + text_pattern: '^#[1-9]\\d*$', + updated: '2026-01-28T17:45:00Z', + }, + { + common_name: 'EUVD IDs', + example_identifiers: ['EUVD-2026-4660'], + published: '2026-01-28T18:00:00Z', + summary: + 'Contains identifiers from the European Vulnerability Database (EUVD).', + system_name: 'https://euvd.enisa.europa.eu', + text_pattern: '^EUVD-[1-9][0-9]{3}-[0-9]{4,}$', + updated: '2026-02-06T15:35:00Z', + }, + ], + last_updated: '2026-02-06T15:35:00Z', + registry_version: 1, +} + +export default rvisc + +export const entries = rvisc.entries diff --git a/scripts/rvisc-importRegistry.js b/scripts/rvisc-importRegistry.js new file mode 100644 index 00000000..0ebf8464 --- /dev/null +++ b/scripts/rvisc-importRegistry.js @@ -0,0 +1,32 @@ +#!/usr/bin/env node + +import { writeFile, readFile } from 'node:fs/promises' +import prettier from 'prettier' + +/** + * Converts the RVISC registry (`csaf/registry/id/registry.json`, part of + * the `csaf/` git subtree and excluded from the npm package) into a plain + * ESM module (`rvisc.js`) that works in Node and in the browser. + * + * Run again after `csaf/registry/id/registry.json` was updated: + * + * node scripts/rvisc-importRegistry.js + */ + +const REGISTRY_FILE = 'csaf/registry/id/registry.json' +const OUTPUT_FILE = 'rvisc.js' + +const json = JSON.parse(await readFile(REGISTRY_FILE, 'utf-8')) + +await writeFile( + OUTPUT_FILE, + prettier.format( + `const rvisc = (${JSON.stringify( + json + )})\n\nexport default rvisc\n\nexport const entries = rvisc.entries`, + { + ...(await prettier.resolveConfig(OUTPUT_FILE)), + filepath: OUTPUT_FILE, + } + ) +) diff --git a/tests/csaf_2_1/recommendedTest_6_2_53.js b/tests/csaf_2_1/recommendedTest_6_2_53.js new file mode 100644 index 00000000..4aaa3c59 --- /dev/null +++ b/tests/csaf_2_1/recommendedTest_6_2_53.js @@ -0,0 +1,46 @@ +import assert from 'node:assert/strict' +import { recommendedTest_6_2_53 } from '../../csaf_2_1/recommendedTests/recommendedTest_6_2_53.js' + +describe('recommendedTest_6_2_53', function () { + it('only runs on relevant documents', function () { + assert.equal(recommendedTest_6_2_53({}).warnings.length, 0) + }) + + it('does not warn when ids are absent', function () { + assert.equal( + recommendedTest_6_2_53({ vulnerabilities: [{}] }).warnings.length, + 0 + ) + }) + + it('does not warn when text is absent', function () { + assert.equal( + recommendedTest_6_2_53({ + vulnerabilities: [ + { + ids: [{ system_name: 'https://example.com' }], + }, + ], + }).warnings.length, + 0 + ) + }) + + it('does not warn when system_name is not in the registry', function () { + assert.equal( + recommendedTest_6_2_53({ + vulnerabilities: [ + { + ids: [ + { + system_name: 'https://unknown-system.example.com', + text: 'some-text', + }, + ], + }, + ], + }).warnings.length, + 0 + ) + }) +})