Skip to content

Commit c8d301a

Browse files
Merge pull request #208 from secvisogram/feat/#197-Mandatory_Tests_CSAF2_1_6.1.7
Feat/#197 mandatory tests csaf2.1 6.1.7
2 parents b23229f + 8d30ec9 commit c8d301a

File tree

7 files changed

+380
-2
lines changed

7 files changed

+380
-2
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -311,7 +311,6 @@ The following tests are not yet implemented and therefore missing:
311311
312312
**Mandatory Tests**
313313
314-
- Mandatory Test 6.1.7
315314
- Mandatory Test 6.1.10
316315
- Mandatory Test 6.1.14
317316
- Mandatory Test 6.1.16
@@ -395,6 +394,7 @@ export const mandatoryTest_6_1_3: DocumentTest
395394
export const mandatoryTest_6_1_4: DocumentTest
396395
export const mandatoryTest_6_1_5: DocumentTest
397396
export const mandatoryTest_6_1_6: DocumentTest
397+
export const mandatoryTest_6_1_7: DocumentTest
398398
export const mandatoryTest_6_1_8: DocumentTest
399399
export const mandatoryTest_6_1_9: DocumentTest
400400
export const mandatoryTest_6_1_11: DocumentTest

csaf_2_1/mandatoryTests.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ export {
3535
mandatoryTest_6_1_33,
3636
} from '../mandatoryTests.js'
3737
export { mandatoryTest_6_1_1 } from './mandatoryTests/mandatoryTest_6_1_1.js'
38+
export { mandatoryTest_6_1_7 } from './mandatoryTests/mandatoryTest_6_1_7.js'
3839
export { mandatoryTest_6_1_8 } from './mandatoryTests/mandatoryTest_6_1_8.js'
3940
export { mandatoryTest_6_1_11 } from './mandatoryTests/mandatoryTest_6_1_11.js'
4041
export { mandatoryTest_6_1_13 } from './mandatoryTests/mandatoryTest_6_1_13.js'
Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
import Ajv from 'ajv/dist/jtd.js'
2+
3+
/**
4+
* @typedef {string} Product
5+
* /
6+
7+
/** @typedef {import('ajv/dist/jtd.js').JTDDataType<typeof inputSchema>} InputSchema */
8+
9+
/** @typedef {InputSchema['vulnerabilities'][number]} Vulnerability */
10+
11+
/** @typedef {NonNullable<Vulnerability['metrics']>[number]} Metric */
12+
13+
/** @typedef {NonNullable<Metric['content']>} MetricContent */
14+
15+
const jtdAjv = new Ajv()
16+
17+
const inputSchema = /** @type {const} */ ({
18+
additionalProperties: true,
19+
properties: {
20+
vulnerabilities: {
21+
elements: {
22+
additionalProperties: true,
23+
optionalProperties: {
24+
metrics: {
25+
elements: {
26+
additionalProperties: true,
27+
optionalProperties: {
28+
source: {
29+
type: 'string',
30+
},
31+
products: {
32+
elements: { type: 'string' },
33+
},
34+
content: {
35+
additionalProperties: true,
36+
optionalProperties: {
37+
cvss_v2: {
38+
additionalProperties: true,
39+
optionalProperties: {
40+
version: { type: 'string' },
41+
},
42+
},
43+
cvss_v3: {
44+
additionalProperties: true,
45+
optionalProperties: {
46+
version: { type: 'string' },
47+
},
48+
},
49+
cvss_v4: {
50+
additionalProperties: true,
51+
optionalProperties: {
52+
version: { type: 'string' },
53+
},
54+
},
55+
},
56+
},
57+
},
58+
},
59+
},
60+
},
61+
},
62+
},
63+
},
64+
})
65+
66+
const validate = jtdAjv.compile(inputSchema)
67+
68+
/**
69+
* For each item in /vulnerabilities it MUST be tested that the same Product ID
70+
* is not a member of more than one CVSS-Vectors with the same version and the same source.
71+
* @param {unknown} doc
72+
*/
73+
export function mandatoryTest_6_1_7(doc) {
74+
const ctx = {
75+
errors:
76+
/** @type {Array<{ instancePath: string; message: string }>} */ ([]),
77+
isValid: true,
78+
}
79+
80+
/** @type {Array<{ message: string; instancePath: string }>} */
81+
const errors = []
82+
let isValid = true
83+
84+
if (!validate(doc)) {
85+
return ctx
86+
}
87+
88+
/** @type {Array<Vulnerability>} */
89+
const vulnerabilities = doc.vulnerabilities
90+
91+
/**
92+
* Create a unique string for the tuple of version and source
93+
* to compare them easily
94+
* @param {string} version
95+
* @param {string | undefined} source
96+
*/
97+
function createIdForVersionAndSource(version, source) {
98+
return JSON.stringify({ version: version, source: source ?? '' })
99+
}
100+
101+
/**
102+
*
103+
* @param {Metric} metric
104+
* @param {string} versionSourceId
105+
* @returns {string|null}
106+
*/
107+
function findCvssVersionWithSameVersionAndSource(metric, versionSourceId) {
108+
if (
109+
metric.content?.cvss_v2?.version !== undefined &&
110+
versionSourceId ===
111+
createIdForVersionAndSource(
112+
metric.content?.cvss_v2.version,
113+
metric.source
114+
)
115+
) {
116+
return metric.content?.cvss_v2?.version
117+
} else if (
118+
metric.content?.cvss_v3?.version !== undefined &&
119+
versionSourceId ===
120+
createIdForVersionAndSource(
121+
metric.content?.cvss_v3.version,
122+
metric.source
123+
)
124+
) {
125+
return metric.content?.cvss_v3?.version
126+
} else if (
127+
metric.content?.cvss_v4?.version !== undefined &&
128+
versionSourceId ===
129+
createIdForVersionAndSource(
130+
metric.content?.cvss_v4.version,
131+
metric.source
132+
)
133+
) {
134+
return metric.content?.cvss_v4?.version
135+
} else {
136+
return null
137+
}
138+
}
139+
140+
/**
141+
* @param {Metric} metric
142+
* @param {Set<string>} versionSourceIdSet
143+
*/
144+
function addAllVersionSourceIdsInMetricToSet(metric, versionSourceIdSet) {
145+
if (metric.content?.cvss_v2?.version !== undefined) {
146+
versionSourceIdSet.add(
147+
createIdForVersionAndSource(
148+
metric.content?.cvss_v2.version,
149+
metric.source
150+
)
151+
)
152+
}
153+
if (metric.content?.cvss_v3?.version !== undefined) {
154+
versionSourceIdSet.add(
155+
createIdForVersionAndSource(
156+
metric.content?.cvss_v3.version,
157+
metric.source
158+
)
159+
)
160+
}
161+
if (metric.content?.cvss_v4?.version !== undefined) {
162+
versionSourceIdSet.add(
163+
createIdForVersionAndSource(
164+
metric.content?.cvss_v4.version,
165+
metric.source
166+
)
167+
)
168+
}
169+
}
170+
171+
vulnerabilities.forEach((vulnerabilityItem, vulnerabilityIndex) => {
172+
/** @type {Map<string, Set<string>>} */
173+
const versionsSourceIdSetByProduct = new Map()
174+
175+
/** @type {Array<Metric> | undefined} */
176+
const metrics = vulnerabilityItem.metrics
177+
metrics?.forEach((metric, metricIndex) => {
178+
/** @type {Array<Product> | undefined} */
179+
const productsOfMetric = metric.products
180+
productsOfMetric?.forEach((product, productIndex) => {
181+
const versionSourceIdsOfProduct =
182+
versionsSourceIdSetByProduct.get(product) ?? new Set()
183+
versionsSourceIdSetByProduct.set(product, versionSourceIdsOfProduct)
184+
185+
versionSourceIdsOfProduct.forEach((versionSourceIdOfProduct) => {
186+
const sameVersion = findCvssVersionWithSameVersionAndSource(
187+
metric,
188+
versionSourceIdOfProduct
189+
)
190+
if (sameVersion) {
191+
isValid = false
192+
const sourceOfMetric = metric.source ? metric.source : ''
193+
errors.push({
194+
message: `Product is member of more than one CVSS-Vectors with the same version '${sameVersion}' and same source '${sourceOfMetric}'.`,
195+
instancePath: `/vulnerabilities/${vulnerabilityIndex}/metrics/${metricIndex}/products/${productIndex}`,
196+
})
197+
}
198+
})
199+
200+
addAllVersionSourceIdsInMetricToSet(metric, versionSourceIdsOfProduct)
201+
})
202+
})
203+
})
204+
205+
return { errors, isValid }
206+
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import { expect } from 'chai'
2+
import { mandatoryTest_6_1_7 } from '../../csaf_2_1/mandatoryTests/mandatoryTest_6_1_7.js'
3+
import minimalDoc from './shared/minimalDoc.js'
4+
import {
5+
cvssV31Content,
6+
productTreeWithFullProductName,
7+
} from './shared/csafDocHelper.js'
8+
import csaf_2_1 from '../../csaf_2_1/schemaTests/csaf_2_1.js'
9+
10+
const emptyMandatoryTest6_1_7 = {
11+
$schema: minimalDoc.$schema,
12+
document: {
13+
...minimalDoc.document,
14+
},
15+
product_tree: productTreeWithFullProductName('CSAFPID-9080700', 'Product A'),
16+
vulnerabilities: [
17+
{
18+
metrics: [
19+
{
20+
cvss_v3: {
21+
version: '3.0',
22+
},
23+
},
24+
],
25+
},
26+
{
27+
metrics: [
28+
{
29+
content: {},
30+
products: [],
31+
},
32+
],
33+
},
34+
],
35+
}
36+
37+
const failingTestWithNotConsideredObject6_1_7 = {
38+
$schema: minimalDoc.$schema,
39+
document: {
40+
...minimalDoc.document,
41+
},
42+
product_tree: productTreeWithFullProductName('CSAFPID-9080700', 'Product A'),
43+
vulnerabilities: [
44+
{}, // input schema should not consider this
45+
{
46+
metrics: [
47+
cvssV31Content(6.5, 'CVSS:3.1/AV:L/AC:L/PR:H/UI:R/S:U/C:H/I:H/A:H', [
48+
'CSAFPID-9080700',
49+
]),
50+
cvssV31Content(6.5, 'CVSS:3.1/AV:L/AC:L/PR:H/UI:R/S:U/C:H/I:H/A:H', [
51+
'CSAFPID-9080700',
52+
]),
53+
],
54+
},
55+
],
56+
}
57+
58+
describe('mandatory test 6.1.7', function () {
59+
describe('valid examples', function () {
60+
it('test empty vulnerabilities', async function () {
61+
const result = mandatoryTest_6_1_7(emptyMandatoryTest6_1_7)
62+
expect(result.errors.length).to.eq(0)
63+
})
64+
65+
it('test input schema with minimal doc', async function () {
66+
expect(csaf_2_1(minimalDoc).isValid).to.be.true
67+
const result = mandatoryTest_6_1_7(minimalDoc)
68+
expect(result.errors.length).to.eq(0)
69+
})
70+
71+
it('test input schema with not considered json object in vulnerabilities', async function () {
72+
const result = mandatoryTest_6_1_7(
73+
failingTestWithNotConsideredObject6_1_7
74+
)
75+
expect(result.errors.length).to.eq(1)
76+
})
77+
})
78+
})

tests/csaf_2_1/oasis.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import * as mandatory from '../../csaf_2_1/mandatoryTests.js'
1111
*/
1212
const excluded = [
1313
'6.1.6',
14-
'6.1.7',
1514
'6.1.10',
1615
'6.1.14',
1716
'6.1.16',
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/**
2+
* @param {string} productId
3+
* @param {string} name
4+
*/
5+
export function productTreeWithFullProductName(productId, name) {
6+
return {
7+
full_product_names: [
8+
{
9+
product_id: productId,
10+
name: name,
11+
},
12+
],
13+
}
14+
}
15+
16+
/**
17+
* @param {number} baseSCore
18+
* @param {string} vectorString
19+
* @param {string[]} products
20+
*/
21+
export function cvssV31Content(baseSCore, vectorString, products) {
22+
return {
23+
content: {
24+
cvss_v3: {
25+
version: '3.1',
26+
vectorString: vectorString,
27+
baseScore: baseSCore,
28+
baseSeverity: severityFromScore(baseSCore),
29+
},
30+
},
31+
products: products,
32+
}
33+
}
34+
35+
/**
36+
* @param {number} score
37+
* @return {string}
38+
*/
39+
export function severityFromScore(score) {
40+
if (score >= 9.0) {
41+
return 'CRITICAL'
42+
}
43+
if (score >= 7.0) {
44+
return 'HIGH'
45+
}
46+
if (score >= 4.0) {
47+
return 'MEDIUM'
48+
}
49+
if (score >= 0.1) {
50+
return 'LOW'
51+
} else {
52+
return 'NONE'
53+
}
54+
}

0 commit comments

Comments
 (0)