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
12 changes: 12 additions & 0 deletions lib/security-release/security-release.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ export const NEXT_SECURITY_RELEASE_REPOSITORY = {
repo: 'security-release'
};

const SEVERITY_RANKS = ['LOW', 'MEDIUM', 'HIGH', 'CRITICAL'];

export const PLACEHOLDERS = {
releaseDate: '%RELEASE_DATE%',
vulnerabilitiesPRURL: '%VULNERABILITIES_PR_URL%',
Expand Down Expand Up @@ -130,6 +132,16 @@ export function formatDateToYYYYMMDD(date) {
return `${year}/${month}/${day}`;
}

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) {
return cli.prompt('Enter the link to the dependency update PR (leave empty to exit): ', {
defaultAnswer: '',
Expand Down
38 changes: 14 additions & 24 deletions lib/security_blog.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import fs from 'node:fs';
import path from 'node:path';
import _ from 'lodash';
import nv from '@pkgjs/nv';
import {
PLACEHOLDERS,
checkoutOnSecurityReleaseBranch,
validateDate,
SecurityRelease,
commitAndPushVulnerabilitiesJSON,
getHighestSeverityAnnouncement,
} from './security-release/security-release.js';
import auth from './auth.js';
import Request from './request.js';
Expand Down Expand Up @@ -323,6 +323,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);
Expand All @@ -331,36 +336,21 @@ 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}.`;
})
.map(([version, reports]) =>
getHighestSeverityAnnouncement(reports, `the ${version} release line`))
.join('\n');

return result;
}

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);
}
Comment on lines 346 to 354
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We probably want to keep the original list for the post release announcement. Currently we call the same function in createPreRelease and createPostRelease

vulnerabilities: this.getVulnerabilities(content),

vulnerabilities: this.getVulnerabilities(content),


getSecurityPreReleaseTemplate() {
Expand Down
117 changes: 117 additions & 0 deletions test/unit/security_release.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import { describe, it } from 'node:test';
import assert from 'node:assert';

import SecurityBlog from '../../lib/security_blog.js';
import {
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(
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(
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.'
);
});
});
Loading