diff --git a/README.md b/README.md index f4af66f7..f9a60d1b 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.10 - Mandatory Test 6.1.14 - Mandatory Test 6.1.16 - Mandatory Test 6.1.27.12 @@ -397,6 +396,7 @@ 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 diff --git a/csaf_2_1/mandatoryTests.js b/csaf_2_1/mandatoryTests.js index efb0241e..f51d680c 100644 --- a/csaf_2_1/mandatoryTests.js +++ b/csaf_2_1/mandatoryTests.js @@ -39,6 +39,7 @@ 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' +export { mandatoryTest_6_1_10 } from './mandatoryTests/mandatoryTest_6_1_10.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_9 } from './mandatoryTests/mandatoryTest_6_1_9.js' diff --git a/csaf_2_1/mandatoryTests/mandatoryTest_6_1_10.js b/csaf_2_1/mandatoryTests/mandatoryTest_6_1_10.js new file mode 100644 index 00000000..295118aa --- /dev/null +++ b/csaf_2_1/mandatoryTests/mandatoryTest_6_1_10.js @@ -0,0 +1,237 @@ +import * as cvss2 from '../../lib/shared/cvss2.js' +import * as cvss3 from '../../lib/shared/cvss3.js' +import * as cvss4 from '../../lib/shared/cvss4.js' +import Ajv from 'ajv/dist/jtd.js' + +/** @typedef {import('ajv/dist/jtd.js').JTDDataType} InputSchema */ + +/** @typedef {InputSchema['vulnerabilities'][number]} Vulnerability */ + +/** @typedef {NonNullable[number]} Metric */ + +/** @typedef {NonNullable} MetricContent */ + +const inputSchema = /** @type {const} */ ({ + additionalProperties: true, + properties: { + vulnerabilities: { + elements: { + additionalProperties: true, + optionalProperties: { + metrics: { + elements: { + additionalProperties: true, + optionalProperties: { + content: { + additionalProperties: true, + optionalProperties: { + cvss_v2: { + additionalProperties: true, + optionalProperties: { + vectorString: { type: 'string' }, + version: { type: 'string' }, + }, + }, + cvss_v3: { + additionalProperties: true, + optionalProperties: { + vectorString: { type: 'string' }, + version: { type: 'string' }, + }, + }, + cvss_v4: { + additionalProperties: true, + optionalProperties: { + vectorString: { type: 'string' }, + version: { type: 'string' }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, +}) +const ajv = new Ajv() +const validateInput = ajv.compile(inputSchema) + +/** @type { Record}>} */ +const cvssV3MappingByMetricKey = Object.fromEntries( + cvss3.mapping.map((mapping) => { + return [ + mapping[1], + { + jsonName: mapping[0], + optionsByKey: Object.fromEntries( + Object.entries(mapping[2]).map(([key, value]) => [value, key]) + ), + }, + ] + }) +) + +/** @type { Record}>} */ +const cvssV2MappingByMetricKey = Object.fromEntries( + cvss2.mapping.map((mapping) => { + return [ + mapping[1], + { + jsonName: mapping[0], + optionsByKey: Object.fromEntries( + Object.entries(mapping[2]).map(([key, value]) => [value.id, key]) + ), + }, + ] + }) +) + +/** + * @param {{optionName: string, optionValue: string, optionKey: string}[]} optionsArray + * @return {Record} + */ +function convertOptionsArrayToObject(optionsArray) { + /** @type {Record} */ + const result = {} + optionsArray.forEach((option) => { + result[option.optionKey] = option.optionValue + }) + return result +} + +/** @type { Record}>} */ +const cvssV4MappingByMetricKey = Object.fromEntries( + cvss4.flatMetrics.map((flatMetric) => { + return [ + flatMetric.metricShort, + { + jsonName: flatMetric.jsonName, + optionsByKey: convertOptionsArrayToObject(flatMetric.options), + }, + ] + }) +) + +/** + * @param {Metric} metric + */ +function validateCvss2(metric) { + if (typeof metric.content?.cvss_v2?.vectorString === 'string') { + return validateCVSSAttributes( + cvssV2MappingByMetricKey, + metric.content.cvss_v2 + ) + } else { + return [] + } +} + +/** + * @param {Metric} metric + */ +function validateCvss3(metric) { + if ( + typeof metric?.content?.cvss_v3?.vectorString === 'string' && + (metric.content.cvss_v3.version === '3.1' || + metric.content.cvss_v3.version === '3.0') + ) { + return validateCVSSAttributes( + cvssV3MappingByMetricKey, + metric.content.cvss_v3 + ) + } else { + return [] + } +} + +/** + * @param {Metric} metric + */ +function validateCvss4(metric) { + if (typeof metric?.content?.cvss_v4?.vectorString === 'string') { + return validateCVSSAttributes( + cvssV4MappingByMetricKey, + metric.content.cvss_v4 + ) + } else { + return [] + } +} + +/** + * @param {unknown} doc + */ +export function mandatoryTest_6_1_10(doc) { + /** @type {Array<{ message: string; instancePath: string }>} */ + const errors = [] + + if (!validateInput(doc)) { + return { errors, isValid: true } + } + + if (Array.isArray(doc.vulnerabilities)) { + /** @type {Array} */ + const vulnerabilities = doc.vulnerabilities + vulnerabilities.forEach((vulnerability, vulnerabilityIndex) => { + if (!Array.isArray(vulnerability.metrics)) return + /** @type {Array} */ + const metrics = vulnerability.metrics + metrics.forEach((metric, metricIndex) => { + validateCvss2(metric).forEach((attributeKey) => { + errors.push({ + instancePath: `/vulnerabilities/${vulnerabilityIndex}/metrics/${metricIndex}/content/cvss_v2/${attributeKey}`, + message: 'value is not consistent with the vector string', + }) + }) + + validateCvss3(metric).forEach((attributeKey) => { + errors.push({ + instancePath: `/vulnerabilities/${vulnerabilityIndex}/metrics/${metricIndex}/content/cvss_v3/${attributeKey}`, + message: 'value is not consistent with the vector string', + }) + }) + + validateCvss4(metric).forEach((attributeKey) => { + errors.push({ + instancePath: `/vulnerabilities/${vulnerabilityIndex}/metrics/${metricIndex}/content/cvss_v4/${attributeKey}`, + message: 'value is not consistent with the vector string', + }) + }) + }) + }) + } + + return { errors, isValid: errors.length === 0 } +} + +/** + * validate the cvss vector against the cvss properties + * @param {Record}>}mappingByMetricKey cvss version specific mapping + * @param {Record} cvss cvss object + + */ +function validateCVSSAttributes(mappingByMetricKey, cvss) { + const vectorString = /** @type {string} */ (cvss.vectorString) + const vectorValues = vectorString.split('/').slice(1) + /** + * @type {string[]} + */ + const invalidKeys = [] + vectorValues.forEach((vectorValue) => { + const [vectorMetricKey, vectorOptionKey] = vectorValue.split(':') + const mapping = mappingByMetricKey[vectorMetricKey] + if (mapping) { + const metricOptionValue = cvss[mapping.jsonName] + if (typeof metricOptionValue == 'string') { + const expectedOptionValue = mapping.optionsByKey[vectorOptionKey] + if (metricOptionValue !== expectedOptionValue) { + invalidKeys.push(mapping.jsonName) + } + } + } + }) + return invalidKeys +} diff --git a/tests/csaf_2_1/mandatoryTest_6_1_10.js b/tests/csaf_2_1/mandatoryTest_6_1_10.js new file mode 100644 index 00000000..3a8cfa94 --- /dev/null +++ b/tests/csaf_2_1/mandatoryTest_6_1_10.js @@ -0,0 +1,8 @@ +import assert from 'node:assert/strict' +import { mandatoryTest_6_1_10 } from '../../csaf_2_1/mandatoryTests/mandatoryTest_6_1_10.js' + +describe('mandatoryTest_6_1_10', function () { + it('only runs on relevant documents', function () { + assert.equal(mandatoryTest_6_1_10({ document: 'mydoc' }).isValid, true) + }) +}) diff --git a/tests/csaf_2_1/oasis.js b/tests/csaf_2_1/oasis.js index 65fc6310..c1c7642d 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.10', '6.1.14', '6.1.16', '6.1.26',