diff --git a/README.md b/README.md index 1c1f5999..292940eb 100644 --- a/README.md +++ b/README.md @@ -433,6 +433,7 @@ export const mandatoryTest_6_1_41: DocumentTest export const mandatoryTest_6_1_43: DocumentTest export const mandatoryTest_6_1_45: DocumentTest export const mandatoryTest_6_1_52: DocumentTest +export const mandatoryTest_6_1_56: DocumentTest ``` [(back to top)](#bsi-csaf-validator-lib) diff --git a/csaf_2_1/mandatoryTests.js b/csaf_2_1/mandatoryTests.js index 482f0616..dde8f5f7 100644 --- a/csaf_2_1/mandatoryTests.js +++ b/csaf_2_1/mandatoryTests.js @@ -61,3 +61,4 @@ export { mandatoryTest_6_1_41 } from './mandatoryTests/mandatoryTest_6_1_41.js' export { mandatoryTest_6_1_43 } from './mandatoryTests/mandatoryTest_6_1_43.js' export { mandatoryTest_6_1_45 } from './mandatoryTests/mandatoryTest_6_1_45.js' export { mandatoryTest_6_1_52 } from './mandatoryTests/mandatoryTest_6_1_52.js' +export { mandatoryTest_6_1_56 } from './mandatoryTests/mandatoryTest_6_1_56.js' diff --git a/csaf_2_1/mandatoryTests/mandatoryTest_6_1_56.js b/csaf_2_1/mandatoryTests/mandatoryTest_6_1_56.js new file mode 100644 index 00000000..23753b61 --- /dev/null +++ b/csaf_2_1/mandatoryTests/mandatoryTest_6_1_56.js @@ -0,0 +1,153 @@ +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 */ + +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' }, + }, + }, + qualitative_severity_rating: { + type: 'string', + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, +}) + +const validate = jtdAjv.compile(inputSchema) + +/** + * For each item in `/vulnerabilities` it MUST be tested that no Qualitative Severity Rating and CVSS values are + * listed for the tuple of Product ID and source. + * @param {unknown} doc + */ +export function mandatoryTest_6_1_56(doc) { + const ctx = { + errors: + /** @type {Array<{ instancePath: string; message: string }>} */ ([]), + isValid: true, + } + + /** @type {Array<{ message: string; instancePath: string }>} */ + const errors = [] + + if (!validate(doc)) { + return ctx + } + + /** @type {Array} */ + const vulnerabilities = doc.vulnerabilities + + /** + * Create a unique string for the tuple of productId and source + * to compare them easily + * @param {string} productId + * @param {string | undefined} source + * + * @return string + */ + function createTupleStringForProductAndSource(productId, source) { + return JSON.stringify({ productId: productId, source: source ?? '' }) + } + + /** + * check whether the given metric contains a cvss (v2, v3 or v4) content + * @param {Metric} metric + * @returns {boolean} + */ + function hasCvssContent(metric) { + return ( + metric.content?.cvss_v2?.version !== undefined || + metric.content?.cvss_v3?.version !== undefined || + metric.content?.cvss_v4?.version !== undefined + ) + } + + vulnerabilities.forEach((vulnerabilityItem, vulnerabilityIndex) => { + /** @type {Map} */ + const productIdServiceTuplesCvss = new Map() + /** @type {Map} */ + const productIdServiceTuplesRating = new Map() + + /** @type {Array | undefined} */ + const metrics = vulnerabilityItem.metrics + metrics?.forEach((metric, metricIndex) => { + /** @type {Array | undefined} */ + const productsOfMetric = metric.products + productsOfMetric?.forEach((product, productIndex) => { + if (hasCvssContent(metric)) { + productIdServiceTuplesCvss.set( + createTupleStringForProductAndSource(product, metric.source), + `/vulnerabilities/${vulnerabilityIndex}/metrics/${metricIndex}/products/${productIndex}` + ) + } + if (metric.content?.qualitative_severity_rating) { + productIdServiceTuplesRating.set( + createTupleStringForProductAndSource(product, metric.source), + `/vulnerabilities/${vulnerabilityIndex}/metrics/${metricIndex}/products/${productIndex}` + ) + } + }) + }) + + productIdServiceTuplesCvss.forEach((value, key) => { + if (productIdServiceTuplesRating.has(key)) + errors.push({ + message: + 'in the metrics of the vulnerability a Qualitative Severity Rating and CVSS value ' + + 'with the same product id and source is used.', + instancePath: value, + }) + }) + }) + + return { errors: errors, isValid: errors.length === 0 } +} diff --git a/tests/csaf_2_1/mandatoryTest_6_1_56.js b/tests/csaf_2_1/mandatoryTest_6_1_56.js new file mode 100644 index 00000000..4072cb02 --- /dev/null +++ b/tests/csaf_2_1/mandatoryTest_6_1_56.js @@ -0,0 +1,8 @@ +import assert from 'node:assert/strict' +import { mandatoryTest_6_1_56 } from '../../csaf_2_1/mandatoryTests/mandatoryTest_6_1_56.js' + +describe('mandatoryTest_6_1_56', function () { + it('only runs on relevant documents', function () { + assert.equal(mandatoryTest_6_1_56({ document: 'mydoc' }).isValid, true) + }) +}) diff --git a/tests/csaf_2_1/oasis.js b/tests/csaf_2_1/oasis.js index dbc5a7b9..fc0a7332 100644 --- a/tests/csaf_2_1/oasis.js +++ b/tests/csaf_2_1/oasis.js @@ -30,7 +30,6 @@ const excluded = [ '6.1.53', '6.1.54', '6.1.55', - '6.1.56', '6.2.11', '6.2.19', '6.2.20',