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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -358,7 +358,6 @@ The following tests are not yet implemented and therefore missing:
**Recommended Tests**

- Recommended Test 6.2.20
- Recommended Test 6.2.24
- Recommended Test 6.2.31
- Recommended Test 6.2.38
- Recommended Test 6.2.39.1
Expand Down Expand Up @@ -502,6 +501,7 @@ export const recommendedTest_6_2_19: DocumentTest
export const recommendedTest_6_2_21: DocumentTest
export const recommendedTest_6_2_22: DocumentTest
export const recommendedTest_6_2_23: DocumentTest
export const recommendedTest_6_2_24: DocumentTest
export const recommendedTest_6_2_25: DocumentTest
export const recommendedTest_6_2_26: DocumentTest
export const recommendedTest_6_2_27: DocumentTest
Expand Down
16 changes: 16 additions & 0 deletions csaf_2_1/dateHelper.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,22 @@ export const compareZonedDateTimes = (a, b) => {
}
}

/**
* Convert a local date in string representation, i.e. a date accurate to the
* day like 'yyyy-mm-dd', to a zoned instant string at the start of that day
* in UTC. This is needed because `compareZonedDateTimes` relies on
* `Temporal.Instant.from`, which cannot parse plain (offset-less) dates.
*
* @param {string} localDateString
* @returns {string} the UTC instant representing the start of the given local date
*/
export const localDateToTimeZonedDate = (localDateString) => {
return Temporal.PlainDate.from(localDateString)
.toZonedDateTime('UTC')
.toInstant()
.toString()
}

/**
* This regex validates a date against RFC 3339 section 5.6.
* See: https://datatracker.ietf.org/doc/html/rfc3339#section-5.6
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 @@ -28,6 +28,7 @@ export { recommendedTest_6_2_19 } from './recommendedTests/recommendedTest_6_2_1
export { recommendedTest_6_2_21 } from './recommendedTests/recommendedTest_6_2_21.js'
export { recommendedTest_6_2_22 } from './recommendedTests/recommendedTest_6_2_22.js'
export { recommendedTest_6_2_23 } from './recommendedTests/recommendedTest_6_2_23.js'
export { recommendedTest_6_2_24 } from './recommendedTests/recommendedTest_6_2_24.js'
export { recommendedTest_6_2_25 } from './recommendedTests/recommendedTest_6_2_25.js'
export { recommendedTest_6_2_26 } from './recommendedTests/recommendedTest_6_2_26.js'
export { recommendedTest_6_2_27 } from './recommendedTests/recommendedTest_6_2_27.js'
Expand Down
144 changes: 144 additions & 0 deletions csaf_2_1/recommendedTests/recommendedTest_6_2_24.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
import { Ajv } from 'ajv/dist/jtd.js'
import { cwecMap } from '../../lib/cwec.js'
import {
compareZonedDateTimes,
localDateToTimeZonedDate,
} from '../dateHelper.js'

const ajv = new Ajv()

/*
This is the jtd schema that needs to match the input document so that the
test is activated. If this schema doesn't match it normally means that the input
document does not validate against the csaf json schema or optional fields that
the test checks are not present.
*/
const inputSchema = /** @type {const} */ ({
additionalProperties: true,
properties: {
document: {
additionalProperties: true,
properties: {
tracking: {
additionalProperties: true,
properties: {
current_release_date: { type: 'string' },
},
},
},
},
vulnerabilities: {
elements: {
additionalProperties: true,
optionalProperties: {
cwes: {
elements: {
additionalProperties: true,
properties: {},
},
},
},
},
},
},
})

const validateInput = ajv.compile(inputSchema)

const cweSchema = /** @type {const} */ ({
additionalProperties: true,
properties: {
id: { type: 'string' },
version: { type: 'string' },
name: { type: 'string' },
},
})

const validateCWE = ajv.compile(cweSchema)

/** @type {Promise<Array<[string, string]>> | undefined} */
let sortedCwecReleases

/**
* Loads the release dates of all known CWE versions once (loading all catalogues is expensive)
* and returns them sorted descending by date (newest first). Versions with the same date
* keep the order of the `cwecMap`, which lists newer versions first.
*
* @returns {Promise<Array<[string, string]>>}
*/
function getSortedCwecReleases() {
sortedCwecReleases ??= (async () => {
/** @type {Array<[string, string]>} */
const releases = []
for (const [version, load] of cwecMap) {
const date = (await load()).default.date
releases.push([version, localDateToTimeZonedDate(date)])
}
return releases.sort((a, b) => compareZonedDateTimes(b[1], a[1]))
})()
return sortedCwecReleases
}

/**
* This implements the recommended test 6.2.24 of the CSAF 2.1 standard.
*
* @param {any} doc
*/
export async function recommendedTest_6_2_24(doc) {
/** @type {Array<{ message: string; instancePath: string }>} */
const warnings = []
const context = { warnings }

if (!validateInput(doc)) {
return context
}

const currentReleaseDate = doc.document.tracking.current_release_date
const cwecArray = await getSortedCwecReleases()

for (let i = 0; i < doc.vulnerabilities.length; ++i) {
const vulnerability = doc.vulnerabilities[i]
if (vulnerability.cwes) {
for (let j = 0; j < vulnerability.cwes.length; ++j) {
const cwe = vulnerability.cwes.at(j)
if (validateCWE(cwe)) {
const idx = cwecArray.findIndex((row) => row[0] === cwe.version)
if (idx === -1) continue
const zonedCweReleaseDate = cwecArray[idx][1]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this might throw an exception if the given cwe version is not found in the catalogue since idx is then -1 which here evaluates to undefined[1] which throws.

// Case 1: Check if the used cwe version was already published
// at the current release date of the CSAF document
if (
compareZonedDateTimes(currentReleaseDate, zonedCweReleaseDate) < 0
) {
context.warnings.push({
instancePath: `/vulnerabilities/${i}/cwes/${j}/version`,
message: `at release date ${currentReleaseDate} the used version ${cwe.version} was not published yet`,
})
// Case 2: Check if a newer cwe version than the used one had already been published
// at the current release date of the CSAF document
} else {
for (let k = idx - 1; k >= 0; k--) {
const zonedCweReleaseDateNewer = cwecArray[k][1]

if (
compareZonedDateTimes(
currentReleaseDate,
zonedCweReleaseDateNewer
) > 0
) {
context.warnings.push({
instancePath: `/vulnerabilities/${i}/cwes/${j}/version`,
message:
`the used cwe version ${cwe.version} ` +
`was not the newest one available at release date ${currentReleaseDate}`,
})
break
}
}
}
}
}
}
}
return context
}
2 changes: 1 addition & 1 deletion lib/cwec.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

/**
* @typedef {object} CweCatalogue
* @property {string} [date]
* @property {string} date
* @property {CweWeakness[]} weaknesses
*/

Expand Down
1 change: 0 additions & 1 deletion tests/csaf_2_1/oasis.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ const excluded = [
'6.1.60.2',
'6.1.60.3',
'6.2.20',
'6.2.24',
'6.2.31',
'6.2.39.1',
'6.2.46',
Expand Down
91 changes: 91 additions & 0 deletions tests/csaf_2_1/recommendedTest_6_2_24.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { recommendedTest_6_2_24 } from '../../csaf_2_1/recommendedTests.js'

describe('recommendedTest_6_2_24', function () {
it('only runs on relevant documents', async function () {
expect(
(await recommendedTest_6_2_24({ vulnerabilities: 'mydoc' })).warnings
.length
).toBe(0)
})

it('skips cwe entries with an unknown version (not in cwecMap)', async function () {
expect(
(
await recommendedTest_6_2_24({
document: {
tracking: {
current_release_date: '2024-01-21T10:00:00.000Z',
},
},
vulnerabilities: [
{
cwes: [
{
id: 'CWE-256',
name: 'Plaintext Storage of a Password',
version: '99.99',
},
],
},
],
})
).warnings.length
).toBe(0)
})

it('skips empty objects', async function () {
expect(
(
await recommendedTest_6_2_24({
document: {
tracking: {
current_release_date: '2024-01-21T10:00:00.000Z',
},
},
vulnerabilities: [
{
cwes: [
{
id: 'CWE-256',
name: 'Plaintext Storage of a Password',
version: '4.12',
},
],
},
{}, // should be ignored
],
})
).warnings.length
).toBe(1)
})

it('skips cwe entries not matching the cwe schema but checks valid ones', async function () {
const result = await recommendedTest_6_2_24({
document: {
tracking: { current_release_date: '2024-01-21T10:00:00.000Z' },
},
vulnerabilities: [
{
cwes: [
{ id: 'CWE-256', name: 'Plaintext Storage of a Password' }, // version missing
{
id: 'CWE-256',
name: 'Plaintext Storage of a Password',
version: 4.12,
}, // wrong type
'CWE-256', // not an object
{
id: 'CWE-256',
name: 'Plaintext Storage of a Password',
version: '4.12',
},
],
},
],
})

expect(result.warnings[0].instancePath).to.equal(
'/vulnerabilities/0/cwes/3/version'
)
})
})
Loading