@node-core/utils 4.4.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.
package/bin/ncu-ci.js CHANGED
@@ -113,6 +113,11 @@ const args = yargs(hideBin(process.argv))
113
113
  describe: 'ID of the PR',
114
114
  type: 'number'
115
115
  })
116
+ .positional('certify-safe', {
117
+ describe: 'If not provided, the command will reject PRs that have ' +
118
+ 'been pushed since the last review',
119
+ type: 'boolean'
120
+ })
116
121
  .option('owner', {
117
122
  default: '',
118
123
  describe: 'GitHub repository owner'
@@ -291,7 +296,7 @@ class RunPRJobCommand {
291
296
  this.cli.setExitCode(1);
292
297
  return;
293
298
  }
294
- const jobRunner = new RunPRJob(cli, request, owner, repo, prid);
299
+ const jobRunner = new RunPRJob(cli, request, owner, repo, prid, this.argv.certifySafe);
295
300
  if (!(await jobRunner.start())) {
296
301
  this.cli.setExitCode(1);
297
302
  process.exitCode = 1;
@@ -1,5 +1,5 @@
1
1
  import CLI from '../../lib/cli.js';
2
- import SecurityReleaseSteward from '../../lib/prepare_security.js';
2
+ import PrepareSecurityRelease from '../../lib/prepare_security.js';
3
3
  import UpdateSecurityRelease from '../../lib/update_security_release.js';
4
4
  import SecurityBlog from '../../lib/security_blog.js';
5
5
  import SecurityAnnouncement from '../../lib/security-announcement.js';
@@ -31,6 +31,10 @@ const securityOptions = {
31
31
  'notify-pre-release': {
32
32
  describe: 'Notify the community about the security release',
33
33
  type: 'boolean'
34
+ },
35
+ 'request-cve': {
36
+ describe: 'Request CVEs for a security release',
37
+ type: 'boolean'
34
38
  }
35
39
  };
36
40
 
@@ -60,6 +64,11 @@ export function builder(yargs) {
60
64
  ).example(
61
65
  'git node security --notify-pre-release' +
62
66
  'Notifies the community about the security release'
67
+ )
68
+ .example(
69
+ 'git node security --request-cve',
70
+ 'Request CVEs for a security release of Node.js based on' +
71
+ ' the next-security-release/vulnerabilities.json'
63
72
  );
64
73
  }
65
74
 
@@ -82,6 +91,9 @@ export function handler(argv) {
82
91
  if (argv['notify-pre-release']) {
83
92
  return notifyPreRelease(argv);
84
93
  }
94
+ if (argv['request-cve']) {
95
+ return requestCVEs(argv);
96
+ }
85
97
  yargsInstance.showHelp();
86
98
  }
87
99
 
@@ -116,10 +128,17 @@ async function createPreRelease() {
116
128
  return preRelease.createPreRelease();
117
129
  }
118
130
 
119
- async function startSecurityRelease() {
131
+ async function requestCVEs() {
132
+ const logStream = process.stdout.isTTY ? process.stdout : process.stderr;
133
+ const cli = new CLI(logStream);
134
+ const hackerOneCve = new UpdateSecurityRelease(cli);
135
+ return hackerOneCve.requestCVEs();
136
+ }
137
+
138
+ async function startSecurityRelease(argv) {
120
139
  const logStream = process.stdout.isTTY ? process.stdout : process.stderr;
121
140
  const cli = new CLI(logStream);
122
- const release = new SecurityReleaseSteward(cli);
141
+ const release = new PrepareSecurityRelease(cli);
123
142
  return release.start();
124
143
  }
125
144
 
package/lib/ci/run_ci.js CHANGED
@@ -7,6 +7,7 @@ import {
7
7
  } from './ci_type_parser.js';
8
8
  import PRData from '../pr_data.js';
9
9
  import { debuglog } from '../verbosity.js';
10
+ import PRChecker from '../pr_checker.js';
10
11
 
11
12
  export const CI_CRUMB_URL = `https://${CI_DOMAIN}/crumbIssuer/api/json`;
12
13
  const CI_PR_NAME = CI_TYPES.get(CI_TYPES_KEYS.PR).jobName;
@@ -16,13 +17,16 @@ const CI_V8_NAME = CI_TYPES.get(CI_TYPES_KEYS.V8).jobName;
16
17
  export const CI_V8_URL = `https://${CI_DOMAIN}/job/${CI_V8_NAME}/build`;
17
18
 
18
19
  export class RunPRJob {
19
- constructor(cli, request, owner, repo, prid) {
20
+ constructor(cli, request, owner, repo, prid, certifySafe) {
20
21
  this.cli = cli;
21
22
  this.request = request;
22
23
  this.owner = owner;
23
24
  this.repo = repo;
24
25
  this.prid = prid;
25
26
  this.prData = new PRData({ prid, owner, repo }, cli, request);
27
+ this.certifySafe =
28
+ certifySafe ||
29
+ new PRChecker(cli, this.prData, request, {}).checkCommitsAfterReview();
26
30
  }
27
31
 
28
32
  async getCrumb() {
@@ -62,7 +66,13 @@ export class RunPRJob {
62
66
  }
63
67
 
64
68
  async start() {
65
- const { cli } = this;
69
+ const { cli, certifySafe } = this;
70
+
71
+ if (!certifySafe) {
72
+ cli.error('Refusing to run CI on potentially unsafe PR');
73
+ return false;
74
+ }
75
+
66
76
  cli.startSpinner('Validating Jenkins credentials');
67
77
  const crumb = await this.getCrumb();
68
78
 
package/lib/pr_checker.js CHANGED
@@ -534,6 +534,7 @@ export default class PRChecker {
534
534
  );
535
535
 
536
536
  if (reviewIndex === -1) {
537
+ cli.warn('No approving reviews found');
537
538
  return false;
538
539
  }
539
540
 
@@ -1,4 +1,3 @@
1
- import nv from '@pkgjs/nv';
2
1
  import fs from 'node:fs';
3
2
  import path from 'node:path';
4
3
  import auth from './auth.js';
@@ -10,92 +9,89 @@ import {
10
9
  PLACEHOLDERS,
11
10
  checkoutOnSecurityReleaseBranch,
12
11
  commitAndPushVulnerabilitiesJSON,
13
- getSummary,
14
- validateDate
12
+ validateDate,
13
+ promptDependencies,
14
+ getSupportedVersions,
15
+ pickReport
15
16
  } from './security-release/security-release.js';
17
+ import _ from 'lodash';
16
18
 
17
- export default class SecurityReleaseSteward {
19
+ export default class PrepareSecurityRelease {
18
20
  repository = NEXT_SECURITY_RELEASE_REPOSITORY;
21
+ title = 'Next Security Release';
19
22
  constructor(cli) {
20
23
  this.cli = cli;
21
24
  }
22
25
 
23
26
  async start() {
24
- const { cli } = this;
25
27
  const credentials = await auth({
26
28
  github: true,
27
29
  h1: true
28
30
  });
29
31
 
30
- const req = new Request(credentials);
31
- const release = new PrepareSecurityRelease(req);
32
- const releaseDate = await release.promptReleaseDate(cli);
33
- validateDate(releaseDate);
34
- const createVulnerabilitiesJSON = await release.promptVulnerabilitiesJSON(cli);
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();
35
38
 
36
39
  let securityReleasePRUrl;
37
40
  if (createVulnerabilitiesJSON) {
38
- securityReleasePRUrl = await this.createVulnerabilitiesJSON(req, release, { cli });
41
+ securityReleasePRUrl = await this.startVulnerabilitiesJSONCreation(releaseDate);
39
42
  }
40
43
 
41
- const createIssue = await release.promptCreateRelaseIssue(cli);
44
+ const createIssue = await this.promptCreateRelaseIssue();
42
45
 
43
46
  if (createIssue) {
44
- const content = await release.buildIssue(releaseDate, securityReleasePRUrl);
45
- 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 });
46
50
  };
47
51
 
48
- cli.ok('Done!');
52
+ this.cli.ok('Done!');
49
53
  }
50
54
 
51
- async createVulnerabilitiesJSON(req, release, { cli }) {
55
+ async startVulnerabilitiesJSONCreation(releaseDate) {
52
56
  // checkout on the next-security-release branch
53
- checkoutOnSecurityReleaseBranch(cli, this.repository);
57
+ checkoutOnSecurityReleaseBranch(this.cli, this.repository);
54
58
 
55
59
  // choose the reports to include in the security release
56
- const reports = await release.chooseReports(cli);
60
+ const reports = await this.chooseReports();
61
+ const depUpdates = await this.getDependencyUpdates();
62
+ const deps = _.groupBy(depUpdates, 'name');
57
63
 
58
64
  // create the vulnerabilities.json file in the security-release repo
59
- const filePath = await release.createVulnerabilitiesJSON(reports, { cli });
65
+ const filePath = await this.createVulnerabilitiesJSON(reports, deps, releaseDate);
60
66
 
61
67
  // review the vulnerabilities.json file
62
- const review = await release.promptReviewVulnerabilitiesJSON(cli);
68
+ const review = await this.promptReviewVulnerabilitiesJSON();
63
69
 
64
70
  if (!review) {
65
- cli.info(`To push the vulnerabilities.json file run:
71
+ this.cli.info(`To push the vulnerabilities.json file run:
66
72
  - git add ${filePath}
67
73
  - git commit -m "chore: create vulnerabilities.json for next security release"
68
74
  - git push -u origin ${NEXT_SECURITY_RELEASE_BRANCH}
69
- - open a PR on ${release.repository.owner}/${release.repository.repo}`);
75
+ - open a PR on ${this.repository.owner}/${this.repository.repo}`);
70
76
  return;
71
77
  };
72
78
 
73
79
  // commit and push the vulnerabilities.json file
74
80
  const commitMessage = 'chore: create vulnerabilities.json for next security release';
75
- commitAndPushVulnerabilitiesJSON(filePath, commitMessage, { cli, repository: this.repository });
81
+ commitAndPushVulnerabilitiesJSON(filePath,
82
+ commitMessage,
83
+ { cli: this.cli, repository: this.repository });
76
84
 
77
- const createPr = await release.promptCreatePR(cli);
85
+ const createPr = await this.promptCreatePR();
78
86
 
79
87
  if (!createPr) return;
80
88
 
81
89
  // create pr on the security-release repo
82
- return release.createPullRequest(req, { cli });
83
- }
84
- }
85
-
86
- class PrepareSecurityRelease {
87
- repository = NEXT_SECURITY_RELEASE_REPOSITORY;
88
- title = 'Next Security Release';
89
-
90
- constructor(req, repository) {
91
- this.req = req;
92
- if (repository) {
93
- this.repository = repository;
94
- }
90
+ return this.createPullRequest();
95
91
  }
96
92
 
97
- promptCreatePR(cli) {
98
- return cli.prompt(
93
+ promptCreatePR() {
94
+ return this.cli.prompt(
99
95
  'Create the Next Security Release PR?',
100
96
  { defaultAnswer: true });
101
97
  }
@@ -117,31 +113,32 @@ class PrepareSecurityRelease {
117
113
  }
118
114
  }
119
115
 
120
- async promptReleaseDate(cli) {
116
+ async promptReleaseDate() {
121
117
  const nextWeekDate = new Date();
122
118
  nextWeekDate.setDate(nextWeekDate.getDate() + 7);
123
119
  // Format the date as YYYY/MM/DD
124
120
  const formattedDate = nextWeekDate.toISOString().slice(0, 10).replace(/-/g, '/');
125
- return cli.prompt('Enter target release date in YYYY/MM/DD format:', {
126
- questionType: 'input',
127
- defaultAnswer: formattedDate
128
- });
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
+ });
129
126
  }
130
127
 
131
- async promptVulnerabilitiesJSON(cli) {
132
- return cli.prompt(
128
+ async promptVulnerabilitiesJSON() {
129
+ return this.cli.prompt(
133
130
  'Create the vulnerabilities.json?',
134
131
  { defaultAnswer: true });
135
132
  }
136
133
 
137
- async promptCreateRelaseIssue(cli) {
138
- return cli.prompt(
134
+ async promptCreateRelaseIssue() {
135
+ return this.cli.prompt(
139
136
  'Create the Next Security Release issue?',
140
137
  { defaultAnswer: true });
141
138
  }
142
139
 
143
- async promptReviewVulnerabilitiesJSON(cli) {
144
- return cli.prompt(
140
+ async promptReviewVulnerabilitiesJSON() {
141
+ return this.cli.prompt(
145
142
  'Please review vulnerabilities.json and press enter to proceed.',
146
143
  { defaultAnswer: true });
147
144
  }
@@ -153,78 +150,25 @@ class PrepareSecurityRelease {
153
150
  return content;
154
151
  }
155
152
 
156
- async createIssue(content, { cli }) {
157
- const data = await this.req.createIssue(this.title, content, this.repository);
158
- if (data.html_url) {
159
- cli.ok(`Created: ${data.html_url}`);
160
- } else {
161
- cli.error(data);
162
- process.exit(1);
163
- }
164
- }
165
-
166
- async chooseReports(cli) {
167
- cli.info('Getting triaged H1 reports...');
153
+ async chooseReports() {
154
+ this.cli.info('Getting triaged H1 reports...');
168
155
  const reports = await this.req.getTriagedReports();
169
- const supportedVersions = (await nv('supported'))
170
- .map((v) => `${v.versionName}.x`)
171
- .join(',');
172
156
  const selectedReports = [];
173
157
 
174
158
  for (const report of reports.data) {
175
- const {
176
- id, attributes: { title, cve_ids, created_at },
177
- relationships: { severity, weakness, reporter }
178
- } = report;
179
- const link = `https://hackerone.com/reports/${id}`;
180
- let reportSeverity = {
181
- rating: '',
182
- cvss_vector_string: '',
183
- weakness_id: ''
184
- };
185
- if (severity?.data?.attributes?.cvss_vector_string) {
186
- const { cvss_vector_string, rating } = severity.data.attributes;
187
- reportSeverity = {
188
- cvss_vector_string,
189
- rating,
190
- weakness_id: weakness?.data?.id
191
- };
192
- }
193
-
194
- cli.separator();
195
- cli.info(`Report: ${link} - ${title} (${reportSeverity?.rating})`);
196
- const include = await cli.prompt(
197
- 'Would you like to include this report to the next security release?',
198
- { defaultAnswer: true });
199
- if (!include) {
200
- continue;
201
- }
202
-
203
- const versions = await cli.prompt('Which active release lines this report affects?', {
204
- questionType: 'input',
205
- defaultAnswer: supportedVersions
206
- });
207
- const summaryContent = await getSummary(id, this.req);
208
-
209
- selectedReports.push({
210
- id,
211
- title,
212
- cve_ids,
213
- severity: reportSeverity,
214
- summary: summaryContent ?? '',
215
- affectedVersions: versions.split(',').map((v) => v.replace('v', '').trim()),
216
- link,
217
- reporter: reporter.data.attributes.username,
218
- created_at // when we request CVE we need to input vulnerability_discovered_at
219
- });
159
+ const rep = await pickReport(report, { cli: this.cli, req: this.req });
160
+ if (!rep) continue;
161
+ selectedReports.push(rep);
220
162
  }
221
163
  return selectedReports;
222
164
  }
223
165
 
224
- async createVulnerabilitiesJSON(reports, { cli }) {
225
- cli.separator('Creating vulnerabilities.json...');
166
+ async createVulnerabilitiesJSON(reports, dependencies, releaseDate) {
167
+ this.cli.separator('Creating vulnerabilities.json...');
226
168
  const file = JSON.stringify({
227
- reports
169
+ releaseDate,
170
+ reports,
171
+ dependencies
228
172
  }, null, 2);
229
173
 
230
174
  const folderPath = path.join(process.cwd(), NEXT_SECURITY_RELEASE_FOLDER);
@@ -236,14 +180,14 @@ class PrepareSecurityRelease {
236
180
 
237
181
  const fullPath = path.join(folderPath, 'vulnerabilities.json');
238
182
  fs.writeFileSync(fullPath, file);
239
- cli.ok(`Created ${fullPath} `);
183
+ this.cli.ok(`Created ${fullPath} `);
240
184
 
241
185
  return fullPath;
242
186
  }
243
187
 
244
- async createPullRequest(req, { cli }) {
188
+ async createPullRequest() {
245
189
  const { owner, repo } = this.repository;
246
- const response = await req.createPullRequest(
190
+ const response = await this.req.createPullRequest(
247
191
  this.title,
248
192
  'List of vulnerabilities to be included in the next security release',
249
193
  {
@@ -256,16 +200,69 @@ class PrepareSecurityRelease {
256
200
  );
257
201
  const url = response?.html_url;
258
202
  if (url) {
259
- cli.ok(`Created: ${url}`);
203
+ this.cli.ok(`Created: ${url}`);
260
204
  return url;
261
205
  }
262
206
  if (response?.errors) {
263
207
  for (const error of response.errors) {
264
- cli.error(error.message);
208
+ this.cli.error(error.message);
265
209
  }
266
210
  } else {
267
- cli.error(response);
211
+ this.cli.error(response);
268
212
  }
269
213
  process.exit(1);
270
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);
264
+ }
265
+ }
266
+ return deps;
267
+ }
271
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 = {
@@ -3,7 +3,8 @@ import {
3
3
  checkoutOnSecurityReleaseBranch,
4
4
  getVulnerabilitiesJSON,
5
5
  validateDate,
6
- formatDateToYYYYMMDD
6
+ formatDateToYYYYMMDD,
7
+ createIssue
7
8
  } from './security-release/security-release.js';
8
9
  import auth from './auth.js';
9
10
  import Request from './request.js';
@@ -39,8 +40,10 @@ export default class SecurityAnnouncement {
39
40
  validateDate(content.releaseDate);
40
41
  const releaseDate = new Date(content.releaseDate);
41
42
 
42
- await Promise.all([this.createDockerNodeIssue(releaseDate),
43
- this.createBuildWGIssue(releaseDate)]);
43
+ await Promise.all([
44
+ this.createDockerNodeIssue(releaseDate),
45
+ this.createBuildWGIssue(releaseDate)
46
+ ]);
44
47
  }
45
48
 
46
49
  async createBuildWGIssue(releaseDate) {
@@ -56,8 +59,8 @@ export default class SecurityAnnouncement {
56
59
  createPreleaseAnnouncementIssue(releaseDate, team) {
57
60
  const title = `[NEXT-SECURITY-RELEASE] Heads up on upcoming Node.js\
58
61
  security release ${formatDateToYYYYMMDD(releaseDate)}`;
59
- const content = 'As per security release workflow,' +
60
- ` creating issue to give the ${team} team a heads up.`;
62
+ const content = `As per security release workflow,\
63
+ creating issue to give the ${team} team a heads up.`;
61
64
  return { title, content };
62
65
  }
63
66
 
@@ -68,16 +71,6 @@ export default class SecurityAnnouncement {
68
71
  };
69
72
 
70
73
  const { title, content } = this.createPreleaseAnnouncementIssue(releaseDate, 'docker');
71
- await this.createIssue(title, content, repository);
72
- }
73
-
74
- async createIssue(title, content, repository) {
75
- const data = await this.req.createIssue(title, content, repository);
76
- if (data.html_url) {
77
- this.cli.ok(`Created: ${data.html_url}`);
78
- } else {
79
- this.cli.error(data);
80
- process.exit(1);
81
- }
74
+ await createIssue(title, content, repository, { cli: this.cli, repository: this.repository });
82
75
  }
83
76
  }
@@ -61,8 +61,22 @@ export function commitAndPushVulnerabilitiesJSON(filePath, commitMessage, { cli,
61
61
  runSync('git', ['add', filePath]);
62
62
  }
63
63
 
64
+ const staged = runSync('git', ['diff', '--name-only', '--cached']).trim();
65
+ if (!staged) {
66
+ cli.ok('No changes to commit');
67
+ return;
68
+ }
69
+
64
70
  runSync('git', ['commit', '-m', commitMessage]);
65
- runSync('git', ['push', '-u', 'origin', NEXT_SECURITY_RELEASE_BRANCH]);
71
+
72
+ try {
73
+ runSync('git', ['push', '-u', 'origin', NEXT_SECURITY_RELEASE_BRANCH]);
74
+ } catch (error) {
75
+ cli.warn('Rebasing...');
76
+ // try to pull rebase and push again
77
+ runSync('git', ['pull', 'origin', NEXT_SECURITY_RELEASE_BRANCH, '--rebase']);
78
+ runSync('git', ['push', '-u', 'origin', NEXT_SECURITY_RELEASE_BRANCH]);
79
+ }
66
80
  cli.ok(`Pushed commit: ${commitMessage} to ${NEXT_SECURITY_RELEASE_BRANCH}`);
67
81
  }
68
82
 
@@ -107,3 +121,73 @@ export function formatDateToYYYYMMDD(date) {
107
121
  // Concatenate year, month, and day with slashes
108
122
  return `${year}/${month}/${day}`;
109
123
  }
124
+
125
+ export function promptDependencies(cli) {
126
+ return cli.prompt('Enter the link to the dependency update PR (leave empty to exit): ', {
127
+ defaultAnswer: '',
128
+ questionType: 'input'
129
+ });
130
+ }
131
+
132
+ export async function createIssue(title, content, repository, { cli, req }) {
133
+ const data = await req.createIssue(title, content, repository);
134
+ if (data.html_url) {
135
+ cli.ok(`Created: ${data.html_url}`);
136
+ } else {
137
+ cli.error(data);
138
+ process.exit(1);
139
+ }
140
+ }
141
+
142
+ export async function pickReport(report, { cli, req }) {
143
+ const {
144
+ id, attributes: { title, cve_ids },
145
+ relationships: { severity, weakness, reporter }
146
+ } = report;
147
+ const link = `https://hackerone.com/reports/${id}`;
148
+ const reportSeverity = {
149
+ rating: severity?.data?.attributes?.rating || '',
150
+ cvss_vector_string: severity?.data?.attributes?.cvss_vector_string || '',
151
+ weakness_id: weakness?.data?.id || ''
152
+ };
153
+
154
+ cli.separator();
155
+ cli.info(`Report: ${link} - ${title} (${reportSeverity?.rating})`);
156
+ const include = await cli.prompt(
157
+ 'Would you like to include this report to the next security release?',
158
+ { defaultAnswer: true });
159
+ if (!include) {
160
+ return;
161
+ }
162
+
163
+ const versions = await cli.prompt('Which active release lines this report affects?', {
164
+ questionType: 'input',
165
+ defaultAnswer: await getSupportedVersions()
166
+ });
167
+
168
+ let patchAuthors = await cli.prompt(
169
+ 'Add github username of the authors of the patch (split by comma if multiple)', {
170
+ questionType: 'input',
171
+ defaultAnswer: ''
172
+ });
173
+
174
+ if (!patchAuthors) {
175
+ patchAuthors = [];
176
+ } else {
177
+ patchAuthors = patchAuthors.split(',').map((p) => p.trim());
178
+ }
179
+
180
+ const summaryContent = await getSummary(id, req);
181
+
182
+ return {
183
+ id,
184
+ title,
185
+ cveIds: cve_ids,
186
+ severity: reportSeverity,
187
+ summary: summaryContent ?? '',
188
+ patchAuthors,
189
+ affectedVersions: versions.split(',').map((v) => v.replace('v', '').trim()),
190
+ link,
191
+ reporter: reporter.data.attributes.username
192
+ };
193
+ }
@@ -3,14 +3,14 @@ import {
3
3
  NEXT_SECURITY_RELEASE_REPOSITORY,
4
4
  checkoutOnSecurityReleaseBranch,
5
5
  commitAndPushVulnerabilitiesJSON,
6
- getSupportedVersions,
7
- getSummary,
8
- validateDate
6
+ validateDate,
7
+ pickReport
9
8
  } from './security-release/security-release.js';
10
9
  import fs from 'node:fs';
11
10
  import path from 'node:path';
12
11
  import auth from './auth.js';
13
12
  import Request from './request.js';
13
+ import nv from '@pkgjs/nv';
14
14
 
15
15
  export default class UpdateSecurityRelease {
16
16
  repository = NEXT_SECURITY_RELEASE_REPOSITORY;
@@ -32,7 +32,7 @@ export default class UpdateSecurityRelease {
32
32
  checkoutOnSecurityReleaseBranch(cli, this.repository);
33
33
 
34
34
  // update the release date in the vulnerabilities.json file
35
- const updatedVulnerabilitiesFiles = await this.updateVulnerabilitiesJSON(releaseDate, { cli });
35
+ const updatedVulnerabilitiesFiles = await this.updateJSONReleaseDate(releaseDate, { cli });
36
36
 
37
37
  const commitMessage = `chore: update the release date to ${releaseDate}`;
38
38
  commitAndPushVulnerabilitiesJSON(updatedVulnerabilitiesFiles,
@@ -56,7 +56,7 @@ export default class UpdateSecurityRelease {
56
56
  NEXT_SECURITY_RELEASE_FOLDER, 'vulnerabilities.json');
57
57
  }
58
58
 
59
- async updateVulnerabilitiesJSON(releaseDate) {
59
+ async updateJSONReleaseDate(releaseDate) {
60
60
  const vulnerabilitiesJSONPath = this.getVulnerabilitiesJSONPath();
61
61
  const content = this.readVulnerabilitiesJSON(vulnerabilitiesJSONPath);
62
62
  content.releaseDate = releaseDate;
@@ -68,7 +68,6 @@ export default class UpdateSecurityRelease {
68
68
  }
69
69
 
70
70
  async addReport(reportId) {
71
- const { cli } = this;
72
71
  const credentials = await auth({
73
72
  github: true,
74
73
  h1: true
@@ -76,43 +75,21 @@ export default class UpdateSecurityRelease {
76
75
 
77
76
  const req = new Request(credentials);
78
77
  // checkout on the next-security-release branch
79
- checkoutOnSecurityReleaseBranch(cli, this.repository);
78
+ checkoutOnSecurityReleaseBranch(this.cli, this.repository);
80
79
 
81
80
  // get h1 report
82
81
  const { data: report } = await req.getReport(reportId);
83
- const { id, attributes: { title, cve_ids }, relationships: { severity, reporter } } = report;
84
- // if severity is not set on h1, set it to TBD
85
- const reportLevel = severity ? severity.data.attributes.rating : 'TBD';
86
-
87
- // get the affected versions
88
- const supportedVersions = await getSupportedVersions();
89
- const versions = await cli.prompt('Which active release lines this report affects?', {
90
- questionType: 'input',
91
- defaultAnswer: supportedVersions
92
- });
93
-
94
- // get the team summary from h1 report
95
- const summaryContent = await getSummary(id, req);
96
-
97
- const entry = {
98
- id,
99
- title,
100
- cve_ids,
101
- severity: reportLevel,
102
- summary: summaryContent ?? '',
103
- affectedVersions: versions.split(',').map((v) => v.replace('v', '').trim()),
104
- reporter: reporter.data.attributes.username
105
- };
82
+ const entry = await pickReport(report, { cli: this.cli, req });
106
83
 
107
84
  const vulnerabilitiesJSONPath = this.getVulnerabilitiesJSONPath();
108
85
  const content = this.readVulnerabilitiesJSON(vulnerabilitiesJSONPath);
109
86
  content.reports.push(entry);
110
87
  fs.writeFileSync(vulnerabilitiesJSONPath, JSON.stringify(content, null, 2));
111
- this.cli.ok(`Updated vulnerabilities.json with the report: ${id}`);
112
- const commitMessage = `chore: added report ${id} to vulnerabilities.json`;
88
+ this.cli.ok(`Updated vulnerabilities.json with the report: ${entry.id}`);
89
+ const commitMessage = `chore: added report ${entry.id} to vulnerabilities.json`;
113
90
  commitAndPushVulnerabilitiesJSON(vulnerabilitiesJSONPath,
114
- commitMessage, { cli, repository: this.repository });
115
- cli.ok('Done!');
91
+ commitMessage, { cli: this.cli, repository: this.repository });
92
+ this.cli.ok('Done!');
116
93
  }
117
94
 
118
95
  removeReport(reportId) {
@@ -135,4 +112,163 @@ export default class UpdateSecurityRelease {
135
112
  commitMessage, { cli, repository: this.repository });
136
113
  cli.ok('Done!');
137
114
  }
115
+
116
+ async requestCVEs() {
117
+ const credentials = await auth({
118
+ github: true,
119
+ h1: true
120
+ });
121
+ const vulnerabilitiesJSONPath = this.getVulnerabilitiesJSONPath();
122
+ const content = this.readVulnerabilitiesJSON(vulnerabilitiesJSONPath);
123
+ const { reports } = content;
124
+ const req = new Request(credentials);
125
+ const programId = await this.getNodeProgramId(req);
126
+ const cves = await this.promptCVECreation(req, reports, programId);
127
+ this.assignCVEtoReport(cves, reports);
128
+ this.updateVulnerabilitiesJSON(content, vulnerabilitiesJSONPath);
129
+ this.updateHackonerReportCve(req, reports);
130
+ }
131
+
132
+ assignCVEtoReport(cves, reports) {
133
+ for (const cve of cves) {
134
+ const report = reports.find(report => report.id === cve.reportId);
135
+ report.cveIds = [cve.cve_identifier];
136
+ }
137
+ }
138
+
139
+ async updateHackonerReportCve(req, reports) {
140
+ for (const report of reports) {
141
+ const { id, cveIds } = report;
142
+ this.cli.startSpinner(`Updating report ${id} with CVEs ${cveIds}..`);
143
+ const body = {
144
+ data: {
145
+ type: 'report-cves',
146
+ attributes: {
147
+ cve_ids: cveIds
148
+ }
149
+ }
150
+ };
151
+ const response = await req.updateReportCVE(id, body);
152
+ if (response.errors) {
153
+ this.cli.error(`Error updating report ${id}`);
154
+ this.cli.error(JSON.stringify(response.errors, null, 2));
155
+ }
156
+ this.cli.stopSpinner(`Done updating report ${id} with CVEs ${cveIds}..`);
157
+ }
158
+ }
159
+
160
+ updateVulnerabilitiesJSON(content, vulnerabilitiesJSONPath) {
161
+ this.cli.startSpinner(`Updating vulnerabilities.json from\
162
+ ${vulnerabilitiesJSONPath}..`);
163
+ const filePath = path.resolve(vulnerabilitiesJSONPath);
164
+ fs.writeFileSync(filePath, JSON.stringify(content, null, 2));
165
+ // push the changes to the repository
166
+ commitAndPushVulnerabilitiesJSON(filePath,
167
+ 'chore: updated vulnerabilities.json with CVEs',
168
+ { cli: this.cli, repository: this.repository });
169
+ this.cli.stopSpinner(`Done updating vulnerabilities.json from ${filePath}`);
170
+ }
171
+
172
+ async promptCVECreation(req, reports, programId) {
173
+ const supportedVersions = (await nv('supported'));
174
+ const cves = [];
175
+ for (const report of reports) {
176
+ const { id, summary, title, affectedVersions, cveIds, link } = report;
177
+ // skip if already has a CVE
178
+ // risky because the CVE associated might be
179
+ // mentioned in the report and not requested by Node
180
+ if (cveIds?.length) continue;
181
+
182
+ let severity = report.severity;
183
+
184
+ if (!severity.cvss_vector_string || !severity.weakness_id) {
185
+ try {
186
+ const h1Report = await req.getReport(id);
187
+ if (!h1Report.data.relationships.severity?.data.attributes.cvss_vector_string) {
188
+ throw new Error('No severity found');
189
+ }
190
+ severity = {
191
+ weakness_id: h1Report.data.relationships.weakness?.data.id,
192
+ cvss_vector_string:
193
+ h1Report.data.relationships.severity?.data.attributes.cvss_vector_string,
194
+ rating: h1Report.data.relationships.severity?.data.attributes.rating
195
+ };
196
+ } catch (error) {
197
+ this.cli.error(`Couldnt not retrieve severity from report ${id}, skipping...`);
198
+ continue;
199
+ }
200
+ }
201
+
202
+ const { cvss_vector_string, weakness_id } = severity;
203
+
204
+ const create = await this.cli.prompt(
205
+ `Request a CVE for: \n
206
+ Title: ${title}\n
207
+ Link: ${link}\n
208
+ Affected versions: ${affectedVersions.join(', ')}\n
209
+ Vector: ${cvss_vector_string}\n
210
+ Summary: ${summary}\n`,
211
+ { defaultAnswer: true });
212
+
213
+ if (!create) continue;
214
+
215
+ const body = {
216
+ data: {
217
+ type: 'cve-request',
218
+ attributes: {
219
+ team_handle: 'nodejs-team',
220
+ versions: await this.formatAffected(affectedVersions, supportedVersions),
221
+ metrics: [
222
+ {
223
+ vectorString: cvss_vector_string
224
+ }
225
+ ],
226
+ weakness_id: Number(weakness_id),
227
+ description: title,
228
+ vulnerability_discovered_at: new Date().toISOString()
229
+ }
230
+ }
231
+ };
232
+ const { data } = await req.requestCVE(programId, body);
233
+ if (data.errors) {
234
+ this.cli.error(`Error requesting CVE for report ${id}`);
235
+ this.cli.error(JSON.stringify(data.errors, null, 2));
236
+ continue;
237
+ }
238
+ const { cve_identifier } = data.attributes;
239
+ cves.push({ cve_identifier, reportId: id });
240
+ }
241
+ return cves;
242
+ }
243
+
244
+ async getNodeProgramId(req) {
245
+ const programs = await req.getPrograms();
246
+ const { data } = programs;
247
+ for (const program of data) {
248
+ const { attributes } = program;
249
+ if (attributes.handle === 'nodejs') {
250
+ return program.id;
251
+ }
252
+ }
253
+ }
254
+
255
+ async formatAffected(affectedVersions, supportedVersions) {
256
+ const result = [];
257
+ for (const affectedVersion of affectedVersions) {
258
+ const major = affectedVersion.split('.')[0];
259
+ const latest = supportedVersions.find((v) => v.major === Number(major)).version;
260
+ const version = await this.cli.prompt(
261
+ `What is the affected version (<=) for release line ${affectedVersion}?`,
262
+ { questionType: 'input', defaultAnswer: latest });
263
+ result.push({
264
+ vendor: 'nodejs',
265
+ product: 'node',
266
+ func: '<=',
267
+ version,
268
+ versionType: 'semver',
269
+ affected: true
270
+ });
271
+ }
272
+ return result;
273
+ }
138
274
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@node-core/utils",
3
- "version": "4.4.0",
3
+ "version": "5.0.0",
4
4
  "description": "Utilities for Node.js core collaborators",
5
5
  "type": "module",
6
6
  "engines": {