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 @@ -344,7 +344,6 @@ The following tests are not yet implemented and therefore missing:
- Recommended Test 6.2.44
- Recommended Test 6.2.45
- Recommended Test 6.2.46
- Recommended Test 6.2.49
- Recommended Test 6.2.50.1
- Recommended Test 6.2.50.2
- Recommended Test 6.2.50.3
Expand Down Expand Up @@ -501,6 +500,7 @@ export const recommendedTest_6_2_41: DocumentTest
export const recommendedTest_6_2_43: DocumentTest
export const recommendedTest_6_2_47: DocumentTest
export const recommendedTest_6_2_48: DocumentTest
export const recommendedTest_6_2_49: DocumentTest
export const recommendedTest_6_2_53: DocumentTest
```

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 @@ -42,4 +42,5 @@ export { recommendedTest_6_2_41 } from './recommendedTests/recommendedTest_6_2_4
export { recommendedTest_6_2_43 } from './recommendedTests/recommendedTest_6_2_43.js'
export { recommendedTest_6_2_47 } from './recommendedTests/recommendedTest_6_2_47.js'
export { recommendedTest_6_2_48 } from './recommendedTests/recommendedTest_6_2_48.js'
export { recommendedTest_6_2_49 } from './recommendedTests/recommendedTest_6_2_49.js'
export { recommendedTest_6_2_53 } from './recommendedTests/recommendedTest_6_2_53.js'
84 changes: 84 additions & 0 deletions csaf_2_1/recommendedTests/recommendedTest_6_2_49.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { Ajv } from 'ajv/dist/jtd.js'
import { walkPath } from '../../lib/walkPaths.js'

const ajv = new Ajv()

const branchSchema = /** @type {const} */ ({
additionalProperties: true,
optionalProperties: {
category: { type: 'string' },
name: { type: 'string' },
},
})

const validateBranch = ajv.compile(branchSchema)

// regex to match the prefix of a product version range that uses the `vers` comparator
const VERS_PREFIX = /^vers:[a-z\.\-\+][a-z0-9\.\-\+]*\//

// regex to match an upper open ended constraint, e.g. `>=1.0.0` or `>1.0.0`
const UPPER_OPEN_ENDED_CONSTRAINT = /^>=?/

/**
* This implements the recommended test 6.2.49 of the CSAF 2.1 standard.
*
* @param {unknown} doc
*/
export async function recommendedTest_6_2_49(doc) {
const ctx = {
warnings:
/** @type {Array<{ instancePath: string; message: string }>} */ ([]),
}

await walkPath(
doc,
'/product_tree/branches[*]',
async (instancePath, value) => {
if (!validateBranch(value)) return
const branch = /** @type {{ category?: string; name?: string }} */ (value)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this type cast is not needed as the validator already infers the type:

Suggested change
const branch = /** @type {{ category?: string; name?: string }} */ (value)
const branch = value


if (
branch.category !== 'product_version_range' ||
typeof branch.name !== 'string'
) {
return
}

if (isUpperOpenEnded(branch.name)) {
ctx.warnings.push({
instancePath: `${instancePath}/name`,
message: `The product version range "${branch.name}" is upper open ended.`,
})
}
}
)

return ctx
}

/**
* Checks whether the last bound defining constraint of a product version
* range (vers or vls) is upper open ended.
*
* @param {string} name
* @returns {boolean}
*/
function isUpperOpenEnded(name) {
const trimmed = name.trim()
if (trimmed === 'vers:all/*') return false

const versMatch = trimmed.match(VERS_PREFIX)
const constraints = versMatch ? trimmed.slice(versMatch[0].length) : trimmed

const boundConstraints = constraints
.split('|')
.map((constraint) => constraint.trim())
.filter(
(constraint) => constraint.length > 0 && !constraint.startsWith('!=')
)

if (boundConstraints.length === 0) return false

const lastConstraint = boundConstraints[boundConstraints.length - 1]
return UPPER_OPEN_ENDED_CONSTRAINT.test(lastConstraint)
}
5 changes: 4 additions & 1 deletion lib/walkPaths.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ export async function walkPath(root, path, onCheck) {
// Reached the end of the path: call the callback now ...

await onCheck('/' + resolvedSegments.join('/'), node)
} else if (typeof node !== 'object') {
// The path continues, but the current node is a primitive and therefore cannot have further
// properties to traverse into.
} else if (keyEntry.endsWith('[*]')) {
// ... Recursive-descent array: visit every element and re-apply this same
// segment on each element to handle arbitrary nesting depth.
Expand Down Expand Up @@ -63,7 +66,7 @@ export async function walkPath(root, path, onCheck) {
const array = Reflect.get(node, arrayName)

if (Array.isArray(array)) {
for (const [elementIndex, element] of array?.entries() ?? []) {
for (const [elementIndex, element] of array.entries()) {
await walk(
[...resolvedSegments, arrayName, String(elementIndex)],
[...remainingSegments.slice(1)],
Expand Down
1 change: 0 additions & 1 deletion tests/csaf_2_1/oasis.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@ const excluded = [
'6.2.44',
'6.2.45',
'6.2.46',
'6.2.49',
'6.2.50.1',
'6.2.50.2',
'6.2.50.3',
Expand Down
41 changes: 41 additions & 0 deletions tests/csaf_2_1/recommendedTest_6_2_49.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import assert from 'node:assert/strict'
import { recommendedTest_6_2_49 } from '../../csaf_2_1/recommendedTests/recommendedTest_6_2_49.js'

describe('recommendedTest_6_2_49', function () {
it('skips invalid child branches that do not pass schema validation', async function () {
const result = await recommendedTest_6_2_49({
product_tree: {
branches: [42, null],
},
})
assert.equal(result.warnings.length, 0)
})

it('does not warn for the special "all versions" vers string', async function () {
const result = await recommendedTest_6_2_49({
product_tree: {
branches: [
{
category: 'product_version_range',
name: 'vers:all/*',
},
],
},
})
assert.equal(result.warnings.length, 0)
})

it('does not warn when only exclusion constraints are present', async function () {
const result = await recommendedTest_6_2_49({
product_tree: {
branches: [
{
category: 'product_version_range',
name: 'vers:intdot/!=5.1|!=6.3.0',
},
],
},
})
assert.equal(result.warnings.length, 0)
})
})
10 changes: 10 additions & 0 deletions tests/walkPaths.js
Original file line number Diff line number Diff line change
Expand Up @@ -140,5 +140,15 @@ describe('walkPath', function () {
const results = await collect({ items: 'not-an-array' }, '/items[]')
expect(results).to.deep.equal([])
})

it('does not throw and skips primitive elements when the path continues into them', async function () {
const results = await collect(
{ branches: [42, null, { name: 'valid' }] },
'/branches[*]/name'
)
expect(results).to.deep.equal([
{ instancePath: '/branches/2/name', value: 'valid' },
])
})
})
})