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 diff --git a/csaf_2_1/mandatoryTests.js b/csaf_2_1/mandatoryTests.js index 95c5c092..efb0241e 100644 --- a/csaf_2_1/mandatoryTests.js +++ b/csaf_2_1/mandatoryTests.js @@ -35,6 +35,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 new file mode 100644 index 00000000..8e55962c --- /dev/null +++ b/csaf_2_1/mandatoryTests/mandatoryTest_6_1_7.js @@ -0,0 +1,206 @@ +import Ajv from 'ajv/dist/jtd.js' + +/** + * @typedef {string} Product + * / + + /** @typedef {import('ajv/dist/jtd.js').JTDDataType} InputSchema */ + +/** @typedef {InputSchema['vulnerabilities'][number]} Vulnerability */ + +/** @typedef {NonNullable[number]} Metric */ + +/** @typedef {NonNullable} MetricContent */ + +const jtdAjv = new Ajv() + +const inputSchema = /** @type {const} */ ({ + additionalProperties: true, + properties: { + vulnerabilities: { + elements: { + additionalProperties: true, + optionalProperties: { + metrics: { + elements: { + additionalProperties: true, + optionalProperties: { + source: { + type: 'string', + }, + products: { + elements: { type: 'string' }, + }, + content: { + additionalProperties: true, + optionalProperties: { + cvss_v2: { + additionalProperties: true, + optionalProperties: { + version: { type: 'string' }, + }, + }, + cvss_v3: { + additionalProperties: true, + optionalProperties: { + version: { type: 'string' }, + }, + }, + cvss_v4: { + additionalProperties: true, + optionalProperties: { + version: { type: 'string' }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, +}) + +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) { + 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 + } + + /** @type {Array} */ + const vulnerabilities = doc.vulnerabilities + + /** + * Create a unique string for the tuple of version and source + * to compare them easily + * @param {string} version + * @param {string | undefined} source + */ + function createIdForVersionAndSource(version, source) { + return JSON.stringify({ version: version, source: source ?? '' }) + } + + /** + * + * @param {Metric} metric + * @param {string} versionSourceId + * @returns {string|null} + */ + function findCvssVersionWithSameVersionAndSource(metric, versionSourceId) { + if ( + metric.content?.cvss_v2?.version !== undefined && + versionSourceId === + createIdForVersionAndSource( + metric.content?.cvss_v2.version, + metric.source + ) + ) { + return metric.content?.cvss_v2?.version + } else if ( + metric.content?.cvss_v3?.version !== undefined && + versionSourceId === + createIdForVersionAndSource( + metric.content?.cvss_v3.version, + metric.source + ) + ) { + return metric.content?.cvss_v3?.version + } else if ( + metric.content?.cvss_v4?.version !== undefined && + versionSourceId === + createIdForVersionAndSource( + metric.content?.cvss_v4.version, + metric.source + ) + ) { + return metric.content?.cvss_v4?.version + } else { + return null + } + } + + /** + * @param {Metric} metric + * @param {Set} versionSourceIdSet + */ + function addAllVersionSourceIdsInMetricToSet(metric, versionSourceIdSet) { + if (metric.content?.cvss_v2?.version !== undefined) { + versionSourceIdSet.add( + createIdForVersionAndSource( + metric.content?.cvss_v2.version, + metric.source + ) + ) + } + if (metric.content?.cvss_v3?.version !== undefined) { + versionSourceIdSet.add( + createIdForVersionAndSource( + metric.content?.cvss_v3.version, + metric.source + ) + ) + } + if (metric.content?.cvss_v4?.version !== undefined) { + versionSourceIdSet.add( + createIdForVersionAndSource( + metric.content?.cvss_v4.version, + metric.source + ) + ) + } + } + + vulnerabilities.forEach((vulnerabilityItem, vulnerabilityIndex) => { + /** @type {Map>} */ + const versionsSourceIdSetByProduct = new Map() + + /** @type {Array | undefined} */ + const metrics = vulnerabilityItem.metrics + metrics?.forEach((metric, metricIndex) => { + /** @type {Array | undefined} */ + 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 '${sourceOfMetric}'.`, + instancePath: `/vulnerabilities/${vulnerabilityIndex}/metrics/${metricIndex}/products/${productIndex}`, + }) + } + }) + + addAllVersionSourceIdsInMetricToSet(metric, versionSourceIdsOfProduct) + }) + }) + }) + + return { errors, isValid } +} 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..c2b85353 --- /dev/null +++ b/tests/csaf_2_1/mandatoryTest_6_1_7.js @@ -0,0 +1,78 @@ +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 = { + $schema: minimalDoc.$schema, + document: { + ...minimalDoc.document, + }, + product_tree: productTreeWithFullProductName('CSAFPID-9080700', 'Product A'), + vulnerabilities: [ + { + metrics: [ + { + cvss_v3: { + version: '3.0', + }, + }, + ], + }, + { + metrics: [ + { + content: {}, + products: [], + }, + ], + }, + ], +} + +const failingTestWithNotConsideredObject6_1_7 = { + $schema: minimalDoc.$schema, + document: { + ...minimalDoc.document, + }, + product_tree: productTreeWithFullProductName('CSAFPID-9080700', 'Product A'), + vulnerabilities: [ + {}, // input schema should not consider this + { + metrics: [ + 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', + ]), + ], + }, + ], +} + +describe('mandatory test 6.1.7', function () { + describe('valid examples', function () { + it('test empty vulnerabilities', async function () { + const result = mandatoryTest_6_1_7(emptyMandatoryTest6_1_7) + expect(result.errors.length).to.eq(0) + }) + + it('test input schema with minimal doc', async function () { + expect(csaf_2_1(minimalDoc).isValid).to.be.true + 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) + }) + }) +}) 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', 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' + } +} diff --git a/tests/csaf_2_1/shared/minimalDoc.js b/tests/csaf_2_1/shared/minimalDoc.js new file mode 100644 index 00000000..560aecdb --- /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/schema/csaf.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', + }, + }, +}