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
8 changes: 8 additions & 0 deletions .github/workflows/run-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,14 @@ jobs:
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- name: Start language tool
run: docker compose up -d
- name: Wait for app start
uses: ifaxity/wait-on-action@v1
with:
delay: 1
timeout: 30000
resource: tcp:localhost:8010
- run: npm ci
- run: npm run test-report
- run: npm run test-coverage-lcov
Expand Down
7 changes: 7 additions & 0 deletions DEVELOPMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,17 @@

## Table of Contents

- [Language Tool](#language-tool)
- [Code Style](#code-style)
- [Formatting with prettier](#formatting-with-prettier)
- [Quoting Strings](#quoting-strings)

## Language Tool

The informative test 6.3.16 needs a running languagetool server. To set one for development you can use the `compose.yml` provided with the repository:

docker compose up -d

## Code Style

### Formatting with prettier
Expand Down
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -359,11 +359,10 @@ The following tests are not yet implemented and therefore missing:

**Informative Tests**

- Informative Test 6.2.13
- Informative Test 6.2.14
- Informative Test 6.2.15
- Informative Test 6.2.16
- Informative Test 6.2.17
- Informative Test 6.3.13
- Informative Test 6.3.14
- Informative Test 6.3.15
- Informative Test 6.3.17

#### Module `csaf_2_1/schemaTests.js`

Expand Down Expand Up @@ -480,6 +479,7 @@ export const informativeTest_6_3_9: DocumentTest
export const informativeTest_6_3_10: DocumentTest
export const informativeTest_6_3_11: DocumentTest
export const informativeTest_6_3_12: DocumentTest
export const informativeTest_6_3_16: DocumentTest
```

[(back to top)](#bsi-csaf-validator-lib)
Expand Down
5 changes: 5 additions & 0 deletions compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
services:
languagetool:
image: collabora/languagetool
ports:
- 8010:8010
12 changes: 12 additions & 0 deletions context.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/**
* @typedef {object} Context
* @property {string} languageToolUrl The url to the language tool
*/

/**
* This is the context that is used to execute the tests. Modify it when
* initializing the library to change settings.
*
* @type {Context}
*/
export const context = { languageToolUrl: 'http://localhost:8010' }
1 change: 1 addition & 0 deletions csaf_2_1/informativeTests.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,4 @@ export { informativeTest_6_3_1 } from './informativeTests/informativeTest_6_3_1.
export { informativeTest_6_3_2 } from './informativeTests/informativeTest_6_3_2.js'
export { informativeTest_6_3_4 } from './informativeTests/informativeTest_6_3_4.js'
export { informativeTest_6_3_12 } from './informativeTests/informativeTest_6_3_12.js'
export { informativeTest_6_3_16 } from './informativeTests/informativeTest_6_3_16.js'
198 changes: 198 additions & 0 deletions csaf_2_1/informativeTests/informativeTest_6_3_16.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
/*
This test depends on the languagetool server to be available. See
https://languagetool.org/de. A `compose.yml` file is available in the
repository root to start an instance.
*/

import Ajv from 'ajv/dist/jtd.js'
import bcp47 from 'bcp47'
import { context } from '../../context.js'

const ajv = new Ajv()

const inputSchema = /** @type {const} */ ({
additionalProperties: true,
optionalProperties: {
document: {
additionalProperties: true,
optionalProperties: {
lang: { type: 'string' },
},
},
},
})

const validateInput = ajv.compile(inputSchema)

/**
* If the document language is given it MUST be tested that a grammar check for
* the given language does not find any mistakes. The test SHALL be skipped if
* the document language is not set. It SHALL fail if the given language is not
* supported.
*
* @param {unknown} doc
* @returns
*/
export async function informativeTest_6_3_16(doc) {
const ctx = {
infos: /** @type {Array<{ message: string; instancePath: string }>} */ ([]),
}

if (!validateInput(doc)) {
return ctx
}

const lang =
(doc.document?.lang &&
bcp47.parse(doc.document.lang)?.langtag.language.language) ??
'en'

/*
Check if the language is supported by the languagetool server.
*/
{
/**
* @typedef {object} Language
* @property {string} code
*/

/** @typedef {Language[]} Response */

const res = await fetch(new URL('/v2/languages', context.languageToolUrl), {
headers: {
accept: 'application/json',
},
})
if (!res.ok) throw new Error('request to languagetool failed')

const json = /** @type {Response} */ (await res.json())

if (!json.some((l) => l.code === lang)) {
ctx.infos.push({
instancePath: '/document/lang',
message: 'language is not supported',
})
}
}

for (const path of [
'/document/acknowledgments[]/summary',
'/document/aggregate_severity/text',
'/document/distribution/text',
'/document/notes[]/audience',
'/document/notes[]/text',
'/document/notes[]/title',
'/document/publisher/issuing_authority',
'/document/references[]/summary',
'/document/title',
'/document/tracking/revision_history[]/summary',
'/product_tree/product_groups[]/summary',
'/vulnerabilities[]/acknowledgments[]/summary',
'/vulnerabilities[]/involvements[]/summary',
'/vulnerabilities[]/notes[]/audience',
'/vulnerabilities[]/notes[]/text',
'/vulnerabilities[]/notes[]/title',
'/vulnerabilities[]/references[]/summary',
'/vulnerabilities[]/remediations[]/details',
'/vulnerabilities[]/remediations[]/entitlements[]',
'/vulnerabilities[]/remediations[]/restart_required/details',
'/vulnerabilities[]/threats[]/details',
'/vulnerabilities[]/title',
]) {
await checkPath(
[],
path.split('/').slice(1),
doc,
async (instancePath, text) => {
if (typeof text !== 'string') return
const result = await checkString(text, lang)
if (result.length) {
ctx.infos.push({
instancePath,
message: result.map((r) => r.message).join(' '),
})
}
}
)
}

return ctx
}

/**
* Checks the value behind `path` using the given `onCheck` function. This is a
* recursive helper function to loop through the list of paths in the spec.
*
* @param {string[]} reminder
* @param {string[]} path
* @param {unknown} value
* @param {(instancePath: string, value: string) => Promise<void>} onCheck
*/
async function checkPath(reminder, path, value, onCheck) {
if (value == null) return
const currentSegment = path.at(0)

if (!currentSegment) {
// We've reached the end. Now the `onCheck` function can be called to check
// the actual value.
if (typeof value === 'string') {
await onCheck('/' + reminder.join('/'), value)
}
} else if (currentSegment.endsWith('[]')) {
// The value is supposed to be an array for which every element needs to be
// checked ...
const arrayName = currentSegment.split('[')[0]
const array = Reflect.get(value, arrayName)

if (Array.isArray(array)) {
// ... But only if it's really an array.
for (const [elementIndex, element] of array.entries() ?? []) {
await checkPath(
[...reminder, arrayName, String(elementIndex)],
[...path.slice(1)],
element,
onCheck
)
}
}
} else {
// Otherwise it's something object-ish which we traverse recursively.
await checkPath(
[...reminder, currentSegment],
path.slice(1),
Reflect.get(value, currentSegment),
onCheck
)
}
}

/**
* Check the given string using the languagetool server.
*
* @param {string} str
* @param {string} lng
* @returns
*/
async function checkString(str, lng) {
/**
* @typedef {object} Match
* @property {string} message
*/

/**
* @typedef {object} Response
* @property {Match[]} matches
*/

const res = await fetch(new URL('/v2/check', context.languageToolUrl), {
method: 'POST',
body: new URLSearchParams([
['language', lng],
['text', str],
]),
})
if (!res.ok) throw new Error('request to languagetool failed')

const json = /** @type {Response} */ (await res.json())
return json.matches
}
26 changes: 15 additions & 11 deletions scripts/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,18 @@
import { spawn } from 'child_process'
import { fileURLToPath } from 'url'

spawn('mocha', ['tests', 'tests/csaf_2_1', ...process.argv.slice(2)], {
stdio: 'inherit',
shell: true,
env: {
...process.env,
DICPATH: fileURLToPath(new URL('../tests/dicts', import.meta.url)),
WORDLIST: fileURLToPath(
new URL('../tests/dicts/csaf_words.txt', import.meta.url)
),
},
})
spawn(
'mocha',
['-t', '10000', 'tests', 'tests/csaf_2_1', ...process.argv.slice(2)],
{
stdio: 'inherit',
shell: true,
env: {
...process.env,
DICPATH: fileURLToPath(new URL('../tests/dicts', import.meta.url)),
WORDLIST: fileURLToPath(
new URL('../tests/dicts/csaf_words.txt', import.meta.url)
),
},
}
)
21 changes: 21 additions & 0 deletions tests/csaf_2_1/informativeTest_6_3_16.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import assert from 'node:assert'
import { informativeTest_6_3_16 } from '../../csaf_2_1/informativeTests.js'
import { expect } from 'chai'

describe('informativeTest_6_3_16', function () {
it('only runs on relevant documents', async function () {
assert.equal(
(await informativeTest_6_3_16({ document: 'mydoc' })).infos.length,
0
)
})

it('fails if the language is not known', async function () {
const result = await informativeTest_6_3_16({
document: {
lang: 'zz',
},
})
expect(result.infos.length).to.eq(1)
})
})
1 change: 0 additions & 1 deletion tests/csaf_2_1/oasis.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,6 @@ const excluded = [
'6.3.13',
'6.3.14',
'6.3.15',
'6.3.16',
'6.3.17',
]

Expand Down