Skip to content

fix: add optional test 6.2.37 #292

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

Open
wants to merge 17 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
ee6005f
Merge pull request #303 from secvisogram/feat/286-rename-optional-2-r…
rainer-exxcellent Jun 5, 2025
784c045
feat: add mandatory test 6.1.37
domachine Feb 27, 2025
5036c14
feat(CSAF2.1): #197 copy and adapt mandatory test 6.1.9 from CSAF 2.0…
domachine Feb 19, 2025
10580af
feat(CSAF2.1): #197 test 6.1.9 - add test for safelyParseCVSSV2Vector…
rainer-exxcellent May 7, 2025
97d4f11
feat(CSAF2.1): #197 test 6.1.9 - fix findings from review
rainer-exxcellent May 8, 2025
aa2dd67
refactor: tighten types
domachine May 14, 2025
c9c6b19
feat(CSAF2.1): #197 test 6.1.9 - rename import CVSS to CVSS30 to be …
rainer-exxcellent Jun 2, 2025
80b5b35
feat(CSAF2.1): #197 test 6.1.9 - fixed invalid cvss4.0 json names
rainer-exxcellent Jun 2, 2025
89bcc25
feat(CSAF2.1): #197 - test 6.1.9 fix and test updateFromVectorString
rainer-exxcellent Jun 4, 2025
a135c4a
feat(CSAF2.1): #197 - test 6.1.9 make severityRating uppercase
rainer-exxcellent Jun 5, 2025
23dbddf
feat(CSAF2.1): #197 - test 6.1.9 comment test 'Updating from an inval…
rainer-exxcellent Jun 5, 2025
d9dda12
feat(CSAF2.1): #197 - test 6.1.9 - made severity Uppercase according …
rainer-exxcellent Jun 5, 2025
60b5f05
feat(CSAF2.1): #197 - test 6.1.9 - changed optionalProperties to prop…
rainer-exxcellent Jun 6, 2025
a7e7792
Merge pull request #261 from secvisogram/197-csaf-2.1-mandatory-test-…
rainer-exxcellent Jun 6, 2025
1a1f0ed
fix: add optional test 6.2.37
christopher-exx May 21, 2025
516c8dd
refactor: rename optional test 6.2.37 to recommended test
christopher-exx Jun 11, 2025
96da837
refactor: rename optional test 6.2.37 to recommended test
christopher-exx Jun 12, 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 @@ -40,6 +40,7 @@ export { mandatoryTest_6_1_11 } from './mandatoryTests/mandatoryTest_6_1_11.js'
export { mandatoryTest_6_1_13 } from './mandatoryTests/mandatoryTest_6_1_13.js'
export { mandatoryTest_6_1_34 } from './mandatoryTests/mandatoryTest_6_1_34.js'
export { mandatoryTest_6_1_35 } from './mandatoryTests/mandatoryTest_6_1_35.js'
export { mandatoryTest_6_1_9 } from './mandatoryTests/mandatoryTest_6_1_9.js'
export { mandatoryTest_6_1_37 } from './mandatoryTests/mandatoryTest_6_1_37.js'
export { mandatoryTest_6_1_38 } from './mandatoryTests/mandatoryTests_6_1_38.js'
export { mandatoryTest_6_1_39 } from './mandatoryTests/mandatoryTest_6_1_39.js'
Expand Down
284 changes: 284 additions & 0 deletions csaf_2_1/mandatoryTests/mandatoryTest_6_1_9.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,284 @@
import cvss2js from 'cvss2js'
import { getEnvironmentalScoreFromVectorString } from '../../lib/shared/cvss2.js'
import { cvss30 as CVSS30, cvss31 as CVSS31 } from '../../lib/shared/first.js'
import Ajv from 'ajv/dist/jtd.js'
import { calculateCvss4_0_Score } from '../../lib/shared/cvss4.js'

const ajv = new Ajv()

const inputSchema = /** @type {const} */ ({
additionalProperties: true,
properties: {
vulnerabilities: {
elements: {
additionalProperties: true,
properties: {
metrics: {
elements: {
additionalProperties: true,
properties: {
content: {
additionalProperties: true,
optionalProperties: {
cvss_v2: {
additionalProperties: true,
optionalProperties: {
vectorString: { type: 'string' },
baseScore: { type: 'float64' },
temporalScore: { type: 'float64' },
environmentalScore: { type: 'float64' },
},
},
cvss_v3: {
additionalProperties: true,
optionalProperties: {
vectorString: { type: 'string' },
version: { type: 'string' },
baseScore: { type: 'float64' },
baseSeverity: { type: 'string' },
temporalScore: { type: 'float64' },
temporalSeverity: { type: 'string' },
environmentalScore: { type: 'float64' },
environmentalSeverity: { type: 'string' },
},
},
cvss_v4: {
additionalProperties: true,
optionalProperties: {
vectorString: { type: 'string' },
version: { type: 'string' },
baseScore: { type: 'float64' },
baseSeverity: { type: 'string' },
threatScore: { type: 'float64' },
threatSeverity: { type: 'string' },
environmentalScore: { type: 'float64' },
environmentalSeverity: { type: 'string' },
},
},
},
},
},
},
},
},
},
},
},
})

const validateInput = ajv.compile(inputSchema)

/**
* @param {any} doc
*/
export function mandatoryTest_6_1_9(doc) {
/** @type {Array<{ message: string; instancePath: string }>} */
const errors = []
let isValid = true

if (!validateInput(doc)) {
return { errors, isValid }
}

const vulnerabilities = doc.vulnerabilities
vulnerabilities.forEach((vulnerability, vulnerabilityIndex) => {
const metrics = vulnerability.metrics
metrics?.forEach((metric, metricIndex) => {
calculateCvss2(metric).forEach((failedMetricName) => {
errors.push({
instancePath: `/vulnerabilities/${vulnerabilityIndex}/scores/${metricIndex}/cvss_v2/${failedMetricName}`,
message: 'invalid calculated value',
})
})

calculateCvss3(metric).forEach((failedMetricName) => {
errors.push({
instancePath: `/vulnerabilities/${vulnerabilityIndex}/scores/${metricIndex}/cvss_v3/${failedMetricName}`,
message: 'invalid calculated value',
})
})

calculateCvss4(metric).forEach((failedMetricName) => {
errors.push({
instancePath: `/vulnerabilities/${vulnerabilityIndex}/scores/${metricIndex}/cvss_v4/${failedMetricName}`,
message: 'invalid calculated value',
})
})
})
})

return { errors, isValid: errors.length === 0 }
}

/**
* @param {string} vectorString
* @returns
*/
function safelyParseCVSSV2Vector(vectorString) {
try {
return {
success: true,
baseMetricScore: cvss2js.getBaseScore(vectorString),
temporalMetricScore: cvss2js.getTemporalScore(vectorString),
environmentalMetricScore:
getEnvironmentalScoreFromVectorString(vectorString),
}
} catch (e) {
return {
success: false,
baseMetricScore: -1,
temporalMetricScore: -1,
environmentalMetricScore: -1,
}
}
}

/**
* @param {any} metric
* @return {string[]}
*/
function calculateCvss2(metric) {
const failedMetrics = []
if (
metric.content?.cvss_v2 &&
typeof metric.content.cvss_v2.vectorString === 'string'
) {
const cvssV2 = metric.content.cvss_v2
const result = safelyParseCVSSV2Vector(metric.content.cvss_v2.vectorString)

if (result.success) {
for (const { score, expectedScore, name } of [
{
score: cvssV2.baseScore,
expectedScore: result.baseMetricScore,
name: 'baseScore',
},
{
score: cvssV2.temporalScore,
expectedScore: result.temporalMetricScore,
name: 'temporalScore',
},
{
score: cvssV2.environmentalScore,
expectedScore: result.environmentalMetricScore,
name: 'environmentalScore',
},
]) {
if (typeof score === 'number') {
if (score !== Number(expectedScore)) {
failedMetrics.push(name)
}
}
}
} else {
// Invalid CVSS string is tested in test 6.1.8
}
}

return failedMetrics
}

/**
* @param {any} metric
* @return {string[]}
*/
function calculateCvss3(metric) {
const failedMetrics = []
if (
metric.content?.cvss_v3 &&
typeof metric.content.cvss_v3.vectorString === 'string' &&
(metric.content.cvss_v3.version === '3.1' ||
metric.content.cvss_v3.version === '3.0')
) {
const calculator =
metric.content.cvss_v3.version === '3.0' ? CVSS30 : CVSS31
const result = calculator.calculateCVSSFromVector(
metric.content.cvss_v3.vectorString
)

if (result.success) {
for (const { score: scoreValue, expectedScore, name } of [
{
score: metric.content.cvss_v3.baseScore,
expectedScore: result.baseMetricScore,
name: 'baseScore',
},
{
score: metric.content.cvss_v3.temporalScore,
expectedScore: result.temporalMetricScore,
name: 'temporalScore',
},
{
score: metric.content.cvss_v3.environmentalScore,
expectedScore: result.environmentalMetricScore,
name: 'environmentalScore',
},
]) {
if (typeof scoreValue === 'number') {
if (scoreValue !== Number(expectedScore)) {
failedMetrics.push(name)
}
}
}

for (const { severity, expectedSeverity, name } of [
{
severity: metric.content.cvss_v3.baseSeverity,
expectedSeverity: result.baseSeverity,
name: 'baseSeverity',
},
{
severity: metric.content.cvss_v3.temporalSeverity,
expectedSeverity: result.temporalSeverity,
name: 'temporalSeverity',
},
{
severity: metric.content.cvss_v3.environmentalSeverity,
expectedSeverity: result.environmentalSeverity,
name: 'environmentalSeverity',
},
]) {
if (typeof severity === 'string') {
if (severity !== expectedSeverity.toUpperCase()) {
failedMetrics.push(name)
}
}
}
} else {
// Invalid CVSS is tested in test 6.1.8
}
}
return failedMetrics
}

/**
* @param {any} metric
* @return {string[]}
*/
function calculateCvss4(metric) {
/**
* @type {string[]}
*/
const failedMetrics = []
if (
metric.content?.cvss_v4 &&
typeof metric.content.cvss_v4.vectorString === 'string'
) {
const scores = calculateCvss4_0_Score(metric.content.cvss_v4.vectorString)
scores.forEach((score) => {
const expectedScore = metric.content.cvss_v4[score.scoreJsonName]
const expectedSeverity = metric.content.cvss_v4[score.severityJsonName]
if (typeof expectedScore === 'number' && score.score !== expectedScore) {
failedMetrics.push(score.scoreJsonName)
}

if (
typeof expectedSeverity === 'string' &&
score.severity.toUpperCase() !== expectedSeverity.toUpperCase()
) {
failedMetrics.push(score.severityJsonName)
}
})
}
return failedMetrics
}
1 change: 1 addition & 0 deletions csaf_2_1/recommendedTests.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,4 @@ export { recommendedTest_6_2_8 } from './recommendedTests/recommendedTest_6_2_8.
export { recommendedTest_6_2_9 } from './recommendedTests/recommendedTest_6_2_9.js'
export { recommendedTest_6_2_3 } from './recommendedTests/recommendedTest_6_2_3.js'
export { recommendedTest_6_2_22 } from './recommendedTests/recommendedTest_6_2_22.js'
export { recommendedTest_6_2_37 } from './recommendedTests/recommendedTest_6_2_37.js'
72 changes: 72 additions & 0 deletions csaf_2_1/recommendedTests/recommendedTest_6_2_37.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import Ajv from 'ajv/dist/jtd.js'

const ajv = new Ajv()

const inputSchema = /** @type {const} */ ({
additionalProperties: true,
properties: {
vulnerabilities: {
elements: {
additionalProperties: true,
properties: {
Copy link
Contributor

Choose a reason for hiding this comment

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

We (@domachine and I) decided to us a lighter approach using optionalProperties. (Applies to all array items.)

metrics: {
elements: {
additionalProperties: true,
properties: {
content: {
additionalProperties: true,
properties: {
ssvc_v1: {
additionalProperties: true,
properties: {
role: {
type: 'string',
},
},
},
},
},
},
},
},
},
},
},
},
})

const validate = ajv.compile(inputSchema)

/**
* This implements the recommended test 6.2.37 of the CSAF 2.1 standard.
*
* @param {any} doc
*/
export function recommendedTest_6_2_37(doc) {
/** @type {Array<{ message: string; instancePath: string }>} */
const warnings = []
const context = { warnings }

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

/*
* Please note that this list can change
* */
const registeredSsvcRoles = ['Supplier', 'Deployer', 'Coordinator']

doc.vulnerabilities?.forEach((vulnerability, vulnerabilityIndex) => {
vulnerability.metrics.forEach((metric, metricIndex) => {
const role = metric.content.ssvc_v1.role
if (!registeredSsvcRoles.includes(role)) {
context.warnings.push({
message: `The used role "${role}" is not a registered role`,
Copy link
Contributor

Choose a reason for hiding this comment

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

Perhaps we should list here the 3 possibles roles

Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
message: `The used role "${role}" is not a registered role`,
message: `The used role "${role}" is not registered`,

instancePath: `/vulnerabilities/${vulnerabilityIndex}/metrics/${metricIndex}/content/ssvc_v1/role`,
})
}
})
})

return context
}
Loading