Skip to content
Open
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -330,7 +330,6 @@ The following tests are not yet implemented and therefore missing:

- Recommended Test 6.2.11
- Recommended Test 6.2.19
- Recommended Test 6.2.20
- Recommended Test 6.2.24
- Recommended Test 6.2.26
- Recommended Test 6.2.31
Expand Down Expand Up @@ -487,6 +486,7 @@ export const recommendedTest_6_2_15: DocumentTest
export const recommendedTest_6_2_16: DocumentTest
export const recommendedTest_6_2_17: DocumentTest
export const recommendedTest_6_2_18: DocumentTest
export const recommendedTest_6_2_20: DocumentTest
export const recommendedTest_6_2_21: DocumentTest
export const recommendedTest_6_2_22: DocumentTest
export const recommendedTest_6_2_23: DocumentTest
Expand Down
6 changes: 3 additions & 3 deletions csaf_2_1/csafAjv.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import addFormats from 'ajv-formats'
import { Ajv2020 } from 'ajv/dist/2020.js'
import cvss_v2_0 from '../schemas/cvss-v2.0.js'
import cvss_v3_0 from '../schemas/cvss-v3.0.js'
import cvss_v3_1 from '../schemas/cvss-v3.1.js'
import cvss_v2_0 from './csafAjv/cvss-v2.0.js'
import cvss_v3_0 from './csafAjv/cvss-v3.0.js'
import cvss_v3_1 from './csafAjv/cvss-v3.1.js'
import cvss_v4_0_0 from './csafAjv/cvss-v4.0.0.js'
import extension_content from './csafAjv/extension-content.js'
import content_schema from './csafAjv/content_schema.js'
Expand Down
1 change: 1 addition & 0 deletions csaf_2_1/csafAjv/cvss-v2.0.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export default {
title: 'JSON Schema for Common Vulnerability Scoring System version 2.0',
$id: 'https://www.first.org/cvss/cvss-v2.0.json?20170531',
type: 'object',
additionalProperties: false,
$defs: {
accessVectorType: {
type: 'string',
Expand Down
1 change: 1 addition & 0 deletions csaf_2_1/csafAjv/cvss-v3.0.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export default {
title: 'JSON Schema for Common Vulnerability Scoring System version 3.0',
$id: 'https://www.first.org/cvss/cvss-v3.0.json?20170531',
type: 'object',
additionalProperties: false,
$defs: {
attackVectorType: {
type: 'string',
Expand Down
1 change: 1 addition & 0 deletions csaf_2_1/csafAjv/cvss-v3.1.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export default {
title: 'JSON Schema for Common Vulnerability Scoring System version 3.1',
$id: 'https://www.first.org/cvss/cvss-v3.1.json?20190610',
type: 'object',
additionalProperties: false,
$defs: {
attackVectorType: {
type: 'string',
Expand Down
1 change: 1 addition & 0 deletions csaf_2_1/recommendedTests.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export { recommendedTest_6_2_16 } from './recommendedTests/recommendedTest_6_2_1
export { recommendedTest_6_2_17 } from './recommendedTests/recommendedTest_6_2_17.js'
export { recommendedTest_6_2_18 } from './recommendedTests/recommendedTest_6_2_18.js'
export { recommendedTest_6_2_19 } from './recommendedTests/recommendedTest_6_2_19.js'
export { recommendedTest_6_2_20 } from './recommendedTests/recommendedTest_6_2_20.js'
export { recommendedTest_6_2_21 } from './recommendedTests/recommendedTest_6_2_21.js'
export { recommendedTest_6_2_22 } from './recommendedTests/recommendedTest_6_2_22.js'
export { recommendedTest_6_2_23 } from './recommendedTests/recommendedTest_6_2_23.js'
Expand Down
216 changes: 214 additions & 2 deletions csaf_2_1/recommendedTests/recommendedTest_6_2_20.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,220 @@
import { optionalTest_6_2_20 } from '../../optionalTests.js'
import schema from '../schemaTests/csaf_2_1_strict/schema.js'
import csafAjv from '../csafAjv.js'
import { Ajv } from 'ajv/dist/jtd.js'

const ajv = new Ajv()
const validateStrictSchema = csafAjv.compile(schema)

const extensionSchema = /** @type {const} */ ({
additionalProperties: true,
optionalProperties: {
$schema: { type: 'string' },
category: { type: 'string' },
},
})

const fullProductNameSchema = /** @type {const} */ ({
additionalProperties: true,
optionalProperties: {
x_extensions: { elements: extensionSchema },
},
})

const branchSchema = /** @type {const} */ ({
additionalProperties: true,
optionalProperties: {
product: fullProductNameSchema,
branches: {
elements: {
additionalProperties: true,
properties: {},
},
},
},
})

const inputSchema = /** @type {const} */ ({
additionalProperties: true,
optionalProperties: {
x_extensions: { elements: extensionSchema },
document: {
additionalProperties: true,
optionalProperties: {
x_extensions: { elements: extensionSchema },
},
},
product_tree: {
additionalProperties: true,
optionalProperties: {
full_product_names: { elements: fullProductNameSchema },
branches: { elements: branchSchema },
product_paths: {
elements: {
additionalProperties: true,
optionalProperties: {
full_product_name: fullProductNameSchema,
},
},
},
},
},
vulnerabilities: {
elements: {
additionalProperties: true,
optionalProperties: {
x_extensions: { elements: extensionSchema },
metrics: {
elements: {
additionalProperties: true,
optionalProperties: {
content: {
additionalProperties: true,
optionalProperties: {
x_extensions: { elements: extensionSchema },
},
},
},
},
},
},
},
},
},
})

const validateInput = ajv.compile(inputSchema)

/**
* @typedef {import('ajv/dist/core.js').JTDDataType<typeof branchSchema>} Branch
* @typedef {import('ajv/dist/core.js').JTDDataType<typeof extensionSchema>} ExtensionSchema
*/

/**
* This implements the recommended test 6.2.20 of the CSAF 2.1 standard.
*
* @param {unknown} doc
*/
export function recommendedTest_6_2_20(doc) {
return optionalTest_6_2_20(doc)
const ctx = {
warnings:
/** @type {Array<{ instancePath: string; message: string }>} */ ([]),
}

// Part 1: strict schema check – report any property not defined in the CSAF schema
if (!validateStrictSchema(doc)) {
const additionalPropertiesErrors =
validateStrictSchema.errors?.filter(
(e) =>
e.keyword === 'additionalProperties' ||
e.keyword === 'unevaluatedProperties'
) ?? []
for (const error of additionalPropertiesErrors) {
const propertyName =
error.params.additionalProperty ?? error.params.unevaluatedProperty
ctx.warnings.push({
instancePath: `${error.instancePath}/${propertyName}`,
message: `property "${propertyName}" is not defined in the schema`,
})
}
}

// Part 2: warn about unsupported CSAF Extensions
if (!validateInput(doc)) return ctx

if (doc.x_extensions) {
checkExtensions(ctx.warnings, doc.x_extensions, '/x_extensions')
}

if (doc.document?.x_extensions) {
checkExtensions(
ctx.warnings,
doc.document.x_extensions,
'/document/x_extensions'
)
}

doc.product_tree?.full_product_names?.forEach((fpn, j) => {
if (fpn.x_extensions) {
checkExtensions(
ctx.warnings,
fpn.x_extensions,
`/product_tree/full_product_names/${j}/x_extensions`
)
}
})

if (doc.product_tree?.branches) {
checkBranchExtensions(
ctx.warnings,
doc.product_tree.branches,
'/product_tree/branches'
)
}

doc.product_tree?.product_paths?.forEach((pp, j) => {
if (pp.full_product_name?.x_extensions) {
checkExtensions(
ctx.warnings,
pp.full_product_name.x_extensions,
`/product_tree/product_paths/${j}/full_product_name/x_extensions`
)
}
})

doc.vulnerabilities?.forEach((vuln, j) => {
if (vuln.x_extensions) {
checkExtensions(
ctx.warnings,
vuln.x_extensions,
`/vulnerabilities/${j}/x_extensions`
)
}
vuln.metrics?.forEach((metric, k) => {
if (metric.content?.x_extensions) {
checkExtensions(
ctx.warnings,
metric.content.x_extensions,
`/vulnerabilities/${j}/metrics/${k}/content/x_extensions`
)
}
})
})

return ctx
}

/**
* Checks an array of extensions for unsupported schemas
* @param {Array<{ instancePath: string; message: string }>} warnings
* @param {ExtensionSchema[]} extensions
* @param {string} basePath
*/
function checkExtensions(warnings, extensions, basePath) {
extensions.forEach((ext, i) => {
warnings.push({
instancePath: `${basePath}/${i}/$schema`,
message: `unsupported CSAF Extension of schema "${ext.$schema}"`,
})
})
}

/**
* Recursively checks branches for unsupported extensions
* @param {Array<{ instancePath: string; message: string }>} warnings
* @param {Branch[]} branches
* @param {string} path
*/
function checkBranchExtensions(warnings, branches, path) {
branches.forEach((branch, i) => {
if (Array.isArray(branch?.product?.x_extensions)) {
checkExtensions(
warnings,
branch.product.x_extensions,
`${path}/${i}/product/x_extensions`
)
}
if (Array.isArray(branch?.branches)) {
checkBranchExtensions(warnings, branch.branches, `${path}/${i}/branches`)
}
})
}
1 change: 0 additions & 1 deletion tests/csaf_2_1/oasis.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ const excluded = [
'6.1.61',
'6.2.11',
'6.2.19',
'6.2.20',
'6.2.24',
'6.2.26',
'6.2.31',
Expand Down
Loading
Loading