From e12dc36eb5ab6680e67c8a12912973b491c1cd20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Burgd=C3=B6rfer?= Date: Thu, 8 May 2025 10:59:08 +0200 Subject: [PATCH 1/4] feat: update csaf and exclude new tests --- 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 ed81ff0..6014d14 100644 --- a/tests/csaf_2_1/oasis.js +++ b/tests/csaf_2_1/oasis.js @@ -71,7 +71,6 @@ const excluded = [ '6.3.14', '6.3.15', '6.3.12', - '6.3.13', '6.3.16', '6.3.17', ] From 6867f76cc56ad71e8a5e186b2ee09b013c5e1be6 Mon Sep 17 00:00:00 2001 From: rschneider <97682836+rainer-exxcellent@users.noreply.github.com> Date: Wed, 14 May 2025 10:29:24 +0200 Subject: [PATCH 2/4] feat(CSAF2.1): #199 test 6.3.13 for CSAF 2.1 --- csaf_2_1/informativeTests.js | 1 + .../informativeTest_6_3_13.js | 121 ++++++++++++++ lib/cvss/decision_points.js | 148 ++++++++++++++++++ scripts/read-ssvc-decision-points.js | 96 ++++++++++++ tests/csaf_2_1/informativeTest_6_3_13.js | 8 + 5 files changed, 374 insertions(+) create mode 100644 csaf_2_1/informativeTests/informativeTest_6_3_13.js create mode 100644 lib/cvss/decision_points.js create mode 100644 scripts/read-ssvc-decision-points.js create mode 100644 tests/csaf_2_1/informativeTest_6_3_13.js diff --git a/csaf_2_1/informativeTests.js b/csaf_2_1/informativeTests.js index 1226951..b4ce625 100644 --- a/csaf_2_1/informativeTests.js +++ b/csaf_2_1/informativeTests.js @@ -11,3 +11,4 @@ export { export { informativeTest_6_3_1 } from './informativeTests/informativeTest_6_3_1.js' export { informativeTest_6_3_4 } from './informativeTests/informativeTest_6_3_4.js' export { informativeTest_6_3_2 } from './informativeTests/informativeTest_6_3_2.js' +export { informativeTest_6_3_13 } from './informativeTests/informativeTest_6_3_13.js' diff --git a/csaf_2_1/informativeTests/informativeTest_6_3_13.js b/csaf_2_1/informativeTests/informativeTest_6_3_13.js new file mode 100644 index 0000000..f701f99 --- /dev/null +++ b/csaf_2_1/informativeTests/informativeTest_6_3_13.js @@ -0,0 +1,121 @@ +import Ajv from 'ajv/dist/jtd.js' +import decision_points from '../../lib/cvss/decision_points.js' + +const ajv = new Ajv() + +/** + * @typedef {object} Selection + * @property {string} [name] + * @property {string} [namespace] + * @property {string} [version] + */ + +/** + * @typedef {object} Ssvc1 + * @property {Array} [selections] + */ + +/** + * @typedef {object} MetricContent + * @property {Ssvc1} [ssvc_v1] + */ + +/** + * @typedef {object} Metric + * @property {MetricContent} [content] + */ + +const inputSchema = /** @type {const} */ ({ + additionalProperties: true, + properties: { + vulnerabilities: { + elements: { + additionalProperties: true, + properties: {}, + optionalProperties: { + metrics: { + elements: { + additionalProperties: true, + properties: {}, + optionalProperties: { + content: { + additionalProperties: true, + properties: {}, + optionalProperties: { + ssvc_v1: { + additionalProperties: true, + optionalProperties: { + selections: { + elements: { + additionalProperties: true, + properties: {}, + optionalProperties: { + name: { type: 'string' }, + namespace: { type: 'string' }, + version: { type: 'string' }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, +}) + +const validateInput = ajv.compile(inputSchema) + +/** + * For each SSVC decision point given under selections with the namespace of ssvc, + * it MUST be tested the latest decision point version available at the time of the timestamp was used. + * The test SHALL fail if a later version was used. + * @param {unknown} doc + * @returns + */ +export function informativeTest_6_3_13(doc) { + const ctx = { + infos: /** @type {Array<{ message: string; instancePath: string }>} */ ([]), + } + + if (!validateInput(doc)) { + return ctx + } + + const decisionPointName2Version = new Map() + decision_points.decisionPoints.forEach((obj) => { + const currentVersion = decisionPointName2Version.get(obj.name) + if (!currentVersion || currentVersion < obj.version) { + decisionPointName2Version.set(obj.name, obj.version) + } + }) + + const vulnerabilities = doc.vulnerabilities + + vulnerabilities.forEach((vulnerability, vulnerabilityIndex) => { + /** @type {Array | undefined} */ + const metrics = vulnerability.metrics + metrics?.forEach((metric, metricIndex) => { + const selections = metric?.content?.ssvc_v1?.selections + selections?.forEach((selection, selectionIndex) => { + const latestVersion = decisionPointName2Version.get(selection?.name) + if ( + selection.version !== latestVersion && + selection.namespace === 'ssvc' + ) { + ctx.infos.push({ + instancePath: `/vulnerabilities/${vulnerabilityIndex}/metrics/${metricIndex}/content/ssvc_v1/selections/${selectionIndex}/version`, + message: `ssvc_v1 version '${selection.version}' is not latest decision point version '${latestVersion}'`, + }) + } + }) + }) + }) + + return ctx +} diff --git a/lib/cvss/decision_points.js b/lib/cvss/decision_points.js new file mode 100644 index 0000000..a50ab76 --- /dev/null +++ b/lib/cvss/decision_points.js @@ -0,0 +1,148 @@ +export default { + decisionPoints: [ + { + name: 'Automatable', + namespace: 'ssvc', + version: '2.0.0', + key: 'A', + }, + { name: 'Exploitation', namespace: 'ssvc', version: '1.0.0', key: 'E' }, + { + name: 'Exploitation', + namespace: 'ssvc', + version: '1.1.0', + key: 'E', + }, + { name: 'Human Impact', namespace: 'ssvc', version: '1.0.0', key: 'HI' }, + { + name: 'Human Impact', + namespace: 'ssvc', + version: '2.0.0', + key: 'HI', + }, + { + name: 'Human Impact', + namespace: 'ssvc', + version: '2.0.1', + key: 'HI', + }, + { + name: 'Mission and Well-Being Impact', + namespace: 'ssvc', + version: '1.0.0', + key: 'MWI', + }, + { name: 'Mission Impact', namespace: 'ssvc', version: '1.0.0', key: 'MI' }, + { + name: 'Mission Impact', + namespace: 'ssvc', + version: '2.0.0', + key: 'MI', + }, + { + name: 'Public Safety Impact', + namespace: 'ssvc', + version: '1.0.0', + key: 'PSI', + }, + { + name: 'Public Safety Impact', + namespace: 'ssvc', + version: '2.0.0', + key: 'PSI', + }, + { + name: 'Public Safety Impact', + namespace: 'ssvc', + version: '2.0.1', + key: 'PSI', + }, + { + name: 'Public Value Added', + namespace: 'ssvc', + version: '1.0.0', + key: 'PVA', + }, + { + name: 'Public Well-Being Impact', + namespace: 'ssvc', + version: '1.0.0', + key: 'PWI', + }, + { + name: 'Report Credibility', + namespace: 'ssvc', + version: '1.0.0', + key: 'RC', + }, + { + name: 'Report Public', + namespace: 'ssvc', + version: '1.0.0', + key: 'RP', + }, + { name: 'Safety Impact', namespace: 'ssvc', version: '1.0.0', key: 'SI' }, + { + name: 'Safety Impact', + namespace: 'ssvc', + version: '2.0.0', + key: 'SI', + }, + { + name: 'Supplier Cardinality', + namespace: 'ssvc', + version: '1.0.0', + key: 'SC', + }, + { + name: 'Supplier Contacted', + namespace: 'ssvc', + version: '1.0.0', + key: 'SC', + }, + { + name: 'Supplier Engagement', + namespace: 'ssvc', + version: '1.0.0', + key: 'SE', + }, + { + name: 'Supplier Involvement', + namespace: 'ssvc', + version: '1.0.0', + key: 'SI', + }, + { + name: 'System Exposure', + namespace: 'ssvc', + version: '1.0.0', + key: 'EXP', + }, + { + name: 'System Exposure', + namespace: 'ssvc', + version: '1.0.1', + key: 'EXP', + }, + { + name: 'Technical Impact', + namespace: 'ssvc', + version: '1.0.0', + key: 'TI', + }, + { + name: 'Utility', + namespace: 'ssvc', + version: '1.0.0', + key: 'U', + }, + { name: 'Utility', namespace: 'ssvc', version: '1.0.1', key: 'U' }, + { + name: 'Value Density', + namespace: 'ssvc', + version: '1.0.0', + key: 'VD', + }, + { name: 'Virulence', namespace: 'ssvc', version: '1.0.0', key: 'V' }, + ], +} diff --git a/scripts/read-ssvc-decision-points.js b/scripts/read-ssvc-decision-points.js new file mode 100644 index 0000000..ca5a6ed --- /dev/null +++ b/scripts/read-ssvc-decision-points.js @@ -0,0 +1,96 @@ +/* +Script to read all CVSS decision points from github and create a json File with all defined decision points + */ + +import fs from 'node:fs' + +const CVSS_DECISION_POINT_URL = + 'https://api.github.com/repos/CERTCC/SSVC/contents/data/json/decision_points?ref=main' +const OUTPUT_FILE = '../lib/cvss/decision_points.js' + +const GITHUB_TOKEN = '' + +/** + * @typedef {object} GithubResponse + * @property {string} name + * @property {string} path + * @property {string} sha + * @property {number} size + * @property {string} url + * @property {string} html_url + * @property {string} git_url + * @property {string} download_url + * @property {string} type + */ + +/** + * @typedef {object} DecisionPoint + * @property {string} name + * @property {string} description + * @property {string} namespace + * @property {string} version + * @property {string} schemaVersion + * @property {string} key + */ + +/** + * @typedef {object} DecisionPointInfo + * @property {string} name + * @property {string} namespace + * @property {string} version + * @property {string} key + */ + +/** + * Read Json from given URL + * @param {string | URL | Request} dataUrl + * @param {string} githubToken + */ +async function readJson(dataUrl, githubToken) { + /** @type {any} */ + const headers = { Accept: 'application/vnd.github.v3+json' } + if (GITHUB_TOKEN) { + headers['Authorization'] = `Bearer ${githubToken}` + } + const response = await fetch(dataUrl, { headers }) + if (!response.ok) { + throw new Error(`Response status: ${response.status}`) + } + + return await response.json() +} + +/** + * Read decision points from github and write them to a JSON file + * @param {string} githubToken + */ +async function readDecisionPoints(githubToken) { + /** @type {Array} */ + const data = await readJson(CVSS_DECISION_POINT_URL, githubToken) + /** @type {Array} */ + const result = [] + for (const item of data) { + if (item.name.endsWith('.json')) { + /** @type {DecisionPoint} */ + const decisionPoint = await readJson(item.download_url, githubToken) + result.push({ + name: decisionPoint.name, + namespace: decisionPoint.namespace, + version: decisionPoint.version, + key: decisionPoint.key, + }) + } + } + return result +} + +readDecisionPoints(GITHUB_TOKEN).then((points) => { + console.log(points) + const pointsObject = { decisionPoints: points } + const pointsJson = 'export default ' + JSON.stringify(pointsObject) + fs.writeFile(OUTPUT_FILE, pointsJson, (err) => { + if (err) { + console.log(err) + } + }) +}) diff --git a/tests/csaf_2_1/informativeTest_6_3_13.js b/tests/csaf_2_1/informativeTest_6_3_13.js new file mode 100644 index 0000000..2db180f --- /dev/null +++ b/tests/csaf_2_1/informativeTest_6_3_13.js @@ -0,0 +1,8 @@ +import assert from 'node:assert' +import { informativeTest_6_3_13 } from '../../csaf_2_1/informativeTests.js' + +describe('informativeTest_6_3_13', function () { + it('only runs on relevant documents', function () { + assert.equal(informativeTest_6_3_13({ document: 'mydoc' }).infos.length, 0) + }) +}) From a7f24a9ec390b492702a8f8c8ec011a7b0ce28f6 Mon Sep 17 00:00:00 2001 From: rschneider <97682836+rainer-exxcellent@users.noreply.github.com> Date: Fri, 13 Jun 2025 06:46:57 +0200 Subject: [PATCH 3/4] feat(CSAF2.1): #199 test 6.3.13 for CSAF 2.1 - change optionalProperties to properties in input schema --- csaf_2_1/informativeTests/informativeTest_6_3_13.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/csaf_2_1/informativeTests/informativeTest_6_3_13.js b/csaf_2_1/informativeTests/informativeTest_6_3_13.js index f701f99..b5198e5 100644 --- a/csaf_2_1/informativeTests/informativeTest_6_3_13.js +++ b/csaf_2_1/informativeTests/informativeTest_6_3_13.js @@ -31,16 +31,13 @@ const inputSchema = /** @type {const} */ ({ vulnerabilities: { elements: { additionalProperties: true, - properties: {}, optionalProperties: { metrics: { elements: { additionalProperties: true, - properties: {}, optionalProperties: { content: { additionalProperties: true, - properties: {}, optionalProperties: { ssvc_v1: { additionalProperties: true, @@ -48,7 +45,6 @@ const inputSchema = /** @type {const} */ ({ selections: { elements: { additionalProperties: true, - properties: {}, optionalProperties: { name: { type: 'string' }, namespace: { type: 'string' }, From 8006afbaa7ec18753eafed1de9747e6e928bb849 Mon Sep 17 00:00:00 2001 From: rschneider <97682836+rainer-exxcellent@users.noreply.github.com> Date: Fri, 20 Jun 2025 13:25:59 +0200 Subject: [PATCH 4/4] feat(CSAF2.1): #199 test 6.3.13 for CSAF 2.1 - move test in README to the supported tests, use types from inputSchema --- README.md | 2 +- .../informativeTest_6_3_13.js | 22 ++-------- tests/csaf_2_1/informativeTest_6_3_13.js | 41 +++++++++++++++++++ 3 files changed, 45 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index f9a60d1..747c2db 100644 --- a/README.md +++ b/README.md @@ -369,7 +369,6 @@ The following tests are not yet implemented and therefore missing: **Informative Tests** - Informative Test 6.2.12 -- Informative Test 6.2.13 - Informative Test 6.2.14 - Informative Test 6.2.15 - Informative Test 6.2.16 @@ -481,6 +480,7 @@ export const informativeTest_6_3_8: DocumentTest export const informativeTest_6_3_9: DocumentTest export const informativeTest_6_3_10: DocumentTest export const informativeTest_6_3_11: DocumentTest +export const informativeTest_6_3_13: DocumentTest ``` [(back to top)](#bsi-csaf-validator-lib) diff --git a/csaf_2_1/informativeTests/informativeTest_6_3_13.js b/csaf_2_1/informativeTests/informativeTest_6_3_13.js index b5198e5..6571f38 100644 --- a/csaf_2_1/informativeTests/informativeTest_6_3_13.js +++ b/csaf_2_1/informativeTests/informativeTest_6_3_13.js @@ -3,27 +3,11 @@ import decision_points from '../../lib/cvss/decision_points.js' const ajv = new Ajv() -/** - * @typedef {object} Selection - * @property {string} [name] - * @property {string} [namespace] - * @property {string} [version] - */ +/** @typedef {import('ajv/dist/jtd.js').JTDDataType} InputSchema */ -/** - * @typedef {object} Ssvc1 - * @property {Array} [selections] - */ +/** @typedef {InputSchema['vulnerabilities'][number]} Vulnerability */ -/** - * @typedef {object} MetricContent - * @property {Ssvc1} [ssvc_v1] - */ - -/** - * @typedef {object} Metric - * @property {MetricContent} [content] - */ +/** @typedef {NonNullable[number]} Metric */ const inputSchema = /** @type {const} */ ({ additionalProperties: true, diff --git a/tests/csaf_2_1/informativeTest_6_3_13.js b/tests/csaf_2_1/informativeTest_6_3_13.js index 2db180f..6711e45 100644 --- a/tests/csaf_2_1/informativeTest_6_3_13.js +++ b/tests/csaf_2_1/informativeTest_6_3_13.js @@ -1,8 +1,49 @@ import assert from 'node:assert' import { informativeTest_6_3_13 } from '../../csaf_2_1/informativeTests.js' +import { expect } from 'chai' + +const failingTestWithNotConsideredObject = { + product_tree: { + full_product_names: [ + { + product_id: 'CSAFPID-9080700', + name: 'Product A', + }, + ], + }, + vulnerabilities: [ + { + metrics: [ + {}, + { + content: { + ssvc_v1: { + id: 'CVE-1900-0001', + schemaVersion: '1-0-1', + selections: [ + { + name: 'Mission Impact', + namespace: 'ssvc', + values: ['Non-Essential Degraded'], + version: '1.0.0', + }, + ], + timestamp: '2024-01-24T10:00:00.000Z', + }, + }, + }, + ], + }, + ], +} describe('informativeTest_6_3_13', function () { it('only runs on relevant documents', function () { assert.equal(informativeTest_6_3_13({ document: 'mydoc' }).infos.length, 0) }) + + it('test input schema with not considered json object in vulnerabilities', async function () { + const result = informativeTest_6_3_13(failingTestWithNotConsideredObject) + expect(result.infos.length).to.eq(1) + }) })