diff --git a/components/git/security.js b/components/git/security.js
index 002daf33..28a1039e 100644
--- a/components/git/security.js
+++ b/components/git/security.js
@@ -39,6 +39,10 @@ const securityOptions = {
'request-cve': {
describe: 'Request CVEs for a security release',
type: 'boolean'
+ },
+ 'post-release': {
+ describe: 'Create the post-release announcement',
+ type: 'boolean'
}
};
@@ -49,7 +53,8 @@ export function builder(yargs) {
return yargs.options(securityOptions)
.example(
'git node security --start',
- 'Prepare a security release of Node.js')
+ 'Prepare a security release of Node.js'
+ )
.example(
'git node security --sync',
'Synchronize an ongoing security release with HackerOne'
@@ -57,26 +62,25 @@ export function builder(yargs) {
.example(
'git node security --update-date=YYYY/MM/DD',
'Updates the target date of the security release'
- )
- .example(
+ ).example(
'git node security --add-report=H1-ID',
'Fetches HackerOne report based on ID provided and adds it into vulnerabilities.json'
- )
- .example(
+ ).example(
'git node security --remove-report=H1-ID',
'Removes the Hackerone report based on ID provided from vulnerabilities.json'
- )
- .example(
+ ).example(
'git node security --pre-release',
'Create the pre-release announcement on the Nodejs.org repo'
).example(
'git node security --notify-pre-release',
'Notifies the community about the security release'
- )
- .example(
+ ).example(
'git node security --request-cve',
'Request CVEs for a security release of Node.js based on' +
' the next-security-release/vulnerabilities.json'
+ ).example(
+ 'git node security --post-release' +
+ 'Create the post-release announcement on the Nodejs.org repo'
);
}
@@ -105,6 +109,9 @@ export function handler(argv) {
if (argv['request-cve']) {
return requestCVEs(argv);
}
+ if (argv['post-release']) {
+ return createPostRelease(argv);
+ }
yargsInstance.showHelp();
}
@@ -146,7 +153,14 @@ async function requestCVEs() {
return hackerOneCve.requestCVEs();
}
-async function startSecurityRelease(argv) {
+async function createPostRelease() {
+ const logStream = process.stdout.isTTY ? process.stdout : process.stderr;
+ const cli = new CLI(logStream);
+ const blog = new SecurityBlog(cli);
+ return blog.createPostRelease();
+}
+
+async function startSecurityRelease() {
const logStream = process.stdout.isTTY ? process.stdout : process.stderr;
const cli = new CLI(logStream);
const release = new PrepareSecurityRelease(cli);
diff --git a/lib/github/templates/security-post-release.md b/lib/github/templates/security-post-release.md
new file mode 100644
index 00000000..39b15008
--- /dev/null
+++ b/lib/github/templates/security-post-release.md
@@ -0,0 +1,18 @@
+---
+date: %ANNOUNCEMENT_DATE%
+category: vulnerability
+title: %RELEASE_DATE% Security Releases
+slug: %SLUG%
+layout: blog-post
+author: %AUTHOR%
+---
+
+## Security releases available
+
+Updates are now available for the %AFFECTED_VERSIONS% Node.js release lines for the
+following issues.
+%DEPENDENCY_UPDATES%
+%REPORTS%
+## Downloads and release details
+
+%DOWNLOADS%
diff --git a/lib/github/templates/security-pre-release.md b/lib/github/templates/security-pre-release.md
index 08299fad..2b8defe4 100644
--- a/lib/github/templates/security-pre-release.md
+++ b/lib/github/templates/security-pre-release.md
@@ -13,7 +13,7 @@ The Node.js project will release new versions of the %AFFECTED_VERSIONS%
releases lines on or shortly after, %RELEASE_DATE% in order to address:
%VULNERABILITIES%
-%OPENSSL_UPDATES%
+
## Impact
%IMPACT%
@@ -28,7 +28,7 @@ Releases will be available on, or shortly after, %RELEASE_DATE%.
## Contact and future updates
-The current Node.js security policy can be found at https://nodejs.org/en/security/.
-Please follow the process outlined in https://github.com/nodejs/node/blob/master/SECURITY.md if you wish to report a vulnerability in Node.js.
+The current Node.js security policy can be found at .
+Please follow the process outlined in if you wish to report a vulnerability in Node.js.
-Subscribe to the low-volume announcement-only nodejs-sec mailing list at https://groups.google.com/forum/#!forum/nodejs-sec to stay up to date on security vulnerabilities and security-related releases of Node.js and the projects maintained in the nodejs GitHub organization.
+Subscribe to the low-volume announcement-only nodejs-sec mailing list at to stay up to date on security vulnerabilities and security-related releases of Node.js and the projects maintained in the nodejs GitHub organization.
diff --git a/lib/security-release/security-release.js b/lib/security-release/security-release.js
index 5acba9ab..73f93cd6 100644
--- a/lib/security-release/security-release.js
+++ b/lib/security-release/security-release.js
@@ -20,9 +20,12 @@ export const PLACEHOLDERS = {
annoucementDate: '%ANNOUNCEMENT_DATE%',
slug: '%SLUG%',
affectedVersions: '%AFFECTED_VERSIONS%',
- openSSLUpdate: '%OPENSSL_UPDATES%',
impact: '%IMPACT%',
- vulnerabilities: '%VULNERABILITIES%'
+ vulnerabilities: '%VULNERABILITIES%',
+ reports: '%REPORTS%',
+ author: '%AUTHOR%',
+ dependencyUpdates: '%DEPENDENCY_UPDATES%',
+ downloads: '%DOWNLOADS%'
};
export function checkRemote(cli, repository) {
diff --git a/lib/security_blog.js b/lib/security_blog.js
index 89ddeb77..217778a2 100644
--- a/lib/security_blog.js
+++ b/lib/security_blog.js
@@ -1,16 +1,25 @@
import fs from 'node:fs';
import path from 'node:path';
import _ from 'lodash';
+import nv from '@pkgjs/nv';
import {
PLACEHOLDERS,
getVulnerabilitiesJSON,
checkoutOnSecurityReleaseBranch,
NEXT_SECURITY_RELEASE_REPOSITORY,
- validateDate
+ validateDate,
+ getSummary,
+ commitAndPushVulnerabilitiesJSON,
+ NEXT_SECURITY_RELEASE_FOLDER
} from './security-release/security-release.js';
+import auth from './auth.js';
+import Request from './request.js';
+
+const kChanged = Symbol('changed');
export default class SecurityBlog {
repository = NEXT_SECURITY_RELEASE_REPOSITORY;
+ req;
constructor(cli) {
this.cli = cli;
}
@@ -40,8 +49,7 @@ export default class SecurityBlog {
affectedVersions: this.getAffectedVersions(content),
vulnerabilities: this.getVulnerabilities(content),
slug: this.getSlug(releaseDate),
- impact: this.getImpact(content),
- openSSLUpdate: await this.promptOpenSSLUpdate(cli)
+ impact: this.getImpact(content)
};
const month = releaseDate.toLocaleString('en-US', { month: 'long' }).toLowerCase();
const year = releaseDate.getFullYear();
@@ -52,9 +60,93 @@ export default class SecurityBlog {
cli.ok(`Pre-release announcement file created at ${file}`);
}
- promptOpenSSLUpdate(cli) {
- return cli.prompt('Does this security release containt OpenSSL updates?', {
- defaultAnswer: true
+ async createPostRelease() {
+ const { cli } = this;
+ const credentials = await auth({
+ github: true,
+ h1: true
+ });
+
+ this.req = new Request(credentials);
+
+ // checkout on security release branch
+ checkoutOnSecurityReleaseBranch(cli, this.repository);
+
+ // read vulnerabilities JSON file
+ const content = getVulnerabilitiesJSON(cli);
+ if (!content.releaseDate) {
+ cli.error('Release date is not set in vulnerabilities.json,' +
+ ' run `git node security --update-date=YYYY/MM/DD` to set the release date.');
+ process.exit(1);
+ }
+
+ validateDate(content.releaseDate);
+ const releaseDate = new Date(content.releaseDate);
+ const template = this.getSecurityPostReleaseTemplate();
+ const data = {
+ annoucementDate: await this.getAnnouncementDate(cli),
+ releaseDate: this.formatReleaseDate(releaseDate),
+ affectedVersions: this.getAffectedVersions(content),
+ vulnerabilities: this.getVulnerabilities(content),
+ slug: this.getSlug(releaseDate),
+ author: await this.promptAuthor(cli),
+ dependencyUpdates: content.dependencies
+ };
+ const postReleaseContent = await this.buildPostRelease(template, data, content);
+
+ const pathPreRelease = await this.promptExistingPreRelease(cli);
+ // read the existing pre-release announcement
+ let preReleaseContent = fs.readFileSync(pathPreRelease, 'utf-8');
+ // cut the part before summary
+ const preSummary = preReleaseContent.indexOf('# Summary');
+ if (preSummary !== -1) {
+ preReleaseContent = preReleaseContent.substring(preSummary);
+ }
+
+ const updatedContent = postReleaseContent + preReleaseContent;
+
+ fs.writeFileSync(pathPreRelease, updatedContent);
+ cli.ok(`Post-release announcement file updated at ${pathPreRelease}`);
+
+ // if the vulnerabilities.json has been changed, update the file
+ if (!content[kChanged]) return;
+ this.updateVulnerabilitiesJSON(content);
+ }
+
+ updateVulnerabilitiesJSON(content) {
+ try {
+ this.cli.info('Updating vulnerabilities.json');
+ const vulnerabilitiesJSONPath = path.join(process.cwd(),
+ NEXT_SECURITY_RELEASE_FOLDER, 'vulnerabilities.json');
+ fs.writeFileSync(vulnerabilitiesJSONPath, JSON.stringify(content, null, 2));
+ const commitMessage = 'chore: updated vulnerabilities.json';
+ commitAndPushVulnerabilitiesJSON(vulnerabilitiesJSONPath,
+ commitMessage,
+ { cli: this.cli, repository: this.repository });
+ } catch (error) {
+ this.cli.error('Error updating vulnerabilities.json');
+ this.cli.error(error);
+ }
+ }
+
+ async promptExistingPreRelease(cli) {
+ const pathPreRelease = await cli.prompt(
+ 'Please provide the path of the existing pre-release announcement:', {
+ questionType: 'input',
+ defaultAnswer: ''
+ });
+
+ if (!pathPreRelease || !fs.existsSync(path.resolve(pathPreRelease))) {
+ return this.promptExistingPreRelease(cli);
+ }
+ return pathPreRelease;
+ }
+
+ promptAuthor(cli) {
+ return cli.prompt('Who is the author of this security release? If multiple' +
+ ' use & as separator', {
+ questionType: 'input',
+ defaultAnswer: PLACEHOLDERS.author
});
}
@@ -69,6 +161,23 @@ export default class SecurityBlog {
}
buildPreRelease(template, data) {
+ const {
+ annoucementDate,
+ releaseDate,
+ affectedVersions,
+ vulnerabilities,
+ slug,
+ impact
+ } = data;
+ return template.replaceAll(PLACEHOLDERS.annoucementDate, annoucementDate)
+ .replaceAll(PLACEHOLDERS.slug, slug)
+ .replaceAll(PLACEHOLDERS.affectedVersions, affectedVersions)
+ .replaceAll(PLACEHOLDERS.vulnerabilities, vulnerabilities)
+ .replaceAll(PLACEHOLDERS.releaseDate, releaseDate)
+ .replaceAll(PLACEHOLDERS.impact, impact);
+ }
+
+ async buildPostRelease(template, data, content) {
const {
annoucementDate,
releaseDate,
@@ -76,7 +185,8 @@ export default class SecurityBlog {
vulnerabilities,
slug,
impact,
- openSSLUpdate
+ author,
+ dependencyUpdates
} = data;
return template.replaceAll(PLACEHOLDERS.annoucementDate, annoucementDate)
.replaceAll(PLACEHOLDERS.slug, slug)
@@ -84,15 +194,89 @@ export default class SecurityBlog {
.replaceAll(PLACEHOLDERS.vulnerabilities, vulnerabilities)
.replaceAll(PLACEHOLDERS.releaseDate, releaseDate)
.replaceAll(PLACEHOLDERS.impact, impact)
- .replaceAll(PLACEHOLDERS.openSSLUpdate, this.getOpenSSLUpdateTemplate(openSSLUpdate));
+ .replaceAll(PLACEHOLDERS.author, author)
+ .replaceAll(PLACEHOLDERS.reports, await this.getReportsTemplate(content))
+ .replaceAll(PLACEHOLDERS.dependencyUpdates,
+ this.getDependencyUpdatesTemplate(dependencyUpdates))
+ .replaceAll(PLACEHOLDERS.downloads, await this.getDownloadsTemplate(affectedVersions));
+ }
+
+ async getReportsTemplate(content) {
+ const reports = content.reports;
+ let template = '';
+ for (const report of reports) {
+ let cveId = report.cve_ids?.join(', ');
+ if (!cveId) {
+ // ask for the CVE ID
+ // it should have been created with the step `--request-cve`
+ cveId = await this.cli.prompt(`What is the CVE ID for vulnerability https://hackerone.com/reports/${report.id} ${report.title}?`, {
+ questionType: 'input',
+ defaultAnswer: 'TBD'
+ });
+ report.cve_ids = [cveId];
+ content[kChanged] = true;
+ }
+ template += `## ${report.title} (${cveId}) - (${report.severity.rating})\n\n`;
+ if (!report.summary) {
+ const fetchIt = await this.cli.prompt(`Summary missing for vulnerability https://hackerone.com/reports/${report.id} ${report.title}.\
+ Do you want to try fetch it from HackerOne??`, {
+ questionType: 'confirm',
+ defaultAnswer: true
+ });
+
+ if (fetchIt) {
+ report.summary = await getSummary(report.id, this.req);
+ content[kChanged] = true;
+ }
+
+ if (!report.summary) {
+ this.cli.error(`Summary missing for vulnerability https://hackerone.com/reports/${report.id} ${report.title}. Please create it before continuing.`);
+ process.exit(1);
+ }
+ }
+ template += `${report.summary}\n\n`;
+ const releaseLines = report.affectedVersions.join(', ');
+ template += `Impact:\n\n- This vulnerability affects all users\
+ in active release lines: ${releaseLines}\n\n`;
+ if (!report.patchAuthors) {
+ const author = await this.cli.prompt(`Who fixed vulnerability https://hackerone.com/reports/${report.id} ${report.title}? If multiple use & as separator`, {
+ questionType: 'input',
+ defaultAnswer: 'TBD'
+ });
+ report.patchAuthors = author.split('&').map((p) => p.trim());
+ content[kChanged] = true;
+ }
+ template += `Thank you, to ${report.reporter} for reporting this vulnerability\
+ and thank you ${report.patchAuthors.join(' and ')} for fixing it.\n\n`;
+ }
+ return template;
+ }
+
+ getDependencyUpdatesTemplate(dependencyUpdates) {
+ if (!dependencyUpdates) return '';
+ let template = 'This security release includes the following dependency' +
+ ' updates to address public vulnerabilities:\n\n';
+ for (const dependencyUpdate of Object.values(dependencyUpdates)) {
+ for (const dependency of dependencyUpdate) {
+ const title = dependency.title.substring(dependency.title.indexOf(':') + ':'.length).trim();
+ template += `- ${title}\
+ on ${dependency.affectedVersions.join(', ')}\n`;
+ }
+ }
+ return template;
}
- getOpenSSLUpdateTemplate(openSSLUpdate) {
- if (openSSLUpdate) {
- return '\n## OpenSSL Security updates\n\n' +
- 'This security release includes OpenSSL security updates\n';
+ async getDownloadsTemplate(affectedVersions) {
+ let template = '';
+ const versionsToBeReleased = (await nv('supported')).filter(
+ (v) => affectedVersions.split(', ').includes(`${v.major}.x`)
+ );
+ for (const version of versionsToBeReleased) {
+ const v = `v${version.major}.${version.minor}.${Number(version.patch) + 1}`;
+ template += `- [Node.js ${v}](/blog/release/${v}/)\n`;
}
- return '';
+
+ return template;
}
getSlug(releaseDate) {
@@ -179,4 +363,14 @@ export default class SecurityBlog {
'utf-8'
);
}
+
+ getSecurityPostReleaseTemplate() {
+ return fs.readFileSync(
+ new URL(
+ './github/templates/security-post-release.md',
+ import.meta.url
+ ),
+ 'utf-8'
+ );
+ }
}