-
Couldn't load subscription status.
- Fork 9
feat(CSAF2.1): #447 add recommended test test 6.2.47 #458
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,172 @@ | ||||||||||
| import Ajv from 'ajv/dist/jtd.js' | ||||||||||
|
|
||||||||||
| const ajv = new Ajv() | ||||||||||
|
|
||||||||||
| /** @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 */ | ||||||||||
|
|
||||||||||
| const jtdAjv = new Ajv() | ||||||||||
|
|
||||||||||
| const inputSchema = /** @type {const} */ ({ | ||||||||||
| additionalProperties: true, | ||||||||||
| properties: { | ||||||||||
| document: { | ||||||||||
| additionalProperties: true, | ||||||||||
| optionalProperties: { | ||||||||||
| references: { | ||||||||||
| elements: { | ||||||||||
| additionalProperties: true, | ||||||||||
| properties: { | ||||||||||
| 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', | ||||||||||
| }, | ||||||||||
| }, | ||||||||||
| }, | ||||||||||
| }, | ||||||||||
| }, | ||||||||||
| }, | ||||||||||
| }, | ||||||||||
| }, | ||||||||||
| }, | ||||||||||
| }, | ||||||||||
| }) | ||||||||||
|
|
||||||||||
| /** @typedef {{ url: string; category: string}} Reference */ | ||||||||||
|
|
||||||||||
| const referenceSchema = /** @type {const} */ ({ | ||||||||||
| additionalProperties: true, | ||||||||||
| properties: { | ||||||||||
| category: { type: 'string' }, | ||||||||||
| url: { type: 'string' }, | ||||||||||
| }, | ||||||||||
| }) | ||||||||||
|
|
||||||||||
| const validate = jtdAjv.compile(inputSchema) | ||||||||||
| const validateReference = ajv.compile(referenceSchema) | ||||||||||
|
|
||||||||||
| /** | ||||||||||
| * Get the canonical url from the document | ||||||||||
| * @return {string} canonical url or empty when no canonical url exists | ||||||||||
| * @param {Array<Reference> | 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) => | ||||||||||
| validateReference(reference) && | ||||||||||
| reference.category === 'self' && | ||||||||||
| reference.url.startsWith('https://') && | ||||||||||
| reference.url.endsWith( | ||||||||||
| trackingId.toLowerCase().replace(/[^+\-a-z0-9]+/g, '_') + '.json' | ||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Actually, there should be a library function that computes the filename. We need that in several places (and don't want to change the all place once that computation is updated... There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe, that should even apply to the whole canonical check... |
||||||||||
| ) | ||||||||||
| ) | ||||||||||
|
|
||||||||||
| // When we find a matching reference, we know it has the url property | ||||||||||
| // because validateReference 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 hasServerRatingAndNoSource(metric, canonicalURL) { | ||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||
| 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) { | ||||||||||
| /** @type {Array<{ message: string; instancePath: string }>} */ | ||||||||||
| const warnings = [] | ||||||||||
| const context = { warnings } | ||||||||||
|
|
||||||||||
| if (!validate(doc)) { | ||||||||||
| return context | ||||||||||
| } | ||||||||||
|
Comment on lines
+132
to
+138
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Was that intentional to write it differently than the other tests? |
||||||||||
|
|
||||||||||
| /** @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) => | ||||||||||
| hasServerRatingAndNoSource(metric, canonicalURL) | ||||||||||
| ? `/vulnerabilities/${vulnerabilityIndex}/metrics/${metricIndex}/content/qualitative_severity_rating` | ||||||||||
| : null | ||||||||||
| ) | ||||||||||
| .filter((path) => path !== null) | ||||||||||
|
|
||||||||||
| if (!!invalidPaths) { | ||||||||||
| invalidPaths.forEach((path) => { | ||||||||||
| context.warnings.push({ | ||||||||||
| message: | ||||||||||
| 'the metric has a qualitative severity rating and no source property' + | ||||||||||
| ' or a source property that ist equal to the canonical URL', | ||||||||||
|
Comment on lines
+163
to
+164
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||
| instancePath: path, | ||||||||||
| }) | ||||||||||
| }) | ||||||||||
| } | ||||||||||
| }) | ||||||||||
|
|
||||||||||
| return context | ||||||||||
| } | ||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -55,7 +55,6 @@ const excluded = [ | |
| '6.2.44', | ||
| '6.2.45', | ||
| '6.2.46', | ||
| '6.2.47', | ||
| '6.3.12', | ||
| '6.3.13', | ||
| '6.3.14', | ||
|
|
||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please include the case from above (regarding empty reference item object), if applicable. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| 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 | ||
| ) | ||
| }) | ||
| }) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shouldn't that be
optionalPropertiesas well? Otherwise, the validation would fail for: