Skip to content

Commit fe876e2

Browse files
committed
feat: add mandatory test 6.1.51
1 parent 573df9c commit fe876e2

File tree

5 files changed

+231
-3
lines changed

5 files changed

+231
-3
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -331,7 +331,6 @@ The following tests are not yet implemented and therefore missing:
331331
- Mandatory Test 6.1.48
332332
- Mandatory Test 6.1.49
333333
- Mandatory Test 6.1.50
334-
- Mandatory Test 6.1.51
335334
- Mandatory Test 6.1.52
336335
- Mandatory Test 6.1.53
337336
- Mandatory Test 6.1.54
@@ -435,6 +434,7 @@ export const mandatoryTest_6_1_38: DocumentTest
435434
export const mandatoryTest_6_1_39: DocumentTest
436435
export const mandatoryTest_6_1_40: DocumentTest
437436
export const mandatoryTest_6_1_41: DocumentTest
437+
export const mandatoryTest_6_1_51: DocumentTest
438438
```
439439
440440
[(back to top)](#bsi-csaf-validator-lib)

csaf_2_1/mandatoryTests.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,15 +37,16 @@ export {
3737
export { mandatoryTest_6_1_1 } from './mandatoryTests/mandatoryTest_6_1_1.js'
3838
export { mandatoryTest_6_1_7 } from './mandatoryTests/mandatoryTest_6_1_7.js'
3939
export { mandatoryTest_6_1_8 } from './mandatoryTests/mandatoryTest_6_1_8.js'
40+
export { mandatoryTest_6_1_9 } from './mandatoryTests/mandatoryTest_6_1_9.js'
4041
export { mandatoryTest_6_1_11 } from './mandatoryTests/mandatoryTest_6_1_11.js'
4142
export { mandatoryTest_6_1_13 } from './mandatoryTests/mandatoryTest_6_1_13.js'
4243
export { mandatoryTest_6_1_10 } from './mandatoryTests/mandatoryTest_6_1_10.js'
4344
export { mandatoryTest_6_1_34 } from './mandatoryTests/mandatoryTest_6_1_34.js'
4445
export { mandatoryTest_6_1_35 } from './mandatoryTests/mandatoryTest_6_1_35.js'
45-
export { mandatoryTest_6_1_9 } from './mandatoryTests/mandatoryTest_6_1_9.js'
4646
export { mandatoryTest_6_1_36 } from './mandatoryTests/mandatoryTest_6_1_36.js'
4747
export { mandatoryTest_6_1_37 } from './mandatoryTests/mandatoryTest_6_1_37.js'
4848
export { mandatoryTest_6_1_38 } from './mandatoryTests/mandatoryTests_6_1_38.js'
4949
export { mandatoryTest_6_1_39 } from './mandatoryTests/mandatoryTest_6_1_39.js'
5050
export { mandatoryTest_6_1_40 } from './mandatoryTests/mandatoryTest_6_1_40.js'
5151
export { mandatoryTest_6_1_41 } from './mandatoryTests/mandatoryTest_6_1_41.js'
52+
export { mandatoryTest_6_1_51 } from './mandatoryTests/mandatoryTest_6_1_51.js'
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
import Ajv from 'ajv/dist/jtd.js'
2+
import {compareZonedDateTimes} from "../../lib/shared/dateHelper.js";
3+
4+
const ajv = new Ajv()
5+
6+
/*
7+
This is the jtd schema that needs to match the input document so that the
8+
test is activated. If this schema doesn't match it normally means that the input
9+
document does not validate against the csaf json schema or optional fields that
10+
the test checks are not present.
11+
*/
12+
const inputSchema = /** @type {const} */ ({
13+
additionalProperties: true,
14+
properties: {
15+
document: {
16+
additionalProperties: true,
17+
properties: {
18+
tracking: {
19+
additionalProperties: true,
20+
properties: {
21+
revision_history: {
22+
elements: {
23+
additionalProperties: true,
24+
optionalProperties: {
25+
date: {type: 'string'},
26+
},
27+
},
28+
},
29+
status: {type: 'string'},
30+
},
31+
},
32+
},
33+
},
34+
vulnerabilities: {
35+
elements: {
36+
additionalProperties: true,
37+
optionalProperties: {
38+
metrics: {
39+
elements: {
40+
additionalProperties: true,
41+
optionalProperties: {
42+
content: {
43+
additionalProperties: true,
44+
optionalProperties: {
45+
epss: {
46+
additionalProperties: true,
47+
optionalProperties: {
48+
timestamp: {type: 'string'},
49+
},
50+
},
51+
},
52+
},
53+
},
54+
},
55+
},
56+
},
57+
},
58+
},
59+
},
60+
})
61+
62+
const validate = ajv.compile(inputSchema)
63+
64+
/**
65+
* This implements the mandatory test 6.1.51 of the CSAF 2.1 standard.
66+
*
67+
* @param {any} doc
68+
*/
69+
export function mandatoryTest_6_1_51(doc) {
70+
/*
71+
The `ctx` variable holds the state that is accumulated during the test ran and is
72+
finally returned by the function.
73+
*/
74+
const ctx = {
75+
errors:
76+
/** @type {Array<{ instancePath: string; message: string }>} */ ([]),
77+
isValid: true,
78+
}
79+
80+
if (!validate(doc)) {
81+
return ctx
82+
}
83+
const status = doc.document.tracking.status
84+
if (status !== 'final' && status !== 'interim') {
85+
return ctx
86+
}
87+
88+
const newestRevisionHistoryItem = doc.document.tracking.revision_history
89+
.filter(item => item.date != null)
90+
.slice()
91+
.sort((a, z) =>
92+
compareZonedDateTimes(
93+
/** @type {string} */ (z.date),
94+
/** @type {string} */ (a.date)
95+
)
96+
)[0]
97+
98+
doc.vulnerabilities?.forEach((vulnerability, vulnerabilityIndex) => {
99+
const metrics = vulnerability.metrics || []
100+
metrics.forEach((metric, metricIdx) => {
101+
const content = metric.content || {}
102+
const epss = content.epss || {}
103+
if (epss.timestamp) {
104+
if (
105+
newestRevisionHistoryItem &&
106+
compareZonedDateTimes(
107+
/** @type {string} */ (newestRevisionHistoryItem.date),
108+
epss.timestamp
109+
) < 0
110+
) {
111+
ctx.isValid = false
112+
ctx.errors.push({
113+
instancePath: `/vulnerabilities/${vulnerabilityIndex}/metrics/${metricIdx}/content/epss/timestamp`,
114+
message: `the status is ${status}, but the EPSS 'timestamp' is newer than the newest revision history date`,
115+
})
116+
}
117+
}
118+
})
119+
})
120+
121+
return ctx
122+
}
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import assert from 'node:assert/strict'
2+
import {mandatoryTest_6_1_51} from '../../csaf_2_1/mandatoryTests/mandatoryTest_6_1_51.js'
3+
import {cvssV31Content} from "./shared/csafDocHelper.js";
4+
5+
describe('mandatoryTest_6_1_51', function () {
6+
it('only runs on relevant documents', function () {
7+
assert.equal(mandatoryTest_6_1_51({ document: 'mydoc' }).isValid, true)
8+
})
9+
10+
it('skips status draft', function () {
11+
assert.equal(
12+
mandatoryTest_6_1_51({
13+
document: {
14+
tracking: {
15+
revision_history: [],
16+
status: 'draft',
17+
},
18+
},
19+
vulnerabilities: [],
20+
}).isValid,
21+
true
22+
)
23+
})
24+
25+
it('skips empty objects', function () {
26+
assert.equal(
27+
mandatoryTest_6_1_51({
28+
document: {
29+
tracking: {
30+
revision_history: [
31+
{},
32+
{ date: '2024-01-24T10:00:00.000Z' }
33+
],
34+
status: 'final',
35+
},
36+
},
37+
vulnerabilities: [
38+
{}, // should be ignored
39+
{
40+
metrics: [
41+
{}, // should be ignored
42+
{
43+
content: {
44+
epss: {
45+
timestamp: '2024-01-24T12:34:56.789Z',
46+
}
47+
}
48+
},
49+
],
50+
},
51+
],
52+
}).isValid,
53+
false
54+
)
55+
})
56+
57+
it('skips empty content object', function () {
58+
assert.equal(
59+
mandatoryTest_6_1_51({
60+
document: {
61+
tracking: {
62+
revision_history: [{ date: '2024-01-24T10:00:00.000Z' }],
63+
status: 'final',
64+
},
65+
},
66+
vulnerabilities: [
67+
{
68+
metrics: [
69+
{
70+
content: {
71+
}
72+
},
73+
],
74+
},
75+
],
76+
}).isValid,
77+
true
78+
)
79+
})
80+
81+
it('skips empty epss object', function () {
82+
assert.equal(
83+
mandatoryTest_6_1_51({
84+
document: {
85+
tracking: {
86+
revision_history: [{ date: '2024-01-24T10:00:00.000Z' }],
87+
status: 'final',
88+
},
89+
},
90+
vulnerabilities: [
91+
{
92+
metrics: [
93+
{
94+
content: {
95+
epss: {
96+
}
97+
}
98+
},
99+
],
100+
},
101+
],
102+
}).isValid,
103+
true
104+
)
105+
})
106+
})

tests/csaf_2_1/oasis.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@ const excluded = [
3636
'6.1.48',
3737
'6.1.49',
3838
'6.1.50',
39-
'6.1.51',
4039
'6.1.52',
4140
'6.1.53',
4241
'6.1.54',

0 commit comments

Comments
 (0)