Skip to content

Commit bdcb455

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

File tree

8 files changed

+316
-2
lines changed

8 files changed

+316
-2
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

+29-2
Original file line numberDiff line numberDiff line change
@@ -330,7 +330,7 @@ ncu-config set waitTimeMultiApproval 48
330330

331331
## `git node v8`
332332

333-
Update or patch the V8 engine.
333+
Update or patch the V8 engine.
334334
This tool will maintain a clone of the V8 repository in `~/.update-v8/v8`
335335
if it's used without `--v8-dir`.
336336

@@ -367,7 +367,7 @@ Options:
367367
### `git node v8 minor`
368368

369369
Compare current V8 version with latest upstream of the same major. Applies a
370-
patch if necessary.
370+
patch if necessary.
371371
If the `git apply` command fails, a patch file will be written in the Node.js
372372
clone directory.
373373

@@ -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

lib/prepare_security.js

+207
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
import nv from '@pkgjs/nv';
2+
import auth from './auth.js';
3+
import Request from './request.js';
4+
5+
const TEMPLATE = `
6+
## Planning
7+
8+
* [X] Open an [issue](https://github.com/nodejs-private/node-private) titled
9+
\`Next Security Release\`, and put this checklist in the description.
10+
11+
* [ ] Get agreement on the list of vulnerabilities to be addressed:
12+
%REPORTS%
13+
14+
* [ ] PR release announcements in [private](https://github.com/nodejs-private/nodejs.org-private):
15+
* [ ] pre-release: %PRE_RELEASE_PRIV%
16+
* [ ] post-release: %POS_RELEASE_PRIV%
17+
* List vulnerabilities in order of descending severity
18+
* Ask the HackerOne reporter if they would like to be credited on the
19+
security release blog page
20+
21+
* [ ] Get agreement on the planned date for the release: %RELEASE_DATE%
22+
23+
* [ ] Get release team volunteers for all affected lines:
24+
%AFFECTED_LINES%
25+
26+
## Announcement (one week in advance of the planned release)
27+
28+
* [ ] Verify that GitHub Actions are working as normal: <https://www.githubstatus.com/>.
29+
30+
* [ ] Check that all vulnerabilities are ready for release integration:
31+
* PRs against all affected release lines or cherry-pick clean
32+
* Approved
33+
* (optional) Approved by the reporter
34+
* Build and send the binary to the reporter according to its architecture
35+
and ask for a review. This step is important to avoid insufficient fixes
36+
between Security Releases.
37+
* Have CVEs
38+
* Make sure that dependent libraries have CVEs for their issues. We should
39+
only create CVEs for vulnerabilities in Node.js itself. This is to avoid
40+
having duplicate CVEs for the same vulnerability.
41+
* Described in the pre/post announcements
42+
43+
* [ ] Pre-release announcement to nodejs.org blog: TBD
44+
(Re-PR the pre-approved branch from nodejs-private/nodejs.org-private to
45+
nodejs/nodejs.org)
46+
47+
* [ ] Pre-release announcement [email](https://groups.google.com/forum/#!forum/nodejs-sec): TBD
48+
* Subject: \`Node.js security updates for all active release lines, Month Year\`
49+
50+
* [ ] CC \`oss-security@lists.openwall.com\` on pre-release
51+
* [ ] Forward the email you receive to \`oss-security@lists.openwall.com\`.
52+
53+
* [ ] Create a new issue in [nodejs/tweet](https://github.com/nodejs/tweet/issues)
54+
55+
* [ ] Request releaser(s) to start integrating the PRs to be released.
56+
57+
* [ ] Notify [docker-node](https://github.com/nodejs/docker-node/issues) of upcoming security release date: TBD
58+
59+
* [ ] Notify build-wg of upcoming security release date by opening an issue
60+
in [nodejs/build](https://github.com/nodejs/build/issues) to request WG members are available to fix any CI issues: TBD
61+
62+
## Release day
63+
64+
* [ ] [Lock CI](https://github.com/nodejs/build/blob/HEAD/doc/jenkins-guide.md#before-the-release)
65+
66+
* [ ] The releaser(s) run the release process to completion.
67+
68+
* [ ] [Unlock CI](https://github.com/nodejs/build/blob/HEAD/doc/jenkins-guide.md#after-the-release)
69+
70+
* [ ] Post-release announcement to Nodejs.org blog: https://github.com/nodejs/nodejs.org/pull/5447
71+
* (Re-PR the pre-approved branch from nodejs-private/nodejs.org-private to
72+
nodejs/nodejs.org)
73+
74+
* [ ] Post-release announcement in reply email: TBD
75+
76+
* [ ] Create a new issue in nodejs/tweet
77+
78+
* [ ] Comment in [docker-node][] issue that release is ready for integration.
79+
The docker-node team will build and release docker image updates.
80+
81+
* [ ] For every H1 report resolved:
82+
* Close as Resolved
83+
* Request Disclosure
84+
* Request publication of H1 CVE requests
85+
* (Check that the "Version Fixed" field in the CVE is correct, and provide
86+
links to the release blogs in the "Public Reference" section)
87+
88+
* [ ] PR machine-readable JSON descriptions of the vulnerabilities to the
89+
[core](https://github.com/nodejs/security-wg/tree/HEAD/vuln/core)
90+
vulnerability DB. https://github.com/nodejs/security-wg/pull/1029
91+
* For each vulnerability add a \`#.json\` file, one can copy an existing
92+
[json](https://github.com/nodejs/security-wg/blob/0d82062d917cb9ddab88f910559469b2b13812bf/vuln/core/78.json)
93+
file, and increment the latest created file number and use that as the name
94+
of the new file to be added. For example, \`79.json\`.
95+
96+
* [ ] Close this issue
97+
98+
* [ ] Make sure the PRs for the vulnerabilities are closed.
99+
100+
* [ ] PR in that you stewarded the release in
101+
[Security release stewards](https://github.com/nodejs/node/blob/HEAD/doc/contributing/security-release-process.md#security-release-stewards).
102+
If necessary add the next rotation of the steward rotation.
103+
`;
104+
105+
export default class SecurityReleaseSteward {
106+
constructor(cli) {
107+
this.cli = cli;
108+
}
109+
110+
async start() {
111+
const { cli } = this;
112+
const credentials = await auth({
113+
github: true,
114+
h1: true
115+
});
116+
117+
const req = new Request(credentials);
118+
const create = await cli.prompt(
119+
'Create the Next Security Release issue?',
120+
{ defaultAnswer: true });
121+
if (create) {
122+
const issue = new SecurityReleaseIssue(req);
123+
const content = await issue.buildIssue(cli);
124+
const { repository: { id } } = await req.gql('RepositoryId', {
125+
owner: 'nodejs-private',
126+
repo: 'node-private'
127+
});
128+
const data = await req.gql('CreateIssue', {
129+
repoId: id,
130+
title: 'Next Security Release',
131+
body: content
132+
});
133+
cli.ok('Created: ' + data.createIssue.issue.url);
134+
}
135+
}
136+
}
137+
138+
class SecurityReleaseIssue {
139+
constructor(req) {
140+
this.req = req;
141+
this.content = '';
142+
this.title = 'Next Security Release';
143+
this.affectedLines = {};
144+
}
145+
146+
async buildIssue(cli) {
147+
this.content = TEMPLATE;
148+
cli.info('Getting triaged H1 reports...');
149+
const reports = await this.req.getTriagedReports();
150+
await this.fillReports(cli, reports);
151+
152+
this.fillAffectedLines(Object.keys(this.affectedLines));
153+
154+
const target = await cli.prompt('Enter target date in YYYY-MM-DD format:', {
155+
questionType: 'input',
156+
defaultAnswer: 'TBD'
157+
});
158+
this.fillTargetDate(target);
159+
160+
return this.content;
161+
}
162+
163+
async fillReports(cli, reports) {
164+
const supportedVersions = (await nv('supported'))
165+
.map((v) => v.versionName + '.x')
166+
.join(',');
167+
168+
let reportsContent = '';
169+
for (const report of reports.data) {
170+
const { id, attributes: { title }, relationships: { severity } } = report;
171+
const reportLevel = severity.data.attributes.rating;
172+
cli.separator();
173+
cli.info(`Report: ${id} - ${title} (${reportLevel})`);
174+
const include = await cli.prompt(
175+
'Would you like to include this report to the next security release?',
176+
{ defaultAnswer: true });
177+
if (!include) {
178+
continue;
179+
}
180+
181+
reportsContent +=
182+
` * **[${id}](https://hackerone.com/bugs?subject=nodejs&report_id=${id}) - ${title} (TBD) - (${reportLevel})**\n`;
183+
const versions = await cli.prompt('Which active release lines this report affects?', {
184+
questionType: 'input',
185+
defaultAnswer: supportedVersions
186+
});
187+
for (const v of versions.split(',')) {
188+
if (!this.affectedLines[v]) this.affectedLines[v] = true;
189+
reportsContent += ` * ${v} - TBD\n`;
190+
}
191+
}
192+
this.content = this.content.replace('%REPORTS%', reportsContent);
193+
}
194+
195+
fillAffectedLines(affectedLines) {
196+
let affected = '';
197+
for (const line of affectedLines) {
198+
affected += ` * ${line} - TBD\n`;
199+
}
200+
this.content =
201+
this.content.replace('%AFFECTED_LINES%', affected);
202+
}
203+
204+
fillTargetDate(date) {
205+
this.content = this.content.replace('%RELEASE_DATE%', date);
206+
}
207+
}

lib/queries/CreateIssue.gql

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
mutation CreateIssue($repoId: ID!, $title: String!, $body: String!) {
2+
createIssue(input: {
3+
repositoryId: $repoId,
4+
title: $title,
5+
body: $body
6+
}) {
7+
issue {
8+
number
9+
url
10+
}
11+
}
12+
}

lib/queries/RepositoryId.gql

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
query RepositoryID($owner: String!, $repo: String!) {
2+
repository(name: $repo, owner: $owner) {
3+
id
4+
}
5+
}

lib/request.js

+13
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,19 @@ export default class Request {
8383
};
8484
}
8585

86+
async getTriagedReports() {
87+
const url = 'https://api.hackerone.com/v1/reports?filter[program][]=nodejs&filter[state][]=triaged';
88+
const options = {
89+
method: 'GET',
90+
headers: {
91+
Authorization: `Basic ${this.credentials.h1}`,
92+
'User-Agent': 'node-core-utils',
93+
Accept: 'application/json'
94+
}
95+
};
96+
return this.json(url, options);
97+
}
98+
8699
// This is for github v4 API queries, for other types of queries
87100
// use .text or .json
88101
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)