@node-core/utils 5.2.1 → 5.3.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.
@@ -39,6 +39,10 @@ const securityOptions = {
39
39
  'request-cve': {
40
40
  describe: 'Request CVEs for a security release',
41
41
  type: 'boolean'
42
+ },
43
+ 'post-release': {
44
+ describe: 'Create the post-release announcement',
45
+ type: 'boolean'
42
46
  }
43
47
  };
44
48
 
@@ -49,7 +53,8 @@ export function builder(yargs) {
49
53
  return yargs.options(securityOptions)
50
54
  .example(
51
55
  'git node security --start',
52
- 'Prepare a security release of Node.js')
56
+ 'Prepare a security release of Node.js'
57
+ )
53
58
  .example(
54
59
  'git node security --sync',
55
60
  'Synchronize an ongoing security release with HackerOne'
@@ -57,26 +62,25 @@ export function builder(yargs) {
57
62
  .example(
58
63
  'git node security --update-date=YYYY/MM/DD',
59
64
  'Updates the target date of the security release'
60
- )
61
- .example(
65
+ ).example(
62
66
  'git node security --add-report=H1-ID',
63
67
  'Fetches HackerOne report based on ID provided and adds it into vulnerabilities.json'
64
- )
65
- .example(
68
+ ).example(
66
69
  'git node security --remove-report=H1-ID',
67
70
  'Removes the Hackerone report based on ID provided from vulnerabilities.json'
68
- )
69
- .example(
70
- 'git node security --pre-release' +
71
+ ).example(
72
+ 'git node security --pre-release',
71
73
  'Create the pre-release announcement on the Nodejs.org repo'
72
74
  ).example(
73
- 'git node security --notify-pre-release' +
75
+ 'git node security --notify-pre-release',
74
76
  'Notifies the community about the security release'
75
- )
76
- .example(
77
+ ).example(
77
78
  'git node security --request-cve',
78
79
  'Request CVEs for a security release of Node.js based on' +
79
80
  ' the next-security-release/vulnerabilities.json'
81
+ ).example(
82
+ 'git node security --post-release' +
83
+ 'Create the post-release announcement on the Nodejs.org repo'
80
84
  );
81
85
  }
82
86
 
@@ -105,6 +109,9 @@ export function handler(argv) {
105
109
  if (argv['request-cve']) {
106
110
  return requestCVEs(argv);
107
111
  }
112
+ if (argv['post-release']) {
113
+ return createPostRelease(argv);
114
+ }
108
115
  yargsInstance.showHelp();
109
116
  }
110
117
 
@@ -146,7 +153,14 @@ async function requestCVEs() {
146
153
  return hackerOneCve.requestCVEs();
147
154
  }
148
155
 
149
- async function startSecurityRelease(argv) {
156
+ async function createPostRelease() {
157
+ const logStream = process.stdout.isTTY ? process.stdout : process.stderr;
158
+ const cli = new CLI(logStream);
159
+ const blog = new SecurityBlog(cli);
160
+ return blog.createPostRelease();
161
+ }
162
+
163
+ async function startSecurityRelease() {
150
164
  const logStream = process.stdout.isTTY ? process.stdout : process.stderr;
151
165
  const cli = new CLI(logStream);
152
166
  const release = new PrepareSecurityRelease(cli);
@@ -0,0 +1,18 @@
1
+ ---
2
+ date: %ANNOUNCEMENT_DATE%
3
+ category: vulnerability
4
+ title: %RELEASE_DATE% Security Releases
5
+ slug: %SLUG%
6
+ layout: blog-post
7
+ author: %AUTHOR%
8
+ ---
9
+
10
+ ## Security releases available
11
+
12
+ Updates are now available for the %AFFECTED_VERSIONS% Node.js release lines for the
13
+ following issues.
14
+ %DEPENDENCY_UPDATES%
15
+ %REPORTS%
16
+ ## Downloads and release details
17
+
18
+ %DOWNLOADS%
@@ -13,7 +13,7 @@ The Node.js project will release new versions of the %AFFECTED_VERSIONS%
13
13
  releases lines on or shortly after, %RELEASE_DATE% in order to address:
14
14
 
15
15
  %VULNERABILITIES%
16
- %OPENSSL_UPDATES%
16
+
17
17
  ## Impact
18
18
 
19
19
  %IMPACT%
@@ -28,7 +28,7 @@ Releases will be available on, or shortly after, %RELEASE_DATE%.
28
28
 
29
29
  ## Contact and future updates
30
30
 
31
- The current Node.js security policy can be found at https://nodejs.org/en/security/.
32
- Please follow the process outlined in https://github.com/nodejs/node/blob/master/SECURITY.md if you wish to report a vulnerability in Node.js.
31
+ The current Node.js security policy can be found at <https://nodejs.org/en/security/>.
32
+ Please follow the process outlined in <https://github.com/nodejs/node/blob/master/SECURITY.md> if you wish to report a vulnerability in Node.js.
33
33
 
34
- Subscribe to the low-volume announcement-only nodejs-sec mailing list at https://groups.google.com/forum/#!forum/nodejs-sec to stay up to date on security vulnerabilities and security-related releases of Node.js and the projects maintained in the nodejs GitHub organization.
34
+ Subscribe to the low-volume announcement-only nodejs-sec mailing list at <https://groups.google.com/forum/#!forum/nodejs-sec> to stay up to date on security vulnerabilities and security-related releases of Node.js and the projects maintained in the nodejs GitHub organization.
@@ -20,9 +20,12 @@ export const PLACEHOLDERS = {
20
20
  annoucementDate: '%ANNOUNCEMENT_DATE%',
21
21
  slug: '%SLUG%',
22
22
  affectedVersions: '%AFFECTED_VERSIONS%',
23
- openSSLUpdate: '%OPENSSL_UPDATES%',
24
23
  impact: '%IMPACT%',
25
- vulnerabilities: '%VULNERABILITIES%'
24
+ vulnerabilities: '%VULNERABILITIES%',
25
+ reports: '%REPORTS%',
26
+ author: '%AUTHOR%',
27
+ dependencyUpdates: '%DEPENDENCY_UPDATES%',
28
+ downloads: '%DOWNLOADS%'
26
29
  };
27
30
 
28
31
  export function checkRemote(cli, repository) {
@@ -1,16 +1,25 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import _ from 'lodash';
4
+ import nv from '@pkgjs/nv';
4
5
  import {
5
6
  PLACEHOLDERS,
6
7
  getVulnerabilitiesJSON,
7
8
  checkoutOnSecurityReleaseBranch,
8
9
  NEXT_SECURITY_RELEASE_REPOSITORY,
9
- validateDate
10
+ validateDate,
11
+ getSummary,
12
+ commitAndPushVulnerabilitiesJSON,
13
+ NEXT_SECURITY_RELEASE_FOLDER
10
14
  } from './security-release/security-release.js';
15
+ import auth from './auth.js';
16
+ import Request from './request.js';
17
+
18
+ const kChanged = Symbol('changed');
11
19
 
12
20
  export default class SecurityBlog {
13
21
  repository = NEXT_SECURITY_RELEASE_REPOSITORY;
22
+ req;
14
23
  constructor(cli) {
15
24
  this.cli = cli;
16
25
  }
@@ -40,8 +49,7 @@ export default class SecurityBlog {
40
49
  affectedVersions: this.getAffectedVersions(content),
41
50
  vulnerabilities: this.getVulnerabilities(content),
42
51
  slug: this.getSlug(releaseDate),
43
- impact: this.getImpact(content),
44
- openSSLUpdate: await this.promptOpenSSLUpdate(cli)
52
+ impact: this.getImpact(content)
45
53
  };
46
54
  const month = releaseDate.toLocaleString('en-US', { month: 'long' }).toLowerCase();
47
55
  const year = releaseDate.getFullYear();
@@ -52,9 +60,93 @@ export default class SecurityBlog {
52
60
  cli.ok(`Pre-release announcement file created at ${file}`);
53
61
  }
54
62
 
55
- promptOpenSSLUpdate(cli) {
56
- return cli.prompt('Does this security release containt OpenSSL updates?', {
57
- defaultAnswer: true
63
+ async createPostRelease() {
64
+ const { cli } = this;
65
+ const credentials = await auth({
66
+ github: true,
67
+ h1: true
68
+ });
69
+
70
+ this.req = new Request(credentials);
71
+
72
+ // checkout on security release branch
73
+ checkoutOnSecurityReleaseBranch(cli, this.repository);
74
+
75
+ // read vulnerabilities JSON file
76
+ const content = getVulnerabilitiesJSON(cli);
77
+ if (!content.releaseDate) {
78
+ cli.error('Release date is not set in vulnerabilities.json,' +
79
+ ' run `git node security --update-date=YYYY/MM/DD` to set the release date.');
80
+ process.exit(1);
81
+ }
82
+
83
+ validateDate(content.releaseDate);
84
+ const releaseDate = new Date(content.releaseDate);
85
+ const template = this.getSecurityPostReleaseTemplate();
86
+ const data = {
87
+ annoucementDate: await this.getAnnouncementDate(cli),
88
+ releaseDate: this.formatReleaseDate(releaseDate),
89
+ affectedVersions: this.getAffectedVersions(content),
90
+ vulnerabilities: this.getVulnerabilities(content),
91
+ slug: this.getSlug(releaseDate),
92
+ author: await this.promptAuthor(cli),
93
+ dependencyUpdates: content.dependencies
94
+ };
95
+ const postReleaseContent = await this.buildPostRelease(template, data, content);
96
+
97
+ const pathPreRelease = await this.promptExistingPreRelease(cli);
98
+ // read the existing pre-release announcement
99
+ let preReleaseContent = fs.readFileSync(pathPreRelease, 'utf-8');
100
+ // cut the part before summary
101
+ const preSummary = preReleaseContent.indexOf('# Summary');
102
+ if (preSummary !== -1) {
103
+ preReleaseContent = preReleaseContent.substring(preSummary);
104
+ }
105
+
106
+ const updatedContent = postReleaseContent + preReleaseContent;
107
+
108
+ fs.writeFileSync(pathPreRelease, updatedContent);
109
+ cli.ok(`Post-release announcement file updated at ${pathPreRelease}`);
110
+
111
+ // if the vulnerabilities.json has been changed, update the file
112
+ if (!content[kChanged]) return;
113
+ this.updateVulnerabilitiesJSON(content);
114
+ }
115
+
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
+
132
+ async promptExistingPreRelease(cli) {
133
+ const pathPreRelease = await cli.prompt(
134
+ 'Please provide the path of the existing pre-release announcement:', {
135
+ questionType: 'input',
136
+ defaultAnswer: ''
137
+ });
138
+
139
+ if (!pathPreRelease || !fs.existsSync(path.resolve(pathPreRelease))) {
140
+ return this.promptExistingPreRelease(cli);
141
+ }
142
+ return pathPreRelease;
143
+ }
144
+
145
+ promptAuthor(cli) {
146
+ return cli.prompt('Who is the author of this security release? If multiple' +
147
+ ' use & as separator', {
148
+ questionType: 'input',
149
+ defaultAnswer: PLACEHOLDERS.author
58
150
  });
59
151
  }
60
152
 
@@ -69,6 +161,23 @@ export default class SecurityBlog {
69
161
  }
70
162
 
71
163
  buildPreRelease(template, data) {
164
+ const {
165
+ annoucementDate,
166
+ releaseDate,
167
+ affectedVersions,
168
+ vulnerabilities,
169
+ slug,
170
+ impact
171
+ } = data;
172
+ return template.replaceAll(PLACEHOLDERS.annoucementDate, annoucementDate)
173
+ .replaceAll(PLACEHOLDERS.slug, slug)
174
+ .replaceAll(PLACEHOLDERS.affectedVersions, affectedVersions)
175
+ .replaceAll(PLACEHOLDERS.vulnerabilities, vulnerabilities)
176
+ .replaceAll(PLACEHOLDERS.releaseDate, releaseDate)
177
+ .replaceAll(PLACEHOLDERS.impact, impact);
178
+ }
179
+
180
+ async buildPostRelease(template, data, content) {
72
181
  const {
73
182
  annoucementDate,
74
183
  releaseDate,
@@ -76,7 +185,8 @@ export default class SecurityBlog {
76
185
  vulnerabilities,
77
186
  slug,
78
187
  impact,
79
- openSSLUpdate
188
+ author,
189
+ dependencyUpdates
80
190
  } = data;
81
191
  return template.replaceAll(PLACEHOLDERS.annoucementDate, annoucementDate)
82
192
  .replaceAll(PLACEHOLDERS.slug, slug)
@@ -84,15 +194,89 @@ export default class SecurityBlog {
84
194
  .replaceAll(PLACEHOLDERS.vulnerabilities, vulnerabilities)
85
195
  .replaceAll(PLACEHOLDERS.releaseDate, releaseDate)
86
196
  .replaceAll(PLACEHOLDERS.impact, impact)
87
- .replaceAll(PLACEHOLDERS.openSSLUpdate, this.getOpenSSLUpdateTemplate(openSSLUpdate));
197
+ .replaceAll(PLACEHOLDERS.author, author)
198
+ .replaceAll(PLACEHOLDERS.reports, await this.getReportsTemplate(content))
199
+ .replaceAll(PLACEHOLDERS.dependencyUpdates,
200
+ this.getDependencyUpdatesTemplate(dependencyUpdates))
201
+ .replaceAll(PLACEHOLDERS.downloads, await this.getDownloadsTemplate(affectedVersions));
202
+ }
203
+
204
+ async getReportsTemplate(content) {
205
+ const reports = content.reports;
206
+ let template = '';
207
+ for (const report of reports) {
208
+ let cveId = report.cve_ids?.join(', ');
209
+ if (!cveId) {
210
+ // ask for the CVE ID
211
+ // it should have been created with the step `--request-cve`
212
+ cveId = await this.cli.prompt(`What is the CVE ID for vulnerability https://hackerone.com/reports/${report.id} ${report.title}?`, {
213
+ questionType: 'input',
214
+ defaultAnswer: 'TBD'
215
+ });
216
+ report.cve_ids = [cveId];
217
+ content[kChanged] = true;
218
+ }
219
+ template += `## ${report.title} (${cveId}) - (${report.severity.rating})\n\n`;
220
+ if (!report.summary) {
221
+ const fetchIt = await this.cli.prompt(`Summary missing for vulnerability https://hackerone.com/reports/${report.id} ${report.title}.\
222
+ Do you want to try fetch it from HackerOne??`, {
223
+ questionType: 'confirm',
224
+ defaultAnswer: true
225
+ });
226
+
227
+ if (fetchIt) {
228
+ report.summary = await getSummary(report.id, this.req);
229
+ content[kChanged] = true;
230
+ }
231
+
232
+ if (!report.summary) {
233
+ this.cli.error(`Summary missing for vulnerability https://hackerone.com/reports/${report.id} ${report.title}. Please create it before continuing.`);
234
+ process.exit(1);
235
+ }
236
+ }
237
+ template += `${report.summary}\n\n`;
238
+ const releaseLines = report.affectedVersions.join(', ');
239
+ template += `Impact:\n\n- This vulnerability affects all users\
240
+ in active release lines: ${releaseLines}\n\n`;
241
+ if (!report.patchAuthors) {
242
+ const author = await this.cli.prompt(`Who fixed vulnerability https://hackerone.com/reports/${report.id} ${report.title}? If multiple use & as separator`, {
243
+ questionType: 'input',
244
+ defaultAnswer: 'TBD'
245
+ });
246
+ report.patchAuthors = author.split('&').map((p) => p.trim());
247
+ content[kChanged] = true;
248
+ }
249
+ template += `Thank you, to ${report.reporter} for reporting this vulnerability\
250
+ and thank you ${report.patchAuthors.join(' and ')} for fixing it.\n\n`;
251
+ }
252
+ return template;
253
+ }
254
+
255
+ getDependencyUpdatesTemplate(dependencyUpdates) {
256
+ if (!dependencyUpdates) return '';
257
+ let template = 'This security release includes the following dependency' +
258
+ ' updates to address public vulnerabilities:\n\n';
259
+ for (const dependencyUpdate of Object.values(dependencyUpdates)) {
260
+ for (const dependency of dependencyUpdate) {
261
+ const title = dependency.title.substring(dependency.title.indexOf(':') + ':'.length).trim();
262
+ template += `- ${title}\
263
+ on ${dependency.affectedVersions.join(', ')}\n`;
264
+ }
265
+ }
266
+ return template;
88
267
  }
89
268
 
90
- getOpenSSLUpdateTemplate(openSSLUpdate) {
91
- if (openSSLUpdate) {
92
- return '\n## OpenSSL Security updates\n\n' +
93
- 'This security release includes OpenSSL security updates\n';
269
+ async getDownloadsTemplate(affectedVersions) {
270
+ let template = '';
271
+ const versionsToBeReleased = (await nv('supported')).filter(
272
+ (v) => affectedVersions.split(', ').includes(`${v.major}.x`)
273
+ );
274
+ for (const version of versionsToBeReleased) {
275
+ const v = `v${version.major}.${version.minor}.${Number(version.patch) + 1}`;
276
+ template += `- [Node.js ${v}](/blog/release/${v}/)\n`;
94
277
  }
95
- return '';
278
+
279
+ return template;
96
280
  }
97
281
 
98
282
  getSlug(releaseDate) {
@@ -179,4 +363,14 @@ export default class SecurityBlog {
179
363
  'utf-8'
180
364
  );
181
365
  }
366
+
367
+ getSecurityPostReleaseTemplate() {
368
+ return fs.readFileSync(
369
+ new URL(
370
+ './github/templates/security-post-release.md',
371
+ import.meta.url
372
+ ),
373
+ 'utf-8'
374
+ );
375
+ }
182
376
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@node-core/utils",
3
- "version": "5.2.1",
3
+ "version": "5.3.0",
4
4
  "description": "Utilities for Node.js core collaborators",
5
5
  "type": "module",
6
6
  "engines": {