From ec2662979bdafbb92bdfc8b4a157b129e9f79445 Mon Sep 17 00:00:00 2001 From: bendo-eXX Date: Mon, 6 Oct 2025 14:39:48 +0200 Subject: [PATCH 1/5] feat(CSAF2.1): #302 add recommendedTest_6_2_20.js --- README.md | 2 +- csaf_2_1/csafAjv.js | 6 ++--- csaf_2_1/csafAjv/cvss-v2.0.js | 1 + csaf_2_1/csafAjv/cvss-v3.0.js | 1 + csaf_2_1/csafAjv/cvss-v3.1.js | 1 + csaf_2_1/csafAjv/cvss-v4.0.js | 7 ++++++ .../recommendedTest_6_2_20.js | 25 +++++++++++++++++-- tests/csaf_2_1/oasis.js | 1 - 8 files changed, 37 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 6f4f9297..dbbe55f7 100644 --- a/README.md +++ b/README.md @@ -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.21 - Recommended Test 6.2.24 - Recommended Test 6.2.25 @@ -459,6 +458,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_22: DocumentTest export const recommendedTest_6_2_23: DocumentTest ``` diff --git a/csaf_2_1/csafAjv.js b/csaf_2_1/csafAjv.js index 833f8bb0..f37d48f2 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 from './csafAjv/cvss-v4.0.js' import meta from './csafAjv/meta.js' import formatAssertion from './csafAjv/format-assertion.js' 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/cvss-v4.0.js b/csaf_2_1/csafAjv/cvss-v4.0.js index 4bf575fc..feef2db5 100644 --- a/csaf_2_1/csafAjv/cvss-v4.0.js +++ b/csaf_2_1/csafAjv/cvss-v4.0.js @@ -25,6 +25,7 @@ export default { title: 'JSON Schema for Common Vulnerability Scoring System version 4.0', $id: 'https://www.first.org/cvss/cvss-v4.0.json?20240216', type: 'object', + additionalProperties: false, definitions: { attackVectorType: { type: 'string', @@ -190,6 +191,12 @@ export default { pattern: '^CVSS:4[.]0/AV:[NALP]/AC:[LH]/AT:[NP]/PR:[NLH]/UI:[NPA]/VC:[HLN]/VI:[HLN]/VA:[HLN]/SC:[HLN]/SI:[HLN]/SA:[HLN](/E:[XAPU])?(/CR:[XHML])?(/IR:[XHML])?(/AR:[XHML])?(/MAV:[XNALP])?(/MAC:[XLH])?(/MAT:[XNP])?(/MPR:[XNLH])?(/MUI:[XNPA])?(/MVC:[XNLH])?(/MVI:[XNLH])?(/MVA:[XNLH])?(/MSC:[XNLH])?(/MSI:[XNLHS])?(/MSA:[XNLHS])?(/S:[XNP])?(/AU:[XNY])?(/R:[XAUI])?(/V:[XDC])?(/RE:[XLMH])?(/U:(X|Clear|Green|Amber|Red))?$', }, + baseScore: { $ref: '#/definitions/noneScoreType' }, + baseSeverity: { $ref: '#/definitions/noneSeverityType' }, + threatScore: { $ref: '#/definitions/noneScoreType' }, + threatSeverity: { $ref: '#/definitions/noneSeverityType' }, + environmentalScore: { $ref: '#/definitions/noneScoreType' }, + environmentalSeverity: { $ref: '#/definitions/noneSeverityType' }, attackVector: { $ref: '#/definitions/attackVectorType' }, attackComplexity: { $ref: '#/definitions/attackComplexityType' }, attackRequirements: { $ref: '#/definitions/attackRequirementsType' }, diff --git a/csaf_2_1/recommendedTests/recommendedTest_6_2_20.js b/csaf_2_1/recommendedTests/recommendedTest_6_2_20.js index b47b588a..b60f5372 100644 --- a/csaf_2_1/recommendedTests/recommendedTest_6_2_20.js +++ b/csaf_2_1/recommendedTests/recommendedTest_6_2_20.js @@ -1,8 +1,29 @@ -import { optionalTest_6_2_20 } from '../../optionalTests.js' +import schema from '../schemaTests/csaf_2_1_strict/schema.js' +import csafAjv from '../csafAjv.js' + +const validateStrictSchema = csafAjv.compile(schema) /** * @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 }>} */ ([]), + } + + if (!validateStrictSchema(doc)) { + const additionalPropertiesErrors = + validateStrictSchema.errors?.filter( + (e) => e.keyword === 'additionalProperties' + ) ?? [] + for (const error of additionalPropertiesErrors) { + ctx.warnings.push({ + instancePath: error.instancePath, + message: error.message ?? '', + }) + } + } + + return ctx } diff --git a/tests/csaf_2_1/oasis.js b/tests/csaf_2_1/oasis.js index 0e9d2e60..100890b1 100644 --- a/tests/csaf_2_1/oasis.js +++ b/tests/csaf_2_1/oasis.js @@ -34,7 +34,6 @@ const excluded = [ '6.1.56', '6.2.11', '6.2.19', - '6.2.20', '6.2.21', '6.2.24', '6.2.25', From 029cc48d65fcafcbee6e9088172a1d59ba2b7b2f Mon Sep 17 00:00:00 2001 From: bendo-eXX Date: Tue, 24 Mar 2026 17:02:28 +0100 Subject: [PATCH 2/5] fix(CSAF2.1): change additionalProperties to unevaluatedProperties --- csaf_2_1/csafAjv/cvss-v4.0.1.js | 6 +----- csaf_2_1/recommendedTests/recommendedTest_6_2_20.js | 10 +++++++--- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/csaf_2_1/csafAjv/cvss-v4.0.1.js b/csaf_2_1/csafAjv/cvss-v4.0.1.js index 3fb46aea..84da9da7 100644 --- a/csaf_2_1/csafAjv/cvss-v4.0.1.js +++ b/csaf_2_1/csafAjv/cvss-v4.0.1.js @@ -26,7 +26,7 @@ export default { 'JSON Schema for Common Vulnerability Scoring System version 4.0, Revision 1', $id: 'https://www.first.org/cvss/cvss-v4.0.1.json?20250704', type: 'object', - additionalProperties: false, + unevaluatedProperties: false, definitions: { attackVectorType: { type: 'string', @@ -194,10 +194,6 @@ export default { }, baseScore: { $ref: '#/definitions/noneScoreType' }, baseSeverity: { $ref: '#/definitions/noneSeverityType' }, - threatScore: { $ref: '#/definitions/noneScoreType' }, - threatSeverity: { $ref: '#/definitions/noneSeverityType' }, - environmentalScore: { $ref: '#/definitions/noneScoreType' }, - environmentalSeverity: { $ref: '#/definitions/noneSeverityType' }, attackVector: { $ref: '#/definitions/attackVectorType' }, attackComplexity: { $ref: '#/definitions/attackComplexityType' }, attackRequirements: { $ref: '#/definitions/attackRequirementsType' }, diff --git a/csaf_2_1/recommendedTests/recommendedTest_6_2_20.js b/csaf_2_1/recommendedTests/recommendedTest_6_2_20.js index b60f5372..e2240232 100644 --- a/csaf_2_1/recommendedTests/recommendedTest_6_2_20.js +++ b/csaf_2_1/recommendedTests/recommendedTest_6_2_20.js @@ -15,12 +15,16 @@ export function recommendedTest_6_2_20(doc) { if (!validateStrictSchema(doc)) { const additionalPropertiesErrors = validateStrictSchema.errors?.filter( - (e) => e.keyword === 'additionalProperties' + (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, - message: error.message ?? '', + instancePath: `${error.instancePath}/${propertyName}`, + message: `property "${propertyName}" is not defined in the schema`, }) } } From edde8756158acb2d861a6808d983fa0b08b9d69c Mon Sep 17 00:00:00 2001 From: bendo-eXX Date: Tue, 24 Mar 2026 17:08:47 +0100 Subject: [PATCH 3/5] fix(CSAF2.1): change additionalProperties to unevaluatedProperties --- tests/csaf_2_1/oasis.js | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/csaf_2_1/oasis.js b/tests/csaf_2_1/oasis.js index 47e0b21e..4ff31671 100644 --- a/tests/csaf_2_1/oasis.js +++ b/tests/csaf_2_1/oasis.js @@ -33,7 +33,6 @@ const excluded = [ '6.1.59', '6.2.11', '6.2.19', - '6.2.21', '6.2.24', '6.2.26', '6.2.31', From b70d9e0d94a853f41ce6f5844bbb9ab52991395a Mon Sep 17 00:00:00 2001 From: bendo-eXX Date: Fri, 29 May 2026 10:19:12 +0200 Subject: [PATCH 4/5] feat(CSAF2.1): fix imports --- csaf_2_1/csafAjv.js | 6 +++--- csaf_2_1/recommendedTests.js | 1 + 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/csaf_2_1/csafAjv.js b/csaf_2_1/csafAjv.js index 44da8f36..deb59d05 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' diff --git a/csaf_2_1/recommendedTests.js b/csaf_2_1/recommendedTests.js index 46213ccb..b058567b 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' From 913814fe7b881ad11d143ac0c2513967f6251458 Mon Sep 17 00:00:00 2001 From: bendo-eXX Date: Fri, 29 May 2026 14:06:53 +0200 Subject: [PATCH 5/5] feat(CSAF2.1): extend the test to include x_extensions --- .../recommendedTest_6_2_20.js | 187 ++++++++++++++++++ tests/csaf_2_1/recommendedTest_6_2_20.js | 136 +++++++++++++ 2 files changed, 323 insertions(+) create mode 100644 tests/csaf_2_1/recommendedTest_6_2_20.js diff --git a/csaf_2_1/recommendedTests/recommendedTest_6_2_20.js b/csaf_2_1/recommendedTests/recommendedTest_6_2_20.js index e2240232..7b8a2fa9 100644 --- a/csaf_2_1/recommendedTests/recommendedTest_6_2_20.js +++ b/csaf_2_1/recommendedTests/recommendedTest_6_2_20.js @@ -1,9 +1,97 @@ 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} Branch + * @typedef {import('ajv/dist/core.js').JTDDataType} 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) { @@ -12,6 +100,7 @@ export function recommendedTest_6_2_20(doc) { /** @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( @@ -29,5 +118,103 @@ export function recommendedTest_6_2_20(doc) { } } + // 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`) + } + }) +} 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..2f27f9b8 --- /dev/null +++ b/tests/csaf_2_1/recommendedTest_6_2_20.js @@ -0,0 +1,136 @@ +import assert from 'node:assert' +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', function () { + assert.equal( + recommendedTest_6_2_20({ vulnerabilities: 'mydoc' }).warnings.length, + 0 + ) + }) + + it('returns no warning when $schema is not a string (root x_extensions)', function () { + const doc = { + x_extensions: [{ category: { type: 'string' } }], + } + const result = recommendedTest_6_2_20(doc) + assert.equal(result.warnings.length, 0) + }) + + it('warns for x_extensions at root level', function () { + const doc = { + x_extensions: [{ $schema: SCHEMA_URL }], + } + const result = recommendedTest_6_2_20(doc) + assert.equal(result.warnings.length, 1) + assert.equal(result.warnings[0].instancePath, '/x_extensions/0/$schema') + }) + + it('warns for x_extensions in document', function () { + const doc = { + document: { + x_extensions: [{ $schema: SCHEMA_URL }], + }, + } + const result = recommendedTest_6_2_20(doc) + assert.equal(result.warnings.length, 1) + assert.equal( + result.warnings[0].instancePath, + '/document/x_extensions/0/$schema' + ) + }) + + it('warns for x_extensions in product_tree/full_product_names', function () { + const doc = { + product_tree: { + full_product_names: [{ x_extensions: [{ $schema: SCHEMA_URL }] }], + }, + } + const result = recommendedTest_6_2_20(doc) + assert.equal(result.warnings.length, 1) + assert.equal( + result.warnings[0].instancePath, + '/product_tree/full_product_names/0/x_extensions/0/$schema' + ) + }) + + it('warns for x_extensions in product_tree/branches', function () { + const doc = { + product_tree: { + branches: [{ product: { x_extensions: [{ $schema: SCHEMA_URL }] } }], + }, + } + const result = recommendedTest_6_2_20(doc) + assert.equal(result.warnings.length, 1) + assert.equal( + result.warnings[0].instancePath, + '/product_tree/branches/0/product/x_extensions/0/$schema' + ) + }) + + it('warns for x_extensions in nested product_tree/branches', function () { + const doc = { + product_tree: { + branches: [ + { + branches: [ + { product: { x_extensions: [{ $schema: SCHEMA_URL }] } }, + ], + }, + ], + }, + } + const result = recommendedTest_6_2_20(doc) + assert.equal(result.warnings.length, 1) + assert.equal( + result.warnings[0].instancePath, + '/product_tree/branches/0/branches/0/product/x_extensions/0/$schema' + ) + }) + + it('warns for x_extensions in product_tree/product_paths', function () { + const doc = { + product_tree: { + product_paths: [ + { full_product_name: { x_extensions: [{ $schema: SCHEMA_URL }] } }, + ], + }, + } + const result = recommendedTest_6_2_20(doc) + assert.equal(result.warnings.length, 1) + assert.equal( + result.warnings[0].instancePath, + '/product_tree/product_paths/0/full_product_name/x_extensions/0/$schema' + ) + }) + + it('warns for x_extensions in vulnerabilities', function () { + const doc = { + vulnerabilities: [{ x_extensions: [{ $schema: SCHEMA_URL }] }], + } + const result = recommendedTest_6_2_20(doc) + assert.equal(result.warnings.length, 1) + assert.equal( + result.warnings[0].instancePath, + '/vulnerabilities/0/x_extensions/0/$schema' + ) + }) + + it('warns for x_extensions in vulnerabilities/metrics/content', function () { + const doc = { + vulnerabilities: [ + { + metrics: [{ content: { x_extensions: [{ $schema: SCHEMA_URL }] } }], + }, + ], + } + const result = recommendedTest_6_2_20(doc) + assert.equal(result.warnings.length, 1) + assert.equal( + result.warnings[0].instancePath, + '/vulnerabilities/0/metrics/0/content/x_extensions/0/$schema' + ) + }) +})