Skip to content

Commit f5aee9a

Browse files
feat(CSAF2.1): #356 add mandatory test 6.1.55
1 parent c8064b6 commit f5aee9a

File tree

8 files changed

+8921
-2
lines changed

8 files changed

+8921
-2
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -335,7 +335,6 @@ The following tests are not yet implemented and therefore missing:
335335
- Mandatory Test 6.1.52
336336
- Mandatory Test 6.1.53
337337
- Mandatory Test 6.1.54
338-
- Mandatory Test 6.1.55
339338
340339
**Recommended Tests**
341340
@@ -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_55: 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_55 } from './mandatoryTests/mandatoryTest_6_1_55.js'
Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
import Ajv from 'ajv/dist/jtd.js'
2+
import { parse, validate } from 'license-expressions'
3+
import license_information from '../../lib/license/license_information.js'
4+
5+
const ajv = new Ajv()
6+
7+
/*
8+
This is the jtd schema that needs to match the input document so that the
9+
test is activated. If this schema doesn't match it normally means that the input
10+
document does not validate against the csaf json schema or optional fields that
11+
the test checks are not present.
12+
*/
13+
const inputSchema = /** @type {const} */ ({
14+
additionalProperties: true,
15+
properties: {
16+
document: {
17+
additionalProperties: true,
18+
properties: {
19+
license_expression: {
20+
type: 'string',
21+
},
22+
},
23+
optionalProperties: {
24+
lang: {
25+
type: 'string',
26+
},
27+
notes: {
28+
elements: {
29+
additionalProperties: true,
30+
optionalProperties: {
31+
category: {
32+
type: 'string',
33+
},
34+
title: {
35+
type: 'string',
36+
},
37+
},
38+
},
39+
},
40+
},
41+
},
42+
},
43+
})
44+
45+
const validateSchema = ajv.compile(inputSchema)
46+
47+
const ENGLISH_LANGUAGES = [
48+
'en',
49+
'en-AU',
50+
'en-BZ',
51+
'en-CA',
52+
'en-CB',
53+
'en-IE',
54+
'en-JM',
55+
'en-NZ',
56+
'en-PH',
57+
'en-PH',
58+
'en-TT',
59+
'en-US',
60+
'en-ZA',
61+
'en-ZW',
62+
]
63+
64+
const ABOUT_CODE_LICENSE_REF_PREFIX = 'LicenseRef-scancode-'
65+
66+
const ABOUT_CODE_LICENSE_KEYS = new Set(
67+
license_information.licenses
68+
.filter((license) => !license.deprecated && license.source === 'aboutCode')
69+
.map((license) => license.license_key)
70+
)
71+
72+
/**
73+
* Check whether license identifiers are not listed Aboutcode's "ScanCode LicenseDB"
74+
* @param {string} licenseRefToCheck
75+
* @return {boolean}
76+
*/
77+
function isAboutCodeLicense(licenseRefToCheck) {
78+
if (!licenseRefToCheck.startsWith(ABOUT_CODE_LICENSE_REF_PREFIX)) {
79+
return false
80+
} else {
81+
const licenseKey = licenseRefToCheck.substring(
82+
ABOUT_CODE_LICENSE_REF_PREFIX.length
83+
)
84+
return ABOUT_CODE_LICENSE_KEYS.has(licenseKey)
85+
}
86+
}
87+
88+
/**
89+
* Recursively checks if a parsed license expression contains not listed licenses.
90+
*
91+
* @param {import('license-expressions').ParsedSpdxExpression} parsedExpression - The parsed license expression
92+
* @returns {boolean} True if the expression contains any license references, false otherwise
93+
*/
94+
function containsNotListedLicenses(parsedExpression) {
95+
// If it's a LicenseRef type directly
96+
if ('licenseRef' in parsedExpression) {
97+
return !isAboutCodeLicense(parsedExpression.licenseRef)
98+
}
99+
100+
// If it's a conjunction, check both sides
101+
if ('conjunction' in parsedExpression) {
102+
return (
103+
containsNotListedLicenses(parsedExpression.left) ||
104+
containsNotListedLicenses(parsedExpression.right)
105+
)
106+
}
107+
108+
// If it's a LicenseInfo type, it doesn't contain not listed licenses
109+
return false
110+
}
111+
112+
/**
113+
* Checks if a license expression string contains any document references.
114+
*
115+
* @param {string} licenseToCheck - The license expression to check
116+
* @returns {boolean} True if the license expression contains any document references, false otherwise
117+
*/
118+
function hasNotListedLicenses(licenseToCheck) {
119+
const parseResult = parse(licenseToCheck)
120+
return containsNotListedLicenses(parseResult)
121+
}
122+
123+
/**
124+
* check if the license_expression contains license identifiers or exceptions
125+
* that are not listed in the SPDX license list or Aboutcode's "ScanCode LicenseDB"
126+
*
127+
* @param {string} licenseToCheck - The license expression to check
128+
* @returns {boolean} True if the license has not listed licenses, false otherwise
129+
*/
130+
export function existsNotListedLicenses(licenseToCheck) {
131+
return (
132+
!licenseToCheck ||
133+
(validate(licenseToCheck).valid && hasNotListedLicenses(licenseToCheck))
134+
)
135+
}
136+
137+
/**
138+
* Checks if the document language is English or unspecified
139+
*
140+
* @param {string | undefined} language - The language expression to check
141+
* @returns {boolean} True if the language is valid, false otherwise
142+
*/
143+
export function isLangEnglishOrUnspecified(language) {
144+
return !language || ENGLISH_LANGUAGES.includes(language)
145+
}
146+
147+
/**
148+
* test whether exactly one item in document notes exists that has the title License. The category of this item MUST be legal_disclaimer.
149+
* @param {({} & { category?: string | undefined; title?: string | undefined; } & Record<string, unknown>)[]} notes
150+
* @returns {boolean} True if the language is valid, false otherwise
151+
*/
152+
function containsOneLegalNote(notes) {
153+
return (
154+
notes.filter(
155+
(note) => note.category === 'legal_disclaimer' && note.title === 'License'
156+
).length === 1
157+
)
158+
}
159+
160+
/**
161+
* It MUST be tested that the license expression is valid.
162+
*
163+
* @param {unknown} doc
164+
*/
165+
export function mandatoryTest_6_1_55(doc) {
166+
/*
167+
The `ctx` variable holds the state that is accumulated during the test run and is
168+
finally returned by the function.
169+
*/
170+
const ctx = {
171+
errors:
172+
/** @type {Array<{ instancePath: string; message: string }>} */ ([]),
173+
isValid: true,
174+
}
175+
176+
if (!validateSchema(doc)) {
177+
return ctx
178+
}
179+
180+
const licenseToCheck = doc.document.license_expression
181+
if (isLangEnglishOrUnspecified(doc.document.lang)) {
182+
if (existsNotListedLicenses(licenseToCheck)) {
183+
const notes = doc.document.notes
184+
if (!notes || !containsOneLegalNote(notes)) {
185+
ctx.isValid = false
186+
ctx.errors.push({
187+
instancePath: '/document/notes',
188+
message:
189+
`The license_expression contains a license identifiers or exceptions that is not ` +
190+
`listed in Aboutcode's or SPDX license list. Therefore exactly one note with ` +
191+
` title 'License' and category 'legal_disclaimer' must exist`,
192+
})
193+
}
194+
}
195+
}
196+
197+
return ctx
198+
}

0 commit comments

Comments
 (0)