Skip to content

Commit 2a0a03e

Browse files
feat(CSAF2.1): #364 add recommended test 6.2.42
1 parent fbc3fd4 commit 2a0a03e

File tree

9 files changed

+8974
-2
lines changed

9 files changed

+8974
-2
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -366,7 +366,6 @@ The following tests are not yet implemented and therefore missing:
366366
- Recommended Test 6.2.43
367367
- Recommended Test 6.2.44
368368
- Recommended Test 6.2.45
369-
- Recommended Test 6.2.46
370369
371370
**Informative Tests**
372371
@@ -462,6 +461,7 @@ export const recommendedTest_6_2_16: DocumentTest
462461
export const recommendedTest_6_2_17: DocumentTest
463462
export const recommendedTest_6_2_18: DocumentTest
464463
export const recommendedTest_6_2_22: DocumentTest
464+
export const recommendedTest_6_2_46: DocumentTest
465465
```
466466
467467
[(back to top)](#bsi-csaf-validator-lib)

csaf_2_1/recommendedTests.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,4 @@ export { recommendedTest_6_2_27 } from './recommendedTests/recommendedTest_6_2_2
3131
export { recommendedTest_6_2_28 } from './recommendedTests/recommendedTest_6_2_28.js'
3232
export { recommendedTest_6_2_29 } from './recommendedTests/recommendedTest_6_2_29.js'
3333
export { recommendedTest_6_2_38 } from './recommendedTests/recommendedTest_6_2_38.js'
34+
export { recommendedTest_6_2_46 } from './recommendedTests/recommendedTest_6_2_46.js'
Lines changed: 237 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,237 @@
1+
import Ajv from 'ajv/dist/jtd.js'
2+
import { parse } from 'license-expressions'
3+
import license_information from '../../lib/license/license_information.js'
4+
import translations from '../../lib/language_specific_translation/translations.js'
5+
import bcp47 from 'bcp47'
6+
7+
const ajv = new Ajv()
8+
9+
/*
10+
This is the jtd schema that needs to match the input document so that the
11+
test is activated. If this schema doesn't match it normally means that the input
12+
document does not validate against the csaf json schema or optional fields that
13+
the test checks are not present.
14+
*/
15+
const inputSchema = /** @type {const} */ ({
16+
additionalProperties: true,
17+
properties: {
18+
document: {
19+
additionalProperties: true,
20+
properties: {
21+
license_expression: {
22+
type: 'string',
23+
},
24+
},
25+
optionalProperties: {
26+
lang: {
27+
type: 'string',
28+
},
29+
notes: {
30+
elements: {
31+
additionalProperties: true,
32+
optionalProperties: {
33+
category: {
34+
type: 'string',
35+
},
36+
title: {
37+
type: 'string',
38+
},
39+
},
40+
},
41+
},
42+
},
43+
},
44+
},
45+
})
46+
47+
const validateSchema = ajv.compile(inputSchema)
48+
49+
const ABOUT_CODE_LICENSE_REF_PREFIX = 'LicenseRef-scancode-'
50+
51+
const ABOUT_CODE_LICENSE_KEYS = new Set(
52+
license_information.licenses
53+
.filter((license) => license.source === 'aboutCode')
54+
.map((license) => license.license_key)
55+
)
56+
57+
const SPDX_LICENSE_KEYS = new Set(
58+
license_information.licenses
59+
.filter((license) => license.source === 'spdx')
60+
.map((license) => license.license_key)
61+
)
62+
63+
/**
64+
* Check whether license identifiers are not listed Aboutcode's "ScanCode LicenseDB"
65+
* @param {string} licenseRefToCheck
66+
* @return {boolean}
67+
*/
68+
function isAboutCodeLicense(licenseRefToCheck) {
69+
if (!licenseRefToCheck.startsWith(ABOUT_CODE_LICENSE_REF_PREFIX)) {
70+
return false
71+
} else {
72+
const licenseKey = licenseRefToCheck.substring(
73+
ABOUT_CODE_LICENSE_REF_PREFIX.length
74+
)
75+
return ABOUT_CODE_LICENSE_KEYS.has(licenseKey)
76+
}
77+
}
78+
79+
/**
80+
* Recursively checks if a parsed license expression contains not listed licenses.
81+
*
82+
* @param {import('license-expressions').ParsedSpdxExpression} parsedExpression - The parsed license expression
83+
* @returns {boolean} True if the expression contains any license references, false otherwise
84+
*/
85+
function containsNotListedLicenses(parsedExpression) {
86+
// If it's a LicenseRef type directly
87+
if ('licenseRef' in parsedExpression) {
88+
return !isAboutCodeLicense(parsedExpression.licenseRef)
89+
}
90+
91+
if (
92+
'license' in parsedExpression &&
93+
!SPDX_LICENSE_KEYS.has(parsedExpression.license)
94+
) {
95+
return true
96+
}
97+
98+
if (
99+
'exception' in parsedExpression &&
100+
parsedExpression.exception &&
101+
!SPDX_LICENSE_KEYS.has(parsedExpression.exception)
102+
) {
103+
return true
104+
}
105+
106+
// If it's a conjunction, check both sides
107+
if ('conjunction' in parsedExpression) {
108+
return (
109+
containsNotListedLicenses(parsedExpression.left) ||
110+
containsNotListedLicenses(parsedExpression.right)
111+
)
112+
}
113+
114+
// If it's a LicenseInfo type, it doesn't contain not listed licenses
115+
return false
116+
}
117+
118+
/**
119+
* Checks if a license expression string contains any document references.
120+
*
121+
* @param {string} licenseToCheck - The license expression to check
122+
* @returns {boolean} True if the license expression contains any document references, false otherwise
123+
*/
124+
function hasNotListedLicenses(licenseToCheck) {
125+
const parseResult = parse(licenseToCheck)
126+
return containsNotListedLicenses(parseResult)
127+
}
128+
129+
/**
130+
* check if the license_expression contains license identifiers or exceptions
131+
* that are not listed in the SPDX license list or Aboutcode's "ScanCode LicenseDB"
132+
*
133+
* @param {string} licenseToCheck - The license expression to check
134+
* @returns {boolean} True if the license has not listed licenses, false otherwise
135+
*/
136+
export function existsNotListedLicenses(licenseToCheck) {
137+
return !!licenseToCheck && hasNotListedLicenses(licenseToCheck)
138+
}
139+
140+
/**
141+
* Checks if the document language is specified and not English
142+
*
143+
* @param {string | undefined} language - The language expression to check
144+
* @returns {boolean} True if the language is valid, false otherwise
145+
*/
146+
export function isLangSpecifiedAndNotEnglish(language) {
147+
return (
148+
!!language && !(bcp47.parse(language)?.langtag.language.language === 'en')
149+
)
150+
}
151+
152+
/**
153+
* test whether exactly one item in document notes exists that has the given title.
154+
* The category of this item MUST be legal_disclaimer.
155+
* @param {({} & { category?: string | undefined; title?: string | undefined; } & Record<string, unknown>)[]} notes
156+
* @param {string} titleToFind
157+
* @returns {boolean} True if the language is valid, false otherwise
158+
*/
159+
function containsOneLegalDisclaimerWithTitle(notes, titleToFind) {
160+
return (
161+
notes.filter(
162+
(note) =>
163+
note.category === 'legal_disclaimer' && note.title === titleToFind
164+
).length === 1
165+
)
166+
}
167+
168+
/**
169+
* Get the language specific translation of the term License
170+
* @param {{ document: { lang?: string; }; }} doc
171+
* @return {string | undefined}
172+
*/
173+
export function getLicenseInDocumentLang(doc) {
174+
if (!doc.document.lang) {
175+
return undefined
176+
}
177+
const language = bcp47.parse(doc.document.lang)?.langtag.language.language
178+
179+
/** @type {Record<string, Record <string,string>>}*/
180+
const translationByLang = translations.translation
181+
if (!language || !translationByLang[language]) {
182+
return undefined
183+
} else {
184+
return translationByLang[language]['license']
185+
}
186+
}
187+
188+
/**
189+
* If the document language is specified but not English, and the license_expression contains license
190+
* identifiers or exceptions that are not listed in the SPDX license list or Aboutcode's "ScanCode LicenseDB",
191+
* it MUST be tested that exactly one item in document notes exists that has the language specific translation
192+
* of the term License as title. The category of this item MUST be legal_disclaimer.
193+
* If no language-specific translation has been recorded, the test MUST be skipped
194+
* and output information to the user that no such translation is known.
195+
*
196+
* @param {unknown} doc
197+
*/
198+
export function recommendedTest_6_2_46(doc) {
199+
/*
200+
The `ctx` variable holds the state that is accumulated during the test run and is
201+
finally returned by the function.
202+
*/
203+
const ctx = {
204+
warnings:
205+
/** @type {Array<{ instancePath: string; message: string }>} */ ([]),
206+
}
207+
208+
if (!validateSchema(doc)) {
209+
return ctx
210+
}
211+
212+
const licenseInDocLang = getLicenseInDocumentLang(doc)
213+
if (!licenseInDocLang) {
214+
return ctx
215+
}
216+
217+
const licenseToCheck = doc.document.license_expression
218+
if (isLangSpecifiedAndNotEnglish(doc.document.lang)) {
219+
if (existsNotListedLicenses(licenseToCheck)) {
220+
const notes = doc.document.notes
221+
if (
222+
!notes ||
223+
!containsOneLegalDisclaimerWithTitle(notes, licenseInDocLang)
224+
) {
225+
ctx.warnings.push({
226+
instancePath: '/document/notes',
227+
message:
228+
'The license_expression contains a license identifiers or exceptions that is not ' +
229+
'listed in Aboutcode or SPDX license list. Therefore exactly one note with ' +
230+
'title "License" and category "legal_disclaimer" must exist',
231+
})
232+
}
233+
}
234+
}
235+
236+
return ctx
237+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
/**
2+
* javascript version of JSON file: csaf_2.1/language_specific_translation/translations.json
3+
*/
4+
export default {
5+
$schema:
6+
'https://raw.githubusercontent.com/oasis-tcs/csaf/master/csaf_2.1/test/language_specific_translation/translations_json_schema.json',
7+
translation_version: '2.1',
8+
translation: {
9+
de: {
10+
license: 'Lizenz',
11+
product_description: 'Produktbeschreibung',
12+
reasoning_for_supersession: 'Begründung für die Ersetzung',
13+
reasoning_for_withdrawal: 'Begründung für die Zurückziehung',
14+
superseding_document: 'Ersetzendes Dokument',
15+
},
16+
},
17+
}

0 commit comments

Comments
 (0)