Skip to content

Commit 99316c8

Browse files
committed
feat(CSAF2.1): add recommendedTest_6_2_41.js
1 parent 4c505d5 commit 99316c8

File tree

5 files changed

+230
-2
lines changed

5 files changed

+230
-2
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -361,7 +361,6 @@ The following tests are not yet implemented and therefore missing:
361361
- Recommended Test 6.2.38
362362
- Recommended Test 6.2.39
363363
- Recommended Test 6.2.40
364-
- Recommended Test 6.2.41
365364
- Recommended Test 6.2.42
366365
- Recommended Test 6.2.43
367366
- Recommended Test 6.2.44
@@ -462,6 +461,7 @@ export const recommendedTest_6_2_16: DocumentTest
462461
export const recommendedTest_6_2_17: DocumentTest
463462
export const recommendedTest_6_2_18: DocumentTest
464463
export const recommendedTest_6_2_22: DocumentTest
464+
export const recommendedTest_6_2_41: DocumentTest
465465
```
466466
467467
[(back to top)](#bsi-csaf-validator-lib)

csaf_2_1/recommendedTests.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,4 @@ export { recommendedTest_6_2_27 } from './recommendedTests/recommendedTest_6_2_2
3131
export { recommendedTest_6_2_28 } from './recommendedTests/recommendedTest_6_2_28.js'
3232
export { recommendedTest_6_2_29 } from './recommendedTests/recommendedTest_6_2_29.js'
3333
export { recommendedTest_6_2_38 } from './recommendedTests/recommendedTest_6_2_38.js'
34+
export { recommendedTest_6_2_41 } from './recommendedTests/recommendedTest_6_2_41.js'
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
import Ajv from 'ajv/dist/jtd.js'
2+
import { compareZonedDateTimes } from '../../lib/shared/dateHelper.js'
3+
4+
const ajv = new Ajv()
5+
6+
const inputSchema = /** @type {const} */ ({
7+
additionalProperties: true,
8+
properties: {
9+
document: {
10+
additionalProperties: true,
11+
properties: {
12+
tracking: {
13+
additionalProperties: true,
14+
properties: {
15+
revision_history: {
16+
elements: {
17+
additionalProperties: true,
18+
optionalProperties: {
19+
date: { type: 'string' },
20+
},
21+
},
22+
},
23+
status: { type: 'string' },
24+
},
25+
},
26+
},
27+
},
28+
vulnerabilities: {
29+
elements: {
30+
additionalProperties: true,
31+
optionalProperties: {
32+
metrics: {
33+
elements: {
34+
additionalProperties: true,
35+
optionalProperties: {
36+
content: {
37+
additionalProperties: true,
38+
optionalProperties: {
39+
epss: {
40+
additionalProperties: true,
41+
optionalProperties: {
42+
timestamp: { type: 'string' },
43+
},
44+
},
45+
},
46+
},
47+
},
48+
},
49+
},
50+
},
51+
},
52+
},
53+
},
54+
})
55+
56+
const validate = ajv.compile(inputSchema)
57+
58+
/**
59+
* This implements the recommended test 6.2.41 of the CSAF 2.1 standard.
60+
*
61+
/**
62+
* @param {any} doc
63+
*/
64+
export function recommendedTest_6_2_41(doc) {
65+
/** @type {Array<{ message: string; instancePath: string }>} */
66+
const warnings = []
67+
const context = { warnings }
68+
69+
if (!validate(doc)) {
70+
return context
71+
}
72+
73+
const status = doc.document.tracking.status
74+
if (status !== 'final' && status !== 'interim') {
75+
return context
76+
}
77+
78+
const newestRevisionHistoryItem = doc.document.tracking.revision_history
79+
.filter((item) => item.date != null)
80+
.sort((a, z) =>
81+
compareZonedDateTimes(
82+
/** @type {string} */ (z.date),
83+
/** @type {string} */ (a.date)
84+
)
85+
)[0]
86+
87+
if (!newestRevisionHistoryItem || !newestRevisionHistoryItem.date) {
88+
return context
89+
}
90+
91+
doc.vulnerabilities?.forEach((vulnerability, vulnerabilityIndex) => {
92+
const metrics = vulnerability.metrics || []
93+
const newestEpss = metrics
94+
.map((m) => m.content?.epss)
95+
.filter((item) => item?.timestamp != null)
96+
.sort((a, z) => {
97+
if (!a || !z) return 0
98+
return compareZonedDateTimes(
99+
/** @type {string} */ (z.timestamp),
100+
/** @type {string} */ (a.timestamp)
101+
)
102+
})[0]
103+
104+
if (
105+
!newestEpss ||
106+
!newestEpss.timestamp ||
107+
!newestRevisionHistoryItem ||
108+
!newestRevisionHistoryItem.date
109+
) {
110+
return context
111+
}
112+
113+
const revisionDateObj = new Date(newestRevisionHistoryItem.date)
114+
const epssDateObj = new Date(newestEpss.timestamp)
115+
116+
// difference in milliseconds
117+
const diffInMs = revisionDateObj.getTime() - epssDateObj.getTime()
118+
// 15 days in milliseconds
119+
const fifteenDaysMs = 15 * 24 * 60 * 60 * 1000
120+
121+
if (diffInMs > fifteenDaysMs) {
122+
context.warnings.push({
123+
instancePath: `/vulnerabilities/${vulnerabilityIndex}/metrics/content/epss/timestamp`,
124+
message:
125+
`the status is ${status}, but the EPSS "timestamp:" ${newestEpss.timestamp} is more than 15 days ` +
126+
`older than the newest "revision history date:" ${newestRevisionHistoryItem.date}`,
127+
})
128+
}
129+
})
130+
131+
return context
132+
}

tests/csaf_2_1/oasis.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,6 @@ const excluded = [
6161
'6.2.39.3',
6262
'6.2.39.4',
6363
'6.2.40',
64-
'6.2.41',
6564
'6.2.42',
6665
'6.2.43',
6766
'6.2.44',
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import assert from 'node:assert'
2+
import { recommendedTest_6_2_41 } from '../../csaf_2_1/recommendedTests.js'
3+
4+
describe('recommendedTest_6_2_41', function () {
5+
it('only runs on relevant documents', function () {
6+
assert.equal(
7+
recommendedTest_6_2_41({ vulnerabilities: 'mydoc' }).warnings.length,
8+
0
9+
)
10+
})
11+
12+
it('skips status draft', function () {
13+
assert.equal(
14+
recommendedTest_6_2_41({
15+
document: {
16+
tracking: {
17+
revision_history: [],
18+
status: 'draft',
19+
},
20+
},
21+
vulnerabilities: [],
22+
}).warnings.length,
23+
0
24+
)
25+
})
26+
27+
it('skips empty revision_history object', function () {
28+
assert.equal(
29+
recommendedTest_6_2_41({
30+
document: {
31+
tracking: {
32+
revision_history: [
33+
{}, // should be ignored
34+
],
35+
status: 'final',
36+
},
37+
},
38+
vulnerabilities: [],
39+
}).warnings.length,
40+
0
41+
)
42+
})
43+
44+
it('skips empty metrics object', function () {
45+
assert.equal(
46+
recommendedTest_6_2_41({
47+
document: {
48+
tracking: {
49+
revision_history: [{ date: '2024-01-24T10:00:00.000Z' }],
50+
status: 'final',
51+
},
52+
},
53+
vulnerabilities: [
54+
{
55+
metrics: [
56+
{}, // should be ignored
57+
],
58+
},
59+
],
60+
}).warnings.length,
61+
0
62+
)
63+
})
64+
65+
it('skips empty epss object', function () {
66+
assert.equal(
67+
recommendedTest_6_2_41({
68+
document: {
69+
tracking: {
70+
revision_history: [{ date: '2024-01-24T10:00:00.000Z' }],
71+
status: 'final',
72+
},
73+
},
74+
vulnerabilities: [
75+
{
76+
metrics: [
77+
{
78+
content: {
79+
epss: {}, // should be ignored
80+
},
81+
},
82+
{
83+
content: {
84+
epss: {
85+
timestamp: '2024-01-01T10:00:00.000Z',
86+
},
87+
},
88+
},
89+
],
90+
},
91+
],
92+
}).warnings.length,
93+
1
94+
)
95+
})
96+
})

0 commit comments

Comments
 (0)