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
53 changes: 51 additions & 2 deletions lib/security_blog.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import {
import auth from './auth.js';
import Request from './request.js';

const SEVERITY_ORDER = ['critical', 'high', 'medium', 'low'];

export default class SecurityBlog extends SecurityRelease {
req;

Expand Down Expand Up @@ -256,7 +258,8 @@ export default class SecurityBlog extends SecurityRelease {
}

async getReportsTemplate(content) {
const reports = content.reports;
const reports = [...content.reports].sort((a, b) =>
this.getSeverityOrder(a) - this.getSeverityOrder(b));
let template = '';
for (const report of reports) {
const cveId = report.cveIds?.join(', ');
Expand All @@ -282,18 +285,64 @@ export default class SecurityBlog extends SecurityRelease {
return template;
}

getSeverityOrder(report) {
const rating = report.severity?.rating?.toLowerCase();
const index = SEVERITY_ORDER.indexOf(rating);
if (index === -1) return SEVERITY_ORDER.length;
return index;
}

getDependencyUpdatesTemplate(dependencyUpdates) {
if (typeof dependencyUpdates !== 'object') return '';
if (Object.keys(dependencyUpdates).length === 0) return '';
let template = '\nThis security release includes the following dependency' +
' updates to address public vulnerabilities:\n';
if (Array.isArray(dependencyUpdates)) {
for (const dependency of dependencyUpdates) {
const title = this.formatDependencyUpdateTitle(dependency);
const releaseLines = getAffectedVersionLines(dependency.affectedVersions);
template += `- ${title} on ${releaseLines.join(', ')}\n`;
}
return template;
}

for (const [dependency, { versions, affectedVersions }] of Object.entries(dependencyUpdates)) {
const releaseLines = getAffectedVersionLines(affectedVersions);
template += `- ${dependency} (${versions.join(', ')}) on ${releaseLines.join(', ')}\n`;
const formattedVersions = this.formatDependencyVersions(versions);
const versionText = formattedVersions ? ` (${formattedVersions})` : '';
template += `- ${dependency}${versionText} on ${releaseLines.join(', ')}\n`;
}
return template;
}

formatDependencyUpdateVersion(version) {
if (typeof version === 'string' || typeof version === 'number') {
return String(version);
}

if (!version || typeof version !== 'object') return '';

return version.version || version.to || version.patched || version.name ||
this.formatDependencyUpdateTitle(version) || version.url || JSON.stringify(version);
}

formatDependencyVersions(versions) {
if (!Array.isArray(versions)) return '';
return versions
.map((version) => this.formatDependencyUpdateVersion(version))
.filter(Boolean)
.join(', ');
}

formatDependencyUpdateTitle(dependency) {
if (!dependency || typeof dependency !== 'object') return '';
if (!dependency.title) return dependency.name ?? '';

const titleSeparator = dependency.title.indexOf(':');
if (titleSeparator === -1) return dependency.title;
return dependency.title.substring(titleSeparator + ':'.length).trim();
}

async getDownloadsTemplate(affectedVersions) {
let template = '';
const versionsToBeReleased = (await nv('supported')).filter(
Expand Down
64 changes: 64 additions & 0 deletions test/unit/security_release.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -300,4 +300,68 @@ describe('security_blog: post-release severity wording', () => {

assert.throws(() => blog.getVulnerabilities(content), /severity\.rating not found for report 1/);
});

it('sorts post-release reports by severity', async() => {
const blog = new SecurityBlog();
const createReport = (title, rating) => ({
title,
cveIds: [`CVE-2026-${title}`],
severity: { rating },
summary: 'summary',
affectedVersions: ['24.x'],
patchAuthors: ['nodejs'],
reporter: 'reporter',
link: `https://hackerone.com/reports/${title}`
});

const output = await blog.getReportsTemplate({
reports: [
createReport('low-report', 'low'),
createReport('high-report', 'high'),
createReport('medium-report', 'medium')
]
});

assert.ok(output.indexOf('## high-report') < output.indexOf('## medium-report'));
assert.ok(output.indexOf('## medium-report') < output.indexOf('## low-report'));
});

it('formats dependency update object versions', () => {
const blog = new SecurityBlog();

const output = blog.getDependencyUpdatesTemplate({
undici: {
versions: [
{ version: '6.22.0' },
{ version: '7.16.0' }
],
affectedVersions: ['22.x', '24.x', '26.x']
},
llhttp: {
versions: ['9.3.0'],
affectedVersions: {
'24.x': { affected: '<=24.4.0', patched: '24.4.1' },
'22.x': { affected: '<=22.17.0', patched: '22.17.1' }
}
}
});

assert.match(output, /- undici \(6\.22\.0, 7\.16\.0\) on 22\.x, 24\.x, 26\.x/);
assert.match(output, /- llhttp \(9\.3\.0\) on 24\.x, 22\.x/);
assert.doesNotMatch(output, /\[object Object\]/);
});

it('formats legacy dependency update arrays', () => {
const blog = new SecurityBlog();

const output = blog.getDependencyUpdatesTemplate([
{
name: 'undici',
title: 'deps: update undici to 6.22.0',
affectedVersions: ['22.x', '24.x']
}
]);

assert.match(output, /- update undici to 6\.22\.0 on 22\.x, 24\.x/);
});
});
Loading