From 80eb42f1fc0f0a4e32144e5853a9c6d8e32d4fbd Mon Sep 17 00:00:00 2001 From: bendo-eXX Date: Thu, 15 Oct 2026 11:32:55 +0200 Subject: [PATCH] feat(csaf2.1): add recommendedTest_6_2_51.js --- README.md | 2 +- csaf_2_1/recommendedTests.js | 1 + .../recommendedTest_6_2_51.js | 107 ++++++++++++++++++ .../shared/productVersionRangeHelper/vers.js | 33 ++++++ package-lock.json | 18 +++ package.json | 2 + tests/csaf_2_1/oasis.js | 1 - tests/csaf_2_1/recommendedTest_6_2_51.js | 54 +++++++++ vitest.config.js | 8 ++ 9 files changed, 224 insertions(+), 2 deletions(-) create mode 100644 csaf_2_1/recommendedTests/recommendedTest_6_2_51.js create mode 100644 csaf_2_1/shared/productVersionRangeHelper/vers.js create mode 100644 tests/csaf_2_1/recommendedTest_6_2_51.js diff --git a/README.md b/README.md index 647244ed..7799b1b1 100644 --- a/README.md +++ b/README.md @@ -366,7 +366,6 @@ The following tests are not yet implemented and therefore missing: - Recommended Test 6.2.50.1 - Recommended Test 6.2.50.2 - Recommended Test 6.2.50.3 -- Recommended Test 6.2.51 - Recommended Test 6.2.54.1 - Recommended Test 6.2.54.2 - Recommended Test 6.2.54.4 @@ -535,6 +534,7 @@ export const recommendedTest_6_2_45: DocumentTest export const recommendedTest_6_2_47: DocumentTest export const recommendedTest_6_2_48: DocumentTest export const recommendedTest_6_2_49: DocumentTest +export const recommendedTest_6_2_51: DocumentTest export const recommendedTest_6_2_52: DocumentTest export const recommendedTest_6_2_53: DocumentTest export const recommendedTest_6_2_54_3: DocumentTest diff --git a/csaf_2_1/recommendedTests.js b/csaf_2_1/recommendedTests.js index 4e74f84a..8a20c6b3 100644 --- a/csaf_2_1/recommendedTests.js +++ b/csaf_2_1/recommendedTests.js @@ -62,6 +62,7 @@ export { recommendedTest_6_2_45 } from './recommendedTests/recommendedTest_6_2_4 export { recommendedTest_6_2_47 } from './recommendedTests/recommendedTest_6_2_47.js' export { recommendedTest_6_2_48 } from './recommendedTests/recommendedTest_6_2_48.js' export { recommendedTest_6_2_49 } from './recommendedTests/recommendedTest_6_2_49.js' +export { recommendedTest_6_2_51 } from './recommendedTests/recommendedTest_6_2_51.js' export { recommendedTest_6_2_52 } from './recommendedTests/recommendedTest_6_2_52.js' export { recommendedTest_6_2_53 } from './recommendedTests/recommendedTest_6_2_53.js' export { recommendedTest_6_2_54_3 } from './recommendedTests/recommendedTest_6_2_54_3.js' diff --git a/csaf_2_1/recommendedTests/recommendedTest_6_2_51.js b/csaf_2_1/recommendedTests/recommendedTest_6_2_51.js new file mode 100644 index 00000000..171a5888 --- /dev/null +++ b/csaf_2_1/recommendedTests/recommendedTest_6_2_51.js @@ -0,0 +1,107 @@ +import { Ajv } from 'ajv/dist/jtd.js' +import { walkPath } from '../../lib/walkPaths.js' +import { + initVers, + isSupportedVersScheme, +} from '../shared/productVersionRangeHelper/vers.js' + +// Regex to extracts the versioning type with the format `vers:/`. +const VERS_PREFIX = new RegExp('^vers:([a-z\\.\\-\\+][a-z0-9\\.\\-\\+]*)/.+') + +const ajv = new Ajv() + +const branchSchema = /** @type {const} */ ({ + additionalProperties: true, + optionalProperties: { + category: { type: 'string' }, + name: { type: 'string' }, + }, +}) + +const validateBranch = ajv.compile(branchSchema) + +const inputSchema = /** @type {const} */ ({ + additionalProperties: true, + properties: { + product_tree: { + additionalProperties: true, + optionalProperties: { + branches: { elements: branchSchema }, + }, + }, + }, +}) + +const validateInput = ajv.compile(inputSchema) + +// VERS types registered in the vers specification (docs/types/vers-types.md) +const REGISTERED_VERS_TYPES = new Set([ + 'alpm', + 'apk', + 'cargo', + 'composer', + 'cpan', + 'dart', + 'deb', + 'gem', + 'golang', + 'hackage', + 'maven', + 'nuget', + 'npm', + 'pypi', + 'rpm', + 'all', + 'datetime', + 'generic', + 'intdot', + 'lexicographic', + 'none', + 'semver', +]) + +/** + * This implements the recommended test 6.2.51 of the CSAF 2.1 standard. + * + * @param {unknown} doc + */ +export async function recommendedTest_6_2_51(doc) { + const ctx = { + warnings: + /** @type {Array<{ instancePath: string; message: string }>} */ ([]), + } + + if (!validateInput(doc)) { + return ctx + } + + await initVers() + + await walkPath( + doc, + '/product_tree/branches[*]', + async (instancePath, branch) => { + if (!validateBranch(branch)) return + if (branch.category !== 'product_version_range') return + if (typeof branch.name !== 'string') return + + const match = branch.name.trim().match(VERS_PREFIX) + const type = match?.[1]?.toLowerCase() + if (!type) return + + if (!REGISTERED_VERS_TYPES.has(type)) { + ctx.warnings.push({ + instancePath: `${instancePath}/name`, + message: `usage of unknown version type \`${type}\` (not registered, not supported)`, + }) + } else if (!isSupportedVersScheme(branch.name)) { + ctx.warnings.push({ + instancePath: `${instancePath}/name`, + message: `usage of unknown version type \`${type}\` (registered, not supported)`, + }) + } + } + ) + + return ctx +} diff --git a/csaf_2_1/shared/productVersionRangeHelper/vers.js b/csaf_2_1/shared/productVersionRangeHelper/vers.js new file mode 100644 index 00000000..bdbdefed --- /dev/null +++ b/csaf_2_1/shared/productVersionRangeHelper/vers.js @@ -0,0 +1,33 @@ +/** @type {Promise | undefined} */ +let loading + +/** @type {typeof import('@csaf-rs/vers-rs') | undefined} */ +let versRs + +/** + * Loads the WebAssembly module of vers-rs. Must be awaited once before + * `isSupportedVersScheme` is used. + * + * @returns {Promise} + */ +export async function initVers() { + loading ??= import('@csaf-rs/vers-rs') + versRs = await loading +} + +/** + * Checks whether the versioning scheme of a product version range in the + * VERS syntax is supported by vers-rs. Syntax errors in the constraints do not + * make the scheme unsupported. + * + * @param {string} range + * @returns {boolean} + */ +export function isSupportedVersScheme(range) { + try { + versRs?.parse(range.trim()) + return true + } catch (e) { + return !String(e).includes('Unsupported versioning scheme') + } +} diff --git a/package-lock.json b/package-lock.json index dba4284d..72515084 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "3.0.0", "license": "MIT", "dependencies": { + "@csaf-rs/vers-rs": "^0.1.2", "@secvisogram/is-leap-second": "^1.0.0", "@secvisogram/license-deprecation-list": "^1.0.1", "ae-cvss-calculator": "^1.0.13", @@ -37,6 +38,7 @@ "playwright": "^1.62.1", "prettier": "^2.8.1", "typescript": "^6.0.3", + "vite-plugin-wasm": "^3.6.0", "vitest": "^4.1.10", "xml2js": "^0.5.0" } @@ -108,6 +110,12 @@ "dev": true, "license": "MIT" }, + "node_modules/@csaf-rs/vers-rs": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@csaf-rs/vers-rs/-/vers-rs-0.1.3.tgz", + "integrity": "sha512-/8ry2LM6zSvz5ZfiE6kmkVMYx5CA5v6x7LmQs9+IlRoqzc5jtFkBjfnuRI+RUadyYJslwTGi+kw8Ee3mkCm6tg==", + "license": "Apache-2.0" + }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", @@ -1851,6 +1859,16 @@ } } }, + "node_modules/vite-plugin-wasm": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/vite-plugin-wasm/-/vite-plugin-wasm-3.6.0.tgz", + "integrity": "sha512-mL/QPziiIA4RAA6DkaZZzOstdwbW5jO4Vz7Zenj0wieKWBlNvIvX5L5ljum9lcUX0ShNfBgCNLKTjNkRVVqcsw==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "vite": "^2 || ^3 || ^4 || ^5 || ^6 || ^7 || ^8" + } + }, "node_modules/vitest": { "version": "4.1.11", "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.11.tgz", diff --git a/package.json b/package.json index 42b55796..5371ba8a 100644 --- a/package.json +++ b/package.json @@ -129,6 +129,7 @@ "access": "public" }, "dependencies": { + "@csaf-rs/vers-rs": "^0.1.2", "@secvisogram/is-leap-second": "^1.0.0", "@secvisogram/license-deprecation-list": "^1.0.1", "ae-cvss-calculator": "^1.0.13", @@ -157,6 +158,7 @@ "playwright": "^1.62.1", "prettier": "^2.8.1", "typescript": "^6.0.3", + "vite-plugin-wasm": "^3.6.0", "vitest": "^4.1.10", "xml2js": "^0.5.0" }, diff --git a/tests/csaf_2_1/oasis.js b/tests/csaf_2_1/oasis.js index a65b2ea6..6728a12b 100644 --- a/tests/csaf_2_1/oasis.js +++ b/tests/csaf_2_1/oasis.js @@ -24,7 +24,6 @@ const excluded = [ '6.2.50.1', '6.2.50.2', '6.2.50.3', - '6.2.51', '6.2.54.1', '6.2.54.2', '6.2.54.4', diff --git a/tests/csaf_2_1/recommendedTest_6_2_51.js b/tests/csaf_2_1/recommendedTest_6_2_51.js new file mode 100644 index 00000000..412dd866 --- /dev/null +++ b/tests/csaf_2_1/recommendedTest_6_2_51.js @@ -0,0 +1,54 @@ +import { recommendedTest_6_2_51 } from '../../csaf_2_1/recommendedTests/recommendedTest_6_2_51.js' + +/** @param {string} name */ +const docWith = (name) => ({ + product_tree: { + branches: [ + { + category: 'product_version_range', + name, + }, + ], + }, +}) + +describe('recommendedTest_6_2_51', function () { + it('skips invalid child branches that do not pass schema validation', async function () { + const result = await recommendedTest_6_2_51({ + product_tree: { branches: [42, null] }, + }) + expect(result.warnings.length).to.equal(0) + }) + + it('does not warn for a supported VERS type', async function () { + const result = await recommendedTest_6_2_51( + docWith('vers:npm/>1.2.0|<3.91.1') + ) + expect(result.warnings.length).to.equal(0) + }) + + it('does not warn for a supported VERS type with invalid constraints', async function () { + const result = await recommendedTest_6_2_51(docWith('vers:npm/>1.2')) + expect(result.warnings.length).to.equal(0) + }) + + it('warns about a VERS type that is not registered', async function () { + const result = await recommendedTest_6_2_51(docWith('vers:alpm/<4.2.0')) + expect(result.warnings).to.eql([ + { + instancePath: '/product_tree/branches/0/name', + message: + 'usage of unknown version type `alpm` (registered, not supported)', + }, + ]) + }) + + it('ignores branches that are not product version ranges', async function () { + const result = await recommendedTest_6_2_51({ + product_tree: { + branches: [{ category: 'product_version', name: 'vers:unknown/1' }], + }, + }) + expect(result.warnings.length).to.equal(0) + }) +}) diff --git a/vitest.config.js b/vitest.config.js index 9decfbdf..9dad8976 100644 --- a/vitest.config.js +++ b/vitest.config.js @@ -1,6 +1,7 @@ import { fileURLToPath } from 'node:url' import { defineConfig } from 'vitest/config' import { playwright } from '@vitest/browser-playwright' +import wasm from 'vite-plugin-wasm' // The OASIS fixture-driven tests (tests/oasis.js, tests/csaf_2_1/oasis.js) run // informativeTest_6_3_8 without a hunspell mock, so they exercise the real @@ -26,6 +27,13 @@ export default defineConfig({ }, }, { + // vers-rs is a WebAssembly ES module, which vite only handles with a plugin + plugins: [ + /** @type {() => import('vite').Plugin} */ ( + /** @type {unknown} */ (wasm) + )(), + ], + optimizeDeps: { exclude: ['@csaf-rs/vers-rs'] }, test: { name: 'browser', globals: true,