From 6b726e9b988720d3e1e18fb5ef25011158d71d7d Mon Sep 17 00:00:00 2001 From: RafaelGSS Date: Tue, 26 May 2026 09:50:55 -0300 Subject: [PATCH 1/4] feat: drop number of vulnerabilities on --pre-release When we announce a security release, we typically say we'll be fixing X High, X Medium, and so on. That policy was set before the AI era, when reports weren't as frequent. Signed-off-by: RafaelGSS --- lib/security-release/security-release.js | 34 ++++++++++ lib/security_blog.js | 37 +++++------ test/unit/security_release.test.js | 83 ++++++++++++++++++++++++ 3 files changed, 132 insertions(+), 22 deletions(-) create mode 100644 test/unit/security_release.test.js diff --git a/lib/security-release/security-release.js b/lib/security-release/security-release.js index e662910a..a2af628b 100644 --- a/lib/security-release/security-release.js +++ b/lib/security-release/security-release.js @@ -11,6 +11,20 @@ export const NEXT_SECURITY_RELEASE_REPOSITORY = { repo: 'security-release' }; +const SEVERITY_RANK = { + critical: 0, + high: 1, + medium: 2, + low: 3 +}; + +const SEVERITY_LABEL = { + critical: 'CRITICAL', + high: 'HIGH', + medium: 'MEDIUM', + low: 'LOW' +}; + export const PLACEHOLDERS = { releaseDate: '%RELEASE_DATE%', vulnerabilitiesPRURL: '%VULNERABILITIES_PR_URL%', @@ -130,6 +144,26 @@ export function formatDateToYYYYMMDD(date) { return `${year}/${month}/${day}`; } +export function getHighestSeverity(reports) { + let highestSeverity = ''; + + for (const report of reports) { + const rating = report.severity.rating.toLowerCase(); + const currentRank = SEVERITY_RANK[rating] ?? Number.MAX_SAFE_INTEGER; + const highestRank = SEVERITY_RANK[highestSeverity] ?? Number.MAX_SAFE_INTEGER; + + if (!highestSeverity || currentRank < highestRank) { + highestSeverity = rating; + } + } + + return SEVERITY_LABEL[highestSeverity] ?? highestSeverity.toUpperCase(); +} + +export function getHighestSeverityAnnouncement(reports) { + return `The highest severity issue fixed in this release is ${getHighestSeverity(reports)}.`; +} + export function promptDependencies(cli) { return cli.prompt('Enter the link to the dependency update PR (leave empty to exit): ', { defaultAnswer: '', diff --git a/lib/security_blog.js b/lib/security_blog.js index 6ecd0e6d..305d1212 100644 --- a/lib/security_blog.js +++ b/lib/security_blog.js @@ -1,6 +1,5 @@ import fs from 'node:fs'; import path from 'node:path'; -import _ from 'lodash'; import nv from '@pkgjs/nv'; import { PLACEHOLDERS, @@ -8,6 +7,8 @@ import { validateDate, SecurityRelease, commitAndPushVulnerabilitiesJSON, + getHighestSeverity, + getHighestSeverityAnnouncement, } from './security-release/security-release.js'; import auth from './auth.js'; import Request from './request.js'; @@ -323,6 +324,11 @@ export default class SecurityBlog extends SecurityRelease { getImpact(content) { const impact = new Map(); for (const report of content.reports) { + if (!report.severity?.rating) { + this.cli.error(`severity.rating not found for report ${report.id}.`); + process.exit(1); + } + for (const version of report.affectedVersions) { if (!impact.has(version)) impact.set(version, []); impact.get(version).push(report); @@ -332,22 +338,8 @@ export default class SecurityBlog extends SecurityRelease { const result = Array.from(impact.entries()) .sort(([a], [b]) => b.localeCompare(a)) // DESC .map(([version, reports]) => { - const severityCount = new Map(); - - for (const report of reports) { - const rating = report.severity.rating?.toLowerCase(); - if (!rating) { - this.cli.error(`severity.rating not found for report ${report.id}.`); - process.exit(1); - } - severityCount.set(rating, (severityCount.get(rating) || 0) + 1); - } - - const groupedByRating = Array.from(severityCount.entries()) - .map(([rating, count]) => `${count} ${rating} severity issues`) - .join(', '); - - return `The ${version} release line of Node.js is vulnerable to ${groupedByRating}.`; + return `The highest severity issue fixed in the ${version} release line is ` + + `${getHighestSeverity(reports)}.`; }) .join('\n'); @@ -355,12 +347,13 @@ export default class SecurityBlog extends SecurityRelease { } getVulnerabilities(content) { - const grouped = _.groupBy(content.reports, 'severity.rating'); - const text = []; - for (const [key, value] of Object.entries(grouped)) { - text.push(`- ${value.length} ${key.toLocaleLowerCase()} severity issues.`); + for (const report of content.reports) { + if (!report.severity?.rating) { + this.cli.error(`severity.rating not found for report ${report.id}.`); + process.exit(1); + } } - return text.join('\n'); + return getHighestSeverityAnnouncement(content.reports); } getSecurityPreReleaseTemplate() { diff --git a/test/unit/security_release.test.js b/test/unit/security_release.test.js new file mode 100644 index 00000000..eef08c3b --- /dev/null +++ b/test/unit/security_release.test.js @@ -0,0 +1,83 @@ +import { describe, it } from 'node:test'; +import assert from 'node:assert'; + +import SecurityBlog from '../../lib/security_blog.js'; +import { + getHighestSeverity, + getHighestSeverityAnnouncement +} from '../../lib/security-release/security-release.js'; + +const cli = { + error() {} +}; + +function report(id, rating, affectedVersions = ['24.x']) { + return { + id, + severity: { rating }, + affectedVersions + }; +} + +describe('security_release: severity announcement', () => { + it('uses the highest severity across reports', () => { + const reports = [ + report(1, 'low'), + report(2, 'medium'), + report(3, 'high') + ]; + + assert.strictEqual(getHighestSeverity(reports), 'HIGH'); + assert.strictEqual( + getHighestSeverityAnnouncement(reports), + 'The highest severity issue fixed in this release is HIGH.' + ); + }); + + it('uses medium severity wording', () => { + const reports = [ + report(1, 'low'), + report(2, 'medium') + ]; + + assert.strictEqual(getHighestSeverity(reports), 'MEDIUM'); + assert.strictEqual( + getHighestSeverityAnnouncement(reports), + 'The highest severity issue fixed in this release is MEDIUM.' + ); + }); +}); + +describe('security_blog: pre-release severity wording', () => { + it('does not include severity counts in the summary', () => { + const blog = new SecurityBlog(cli); + const content = { + reports: [ + report(1, 'low'), + report(2, 'medium') + ] + }; + + assert.strictEqual( + blog.getVulnerabilities(content), + 'The highest severity issue fixed in this release is MEDIUM.' + ); + }); + + it('uses the highest severity per release line in impact text', () => { + const blog = new SecurityBlog(cli); + const content = { + reports: [ + report(1, 'low', ['22.x', '20.x']), + report(2, 'medium', ['22.x']), + report(3, 'high', ['20.x']) + ] + }; + + assert.strictEqual( + blog.getImpact(content), + 'The highest severity issue fixed in the 22.x release line is MEDIUM.\n' + + 'The highest severity issue fixed in the 20.x release line is HIGH.' + ); + }); +}); From 1169a82f0ff1a6f9ef75c2fdc00e6cfdae184d06 Mon Sep 17 00:00:00 2001 From: Antoine du Hamel Date: Tue, 26 May 2026 15:04:48 +0200 Subject: [PATCH 2/4] fixup! feat: drop number of vulnerabilities on --pre-release --- lib/security-release/security-release.js | 34 +++--------------------- 1 file changed, 4 insertions(+), 30 deletions(-) diff --git a/lib/security-release/security-release.js b/lib/security-release/security-release.js index a2af628b..6af046af 100644 --- a/lib/security-release/security-release.js +++ b/lib/security-release/security-release.js @@ -11,19 +11,7 @@ export const NEXT_SECURITY_RELEASE_REPOSITORY = { repo: 'security-release' }; -const SEVERITY_RANK = { - critical: 0, - high: 1, - medium: 2, - low: 3 -}; - -const SEVERITY_LABEL = { - critical: 'CRITICAL', - high: 'HIGH', - medium: 'MEDIUM', - low: 'LOW' -}; +const SEVERITY_RANKS = ['LOW', 'MEDIUM', 'HIGH', 'CRITICAL']; export const PLACEHOLDERS = { releaseDate: '%RELEASE_DATE%', @@ -144,24 +132,10 @@ export function formatDateToYYYYMMDD(date) { return `${year}/${month}/${day}`; } -export function getHighestSeverity(reports) { - let highestSeverity = ''; - - for (const report of reports) { - const rating = report.severity.rating.toLowerCase(); - const currentRank = SEVERITY_RANK[rating] ?? Number.MAX_SAFE_INTEGER; - const highestRank = SEVERITY_RANK[highestSeverity] ?? Number.MAX_SAFE_INTEGER; - - if (!highestSeverity || currentRank < highestRank) { - highestSeverity = rating; - } - } - - return SEVERITY_LABEL[highestSeverity] ?? highestSeverity.toUpperCase(); -} - export function getHighestSeverityAnnouncement(reports) { - return `The highest severity issue fixed in this release is ${getHighestSeverity(reports)}.`; + const highestSeverityIndex = Math.max(...reports.map(r => SECURITY_RANKS.indexOf(report.severity.rating))); + + return `The highest severity issue fixed in this release is ${SEVERITY_RANKS[highestSeverityIndex] ?? 'NONE'}.`; } export function promptDependencies(cli) { From 966089e9ae053be61889cb456db148d3d2e6ea78 Mon Sep 17 00:00:00 2001 From: Antoine du Hamel Date: Tue, 26 May 2026 15:08:40 +0200 Subject: [PATCH 3/4] fixup! fixup! feat: drop number of vulnerabilities on --pre-release --- lib/security-release/security-release.js | 12 ++++--- lib/security_blog.js | 7 ++--- test/unit/security_release.test.js | 40 ++++++++++++++++++++++-- 3 files changed, 47 insertions(+), 12 deletions(-) diff --git a/lib/security-release/security-release.js b/lib/security-release/security-release.js index 6af046af..94e8780d 100644 --- a/lib/security-release/security-release.js +++ b/lib/security-release/security-release.js @@ -132,10 +132,14 @@ export function formatDateToYYYYMMDD(date) { return `${year}/${month}/${day}`; } -export function getHighestSeverityAnnouncement(reports) { - const highestSeverityIndex = Math.max(...reports.map(r => SECURITY_RANKS.indexOf(report.severity.rating))); - - return `The highest severity issue fixed in this release is ${SEVERITY_RANKS[highestSeverityIndex] ?? 'NONE'}.`; +export function getHighestSeverityAnnouncement(reports, releaseLine = 'this release') { + const highestSeverityIndex = Math.max(...reports.map( + r => SEVERITY_RANKS.indexOf(r.severity.rating.toUpperCase()) + )); + + return `The highest severity issue fixed in ${releaseLine} is ${ + SEVERITY_RANKS[highestSeverityIndex] ?? 'NONE' + }.`; } export function promptDependencies(cli) { diff --git a/lib/security_blog.js b/lib/security_blog.js index 305d1212..f5d80bea 100644 --- a/lib/security_blog.js +++ b/lib/security_blog.js @@ -7,7 +7,6 @@ import { validateDate, SecurityRelease, commitAndPushVulnerabilitiesJSON, - getHighestSeverity, getHighestSeverityAnnouncement, } from './security-release/security-release.js'; import auth from './auth.js'; @@ -337,10 +336,8 @@ export default class SecurityBlog extends SecurityRelease { const result = Array.from(impact.entries()) .sort(([a], [b]) => b.localeCompare(a)) // DESC - .map(([version, reports]) => { - return `The highest severity issue fixed in the ${version} release line is ` + - `${getHighestSeverity(reports)}.`; - }) + .map(([version, reports]) => + getHighestSeverityAnnouncement(reports, `the ${version} release line`)) .join('\n'); return result; diff --git a/test/unit/security_release.test.js b/test/unit/security_release.test.js index eef08c3b..82ea1106 100644 --- a/test/unit/security_release.test.js +++ b/test/unit/security_release.test.js @@ -3,7 +3,6 @@ import assert from 'node:assert'; import SecurityBlog from '../../lib/security_blog.js'; import { - getHighestSeverity, getHighestSeverityAnnouncement } from '../../lib/security-release/security-release.js'; @@ -27,20 +26,55 @@ describe('security_release: severity announcement', () => { report(3, 'high') ]; - assert.strictEqual(getHighestSeverity(reports), 'HIGH'); assert.strictEqual( getHighestSeverityAnnouncement(reports), 'The highest severity issue fixed in this release is HIGH.' ); }); + it('can be customized with second argument', () => { + const reports = [ + report(1, 'low'), + report(2, 'medium'), + report(3, 'high') + ]; + + assert.strictEqual( + getHighestSeverityAnnouncement(reports, 'special release'), + 'The highest severity issue fixed in special release is HIGH.' + ); + }); + + it('invalid severity ratings are ignored', () => { + const reports = [ + report(1, 'low'), + report(2, 'medium'), + report(3, 'hypercritical') + ]; + + assert.strictEqual( + getHighestSeverityAnnouncement(reports), + 'The highest severity issue fixed in this release is MEDIUM.' + ); + }); + + it('if no valid rating is passed, output NONE', () => { + const reports = [ + report(3, 'hypercritical') + ]; + + assert.strictEqual( + getHighestSeverityAnnouncement(reports), + 'The highest severity issue fixed in this release is NONE.' + ); + }); + it('uses medium severity wording', () => { const reports = [ report(1, 'low'), report(2, 'medium') ]; - assert.strictEqual(getHighestSeverity(reports), 'MEDIUM'); assert.strictEqual( getHighestSeverityAnnouncement(reports), 'The highest severity issue fixed in this release is MEDIUM.' From f5806448c72a47b4f024bff267087a264f743890 Mon Sep 17 00:00:00 2001 From: RafaelGSS Date: Thu, 28 May 2026 15:28:06 -0300 Subject: [PATCH 4/4] fix: split security release vulnerability wording Signed-off-by: RafaelGSS --- lib/security_blog.js | 24 +++++- test/unit/security_release.test.js | 122 ++++++++++++++++++++++++++++- 2 files changed, 144 insertions(+), 2 deletions(-) diff --git a/lib/security_blog.js b/lib/security_blog.js index f5d80bea..194e4d06 100644 --- a/lib/security_blog.js +++ b/lib/security_blog.js @@ -38,7 +38,7 @@ export default class SecurityBlog extends SecurityRelease { annoucementDate: await this.getAnnouncementDate(cli), releaseDate: this.formatReleaseDate(releaseDate), affectedVersions: this.getAffectedVersions(content), - vulnerabilities: this.getVulnerabilities(content), + vulnerabilities: this.getPreReleaseVulnerabilities(content), slug: this.getSlug(releaseDate), impact: this.getImpact(content) }; @@ -344,12 +344,34 @@ export default class SecurityBlog extends SecurityRelease { } getVulnerabilities(content) { + const severityCount = new Map(); + for (const report of content.reports) { if (!report.severity?.rating) { this.cli.error(`severity.rating not found for report ${report.id}.`); process.exit(1); } + + const rating = report.severity.rating; + severityCount.set(rating, (severityCount.get(rating) || 0) + 1); + } + + const text = []; + for (const [rating, count] of severityCount) { + text.push(`- ${count} ${rating} severity issues.`); } + + return text.join('\n'); + } + + getPreReleaseVulnerabilities(content) { + for (const report of content.reports) { + if (!report.severity?.rating) { + this.cli.error(`severity.rating not found for report ${report.id}.`); + process.exit(1); + } + } + return getHighestSeverityAnnouncement(content.reports); } diff --git a/test/unit/security_release.test.js b/test/unit/security_release.test.js index 82ea1106..80541024 100644 --- a/test/unit/security_release.test.js +++ b/test/unit/security_release.test.js @@ -10,6 +10,19 @@ const cli = { error() {} }; +function assertExits(fn) { + const originalExit = process.exit; + process.exit = () => { + throw new Error('process.exit'); + }; + + try { + assert.throws(fn, /process\.exit/); + } finally { + process.exit = originalExit; + } +} + function report(id, rating, affectedVersions = ['24.x']) { return { id, @@ -80,6 +93,19 @@ describe('security_release: severity announcement', () => { 'The highest severity issue fixed in this release is MEDIUM.' ); }); + + it('ignores invalid severity ratings', () => { + const reports = [ + report(1, 'low'), + report(2, 'hypercritical'), + report(3, 'medium') + ]; + + assert.strictEqual( + getHighestSeverityAnnouncement(reports), + 'The highest severity issue fixed in this release is MEDIUM.' + ); + }); }); describe('security_blog: pre-release severity wording', () => { @@ -93,9 +119,13 @@ describe('security_blog: pre-release severity wording', () => { }; assert.strictEqual( - blog.getVulnerabilities(content), + blog.getPreReleaseVulnerabilities(content), 'The highest severity issue fixed in this release is MEDIUM.' ); + assert.strictEqual( + blog.getVulnerabilities(content), + '- 1 low severity issues.\n- 1 medium severity issues.' + ); }); it('uses the highest severity per release line in impact text', () => { @@ -114,4 +144,94 @@ describe('security_blog: pre-release severity wording', () => { 'The highest severity issue fixed in the 20.x release line is HIGH.' ); }); + + it('replaces the pre-release template placeholder with the highest severity sentence', () => { + const blog = new SecurityBlog(cli); + const template = blog.getSecurityPreReleaseTemplate(); + const preRelease = blog.buildPreRelease(template, { + annoucementDate: '2026-06-01T00:00:00.000Z', + releaseDate: 'Tuesday, June 2, 2026', + affectedVersions: '24.x, 22.x', + vulnerabilities: blog.getPreReleaseVulnerabilities({ + reports: [ + report(1, 'low'), + report(2, 'high') + ] + }), + slug: 'june-2026-security-releases', + impact: 'The highest severity issue fixed in the 24.x release line is HIGH.' + }); + + assert.match( + preRelease, + /The highest severity issue fixed in this release is HIGH\./ + ); + assert.doesNotMatch(preRelease, /%VULNERABILITIES%/); + }); + + it('exits when a report is missing a severity rating', () => { + const errors = []; + const blog = new SecurityBlog({ + error(message) { + errors.push(message); + } + }); + const content = { + reports: [ + { + id: 1, + severity: {}, + affectedVersions: ['24.x'] + } + ] + }; + + assertExits(() => blog.getPreReleaseVulnerabilities(content)); + assertExits(() => blog.getImpact(content)); + assert.deepStrictEqual(errors, [ + 'severity.rating not found for report 1.', + 'severity.rating not found for report 1.' + ]); + }); +}); + +describe('security_blog: post-release severity wording', () => { + it('keeps the vulnerability count list', () => { + const blog = new SecurityBlog(cli); + const content = { + reports: [ + report(1, 'low'), + report(2, 'medium'), + report(3, 'medium') + ] + }; + + assert.strictEqual( + blog.getVulnerabilities(content), + '- 1 low severity issues.\n- 2 medium severity issues.' + ); + }); + + it('exits when a report is missing a severity rating', () => { + const errors = []; + const blog = new SecurityBlog({ + error(message) { + errors.push(message); + } + }); + const content = { + reports: [ + { + id: 1, + severity: {}, + affectedVersions: ['24.x'] + } + ] + }; + + assertExits(() => blog.getVulnerabilities(content)); + assert.deepStrictEqual(errors, [ + 'severity.rating not found for report 1.' + ]); + }); });