Skip to content

Commit c2f0efe

Browse files
feat(CSAF2.1): #447 add mandatory test 6.2.47
1 parent b084e83 commit c2f0efe

File tree

5 files changed

+185
-1
lines changed

5 files changed

+185
-1
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -461,6 +461,7 @@ export const recommendedTest_6_2_28: DocumentTest
461461
export const recommendedTest_6_2_29: DocumentTest
462462
export const recommendedTest_6_2_30: DocumentTest
463463
export const recommendedTest_6_2_43: DocumentTest
464+
export const recommendedTest_6_2_47: DocumentTest
464465
```
465466
466467
[(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
@@ -35,3 +35,4 @@ export { recommendedTest_6_2_29 } from './recommendedTests/recommendedTest_6_2_2
3535
export { recommendedTest_6_2_30 } from './recommendedTests/recommendedTest_6_2_30.js'
3636
export { recommendedTest_6_2_38 } from './recommendedTests/recommendedTest_6_2_38.js'
3737
export { recommendedTest_6_2_43 } from './recommendedTests/recommendedTest_6_2_43.js'
38+
export { recommendedTest_6_2_47 } from './recommendedTests/recommendedTest_6_2_47.js'
Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
import Ajv from 'ajv/dist/jtd.js'
2+
3+
const ajv = new Ajv()
4+
5+
/** @typedef {import('ajv/dist/jtd.js').JTDDataType<typeof inputSchema>} InputSchema */
6+
7+
/** @typedef {InputSchema['vulnerabilities'][number]} Vulnerability */
8+
9+
/** @typedef {NonNullable<Vulnerability['metrics']>[number]} Metric */
10+
11+
/** @typedef {NonNullable<Metric['content']>} MetricContent */
12+
13+
const jtdAjv = new Ajv()
14+
15+
const inputSchema = /** @type {const} */ ({
16+
additionalProperties: true,
17+
properties: {
18+
document: {
19+
additionalProperties: true,
20+
optionalProperties: {
21+
references: {
22+
elements: {
23+
additionalProperties: true,
24+
properties: {
25+
category: { type: 'string' },
26+
url: { type: 'string' },
27+
},
28+
},
29+
},
30+
31+
tracking: {
32+
additionalProperties: true,
33+
optionalProperties: {
34+
id: { type: 'string' },
35+
},
36+
},
37+
},
38+
},
39+
vulnerabilities: {
40+
elements: {
41+
additionalProperties: true,
42+
optionalProperties: {
43+
metrics: {
44+
elements: {
45+
additionalProperties: true,
46+
optionalProperties: {
47+
source: {
48+
type: 'string',
49+
},
50+
content: {
51+
additionalProperties: true,
52+
optionalProperties: {
53+
qualitative_severity_rating: {
54+
type: 'string',
55+
},
56+
},
57+
},
58+
},
59+
},
60+
},
61+
},
62+
},
63+
},
64+
},
65+
})
66+
67+
/** @typedef {{ url: string; category: string}} Reference */
68+
69+
const referenceSchema = /** @type {const} */ ({
70+
additionalProperties: true,
71+
properties: {
72+
category: { type: 'string' },
73+
url: { type: 'string' },
74+
},
75+
})
76+
77+
const validate = jtdAjv.compile(inputSchema)
78+
const validateReference = ajv.compile(referenceSchema)
79+
80+
/**
81+
* Get the canonical url from the document
82+
* @return {string} canonical url or empty when no canonical url exists
83+
* @param {Array<Reference> | undefined} references
84+
* @param {string | undefined} trackingId
85+
*/
86+
function getCanonicalUrl(references, trackingId) {
87+
if (references && trackingId) {
88+
// Find the reference that matches our criteria
89+
/** @type {Reference| undefined} */
90+
const canonicalUrlReference = references.find(
91+
(reference) =>
92+
validateReference(reference) &&
93+
reference.category === 'self' &&
94+
reference.url.startsWith('https://') &&
95+
reference.url.endsWith(
96+
trackingId.toLowerCase().replace(/[^+\-a-z0-9]+/g, '_') + '.json'
97+
)
98+
)
99+
100+
// When we find a matching reference, we know it has the url property
101+
// because validateReference ensures it matches the Reference schema
102+
return canonicalUrlReference?.url ?? ''
103+
} else {
104+
return ''
105+
}
106+
}
107+
108+
/**
109+
* check whether metric has a qualitative_severity_rating
110+
* and no `source` or `source` that is equal to the canonical URL.
111+
* @param {Metric} metric
112+
* @param {string} canonicalURL
113+
* @return {boolean}
114+
*/
115+
function hasServerRatingAndNoSource(metric, canonicalURL) {
116+
return (
117+
(!metric.source || metric.source === canonicalURL) &&
118+
!!metric?.content?.qualitative_severity_rating
119+
)
120+
}
121+
122+
/**
123+
* For each item in `metrics` provided by the issuing party it MUST be tested
124+
* that it does not use the qualitative severity rating.
125+
* This covers all items in `metrics` that do not have a `source` property and those where the `source` is equal to
126+
* the canonical URL.
127+
*
128+
/**
129+
* @param {any} doc
130+
*/
131+
export function recommendedTest_6_2_47(doc) {
132+
/** @type {Array<{ message: string; instancePath: string }>} */
133+
const warnings = []
134+
const context = { warnings }
135+
136+
if (!validate(doc)) {
137+
return context
138+
}
139+
140+
/** @type {Array<Vulnerability>} */
141+
const vulnerabilities = doc.vulnerabilities
142+
const canonicalURL = getCanonicalUrl(
143+
doc.document.references,
144+
doc.document?.tracking?.id
145+
)
146+
147+
vulnerabilities.forEach((vulnerabilityItem, vulnerabilityIndex) => {
148+
/** @type {Array<Metric> | undefined} */
149+
const metrics = vulnerabilityItem.metrics
150+
/** @type {Array<String> | undefined} */
151+
const invalidPaths = metrics
152+
?.map((metric, metricIndex) =>
153+
hasServerRatingAndNoSource(metric, canonicalURL)
154+
? `/vulnerabilities/${vulnerabilityIndex}/metrics/${metricIndex}/content/qualitative_severity_rating`
155+
: null
156+
)
157+
.filter((path) => path !== null)
158+
159+
if (!!invalidPaths) {
160+
invalidPaths.forEach((path) => {
161+
context.warnings.push({
162+
message:
163+
'the metric has a qualitative severity rating and no source property' +
164+
' or a source property that ist equal to the canonical URL',
165+
instancePath: path,
166+
})
167+
})
168+
}
169+
})
170+
171+
return context
172+
}

tests/csaf_2_1/oasis.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,6 @@ const excluded = [
5252
'6.2.44',
5353
'6.2.45',
5454
'6.2.46',
55-
'6.2.47',
5655
'6.3.12',
5756
'6.3.13',
5857
'6.3.14',
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import assert from 'node:assert'
2+
import { recommendedTest_6_2_47 } from '../../csaf_2_1/recommendedTests.js'
3+
4+
describe('recommendedTest_6_2_47', function () {
5+
it('only runs on relevant documents', function () {
6+
assert.equal(
7+
recommendedTest_6_2_47({ vulnerabilities: 'mydoc' }).warnings.length,
8+
0
9+
)
10+
})
11+
})

0 commit comments

Comments
 (0)