From b38b34edb8074b0c4b2fb4fc3715435f05880a17 Mon Sep 17 00:00:00 2001 From: bendo-eXX Date: Thu, 16 Jul 2026 12:48:40 +0200 Subject: [PATCH 1/2] feat(CSAF2.1): add mandatoryTest_6_1_60.2.js --- README.md | 2 +- csaf_2_1/csafAjv.js | 12 +++- csaf_2_1/csafAjv/content_schema.js | 2 +- csaf_2_1/csafAjv/extension-content.js | 2 +- .../csafAjv/extension-documentation-11.js | 54 +++++++++++++++ .../csafAjv/extension-documentation-12.js | 68 +++++++++++++++++++ csaf_2_1/mandatoryTests.js | 1 + .../mandatoryTests/mandatoryTest_6_1_60_2.js | 59 ++++++++++++++++ tests/csaf_2_1/mandatoryTest_6_1_60_2.js | 36 ++++++++++ tests/csaf_2_1/oasis.js | 1 - 10 files changed, 232 insertions(+), 5 deletions(-) create mode 100644 csaf_2_1/csafAjv/extension-documentation-11.js create mode 100644 csaf_2_1/csafAjv/extension-documentation-12.js create mode 100644 csaf_2_1/mandatoryTests/mandatoryTest_6_1_60_2.js create mode 100644 tests/csaf_2_1/mandatoryTest_6_1_60_2.js diff --git a/README.md b/README.md index 2910de45..3cab57bf 100644 --- a/README.md +++ b/README.md @@ -321,7 +321,6 @@ The following tests are not yet implemented and therefore missing: - Mandatory Test 6.1.55 - Mandatory Test 6.1.59 - Mandatory Test 6.1.60.1 -- Mandatory Test 6.1.60.2 - Mandatory Test 6.1.60.3 **Recommended Tests** @@ -462,6 +461,7 @@ export const mandatoryTest_6_1_52: DocumentTest export const mandatoryTest_6_1_53: DocumentTest export const mandatoryTest_6_1_57: DocumentTest export const mandatoryTest_6_1_58: DocumentTest +export const mandatoryTest_6_1_60_2: DocumentTest export const mandatoryTest_6_1_61: DocumentTest ``` diff --git a/csaf_2_1/csafAjv.js b/csaf_2_1/csafAjv.js index 9c05c814..53616125 100644 --- a/csaf_2_1/csafAjv.js +++ b/csaf_2_1/csafAjv.js @@ -5,6 +5,8 @@ import cvss_v3_0 from '../schemas/cvss-v3.0.js' import cvss_v3_1 from '../schemas/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 extension_documentation_11 from './csafAjv/extension-documentation-11.js' +import extension_documentation_12 from './csafAjv/extension-documentation-12.js' import content_schema from './csafAjv/content_schema.js' import csaf_meta from './csafAjv/csaf_meta.js' import cvss_meta from './csafAjv/cvss_meta.js' @@ -26,7 +28,7 @@ csafAjv.addSchema(cvss_v3_1, 'https://www.first.org/cvss/cvss-v3.1.json') csafAjv.addSchema(cvss_meta, 'https://www.first.org/cvss/meta.json') csafAjv.addSchema( content_schema, - 'https://docs.oasis-open.org/csaf/csaf/v2.1/schema/extension-metaschema.json#/$defs/content_schema_t' + 'https://docs.oasis-open.org/csaf/csaf/v2.1/schema/extension-metaschema.json' ) csafAjv.addSchema( meta_format_assertion, @@ -41,6 +43,14 @@ csafAjv.addSchema( extension_content, 'https://docs.oasis-open.org/csaf/csaf/v2.1/schema/extension-content.json' ) +csafAjv.addSchema( + extension_documentation_11, + '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' +) +csafAjv.addSchema( + extension_documentation_12, + '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' +) csafAjv.addSchema( selectionList_2_0_0Schema, 'https://certcc.github.io/SSVC/data/schema/v2/SelectionList_2_0_0.schema.json' diff --git a/csaf_2_1/csafAjv/content_schema.js b/csaf_2_1/csafAjv/content_schema.js index a985c9bb..cdee0f62 100644 --- a/csaf_2_1/csafAjv/content_schema.js +++ b/csaf_2_1/csafAjv/content_schema.js @@ -194,7 +194,7 @@ export default { uniqueItems: true, items: { type: 'string', - enum: ['critical', 'high_value', 'informational'], + enum: ['essential', 'significant', 'supplementary'], }, }, }, diff --git a/csaf_2_1/csafAjv/extension-content.js b/csaf_2_1/csafAjv/extension-content.js index 9a79f18c..031fc052 100644 --- a/csaf_2_1/csafAjv/extension-content.js +++ b/csaf_2_1/csafAjv/extension-content.js @@ -18,7 +18,7 @@ export default { title: 'Extension Category', description: 'Holds the category of the extension content.', type: 'string', - enum: ['critical', 'high_value', 'informational'], + enum: ['essential', 'significant', 'supplementary'], }, content: { title: 'Content', diff --git a/csaf_2_1/csafAjv/extension-documentation-11.js b/csaf_2_1/csafAjv/extension-documentation-11.js new file mode 100644 index 00000000..c8a9125d --- /dev/null +++ b/csaf_2_1/csafAjv/extension-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 +// A CSAF Extension Schema registered/known to this implementation. It is used +// by the OASIS CSAF conformance test suite to exercise mandatory test 6.1.60.2 +// ("Extension Schema"). +export default { + $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/extension-documentation-12.js b/csaf_2_1/csafAjv/extension-documentation-12.js new file mode 100644 index 00000000..06eabbc3 --- /dev/null +++ b/csaf_2_1/csafAjv/extension-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 +// A CSAF Extension Schema registered/known to this implementation. It is used +// by the OASIS CSAF conformance test suite to exercise mandatory test 6.1.60.2 +// ("Extension Schema"). +export default { + $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/mandatoryTests.js b/csaf_2_1/mandatoryTests.js index bf9715a3..5ef032fd 100644 --- a/csaf_2_1/mandatoryTests.js +++ b/csaf_2_1/mandatoryTests.js @@ -68,4 +68,5 @@ export { mandatoryTest_6_1_52 } from './mandatoryTests/mandatoryTest_6_1_52.js' export { mandatoryTest_6_1_53 } from './mandatoryTests/mandatoryTest_6_1_53.js' export { mandatoryTest_6_1_57 } from './mandatoryTests/mandatoryTest_6_1_57.js' export { mandatoryTest_6_1_58 } from './mandatoryTests/mandatoryTest_6_1_58.js' +export { mandatoryTest_6_1_60_2 } from './mandatoryTests/mandatoryTest_6_1_60_2.js' export { mandatoryTest_6_1_61 } from './mandatoryTests/mandatoryTest_6_1_61.js' diff --git a/csaf_2_1/mandatoryTests/mandatoryTest_6_1_60_2.js b/csaf_2_1/mandatoryTests/mandatoryTest_6_1_60_2.js new file mode 100644 index 00000000..b3e0cb7e --- /dev/null +++ b/csaf_2_1/mandatoryTests/mandatoryTest_6_1_60_2.js @@ -0,0 +1,59 @@ +import { walkPath } from '../../lib/walkPaths.js' +import csafAjv from '../csafAjv.js' + +const X_EXTENSIONS_PATHS /** @type {string[]} */ = [ + '/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 mandatory test 6.1.60.2 of the CSAF 2.1 standard. + * + * @param {unknown} doc + */ +export async function mandatoryTest_6_1_60_2(doc) { + const ctx = { + errors: + /** @type {Array<{ instancePath: string; message: string }>} */ ([]), + warnings: + /** @type {Array<{ instancePath: string; message: string }>} */ ([]), + isValid: true, + } + + for (const path of X_EXTENSIONS_PATHS) { + await walkPath(doc, path, async (instancePath, value) => { + const schemaUrl = + value && typeof value === 'object' && '$schema' in value + ? /** @type {{ $schema: unknown }} */ (value).$schema + : undefined + + if (typeof schemaUrl !== 'string') return + const validateDeclaredSchema = csafAjv.getSchema(schemaUrl) + + if (typeof validateDeclaredSchema !== 'function') { + ctx.warnings.push({ + instancePath, + message: `declared CSAF Extension Schema "${schemaUrl}" is not supported and could not be validated`, + }) + return + } + + if (!validateDeclaredSchema(value)) { + ctx.isValid = false + validateDeclaredSchema.errors?.forEach((err) => { + ctx.errors.push({ + instancePath: `${instancePath}${err.instancePath}`, + message: err.message ?? 'invalid extension content', + }) + }) + } + }) + } + + return ctx +} diff --git a/tests/csaf_2_1/mandatoryTest_6_1_60_2.js b/tests/csaf_2_1/mandatoryTest_6_1_60_2.js new file mode 100644 index 00000000..c364a45d --- /dev/null +++ b/tests/csaf_2_1/mandatoryTest_6_1_60_2.js @@ -0,0 +1,36 @@ +import assert from 'node:assert/strict' +import { mandatoryTest_6_1_60_2 } from '../../csaf_2_1/mandatoryTests.js' + +describe('mandatoryTest_6_1_60_2', function () { + it('reports a warning when the declared schema is unknown', async function () { + const result = await mandatoryTest_6_1_60_2({ + x_extensions: [ + { + $schema: 'https://example.com/csaf/extension/unknown_1.0.0.json', + category: 'supplementary', + content: { note: 'unknown schema' }, + critical: false, + }, + ], + }) + + assert.equal(result.isValid, true) + assert.equal(result.errors.length, 0) + assert.equal(result.warnings.length, 1) + assert.equal(result.warnings[0].instancePath, '/x_extensions/0') + assert.match( + result.warnings[0].message, + /https:\/\/example\.com\/csaf\/extension\/unknown_1\.0\.0\.json/ + ) + }) + + it('skips extensions that have no $schema property', async function () { + const result = await mandatoryTest_6_1_60_2({ + x_extensions: [{ category: 'supplementary', content: {} }], + }) + + assert.equal(result.isValid, true) + assert.equal(result.errors.length, 0) + assert.equal(result.warnings.length, 0) + }) +}) diff --git a/tests/csaf_2_1/oasis.js b/tests/csaf_2_1/oasis.js index f6b8d926..5490cc92 100644 --- a/tests/csaf_2_1/oasis.js +++ b/tests/csaf_2_1/oasis.js @@ -23,7 +23,6 @@ const excluded = [ '6.1.56', '6.1.59', '6.1.60.1', - '6.1.60.2', '6.1.60.3', '6.2.11', '6.2.19', From 6afb8c4e08889df39aefa8697b15bd1277be6d2c Mon Sep 17 00:00:00 2001 From: bendo-eXX Date: Mon, 20 Jul 2026 15:05:00 +0200 Subject: [PATCH 2/2] feat(CSAF2.1): add dynamic ajv loadSchema --- csaf_2_1/csafAjv.js | 75 ++++++++++++++++--- .../csafAjv/extension-documentation-11.js | 54 ------------- .../csafAjv/extension-documentation-12.js | 68 ----------------- .../mandatoryTests/mandatoryTest_6_1_60_2.js | 18 +++-- tests/csaf_2_1/oasis.js | 48 ++++++++++++ 5 files changed, 124 insertions(+), 139 deletions(-) delete mode 100644 csaf_2_1/csafAjv/extension-documentation-11.js delete mode 100644 csaf_2_1/csafAjv/extension-documentation-12.js diff --git a/csaf_2_1/csafAjv.js b/csaf_2_1/csafAjv.js index 53616125..a4de87e6 100644 --- a/csaf_2_1/csafAjv.js +++ b/csaf_2_1/csafAjv.js @@ -1,12 +1,11 @@ import addFormats from 'ajv-formats' import { Ajv2020 } from 'ajv/dist/2020.js' +import { request } from 'undici' 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_v4_0_0 from './csafAjv/cvss-v4.0.0.js' import extension_content from './csafAjv/extension-content.js' -import extension_documentation_11 from './csafAjv/extension-documentation-11.js' -import extension_documentation_12 from './csafAjv/extension-documentation-12.js' import content_schema from './csafAjv/content_schema.js' import csaf_meta from './csafAjv/csaf_meta.js' import cvss_meta from './csafAjv/cvss_meta.js' @@ -16,7 +15,69 @@ import selectionList_2_0_0Schema from './csafAjv/SelectionList_2_0_0.schema.js' import { validateTimestamp } from './dateHelper.js' -const csafAjv = new Ajv2020({ strict: false, allErrors: true }) +/** + * Cache of in-flight/loaded remote schemas, keyed by URI, so that a schema + * referenced multiple times (e.g. by several `x_extensions` in the same + * document) is only fetched once per process. + * + * @type {Map>} + */ +const remoteSchemaCache = new Map() + +/** + * Loader used by ajv to resolve `$ref`s that point to schemas which are not + * already registered via `addSchema` above (e.g. CSAF extension schemas + * declared via a document's own `$schema` property). + * + * SECURITY NOTE: `uri` can originate directly from the document being + * validated (attacker-controlled). Restricting the protocol to `https:` + * blocks the obvious SSRF vectors (`file:`, plaintext `http:`), but not + * requests to internal hosts reachable via `https:`. Further hardening + * (request timeout, response size limit, redirect handling) is not + * implemented yet and should be added for security-sensitive/production + * environments. + * + * @param {string} uri + * @returns {Promise} + */ +async function loadSchema(uri) { + const cached = remoteSchemaCache.get(uri) + if (cached) return cached + + const promise = (async () => { + let parsed + try { + parsed = new URL(uri) + } catch { + throw new Error(`Cannot load schema "${uri}": not a valid URL`) + } + if (parsed.protocol !== 'https:') { + throw new Error( + `Cannot load schema "${uri}": only "https:" URLs may be loaded, got "${parsed.protocol}"` + ) + } + + const res = await request(uri, { + method: 'GET', + headers: { Accept: 'application/json' }, + }) + if (res.statusCode < 200 || 400 <= res.statusCode) { + throw new Error( + `Cannot load schema "${uri}": received HTTP status ${res.statusCode}` + ) + } + return /** @type {Promise} */ ( + res.body.json() + ) + })() + + remoteSchemaCache.set(uri, promise) + // Don't keep failed lookups cached - allow a retry on the next call. + promise.catch(() => remoteSchemaCache.delete(uri)) + return promise +} + +const csafAjv = new Ajv2020({ strict: false, allErrors: true, loadSchema }) addFormats.default(csafAjv) csafAjv.addMetaSchema( draft_07_schema, @@ -43,14 +104,6 @@ csafAjv.addSchema( extension_content, 'https://docs.oasis-open.org/csaf/csaf/v2.1/schema/extension-content.json' ) -csafAjv.addSchema( - extension_documentation_11, - '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' -) -csafAjv.addSchema( - extension_documentation_12, - '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' -) csafAjv.addSchema( selectionList_2_0_0Schema, 'https://certcc.github.io/SSVC/data/schema/v2/SelectionList_2_0_0.schema.json' diff --git a/csaf_2_1/csafAjv/extension-documentation-11.js b/csaf_2_1/csafAjv/extension-documentation-11.js deleted file mode 100644 index c8a9125d..00000000 --- a/csaf_2_1/csafAjv/extension-documentation-11.js +++ /dev/null @@ -1,54 +0,0 @@ -// 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 -// A CSAF Extension Schema registered/known to this implementation. It is used -// by the OASIS CSAF conformance test suite to exercise mandatory test 6.1.60.2 -// ("Extension Schema"). -export default { - $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/extension-documentation-12.js b/csaf_2_1/csafAjv/extension-documentation-12.js deleted file mode 100644 index 06eabbc3..00000000 --- a/csaf_2_1/csafAjv/extension-documentation-12.js +++ /dev/null @@ -1,68 +0,0 @@ -// 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 -// A CSAF Extension Schema registered/known to this implementation. It is used -// by the OASIS CSAF conformance test suite to exercise mandatory test 6.1.60.2 -// ("Extension Schema"). -export default { - $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/mandatoryTests/mandatoryTest_6_1_60_2.js b/csaf_2_1/mandatoryTests/mandatoryTest_6_1_60_2.js index b3e0cb7e..6e78acad 100644 --- a/csaf_2_1/mandatoryTests/mandatoryTest_6_1_60_2.js +++ b/csaf_2_1/mandatoryTests/mandatoryTest_6_1_60_2.js @@ -33,14 +33,20 @@ export async function mandatoryTest_6_1_60_2(doc) { : undefined if (typeof schemaUrl !== 'string') return - const validateDeclaredSchema = csafAjv.getSchema(schemaUrl) + let validateDeclaredSchema = csafAjv.getSchema(schemaUrl) if (typeof validateDeclaredSchema !== 'function') { - ctx.warnings.push({ - instancePath, - message: `declared CSAF Extension Schema "${schemaUrl}" is not supported and could not be validated`, - }) - return + try { + validateDeclaredSchema = await csafAjv.compileAsync({ + $ref: schemaUrl, + }) + } catch { + ctx.warnings.push({ + instancePath, + message: `declared CSAF Extension Schema "${schemaUrl}" is not supported and could not be validated`, + }) + return + } } if (!validateDeclaredSchema(value)) { diff --git a/tests/csaf_2_1/oasis.js b/tests/csaf_2_1/oasis.js index 5490cc92..4b645bfd 100644 --- a/tests/csaf_2_1/oasis.js +++ b/tests/csaf_2_1/oasis.js @@ -1,6 +1,7 @@ import { readFile } from 'node:fs/promises' import { readFileSync } from 'node:fs' import assert from 'node:assert/strict' +import { getGlobalDispatcher, MockAgent, setGlobalDispatcher } from 'undici' import * as informative from '../../csaf_2_1/informativeTests.js' import * as recommended from '../../csaf_2_1/recommendedTests.js' import * as mandatory from '../../csaf_2_1/mandatoryTests.js' @@ -131,6 +132,42 @@ const testDataBaseUrl = new URL( import.meta.url ) +const extensionDataBaseUrl = new URL( + '../../csaf/csaf_2.1/test/extension/data/valid/', + import.meta.url +) + +/** + * Mocks GitHub raw-content requests for extension schemas so that tests + * exercising csafAjv's dynamic `loadSchema` mechanism stay hermetic (no real + * network access). + * + * @returns {MockAgent} + */ +function extensionSchemaMockAgent() { + const mockAgent = new MockAgent() + mockAgent.disableNetConnect() + + const pool = mockAgent.get('https://raw.githubusercontent.com') + for (const name of ['documentation-11', 'documentation-12']) { + const content = readFileSync( + new URL(`${name}/${name}-content_1.0.0.json`, extensionDataBaseUrl), + 'utf-8' + ) + pool + .intercept({ + method: 'GET', + path: `/oasis-tcs/csaf/refs/heads/master/csaf_2.1/extension/data/valid/${name}/${name}-content_1.0.0.json`, + }) + .reply(200, content, { + headers: { 'content-type': 'application/json' }, + }) + .persist() + } + + return mockAgent +} + const testCases = /** @type {TestCases} */ ( JSON.parse( await readFile(new URL('testcases.json', testDataBaseUrl), 'utf-8') @@ -143,6 +180,17 @@ for (const [group, t] of testMap) { describe(group, function () { for (const [testId, u] of t) { describe(testId, function () { + const mockedTestIds = new Set(['6.1.60.2']) + if (mockedTestIds.has(testId)) { + const globalDispatcher = getGlobalDispatcher() + before(function () { + setGlobalDispatcher(extensionSchemaMockAgent()) + }) + after(function () { + setGlobalDispatcher(globalDispatcher) + }) + } + for (const [type, testSpecs] of u) { describe(type, function () { for (const testSpec of testSpecs) {