Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
f4a9d60
chore: update typescript
domachine Feb 12, 2025
a5e9604
feat: setup csaf 2.1 infrastructure
domachine Feb 12, 2025
913711f
test: exclude all unimplemented CSAF 2.1 tests from test suite
domachine Feb 26, 2025
20980dd
test: revert to mocha to reuse coverage toolchain
domachine Feb 26, 2025
27a76fb
chore: adapt `runTest.js` script to allow csaf 2.1 tests
domachine Feb 28, 2025
6061183
test: exclude tests that were newly added
domachine Mar 3, 2025
4828789
feat: add mandatory test 6.1.34
domachine Feb 20, 2025
98de13f
feat: add mandatory test 6.1.35
domachine Feb 26, 2025
ad7b2cb
feat: setup csaf 2.1 infrastructure
domachine Feb 12, 2025
f6f1edd
test: exclude all unimplemented CSAF 2.1 tests from test suite
domachine Feb 26, 2025
bc8ac10
test: revert to mocha to reuse coverage toolchain
domachine Feb 26, 2025
d7c3080
feat(CSAF2.1): #197 copy and adapt mandatory test 6.1.1 from CSAF 2.0…
rainer-exxcellent Feb 14, 2025
2d39487
feat(CSAF2.1): #197 mandatory test 6.1.1 rebase to 196-csaf-2.1, Impo…
rainer-exxcellent Mar 13, 2025
f253018
feat: setup csaf 2.1 infrastructure
domachine Feb 12, 2025
ea2d47b
test: exclude all unimplemented CSAF 2.1 tests from test suite
domachine Feb 26, 2025
0f09268
test: revert to mocha to reuse coverage toolchain
domachine Feb 26, 2025
987c23e
feat(CSAF2.1): #197 copy and adapt mandatory test 6.1.8 from CSAF 2.0…
rainer-exxcellent Feb 18, 2025
3bdd1f8
feat(CSAF2.1): #197 6.1.8. rebase and remove old test which is now in…
rainer-exxcellent Mar 6, 2025
03a563e
feat(CSAF2.1): #197 rebase mandatory test 6.1.8 to 196-csaf-2.1, Impo…
rainer-exxcellent Mar 14, 2025
a337402
feat(CSAF2.1): #196 disable new CSAF 2.1. Tests
rainer-exxcellent Mar 14, 2025
67e6ab2
feat: add optional test 6.2.19
bendo-eXX Mar 28, 2025
6a4499e
feat: refactor after code review optional test 6.2.19
bendo-eXX Apr 11, 2025
66cd55e
Merge branch 'main' into 196-csaf-2.1_optional_test_6.2.19
bendo-eXX Feb 9, 2026
ee7ee32
feat(CSAF2.1): add CVSS check vor cvss4.0
bendo-eXX Feb 9, 2026
bb976a5
feat(CSAF2.1): remove optionalTests.js
bendo-eXX Feb 9, 2026
a9333d2
feat(CSAF2.1): update README.md
bendo-eXX Feb 9, 2026
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 @@ -325,7 +325,6 @@ The following tests are not yet implemented and therefore missing:
**Recommended Tests**

- Recommended Test 6.2.11
- Recommended Test 6.2.19
- Recommended Test 6.2.20
- Recommended Test 6.2.21
- Recommended Test 6.2.24
Expand Down Expand Up @@ -453,6 +452,7 @@ export const recommendedTest_6_2_15: DocumentTest
export const recommendedTest_6_2_16: DocumentTest
export const recommendedTest_6_2_17: DocumentTest
export const recommendedTest_6_2_18: DocumentTest
export const recommendedTest_6_2_19: DocumentTest
export const recommendedTest_6_2_22: DocumentTest
export const recommendedTest_6_2_23: DocumentTest
export const recommendedTest_6_2_25: DocumentTest
Expand Down
343 changes: 340 additions & 3 deletions csaf_2_1/recommendedTests/recommendedTest_6_2_19.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,345 @@
import { optionalTest_6_2_19 } from '../../optionalTests.js'
import Ajv from 'ajv/dist/jtd.js'
import { cvss30, cvss31 } from '../../lib/shared/first.js'
import * as cvss2 from '../../lib/shared/cvss2.js'
import * as cvss3 from '../../lib/shared/cvss3.js'
import * as cvss4 from '../../lib/shared/cvss4.js'

const ajv = new Ajv()

const inputSchema = /** @type {const} */ ({
additionalProperties: true,
properties: {
vulnerabilities: {
elements: {
additionalProperties: true,
optionalProperties: {
product_status: {
additionalProperties: true,
optionalProperties: {
fixed: {
elements: { type: 'string' },
},
first_fixed: {
elements: { type: 'string' },
},
},
},
metrics: {
elements: {
additionalProperties: true,
optionalProperties: {
content: {
additionalProperties: true,
optionalProperties: {
cvss_v4: {
additionalProperties: true,
optionalProperties: {
environmentalScore: { type: 'float64' },
vectorString: { type: 'string' },
version: { type: 'string' },
},
},
cvss_v3: {
additionalProperties: true,
optionalProperties: {
environmentalScore: { type: 'float64' },
vectorString: { type: 'string' },
version: { type: 'string' },
},
},
cvss_v2: {
additionalProperties: true,
optionalProperties: {
environmentalScore: { type: 'float64' },
vectorString: { type: 'string' },
version: { type: 'string' },
},
},
},
},
products: {
elements: { type: 'string' },
},
},
},
},
},
},
},
},
})
const validateInput = ajv.compile(inputSchema)

/**
* @param {unknown} doc
* @param {any} doc
*/
export function recommendedTest_6_2_19(doc) {
return optionalTest_6_2_19(doc)
const ctx = {
warnings:
/** @type {Array<{ instancePath: string; message: string }>} */ ([]),
}

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

doc.vulnerabilities.forEach((vulnerability, vulnerabilityIndex) => {
const fixedProductIDs = new Set([
...(vulnerability.product_status?.first_fixed ?? []),
...(vulnerability.product_status?.fixed ?? []),
])
for (const productID of fixedProductIDs) {
vulnerability.metrics?.forEach((metric, metricIndex) => {
if (!metric.products?.includes(productID)) return
const content = metric.content
if (content !== undefined) {
const cvssTypes = ['cvss_v4', 'cvss_v3', 'cvss_v2']
cvssTypes.forEach((cvssType) => {
if (content[cvssType] && checkCVSS(content[cvssType])) {
ctx.warnings.push({
instancePath: `/vulnerabilities/${vulnerabilityIndex}/metrics/${metricIndex}/${cvssType}`,
message: `environmental score should be 0 since "${productID}" is listed as fixed`,
})
}
})
}
})
}
})

return ctx
}

/**
* Check if the cvss object has a valid environmental score.
* @param {any} cvss
* @returns {boolean}
*/
function checkCVSS(cvss) {
if (!cvss) return false
const calculatedValue = calculateEnvironmentalScoreFromMetrics({
version: cvss.version,
vectorString: cvss.vectorString ?? '',
metrics: cvss,
})
return (
(typeof cvss.environmentalScore === 'number' &&
cvss.environmentalScore > 0) ||
(typeof calculatedValue === 'number' && calculatedValue > 0) ||
calculatedValue === null
)
}

/**
* @param {object} params
* @param {'2.0' | '3.0' | '3.1' | '4.0'} params.version
* @param {string} params.vectorString
* @param {Record<string, unknown>} params.metrics
*/
function calculateEnvironmentalScoreFromMetrics({
version,
vectorString,
metrics,
}) {
const vectorFromVectorString = new Map(
vectorString
.split('/')
.map((e) => {
const [key, value] = e.split(':')
return /** @type {const} */ ([key, value])
})
.filter(([, value]) => value)
)

if (version === '4.0') {
return calculateMetricScoreForCVSS4(
vectorString,
metrics,
vectorFromVectorString
)
} else if (version === '3.1' || version === '3.0') {
return calculateMetricScoreForCVSS3(
vectorFromVectorString,
metrics,
version
)
} else {
return calculateMetricScoreForCVSS2(vectorFromVectorString, metrics)
}
}

/**
* @param {string} vectorString
* @param {Record<string, unknown>} metrics
* @param {Map<string, string>} vectorFromVectorString
*/
function calculateMetricScoreForCVSS4(
vectorString,
metrics,
vectorFromVectorString
) {
// Extract all metrics from the metrics object and combine with vector string
const metricArray = calculateMetricArray({
mapping: cvss4Mapping,
metrics,
vector: vectorFromVectorString,
})

// Build complete vector string with all metrics including Modified ones
const completeVectorParts = metricArray
.filter(([, value]) => value !== undefined)
.map(([key, value]) => `${key}:${value}`)

// Keep CVSS version prefix from original vector
const versionPrefix = vectorString.split('/')[0]
const completeVectorString = [versionPrefix, ...completeVectorParts].join('/')

const calculateScoreObject =
cvss4.calculateCvss4_0_Score(completeVectorString)
const environmentalScoreObject = calculateScoreObject.find(
(scoreObject) => scoreObject.metricTypeId === 'ENVIRONMENTAL'
)
return environmentalScoreObject?.score ?? null
}

/**
* This function takes a cvss vector and a metric object and extracts all cvss
* @param {Map<string, string>} vectorFromVectorString
* @param {Record<string, unknown>} metrics
* @param {'3.0' | '3.1'} version
* @returns {number|null}
*/
function calculateMetricScoreForCVSS3(
vectorFromVectorString,
metrics,
version
) {
const args = /**
* @type {[
* string,
* string,
* string,
* string,
* string,
* string,
* string,
* string,
* string,
* string,
* string,
* string,
* string,
* string,
* string,
* string,
* string,
* string,
* string,
* string,
* string,
* string,
* ]}
*/ (
calculateMetricArray({
mapping: cvss3Mapping,
metrics,
vector: vectorFromVectorString,
}).map((e) => e[1])
)
const metric = (version === '3.1' ? cvss31 : cvss30).calculateCVSSFromMetrics(
...args
)
if (!metric.success) return null
return Number(metric.environmentalMetricScore)
}

/**
* This function takes a cvss vector and a metric object and extracts all cvss
* @param {Map<string, string>} vectorFromVectorString
* @param {Record<string, unknown>} metrics
* @returns {number|*|null}
*/
function calculateMetricScoreForCVSS2(vectorFromVectorString, metrics) {
const vector = Object.fromEntries(
calculateMetricArray({
mapping: cvss2Mapping,
metrics,
vector: vectorFromVectorString,
})
)
const metric = safelyParseCVSSV2Vector(vector)
if (!metric.success) return null
return metric.environmentalMetricScore
}

const cvss2Mapping =
/** @type {ReadonlyArray<readonly [string, string, Record<string, string>]>} */ (
cvss2.mapping.map((mapping) => [
mapping[0],
mapping[1],
Object.fromEntries(
Object.entries(mapping[2]).map(([key, value]) => [key, value.id])
),
])
)

const cvss3Mapping = cvss3.mapping

const cvss4Mapping =
/** @type {ReadonlyArray<readonly [string, string, Record<string, string>]>} */ (
cvss4.flatMetrics.map((metric) => [
metric.jsonName,
metric.metricShort,
Object.fromEntries(
metric.options.map((option) => [option.optionValue, option.optionKey])
),
])
)

/**
* This function takes a cvss vector and a metric object and extracts all cvss
* values according to the mapping. It does this by first looking up every property
* in the `vector`. If the property doesn't exist there but in the metrics objects,
* it takes the value from the corresponding metrics object.
*
* @param {object} params
* @param {Map<string, string>} params.vector
* @param {Record<string, unknown>} params.metrics
* @param {ReadonlyArray<readonly [string, string, Record<string, string>]>} params.mapping
* @returns an array of pairs where the first element is the metric name (abbreviated) and the
* second is the value (abbreviated). If no value is found the value is `undefined`.
* The order of the array is the same as in the mapping.
*/
function calculateMetricArray({ vector, metrics, mapping }) {
return mapping.map((e) => {
const metricAbbrev = e[1]
const metricPropertyName = e[0]
/** @type {any} */
const metricValueAbbrevMap = e[2]
/** @type {any} */
const metricValue = metrics[metricPropertyName]
return [
metricAbbrev,
vector.get(metricAbbrev) ?? metricValueAbbrevMap[metricValue],
]
})
}

/**
* @param {string | {}} vectorString
* @returns
*/
function safelyParseCVSSV2Vector(vectorString) {
try {
return {
success: true,
environmentalMetricScore:
cvss2.getEnvironmentalScoreFromVectorString(vectorString),
}
} catch (e) {
return {
success: false,
environmentalMetricScore: -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 @@ -29,7 +29,6 @@ const excluded = [
'6.1.55',
'6.1.56',
'6.2.11',
'6.2.19',
'6.2.20',
'6.2.21',
'6.2.24',
Expand Down