@node-core/utils 4.3.0 → 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,244 +1,177 @@
1
- import nv from '@pkgjs/nv';
2
- import auth from './auth.js';
3
- import Request from './request.js';
4
1
  import fs from 'node:fs';
5
- import { runSync } from './run.js';
6
2
  import path from 'node:path';
7
-
8
- export const PLACEHOLDERS = {
9
- releaseDate: '%RELEASE_DATE%',
10
- vulnerabilitiesPRURL: '%VULNERABILITIES_PR_URL%',
11
- preReleasePrivate: '%PRE_RELEASE_PRIV%',
12
- postReleasePrivate: '%POS_RELEASE_PRIV%',
13
- affectedLines: '%AFFECTED_LINES%'
14
- };
15
-
16
- export default class SecurityReleaseSteward {
3
+ import auth from './auth.js';
4
+ import Request from './request.js';
5
+ import {
6
+ NEXT_SECURITY_RELEASE_BRANCH,
7
+ NEXT_SECURITY_RELEASE_FOLDER,
8
+ NEXT_SECURITY_RELEASE_REPOSITORY,
9
+ PLACEHOLDERS,
10
+ checkoutOnSecurityReleaseBranch,
11
+ commitAndPushVulnerabilitiesJSON,
12
+ validateDate,
13
+ promptDependencies,
14
+ getSupportedVersions,
15
+ pickReport
16
+ } from './security-release/security-release.js';
17
+ import _ from 'lodash';
18
+
19
+ export default class PrepareSecurityRelease {
20
+ repository = NEXT_SECURITY_RELEASE_REPOSITORY;
21
+ title = 'Next Security Release';
17
22
  constructor(cli) {
18
23
  this.cli = cli;
19
24
  }
20
25
 
21
26
  async start() {
22
- const { cli } = this;
23
27
  const credentials = await auth({
24
28
  github: true,
25
29
  h1: true
26
30
  });
27
31
 
28
- const req = new Request(credentials);
29
- const release = new PrepareSecurityRelease(req);
30
- const releaseDate = await release.promptReleaseDate(cli);
31
- let securityReleasePRUrl = PLACEHOLDERS.vulnerabilitiesPRURL;
32
+ this.req = new Request(credentials);
33
+ const releaseDate = await this.promptReleaseDate();
34
+ if (releaseDate !== 'TBD') {
35
+ validateDate(releaseDate);
36
+ }
37
+ const createVulnerabilitiesJSON = await this.promptVulnerabilitiesJSON();
32
38
 
33
- const createVulnerabilitiesJSON = await release.promptVulnerabilitiesJSON(cli);
39
+ let securityReleasePRUrl;
34
40
  if (createVulnerabilitiesJSON) {
35
- securityReleasePRUrl = await this.createVulnerabilitiesJSON(req, release, { cli });
41
+ securityReleasePRUrl = await this.startVulnerabilitiesJSONCreation(releaseDate);
36
42
  }
37
43
 
38
- const createIssue = await release.promptCreateRelaseIssue(cli);
44
+ const createIssue = await this.promptCreateRelaseIssue();
39
45
 
40
46
  if (createIssue) {
41
- const { content } = release.buildIssue(releaseDate, securityReleasePRUrl);
42
- await release.createIssue(content, { cli });
47
+ const content = await this.buildIssue(releaseDate, securityReleasePRUrl);
48
+ await createIssue(
49
+ this.title, content, this.repository, { cli: this.cli, repository: this.repository });
43
50
  };
44
51
 
45
- cli.ok('Done!');
52
+ this.cli.ok('Done!');
46
53
  }
47
54
 
48
- async createVulnerabilitiesJSON(req, release, { cli }) {
55
+ async startVulnerabilitiesJSONCreation(releaseDate) {
49
56
  // checkout on the next-security-release branch
50
- release.checkoutOnSecurityReleaseBranch(cli);
57
+ checkoutOnSecurityReleaseBranch(this.cli, this.repository);
51
58
 
52
59
  // choose the reports to include in the security release
53
- const reports = await release.chooseReports(cli);
60
+ const reports = await this.chooseReports();
61
+ const depUpdates = await this.getDependencyUpdates();
62
+ const deps = _.groupBy(depUpdates, 'name');
54
63
 
55
64
  // create the vulnerabilities.json file in the security-release repo
56
- const filePath = await release.createVulnerabilitiesJSON(reports, { cli });
65
+ const filePath = await this.createVulnerabilitiesJSON(reports, deps, releaseDate);
57
66
 
58
67
  // review the vulnerabilities.json file
59
- const review = await release.promptReviewVulnerabilitiesJSON(cli);
68
+ const review = await this.promptReviewVulnerabilitiesJSON();
60
69
 
61
70
  if (!review) {
62
- cli.info(`To push the vulnerabilities.json file run:
71
+ this.cli.info(`To push the vulnerabilities.json file run:
63
72
  - git add ${filePath}
64
73
  - git commit -m "chore: create vulnerabilities.json for next security release"
65
- - git push -u origin next-security-release
66
- - open a PR on ${release.repository.owner}/${release.repository.repo}`);
74
+ - git push -u origin ${NEXT_SECURITY_RELEASE_BRANCH}
75
+ - open a PR on ${this.repository.owner}/${this.repository.repo}`);
67
76
  return;
68
77
  };
69
78
 
70
79
  // commit and push the vulnerabilities.json file
71
- release.commitAndPushVulnerabilitiesJSON(filePath, cli);
80
+ const commitMessage = 'chore: create vulnerabilities.json for next security release';
81
+ commitAndPushVulnerabilitiesJSON(filePath,
82
+ commitMessage,
83
+ { cli: this.cli, repository: this.repository });
72
84
 
73
- const createPr = await release.promptCreatePR(cli);
85
+ const createPr = await this.promptCreatePR();
74
86
 
75
87
  if (!createPr) return;
76
88
 
77
89
  // create pr on the security-release repo
78
- return release.createPullRequest(req, { cli });
90
+ return this.createPullRequest();
79
91
  }
80
- }
81
92
 
82
- class PrepareSecurityRelease {
83
- repository = {
84
- owner: 'nodejs-private',
85
- repo: 'security-release'
86
- };
87
-
88
- title = 'Next Security Release';
89
- nextSecurityReleaseBranch = 'next-security-release';
90
-
91
- constructor(req, repository) {
92
- this.req = req;
93
- if (repository) {
94
- this.repository = repository;
95
- }
96
- }
97
-
98
- promptCreatePR(cli) {
99
- return cli.prompt(
93
+ promptCreatePR() {
94
+ return this.cli.prompt(
100
95
  'Create the Next Security Release PR?',
101
96
  { defaultAnswer: true });
102
97
  }
103
98
 
104
- checkRemote(cli) {
105
- const remote = runSync('git', ['ls-remote', '--get-url', 'origin']).trim();
106
- const { owner, repo } = this.repository;
107
- const securityReleaseOrigin = `https://github.com/${owner}/${repo}.git`;
108
-
109
- if (remote !== securityReleaseOrigin) {
110
- cli.error(`Wrong repository! It should be ${securityReleaseOrigin}`);
111
- process.exit(1);
99
+ async getSecurityIssueTemplate() {
100
+ const url = 'https://raw.githubusercontent.com/nodejs/node/main/doc/contributing/security-release-process.md';
101
+ try {
102
+ // fetch document from nodejs/node main so we dont need to keep a copy
103
+ const response = await fetch(url);
104
+ const body = await response.text();
105
+ // remove everything before the Planning section
106
+ const index = body.indexOf('## Planning');
107
+ if (index !== -1) {
108
+ return body.substring(index);
109
+ }
110
+ return body;
111
+ } catch (error) {
112
+ this.cli.error(`Could not retrieve the security issue template from ${url}`);
112
113
  }
113
114
  }
114
115
 
115
- commitAndPushVulnerabilitiesJSON(filePath, cli) {
116
- this.checkRemote(cli);
117
-
118
- runSync('git', ['add', filePath]);
119
- const commitMessage = 'chore: create vulnerabilities.json for next security release';
120
- runSync('git', ['commit', '-m', commitMessage]);
121
- runSync('git', ['push', '-u', 'origin', 'next-security-release']);
122
- cli.ok(`Pushed commit: ${commitMessage} to ${this.nextSecurityReleaseBranch}`);
123
- }
124
-
125
- getSecurityIssueTemplate() {
126
- return fs.readFileSync(
127
- new URL(
128
- './github/templates/next-security-release.md',
129
- import.meta.url
130
- ),
131
- 'utf-8'
132
- );
133
- }
134
-
135
- async promptReleaseDate(cli) {
136
- return cli.prompt('Enter target release date in YYYY-MM-DD format:', {
137
- questionType: 'input',
138
- defaultAnswer: 'TBD'
139
- });
116
+ async promptReleaseDate() {
117
+ const nextWeekDate = new Date();
118
+ nextWeekDate.setDate(nextWeekDate.getDate() + 7);
119
+ // Format the date as YYYY/MM/DD
120
+ const formattedDate = nextWeekDate.toISOString().slice(0, 10).replace(/-/g, '/');
121
+ return this.cli.prompt(
122
+ 'Enter target release date in YYYY/MM/DD format (TBD if not defined yet):', {
123
+ questionType: 'input',
124
+ defaultAnswer: formattedDate
125
+ });
140
126
  }
141
127
 
142
- async promptVulnerabilitiesJSON(cli) {
143
- return cli.prompt(
128
+ async promptVulnerabilitiesJSON() {
129
+ return this.cli.prompt(
144
130
  'Create the vulnerabilities.json?',
145
131
  { defaultAnswer: true });
146
132
  }
147
133
 
148
- async promptCreateRelaseIssue(cli) {
149
- return cli.prompt(
134
+ async promptCreateRelaseIssue() {
135
+ return this.cli.prompt(
150
136
  'Create the Next Security Release issue?',
151
137
  { defaultAnswer: true });
152
138
  }
153
139
 
154
- async promptReviewVulnerabilitiesJSON(cli) {
155
- return cli.prompt(
140
+ async promptReviewVulnerabilitiesJSON() {
141
+ return this.cli.prompt(
156
142
  'Please review vulnerabilities.json and press enter to proceed.',
157
143
  { defaultAnswer: true });
158
144
  }
159
145
 
160
- buildIssue(releaseDate, securityReleasePRUrl) {
161
- const template = this.getSecurityIssueTemplate();
146
+ async buildIssue(releaseDate, securityReleasePRUrl = PLACEHOLDERS.vulnerabilitiesPRURL) {
147
+ const template = await this.getSecurityIssueTemplate();
162
148
  const content = template.replace(PLACEHOLDERS.releaseDate, releaseDate)
163
149
  .replace(PLACEHOLDERS.vulnerabilitiesPRURL, securityReleasePRUrl);
164
- return { releaseDate, content, securityReleasePRUrl };
165
- }
166
-
167
- async createIssue(content, { cli }) {
168
- const data = await this.req.createIssue(this.title, content, this.repository);
169
- if (data.html_url) {
170
- cli.ok('Created: ' + data.html_url);
171
- } else {
172
- cli.error(data);
173
- process.exit(1);
174
- }
150
+ return content;
175
151
  }
176
152
 
177
- async chooseReports(cli) {
178
- cli.info('Getting triaged H1 reports...');
153
+ async chooseReports() {
154
+ this.cli.info('Getting triaged H1 reports...');
179
155
  const reports = await this.req.getTriagedReports();
180
- const supportedVersions = (await nv('supported'))
181
- .map((v) => v.versionName + '.x')
182
- .join(',');
183
156
  const selectedReports = [];
184
157
 
185
158
  for (const report of reports.data) {
186
- const { id, attributes: { title, cve_ids }, relationships: { severity } } = report;
187
- const reportLevel = severity ? severity.data.attributes.rating : 'TBD';
188
- cli.separator();
189
- cli.info(`Report: ${id} - ${title} (${reportLevel})`);
190
- const include = await cli.prompt(
191
- 'Would you like to include this report to the next security release?',
192
- { defaultAnswer: true });
193
- if (!include) {
194
- continue;
195
- }
196
-
197
- const versions = await cli.prompt('Which active release lines this report affects?', {
198
- questionType: 'input',
199
- defaultAnswer: supportedVersions
200
- });
201
- const summaryContent = await this.getSummary(id);
202
-
203
- selectedReports.push({
204
- id,
205
- title,
206
- cve_ids,
207
- severity: reportLevel,
208
- summary: summaryContent ?? '',
209
- affectedVersions: versions.split(',').map((v) => v.replace('v', '').trim())
210
- });
159
+ const rep = await pickReport(report, { cli: this.cli, req: this.req });
160
+ if (!rep) continue;
161
+ selectedReports.push(rep);
211
162
  }
212
163
  return selectedReports;
213
164
  }
214
165
 
215
- async getSummary(reportId) {
216
- const { data } = await this.req.getReport(reportId);
217
- const summaryList = data?.relationships?.summaries?.data;
218
- if (!summaryList?.length) return;
219
- const summaries = summaryList.filter((summary) => summary?.attributes?.category === 'team');
220
- if (!summaries?.length) return;
221
- return summaries?.[0].attributes?.content;
222
- }
223
-
224
- checkoutOnSecurityReleaseBranch(cli) {
225
- this.checkRemote(cli);
226
- const currentBranch = runSync('git', ['branch', '--show-current']).trim();
227
- cli.info(`Current branch: ${currentBranch} `);
228
-
229
- if (currentBranch !== this.nextSecurityReleaseBranch) {
230
- runSync('git', ['checkout', '-B', this.nextSecurityReleaseBranch]);
231
- cli.ok(`Checkout on branch: ${this.nextSecurityReleaseBranch} `);
232
- };
233
- }
234
-
235
- async createVulnerabilitiesJSON(reports, { cli }) {
236
- cli.separator('Creating vulnerabilities.json...');
166
+ async createVulnerabilitiesJSON(reports, dependencies, releaseDate) {
167
+ this.cli.separator('Creating vulnerabilities.json...');
237
168
  const file = JSON.stringify({
238
- reports
169
+ releaseDate,
170
+ reports,
171
+ dependencies
239
172
  }, null, 2);
240
173
 
241
- const folderPath = path.join(process.cwd(), 'security-release', 'next-security-release');
174
+ const folderPath = path.join(process.cwd(), NEXT_SECURITY_RELEASE_FOLDER);
242
175
  try {
243
176
  await fs.accessSync(folderPath);
244
177
  } catch (error) {
@@ -247,14 +180,14 @@ class PrepareSecurityRelease {
247
180
 
248
181
  const fullPath = path.join(folderPath, 'vulnerabilities.json');
249
182
  fs.writeFileSync(fullPath, file);
250
- cli.ok(`Created ${fullPath} `);
183
+ this.cli.ok(`Created ${fullPath} `);
251
184
 
252
185
  return fullPath;
253
186
  }
254
187
 
255
- async createPullRequest(req, { cli }) {
188
+ async createPullRequest() {
256
189
  const { owner, repo } = this.repository;
257
- const response = await req.createPullRequest(
190
+ const response = await this.req.createPullRequest(
258
191
  this.title,
259
192
  'List of vulnerabilities to be included in the next security release',
260
193
  {
@@ -267,17 +200,69 @@ class PrepareSecurityRelease {
267
200
  );
268
201
  const url = response?.html_url;
269
202
  if (url) {
270
- cli.ok('Created: ' + url);
203
+ this.cli.ok(`Created: ${url}`);
271
204
  return url;
205
+ }
206
+ if (response?.errors) {
207
+ for (const error of response.errors) {
208
+ this.cli.error(error.message);
209
+ }
272
210
  } else {
273
- if (response?.errors) {
274
- for (const error of response.errors) {
275
- cli.error(error.message);
276
- }
277
- } else {
278
- cli.error(response);
211
+ this.cli.error(response);
212
+ }
213
+ process.exit(1);
214
+ }
215
+
216
+ async getDependencyUpdates() {
217
+ const deps = [];
218
+ this.cli.log('\n');
219
+ this.cli.separator('Dependency Updates');
220
+ const updates = await this.cli.prompt('Are there dependency updates in this security release?',
221
+ {
222
+ defaultAnswer: true,
223
+ questionType: 'confirm'
224
+ });
225
+
226
+ if (!updates) return deps;
227
+
228
+ const supportedVersions = await getSupportedVersions();
229
+
230
+ let asking = true;
231
+ while (asking) {
232
+ const dep = await promptDependencies(this.cli);
233
+ if (!dep) {
234
+ asking = false;
235
+ break;
236
+ }
237
+
238
+ const name = await this.cli.prompt(
239
+ 'What is the name of the dependency that has been updated?', {
240
+ defaultAnswer: '',
241
+ questionType: 'input'
242
+ });
243
+
244
+ const versions = await this.cli.prompt(
245
+ 'Which release line does this dependency update affect?', {
246
+ defaultAnswer: supportedVersions,
247
+ questionType: 'input'
248
+ });
249
+
250
+ try {
251
+ const prUrl = dep.replace('https://github.com/', 'https://api.github.com/repos/').replace('pull', 'pulls');
252
+ const res = await this.req.getPullRequest(prUrl);
253
+ const { html_url, title } = res;
254
+ deps.push({
255
+ name,
256
+ url: html_url,
257
+ title,
258
+ affectedVersions: versions.split(',').map((v) => v.replace('v', '').trim())
259
+ });
260
+ this.cli.separator();
261
+ } catch (error) {
262
+ this.cli.error('Invalid PR url. Please provide a valid PR url.');
263
+ this.cli.error(error);
279
264
  }
280
- process.exit(1);
281
265
  }
266
+ return deps;
282
267
  }
283
268
  }
package/lib/request.js CHANGED
@@ -77,6 +77,18 @@ export default class Request {
77
77
  return this.json(url, options);
78
78
  }
79
79
 
80
+ async getPullRequest(url) {
81
+ const options = {
82
+ method: 'GET',
83
+ headers: {
84
+ Authorization: `Basic ${this.credentials.github}`,
85
+ 'User-Agent': 'node-core-utils',
86
+ Accept: 'application/vnd.github+json'
87
+ }
88
+ };
89
+ return this.json(url, options);
90
+ }
91
+
80
92
  async createPullRequest(title, body, { owner, repo, head, base }) {
81
93
  const url = `https://api.github.com/repos/${owner}/${repo}/pulls`;
82
94
  const options = {
@@ -132,6 +144,49 @@ export default class Request {
132
144
  return this.json(url, options);
133
145
  }
134
146
 
147
+ async getPrograms() {
148
+ const url = 'https://api.hackerone.com/v1/me/programs';
149
+ const options = {
150
+ method: 'GET',
151
+ headers: {
152
+ Authorization: `Basic ${this.credentials.h1}`,
153
+ 'User-Agent': 'node-core-utils',
154
+ Accept: 'application/json'
155
+ }
156
+ };
157
+ return this.json(url, options);
158
+ }
159
+
160
+ async requestCVE(programId, opts) {
161
+ const url = `https://api.hackerone.com/v1/programs/${programId}/cve_requests`;
162
+ const options = {
163
+ method: 'POST',
164
+ headers: {
165
+ Authorization: `Basic ${this.credentials.h1}`,
166
+ 'User-Agent': 'node-core-utils',
167
+ 'Content-Type': 'application/json',
168
+ Accept: 'application/json'
169
+ },
170
+ body: JSON.stringify(opts)
171
+ };
172
+ return this.json(url, options);
173
+ }
174
+
175
+ async updateReportCVE(reportId, opts) {
176
+ const url = `https://api.hackerone.com/v1/reports/${reportId}/cves`;
177
+ const options = {
178
+ method: 'PUT',
179
+ headers: {
180
+ Authorization: `Basic ${this.credentials.h1}`,
181
+ 'User-Agent': 'node-core-utils',
182
+ Accept: 'application/json',
183
+ 'Content-Type': 'application/json'
184
+ },
185
+ body: JSON.stringify(opts)
186
+ };
187
+ return this.json(url, options);
188
+ }
189
+
135
190
  async getReport(reportId) {
136
191
  const url = `https://api.hackerone.com/v1/reports/${reportId}`;
137
192
  const options = {
@@ -0,0 +1,76 @@
1
+ import {
2
+ NEXT_SECURITY_RELEASE_REPOSITORY,
3
+ checkoutOnSecurityReleaseBranch,
4
+ getVulnerabilitiesJSON,
5
+ validateDate,
6
+ formatDateToYYYYMMDD,
7
+ createIssue
8
+ } from './security-release/security-release.js';
9
+ import auth from './auth.js';
10
+ import Request from './request.js';
11
+
12
+ export default class SecurityAnnouncement {
13
+ repository = NEXT_SECURITY_RELEASE_REPOSITORY;
14
+ req;
15
+ constructor(cli) {
16
+ this.cli = cli;
17
+ }
18
+
19
+ async notifyPreRelease() {
20
+ const { cli } = this;
21
+
22
+ const credentials = await auth({
23
+ github: true,
24
+ h1: true
25
+ });
26
+
27
+ this.req = new Request(credentials);
28
+
29
+ // checkout on security release branch
30
+ checkoutOnSecurityReleaseBranch(cli, this.repository);
31
+ // read vulnerabilities JSON file
32
+ const content = getVulnerabilitiesJSON(cli);
33
+ // validate the release date read from vulnerabilities JSON
34
+ if (!content.releaseDate) {
35
+ cli.error('Release date is not set in vulnerabilities.json,' +
36
+ ' run `git node security --update-date=YYYY/MM/DD` to set the release date.');
37
+ process.exit(1);
38
+ }
39
+
40
+ validateDate(content.releaseDate);
41
+ const releaseDate = new Date(content.releaseDate);
42
+
43
+ await Promise.all([
44
+ this.createDockerNodeIssue(releaseDate),
45
+ this.createBuildWGIssue(releaseDate)
46
+ ]);
47
+ }
48
+
49
+ async createBuildWGIssue(releaseDate) {
50
+ const repository = {
51
+ owner: 'nodejs',
52
+ repo: 'build'
53
+ };
54
+
55
+ const { title, content } = this.createPreleaseAnnouncementIssue(releaseDate, 'build');
56
+ await this.createIssue(title, content, repository);
57
+ }
58
+
59
+ createPreleaseAnnouncementIssue(releaseDate, team) {
60
+ const title = `[NEXT-SECURITY-RELEASE] Heads up on upcoming Node.js\
61
+ security release ${formatDateToYYYYMMDD(releaseDate)}`;
62
+ const content = `As per security release workflow,\
63
+ creating issue to give the ${team} team a heads up.`;
64
+ return { title, content };
65
+ }
66
+
67
+ async createDockerNodeIssue(releaseDate) {
68
+ const repository = {
69
+ owner: 'nodejs',
70
+ repo: 'docker-node'
71
+ };
72
+
73
+ const { title, content } = this.createPreleaseAnnouncementIssue(releaseDate, 'docker');
74
+ await createIssue(title, content, repository, { cli: this.cli, repository: this.repository });
75
+ }
76
+ }