Skip to content

Commit 43f07da

Browse files
feat(CSAF2.1): #403 add mandatory test 6.2.39.2
1 parent 54d3dd5 commit 43f07da

File tree

6 files changed

+211
-1
lines changed

6 files changed

+211
-1
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -463,6 +463,7 @@ export const recommendedTest_6_2_18: DocumentTest
463463
export const recommendedTest_6_2_19: DocumentTest
464464
export const recommendedTest_6_2_20: DocumentTest
465465
export const recommendedTest_6_2_22: DocumentTest
466+
export const recommendedTest_6_2_39_2: DocumentTest
466467
```
467468
468469
[(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_39_2 } from './recommendedTests/recommendedTest_6_2_39_2.js'
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
import Ajv from 'ajv/dist/jtd.js'
2+
import translations from '../../lib/language_specific_translation/translations.js'
3+
import bcp47 from 'bcp47'
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+
category: { type: 'string' },
20+
},
21+
optionalProperties: {
22+
lang: {
23+
type: 'string',
24+
},
25+
notes: {
26+
elements: {
27+
additionalProperties: true,
28+
optionalProperties: {
29+
category: {
30+
type: 'string',
31+
},
32+
title: {
33+
type: 'string',
34+
},
35+
},
36+
},
37+
},
38+
},
39+
},
40+
},
41+
})
42+
43+
const validateSchema = ajv.compile(inputSchema)
44+
45+
/**
46+
* Checks if the document language is specified and not English
47+
*
48+
* @param {string | undefined} language - The language expression to check
49+
* @returns {boolean} True if the language is valid, false otherwise
50+
*/
51+
export function isLangSpecifiedAndNotEnglish(language) {
52+
return (
53+
!!language && !(bcp47.parse(language)?.langtag.language.language === 'en')
54+
)
55+
}
56+
57+
/**
58+
* test whether exactly one item in document notes exists that has the given title.
59+
* and the given category.
60+
* @param {({} & { category?: string | undefined; title?: string | undefined; } & Record<string, unknown>)[]} notes
61+
* @param {string} titleToFind
62+
* @param {string} category
63+
* @returns {boolean} True if the language is valid, false otherwise
64+
*/
65+
function containsOneNoteWithTitleAndCategory(notes, titleToFind, category) {
66+
return (
67+
notes.filter(
68+
(note) => note.category === category && note.title === titleToFind
69+
).length === 1
70+
)
71+
}
72+
73+
/**
74+
* Get the language specific translation of the given i18nKey
75+
* @param {{ document: { lang?: string; }; }} doc
76+
* @param {string} i18nKey
77+
* @return {string | undefined}
78+
*/
79+
export function getTranslationInDocumentLang(doc, i18nKey) {
80+
if (!doc.document.lang) {
81+
return undefined
82+
}
83+
const language = bcp47.parse(doc.document.lang)?.langtag.language.language
84+
85+
/** @type {Record<string, Record <string,string>>}*/
86+
const translationByLang = translations.translation
87+
if (!language || !translationByLang[language]) {
88+
return undefined
89+
} else {
90+
return translationByLang[language][i18nKey]
91+
}
92+
}
93+
94+
/**
95+
* If the document language is specified but not English, and the license_expression contains license
96+
* identifiers or exceptions that are not listed in the SPDX license list or Aboutcode's "ScanCode LicenseDB",
97+
* it MUST be tested that exactly one item in document notes exists that has the language specific translation
98+
* of the term License as title. The category of this item MUST be legal_disclaimer.
99+
* If no language-specific translation has been recorded, the test MUST be skipped
100+
* and output information to the user that no such translation is known.
101+
*
102+
* @param {unknown} doc
103+
*/
104+
export function recommendedTest_6_2_39_2(doc) {
105+
/*
106+
The `ctx` variable holds the state that is accumulated during the test run and is
107+
finally returned by the function.
108+
*/
109+
const ctx = {
110+
warnings:
111+
/** @type {Array<{ instancePath: string; message: string }>} */ ([]),
112+
}
113+
114+
const noteCategory = 'description'
115+
116+
if (!validateSchema(doc) || doc.document.category !== 'csaf_withdrawn') {
117+
return ctx
118+
}
119+
120+
const withdrawalInDocLang = getTranslationInDocumentLang(
121+
doc,
122+
'reasoning_for_withdrawal'
123+
)
124+
if (!withdrawalInDocLang) {
125+
ctx.warnings.push({
126+
instancePath: '/document/notes',
127+
message:
128+
'no language specific translation for "Reasoning for Withdrawal" has been recorded',
129+
})
130+
return ctx
131+
}
132+
133+
if (isLangSpecifiedAndNotEnglish(doc.document.lang)) {
134+
const notes = doc.document.notes
135+
if (
136+
!notes ||
137+
!containsOneNoteWithTitleAndCategory(
138+
notes,
139+
withdrawalInDocLang,
140+
'description'
141+
)
142+
) {
143+
ctx.warnings.push({
144+
instancePath: '/document/notes',
145+
message:
146+
`for document category "csaf_withdrawn" exactly one note must exist ` +
147+
`with note category "${noteCategory}" and title "${withdrawalInDocLang}`,
148+
})
149+
}
150+
}
151+
152+
return ctx
153+
}
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+
}

tests/csaf_2_1/oasis.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,6 @@ const excluded = [
5858
'6.2.36',
5959
'6.2.37',
6060
'6.2.39.1',
61-
'6.2.39.2',
6261
'6.2.39.3',
6362
'6.2.39.4',
6463
'6.2.40',
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import {
2+
getTranslationInDocumentLang,
3+
recommendedTest_6_2_39_2,
4+
} from '../../csaf_2_1/recommendedTests/recommendedTest_6_2_39_2.js'
5+
import { expect } from 'chai'
6+
import assert from 'node:assert'
7+
8+
describe('recommendedTest_6_2_39_2', function () {
9+
it('only runs on relevant documents', function () {
10+
assert.equal(recommendedTest_6_2_39_2({}).warnings.length, 0)
11+
})
12+
13+
it('only runs on valid language', function () {
14+
assert.equal(
15+
recommendedTest_6_2_39_2({
16+
document: { lang: '123', license_expression: 'MIT' },
17+
}).warnings.length,
18+
0
19+
)
20+
})
21+
22+
it('check get ReasoningForWithdrawal in document lang', function () {
23+
expect(
24+
getTranslationInDocumentLang(
25+
{ document: { lang: 'de' } },
26+
'reasoning_for_withdrawal'
27+
)
28+
).to.eq('Begründung für die Zurückziehung')
29+
expect(
30+
getTranslationInDocumentLang(
31+
{ document: { lang: 'es' } },
32+
'reasoning_for_withdrawal'
33+
)
34+
).to.eq(undefined)
35+
expect(
36+
getTranslationInDocumentLang({ document: {} }, 'reasoning_for_withdrawal')
37+
).to.eq(undefined)
38+
})
39+
})

0 commit comments

Comments
 (0)