From f4a9d6030aa94ab82c33274b1b602680fcc094e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Burgd=C3=B6rfer?= Date: Wed, 12 Feb 2025 14:00:55 +0100 Subject: [PATCH 01/25] chore: update typescript --- package-lock.json | 10 +++++----- package.json | 2 +- tsconfig.json | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/package-lock.json b/package-lock.json index fec4645c..eefbfaad 100644 --- a/package-lock.json +++ b/package-lock.json @@ -34,7 +34,7 @@ "chai": "^4.3.7", "mocha": "^10.2.0", "prettier": "^2.8.1", - "typescript": "^4.9.4", + "typescript": "^5.7.3", "xml2js": "^0.5.0" } }, @@ -1589,9 +1589,9 @@ } }, "node_modules/typescript": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", - "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", + "version": "5.7.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz", + "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==", "dev": true, "license": "Apache-2.0", "bin": { @@ -1599,7 +1599,7 @@ "tsserver": "bin/tsserver" }, "engines": { - "node": ">=4.2.0" + "node": ">=14.17" } }, "node_modules/undici": { diff --git a/package.json b/package.json index 2018f1af..9507420f 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,7 @@ "chai": "^4.3.7", "mocha": "^10.2.0", "prettier": "^2.8.1", - "typescript": "^4.9.4", + "typescript": "^5.7.3", "xml2js": "^0.5.0" }, "version": "1.3.46" diff --git a/tsconfig.json b/tsconfig.json index a6b04a95..30aa0c4a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -33,7 +33,7 @@ // "typeRoots": [], /* Specify multiple folders that act like `./node_modules/@types`. */ // "types": [], /* Specify type package names to be included without being referenced in a source file. */ // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ - "resolveJsonModule": true /* Enable importing .json files */, + // "resolveJsonModule": true /* Enable importing .json files */, // "noResolve": true, /* Disallow `import`s, `require`s or ``s from expanding the number of files TypeScript should add to a project. */ /* JavaScript Support */ @@ -98,6 +98,6 @@ // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ "skipLibCheck": true /* Skip type checking all .d.ts files. */ }, - "include": ["**/*.js", "**/*.ts", "**/*.json", "**/*.cjs"], + "include": ["**/*.js", "**/*.ts", "**/*.cjs"], "exclude": ["node_modules", "build", "csaf"] } From a5e960476d75ad71acfd54115a70a4819fdc170c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Burgd=C3=B6rfer?= Date: Wed, 12 Feb 2025 15:29:54 +0100 Subject: [PATCH 02/25] feat: setup csaf 2.1 infrastructure --- README.md | 179 +- csaf_2_1/basic.js | 46 + csaf_2_1/extended.js | 2 + csaf_2_1/full.js | 2 + csaf_2_1/informativeTests.js | 13 + csaf_2_1/mandatoryTests.js | 45 + csaf_2_1/optionalTests.js | 22 + csaf_2_1/schemaTests.js | 2 + csaf_2_1/schemaTests/csaf_2_1.js | 26 + csaf_2_1/schemaTests/csaf_2_1/schema.js | 1485 ++++++++++++++++ csaf_2_1/schemaTests/csaf_2_1_strict.js | 26 + .../schemaTests/csaf_2_1_strict/schema.js | 1519 +++++++++++++++++ lib/shared/csafAjv.js | 2 + lib/shared/csafAjv/cvss-v4.0.js | 407 +++++ package.json | 2 +- scripts/mocha.js | 19 + scripts/test.js | 2 +- tests/csaf_2_1/oasis.test.js | 143 ++ 18 files changed, 3920 insertions(+), 22 deletions(-) create mode 100644 csaf_2_1/basic.js create mode 100644 csaf_2_1/extended.js create mode 100644 csaf_2_1/full.js create mode 100644 csaf_2_1/informativeTests.js create mode 100644 csaf_2_1/mandatoryTests.js create mode 100644 csaf_2_1/optionalTests.js create mode 100644 csaf_2_1/schemaTests.js create mode 100644 csaf_2_1/schemaTests/csaf_2_1.js create mode 100644 csaf_2_1/schemaTests/csaf_2_1/schema.js create mode 100644 csaf_2_1/schemaTests/csaf_2_1_strict.js create mode 100644 csaf_2_1/schemaTests/csaf_2_1_strict/schema.js create mode 100644 lib/shared/csafAjv/cvss-v4.0.js create mode 100644 scripts/mocha.js create mode 100644 tests/csaf_2_1/oasis.test.js diff --git a/README.md b/README.md index a4cb03de..f0b6ef24 100644 --- a/README.md +++ b/README.md @@ -6,13 +6,22 @@ - [Strict Mode](#strict-mode) - [API](#api) - [Interfaces](#interfaces) - - [Module `schemaTests.js`](#module-schematestsjs) - - [Module `mandatoryTests.js`](#module-mandatorytestsjs) - - [Module `optionalTests.js`](#module-optionaltestsjs) - - [Module `informativeTests.js`](#module-informativetestsjs) - - [Module `basic.js`](#module-basicjs) - - [Module `extended.js`](#module-extendedjs) - - [Module `full.js`](#module-fulljs) + - CSAF 2.0 + - [Module `schemaTests.js`](#module-schematestsjs) + - [Module `mandatoryTests.js`](#module-mandatorytestsjs) + - [Module `optionalTests.js`](#module-optionaltestsjs) + - [Module `informativeTests.js`](#module-informativetestsjs) + - [Module `basic.js`](#module-basicjs) + - [Module `extended.js`](#module-extendedjs) + - [Module `full.js`](#module-fulljs) + - CSAF 2.1 + - [Module `csaf_2_1/schemaTests.js`](#module-csaf_2_1schematestsjs) + - [Module `csaf_2_1/mandatoryTests.js`](#module-csaf_2_1mandatorytestsjs) + - [Module `csaf_2_1/optionalTests.js`](#module-csaf_2_1optionaltestsjs) + - [Module `csaf_2_1/informativeTests.js`](#module-csaf_2_1informativetestsjs) + - [Module `csaf_2_1/basic.js`](#module-csaf_2_1basicjs) + - [Module `csaf_2_1/extended.js`](#module-csaf_2_1extendedjs) + - [Module `csaf_2_1/full.js`](#module-csaf_2_1fulljs) - [Module `validate.js`](#module-validatejs) - [Module `validateStrict.js`](#module-validatestrictjs) - [Module `strip.js`](#module-stripjs) @@ -37,6 +46,7 @@ After that you can reference the modules from within your JavaScript application There is an [official package](https://www.npmjs.com/package/@secvisogram/csaf-validator-lib) in the npm registry. You can add it to your project using the following command: + ```sh npm install @secvisogram/csaf-validator-lib ``` @@ -52,30 +62,33 @@ You can also include this library as a subtree in your repository. ``` - install dependencies + ```sh cd csaf-validator-lib && npm ci --prod ``` -- This repository includes git submodules. Make sure to initialize and update +- This repository includes git submodules. Make sure to initialize and update the submodules before you start working with the repository. + ```sh git submodule update --init --recursive ``` -- For test 6.3.8 an installation of hunspell as well as all languages that +- For test 6.3.8 an installation of hunspell as well as all languages that you want to spell check is required. ### Managing Hunspell languages A CSAF Document can contain a [language](https://docs.oasis-open.org/csaf/csaf/v2.0/cs02/csaf-v2.0-cs02.html#3216-document-property---language). For example, valid entries could be `en` or `en-US`. When running test 6.3.8 we -try to match this language to the list of installed hunspell languages. If the +try to match this language to the list of installed hunspell languages. If the region is specified (like in `en-US`) and the corresponding language is installed the test will run. If you want/need to check a `en` language specifically with `en-US` (or any other variant) you need to make sure that you link `en` to `en-US` using a symlink. Example of linking `en` to `en-US`: + ```sh ln -s /usr/share/hunspell/en_US.aff /usr/share/hunspell/en.aff ln -s /usr/share/hunspell/en_US.dic /usr/share/hunspell/en.dic @@ -83,9 +96,9 @@ ln -s /usr/share/hunspell/en_US.dic /usr/share/hunspell/en.dic You can find out what languages you have installed by running `hunspell -D`. -If you need additional languages they are most likely available in the +If you need additional languages they are most likely available in the repository of your distribution. If you have a custom dictionary -copy them in the directory provided by the command above. Hunspell should +copy them in the directory provided by the command above. Hunspell should automatically recognize them. [(back to top)](#bsi-csaf-validator-lib) @@ -117,7 +130,7 @@ automatically recognize them. The library has two validate functions, `validate` and `validateStrict`. `validateStrict` checks whether the test that should be executed was defined in the library. Otherwise, it throws an error. To extend the library you can use -the `validate` function instead. In such case, **the calling function is +the `validate` function instead. In such case, **the calling function is responsible for checking** whether the test function passed to the `csaf-validator-lib` is benign. **Calling arbitrary** functions (especially those resulting from user input) may result in a **code execution @@ -127,9 +140,9 @@ To proceed this dangerous path, use the `validate` function. [(back to top)](#bsi-csaf-validator-lib) -### API +## API -#### Interfaces +### Interfaces ```typescript interface Result { @@ -159,6 +172,8 @@ type DocumentTest = (doc: any) => TestResult | Promise [(back to top)](#bsi-csaf-validator-lib) +### CSAF 2.0 + #### Module `schemaTests.js` ```typescript @@ -281,19 +296,143 @@ This module exports all tests included in `extended.js` and all informative test [(back to top)](#bsi-csaf-validator-lib) -#### Module `validate.js` +### CSAF 2.1 + +#### Module `csaf_2_1/schemaTests.js` + +```typescript +export const csaf_2_0_strict: DocumentTest +export const csaf_2_0: DocumentTest +``` + +[(back to top)](#bsi-csaf-validator-lib) + +#### Module `csaf_2_1/mandatoryTests.js` + +```typescript +export const mandatoryTest_6_1_1: DocumentTest +export const mandatoryTest_6_1_2: DocumentTest +export const mandatoryTest_6_1_3: DocumentTest +export const mandatoryTest_6_1_4: DocumentTest +export const mandatoryTest_6_1_5: DocumentTest +export const mandatoryTest_6_1_6: DocumentTest +export const mandatoryTest_6_1_7: DocumentTest +export const mandatoryTest_6_1_8: DocumentTest +export const mandatoryTest_6_1_9: DocumentTest +export const mandatoryTest_6_1_10: DocumentTest +export const mandatoryTest_6_1_11: DocumentTest +export const mandatoryTest_6_1_12: DocumentTest +export const mandatoryTest_6_1_13: DocumentTest +export const mandatoryTest_6_1_14: DocumentTest +export const mandatoryTest_6_1_15: DocumentTest +export const mandatoryTest_6_1_16: DocumentTest +export const mandatoryTest_6_1_17: DocumentTest +export const mandatoryTest_6_1_18: DocumentTest +export const mandatoryTest_6_1_19: DocumentTest +export const mandatoryTest_6_1_20: DocumentTest +export const mandatoryTest_6_1_21: DocumentTest +export const mandatoryTest_6_1_22: DocumentTest +export const mandatoryTest_6_1_23: DocumentTest +export const mandatoryTest_6_1_24: DocumentTest +export const mandatoryTest_6_1_25: DocumentTest +export const mandatoryTest_6_1_26: DocumentTest +export const mandatoryTest_6_1_27_1: DocumentTest +export const mandatoryTest_6_1_27_2: DocumentTest +export const mandatoryTest_6_1_27_3: DocumentTest +export const mandatoryTest_6_1_27_4: DocumentTest +export const mandatoryTest_6_1_27_5: DocumentTest +export const mandatoryTest_6_1_27_6: DocumentTest +export const mandatoryTest_6_1_27_7: DocumentTest +export const mandatoryTest_6_1_27_8: DocumentTest +export const mandatoryTest_6_1_27_9: DocumentTest +export const mandatoryTest_6_1_27_10: DocumentTest +export const mandatoryTest_6_1_27_11: DocumentTest +export const mandatoryTest_6_1_28: DocumentTest +export const mandatoryTest_6_1_29: DocumentTest +export const mandatoryTest_6_1_30: DocumentTest +export const mandatoryTest_6_1_31: DocumentTest +export const mandatoryTest_6_1_32: DocumentTest +export const mandatoryTest_6_1_33: DocumentTest +``` + +[(back to top)](#bsi-csaf-validator-lib) + +#### Module `csaf_2_1/optionalTests.js` + +```typescript +export const optionalTest_6_2_1: DocumentTest +export const optionalTest_6_2_2: DocumentTest +export const optionalTest_6_2_3: DocumentTest +export const optionalTest_6_2_4: DocumentTest +export const optionalTest_6_2_5: DocumentTest +export const optionalTest_6_2_6: DocumentTest +export const optionalTest_6_2_7: DocumentTest +export const optionalTest_6_2_8: DocumentTest +export const optionalTest_6_2_9: DocumentTest +export const optionalTest_6_2_10: DocumentTest +export const optionalTest_6_2_11: DocumentTest +export const optionalTest_6_2_12: DocumentTest +export const optionalTest_6_2_13: DocumentTest +export const optionalTest_6_2_14: DocumentTest +export const optionalTest_6_2_15: DocumentTest +export const optionalTest_6_2_16: DocumentTest +export const optionalTest_6_2_17: DocumentTest +export const optionalTest_6_2_18: DocumentTest +export const optionalTest_6_2_19: DocumentTest +export const optionalTest_6_2_20: DocumentTest +``` + +[(back to top)](#bsi-csaf-validator-lib) + +#### Module `csaf_2_1/informativeTests.js` + +```typescript +export const informativeTest_6_3_1: DocumentTest +export const informativeTest_6_3_2: DocumentTest +export const informativeTest_6_3_3: DocumentTest +export const informativeTest_6_3_4: DocumentTest +export const informativeTest_6_3_5: DocumentTest +export const informativeTest_6_3_6: DocumentTest +export const informativeTest_6_3_7: DocumentTest +export const informativeTest_6_3_8: DocumentTest +export const informativeTest_6_3_9: DocumentTest +export const informativeTest_6_3_10: DocumentTest +export const informativeTest_6_3_11: DocumentTest +``` + +[(back to top)](#bsi-csaf-validator-lib) + +#### Module `csaf_2_1/basic.js` + +This module exports the strict schema test and all mandatory tests except `6.1.8`. + +[(back to top)](#bsi-csaf-validator-lib) + +#### Module `csaf_2_1/extended.js` + +This module exports all tests included in `basic.js` and all optional tests. + +[(back to top)](#bsi-csaf-validator-lib) + +#### Module `csaf_2_1/full.js` + +This module exports all tests included in `extended.js` and all informative tests. + +[(back to top)](#bsi-csaf-validator-lib) + +### Module `validate.js` This function validates the given document against the given tests. -#### Module `validateStrict.js` +### Module `validateStrict.js` -This function validates the given document against the given tests. It throws +This function validates the given document against the given tests. It throws an error if an unknown test function was passed. See [Strict Mode](#strict-mode) for more details. [(back to top)](#bsi-csaf-validator-lib) -#### Module `strip.js` +### Module `strip.js` This function strips empty nodes and nodes with errors. The `strict` option (default `true`) throws an error if an unknown test function was passed. See [Strict Mode](#strict-mode) for more details. @@ -316,7 +455,7 @@ export default StripFn [(back to top)](#bsi-csaf-validator-lib) -#### Module `cwe.js` +### Module `cwe.js` ```typescript export const weaknesses: Array<{ id: string; name: string }> diff --git a/csaf_2_1/basic.js b/csaf_2_1/basic.js new file mode 100644 index 00000000..bea4d692 --- /dev/null +++ b/csaf_2_1/basic.js @@ -0,0 +1,46 @@ +export { csaf_2_0_strict } from '../schemaTests.js' +export { + mandatoryTest_6_1_1, + mandatoryTest_6_1_2, + mandatoryTest_6_1_3, + mandatoryTest_6_1_4, + mandatoryTest_6_1_5, + mandatoryTest_6_1_6, + mandatoryTest_6_1_7, + // Mandatory Test 6.1.8 skipped since included in schema tests + mandatoryTest_6_1_9, + mandatoryTest_6_1_10, + mandatoryTest_6_1_11, + mandatoryTest_6_1_12, + mandatoryTest_6_1_13, + mandatoryTest_6_1_14, + mandatoryTest_6_1_15, + mandatoryTest_6_1_16, + mandatoryTest_6_1_17, + mandatoryTest_6_1_18, + mandatoryTest_6_1_19, + mandatoryTest_6_1_20, + mandatoryTest_6_1_21, + mandatoryTest_6_1_22, + mandatoryTest_6_1_23, + mandatoryTest_6_1_24, + mandatoryTest_6_1_25, + mandatoryTest_6_1_26, + mandatoryTest_6_1_27_1, + mandatoryTest_6_1_27_2, + mandatoryTest_6_1_27_3, + mandatoryTest_6_1_27_4, + mandatoryTest_6_1_27_5, + mandatoryTest_6_1_27_6, + mandatoryTest_6_1_27_7, + mandatoryTest_6_1_27_8, + mandatoryTest_6_1_27_9, + mandatoryTest_6_1_27_10, + mandatoryTest_6_1_27_11, + mandatoryTest_6_1_28, + mandatoryTest_6_1_29, + mandatoryTest_6_1_30, + mandatoryTest_6_1_31, + mandatoryTest_6_1_32, + mandatoryTest_6_1_33, +} from '../mandatoryTests.js' diff --git a/csaf_2_1/extended.js b/csaf_2_1/extended.js new file mode 100644 index 00000000..d83e0ac0 --- /dev/null +++ b/csaf_2_1/extended.js @@ -0,0 +1,2 @@ +export * from './basic.js' +export * from './optionalTests.js' diff --git a/csaf_2_1/full.js b/csaf_2_1/full.js new file mode 100644 index 00000000..354517b9 --- /dev/null +++ b/csaf_2_1/full.js @@ -0,0 +1,2 @@ +export * from './extended.js' +export * from './informativeTests.js' diff --git a/csaf_2_1/informativeTests.js b/csaf_2_1/informativeTests.js new file mode 100644 index 00000000..3c193ad2 --- /dev/null +++ b/csaf_2_1/informativeTests.js @@ -0,0 +1,13 @@ +export { + informativeTest_6_3_1, + informativeTest_6_3_2, + informativeTest_6_3_3, + informativeTest_6_3_4, + informativeTest_6_3_5, + informativeTest_6_3_6, + informativeTest_6_3_7, + informativeTest_6_3_8, + informativeTest_6_3_9, + informativeTest_6_3_10, + informativeTest_6_3_11, +} from '../informativeTests.js' diff --git a/csaf_2_1/mandatoryTests.js b/csaf_2_1/mandatoryTests.js new file mode 100644 index 00000000..be6b62ce --- /dev/null +++ b/csaf_2_1/mandatoryTests.js @@ -0,0 +1,45 @@ +export { + mandatoryTest_6_1_1, + mandatoryTest_6_1_2, + mandatoryTest_6_1_3, + mandatoryTest_6_1_4, + mandatoryTest_6_1_5, + mandatoryTest_6_1_6, + mandatoryTest_6_1_7, + mandatoryTest_6_1_8, + mandatoryTest_6_1_9, + mandatoryTest_6_1_10, + mandatoryTest_6_1_11, + mandatoryTest_6_1_12, + mandatoryTest_6_1_13, + mandatoryTest_6_1_14, + mandatoryTest_6_1_15, + mandatoryTest_6_1_16, + mandatoryTest_6_1_17, + mandatoryTest_6_1_18, + mandatoryTest_6_1_19, + mandatoryTest_6_1_20, + mandatoryTest_6_1_21, + mandatoryTest_6_1_22, + mandatoryTest_6_1_23, + mandatoryTest_6_1_24, + mandatoryTest_6_1_25, + mandatoryTest_6_1_26, + mandatoryTest_6_1_27_1, + mandatoryTest_6_1_27_2, + mandatoryTest_6_1_27_3, + mandatoryTest_6_1_27_4, + mandatoryTest_6_1_27_5, + mandatoryTest_6_1_27_6, + mandatoryTest_6_1_27_7, + mandatoryTest_6_1_27_8, + mandatoryTest_6_1_27_9, + mandatoryTest_6_1_27_10, + mandatoryTest_6_1_27_11, + mandatoryTest_6_1_28, + mandatoryTest_6_1_29, + mandatoryTest_6_1_30, + mandatoryTest_6_1_31, + mandatoryTest_6_1_32, + mandatoryTest_6_1_33, +} from '../mandatoryTests.js' diff --git a/csaf_2_1/optionalTests.js b/csaf_2_1/optionalTests.js new file mode 100644 index 00000000..ba67ec52 --- /dev/null +++ b/csaf_2_1/optionalTests.js @@ -0,0 +1,22 @@ +export { + optionalTest_6_2_1, + optionalTest_6_2_2, + optionalTest_6_2_3, + optionalTest_6_2_4, + optionalTest_6_2_5, + optionalTest_6_2_6, + optionalTest_6_2_7, + optionalTest_6_2_8, + optionalTest_6_2_9, + optionalTest_6_2_10, + optionalTest_6_2_11, + optionalTest_6_2_12, + optionalTest_6_2_13, + optionalTest_6_2_14, + optionalTest_6_2_15, + optionalTest_6_2_16, + optionalTest_6_2_17, + optionalTest_6_2_18, + optionalTest_6_2_19, + optionalTest_6_2_20, +} from '../optionalTests.js' diff --git a/csaf_2_1/schemaTests.js b/csaf_2_1/schemaTests.js new file mode 100644 index 00000000..bee19515 --- /dev/null +++ b/csaf_2_1/schemaTests.js @@ -0,0 +1,2 @@ +export { default as csaf_2_1_strict } from './schemaTests/csaf_2_1_strict.js' +export { default as csaf_2_1 } from './schemaTests/csaf_2_1.js' diff --git a/csaf_2_1/schemaTests/csaf_2_1.js b/csaf_2_1/schemaTests/csaf_2_1.js new file mode 100644 index 00000000..cef04aa1 --- /dev/null +++ b/csaf_2_1/schemaTests/csaf_2_1.js @@ -0,0 +1,26 @@ +import csafAjv from '../../lib/shared/csafAjv.js' +import schema from './csaf_2_1/schema.js' + +const validate = csafAjv.compile(schema) + +/** + * @param {any} doc + */ +export default function csaf_2_1(doc) { + let isValid = validate(doc) + /** + * + * @type {Array<{ + * message?: string + * instancePath: string + * }>} + */ + const errors = validate.errors ?? [] + return { + isValid, + errors: errors.map((e) => ({ + ...e, + message: e.message ?? 'unexpected empty error message', + })), + } +} diff --git a/csaf_2_1/schemaTests/csaf_2_1/schema.js b/csaf_2_1/schemaTests/csaf_2_1/schema.js new file mode 100644 index 00000000..e2d53e2b --- /dev/null +++ b/csaf_2_1/schemaTests/csaf_2_1/schema.js @@ -0,0 +1,1485 @@ +export default { + $schema: 'https://json-schema.org/draft/2020-12/schema', + $id: 'https://docs.oasis-open.org/csaf/csaf/v2.1/csaf_json_schema.json', + title: 'Common Security Advisory Framework', + description: + 'Representation of security advisory information as a JSON document.', + type: 'object', + $defs: { + acknowledgments_t: { + title: 'List of acknowledgments', + description: 'Contains a list of acknowledgment elements.', + type: 'array', + minItems: 1, + items: { + title: 'Acknowledgment', + description: + 'Acknowledges contributions by describing those that contributed.', + type: 'object', + minProperties: 1, + properties: { + names: { + title: 'List of acknowledged names', + description: 'Contains the names of contributors being recognized.', + type: 'array', + minItems: 1, + items: { + title: 'Name of the contributor', + description: + 'Contains the name of a single contributor being recognized.', + type: 'string', + minLength: 1, + examples: ['Albert Einstein', 'Johann Sebastian Bach'], + }, + }, + organization: { + title: 'Contributing organization', + description: + 'Contains the name of a contributing organization being recognized.', + type: 'string', + minLength: 1, + examples: ['CISA', 'Google Project Zero', 'Talos'], + }, + summary: { + title: 'Summary of the acknowledgment', + description: + 'SHOULD represent any contextual details the document producers wish to make known about the acknowledgment or acknowledged parties.', + type: 'string', + minLength: 1, + examples: [ + 'First analysis of Coordinated Multi-Stream Attack (CMSA)', + ], + }, + urls: { + title: 'List of URLs', + description: + 'Specifies a list of URLs or location of the reference to be acknowledged.', + type: 'array', + minItems: 1, + items: { + title: 'URL of acknowledgment', + description: + 'Contains the URL or location of the reference to be acknowledged.', + type: 'string', + format: 'uri', + }, + }, + }, + }, + }, + branches_t: { + title: 'List of branches', + description: + 'Contains branch elements as children of the current element.', + type: 'array', + minItems: 1, + items: { + title: 'Branch', + description: + 'Is a part of the hierarchical structure of the product tree.', + type: 'object', + maxProperties: 3, + minProperties: 3, + required: ['category', 'name'], + properties: { + branches: { + $ref: '#/$defs/branches_t', + }, + category: { + title: 'Category of the branch', + description: 'Describes the characteristics of the labeled branch.', + type: 'string', + enum: [ + 'architecture', + 'host_name', + 'language', + 'legacy', + 'patch_level', + 'platform', + 'product_family', + 'product_name', + 'product_version', + 'product_version_range', + 'service_pack', + 'specification', + 'vendor', + ], + }, + name: { + title: 'Name of the branch', + description: + "Contains the canonical descriptor or 'friendly name' of the branch.", + type: 'string', + minLength: 1, + examples: [ + '10', + '365', + 'Microsoft', + 'Office', + 'PCS 7', + 'SIMATIC', + 'Siemens', + 'Windows', + ], + }, + product: { + $ref: '#/$defs/full_product_name_t', + }, + }, + }, + }, + full_product_name_t: { + title: 'Full product name', + description: + 'Specifies information about the product and assigns the product_id.', + type: 'object', + required: ['name', 'product_id'], + properties: { + name: { + title: 'Textual description of the product', + description: + 'The value should be the product’s full canonical name, including version number and other attributes, as it would be used in a human-friendly document.', + type: 'string', + minLength: 1, + examples: [ + 'Cisco AnyConnect Secure Mobility Client 2.3.185', + 'Microsoft Host Integration Server 2006 Service Pack 1', + ], + }, + product_id: { + $ref: '#/$defs/product_id_t', + }, + product_identification_helper: { + title: 'Helper to identify the product', + description: + 'Provides at least one method which aids in identifying the product in an asset database.', + type: 'object', + minProperties: 1, + properties: { + cpe: { + title: 'Common Platform Enumeration representation', + description: + 'The Common Platform Enumeration (CPE) attribute refers to a method for naming platforms external to this specification.', + type: 'string', + pattern: + '^((cpe:2\\.3:[aho\\*\\-](:(((\\?*|\\*?)([a-zA-Z0-9\\-\\._]|(\\\\[\\\\\\*\\?!"#\\$%&\'\\(\\)\\+,\\/:;<=>@\\[\\]\\^`\\{\\|\\}~]))+(\\?*|\\*?))|[\\*\\-])){5}(:(([a-zA-Z]{2,3}(-([a-zA-Z]{2}|[0-9]{3}))?)|[\\*\\-]))(:(((\\?*|\\*?)([a-zA-Z0-9\\-\\._]|(\\\\[\\\\\\*\\?!"#\\$%&\'\\(\\)\\+,\\/:;<=>@\\[\\]\\^`\\{\\|\\}~]))+(\\?*|\\*?))|[\\*\\-])){4})|([c][pP][eE]:\\/[AHOaho]?(:[A-Za-z0-9\\._\\-~%]*){0,6}))$', + minLength: 5, + }, + hashes: { + title: 'List of hashes', + description: + 'Contains a list of cryptographic hashes usable to identify files.', + type: 'array', + minItems: 1, + items: { + title: 'Cryptographic hashes', + description: + 'Contains all information to identify a file based on its cryptographic hash values.', + type: 'object', + required: ['file_hashes', 'filename'], + properties: { + file_hashes: { + title: 'List of file hashes', + description: + 'Contains a list of cryptographic hashes for this file.', + type: 'array', + minItems: 1, + items: { + title: 'File hash', + description: + 'Contains one hash value and algorithm of the file to be identified.', + type: 'object', + required: ['algorithm', 'value'], + properties: { + algorithm: { + title: 'Algorithm of the cryptographic hash', + description: + 'Contains the name of the cryptographic hash algorithm used to calculate the value.', + type: 'string', + default: 'sha256', + minLength: 1, + examples: [ + 'blake2b512', + 'sha256', + 'sha3-512', + 'sha384', + 'sha512', + ], + }, + value: { + title: 'Value of the cryptographic hash', + description: + 'Contains the cryptographic hash value in hexadecimal representation.', + type: 'string', + pattern: '^[0-9a-fA-F]{32,}$', + minLength: 32, + examples: [ + '37df33cb7464da5c7f077f4d56a32bc84987ec1d85b234537c1c1a4d4fc8d09dc29e2e762cb5203677bf849a2855a0283710f1f5fe1d6ce8d5ac85c645d0fcb3', + '4775203615d9534a8bfca96a93dc8b461a489f69124a130d786b42204f3341cc', + '9ea4c8200113d49d26505da0e02e2f49055dc078d1ad7a419b32e291c7afebbb84badfbd46dec42883bea0b2a1fa697c', + ], + }, + }, + }, + }, + filename: { + title: 'Filename', + description: + 'Contains the name of the file which is identified by the hash values.', + type: 'string', + minLength: 1, + examples: ['WINWORD.EXE', 'msotadddin.dll', 'sudoers.so'], + }, + }, + }, + }, + model_numbers: { + title: 'List of models', + description: + 'Contains a list of full or abbreviated (partial) model numbers.', + type: 'array', + minItems: 1, + uniqueItems: true, + items: { + title: 'Model number', + description: + 'Contains a full or abbreviated (partial) model number of the component to identify.', + type: 'string', + minLength: 1, + }, + }, + purl: { + title: 'package URL representation', + description: + 'The package URL (purl) attribute refers to a method for reliably identifying and locating software packages external to this specification.', + type: 'string', + format: 'uri', + pattern: '^pkg:[A-Za-z\\.\\-\\+][A-Za-z0-9\\.\\-\\+]*\\/.+', + minLength: 7, + }, + sbom_urls: { + title: 'List of SBOM URLs', + description: + 'Contains a list of URLs where SBOMs for this product can be retrieved.', + type: 'array', + minItems: 1, + items: { + title: 'SBOM URL', + description: 'Contains a URL of one SBOM for this product.', + type: 'string', + format: 'uri', + }, + }, + serial_numbers: { + title: 'List of serial numbers', + description: + 'Contains a list of full or abbreviated (partial) serial numbers.', + type: 'array', + minItems: 1, + uniqueItems: true, + items: { + title: 'Serial number', + description: + 'Contains a full or abbreviated (partial) serial number of the component to identify.', + type: 'string', + minLength: 1, + }, + }, + skus: { + title: 'List of stock keeping units', + description: + 'Contains a list of full or abbreviated (partial) stock keeping units.', + type: 'array', + minItems: 1, + items: { + title: 'Stock keeping unit', + description: + 'Contains a full or abbreviated (partial) stock keeping unit (SKU) which is used in the ordering process to identify the component.', + type: 'string', + minLength: 1, + }, + }, + x_generic_uris: { + title: 'List of generic URIs', + description: + 'Contains a list of identifiers which are either vendor-specific or derived from a standard not yet supported.', + type: 'array', + minItems: 1, + items: { + title: 'Generic URI', + description: + 'Provides a generic extension point for any identifier which is either vendor-specific or derived from a standard not yet supported.', + type: 'object', + required: ['namespace', 'uri'], + properties: { + namespace: { + title: 'Namespace of the generic URI', + description: + 'Refers to a URL which provides the name and knowledge about the specification used or is the namespace in which these values are valid.', + type: 'string', + format: 'uri', + }, + uri: { + title: 'URI', + description: 'Contains the identifier itself.', + type: 'string', + format: 'uri', + }, + }, + }, + }, + }, + }, + }, + }, + lang_t: { + title: 'Language type', + description: + 'Identifies a language, corresponding to IETF BCP 47 / RFC 5646. See IETF language registry: https://www.iana.org/assignments/language-subtag-registry/language-subtag-registry', + type: 'string', + pattern: + '^(([A-Za-z]{2,3}(-[A-Za-z]{3}(-[A-Za-z]{3}){0,2})?|[A-Za-z]{4,8})(-[A-Za-z]{4})?(-([A-Za-z]{2}|[0-9]{3}))?(-([A-Za-z0-9]{5,8}|[0-9][A-Za-z0-9]{3}))*(-[A-WY-Za-wy-z0-9](-[A-Za-z0-9]{2,8})+)*(-[Xx](-[A-Za-z0-9]{1,8})+)?|[Xx](-[A-Za-z0-9]{1,8})+|[Ii]-[Dd][Ee][Ff][Aa][Uu][Ll][Tt]|[Ii]-[Mm][Ii][Nn][Gg][Oo])$', + examples: ['de', 'en', 'fr', 'frc', 'jp'], + }, + notes_t: { + title: 'List of notes', + description: 'Contains notes which are specific to the current context.', + type: 'array', + minItems: 1, + items: { + title: 'Note', + description: + 'Is a place to put all manner of text blobs related to the current context.', + type: 'object', + required: ['category', 'text'], + properties: { + audience: { + title: 'Audience of note', + description: 'Indicates who is intended to read it.', + type: 'string', + minLength: 1, + examples: [ + 'all', + 'executives', + 'operational management and system administrators', + 'safety engineers', + ], + }, + category: { + title: 'Note category', + description: + 'Contains the information of what kind of note this is.', + type: 'string', + enum: [ + 'description', + 'details', + 'faq', + 'general', + 'legal_disclaimer', + 'other', + 'summary', + ], + }, + text: { + title: 'Note content', + description: + 'Holds the content of the note. Content varies depending on type.', + type: 'string', + minLength: 1, + }, + title: { + title: 'Title of note', + description: + 'Provides a concise description of what is contained in the text of the note.', + type: 'string', + minLength: 1, + examples: [ + 'Details', + 'Executive summary', + 'Technical summary', + 'Impact on safety systems', + ], + }, + }, + }, + }, + product_group_id_t: { + title: 'Reference token for product group instance', + description: + 'Token required to identify a group of products so that it can be referred to from other parts in the document. There is no predefined or required format for the product_group_id as long as it uniquely identifies a group in the context of the current document.', + type: 'string', + minLength: 1, + examples: ['CSAFGID-0001', 'CSAFGID-0002', 'CSAFGID-0020'], + }, + product_groups_t: { + title: 'List of product_group_ids', + description: + 'Specifies a list of product_group_ids to give context to the parent item.', + type: 'array', + minItems: 1, + uniqueItems: true, + items: { + $ref: '#/$defs/product_group_id_t', + }, + }, + product_id_t: { + title: 'Reference token for product instance', + description: + 'Token required to identify a full_product_name so that it can be referred to from other parts in the document. There is no predefined or required format for the product_id as long as it uniquely identifies a product in the context of the current document.', + type: 'string', + minLength: 1, + examples: ['CSAFPID-0004', 'CSAFPID-0008'], + }, + products_t: { + title: 'List of product_ids', + description: + 'Specifies a list of product_ids to give context to the parent item.', + type: 'array', + minItems: 1, + uniqueItems: true, + items: { + $ref: '#/$defs/product_id_t', + }, + }, + references_t: { + title: 'List of references', + description: 'Holds a list of references.', + type: 'array', + minItems: 1, + items: { + title: 'Reference', + description: + 'Holds any reference to conferences, papers, advisories, and other resources that are related and considered related to either a surrounding part of or the entire document and to be of value to the document consumer.', + type: 'object', + required: ['summary', 'url'], + properties: { + category: { + title: 'Category of reference', + description: + 'Indicates whether the reference points to the same document or vulnerability in focus (depending on scope) or to an external resource.', + type: 'string', + default: 'external', + enum: ['external', 'self'], + }, + summary: { + title: 'Summary of the reference', + description: 'Indicates what this reference refers to.', + type: 'string', + minLength: 1, + }, + url: { + title: 'URL of reference', + description: 'Provides the URL for the reference.', + type: 'string', + format: 'uri', + }, + }, + }, + }, + version_t: { + title: 'Version', + description: + 'Specifies a version string to denote clearly the evolution of the content of the document. Format must be either integer or semantic versioning.', + type: 'string', + pattern: + '^(0|[1-9][0-9]*)$|^((0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?)$', + examples: ['1', '4', '0.9.0', '1.4.3', '2.40.0+21AF26D3'], + }, + }, + required: ['$schema', 'document'], + properties: { + $schema: { + title: 'JSON schema', + description: + 'Contains the URL of the CSAF JSON schema which the document promises to be valid for.', + type: 'string', + enum: [ + 'https://docs.oasis-open.org/csaf/csaf/v2.1/csaf_json_schema.json', + ], + format: 'uri', + }, + document: { + title: 'Document level meta-data', + description: + 'Captures the meta-data about this document describing a particular set of security advisories.', + type: 'object', + required: [ + 'category', + 'csaf_version', + 'distribution', + 'publisher', + 'title', + 'tracking', + ], + properties: { + acknowledgments: { + title: 'Document acknowledgments', + description: + 'Contains a list of acknowledgment elements associated with the whole document.', + $ref: '#/$defs/acknowledgments_t', + }, + aggregate_severity: { + title: 'Aggregate severity', + description: + "Is a vehicle that is provided by the document producer to convey the urgency and criticality with which the one or more vulnerabilities reported should be addressed. It is a document-level metric and applied to the document as a whole — not any specific vulnerability. The range of values in this field is defined according to the document producer's policies and procedures.", + type: 'object', + required: ['text'], + properties: { + namespace: { + title: 'Namespace of aggregate severity', + description: 'Points to the namespace so referenced.', + type: 'string', + format: 'uri', + }, + text: { + title: 'Text of aggregate severity', + description: + 'Provides a severity which is independent of - and in addition to - any other standard metric for determining the impact or severity of a given vulnerability (such as CVSS).', + type: 'string', + minLength: 1, + examples: ['Critical', 'Important', 'Moderate'], + }, + }, + }, + category: { + title: 'Document category', + description: + 'Defines a short canonical name, chosen by the document producer, which will inform the end user as to the category of document.', + type: 'string', + pattern: '^[^\\s\\-_\\.](.*[^\\s\\-_\\.])?$', + minLength: 1, + examples: [ + 'csaf_base', + 'csaf_security_advisory', + 'csaf_vex', + 'Example Company Security Notice', + ], + }, + csaf_version: { + title: 'CSAF version', + description: + 'Gives the version of the CSAF specification which the document was generated for.', + type: 'string', + enum: ['2.1'], + }, + distribution: { + title: 'Rules for sharing document', + description: + 'Describe any constraints on how this document might be shared.', + type: 'object', + required: ['tlp'], + properties: { + sharing_group: { + title: 'Sharing Group', + description: + 'Contains information about the group this document is intended to be shared with.', + type: 'object', + required: ['id'], + properties: { + id: { + title: 'Sharing Group ID', + description: 'Provides the unique ID for the sharing group.', + type: 'string', + format: 'uuid', + pattern: + '^(([0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[0-9a-f]{4}-[0-9a-f]{12})|([0]{8}-([0]{4}-){3}[0]{12})|([f]{8}-([f]{4}-){3}[f]{12}))$', + }, + name: { + title: 'Sharing Group Name', + description: + 'Contains a human-readable name for the sharing group.', + type: 'string', + minLength: 1, + examples: [ + 'Customer A', + 'ISAC members', + 'NIS2 regulated important entities in Germany, sector water', + 'Pre-Sharing group for advisory discussion', + 'Users of Product A', + 'US Federal Civilian Authorities', + ], + }, + }, + }, + text: { + title: 'Textual description', + description: + 'Provides a textual description of additional constraints.', + type: 'string', + minLength: 1, + examples: [ + 'Copyright 2021, Example Company, All Rights Reserved.', + 'Distribute freely.', + 'Share only on a need-to-know-basis only.', + ], + }, + tlp: { + title: 'Traffic Light Protocol (TLP)', + description: + 'Provides details about the TLP classification of the document.', + type: 'object', + required: ['label'], + properties: { + label: { + title: 'Label of TLP', + description: 'Provides the TLP label of the document.', + type: 'string', + default: 'CLEAR', + enum: ['AMBER', 'AMBER+STRICT', 'CLEAR', 'GREEN', 'RED'], + }, + url: { + title: 'URL of TLP version', + description: + 'Provides a URL where to find the textual description of the TLP version which is used in this document. Default is the URL to the definition by FIRST.', + type: 'string', + default: 'https://www.first.org/tlp/', + format: 'uri', + examples: [ + 'https://www.us-cert.gov/tlp', + 'https://www.bsi.bund.de/SharedDocs/Downloads/DE/BSI/Kritis/Merkblatt_TLP.pdf', + ], + }, + }, + }, + }, + }, + lang: { + title: 'Document language', + description: + 'Identifies the language used by this document, corresponding to IETF BCP 47 / RFC 5646.', + $ref: '#/$defs/lang_t', + }, + notes: { + title: 'Document notes', + description: 'Holds notes associated with the whole document.', + $ref: '#/$defs/notes_t', + }, + publisher: { + title: 'Publisher', + description: + 'Provides information about the publisher of the document.', + type: 'object', + required: ['category', 'name', 'namespace'], + properties: { + category: { + title: 'Category of publisher', + description: + 'Provides information about the category of publisher releasing the document.', + type: 'string', + enum: [ + 'coordinator', + 'discoverer', + 'multiplier', + 'other', + 'translator', + 'user', + 'vendor', + ], + }, + contact_details: { + title: 'Contact details', + description: + 'Information on how to contact the publisher, possibly including details such as web sites, email addresses, phone numbers, and postal mail addresses.', + type: 'string', + minLength: 1, + examples: [ + 'Example Company can be reached at contact_us@example.com, or via our website at https://www.example.com/contact.', + ], + }, + issuing_authority: { + title: 'Issuing authority', + description: + "Provides information about the authority of the issuing party to release the document, in particular, the party's constituency and responsibilities or other obligations.", + type: 'string', + minLength: 1, + }, + name: { + title: 'Name of publisher', + description: 'Contains the name of the issuing party.', + type: 'string', + minLength: 1, + examples: ['BSI', 'Cisco PSIRT', 'Siemens ProductCERT'], + }, + namespace: { + title: 'Namespace of publisher', + description: + 'Contains a URL which is under control of the issuing party and can be used as a globally unique identifier for that issuing party.', + type: 'string', + format: 'uri', + examples: ['https://csaf.io', 'https://www.example.com'], + }, + }, + }, + references: { + title: 'Document references', + description: + 'Holds a list of references associated with the whole document.', + $ref: '#/$defs/references_t', + }, + source_lang: { + title: 'Source language', + description: + 'If this copy of the document is a translation then the value of this property describes from which language this document was translated.', + $ref: '#/$defs/lang_t', + }, + title: { + title: 'Title of this document', + description: + 'This SHOULD be a canonical name for the document, and sufficiently unique to distinguish it from similar documents.', + type: 'string', + minLength: 1, + examples: [ + 'Cisco IPv6 Crafted Packet Denial of Service Vulnerability', + 'Example Company Cross-Site-Scripting Vulnerability in Example Generator', + ], + }, + tracking: { + title: 'Tracking', + description: + 'Is a container designated to hold all management attributes necessary to track a CSAF document as a whole.', + type: 'object', + required: [ + 'current_release_date', + 'id', + 'initial_release_date', + 'revision_history', + 'status', + 'version', + ], + properties: { + aliases: { + title: 'Aliases', + description: + 'Contains a list of alternate names for the same document.', + type: 'array', + minItems: 1, + uniqueItems: true, + items: { + title: 'Alternate name', + description: + 'Specifies a non-empty string that represents a distinct optional alternative ID used to refer to the document.', + type: 'string', + minLength: 1, + examples: ['CVE-2019-12345'], + }, + }, + current_release_date: { + title: 'Current release date', + description: + 'The date when the current revision of this document was released', + type: 'string', + format: 'date-time', + }, + generator: { + title: 'Document generator', + description: + 'Is a container to hold all elements related to the generation of the document. These items will reference when the document was actually created, including the date it was generated and the entity that generated it.', + type: 'object', + required: ['engine'], + properties: { + date: { + title: 'Date of document generation', + description: + 'This SHOULD be the current date that the document was generated. Because documents are often generated internally by a document producer and exist for a nonzero amount of time before being released, this field MAY be different from the Initial Release Date and Current Release Date.', + type: 'string', + format: 'date-time', + }, + engine: { + title: 'Engine of document generation', + description: + 'Contains information about the engine that generated the CSAF document.', + type: 'object', + required: ['name'], + properties: { + name: { + title: 'Engine name', + description: + 'Represents the name of the engine that generated the CSAF document.', + type: 'string', + minLength: 1, + examples: ['Red Hat rhsa-to-cvrf', 'Secvisogram', 'TVCE'], + }, + version: { + title: 'Engine version', + description: + 'Contains the version of the engine that generated the CSAF document.', + type: 'string', + minLength: 1, + examples: ['0.6.0', '1.0.0-beta+exp.sha.a1c44f85', '2'], + }, + }, + }, + }, + }, + id: { + title: 'Unique identifier for the document', + description: + 'The ID is a simple label that provides for a wide range of numbering values, types, and schemes. Its value SHOULD be assigned and maintained by the original document issuing authority.', + type: 'string', + pattern: '^[\\S](.*[\\S])?$', + minLength: 1, + examples: [ + 'Example Company - 2019-YH3234', + 'RHBA-2019:0024', + 'cisco-sa-20190513-secureboot', + ], + }, + initial_release_date: { + title: 'Initial release date', + description: 'The date when this document was first published.', + type: 'string', + format: 'date-time', + }, + revision_history: { + title: 'Revision history', + description: + 'Holds one revision item for each version of the CSAF document, including the initial one.', + type: 'array', + minItems: 1, + items: { + title: 'Revision', + description: + 'Contains all the information elements required to track the evolution of a CSAF document.', + type: 'object', + required: ['date', 'number', 'summary'], + properties: { + date: { + title: 'Date of the revision', + description: 'The date of the revision entry', + type: 'string', + format: 'date-time', + }, + legacy_version: { + title: 'Legacy version of the revision', + description: + 'Contains the version string used in an existing document with the same content.', + type: 'string', + minLength: 1, + }, + number: { + $ref: '#/$defs/version_t', + }, + summary: { + title: 'Summary of the revision', + description: + 'Holds a single non-empty string representing a short description of the changes.', + type: 'string', + minLength: 1, + examples: ['Initial version.'], + }, + }, + }, + }, + status: { + title: 'Document status', + description: 'Defines the draft status of the document.', + type: 'string', + enum: ['draft', 'final', 'interim'], + }, + version: { + $ref: '#/$defs/version_t', + }, + }, + }, + }, + }, + product_tree: { + title: 'Product tree', + description: + 'Is a container for all fully qualified product names that can be referenced elsewhere in the document.', + type: 'object', + minProperties: 1, + properties: { + branches: { + $ref: '#/$defs/branches_t', + }, + full_product_names: { + title: 'List of full product names', + description: 'Contains a list of full product names.', + type: 'array', + minItems: 1, + items: { + $ref: '#/$defs/full_product_name_t', + }, + }, + product_groups: { + title: 'List of product groups', + description: 'Contains a list of product groups.', + type: 'array', + minItems: 1, + items: { + title: 'Product group', + description: + 'Defines a new logical group of products that can then be referred to in other parts of the document to address a group of products with a single identifier.', + type: 'object', + required: ['group_id', 'product_ids'], + properties: { + group_id: { + $ref: '#/$defs/product_group_id_t', + }, + product_ids: { + title: 'List of Product IDs', + description: + 'Lists the product_ids of those products which known as one group in the document.', + type: 'array', + minItems: 2, + uniqueItems: true, + items: { + $ref: '#/$defs/product_id_t', + }, + }, + summary: { + title: 'Summary of the product group', + description: + 'Gives a short, optional description of the group.', + type: 'string', + minLength: 1, + examples: [ + 'Products supporting Modbus.', + 'The x64 versions of the operating system.', + ], + }, + }, + }, + }, + relationships: { + title: 'List of relationships', + description: 'Contains a list of relationships.', + type: 'array', + minItems: 1, + items: { + title: 'Relationship', + description: + 'Establishes a link between two existing full_product_name_t elements, allowing the document producer to define a combination of two products that form a new full_product_name entry.', + type: 'object', + required: [ + 'category', + 'full_product_name', + 'product_reference', + 'relates_to_product_reference', + ], + properties: { + category: { + title: 'Relationship category', + description: + 'Defines the category of relationship for the referenced component.', + type: 'string', + enum: [ + 'default_component_of', + 'external_component_of', + 'installed_on', + 'installed_with', + 'optional_component_of', + ], + }, + full_product_name: { + $ref: '#/$defs/full_product_name_t', + }, + product_reference: { + title: 'Product reference', + description: + 'Holds a Product ID that refers to the Full Product Name element, which is referenced as the first element of the relationship.', + $ref: '#/$defs/product_id_t', + }, + relates_to_product_reference: { + title: 'Relates to product reference', + description: + 'Holds a Product ID that refers to the Full Product Name element, which is referenced as the second element of the relationship.', + $ref: '#/$defs/product_id_t', + }, + }, + }, + }, + }, + }, + vulnerabilities: { + title: 'Vulnerabilities', + description: + 'Represents a list of all relevant vulnerability information items.', + type: 'array', + minItems: 1, + items: { + title: 'Vulnerability', + description: + 'Is a container for the aggregation of all fields that are related to a single vulnerability in the document.', + type: 'object', + minProperties: 1, + properties: { + acknowledgments: { + title: 'Vulnerability acknowledgments', + description: + 'Contains a list of acknowledgment elements associated with this vulnerability item.', + $ref: '#/$defs/acknowledgments_t', + }, + cve: { + title: 'CVE', + description: + 'Holds the MITRE standard Common Vulnerabilities and Exposures (CVE) tracking number for the vulnerability.', + type: 'string', + pattern: '^CVE-[0-9]{4}-[0-9]{4,}$', + }, + cwes: { + title: 'List of CWEs', + description: 'Contains a list of CWEs.', + type: 'array', + minItems: 1, + uniqueItems: true, + items: { + title: 'CWE', + description: + 'Holds the MITRE standard Common Weakness Enumeration (CWE) for the weakness associated.', + type: 'object', + required: ['id', 'name', 'version'], + properties: { + id: { + title: 'Weakness ID', + description: 'Holds the ID for the weakness associated.', + type: 'string', + pattern: '^CWE-[1-9]\\d{0,5}$', + examples: ['CWE-22', 'CWE-352', 'CWE-79'], + }, + name: { + title: 'Weakness name', + description: + 'Holds the full name of the weakness as given in the CWE specification.', + type: 'string', + minLength: 1, + examples: [ + 'Cross-Site Request Forgery (CSRF)', + "Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')", + "Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')", + ], + }, + version: { + title: 'CWE version', + description: + 'Holds the version string of the CWE specification this weakness was extracted from.', + type: 'string', + minLength: 1, + pattern: '^[1-9]\\d*\\.([0-9]|([1-9]\\d+))(\\.\\d+)?$', + examples: ['1.0', '3.4.1', '4.0', '4.11', '4.12'], + }, + }, + }, + }, + discovery_date: { + title: 'Discovery date', + description: + 'Holds the date and time the vulnerability was originally discovered.', + type: 'string', + format: 'date-time', + }, + flags: { + title: 'List of flags', + description: 'Contains a list of machine readable flags.', + type: 'array', + minItems: 1, + uniqueItems: true, + items: { + title: 'Flag', + description: + 'Contains product specific information in regard to this vulnerability as a single machine readable flag.', + type: 'object', + required: ['label'], + properties: { + date: { + title: 'Date of the flag', + description: + 'Contains the date when assessment was done or the flag was assigned.', + type: 'string', + format: 'date-time', + }, + group_ids: { + $ref: '#/$defs/product_groups_t', + }, + label: { + title: 'Label of the flag', + description: 'Specifies the machine readable label.', + type: 'string', + enum: [ + 'component_not_present', + 'inline_mitigations_already_exist', + 'vulnerable_code_cannot_be_controlled_by_adversary', + 'vulnerable_code_not_in_execute_path', + 'vulnerable_code_not_present', + ], + }, + product_ids: { + $ref: '#/$defs/products_t', + }, + }, + }, + }, + ids: { + title: 'List of IDs', + description: + 'Represents a list of unique labels or tracking IDs for the vulnerability (if such information exists).', + type: 'array', + minItems: 1, + uniqueItems: true, + items: { + title: 'ID', + description: + 'Contains a single unique label or tracking ID for the vulnerability.', + type: 'object', + required: ['system_name', 'text'], + properties: { + system_name: { + title: 'System name', + description: + 'Indicates the name of the vulnerability tracking or numbering system.', + type: 'string', + minLength: 1, + examples: ['Cisco Bug ID', 'GitHub Issue'], + }, + text: { + title: 'Text', + description: + 'Is unique label or tracking ID for the vulnerability (if such information exists).', + type: 'string', + minLength: 1, + examples: ['CSCso66472', 'oasis-tcs/csaf#210'], + }, + }, + }, + }, + involvements: { + title: 'List of involvements', + description: 'Contains a list of involvements.', + type: 'array', + minItems: 1, + uniqueItems: true, + items: { + title: 'Involvement', + description: + 'Is a container, that allows the document producers to comment on the level of involvement (or engagement) of themselves or third parties in the vulnerability identification, scoping, and remediation process.', + type: 'object', + required: ['party', 'status'], + properties: { + date: { + title: 'Date of involvement', + description: + 'Holds the date and time of the involvement entry.', + type: 'string', + format: 'date-time', + }, + party: { + title: 'Party category', + description: 'Defines the category of the involved party.', + type: 'string', + enum: [ + 'coordinator', + 'discoverer', + 'other', + 'user', + 'vendor', + ], + }, + status: { + title: 'Party status', + description: 'Defines contact status of the involved party.', + type: 'string', + enum: [ + 'completed', + 'contact_attempted', + 'disputed', + 'in_progress', + 'not_contacted', + 'open', + ], + }, + summary: { + title: 'Summary of the involvement', + description: + 'Contains additional context regarding what is going on.', + type: 'string', + minLength: 1, + }, + }, + }, + }, + metrics: { + title: 'List of metrics', + description: + 'Contains metric objects for the current vulnerability.', + type: 'array', + minItems: 1, + uniqueItems: true, + items: { + title: 'metric', + description: + 'Contains all metadata about the metric including products it applies to and the source and the content itself.', + type: 'object', + required: ['content', 'products'], + properties: { + content: { + title: 'Content', + description: + 'Specifies information about (at least one) metric or score for the given products regarding the current vulnerability.', + type: 'object', + minProperties: 1, + properties: { + cvss_v2: { + $ref: 'https://www.first.org/cvss/cvss-v2.0.json', + }, + cvss_v3: { + oneOf: [ + { + $ref: 'https://www.first.org/cvss/cvss-v3.0.json', + }, + { + $ref: 'https://www.first.org/cvss/cvss-v3.1.json', + }, + ], + }, + cvss_v4: { + $ref: 'https://www.first.org/cvss/cvss-v4.0.json', + }, + }, + }, + products: { + $ref: '#/$defs/products_t', + }, + source: { + title: 'Source', + description: + 'Contains the URL of the source that originally determined the metric.', + type: 'string', + format: 'uri', + }, + }, + }, + }, + notes: { + title: 'Vulnerability notes', + description: 'Holds notes associated with this vulnerability item.', + $ref: '#/$defs/notes_t', + }, + product_status: { + title: 'Product status', + description: + 'Contains different lists of product_ids which provide details on the status of the referenced product related to the current vulnerability. ', + type: 'object', + minProperties: 1, + properties: { + first_affected: { + title: 'First affected', + description: + 'These are the first versions of the releases known to be affected by the vulnerability.', + $ref: '#/$defs/products_t', + }, + first_fixed: { + title: 'First fixed', + description: + 'These versions contain the first fix for the vulnerability but may not be the recommended fixed versions.', + $ref: '#/$defs/products_t', + }, + fixed: { + title: 'Fixed', + description: + 'These versions contain a fix for the vulnerability but may not be the recommended fixed versions.', + $ref: '#/$defs/products_t', + }, + known_affected: { + title: 'Known affected', + description: + 'These versions are known to be affected by the vulnerability.', + $ref: '#/$defs/products_t', + }, + known_not_affected: { + title: 'Known not affected', + description: + 'These versions are known not to be affected by the vulnerability.', + $ref: '#/$defs/products_t', + }, + last_affected: { + title: 'Last affected', + description: + 'These are the last versions in a release train known to be affected by the vulnerability. Subsequently released versions would contain a fix for the vulnerability.', + $ref: '#/$defs/products_t', + }, + recommended: { + title: 'Recommended', + description: + 'These versions have a fix for the vulnerability and are the vendor-recommended versions for fixing the vulnerability.', + $ref: '#/$defs/products_t', + }, + under_investigation: { + title: 'Under investigation', + description: + 'It is not known yet whether these versions are or are not affected by the vulnerability. However, it is still under investigation - the result will be provided in a later release of the document.', + $ref: '#/$defs/products_t', + }, + }, + }, + references: { + title: 'Vulnerability references', + description: + 'Holds a list of references associated with this vulnerability item.', + $ref: '#/$defs/references_t', + }, + release_date: { + title: 'Release date', + description: + 'Holds the date and time the vulnerability was originally released into the wild.', + type: 'string', + format: 'date-time', + }, + remediations: { + title: 'List of remediations', + description: 'Contains a list of remediations.', + type: 'array', + minItems: 1, + items: { + title: 'Remediation', + description: + 'Specifies details on how to handle (and presumably, fix) a vulnerability.', + type: 'object', + required: ['category', 'details'], + properties: { + category: { + title: 'Category of the remediation', + description: + 'Specifies the category which this remediation belongs to.', + type: 'string', + enum: [ + 'fix_planned', + 'mitigation', + 'no_fix_planned', + 'none_available', + 'optional_patch', + 'vendor_fix', + 'workaround', + ], + }, + date: { + title: 'Date of the remediation', + description: + 'Contains the date from which the remediation is available.', + type: 'string', + format: 'date-time', + }, + details: { + title: 'Details of the remediation', + description: + 'Contains a thorough human-readable discussion of the remediation.', + type: 'string', + minLength: 1, + }, + entitlements: { + title: 'List of entitlements', + description: 'Contains a list of entitlements.', + type: 'array', + minItems: 1, + items: { + title: 'Entitlement of the remediation', + description: + 'Contains any possible vendor-defined constraints for obtaining fixed software or hardware that fully resolves the vulnerability.', + type: 'string', + minLength: 1, + }, + }, + group_ids: { + $ref: '#/$defs/product_groups_t', + }, + product_ids: { + $ref: '#/$defs/products_t', + }, + restart_required: { + title: 'Restart required by remediation', + description: + 'Provides information on category of restart is required by this remediation to become effective.', + type: 'object', + required: ['category'], + properties: { + category: { + title: 'Category of restart', + description: + 'Specifies what category of restart is required by this remediation to become effective.', + type: 'string', + enum: [ + 'connected', + 'dependencies', + 'machine', + 'none', + 'parent', + 'service', + 'system', + 'vulnerable_component', + 'zone', + ], + }, + details: { + title: 'Additional restart information', + description: + 'Provides additional information for the restart. This can include details on procedures, scope or impact.', + type: 'string', + minLength: 1, + }, + }, + }, + url: { + title: 'URL to the remediation', + description: + 'Contains the URL where to obtain the remediation.', + type: 'string', + format: 'uri', + }, + }, + }, + }, + threats: { + title: 'List of threats', + description: + 'Contains information about a vulnerability that can change with time.', + type: 'array', + minItems: 1, + items: { + title: 'Threat', + description: + 'Contains the vulnerability kinetic information. This information can change as the vulnerability ages and new information becomes available.', + type: 'object', + required: ['category', 'details'], + properties: { + category: { + title: 'Category of the threat', + description: + 'Categorizes the threat according to the rules of the specification.', + type: 'string', + enum: ['exploit_status', 'impact', 'target_set'], + }, + date: { + title: 'Date of the threat', + description: + 'Contains the date when the assessment was done or the threat appeared.', + type: 'string', + format: 'date-time', + }, + details: { + title: 'Details of the threat', + description: + 'Represents a thorough human-readable discussion of the threat.', + type: 'string', + minLength: 1, + }, + group_ids: { + $ref: '#/$defs/product_groups_t', + }, + product_ids: { + $ref: '#/$defs/products_t', + }, + }, + }, + }, + title: { + title: 'Title', + description: + 'Gives the document producer the ability to apply a canonical name or title to the vulnerability.', + type: 'string', + minLength: 1, + }, + }, + }, + }, + }, +} diff --git a/csaf_2_1/schemaTests/csaf_2_1_strict.js b/csaf_2_1/schemaTests/csaf_2_1_strict.js new file mode 100644 index 00000000..f8b12296 --- /dev/null +++ b/csaf_2_1/schemaTests/csaf_2_1_strict.js @@ -0,0 +1,26 @@ +import csafAjv from '../../lib/shared/csafAjv.js' +import schema from './csaf_2_1_strict/schema.js' + +const validate = csafAjv.compile(schema) + +/** + * @param {any} doc + */ +export default function csaf_2_1_strict(doc) { + let isValid = validate(doc) + /** + * + * @type {Array<{ + * message?: string + * instancePath: string + * }>} + */ + const errors = validate.errors ?? [] + return { + isValid, + errors: errors.map((e) => ({ + ...e, + message: e.message ?? 'unexpected empty error message', + })), + } +} diff --git a/csaf_2_1/schemaTests/csaf_2_1_strict/schema.js b/csaf_2_1/schemaTests/csaf_2_1_strict/schema.js new file mode 100644 index 00000000..b0b05b86 --- /dev/null +++ b/csaf_2_1/schemaTests/csaf_2_1_strict/schema.js @@ -0,0 +1,1519 @@ +export default { + $defs: { + acknowledgments_t: { + description: 'Contains a list of acknowledgment elements.', + items: { + additionalProperties: false, + description: + 'Acknowledges contributions by describing those that contributed.', + minProperties: 1, + properties: { + names: { + description: 'Contains the names of contributors being recognized.', + items: { + description: + 'Contains the name of a single contributor being recognized.', + examples: ['Albert Einstein', 'Johann Sebastian Bach'], + minLength: 1, + title: 'Name of the contributor', + type: 'string', + }, + minItems: 1, + title: 'List of acknowledged names', + type: 'array', + }, + organization: { + description: + 'Contains the name of a contributing organization being recognized.', + examples: ['CISA', 'Google Project Zero', 'Talos'], + minLength: 1, + title: 'Contributing organization', + type: 'string', + }, + summary: { + description: + 'SHOULD represent any contextual details the document producers wish to make known about the acknowledgment or acknowledged parties.', + examples: [ + 'First analysis of Coordinated Multi-Stream Attack (CMSA)', + ], + minLength: 1, + title: 'Summary of the acknowledgment', + type: 'string', + }, + urls: { + description: + 'Specifies a list of URLs or location of the reference to be acknowledged.', + items: { + description: + 'Contains the URL or location of the reference to be acknowledged.', + format: 'uri', + title: 'URL of acknowledgment', + type: 'string', + }, + minItems: 1, + title: 'List of URLs', + type: 'array', + }, + }, + title: 'Acknowledgment', + type: 'object', + }, + minItems: 1, + title: 'List of acknowledgments', + type: 'array', + }, + branches_t: { + description: + 'Contains branch elements as children of the current element.', + items: { + additionalProperties: false, + description: + 'Is a part of the hierarchical structure of the product tree.', + maxProperties: 3, + minProperties: 3, + properties: { + branches: { + $ref: '#/$defs/branches_t', + }, + category: { + description: 'Describes the characteristics of the labeled branch.', + enum: [ + 'architecture', + 'host_name', + 'language', + 'legacy', + 'patch_level', + 'platform', + 'product_family', + 'product_name', + 'product_version', + 'product_version_range', + 'service_pack', + 'specification', + 'vendor', + ], + title: 'Category of the branch', + type: 'string', + }, + name: { + description: + "Contains the canonical descriptor or 'friendly name' of the branch.", + examples: [ + '10', + '365', + 'Microsoft', + 'Office', + 'PCS 7', + 'SIMATIC', + 'Siemens', + 'Windows', + ], + minLength: 1, + title: 'Name of the branch', + type: 'string', + }, + product: { + $ref: '#/$defs/full_product_name_t', + }, + }, + required: ['category', 'name'], + title: 'Branch', + type: 'object', + }, + minItems: 1, + title: 'List of branches', + type: 'array', + }, + full_product_name_t: { + additionalProperties: false, + description: + 'Specifies information about the product and assigns the product_id.', + properties: { + name: { + description: + 'The value should be the product\u00e2\u20ac\u2122s full canonical name, including version number and other attributes, as it would be used in a human-friendly document.', + examples: [ + 'Cisco AnyConnect Secure Mobility Client 2.3.185', + 'Microsoft Host Integration Server 2006 Service Pack 1', + ], + minLength: 1, + title: 'Textual description of the product', + type: 'string', + }, + product_id: { + $ref: '#/$defs/product_id_t', + }, + product_identification_helper: { + additionalProperties: false, + description: + 'Provides at least one method which aids in identifying the product in an asset database.', + minProperties: 1, + properties: { + cpe: { + description: + 'The Common Platform Enumeration (CPE) attribute refers to a method for naming platforms external to this specification.', + minLength: 5, + pattern: + '^((cpe:2\\.3:[aho\\*\\-](:(((\\?*|\\*?)([a-zA-Z0-9\\-\\._]|(\\\\[\\\\\\*\\?!"#\\$%&\'\\(\\)\\+,\\/:;<=>@\\[\\]\\^`\\{\\|\\}~]))+(\\?*|\\*?))|[\\*\\-])){5}(:(([a-zA-Z]{2,3}(-([a-zA-Z]{2}|[0-9]{3}))?)|[\\*\\-]))(:(((\\?*|\\*?)([a-zA-Z0-9\\-\\._]|(\\\\[\\\\\\*\\?!"#\\$%&\'\\(\\)\\+,\\/:;<=>@\\[\\]\\^`\\{\\|\\}~]))+(\\?*|\\*?))|[\\*\\-])){4})|([c][pP][eE]:\\/[AHOaho]?(:[A-Za-z0-9\\._\\-~%]*){0,6}))$', + title: 'Common Platform Enumeration representation', + type: 'string', + }, + hashes: { + description: + 'Contains a list of cryptographic hashes usable to identify files.', + items: { + additionalProperties: false, + description: + 'Contains all information to identify a file based on its cryptographic hash values.', + properties: { + file_hashes: { + description: + 'Contains a list of cryptographic hashes for this file.', + items: { + additionalProperties: false, + description: + 'Contains one hash value and algorithm of the file to be identified.', + properties: { + algorithm: { + default: 'sha256', + description: + 'Contains the name of the cryptographic hash algorithm used to calculate the value.', + examples: [ + 'blake2b512', + 'sha256', + 'sha3-512', + 'sha384', + 'sha512', + ], + minLength: 1, + title: 'Algorithm of the cryptographic hash', + type: 'string', + }, + value: { + description: + 'Contains the cryptographic hash value in hexadecimal representation.', + examples: [ + '37df33cb7464da5c7f077f4d56a32bc84987ec1d85b234537c1c1a4d4fc8d09dc29e2e762cb5203677bf849a2855a0283710f1f5fe1d6ce8d5ac85c645d0fcb3', + '4775203615d9534a8bfca96a93dc8b461a489f69124a130d786b42204f3341cc', + '9ea4c8200113d49d26505da0e02e2f49055dc078d1ad7a419b32e291c7afebbb84badfbd46dec42883bea0b2a1fa697c', + ], + minLength: 32, + pattern: '^[0-9a-fA-F]{32,}$', + title: 'Value of the cryptographic hash', + type: 'string', + }, + }, + required: ['algorithm', 'value'], + title: 'File hash', + type: 'object', + }, + minItems: 1, + title: 'List of file hashes', + type: 'array', + }, + filename: { + description: + 'Contains the name of the file which is identified by the hash values.', + examples: ['WINWORD.EXE', 'msotadddin.dll', 'sudoers.so'], + minLength: 1, + title: 'Filename', + type: 'string', + }, + }, + required: ['file_hashes', 'filename'], + title: 'Cryptographic hashes', + type: 'object', + }, + minItems: 1, + title: 'List of hashes', + type: 'array', + }, + model_numbers: { + description: + 'Contains a list of full or abbreviated (partial) model numbers.', + items: { + description: + 'Contains a full or abbreviated (partial) model number of the component to identify.', + minLength: 1, + title: 'Model number', + type: 'string', + }, + minItems: 1, + title: 'List of models', + type: 'array', + uniqueItems: true, + }, + purl: { + description: + 'The package URL (purl) attribute refers to a method for reliably identifying and locating software packages external to this specification.', + format: 'uri', + minLength: 7, + pattern: '^pkg:[A-Za-z\\.\\-\\+][A-Za-z0-9\\.\\-\\+]*\\/.+', + title: 'package URL representation', + type: 'string', + }, + sbom_urls: { + description: + 'Contains a list of URLs where SBOMs for this product can be retrieved.', + items: { + description: 'Contains a URL of one SBOM for this product.', + format: 'uri', + title: 'SBOM URL', + type: 'string', + }, + minItems: 1, + title: 'List of SBOM URLs', + type: 'array', + }, + serial_numbers: { + description: + 'Contains a list of full or abbreviated (partial) serial numbers.', + items: { + description: + 'Contains a full or abbreviated (partial) serial number of the component to identify.', + minLength: 1, + title: 'Serial number', + type: 'string', + }, + minItems: 1, + title: 'List of serial numbers', + type: 'array', + uniqueItems: true, + }, + skus: { + description: + 'Contains a list of full or abbreviated (partial) stock keeping units.', + items: { + description: + 'Contains a full or abbreviated (partial) stock keeping unit (SKU) which is used in the ordering process to identify the component.', + minLength: 1, + title: 'Stock keeping unit', + type: 'string', + }, + minItems: 1, + title: 'List of stock keeping units', + type: 'array', + }, + x_generic_uris: { + description: + 'Contains a list of identifiers which are either vendor-specific or derived from a standard not yet supported.', + items: { + additionalProperties: false, + description: + 'Provides a generic extension point for any identifier which is either vendor-specific or derived from a standard not yet supported.', + properties: { + namespace: { + description: + 'Refers to a URL which provides the name and knowledge about the specification used or is the namespace in which these values are valid.', + format: 'uri', + title: 'Namespace of the generic URI', + type: 'string', + }, + uri: { + description: 'Contains the identifier itself.', + format: 'uri', + title: 'URI', + type: 'string', + }, + }, + required: ['namespace', 'uri'], + title: 'Generic URI', + type: 'object', + }, + minItems: 1, + title: 'List of generic URIs', + type: 'array', + }, + }, + title: 'Helper to identify the product', + type: 'object', + }, + }, + required: ['name', 'product_id'], + title: 'Full product name', + type: 'object', + }, + lang_t: { + description: + 'Identifies a language, corresponding to IETF BCP 47 / RFC 5646. See IETF language registry: https://www.iana.org/assignments/language-subtag-registry/language-subtag-registry', + examples: ['de', 'en', 'fr', 'frc', 'jp'], + pattern: + '^(([A-Za-z]{2,3}(-[A-Za-z]{3}(-[A-Za-z]{3}){0,2})?|[A-Za-z]{4,8})(-[A-Za-z]{4})?(-([A-Za-z]{2}|[0-9]{3}))?(-([A-Za-z0-9]{5,8}|[0-9][A-Za-z0-9]{3}))*(-[A-WY-Za-wy-z0-9](-[A-Za-z0-9]{2,8})+)*(-[Xx](-[A-Za-z0-9]{1,8})+)?|[Xx](-[A-Za-z0-9]{1,8})+|[Ii]-[Dd][Ee][Ff][Aa][Uu][Ll][Tt]|[Ii]-[Mm][Ii][Nn][Gg][Oo])$', + title: 'Language type', + type: 'string', + }, + notes_t: { + description: 'Contains notes which are specific to the current context.', + items: { + additionalProperties: false, + description: + 'Is a place to put all manner of text blobs related to the current context.', + properties: { + audience: { + description: 'Indicates who is intended to read it.', + examples: [ + 'all', + 'executives', + 'operational management and system administrators', + 'safety engineers', + ], + minLength: 1, + title: 'Audience of note', + type: 'string', + }, + category: { + description: + 'Contains the information of what kind of note this is.', + enum: [ + 'description', + 'details', + 'faq', + 'general', + 'legal_disclaimer', + 'other', + 'summary', + ], + title: 'Note category', + type: 'string', + }, + text: { + description: + 'Holds the content of the note. Content varies depending on type.', + minLength: 1, + title: 'Note content', + type: 'string', + }, + title: { + description: + 'Provides a concise description of what is contained in the text of the note.', + examples: [ + 'Details', + 'Executive summary', + 'Technical summary', + 'Impact on safety systems', + ], + minLength: 1, + title: 'Title of note', + type: 'string', + }, + }, + required: ['category', 'text'], + title: 'Note', + type: 'object', + }, + minItems: 1, + title: 'List of notes', + type: 'array', + }, + product_group_id_t: { + description: + 'Token required to identify a group of products so that it can be referred to from other parts in the document. There is no predefined or required format for the product_group_id as long as it uniquely identifies a group in the context of the current document.', + examples: ['CSAFGID-0001', 'CSAFGID-0002', 'CSAFGID-0020'], + minLength: 1, + title: 'Reference token for product group instance', + type: 'string', + }, + product_groups_t: { + description: + 'Specifies a list of product_group_ids to give context to the parent item.', + items: { + $ref: '#/$defs/product_group_id_t', + }, + minItems: 1, + title: 'List of product_group_ids', + type: 'array', + uniqueItems: true, + }, + product_id_t: { + description: + 'Token required to identify a full_product_name so that it can be referred to from other parts in the document. There is no predefined or required format for the product_id as long as it uniquely identifies a product in the context of the current document.', + examples: ['CSAFPID-0004', 'CSAFPID-0008'], + minLength: 1, + title: 'Reference token for product instance', + type: 'string', + }, + products_t: { + description: + 'Specifies a list of product_ids to give context to the parent item.', + items: { + $ref: '#/$defs/product_id_t', + }, + minItems: 1, + title: 'List of product_ids', + type: 'array', + uniqueItems: true, + }, + references_t: { + description: 'Holds a list of references.', + items: { + additionalProperties: false, + description: + 'Holds any reference to conferences, papers, advisories, and other resources that are related and considered related to either a surrounding part of or the entire document and to be of value to the document consumer.', + properties: { + category: { + default: 'external', + description: + 'Indicates whether the reference points to the same document or vulnerability in focus (depending on scope) or to an external resource.', + enum: ['external', 'self'], + title: 'Category of reference', + type: 'string', + }, + summary: { + description: 'Indicates what this reference refers to.', + minLength: 1, + title: 'Summary of the reference', + type: 'string', + }, + url: { + description: 'Provides the URL for the reference.', + format: 'uri', + title: 'URL of reference', + type: 'string', + }, + }, + required: ['summary', 'url'], + title: 'Reference', + type: 'object', + }, + minItems: 1, + title: 'List of references', + type: 'array', + }, + version_t: { + description: + 'Specifies a version string to denote clearly the evolution of the content of the document. Format must be either integer or semantic versioning.', + examples: ['1', '4', '0.9.0', '1.4.3', '2.40.0+21AF26D3'], + pattern: + '^(0|[1-9][0-9]*)$|^((0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?)$', + title: 'Version', + type: 'string', + }, + }, + $id: 'https://docs.oasis-open.org/csaf/csaf/v2.1/csaf_json_schema.json?strict', + $schema: 'https://json-schema.org/draft/2020-12/schema', + additionalProperties: false, + description: + 'Representation of security advisory information as a JSON document.', + properties: { + $schema: { + description: + 'Contains the URL of the CSAF JSON schema which the document promises to be valid for.', + enum: [ + 'https://docs.oasis-open.org/csaf/csaf/v2.1/csaf_json_schema.json', + ], + format: 'uri', + title: 'JSON schema', + type: 'string', + }, + document: { + additionalProperties: false, + description: + 'Captures the meta-data about this document describing a particular set of security advisories.', + properties: { + acknowledgments: { + $ref: '#/$defs/acknowledgments_t', + description: + 'Contains a list of acknowledgment elements associated with the whole document.', + title: 'Document acknowledgments', + }, + aggregate_severity: { + additionalProperties: false, + description: + "Is a vehicle that is provided by the document producer to convey the urgency and criticality with which the one or more vulnerabilities reported should be addressed. It is a document-level metric and applied to the document as a whole \u00e2\u20ac\u201d not any specific vulnerability. The range of values in this field is defined according to the document producer's policies and procedures.", + properties: { + namespace: { + description: 'Points to the namespace so referenced.', + format: 'uri', + title: 'Namespace of aggregate severity', + type: 'string', + }, + text: { + description: + 'Provides a severity which is independent of - and in addition to - any other standard metric for determining the impact or severity of a given vulnerability (such as CVSS).', + examples: ['Critical', 'Important', 'Moderate'], + minLength: 1, + title: 'Text of aggregate severity', + type: 'string', + }, + }, + required: ['text'], + title: 'Aggregate severity', + type: 'object', + }, + category: { + description: + 'Defines a short canonical name, chosen by the document producer, which will inform the end user as to the category of document.', + examples: [ + 'csaf_base', + 'csaf_security_advisory', + 'csaf_vex', + 'Example Company Security Notice', + ], + minLength: 1, + pattern: '^[^\\s\\-_\\.](.*[^\\s\\-_\\.])?$', + title: 'Document category', + type: 'string', + }, + csaf_version: { + description: + 'Gives the version of the CSAF specification which the document was generated for.', + enum: ['2.1'], + title: 'CSAF version', + type: 'string', + }, + distribution: { + additionalProperties: false, + description: + 'Describe any constraints on how this document might be shared.', + properties: { + sharing_group: { + additionalProperties: false, + description: + 'Contains information about the group this document is intended to be shared with.', + properties: { + id: { + description: 'Provides the unique ID for the sharing group.', + format: 'uuid', + pattern: + '^(([0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[0-9a-f]{4}-[0-9a-f]{12})|([0]{8}-([0]{4}-){3}[0]{12})|([f]{8}-([f]{4}-){3}[f]{12}))$', + title: 'Sharing Group ID', + type: 'string', + }, + name: { + description: + 'Contains a human-readable name for the sharing group.', + examples: [ + 'Customer A', + 'ISAC members', + 'NIS2 regulated important entities in Germany, sector water', + 'Pre-Sharing group for advisory discussion', + 'Users of Product A', + 'US Federal Civilian Authorities', + ], + minLength: 1, + title: 'Sharing Group Name', + type: 'string', + }, + }, + required: ['id'], + title: 'Sharing Group', + type: 'object', + }, + text: { + description: + 'Provides a textual description of additional constraints.', + examples: [ + 'Copyright 2021, Example Company, All Rights Reserved.', + 'Distribute freely.', + 'Share only on a need-to-know-basis only.', + ], + minLength: 1, + title: 'Textual description', + type: 'string', + }, + tlp: { + additionalProperties: false, + description: + 'Provides details about the TLP classification of the document.', + properties: { + label: { + default: 'CLEAR', + description: 'Provides the TLP label of the document.', + enum: ['AMBER', 'AMBER+STRICT', 'CLEAR', 'GREEN', 'RED'], + title: 'Label of TLP', + type: 'string', + }, + url: { + default: 'https://www.first.org/tlp/', + description: + 'Provides a URL where to find the textual description of the TLP version which is used in this document. Default is the URL to the definition by FIRST.', + examples: [ + 'https://www.us-cert.gov/tlp', + 'https://www.bsi.bund.de/SharedDocs/Downloads/DE/BSI/Kritis/Merkblatt_TLP.pdf', + ], + format: 'uri', + title: 'URL of TLP version', + type: 'string', + }, + }, + required: ['label'], + title: 'Traffic Light Protocol (TLP)', + type: 'object', + }, + }, + required: ['tlp'], + title: 'Rules for sharing document', + type: 'object', + }, + lang: { + $ref: '#/$defs/lang_t', + description: + 'Identifies the language used by this document, corresponding to IETF BCP 47 / RFC 5646.', + title: 'Document language', + }, + notes: { + $ref: '#/$defs/notes_t', + description: 'Holds notes associated with the whole document.', + title: 'Document notes', + }, + publisher: { + additionalProperties: false, + description: + 'Provides information about the publisher of the document.', + properties: { + category: { + description: + 'Provides information about the category of publisher releasing the document.', + enum: [ + 'coordinator', + 'discoverer', + 'multiplier', + 'other', + 'translator', + 'user', + 'vendor', + ], + title: 'Category of publisher', + type: 'string', + }, + contact_details: { + description: + 'Information on how to contact the publisher, possibly including details such as web sites, email addresses, phone numbers, and postal mail addresses.', + examples: [ + 'Example Company can be reached at contact_us@example.com, or via our website at https://www.example.com/contact.', + ], + minLength: 1, + title: 'Contact details', + type: 'string', + }, + issuing_authority: { + description: + "Provides information about the authority of the issuing party to release the document, in particular, the party's constituency and responsibilities or other obligations.", + minLength: 1, + title: 'Issuing authority', + type: 'string', + }, + name: { + description: 'Contains the name of the issuing party.', + examples: ['BSI', 'Cisco PSIRT', 'Siemens ProductCERT'], + minLength: 1, + title: 'Name of publisher', + type: 'string', + }, + namespace: { + description: + 'Contains a URL which is under control of the issuing party and can be used as a globally unique identifier for that issuing party.', + examples: ['https://csaf.io', 'https://www.example.com'], + format: 'uri', + title: 'Namespace of publisher', + type: 'string', + }, + }, + required: ['category', 'name', 'namespace'], + title: 'Publisher', + type: 'object', + }, + references: { + $ref: '#/$defs/references_t', + description: + 'Holds a list of references associated with the whole document.', + title: 'Document references', + }, + source_lang: { + $ref: '#/$defs/lang_t', + description: + 'If this copy of the document is a translation then the value of this property describes from which language this document was translated.', + title: 'Source language', + }, + title: { + description: + 'This SHOULD be a canonical name for the document, and sufficiently unique to distinguish it from similar documents.', + examples: [ + 'Cisco IPv6 Crafted Packet Denial of Service Vulnerability', + 'Example Company Cross-Site-Scripting Vulnerability in Example Generator', + ], + minLength: 1, + title: 'Title of this document', + type: 'string', + }, + tracking: { + additionalProperties: false, + description: + 'Is a container designated to hold all management attributes necessary to track a CSAF document as a whole.', + properties: { + aliases: { + description: + 'Contains a list of alternate names for the same document.', + items: { + description: + 'Specifies a non-empty string that represents a distinct optional alternative ID used to refer to the document.', + examples: ['CVE-2019-12345'], + minLength: 1, + title: 'Alternate name', + type: 'string', + }, + minItems: 1, + title: 'Aliases', + type: 'array', + uniqueItems: true, + }, + current_release_date: { + description: + 'The date when the current revision of this document was released', + format: 'date-time', + title: 'Current release date', + type: 'string', + }, + generator: { + additionalProperties: false, + description: + 'Is a container to hold all elements related to the generation of the document. These items will reference when the document was actually created, including the date it was generated and the entity that generated it.', + properties: { + date: { + description: + 'This SHOULD be the current date that the document was generated. Because documents are often generated internally by a document producer and exist for a nonzero amount of time before being released, this field MAY be different from the Initial Release Date and Current Release Date.', + format: 'date-time', + title: 'Date of document generation', + type: 'string', + }, + engine: { + additionalProperties: false, + description: + 'Contains information about the engine that generated the CSAF document.', + properties: { + name: { + description: + 'Represents the name of the engine that generated the CSAF document.', + examples: ['Red Hat rhsa-to-cvrf', 'Secvisogram', 'TVCE'], + minLength: 1, + title: 'Engine name', + type: 'string', + }, + version: { + description: + 'Contains the version of the engine that generated the CSAF document.', + examples: ['0.6.0', '1.0.0-beta+exp.sha.a1c44f85', '2'], + minLength: 1, + title: 'Engine version', + type: 'string', + }, + }, + required: ['name'], + title: 'Engine of document generation', + type: 'object', + }, + }, + required: ['engine'], + title: 'Document generator', + type: 'object', + }, + id: { + description: + 'The ID is a simple label that provides for a wide range of numbering values, types, and schemes. Its value SHOULD be assigned and maintained by the original document issuing authority.', + examples: [ + 'Example Company - 2019-YH3234', + 'RHBA-2019:0024', + 'cisco-sa-20190513-secureboot', + ], + minLength: 1, + pattern: '^[\\S](.*[\\S])?$', + title: 'Unique identifier for the document', + type: 'string', + }, + initial_release_date: { + description: 'The date when this document was first published.', + format: 'date-time', + title: 'Initial release date', + type: 'string', + }, + revision_history: { + description: + 'Holds one revision item for each version of the CSAF document, including the initial one.', + items: { + additionalProperties: false, + description: + 'Contains all the information elements required to track the evolution of a CSAF document.', + properties: { + date: { + description: 'The date of the revision entry', + format: 'date-time', + title: 'Date of the revision', + type: 'string', + }, + legacy_version: { + description: + 'Contains the version string used in an existing document with the same content.', + minLength: 1, + title: 'Legacy version of the revision', + type: 'string', + }, + number: { + $ref: '#/$defs/version_t', + }, + summary: { + description: + 'Holds a single non-empty string representing a short description of the changes.', + examples: ['Initial version.'], + minLength: 1, + title: 'Summary of the revision', + type: 'string', + }, + }, + required: ['date', 'number', 'summary'], + title: 'Revision', + type: 'object', + }, + minItems: 1, + title: 'Revision history', + type: 'array', + }, + status: { + description: 'Defines the draft status of the document.', + enum: ['draft', 'final', 'interim'], + title: 'Document status', + type: 'string', + }, + version: { + $ref: '#/$defs/version_t', + }, + }, + required: [ + 'current_release_date', + 'id', + 'initial_release_date', + 'revision_history', + 'status', + 'version', + ], + title: 'Tracking', + type: 'object', + }, + }, + required: [ + 'category', + 'csaf_version', + 'distribution', + 'publisher', + 'title', + 'tracking', + ], + title: 'Document level meta-data', + type: 'object', + }, + product_tree: { + additionalProperties: false, + description: + 'Is a container for all fully qualified product names that can be referenced elsewhere in the document.', + minProperties: 1, + properties: { + branches: { + $ref: '#/$defs/branches_t', + }, + full_product_names: { + description: 'Contains a list of full product names.', + items: { + $ref: '#/$defs/full_product_name_t', + }, + minItems: 1, + title: 'List of full product names', + type: 'array', + }, + product_groups: { + description: 'Contains a list of product groups.', + items: { + additionalProperties: false, + description: + 'Defines a new logical group of products that can then be referred to in other parts of the document to address a group of products with a single identifier.', + properties: { + group_id: { + $ref: '#/$defs/product_group_id_t', + }, + product_ids: { + description: + 'Lists the product_ids of those products which known as one group in the document.', + items: { + $ref: '#/$defs/product_id_t', + }, + minItems: 2, + title: 'List of Product IDs', + type: 'array', + uniqueItems: true, + }, + summary: { + description: + 'Gives a short, optional description of the group.', + examples: [ + 'Products supporting Modbus.', + 'The x64 versions of the operating system.', + ], + minLength: 1, + title: 'Summary of the product group', + type: 'string', + }, + }, + required: ['group_id', 'product_ids'], + title: 'Product group', + type: 'object', + }, + minItems: 1, + title: 'List of product groups', + type: 'array', + }, + relationships: { + description: 'Contains a list of relationships.', + items: { + additionalProperties: false, + description: + 'Establishes a link between two existing full_product_name_t elements, allowing the document producer to define a combination of two products that form a new full_product_name entry.', + properties: { + category: { + description: + 'Defines the category of relationship for the referenced component.', + enum: [ + 'default_component_of', + 'external_component_of', + 'installed_on', + 'installed_with', + 'optional_component_of', + ], + title: 'Relationship category', + type: 'string', + }, + full_product_name: { + $ref: '#/$defs/full_product_name_t', + }, + product_reference: { + $ref: '#/$defs/product_id_t', + description: + 'Holds a Product ID that refers to the Full Product Name element, which is referenced as the first element of the relationship.', + title: 'Product reference', + }, + relates_to_product_reference: { + $ref: '#/$defs/product_id_t', + description: + 'Holds a Product ID that refers to the Full Product Name element, which is referenced as the second element of the relationship.', + title: 'Relates to product reference', + }, + }, + required: [ + 'category', + 'full_product_name', + 'product_reference', + 'relates_to_product_reference', + ], + title: 'Relationship', + type: 'object', + }, + minItems: 1, + title: 'List of relationships', + type: 'array', + }, + }, + title: 'Product tree', + type: 'object', + }, + vulnerabilities: { + description: + 'Represents a list of all relevant vulnerability information items.', + items: { + additionalProperties: false, + description: + 'Is a container for the aggregation of all fields that are related to a single vulnerability in the document.', + minProperties: 1, + properties: { + acknowledgments: { + $ref: '#/$defs/acknowledgments_t', + description: + 'Contains a list of acknowledgment elements associated with this vulnerability item.', + title: 'Vulnerability acknowledgments', + }, + cve: { + description: + 'Holds the MITRE standard Common Vulnerabilities and Exposures (CVE) tracking number for the vulnerability.', + pattern: '^CVE-[0-9]{4}-[0-9]{4,}$', + title: 'CVE', + type: 'string', + }, + cwes: { + description: 'Contains a list of CWEs.', + items: { + additionalProperties: false, + description: + 'Holds the MITRE standard Common Weakness Enumeration (CWE) for the weakness associated.', + properties: { + id: { + description: 'Holds the ID for the weakness associated.', + examples: ['CWE-22', 'CWE-352', 'CWE-79'], + pattern: '^CWE-[1-9]\\d{0,5}$', + title: 'Weakness ID', + type: 'string', + }, + name: { + description: + 'Holds the full name of the weakness as given in the CWE specification.', + examples: [ + 'Cross-Site Request Forgery (CSRF)', + "Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')", + "Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')", + ], + minLength: 1, + title: 'Weakness name', + type: 'string', + }, + version: { + description: + 'Holds the version string of the CWE specification this weakness was extracted from.', + examples: ['1.0', '3.4.1', '4.0', '4.11', '4.12'], + minLength: 1, + pattern: '^[1-9]\\d*\\.([0-9]|([1-9]\\d+))(\\.\\d+)?$', + title: 'CWE version', + type: 'string', + }, + }, + required: ['id', 'name', 'version'], + title: 'CWE', + type: 'object', + }, + minItems: 1, + title: 'List of CWEs', + type: 'array', + uniqueItems: true, + }, + discovery_date: { + description: + 'Holds the date and time the vulnerability was originally discovered.', + format: 'date-time', + title: 'Discovery date', + type: 'string', + }, + flags: { + description: 'Contains a list of machine readable flags.', + items: { + additionalProperties: false, + description: + 'Contains product specific information in regard to this vulnerability as a single machine readable flag.', + properties: { + date: { + description: + 'Contains the date when assessment was done or the flag was assigned.', + format: 'date-time', + title: 'Date of the flag', + type: 'string', + }, + group_ids: { + $ref: '#/$defs/product_groups_t', + }, + label: { + description: 'Specifies the machine readable label.', + enum: [ + 'component_not_present', + 'inline_mitigations_already_exist', + 'vulnerable_code_cannot_be_controlled_by_adversary', + 'vulnerable_code_not_in_execute_path', + 'vulnerable_code_not_present', + ], + title: 'Label of the flag', + type: 'string', + }, + product_ids: { + $ref: '#/$defs/products_t', + }, + }, + required: ['label'], + title: 'Flag', + type: 'object', + }, + minItems: 1, + title: 'List of flags', + type: 'array', + uniqueItems: true, + }, + ids: { + description: + 'Represents a list of unique labels or tracking IDs for the vulnerability (if such information exists).', + items: { + additionalProperties: false, + description: + 'Contains a single unique label or tracking ID for the vulnerability.', + properties: { + system_name: { + description: + 'Indicates the name of the vulnerability tracking or numbering system.', + examples: ['Cisco Bug ID', 'GitHub Issue'], + minLength: 1, + title: 'System name', + type: 'string', + }, + text: { + description: + 'Is unique label or tracking ID for the vulnerability (if such information exists).', + examples: ['CSCso66472', 'oasis-tcs/csaf#210'], + minLength: 1, + title: 'Text', + type: 'string', + }, + }, + required: ['system_name', 'text'], + title: 'ID', + type: 'object', + }, + minItems: 1, + title: 'List of IDs', + type: 'array', + uniqueItems: true, + }, + involvements: { + description: 'Contains a list of involvements.', + items: { + additionalProperties: false, + description: + 'Is a container, that allows the document producers to comment on the level of involvement (or engagement) of themselves or third parties in the vulnerability identification, scoping, and remediation process.', + properties: { + date: { + description: + 'Holds the date and time of the involvement entry.', + format: 'date-time', + title: 'Date of involvement', + type: 'string', + }, + party: { + description: 'Defines the category of the involved party.', + enum: [ + 'coordinator', + 'discoverer', + 'other', + 'user', + 'vendor', + ], + title: 'Party category', + type: 'string', + }, + status: { + description: 'Defines contact status of the involved party.', + enum: [ + 'completed', + 'contact_attempted', + 'disputed', + 'in_progress', + 'not_contacted', + 'open', + ], + title: 'Party status', + type: 'string', + }, + summary: { + description: + 'Contains additional context regarding what is going on.', + minLength: 1, + title: 'Summary of the involvement', + type: 'string', + }, + }, + required: ['party', 'status'], + title: 'Involvement', + type: 'object', + }, + minItems: 1, + title: 'List of involvements', + type: 'array', + uniqueItems: true, + }, + metrics: { + description: + 'Contains metric objects for the current vulnerability.', + items: { + additionalProperties: false, + description: + 'Contains all metadata about the metric including products it applies to and the source and the content itself.', + properties: { + content: { + additionalProperties: false, + description: + 'Specifies information about (at least one) metric or score for the given products regarding the current vulnerability.', + minProperties: 1, + properties: { + cvss_v2: { + $ref: 'https://www.first.org/cvss/cvss-v2.0.json', + }, + cvss_v3: { + oneOf: [ + { + $ref: 'https://www.first.org/cvss/cvss-v3.0.json', + }, + { + $ref: 'https://www.first.org/cvss/cvss-v3.1.json', + }, + ], + }, + cvss_v4: { + $ref: 'https://www.first.org/cvss/cvss-v4.0.json', + }, + }, + title: 'Content', + type: 'object', + }, + products: { + $ref: '#/$defs/products_t', + }, + source: { + description: + 'Contains the URL of the source that originally determined the metric.', + format: 'uri', + title: 'Source', + type: 'string', + }, + }, + required: ['content', 'products'], + title: 'metric', + type: 'object', + }, + minItems: 1, + title: 'List of metrics', + type: 'array', + uniqueItems: true, + }, + notes: { + $ref: '#/$defs/notes_t', + description: 'Holds notes associated with this vulnerability item.', + title: 'Vulnerability notes', + }, + product_status: { + additionalProperties: false, + description: + 'Contains different lists of product_ids which provide details on the status of the referenced product related to the current vulnerability. ', + minProperties: 1, + properties: { + first_affected: { + $ref: '#/$defs/products_t', + description: + 'These are the first versions of the releases known to be affected by the vulnerability.', + title: 'First affected', + }, + first_fixed: { + $ref: '#/$defs/products_t', + description: + 'These versions contain the first fix for the vulnerability but may not be the recommended fixed versions.', + title: 'First fixed', + }, + fixed: { + $ref: '#/$defs/products_t', + description: + 'These versions contain a fix for the vulnerability but may not be the recommended fixed versions.', + title: 'Fixed', + }, + known_affected: { + $ref: '#/$defs/products_t', + description: + 'These versions are known to be affected by the vulnerability.', + title: 'Known affected', + }, + known_not_affected: { + $ref: '#/$defs/products_t', + description: + 'These versions are known not to be affected by the vulnerability.', + title: 'Known not affected', + }, + last_affected: { + $ref: '#/$defs/products_t', + description: + 'These are the last versions in a release train known to be affected by the vulnerability. Subsequently released versions would contain a fix for the vulnerability.', + title: 'Last affected', + }, + recommended: { + $ref: '#/$defs/products_t', + description: + 'These versions have a fix for the vulnerability and are the vendor-recommended versions for fixing the vulnerability.', + title: 'Recommended', + }, + under_investigation: { + $ref: '#/$defs/products_t', + description: + 'It is not known yet whether these versions are or are not affected by the vulnerability. However, it is still under investigation - the result will be provided in a later release of the document.', + title: 'Under investigation', + }, + }, + title: 'Product status', + type: 'object', + }, + references: { + $ref: '#/$defs/references_t', + description: + 'Holds a list of references associated with this vulnerability item.', + title: 'Vulnerability references', + }, + release_date: { + description: + 'Holds the date and time the vulnerability was originally released into the wild.', + format: 'date-time', + title: 'Release date', + type: 'string', + }, + remediations: { + description: 'Contains a list of remediations.', + items: { + additionalProperties: false, + description: + 'Specifies details on how to handle (and presumably, fix) a vulnerability.', + properties: { + category: { + description: + 'Specifies the category which this remediation belongs to.', + enum: [ + 'fix_planned', + 'mitigation', + 'no_fix_planned', + 'none_available', + 'optional_patch', + 'vendor_fix', + 'workaround', + ], + title: 'Category of the remediation', + type: 'string', + }, + date: { + description: + 'Contains the date from which the remediation is available.', + format: 'date-time', + title: 'Date of the remediation', + type: 'string', + }, + details: { + description: + 'Contains a thorough human-readable discussion of the remediation.', + minLength: 1, + title: 'Details of the remediation', + type: 'string', + }, + entitlements: { + description: 'Contains a list of entitlements.', + items: { + description: + 'Contains any possible vendor-defined constraints for obtaining fixed software or hardware that fully resolves the vulnerability.', + minLength: 1, + title: 'Entitlement of the remediation', + type: 'string', + }, + minItems: 1, + title: 'List of entitlements', + type: 'array', + }, + group_ids: { + $ref: '#/$defs/product_groups_t', + }, + product_ids: { + $ref: '#/$defs/products_t', + }, + restart_required: { + additionalProperties: false, + description: + 'Provides information on category of restart is required by this remediation to become effective.', + properties: { + category: { + description: + 'Specifies what category of restart is required by this remediation to become effective.', + enum: [ + 'connected', + 'dependencies', + 'machine', + 'none', + 'parent', + 'service', + 'system', + 'vulnerable_component', + 'zone', + ], + title: 'Category of restart', + type: 'string', + }, + details: { + description: + 'Provides additional information for the restart. This can include details on procedures, scope or impact.', + minLength: 1, + title: 'Additional restart information', + type: 'string', + }, + }, + required: ['category'], + title: 'Restart required by remediation', + type: 'object', + }, + url: { + description: + 'Contains the URL where to obtain the remediation.', + format: 'uri', + title: 'URL to the remediation', + type: 'string', + }, + }, + required: ['category', 'details'], + title: 'Remediation', + type: 'object', + }, + minItems: 1, + title: 'List of remediations', + type: 'array', + }, + threats: { + description: + 'Contains information about a vulnerability that can change with time.', + items: { + additionalProperties: false, + description: + 'Contains the vulnerability kinetic information. This information can change as the vulnerability ages and new information becomes available.', + properties: { + category: { + description: + 'Categorizes the threat according to the rules of the specification.', + enum: ['exploit_status', 'impact', 'target_set'], + title: 'Category of the threat', + type: 'string', + }, + date: { + description: + 'Contains the date when the assessment was done or the threat appeared.', + format: 'date-time', + title: 'Date of the threat', + type: 'string', + }, + details: { + description: + 'Represents a thorough human-readable discussion of the threat.', + minLength: 1, + title: 'Details of the threat', + type: 'string', + }, + group_ids: { + $ref: '#/$defs/product_groups_t', + }, + product_ids: { + $ref: '#/$defs/products_t', + }, + }, + required: ['category', 'details'], + title: 'Threat', + type: 'object', + }, + minItems: 1, + title: 'List of threats', + type: 'array', + }, + title: { + description: + 'Gives the document producer the ability to apply a canonical name or title to the vulnerability.', + minLength: 1, + title: 'Title', + type: 'string', + }, + }, + title: 'Vulnerability', + type: 'object', + }, + minItems: 1, + title: 'Vulnerabilities', + type: 'array', + }, + }, + required: ['$schema', 'document'], + title: 'Common Security Advisory Framework', + type: 'object', +} diff --git a/lib/shared/csafAjv.js b/lib/shared/csafAjv.js index 816e492a..b528ce65 100644 --- a/lib/shared/csafAjv.js +++ b/lib/shared/csafAjv.js @@ -3,11 +3,13 @@ import Ajv2020 from 'ajv/dist/2020.js' import cvss_v2_0 from './csafAjv/cvss-v2.0.js' import cvss_v3_0 from './csafAjv/cvss-v3.0.js' import cvss_v3_1 from './csafAjv/cvss-v3.1.js' +import cvss_v4_0 from './csafAjv/cvss-v4.0.js' const csafAjv = new Ajv2020({ strict: false, allErrors: true }) addFormats(csafAjv) csafAjv.addSchema(cvss_v2_0, 'https://www.first.org/cvss/cvss-v2.0.json') csafAjv.addSchema(cvss_v3_0, 'https://www.first.org/cvss/cvss-v3.0.json') csafAjv.addSchema(cvss_v3_1, 'https://www.first.org/cvss/cvss-v3.1.json') +csafAjv.addSchema(cvss_v4_0, 'https://www.first.org/cvss/cvss-v4.0.json') export default csafAjv diff --git a/lib/shared/csafAjv/cvss-v4.0.js b/lib/shared/csafAjv/cvss-v4.0.js new file mode 100644 index 00000000..4bf575fc --- /dev/null +++ b/lib/shared/csafAjv/cvss-v4.0.js @@ -0,0 +1,407 @@ +export default { + license: [ + 'Copyright (c) 2023, FIRST.ORG, INC.', + 'All rights reserved.', + '', + 'Redistribution and use in source and binary forms, with or without modification, are permitted provided that the ', + 'following conditions are met:', + '1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following ', + ' disclaimer.', + '2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the ', + ' following disclaimer in the documentation and/or other materials provided with the distribution.', + '3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote ', + ' products derived from this software without specific prior written permission.', + '', + "THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 'AS IS' AND ANY EXPRESS OR IMPLIED WARRANTIES, ", + 'INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE ', + 'DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, ', + 'SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR ', + 'SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, ', + 'WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE ', + 'OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.', + ], + + $schema: 'https://json-schema.org/draft/2020-12/schema', + title: 'JSON Schema for Common Vulnerability Scoring System version 4.0', + $id: 'https://www.first.org/cvss/cvss-v4.0.json?20240216', + type: 'object', + definitions: { + attackVectorType: { + type: 'string', + enum: ['NETWORK', 'ADJACENT', 'LOCAL', 'PHYSICAL'], + }, + modifiedAttackVectorType: { + type: 'string', + enum: ['NETWORK', 'ADJACENT', 'LOCAL', 'PHYSICAL', 'NOT_DEFINED'], + default: 'NOT_DEFINED', + }, + attackComplexityType: { + type: 'string', + enum: ['HIGH', 'LOW'], + }, + modifiedAttackComplexityType: { + type: 'string', + enum: ['HIGH', 'LOW', 'NOT_DEFINED'], + default: 'NOT_DEFINED', + }, + attackRequirementsType: { + type: 'string', + enum: ['NONE', 'PRESENT'], + }, + modifiedAttackRequirementsType: { + type: 'string', + enum: ['NONE', 'PRESENT', 'NOT_DEFINED'], + default: 'NOT_DEFINED', + }, + privilegesRequiredType: { + type: 'string', + enum: ['HIGH', 'LOW', 'NONE'], + }, + modifiedPrivilegesRequiredType: { + type: 'string', + enum: ['HIGH', 'LOW', 'NONE', 'NOT_DEFINED'], + default: 'NOT_DEFINED', + }, + userInteractionType: { + type: 'string', + enum: ['NONE', 'PASSIVE', 'ACTIVE'], + }, + modifiedUserInteractionType: { + type: 'string', + enum: ['NONE', 'PASSIVE', 'ACTIVE', 'NOT_DEFINED'], + default: 'NOT_DEFINED', + }, + vulnCiaType: { + type: 'string', + enum: ['NONE', 'LOW', 'HIGH'], + }, + modifiedVulnCiaType: { + type: 'string', + enum: ['NONE', 'LOW', 'HIGH', 'NOT_DEFINED'], + default: 'NOT_DEFINED', + }, + subCiaType: { + type: 'string', + enum: ['NONE', 'LOW', 'HIGH'], + }, + modifiedSubCType: { + type: 'string', + enum: ['NEGLIGIBLE', 'LOW', 'HIGH', 'NOT_DEFINED'], + default: 'NOT_DEFINED', + }, + modifiedSubIaType: { + type: 'string', + enum: ['NEGLIGIBLE', 'LOW', 'HIGH', 'SAFETY', 'NOT_DEFINED'], + default: 'NOT_DEFINED', + }, + exploitMaturityType: { + type: 'string', + enum: ['UNREPORTED', 'PROOF_OF_CONCEPT', 'ATTACKED', 'NOT_DEFINED'], + default: 'NOT_DEFINED', + }, + ciaRequirementType: { + type: 'string', + enum: ['LOW', 'MEDIUM', 'HIGH', 'NOT_DEFINED'], + default: 'NOT_DEFINED', + }, + safetyType: { + type: 'string', + enum: ['NEGLIGIBLE', 'PRESENT', 'NOT_DEFINED'], + default: 'NOT_DEFINED', + }, + automatableType: { + type: 'string', + enum: ['NO', 'YES', 'NOT_DEFINED'], + default: 'NOT_DEFINED', + }, + recoveryType: { + type: 'string', + enum: ['AUTOMATIC', 'USER', 'IRRECOVERABLE', 'NOT_DEFINED'], + default: 'NOT_DEFINED', + }, + valueDensityType: { + type: 'string', + enum: ['DIFFUSE', 'CONCENTRATED', 'NOT_DEFINED'], + default: 'NOT_DEFINED', + }, + vulnerabilityResponseEffortType: { + type: 'string', + enum: ['LOW', 'MODERATE', 'HIGH', 'NOT_DEFINED'], + default: 'NOT_DEFINED', + }, + providerUrgencyType: { + type: 'string', + enum: ['CLEAR', 'GREEN', 'AMBER', 'RED', 'NOT_DEFINED'], + default: 'NOT_DEFINED', + }, + noneScoreType: { + type: 'number', + minimum: 0.0, + maximum: 0.0, + }, + lowScoreType: { + type: 'number', + minimum: 0.1, + maximum: 3.9, + multipleOf: 0.1, + }, + mediumScoreType: { + type: 'number', + minimum: 4.0, + maximum: 6.9, + multipleOf: 0.1, + }, + highScoreType: { + type: 'number', + minimum: 7.0, + maximum: 8.9, + multipleOf: 0.1, + }, + criticalScoreType: { + type: 'number', + minimum: 9.0, + maximum: 10, + multipleOf: 0.1, + }, + noneSeverityType: { + const: 'NONE', + }, + lowSeverityType: { + const: 'LOW', + }, + mediumSeverityType: { + const: 'MEDIUM', + }, + highSeverityType: { + const: 'HIGH', + }, + criticalSeverityType: { + const: 'CRITICAL', + }, + }, + properties: { + version: { + description: 'CVSS Version', + type: 'string', + enum: ['4.0'], + }, + vectorString: { + type: 'string', + pattern: + '^CVSS:4[.]0/AV:[NALP]/AC:[LH]/AT:[NP]/PR:[NLH]/UI:[NPA]/VC:[HLN]/VI:[HLN]/VA:[HLN]/SC:[HLN]/SI:[HLN]/SA:[HLN](/E:[XAPU])?(/CR:[XHML])?(/IR:[XHML])?(/AR:[XHML])?(/MAV:[XNALP])?(/MAC:[XLH])?(/MAT:[XNP])?(/MPR:[XNLH])?(/MUI:[XNPA])?(/MVC:[XNLH])?(/MVI:[XNLH])?(/MVA:[XNLH])?(/MSC:[XNLH])?(/MSI:[XNLHS])?(/MSA:[XNLHS])?(/S:[XNP])?(/AU:[XNY])?(/R:[XAUI])?(/V:[XDC])?(/RE:[XLMH])?(/U:(X|Clear|Green|Amber|Red))?$', + }, + attackVector: { $ref: '#/definitions/attackVectorType' }, + attackComplexity: { $ref: '#/definitions/attackComplexityType' }, + attackRequirements: { $ref: '#/definitions/attackRequirementsType' }, + privilegesRequired: { $ref: '#/definitions/privilegesRequiredType' }, + userInteraction: { $ref: '#/definitions/userInteractionType' }, + vulnConfidentialityImpact: { $ref: '#/definitions/vulnCiaType' }, + vulnIntegrityImpact: { $ref: '#/definitions/vulnCiaType' }, + vulnAvailabilityImpact: { $ref: '#/definitions/vulnCiaType' }, + subConfidentialityImpact: { $ref: '#/definitions/subCiaType' }, + subIntegrityImpact: { $ref: '#/definitions/subCiaType' }, + subAvailabilityImpact: { $ref: '#/definitions/subCiaType' }, + exploitMaturity: { $ref: '#/definitions/exploitMaturityType' }, + confidentialityRequirement: { $ref: '#/definitions/ciaRequirementType' }, + integrityRequirement: { $ref: '#/definitions/ciaRequirementType' }, + availabilityRequirement: { $ref: '#/definitions/ciaRequirementType' }, + modifiedAttackVector: { $ref: '#/definitions/modifiedAttackVectorType' }, + modifiedAttackComplexity: { + $ref: '#/definitions/modifiedAttackComplexityType', + }, + modifiedAttackRequirements: { + $ref: '#/definitions/modifiedAttackRequirementsType', + }, + modifiedPrivilegesRequired: { + $ref: '#/definitions/modifiedPrivilegesRequiredType', + }, + modifiedUserInteraction: { + $ref: '#/definitions/modifiedUserInteractionType', + }, + modifiedVulnConfidentialityImpact: { + $ref: '#/definitions/modifiedVulnCiaType', + }, + modifiedVulnIntegrityImpact: { $ref: '#/definitions/modifiedVulnCiaType' }, + modifiedVulnAvailabilityImpact: { + $ref: '#/definitions/modifiedVulnCiaType', + }, + modifiedSubConfidentialityImpact: { + $ref: '#/definitions/modifiedSubCType', + }, + modifiedSubIntegrityImpact: { $ref: '#/definitions/modifiedSubIaType' }, + modifiedSubAvailabilityImpact: { $ref: '#/definitions/modifiedSubIaType' }, + Safety: { $ref: '#/definitions/safetyType' }, + Automatable: { $ref: '#/definitions/automatableType' }, + Recovery: { $ref: '#/definitions/recoveryType' }, + valueDensity: { $ref: '#/definitions/valueDensityType' }, + vulnerabilityResponseEffort: { + $ref: '#/definitions/vulnerabilityResponseEffortType', + }, + providerUrgency: { $ref: '#/definitions/providerUrgencyType' }, + }, + allOf: [ + { + anyOf: [ + { + properties: { + baseScore: { + $ref: '#/definitions/noneScoreType', + }, + baseSeverity: { + $ref: '#/definitions/noneSeverityType', + }, + }, + }, + { + properties: { + baseScore: { + $ref: '#/definitions/lowScoreType', + }, + baseSeverity: { + $ref: '#/definitions/lowSeverityType', + }, + }, + }, + { + properties: { + baseScore: { + $ref: '#/definitions/mediumScoreType', + }, + baseSeverity: { + $ref: '#/definitions/mediumSeverityType', + }, + }, + }, + { + properties: { + baseScore: { + $ref: '#/definitions/highScoreType', + }, + baseSeverity: { + $ref: '#/definitions/highSeverityType', + }, + }, + }, + { + properties: { + baseScore: { + $ref: '#/definitions/criticalScoreType', + }, + baseSeverity: { + $ref: '#/definitions/criticalSeverityType', + }, + }, + }, + ], + }, + { + anyOf: [ + { + properties: { + threatScore: { + $ref: '#/definitions/noneScoreType', + }, + threatSeverity: { + $ref: '#/definitions/noneSeverityType', + }, + }, + }, + { + properties: { + threatScore: { + $ref: '#/definitions/lowScoreType', + }, + threatSeverity: { + $ref: '#/definitions/lowSeverityType', + }, + }, + }, + { + properties: { + threatScore: { + $ref: '#/definitions/mediumScoreType', + }, + threatSeverity: { + $ref: '#/definitions/mediumSeverityType', + }, + }, + }, + { + properties: { + threatScore: { + $ref: '#/definitions/highScoreType', + }, + threatSeverity: { + $ref: '#/definitions/highSeverityType', + }, + }, + }, + { + properties: { + threatScore: { + $ref: '#/definitions/criticalScoreType', + }, + threatSeverity: { + $ref: '#/definitions/criticalSeverityType', + }, + }, + }, + ], + }, + { + anyOf: [ + { + properties: { + environmentalScore: { + $ref: '#/definitions/noneScoreType', + }, + environmentalSeverity: { + $ref: '#/definitions/noneSeverityType', + }, + }, + }, + { + properties: { + environmentalScore: { + $ref: '#/definitions/lowScoreType', + }, + environmentalSeverity: { + $ref: '#/definitions/lowSeverityType', + }, + }, + }, + { + properties: { + environmentalScore: { + $ref: '#/definitions/mediumScoreType', + }, + environmentalSeverity: { + $ref: '#/definitions/mediumSeverityType', + }, + }, + }, + { + properties: { + environmentalScore: { + $ref: '#/definitions/highScoreType', + }, + environmentalSeverity: { + $ref: '#/definitions/highSeverityType', + }, + }, + }, + { + properties: { + environmentalScore: { + $ref: '#/definitions/criticalScoreType', + }, + environmentalSeverity: { + $ref: '#/definitions/criticalSeverityType', + }, + }, + }, + ], + }, + ], + required: ['version', 'vectorString', 'baseScore', 'baseSeverity'], +} diff --git a/package.json b/package.json index 9507420f..52205bba 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "license": "MIT", "scripts": { "pretest": "tsc -b .", - "test": "node scripts/test.js", + "test": "node scripts/mocha.js && node scripts/test.js", "pretest-report": "tsc -b .", "test-report": "node scripts/test.js --reporter json > test-results.json", "test-coverage": "c8 node scripts/test.js", diff --git a/scripts/mocha.js b/scripts/mocha.js new file mode 100644 index 00000000..9f244ede --- /dev/null +++ b/scripts/mocha.js @@ -0,0 +1,19 @@ +#!/usr/bin/env node + +import { spawn } from 'child_process' +import { fileURLToPath } from 'url' + +spawn( + 'mocha', + ['--exclude', '**/*.test.js', 'tests', ...process.argv.slice(2)], + { + stdio: 'inherit', + env: { + ...process.env, + DICPATH: fileURLToPath(new URL('../tests/dicts', import.meta.url)), + WORDLIST: fileURLToPath( + new URL('../tests/dicts/csaf_words.txt', import.meta.url) + ), + }, + } +) diff --git a/scripts/test.js b/scripts/test.js index 54896a70..e005e80b 100644 --- a/scripts/test.js +++ b/scripts/test.js @@ -3,7 +3,7 @@ import { spawn } from 'child_process' import { fileURLToPath } from 'url' -spawn('mocha', ['tests', ...process.argv.slice(2)], { +spawn('node', ['--test', ...process.argv.slice(2), 'tests'], { stdio: 'inherit', shell: true, env: { diff --git a/tests/csaf_2_1/oasis.test.js b/tests/csaf_2_1/oasis.test.js new file mode 100644 index 00000000..5d787ad7 --- /dev/null +++ b/tests/csaf_2_1/oasis.test.js @@ -0,0 +1,143 @@ +import { readFile } from 'fs/promises' +import * as informative from '../../csaf_2_1/informativeTests.js' +import * as optional from '../../csaf_2_1/optionalTests.js' +import * as mandatory from '../../csaf_2_1/mandatoryTests.js' +import { readFileSync } from 'fs' +import test from 'node:test' +import assert from 'node:assert/strict' + +/** @typedef {import('../../lib/shared/types.js').DocumentTest} DocumentTest */ + +/** @typedef {Map} TestMap */ + +/** + * @typedef {object} TestCases + * @property {TestCase[]} tests + */ + +/** + * @typedef {object} TestCase + * @property {string} id + * @property {string} group + * @property {TestSpec[]} [failures] + * @property {TestSpec[]} [valid] + */ + +/** + * @typedef {object} TestSpec + * @property {string} name + * @property {boolean} valid + */ + +const tests = new Map([ + [ + 'informative', + /** @type {TestMap} */ (new Map(Object.entries(informative))), + ], + ['optional', /** @type {TestMap} */ (new Map(Object.entries(optional)))], + ['mandatory', /** @type {TestMap} */ (new Map(Object.entries(mandatory)))], +]) + +const testDataBaseUrl = new URL( + '../../csaf/csaf_2.1/test/validator/data/', + import.meta.url +) + +const testCases = /** @type {TestCases} */ ( + JSON.parse( + await readFile(new URL('testcases.json', testDataBaseUrl), 'utf-8') + ) +) + +const testMap = parseTestCases() + +for (const [group, t] of testMap) { + test.describe(group, function () { + for (const [testId, u] of t) { + test.describe(testId, function () { + for (const [type, testSpecs] of u) { + test.describe(type, function () { + for (const testSpec of testSpecs) { + test(testSpec.name, async (t) => { + const test = tests + .get(group) + ?.get(`${group}Test_${testId.replace(/\./g, '_')}`) + + if (!test) { + t.todo() + return + } + + const doc = JSON.parse( + readFileSync(new URL(testSpec.name, testDataBaseUrl), 'utf-8') + ) + + const result = await test(doc) + + if (group === 'mandatory') { + assert.equal(result.isValid, testSpec.valid) + assert.equal( + Boolean(result.errors?.length), + type === 'failures', + type === 'failures' + ? 'should have errors' + : `should not have errors, but had ${result.errors?.length}` + ) + } else { + assert.equal(result.isValid === undefined, testSpec.valid) + + if (group === 'optional') { + assert.equal( + Boolean(result.warnings?.length), + type === 'failures', + type === 'failures' + ? 'should have warnings' + : `should not have warnings, but had ${result.warnings?.length}` + ) + } else if (group === 'informative') { + assert.equal( + Boolean(result.infos?.length), + type === 'failures', + type === 'failures' + ? 'should have infos' + : `should not have infos, but had ${result.infos?.length}` + ) + } + } + }) + } + }) + } + }) + } + }) +} + +function parseTestCases() { + /** @type {Map>>} */ + const testData = new Map() + for (const test of testCases.tests) { + const valids = testData.get(test.group)?.get(test.id)?.get('valid') ?? [] + const failures = + testData.get(test.group)?.get(test.id)?.get('failures') ?? [] + + for (const valid of test.valid ?? []) { + valids.push(valid) + } + for (const failure of test.failures ?? []) { + failures.push(failure) + } + + testData.set( + test.group, + new Map(testData.get(test.group)).set( + test.id, + new Map(testData.get(test.group)?.get(test.id)) + .set('valid', valids) + .set('failures', failures) + ) + ) + } + + return testData +} From 913711ff27349fa04438f7ff3b4f602cc76086a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Burgd=C3=B6rfer?= Date: Wed, 26 Feb 2025 12:21:34 +0100 Subject: [PATCH 03/25] test: exclude all unimplemented CSAF 2.1 tests from test suite --- .github/workflows/run-tests.yml | 2 +- tests/csaf_2_1/oasis.test.js | 49 ++++++++++++++++++++++++++++++--- 2 files changed, 46 insertions(+), 5 deletions(-) diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index f86c1aec..8adbc88c 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -2,7 +2,7 @@ name: Run Tests on: pull_request: branches: - - main + - '**' permissions: contents: read actions: read diff --git a/tests/csaf_2_1/oasis.test.js b/tests/csaf_2_1/oasis.test.js index 5d787ad7..2426e593 100644 --- a/tests/csaf_2_1/oasis.test.js +++ b/tests/csaf_2_1/oasis.test.js @@ -6,6 +6,45 @@ import { readFileSync } from 'fs' import test from 'node:test' import assert from 'node:assert/strict' +/* + This is a list that includes all test numbers that are not yet implemented. + Once all tests are implemented for CSAF 2.1 this should be deleted. + */ +const excluded = [ + '6.1.7', + '6.1.8', + '6.1.9', + '6.1.10', + '6.1.11', + '6.1.14', + '6.1.16', + '6.1.34', + '6.1.35', + '6.1.36', + '6.1.37', + '6.1.38', + '6.1.39', + '6.1.40', + '6.1.41', + '6.2.6', + '6.2.11', + '6.2.19', + '6.2.21', + '6.2.22', + '6.2.23', + '6.2.24', + '6.2.25', + '6.2.26', + '6.2.27', + '6.2.28', + '6.2.29', + '6.2.30', + '6.3.1', + '6.3.2', + '6.3.4', + '6.3.12', +] + /** @typedef {import('../../lib/shared/types.js').DocumentTest} DocumentTest */ /** @typedef {Map} TestMap */ @@ -59,14 +98,16 @@ for (const [group, t] of testMap) { test.describe(type, function () { for (const testSpec of testSpecs) { test(testSpec.name, async (t) => { + if (excluded.includes(testId)) { + t.todo() + return + } + const test = tests .get(group) ?.get(`${group}Test_${testId.replace(/\./g, '_')}`) - if (!test) { - t.todo() - return - } + assert(test, 'test does not exist') const doc = JSON.parse( readFileSync(new URL(testSpec.name, testDataBaseUrl), 'utf-8') From 20980dd3437045d3fc82d8923089e1597cdbe45f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Burgd=C3=B6rfer?= Date: Wed, 26 Feb 2025 12:35:36 +0100 Subject: [PATCH 04/25] test: revert to mocha to reuse coverage toolchain --- package.json | 2 +- scripts/mocha.js | 19 ------------------- scripts/test.js | 2 +- tests/csaf_2_1/{oasis.test.js => oasis.js} | 20 ++++++++------------ 4 files changed, 10 insertions(+), 33 deletions(-) delete mode 100644 scripts/mocha.js rename tests/csaf_2_1/{oasis.test.js => oasis.js} (91%) diff --git a/package.json b/package.json index 52205bba..9507420f 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "license": "MIT", "scripts": { "pretest": "tsc -b .", - "test": "node scripts/mocha.js && node scripts/test.js", + "test": "node scripts/test.js", "pretest-report": "tsc -b .", "test-report": "node scripts/test.js --reporter json > test-results.json", "test-coverage": "c8 node scripts/test.js", diff --git a/scripts/mocha.js b/scripts/mocha.js deleted file mode 100644 index 9f244ede..00000000 --- a/scripts/mocha.js +++ /dev/null @@ -1,19 +0,0 @@ -#!/usr/bin/env node - -import { spawn } from 'child_process' -import { fileURLToPath } from 'url' - -spawn( - 'mocha', - ['--exclude', '**/*.test.js', 'tests', ...process.argv.slice(2)], - { - stdio: 'inherit', - env: { - ...process.env, - DICPATH: fileURLToPath(new URL('../tests/dicts', import.meta.url)), - WORDLIST: fileURLToPath( - new URL('../tests/dicts/csaf_words.txt', import.meta.url) - ), - }, - } -) diff --git a/scripts/test.js b/scripts/test.js index e005e80b..c2b42993 100644 --- a/scripts/test.js +++ b/scripts/test.js @@ -3,7 +3,7 @@ import { spawn } from 'child_process' import { fileURLToPath } from 'url' -spawn('node', ['--test', ...process.argv.slice(2), 'tests'], { +spawn('mocha', ['tests', 'tests/csaf_2_1', ...process.argv.slice(2)], { stdio: 'inherit', shell: true, env: { diff --git a/tests/csaf_2_1/oasis.test.js b/tests/csaf_2_1/oasis.js similarity index 91% rename from tests/csaf_2_1/oasis.test.js rename to tests/csaf_2_1/oasis.js index 2426e593..d147f6c3 100644 --- a/tests/csaf_2_1/oasis.test.js +++ b/tests/csaf_2_1/oasis.js @@ -1,10 +1,9 @@ -import { readFile } from 'fs/promises' +import { readFile } from 'node:fs/promises' +import { readFileSync } from 'node:fs' +import assert from 'node:assert/strict' import * as informative from '../../csaf_2_1/informativeTests.js' import * as optional from '../../csaf_2_1/optionalTests.js' import * as mandatory from '../../csaf_2_1/mandatoryTests.js' -import { readFileSync } from 'fs' -import test from 'node:test' -import assert from 'node:assert/strict' /* This is a list that includes all test numbers that are not yet implemented. @@ -91,18 +90,15 @@ const testCases = /** @type {TestCases} */ ( const testMap = parseTestCases() for (const [group, t] of testMap) { - test.describe(group, function () { + describe(group, function () { for (const [testId, u] of t) { - test.describe(testId, function () { + describe(testId, function () { for (const [type, testSpecs] of u) { - test.describe(type, function () { + describe(type, function () { for (const testSpec of testSpecs) { - test(testSpec.name, async (t) => { - if (excluded.includes(testId)) { - t.todo() - return - } + if (excluded.includes(testId)) continue + it(testSpec.name, async () => { const test = tests .get(group) ?.get(`${group}Test_${testId.replace(/\./g, '_')}`) From 27a76fb903185a98586501fd801c0d444c55a4a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Burgd=C3=B6rfer?= Date: Fri, 28 Feb 2025 08:51:44 +0100 Subject: [PATCH 05/25] chore: adapt `runTest.js` script to allow csaf 2.1 tests --- scripts/runTest.js | 152 ++++++++++++++++++++++++++++++++++----------- 1 file changed, 117 insertions(+), 35 deletions(-) diff --git a/scripts/runTest.js b/scripts/runTest.js index 773b6684..f58f067d 100755 --- a/scripts/runTest.js +++ b/scripts/runTest.js @@ -3,44 +3,126 @@ /** * @file Script to validate JSON files against given tests * - * Usage: node .js json/file/path.json mandatoryTest_6_1_1 + * Usage: node .js -f -t [-c ] * - * `mandatoryTest_6_1_1` is a sample here. You can insert any test name from lib/mandatoryTests.js, - * `lib/optionalTests.js`, `lib/schemaTests.js` and `lib/schemaTests.js`. + * -f + * Specifies the path to the csaf json file to validate the given test against. + * + * -t + * Specifies the test(s) to run. The values that you can pass here depend on the value + * of the `-c` option which specifies the used csaf version. If you use 2.0 here you + * can insert any test name from `mandatoryTests.js`, `optionalTests.js`, + * `informativeTests.js` and `schemaTests.js`. + * If you use 2.1 here you can insert any test name from `csaf_2_1/mandatoryTests.js`, + * `csaf_2_1/optionalTests.js`, `csaf_2_1/informativeTests.js` and `csaf_2_1/schemaTests.js`. + * Some presets are also allowed such as `mandatory`, `optional`, `informative`, + * `schema` and `base`. + * + * -c (default: 2.0) + * Specifies the csaf version to use. The currently allowed versions are `2.0` (the default) + * and `2.1`. */ import { readFile } from 'fs/promises' -import * as schemaTests from '../schemaTests.js' -import * as mandatoryTests from '../mandatoryTests.js' -import * as optionalTests from '../optionalTests.js' -import * as informativeTests from '../informativeTests.js' import validate from '../validate.js' +import { parseArgs } from 'node:util' +import assert from 'node:assert' + +/** + * Types a function that can lazily load a set of tests. This is used to speed up the script + * by avoiding to load unused test sets. + * + * @typedef {() => Promise>} DocumentTestLoader + */ + +/** + * This is the main function that reads the file, executes the resolved test + * and logs the result. + * + * @param {object} ctx + * @param {DocumentTestLoader} ctx.schemaTests + * @param {DocumentTestLoader} ctx.mandatoryTests + * @param {DocumentTestLoader} ctx.optionalTests + * @param {DocumentTestLoader} ctx.informativeTests + * @param {object} params + * @param {string} params.testName + * @param {string} params.filePath + */ +const main = async ( + { informativeTests, mandatoryTests, optionalTests, schemaTests }, + { testName, filePath } +) => { + const json = JSON.parse(await readFile(filePath, { encoding: 'utf-8' })) + + const matchingTests = + testName === 'mandatory' + ? Object.values(await mandatoryTests()) + : testName === 'optional' + ? Object.values(await optionalTests()) + : testName === 'informative' + ? Object.values(await informativeTests()) + : testName === 'schema' + ? Object.values(await schemaTests()) + : testName === 'base' + ? Object.values(await schemaTests()).concat( + Object.values(await mandatoryTests()) + ) + : Object.values(await mandatoryTests()) + .concat(Object.values(await optionalTests())) + .concat(Object.values(await informativeTests())) + .concat(Object.values(await schemaTests())) + .filter((t) => t.name === testName) + + if (!matchingTests.length) + throw new Error(`No test matching "${testName}" found`) + const result = await validate(matchingTests, json) + process.exitCode = result.isValid ? 0 : 1 + console.log(JSON.stringify(result, null, 2)) +} + +const { values: cliOptions } = parseArgs({ + options: { + file: { + type: 'string', + short: 'f', + }, + 'csaf-version': { + type: 'string', + short: 'c', + default: '2.0', + }, + test: { + type: 'string', + short: 't', + }, + }, +}) + +const filePath = cliOptions.file +const testName = cliOptions.test +assert(filePath) +assert(testName) -const [, , filePath, testName] = process.argv - -const json = JSON.parse(await readFile(filePath, { encoding: 'utf-8' })) - -const matchingTests = - testName === 'mandatory' - ? Object.values(mandatoryTests) - : testName === 'optional' - ? Object.values(optionalTests) - : testName === 'informative' - ? Object.values(informativeTests) - : testName === 'schema' - ? Object.values(schemaTests) - : testName === 'base' - ? Object.values(schemaTests).concat(Object.values(mandatoryTests)) - : /** @type {Array} */ ( - Object.values(mandatoryTests) - ) - .concat(Object.values(optionalTests)) - .concat(Object.values(informativeTests)) - .concat(Object.values(schemaTests)) - .filter((t) => t.name === testName) - -if (!matchingTests.length) - throw new Error(`No test matching "${testName}" found`) -const result = await validate(matchingTests, json) -process.exitCode = result.isValid ? 0 : 1 -console.log(JSON.stringify(result, null, 2)) +if (cliOptions['csaf-version'] === '2.0') { + await main( + { + mandatoryTests: () => import('../mandatoryTests.js'), + informativeTests: () => import('../informativeTests.js'), + optionalTests: () => import('../optionalTests.js'), + schemaTests: () => import('../schemaTests.js'), + }, + { filePath, testName } + ) +} else if (cliOptions['csaf-version'] === '2.1') { + await main( + { + mandatoryTests: () => import('../csaf_2_1/mandatoryTests.js'), + informativeTests: () => import('../csaf_2_1/informativeTests.js'), + optionalTests: () => import('../csaf_2_1/optionalTests.js'), + schemaTests: () => import('../csaf_2_1/schemaTests.js'), + }, + { filePath, testName } + ) +} else { + throw new Error('Unknown CSAF version') +} From 606118374d5f98ab6ccb6af06bd00fc82dce49c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Burgd=C3=B6rfer?= Date: Mon, 3 Mar 2025 15:14:46 +0100 Subject: [PATCH 06/25] test: exclude tests that were newly added --- tests/csaf_2_1/oasis.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/csaf_2_1/oasis.js b/tests/csaf_2_1/oasis.js index d147f6c3..691311af 100644 --- a/tests/csaf_2_1/oasis.js +++ b/tests/csaf_2_1/oasis.js @@ -15,6 +15,7 @@ const excluded = [ '6.1.9', '6.1.10', '6.1.11', + '6.1.13', '6.1.14', '6.1.16', '6.1.34', @@ -25,6 +26,7 @@ const excluded = [ '6.1.39', '6.1.40', '6.1.41', + '6.1.42', '6.2.6', '6.2.11', '6.2.19', @@ -38,6 +40,8 @@ const excluded = [ '6.2.28', '6.2.29', '6.2.30', + '6.2.31', + '6.2.32', '6.3.1', '6.3.2', '6.3.4', From 48287893d767659fb063ae8edd0d5fe00802d263 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Burgd=C3=B6rfer?= Date: Thu, 20 Feb 2025 17:35:49 +0100 Subject: [PATCH 07/25] feat: add mandatory test 6.1.34 --- csaf_2_1/mandatoryTests.js | 1 + .../mandatoryTests/mandatoryTest_6_1_34.js | 85 +++++++++++++++++++ .../mandatoryTest_6_1_34/types.ts | 3 + tests/csaf_2_1/oasis.js | 1 - 4 files changed, 89 insertions(+), 1 deletion(-) create mode 100644 csaf_2_1/mandatoryTests/mandatoryTest_6_1_34.js create mode 100644 csaf_2_1/mandatoryTests/mandatoryTest_6_1_34/types.ts diff --git a/csaf_2_1/mandatoryTests.js b/csaf_2_1/mandatoryTests.js index be6b62ce..9b5b19ad 100644 --- a/csaf_2_1/mandatoryTests.js +++ b/csaf_2_1/mandatoryTests.js @@ -43,3 +43,4 @@ export { mandatoryTest_6_1_32, mandatoryTest_6_1_33, } from '../mandatoryTests.js' +export { mandatoryTest_6_1_34 } from './mandatoryTests/mandatoryTest_6_1_34.js' diff --git a/csaf_2_1/mandatoryTests/mandatoryTest_6_1_34.js b/csaf_2_1/mandatoryTests/mandatoryTest_6_1_34.js new file mode 100644 index 00000000..3fcef2d6 --- /dev/null +++ b/csaf_2_1/mandatoryTests/mandatoryTest_6_1_34.js @@ -0,0 +1,85 @@ +import Ajv from 'ajv/dist/jtd.js' + +/* + The maximum allowed nesting level of branches. + */ +const MAX_DEPTH = 30 + +const ajv = new Ajv() + +const branchSchema = /** @type {const} */ ({ + additionalProperties: true, + optionalProperties: { + branches: { + elements: { + additionalProperties: true, + properties: {}, + }, + }, + }, +}) + +const validateBranch = ajv.compile(branchSchema) + +/* + 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: { + product_tree: branchSchema, + }, +}) + +const validate = ajv.compile(inputSchema) + +/** + * This implements the mandatory test 6.1.34 of the CSAF 2.1 standard. + * + * @param {any} doc + */ +export function mandatoryTest_6_1_34(doc) { + /* + The `ctx` variable holds the state that is accumulated during the test ran and is + finally returned by the function. + */ + const ctx = { + errors: + /** @type {Array<{ instancePath: string; message: string }>} */ ([]), + isValid: true, + } + + if (!validate(doc)) { + return ctx + } + + /** + * This recursive function checks if the given branch is too deep. A maximum of 30 + * levels is allowed. + * + * @param {import('./mandatoryTest_6_1_34/types').TypeOf} branch + * @param {string} prefix The json path to the given branch. + * Is used to generate the error messages. + */ + const checkBranch = (branch, prefix, count = 0) => { + if (!branch.branches?.length && count > MAX_DEPTH) { + ctx.isValid = false + ctx.errors.push({ + instancePath: prefix, + message: `branch structure nesting exceeds ${MAX_DEPTH} branches (it is ${count} levels deep)`, + }) + return + } + branch.branches?.forEach((branch, index) => { + if (!validateBranch(branch)) return + checkBranch(branch, `${prefix}/branches/${index}`, count + 1) + }) + } + + checkBranch(doc.product_tree, '/product_tree') + + return ctx +} diff --git a/csaf_2_1/mandatoryTests/mandatoryTest_6_1_34/types.ts b/csaf_2_1/mandatoryTests/mandatoryTest_6_1_34/types.ts new file mode 100644 index 00000000..291b2d19 --- /dev/null +++ b/csaf_2_1/mandatoryTests/mandatoryTest_6_1_34/types.ts @@ -0,0 +1,3 @@ +import { ValidateFunction } from 'ajv' + +export type TypeOf = T extends ValidateFunction ? R : never diff --git a/tests/csaf_2_1/oasis.js b/tests/csaf_2_1/oasis.js index 691311af..e3bb154d 100644 --- a/tests/csaf_2_1/oasis.js +++ b/tests/csaf_2_1/oasis.js @@ -18,7 +18,6 @@ const excluded = [ '6.1.13', '6.1.14', '6.1.16', - '6.1.34', '6.1.35', '6.1.36', '6.1.37', From 98de13f53cb13449bb23e4b4301e636b32b70893 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Burgd=C3=B6rfer?= Date: Wed, 26 Feb 2025 15:45:06 +0100 Subject: [PATCH 08/25] feat: add mandatory test 6.1.35 --- csaf_2_1/mandatoryTests.js | 1 + .../mandatoryTests/mandatoryTest_6_1_35.js | 209 ++++++++++++++++++ tests/csaf_2_1/mandatoryTest_6_1_35.js | 35 +++ tests/csaf_2_1/oasis.js | 1 - 4 files changed, 245 insertions(+), 1 deletion(-) create mode 100644 csaf_2_1/mandatoryTests/mandatoryTest_6_1_35.js create mode 100644 tests/csaf_2_1/mandatoryTest_6_1_35.js diff --git a/csaf_2_1/mandatoryTests.js b/csaf_2_1/mandatoryTests.js index 9b5b19ad..18eea4a9 100644 --- a/csaf_2_1/mandatoryTests.js +++ b/csaf_2_1/mandatoryTests.js @@ -44,3 +44,4 @@ export { mandatoryTest_6_1_33, } from '../mandatoryTests.js' export { mandatoryTest_6_1_34 } from './mandatoryTests/mandatoryTest_6_1_34.js' +export { mandatoryTest_6_1_35 } from './mandatoryTests/mandatoryTest_6_1_35.js' diff --git a/csaf_2_1/mandatoryTests/mandatoryTest_6_1_35.js b/csaf_2_1/mandatoryTests/mandatoryTest_6_1_35.js new file mode 100644 index 00000000..b74c95f0 --- /dev/null +++ b/csaf_2_1/mandatoryTests/mandatoryTest_6_1_35.js @@ -0,0 +1,209 @@ +import Ajv from 'ajv/dist/jtd.js' + +const ajv = new Ajv() + +/** + * @typedef {'workaround' + * | 'mitigation' + * | 'vendor_fix' + * | 'optional_patch' + * | 'none_available' + * | 'fix_planned' + * | 'no_fix_planned'} Category + */ + +/** + * This map holds prohibited category combinations. + * See https://github.com/oasis-tcs/csaf/blob/master/csaf_2.1/prose/share/csaf-v2.1-draft.md#324131-vulnerabilities-property---remediations---category- + * + * @type {Map>} + */ +const prohibitionRuleMap = new Map( + /** @satisfies {Array<[Category, Category[]]>} */ ([ + ['workaround', ['optional_patch', 'none_available']], + ['mitigation', ['optional_patch', 'none_available']], + [ + 'vendor_fix', + ['optional_patch', 'none_available', 'fix_planned', 'no_fix_planned'], + ], + [ + 'optional_patch', + [ + 'workaround', + 'mitigation', + 'vendor_fix', + 'none_available', + 'fix_planned', + 'no_fix_planned', + ], + ], + [ + 'none_available', + [ + 'workaround', + 'mitigation', + 'vendor_fix', + 'optional_patch', + 'fix_planned', + 'no_fix_planned', + ], + ], + [ + 'fix_planned', + ['vendor_fix', 'optional_patch', 'none_available', 'no_fix_planned'], + ], + [ + 'no_fix_planned', + ['vendor_fix', 'optional_patch', 'none_available', 'fix_planned'], + ], + ]).map((e) => [e[0], new Set(e[1])]) +) + +const remediationSchema = /** @type {const} */ ({ + additionalProperties: true, + optionalProperties: { + group_ids: { + elements: { + type: 'string', + }, + }, + product_ids: { + elements: { + type: 'string', + }, + }, + category: { type: 'string' }, + }, +}) + +/* + 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, + optionalProperties: { + product_tree: { + additionalProperties: true, + optionalProperties: { + product_groups: { + elements: { + additionalProperties: true, + optionalProperties: { + group_id: { type: 'string' }, + product_ids: { + elements: { + type: 'string', + }, + }, + }, + }, + }, + }, + }, + }, + properties: { + vulnerabilities: { + elements: { + additionalProperties: true, + optionalProperties: { + remediations: { + elements: remediationSchema, + }, + }, + }, + }, + }, +}) + +const validate = ajv.compile(inputSchema) + +/** + * This implements the mandatory test of the CSAF 2.1 standard. + * + * @param {any} doc + */ +export function mandatoryTest_6_1_35(doc) { + /* + The `ctx` variable holds the state that is accumulated during the test ran and is + finally returned by the function. + */ + const ctx = { + /** @type {Array<{ instancePath: string; message: string }>} */ + errors: [], + isValid: true, + } + + if (!validate(doc)) { + return ctx + } + + for (const [vulnerabilityIndex, vulnerability] of Object.entries( + doc.vulnerabilities + )) { + /** + * This map holds all discovered product ids and maps them to the set of corresponding + * remediation categories. Later we can check this map to find out if there are any + * contradicting remediations. + * + * @type {Map>} + */ + const productToCategoriesMap = new Map() + + vulnerability.remediations?.forEach((remediation, remediationIndex) => { + const category = remediation.category + if (!category) return + + /** + * This function adds the current category to the given product id in the + * `productMap`. If the product does not yet exist in the map, it is added. + * + * @param {string} id + */ + const collectCategory = (id) => { + productToCategoriesMap.set( + id, + new Set(productToCategoriesMap.get(id)).add(category) + ) + } + + remediation.product_ids?.forEach(collectCategory) + + remediation.group_ids?.forEach((id) => { + const group = doc.product_tree?.product_groups?.find( + (g) => g.group_id === id + ) + if (!group) return + group.product_ids?.forEach(collectCategory) + }) + + for (const [productId, categories] of productToCategoriesMap) { + /** + * This set will hold all already checked categories to avoid double checks + * and doubled error messages. + */ + const checkedCategories = new Set() + + for (const categoryA of categories) { + checkedCategories.add(categoryA) + + for (const categoryB of categories) { + if (checkedCategories.has(categoryB)) continue + + if (prohibitionRuleMap.get(categoryA)?.has(categoryB)) { + ctx.errors.push({ + instancePath: `/vulnerabilities/${vulnerabilityIndex}/remediations/${remediationIndex}`, + message: `contradicting remediation categories for product id "${productId}": ${categoryA}, ${categoryB}`, + }) + ctx.isValid = false + } + } + } + } + }) + } + + return ctx +} diff --git a/tests/csaf_2_1/mandatoryTest_6_1_35.js b/tests/csaf_2_1/mandatoryTest_6_1_35.js new file mode 100644 index 00000000..13da1457 --- /dev/null +++ b/tests/csaf_2_1/mandatoryTest_6_1_35.js @@ -0,0 +1,35 @@ +import assert from 'node:assert' +import { mandatoryTest_6_1_35 } from '../../csaf_2_1/mandatoryTests.js' + +describe('mandatoryTest_6_1_37', function () { + it('only runs on relevant documents', function () { + assert.equal(mandatoryTest_6_1_35({ document: 'mydoc' }).isValid, true) + }) + + it('skips remediations without valid category', function () { + assert.equal( + mandatoryTest_6_1_35({ + vulnerabilities: [{ remediations: [{}] }], + }).isValid, + true + ) + }) + + it('skips remediation group checks without declared group', function () { + assert.equal( + mandatoryTest_6_1_35({ + vulnerabilities: [ + { + remediations: [ + { + category: 'some_category', + group_ids: ['my_not_existing_group'], + }, + ], + }, + ], + }).isValid, + true + ) + }) +}) diff --git a/tests/csaf_2_1/oasis.js b/tests/csaf_2_1/oasis.js index e3bb154d..c5c791fa 100644 --- a/tests/csaf_2_1/oasis.js +++ b/tests/csaf_2_1/oasis.js @@ -18,7 +18,6 @@ const excluded = [ '6.1.13', '6.1.14', '6.1.16', - '6.1.35', '6.1.36', '6.1.37', '6.1.38', From ad7b2cb90617c5529e027fdcb2517b54773b7c54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Burgd=C3=B6rfer?= Date: Wed, 12 Feb 2025 15:29:54 +0100 Subject: [PATCH 09/25] feat: setup csaf 2.1 infrastructure --- package.json | 2 +- scripts/mocha.js | 19 +++++ tests/csaf_2_1/oasis.test.js | 143 +++++++++++++++++++++++++++++++++++ 3 files changed, 163 insertions(+), 1 deletion(-) create mode 100644 scripts/mocha.js create mode 100644 tests/csaf_2_1/oasis.test.js diff --git a/package.json b/package.json index 9507420f..52205bba 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "license": "MIT", "scripts": { "pretest": "tsc -b .", - "test": "node scripts/test.js", + "test": "node scripts/mocha.js && node scripts/test.js", "pretest-report": "tsc -b .", "test-report": "node scripts/test.js --reporter json > test-results.json", "test-coverage": "c8 node scripts/test.js", diff --git a/scripts/mocha.js b/scripts/mocha.js new file mode 100644 index 00000000..9f244ede --- /dev/null +++ b/scripts/mocha.js @@ -0,0 +1,19 @@ +#!/usr/bin/env node + +import { spawn } from 'child_process' +import { fileURLToPath } from 'url' + +spawn( + 'mocha', + ['--exclude', '**/*.test.js', 'tests', ...process.argv.slice(2)], + { + stdio: 'inherit', + env: { + ...process.env, + DICPATH: fileURLToPath(new URL('../tests/dicts', import.meta.url)), + WORDLIST: fileURLToPath( + new URL('../tests/dicts/csaf_words.txt', import.meta.url) + ), + }, + } +) diff --git a/tests/csaf_2_1/oasis.test.js b/tests/csaf_2_1/oasis.test.js new file mode 100644 index 00000000..5d787ad7 --- /dev/null +++ b/tests/csaf_2_1/oasis.test.js @@ -0,0 +1,143 @@ +import { readFile } from 'fs/promises' +import * as informative from '../../csaf_2_1/informativeTests.js' +import * as optional from '../../csaf_2_1/optionalTests.js' +import * as mandatory from '../../csaf_2_1/mandatoryTests.js' +import { readFileSync } from 'fs' +import test from 'node:test' +import assert from 'node:assert/strict' + +/** @typedef {import('../../lib/shared/types.js').DocumentTest} DocumentTest */ + +/** @typedef {Map} TestMap */ + +/** + * @typedef {object} TestCases + * @property {TestCase[]} tests + */ + +/** + * @typedef {object} TestCase + * @property {string} id + * @property {string} group + * @property {TestSpec[]} [failures] + * @property {TestSpec[]} [valid] + */ + +/** + * @typedef {object} TestSpec + * @property {string} name + * @property {boolean} valid + */ + +const tests = new Map([ + [ + 'informative', + /** @type {TestMap} */ (new Map(Object.entries(informative))), + ], + ['optional', /** @type {TestMap} */ (new Map(Object.entries(optional)))], + ['mandatory', /** @type {TestMap} */ (new Map(Object.entries(mandatory)))], +]) + +const testDataBaseUrl = new URL( + '../../csaf/csaf_2.1/test/validator/data/', + import.meta.url +) + +const testCases = /** @type {TestCases} */ ( + JSON.parse( + await readFile(new URL('testcases.json', testDataBaseUrl), 'utf-8') + ) +) + +const testMap = parseTestCases() + +for (const [group, t] of testMap) { + test.describe(group, function () { + for (const [testId, u] of t) { + test.describe(testId, function () { + for (const [type, testSpecs] of u) { + test.describe(type, function () { + for (const testSpec of testSpecs) { + test(testSpec.name, async (t) => { + const test = tests + .get(group) + ?.get(`${group}Test_${testId.replace(/\./g, '_')}`) + + if (!test) { + t.todo() + return + } + + const doc = JSON.parse( + readFileSync(new URL(testSpec.name, testDataBaseUrl), 'utf-8') + ) + + const result = await test(doc) + + if (group === 'mandatory') { + assert.equal(result.isValid, testSpec.valid) + assert.equal( + Boolean(result.errors?.length), + type === 'failures', + type === 'failures' + ? 'should have errors' + : `should not have errors, but had ${result.errors?.length}` + ) + } else { + assert.equal(result.isValid === undefined, testSpec.valid) + + if (group === 'optional') { + assert.equal( + Boolean(result.warnings?.length), + type === 'failures', + type === 'failures' + ? 'should have warnings' + : `should not have warnings, but had ${result.warnings?.length}` + ) + } else if (group === 'informative') { + assert.equal( + Boolean(result.infos?.length), + type === 'failures', + type === 'failures' + ? 'should have infos' + : `should not have infos, but had ${result.infos?.length}` + ) + } + } + }) + } + }) + } + }) + } + }) +} + +function parseTestCases() { + /** @type {Map>>} */ + const testData = new Map() + for (const test of testCases.tests) { + const valids = testData.get(test.group)?.get(test.id)?.get('valid') ?? [] + const failures = + testData.get(test.group)?.get(test.id)?.get('failures') ?? [] + + for (const valid of test.valid ?? []) { + valids.push(valid) + } + for (const failure of test.failures ?? []) { + failures.push(failure) + } + + testData.set( + test.group, + new Map(testData.get(test.group)).set( + test.id, + new Map(testData.get(test.group)?.get(test.id)) + .set('valid', valids) + .set('failures', failures) + ) + ) + } + + return testData +} From f6f1edd70cd7ad3d10c65d79847dfb4da12ec520 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Burgd=C3=B6rfer?= Date: Wed, 26 Feb 2025 12:21:34 +0100 Subject: [PATCH 10/25] test: exclude all unimplemented CSAF 2.1 tests from test suite --- tests/csaf_2_1/oasis.test.js | 49 +++++++++++++++++++++++++++++++++--- 1 file changed, 45 insertions(+), 4 deletions(-) diff --git a/tests/csaf_2_1/oasis.test.js b/tests/csaf_2_1/oasis.test.js index 5d787ad7..2426e593 100644 --- a/tests/csaf_2_1/oasis.test.js +++ b/tests/csaf_2_1/oasis.test.js @@ -6,6 +6,45 @@ import { readFileSync } from 'fs' import test from 'node:test' import assert from 'node:assert/strict' +/* + This is a list that includes all test numbers that are not yet implemented. + Once all tests are implemented for CSAF 2.1 this should be deleted. + */ +const excluded = [ + '6.1.7', + '6.1.8', + '6.1.9', + '6.1.10', + '6.1.11', + '6.1.14', + '6.1.16', + '6.1.34', + '6.1.35', + '6.1.36', + '6.1.37', + '6.1.38', + '6.1.39', + '6.1.40', + '6.1.41', + '6.2.6', + '6.2.11', + '6.2.19', + '6.2.21', + '6.2.22', + '6.2.23', + '6.2.24', + '6.2.25', + '6.2.26', + '6.2.27', + '6.2.28', + '6.2.29', + '6.2.30', + '6.3.1', + '6.3.2', + '6.3.4', + '6.3.12', +] + /** @typedef {import('../../lib/shared/types.js').DocumentTest} DocumentTest */ /** @typedef {Map} TestMap */ @@ -59,14 +98,16 @@ for (const [group, t] of testMap) { test.describe(type, function () { for (const testSpec of testSpecs) { test(testSpec.name, async (t) => { + if (excluded.includes(testId)) { + t.todo() + return + } + const test = tests .get(group) ?.get(`${group}Test_${testId.replace(/\./g, '_')}`) - if (!test) { - t.todo() - return - } + assert(test, 'test does not exist') const doc = JSON.parse( readFileSync(new URL(testSpec.name, testDataBaseUrl), 'utf-8') From bc8ac10d5a7554ba530bb2b63a3eef0fbf747894 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Burgd=C3=B6rfer?= Date: Wed, 26 Feb 2025 12:35:36 +0100 Subject: [PATCH 11/25] test: revert to mocha to reuse coverage toolchain --- package.json | 2 +- scripts/mocha.js | 19 ---- tests/csaf_2_1/oasis.test.js | 184 ----------------------------------- 3 files changed, 1 insertion(+), 204 deletions(-) delete mode 100644 scripts/mocha.js delete mode 100644 tests/csaf_2_1/oasis.test.js diff --git a/package.json b/package.json index 52205bba..9507420f 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "license": "MIT", "scripts": { "pretest": "tsc -b .", - "test": "node scripts/mocha.js && node scripts/test.js", + "test": "node scripts/test.js", "pretest-report": "tsc -b .", "test-report": "node scripts/test.js --reporter json > test-results.json", "test-coverage": "c8 node scripts/test.js", diff --git a/scripts/mocha.js b/scripts/mocha.js deleted file mode 100644 index 9f244ede..00000000 --- a/scripts/mocha.js +++ /dev/null @@ -1,19 +0,0 @@ -#!/usr/bin/env node - -import { spawn } from 'child_process' -import { fileURLToPath } from 'url' - -spawn( - 'mocha', - ['--exclude', '**/*.test.js', 'tests', ...process.argv.slice(2)], - { - stdio: 'inherit', - env: { - ...process.env, - DICPATH: fileURLToPath(new URL('../tests/dicts', import.meta.url)), - WORDLIST: fileURLToPath( - new URL('../tests/dicts/csaf_words.txt', import.meta.url) - ), - }, - } -) diff --git a/tests/csaf_2_1/oasis.test.js b/tests/csaf_2_1/oasis.test.js deleted file mode 100644 index 2426e593..00000000 --- a/tests/csaf_2_1/oasis.test.js +++ /dev/null @@ -1,184 +0,0 @@ -import { readFile } from 'fs/promises' -import * as informative from '../../csaf_2_1/informativeTests.js' -import * as optional from '../../csaf_2_1/optionalTests.js' -import * as mandatory from '../../csaf_2_1/mandatoryTests.js' -import { readFileSync } from 'fs' -import test from 'node:test' -import assert from 'node:assert/strict' - -/* - This is a list that includes all test numbers that are not yet implemented. - Once all tests are implemented for CSAF 2.1 this should be deleted. - */ -const excluded = [ - '6.1.7', - '6.1.8', - '6.1.9', - '6.1.10', - '6.1.11', - '6.1.14', - '6.1.16', - '6.1.34', - '6.1.35', - '6.1.36', - '6.1.37', - '6.1.38', - '6.1.39', - '6.1.40', - '6.1.41', - '6.2.6', - '6.2.11', - '6.2.19', - '6.2.21', - '6.2.22', - '6.2.23', - '6.2.24', - '6.2.25', - '6.2.26', - '6.2.27', - '6.2.28', - '6.2.29', - '6.2.30', - '6.3.1', - '6.3.2', - '6.3.4', - '6.3.12', -] - -/** @typedef {import('../../lib/shared/types.js').DocumentTest} DocumentTest */ - -/** @typedef {Map} TestMap */ - -/** - * @typedef {object} TestCases - * @property {TestCase[]} tests - */ - -/** - * @typedef {object} TestCase - * @property {string} id - * @property {string} group - * @property {TestSpec[]} [failures] - * @property {TestSpec[]} [valid] - */ - -/** - * @typedef {object} TestSpec - * @property {string} name - * @property {boolean} valid - */ - -const tests = new Map([ - [ - 'informative', - /** @type {TestMap} */ (new Map(Object.entries(informative))), - ], - ['optional', /** @type {TestMap} */ (new Map(Object.entries(optional)))], - ['mandatory', /** @type {TestMap} */ (new Map(Object.entries(mandatory)))], -]) - -const testDataBaseUrl = new URL( - '../../csaf/csaf_2.1/test/validator/data/', - import.meta.url -) - -const testCases = /** @type {TestCases} */ ( - JSON.parse( - await readFile(new URL('testcases.json', testDataBaseUrl), 'utf-8') - ) -) - -const testMap = parseTestCases() - -for (const [group, t] of testMap) { - test.describe(group, function () { - for (const [testId, u] of t) { - test.describe(testId, function () { - for (const [type, testSpecs] of u) { - test.describe(type, function () { - for (const testSpec of testSpecs) { - test(testSpec.name, async (t) => { - if (excluded.includes(testId)) { - t.todo() - return - } - - const test = tests - .get(group) - ?.get(`${group}Test_${testId.replace(/\./g, '_')}`) - - assert(test, 'test does not exist') - - const doc = JSON.parse( - readFileSync(new URL(testSpec.name, testDataBaseUrl), 'utf-8') - ) - - const result = await test(doc) - - if (group === 'mandatory') { - assert.equal(result.isValid, testSpec.valid) - assert.equal( - Boolean(result.errors?.length), - type === 'failures', - type === 'failures' - ? 'should have errors' - : `should not have errors, but had ${result.errors?.length}` - ) - } else { - assert.equal(result.isValid === undefined, testSpec.valid) - - if (group === 'optional') { - assert.equal( - Boolean(result.warnings?.length), - type === 'failures', - type === 'failures' - ? 'should have warnings' - : `should not have warnings, but had ${result.warnings?.length}` - ) - } else if (group === 'informative') { - assert.equal( - Boolean(result.infos?.length), - type === 'failures', - type === 'failures' - ? 'should have infos' - : `should not have infos, but had ${result.infos?.length}` - ) - } - } - }) - } - }) - } - }) - } - }) -} - -function parseTestCases() { - /** @type {Map>>} */ - const testData = new Map() - for (const test of testCases.tests) { - const valids = testData.get(test.group)?.get(test.id)?.get('valid') ?? [] - const failures = - testData.get(test.group)?.get(test.id)?.get('failures') ?? [] - - for (const valid of test.valid ?? []) { - valids.push(valid) - } - for (const failure of test.failures ?? []) { - failures.push(failure) - } - - testData.set( - test.group, - new Map(testData.get(test.group)).set( - test.id, - new Map(testData.get(test.group)?.get(test.id)) - .set('valid', valids) - .set('failures', failures) - ) - ) - } - - return testData -} From d7c3080e79b9aee49b2d93c4e38efbf2280acb24 Mon Sep 17 00:00:00 2001 From: rschneider <97682836+rainer-exxcellent@users.noreply.github.com> Date: Fri, 14 Feb 2025 15:46:25 +0100 Subject: [PATCH 12/25] feat(CSAF2.1): #197 copy and adapt mandatory test 6.1.1 from CSAF 2.0 to CSAF 2.1 feat(CSAF2.1): #197 copy and adapt mandatory test 6.1.8 from CSAF 2.0 to CSAF 2.1 --- csaf_2_1/mandatoryTests.js | 2 +- .../mandatoryTests/mandatoryTest_6_1_1.js | 278 ++++++++++++++++++ 2 files changed, 279 insertions(+), 1 deletion(-) create mode 100644 csaf_2_1/mandatoryTests/mandatoryTest_6_1_1.js diff --git a/csaf_2_1/mandatoryTests.js b/csaf_2_1/mandatoryTests.js index 18eea4a9..3d53da3e 100644 --- a/csaf_2_1/mandatoryTests.js +++ b/csaf_2_1/mandatoryTests.js @@ -1,5 +1,5 @@ +export { default as mandatoryTest_6_1_1 } from './mandatoryTests/mandatoryTest_6_1_1.js' export { - mandatoryTest_6_1_1, mandatoryTest_6_1_2, mandatoryTest_6_1_3, mandatoryTest_6_1_4, diff --git a/csaf_2_1/mandatoryTests/mandatoryTest_6_1_1.js b/csaf_2_1/mandatoryTests/mandatoryTest_6_1_1.js new file mode 100644 index 00000000..06edef30 --- /dev/null +++ b/csaf_2_1/mandatoryTests/mandatoryTest_6_1_1.js @@ -0,0 +1,278 @@ +import * as docUtils from '../../lib/mandatoryTests/shared/docUtils.js' + +const { collectProductIds } = docUtils + +/** + * @typedef {Object} FullProductName + * @property {string} name + * @property {string} product_id + */ + +/** + * @typedef {Object} Branch + * @property {Array} branches + * @property {FullProductName} product + */ + +/** + * @param {any} doc + */ +export default function mandatoryTest_6_1_1(doc) { + /** @type {Array<{ message: string; instancePath: string }>} */ + const errors = [] + let isValid = true + + const productIds = collectProductIds({ document: doc }) + const productIdRefs = collectProductIdRefs({ document: doc }) + const missingProductDefinitions = findMissingDefinitions( + productIds, + productIdRefs + ) + if (missingProductDefinitions.length > 0) { + isValid = false + missingProductDefinitions.forEach((missingProductDefinition) => { + errors.push({ + message: 'definition of product id missing', + instancePath: missingProductDefinition.instancePath, + }) + }) + } + return { isValid, errors } +} + +/** + * This method collects references to product ids and corresponding instancePaths in the given document and returns a result object. + * @param {any} document + * @returns {{id: string, instancePath: string}[]} + */ +function collectProductIdRefs({ document }) { + const entries = /** @type {{id: string, instancePath: string}[]} */ ([]) + + const productGroups = document.product_tree?.product_groups + if (productGroups) { + for (let i = 0; i < productGroups.length; ++i) { + const productGroup = productGroups[i] + const productIds = productGroup.product_ids + if (productIds) { + for (let j = 0; j < productIds.length; ++j) { + const productId = productIds[j] + if (productId) { + entries.push({ + id: productId, + instancePath: `/product_tree/product_groups/${i}/product_ids/${j}`, + }) + } + } + } + } + } + + const relationshipGroups = document.product_tree?.relationships + if (relationshipGroups) { + for (let i = 0; i < relationshipGroups.length; ++i) { + const relationshipGroup = relationshipGroups[i] + const productRef = relationshipGroup.product_reference + if (productRef) { + entries.push({ + id: productRef, + instancePath: `/product_tree/relationships/${i}/product_reference`, + }) + } + const relToProductRef = relationshipGroup.relates_to_product_reference + if (relToProductRef) { + entries.push({ + id: relToProductRef, + instancePath: `/product_tree/relationships/${i}/relates_to_product_reference`, + }) + } + } + } + + const vulnerabilities = document.vulnerabilities + if (vulnerabilities) { + for (let i = 0; i < vulnerabilities.length; ++i) { + const vulnerability = vulnerabilities[i] + collectRefsInProductStatus( + `/vulnerabilities/${i}/product_status`, + vulnerability, + entries + ) + collectProductRefsInRemediations( + `/vulnerabilities/${i}/remediations`, + vulnerability, + entries + ) + collectRefsInMetrics( + `/vulnerabilities/${i}/metrics`, + vulnerability, + entries + ) + collectProductRefsInThreats( + `/vulnerabilities/${i}/threats`, + vulnerability, + entries + ) + } + } + + return entries +} + +/** + * @param {string} instancePath + * @param {{product_status: any}} vulnerability + * @param {*} entries + */ +const collectRefsInProductStatus = (instancePath, vulnerability, entries) => { + findRefsInProductStatus( + vulnerability.product_status?.first_affected, + `${instancePath}/first_affected`, + entries + ) + findRefsInProductStatus( + vulnerability.product_status?.first_fixed, + `${instancePath}/first_fixed`, + entries + ) + findRefsInProductStatus( + vulnerability.product_status?.fixed, + `${instancePath}/fixed`, + entries + ) + findRefsInProductStatus( + vulnerability.product_status?.known_affected, + `${instancePath}/known_affected`, + entries + ) + findRefsInProductStatus( + vulnerability.product_status?.known_not_affected, + `${instancePath}/known_not_affected`, + entries + ) + findRefsInProductStatus( + vulnerability.product_status?.last_affected, + `${instancePath}/last_affected`, + entries + ) + findRefsInProductStatus( + vulnerability.product_status?.recommended, + `${instancePath}/recommended`, + entries + ) + findRefsInProductStatus( + vulnerability.product_status?.under_investigation, + `${instancePath}/under_investigation`, + entries + ) +} + +/** + * @param {string[]} refs + * @param {string} instancePath + * @param {{id: string, instancePath: string}[]} entries + */ +const findRefsInProductStatus = (refs, instancePath, entries) => { + if (refs) { + for (let i = 0; i < refs.length; ++i) { + const ref = refs[i] + if (ref) { + entries.push({ + id: ref, + instancePath: `${instancePath}/${i}`, + }) + } + } + } +} + +/** + * @param {string} instancePath + * @param {{threats: any}} vulnerability + * @param {*} entries + */ +const collectProductRefsInThreats = (instancePath, vulnerability, entries) => { + const threats = vulnerability.threats + if (threats) { + for (let i = 0; i < threats.length; ++i) { + const threat = threats[i] + const productIds = threat.product_ids + if (productIds) { + for (let j = 0; j < productIds.length; ++j) { + const productId = productIds[j] + if (productId) { + entries.push({ + id: productId, + instancePath: `${instancePath}/${i}/product_ids/${j}`, + }) + } + } + } + } + } +} + +/** + * @param {string} instancePath + * @param {{metrics: any}} vulnerability + * @param {*} entries + */ +const collectRefsInMetrics = (instancePath, vulnerability, entries) => { + const metrics = vulnerability.metrics + if (metrics) { + for (let i = 0; i < metrics.length; ++i) { + const metric = metrics[i] + const products = metric.products + if (products) { + for (let j = 0; j < products.length; ++j) { + const productId = products[j] + if (productId) { + entries.push({ + id: productId, + instancePath: `${instancePath}/${i}/products/${j}`, + }) + } + } + } + } + } +} + +/** + * @param {string} instancePath + * @param {{remediations: any}} vulnerability + * @param {*} entries + */ +const collectProductRefsInRemediations = ( + instancePath, + vulnerability, + entries +) => { + const remediations = vulnerability.remediations + if (remediations) { + for (let i = 0; i < remediations.length; ++i) { + const remediation = remediations[i] + const productIds = remediation.product_ids + if (productIds) { + for (let j = 0; j < productIds.length; ++j) { + const productId = productIds[j] + if (productId) { + entries.push({ + id: productId, + instancePath: `${instancePath}/${i}/product_ids/${j}`, + }) + } + } + } + } + } +} + +/** + * @param {{id: string}[]} entries + * @param {{id: string, instancePath: string}[]} refs + */ +const findMissingDefinitions = (entries, refs) => { + return refs.filter( + (ref) => entries.find((e) => e.id === ref.id) === undefined + ) +} From 2d3948735675c7cac1467bb16d1ebbda9eaf1dcb Mon Sep 17 00:00:00 2001 From: rschneider <97682836+rainer-exxcellent@users.noreply.github.com> Date: Thu, 13 Mar 2025 07:56:30 +0100 Subject: [PATCH 13/25] feat(CSAF2.1): #197 mandatory test 6.1.1 rebase to 196-csaf-2.1, Import adjusted to other tests --- csaf_2_1/mandatoryTests.js | 90 +++++++++---------- .../mandatoryTests/mandatoryTest_6_1_1.js | 2 +- 2 files changed, 46 insertions(+), 46 deletions(-) diff --git a/csaf_2_1/mandatoryTests.js b/csaf_2_1/mandatoryTests.js index 3d53da3e..fa990d0f 100644 --- a/csaf_2_1/mandatoryTests.js +++ b/csaf_2_1/mandatoryTests.js @@ -1,47 +1,47 @@ -export { default as mandatoryTest_6_1_1 } from './mandatoryTests/mandatoryTest_6_1_1.js' export { - mandatoryTest_6_1_2, - mandatoryTest_6_1_3, - mandatoryTest_6_1_4, - mandatoryTest_6_1_5, - mandatoryTest_6_1_6, - mandatoryTest_6_1_7, - mandatoryTest_6_1_8, - mandatoryTest_6_1_9, - mandatoryTest_6_1_10, - mandatoryTest_6_1_11, - mandatoryTest_6_1_12, - mandatoryTest_6_1_13, - mandatoryTest_6_1_14, - mandatoryTest_6_1_15, - mandatoryTest_6_1_16, - mandatoryTest_6_1_17, - mandatoryTest_6_1_18, - mandatoryTest_6_1_19, - mandatoryTest_6_1_20, - mandatoryTest_6_1_21, - mandatoryTest_6_1_22, - mandatoryTest_6_1_23, - mandatoryTest_6_1_24, - mandatoryTest_6_1_25, - mandatoryTest_6_1_26, - mandatoryTest_6_1_27_1, - mandatoryTest_6_1_27_2, - mandatoryTest_6_1_27_3, - mandatoryTest_6_1_27_4, - mandatoryTest_6_1_27_5, - mandatoryTest_6_1_27_6, - mandatoryTest_6_1_27_7, - mandatoryTest_6_1_27_8, - mandatoryTest_6_1_27_9, - mandatoryTest_6_1_27_10, - mandatoryTest_6_1_27_11, - mandatoryTest_6_1_28, - mandatoryTest_6_1_29, - mandatoryTest_6_1_30, - mandatoryTest_6_1_31, - mandatoryTest_6_1_32, - mandatoryTest_6_1_33, + mandatoryTest_6_1_2, + mandatoryTest_6_1_3, + mandatoryTest_6_1_4, + mandatoryTest_6_1_5, + mandatoryTest_6_1_6, + mandatoryTest_6_1_7, + mandatoryTest_6_1_8, + mandatoryTest_6_1_9, + mandatoryTest_6_1_10, + mandatoryTest_6_1_11, + mandatoryTest_6_1_12, + mandatoryTest_6_1_13, + mandatoryTest_6_1_14, + mandatoryTest_6_1_15, + mandatoryTest_6_1_16, + mandatoryTest_6_1_17, + mandatoryTest_6_1_18, + mandatoryTest_6_1_19, + mandatoryTest_6_1_20, + mandatoryTest_6_1_21, + mandatoryTest_6_1_22, + mandatoryTest_6_1_23, + mandatoryTest_6_1_24, + mandatoryTest_6_1_25, + mandatoryTest_6_1_26, + mandatoryTest_6_1_27_1, + mandatoryTest_6_1_27_2, + mandatoryTest_6_1_27_3, + mandatoryTest_6_1_27_4, + mandatoryTest_6_1_27_5, + mandatoryTest_6_1_27_6, + mandatoryTest_6_1_27_7, + mandatoryTest_6_1_27_8, + mandatoryTest_6_1_27_9, + mandatoryTest_6_1_27_10, + mandatoryTest_6_1_27_11, + mandatoryTest_6_1_28, + mandatoryTest_6_1_29, + mandatoryTest_6_1_30, + mandatoryTest_6_1_31, + mandatoryTest_6_1_32, + mandatoryTest_6_1_33, } from '../mandatoryTests.js' -export { mandatoryTest_6_1_34 } from './mandatoryTests/mandatoryTest_6_1_34.js' -export { mandatoryTest_6_1_35 } from './mandatoryTests/mandatoryTest_6_1_35.js' +export {mandatoryTest_6_1_1} from './mandatoryTests/mandatoryTest_6_1_1.js' +export {mandatoryTest_6_1_34} from './mandatoryTests/mandatoryTest_6_1_34.js' +export {mandatoryTest_6_1_35} from './mandatoryTests/mandatoryTest_6_1_35.js' diff --git a/csaf_2_1/mandatoryTests/mandatoryTest_6_1_1.js b/csaf_2_1/mandatoryTests/mandatoryTest_6_1_1.js index 06edef30..f2523632 100644 --- a/csaf_2_1/mandatoryTests/mandatoryTest_6_1_1.js +++ b/csaf_2_1/mandatoryTests/mandatoryTest_6_1_1.js @@ -17,7 +17,7 @@ const { collectProductIds } = docUtils /** * @param {any} doc */ -export default function mandatoryTest_6_1_1(doc) { +export function mandatoryTest_6_1_1(doc) { /** @type {Array<{ message: string; instancePath: string }>} */ const errors = [] let isValid = true From f2530189b29fb4d5a1eca878979bf5aeee4fa3a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Burgd=C3=B6rfer?= Date: Wed, 12 Feb 2025 15:29:54 +0100 Subject: [PATCH 14/25] feat: setup csaf 2.1 infrastructure --- package.json | 2 +- scripts/mocha.js | 19 +++++ tests/csaf_2_1/oasis.test.js | 143 +++++++++++++++++++++++++++++++++++ 3 files changed, 163 insertions(+), 1 deletion(-) create mode 100644 scripts/mocha.js create mode 100644 tests/csaf_2_1/oasis.test.js diff --git a/package.json b/package.json index 9507420f..52205bba 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "license": "MIT", "scripts": { "pretest": "tsc -b .", - "test": "node scripts/test.js", + "test": "node scripts/mocha.js && node scripts/test.js", "pretest-report": "tsc -b .", "test-report": "node scripts/test.js --reporter json > test-results.json", "test-coverage": "c8 node scripts/test.js", diff --git a/scripts/mocha.js b/scripts/mocha.js new file mode 100644 index 00000000..9f244ede --- /dev/null +++ b/scripts/mocha.js @@ -0,0 +1,19 @@ +#!/usr/bin/env node + +import { spawn } from 'child_process' +import { fileURLToPath } from 'url' + +spawn( + 'mocha', + ['--exclude', '**/*.test.js', 'tests', ...process.argv.slice(2)], + { + stdio: 'inherit', + env: { + ...process.env, + DICPATH: fileURLToPath(new URL('../tests/dicts', import.meta.url)), + WORDLIST: fileURLToPath( + new URL('../tests/dicts/csaf_words.txt', import.meta.url) + ), + }, + } +) diff --git a/tests/csaf_2_1/oasis.test.js b/tests/csaf_2_1/oasis.test.js new file mode 100644 index 00000000..5d787ad7 --- /dev/null +++ b/tests/csaf_2_1/oasis.test.js @@ -0,0 +1,143 @@ +import { readFile } from 'fs/promises' +import * as informative from '../../csaf_2_1/informativeTests.js' +import * as optional from '../../csaf_2_1/optionalTests.js' +import * as mandatory from '../../csaf_2_1/mandatoryTests.js' +import { readFileSync } from 'fs' +import test from 'node:test' +import assert from 'node:assert/strict' + +/** @typedef {import('../../lib/shared/types.js').DocumentTest} DocumentTest */ + +/** @typedef {Map} TestMap */ + +/** + * @typedef {object} TestCases + * @property {TestCase[]} tests + */ + +/** + * @typedef {object} TestCase + * @property {string} id + * @property {string} group + * @property {TestSpec[]} [failures] + * @property {TestSpec[]} [valid] + */ + +/** + * @typedef {object} TestSpec + * @property {string} name + * @property {boolean} valid + */ + +const tests = new Map([ + [ + 'informative', + /** @type {TestMap} */ (new Map(Object.entries(informative))), + ], + ['optional', /** @type {TestMap} */ (new Map(Object.entries(optional)))], + ['mandatory', /** @type {TestMap} */ (new Map(Object.entries(mandatory)))], +]) + +const testDataBaseUrl = new URL( + '../../csaf/csaf_2.1/test/validator/data/', + import.meta.url +) + +const testCases = /** @type {TestCases} */ ( + JSON.parse( + await readFile(new URL('testcases.json', testDataBaseUrl), 'utf-8') + ) +) + +const testMap = parseTestCases() + +for (const [group, t] of testMap) { + test.describe(group, function () { + for (const [testId, u] of t) { + test.describe(testId, function () { + for (const [type, testSpecs] of u) { + test.describe(type, function () { + for (const testSpec of testSpecs) { + test(testSpec.name, async (t) => { + const test = tests + .get(group) + ?.get(`${group}Test_${testId.replace(/\./g, '_')}`) + + if (!test) { + t.todo() + return + } + + const doc = JSON.parse( + readFileSync(new URL(testSpec.name, testDataBaseUrl), 'utf-8') + ) + + const result = await test(doc) + + if (group === 'mandatory') { + assert.equal(result.isValid, testSpec.valid) + assert.equal( + Boolean(result.errors?.length), + type === 'failures', + type === 'failures' + ? 'should have errors' + : `should not have errors, but had ${result.errors?.length}` + ) + } else { + assert.equal(result.isValid === undefined, testSpec.valid) + + if (group === 'optional') { + assert.equal( + Boolean(result.warnings?.length), + type === 'failures', + type === 'failures' + ? 'should have warnings' + : `should not have warnings, but had ${result.warnings?.length}` + ) + } else if (group === 'informative') { + assert.equal( + Boolean(result.infos?.length), + type === 'failures', + type === 'failures' + ? 'should have infos' + : `should not have infos, but had ${result.infos?.length}` + ) + } + } + }) + } + }) + } + }) + } + }) +} + +function parseTestCases() { + /** @type {Map>>} */ + const testData = new Map() + for (const test of testCases.tests) { + const valids = testData.get(test.group)?.get(test.id)?.get('valid') ?? [] + const failures = + testData.get(test.group)?.get(test.id)?.get('failures') ?? [] + + for (const valid of test.valid ?? []) { + valids.push(valid) + } + for (const failure of test.failures ?? []) { + failures.push(failure) + } + + testData.set( + test.group, + new Map(testData.get(test.group)).set( + test.id, + new Map(testData.get(test.group)?.get(test.id)) + .set('valid', valids) + .set('failures', failures) + ) + ) + } + + return testData +} From ea2d47bc3c341aebb34594315bc0811cc3d4cc4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Burgd=C3=B6rfer?= Date: Wed, 26 Feb 2025 12:21:34 +0100 Subject: [PATCH 15/25] test: exclude all unimplemented CSAF 2.1 tests from test suite --- tests/csaf_2_1/oasis.test.js | 49 +++++++++++++++++++++++++++++++++--- 1 file changed, 45 insertions(+), 4 deletions(-) diff --git a/tests/csaf_2_1/oasis.test.js b/tests/csaf_2_1/oasis.test.js index 5d787ad7..2426e593 100644 --- a/tests/csaf_2_1/oasis.test.js +++ b/tests/csaf_2_1/oasis.test.js @@ -6,6 +6,45 @@ import { readFileSync } from 'fs' import test from 'node:test' import assert from 'node:assert/strict' +/* + This is a list that includes all test numbers that are not yet implemented. + Once all tests are implemented for CSAF 2.1 this should be deleted. + */ +const excluded = [ + '6.1.7', + '6.1.8', + '6.1.9', + '6.1.10', + '6.1.11', + '6.1.14', + '6.1.16', + '6.1.34', + '6.1.35', + '6.1.36', + '6.1.37', + '6.1.38', + '6.1.39', + '6.1.40', + '6.1.41', + '6.2.6', + '6.2.11', + '6.2.19', + '6.2.21', + '6.2.22', + '6.2.23', + '6.2.24', + '6.2.25', + '6.2.26', + '6.2.27', + '6.2.28', + '6.2.29', + '6.2.30', + '6.3.1', + '6.3.2', + '6.3.4', + '6.3.12', +] + /** @typedef {import('../../lib/shared/types.js').DocumentTest} DocumentTest */ /** @typedef {Map} TestMap */ @@ -59,14 +98,16 @@ for (const [group, t] of testMap) { test.describe(type, function () { for (const testSpec of testSpecs) { test(testSpec.name, async (t) => { + if (excluded.includes(testId)) { + t.todo() + return + } + const test = tests .get(group) ?.get(`${group}Test_${testId.replace(/\./g, '_')}`) - if (!test) { - t.todo() - return - } + assert(test, 'test does not exist') const doc = JSON.parse( readFileSync(new URL(testSpec.name, testDataBaseUrl), 'utf-8') From 0f0926857815bdbd347fabd86f37ae1606ecbc56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Burgd=C3=B6rfer?= Date: Wed, 26 Feb 2025 12:35:36 +0100 Subject: [PATCH 16/25] test: revert to mocha to reuse coverage toolchain --- package.json | 2 +- scripts/mocha.js | 19 ---- tests/csaf_2_1/oasis.test.js | 184 ----------------------------------- 3 files changed, 1 insertion(+), 204 deletions(-) delete mode 100644 scripts/mocha.js delete mode 100644 tests/csaf_2_1/oasis.test.js diff --git a/package.json b/package.json index 52205bba..9507420f 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "license": "MIT", "scripts": { "pretest": "tsc -b .", - "test": "node scripts/mocha.js && node scripts/test.js", + "test": "node scripts/test.js", "pretest-report": "tsc -b .", "test-report": "node scripts/test.js --reporter json > test-results.json", "test-coverage": "c8 node scripts/test.js", diff --git a/scripts/mocha.js b/scripts/mocha.js deleted file mode 100644 index 9f244ede..00000000 --- a/scripts/mocha.js +++ /dev/null @@ -1,19 +0,0 @@ -#!/usr/bin/env node - -import { spawn } from 'child_process' -import { fileURLToPath } from 'url' - -spawn( - 'mocha', - ['--exclude', '**/*.test.js', 'tests', ...process.argv.slice(2)], - { - stdio: 'inherit', - env: { - ...process.env, - DICPATH: fileURLToPath(new URL('../tests/dicts', import.meta.url)), - WORDLIST: fileURLToPath( - new URL('../tests/dicts/csaf_words.txt', import.meta.url) - ), - }, - } -) diff --git a/tests/csaf_2_1/oasis.test.js b/tests/csaf_2_1/oasis.test.js deleted file mode 100644 index 2426e593..00000000 --- a/tests/csaf_2_1/oasis.test.js +++ /dev/null @@ -1,184 +0,0 @@ -import { readFile } from 'fs/promises' -import * as informative from '../../csaf_2_1/informativeTests.js' -import * as optional from '../../csaf_2_1/optionalTests.js' -import * as mandatory from '../../csaf_2_1/mandatoryTests.js' -import { readFileSync } from 'fs' -import test from 'node:test' -import assert from 'node:assert/strict' - -/* - This is a list that includes all test numbers that are not yet implemented. - Once all tests are implemented for CSAF 2.1 this should be deleted. - */ -const excluded = [ - '6.1.7', - '6.1.8', - '6.1.9', - '6.1.10', - '6.1.11', - '6.1.14', - '6.1.16', - '6.1.34', - '6.1.35', - '6.1.36', - '6.1.37', - '6.1.38', - '6.1.39', - '6.1.40', - '6.1.41', - '6.2.6', - '6.2.11', - '6.2.19', - '6.2.21', - '6.2.22', - '6.2.23', - '6.2.24', - '6.2.25', - '6.2.26', - '6.2.27', - '6.2.28', - '6.2.29', - '6.2.30', - '6.3.1', - '6.3.2', - '6.3.4', - '6.3.12', -] - -/** @typedef {import('../../lib/shared/types.js').DocumentTest} DocumentTest */ - -/** @typedef {Map} TestMap */ - -/** - * @typedef {object} TestCases - * @property {TestCase[]} tests - */ - -/** - * @typedef {object} TestCase - * @property {string} id - * @property {string} group - * @property {TestSpec[]} [failures] - * @property {TestSpec[]} [valid] - */ - -/** - * @typedef {object} TestSpec - * @property {string} name - * @property {boolean} valid - */ - -const tests = new Map([ - [ - 'informative', - /** @type {TestMap} */ (new Map(Object.entries(informative))), - ], - ['optional', /** @type {TestMap} */ (new Map(Object.entries(optional)))], - ['mandatory', /** @type {TestMap} */ (new Map(Object.entries(mandatory)))], -]) - -const testDataBaseUrl = new URL( - '../../csaf/csaf_2.1/test/validator/data/', - import.meta.url -) - -const testCases = /** @type {TestCases} */ ( - JSON.parse( - await readFile(new URL('testcases.json', testDataBaseUrl), 'utf-8') - ) -) - -const testMap = parseTestCases() - -for (const [group, t] of testMap) { - test.describe(group, function () { - for (const [testId, u] of t) { - test.describe(testId, function () { - for (const [type, testSpecs] of u) { - test.describe(type, function () { - for (const testSpec of testSpecs) { - test(testSpec.name, async (t) => { - if (excluded.includes(testId)) { - t.todo() - return - } - - const test = tests - .get(group) - ?.get(`${group}Test_${testId.replace(/\./g, '_')}`) - - assert(test, 'test does not exist') - - const doc = JSON.parse( - readFileSync(new URL(testSpec.name, testDataBaseUrl), 'utf-8') - ) - - const result = await test(doc) - - if (group === 'mandatory') { - assert.equal(result.isValid, testSpec.valid) - assert.equal( - Boolean(result.errors?.length), - type === 'failures', - type === 'failures' - ? 'should have errors' - : `should not have errors, but had ${result.errors?.length}` - ) - } else { - assert.equal(result.isValid === undefined, testSpec.valid) - - if (group === 'optional') { - assert.equal( - Boolean(result.warnings?.length), - type === 'failures', - type === 'failures' - ? 'should have warnings' - : `should not have warnings, but had ${result.warnings?.length}` - ) - } else if (group === 'informative') { - assert.equal( - Boolean(result.infos?.length), - type === 'failures', - type === 'failures' - ? 'should have infos' - : `should not have infos, but had ${result.infos?.length}` - ) - } - } - }) - } - }) - } - }) - } - }) -} - -function parseTestCases() { - /** @type {Map>>} */ - const testData = new Map() - for (const test of testCases.tests) { - const valids = testData.get(test.group)?.get(test.id)?.get('valid') ?? [] - const failures = - testData.get(test.group)?.get(test.id)?.get('failures') ?? [] - - for (const valid of test.valid ?? []) { - valids.push(valid) - } - for (const failure of test.failures ?? []) { - failures.push(failure) - } - - testData.set( - test.group, - new Map(testData.get(test.group)).set( - test.id, - new Map(testData.get(test.group)?.get(test.id)) - .set('valid', valids) - .set('failures', failures) - ) - ) - } - - return testData -} From 987c23e064e5fc38236dd1d748047fe26612b0bf Mon Sep 17 00:00:00 2001 From: rschneider <97682836+rainer-exxcellent@users.noreply.github.com> Date: Tue, 18 Feb 2025 09:43:09 +0100 Subject: [PATCH 17/25] feat(CSAF2.1): #197 copy and adapt mandatory test 6.1.8 from CSAF 2.0 to CSAF 2.1 --- csaf_2_1/mandatoryTests.js | 84 ++++++------- .../mandatoryTests/mandatoryTest_6_1_8.js | 119 ++++++++++++++++++ csaf_2_1/tests/mandatoryTest_6_1_8.js | 20 +++ ...oasis_csaf_tc-csaf_2_0-2021-6-1-08-01.json | 58 +++++++++ 4 files changed, 239 insertions(+), 42 deletions(-) create mode 100644 csaf_2_1/mandatoryTests/mandatoryTest_6_1_8.js create mode 100644 csaf_2_1/tests/mandatoryTest_6_1_8.js create mode 100644 csaf_2_1/tests/mandatoryTest_6_1_8/failing/oasis_csaf_tc-csaf_2_0-2021-6-1-08-01.json diff --git a/csaf_2_1/mandatoryTests.js b/csaf_2_1/mandatoryTests.js index fa990d0f..4a0d79b0 100644 --- a/csaf_2_1/mandatoryTests.js +++ b/csaf_2_1/mandatoryTests.js @@ -1,46 +1,46 @@ export { - mandatoryTest_6_1_2, - mandatoryTest_6_1_3, - mandatoryTest_6_1_4, - mandatoryTest_6_1_5, - mandatoryTest_6_1_6, - mandatoryTest_6_1_7, - mandatoryTest_6_1_8, - mandatoryTest_6_1_9, - mandatoryTest_6_1_10, - mandatoryTest_6_1_11, - mandatoryTest_6_1_12, - mandatoryTest_6_1_13, - mandatoryTest_6_1_14, - mandatoryTest_6_1_15, - mandatoryTest_6_1_16, - mandatoryTest_6_1_17, - mandatoryTest_6_1_18, - mandatoryTest_6_1_19, - mandatoryTest_6_1_20, - mandatoryTest_6_1_21, - mandatoryTest_6_1_22, - mandatoryTest_6_1_23, - mandatoryTest_6_1_24, - mandatoryTest_6_1_25, - mandatoryTest_6_1_26, - mandatoryTest_6_1_27_1, - mandatoryTest_6_1_27_2, - mandatoryTest_6_1_27_3, - mandatoryTest_6_1_27_4, - mandatoryTest_6_1_27_5, - mandatoryTest_6_1_27_6, - mandatoryTest_6_1_27_7, - mandatoryTest_6_1_27_8, - mandatoryTest_6_1_27_9, - mandatoryTest_6_1_27_10, - mandatoryTest_6_1_27_11, - mandatoryTest_6_1_28, - mandatoryTest_6_1_29, - mandatoryTest_6_1_30, - mandatoryTest_6_1_31, - mandatoryTest_6_1_32, - mandatoryTest_6_1_33, + mandatoryTest_6_1_1, + mandatoryTest_6_1_2, + mandatoryTest_6_1_3, + mandatoryTest_6_1_4, + mandatoryTest_6_1_5, + mandatoryTest_6_1_6, + mandatoryTest_6_1_7, + mandatoryTest_6_1_9, + mandatoryTest_6_1_10, + mandatoryTest_6_1_11, + mandatoryTest_6_1_12, + mandatoryTest_6_1_13, + mandatoryTest_6_1_14, + mandatoryTest_6_1_15, + mandatoryTest_6_1_16, + mandatoryTest_6_1_17, + mandatoryTest_6_1_18, + mandatoryTest_6_1_19, + mandatoryTest_6_1_20, + mandatoryTest_6_1_21, + mandatoryTest_6_1_22, + mandatoryTest_6_1_23, + mandatoryTest_6_1_24, + mandatoryTest_6_1_25, + mandatoryTest_6_1_26, + mandatoryTest_6_1_27_1, + mandatoryTest_6_1_27_2, + mandatoryTest_6_1_27_3, + mandatoryTest_6_1_27_4, + mandatoryTest_6_1_27_5, + mandatoryTest_6_1_27_6, + mandatoryTest_6_1_27_7, + mandatoryTest_6_1_27_8, + mandatoryTest_6_1_27_9, + mandatoryTest_6_1_27_10, + mandatoryTest_6_1_27_11, + mandatoryTest_6_1_28, + mandatoryTest_6_1_29, + mandatoryTest_6_1_30, + mandatoryTest_6_1_31, + mandatoryTest_6_1_32, + mandatoryTest_6_1_33, } from '../mandatoryTests.js' export {mandatoryTest_6_1_1} from './mandatoryTests/mandatoryTest_6_1_1.js' export {mandatoryTest_6_1_34} from './mandatoryTests/mandatoryTest_6_1_34.js' diff --git a/csaf_2_1/mandatoryTests/mandatoryTest_6_1_8.js b/csaf_2_1/mandatoryTests/mandatoryTest_6_1_8.js new file mode 100644 index 00000000..20a02991 --- /dev/null +++ b/csaf_2_1/mandatoryTests/mandatoryTest_6_1_8.js @@ -0,0 +1,119 @@ +import Ajv from 'ajv/dist/jtd.js' +import csafAjv from '../../lib/shared/csafAjv.js' + +const jtdAjv = new Ajv() + +const inputSchema = /** @type {const} */ ({ + additionalProperties: true, + optionalProperties: { + vulnerabilities: { + elements: { + additionalProperties: true, + optionalProperties: { + metrics: { + elements: { + additionalProperties: true, + optionalProperties: { + content: { + additionalProperties: true, + optionalProperties: { + cvss_v2: { + additionalProperties: true, + properties: {}, + }, + cvss_v3: { + additionalProperties: true, + properties: {}, + }, + } + }, + }, + }, + }, + }, + }, + }, + }, +}) + +const validate = jtdAjv.compile(inputSchema) + +const validate_2_0 = csafAjv.compile({ + $ref: 'https://www.first.org/cvss/cvss-v2.0.json', +}) + +const validate_3 = csafAjv.compile({ + oneOf: [ + { + $ref: 'https://www.first.org/cvss/cvss-v3.0.json', + }, + { + $ref: 'https://www.first.org/cvss/cvss-v3.1.json', + }, + ], +}) + +const validate_4_0 = csafAjv.compile({ + $ref: 'https://www.first.org/cvss/cvss-v4.0.json', +}) + +/** + * @param {any} doc + */ +export default function mandatoryTest_6_1_8(doc) { + const ctx = { + errors: + /** @type {Array<{ instancePath: string; message: string }>} */ ([]), + isValid: true, + } + + if (!validate(doc)) { + return ctx + } + + for (const [ + vulnerabilityIndex, + vulnerability, + ] of doc.vulnerabilities?.entries() ?? []) { + for (const [metricIndex, metric] of vulnerability.metrics?.entries() ?? []) { + if (metric?.content?.cvss_v2) { + const valid = validate_2_0(metric?.content.cvss_v2) + if (!valid) { + ctx.isValid = false + for (const err of validate_2_0.errors ?? []) { + ctx.errors.push({ + instancePath: `/vulnerabilities/${vulnerabilityIndex}/metrics/${metricIndex}/content/cvss_v2${err.instancePath}`, + message: err.message ?? '', + }) + } + } + } + if (metric?.content?.cvss_v3) { + const valid = validate_3(metric?.content?.cvss_v3) + if (!valid) { + ctx.isValid = false + for (const err of validate_3.errors ?? []) { + ctx.errors.push({ + instancePath: `/vulnerabilities/${vulnerabilityIndex}/metrics/${metricIndex}/content/cvss_v3${err.instancePath}`, + message: err.message ?? '', + }) + } + } + } + if (metric?.content?.cvss_v4) { + const valid = validate_4_0(metric?.content?.cvss_v4) + if (!valid) { + ctx.isValid = false + for (const err of validate_4_0.errors ?? []) { + ctx.errors.push({ + instancePath: `/vulnerabilities/${vulnerabilityIndex}/metrics/${metricIndex}/content/cvss_v4${err.instancePath}`, + message: err.message ?? '', + }) + } + } + } + } + } + + return ctx +} diff --git a/csaf_2_1/tests/mandatoryTest_6_1_8.js b/csaf_2_1/tests/mandatoryTest_6_1_8.js new file mode 100644 index 00000000..38a535fc --- /dev/null +++ b/csaf_2_1/tests/mandatoryTest_6_1_8.js @@ -0,0 +1,20 @@ +import { expect } from 'chai' +import mandatoryTest_6_1_8 from '../mandatoryTests/mandatoryTest_6_1_8.js' +import readExampleFiles from '../../tests/shared/readExampleFiles.js' + +const failingExamples = await readExampleFiles( + new URL('mandatoryTest_6_1_8/failing', import.meta.url) +) + +describe('Mandatory test 6.1.8', function () { + describe('failing examples', function () { + for (const [title, failingExample] of failingExamples) { + it(title, async function () { + const result = mandatoryTest_6_1_8(failingExample) + + expect(result.isValid).to.be.false + expect(result.errors).to.have.length.greaterThan(0) + }) + } + }) +}) diff --git a/csaf_2_1/tests/mandatoryTest_6_1_8/failing/oasis_csaf_tc-csaf_2_0-2021-6-1-08-01.json b/csaf_2_1/tests/mandatoryTest_6_1_8/failing/oasis_csaf_tc-csaf_2_0-2021-6-1-08-01.json new file mode 100644 index 00000000..2c150368 --- /dev/null +++ b/csaf_2_1/tests/mandatoryTest_6_1_8/failing/oasis_csaf_tc-csaf_2_0-2021-6-1-08-01.json @@ -0,0 +1,58 @@ +{ + "$schema": "https://docs.oasis-open.org/csaf/csaf/v2.1/csaf_json_schema.json", + "document": { + "category": "csaf_base", + "csaf_version": "2.1", + "distribution": { + "tlp": { + "label": "CLEAR" + } + }, + "publisher": { + "category": "other", + "name": "OASIS CSAF TC", + "namespace": "https://csaf.io" + }, + "title": "Mandatory test: Invalid CVSS (failing example 1)", + "tracking": { + "current_release_date": "2021-07-21T10:00:00.000Z", + "id": "OASIS_CSAF_TC-CSAF_2.0-2021-6-1-08-01", + "initial_release_date": "2021-07-21T10:00:00.000Z", + "revision_history": [ + { + "date": "2021-07-21T10:00:00.000Z", + "number": "1", + "summary": "Initial version." + } + ], + "status": "final", + "version": "1" + } + }, + "product_tree": { + "full_product_names": [ + { + "product_id": "CSAFPID-9080700", + "name": "Product A" + } + ] + }, + "vulnerabilities": [ + { + "metrics": [ + { + "content": { + "cvss_v3": { + "version": "3.1", + "vectorString": "CVSS:3.1/AV:L/AC:L/PR:H/UI:R/S:U/C:H/I:H/A:H", + "baseScore": 6.5 + } + }, + "products": [ + "CSAFPID-9080700" + ] + } + ] + } + ] +} \ No newline at end of file From 3bdd1f8a4710aca238378633de53f89b4946063b Mon Sep 17 00:00:00 2001 From: rschneider <97682836+rainer-exxcellent@users.noreply.github.com> Date: Thu, 6 Mar 2025 08:13:37 +0100 Subject: [PATCH 18/25] feat(CSAF2.1): #197 6.1.8. rebase and remove old test which is now in the oasis tests --- .../mandatoryTests/mandatoryTest_6_1_8.js | 9 ++- csaf_2_1/tests/mandatoryTest_6_1_8.js | 20 ------- ...oasis_csaf_tc-csaf_2_0-2021-6-1-08-01.json | 58 ------------------- tests/csaf_2_1/oasis.js | 1 - 4 files changed, 7 insertions(+), 81 deletions(-) delete mode 100644 csaf_2_1/tests/mandatoryTest_6_1_8.js delete mode 100644 csaf_2_1/tests/mandatoryTest_6_1_8/failing/oasis_csaf_tc-csaf_2_0-2021-6-1-08-01.json diff --git a/csaf_2_1/mandatoryTests/mandatoryTest_6_1_8.js b/csaf_2_1/mandatoryTests/mandatoryTest_6_1_8.js index 20a02991..60c45c85 100644 --- a/csaf_2_1/mandatoryTests/mandatoryTest_6_1_8.js +++ b/csaf_2_1/mandatoryTests/mandatoryTest_6_1_8.js @@ -25,7 +25,11 @@ const inputSchema = /** @type {const} */ ({ additionalProperties: true, properties: {}, }, - } + cvss_v4: { + additionalProperties: true, + properties: {}, + }, + }, }, }, }, @@ -75,7 +79,8 @@ export default function mandatoryTest_6_1_8(doc) { vulnerabilityIndex, vulnerability, ] of doc.vulnerabilities?.entries() ?? []) { - for (const [metricIndex, metric] of vulnerability.metrics?.entries() ?? []) { + for (const [metricIndex, metric] of vulnerability.metrics?.entries() ?? + []) { if (metric?.content?.cvss_v2) { const valid = validate_2_0(metric?.content.cvss_v2) if (!valid) { diff --git a/csaf_2_1/tests/mandatoryTest_6_1_8.js b/csaf_2_1/tests/mandatoryTest_6_1_8.js deleted file mode 100644 index 38a535fc..00000000 --- a/csaf_2_1/tests/mandatoryTest_6_1_8.js +++ /dev/null @@ -1,20 +0,0 @@ -import { expect } from 'chai' -import mandatoryTest_6_1_8 from '../mandatoryTests/mandatoryTest_6_1_8.js' -import readExampleFiles from '../../tests/shared/readExampleFiles.js' - -const failingExamples = await readExampleFiles( - new URL('mandatoryTest_6_1_8/failing', import.meta.url) -) - -describe('Mandatory test 6.1.8', function () { - describe('failing examples', function () { - for (const [title, failingExample] of failingExamples) { - it(title, async function () { - const result = mandatoryTest_6_1_8(failingExample) - - expect(result.isValid).to.be.false - expect(result.errors).to.have.length.greaterThan(0) - }) - } - }) -}) diff --git a/csaf_2_1/tests/mandatoryTest_6_1_8/failing/oasis_csaf_tc-csaf_2_0-2021-6-1-08-01.json b/csaf_2_1/tests/mandatoryTest_6_1_8/failing/oasis_csaf_tc-csaf_2_0-2021-6-1-08-01.json deleted file mode 100644 index 2c150368..00000000 --- a/csaf_2_1/tests/mandatoryTest_6_1_8/failing/oasis_csaf_tc-csaf_2_0-2021-6-1-08-01.json +++ /dev/null @@ -1,58 +0,0 @@ -{ - "$schema": "https://docs.oasis-open.org/csaf/csaf/v2.1/csaf_json_schema.json", - "document": { - "category": "csaf_base", - "csaf_version": "2.1", - "distribution": { - "tlp": { - "label": "CLEAR" - } - }, - "publisher": { - "category": "other", - "name": "OASIS CSAF TC", - "namespace": "https://csaf.io" - }, - "title": "Mandatory test: Invalid CVSS (failing example 1)", - "tracking": { - "current_release_date": "2021-07-21T10:00:00.000Z", - "id": "OASIS_CSAF_TC-CSAF_2.0-2021-6-1-08-01", - "initial_release_date": "2021-07-21T10:00:00.000Z", - "revision_history": [ - { - "date": "2021-07-21T10:00:00.000Z", - "number": "1", - "summary": "Initial version." - } - ], - "status": "final", - "version": "1" - } - }, - "product_tree": { - "full_product_names": [ - { - "product_id": "CSAFPID-9080700", - "name": "Product A" - } - ] - }, - "vulnerabilities": [ - { - "metrics": [ - { - "content": { - "cvss_v3": { - "version": "3.1", - "vectorString": "CVSS:3.1/AV:L/AC:L/PR:H/UI:R/S:U/C:H/I:H/A:H", - "baseScore": 6.5 - } - }, - "products": [ - "CSAFPID-9080700" - ] - } - ] - } - ] -} \ No newline at end of file diff --git a/tests/csaf_2_1/oasis.js b/tests/csaf_2_1/oasis.js index c5c791fa..58d207df 100644 --- a/tests/csaf_2_1/oasis.js +++ b/tests/csaf_2_1/oasis.js @@ -11,7 +11,6 @@ import * as mandatory from '../../csaf_2_1/mandatoryTests.js' */ const excluded = [ '6.1.7', - '6.1.8', '6.1.9', '6.1.10', '6.1.11', From 03a563edc44753a397bb0650f8997dfc758fb411 Mon Sep 17 00:00:00 2001 From: rschneider <97682836+rainer-exxcellent@users.noreply.github.com> Date: Fri, 14 Mar 2025 07:11:56 +0100 Subject: [PATCH 19/25] feat(CSAF2.1): #197 rebase mandatory test 6.1.8 to 196-csaf-2.1, Import adjusted to other tests --- csaf_2_1/mandatoryTests.js | 8 ++++---- csaf_2_1/mandatoryTests/mandatoryTest_6_1_8.js | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/csaf_2_1/mandatoryTests.js b/csaf_2_1/mandatoryTests.js index 4a0d79b0..7c666f8d 100644 --- a/csaf_2_1/mandatoryTests.js +++ b/csaf_2_1/mandatoryTests.js @@ -1,5 +1,4 @@ export { - mandatoryTest_6_1_1, mandatoryTest_6_1_2, mandatoryTest_6_1_3, mandatoryTest_6_1_4, @@ -42,6 +41,7 @@ export { mandatoryTest_6_1_32, mandatoryTest_6_1_33, } from '../mandatoryTests.js' -export {mandatoryTest_6_1_1} from './mandatoryTests/mandatoryTest_6_1_1.js' -export {mandatoryTest_6_1_34} from './mandatoryTests/mandatoryTest_6_1_34.js' -export {mandatoryTest_6_1_35} from './mandatoryTests/mandatoryTest_6_1_35.js' +export { mandatoryTest_6_1_1 } from './mandatoryTests/mandatoryTest_6_1_1.js' +export { mandatoryTest_6_1_8 } from './mandatoryTests/mandatoryTest_6_1_8.js' +export { mandatoryTest_6_1_34 } from './mandatoryTests/mandatoryTest_6_1_34.js' +export { mandatoryTest_6_1_35 } from './mandatoryTests/mandatoryTest_6_1_35.js' diff --git a/csaf_2_1/mandatoryTests/mandatoryTest_6_1_8.js b/csaf_2_1/mandatoryTests/mandatoryTest_6_1_8.js index 60c45c85..5b70e62b 100644 --- a/csaf_2_1/mandatoryTests/mandatoryTest_6_1_8.js +++ b/csaf_2_1/mandatoryTests/mandatoryTest_6_1_8.js @@ -64,7 +64,7 @@ const validate_4_0 = csafAjv.compile({ /** * @param {any} doc */ -export default function mandatoryTest_6_1_8(doc) { +export function mandatoryTest_6_1_8(doc) { const ctx = { errors: /** @type {Array<{ instancePath: string; message: string }>} */ ([]), From a33740243c684e072aafefd9dd8d7c359440f1d4 Mon Sep 17 00:00:00 2001 From: rschneider <97682836+rainer-exxcellent@users.noreply.github.com> Date: Fri, 14 Mar 2025 13:20:39 +0100 Subject: [PATCH 20/25] feat(CSAF2.1): #196 disable new CSAF 2.1. Tests --- tests/csaf_2_1/oasis.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/csaf_2_1/oasis.js b/tests/csaf_2_1/oasis.js index 58d207df..44d46ce4 100644 --- a/tests/csaf_2_1/oasis.js +++ b/tests/csaf_2_1/oasis.js @@ -24,6 +24,13 @@ const excluded = [ '6.1.40', '6.1.41', '6.1.42', + '6.1.43', + '6.1.44', + '6.1.45', + '6.1.46', + '6.1.47', + '6.1.48', + '6.1.49', '6.2.6', '6.2.11', '6.2.19', @@ -39,10 +46,14 @@ const excluded = [ '6.2.30', '6.2.31', '6.2.32', + '6.2.33', + '6.2.34', + '6.2.35', '6.3.1', '6.3.2', '6.3.4', '6.3.12', + '6.3.13', ] /** @typedef {import('../../lib/shared/types.js').DocumentTest} DocumentTest */ From 67e6ab2cacd540131e77c44686a5030d5fcdd292 Mon Sep 17 00:00:00 2001 From: Benjamin Date: Fri, 28 Mar 2025 09:26:49 +0100 Subject: [PATCH 21/25] feat: add optional test 6.2.19 --- csaf_2_1/optionalTests.js | 2 +- csaf_2_1/optionalTests/optionalTest_6_2_19.js | 273 ++++++++++++++++++ tests/csaf_2_1/oasis.js | 1 - 3 files changed, 274 insertions(+), 2 deletions(-) create mode 100644 csaf_2_1/optionalTests/optionalTest_6_2_19.js diff --git a/csaf_2_1/optionalTests.js b/csaf_2_1/optionalTests.js index ba67ec52..690e0d26 100644 --- a/csaf_2_1/optionalTests.js +++ b/csaf_2_1/optionalTests.js @@ -17,6 +17,6 @@ export { optionalTest_6_2_16, optionalTest_6_2_17, optionalTest_6_2_18, - optionalTest_6_2_19, optionalTest_6_2_20, } from '../optionalTests.js' +export { optionalTest_6_2_19 } from './optionalTests/optionalTest_6_2_19.js' diff --git a/csaf_2_1/optionalTests/optionalTest_6_2_19.js b/csaf_2_1/optionalTests/optionalTest_6_2_19.js new file mode 100644 index 00000000..5ed20e9c --- /dev/null +++ b/csaf_2_1/optionalTests/optionalTest_6_2_19.js @@ -0,0 +1,273 @@ +import Ajv from 'ajv/dist/jtd.js' +import { cvss30, cvss31 } from '../../lib/shared/first.js' +import * as cvss2 from '../../lib/shared/cvss2.js' +import * as cvss3 from '../../lib/shared/cvss3.js' + +const ajv = new Ajv() + +const inputSchema = /** @type {const} */ ({ + additionalProperties: true, + properties: { + vulnerabilities: { + elements: { + additionalProperties: true, + optionalProperties: { + product_status: { + additionalProperties: true, + optionalProperties: { + fixed: { + elements: { type: 'string' }, + }, + first_fixed: { + elements: { type: 'string' }, + }, + }, + }, + metrics: { + elements: { + additionalProperties: true, + optionalProperties: { + content: { + additionalProperties: true, + optionalProperties: { + cvss_v4: { + additionalProperties: true, + optionalProperties: { + environmentalScore: { type: 'float64' }, + vectorString: { type: 'string' }, + version: { type: 'string' }, + }, + }, + cvss_v3: { + additionalProperties: true, + optionalProperties: { + environmentalScore: { type: 'float64' }, + vectorString: { type: 'string' }, + version: { type: 'string' }, + }, + }, + cvss_v2: { + additionalProperties: true, + optionalProperties: { + environmentalScore: { type: 'float64' }, + vectorString: { type: 'string' }, + version: { type: 'string' }, + }, + }, + } + }, + products: { + elements: { type: 'string' }, + }, + }, + }, + }, + }, + }, + }, + }, +}) +const validateInput = ajv.compile(inputSchema) + +/** + * @param {any} doc + */ +export function optionalTest_6_2_19(doc) { + const ctx = { + warnings: + /** @type {Array<{ instancePath: string; message: string }>} */ ([]), + } + + if (!validateInput(doc)) { + return ctx + } + + doc.vulnerabilities.forEach((vulnerability, vulnerabilityIndex) => { + const fixedProductIDs = new Set([ + ...(vulnerability.product_status?.first_fixed ?? []), + ...(vulnerability.product_status?.fixed ?? []), + ]) + for (const productID of fixedProductIDs) { + vulnerability.metrics?.forEach((metric, metricIndex) => { + if (!metric.products?.includes(productID)) return + const content = metric.content; + if (content !== undefined){ + if (content.cvss_v4){ + checkCVSS(content, 'cvss_v4', ctx, vulnerabilityIndex, metricIndex, productID) + } + if (content.cvss_v3){ + checkCVSS(content, 'cvss_v3', ctx, vulnerabilityIndex, metricIndex, productID) + } + if (content.cvss_v2){ + checkCVSS(content, 'cvss_v2', ctx, vulnerabilityIndex, metricIndex, productID) + } + } + }) + } + }) + + return ctx +} + +/** + * @param {{} & { cvss_v4?: ({} & { enviralScore?: number | undefined; vectorString?: string | undefined; version?: string | undefined; } & Record) | undefined; cvss_v3?: ({} & { environmentalScore?: number | undefined; vectorString?: string | undefined; version?: string | undefined; } & Record) | undefined; cvss_v2?: ({} & { environmentalScore?: number | undefined; vectorString?: string | undefined; version?: string | undefined; } & Record) | undefined; } & Record} content + * @param {string} type + * @param {{warnings: any;}} ctx + * @param {number} vulnerabilityIndex + * @param {number} metricIndex + * @param {string} productID + */ +function checkCVSS(content, type, ctx, vulnerabilityIndex, metricIndex, productID) { + if (!content || !content[type]) return; + const cvss = /** @type {{ environmentalScore?: number; vectorString?: string; version?: string }} */ (content[type]); + const calculatedValue = cvss.version === '3.1' || cvss.version === '3.0' || cvss.version === '2.0' + ? calculateEnvironmentalScoreFromMetrics({ + version: cvss.version, + vectorString: cvss.vectorString ?? '', + metrics: cvss, + }) + : null; + if ( + (typeof cvss.environmentalScore === 'number' && cvss.environmentalScore > 0) || + (typeof calculatedValue === 'number' && calculatedValue > 0) || + calculatedValue === null + ) { + ctx.warnings.push({ + instancePath: `/vulnerabilities/${vulnerabilityIndex}/metrics/${metricIndex}/${type}`, + message: `environmental score should be 0 since "${productID}" is listed as fixed`, + }); + } +} + +const cvss2Mapping = + /** @type {ReadonlyArray]>} */ ( + cvss2.mapping.map((mapping) => [ + mapping[0], + mapping[1], + Object.fromEntries( + Object.entries(mapping[2]).map(([key, value]) => [key, value.id]) + ), + ]) +) + +const cvss3Mapping = cvss3.mapping + +/** + * @param {object} params + * @param {'2.0' | '3.0' | '3.1' | '4.0'} params.version + * @param {string} params.vectorString + * @param {Record} params.metrics + */ +function calculateEnvironmentalScoreFromMetrics({ + version, + vectorString, + metrics, + }) { + const vectorFromVectorString = new Map( + vectorString + .split('/') + .map((e) => { + const [key, value] = e.split(':') + return /** @type {const} */ ([key, value]) + }) + .filter(([, value]) => value) + ) + if (version === '3.1' || version === '3.0') { + const args = /** + * @type {[ + * string, + * string, + * string, + * string, + * string, + * string, + * string, + * string, + * string, + * string, + * string, + * string, + * string, + * string, + * string, + * string, + * string, + * string, + * string, + * string, + * string, + * string, + * ]} + */ ( + calculateMetricArray({ + mapping: cvss3Mapping, + metrics, + vector: vectorFromVectorString, + }).map((e) => e[1]) + ) + const metric = ( + version === '3.1' ? cvss31 : cvss30 + ).calculateCVSSFromMetrics(...args) + if (!metric.success) return null + return Number(metric.environmentalMetricScore) + } else { + const vector = Object.fromEntries( + calculateMetricArray({ + mapping: cvss2Mapping, + metrics, + vector: vectorFromVectorString, + }) + ) + const metric = safelyParseCVSSV2Vector(vector) + if (!metric.success) return null + return metric.environmentalMetricScore + } +} + +/** + * This function takes a cvss vector and a metric object and extracts all cvss + * values according to the mapping. It does this by first looking up every property + * in the `vector`. If the property doesn't exist there but in the metrics objects, + * it takes the value from the corresponding metrics object. + * + * @param {object} params + * @param {Map} params.vector + * @param {Record} params.metrics + * @param {ReadonlyArray]>} params.mapping + * @returns an array of pairs where the first element is the metric name (abbreviated) and the + * second is the value (abbreviated). If no value is found the value is `undefined`. + * The order of the array is the same as in the mapping. + */ +function calculateMetricArray({ vector, metrics, mapping }) { + return mapping.map((e) => { + const metricAbbrev = e[1] + const metricPropertyName = e[0] + /** @type {any} */ + const metricValueAbbrevMap = e[2] + /** @type {any} */ + const metricValue = metrics[metricPropertyName] + return [ + metricAbbrev, + vector.get(metricAbbrev) ?? metricValueAbbrevMap[metricValue], + ] + }) +} + +/** + * @param {string | {}} vectorString + * @returns + */ +function safelyParseCVSSV2Vector(vectorString) { + try { + return { + success: true, + environmentalMetricScore: + cvss2.getEnvironmentalScoreFromVectorString(vectorString), + } + } catch (e) { + return { + success: false, + environmentalMetricScore: -1, + } + } +} diff --git a/tests/csaf_2_1/oasis.js b/tests/csaf_2_1/oasis.js index 44d46ce4..29588abe 100644 --- a/tests/csaf_2_1/oasis.js +++ b/tests/csaf_2_1/oasis.js @@ -33,7 +33,6 @@ const excluded = [ '6.1.49', '6.2.6', '6.2.11', - '6.2.19', '6.2.21', '6.2.22', '6.2.23', From 6a4499ee4d14c9c6c6917eb378d34c6f45fbc552 Mon Sep 17 00:00:00 2001 From: Benjamin Date: Fri, 11 Apr 2025 14:16:26 +0200 Subject: [PATCH 22/25] feat: refactor after code review optional test 6.2.19 --- csaf_2_1/optionalTests/optionalTest_6_2_19.js | 436 +++++++++--------- 1 file changed, 229 insertions(+), 207 deletions(-) diff --git a/csaf_2_1/optionalTests/optionalTest_6_2_19.js b/csaf_2_1/optionalTests/optionalTest_6_2_19.js index 5ed20e9c..b254409b 100644 --- a/csaf_2_1/optionalTests/optionalTest_6_2_19.js +++ b/csaf_2_1/optionalTests/optionalTest_6_2_19.js @@ -6,66 +6,66 @@ import * as cvss3 from '../../lib/shared/cvss3.js' const ajv = new Ajv() const inputSchema = /** @type {const} */ ({ - additionalProperties: true, - properties: { - vulnerabilities: { + additionalProperties: true, + properties: { + vulnerabilities: { + elements: { + additionalProperties: true, + optionalProperties: { + product_status: { + additionalProperties: true, + optionalProperties: { + fixed: { + elements: { type: 'string' }, + }, + first_fixed: { + elements: { type: 'string' }, + }, + }, + }, + metrics: { elements: { - additionalProperties: true, - optionalProperties: { - product_status: { - additionalProperties: true, - optionalProperties: { - fixed: { - elements: { type: 'string' }, - }, - first_fixed: { - elements: { type: 'string' }, - }, - }, + additionalProperties: true, + optionalProperties: { + content: { + additionalProperties: true, + optionalProperties: { + cvss_v4: { + additionalProperties: true, + optionalProperties: { + environmentalScore: { type: 'float64' }, + vectorString: { type: 'string' }, + version: { type: 'string' }, + }, + }, + cvss_v3: { + additionalProperties: true, + optionalProperties: { + environmentalScore: { type: 'float64' }, + vectorString: { type: 'string' }, + version: { type: 'string' }, + }, }, - metrics: { - elements: { - additionalProperties: true, - optionalProperties: { - content: { - additionalProperties: true, - optionalProperties: { - cvss_v4: { - additionalProperties: true, - optionalProperties: { - environmentalScore: { type: 'float64' }, - vectorString: { type: 'string' }, - version: { type: 'string' }, - }, - }, - cvss_v3: { - additionalProperties: true, - optionalProperties: { - environmentalScore: { type: 'float64' }, - vectorString: { type: 'string' }, - version: { type: 'string' }, - }, - }, - cvss_v2: { - additionalProperties: true, - optionalProperties: { - environmentalScore: { type: 'float64' }, - vectorString: { type: 'string' }, - version: { type: 'string' }, - }, - }, - } - }, - products: { - elements: { type: 'string' }, - }, - }, - }, + cvss_v2: { + additionalProperties: true, + optionalProperties: { + environmentalScore: { type: 'float64' }, + vectorString: { type: 'string' }, + version: { type: 'string' }, + }, }, + }, + }, + products: { + elements: { type: 'string' }, }, + }, }, + }, }, + }, }, + }, }) const validateInput = ajv.compile(inputSchema) @@ -73,85 +73,61 @@ const validateInput = ajv.compile(inputSchema) * @param {any} doc */ export function optionalTest_6_2_19(doc) { - const ctx = { - warnings: - /** @type {Array<{ instancePath: string; message: string }>} */ ([]), - } + const ctx = { + warnings: + /** @type {Array<{ instancePath: string; message: string }>} */ ([]), + } - if (!validateInput(doc)) { - return ctx - } + if (!validateInput(doc)) { + return ctx + } - doc.vulnerabilities.forEach((vulnerability, vulnerabilityIndex) => { - const fixedProductIDs = new Set([ - ...(vulnerability.product_status?.first_fixed ?? []), - ...(vulnerability.product_status?.fixed ?? []), - ]) - for (const productID of fixedProductIDs) { - vulnerability.metrics?.forEach((metric, metricIndex) => { - if (!metric.products?.includes(productID)) return - const content = metric.content; - if (content !== undefined){ - if (content.cvss_v4){ - checkCVSS(content, 'cvss_v4', ctx, vulnerabilityIndex, metricIndex, productID) - } - if (content.cvss_v3){ - checkCVSS(content, 'cvss_v3', ctx, vulnerabilityIndex, metricIndex, productID) - } - if (content.cvss_v2){ - checkCVSS(content, 'cvss_v2', ctx, vulnerabilityIndex, metricIndex, productID) - } - } - }) + doc.vulnerabilities.forEach((vulnerability, vulnerabilityIndex) => { + const fixedProductIDs = new Set([ + ...(vulnerability.product_status?.first_fixed ?? []), + ...(vulnerability.product_status?.fixed ?? []), + ]) + for (const productID of fixedProductIDs) { + vulnerability.metrics?.forEach((metric, metricIndex) => { + if (!metric.products?.includes(productID)) return + const content = metric.content + if (content !== undefined) { + const cvssTypes = ['cvss_v4', 'cvss_v3', 'cvss_v2'] + cvssTypes.forEach((cvssType) => { + if (content[cvssType] && checkCVSS(content[cvssType])) { + ctx.warnings.push({ + instancePath: `/vulnerabilities/${vulnerabilityIndex}/metrics/${metricIndex}/${cvssType}`, + message: `environmental score should be 0 since "${productID}" is listed as fixed`, + }) + } + }) } - }) + }) + } + }) - return ctx + return ctx } /** - * @param {{} & { cvss_v4?: ({} & { enviralScore?: number | undefined; vectorString?: string | undefined; version?: string | undefined; } & Record) | undefined; cvss_v3?: ({} & { environmentalScore?: number | undefined; vectorString?: string | undefined; version?: string | undefined; } & Record) | undefined; cvss_v2?: ({} & { environmentalScore?: number | undefined; vectorString?: string | undefined; version?: string | undefined; } & Record) | undefined; } & Record} content - * @param {string} type - * @param {{warnings: any;}} ctx - * @param {number} vulnerabilityIndex - * @param {number} metricIndex - * @param {string} productID + * Check if the cvss object has a valid environmental score. + * @param {any} cvss + * @returns {boolean} */ -function checkCVSS(content, type, ctx, vulnerabilityIndex, metricIndex, productID) { - if (!content || !content[type]) return; - const cvss = /** @type {{ environmentalScore?: number; vectorString?: string; version?: string }} */ (content[type]); - const calculatedValue = cvss.version === '3.1' || cvss.version === '3.0' || cvss.version === '2.0' - ? calculateEnvironmentalScoreFromMetrics({ - version: cvss.version, - vectorString: cvss.vectorString ?? '', - metrics: cvss, - }) - : null; - if ( - (typeof cvss.environmentalScore === 'number' && cvss.environmentalScore > 0) || - (typeof calculatedValue === 'number' && calculatedValue > 0) || - calculatedValue === null - ) { - ctx.warnings.push({ - instancePath: `/vulnerabilities/${vulnerabilityIndex}/metrics/${metricIndex}/${type}`, - message: `environmental score should be 0 since "${productID}" is listed as fixed`, - }); - } +function checkCVSS(cvss) { + if (!cvss) return false + const calculatedValue = calculateEnvironmentalScoreFromMetrics({ + version: cvss.version, + vectorString: cvss.vectorString ?? '', + metrics: cvss, + }) + return ( + (typeof cvss.environmentalScore === 'number' && cvss.environmentalScore > 0) || + (typeof calculatedValue === 'number' && calculatedValue > 0) || + calculatedValue === null + ) } -const cvss2Mapping = - /** @type {ReadonlyArray]>} */ ( - cvss2.mapping.map((mapping) => [ - mapping[0], - mapping[1], - Object.fromEntries( - Object.entries(mapping[2]).map(([key, value]) => [key, value.id]) - ), - ]) -) - -const cvss3Mapping = cvss3.mapping - /** * @param {object} params * @param {'2.0' | '3.0' | '3.1' | '4.0'} params.version @@ -159,71 +135,117 @@ const cvss3Mapping = cvss3.mapping * @param {Record} params.metrics */ function calculateEnvironmentalScoreFromMetrics({ - version, - vectorString, - metrics, - }) { - const vectorFromVectorString = new Map( - vectorString - .split('/') - .map((e) => { - const [key, value] = e.split(':') - return /** @type {const} */ ([key, value]) - }) - .filter(([, value]) => value) + version, + vectorString, + metrics, +}) { + const vectorFromVectorString = new Map( + vectorString + .split('/') + .map((e) => { + const [key, value] = e.split(':') + return /** @type {const} */ ([key, value]) + }) + .filter(([, value]) => value) + ) + + if (version === '4.0') { + // Placeholder for version 4.0 logic + return null // Adjust this when the logic for version 4.0 is implemented + } else if (version === '3.1' || version === '3.0') { + return calculateMetricScoreForCVSS3( + vectorFromVectorString, + metrics, + version ) - if (version === '3.1' || version === '3.0') { - const args = /** - * @type {[ - * string, - * string, - * string, - * string, - * string, - * string, - * string, - * string, - * string, - * string, - * string, - * string, - * string, - * string, - * string, - * string, - * string, - * string, - * string, - * string, - * string, - * string, - * ]} - */ ( - calculateMetricArray({ - mapping: cvss3Mapping, - metrics, - vector: vectorFromVectorString, - }).map((e) => e[1]) - ) - const metric = ( - version === '3.1' ? cvss31 : cvss30 - ).calculateCVSSFromMetrics(...args) - if (!metric.success) return null - return Number(metric.environmentalMetricScore) - } else { - const vector = Object.fromEntries( - calculateMetricArray({ - mapping: cvss2Mapping, - metrics, - vector: vectorFromVectorString, - }) - ) - const metric = safelyParseCVSSV2Vector(vector) - if (!metric.success) return null - return metric.environmentalMetricScore - } + } else { + return calculateMetricScoreForCVSS2(vectorFromVectorString, metrics) + } +} + +/** + * This function takes a cvss vector and a metric object and extracts all cvss + * @param {Map} vectorFromVectorString + * @param {Record} metrics + * @param {'3.0' | '3.1'} version + * @returns {number|null} + */ +function calculateMetricScoreForCVSS3( + vectorFromVectorString, + metrics, + version +) { + const args = /** + * @type {[ + * string, + * string, + * string, + * string, + * string, + * string, + * string, + * string, + * string, + * string, + * string, + * string, + * string, + * string, + * string, + * string, + * string, + * string, + * string, + * string, + * string, + * string, + * ]} + */ ( + calculateMetricArray({ + mapping: cvss3Mapping, + metrics, + vector: vectorFromVectorString, + }).map((e) => e[1]) + ) + const metric = (version === '3.1' ? cvss31 : cvss30).calculateCVSSFromMetrics( + ...args + ) + if (!metric.success) return null + return Number(metric.environmentalMetricScore) +} + +/** + * This function takes a cvss vector and a metric object and extracts all cvss + * @param {Map} vectorFromVectorString + * @param {Record} metrics + * @returns {number|*|null} + */ +function calculateMetricScoreForCVSS2(vectorFromVectorString, metrics) { + const vector = Object.fromEntries( + calculateMetricArray({ + mapping: cvss2Mapping, + metrics, + vector: vectorFromVectorString, + }) + ) + const metric = safelyParseCVSSV2Vector(vector) + if (!metric.success) return null + return metric.environmentalMetricScore } +const cvss2Mapping = + /** @type {ReadonlyArray]>} */ ( + cvss2.mapping.map((mapping) => [ + mapping[0], + mapping[1], + Object.fromEntries( + Object.entries(mapping[2]).map(([key, value]) => [key, value.id]) + ), + ]) + ) + +const cvss3Mapping = cvss3.mapping + /** * This function takes a cvss vector and a metric object and extracts all cvss * values according to the mapping. It does this by first looking up every property @@ -239,18 +261,18 @@ function calculateEnvironmentalScoreFromMetrics({ * The order of the array is the same as in the mapping. */ function calculateMetricArray({ vector, metrics, mapping }) { - return mapping.map((e) => { - const metricAbbrev = e[1] - const metricPropertyName = e[0] - /** @type {any} */ - const metricValueAbbrevMap = e[2] - /** @type {any} */ - const metricValue = metrics[metricPropertyName] - return [ - metricAbbrev, - vector.get(metricAbbrev) ?? metricValueAbbrevMap[metricValue], - ] - }) + return mapping.map((e) => { + const metricAbbrev = e[1] + const metricPropertyName = e[0] + /** @type {any} */ + const metricValueAbbrevMap = e[2] + /** @type {any} */ + const metricValue = metrics[metricPropertyName] + return [ + metricAbbrev, + vector.get(metricAbbrev) ?? metricValueAbbrevMap[metricValue], + ] + }) } /** @@ -258,16 +280,16 @@ function calculateMetricArray({ vector, metrics, mapping }) { * @returns */ function safelyParseCVSSV2Vector(vectorString) { - try { - return { - success: true, - environmentalMetricScore: - cvss2.getEnvironmentalScoreFromVectorString(vectorString), - } - } catch (e) { - return { - success: false, - environmentalMetricScore: -1, - } + try { + return { + success: true, + environmentalMetricScore: + cvss2.getEnvironmentalScoreFromVectorString(vectorString), + } + } catch (e) { + return { + success: false, + environmentalMetricScore: -1, } + } } From ee7ee321e67e0f338538bf2227bf390163f85403 Mon Sep 17 00:00:00 2001 From: bendo-eXX Date: Mon, 9 Feb 2026 14:27:46 +0100 Subject: [PATCH 23/25] feat(CSAF2.1): add CVSS check vor cvss4.0 --- csaf_2_1/optionalTests/optionalTest_6_2_19.js | 295 --------------- .../recommendedTest_6_2_19.js | 343 +++++++++++++++++- tests/csaf_2_1/oasis.js | 1 - 3 files changed, 340 insertions(+), 299 deletions(-) delete mode 100644 csaf_2_1/optionalTests/optionalTest_6_2_19.js diff --git a/csaf_2_1/optionalTests/optionalTest_6_2_19.js b/csaf_2_1/optionalTests/optionalTest_6_2_19.js deleted file mode 100644 index b254409b..00000000 --- a/csaf_2_1/optionalTests/optionalTest_6_2_19.js +++ /dev/null @@ -1,295 +0,0 @@ -import Ajv from 'ajv/dist/jtd.js' -import { cvss30, cvss31 } from '../../lib/shared/first.js' -import * as cvss2 from '../../lib/shared/cvss2.js' -import * as cvss3 from '../../lib/shared/cvss3.js' - -const ajv = new Ajv() - -const inputSchema = /** @type {const} */ ({ - additionalProperties: true, - properties: { - vulnerabilities: { - elements: { - additionalProperties: true, - optionalProperties: { - product_status: { - additionalProperties: true, - optionalProperties: { - fixed: { - elements: { type: 'string' }, - }, - first_fixed: { - elements: { type: 'string' }, - }, - }, - }, - metrics: { - elements: { - additionalProperties: true, - optionalProperties: { - content: { - additionalProperties: true, - optionalProperties: { - cvss_v4: { - additionalProperties: true, - optionalProperties: { - environmentalScore: { type: 'float64' }, - vectorString: { type: 'string' }, - version: { type: 'string' }, - }, - }, - cvss_v3: { - additionalProperties: true, - optionalProperties: { - environmentalScore: { type: 'float64' }, - vectorString: { type: 'string' }, - version: { type: 'string' }, - }, - }, - cvss_v2: { - additionalProperties: true, - optionalProperties: { - environmentalScore: { type: 'float64' }, - vectorString: { type: 'string' }, - version: { type: 'string' }, - }, - }, - }, - }, - products: { - elements: { type: 'string' }, - }, - }, - }, - }, - }, - }, - }, - }, -}) -const validateInput = ajv.compile(inputSchema) - -/** - * @param {any} doc - */ -export function optionalTest_6_2_19(doc) { - const ctx = { - warnings: - /** @type {Array<{ instancePath: string; message: string }>} */ ([]), - } - - if (!validateInput(doc)) { - return ctx - } - - doc.vulnerabilities.forEach((vulnerability, vulnerabilityIndex) => { - const fixedProductIDs = new Set([ - ...(vulnerability.product_status?.first_fixed ?? []), - ...(vulnerability.product_status?.fixed ?? []), - ]) - for (const productID of fixedProductIDs) { - vulnerability.metrics?.forEach((metric, metricIndex) => { - if (!metric.products?.includes(productID)) return - const content = metric.content - if (content !== undefined) { - const cvssTypes = ['cvss_v4', 'cvss_v3', 'cvss_v2'] - cvssTypes.forEach((cvssType) => { - if (content[cvssType] && checkCVSS(content[cvssType])) { - ctx.warnings.push({ - instancePath: `/vulnerabilities/${vulnerabilityIndex}/metrics/${metricIndex}/${cvssType}`, - message: `environmental score should be 0 since "${productID}" is listed as fixed`, - }) - } - }) - } - }) - } - }) - - return ctx -} - -/** - * Check if the cvss object has a valid environmental score. - * @param {any} cvss - * @returns {boolean} - */ -function checkCVSS(cvss) { - if (!cvss) return false - const calculatedValue = calculateEnvironmentalScoreFromMetrics({ - version: cvss.version, - vectorString: cvss.vectorString ?? '', - metrics: cvss, - }) - return ( - (typeof cvss.environmentalScore === 'number' && cvss.environmentalScore > 0) || - (typeof calculatedValue === 'number' && calculatedValue > 0) || - calculatedValue === null - ) -} - -/** - * @param {object} params - * @param {'2.0' | '3.0' | '3.1' | '4.0'} params.version - * @param {string} params.vectorString - * @param {Record} params.metrics - */ -function calculateEnvironmentalScoreFromMetrics({ - version, - vectorString, - metrics, -}) { - const vectorFromVectorString = new Map( - vectorString - .split('/') - .map((e) => { - const [key, value] = e.split(':') - return /** @type {const} */ ([key, value]) - }) - .filter(([, value]) => value) - ) - - if (version === '4.0') { - // Placeholder for version 4.0 logic - return null // Adjust this when the logic for version 4.0 is implemented - } else if (version === '3.1' || version === '3.0') { - return calculateMetricScoreForCVSS3( - vectorFromVectorString, - metrics, - version - ) - } else { - return calculateMetricScoreForCVSS2(vectorFromVectorString, metrics) - } -} - -/** - * This function takes a cvss vector and a metric object and extracts all cvss - * @param {Map} vectorFromVectorString - * @param {Record} metrics - * @param {'3.0' | '3.1'} version - * @returns {number|null} - */ -function calculateMetricScoreForCVSS3( - vectorFromVectorString, - metrics, - version -) { - const args = /** - * @type {[ - * string, - * string, - * string, - * string, - * string, - * string, - * string, - * string, - * string, - * string, - * string, - * string, - * string, - * string, - * string, - * string, - * string, - * string, - * string, - * string, - * string, - * string, - * ]} - */ ( - calculateMetricArray({ - mapping: cvss3Mapping, - metrics, - vector: vectorFromVectorString, - }).map((e) => e[1]) - ) - const metric = (version === '3.1' ? cvss31 : cvss30).calculateCVSSFromMetrics( - ...args - ) - if (!metric.success) return null - return Number(metric.environmentalMetricScore) -} - -/** - * This function takes a cvss vector and a metric object and extracts all cvss - * @param {Map} vectorFromVectorString - * @param {Record} metrics - * @returns {number|*|null} - */ -function calculateMetricScoreForCVSS2(vectorFromVectorString, metrics) { - const vector = Object.fromEntries( - calculateMetricArray({ - mapping: cvss2Mapping, - metrics, - vector: vectorFromVectorString, - }) - ) - const metric = safelyParseCVSSV2Vector(vector) - if (!metric.success) return null - return metric.environmentalMetricScore -} - -const cvss2Mapping = - /** @type {ReadonlyArray]>} */ ( - cvss2.mapping.map((mapping) => [ - mapping[0], - mapping[1], - Object.fromEntries( - Object.entries(mapping[2]).map(([key, value]) => [key, value.id]) - ), - ]) - ) - -const cvss3Mapping = cvss3.mapping - -/** - * This function takes a cvss vector and a metric object and extracts all cvss - * values according to the mapping. It does this by first looking up every property - * in the `vector`. If the property doesn't exist there but in the metrics objects, - * it takes the value from the corresponding metrics object. - * - * @param {object} params - * @param {Map} params.vector - * @param {Record} params.metrics - * @param {ReadonlyArray]>} params.mapping - * @returns an array of pairs where the first element is the metric name (abbreviated) and the - * second is the value (abbreviated). If no value is found the value is `undefined`. - * The order of the array is the same as in the mapping. - */ -function calculateMetricArray({ vector, metrics, mapping }) { - return mapping.map((e) => { - const metricAbbrev = e[1] - const metricPropertyName = e[0] - /** @type {any} */ - const metricValueAbbrevMap = e[2] - /** @type {any} */ - const metricValue = metrics[metricPropertyName] - return [ - metricAbbrev, - vector.get(metricAbbrev) ?? metricValueAbbrevMap[metricValue], - ] - }) -} - -/** - * @param {string | {}} vectorString - * @returns - */ -function safelyParseCVSSV2Vector(vectorString) { - try { - return { - success: true, - environmentalMetricScore: - cvss2.getEnvironmentalScoreFromVectorString(vectorString), - } - } catch (e) { - return { - success: false, - environmentalMetricScore: -1, - } - } -} diff --git a/csaf_2_1/recommendedTests/recommendedTest_6_2_19.js b/csaf_2_1/recommendedTests/recommendedTest_6_2_19.js index 643f159f..abd38f0d 100644 --- a/csaf_2_1/recommendedTests/recommendedTest_6_2_19.js +++ b/csaf_2_1/recommendedTests/recommendedTest_6_2_19.js @@ -1,8 +1,345 @@ -import { optionalTest_6_2_19 } from '../../optionalTests.js' +import Ajv from 'ajv/dist/jtd.js' +import { cvss30, cvss31 } from '../../lib/shared/first.js' +import * as cvss2 from '../../lib/shared/cvss2.js' +import * as cvss3 from '../../lib/shared/cvss3.js' +import * as cvss4 from '../../lib/shared/cvss4.js' + +const ajv = new Ajv() + +const inputSchema = /** @type {const} */ ({ + additionalProperties: true, + properties: { + vulnerabilities: { + elements: { + additionalProperties: true, + optionalProperties: { + product_status: { + additionalProperties: true, + optionalProperties: { + fixed: { + elements: { type: 'string' }, + }, + first_fixed: { + elements: { type: 'string' }, + }, + }, + }, + metrics: { + elements: { + additionalProperties: true, + optionalProperties: { + content: { + additionalProperties: true, + optionalProperties: { + cvss_v4: { + additionalProperties: true, + optionalProperties: { + environmentalScore: { type: 'float64' }, + vectorString: { type: 'string' }, + version: { type: 'string' }, + }, + }, + cvss_v3: { + additionalProperties: true, + optionalProperties: { + environmentalScore: { type: 'float64' }, + vectorString: { type: 'string' }, + version: { type: 'string' }, + }, + }, + cvss_v2: { + additionalProperties: true, + optionalProperties: { + environmentalScore: { type: 'float64' }, + vectorString: { type: 'string' }, + version: { type: 'string' }, + }, + }, + }, + }, + products: { + elements: { type: 'string' }, + }, + }, + }, + }, + }, + }, + }, + }, +}) +const validateInput = ajv.compile(inputSchema) /** - * @param {unknown} doc + * @param {any} doc */ export function recommendedTest_6_2_19(doc) { - return optionalTest_6_2_19(doc) + const ctx = { + warnings: + /** @type {Array<{ instancePath: string; message: string }>} */ ([]), + } + + if (!validateInput(doc)) { + return ctx + } + + doc.vulnerabilities.forEach((vulnerability, vulnerabilityIndex) => { + const fixedProductIDs = new Set([ + ...(vulnerability.product_status?.first_fixed ?? []), + ...(vulnerability.product_status?.fixed ?? []), + ]) + for (const productID of fixedProductIDs) { + vulnerability.metrics?.forEach((metric, metricIndex) => { + if (!metric.products?.includes(productID)) return + const content = metric.content + if (content !== undefined) { + const cvssTypes = ['cvss_v4', 'cvss_v3', 'cvss_v2'] + cvssTypes.forEach((cvssType) => { + if (content[cvssType] && checkCVSS(content[cvssType])) { + ctx.warnings.push({ + instancePath: `/vulnerabilities/${vulnerabilityIndex}/metrics/${metricIndex}/${cvssType}`, + message: `environmental score should be 0 since "${productID}" is listed as fixed`, + }) + } + }) + } + }) + } + }) + + return ctx +} + +/** + * Check if the cvss object has a valid environmental score. + * @param {any} cvss + * @returns {boolean} + */ +function checkCVSS(cvss) { + if (!cvss) return false + const calculatedValue = calculateEnvironmentalScoreFromMetrics({ + version: cvss.version, + vectorString: cvss.vectorString ?? '', + metrics: cvss, + }) + return ( + (typeof cvss.environmentalScore === 'number' && + cvss.environmentalScore > 0) || + (typeof calculatedValue === 'number' && calculatedValue > 0) || + calculatedValue === null + ) +} + +/** + * @param {object} params + * @param {'2.0' | '3.0' | '3.1' | '4.0'} params.version + * @param {string} params.vectorString + * @param {Record} params.metrics + */ +function calculateEnvironmentalScoreFromMetrics({ + version, + vectorString, + metrics, +}) { + const vectorFromVectorString = new Map( + vectorString + .split('/') + .map((e) => { + const [key, value] = e.split(':') + return /** @type {const} */ ([key, value]) + }) + .filter(([, value]) => value) + ) + + if (version === '4.0') { + return calculateMetricScoreForCVSS4( + vectorString, + metrics, + vectorFromVectorString + ) + } else if (version === '3.1' || version === '3.0') { + return calculateMetricScoreForCVSS3( + vectorFromVectorString, + metrics, + version + ) + } else { + return calculateMetricScoreForCVSS2(vectorFromVectorString, metrics) + } +} + +/** + * @param {string} vectorString + * @param {Record} metrics + * @param {Map} vectorFromVectorString + */ +function calculateMetricScoreForCVSS4( + vectorString, + metrics, + vectorFromVectorString +) { + // Extract all metrics from the metrics object and combine with vector string + const metricArray = calculateMetricArray({ + mapping: cvss4Mapping, + metrics, + vector: vectorFromVectorString, + }) + + // Build complete vector string with all metrics including Modified ones + const completeVectorParts = metricArray + .filter(([, value]) => value !== undefined) + .map(([key, value]) => `${key}:${value}`) + + // Keep CVSS version prefix from original vector + const versionPrefix = vectorString.split('/')[0] + const completeVectorString = [versionPrefix, ...completeVectorParts].join('/') + + const calculateScoreObject = + cvss4.calculateCvss4_0_Score(completeVectorString) + const environmentalScoreObject = calculateScoreObject.find( + (scoreObject) => scoreObject.metricTypeId === 'ENVIRONMENTAL' + ) + return environmentalScoreObject?.score ?? null +} + +/** + * This function takes a cvss vector and a metric object and extracts all cvss + * @param {Map} vectorFromVectorString + * @param {Record} metrics + * @param {'3.0' | '3.1'} version + * @returns {number|null} + */ +function calculateMetricScoreForCVSS3( + vectorFromVectorString, + metrics, + version +) { + const args = /** + * @type {[ + * string, + * string, + * string, + * string, + * string, + * string, + * string, + * string, + * string, + * string, + * string, + * string, + * string, + * string, + * string, + * string, + * string, + * string, + * string, + * string, + * string, + * string, + * ]} + */ ( + calculateMetricArray({ + mapping: cvss3Mapping, + metrics, + vector: vectorFromVectorString, + }).map((e) => e[1]) + ) + const metric = (version === '3.1' ? cvss31 : cvss30).calculateCVSSFromMetrics( + ...args + ) + if (!metric.success) return null + return Number(metric.environmentalMetricScore) +} + +/** + * This function takes a cvss vector and a metric object and extracts all cvss + * @param {Map} vectorFromVectorString + * @param {Record} metrics + * @returns {number|*|null} + */ +function calculateMetricScoreForCVSS2(vectorFromVectorString, metrics) { + const vector = Object.fromEntries( + calculateMetricArray({ + mapping: cvss2Mapping, + metrics, + vector: vectorFromVectorString, + }) + ) + const metric = safelyParseCVSSV2Vector(vector) + if (!metric.success) return null + return metric.environmentalMetricScore +} + +const cvss2Mapping = + /** @type {ReadonlyArray]>} */ ( + cvss2.mapping.map((mapping) => [ + mapping[0], + mapping[1], + Object.fromEntries( + Object.entries(mapping[2]).map(([key, value]) => [key, value.id]) + ), + ]) + ) + +const cvss3Mapping = cvss3.mapping + +const cvss4Mapping = + /** @type {ReadonlyArray]>} */ ( + cvss4.flatMetrics.map((metric) => [ + metric.jsonName, + metric.metricShort, + Object.fromEntries( + metric.options.map((option) => [option.optionValue, option.optionKey]) + ), + ]) + ) + +/** + * This function takes a cvss vector and a metric object and extracts all cvss + * values according to the mapping. It does this by first looking up every property + * in the `vector`. If the property doesn't exist there but in the metrics objects, + * it takes the value from the corresponding metrics object. + * + * @param {object} params + * @param {Map} params.vector + * @param {Record} params.metrics + * @param {ReadonlyArray]>} params.mapping + * @returns an array of pairs where the first element is the metric name (abbreviated) and the + * second is the value (abbreviated). If no value is found the value is `undefined`. + * The order of the array is the same as in the mapping. + */ +function calculateMetricArray({ vector, metrics, mapping }) { + return mapping.map((e) => { + const metricAbbrev = e[1] + const metricPropertyName = e[0] + /** @type {any} */ + const metricValueAbbrevMap = e[2] + /** @type {any} */ + const metricValue = metrics[metricPropertyName] + return [ + metricAbbrev, + vector.get(metricAbbrev) ?? metricValueAbbrevMap[metricValue], + ] + }) +} + +/** + * @param {string | {}} vectorString + * @returns + */ +function safelyParseCVSSV2Vector(vectorString) { + try { + return { + success: true, + environmentalMetricScore: + cvss2.getEnvironmentalScoreFromVectorString(vectorString), + } + } catch (e) { + return { + success: false, + environmentalMetricScore: -1, + } + } } diff --git a/tests/csaf_2_1/oasis.js b/tests/csaf_2_1/oasis.js index 0218362b..7bee046e 100644 --- a/tests/csaf_2_1/oasis.js +++ b/tests/csaf_2_1/oasis.js @@ -29,7 +29,6 @@ const excluded = [ '6.1.55', '6.1.56', '6.2.11', - '6.2.19', '6.2.20', '6.2.21', '6.2.24', From bb976a5695717c7b367feb21ab134ab6fd6c8c8e Mon Sep 17 00:00:00 2001 From: bendo-eXX Date: Mon, 9 Feb 2026 14:32:31 +0100 Subject: [PATCH 24/25] feat(CSAF2.1): remove optionalTests.js --- csaf_2_1/optionalTests.js | 22 ---------------------- 1 file changed, 22 deletions(-) delete mode 100644 csaf_2_1/optionalTests.js diff --git a/csaf_2_1/optionalTests.js b/csaf_2_1/optionalTests.js deleted file mode 100644 index 690e0d26..00000000 --- a/csaf_2_1/optionalTests.js +++ /dev/null @@ -1,22 +0,0 @@ -export { - optionalTest_6_2_1, - optionalTest_6_2_2, - optionalTest_6_2_3, - optionalTest_6_2_4, - optionalTest_6_2_5, - optionalTest_6_2_6, - optionalTest_6_2_7, - optionalTest_6_2_8, - optionalTest_6_2_9, - optionalTest_6_2_10, - optionalTest_6_2_11, - optionalTest_6_2_12, - optionalTest_6_2_13, - optionalTest_6_2_14, - optionalTest_6_2_15, - optionalTest_6_2_16, - optionalTest_6_2_17, - optionalTest_6_2_18, - optionalTest_6_2_20, -} from '../optionalTests.js' -export { optionalTest_6_2_19 } from './optionalTests/optionalTest_6_2_19.js' From a9333d2992da3a1d27541789b22c5323268f9bb6 Mon Sep 17 00:00:00 2001 From: bendo-eXX Date: Mon, 9 Feb 2026 14:37:41 +0100 Subject: [PATCH 25/25] feat(CSAF2.1): update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 5753a4a1..8d476cb0 100644 --- a/README.md +++ b/README.md @@ -325,7 +325,6 @@ The following tests are not yet implemented and therefore missing: **Recommended Tests** - Recommended Test 6.2.11 -- Recommended Test 6.2.19 - Recommended Test 6.2.20 - Recommended Test 6.2.21 - Recommended Test 6.2.24 @@ -453,6 +452,7 @@ export const recommendedTest_6_2_15: DocumentTest export const recommendedTest_6_2_16: DocumentTest export const recommendedTest_6_2_17: DocumentTest export const recommendedTest_6_2_18: DocumentTest +export const recommendedTest_6_2_19: DocumentTest export const recommendedTest_6_2_22: DocumentTest export const recommendedTest_6_2_23: DocumentTest export const recommendedTest_6_2_25: DocumentTest