Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -461,6 +461,7 @@ export const recommendedTest_6_2_28: DocumentTest
export const recommendedTest_6_2_29: DocumentTest
export const recommendedTest_6_2_30: DocumentTest
export const recommendedTest_6_2_43: DocumentTest
export const recommendedTest_6_2_47: DocumentTest
```

[(back to top)](#bsi-csaf-validator-lib)
Expand Down
1 change: 1 addition & 0 deletions csaf_2_1/recommendedTests.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,4 @@ export { recommendedTest_6_2_29 } from './recommendedTests/recommendedTest_6_2_2
export { recommendedTest_6_2_30 } from './recommendedTests/recommendedTest_6_2_30.js'
export { recommendedTest_6_2_38 } from './recommendedTests/recommendedTest_6_2_38.js'
export { recommendedTest_6_2_43 } from './recommendedTests/recommendedTest_6_2_43.js'
export { recommendedTest_6_2_47 } from './recommendedTests/recommendedTest_6_2_47.js'
156 changes: 156 additions & 0 deletions csaf_2_1/recommendedTests/recommendedTest_6_2_47.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
import Ajv from 'ajv/dist/jtd.js'
import { isCanonicalUrl } from '../../lib/shared/urlHelper.js'

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

/** @typedef {InputSchema['vulnerabilities'][number]} Vulnerability */

/** @typedef {NonNullable<Vulnerability['metrics']>[number]} Metric */

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

/** @typedef {{url?: string, category?: string}} Reference */

const jtdAjv = new Ajv()

const inputSchema = /** @type {const} */ ({
additionalProperties: true,
properties: {
document: {
additionalProperties: true,
optionalProperties: {
references: {
elements: {
additionalProperties: true,
optionalProperties: {
category: { type: 'string' },
url: { type: 'string' },
},
},
},

tracking: {
additionalProperties: true,
optionalProperties: {
id: { type: 'string' },
},
},
},
},
vulnerabilities: {
elements: {
additionalProperties: true,
optionalProperties: {
metrics: {
elements: {
additionalProperties: true,
optionalProperties: {
source: {
type: 'string',
},
content: {
additionalProperties: true,
optionalProperties: {
qualitative_severity_rating: {
type: 'string',
},
},
},
},
},
},
},
},
},
},
})

const validateInput = jtdAjv.compile(inputSchema)

/**
* Get the canonical url from the document
* @return {string} canonical url or empty when no canonical url exists
* @param {Array<{url?: string, category?: string}>|undefined} references
* @param {string|undefined} trackingId
*/
function getCanonicalUrl(references, trackingId) {
if (references && trackingId) {
// Find the reference that matches our criteria
/** @type {Reference| undefined} */
const canonicalUrlReference = references.find((reference) =>
isCanonicalUrl(reference, trackingId)
)

// When we find a matching reference, we know it has the url property
// because isCanonicalUrl ensures it matches the Reference schema
return canonicalUrlReference?.url ?? ''
} else {
return ''
}
}

/**
* check whether metric has a qualitative_severity_rating
* and no `source` or `source` that is equal to the canonical URL.
* @param {Metric} metric
* @param {string} canonicalURL
* @return {boolean}
*/
function hasSeverityRatingAndNoSource(metric, canonicalURL) {
return (
(!metric.source || metric.source === canonicalURL) &&
!!metric?.content?.qualitative_severity_rating
)
}

/**
* For each item in `metrics` provided by the issuing party it MUST be tested
* that it does not use the qualitative severity rating.
* This covers all items in `metrics` that do not have a `source` property and those where the `source` is equal to
* the canonical URL.
*
/**
* @param {any} doc
*/
export function recommendedTest_6_2_47(doc) {
const ctx = {
warnings:
/** @type {Array<{ instancePath: string; message: string }>} */ ([]),
}
if (!validateInput(doc)) {
return ctx
}

/** @type {Array<Vulnerability>} */
const vulnerabilities = doc.vulnerabilities
const canonicalURL = getCanonicalUrl(
doc.document?.references,
doc.document?.tracking?.id
)

vulnerabilities.forEach((vulnerabilityItem, vulnerabilityIndex) => {
/** @type {Array<Metric> | undefined} */
const metrics = vulnerabilityItem.metrics
/** @type {Array<String> | undefined} */
const invalidPaths = metrics
?.map((metric, metricIndex) =>
hasSeverityRatingAndNoSource(metric, canonicalURL)
? `/vulnerabilities/${vulnerabilityIndex}/metrics/${metricIndex}/content/qualitative_severity_rating`
: null
)
.filter((path) => path !== null)

if (!!invalidPaths) {
invalidPaths.forEach((path) => {
ctx.warnings.push({
message:
'a qualitative severity rating is used by the issuing party (as no "source" is given' +
' or the source property equals to the canonical URL)',
instancePath: path,
})
})
}
})

return ctx
}
21 changes: 3 additions & 18 deletions lib/optionalTests/optionalTest_6_2_11.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import Ajv from 'ajv/dist/jtd.js'
import { isCanonicalUrl } from '../shared/urlHelper.js'

const ajv = new Ajv()

Expand Down Expand Up @@ -26,16 +27,7 @@ const inputSchema = /** @type {const} */ ({
},
})

const referenceSchema = /** @type {const} */ ({
additionalProperties: true,
properties: {
category: { type: 'string' },
url: { type: 'string' },
},
})

const validate = ajv.compile(inputSchema)
const validateReference = ajv.compile(referenceSchema)

/**
* @param {any} doc
Expand All @@ -58,15 +50,8 @@ export default function optionalTest_6_2_11(doc) {
return ctx
}

const hasCanonicalURL = doc.document.references.some(
(r) =>
validateReference(r) &&
r.category === 'self' &&
r.url.startsWith('https://') &&
r.url.endsWith(
doc.document.tracking.id.toLowerCase().replace(/[^+\-a-z0-9]+/g, '_') +
'.json'
)
const hasCanonicalURL = doc.document.references.some((reference) =>
isCanonicalUrl(reference, doc.document.tracking.id)
)

if (!hasCanonicalURL) {
Expand Down
42 changes: 42 additions & 0 deletions lib/shared/urlHelper.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import Ajv from 'ajv/dist/jtd.js'

const ajv = new Ajv()

const referenceSchema = /** @type {const} */ ({
additionalProperties: true,
properties: {
category: { type: 'string' },
url: { type: 'string' },
},
})
const validateReference = ajv.compile(referenceSchema)

/**
* Checks whether a reference contains a canonical URL
* It works for CSAF 2.0 and CSAF 2.1
* A canonical URL fulfills all the following:
* - It has the category self
* - The url starts with https://
* - The url ends with the valid filename for the CSAF document
* A filename must apply the following rules
* - The value /trackingId is converted into lower case
* - Any character sequence which is not part of one of the following groups MUST be replaced by a single underscore (_)
* Lower case ASCII letters (0x61 - 0x7A)
* digits (0x30 - 0x39)
* special characters: + (0x2B), - (0x2D)
* - The file extension .json MUST be appended.
* @param {{url?: string, category?: string}} reference
* @param {string} trackingId
* @return {boolean}
*/
export function isCanonicalUrl(reference, trackingId) {
return (
validateReference(reference) &&
reference.category === 'self' &&
reference.url !== undefined &&
reference.url.startsWith('https://') &&
reference.url.endsWith(
trackingId.toLowerCase().replace(/[^+\-a-z0-9]+/g, '_') + '.json'
)
)
}
1 change: 0 additions & 1 deletion tests/csaf_2_1/oasis.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,6 @@ const excluded = [
'6.2.44',
'6.2.45',
'6.2.46',
'6.2.47',
'6.3.12',
'6.3.13',
'6.3.14',
Expand Down
44 changes: 44 additions & 0 deletions tests/csaf_2_1/recommendedTest_6_2_47.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import assert from 'node:assert'
import { recommendedTest_6_2_47 } from '../../csaf_2_1/recommendedTests.js'

describe('recommendedTest_6_2_47', function () {
it('only runs on relevant documents', function () {
assert.equal(
recommendedTest_6_2_47({ vulnerabilities: 'mydoc' }).warnings.length,
0
)
})

it('runs on references with empty category in reference', function () {
assert.equal(
recommendedTest_6_2_47({
document: {
references: [
{
category: 'self',
summary: 'The canonical URL for the CSAF document.',
url: 'https://example.com/.well-known/csaf/clear/2024/oasis_csaf_tc-csaf_2_1-2024-6-2-47-02.json',
},
{ url: 'https://some.other.url' },
],
tracking: {
id: 'OASIS_CSAF_TC-CSAF_2.1-2024-6-2-47-11',
},
},
vulnerabilities: [
{
metrics: [
{
content: {
qualitative_severity_rating: 'low',
},
products: ['CSAFPID-9080700'],
},
],
},
],
}).warnings.length,
1
)
})
})
60 changes: 60 additions & 0 deletions tests/urlHelper.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { isCanonicalUrl } from '../lib/shared/urlHelper.js'
import { expect } from 'chai'

describe('test url helper', function () {
it('test isCanonicalUrl', function () {
expect(
isCanonicalUrl(
{
url: 'https://example.com/.well-known/csaf/clear/2024/oasis_csaf_tc-csaf_2_1-2024-6-2-47-12.json',
category: 'self',
},
'OASIS_CSAF_TC-CSAF_2.1-2024-6-2-47-12'
),
'Valid canonical URL'
).to.be.true

expect(
isCanonicalUrl(
{
url: 'https://example.com/.well-known/csaf/clear/2024/oasis_csaf_tc-csaf_2_1-2024-6-2-47-12.json',
category: 'not_self',
},
'OASIS_CSAF_TC-CSAF_2.1-2024-6-2-47-12'
),
'Invalid canonical URL - category not self'
).to.be.false
})

expect(
isCanonicalUrl(
{
url: 'http://example.com/.well-known/csaf/clear/2024/oasis_csaf_tc-csaf_2_1-2024-6-2-47-12.json',
category: 'self',
},
'OASIS_CSAF_TC-CSAF_2.1-2024-6-2-47-12'
),
'Invalid canonical URL - url starts not with https://'
).to.be.false

expect(
isCanonicalUrl(
{
category: 'self',
},
'OASIS_CSAF_TC-CSAF_2.1-2024-6-2-47-12'
),
'Invalid canonical URL - no URL '
).to.be.false

expect(
isCanonicalUrl(
{
url: 'https://example.com/.well-known/csaf/clear/2024/oasis_csaf_tc-csaf_2_1-2024-6-2-47-12_invalid.json',
category: 'self',
},
'OASIS_CSAF_TC-CSAF_2.1-2024-6-2-47-12'
),
'Valid canonical URL - URL ends not with valid filename'
).to.be.false
})