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: 0 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -349,7 +349,6 @@ The following tests are not yet implemented and therefore missing:
- Recommended Test 6.2.38
- Recommended Test 6.2.39
- Recommended Test 6.2.40
- Recommended Test 6.2.41
- Recommended Test 6.2.42
- Recommended Test 6.2.43
- Recommended Test 6.2.44
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 @@ -32,3 +32,4 @@ export { recommendedTest_6_2_27 } from './recommendedTests/recommendedTest_6_2_2
export { recommendedTest_6_2_28 } from './recommendedTests/recommendedTest_6_2_28.js'
export { recommendedTest_6_2_29 } from './recommendedTests/recommendedTest_6_2_29.js'
export { recommendedTest_6_2_38 } from './recommendedTests/recommendedTest_6_2_38.js'
export { recommendedTest_6_2_41 } from './recommendedTests/recommendedTest_6_2_41.js'
132 changes: 132 additions & 0 deletions csaf_2_1/recommendedTests/recommendedTest_6_2_41.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import Ajv from 'ajv/dist/jtd.js'
import { compareZonedDateTimes } from '../dateHelper.js'

const ajv = new Ajv()

const inputSchema = /** @type {const} */ ({
additionalProperties: true,
properties: {
document: {
additionalProperties: true,
properties: {
tracking: {
additionalProperties: true,
properties: {
revision_history: {
elements: {
additionalProperties: true,
optionalProperties: {
date: { type: 'string' },
},
},
},
status: { type: 'string' },
},
},
},
},
vulnerabilities: {
elements: {
additionalProperties: true,
optionalProperties: {
metrics: {
elements: {
additionalProperties: true,
optionalProperties: {
content: {
additionalProperties: true,
optionalProperties: {
epss: {
additionalProperties: true,
optionalProperties: {
timestamp: { type: 'string' },
},
},
},
},
},
},
},
},
},
},
},
})

const validate = ajv.compile(inputSchema)

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

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

const status = doc.document.tracking.status
if (status !== 'final' && status !== 'interim') {
return context
}

const newestRevisionHistoryItem = doc.document.tracking.revision_history
.filter((item) => item.date != null)
.sort((a, z) =>
compareZonedDateTimes(
/** @type {string} */ (z.date),
/** @type {string} */ (a.date)
)
)[0]

if (!newestRevisionHistoryItem || !newestRevisionHistoryItem.date) {
return context
}

doc.vulnerabilities?.forEach((vulnerability, vulnerabilityIndex) => {
const metrics = vulnerability.metrics || []
const newestEpss = metrics
.map((m) => m.content?.epss)
.filter((item) => item?.timestamp != null)
.sort((a, z) => {
if (!a || !z) return 0
return compareZonedDateTimes(
/** @type {string} */ (z.timestamp),
/** @type {string} */ (a.timestamp)
)
})[0]

if (
!newestEpss ||
!newestEpss.timestamp ||
!newestRevisionHistoryItem ||
!newestRevisionHistoryItem.date
) {
return context
}

const revisionDateObj = new Date(newestRevisionHistoryItem.date)
const epssDateObj = new Date(newestEpss.timestamp)

// difference in milliseconds
const diffInMs = revisionDateObj.getTime() - epssDateObj.getTime()
// 15 days in milliseconds
const fifteenDaysMs = 15 * 24 * 60 * 60 * 1000

if (diffInMs > fifteenDaysMs) {
context.warnings.push({
instancePath: `/vulnerabilities/${vulnerabilityIndex}/metrics/content/epss/timestamp`,
message:
`the status is ${status}, but the EPSS "timestamp:" ${newestEpss.timestamp} is more than 15 days ` +
`older than the newest "revision history date:" ${newestRevisionHistoryItem.date}`,
})
}
})

return context
}
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.39.3',
'6.2.39.4',
'6.2.40',
'6.2.41',
'6.2.42',
'6.2.43',
'6.2.44',
Expand Down
90 changes: 90 additions & 0 deletions tests/csaf_2_1/recommendedTest_6_2_41.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import assert from 'node:assert'
import { recommendedTest_6_2_41 } from '../../csaf_2_1/recommendedTests.js'

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

it('skips status draft', function () {
assert.equal(
recommendedTest_6_2_41({
document: {
tracking: {
revision_history: [],
status: 'draft',
},
},
vulnerabilities: [],
}).warnings.length,
0
)
})

it('skips empty revision_history object', function () {
assert.equal(
recommendedTest_6_2_41({
document: {
tracking: {
revision_history: [
{}, // should be ignored
],
status: 'final',
},
},
vulnerabilities: [],
}).warnings.length,
0
)
})

it('Skips vulnerabilities without metrics object', function () {
assert.equal(
recommendedTest_6_2_41({
document: {
tracking: {
revision_history: [{ date: '2024-01-24T10:00:00.000Z' }],
status: 'final',
},
},
vulnerabilities: [{}],
}).warnings.length,
0
)
})

it('skips empty epss object', function () {
assert.equal(
recommendedTest_6_2_41({
document: {
tracking: {
revision_history: [{ date: '2024-01-24T10:00:00.000Z' }],
status: 'final',
},
},
vulnerabilities: [
{
metrics: [
{
content: {
epss: {}, // should be ignored
},
},
{
content: {
epss: {
timestamp: '2024-01-01T10:00:00.000Z',
},
},
},
],
},
],
}).warnings.length,
1
)
})
})