diff --git a/README.md b/README.md index 44bae60c..5e80e739 100644 --- a/README.md +++ b/README.md @@ -356,7 +356,6 @@ The following tests are not yet implemented and therefore missing: **Recommended Tests** -- Recommended Test 6.2.20 - Recommended Test 6.2.24 - Recommended Test 6.2.26 - Recommended Test 6.2.31 @@ -502,6 +501,7 @@ 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_19: 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 diff --git a/csaf_2_1/csafAjv.js b/csaf_2_1/csafAjv.js index 429f5d87..6f7df1ec 100644 --- a/csaf_2_1/csafAjv.js +++ b/csaf_2_1/csafAjv.js @@ -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' @@ -11,6 +11,7 @@ import cvss_meta from './csafAjv/cvss_meta.js' import meta_format_assertion from './csafAjv/meta-format-assertion.js' import draft_07_schema from './csafAjv/draft-07-schema.js' import selectionList_2_0_0Schema from './csafAjv/SelectionList_2_0_0.schema.js' +import { registerExtensionSchemas } from './csafAjv/extensionSchemas/index.js' import { validateTimestamp } from './dateHelper.js' @@ -45,6 +46,7 @@ csafAjv.addSchema( selectionList_2_0_0Schema, 'https://certcc.github.io/SSVC/data/schema/v2/SelectionList_2_0_0.schema.json' ) +registerExtensionSchemas(csafAjv) csafAjv.addFormat('date-time', { type: 'string', diff --git a/csaf_2_1/csafAjv/cvss-v2.0.js b/csaf_2_1/csafAjv/cvss-v2.0.js index cf791137..4833f1ce 100644 --- a/csaf_2_1/csafAjv/cvss-v2.0.js +++ b/csaf_2_1/csafAjv/cvss-v2.0.js @@ -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', diff --git a/csaf_2_1/csafAjv/cvss-v3.0.js b/csaf_2_1/csafAjv/cvss-v3.0.js index c46f4ef2..f7c4e7cf 100644 --- a/csaf_2_1/csafAjv/cvss-v3.0.js +++ b/csaf_2_1/csafAjv/cvss-v3.0.js @@ -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', diff --git a/csaf_2_1/csafAjv/cvss-v3.1.js b/csaf_2_1/csafAjv/cvss-v3.1.js index d4b86cee..d6d0eefc 100644 --- a/csaf_2_1/csafAjv/cvss-v3.1.js +++ b/csaf_2_1/csafAjv/cvss-v3.1.js @@ -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', diff --git a/csaf_2_1/csafAjv/extensionSchemas/documentation-11.js b/csaf_2_1/csafAjv/extensionSchemas/documentation-11.js new file mode 100644 index 00000000..6f5d2538 --- /dev/null +++ b/csaf_2_1/csafAjv/extensionSchemas/documentation-11.js @@ -0,0 +1,54 @@ +// https://raw.githubusercontent.com/oasis-tcs/csaf/refs/heads/master/csaf_2.1/extension/data/valid/documentation-11/documentation-11-content_1.0.0.json +// Copied from ../../../csaf/csaf_2.1/test/extension/data/valid/documentation-11/documentation-11-content_1.0.0.json +export default { + $schema: + 'https://docs.oasis-open.org/csaf/csaf/v2.1/schema/extension-metaschema.json', + $id: 'https://raw.githubusercontent.com/oasis-tcs/csaf/refs/heads/master/csaf_2.1/extension/data/valid/documentation-11/documentation-11-content_1.0.0.json', + title: 'CSAF Extension: Documentation 11', + description: + 'Representation of a valid extension used for documentation purposes.', + type: 'object', + required: ['$schema', 'category', 'critical', 'content'], + properties: { + $schema: { + title: 'CSAF Extension Schema', + description: + 'Contains the URL of the CSAF Extension JSON schema which the JSON object promises to be valid for.', + type: 'string', + format: 'uri', + const: + 'https://raw.githubusercontent.com/oasis-tcs/csaf/refs/heads/master/csaf_2.1/extension/data/valid/documentation-11/documentation-11-content_1.0.0.json', + }, + category: { + title: 'Extension Category', + description: 'Holds the category of the extension content.', + type: 'string', + enum: ['essential', 'significant', 'supplementary'], + }, + content: { + title: 'Content', + description: 'Contains the additional information in its properties.', + type: 'object', + minProperties: 1, + properties: { + documentation: { + title: 'Documentation content', + description: + 'Contains a constant string to clarify that this is for documentation purposes only and not to be used in production.', + type: 'string', + const: + 'This extension is for documentation and test purposed only. It is valid. It is not allowed to be used in a production CSAF.', + }, + }, + unevaluatedProperties: false, + }, + critical: { + title: 'Critical', + description: + 'Determines whether using the extension would fail a mandatory test.', + type: 'boolean', + const: false, + }, + }, + additionalProperties: false, +} diff --git a/csaf_2_1/csafAjv/extensionSchemas/documentation-12.js b/csaf_2_1/csafAjv/extensionSchemas/documentation-12.js new file mode 100644 index 00000000..33f8eb88 --- /dev/null +++ b/csaf_2_1/csafAjv/extensionSchemas/documentation-12.js @@ -0,0 +1,68 @@ +// https://raw.githubusercontent.com/oasis-tcs/csaf/refs/heads/master/csaf_2.1/extension/data/valid/documentation-12/documentation-12-content_1.0.0.json +// Copied from ../../../csaf/csaf_2.1/test/extension/data/valid/documentation-12/documentation-12-content_1.0.0.json +export default { + $schema: + 'https://docs.oasis-open.org/csaf/csaf/v2.1/schema/extension-metaschema.json', + $id: 'https://raw.githubusercontent.com/oasis-tcs/csaf/refs/heads/master/csaf_2.1/extension/data/valid/documentation-12/documentation-12-content_1.0.0.json', + title: 'CSAF Extension: Documentation 12', + description: + 'Representation of a valid extension used for documentation purposes.', + type: 'object', + required: ['$schema', 'category', 'critical', 'content'], + properties: { + $schema: { + title: 'CSAF Extension Schema', + description: + 'Contains the URL of the CSAF Extension JSON schema which the JSON object promises to be valid for.', + type: 'string', + format: 'uri', + const: + 'https://raw.githubusercontent.com/oasis-tcs/csaf/refs/heads/master/csaf_2.1/extension/data/valid/documentation-12/documentation-12-content_1.0.0.json', + }, + category: { + title: 'Extension Category', + description: 'Holds the category of the extension content.', + type: 'string', + enum: ['essential', 'significant', 'supplementary'], + }, + content: { + title: 'Content', + description: 'Contains the additional information in its properties.', + type: 'object', + minProperties: 1, + required: ['documentation'], + properties: { + documentation: { + title: 'Documentation content', + description: + 'Contains a constant string to clarify that this is for documentation purposes only and not to be used in production.', + type: 'string', + const: + 'This extension is for documentation and test purposed only. It is valid. It is not allowed to be used in a production CSAF.', + }, + notes: { + title: 'Notes', + description: + 'Contains a list of notes to convey a more complex structure.', + type: 'array', + minItems: 1, + items: { + title: 'Entry', + description: 'Contains a single entry.', + type: 'string', + minLength: 1, + }, + }, + }, + unevaluatedProperties: false, + }, + critical: { + title: 'Critical', + description: + 'Determines whether using the extension would fail a mandatory test.', + type: 'boolean', + const: false, + }, + }, + additionalProperties: false, +} diff --git a/csaf_2_1/csafAjv/extensionSchemas/documentation-13.js b/csaf_2_1/csafAjv/extensionSchemas/documentation-13.js new file mode 100644 index 00000000..343e23f8 --- /dev/null +++ b/csaf_2_1/csafAjv/extensionSchemas/documentation-13.js @@ -0,0 +1,129 @@ +// https://raw.githubusercontent.com/oasis-tcs/csaf/refs/heads/master/csaf_2.1/extension/data/valid/documentation-13/documentation-13-content_1.0.0.json +// Copied from ../../../csaf/csaf_2.1/test/extension/data/valid/documentation-13/documentation-13-content_1.0.0.json +export default { + $schema: + 'https://docs.oasis-open.org/csaf/csaf/v2.1/schema/extension-metaschema.json', + $id: 'https://raw.githubusercontent.com/oasis-tcs/csaf/refs/heads/master/csaf_2.1/extension/data/valid/documentation-13/documentation-13-content_1.0.0.json', + title: 'CSAF Extension: Documentation 13', + description: + 'Representation of a valid extension for configurations used for documentation purposes.', + type: 'object', + $defs: { + configuration_id_t: { + title: 'Reference token for configuration instance', + description: + 'Token required to identify a configuration so that it can be referred to from other parts in the document. There is no predefined or required format for the configuration_id as long as it uniquely identifies a configuration in the context of the current document.', + type: 'string', + minLength: 1, + examples: ['CSAFCID-0001', 'CSAFCID-0002'], + }, + configuration_t: { + title: 'Configuration', + description: + 'Contains the description of a configuration and assigns it a configuration id.', + type: 'object', + required: ['configuration_id', 'details'], + properties: { + configuration_id: { $ref: '#/$defs/configuration_id_t' }, + details: { + title: 'Details of the Configuration', + description: + 'Contains all details as human-readable description on how to identify the configuration.', + type: 'string', + minLength: 1, + }, + }, + additionalProperties: false, + }, + relationship_t: { + title: 'Relationship', + description: + 'Establishes a link between an existing full_product_name_t element and a configuration, allowing the document producer to define a combination that form a new full_product_name entry.', + type: 'object', + required: [ + 'configuration_reference', + 'full_product_name', + 'product_reference', + ], + properties: { + configuration_reference: { + title: 'Configuration Reference', + description: + 'Holds a Configuration ID that refers to the Configuration element, which is referenced as the second element of the relationship.', + $ref: '#/$defs/configuration_id_t', + }, + full_product_name: { + $ref: 'https://docs.oasis-open.org/csaf/csaf/v2.1/schema/csaf.json#/$defs/full_product_name_t', + }, + product_reference: { + title: 'Product reference', + description: + 'Holds a Product ID that refers to the Full Product Name element, which is referenced as the first element of the relationship.', + $ref: 'https://docs.oasis-open.org/csaf/csaf/v2.1/schema/csaf.json#/$defs/product_id_t', + }, + }, + additionalProperties: false, + }, + }, + required: ['$schema', 'category', 'critical', 'content'], + properties: { + $schema: { + title: 'CSAF Extension Schema', + description: + 'Contains the URL of the CSAF Extension JSON schema which the JSON object promises to be valid for.', + type: 'string', + format: 'uri', + const: + 'https://raw.githubusercontent.com/oasis-tcs/csaf/refs/heads/master/csaf_2.1/extension/data/valid/documentation-13/documentation-13-content_1.0.0.json', + }, + category: { + title: 'Extension Category', + description: 'Holds the category of the extension content.', + type: 'string', + enum: ['essential', 'significant', 'supplementary'], + }, + content: { + title: 'Content', + description: 'Contains the additional information in its properties.', + type: 'object', + minProperties: 1, + properties: { + configuration_tree: { + title: 'Configuration Tree', + description: + 'Contains configurations and their bindings to products.', + type: 'object', + required: ['configurations', 'relationships'], + properties: { + configurations: { + title: 'List of Configurations', + description: 'Contains a list of configuration elements.', + type: 'array', + minItems: 1, + uniqueItems: true, + items: { $ref: '#/$defs/configuration_t' }, + }, + relationships: { + title: 'List of Relationships', + description: + 'Contains a list of product-configuration combinations.', + type: 'array', + minItems: 1, + uniqueItems: true, + items: { $ref: '#/$defs/relationship_t' }, + }, + }, + }, + }, + unevaluatedProperties: false, + }, + critical: { + title: 'Critical', + description: + 'Determines whether using the extension would fail a mandatory test.', + type: 'boolean', + const: true, + }, + }, + additionalProperties: false, +} diff --git a/csaf_2_1/csafAjv/extensionSchemas/index.js b/csaf_2_1/csafAjv/extensionSchemas/index.js new file mode 100644 index 00000000..b6c4fa53 --- /dev/null +++ b/csaf_2_1/csafAjv/extensionSchemas/index.js @@ -0,0 +1,100 @@ +import documentation11 from './documentation-11.js' +import documentation12 from './documentation-12.js' +import documentation13 from './documentation-13.js' + +/** + * @typedef {object} ExtensionSchemaEntry + * @property {import('ajv').AnySchemaObject} schema + * @property {'official' | 'registered' | 'experimental'} class Extension + * class as defined in spec section 2.4.1 ("Classes"). + * @property {boolean} deprecated Whether this extension is on the OASIS CSAF + * TC's list of deprecated extensions (spec section 2.4.2 "Lists"). Can + * still be used, but support for it will be removed in the near future. + * @property {boolean} denyListed Whether this extension is on the OASIS CSAF + * TC's list of deny-listed extensions (spec section 2.4.2 "Lists"). MUST + * NOT be used. + */ + +/** + * Allow list of CSAF Extension Schemas that this validator explicitly + * implements and supports at runtime. + * + * Per CSAF 2.1 spec chapter 8 ("Safety, Security, and Data Protection + * Considerations"), CSAF validators SHOULD NOT automatically retrieve JSON + * schemas from a URL declared in a CSAF document, and SHOULD instead keep a + * local copy of all schemas necessary to fulfill their tasks. Therefore, no + * schema referenced by an `x_extensions[].$schema` value is ever fetched over + * the network - only the schemas listed here are used to validate mandatory + * test 6.1.60.2. + * + * The `class`/`deprecated`/`denyListed` fields mirror the OASIS CSAF TC's + * lists described in spec section 2.4.2 ("Lists"). Those lists are not + * (yet) available as real, importable data in this repository - the entries + * below are currently limited to the OASIS documentation-11/12/13 test + * fixtures, which are themselves explicitly marked as "not allowed to be + * used in a production CSAF" and are therefore classified as `experimental`. + * The fields are still modeled explicitly so that real official/registered/ + * deprecated/deny-listed extensions can be added later without having to + * change any consumer of this module. + * + * To add support for a new CSAF Extension Schema: + * 1. Add a new module next to this file that exports the schema (its `$id` + * MUST match the `$schema` value that CSAF documents will declare). + * 2. Import it here and add an entry to `extensionSchemas` below with its + * class and deprecated/deny-listed status. + * + * @type {ExtensionSchemaEntry[]} + */ +export const extensionSchemas = [ + { + schema: documentation11, + class: 'experimental', + deprecated: false, + denyListed: false, + }, + { + schema: documentation12, + class: 'experimental', + deprecated: false, + denyListed: false, + }, + { + schema: documentation13, + class: 'experimental', + deprecated: false, + denyListed: false, + }, +] + +/** + * Registers all allow-listed extension schemas on the given Ajv instance so + * that `ajv.getSchema(schemaUrl)` resolves them without any network access. + * + * @param {import('ajv').default} ajv + */ +export function registerExtensionSchemas(ajv) { + for (const { schema } of extensionSchemas) { + ajv.addSchema(schema, schema.$id) + } +} + +/** + * Classifies a CSAF Extension by its declared `$schema` URL, used to + * differentiate the warning raised for extensions that are not in the allow + * list above (see mandatoryTest_6_1_60_2). + * + * For allow-listed schemas, this returns the explicit class recorded in + * `extensionSchemas`. For any other schema, the class cannot be determined + * reliably: this validator does not have access to the OASIS CSAF TC's real + * lists of official/registered/deprecated/deny-listed extensions (spec + * section 2.4.2 "Lists"), which are not available as data in this + * repository, so guessing from the URL alone (e.g. by host name) would be + * misleading. `'unknown'` is returned in that case instead. + * + * @param {string} schemaUrl + * @returns {'official' | 'registered' | 'experimental' | 'unknown'} + */ +export function classifyExtensionSchema(schemaUrl) { + const entry = extensionSchemas.find((e) => e.schema.$id === schemaUrl) + return entry?.class ?? 'unknown' +} diff --git a/csaf_2_1/recommendedTests.js b/csaf_2_1/recommendedTests.js index 714e3612..1a6f8837 100644 --- a/csaf_2_1/recommendedTests.js +++ b/csaf_2_1/recommendedTests.js @@ -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' diff --git a/csaf_2_1/recommendedTests/recommendedTest_6_2_20.js b/csaf_2_1/recommendedTests/recommendedTest_6_2_20.js index b47b588a..bc2d0ccf 100644 --- a/csaf_2_1/recommendedTests/recommendedTest_6_2_20.js +++ b/csaf_2_1/recommendedTests/recommendedTest_6_2_20.js @@ -1,8 +1,70 @@ -import { optionalTest_6_2_20 } from '../../optionalTests.js' +import schema from '../schemaTests/csaf_2_1_strict/schema.js' +import csafAjv from '../csafAjv.js' +import { walkPath } from '../../lib/walkPaths.js' +import { classifyExtensionSchema } from '../csafAjv/extensionSchemas/index.js' + +const validateStrictSchema = csafAjv.compile(schema) + +const X_EXTENSIONS_PATHS = /** @type {const} */ ([ + '/document/x_extensions[]', + '/product_tree/branches[*]/product/x_extensions[]', + '/product_tree/full_product_names[]/x_extensions[]', + '/product_tree/product_paths[]/full_product_name/x_extensions[]', + '/vulnerabilities[]/metrics[]/content/x_extensions[]', + '/vulnerabilities[]/x_extensions[]', + '/x_extensions[]', +]) /** + * 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) +export async function recommendedTest_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 + for (const path of X_EXTENSIONS_PATHS) { + await walkPath(doc, path, async (instancePath, value) => { + const schemaUrl = + value && typeof value === 'object' && '$schema' in value + ? value.$schema + : undefined + + if (typeof schemaUrl !== 'string') return + + const isSupported = typeof csafAjv.getSchema(schemaUrl) === 'function' + if (isSupported) return + + ctx.warnings.push({ + instancePath: `${instancePath}/$schema`, + message: `unsupported CSAF Extension of schema "${schemaUrl}" (class: ${classifyExtensionSchema( + schemaUrl + )})`, + }) + }) + } + + return ctx } diff --git a/tests/csaf_2_1/oasis.js b/tests/csaf_2_1/oasis.js index 3cd9fcaf..e8dfbbbc 100644 --- a/tests/csaf_2_1/oasis.js +++ b/tests/csaf_2_1/oasis.js @@ -17,7 +17,6 @@ const excluded = [ '6.1.59', '6.1.60.2', '6.1.60.3', - '6.2.20', '6.2.24', '6.2.26', '6.2.31', diff --git a/tests/csaf_2_1/recommendedTest_6_2_20.js b/tests/csaf_2_1/recommendedTest_6_2_20.js new file mode 100644 index 00000000..ff86a39e --- /dev/null +++ b/tests/csaf_2_1/recommendedTest_6_2_20.js @@ -0,0 +1,122 @@ +import { recommendedTest_6_2_20 } from '../../csaf_2_1/recommendedTests.js' + +const SCHEMA_URL = 'https://example.com/my-extension/schema/1.0.0.json' + +describe('recommendedTest_6_2_20', function () { + it('only runs on relevant documents', async function () { + expect( + (await recommendedTest_6_2_20({ vulnerabilities: 'mydoc' })).warnings + .length + ).to.equal(0) + }) + + it('warns for x_extensions at root level', async function () { + const doc = { + x_extensions: [{ $schema: SCHEMA_URL }], + } + const result = await recommendedTest_6_2_20(doc) + expect(result.warnings.length).to.equal(1) + expect(result.warnings[0].instancePath).to.equal('/x_extensions/0/$schema') + expect(result.warnings[0].message).to.contain(SCHEMA_URL) + expect(result.warnings[0].message).to.contain('class: unknown') + }) + + it('warns for x_extensions in document', async function () { + const doc = { + document: { + x_extensions: [{ $schema: SCHEMA_URL }], + }, + } + const result = await recommendedTest_6_2_20(doc) + expect(result.warnings.length).to.equal(1) + expect(result.warnings[0].instancePath).to.equal( + '/document/x_extensions/0/$schema' + ) + }) + + it('warns for x_extensions in product_tree/full_product_names', async function () { + const doc = { + product_tree: { + full_product_names: [{ x_extensions: [{ $schema: SCHEMA_URL }] }], + }, + } + const result = await recommendedTest_6_2_20(doc) + expect(result.warnings.length).to.equal(1) + expect(result.warnings[0].instancePath).to.equal( + '/product_tree/full_product_names/0/x_extensions/0/$schema' + ) + }) + + it('warns for x_extensions in product_tree/branches', async function () { + const doc = { + product_tree: { + branches: [{ product: { x_extensions: [{ $schema: SCHEMA_URL }] } }], + }, + } + const result = await recommendedTest_6_2_20(doc) + expect(result.warnings.length).to.equal(1) + expect(result.warnings[0].instancePath).to.equal( + '/product_tree/branches/0/product/x_extensions/0/$schema' + ) + }) + + it('warns for x_extensions in nested product_tree/branches', async function () { + const doc = { + product_tree: { + branches: [ + { + branches: [ + { product: { x_extensions: [{ $schema: SCHEMA_URL }] } }, + ], + }, + ], + }, + } + const result = await recommendedTest_6_2_20(doc) + expect(result.warnings.length).to.equal(1) + expect(result.warnings[0].instancePath).to.equal( + '/product_tree/branches/0/branches/0/product/x_extensions/0/$schema' + ) + }) + + it('warns for x_extensions in product_tree/product_paths', async function () { + const doc = { + product_tree: { + product_paths: [ + { full_product_name: { x_extensions: [{ $schema: SCHEMA_URL }] } }, + ], + }, + } + const result = await recommendedTest_6_2_20(doc) + expect(result.warnings.length).to.equal(1) + expect(result.warnings[0].instancePath).to.equal( + '/product_tree/product_paths/0/full_product_name/x_extensions/0/$schema' + ) + }) + + it('warns for x_extensions in vulnerabilities', async function () { + const doc = { + vulnerabilities: [{ x_extensions: [{ $schema: SCHEMA_URL }] }], + } + const result = await recommendedTest_6_2_20(doc) + expect(result.warnings.length).to.equal(1) + expect(result.warnings[0].instancePath).to.equal( + '/vulnerabilities/0/x_extensions/0/$schema' + ) + }) + + it('warns for x_extensions in vulnerabilities/metrics/content', async function () { + const doc = { + vulnerabilities: [ + { + metrics: [{ content: { x_extensions: [{ $schema: SCHEMA_URL }] } }], + }, + ], + } + const result = await recommendedTest_6_2_20(doc) + expect(result.warnings.length).to.equal(1) + expect(result.warnings[0].instancePath).to.equal( + '/vulnerabilities/0/metrics/0/content/x_extensions/0/$schema' + ) + }) +})