From d5cb05ef669b75de3c316103f4ec4d01901bf35e Mon Sep 17 00:00:00 2001 From: rschneider <97682836+rainer-exxcellent@users.noreply.github.com> Date: Mon, 17 Feb 2025 14:36:36 +0100 Subject: [PATCH 01/17] feat(CSAF2.1): #197 copy and adapt mandatory test 6.1.7 from CSAF 2.0 to CSAF 2.1 --- csaf_2_1/mandatoryTests.js | 4 + .../mandatoryTests/mandatoryTest_6_1_7.js | 100 ++++++++++++++++++ 2 files changed, 104 insertions(+) create mode 100644 csaf_2_1/mandatoryTests/mandatoryTest_6_1_7.js diff --git a/csaf_2_1/mandatoryTests.js b/csaf_2_1/mandatoryTests.js index 95c5c092..26226307 100644 --- a/csaf_2_1/mandatoryTests.js +++ b/csaf_2_1/mandatoryTests.js @@ -1,9 +1,13 @@ +export { default as mandatoryTest_6_1_7 } from './mandatoryTests/mandatoryTest_6_1_7.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_9, + mandatoryTest_6_1_10, + mandatoryTest_6_1_11, mandatoryTest_6_1_12, mandatoryTest_6_1_15, mandatoryTest_6_1_17, diff --git a/csaf_2_1/mandatoryTests/mandatoryTest_6_1_7.js b/csaf_2_1/mandatoryTests/mandatoryTest_6_1_7.js new file mode 100644 index 00000000..d6dc9f26 --- /dev/null +++ b/csaf_2_1/mandatoryTests/mandatoryTest_6_1_7.js @@ -0,0 +1,100 @@ +/** + * + * @param {unknown} doc + */ +export default function mandatoryTest_6_1_7(doc) { + /** @type {Array<{ message: string; instancePath: string }>} */ + const errors = [] + let isValid = true + + // 6.1.7 Multiple Scores with same Version per Product + if (preconditionFor_6_1_7_Matches(doc)) { + doc.vulnerabilities.forEach((vulnerability, vulnerabilityIndex) => { + /** @type {Map>} */ + const cvssVersionsByProductName = new Map() + + vulnerability.metrics?.forEach((metric, scoreIndex) => { + metric.products?.forEach((product, productIndex) => { + const versionSet = cvssVersionsByProductName.get(product) ?? new Set() + cvssVersionsByProductName.set(product, versionSet) + + if ( + (metric.content?.cvss_v2?.version !== undefined && + versionSet.has(metric.content?.cvss_v2.version)) || + (metric.content?.cvss_v3?.version !== undefined && + versionSet.has(metric.content?.cvss_v3.version))|| + (metric.content?.cvss_v4?.version !== undefined && + versionSet.has(metric.content?.cvss_v4.version)) + ) { + isValid = false + errors.push({ + message: `product is already included in these cvss-versions: ${Array.from( + versionSet.keys() + ).join(', ')}`, + instancePath: `/vulnerabilities/${vulnerabilityIndex}/metrics/${scoreIndex}/products/${productIndex}`, + }) + } + if (metric.content?.cvss_v2?.version !== undefined) { + versionSet.add(metric.content?.cvss_v2.version) + } + if (metric.content?.cvss_v3?.version !== undefined) { + versionSet.add(metric.content?.cvss_v3.version) + } + if (metric.content?.cvss_v4?.version !== undefined) { + versionSet.add(metric.content?.cvss_v4.version) + } + }) + }) + }) + } + + return { errors, isValid } +} + +/** + * @param {unknown} rawDoc + * @returns {rawDoc is { + * vulnerabilities: Array<{ + * metrics?: Array<{ + * products?: string[] + * content?: { + * cvss_v3?: { version?: string } + * cvss_v2?: { version?: string } + * cvss_v4?: { version?: string } + * } + * }> + * }> + * }} + */ +const preconditionFor_6_1_7_Matches = (rawDoc) => { + if (typeof rawDoc !== 'object' || !rawDoc) return false + /** @type {{ vulnerabilities?: unknown }} */ + const doc = rawDoc + return ( + Array.isArray(doc.vulnerabilities) && + doc.vulnerabilities.every( + (vulnerability) => + (Array.isArray(vulnerability.metrics) && + vulnerability.metrics.every( + ( + /** @type {{ products?: unknown; content?: {cvss_v2?: any; cvss_v3?: any; cvss_v4?: any }}} */ metric + ) => + Array.isArray(metric.products) && + metric.products.every((product) => typeof product === 'string') && + ((metric.content?.cvss_v2 && + (typeof metric.content?.cvss_v2.version === 'string' || + metric.content?.cvss_v2.version === undefined)) || + metric.content?.cvss_v2 === undefined) && + ((metric.content?.cvss_v3 && + (typeof metric.content?.cvss_v3.version === 'string' || + metric.content?.cvss_v3.version === undefined)) || + metric.content?.cvss_v3 === undefined) && + ((metric.content?.cvss_v4 && + (typeof metric.content?.cvss_v4.version === 'string' || + metric.content?.cvss_v4.version === undefined)) || + metric.content?.cvss_v4 === undefined) + )) || + vulnerability.metrics === undefined + ) + ) +} From dc27816247310e4d98f80736f4dca929c83c0781 Mon Sep 17 00:00:00 2001 From: rschneider <97682836+rainer-exxcellent@users.noreply.github.com> Date: Mon, 3 Mar 2025 08:21:14 +0100 Subject: [PATCH 02/17] feat(CSAF2.1): #197 replace preconditionFor_6_1_7_Matches with schema test --- .../mandatoryTests/mandatoryTest_6_1_7.js | 176 +++++++++--------- tests/csaf_2_1/mandatoryTest_6_1_7.js | 158 ++++++++++++++++ tests/csaf_2_1/shared/minimalDoc.js | 40 ++++ 3 files changed, 291 insertions(+), 83 deletions(-) create mode 100644 tests/csaf_2_1/mandatoryTest_6_1_7.js create mode 100644 tests/csaf_2_1/shared/minimalDoc.js diff --git a/csaf_2_1/mandatoryTests/mandatoryTest_6_1_7.js b/csaf_2_1/mandatoryTests/mandatoryTest_6_1_7.js index d6dc9f26..b1b2cd6a 100644 --- a/csaf_2_1/mandatoryTests/mandatoryTest_6_1_7.js +++ b/csaf_2_1/mandatoryTests/mandatoryTest_6_1_7.js @@ -1,100 +1,110 @@ +import Ajv from 'ajv/dist/jtd.js' + +const jtdAjv = new Ajv() + +const inputSchema = /** @type {const} */ ({ + additionalProperties: true, + properties: { + vulnerabilities: { + elements: { + additionalProperties: true, + properties: { + metrics: { + elements: { + additionalProperties: true, + optionalProperties: { + cvss_v2: { + additionalProperties: true, + properties: { + version: { type: 'string' }, + }, + }, + cvss_v3: { + additionalProperties: true, + properties: { + version: { type: 'string' }, + }, + }, + cvss_v4: { + additionalProperties: true, + properties: { + version: { type: 'string' }, + }, + }, + }, + }, + }, + }, + }, + }, + }, +}) + +const validate = jtdAjv.compile(inputSchema) + /** * * @param {unknown} doc */ export default function mandatoryTest_6_1_7(doc) { + const ctx = { + errors: + /** @type {Array<{ instancePath: string; message: string }>} */ ([]), + isValid: true, + } + /** @type {Array<{ message: string; instancePath: string }>} */ const errors = [] let isValid = true + if (!validate(doc)) { + return ctx + } + // 6.1.7 Multiple Scores with same Version per Product - if (preconditionFor_6_1_7_Matches(doc)) { - doc.vulnerabilities.forEach((vulnerability, vulnerabilityIndex) => { - /** @type {Map>} */ - const cvssVersionsByProductName = new Map() + /** @type {Array} */ + const vulnerabilities = doc.vulnerabilities + vulnerabilities.forEach((vulnerability, vulnerabilityIndex) => { + /** @type {Map>} */ + const cvssVersionsByProductName = new Map() - vulnerability.metrics?.forEach((metric, scoreIndex) => { - metric.products?.forEach((product, productIndex) => { - const versionSet = cvssVersionsByProductName.get(product) ?? new Set() - cvssVersionsByProductName.set(product, versionSet) + /** @type {Array} */ + const metrics = vulnerability.metrics + metrics?.forEach((metric, scoreIndex) => { + /** @type {Array} */ + const products = metric.products + products?.forEach((product, productIndex) => { + const versionSet = cvssVersionsByProductName.get(product) ?? new Set() + cvssVersionsByProductName.set(product, versionSet) - if ( - (metric.content?.cvss_v2?.version !== undefined && - versionSet.has(metric.content?.cvss_v2.version)) || - (metric.content?.cvss_v3?.version !== undefined && - versionSet.has(metric.content?.cvss_v3.version))|| - (metric.content?.cvss_v4?.version !== undefined && - versionSet.has(metric.content?.cvss_v4.version)) - ) { - isValid = false - errors.push({ - message: `product is already included in these cvss-versions: ${Array.from( - versionSet.keys() - ).join(', ')}`, - instancePath: `/vulnerabilities/${vulnerabilityIndex}/metrics/${scoreIndex}/products/${productIndex}`, - }) - } - if (metric.content?.cvss_v2?.version !== undefined) { - versionSet.add(metric.content?.cvss_v2.version) - } - if (metric.content?.cvss_v3?.version !== undefined) { - versionSet.add(metric.content?.cvss_v3.version) - } - if (metric.content?.cvss_v4?.version !== undefined) { - versionSet.add(metric.content?.cvss_v4.version) - } - }) + if ( + (metric.content?.cvss_v2?.version !== undefined && + versionSet.has(metric.content?.cvss_v2.version)) || + (metric.content?.cvss_v3?.version !== undefined && + versionSet.has(metric.content?.cvss_v3.version)) || + (metric.content?.cvss_v4?.version !== undefined && + versionSet.has(metric.content?.cvss_v4.version)) + ) { + isValid = false + errors.push({ + message: `product is already included in these cvss-versions: ${Array.from( + versionSet.keys() + ).join(', ')}`, + instancePath: `/vulnerabilities/${vulnerabilityIndex}/metrics/${scoreIndex}/products/${productIndex}`, + }) + } + if (metric.content?.cvss_v2?.version !== undefined) { + versionSet.add(metric.content?.cvss_v2.version) + } + if (metric.content?.cvss_v3?.version !== undefined) { + versionSet.add(metric.content?.cvss_v3.version) + } + if (metric.content?.cvss_v4?.version !== undefined) { + versionSet.add(metric.content?.cvss_v4.version) + } }) }) - } + }) return { errors, isValid } } - -/** - * @param {unknown} rawDoc - * @returns {rawDoc is { - * vulnerabilities: Array<{ - * metrics?: Array<{ - * products?: string[] - * content?: { - * cvss_v3?: { version?: string } - * cvss_v2?: { version?: string } - * cvss_v4?: { version?: string } - * } - * }> - * }> - * }} - */ -const preconditionFor_6_1_7_Matches = (rawDoc) => { - if (typeof rawDoc !== 'object' || !rawDoc) return false - /** @type {{ vulnerabilities?: unknown }} */ - const doc = rawDoc - return ( - Array.isArray(doc.vulnerabilities) && - doc.vulnerabilities.every( - (vulnerability) => - (Array.isArray(vulnerability.metrics) && - vulnerability.metrics.every( - ( - /** @type {{ products?: unknown; content?: {cvss_v2?: any; cvss_v3?: any; cvss_v4?: any }}} */ metric - ) => - Array.isArray(metric.products) && - metric.products.every((product) => typeof product === 'string') && - ((metric.content?.cvss_v2 && - (typeof metric.content?.cvss_v2.version === 'string' || - metric.content?.cvss_v2.version === undefined)) || - metric.content?.cvss_v2 === undefined) && - ((metric.content?.cvss_v3 && - (typeof metric.content?.cvss_v3.version === 'string' || - metric.content?.cvss_v3.version === undefined)) || - metric.content?.cvss_v3 === undefined) && - ((metric.content?.cvss_v4 && - (typeof metric.content?.cvss_v4.version === 'string' || - metric.content?.cvss_v4.version === undefined)) || - metric.content?.cvss_v4 === undefined) - )) || - vulnerability.metrics === undefined - ) - ) -} diff --git a/tests/csaf_2_1/mandatoryTest_6_1_7.js b/tests/csaf_2_1/mandatoryTest_6_1_7.js new file mode 100644 index 00000000..e2d5230f --- /dev/null +++ b/tests/csaf_2_1/mandatoryTest_6_1_7.js @@ -0,0 +1,158 @@ +import { expect } from 'chai' +import mandatoryTest_6_1_7 from '../../csaf_2_1/mandatoryTests/mandatoryTest_6_1_7.js' +import minimalDoc from './shared/minimalDoc.js' +import csaf_2_1 from '../../csaf_2_1/schemaTests/csaf_2_1.js' + +const emptyMandatoryTest6_1_7 = { + $schema: minimalDoc.$schema, + document: { + ...minimalDoc.document, + }, + product_tree: { + full_product_names: [ + { + product_id: 'CSAFPID-9080700', + name: 'Product A', + }, + ], + }, + vulnerabilities: [ + { + metrics: [ + { + content: { + cvss_v3: {}, + }, + products: ['CSAFPID-9080700'], + }, + ], + }, + { + metrics: [ + { + content: {}, + products: [], + }, + ], + }, + ], +} + +const invalidMandatoryTest6_1_7 = { + $schema: minimalDoc.$schema, + document: { + ...minimalDoc.document, + }, + product_tree: { + full_product_names: [ + { + product_id: 'CSAFPID-9080700', + name: 'Product A', + }, + ], + }, + vulnerabilities: [ + { + metrics: [ + { + content: { + cvss_v4: { + version: '4.0', + vectorString: + 'CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H', + baseScore: 10, + baseSeverity: 'CRITICAL', + }, + }, + products: ['CSAFPID-9080700'], + }, + { + content: { + cvss_v4: { + version: '4.0', + vectorString: + 'CVSS:4.0/AV:N/AC:L/AT:P/PR:L/UI:N/VC:L/VI:L/VA:N/SC:H/SI:N/SA:N', + baseScore: 4.9, + baseSeverity: 'MEDIUM', + }, + }, + products: ['CSAFPID-9080700'], + }, + ], + }, + ], +} + +const validMandatoryTest6_1_7 = { + $schema: minimalDoc.$schema, + document: { + ...minimalDoc.document, + }, + 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:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H', + baseScore: 10, + baseSeverity: 'CRITICAL', + }, + }, + products: ['CSAFPID-9080700'], + }, + ], + }, + { + 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, + baseSeverity: 'MEDIUM', + }, + }, + products: ['CSAFPID-9080700'], + }, + ], + }, + ], +} + +describe('mandatory test 6.1.7', function () { + describe('failing examples', function () { + it('test duplicate product id in cvss_v3 ', async function () { + expect(csaf_2_1(invalidMandatoryTest6_1_7).isValid).to.be.true + const result = await mandatoryTest_6_1_7(invalidMandatoryTest6_1_7) + expect(result.errors).to.have.length.greaterThan(0) + }) + }) + + describe('valid examples', function () { + it('test duplicate product id different vulnerabilities', async function () { + expect(csaf_2_1(validMandatoryTest6_1_7).isValid).to.be.true + const result = await mandatoryTest_6_1_7(validMandatoryTest6_1_7) + expect(result.errors.length).to.eq(0) + }) + it('test empty vulnerabilities', async function () { + const result = await mandatoryTest_6_1_7(emptyMandatoryTest6_1_7) + expect(result.errors.length).to.eq(0) + }) + it('test minimal doc', async function () { + expect(csaf_2_1(minimalDoc).isValid).to.be.true + const result = await mandatoryTest_6_1_7(minimalDoc) + expect(result.errors.length).to.eq(0) + }) + }) +}) diff --git a/tests/csaf_2_1/shared/minimalDoc.js b/tests/csaf_2_1/shared/minimalDoc.js new file mode 100644 index 00000000..50011d82 --- /dev/null +++ b/tests/csaf_2_1/shared/minimalDoc.js @@ -0,0 +1,40 @@ +export default { + $schema: "https://docs.oasis-open.org/csaf/csaf/v2.1/csaf_json_schema.json", + document: { + category: 'Test Report', + csaf_version: '2.1', + title: 'Minimal valid', + lang: 'en', + distribution: { + tlp: { + label: 'AMBER', + }, + }, + publisher: { + category: 'other', + name: 'Secvisogram Automated Tester', + namespace: 'https://github.com/secvisogram/secvisogram', + }, + references: [ + { + category: 'self', + summary: 'A non-canonical URL.', + url: 'https://example.com/security/data/csaf/2021/my-thing-_10.json', + }, + ], + tracking: { + current_release_date: '2021-01-14T00:00:00.000Z', + id: 'My-Thing-.10', + initial_release_date: '2021-01-14T00:00:00.000Z', + revision_history: [ + { + number: '1', + date: '2021-01-14T00:00:00.000Z', + summary: 'Summary', + }, + ], + status: 'draft', + version: '1', + }, + }, +} From 12b0caea7cd9482977de4d7a97472ceee59bf6c3 Mon Sep 17 00:00:00 2001 From: rschneider <97682836+rainer-exxcellent@users.noreply.github.com> Date: Thu, 6 Mar 2025 07:48:42 +0100 Subject: [PATCH 03/17] feat(CSAF2.1): #197 6.1.7. reformat with prettier --- tests/csaf_2_1/shared/minimalDoc.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/csaf_2_1/shared/minimalDoc.js b/tests/csaf_2_1/shared/minimalDoc.js index 50011d82..7eefd2c8 100644 --- a/tests/csaf_2_1/shared/minimalDoc.js +++ b/tests/csaf_2_1/shared/minimalDoc.js @@ -1,5 +1,5 @@ export default { - $schema: "https://docs.oasis-open.org/csaf/csaf/v2.1/csaf_json_schema.json", + $schema: 'https://docs.oasis-open.org/csaf/csaf/v2.1/csaf_json_schema.json', document: { category: 'Test Report', csaf_version: '2.1', From d8fb113b6cdb24feb3ced8bbef8253d856fcfc92 Mon Sep 17 00:00:00 2001 From: rschneider <97682836+rainer-exxcellent@users.noreply.github.com> Date: Fri, 14 Mar 2025 11:01:24 +0100 Subject: [PATCH 04/17] feat(CSAF2.1): #197 mandatory test 6.1.7 - add content to schema in validation, changed scoreIndex to metricIndex, removed unnecessary tests, rebase --- csaf_2_1/mandatoryTests.js | 2 +- .../mandatoryTests/mandatoryTest_6_1_7.js | 46 +++++--- tests/csaf_2_1/mandatoryTest_6_1_7.js | 110 +----------------- 3 files changed, 33 insertions(+), 125 deletions(-) diff --git a/csaf_2_1/mandatoryTests.js b/csaf_2_1/mandatoryTests.js index 26226307..0bd170c1 100644 --- a/csaf_2_1/mandatoryTests.js +++ b/csaf_2_1/mandatoryTests.js @@ -1,4 +1,3 @@ -export { default as mandatoryTest_6_1_7 } from './mandatoryTests/mandatoryTest_6_1_7.js' export { mandatoryTest_6_1_2, mandatoryTest_6_1_3, @@ -39,6 +38,7 @@ export { mandatoryTest_6_1_33, } from '../mandatoryTests.js' export { mandatoryTest_6_1_1 } from './mandatoryTests/mandatoryTest_6_1_1.js' +export { mandatoryTest_6_1_7 } from './mandatoryTests/mandatoryTest_6_1_7.js' export { mandatoryTest_6_1_8 } from './mandatoryTests/mandatoryTest_6_1_8.js' export { mandatoryTest_6_1_11 } from './mandatoryTests/mandatoryTest_6_1_11.js' export { mandatoryTest_6_1_13 } from './mandatoryTests/mandatoryTest_6_1_13.js' diff --git a/csaf_2_1/mandatoryTests/mandatoryTest_6_1_7.js b/csaf_2_1/mandatoryTests/mandatoryTest_6_1_7.js index b1b2cd6a..1cff6763 100644 --- a/csaf_2_1/mandatoryTests/mandatoryTest_6_1_7.js +++ b/csaf_2_1/mandatoryTests/mandatoryTest_6_1_7.js @@ -12,23 +12,33 @@ const inputSchema = /** @type {const} */ ({ metrics: { elements: { additionalProperties: true, - optionalProperties: { - cvss_v2: { - additionalProperties: true, - properties: { - version: { type: 'string' }, - }, - }, - cvss_v3: { - additionalProperties: true, - properties: { - version: { type: 'string' }, - }, + properties: { + products: { + elements: {}, }, - cvss_v4: { + }, + optionalProperties: { + content: { additionalProperties: true, - properties: { - version: { type: 'string' }, + optionalProperties: { + cvss_v2: { + additionalProperties: true, + properties: { + version: { type: 'string' }, + }, + }, + cvss_v3: { + additionalProperties: true, + properties: { + version: { type: 'string' }, + }, + }, + cvss_v4: { + additionalProperties: true, + properties: { + version: { type: 'string' }, + }, + }, }, }, }, @@ -46,7 +56,7 @@ const validate = jtdAjv.compile(inputSchema) * * @param {unknown} doc */ -export default function mandatoryTest_6_1_7(doc) { +export function mandatoryTest_6_1_7(doc) { const ctx = { errors: /** @type {Array<{ instancePath: string; message: string }>} */ ([]), @@ -70,7 +80,7 @@ export default function mandatoryTest_6_1_7(doc) { /** @type {Array} */ const metrics = vulnerability.metrics - metrics?.forEach((metric, scoreIndex) => { + metrics?.forEach((metric, metricIndex) => { /** @type {Array} */ const products = metric.products products?.forEach((product, productIndex) => { @@ -90,7 +100,7 @@ export default function mandatoryTest_6_1_7(doc) { message: `product is already included in these cvss-versions: ${Array.from( versionSet.keys() ).join(', ')}`, - instancePath: `/vulnerabilities/${vulnerabilityIndex}/metrics/${scoreIndex}/products/${productIndex}`, + instancePath: `/vulnerabilities/${vulnerabilityIndex}/metrics/${metricIndex}/products/${productIndex}`, }) } if (metric.content?.cvss_v2?.version !== undefined) { diff --git a/tests/csaf_2_1/mandatoryTest_6_1_7.js b/tests/csaf_2_1/mandatoryTest_6_1_7.js index e2d5230f..fd8f2903 100644 --- a/tests/csaf_2_1/mandatoryTest_6_1_7.js +++ b/tests/csaf_2_1/mandatoryTest_6_1_7.js @@ -1,5 +1,5 @@ import { expect } from 'chai' -import mandatoryTest_6_1_7 from '../../csaf_2_1/mandatoryTests/mandatoryTest_6_1_7.js' +import { mandatoryTest_6_1_7} from '../../csaf_2_1/mandatoryTests/mandatoryTest_6_1_7.js' import minimalDoc from './shared/minimalDoc.js' import csaf_2_1 from '../../csaf_2_1/schemaTests/csaf_2_1.js' @@ -20,10 +20,9 @@ const emptyMandatoryTest6_1_7 = { { metrics: [ { - content: { - cvss_v3: {}, + cvss_v3: { + "version": "2.0", }, - products: ['CSAFPID-9080700'], }, ], }, @@ -38,113 +37,12 @@ const emptyMandatoryTest6_1_7 = { ], } -const invalidMandatoryTest6_1_7 = { - $schema: minimalDoc.$schema, - document: { - ...minimalDoc.document, - }, - product_tree: { - full_product_names: [ - { - product_id: 'CSAFPID-9080700', - name: 'Product A', - }, - ], - }, - vulnerabilities: [ - { - metrics: [ - { - content: { - cvss_v4: { - version: '4.0', - vectorString: - 'CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H', - baseScore: 10, - baseSeverity: 'CRITICAL', - }, - }, - products: ['CSAFPID-9080700'], - }, - { - content: { - cvss_v4: { - version: '4.0', - vectorString: - 'CVSS:4.0/AV:N/AC:L/AT:P/PR:L/UI:N/VC:L/VI:L/VA:N/SC:H/SI:N/SA:N', - baseScore: 4.9, - baseSeverity: 'MEDIUM', - }, - }, - products: ['CSAFPID-9080700'], - }, - ], - }, - ], -} -const validMandatoryTest6_1_7 = { - $schema: minimalDoc.$schema, - document: { - ...minimalDoc.document, - }, - 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:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H', - baseScore: 10, - baseSeverity: 'CRITICAL', - }, - }, - products: ['CSAFPID-9080700'], - }, - ], - }, - { - 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, - baseSeverity: 'MEDIUM', - }, - }, - products: ['CSAFPID-9080700'], - }, - ], - }, - ], -} describe('mandatory test 6.1.7', function () { - describe('failing examples', function () { - it('test duplicate product id in cvss_v3 ', async function () { - expect(csaf_2_1(invalidMandatoryTest6_1_7).isValid).to.be.true - const result = await mandatoryTest_6_1_7(invalidMandatoryTest6_1_7) - expect(result.errors).to.have.length.greaterThan(0) - }) - }) describe('valid examples', function () { - it('test duplicate product id different vulnerabilities', async function () { - expect(csaf_2_1(validMandatoryTest6_1_7).isValid).to.be.true - const result = await mandatoryTest_6_1_7(validMandatoryTest6_1_7) - expect(result.errors.length).to.eq(0) - }) + it('test empty vulnerabilities', async function () { const result = await mandatoryTest_6_1_7(emptyMandatoryTest6_1_7) expect(result.errors.length).to.eq(0) From ab454d833b577857a4fb492b0f92a7634e4da686 Mon Sep 17 00:00:00 2001 From: rschneider <97682836+rainer-exxcellent@users.noreply.github.com> Date: Fri, 25 Apr 2025 07:39:12 +0200 Subject: [PATCH 05/17] feat(CSAF2.1): #197 test 6.1.9 - take source into account --- .../mandatoryTests/mandatoryTest_6_1_7.js | 110 ++++++++++++++---- 1 file changed, 85 insertions(+), 25 deletions(-) diff --git a/csaf_2_1/mandatoryTests/mandatoryTest_6_1_7.js b/csaf_2_1/mandatoryTests/mandatoryTest_6_1_7.js index 1cff6763..bb547569 100644 --- a/csaf_2_1/mandatoryTests/mandatoryTest_6_1_7.js +++ b/csaf_2_1/mandatoryTests/mandatoryTest_6_1_7.js @@ -1,5 +1,16 @@ import Ajv from 'ajv/dist/jtd.js' +/** + * @typedef {{ cvss_v2: {version: string}, cvss_v3: {version: string}, cvss_v4: {version: string}}} MetricContent + */ + +/** + * @typedef {Object} Metric + * @property {MetricContent} content + */ + +const VERSION_SEPARATOR = '°' + const jtdAjv = new Ajv() const inputSchema = /** @type {const} */ ({ @@ -74,6 +85,66 @@ export function mandatoryTest_6_1_7(doc) { // 6.1.7 Multiple Scores with same Version per Product /** @type {Array} */ const vulnerabilities = doc.vulnerabilities + + /** + * @param {string} version + * @param {string} source + */ + function concatVersionSource(version, source) { + return version + VERSION_SEPARATOR + source + } + + /** + * + * @param {Metric} metric + * @param {string} version + * @param {string} source + * @returns {string|null} + */ + function getSameVersionInMetric(metric, version, source) { + if ( + metric.content?.cvss_v2?.version !== undefined && + version === concatVersionSource(metric.content?.cvss_v2.version, source) + ) { + return metric.content?.cvss_v2?.version + } else if ( + metric.content?.cvss_v3?.version !== undefined && + version === concatVersionSource(metric.content?.cvss_v3.version, source) + ) { + return metric.content?.cvss_v3?.version + } else if ( + metric.content?.cvss_v4?.version !== undefined && + version === concatVersionSource(metric.content?.cvss_v4.version, source) + ) { + return metric.content?.cvss_v4?.version + } else { + return null + } + } + + /** + * @param {Metric} metric + * @param {Set} versionSet + * @param {string} source + */ + function addVersionsInMetricToSet(metric, versionSet, source) { + if (metric.content?.cvss_v2?.version !== undefined) { + versionSet.add( + concatVersionSource(metric.content?.cvss_v2.version, source) + ) + } + if (metric.content?.cvss_v3?.version !== undefined) { + versionSet.add( + concatVersionSource(metric.content?.cvss_v3.version, source) + ) + } + if (metric.content?.cvss_v4?.version !== undefined) { + versionSet.add( + concatVersionSource(metric.content?.cvss_v4.version, source) + ) + } + } + vulnerabilities.forEach((vulnerability, vulnerabilityIndex) => { /** @type {Map>} */ const cvssVersionsByProductName = new Map() @@ -83,35 +154,24 @@ export function mandatoryTest_6_1_7(doc) { metrics?.forEach((metric, metricIndex) => { /** @type {Array} */ const products = metric.products + const source = metric.source ? metric.source : '' products?.forEach((product, productIndex) => { const versionSet = cvssVersionsByProductName.get(product) ?? new Set() cvssVersionsByProductName.set(product, versionSet) - if ( - (metric.content?.cvss_v2?.version !== undefined && - versionSet.has(metric.content?.cvss_v2.version)) || - (metric.content?.cvss_v3?.version !== undefined && - versionSet.has(metric.content?.cvss_v3.version)) || - (metric.content?.cvss_v4?.version !== undefined && - versionSet.has(metric.content?.cvss_v4.version)) - ) { - isValid = false - errors.push({ - message: `product is already included in these cvss-versions: ${Array.from( - versionSet.keys() - ).join(', ')}`, - instancePath: `/vulnerabilities/${vulnerabilityIndex}/metrics/${metricIndex}/products/${productIndex}`, - }) - } - if (metric.content?.cvss_v2?.version !== undefined) { - versionSet.add(metric.content?.cvss_v2.version) - } - if (metric.content?.cvss_v3?.version !== undefined) { - versionSet.add(metric.content?.cvss_v3.version) - } - if (metric.content?.cvss_v4?.version !== undefined) { - versionSet.add(metric.content?.cvss_v4.version) - } + versionSet.forEach((version) => { + const sameVersion = getSameVersionInMetric(metric, version, source) + if (sameVersion) { + isValid = false + + errors.push({ + message: `Product is member of more than one CVSS-Vectors with the same version '${sameVersion}' and same source ${source}.`, + instancePath: `/vulnerabilities/${vulnerabilityIndex}/metrics/${metricIndex}/products/${productIndex}`, + }) + } + }) + + addVersionsInMetricToSet(metric, versionSet, source) }) }) }) From 2136383dcb7afb4f69d1b72830912b09c38b3bee Mon Sep 17 00:00:00 2001 From: rschneider <97682836+rainer-exxcellent@users.noreply.github.com> Date: Thu, 8 May 2025 13:45:16 +0200 Subject: [PATCH 06/17] feat(CSAF2.1): #197 mandatory test 6.1.7 - fix tests after rebase --- csaf_2_1/mandatoryTests.js | 1 - 1 file changed, 1 deletion(-) diff --git a/csaf_2_1/mandatoryTests.js b/csaf_2_1/mandatoryTests.js index 0bd170c1..10fdcb2a 100644 --- a/csaf_2_1/mandatoryTests.js +++ b/csaf_2_1/mandatoryTests.js @@ -6,7 +6,6 @@ export { mandatoryTest_6_1_6, mandatoryTest_6_1_9, mandatoryTest_6_1_10, - mandatoryTest_6_1_11, mandatoryTest_6_1_12, mandatoryTest_6_1_15, mandatoryTest_6_1_17, From a05f7357a5d71cb5133243e9d92b7d62a74780dc Mon Sep 17 00:00:00 2001 From: rschneider <97682836+rainer-exxcellent@users.noreply.github.com> Date: Thu, 8 May 2025 14:13:52 +0200 Subject: [PATCH 07/17] feat(CSAF2.1): #197 mandatory test 6.1.7 - use JSON.stringify to hash version and source --- .../mandatoryTests/mandatoryTest_6_1_7.js | 24 +++++++------------ 1 file changed, 8 insertions(+), 16 deletions(-) diff --git a/csaf_2_1/mandatoryTests/mandatoryTest_6_1_7.js b/csaf_2_1/mandatoryTests/mandatoryTest_6_1_7.js index bb547569..f1f606ad 100644 --- a/csaf_2_1/mandatoryTests/mandatoryTest_6_1_7.js +++ b/csaf_2_1/mandatoryTests/mandatoryTest_6_1_7.js @@ -9,8 +9,6 @@ import Ajv from 'ajv/dist/jtd.js' * @property {MetricContent} content */ -const VERSION_SEPARATOR = '°' - const jtdAjv = new Ajv() const inputSchema = /** @type {const} */ ({ @@ -90,8 +88,8 @@ export function mandatoryTest_6_1_7(doc) { * @param {string} version * @param {string} source */ - function concatVersionSource(version, source) { - return version + VERSION_SEPARATOR + source + function hashVersionSource(version, source) { + return JSON.stringify({ version: version, source: source }) } /** @@ -104,17 +102,17 @@ export function mandatoryTest_6_1_7(doc) { function getSameVersionInMetric(metric, version, source) { if ( metric.content?.cvss_v2?.version !== undefined && - version === concatVersionSource(metric.content?.cvss_v2.version, source) + version === hashVersionSource(metric.content?.cvss_v2.version, source) ) { return metric.content?.cvss_v2?.version } else if ( metric.content?.cvss_v3?.version !== undefined && - version === concatVersionSource(metric.content?.cvss_v3.version, source) + version === hashVersionSource(metric.content?.cvss_v3.version, source) ) { return metric.content?.cvss_v3?.version } else if ( metric.content?.cvss_v4?.version !== undefined && - version === concatVersionSource(metric.content?.cvss_v4.version, source) + version === hashVersionSource(metric.content?.cvss_v4.version, source) ) { return metric.content?.cvss_v4?.version } else { @@ -129,19 +127,13 @@ export function mandatoryTest_6_1_7(doc) { */ function addVersionsInMetricToSet(metric, versionSet, source) { if (metric.content?.cvss_v2?.version !== undefined) { - versionSet.add( - concatVersionSource(metric.content?.cvss_v2.version, source) - ) + versionSet.add(hashVersionSource(metric.content?.cvss_v2.version, source)) } if (metric.content?.cvss_v3?.version !== undefined) { - versionSet.add( - concatVersionSource(metric.content?.cvss_v3.version, source) - ) + versionSet.add(hashVersionSource(metric.content?.cvss_v3.version, source)) } if (metric.content?.cvss_v4?.version !== undefined) { - versionSet.add( - concatVersionSource(metric.content?.cvss_v4.version, source) - ) + versionSet.add(hashVersionSource(metric.content?.cvss_v4.version, source)) } } From 23b361564060a2da4714501af26a4d3d5f1c5617 Mon Sep 17 00:00:00 2001 From: rschneider <97682836+rainer-exxcellent@users.noreply.github.com> Date: Thu, 8 May 2025 14:48:12 +0200 Subject: [PATCH 08/17] feat(CSAF2.1): #197 mandatory test 6.1.7 -run prettier on testfile --- tests/csaf_2_1/mandatoryTest_6_1_7.js | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/tests/csaf_2_1/mandatoryTest_6_1_7.js b/tests/csaf_2_1/mandatoryTest_6_1_7.js index fd8f2903..cd6dd2db 100644 --- a/tests/csaf_2_1/mandatoryTest_6_1_7.js +++ b/tests/csaf_2_1/mandatoryTest_6_1_7.js @@ -1,5 +1,5 @@ import { expect } from 'chai' -import { mandatoryTest_6_1_7} from '../../csaf_2_1/mandatoryTests/mandatoryTest_6_1_7.js' +import { mandatoryTest_6_1_7 } from '../../csaf_2_1/mandatoryTests/mandatoryTest_6_1_7.js' import minimalDoc from './shared/minimalDoc.js' import csaf_2_1 from '../../csaf_2_1/schemaTests/csaf_2_1.js' @@ -21,7 +21,7 @@ const emptyMandatoryTest6_1_7 = { metrics: [ { cvss_v3: { - "version": "2.0", + version: '2.0', }, }, ], @@ -37,12 +37,8 @@ const emptyMandatoryTest6_1_7 = { ], } - - describe('mandatory test 6.1.7', function () { - describe('valid examples', function () { - it('test empty vulnerabilities', async function () { const result = await mandatoryTest_6_1_7(emptyMandatoryTest6_1_7) expect(result.errors.length).to.eq(0) From 3301133da54e374da72ca8e69242988f5f3cdc15 Mon Sep 17 00:00:00 2001 From: rschneider <97682836+rainer-exxcellent@users.noreply.github.com> Date: Thu, 15 May 2025 07:44:44 +0200 Subject: [PATCH 09/17] feat(CSAF2.1): #197 mandatory test 6.1.7 - remove type any --- .../mandatoryTests/mandatoryTest_6_1_7.js | 33 +++++++++++++++---- 1 file changed, 27 insertions(+), 6 deletions(-) diff --git a/csaf_2_1/mandatoryTests/mandatoryTest_6_1_7.js b/csaf_2_1/mandatoryTests/mandatoryTest_6_1_7.js index f1f606ad..0531d474 100644 --- a/csaf_2_1/mandatoryTests/mandatoryTest_6_1_7.js +++ b/csaf_2_1/mandatoryTests/mandatoryTest_6_1_7.js @@ -1,12 +1,30 @@ import Ajv from 'ajv/dist/jtd.js' /** - * @typedef {{ cvss_v2: {version: string}, cvss_v3: {version: string}, cvss_v4: {version: string}}} MetricContent + * @typedef {object} MetricContent + * @property {object} [cvss_v2] + * @property {string} cvss_v2.version + * @property {object} [cvss_v3] + * @property {string} cvss_v3.version + * @property {object} [cvss_v4] + * @property {string} cvss_v4.version */ +/** + * @typedef {string} Product + * / + + /** * @typedef {Object} Metric - * @property {MetricContent} content + * @property {MetricContent} [content] + * @property {Array} [products] + * @property {string} [source] + */ + +/** + * @typedef {Object} Vulnerability + * @property {Array} metrics */ const jtdAjv = new Ajv() @@ -23,10 +41,13 @@ const inputSchema = /** @type {const} */ ({ additionalProperties: true, properties: { products: { - elements: {}, + elements: { type: 'string' }, }, }, optionalProperties: { + source: { + type: 'string', + }, content: { additionalProperties: true, optionalProperties: { @@ -81,7 +102,7 @@ export function mandatoryTest_6_1_7(doc) { } // 6.1.7 Multiple Scores with same Version per Product - /** @type {Array} */ + /** @type {Array} */ const vulnerabilities = doc.vulnerabilities /** @@ -141,10 +162,10 @@ export function mandatoryTest_6_1_7(doc) { /** @type {Map>} */ const cvssVersionsByProductName = new Map() - /** @type {Array} */ + /** @type {Array} */ const metrics = vulnerability.metrics metrics?.forEach((metric, metricIndex) => { - /** @type {Array} */ + /** @type {Array | undefined} */ const products = metric.products const source = metric.source ? metric.source : '' products?.forEach((product, productIndex) => { From 0e409ba8bb719ef007a4db303e5d432b996928c2 Mon Sep 17 00:00:00 2001 From: rschneider <97682836+rainer-exxcellent@users.noreply.github.com> Date: Fri, 6 Jun 2025 09:58:58 +0200 Subject: [PATCH 10/17] feat(CSAF2.1): #197 - test 6.1.7 - fix after rebase --- csaf_2_1/mandatoryTests.js | 2 -- tests/csaf_2_1/oasis.js | 1 - 2 files changed, 3 deletions(-) diff --git a/csaf_2_1/mandatoryTests.js b/csaf_2_1/mandatoryTests.js index 10fdcb2a..efb0241e 100644 --- a/csaf_2_1/mandatoryTests.js +++ b/csaf_2_1/mandatoryTests.js @@ -4,8 +4,6 @@ export { mandatoryTest_6_1_4, mandatoryTest_6_1_5, mandatoryTest_6_1_6, - mandatoryTest_6_1_9, - mandatoryTest_6_1_10, mandatoryTest_6_1_12, mandatoryTest_6_1_15, mandatoryTest_6_1_17, diff --git a/tests/csaf_2_1/oasis.js b/tests/csaf_2_1/oasis.js index 6fb44c13..81ab230c 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.6', - '6.1.7', '6.1.10', '6.1.14', '6.1.16', From d3c96b1b4268ee666974ba2b8ecb43227e6cc5d0 Mon Sep 17 00:00:00 2001 From: rschneider <97682836+rainer-exxcellent@users.noreply.github.com> Date: Mon, 16 Jun 2025 13:46:49 +0200 Subject: [PATCH 11/17] feat(CSAF2.1): #197 - test 6.1.7 - changed input schema and add test for it --- .../mandatoryTests/mandatoryTest_6_1_7.js | 34 +++++----- tests/csaf_2_1/mandatoryTest_6_1_7.js | 68 ++++++++++++++++++- 2 files changed, 81 insertions(+), 21 deletions(-) diff --git a/csaf_2_1/mandatoryTests/mandatoryTest_6_1_7.js b/csaf_2_1/mandatoryTests/mandatoryTest_6_1_7.js index 0531d474..e7514f82 100644 --- a/csaf_2_1/mandatoryTests/mandatoryTest_6_1_7.js +++ b/csaf_2_1/mandatoryTests/mandatoryTest_6_1_7.js @@ -3,19 +3,19 @@ import Ajv from 'ajv/dist/jtd.js' /** * @typedef {object} MetricContent * @property {object} [cvss_v2] - * @property {string} cvss_v2.version + * @property {string} [cvss_v2.version] * @property {object} [cvss_v3] - * @property {string} cvss_v3.version + * @property {string} [cvss_v3.version] * @property {object} [cvss_v4] - * @property {string} cvss_v4.version + * @property {string} [cvss_v4.version] */ /** * @typedef {string} Product - * / + * / -/** + /** * @typedef {Object} Metric * @property {MetricContent} [content] * @property {Array} [products] @@ -24,7 +24,7 @@ import Ajv from 'ajv/dist/jtd.js' /** * @typedef {Object} Vulnerability - * @property {Array} metrics + * @property {Array} [metrics] */ const jtdAjv = new Ajv() @@ -35,37 +35,35 @@ const inputSchema = /** @type {const} */ ({ vulnerabilities: { elements: { additionalProperties: true, - properties: { + optionalProperties: { metrics: { elements: { additionalProperties: true, - properties: { - products: { - elements: { type: 'string' }, - }, - }, optionalProperties: { source: { type: 'string', }, + products: { + elements: { type: 'string' }, + }, content: { additionalProperties: true, optionalProperties: { cvss_v2: { additionalProperties: true, - properties: { + optionalProperties: { version: { type: 'string' }, }, }, cvss_v3: { additionalProperties: true, - properties: { + optionalProperties: { version: { type: 'string' }, }, }, cvss_v4: { additionalProperties: true, - properties: { + optionalProperties: { version: { type: 'string' }, }, }, @@ -83,7 +81,8 @@ const inputSchema = /** @type {const} */ ({ const validate = jtdAjv.compile(inputSchema) /** - * + * For each item in /vulnerabilities it MUST be tested that the same Product ID + * is not a member of more than one CVSS-Vectors with the same version and the same source. * @param {unknown} doc */ export function mandatoryTest_6_1_7(doc) { @@ -101,7 +100,6 @@ export function mandatoryTest_6_1_7(doc) { return ctx } - // 6.1.7 Multiple Scores with same Version per Product /** @type {Array} */ const vulnerabilities = doc.vulnerabilities @@ -162,7 +160,7 @@ export function mandatoryTest_6_1_7(doc) { /** @type {Map>} */ const cvssVersionsByProductName = new Map() - /** @type {Array} */ + /** @type {Array | undefined} */ const metrics = vulnerability.metrics metrics?.forEach((metric, metricIndex) => { /** @type {Array | undefined} */ diff --git a/tests/csaf_2_1/mandatoryTest_6_1_7.js b/tests/csaf_2_1/mandatoryTest_6_1_7.js index cd6dd2db..126cd203 100644 --- a/tests/csaf_2_1/mandatoryTest_6_1_7.js +++ b/tests/csaf_2_1/mandatoryTest_6_1_7.js @@ -37,16 +37,78 @@ const emptyMandatoryTest6_1_7 = { ], } +const failingTestWithNotConsideredObject6_1_7 = { + $schema: minimalDoc.$schema, + document: { + ...minimalDoc.document, + }, + product_tree: { + full_product_names: [ + { + product_id: 'CSAFPID-9080700', + name: 'Product A', + }, + ], + }, + vulnerabilities: [ + {}, + { + metrics: [ + { + content: { + cvss_v3: { + version: '3.0', + vectorString: 'CVSS:3.1/AV:L/AC:L/PR:H/UI:R/S:U/C:H/I:H/A:H', + baseScore: 6.5, + baseSeverity: 'MEDIUM', + }, + }, + }, + { + 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, + baseSeverity: 'MEDIUM', + }, + }, + products: ['CSAFPID-9080700'], + }, + { + 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, + baseSeverity: 'MEDIUM', + }, + }, + products: ['CSAFPID-9080700'], + }, + ], + }, + ], +} + describe('mandatory test 6.1.7', function () { describe('valid examples', function () { it('test empty vulnerabilities', async function () { - const result = await mandatoryTest_6_1_7(emptyMandatoryTest6_1_7) + const result = mandatoryTest_6_1_7(emptyMandatoryTest6_1_7) expect(result.errors.length).to.eq(0) }) - it('test minimal doc', async function () { + + it('test input schema with minimal doc', async function () { expect(csaf_2_1(minimalDoc).isValid).to.be.true - const result = await mandatoryTest_6_1_7(minimalDoc) + const result = mandatoryTest_6_1_7(minimalDoc) expect(result.errors.length).to.eq(0) }) + + it('test input schema with not considered json object in vulnerabilities', async function () { + const result = mandatoryTest_6_1_7( + failingTestWithNotConsideredObject6_1_7 + ) + expect(result.errors.length).to.eq(1) + }) }) }) From 80a0759e20aae72c44579a9400559c7ee97d28f4 Mon Sep 17 00:00:00 2001 From: rschneider <97682836+rainer-exxcellent@users.noreply.github.com> Date: Wed, 18 Jun 2025 07:55:44 +0200 Subject: [PATCH 12/17] feat(CSAF2.1): #197 - test 6.1.7 - add some helper function to test --- tests/csaf_2_1/mandatoryTest_6_1_7.js | 64 ++++++-------------------- tests/csaf_2_1/shared/csafDocHelper.js | 54 ++++++++++++++++++++++ 2 files changed, 68 insertions(+), 50 deletions(-) create mode 100644 tests/csaf_2_1/shared/csafDocHelper.js diff --git a/tests/csaf_2_1/mandatoryTest_6_1_7.js b/tests/csaf_2_1/mandatoryTest_6_1_7.js index 126cd203..c2b85353 100644 --- a/tests/csaf_2_1/mandatoryTest_6_1_7.js +++ b/tests/csaf_2_1/mandatoryTest_6_1_7.js @@ -1,6 +1,10 @@ import { expect } from 'chai' import { mandatoryTest_6_1_7 } from '../../csaf_2_1/mandatoryTests/mandatoryTest_6_1_7.js' import minimalDoc from './shared/minimalDoc.js' +import { + cvssV31Content, + productTreeWithFullProductName, +} from './shared/csafDocHelper.js' import csaf_2_1 from '../../csaf_2_1/schemaTests/csaf_2_1.js' const emptyMandatoryTest6_1_7 = { @@ -8,20 +12,13 @@ const emptyMandatoryTest6_1_7 = { document: { ...minimalDoc.document, }, - product_tree: { - full_product_names: [ - { - product_id: 'CSAFPID-9080700', - name: 'Product A', - }, - ], - }, + product_tree: productTreeWithFullProductName('CSAFPID-9080700', 'Product A'), vulnerabilities: [ { metrics: [ { cvss_v3: { - version: '2.0', + version: '3.0', }, }, ], @@ -42,50 +39,17 @@ const failingTestWithNotConsideredObject6_1_7 = { document: { ...minimalDoc.document, }, - product_tree: { - full_product_names: [ - { - product_id: 'CSAFPID-9080700', - name: 'Product A', - }, - ], - }, + product_tree: productTreeWithFullProductName('CSAFPID-9080700', 'Product A'), vulnerabilities: [ - {}, + {}, // input schema should not consider this { metrics: [ - { - content: { - cvss_v3: { - version: '3.0', - vectorString: 'CVSS:3.1/AV:L/AC:L/PR:H/UI:R/S:U/C:H/I:H/A:H', - baseScore: 6.5, - baseSeverity: 'MEDIUM', - }, - }, - }, - { - 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, - baseSeverity: 'MEDIUM', - }, - }, - products: ['CSAFPID-9080700'], - }, - { - 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, - baseSeverity: 'MEDIUM', - }, - }, - products: ['CSAFPID-9080700'], - }, + cvssV31Content(6.5, 'CVSS:3.1/AV:L/AC:L/PR:H/UI:R/S:U/C:H/I:H/A:H', [ + 'CSAFPID-9080700', + ]), + cvssV31Content(6.5, 'CVSS:3.1/AV:L/AC:L/PR:H/UI:R/S:U/C:H/I:H/A:H', [ + 'CSAFPID-9080700', + ]), ], }, ], diff --git a/tests/csaf_2_1/shared/csafDocHelper.js b/tests/csaf_2_1/shared/csafDocHelper.js new file mode 100644 index 00000000..359e4e10 --- /dev/null +++ b/tests/csaf_2_1/shared/csafDocHelper.js @@ -0,0 +1,54 @@ +/** + * @param {string} productId + * @param {string} name + */ +export function productTreeWithFullProductName(productId, name) { + return { + full_product_names: [ + { + product_id: productId, + name: name, + }, + ], + } +} + +/** + * @param {number} baseSCore + * @param {string} vectorString + * @param {string[]} products + */ +export function cvssV31Content(baseSCore, vectorString, products) { + return { + content: { + cvss_v3: { + version: '3.1', + vectorString: vectorString, + baseScore: baseSCore, + baseSeverity: severityFromScore(baseSCore), + }, + }, + products: products, + } +} + +/** + * @param {number} score + * @return {string} + */ +export function severityFromScore(score) { + if (score >= 9.0) { + return 'CRITICAL' + } + if (score >= 7.0) { + return 'HIGH' + } + if (score >= 4.0) { + return 'MEDIUM' + } + if (score >= 0.1) { + return 'LOW' + } else { + return 'NONE' + } +} From b773f76a26813996661e9e1d6f8531f3d3e9653a Mon Sep 17 00:00:00 2001 From: rschneider <97682836+rainer-exxcellent@users.noreply.github.com> Date: Wed, 18 Jun 2025 15:53:33 +0200 Subject: [PATCH 13/17] feat(CSAF2.1): #197 - test 6.1.7 - use types from inputSchema --- .../mandatoryTests/mandatoryTest_6_1_7.js | 25 ++++--------------- 1 file changed, 5 insertions(+), 20 deletions(-) diff --git a/csaf_2_1/mandatoryTests/mandatoryTest_6_1_7.js b/csaf_2_1/mandatoryTests/mandatoryTest_6_1_7.js index e7514f82..c81584f7 100644 --- a/csaf_2_1/mandatoryTests/mandatoryTest_6_1_7.js +++ b/csaf_2_1/mandatoryTests/mandatoryTest_6_1_7.js @@ -1,31 +1,16 @@ import Ajv from 'ajv/dist/jtd.js' -/** - * @typedef {object} MetricContent - * @property {object} [cvss_v2] - * @property {string} [cvss_v2.version] - * @property {object} [cvss_v3] - * @property {string} [cvss_v3.version] - * @property {object} [cvss_v4] - * @property {string} [cvss_v4.version] - */ - /** * @typedef {string} Product * / + /** @typedef {import('ajv/dist/jtd.js').JTDDataType} InputSchema */ - /** - * @typedef {Object} Metric - * @property {MetricContent} [content] - * @property {Array} [products] - * @property {string} [source] - */ +/** @typedef {InputSchema['vulnerabilities'][number]} Vulnerability */ -/** - * @typedef {Object} Vulnerability - * @property {Array} [metrics] - */ +/** @typedef {NonNullable[number]} Metric */ + +/** @typedef {NonNullable} MetricContent */ const jtdAjv = new Ajv() From cfcd7cb3db3e275c2dcdd64c4c33eb704085ad9d Mon Sep 17 00:00:00 2001 From: rschneider <97682836+rainer-exxcellent@users.noreply.github.com> Date: Fri, 20 Jun 2025 09:19:09 +0200 Subject: [PATCH 14/17] feat(CSAF2.1): #197 - test 6.1.7 - use versionSourceId for the tuple of version and source to clarify things --- .../mandatoryTests/mandatoryTest_6_1_7.js | 88 +++++++++++++------ 1 file changed, 59 insertions(+), 29 deletions(-) diff --git a/csaf_2_1/mandatoryTests/mandatoryTest_6_1_7.js b/csaf_2_1/mandatoryTests/mandatoryTest_6_1_7.js index c81584f7..360b9a8c 100644 --- a/csaf_2_1/mandatoryTests/mandatoryTest_6_1_7.js +++ b/csaf_2_1/mandatoryTests/mandatoryTest_6_1_7.js @@ -89,34 +89,47 @@ export function mandatoryTest_6_1_7(doc) { const vulnerabilities = doc.vulnerabilities /** + * Create a unique string for the tuple of version and source + * to compare them easily * @param {string} version - * @param {string} source + * @param {string | undefined} source */ - function hashVersionSource(version, source) { - return JSON.stringify({ version: version, source: source }) + function createIdForVersionAndSource(version, source) { + return JSON.stringify({ version: version, source: source ?? '' }) } /** * * @param {Metric} metric - * @param {string} version - * @param {string} source + * @param {string} versionSourceId * @returns {string|null} */ - function getSameVersionInMetric(metric, version, source) { + function findCvssVersionWithSameVersionAndSource(metric, versionSourceId) { if ( metric.content?.cvss_v2?.version !== undefined && - version === hashVersionSource(metric.content?.cvss_v2.version, source) + versionSourceId === + createIdForVersionAndSource( + metric.content?.cvss_v2.version, + metric.source + ) ) { return metric.content?.cvss_v2?.version } else if ( metric.content?.cvss_v3?.version !== undefined && - version === hashVersionSource(metric.content?.cvss_v3.version, source) + versionSourceId === + createIdForVersionAndSource( + metric.content?.cvss_v3.version, + metric.source + ) ) { return metric.content?.cvss_v3?.version } else if ( metric.content?.cvss_v4?.version !== undefined && - version === hashVersionSource(metric.content?.cvss_v4.version, source) + versionSourceId === + createIdForVersionAndSource( + metric.content?.cvss_v4.version, + metric.source + ) ) { return metric.content?.cvss_v4?.version } else { @@ -126,48 +139,65 @@ export function mandatoryTest_6_1_7(doc) { /** * @param {Metric} metric - * @param {Set} versionSet - * @param {string} source + * @param {Set} versionSourceIdSet */ - function addVersionsInMetricToSet(metric, versionSet, source) { + function addAllVersionSourceIdsInMetricToSet(metric, versionSourceIdSet) { if (metric.content?.cvss_v2?.version !== undefined) { - versionSet.add(hashVersionSource(metric.content?.cvss_v2.version, source)) + versionSourceIdSet.add( + createIdForVersionAndSource( + metric.content?.cvss_v2.version, + metric.source + ) + ) } if (metric.content?.cvss_v3?.version !== undefined) { - versionSet.add(hashVersionSource(metric.content?.cvss_v3.version, source)) + versionSourceIdSet.add( + createIdForVersionAndSource( + metric.content?.cvss_v3.version, + metric.source + ) + ) } if (metric.content?.cvss_v4?.version !== undefined) { - versionSet.add(hashVersionSource(metric.content?.cvss_v4.version, source)) + versionSourceIdSet.add( + createIdForVersionAndSource( + metric.content?.cvss_v4.version, + metric.source + ) + ) } } - vulnerabilities.forEach((vulnerability, vulnerabilityIndex) => { + vulnerabilities.forEach((vulnerabilityItem, vulnerabilityIndex) => { /** @type {Map>} */ - const cvssVersionsByProductName = new Map() + const versionsSourceIdSetByProduct = new Map() /** @type {Array | undefined} */ - const metrics = vulnerability.metrics + const metrics = vulnerabilityItem.metrics metrics?.forEach((metric, metricIndex) => { /** @type {Array | undefined} */ - const products = metric.products - const source = metric.source ? metric.source : '' - products?.forEach((product, productIndex) => { - const versionSet = cvssVersionsByProductName.get(product) ?? new Set() - cvssVersionsByProductName.set(product, versionSet) - - versionSet.forEach((version) => { - const sameVersion = getSameVersionInMetric(metric, version, source) + const productsOfMetric = metric.products + productsOfMetric?.forEach((product, productIndex) => { + const versionSourceIdsOfProduct = + versionsSourceIdSetByProduct.get(product) ?? new Set() + versionsSourceIdSetByProduct.set(product, versionSourceIdsOfProduct) + + versionSourceIdsOfProduct.forEach((versionSourceIdOfProduct) => { + const sameVersion = findCvssVersionWithSameVersionAndSource( + metric, + versionSourceIdOfProduct + ) if (sameVersion) { isValid = false - + const sourceOfMetric = metric.source ? metric.source : '' errors.push({ - message: `Product is member of more than one CVSS-Vectors with the same version '${sameVersion}' and same source ${source}.`, + message: `Product is member of more than one CVSS-Vectors with the same version '${sameVersion}' and same source ${sourceOfMetric}.`, instancePath: `/vulnerabilities/${vulnerabilityIndex}/metrics/${metricIndex}/products/${productIndex}`, }) } }) - addVersionsInMetricToSet(metric, versionSet, source) + addAllVersionSourceIdsInMetricToSet(metric, versionSourceIdsOfProduct) }) }) }) From 848b6fd2d4006cccd16eaecfb472d90be11437c8 Mon Sep 17 00:00:00 2001 From: rschneider <97682836+rainer-exxcellent@users.noreply.github.com> Date: Fri, 20 Jun 2025 09:55:13 +0200 Subject: [PATCH 15/17] feat(CSAF2.1): #197 - test 6.1.10 - use cahnged csaf2.1 schema in minimal doc --- tests/csaf_2_1/shared/minimalDoc.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/csaf_2_1/shared/minimalDoc.js b/tests/csaf_2_1/shared/minimalDoc.js index 7eefd2c8..560aecdb 100644 --- a/tests/csaf_2_1/shared/minimalDoc.js +++ b/tests/csaf_2_1/shared/minimalDoc.js @@ -1,5 +1,5 @@ export default { - $schema: 'https://docs.oasis-open.org/csaf/csaf/v2.1/csaf_json_schema.json', + $schema: 'https://docs.oasis-open.org/csaf/csaf/v2.1/schema/csaf.json', document: { category: 'Test Report', csaf_version: '2.1', From 56426c1591db69c6702b973bef121ac58bcbcb27 Mon Sep 17 00:00:00 2001 From: rschneider <97682836+rainer-exxcellent@users.noreply.github.com> Date: Fri, 20 Jun 2025 11:49:27 +0200 Subject: [PATCH 16/17] feat(CSAF2.1): #197 mandatory test 6.1.7 - move test in README to the supported tests --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 574c7668..f4af66f7 100644 --- a/README.md +++ b/README.md @@ -311,7 +311,6 @@ The following tests are not yet implemented and therefore missing: **Mandatory Tests** -- Mandatory Test 6.1.7 - Mandatory Test 6.1.10 - Mandatory Test 6.1.14 - Mandatory Test 6.1.16 @@ -395,6 +394,7 @@ 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_11: DocumentTest From 8d30ec9e2071b608ad9edd9b8845089237b47712 Mon Sep 17 00:00:00 2001 From: Rainer Schneider <97682836+rainer-exxcellent@users.noreply.github.com> Date: Fri, 20 Jun 2025 12:02:04 +0200 Subject: [PATCH 17/17] feat(CSAF2.1): #197 mandatory test 6.1.7 - add '' to message Co-authored-by: tschmidtb51 <65305130+tschmidtb51@users.noreply.github.com> --- csaf_2_1/mandatoryTests/mandatoryTest_6_1_7.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/csaf_2_1/mandatoryTests/mandatoryTest_6_1_7.js b/csaf_2_1/mandatoryTests/mandatoryTest_6_1_7.js index 360b9a8c..8e55962c 100644 --- a/csaf_2_1/mandatoryTests/mandatoryTest_6_1_7.js +++ b/csaf_2_1/mandatoryTests/mandatoryTest_6_1_7.js @@ -191,7 +191,7 @@ export function mandatoryTest_6_1_7(doc) { isValid = false const sourceOfMetric = metric.source ? metric.source : '' errors.push({ - message: `Product is member of more than one CVSS-Vectors with the same version '${sameVersion}' and same source ${sourceOfMetric}.`, + message: `Product is member of more than one CVSS-Vectors with the same version '${sameVersion}' and same source '${sourceOfMetric}'.`, instancePath: `/vulnerabilities/${vulnerabilityIndex}/metrics/${metricIndex}/products/${productIndex}`, }) }