Skip to content

Commit 082931a

Browse files
Merge pull request #409 from secvisogram/feat/287-csaf-2.1-mandatory-test-6.1.42
feat(CSAF2.1): #287 add mandatory test 6.1.42
2 parents 9bc11b4 + 1856a92 commit 082931a

File tree

5 files changed

+295
-2
lines changed

5 files changed

+295
-2
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -313,7 +313,6 @@ The following tests are not yet implemented and therefore missing:
313313
314314
- Mandatory Test 6.1.26
315315
- Mandatory Test 6.1.27.13
316-
- Mandatory Test 6.1.42
317316
- Mandatory Test 6.1.44
318317
- Mandatory Test 6.1.46
319318
- Mandatory Test 6.1.47
@@ -425,6 +424,7 @@ export const mandatoryTest_6_1_38: DocumentTest
425424
export const mandatoryTest_6_1_39: DocumentTest
426425
export const mandatoryTest_6_1_40: DocumentTest
427426
export const mandatoryTest_6_1_41: DocumentTest
427+
export const mandatoryTest_6_1_42: DocumentTest
428428
export const mandatoryTest_6_1_43: DocumentTest
429429
export const mandatoryTest_6_1_45: DocumentTest
430430
export const mandatoryTest_6_1_51: DocumentTest

csaf_2_1/mandatoryTests.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ export { mandatoryTest_6_1_38 } from './mandatoryTests/mandatoryTests_6_1_38.js'
5858
export { mandatoryTest_6_1_39 } from './mandatoryTests/mandatoryTest_6_1_39.js'
5959
export { mandatoryTest_6_1_40 } from './mandatoryTests/mandatoryTest_6_1_40.js'
6060
export { mandatoryTest_6_1_41 } from './mandatoryTests/mandatoryTest_6_1_41.js'
61+
export { mandatoryTest_6_1_42 } from './mandatoryTests/mandatoryTest_6_1_42.js'
6162
export { mandatoryTest_6_1_43 } from './mandatoryTests/mandatoryTest_6_1_43.js'
6263
export { mandatoryTest_6_1_45 } from './mandatoryTests/mandatoryTest_6_1_45.js'
6364
export { mandatoryTest_6_1_51 } from './mandatoryTests/mandatoryTest_6_1_51.js'
Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
1+
import { PackageURL } from 'packageurl-js'
2+
import Ajv from 'ajv/dist/jtd.js'
3+
4+
const ajv = new Ajv()
5+
6+
const fullProductNameSchema = /** @type {const} */ ({
7+
additionalProperties: true,
8+
optionalProperties: {
9+
product_identification_helper: {
10+
additionalProperties: true,
11+
optionalProperties: {
12+
purls: { elements: { type: 'string' } },
13+
},
14+
},
15+
},
16+
})
17+
18+
const branchSchema = /** @type {const} */ ({
19+
additionalProperties: true,
20+
optionalProperties: {
21+
branches: {
22+
elements: {
23+
additionalProperties: true,
24+
properties: {},
25+
},
26+
},
27+
product: fullProductNameSchema,
28+
},
29+
})
30+
31+
const validateBranch = ajv.compile(branchSchema)
32+
33+
/*
34+
This is the jtd schema that needs to match the input document so that the
35+
test is activated. If this schema doesn't match, it normally means that the input
36+
document does not validate against the csaf JSON schema or optional fields that
37+
the test checks are not present.
38+
*/
39+
const inputSchema = /** @type {const} */ ({
40+
additionalProperties: true,
41+
optionalProperties: {
42+
product_tree: {
43+
additionalProperties: true,
44+
optionalProperties: {
45+
branches: {
46+
elements: branchSchema,
47+
},
48+
full_product_names: {
49+
elements: fullProductNameSchema,
50+
},
51+
relationships: {
52+
elements: {
53+
additionalProperties: true,
54+
optionalProperties: {
55+
full_product_name: fullProductNameSchema,
56+
},
57+
},
58+
},
59+
},
60+
},
61+
},
62+
})
63+
64+
const validate = ajv.compile(inputSchema)
65+
66+
/**
67+
* @typedef {import('ajv/dist/core').JTDDataType<typeof branchSchema>} Branch
68+
* @typedef {import('ajv/dist/core').JTDDataType<typeof fullProductNameSchema>} FullProductName
69+
*/
70+
71+
/**
72+
*
73+
* @param {PackageURL | null} firstPurl
74+
* @param {PackageURL | null} otherPurl
75+
* @return {Array<string>} the parts of the PURLS that differ
76+
*/
77+
function purlPartsThatDifferExceptQualifiers(firstPurl, otherPurl) {
78+
/** @type {Array<string>}*/
79+
const partsThatDiffer = []
80+
81+
if (firstPurl && otherPurl) {
82+
if (firstPurl.type !== otherPurl.type) {
83+
partsThatDiffer.push('type')
84+
}
85+
if (firstPurl.namespace !== otherPurl.namespace) {
86+
partsThatDiffer.push('namespace')
87+
}
88+
if (firstPurl.name !== otherPurl.name) {
89+
partsThatDiffer.push('name')
90+
}
91+
if (firstPurl.version !== otherPurl.version) {
92+
partsThatDiffer.push('version')
93+
}
94+
}
95+
return partsThatDiffer
96+
}
97+
98+
/**
99+
* Validates all given PURLs and check whether the PURLs
100+
* differ only in qualifiers to the first URL
101+
*
102+
* @param {Array<string> | undefined} purls PURLs to check
103+
* @return {Array<{index:number, purlParts: Array<string> }>} indexes and parts of the PURLs that differ
104+
*/
105+
export function checkPurls(purls) {
106+
/** @type {Array<{index:number, purlParts: Array<string> }>} */
107+
const invalidPurls = []
108+
if (purls) {
109+
/** @type {Array<PackageURL | null>} */
110+
const packageUrls = purls.map((purl) => {
111+
try {
112+
return PackageURL.fromString(purl)
113+
} catch (e) {
114+
// ignore, tested in CSAF 2.1 test 6.1.13
115+
return null
116+
}
117+
})
118+
119+
/**
120+
* @type {Array<PackageURL>}
121+
*/
122+
if (packageUrls.length > 1) {
123+
const firstPurl = packageUrls[0]
124+
for (let i = 1; i < packageUrls.length; i++) {
125+
/** @type {Array<string>}*/
126+
const purlParts = purlPartsThatDifferExceptQualifiers(
127+
firstPurl,
128+
packageUrls[i]
129+
)
130+
if (purlParts.length > 0) {
131+
invalidPurls.push({ index: i, purlParts: purlParts })
132+
}
133+
}
134+
}
135+
}
136+
return invalidPurls
137+
}
138+
139+
/**
140+
* For each product_identification_helper object containing multiple purls,
141+
* it MUST be tested that the purls only differ in their qualifiers.
142+
*
143+
* @param {unknown} doc
144+
*/
145+
export function mandatoryTest_6_1_42(doc) {
146+
/*
147+
The `ctx` variable holds the state that is accumulated during the test ran and is
148+
finally returned by the function.
149+
*/
150+
const ctx = {
151+
errors:
152+
/** @type {Array<{ instancePath: string; message: string }>} */ ([]),
153+
isValid: true,
154+
}
155+
156+
if (!validate(doc)) {
157+
return ctx
158+
}
159+
160+
doc.product_tree?.branches?.forEach((branch, index) => {
161+
checkBranch(`/product_tree/branches/${index}`, branch)
162+
})
163+
164+
doc.product_tree?.full_product_names?.forEach((fullProduceName, index) => {
165+
checkFullProductName(
166+
`/product_tree/full_product_names/${index}`,
167+
fullProduceName
168+
)
169+
})
170+
171+
doc.product_tree?.relationships?.forEach((relationship, index) => {
172+
const fullProductName = relationship.full_product_name
173+
if (fullProductName) {
174+
checkFullProductName(
175+
`/product_tree/relationships/${index}/full_product_name`,
176+
fullProductName
177+
)
178+
}
179+
})
180+
181+
return ctx
182+
183+
/**
184+
* Check whether the PURLs only differ in their qualifiers for a full product name.
185+
*
186+
* @param {string} prefix The instance path prefix of the "full product name". It is
187+
* used to generate error messages.
188+
* @param {FullProductName} fullProductName The "full product name" object.
189+
*/
190+
function checkFullProductName(prefix, fullProductName) {
191+
const invalidPurls = checkPurls(
192+
fullProductName.product_identification_helper?.purls
193+
)
194+
invalidPurls.forEach((invalidPurl) => {
195+
ctx.isValid = false
196+
ctx.errors.push({
197+
instancePath: `${prefix}/product_identification_helper/purls/${invalidPurl.index}`,
198+
message: `the PURL differs from the first PURL in the following part(s): ${invalidPurl.purlParts.join()}`,
199+
})
200+
})
201+
}
202+
203+
/**
204+
* Check whether the PURLs only differ in their qualifiers for the given branch object
205+
* and its branch children.
206+
*
207+
* @param {string} prefix The instance path prefix of the "branch". It is
208+
* used to generate error messages.
209+
* @param {Branch} branch The "branch" object.
210+
*/
211+
function checkBranch(prefix, branch) {
212+
const invalidPurls = checkPurls(
213+
branch.product?.product_identification_helper?.purls
214+
)
215+
invalidPurls.forEach((invalidPurl) => {
216+
ctx.isValid = false
217+
ctx.errors.push({
218+
instancePath: `${prefix}/product/product_identification_helper/purls/${invalidPurl.index}`,
219+
message: `the PURL differs from the first PURL in the following parts: ${invalidPurl.purlParts.join()}`,
220+
})
221+
})
222+
branch.branches?.forEach((branch, index) => {
223+
if (validateBranch(branch)) {
224+
checkBranch(`${prefix}/branches/${index}`, branch)
225+
}
226+
})
227+
}
228+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import assert from 'node:assert/strict'
2+
import { expect } from 'chai'
3+
4+
import {
5+
mandatoryTest_6_1_42,
6+
checkPurls,
7+
} from '../../csaf_2_1/mandatoryTests/mandatoryTest_6_1_42.js'
8+
9+
describe('mandatoryTest_6_1_42', function () {
10+
it('only runs on relevant documents', function () {
11+
assert.equal(mandatoryTest_6_1_42({ product_tree: 'mydoc' }).isValid, true)
12+
})
13+
14+
it('test checkPurls', function () {
15+
expect(checkPurls([]), 'empty purl array').to.eql([])
16+
expect(checkPurls(['invalid']), 'invalid PURL').to.eql([])
17+
expect(
18+
checkPurls([
19+
'pkg:golang/google.golang.org/genproto#googleapis/api/annotations',
20+
'pkg:golang/google.golang.org/genproto#googleapis/api/test',
21+
]),
22+
'only change in subpath'
23+
).to.eql([])
24+
expect(
25+
checkPurls([
26+
'pkg:deb/debian/[email protected]?arch=i386&distro=jessie',
27+
'pkg:deb/debian/[email protected]?arch=i386&distro=buster',
28+
]),
29+
'only change in qualifier'
30+
).to.eql([])
31+
expect(
32+
checkPurls([
33+
'pkg:golang/google.golang.org/genproto#googleapis/api/annotations',
34+
'pkg:golang/google.golang.com/genproto#googleapis/api/annotations',
35+
]),
36+
'change in namespace'
37+
).to.eql([{ index: 1, purlParts: ['namespace'] }])
38+
expect(
39+
checkPurls([
40+
'pkg:golang/google.golang.org/genproto#googleapis/api/annotations',
41+
'pkg:npm/google.golang.org/genproto#googleapis/api/annotations',
42+
]),
43+
'change in type'
44+
).to.eql([{ index: 1, purlParts: ['type'] }])
45+
expect(
46+
checkPurls([
47+
'pkg:golang/google.golang.org/genproto#googleapis/api/annotations',
48+
'pkg:golang/google.golang.org/genproto2#googleapis/api/annotations',
49+
]),
50+
'change in name'
51+
).to.eql([{ index: 1, purlParts: ['name'] }])
52+
expect(
53+
checkPurls([
54+
'pkg:npm/%40angular/[email protected]',
55+
'invalid',
56+
'pkg:npm/%40angular/[email protected]',
57+
'pkg:golang/%40angular/[email protected]',
58+
]),
59+
'change in version and invalid PURL'
60+
).to.eql([
61+
{ index: 2, purlParts: ['version'] },
62+
{ index: 3, purlParts: ['type', 'version'] },
63+
])
64+
})
65+
})

tests/csaf_2_1/oasis.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ const excluded = [
1919
'6.1.27.11',
2020
'6.1.27.13',
2121
'6.1.37',
22-
'6.1.42',
2322
'6.1.44',
2423
'6.1.46',
2524
'6.1.47',

0 commit comments

Comments
 (0)