Skip to content

Feat/#197 mandatory tests csaf2.1 6.1.7 #208

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 17 commits into from
Jun 24, 2025
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
d5cb05e
feat(CSAF2.1): #197 copy and adapt mandatory test 6.1.7 from CSAF 2.0…
rainer-exxcellent Feb 17, 2025
dc27816
feat(CSAF2.1): #197 replace preconditionFor_6_1_7_Matches with schema…
rainer-exxcellent Mar 3, 2025
12b0cae
feat(CSAF2.1): #197 6.1.7. reformat with prettier
rainer-exxcellent Mar 6, 2025
d8fb113
feat(CSAF2.1): #197 mandatory test 6.1.7 - add content to schema in v…
rainer-exxcellent Mar 14, 2025
ab454d8
feat(CSAF2.1): #197 test 6.1.9 - take source into account
rainer-exxcellent Apr 25, 2025
2136383
feat(CSAF2.1): #197 mandatory test 6.1.7 - fix tests after rebase
rainer-exxcellent May 8, 2025
a05f735
feat(CSAF2.1): #197 mandatory test 6.1.7 - use JSON.stringify to hash…
rainer-exxcellent May 8, 2025
23b3615
feat(CSAF2.1): #197 mandatory test 6.1.7 -run prettier on testfile
rainer-exxcellent May 8, 2025
3301133
feat(CSAF2.1): #197 mandatory test 6.1.7 - remove type any
rainer-exxcellent May 15, 2025
0e409ba
feat(CSAF2.1): #197 - test 6.1.7 - fix after rebase
rainer-exxcellent Jun 6, 2025
d3c96b1
feat(CSAF2.1): #197 - test 6.1.7 - changed input schema and add test …
rainer-exxcellent Jun 16, 2025
80a0759
feat(CSAF2.1): #197 - test 6.1.7 - add some helper function to test
rainer-exxcellent Jun 18, 2025
b773f76
feat(CSAF2.1): #197 - test 6.1.7 - use types from inputSchema
rainer-exxcellent Jun 18, 2025
cfcd7cb
feat(CSAF2.1): #197 - test 6.1.7 - use versionSourceId for the tuple …
rainer-exxcellent Jun 20, 2025
848b6fd
feat(CSAF2.1): #197 - test 6.1.10 - use cahnged csaf2.1 schema in min…
rainer-exxcellent Jun 20, 2025
56426c1
feat(CSAF2.1): #197 mandatory test 6.1.7 - move test in README to the…
rainer-exxcellent Jun 20, 2025
8d30ec9
feat(CSAF2.1): #197 mandatory test 6.1.7 - add '' to message
rainer-exxcellent Jun 20, 2025
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
1 change: 1 addition & 0 deletions csaf_2_1/mandatoryTests.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export {
mandatoryTest_6_1_33,
} from '../mandatoryTests.js'
export { mandatoryTest_6_1_1 } from './mandatoryTests/mandatoryTest_6_1_1.js'
export { mandatoryTest_6_1_7 } from './mandatoryTests/mandatoryTest_6_1_7.js'
export { mandatoryTest_6_1_8 } from './mandatoryTests/mandatoryTest_6_1_8.js'
export { mandatoryTest_6_1_11 } from './mandatoryTests/mandatoryTest_6_1_11.js'
export { mandatoryTest_6_1_13 } from './mandatoryTests/mandatoryTest_6_1_13.js'
Expand Down
206 changes: 206 additions & 0 deletions csaf_2_1/mandatoryTests/mandatoryTest_6_1_7.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
import Ajv from 'ajv/dist/jtd.js'

/**
* @typedef {string} Product
* /

/** @typedef {import('ajv/dist/jtd.js').JTDDataType<typeof inputSchema>} InputSchema */

/** @typedef {InputSchema['vulnerabilities'][number]} Vulnerability */

/** @typedef {NonNullable<Vulnerability['metrics']>[number]} Metric */

/** @typedef {NonNullable<Metric['content']>} MetricContent */

const jtdAjv = new Ajv()

const inputSchema = /** @type {const} */ ({
additionalProperties: true,
properties: {
vulnerabilities: {
elements: {
additionalProperties: true,
optionalProperties: {
metrics: {
elements: {
additionalProperties: true,
optionalProperties: {
source: {
type: 'string',
},
products: {
elements: { type: 'string' },
},
content: {
additionalProperties: true,
optionalProperties: {
cvss_v2: {
additionalProperties: true,
optionalProperties: {
version: { type: 'string' },
},
},
cvss_v3: {
additionalProperties: true,
optionalProperties: {
version: { type: 'string' },
},
},
cvss_v4: {
additionalProperties: true,
optionalProperties: {
version: { type: 'string' },
},
},
},
},
},
},
},
},
},
},
},
})

const validate = jtdAjv.compile(inputSchema)

/**
* For each item in /vulnerabilities it MUST be tested that the same Product ID
* is not a member of more than one CVSS-Vectors with the same version and the same source.
* @param {unknown} doc
*/
export function mandatoryTest_6_1_7(doc) {
const ctx = {
errors:
/** @type {Array<{ instancePath: string; message: string }>} */ ([]),
isValid: true,
}

/** @type {Array<{ message: string; instancePath: string }>} */
const errors = []
let isValid = true

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

/** @type {Array<Vulnerability>} */
const vulnerabilities = doc.vulnerabilities

/**
* Create a unique string for the tuple of version and source
* to compare them easily
* @param {string} version
* @param {string | undefined} source
*/
function createIdForVersionAndSource(version, source) {
return JSON.stringify({ version: version, source: source ?? '' })
}

/**
*
* @param {Metric} metric
* @param {string} versionSourceId
* @returns {string|null}
*/
function findCvssVersionWithSameVersionAndSource(metric, versionSourceId) {
if (
metric.content?.cvss_v2?.version !== undefined &&
versionSourceId ===
createIdForVersionAndSource(
metric.content?.cvss_v2.version,
metric.source
)
) {
return metric.content?.cvss_v2?.version
} else if (
metric.content?.cvss_v3?.version !== undefined &&
versionSourceId ===
createIdForVersionAndSource(
metric.content?.cvss_v3.version,
metric.source
)
) {
return metric.content?.cvss_v3?.version
} else if (
metric.content?.cvss_v4?.version !== undefined &&
versionSourceId ===
createIdForVersionAndSource(
metric.content?.cvss_v4.version,
metric.source
)
) {
return metric.content?.cvss_v4?.version
} else {
return null
}
}

/**
* @param {Metric} metric
* @param {Set<string>} versionSourceIdSet
*/
function addAllVersionSourceIdsInMetricToSet(metric, versionSourceIdSet) {
if (metric.content?.cvss_v2?.version !== undefined) {
versionSourceIdSet.add(
createIdForVersionAndSource(
metric.content?.cvss_v2.version,
metric.source
)
)
}
if (metric.content?.cvss_v3?.version !== undefined) {
versionSourceIdSet.add(
createIdForVersionAndSource(
metric.content?.cvss_v3.version,
metric.source
)
)
}
if (metric.content?.cvss_v4?.version !== undefined) {
versionSourceIdSet.add(
createIdForVersionAndSource(
metric.content?.cvss_v4.version,
metric.source
)
)
}
}

vulnerabilities.forEach((vulnerabilityItem, vulnerabilityIndex) => {
/** @type {Map<string, Set<string>>} */
const versionsSourceIdSetByProduct = new Map()

/** @type {Array<Metric> | undefined} */
const metrics = vulnerabilityItem.metrics
metrics?.forEach((metric, metricIndex) => {
/** @type {Array<Product> | undefined} */
const productsOfMetric = metric.products
productsOfMetric?.forEach((product, productIndex) => {
const versionSourceIdsOfProduct =
versionsSourceIdSetByProduct.get(product) ?? new Set()
versionsSourceIdSetByProduct.set(product, versionSourceIdsOfProduct)

versionSourceIdsOfProduct.forEach((versionSourceIdOfProduct) => {
const sameVersion = findCvssVersionWithSameVersionAndSource(
metric,
versionSourceIdOfProduct
)
if (sameVersion) {
isValid = false
const sourceOfMetric = metric.source ? metric.source : ''
errors.push({
message: `Product is member of more than one CVSS-Vectors with the same version '${sameVersion}' and same source ${sourceOfMetric}.`,
instancePath: `/vulnerabilities/${vulnerabilityIndex}/metrics/${metricIndex}/products/${productIndex}`,
})
}
})

addAllVersionSourceIdsInMetricToSet(metric, versionSourceIdsOfProduct)
})
})
})

return { errors, isValid }
}
78 changes: 78 additions & 0 deletions tests/csaf_2_1/mandatoryTest_6_1_7.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { expect } from 'chai'
import { mandatoryTest_6_1_7 } from '../../csaf_2_1/mandatoryTests/mandatoryTest_6_1_7.js'
import minimalDoc from './shared/minimalDoc.js'
import {
cvssV31Content,
productTreeWithFullProductName,
} from './shared/csafDocHelper.js'
import csaf_2_1 from '../../csaf_2_1/schemaTests/csaf_2_1.js'

const emptyMandatoryTest6_1_7 = {
$schema: minimalDoc.$schema,
document: {
...minimalDoc.document,
},
product_tree: productTreeWithFullProductName('CSAFPID-9080700', 'Product A'),
vulnerabilities: [
{
metrics: [
{
cvss_v3: {
version: '3.0',
},
},
],
},
{
metrics: [
{
content: {},
products: [],
},
],
},
],
}

const failingTestWithNotConsideredObject6_1_7 = {
$schema: minimalDoc.$schema,
document: {
...minimalDoc.document,
},
product_tree: productTreeWithFullProductName('CSAFPID-9080700', 'Product A'),
vulnerabilities: [
{}, // input schema should not consider this
{
metrics: [
cvssV31Content(6.5, 'CVSS:3.1/AV:L/AC:L/PR:H/UI:R/S:U/C:H/I:H/A:H', [
'CSAFPID-9080700',
]),
cvssV31Content(6.5, 'CVSS:3.1/AV:L/AC:L/PR:H/UI:R/S:U/C:H/I:H/A:H', [
'CSAFPID-9080700',
]),
],
},
],
}

describe('mandatory test 6.1.7', function () {
describe('valid examples', function () {
it('test empty vulnerabilities', async function () {
const result = mandatoryTest_6_1_7(emptyMandatoryTest6_1_7)
expect(result.errors.length).to.eq(0)
})

it('test input schema with minimal doc', async function () {
expect(csaf_2_1(minimalDoc).isValid).to.be.true
const result = mandatoryTest_6_1_7(minimalDoc)
expect(result.errors.length).to.eq(0)
})

it('test input schema with not considered json object in vulnerabilities', async function () {
const result = mandatoryTest_6_1_7(
failingTestWithNotConsideredObject6_1_7
)
expect(result.errors.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 @@ -11,7 +11,6 @@ import * as mandatory from '../../csaf_2_1/mandatoryTests.js'
*/
const excluded = [
'6.1.6',
'6.1.7',
'6.1.10',
'6.1.14',
'6.1.16',
Expand Down
54 changes: 54 additions & 0 deletions tests/csaf_2_1/shared/csafDocHelper.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/**
* @param {string} productId
* @param {string} name
*/
export function productTreeWithFullProductName(productId, name) {
return {
full_product_names: [
{
product_id: productId,
name: name,
},
],
}
}

/**
* @param {number} baseSCore
* @param {string} vectorString
* @param {string[]} products
*/
export function cvssV31Content(baseSCore, vectorString, products) {
return {
content: {
cvss_v3: {
version: '3.1',
vectorString: vectorString,
baseScore: baseSCore,
baseSeverity: severityFromScore(baseSCore),
},
},
products: products,
}
}

/**
* @param {number} score
* @return {string}
*/
export function severityFromScore(score) {
if (score >= 9.0) {
return 'CRITICAL'
}
if (score >= 7.0) {
return 'HIGH'
}
if (score >= 4.0) {
return 'MEDIUM'
}
if (score >= 0.1) {
return 'LOW'
} else {
return 'NONE'
}
}
40 changes: 40 additions & 0 deletions tests/csaf_2_1/shared/minimalDoc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
export default {
$schema: 'https://docs.oasis-open.org/csaf/csaf/v2.1/schema/csaf.json',
document: {
category: 'Test Report',
csaf_version: '2.1',
title: 'Minimal valid',
lang: 'en',
distribution: {
tlp: {
label: 'AMBER',
},
},
publisher: {
category: 'other',
name: 'Secvisogram Automated Tester',
namespace: 'https://github.com/secvisogram/secvisogram',
},
references: [
{
category: 'self',
summary: 'A non-canonical URL.',
url: 'https://example.com/security/data/csaf/2021/my-thing-_10.json',
},
],
tracking: {
current_release_date: '2021-01-14T00:00:00.000Z',
id: 'My-Thing-.10',
initial_release_date: '2021-01-14T00:00:00.000Z',
revision_history: [
{
number: '1',
date: '2021-01-14T00:00:00.000Z',
summary: 'Summary',
},
],
status: 'draft',
version: '1',
},
},
}