Skip to content
Closed
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 @@ -462,6 +462,7 @@ export const recommendedTest_6_2_39_2: DocumentTest
export const recommendedTest_6_2_40: DocumentTest
export const recommendedTest_6_2_41: 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 @@ -39,3 +39,4 @@ export { recommendedTest_6_2_39_2 } from './recommendedTests/recommendedTest_6_2
export { recommendedTest_6_2_40 } from './recommendedTests/recommendedTest_6_2_40.js'
export { recommendedTest_6_2_41 } from './recommendedTests/recommendedTest_6_2_41.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'
163 changes: 163 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,163 @@
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 {string | null}
*/
function checkSeverityRatingAndNoSource(metric, canonicalURL) {
if (metric?.content?.qualitative_severity_rating) {
if (!metric.source) {
return 'as no "source" is given'
} else if (metric.source === canonicalURL) {
return 'as the "source" property equals to the canonical URL'
} else {
return null
}
} else {
return null
}
}

/**
* 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<{path: string, message: string}> | undefined} */
const invalidPaths = metrics
?.map((metric, metricIndex) => {
const message = checkSeverityRatingAndNoSource(metric, canonicalURL)
return message != null
? {
path: `/vulnerabilities/${vulnerabilityIndex}/metrics/${metricIndex}/content/qualitative_severity_rating`,
message: message,
}
: null
})
.filter((path) => path !== null)

invalidPaths?.forEach((path) => {
ctx.warnings.push({
message: `a qualitative severity rating is used by the issuing party (${path.message})`,
instancePath: path.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
54 changes: 54 additions & 0 deletions lib/shared/urlHelper.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
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)

/**
* Convert the tracking id to apply the csaf filename conventions
* - 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)
* @param {string} trackingId
* @return {string}
*/
export function convertTrackingIdToFilename(trackingId) {
return trackingId.toLowerCase().replace(/[^+\-a-z0-9]+/g, '_')
}

/**
* 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(convertTrackingIdToFilename(trackingId) + '.json')
)
}
Loading