Skip to content

Commit 8bd8ce4

Browse files
authored
Merge pull request #216 from secvisogram/197-csaf-2.1-mandatory-test-6.1.36
feat: add mandatory test 6.1.36
2 parents 59c2a46 + cebe7aa commit 8bd8ce4

File tree

3 files changed

+222
-1
lines changed

3 files changed

+222
-1
lines changed

csaf_2_1/mandatoryTests.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ 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'
4343
export { mandatoryTest_6_1_9 } from './mandatoryTests/mandatoryTest_6_1_9.js'
44+
export { mandatoryTest_6_1_36 } from './mandatoryTests/mandatoryTest_6_1_36.js'
4445
export { mandatoryTest_6_1_37 } from './mandatoryTests/mandatoryTest_6_1_37.js'
4546
export { mandatoryTest_6_1_38 } from './mandatoryTests/mandatoryTests_6_1_38.js'
4647
export { mandatoryTest_6_1_39 } from './mandatoryTests/mandatoryTest_6_1_39.js'
Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
import Ajv from 'ajv/dist/jtd.js'
2+
3+
const ajv = new Ajv()
4+
5+
/**
6+
* @typedef {'workaround'
7+
* | 'mitigation'
8+
* | 'vendor_fix'
9+
* | 'optional_patch'
10+
* | 'none_available'
11+
* | 'fix_planned'
12+
* | 'no_fix_planned'} Category
13+
*/
14+
15+
/**
16+
* @typedef {'first_affected'
17+
* | 'known_affected'
18+
* | 'last_affected'
19+
* | 'known_not_affected'
20+
* | 'first_fixed'
21+
* | 'fixed'
22+
* | 'under_investigation'} ProductStatus
23+
*/
24+
25+
/*
26+
The spec groups the product statuses in groups. This grouping is
27+
expressed in this object.
28+
*/
29+
const productStatus = /**
30+
* @type {const}
31+
* @satisfies {Record<string, ProductStatus[]>}
32+
*/ ({
33+
affected: ['first_affected', 'known_affected', 'last_affected'],
34+
notAffected: ['known_not_affected'],
35+
fixed: ['first_fixed', 'fixed'],
36+
underInvestigation: ['under_investigation'],
37+
})
38+
39+
/**
40+
* This map holds prohibited category / product status combinations.
41+
* See https://github.com/oasis-tcs/csaf/blob/master/csaf_2.1/prose/share/csaf-v2.1-draft.md#324131-vulnerabilities-property---remediations---category-
42+
*
43+
* @type {Map<string, Set<string>>}
44+
*/
45+
const prohibitionRuleMap = new Map(
46+
/** @satisfies {Array<[Category, ProductStatus[]]>} */ ([
47+
['workaround', [...productStatus.notAffected, ...productStatus.fixed]],
48+
['mitigation', [...productStatus.notAffected, ...productStatus.fixed]],
49+
['vendor_fix', [...productStatus.notAffected, ...productStatus.fixed]],
50+
['optional_patch', [...productStatus.affected]],
51+
['none_available', [...productStatus.notAffected, ...productStatus.fixed]],
52+
['fix_planned', [...productStatus.fixed]],
53+
['no_fix_planned', [...productStatus.fixed]],
54+
]).map((e) => [e[0], new Set(e[1])])
55+
)
56+
57+
const remediationSchema = /** @type {const} */ ({
58+
additionalProperties: true,
59+
optionalProperties: {
60+
group_ids: {
61+
elements: {
62+
type: 'string',
63+
},
64+
},
65+
product_ids: {
66+
elements: {
67+
type: 'string',
68+
},
69+
},
70+
category: { type: 'string' },
71+
},
72+
})
73+
74+
/*
75+
This is the jtd schema that needs to match the input document so that the
76+
test is activated. If this schema doesn't match it normally means that the input
77+
document does not validate against the csaf json schema or optional fields that
78+
the test checks are not present.
79+
*/
80+
const inputSchema = /** @type {const} */ ({
81+
additionalProperties: true,
82+
optionalProperties: {
83+
product_tree: {
84+
additionalProperties: true,
85+
optionalProperties: {
86+
product_groups: {
87+
elements: {
88+
additionalProperties: true,
89+
optionalProperties: {
90+
group_id: { type: 'string' },
91+
product_ids: {
92+
elements: {
93+
type: 'string',
94+
},
95+
},
96+
},
97+
},
98+
},
99+
},
100+
},
101+
},
102+
properties: {
103+
vulnerabilities: {
104+
elements: {
105+
additionalProperties: true,
106+
optionalProperties: {
107+
remediations: {
108+
elements: remediationSchema,
109+
},
110+
product_status: {
111+
additionalProperties: true,
112+
optionalProperties: {
113+
first_affected: { elements: { type: 'string' } },
114+
known_affected: { elements: { type: 'string' } },
115+
last_affected: { elements: { type: 'string' } },
116+
known_not_affected: { elements: { type: 'string' } },
117+
first_fixed: { elements: { type: 'string' } },
118+
fixed: { elements: { type: 'string' } },
119+
under_investigation: { elements: { type: 'string' } },
120+
},
121+
},
122+
},
123+
},
124+
},
125+
},
126+
})
127+
128+
const validate = ajv.compile(inputSchema)
129+
130+
/**
131+
* This implements the mandatory test 6.1.36 of the CSAF 2.1 standard.
132+
*
133+
* @param {any} doc
134+
*/
135+
export function mandatoryTest_6_1_36(doc) {
136+
/*
137+
The `ctx` variable holds the state that is accumulated during the test ran and is
138+
finally returned by the function.
139+
*/
140+
const ctx = {
141+
/** @type {Array<{ instancePath: string; message: string }>} */
142+
errors: [],
143+
isValid: true,
144+
}
145+
146+
if (!validate(doc)) {
147+
return ctx
148+
}
149+
150+
for (const [
151+
vulnerabilityIndex,
152+
vulnerability,
153+
] of doc.vulnerabilities.entries()) {
154+
vulnerability.remediations?.forEach((remediation, remediationIndex) => {
155+
const category = remediation.category
156+
if (!category) return
157+
158+
/**
159+
* This map holds the discovered product ids for the remediation and maps them to
160+
* the set of corresponding product status names. Later we can check this map to
161+
* find out if there are any contradicting remediations.
162+
*
163+
* @type {Map<string, Set<string>>}
164+
*/
165+
const productToProductStatusNamesMap = new Map()
166+
167+
/**
168+
* This function adds all product status names for the given product id to the
169+
* `productMap`. If the product does not yet exist in the map, it is added.
170+
*
171+
* @param {string} id
172+
*/
173+
const collectProductStatusNames = (id) => {
174+
const productStatusNames =
175+
/*
176+
To speed things up we first check if the product status names where already
177+
collected and do not search again. The product names are always for a
178+
product in the same vulnerability.
179+
*/
180+
productToProductStatusNamesMap.get(id) ??
181+
new Set(
182+
/** @type {string[]} */ (
183+
Object.entries(vulnerability.product_status ?? {})
184+
.filter((e) =>
185+
Array.isArray(e[1]) ? e[1].includes(id) : false
186+
)
187+
.map((e) => e[0])
188+
)
189+
)
190+
productToProductStatusNamesMap.set(id, productStatusNames)
191+
}
192+
193+
remediation.product_ids?.forEach(collectProductStatusNames)
194+
195+
remediation.group_ids?.forEach((id) => {
196+
const group = doc.product_tree?.product_groups?.find(
197+
(g) => g.group_id === id
198+
)
199+
if (!group) return
200+
group.product_ids?.forEach(collectProductStatusNames)
201+
})
202+
203+
for (const [
204+
productId,
205+
productStatusNames,
206+
] of productToProductStatusNamesMap) {
207+
for (const productStatus of productStatusNames) {
208+
if (prohibitionRuleMap.get(category)?.has(productStatus)) {
209+
ctx.errors.push({
210+
instancePath: `/vulnerabilities/${vulnerabilityIndex}/remediations/${remediationIndex}`,
211+
message: `contradicting combination of product status ${productStatus} and remediation category ${category} for product id "${productId}"`,
212+
})
213+
ctx.isValid = false
214+
}
215+
}
216+
}
217+
})
218+
}
219+
220+
return ctx
221+
}

tests/csaf_2_1/oasis.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@ const excluded = [
2828
'6.1.27.17',
2929
'6.1.27.18',
3030
'6.1.27.19',
31-
'6.1.36',
3231
'6.1.37',
3332
'6.1.42',
3433
'6.1.43',

0 commit comments

Comments
 (0)