Skip to content

Commit cf37942

Browse files
feat(CSAF2.1): #287 add mandatory test 6.1.42
1 parent 54d3dd5 commit cf37942

File tree

5 files changed

+274
-2
lines changed

5 files changed

+274
-2
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -322,7 +322,6 @@ The following tests are not yet implemented and therefore missing:
322322
- Mandatory Test 6.1.27.18
323323
- Mandatory Test 6.1.27.19
324324
- Mandatory Test 6.1.36
325-
- Mandatory Test 6.1.42
326325
- Mandatory Test 6.1.43
327326
- Mandatory Test 6.1.44
328327
- Mandatory Test 6.1.45
@@ -435,6 +434,7 @@ export const mandatoryTest_6_1_38: DocumentTest
435434
export const mandatoryTest_6_1_39: DocumentTest
436435
export const mandatoryTest_6_1_40: DocumentTest
437436
export const mandatoryTest_6_1_41: DocumentTest
437+
export const mandatoryTest_6_1_42: DocumentTest
438438
```
439439
440440
[(back to top)](#bsi-csaf-validator-lib)

csaf_2_1/mandatoryTests.js

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

tests/csaf_2_1/oasis.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@ const excluded = [
2727
'6.1.27.18',
2828
'6.1.27.19',
2929
'6.1.37',
30-
'6.1.42',
3130
'6.1.43',
3231
'6.1.44',
3332
'6.1.45',

0 commit comments

Comments
 (0)