From 89d5a87c50473d318f618f1400046106721a553e Mon Sep 17 00:00:00 2001 From: RafaelGSS Date: Tue, 21 Jul 2026 17:03:14 -0300 Subject: [PATCH 1/2] fix: handle affectedVersions as object for post-release Refs: https://github.com/nodejs-private/nodejs.org-private/pull/567#discussion_r3625265939 --- lib/security_blog.js | 41 +++++++++++++++++++++++++++++- test/unit/security_release.test.js | 39 ++++++++++++++++++++++++++++ 2 files changed, 79 insertions(+), 1 deletion(-) diff --git a/lib/security_blog.js b/lib/security_blog.js index 15e4a724..d2fff268 100644 --- a/lib/security_blog.js +++ b/lib/security_blog.js @@ -287,13 +287,52 @@ export default class SecurityBlog extends SecurityRelease { 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( diff --git a/test/unit/security_release.test.js b/test/unit/security_release.test.js index ae13364a..6b8350f4 100644 --- a/test/unit/security_release.test.js +++ b/test/unit/security_release.test.js @@ -300,4 +300,43 @@ describe('security_blog: post-release severity wording', () => { assert.throws(() => blog.getVulnerabilities(content), /severity\.rating not found for report 1/); }); + + 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/); + }); }); From 470c0214a4da62c1265d6f765f6a02ca0f18dfea Mon Sep 17 00:00:00 2001 From: RafaelGSS Date: Tue, 21 Jul 2026 17:22:31 -0300 Subject: [PATCH 2/2] feat: sort by severity on post-release --- lib/security_blog.js | 12 +++++++++++- test/unit/security_release.test.js | 25 +++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/lib/security_blog.js b/lib/security_blog.js index d2fff268..ac700e27 100644 --- a/lib/security_blog.js +++ b/lib/security_blog.js @@ -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; @@ -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(', '); @@ -282,6 +285,13 @@ 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 ''; diff --git a/test/unit/security_release.test.js b/test/unit/security_release.test.js index 6b8350f4..4236c3f7 100644 --- a/test/unit/security_release.test.js +++ b/test/unit/security_release.test.js @@ -301,6 +301,31 @@ 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();