Skip to content

Commit 7172d72

Browse files
committed
feat: create git-node security release command
1 parent 8672c88 commit 7172d72

File tree

7 files changed

+321
-0
lines changed

7 files changed

+321
-0
lines changed

components/git/security.js

+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import CLI from '../../lib/cli.js';
2+
import SecurityReleaseSteward from '../../lib/prepare_security.js';
3+
4+
export const command = 'security [options]';
5+
export const describe = 'Manage an in-progress security release or start a new one.';
6+
7+
const securityOptions = {
8+
start: {
9+
describe: 'Start security release process',
10+
type: 'boolean'
11+
}
12+
};
13+
14+
let yargsInstance;
15+
16+
export function builder(yargs) {
17+
yargsInstance = yargs;
18+
return yargs.options(securityOptions).example(
19+
'git node security --start',
20+
'Prepare a security release of Node.js');
21+
}
22+
23+
export function handler(argv) {
24+
if (argv.start) {
25+
return startSecurityRelease(argv);
26+
}
27+
yargsInstance.showHelp();
28+
}
29+
30+
async function startSecurityRelease(argv) {
31+
const logStream = process.stdout.isTTY ? process.stdout : process.stderr;
32+
const cli = new CLI(logStream);
33+
const release = new SecurityReleaseSteward(cli);
34+
return release.start();
35+
}

docs/git-node.md

+27
Original file line numberDiff line numberDiff line change
@@ -427,6 +427,32 @@ $ git node vote \
427427
==============================================================================
428428
```
429429

430+
## `git node security`
431+
432+
Manage or starts a security release process.
433+
434+
<a id="git-node-security-prerequisites"></a>
435+
436+
### Prerequisites
437+
438+
It's necessary to set up `.ncurc` with HackerOne keys:
439+
440+
```console
441+
$ ncu-config --global set h1_token $H1_TOKEN
442+
$ ncu-config --global set h1_username $H1_TOKEN
443+
```
444+
445+
- `h1_token`: HackerOne Organization API Token, preferable with read-only
446+
access.
447+
- `h1_username`: HackerOne API Token username.
448+
449+
### `git node security --start`
450+
451+
This command creates the Next Security Issue in Node.js private repository
452+
following the [Security Release Process][] document.
453+
It will retrieve all the triaged HackerOne reports and add them to the list
454+
with the affected release line.
455+
430456
## `git node status`
431457

432458
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
488514
```
489515

490516
[node.js abi version registry]: https://github.com/nodejs/node/blob/main/doc/abi_version_registry.json
517+
[Security Release Process]: https://github.com/nodejs/node/blob/main/doc/contributing/security-release-process.md

lib/auth.js

+14
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,20 @@ async function auth(
107107
check(username, jenkins_token);
108108
result.jenkins = encode(username, jenkins_token);
109109
}
110+
111+
if (options.h1) {
112+
const { h1_username, h1_token } = getMergedConfig();
113+
if (!h1_username || !h1_token) {
114+
errorExit(
115+
'Get your HackerOne API token in ' +
116+
'https://docs.hackerone.com/organizations/api-tokens.html ' +
117+
'and run the following command to add it to your ncu config: ' +
118+
'ncu-config --global set h1_token TOKEN or ' +
119+
'ncu-config --global set h1_username USERNAME'
120+
);
121+
};
122+
result.h1 = encode(h1_username, h1_token);
123+
}
110124
return result;
111125
}
112126

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
## Planning
2+
3+
* [X] Open an [issue](https://github.com/nodejs-private/node-private) titled
4+
`Next Security Release`, and put this checklist in the description.
5+
6+
* [ ] Get agreement on the list of vulnerabilities to be addressed:
7+
%REPORTS%
8+
9+
* [ ] PR release announcements in [private](https://github.com/nodejs-private/nodejs.org-private):
10+
* [ ] pre-release: %PRE_RELEASE_PRIV%
11+
* [ ] post-release: %POS_RELEASE_PRIV%
12+
* List vulnerabilities in order of descending severity
13+
* Ask the HackerOne reporter if they would like to be credited on the
14+
security release blog page
15+
16+
* [ ] Get agreement on the planned date for the release: %RELEASE_DATE%
17+
18+
* [ ] Get release team volunteers for all affected lines:
19+
%AFFECTED_LINES%
20+
21+
## Announcement (one week in advance of the planned release)
22+
23+
* [ ] Verify that GitHub Actions are working as normal: <https://www.githubstatus.com/>.
24+
25+
* [ ] Check that all vulnerabilities are ready for release integration:
26+
* PRs against all affected release lines or cherry-pick clean
27+
* Approved
28+
* (optional) Approved by the reporter
29+
* Build and send the binary to the reporter according to its architecture
30+
and ask for a review. This step is important to avoid insufficient fixes
31+
between Security Releases.
32+
* Have CVEs
33+
* Make sure that dependent libraries have CVEs for their issues. We should
34+
only create CVEs for vulnerabilities in Node.js itself. This is to avoid
35+
having duplicate CVEs for the same vulnerability.
36+
* Described in the pre/post announcements
37+
38+
* [ ] Pre-release announcement to nodejs.org blog: TBD
39+
(Re-PR the pre-approved branch from nodejs-private/nodejs.org-private to
40+
nodejs/nodejs.org)
41+
42+
* [ ] Pre-release announcement [email](https://groups.google.com/forum/#!forum/nodejs-sec): TBD
43+
* Subject: `Node.js security updates for all active release lines, Month Year`
44+
45+
* [ ] CC `oss-security@lists.openwall.com` on pre-release
46+
* [ ] Forward the email you receive to `oss-security@lists.openwall.com`.
47+
48+
* [ ] Create a new issue in [nodejs/tweet](https://github.com/nodejs/tweet/issues)
49+
50+
* [ ] Request releaser(s) to start integrating the PRs to be released.
51+
52+
* [ ] Notify [docker-node](https://github.com/nodejs/docker-node/issues) of upcoming security release date: TBD
53+
54+
* [ ] Notify build-wg of upcoming security release date by opening an issue
55+
in [nodejs/build](https://github.com/nodejs/build/issues) to request WG members are available to fix any CI issues: TBD
56+
57+
## Release day
58+
59+
* [ ] [Lock CI](https://github.com/nodejs/build/blob/HEAD/doc/jenkins-guide.md#before-the-release)
60+
61+
* [ ] The releaser(s) run the release process to completion.
62+
63+
* [ ] [Unlock CI](https://github.com/nodejs/build/blob/HEAD/doc/jenkins-guide.md#after-the-release)
64+
65+
* [ ] Post-release announcement to Nodejs.org blog: https://github.com/nodejs/nodejs.org/pull/5447
66+
* (Re-PR the pre-approved branch from nodejs-private/nodejs.org-private to
67+
nodejs/nodejs.org)
68+
69+
* [ ] Post-release announcement in reply email: TBD
70+
71+
* [ ] Create a new issue in nodejs/tweet
72+
73+
* [ ] Comment in [docker-node][] issue that release is ready for integration.
74+
The docker-node team will build and release docker image updates.
75+
76+
* [ ] For every H1 report resolved:
77+
* Close as Resolved
78+
* Request Disclosure
79+
* Request publication of H1 CVE requests
80+
* (Check that the "Version Fixed" field in the CVE is correct, and provide
81+
links to the release blogs in the "Public Reference" section)
82+
83+
* [ ] PR machine-readable JSON descriptions of the vulnerabilities to the
84+
[core](https://github.com/nodejs/security-wg/tree/HEAD/vuln/core)
85+
vulnerability DB. https://github.com/nodejs/security-wg/pull/1029
86+
* For each vulnerability add a `#.json` file, one can copy an existing
87+
[json](https://github.com/nodejs/security-wg/blob/0d82062d917cb9ddab88f910559469b2b13812bf/vuln/core/78.json)
88+
file, and increment the latest created file number and use that as the name
89+
of the new file to be added. For example, `79.json`.
90+
91+
* [ ] Close this issue
92+
93+
* [ ] Make sure the PRs for the vulnerabilities are closed.
94+
95+
* [ ] PR in that you stewarded the release in
96+
[Security release stewards](https://github.com/nodejs/node/blob/HEAD/doc/contributing/security-release-process.md#security-release-stewards).
97+
If necessary add the next rotation of the steward rotation.

lib/prepare_security.js

+117
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
import nv from '@pkgjs/nv';
2+
import auth from './auth.js';
3+
import Request from './request.js';
4+
import fs from 'node:fs';
5+
6+
export default class SecurityReleaseSteward {
7+
constructor(cli) {
8+
this.cli = cli;
9+
}
10+
11+
async start() {
12+
const { cli } = this;
13+
const credentials = await auth({
14+
github: true,
15+
h1: true
16+
});
17+
18+
const req = new Request(credentials);
19+
const create = await cli.prompt(
20+
'Create the Next Security Release issue?',
21+
{ defaultAnswer: true });
22+
if (create) {
23+
const issue = new SecurityReleaseIssue(req);
24+
const content = await issue.buildIssue(cli);
25+
const data = await req.createIssue('Next Security Release', content, {
26+
owner: 'nodejs-private',
27+
repo: 'node-private'
28+
});
29+
if (data.html_url) {
30+
cli.ok('Created: ' + data.html_url);
31+
} else {
32+
cli.error(data);
33+
}
34+
}
35+
}
36+
}
37+
38+
class SecurityReleaseIssue {
39+
constructor(req) {
40+
this.req = req;
41+
this.content = '';
42+
this.title = 'Next Security Release';
43+
this.affectedLines = {};
44+
}
45+
46+
getSecurityIssueTemplate() {
47+
return fs.readFileSync(
48+
new URL(
49+
'./github/templates/next-security-release.md',
50+
import.meta.url
51+
),
52+
'utf-8'
53+
);
54+
}
55+
56+
async buildIssue(cli) {
57+
this.content = this.getSecurityIssueTemplate();
58+
cli.info('Getting triaged H1 reports...');
59+
const reports = await this.req.getTriagedReports();
60+
await this.fillReports(cli, reports);
61+
62+
this.fillAffectedLines(Object.keys(this.affectedLines));
63+
64+
const target = await cli.prompt('Enter target date in YYYY-MM-DD format:', {
65+
questionType: 'input',
66+
defaultAnswer: 'TBD'
67+
});
68+
this.fillTargetDate(target);
69+
70+
return this.content;
71+
}
72+
73+
async fillReports(cli, reports) {
74+
const supportedVersions = (await nv('supported'))
75+
.map((v) => v.versionName + '.x')
76+
.join(',');
77+
78+
let reportsContent = '';
79+
for (const report of reports.data) {
80+
const { id, attributes: { title }, relationships: { severity } } = report;
81+
const reportLevel = severity.data.attributes.rating;
82+
cli.separator();
83+
cli.info(`Report: ${id} - ${title} (${reportLevel})`);
84+
const include = await cli.prompt(
85+
'Would you like to include this report to the next security release?',
86+
{ defaultAnswer: true });
87+
if (!include) {
88+
continue;
89+
}
90+
91+
reportsContent +=
92+
` * **[${id}](https://hackerone.com/bugs?subject=nodejs&report_id=${id}) - ${title} (TBD) - (${reportLevel})**\n`;
93+
const versions = await cli.prompt('Which active release lines this report affects?', {
94+
questionType: 'input',
95+
defaultAnswer: supportedVersions
96+
});
97+
for (const v of versions.split(',')) {
98+
if (!this.affectedLines[v]) this.affectedLines[v] = true;
99+
reportsContent += ` * ${v} - TBD\n`;
100+
}
101+
}
102+
this.content = this.content.replace('%REPORTS%', reportsContent);
103+
}
104+
105+
fillAffectedLines(affectedLines) {
106+
let affected = '';
107+
for (const line of affectedLines) {
108+
affected += ` * ${line} - TBD\n`;
109+
}
110+
this.content =
111+
this.content.replace('%AFFECTED_LINES%', affected);
112+
}
113+
114+
fillTargetDate(date) {
115+
this.content = this.content.replace('%RELEASE_DATE%', date);
116+
}
117+
}

lib/request.js

+30
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,23 @@ export default class Request {
6060
}
6161
}
6262

63+
async createIssue(title, body, { owner, repo }) {
64+
const url = `https://api.github.com/repos/${owner}/${repo}/issues`;
65+
const options = {
66+
method: 'POST',
67+
headers: {
68+
Authorization: `Basic ${this.credentials.github}`,
69+
'User-Agent': 'node-core-utils',
70+
Accept: 'application/vnd.github+json'
71+
},
72+
body: JSON.stringify({
73+
title,
74+
body
75+
})
76+
};
77+
return this.json(url, options);
78+
}
79+
6380
async gql(name, variables, path) {
6481
const query = this.loadQuery(name);
6582
if (path) {
@@ -83,6 +100,19 @@ export default class Request {
83100
};
84101
}
85102

103+
async getTriagedReports() {
104+
const url = 'https://api.hackerone.com/v1/reports?filter[program][]=nodejs&filter[state][]=triaged';
105+
const options = {
106+
method: 'GET',
107+
headers: {
108+
Authorization: `Basic ${this.credentials.h1}`,
109+
'User-Agent': 'node-core-utils',
110+
Accept: 'application/json'
111+
}
112+
};
113+
return this.json(url, options);
114+
}
115+
86116
// This is for github v4 API queries, for other types of queries
87117
// use .text or .json
88118
async query(query, variables) {

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
"branch-diff": "^2.1.4",
3939
"chalk": "^5.3.0",
4040
"changelog-maker": "^3.2.4",
41+
"@pkgjs/nv": "^0.2.1",
4142
"cheerio": "^1.0.0-rc.12",
4243
"clipboardy": "^3.0.0",
4344
"core-validate-commit": "^4.0.0",

0 commit comments

Comments
 (0)