Skip to content

Commit c263c2a

Browse files
authored
fix: handle affectedVersions as object for post-release (#1122)
* fix: handle affectedVersions as object for post-release Refs: nodejs-private/nodejs.org-private#567 (comment) Signed-off-by: RafaelGSS <rafael.nunu@hotmail.com> * feat: sort by severity on post-release Signed-off-by: RafaelGSS <rafael.nunu@hotmail.com> --------- Signed-off-by: RafaelGSS <rafael.nunu@hotmail.com>
1 parent 5b3296a commit c263c2a

2 files changed

Lines changed: 117 additions & 8 deletions

File tree

lib/security_blog.js

Lines changed: 53 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ import {
1414
import auth from './auth.js';
1515
import Request from './request.js';
1616

17+
const SEVERITY_ORDER = ['critical', 'high', 'medium', 'low'];
18+
1719
export default class SecurityBlog extends SecurityRelease {
1820
req;
1921

@@ -256,7 +258,8 @@ export default class SecurityBlog extends SecurityRelease {
256258
}
257259

258260
async getReportsTemplate(content) {
259-
const reports = content.reports;
261+
const reports = [...content.reports].sort((a, b) =>
262+
this.getSeverityOrder(a) - this.getSeverityOrder(b));
260263
let template = '';
261264
for (const report of reports) {
262265
const cveId = report.cveIds?.join(', ');
@@ -282,22 +285,64 @@ export default class SecurityBlog extends SecurityRelease {
282285
return template;
283286
}
284287

288+
getSeverityOrder(report) {
289+
const rating = report.severity?.rating?.toLowerCase();
290+
const index = SEVERITY_ORDER.indexOf(rating);
291+
if (index === -1) return SEVERITY_ORDER.length;
292+
return index;
293+
}
294+
285295
getDependencyUpdatesTemplate(dependencyUpdates) {
286296
if (typeof dependencyUpdates !== 'object') return '';
287297
if (Object.keys(dependencyUpdates).length === 0) return '';
288298
let template = '\nThis security release includes the following dependency' +
289299
' updates to address public vulnerabilities:\n';
290-
for (const [dependency, entry] of Object.entries(dependencyUpdates)) {
291-
const releaseLines = getAffectedVersionLines(entry.affectedVersions);
292-
const versions = Array.isArray(entry.versions)
293-
? entry.versions.map((v) => (typeof v === 'string' ? v : v.version))
294-
: [];
295-
const versionSuffix = versions.length ? ` (${versions.join(', ')})` : '';
296-
template += `- ${dependency}${versionSuffix} on ${releaseLines.join(', ')}\n`;
300+
if (Array.isArray(dependencyUpdates)) {
301+
for (const dependency of dependencyUpdates) {
302+
const title = this.formatDependencyUpdateTitle(dependency);
303+
const releaseLines = getAffectedVersionLines(dependency.affectedVersions);
304+
template += `- ${title} on ${releaseLines.join(', ')}\n`;
305+
}
306+
return template;
307+
}
308+
309+
for (const [dependency, { versions, affectedVersions }] of Object.entries(dependencyUpdates)) {
310+
const releaseLines = getAffectedVersionLines(affectedVersions);
311+
const formattedVersions = this.formatDependencyVersions(versions);
312+
const versionText = formattedVersions ? ` (${formattedVersions})` : '';
313+
template += `- ${dependency}${versionText} on ${releaseLines.join(', ')}\n`;
297314
}
298315
return template;
299316
}
300317

318+
formatDependencyUpdateVersion(version) {
319+
if (typeof version === 'string' || typeof version === 'number') {
320+
return String(version);
321+
}
322+
323+
if (!version || typeof version !== 'object') return '';
324+
325+
return version.version || version.to || version.patched || version.name ||
326+
this.formatDependencyUpdateTitle(version) || version.url || JSON.stringify(version);
327+
}
328+
329+
formatDependencyVersions(versions) {
330+
if (!Array.isArray(versions)) return '';
331+
return versions
332+
.map((version) => this.formatDependencyUpdateVersion(version))
333+
.filter(Boolean)
334+
.join(', ');
335+
}
336+
337+
formatDependencyUpdateTitle(dependency) {
338+
if (!dependency || typeof dependency !== 'object') return '';
339+
if (!dependency.title) return dependency.name ?? '';
340+
341+
const titleSeparator = dependency.title.indexOf(':');
342+
if (titleSeparator === -1) return dependency.title;
343+
return dependency.title.substring(titleSeparator + ':'.length).trim();
344+
}
345+
301346
async getDownloadsTemplate(affectedVersions) {
302347
let template = '';
303348
const versionsToBeReleased = (await nv('supported')).filter(

test/unit/security_release.test.js

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -816,6 +816,70 @@ describe('security_blog: post-release severity wording', () => {
816816

817817
assert.throws(() => blog.getVulnerabilities(content), /severity\.rating not found for report 1/);
818818
});
819+
820+
it('sorts post-release reports by severity', async() => {
821+
const blog = new SecurityBlog();
822+
const createReport = (title, rating) => ({
823+
title,
824+
cveIds: [`CVE-2026-${title}`],
825+
severity: { rating },
826+
summary: 'summary',
827+
affectedVersions: ['24.x'],
828+
patchAuthors: ['nodejs'],
829+
reporter: 'reporter',
830+
link: `https://hackerone.com/reports/${title}`
831+
});
832+
833+
const output = await blog.getReportsTemplate({
834+
reports: [
835+
createReport('low-report', 'low'),
836+
createReport('high-report', 'high'),
837+
createReport('medium-report', 'medium')
838+
]
839+
});
840+
841+
assert.ok(output.indexOf('## high-report') < output.indexOf('## medium-report'));
842+
assert.ok(output.indexOf('## medium-report') < output.indexOf('## low-report'));
843+
});
844+
845+
it('formats dependency update object versions', () => {
846+
const blog = new SecurityBlog();
847+
848+
const output = blog.getDependencyUpdatesTemplate({
849+
undici: {
850+
versions: [
851+
{ version: '6.22.0' },
852+
{ version: '7.16.0' }
853+
],
854+
affectedVersions: ['22.x', '24.x', '26.x']
855+
},
856+
llhttp: {
857+
versions: ['9.3.0'],
858+
affectedVersions: {
859+
'24.x': { affected: '<=24.4.0', patched: '24.4.1' },
860+
'22.x': { affected: '<=22.17.0', patched: '22.17.1' }
861+
}
862+
}
863+
});
864+
865+
assert.match(output, /- undici \(6\.22\.0, 7\.16\.0\) on 22\.x, 24\.x, 26\.x/);
866+
assert.match(output, /- llhttp \(9\.3\.0\) on 24\.x, 22\.x/);
867+
assert.doesNotMatch(output, /\[object Object\]/);
868+
});
869+
870+
it('formats legacy dependency update arrays', () => {
871+
const blog = new SecurityBlog();
872+
873+
const output = blog.getDependencyUpdatesTemplate([
874+
{
875+
name: 'undici',
876+
title: 'deps: update undici to 6.22.0',
877+
affectedVersions: ['22.x', '24.x']
878+
}
879+
]);
880+
881+
assert.match(output, /- update undici to 6\.22\.0 on 22\.x, 24\.x/);
882+
});
819883
});
820884

821885
describe('security_blog: getDependencyUpdatesTemplate', () => {

0 commit comments

Comments
 (0)