Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -348,7 +348,6 @@ The following tests are not yet implemented and therefore missing:
- Recommended Test 6.2.37
- Recommended Test 6.2.38
- Recommended Test 6.2.39
- Recommended Test 6.2.40
- Recommended Test 6.2.41
- Recommended Test 6.2.42
- Recommended Test 6.2.43
Expand Down Expand Up @@ -461,6 +460,7 @@ export const recommendedTest_6_2_17: DocumentTest
export const recommendedTest_6_2_18: DocumentTest
export const recommendedTest_6_2_22: DocumentTest
export const recommendedTest_6_2_23: DocumentTest
export const recommendedTest_6_2_40: DocumentTest
```

[(back to top)](#bsi-csaf-validator-lib)
Expand Down
1 change: 1 addition & 0 deletions csaf_2_1/recommendedTests.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,4 @@ export { recommendedTest_6_2_27 } from './recommendedTests/recommendedTest_6_2_2
export { recommendedTest_6_2_28 } from './recommendedTests/recommendedTest_6_2_28.js'
export { recommendedTest_6_2_29 } from './recommendedTests/recommendedTest_6_2_29.js'
export { recommendedTest_6_2_38 } from './recommendedTests/recommendedTest_6_2_38.js'
export { recommendedTest_6_2_40 } from './recommendedTests/recommendedTest_6_2_40.js'
132 changes: 132 additions & 0 deletions csaf_2_1/recommendedTests/recommendedTest_6_2_40.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import Ajv from 'ajv/dist/jtd.js'
import translations from '../../lib/language_specific_translation/translations.js'
import bcp47 from 'bcp47'

const ajv = new Ajv()

const inputSchema = /** @type {const} */ ({
additionalProperties: true,
properties: {
document: {
additionalProperties: true,
optionalProperties: {
lang: { type: 'string' },
notes: {
elements: {
additionalProperties: true,
optionalProperties: {
category: { type: 'string' },
title: { type: 'string' },
group_ids: { elements: { type: 'string' } },
product_ids: { elements: { type: 'string' } },
},
},
},
},
},
},
})

const validate = ajv.compile(inputSchema)

/**
* Checks if the document language is English or unspecified
*
* @param {string | undefined} language The language expression to check
* @returns {boolean} True if the language is English or unspecified, false otherwise
*/
export function isLangEnglishOrUnspecified(language) {
return !language || bcp47.parse(language)?.langtag.language.language === 'en'
}

/**
* Get the language specific translation of the given i18nKey
* @param {string } lang
* @param {string} i18nKey
* @returns {string | undefined} - The language specific translation of the `i18nKey`
* - or undefined if the provided language could not be parsed as a BCP 47 tag
* - or undefined if no translation of the `i18nKey` could be found
*/
export function getTranslationInDocumentLang(lang, i18nKey) {
const language = bcp47.parse(lang)?.langtag.language.language

/** @type {Record<string, Record <string,string>>}*/
const translationByLang = translations.translation

if (!language || !translationByLang[language]) {
return undefined
} else {
return translationByLang[language][i18nKey]
}
}

/**
* Check if the given note item contains at least one of `group_ids` or `product_ids`
* @param {{ group_ids?: string[]; product_ids?: string[]}} note
* @return {boolean}
*/
export function containsNoteGroupIdOrProductId(note) {
return !!(note.group_ids || note.product_ids)
}

/**
* This implements the recommended test 6.2.40 of the CSAF 2.1 standard.
*
/**
* @param {any} doc
*/
export function recommendedTest_6_2_40(doc) {
/** @type { {warnings: Array<{ message: string; instancePath: string }>;
* infos: Array<{ message: string; instancePath: string }>}} */
const context = {
warnings: [],
infos: [],
}

if (!validate(doc)) {
return context
}
const documentLanguage = doc.document.lang
doc.document.notes?.forEach((note, noteIndex) => {
if (note.category === 'description') {
if (isLangEnglishOrUnspecified(documentLanguage)) {
if (note.title?.startsWith('Product Description')) {
if (!containsNoteGroupIdOrProductId(note)) {
context.warnings.push({
instancePath: `/document/notes/${noteIndex}`,
message:
'the given note item must include one of the elements "group_id" or "product_id"',
})
}
}
} else {
const translation = getTranslationInDocumentLang(
/** @type {string} */ (
documentLanguage
) /* This cast is allowed since the else statement is just called
id documentLanguage is not undefined. Without the cast one must check here too
if documentLanguage is not undefined, which would be a code fragment that is newer used*/,
'product_description'
)
if (!translation) {
context.infos.push({
instancePath: `/document/notes/${noteIndex}`,
message:
'no language specific translation for the "title" of this note has been recorded',
})
return context
}
if (note.title?.startsWith(translation)) {
if (!containsNoteGroupIdOrProductId(note)) {
context.warnings.push({
instancePath: `/document/notes/${noteIndex}`,
message:
'the given note item must include one of the elements "group_id" or "product_id"',
})
}
}
}
}
})
return context
}
14 changes: 14 additions & 0 deletions lib/language_specific_translation/translations.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
export default {
$schema:
'https://raw.githubusercontent.com/oasis-tcs/csaf/master/csaf_2.1/test/language_specific_translation/translations_json_schema.json',
translation_version: '2.1',
translation: {
de: {
license: 'Lizenz',
product_description: 'Produktbeschreibung',
reasoning_for_supersession: 'Begründung für die Ersetzung',
reasoning_for_withdrawal: 'Begründung für die Zurückziehung',
superseding_document: 'Ersetzendes Dokument',
},
},
}
1 change: 0 additions & 1 deletion tests/csaf_2_1/oasis.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,6 @@ const excluded = [
'6.2.39.2',
'6.2.39.3',
'6.2.39.4',
'6.2.40',
'6.2.41',
'6.2.42',
'6.2.43',
Expand Down
43 changes: 43 additions & 0 deletions tests/csaf_2_1/recommendedTest_6_2_40.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import assert from 'node:assert/strict'
import { recommendedTest_6_2_40 } from '../../csaf_2_1/recommendedTests/recommendedTest_6_2_40.js'

describe('recommendedTest_6_2_40', function () {
it('only runs on relevant documents', function () {
assert.equal(recommendedTest_6_2_40({}).warnings.length, 0)
})
it('skips empty objects', function () {
assert.equal(
recommendedTest_6_2_40({
document: {
notes: [
{
category: 'description',
text: 'Product A is a local time tracking tool. It is mainly used by software developers and can be connected with most modern time-tracking systems.',
title: 'Product Description',
},
{}, // skip this empty object
],
},
}).warnings.length,
1
)
})
it('no language specific translation', function () {
assert.equal(
recommendedTest_6_2_40({
document: {
lang: '123456789',
notes: [
{
category: 'description',
product_ids: ['CSAFPID-9080700'],
text: 'Produkt A is ein lokales Zeiterfassungstool. Es wird hauptsächlich von Softwareentwicklern verwendet und kann an die meisten modernen Zeiterfasssungssysteme angebunden werden.',
title: 'Produkt A wird hier beschrieben',
},
],
},
}).infos.length,
1
)
})
})