Skip to content

Commit 0fb89f9

Browse files
feat(CSAF2.1): #447 add mandatory test 6.2.47 - refactor getCanonicalUrl, improve naming and messages
1 parent c2f0efe commit 0fb89f9

File tree

5 files changed

+161
-57
lines changed

5 files changed

+161
-57
lines changed

csaf_2_1/recommendedTests/recommendedTest_6_2_47.js

Lines changed: 23 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import Ajv from 'ajv/dist/jtd.js'
2-
3-
const ajv = new Ajv()
2+
import { isCanonicalUrl } from '../../lib/shared/urlHelper.js'
43

54
/** @typedef {import('ajv/dist/jtd.js').JTDDataType<typeof inputSchema>} InputSchema */
65

@@ -10,6 +9,8 @@ const ajv = new Ajv()
109

1110
/** @typedef {NonNullable<Metric['content']>} MetricContent */
1211

12+
/** @typedef {{url?: string, category?: string}} Reference */
13+
1314
const jtdAjv = new Ajv()
1415

1516
const inputSchema = /** @type {const} */ ({
@@ -21,7 +22,7 @@ const inputSchema = /** @type {const} */ ({
2122
references: {
2223
elements: {
2324
additionalProperties: true,
24-
properties: {
25+
optionalProperties: {
2526
category: { type: 'string' },
2627
url: { type: 'string' },
2728
},
@@ -64,41 +65,24 @@ const inputSchema = /** @type {const} */ ({
6465
},
6566
})
6667

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)
68+
const validateInput = jtdAjv.compile(inputSchema)
7969

8070
/**
8171
* Get the canonical url from the document
8272
* @return {string} canonical url or empty when no canonical url exists
83-
* @param {Array<Reference> | undefined} references
84-
* @param {string | undefined} trackingId
73+
* @param {Array<{url?: string, category?: string}>|undefined} references
74+
* @param {string|undefined} trackingId
8575
*/
8676
function getCanonicalUrl(references, trackingId) {
8777
if (references && trackingId) {
8878
// Find the reference that matches our criteria
8979
/** @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-
)
80+
const canonicalUrlReference = references.find((reference) =>
81+
isCanonicalUrl(reference, trackingId)
9882
)
9983

10084
// When we find a matching reference, we know it has the url property
101-
// because validateReference ensures it matches the Reference schema
85+
// because isCanonicalUrl ensures it matches the Reference schema
10286
return canonicalUrlReference?.url ?? ''
10387
} else {
10488
return ''
@@ -112,7 +96,7 @@ function getCanonicalUrl(references, trackingId) {
11296
* @param {string} canonicalURL
11397
* @return {boolean}
11498
*/
115-
function hasServerRatingAndNoSource(metric, canonicalURL) {
99+
function hasSeverityRatingAndNoSource(metric, canonicalURL) {
116100
return (
117101
(!metric.source || metric.source === canonicalURL) &&
118102
!!metric?.content?.qualitative_severity_rating
@@ -129,18 +113,18 @@ function hasServerRatingAndNoSource(metric, canonicalURL) {
129113
* @param {any} doc
130114
*/
131115
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
116+
const ctx = {
117+
warnings:
118+
/** @type {Array<{ instancePath: string; message: string }>} */ ([]),
119+
}
120+
if (!validateInput(doc)) {
121+
return ctx
138122
}
139123

140124
/** @type {Array<Vulnerability>} */
141125
const vulnerabilities = doc.vulnerabilities
142126
const canonicalURL = getCanonicalUrl(
143-
doc.document.references,
127+
doc.document?.references,
144128
doc.document?.tracking?.id
145129
)
146130

@@ -150,23 +134,23 @@ export function recommendedTest_6_2_47(doc) {
150134
/** @type {Array<String> | undefined} */
151135
const invalidPaths = metrics
152136
?.map((metric, metricIndex) =>
153-
hasServerRatingAndNoSource(metric, canonicalURL)
137+
hasSeverityRatingAndNoSource(metric, canonicalURL)
154138
? `/vulnerabilities/${vulnerabilityIndex}/metrics/${metricIndex}/content/qualitative_severity_rating`
155139
: null
156140
)
157141
.filter((path) => path !== null)
158142

159143
if (!!invalidPaths) {
160144
invalidPaths.forEach((path) => {
161-
context.warnings.push({
145+
ctx.warnings.push({
162146
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',
147+
'a qualitative severity rating is used by the issuing party (as no "source" is given' +
148+
' or the source property equals to the canonical URL)',
165149
instancePath: path,
166150
})
167151
})
168152
}
169153
})
170154

171-
return context
155+
return ctx
172156
}

lib/optionalTests/optionalTest_6_2_11.js

Lines changed: 3 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import Ajv from 'ajv/dist/jtd.js'
2+
import { isCanonicalUrl } from '../shared/urlHelper.js'
23

34
const ajv = new Ajv()
45

@@ -26,16 +27,7 @@ const inputSchema = /** @type {const} */ ({
2627
},
2728
})
2829

29-
const referenceSchema = /** @type {const} */ ({
30-
additionalProperties: true,
31-
properties: {
32-
category: { type: 'string' },
33-
url: { type: 'string' },
34-
},
35-
})
36-
3730
const validate = ajv.compile(inputSchema)
38-
const validateReference = ajv.compile(referenceSchema)
3931

4032
/**
4133
* @param {any} doc
@@ -58,15 +50,8 @@ export default function optionalTest_6_2_11(doc) {
5850
return ctx
5951
}
6052

61-
const hasCanonicalURL = doc.document.references.some(
62-
(r) =>
63-
validateReference(r) &&
64-
r.category === 'self' &&
65-
r.url.startsWith('https://') &&
66-
r.url.endsWith(
67-
doc.document.tracking.id.toLowerCase().replace(/[^+\-a-z0-9]+/g, '_') +
68-
'.json'
69-
)
53+
const hasCanonicalURL = doc.document.references.some((reference) =>
54+
isCanonicalUrl(reference, doc.document.tracking.id)
7055
)
7156

7257
if (!hasCanonicalURL) {

lib/shared/urlHelper.js

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import Ajv from 'ajv/dist/jtd.js'
2+
3+
const ajv = new Ajv()
4+
5+
const referenceSchema = /** @type {const} */ ({
6+
additionalProperties: true,
7+
properties: {
8+
category: { type: 'string' },
9+
url: { type: 'string' },
10+
},
11+
})
12+
const validateReference = ajv.compile(referenceSchema)
13+
14+
/**
15+
* Checks whether a reference contains a canonical URL
16+
* It works for CSAF 2.0 and CSAF 2.1
17+
* A canonical URL fulfills all the following:
18+
* - It has the category self
19+
* - The url starts with https://
20+
* - The url ends with the valid filename for the CSAF document
21+
* A filename must apply the following rules
22+
* - The value /trackingId is converted into lower case
23+
* - Any character sequence which is not part of one of the following groups MUST be replaced by a single underscore (_)
24+
* Lower case ASCII letters (0x61 - 0x7A)
25+
* digits (0x30 - 0x39)
26+
* special characters: + (0x2B), - (0x2D)
27+
* - The file extension .json MUST be appended.
28+
* @param {{url?: string, category?: string}} reference
29+
* @param {string} trackingId
30+
* @return {boolean}
31+
*/
32+
export function isCanonicalUrl(reference, trackingId) {
33+
return (
34+
validateReference(reference) &&
35+
reference.category === 'self' &&
36+
reference.url !== undefined &&
37+
reference.url.startsWith('https://') &&
38+
reference.url.endsWith(
39+
trackingId.toLowerCase().replace(/[^+\-a-z0-9]+/g, '_') + '.json'
40+
)
41+
)
42+
}

tests/csaf_2_1/recommendedTest_6_2_47.js

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,37 @@ describe('recommendedTest_6_2_47', function () {
88
0
99
)
1010
})
11+
12+
it('runs on references with empty category in reference', function () {
13+
assert.equal(
14+
recommendedTest_6_2_47({
15+
document: {
16+
references: [
17+
{
18+
category: 'self',
19+
summary: 'The canonical URL for the CSAF document.',
20+
url: 'https://example.com/.well-known/csaf/clear/2024/oasis_csaf_tc-csaf_2_1-2024-6-2-47-02.json',
21+
},
22+
{ url: 'https://some.other.url' },
23+
],
24+
tracking: {
25+
id: 'OASIS_CSAF_TC-CSAF_2.1-2024-6-2-47-11',
26+
},
27+
},
28+
vulnerabilities: [
29+
{
30+
metrics: [
31+
{
32+
content: {
33+
qualitative_severity_rating: 'low',
34+
},
35+
products: ['CSAFPID-9080700'],
36+
},
37+
],
38+
},
39+
],
40+
}).warnings.length,
41+
1
42+
)
43+
})
1144
})

tests/urlHelper.js

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { isCanonicalUrl } from '../lib/shared/urlHelper.js'
2+
import { expect } from 'chai'
3+
4+
describe('test url helper', function () {
5+
it('test isCanonicalUrl', function () {
6+
expect(
7+
isCanonicalUrl(
8+
{
9+
url: 'https://example.com/.well-known/csaf/clear/2024/oasis_csaf_tc-csaf_2_1-2024-6-2-47-12.json',
10+
category: 'self',
11+
},
12+
'OASIS_CSAF_TC-CSAF_2.1-2024-6-2-47-12'
13+
),
14+
'Valid canonical URL'
15+
).to.be.true
16+
17+
expect(
18+
isCanonicalUrl(
19+
{
20+
url: 'https://example.com/.well-known/csaf/clear/2024/oasis_csaf_tc-csaf_2_1-2024-6-2-47-12.json',
21+
category: 'not_self',
22+
},
23+
'OASIS_CSAF_TC-CSAF_2.1-2024-6-2-47-12'
24+
),
25+
'Invalid canonical URL - category not self'
26+
).to.be.false
27+
})
28+
29+
expect(
30+
isCanonicalUrl(
31+
{
32+
url: 'http://example.com/.well-known/csaf/clear/2024/oasis_csaf_tc-csaf_2_1-2024-6-2-47-12.json',
33+
category: 'self',
34+
},
35+
'OASIS_CSAF_TC-CSAF_2.1-2024-6-2-47-12'
36+
),
37+
'Invalid canonical URL - url starts not with https://'
38+
).to.be.false
39+
40+
expect(
41+
isCanonicalUrl(
42+
{
43+
category: 'self',
44+
},
45+
'OASIS_CSAF_TC-CSAF_2.1-2024-6-2-47-12'
46+
),
47+
'Invalid canonical URL - no URL '
48+
).to.be.false
49+
50+
expect(
51+
isCanonicalUrl(
52+
{
53+
url: 'https://example.com/.well-known/csaf/clear/2024/oasis_csaf_tc-csaf_2_1-2024-6-2-47-12_invalid.json',
54+
category: 'self',
55+
},
56+
'OASIS_CSAF_TC-CSAF_2.1-2024-6-2-47-12'
57+
),
58+
'Valid canonical URL - URL ends not with valid filename'
59+
).to.be.false
60+
})

0 commit comments

Comments
 (0)