Skip to content

Commit 881fef0

Browse files
committed
feat: add git node security --finish
feat: use SecurityRelease as base class
1 parent 09cf7fd commit 881fef0

File tree

6 files changed

+199
-92
lines changed

6 files changed

+199
-92
lines changed

components/git/security.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,10 @@ const securityOptions = {
4343
'post-release': {
4444
describe: 'Create the post-release announcement',
4545
type: 'boolean'
46+
},
47+
finish: {
48+
describe: 'Finish the security release.',
49+
type: 'boolean'
4650
}
4751
};
4852

@@ -81,6 +85,9 @@ export function builder(yargs) {
8185
).example(
8286
'git node security --post-release',
8387
'Create the post-release announcement on the Nodejs.org repo'
88+
).example(
89+
'git node security --finish',
90+
'Finish the security release. Merge the PR and close H1 reports'
8491
);
8592
}
8693

@@ -112,6 +119,9 @@ export function handler(argv) {
112119
if (argv['post-release']) {
113120
return createPostRelease(argv);
114121
}
122+
if (argv.finish) {
123+
return finishSecurityRelease(argv);
124+
}
115125
yargsInstance.showHelp();
116126
}
117127

@@ -167,6 +177,13 @@ async function startSecurityRelease() {
167177
return release.start();
168178
}
169179

180+
async function finishSecurityRelease() {
181+
const logStream = process.stdout.isTTY ? process.stdout : process.stderr;
182+
const cli = new CLI(logStream);
183+
const release = new PrepareSecurityRelease(cli);
184+
return release.start();
185+
}
186+
170187
async function syncSecurityRelease(argv) {
171188
const logStream = process.stdout.isTTY ? process.stdout : process.stderr;
172189
const cli = new CLI(logStream);

lib/prepare_security.js

Lines changed: 60 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,23 +5,19 @@ import Request from './request.js';
55
import {
66
NEXT_SECURITY_RELEASE_BRANCH,
77
NEXT_SECURITY_RELEASE_FOLDER,
8-
NEXT_SECURITY_RELEASE_REPOSITORY,
98
PLACEHOLDERS,
109
checkoutOnSecurityReleaseBranch,
1110
commitAndPushVulnerabilitiesJSON,
1211
validateDate,
1312
promptDependencies,
1413
getSupportedVersions,
15-
pickReport
14+
pickReport,
15+
SecurityRelease
1616
} from './security-release/security-release.js';
1717
import _ from 'lodash';
1818

19-
export default class PrepareSecurityRelease {
20-
repository = NEXT_SECURITY_RELEASE_REPOSITORY;
19+
export default class PrepareSecurityRelease extends SecurityRelease {
2120
title = 'Next Security Release';
22-
constructor(cli) {
23-
this.cli = cli;
24-
}
2521

2622
async start() {
2723
const credentials = await auth({
@@ -52,6 +48,27 @@ export default class PrepareSecurityRelease {
5248
this.cli.ok('Done!');
5349
}
5450

51+
async finish() {
52+
const credentials = await auth({
53+
github: true,
54+
h1: true
55+
});
56+
57+
this.req = new Request(credentials);
58+
const vulnerabilityJSON = this.readVulnerabilitiesJSON();
59+
this.cli.info('Closing and request disclosure to HackerOne reports');
60+
await this.closeAndRequestDisclosure(vulnerabilityJSON.reports);
61+
62+
this.cli.info('Closing pull requests');
63+
// For now, close the ones with vN.x label
64+
await this.closePRWithLabel(this.getAffectedVersions(vulnerabilityJSON));
65+
this.cli.info(`Merge pull request with:
66+
- git checkout main
67+
- git merge --squash ${NEXT_SECURITY_RELEASE_BRANCH}
68+
- git push origin main`);
69+
this.cli.ok('Done!');
70+
}
71+
5572
async startVulnerabilitiesJSONCreation(releaseDate) {
5673
// checkout on the next-security-release branch
5774
checkoutOnSecurityReleaseBranch(this.cli, this.repository);
@@ -173,9 +190,9 @@ export default class PrepareSecurityRelease {
173190

174191
const folderPath = path.join(process.cwd(), NEXT_SECURITY_RELEASE_FOLDER);
175192
try {
176-
await fs.accessSync(folderPath);
193+
fs.accessSync(folderPath);
177194
} catch (error) {
178-
await fs.mkdirSync(folderPath, { recursive: true });
195+
fs.mkdirSync(folderPath, { recursive: true });
179196
}
180197

181198
const fullPath = path.join(folderPath, 'vulnerabilities.json');
@@ -264,4 +281,38 @@ export default class PrepareSecurityRelease {
264281
}
265282
return deps;
266283
}
284+
285+
async closeAndRequestDisclosure(jsonReports) {
286+
this.cli.startSpinner('Closing HackerOne reports');
287+
for (const report of jsonReports) {
288+
this.cli.updateSpinner(`Closing report ${report.id}...`);
289+
await this.req.updateReportState(
290+
report.id,
291+
'resolved',
292+
'Closing as resolved'
293+
);
294+
295+
this.cli.updateSpinner(`Requesting disclosure to report ${report.id}...`);
296+
await this.req.requestDisclosure(report.id);
297+
}
298+
this.cli.stopSpinner('Done closing H1 Reports and requesting disclosure');
299+
}
300+
301+
async closePRWithLabel(labels) {
302+
if (typeof labels === 'string') {
303+
labels = [labels];
304+
}
305+
306+
const url = 'https://github.com/nodejs-private/node-private/pulls';
307+
this.cli.startSpinner('Closing GitHub Pull Requests...');
308+
// At this point, GitHub does not provide filters through their REST API
309+
const prs = this.req.getPullRequest(url);
310+
for (const pr of prs) {
311+
if (pr.labels.some((l) => labels.includes(l))) {
312+
this.cli.updateSpinner(`Closing Pull Request: ${pr.id}`);
313+
await this.req.closePullRequest(pr.id);
314+
}
315+
}
316+
this.cli.startSpinner('Closed GitHub Pull Requests.');
317+
}
267318
}

lib/request.js

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,22 @@ export default class Request {
109109
return this.json(url, options);
110110
}
111111

112+
async closePullRequest({ owner, repo }) {
113+
const url = `https://api.github.com/repos/${owner}/${repo}/pulls`;
114+
const options = {
115+
method: 'POST',
116+
headers: {
117+
Authorization: `Basic ${this.credentials.github}`,
118+
'User-Agent': 'node-core-utils',
119+
Accept: 'application/vnd.github+json'
120+
},
121+
body: JSON.stringify({
122+
state: 'closed'
123+
})
124+
};
125+
return this.json(url, options);
126+
}
127+
112128
async gql(name, variables, path) {
113129
const query = this.loadQuery(name);
114130
if (path) {
@@ -201,6 +217,49 @@ export default class Request {
201217
return this.json(url, options);
202218
}
203219

220+
async updateReportState(reportId, state, message) {
221+
const url = `https://api.hackerone.com/v1/reports/${reportId}/state_changes`;
222+
const options = {
223+
method: 'POST',
224+
headers: {
225+
Authorization: `Basic ${this.credentials.h1}`,
226+
'User-Agent': 'node-core-utils',
227+
Accept: 'application/json'
228+
},
229+
body: JSON.stringify({
230+
data: {
231+
type: 'state-change',
232+
attributes: {
233+
message,
234+
state
235+
}
236+
}
237+
})
238+
};
239+
return this.json(url, options);
240+
}
241+
242+
async requestDisclosure(reportId) {
243+
const url = `https://api.hackerone.com/v1/reports/${reportId}/disclosure_requests`;
244+
const options = {
245+
method: 'POST',
246+
headers: {
247+
Authorization: `Basic ${this.credentials.h1}`,
248+
'User-Agent': 'node-core-utils',
249+
Accept: 'application/json'
250+
},
251+
body: JSON.stringify({
252+
data: {
253+
attributes: {
254+
// default to limited version
255+
substate: 'no-content'
256+
}
257+
}
258+
})
259+
};
260+
return this.json(url, options);
261+
}
262+
204263
// This is for github v4 API queries, for other types of queries
205264
// use .text or .json
206265
async query(query, variables) {

lib/security-release/security-release.js

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,3 +210,56 @@ export async function pickReport(report, { cli, req }) {
210210
reporter: reporter.data.attributes.username
211211
};
212212
}
213+
214+
export class SecurityRelease {
215+
constructor(cli, repository = NEXT_SECURITY_RELEASE_REPOSITORY) {
216+
this.cli = cli;
217+
this.repository = repository;
218+
}
219+
220+
readVulnerabilitiesJSON(vulnerabilitiesJSONPath = this.getVulnerabilitiesJSONPath()) {
221+
const exists = fs.existsSync(vulnerabilitiesJSONPath);
222+
223+
if (!exists) {
224+
this.cli.error(`The file vulnerabilities.json does not exist at ${vulnerabilitiesJSONPath}`);
225+
process.exit(1);
226+
}
227+
228+
return JSON.parse(fs.readFileSync(vulnerabilitiesJSONPath, 'utf8'));
229+
}
230+
231+
getVulnerabilitiesJSONPath() {
232+
return path.join(process.cwd(),
233+
NEXT_SECURITY_RELEASE_FOLDER, 'vulnerabilities.json');
234+
}
235+
236+
updateVulnerabilitiesJSON(content) {
237+
try {
238+
const vulnerabilitiesJSONPath = this.getVulnerabilitiesJSONPath();
239+
this.cli.startSpinner(`Updating vulnerabilities.json from ${vulnerabilitiesJSONPath}...`);
240+
fs.writeFileSync(vulnerabilitiesJSONPath, JSON.stringify(content, null, 2));
241+
commitAndPushVulnerabilitiesJSON(vulnerabilitiesJSONPath,
242+
'chore: updated vulnerabilities.json',
243+
{ cli: this.cli, repository: this.repository });
244+
this.cli.stopSpinner(`Done updating vulnerabilities.json from ${vulnerabilitiesJSONPath}`);
245+
} catch (error) {
246+
this.cli.error('Error updating vulnerabilities.json');
247+
this.cli.error(error);
248+
}
249+
}
250+
251+
getAffectedVersions(content) {
252+
const affectedVersions = new Set();
253+
for (const report of Object.values(content.reports)) {
254+
for (const affectedVersion of report.affectedVersions) {
255+
affectedVersions.add(affectedVersion);
256+
}
257+
}
258+
const parseToNumber = str => +(str.match(/[\d.]+/g)[0]);
259+
return Array.from(affectedVersions)
260+
.sort((a, b) => {
261+
return parseToNumber(a) > parseToNumber(b) ? -1 : 1;
262+
})
263+
.join(', ');
264+
}
265+
}

lib/security_blog.js

Lines changed: 4 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -4,24 +4,17 @@ import _ from 'lodash';
44
import nv from '@pkgjs/nv';
55
import {
66
PLACEHOLDERS,
7-
getVulnerabilitiesJSON,
87
checkoutOnSecurityReleaseBranch,
9-
NEXT_SECURITY_RELEASE_REPOSITORY,
108
validateDate,
11-
commitAndPushVulnerabilitiesJSON,
12-
NEXT_SECURITY_RELEASE_FOLDER
9+
SecurityRelease
1310
} from './security-release/security-release.js';
1411
import auth from './auth.js';
1512
import Request from './request.js';
1613

1714
const kChanged = Symbol('changed');
1815

19-
export default class SecurityBlog {
20-
repository = NEXT_SECURITY_RELEASE_REPOSITORY;
16+
export default class SecurityBlog extends SecurityRelease {
2117
req;
22-
constructor(cli) {
23-
this.cli = cli;
24-
}
2518

2619
async createPreRelease() {
2720
const { cli } = this;
@@ -30,7 +23,7 @@ export default class SecurityBlog {
3023
checkoutOnSecurityReleaseBranch(cli, this.repository);
3124

3225
// read vulnerabilities JSON file
33-
const content = getVulnerabilitiesJSON(cli);
26+
const content = this.readVulnerabilitiesJSON();
3427
// validate the release date read from vulnerabilities JSON
3528
if (!content.releaseDate) {
3629
cli.error('Release date is not set in vulnerabilities.json,' +
@@ -72,7 +65,7 @@ export default class SecurityBlog {
7265
checkoutOnSecurityReleaseBranch(cli, this.repository);
7366

7467
// read vulnerabilities JSON file
75-
const content = getVulnerabilitiesJSON(cli);
68+
const content = this.readVulnerabilitiesJSON(cli);
7669
if (!content.releaseDate) {
7770
cli.error('Release date is not set in vulnerabilities.json,' +
7871
' run `git node security --update-date=YYYY/MM/DD` to set the release date.');
@@ -113,22 +106,6 @@ export default class SecurityBlog {
113106
this.updateVulnerabilitiesJSON(content);
114107
}
115108

116-
updateVulnerabilitiesJSON(content) {
117-
try {
118-
this.cli.info('Updating vulnerabilities.json');
119-
const vulnerabilitiesJSONPath = path.join(process.cwd(),
120-
NEXT_SECURITY_RELEASE_FOLDER, 'vulnerabilities.json');
121-
fs.writeFileSync(vulnerabilitiesJSONPath, JSON.stringify(content, null, 2));
122-
const commitMessage = 'chore: updated vulnerabilities.json';
123-
commitAndPushVulnerabilitiesJSON(vulnerabilitiesJSONPath,
124-
commitMessage,
125-
{ cli: this.cli, repository: this.repository });
126-
} catch (error) {
127-
this.cli.error('Error updating vulnerabilities.json');
128-
this.cli.error(error);
129-
}
130-
}
131-
132109
async promptExistingPreRelease(cli) {
133110
const pathPreRelease = await cli.prompt(
134111
'Please provide the path of the existing pre-release announcement:', {
@@ -324,21 +301,6 @@ export default class SecurityBlog {
324301
return text.join('\n');
325302
}
326303

327-
getAffectedVersions(content) {
328-
const affectedVersions = new Set();
329-
for (const report of Object.values(content.reports)) {
330-
for (const affectedVersion of report.affectedVersions) {
331-
affectedVersions.add(affectedVersion);
332-
}
333-
}
334-
const parseToNumber = str => +(str.match(/[\d.]+/g)[0]);
335-
return Array.from(affectedVersions)
336-
.sort((a, b) => {
337-
return parseToNumber(a) > parseToNumber(b) ? -1 : 1;
338-
})
339-
.join(', ');
340-
}
341-
342304
getSecurityPreReleaseTemplate() {
343305
return fs.readFileSync(
344306
new URL(

0 commit comments

Comments
 (0)