From fb74bfdd2efc0cf3815b418ea6793e9892e4e893 Mon Sep 17 00:00:00 2001 From: rschneider <97682836+rainer-exxcellent@users.noreply.github.com> Date: Tue, 24 Jun 2025 10:35:45 +0200 Subject: [PATCH] feat(CSAF2.1): #340 add mandatory test 6.1.54 --- README.md | 5 +- csaf_2_1/mandatoryTests.js | 1 + .../mandatoryTests/mandatoryTest_6_1_54.js | 106 ++++++++++++++++++ package-lock.json | 45 ++++++++ package.json | 1 + tests/csaf_2_1/mandatoryTest_6_1_54.js | 63 +++++++++++ tests/csaf_2_1/oasis.js | 1 - 7 files changed, 220 insertions(+), 2 deletions(-) create mode 100644 csaf_2_1/mandatoryTests/mandatoryTest_6_1_54.js create mode 100644 tests/csaf_2_1/mandatoryTest_6_1_54.js diff --git a/README.md b/README.md index f9a60d1..1f85a00 100644 --- a/README.md +++ b/README.md @@ -334,7 +334,6 @@ The following tests are not yet implemented and therefore missing: - Mandatory Test 6.1.51 - Mandatory Test 6.1.52 - Mandatory Test 6.1.53 -- Mandatory Test 6.1.54 - Mandatory Test 6.1.55 **Recommended Tests** @@ -435,6 +434,7 @@ export const mandatoryTest_6_1_38: DocumentTest export const mandatoryTest_6_1_39: DocumentTest export const mandatoryTest_6_1_40: DocumentTest export const mandatoryTest_6_1_41: DocumentTest +export const mandatoryTest_6_1_54: DocumentTest ``` [(back to top)](#bsi-csaf-validator-lib) @@ -577,5 +577,8 @@ For the complete list of dependencies please take a look at [package.json](https - [undici](https://undici.nodejs.org) - [@js-joda/core](https://js-joda.github.io/js-joda/) - [@js-joda/timezone](https://js-joda.github.io/js-joda/) +- [aboutcode licenses](https://scancode-licensedb.aboutcode.org/index.json) +- [SPDX licenses](https://raw.githubusercontent.com/spdx/license-list-data/refs/heads/main/json/licenses.json) +- [license-expressions](https://github.com/lkoskela/license-expressions) [(back to top)](#bsi-csaf-validator-lib) diff --git a/csaf_2_1/mandatoryTests.js b/csaf_2_1/mandatoryTests.js index f51d680..1ab3c0d 100644 --- a/csaf_2_1/mandatoryTests.js +++ b/csaf_2_1/mandatoryTests.js @@ -49,3 +49,4 @@ export { mandatoryTest_6_1_38 } from './mandatoryTests/mandatoryTests_6_1_38.js' export { mandatoryTest_6_1_39 } from './mandatoryTests/mandatoryTest_6_1_39.js' export { mandatoryTest_6_1_40 } from './mandatoryTests/mandatoryTest_6_1_40.js' export { mandatoryTest_6_1_41 } from './mandatoryTests/mandatoryTest_6_1_41.js' +export { mandatoryTest_6_1_54 } from './mandatoryTests/mandatoryTest_6_1_54.js' diff --git a/csaf_2_1/mandatoryTests/mandatoryTest_6_1_54.js b/csaf_2_1/mandatoryTests/mandatoryTest_6_1_54.js new file mode 100644 index 0000000..40cbf50 --- /dev/null +++ b/csaf_2_1/mandatoryTests/mandatoryTest_6_1_54.js @@ -0,0 +1,106 @@ +import Ajv from 'ajv/dist/jtd.js' +import { validate, parse } from 'license-expressions' + +const ajv = new Ajv() + +/* + This is the jtd schema that needs to match the input document so that the + test is activated. If this schema doesn't match it normally means that the input + document does not validate against the csaf json schema or optional fields that + the test checks are not present. + */ +const inputSchema = /** @type {const} */ ({ + additionalProperties: true, + properties: { + document: { + additionalProperties: true, + properties: { + license_expression: { + type: 'string', + }, + }, + }, + }, +}) + +const validateSchema = ajv.compile(inputSchema) + +/** + * Recursively checks if a parsed license expression contains any license references. + * + * @param {import('license-expressions').ParsedSpdxExpression} parsedExpression - The parsed license expression + * @returns {boolean} True if the expression contains any license references, false otherwise + */ +function containsLicenseRef(parsedExpression) { + // If it's a LicenseRef type directly + if ('documentRef' in parsedExpression && parsedExpression.documentRef) { + return true + } + + // If it's a conjunction, check both sides + if ('conjunction' in parsedExpression) { + return ( + containsLicenseRef(parsedExpression.left) || + containsLicenseRef(parsedExpression.right) + ) + } + + // If it's a LicenseInfo type, it doesn't contain a document reference + return false +} + +/** + * Checks if a license expression contains any document references. + * + * @param {string} licenseToCheck - The license expression to check + * @returns {boolean} True if the license expression contains any document references, false otherwise + */ +export function hasDocumentRef(licenseToCheck) { + const parseResult = parse(licenseToCheck) + return containsLicenseRef(parseResult) +} + +/** + * Checks if a license expression is valid, according to SPDX standards. + * + * @param {string} licenseToCheck - The license expression to check + * @returns {boolean} True if the license is valid, false otherwise + */ +export function isValidLicenseExpression(licenseToCheck) { + return ( + !licenseToCheck || + (validate(licenseToCheck).valid && !hasDocumentRef(licenseToCheck)) + ) +} + +/** + * It MUST be tested that the license expression is valid. + * + * @param {unknown} doc + */ +export function mandatoryTest_6_1_54(doc) { + /* + The `ctx` variable holds the state that is accumulated during the test ran and is + finally returned by the function. + */ + const ctx = { + errors: + /** @type {Array<{ instancePath: string; message: string }>} */ ([]), + isValid: true, + } + + if (!validateSchema(doc)) { + return ctx + } + + const licenseToCheck = doc.document.license_expression + if (!isValidLicenseExpression(licenseToCheck)) { + ctx.isValid = false + ctx.errors.push({ + instancePath: '/document/license_expression', + message: `Invalid license expression: '${licenseToCheck}'`, + }) + } + + return ctx +} diff --git a/package-lock.json b/package-lock.json index 75724a5..7ee5267 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,7 @@ "bcp47": "^1.1.2", "cvss2js": "^1.1.0", "json-pointer": "^0.6.1", + "license-expressions": "^0.7.3", "lodash": "^4.17.21", "packageurl-js": "^2.0.1", "semver": "^7.5.4", @@ -912,6 +913,18 @@ "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", "license": "MIT" }, + "node_modules/license-expressions": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/license-expressions/-/license-expressions-0.7.3.tgz", + "integrity": "sha512-VxK7K/LcRX865F83FQwpUp0xjijUEKnLO9um8HcfJK3AZn6QRIaMbJ1bZFuHoCF9hMLSrf9criGjh9UjniEZ7g==", + "license": "MIT", + "dependencies": { + "spdx-correct": "^3.2.0" + }, + "bin": { + "spdx": "build/cli/index.js" + } + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -1313,6 +1326,38 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/spdx-correct": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", + "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", + "license": "Apache-2.0", + "dependencies": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-exceptions": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", + "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", + "license": "CC-BY-3.0" + }, + "node_modules/spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "license": "MIT", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-license-ids": { + "version": "3.0.21", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.21.tgz", + "integrity": "sha512-Bvg/8F5XephndSK3JffaRqdT+gyhfqIPwDHpX80tJrF8QQRYMo8sNMeaZ2Dp5+jhwKnUmIOyFFQfHRkjJm5nXg==", + "license": "CC0-1.0" + }, "node_modules/string-width": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", diff --git a/package.json b/package.json index 01f1759..2b15e85 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "bcp47": "^1.1.2", "cvss2js": "^1.1.0", "json-pointer": "^0.6.1", + "license-expressions": "^0.7.3", "lodash": "^4.17.21", "packageurl-js": "^2.0.1", "semver": "^7.5.4", diff --git a/tests/csaf_2_1/mandatoryTest_6_1_54.js b/tests/csaf_2_1/mandatoryTest_6_1_54.js new file mode 100644 index 0000000..7b38d6f --- /dev/null +++ b/tests/csaf_2_1/mandatoryTest_6_1_54.js @@ -0,0 +1,63 @@ +import { + isValidLicenseExpression, + mandatoryTest_6_1_54, +} from '../../csaf_2_1/mandatoryTests/mandatoryTest_6_1_54.js' +import { expect } from 'chai' + +describe('mandatoryTest_6_1_54', function () { + it('only runs on relevant documents', function () { + expect(mandatoryTest_6_1_54({ document: 'mydoc' }).isValid).to.be.true + }) + + it('check license expressions', function () { + expect(isValidLicenseExpression('GPL-3.0+')).to.be.true + expect(isValidLicenseExpression('GPL-3.0-only')).to.be.true + expect(isValidLicenseExpression('MIT OR (Apache-2.0 AND 0BSD)')).to.be.true + expect(isValidLicenseExpression('Invalid-license-expression')).to.be.false + expect(isValidLicenseExpression('GPL-2.0 OR BSD-3-Clause')).to.be.true + expect(isValidLicenseExpression('LGPL-2.1 OR BSD-3-Clause AND MIT')).to.be + .true + expect(isValidLicenseExpression('(MIT AND (LGPL-2.1+ AND BSD-3-Clause))')) + .to.be.true + // Exception associated with unrelated license: + expect( + isValidLicenseExpression('MIT OR Apache-2.0 WITH Autoconf-exception-2.0'), + 'Exception associated with unrelated license' + ).to.be.false + expect( + isValidLicenseExpression('3dslicer-1.0'), + 'SPDX License List matching guidelines' + ).to.be.true + + expect( + isValidLicenseExpression('LicenseRef-www.example.com-no-work-pd'), + 'Valid SPDX expression with License Ref' + ).to.be.true + + expect( + isValidLicenseExpression( + 'LicenseRef-www.example.com-no-work-pd OR BSD-3-Clause AND MIT' + ), + 'Valid SPDX expression with compound-expression and License Ref' + ).to.be.true + + expect(isValidLicenseExpression('wxWindows'), 'Deprecated License').to.be + .true + expect( + isValidLicenseExpression('DocumentRef-X:LicenseRef-Y AND MIT'), + 'DocumentRef in License with compound-expression ' + ).to.be.false + expect( + isValidLicenseExpression( + 'DocumentRef-some-document-reference:LicenseRef-www.example.org-Example-CSAF-License-2.0' + ), + 'DocumentRef in License' + ).to.be.false + expect( + isValidLicenseExpression( + 'LicenseRef-www.example.org-Example-CSAF-License-3.0+' + ), + 'LicenseRef in License with trailing +' + ).to.be.false + }) +}) diff --git a/tests/csaf_2_1/oasis.js b/tests/csaf_2_1/oasis.js index ed81ff0..b766e88 100644 --- a/tests/csaf_2_1/oasis.js +++ b/tests/csaf_2_1/oasis.js @@ -39,7 +39,6 @@ const excluded = [ '6.1.51', '6.1.52', '6.1.53', - '6.1.54', '6.1.55', '6.2.11', '6.2.19',