Skip to content

Commit a7e7792

Browse files
Merge pull request #261 from secvisogram/197-csaf-2.1-mandatory-test-6.1.9
feat(CSAF2.1): #197 copy and adapt mandatory test 6.1.9 from CSAF 2.0…
2 parents ee6005f + 60b5f05 commit a7e7792

File tree

7 files changed

+2946
-1
lines changed

7 files changed

+2946
-1
lines changed

csaf_2_1/mandatoryTests.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ export { mandatoryTest_6_1_11 } from './mandatoryTests/mandatoryTest_6_1_11.js'
4040
export { mandatoryTest_6_1_13 } from './mandatoryTests/mandatoryTest_6_1_13.js'
4141
export { mandatoryTest_6_1_34 } from './mandatoryTests/mandatoryTest_6_1_34.js'
4242
export { mandatoryTest_6_1_35 } from './mandatoryTests/mandatoryTest_6_1_35.js'
43+
export { mandatoryTest_6_1_9 } from './mandatoryTests/mandatoryTest_6_1_9.js'
4344
export { mandatoryTest_6_1_37 } from './mandatoryTests/mandatoryTest_6_1_37.js'
4445
export { mandatoryTest_6_1_38 } from './mandatoryTests/mandatoryTests_6_1_38.js'
4546
export { mandatoryTest_6_1_39 } from './mandatoryTests/mandatoryTest_6_1_39.js'
Lines changed: 284 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,284 @@
1+
import cvss2js from 'cvss2js'
2+
import { getEnvironmentalScoreFromVectorString } from '../../lib/shared/cvss2.js'
3+
import { cvss30 as CVSS30, cvss31 as CVSS31 } from '../../lib/shared/first.js'
4+
import Ajv from 'ajv/dist/jtd.js'
5+
import { calculateCvss4_0_Score } from '../../lib/shared/cvss4.js'
6+
7+
const ajv = new Ajv()
8+
9+
const inputSchema = /** @type {const} */ ({
10+
additionalProperties: true,
11+
properties: {
12+
vulnerabilities: {
13+
elements: {
14+
additionalProperties: true,
15+
properties: {
16+
metrics: {
17+
elements: {
18+
additionalProperties: true,
19+
properties: {
20+
content: {
21+
additionalProperties: true,
22+
optionalProperties: {
23+
cvss_v2: {
24+
additionalProperties: true,
25+
optionalProperties: {
26+
vectorString: { type: 'string' },
27+
baseScore: { type: 'float64' },
28+
temporalScore: { type: 'float64' },
29+
environmentalScore: { type: 'float64' },
30+
},
31+
},
32+
cvss_v3: {
33+
additionalProperties: true,
34+
optionalProperties: {
35+
vectorString: { type: 'string' },
36+
version: { type: 'string' },
37+
baseScore: { type: 'float64' },
38+
baseSeverity: { type: 'string' },
39+
temporalScore: { type: 'float64' },
40+
temporalSeverity: { type: 'string' },
41+
environmentalScore: { type: 'float64' },
42+
environmentalSeverity: { type: 'string' },
43+
},
44+
},
45+
cvss_v4: {
46+
additionalProperties: true,
47+
optionalProperties: {
48+
vectorString: { type: 'string' },
49+
version: { type: 'string' },
50+
baseScore: { type: 'float64' },
51+
baseSeverity: { type: 'string' },
52+
threatScore: { type: 'float64' },
53+
threatSeverity: { type: 'string' },
54+
environmentalScore: { type: 'float64' },
55+
environmentalSeverity: { type: 'string' },
56+
},
57+
},
58+
},
59+
},
60+
},
61+
},
62+
},
63+
},
64+
},
65+
},
66+
},
67+
})
68+
69+
const validateInput = ajv.compile(inputSchema)
70+
71+
/**
72+
* @param {any} doc
73+
*/
74+
export function mandatoryTest_6_1_9(doc) {
75+
/** @type {Array<{ message: string; instancePath: string }>} */
76+
const errors = []
77+
let isValid = true
78+
79+
if (!validateInput(doc)) {
80+
return { errors, isValid }
81+
}
82+
83+
const vulnerabilities = doc.vulnerabilities
84+
vulnerabilities.forEach((vulnerability, vulnerabilityIndex) => {
85+
const metrics = vulnerability.metrics
86+
metrics?.forEach((metric, metricIndex) => {
87+
calculateCvss2(metric).forEach((failedMetricName) => {
88+
errors.push({
89+
instancePath: `/vulnerabilities/${vulnerabilityIndex}/scores/${metricIndex}/cvss_v2/${failedMetricName}`,
90+
message: 'invalid calculated value',
91+
})
92+
})
93+
94+
calculateCvss3(metric).forEach((failedMetricName) => {
95+
errors.push({
96+
instancePath: `/vulnerabilities/${vulnerabilityIndex}/scores/${metricIndex}/cvss_v3/${failedMetricName}`,
97+
message: 'invalid calculated value',
98+
})
99+
})
100+
101+
calculateCvss4(metric).forEach((failedMetricName) => {
102+
errors.push({
103+
instancePath: `/vulnerabilities/${vulnerabilityIndex}/scores/${metricIndex}/cvss_v4/${failedMetricName}`,
104+
message: 'invalid calculated value',
105+
})
106+
})
107+
})
108+
})
109+
110+
return { errors, isValid: errors.length === 0 }
111+
}
112+
113+
/**
114+
* @param {string} vectorString
115+
* @returns
116+
*/
117+
function safelyParseCVSSV2Vector(vectorString) {
118+
try {
119+
return {
120+
success: true,
121+
baseMetricScore: cvss2js.getBaseScore(vectorString),
122+
temporalMetricScore: cvss2js.getTemporalScore(vectorString),
123+
environmentalMetricScore:
124+
getEnvironmentalScoreFromVectorString(vectorString),
125+
}
126+
} catch (e) {
127+
return {
128+
success: false,
129+
baseMetricScore: -1,
130+
temporalMetricScore: -1,
131+
environmentalMetricScore: -1,
132+
}
133+
}
134+
}
135+
136+
/**
137+
* @param {any} metric
138+
* @return {string[]}
139+
*/
140+
function calculateCvss2(metric) {
141+
const failedMetrics = []
142+
if (
143+
metric.content?.cvss_v2 &&
144+
typeof metric.content.cvss_v2.vectorString === 'string'
145+
) {
146+
const cvssV2 = metric.content.cvss_v2
147+
const result = safelyParseCVSSV2Vector(metric.content.cvss_v2.vectorString)
148+
149+
if (result.success) {
150+
for (const { score, expectedScore, name } of [
151+
{
152+
score: cvssV2.baseScore,
153+
expectedScore: result.baseMetricScore,
154+
name: 'baseScore',
155+
},
156+
{
157+
score: cvssV2.temporalScore,
158+
expectedScore: result.temporalMetricScore,
159+
name: 'temporalScore',
160+
},
161+
{
162+
score: cvssV2.environmentalScore,
163+
expectedScore: result.environmentalMetricScore,
164+
name: 'environmentalScore',
165+
},
166+
]) {
167+
if (typeof score === 'number') {
168+
if (score !== Number(expectedScore)) {
169+
failedMetrics.push(name)
170+
}
171+
}
172+
}
173+
} else {
174+
// Invalid CVSS string is tested in test 6.1.8
175+
}
176+
}
177+
178+
return failedMetrics
179+
}
180+
181+
/**
182+
* @param {any} metric
183+
* @return {string[]}
184+
*/
185+
function calculateCvss3(metric) {
186+
const failedMetrics = []
187+
if (
188+
metric.content?.cvss_v3 &&
189+
typeof metric.content.cvss_v3.vectorString === 'string' &&
190+
(metric.content.cvss_v3.version === '3.1' ||
191+
metric.content.cvss_v3.version === '3.0')
192+
) {
193+
const calculator =
194+
metric.content.cvss_v3.version === '3.0' ? CVSS30 : CVSS31
195+
const result = calculator.calculateCVSSFromVector(
196+
metric.content.cvss_v3.vectorString
197+
)
198+
199+
if (result.success) {
200+
for (const { score: scoreValue, expectedScore, name } of [
201+
{
202+
score: metric.content.cvss_v3.baseScore,
203+
expectedScore: result.baseMetricScore,
204+
name: 'baseScore',
205+
},
206+
{
207+
score: metric.content.cvss_v3.temporalScore,
208+
expectedScore: result.temporalMetricScore,
209+
name: 'temporalScore',
210+
},
211+
{
212+
score: metric.content.cvss_v3.environmentalScore,
213+
expectedScore: result.environmentalMetricScore,
214+
name: 'environmentalScore',
215+
},
216+
]) {
217+
if (typeof scoreValue === 'number') {
218+
if (scoreValue !== Number(expectedScore)) {
219+
failedMetrics.push(name)
220+
}
221+
}
222+
}
223+
224+
for (const { severity, expectedSeverity, name } of [
225+
{
226+
severity: metric.content.cvss_v3.baseSeverity,
227+
expectedSeverity: result.baseSeverity,
228+
name: 'baseSeverity',
229+
},
230+
{
231+
severity: metric.content.cvss_v3.temporalSeverity,
232+
expectedSeverity: result.temporalSeverity,
233+
name: 'temporalSeverity',
234+
},
235+
{
236+
severity: metric.content.cvss_v3.environmentalSeverity,
237+
expectedSeverity: result.environmentalSeverity,
238+
name: 'environmentalSeverity',
239+
},
240+
]) {
241+
if (typeof severity === 'string') {
242+
if (severity !== expectedSeverity.toUpperCase()) {
243+
failedMetrics.push(name)
244+
}
245+
}
246+
}
247+
} else {
248+
// Invalid CVSS is tested in test 6.1.8
249+
}
250+
}
251+
return failedMetrics
252+
}
253+
254+
/**
255+
* @param {any} metric
256+
* @return {string[]}
257+
*/
258+
function calculateCvss4(metric) {
259+
/**
260+
* @type {string[]}
261+
*/
262+
const failedMetrics = []
263+
if (
264+
metric.content?.cvss_v4 &&
265+
typeof metric.content.cvss_v4.vectorString === 'string'
266+
) {
267+
const scores = calculateCvss4_0_Score(metric.content.cvss_v4.vectorString)
268+
scores.forEach((score) => {
269+
const expectedScore = metric.content.cvss_v4[score.scoreJsonName]
270+
const expectedSeverity = metric.content.cvss_v4[score.severityJsonName]
271+
if (typeof expectedScore === 'number' && score.score !== expectedScore) {
272+
failedMetrics.push(score.scoreJsonName)
273+
}
274+
275+
if (
276+
typeof expectedSeverity === 'string' &&
277+
score.severity.toUpperCase() !== expectedSeverity.toUpperCase()
278+
) {
279+
failedMetrics.push(score.severityJsonName)
280+
}
281+
})
282+
}
283+
return failedMetrics
284+
}

0 commit comments

Comments
 (0)