diff --git a/README.md b/README.md index f9a60d1b..747c2dbe 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.js b/csaf_2_1/informativeTests.js index 12269510..b4ce625d 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 00000000..6571f381 --- /dev/null +++ b/csaf_2_1/informativeTests/informativeTest_6_3_13.js @@ -0,0 +1,101 @@ +import Ajv from 'ajv/dist/jtd.js' +import decision_points from '../../lib/cvss/decision_points.js' + +const ajv = new Ajv() + +/** @typedef {import('ajv/dist/jtd.js').JTDDataType} InputSchema */ + +/** @typedef {InputSchema['vulnerabilities'][number]} Vulnerability */ + +/** @typedef {NonNullable[number]} Metric */ + +const inputSchema = /** @type {const} */ ({ + additionalProperties: true, + properties: { + vulnerabilities: { + elements: { + additionalProperties: true, + optionalProperties: { + metrics: { + elements: { + additionalProperties: true, + optionalProperties: { + content: { + additionalProperties: true, + optionalProperties: { + ssvc_v1: { + additionalProperties: true, + optionalProperties: { + selections: { + elements: { + additionalProperties: true, + 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 00000000..a50ab765 --- /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 00000000..ca5a6ed0 --- /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 00000000..6711e452 --- /dev/null +++ b/tests/csaf_2_1/informativeTest_6_3_13.js @@ -0,0 +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) + }) +}) diff --git a/tests/csaf_2_1/oasis.js b/tests/csaf_2_1/oasis.js index ed81ff09..6014d144 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', ]