diff --git a/README.md b/README.md index a8a1e154..e9ac9ccd 100644 --- a/README.md +++ b/README.md @@ -313,7 +313,6 @@ The following tests are not yet implemented and therefore missing: - Mandatory Test 6.1.26 - Mandatory Test 6.1.27.13 -- Mandatory Test 6.1.42 - Mandatory Test 6.1.44 - Mandatory Test 6.1.46 - Mandatory Test 6.1.47 @@ -429,6 +428,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_42: DocumentTest export const mandatoryTest_6_1_43: DocumentTest export const mandatoryTest_6_1_45: DocumentTest export const mandatoryTest_6_1_51: DocumentTest diff --git a/csaf_2_1/mandatoryTests.js b/csaf_2_1/mandatoryTests.js index 92ef79ff..cb5dc607 100644 --- a/csaf_2_1/mandatoryTests.js +++ b/csaf_2_1/mandatoryTests.js @@ -58,6 +58,7 @@ 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_42 } from './mandatoryTests/mandatoryTest_6_1_42.js' export { mandatoryTest_6_1_43 } from './mandatoryTests/mandatoryTest_6_1_43.js' export { mandatoryTest_6_1_45 } from './mandatoryTests/mandatoryTest_6_1_45.js' export { mandatoryTest_6_1_51 } from './mandatoryTests/mandatoryTest_6_1_51.js' diff --git a/csaf_2_1/mandatoryTests/mandatoryTest_6_1_42.js b/csaf_2_1/mandatoryTests/mandatoryTest_6_1_42.js new file mode 100644 index 00000000..343b7a53 --- /dev/null +++ b/csaf_2_1/mandatoryTests/mandatoryTest_6_1_42.js @@ -0,0 +1,228 @@ +import { PackageURL } from 'packageurl-js' +import Ajv from 'ajv/dist/jtd.js' + +const ajv = new Ajv() + +const fullProductNameSchema = /** @type {const} */ ({ + additionalProperties: true, + optionalProperties: { + product_identification_helper: { + additionalProperties: true, + optionalProperties: { + purls: { elements: { type: 'string' } }, + }, + }, + }, +}) + +const branchSchema = /** @type {const} */ ({ + additionalProperties: true, + optionalProperties: { + branches: { + elements: { + additionalProperties: true, + properties: {}, + }, + }, + product: fullProductNameSchema, + }, +}) + +const validateBranch = ajv.compile(branchSchema) + +/* + 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, + optionalProperties: { + product_tree: { + additionalProperties: true, + optionalProperties: { + branches: { + elements: branchSchema, + }, + full_product_names: { + elements: fullProductNameSchema, + }, + relationships: { + elements: { + additionalProperties: true, + optionalProperties: { + full_product_name: fullProductNameSchema, + }, + }, + }, + }, + }, + }, +}) + +const validate = ajv.compile(inputSchema) + +/** + * @typedef {import('ajv/dist/core').JTDDataType} Branch + * @typedef {import('ajv/dist/core').JTDDataType} FullProductName + */ + +/** + * + * @param {PackageURL | null} firstPurl + * @param {PackageURL | null} otherPurl + * @return {Array} the parts of the PURLS that differ + */ +function purlPartsThatDifferExceptQualifiers(firstPurl, otherPurl) { + /** @type {Array}*/ + const partsThatDiffer = [] + + if (firstPurl && otherPurl) { + if (firstPurl.type !== otherPurl.type) { + partsThatDiffer.push('type') + } + if (firstPurl.namespace !== otherPurl.namespace) { + partsThatDiffer.push('namespace') + } + if (firstPurl.name !== otherPurl.name) { + partsThatDiffer.push('name') + } + if (firstPurl.version !== otherPurl.version) { + partsThatDiffer.push('version') + } + } + return partsThatDiffer +} + +/** + * Validates all given PURLs and check whether the PURLs + * differ only in qualifiers to the first URL + * + * @param {Array | undefined} purls PURLs to check + * @return {Array<{index:number, purlParts: Array }>} indexes and parts of the PURLs that differ + */ +export function checkPurls(purls) { + /** @type {Array<{index:number, purlParts: Array }>} */ + const invalidPurls = [] + if (purls) { + /** @type {Array} */ + const packageUrls = purls.map((purl) => { + try { + return PackageURL.fromString(purl) + } catch (e) { + // ignore, tested in CSAF 2.1 test 6.1.13 + return null + } + }) + + /** + * @type {Array} + */ + if (packageUrls.length > 1) { + const firstPurl = packageUrls[0] + for (let i = 1; i < packageUrls.length; i++) { + /** @type {Array}*/ + const purlParts = purlPartsThatDifferExceptQualifiers( + firstPurl, + packageUrls[i] + ) + if (purlParts.length > 0) { + invalidPurls.push({ index: i, purlParts: purlParts }) + } + } + } + } + return invalidPurls +} + +/** + * For each product_identification_helper object containing multiple purls, + * it MUST be tested that the purls only differ in their qualifiers. + * + * @param {unknown} doc + */ +export function mandatoryTest_6_1_42(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 (!validate(doc)) { + return ctx + } + + doc.product_tree?.branches?.forEach((branch, index) => { + checkBranch(`/product_tree/branches/${index}`, branch) + }) + + doc.product_tree?.full_product_names?.forEach((fullProduceName, index) => { + checkFullProductName( + `/product_tree/full_product_names/${index}`, + fullProduceName + ) + }) + + doc.product_tree?.relationships?.forEach((relationship, index) => { + const fullProductName = relationship.full_product_name + if (fullProductName) { + checkFullProductName( + `/product_tree/relationships/${index}/full_product_name`, + fullProductName + ) + } + }) + + return ctx + + /** + * Check whether the PURLs only differ in their qualifiers for a full product name. + * + * @param {string} prefix The instance path prefix of the "full product name". It is + * used to generate error messages. + * @param {FullProductName} fullProductName The "full product name" object. + */ + function checkFullProductName(prefix, fullProductName) { + const invalidPurls = checkPurls( + fullProductName.product_identification_helper?.purls + ) + invalidPurls.forEach((invalidPurl) => { + ctx.isValid = false + ctx.errors.push({ + instancePath: `${prefix}/product_identification_helper/purls/${invalidPurl.index}`, + message: `the PURL differs from the first PURL in the following part(s): ${invalidPurl.purlParts.join()}`, + }) + }) + } + + /** + * Check whether the PURLs only differ in their qualifiers for the given branch object + * and its branch children. + * + * @param {string} prefix The instance path prefix of the "branch". It is + * used to generate error messages. + * @param {Branch} branch The "branch" object. + */ + function checkBranch(prefix, branch) { + const invalidPurls = checkPurls( + branch.product?.product_identification_helper?.purls + ) + invalidPurls.forEach((invalidPurl) => { + ctx.isValid = false + ctx.errors.push({ + instancePath: `${prefix}/product/product_identification_helper/purls/${invalidPurl.index}`, + message: `the PURL differs from the first PURL in the following parts: ${invalidPurl.purlParts.join()}`, + }) + }) + branch.branches?.forEach((branch, index) => { + if (validateBranch(branch)) { + checkBranch(`${prefix}/branches/${index}`, branch) + } + }) + } +} diff --git a/tests/csaf_2_1/mandatoryTest_6_1_42.js b/tests/csaf_2_1/mandatoryTest_6_1_42.js new file mode 100644 index 00000000..145d289c --- /dev/null +++ b/tests/csaf_2_1/mandatoryTest_6_1_42.js @@ -0,0 +1,65 @@ +import assert from 'node:assert/strict' +import { expect } from 'chai' + +import { + mandatoryTest_6_1_42, + checkPurls, +} from '../../csaf_2_1/mandatoryTests/mandatoryTest_6_1_42.js' + +describe('mandatoryTest_6_1_42', function () { + it('only runs on relevant documents', function () { + assert.equal(mandatoryTest_6_1_42({ product_tree: 'mydoc' }).isValid, true) + }) + + it('test checkPurls', function () { + expect(checkPurls([]), 'empty purl array').to.eql([]) + expect(checkPurls(['invalid']), 'invalid PURL').to.eql([]) + expect( + checkPurls([ + 'pkg:golang/google.golang.org/genproto#googleapis/api/annotations', + 'pkg:golang/google.golang.org/genproto#googleapis/api/test', + ]), + 'only change in subpath' + ).to.eql([]) + expect( + checkPurls([ + 'pkg:deb/debian/curl@7.50.3-1?arch=i386&distro=jessie', + 'pkg:deb/debian/curl@7.50.3-1?arch=i386&distro=buster', + ]), + 'only change in qualifier' + ).to.eql([]) + expect( + checkPurls([ + 'pkg:golang/google.golang.org/genproto#googleapis/api/annotations', + 'pkg:golang/google.golang.com/genproto#googleapis/api/annotations', + ]), + 'change in namespace' + ).to.eql([{ index: 1, purlParts: ['namespace'] }]) + expect( + checkPurls([ + 'pkg:golang/google.golang.org/genproto#googleapis/api/annotations', + 'pkg:npm/google.golang.org/genproto#googleapis/api/annotations', + ]), + 'change in type' + ).to.eql([{ index: 1, purlParts: ['type'] }]) + expect( + checkPurls([ + 'pkg:golang/google.golang.org/genproto#googleapis/api/annotations', + 'pkg:golang/google.golang.org/genproto2#googleapis/api/annotations', + ]), + 'change in name' + ).to.eql([{ index: 1, purlParts: ['name'] }]) + expect( + checkPurls([ + 'pkg:npm/%40angular/animation@12.3.1', + 'invalid', + 'pkg:npm/%40angular/animation@12.3.2', + 'pkg:golang/%40angular/animation@12.3.3', + ]), + 'change in version and invalid PURL' + ).to.eql([ + { index: 2, purlParts: ['version'] }, + { index: 3, purlParts: ['type', 'version'] }, + ]) + }) +}) diff --git a/tests/csaf_2_1/oasis.js b/tests/csaf_2_1/oasis.js index 597989f9..72628171 100644 --- a/tests/csaf_2_1/oasis.js +++ b/tests/csaf_2_1/oasis.js @@ -19,7 +19,6 @@ const excluded = [ '6.1.27.11', '6.1.27.13', '6.1.37', - '6.1.42', '6.1.44', '6.1.46', '6.1.47',