diff --git a/components/git/security.js b/components/git/security.js
new file mode 100644
index 00000000..d11e5bf5
--- /dev/null
+++ b/components/git/security.js
@@ -0,0 +1,35 @@
+import CLI from '../../lib/cli.js';
+import SecurityReleaseSteward from '../../lib/prepare_security.js';
+
+export const command = 'security [options]';
+export const describe = 'Manage an in-progress security release or start a new one.';
+
+const securityOptions = {
+ start: {
+ describe: 'Start security release process',
+ type: 'boolean'
+ }
+};
+
+let yargsInstance;
+
+export function builder(yargs) {
+ yargsInstance = yargs;
+ return yargs.options(securityOptions).example(
+ 'git node security --start',
+ 'Prepare a security release of Node.js');
+}
+
+export function handler(argv) {
+ if (argv.start) {
+ return startSecurityRelease(argv);
+ }
+ yargsInstance.showHelp();
+}
+
+async function startSecurityRelease(argv) {
+ const logStream = process.stdout.isTTY ? process.stdout : process.stderr;
+ const cli = new CLI(logStream);
+ const release = new SecurityReleaseSteward(cli);
+ return release.start();
+}
diff --git a/docs/git-node.md b/docs/git-node.md
index 2d0f3bac..26d2d482 100644
--- a/docs/git-node.md
+++ b/docs/git-node.md
@@ -427,6 +427,32 @@ $ git node vote \
==============================================================================
```
+## `git node security`
+
+Manage or starts a security release process.
+
+
+
+### Prerequisites
+
+It's necessary to set up `.ncurc` with HackerOne keys:
+
+```console
+$ ncu-config --global set h1_token $H1_TOKEN
+$ ncu-config --global set h1_username $H1_TOKEN
+```
+
+- `h1_token`: HackerOne Organization API Token, preferable with read-only
+ access.
+- `h1_username`: HackerOne API Token username.
+
+### `git node security --start`
+
+This command creates the Next Security Issue in Node.js private repository
+following the [Security Release Process][] document.
+It will retrieve all the triaged HackerOne reports and add them to the list
+with the affected release line.
+
## `git node status`
Return status and information about the current git-node land session. Shows the following information:
@@ -488,3 +514,4 @@ $ git node wpt url --commit=43feb7f612fe9160639e09a47933a29834904d69
```
[node.js abi version registry]: https://github.com/nodejs/node/blob/main/doc/abi_version_registry.json
+[Security Release Process]: https://github.com/nodejs/node/blob/main/doc/contributing/security-release-process.md
diff --git a/lib/auth.js b/lib/auth.js
index c13ef196..a0cf5b4a 100644
--- a/lib/auth.js
+++ b/lib/auth.js
@@ -107,6 +107,20 @@ async function auth(
check(username, jenkins_token);
result.jenkins = encode(username, jenkins_token);
}
+
+ if (options.h1) {
+ const { h1_username, h1_token } = getMergedConfig();
+ if (!h1_username || !h1_token) {
+ errorExit(
+ 'Get your HackerOne API token in ' +
+ 'https://docs.hackerone.com/organizations/api-tokens.html ' +
+ 'and run the following command to add it to your ncu config: ' +
+ 'ncu-config --global set h1_token TOKEN or ' +
+ 'ncu-config --global set h1_username USERNAME'
+ );
+ };
+ result.h1 = encode(h1_username, h1_token);
+ }
return result;
}
diff --git a/lib/github/templates/next-security-release.md b/lib/github/templates/next-security-release.md
new file mode 100644
index 00000000..ae5d6176
--- /dev/null
+++ b/lib/github/templates/next-security-release.md
@@ -0,0 +1,97 @@
+## Planning
+
+* [X] Open an [issue](https://github.com/nodejs-private/node-private) titled
+ `Next Security Release`, and put this checklist in the description.
+
+* [ ] Get agreement on the list of vulnerabilities to be addressed:
+%REPORTS%
+
+* [ ] PR release announcements in [private](https://github.com/nodejs-private/nodejs.org-private):
+ * [ ] pre-release: %PRE_RELEASE_PRIV%
+ * [ ] post-release: %POS_RELEASE_PRIV%
+ * List vulnerabilities in order of descending severity
+ * Ask the HackerOne reporter if they would like to be credited on the
+ security release blog page
+
+* [ ] Get agreement on the planned date for the release: %RELEASE_DATE%
+
+* [ ] Get release team volunteers for all affected lines:
+%AFFECTED_LINES%
+
+## Announcement (one week in advance of the planned release)
+
+* [ ] Verify that GitHub Actions are working as normal: .
+
+* [ ] Check that all vulnerabilities are ready for release integration:
+ * PRs against all affected release lines or cherry-pick clean
+ * Approved
+ * (optional) Approved by the reporter
+ * Build and send the binary to the reporter according to its architecture
+ and ask for a review. This step is important to avoid insufficient fixes
+ between Security Releases.
+ * Have CVEs
+ * Make sure that dependent libraries have CVEs for their issues. We should
+ only create CVEs for vulnerabilities in Node.js itself. This is to avoid
+ having duplicate CVEs for the same vulnerability.
+ * Described in the pre/post announcements
+
+* [ ] Pre-release announcement to nodejs.org blog: TBD
+ (Re-PR the pre-approved branch from nodejs-private/nodejs.org-private to
+ nodejs/nodejs.org)
+
+* [ ] Pre-release announcement [email](https://groups.google.com/forum/#!forum/nodejs-sec): TBD
+ * Subject: `Node.js security updates for all active release lines, Month Year`
+
+* [ ] CC `oss-security@lists.openwall.com` on pre-release
+ * [ ] Forward the email you receive to `oss-security@lists.openwall.com`.
+
+* [ ] Create a new issue in [nodejs/tweet](https://github.com/nodejs/tweet/issues)
+
+* [ ] Request releaser(s) to start integrating the PRs to be released.
+
+* [ ] Notify [docker-node](https://github.com/nodejs/docker-node/issues) of upcoming security release date: TBD
+
+* [ ] Notify build-wg of upcoming security release date by opening an issue
+ in [nodejs/build](https://github.com/nodejs/build/issues) to request WG members are available to fix any CI issues: TBD
+
+## Release day
+
+* [ ] [Lock CI](https://github.com/nodejs/build/blob/HEAD/doc/jenkins-guide.md#before-the-release)
+
+* [ ] The releaser(s) run the release process to completion.
+
+* [ ] [Unlock CI](https://github.com/nodejs/build/blob/HEAD/doc/jenkins-guide.md#after-the-release)
+
+* [ ] Post-release announcement to Nodejs.org blog: https://github.com/nodejs/nodejs.org/pull/5447
+ * (Re-PR the pre-approved branch from nodejs-private/nodejs.org-private to
+ nodejs/nodejs.org)
+
+* [ ] Post-release announcement in reply email: TBD
+
+* [ ] Create a new issue in nodejs/tweet
+
+* [ ] Comment in [docker-node][] issue that release is ready for integration.
+ The docker-node team will build and release docker image updates.
+
+* [ ] For every H1 report resolved:
+ * Close as Resolved
+ * Request Disclosure
+ * Request publication of H1 CVE requests
+ * (Check that the "Version Fixed" field in the CVE is correct, and provide
+ links to the release blogs in the "Public Reference" section)
+
+* [ ] PR machine-readable JSON descriptions of the vulnerabilities to the
+ [core](https://github.com/nodejs/security-wg/tree/HEAD/vuln/core)
+ vulnerability DB. https://github.com/nodejs/security-wg/pull/1029
+ * For each vulnerability add a `#.json` file, one can copy an existing
+ [json](https://github.com/nodejs/security-wg/blob/0d82062d917cb9ddab88f910559469b2b13812bf/vuln/core/78.json)
+ file, and increment the latest created file number and use that as the name
+ of the new file to be added. For example, `79.json`.
+
+* [ ] Close this issue
+
+* [ ] Make sure the PRs for the vulnerabilities are closed.
+
+* [ ] PR in that you stewarded the release in
+ [Security release stewards](https://github.com/nodejs/node/blob/HEAD/doc/contributing/security-release-process.md#security-release-stewards).
+ If necessary add the next rotation of the steward rotation.
diff --git a/lib/prepare_security.js b/lib/prepare_security.js
new file mode 100644
index 00000000..68a7d942
--- /dev/null
+++ b/lib/prepare_security.js
@@ -0,0 +1,117 @@
+import nv from '@pkgjs/nv';
+import auth from './auth.js';
+import Request from './request.js';
+import fs from 'node:fs';
+
+export default class SecurityReleaseSteward {
+ constructor(cli) {
+ this.cli = cli;
+ }
+
+ async start() {
+ const { cli } = this;
+ const credentials = await auth({
+ github: true,
+ h1: true
+ });
+
+ const req = new Request(credentials);
+ const create = await cli.prompt(
+ 'Create the Next Security Release issue?',
+ { defaultAnswer: true });
+ if (create) {
+ const issue = new SecurityReleaseIssue(req);
+ const content = await issue.buildIssue(cli);
+ const data = await req.createIssue('Next Security Release', content, {
+ owner: 'nodejs-private',
+ repo: 'node-private'
+ });
+ if (data.html_url) {
+ cli.ok('Created: ' + data.html_url);
+ } else {
+ cli.error(data);
+ }
+ }
+ }
+}
+
+class SecurityReleaseIssue {
+ constructor(req) {
+ this.req = req;
+ this.content = '';
+ this.title = 'Next Security Release';
+ this.affectedLines = {};
+ }
+
+ getSecurityIssueTemplate() {
+ return fs.readFileSync(
+ new URL(
+ './github/templates/next-security-release.md',
+ import.meta.url
+ ),
+ 'utf-8'
+ );
+ }
+
+ async buildIssue(cli) {
+ this.content = this.getSecurityIssueTemplate();
+ cli.info('Getting triaged H1 reports...');
+ const reports = await this.req.getTriagedReports();
+ await this.fillReports(cli, reports);
+
+ this.fillAffectedLines(Object.keys(this.affectedLines));
+
+ const target = await cli.prompt('Enter target date in YYYY-MM-DD format:', {
+ questionType: 'input',
+ defaultAnswer: 'TBD'
+ });
+ this.fillTargetDate(target);
+
+ return this.content;
+ }
+
+ async fillReports(cli, reports) {
+ const supportedVersions = (await nv('supported'))
+ .map((v) => v.versionName + '.x')
+ .join(',');
+
+ let reportsContent = '';
+ for (const report of reports.data) {
+ const { id, attributes: { title }, relationships: { severity } } = report;
+ const reportLevel = severity.data.attributes.rating;
+ cli.separator();
+ cli.info(`Report: ${id} - ${title} (${reportLevel})`);
+ const include = await cli.prompt(
+ 'Would you like to include this report to the next security release?',
+ { defaultAnswer: true });
+ if (!include) {
+ continue;
+ }
+
+ reportsContent +=
+ ` * **[${id}](https://hackerone.com/bugs?subject=nodejs&report_id=${id}) - ${title} (TBD) - (${reportLevel})**\n`;
+ const versions = await cli.prompt('Which active release lines this report affects?', {
+ questionType: 'input',
+ defaultAnswer: supportedVersions
+ });
+ for (const v of versions.split(',')) {
+ if (!this.affectedLines[v]) this.affectedLines[v] = true;
+ reportsContent += ` * ${v} - TBD\n`;
+ }
+ }
+ this.content = this.content.replace('%REPORTS%', reportsContent);
+ }
+
+ fillAffectedLines(affectedLines) {
+ let affected = '';
+ for (const line of affectedLines) {
+ affected += ` * ${line} - TBD\n`;
+ }
+ this.content =
+ this.content.replace('%AFFECTED_LINES%', affected);
+ }
+
+ fillTargetDate(date) {
+ this.content = this.content.replace('%RELEASE_DATE%', date);
+ }
+}
diff --git a/lib/request.js b/lib/request.js
index e15d3904..3e5f02d5 100644
--- a/lib/request.js
+++ b/lib/request.js
@@ -60,6 +60,23 @@ export default class Request {
}
}
+ async createIssue(title, body, { owner, repo }) {
+ const url = `https://api.github.com/repos/${owner}/${repo}/issues`;
+ const options = {
+ method: 'POST',
+ headers: {
+ Authorization: `Basic ${this.credentials.github}`,
+ 'User-Agent': 'node-core-utils',
+ Accept: 'application/vnd.github+json'
+ },
+ body: JSON.stringify({
+ title,
+ body
+ })
+ };
+ return this.json(url, options);
+ }
+
async gql(name, variables, path) {
const query = this.loadQuery(name);
if (path) {
@@ -83,6 +100,19 @@ export default class Request {
};
}
+ async getTriagedReports() {
+ const url = 'https://api.hackerone.com/v1/reports?filter[program][]=nodejs&filter[state][]=triaged';
+ const options = {
+ method: 'GET',
+ headers: {
+ Authorization: `Basic ${this.credentials.h1}`,
+ 'User-Agent': 'node-core-utils',
+ Accept: 'application/json'
+ }
+ };
+ return this.json(url, options);
+ }
+
// This is for github v4 API queries, for other types of queries
// use .text or .json
async query(query, variables) {
diff --git a/package.json b/package.json
index afb6fd79..e1259e95 100644
--- a/package.json
+++ b/package.json
@@ -38,6 +38,7 @@
"branch-diff": "^2.1.4",
"chalk": "^5.3.0",
"changelog-maker": "^3.2.4",
+ "@pkgjs/nv": "^0.2.1",
"cheerio": "^1.0.0-rc.12",
"clipboardy": "^3.0.0",
"core-validate-commit": "^4.0.0",