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 @@ -329,7 +329,6 @@ The following tests are not yet implemented and therefore missing:
- Recommended Test 6.2.20
- Recommended Test 6.2.24
- Recommended Test 6.2.26
- Recommended Test 6.2.31
- Recommended Test 6.2.32
- Recommended Test 6.2.33
- Recommended Test 6.2.34
Expand Down Expand Up @@ -458,6 +457,7 @@ export const recommendedTest_6_2_27: DocumentTest
export const recommendedTest_6_2_28: DocumentTest
export const recommendedTest_6_2_29: DocumentTest
export const recommendedTest_6_2_30: DocumentTest
export const recommendedTest_6_2_31: DocumentTest
export const recommendedTest_6_2_39_2: DocumentTest
export const recommendedTest_6_2_40: DocumentTest
export const recommendedTest_6_2_41: 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 @@ -34,6 +34,7 @@ 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_30 } from './recommendedTests/recommendedTest_6_2_30.js'
export { recommendedTest_6_2_31 } from './recommendedTests/recommendedTest_6_2_31.js'
export { recommendedTest_6_2_38 } from './recommendedTests/recommendedTest_6_2_38.js'
export { recommendedTest_6_2_39_2 } from './recommendedTests/recommendedTest_6_2_39_2.js'
export { recommendedTest_6_2_40 } from './recommendedTests/recommendedTest_6_2_40.js'
Expand Down
239 changes: 239 additions & 0 deletions csaf_2_1/recommendedTests/recommendedTest_6_2_31.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,239 @@
import Ajv from 'ajv/dist/jtd.js'

const ajv = new Ajv()

const productIdentificationHelperSchema = /** @type {const} */ ({
additionalProperties: true,
optionalProperties: {
serial_numbers: {
elements: { type: 'string' },
},
model_numbers: {
elements: { type: 'string' },
},
},
})

const productSchema = /** @type {const} */ ({
additionalProperties: true,
optionalProperties: {
product_id: { type: 'string' },
product_identification_helper: productIdentificationHelperSchema,
},
})

const relationshipSchema = /** @type {const} */ ({
additionalProperties: true,
optionalProperties: {
full_product_name: productSchema,
product_reference: { type: 'string' },
relates_to_product_reference: { type: 'string' },
},
})

const branchSchema = /** @type {const} */ ({
additionalProperties: true,
optionalProperties: {
product: productSchema,
branches: {
elements: {
additionalProperties: true,
// AJV's JTD does not support recursive schemas.
// Nested branches are validated at runtime in checkBranches() by calling
// validateBranch() on each child branch individually during the recursive traversal.
properties: {},
Comment thread
bendo-eXX marked this conversation as resolved.
},
},
},
})

const inputSchema = /** @type {const} */ ({
Comment thread
rainer-exxcellent marked this conversation as resolved.
additionalProperties: true,
properties: {
product_tree: {
additionalProperties: true,
optionalProperties: {
branches: {
elements: branchSchema,
},
full_product_names: {
elements: productSchema,
},
relationships: {
elements: relationshipSchema,
},
},
},
},
})

const validateInput = ajv.compile(inputSchema)
const validateBranch = ajv.compile(branchSchema)

/**
* @typedef {import('ajv/dist/core').JTDDataType<typeof branchSchema>} Branch
* @typedef {import('ajv/dist/core').JTDDataType<typeof productSchema>} FullProductName
* @typedef {import('ajv/dist/core').JTDDataType<typeof relationshipSchema>} Relationship
*/

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

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

const relationships = Array.isArray(doc.product_tree?.relationships)
? doc.product_tree.relationships
: []

// Start the recursive check from the root branches
checkBranches(doc.product_tree?.branches ?? [], relationships, ctx)

checkFullProductNames(
doc.product_tree?.full_product_names ?? [],
relationships,
ctx
)

Comment thread
bendo-eXX marked this conversation as resolved.
relationships.forEach((rel, index) => {
if (rel?.full_product_name) {
checkFullProductNames(
[rel.full_product_name],
relationships,
ctx,
`/product_tree/relationships/${index}/full_product_name`
)
}
})

return ctx
}

/**
* Check full_product_names for serial_numbers or model_numbers
* @param {FullProductName[]} full_product_names
* @param {Relationship[]} relationships
* @param {{ warnings: Array<{ instancePath: string; message: string }> }} ctx
* @param {string} [basePath='/product_tree/full_product_names'] - The base JSON path for warnings
*/
function checkFullProductNames(
full_product_names,
relationships,
ctx,
basePath = '/product_tree/full_product_names'
) {
full_product_names.forEach((fullProductName, index) => {
if (fullProductName?.product_identification_helper) {
if (!fullProductName.product_id) {
ctx.warnings.push({
instancePath: `${basePath}/${index}`,
message:
'missing product_id: full product name cannot be referenced without a product id.',
})
return
}
const { serial_numbers, model_numbers } =
fullProductName.product_identification_helper

if (
(serial_numbers?.length || model_numbers?.length) &&
!checkRelationship(relationships, fullProductName.product_id)
) {
ctx.warnings.push({
instancePath: `${basePath}/${index}`,
message:
'missing relationship: product with serial number or model number should be referenced.',
})
}
}
})
}

/**
* Recursive function to check branches for products with serial_numbers or model_numbers
* but no corresponding relationship.
* @param {Branch[]} branches - The current level of branches to process.
* @param {Relationship[]} relationships - The relationships array to check against.
* @param {{ warnings: Array<{ instancePath: string; message: string }> }} ctx - The context to store warnings.
* @param {string} [path='/product_tree/branches'] - The current JSON path.
*/
function checkBranches(
branches,
relationships,
ctx,
path = '/product_tree/branches'
) {
branches?.forEach((branch, branchIndex) => {
// Skip invalid branches
if (!validateBranch(branch)) return

const currentPath = `${path}/${branchIndex}`
const product = branch.product

if (product) {
if (!product?.product_id) {
ctx.warnings.push({
instancePath: `${currentPath}/product`,
message:
'missing product_id: product cannot be referenced without a product id.',
})
} else if (product.product_identification_helper) {
const { serial_numbers, model_numbers } =
product.product_identification_helper

if (
(serial_numbers?.length || model_numbers?.length) &&
!checkRelationship(relationships, product.product_id)
) {
ctx.warnings.push({
instancePath: `${currentPath}/product`,
message:
'missing relationship: product with serial number or model number should be referenced.',
})
}
}
}

// Recursively check nested branches
if (Array.isArray(branch.branches)) {
checkBranches(
branch.branches,
relationships,
ctx,
`${currentPath}/branches`
)
}
})
}

/**
* Check if there is a valid relationship for the given productId.
* @param {Relationship[]} relationships
* @param {string} productId
* @returns {boolean}
*/
function checkRelationship(relationships, productId) {
return relationships.some((rel) => {
if (!rel.product_reference || !rel.relates_to_product_reference) {
return false
}

// Check for self-referencing relationships
if (rel.product_reference === rel.relates_to_product_reference) {
return false
}
// Check if the productId matches either reference
return (
rel.product_reference === productId ||
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.

Please explain, in which situation this happens in our next meeting.

Copy link
Copy Markdown
Contributor Author

@bendo-eXX bendo-eXX Oct 6, 2025

Choose a reason for hiding this comment

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

I want to verifies that a product identified by productId has a valid relationship entry in the relationships array. The relationship can reference the product in either direction:

  • product_reference: Points to the product that has serial/model numbers
  • relates_to_product_reference: Points to the related product
    The function returns true if a relationship exists that connects to the specified product ID.

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.

Under which category does your first case happen?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

In checkRelationship, I am currently checking whether a relationship product_reference or relates_to_product_reference contains the productId I am looking for, regardless of the category of the relationship.

Do I also need to check whether the relationship has a specific category (e.g. installed_on)? Or is any valid relationship sufficient for a product with serial_numbers/model_numbers to be considered correctly referenced?

If so, which category values should be accepted, and on which side of the relationship (product_reference vs. relates_to_product_reference) must the product with serial_numbers/model_numbers be located?

rel.relates_to_product_reference === productId
)
})
}
1 change: 0 additions & 1 deletion tests/csaf_2_1/oasis.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ const excluded = [
'6.2.20',
'6.2.24',
'6.2.26',
'6.2.31',
'6.2.32',
'6.2.33',
'6.2.34',
Expand Down
Loading