diff --git a/README.md b/README.md index 44bae60c..9b18a683 100644 --- a/README.md +++ b/README.md @@ -373,7 +373,6 @@ The following tests are not yet implemented and therefore missing: - Recommended Test 6.2.50.3 - Recommended Test 6.2.51 - Recommended Test 6.2.54.1 -- Recommended Test 6.2.54.2 - Recommended Test 6.2.54.4 **Informative Tests** @@ -526,6 +525,7 @@ export const recommendedTest_6_2_48: DocumentTest export const recommendedTest_6_2_49: DocumentTest export const recommendedTest_6_2_52: DocumentTest export const recommendedTest_6_2_53: DocumentTest +export const recommendedTest_6_2_54_2: DocumentTest export const recommendedTest_6_2_54_3: DocumentTest ``` diff --git a/csaf_2_1/csafAjv.js b/csaf_2_1/csafAjv.js index 429f5d87..a2b83b50 100644 --- a/csaf_2_1/csafAjv.js +++ b/csaf_2_1/csafAjv.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/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..e28cf5e8 --- /dev/null +++ b/csaf_2_1/csafAjv/extensionSchemas/index.js @@ -0,0 +1,78 @@ +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 Class per + * spec section 2.4.1 ("Classes"). + * @property {boolean} deprecated On the OASIS deprecated list (spec 2.4.2)? + * @property {boolean} denyListed On the OASIS deny list (spec 2.4.2)? MUST + * NOT be used if true. + */ + +/** + * Allow list of CSAF Extension Schemas this validator implements locally + * (per spec ch. 8, schemas are never fetched over the network). + * + * The OASIS official/registered/deprecated/deny-listed lists (spec 2.4.2) + * aren't available as data here, so entries are limited to the + * documentation-11/12/13 test fixtures (not for production use). + * `documentation-11`'s class is fixed by the spec's own prose (recommended + * tests 6.2.54.1/.2/.4): it's the failing example for "neither official nor + * registered", i.e. `experimental`. `documentation-12`/`-13` have no such + * fixture tie-in and are arbitrarily assigned `registered`/`official` so all + * three code paths of recommendedTest_6_2_54_1 are exercised. + * + * To add a new schema: add a module next to this file (its `$id` must match + * the `$schema` value used in documents), import it here, and add an entry. + * + * @type {ExtensionSchemaEntry[]} + */ +export const extensionSchemas = [ + { + schema: documentation11, + class: 'experimental', + deprecated: false, + denyListed: false, + }, + { + schema: documentation12, + class: 'registered', + deprecated: false, + denyListed: false, + }, + { + schema: documentation13, + class: 'official', + 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. Returns the + * class recorded in `extensionSchemas` if allow-listed, otherwise + * `'unknown'` (the real OASIS lists aren't available as data here, so + * guessing from the URL would be misleading). + * + * @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..57e8ef9d 100644 --- a/csaf_2_1/recommendedTests.js +++ b/csaf_2_1/recommendedTests.js @@ -50,4 +50,5 @@ export { recommendedTest_6_2_48 } from './recommendedTests/recommendedTest_6_2_4 export { recommendedTest_6_2_49 } from './recommendedTests/recommendedTest_6_2_49.js' export { recommendedTest_6_2_52 } from './recommendedTests/recommendedTest_6_2_52.js' export { recommendedTest_6_2_53 } from './recommendedTests/recommendedTest_6_2_53.js' +export { recommendedTest_6_2_54_2 } from './recommendedTests/recommendedTest_6_2_54_2.js' export { recommendedTest_6_2_54_3 } from './recommendedTests/recommendedTest_6_2_54_3.js' diff --git a/csaf_2_1/recommendedTests/recommendedTest_6_2_54_2.js b/csaf_2_1/recommendedTests/recommendedTest_6_2_54_2.js new file mode 100644 index 00000000..36bb17e7 --- /dev/null +++ b/csaf_2_1/recommendedTests/recommendedTest_6_2_54_2.js @@ -0,0 +1,44 @@ +import { walkPath } from '../../lib/walkPaths.js' +import { classifyExtensionSchema } from '#csaf_2_1/csafAjv/extensionSchemas/index.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 recommended test 6.2.54.2 of the CSAF 2.1 standard. + * + * @param {unknown} doc + */ +export async function recommendedTest_6_2_54_2(doc) { + const ctx = { + warnings: + /** @type {Array<{ instancePath: string; message: string }>} */ ([]), + } + + 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: String }} */ (value).$schema + : undefined + + if (typeof schemaUrl !== 'string') return + + if (classifyExtensionSchema(schemaUrl) !== 'official') { + ctx.warnings.push({ + instancePath: `${instancePath}`, + message: 'the extension is not an official CSAF Extension', + }) + } + }) + } + + return ctx +} diff --git a/tests/csaf_2_1/oasis.js b/tests/csaf_2_1/oasis.js index 3cd9fcaf..bf413c14 100644 --- a/tests/csaf_2_1/oasis.js +++ b/tests/csaf_2_1/oasis.js @@ -33,7 +33,6 @@ const excluded = [ '6.2.50.3', '6.2.51', '6.2.54.1', - '6.2.54.2', '6.2.54.4', '6.3.12', '6.3.13', diff --git a/tests/csaf_2_1/recommendedTest_6_2_54_2.js b/tests/csaf_2_1/recommendedTest_6_2_54_2.js new file mode 100644 index 00000000..4709e21e --- /dev/null +++ b/tests/csaf_2_1/recommendedTest_6_2_54_2.js @@ -0,0 +1,45 @@ +import { recommendedTest_6_2_54_2 } from '../../csaf_2_1/recommendedTests.js' +import documentation11 from '../../csaf_2_1/csafAjv/extensionSchemas/documentation-11.js' +import documentation13 from '../../csaf_2_1/csafAjv/extensionSchemas/documentation-13.js' + +describe('recommendedTest_6_2_54_2', function () { + it('only runs on relevant documents', async function () { + const result = await recommendedTest_6_2_54_2({}) + expect(result.warnings.length).to.equal(0) + }) + + it('skips extensions that have no $schema property', async function () { + const result = await recommendedTest_6_2_54_2({ + x_extensions: [{ category: 'supplementary', content: {} }], + }) + expect(result.warnings.length).to.equal(0) + }) + + it('warn when the extension schema is not classified as "official"', async function () { + const result = await recommendedTest_6_2_54_2({ + x_extensions: [ + { + $schema: documentation11.$id, + category: 'supplementary', + }, + ], + }) + expect(result.warnings.length).to.equal(1) + expect(result.warnings[0].instancePath).to.equal('/x_extensions/0') + expect(result.warnings[0].message).to.equal( + 'the extension is not an official CSAF Extension' + ) + }) + + it('does not warn when the extension schema is classified as "official"', async function () { + const result = await recommendedTest_6_2_54_2({ + x_extensions: [ + { + $schema: documentation13.$id, + category: 'supplementary', + }, + ], + }) + expect(result.warnings.length).to.equal(0) + }) +})