Skip to content

Feat/197 mandatory tests csaf2.1 6.1.10 #230

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
Jun 24, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions csaf_2_1/mandatoryTests.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
237 changes: 237 additions & 0 deletions csaf_2_1/mandatoryTests/mandatoryTest_6_1_10.js
Original file line number Diff line number Diff line change
@@ -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<typeof inputSchema>} InputSchema */

/** @typedef {InputSchema['vulnerabilities'][number]} Vulnerability */

/** @typedef {NonNullable<Vulnerability['metrics']>[number]} Metric */

/** @typedef {NonNullable<Metric['content']>} 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<string, { jsonName:string, optionsByKey:Record<string, string>}>} */
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<string, { jsonName:string, optionsByKey:Record<string, string>}>} */
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<string, string>}
*/
function convertOptionsArrayToObject(optionsArray) {
/** @type {Record<string, string>} */
const result = {}
optionsArray.forEach((option) => {
result[option.optionKey] = option.optionValue
})
return result
}

/** @type { Record<string, { jsonName:string, optionsByKey:Record<string, string>}>} */
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<Vulnerability>} */
const vulnerabilities = doc.vulnerabilities
vulnerabilities.forEach((vulnerability, vulnerabilityIndex) => {
if (!Array.isArray(vulnerability.metrics)) return
/** @type {Array<Metric>} */
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<string, { jsonName:string, optionsByKey:Record<string, string>}>}mappingByMetricKey cvss version specific mapping
* @param {Record<string, unknown>} 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
}
8 changes: 8 additions & 0 deletions tests/csaf_2_1/mandatoryTest_6_1_10.js
Original file line number Diff line number Diff line change
@@ -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)
})
})
1 change: 0 additions & 1 deletion tests/csaf_2_1/oasis.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down