@node-core/utils 5.2.1 → 5.3.1

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.
@@ -2,7 +2,7 @@ import path from 'node:path';
2
2
  import { promises as fs } from 'node:fs';
3
3
 
4
4
  import semver from 'semver';
5
- import replace from 'replace-in-file';
5
+ import { replaceInFile } from 'replace-in-file';
6
6
 
7
7
  import { getMergedConfig } from './config.js';
8
8
  import { runAsync, runSync } from './run.js';
@@ -427,7 +427,7 @@ export default class ReleasePreparation {
427
427
  async updateREPLACEMEs() {
428
428
  const { newVersion } = this;
429
429
 
430
- await replace({
430
+ await replaceInFile({
431
431
  files: 'doc/api/*.md',
432
432
  from: /REPLACEME/g,
433
433
  to: `v${newVersion}`
@@ -53,7 +53,7 @@ export default class SecurityAnnouncement {
53
53
  };
54
54
 
55
55
  const { title, content } = this.createPreleaseAnnouncementIssue(releaseDate, 'build');
56
- await this.createIssue(title, content, repository);
56
+ await createIssue(title, content, repository, { cli: this.cli, req: this.req });
57
57
  }
58
58
 
59
59
  createPreleaseAnnouncementIssue(releaseDate, team) {
@@ -71,6 +71,6 @@ export default class SecurityAnnouncement {
71
71
  };
72
72
 
73
73
  const { title, content } = this.createPreleaseAnnouncementIssue(releaseDate, 'docker');
74
- await createIssue(title, content, repository, { cli: this.cli, repository: this.repository });
74
+ await createIssue(title, content, repository, { cli: this.cli, req: this.req });
75
75
  }
76
76
  }
@@ -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,24 @@
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
+ commitAndPushVulnerabilitiesJSON,
12
+ NEXT_SECURITY_RELEASE_FOLDER
10
13
  } from './security-release/security-release.js';
14
+ import auth from './auth.js';
15
+ import Request from './request.js';
16
+
17
+ const kChanged = Symbol('changed');
11
18
 
12
19
  export default class SecurityBlog {
13
20
  repository = NEXT_SECURITY_RELEASE_REPOSITORY;
21
+ req;
14
22
  constructor(cli) {
15
23
  this.cli = cli;
16
24
  }
@@ -40,8 +48,7 @@ export default class SecurityBlog {
40
48
  affectedVersions: this.getAffectedVersions(content),
41
49
  vulnerabilities: this.getVulnerabilities(content),
42
50
  slug: this.getSlug(releaseDate),
43
- impact: this.getImpact(content),
44
- openSSLUpdate: await this.promptOpenSSLUpdate(cli)
51
+ impact: this.getImpact(content)
45
52
  };
46
53
  const month = releaseDate.toLocaleString('en-US', { month: 'long' }).toLowerCase();
47
54
  const year = releaseDate.getFullYear();
@@ -52,9 +59,94 @@ export default class SecurityBlog {
52
59
  cli.ok(`Pre-release announcement file created at ${file}`);
53
60
  }
54
61
 
55
- promptOpenSSLUpdate(cli) {
56
- return cli.prompt('Does this security release containt OpenSSL updates?', {
57
- defaultAnswer: true
62
+ async createPostRelease() {
63
+ const { cli } = this;
64
+ const credentials = await auth({
65
+ github: true,
66
+ h1: true
67
+ });
68
+
69
+ this.req = new Request(credentials);
70
+
71
+ // checkout on security release branch
72
+ checkoutOnSecurityReleaseBranch(cli, this.repository);
73
+
74
+ // read vulnerabilities JSON file
75
+ const content = getVulnerabilitiesJSON(cli);
76
+ if (!content.releaseDate) {
77
+ cli.error('Release date is not set in vulnerabilities.json,' +
78
+ ' run `git node security --update-date=YYYY/MM/DD` to set the release date.');
79
+ process.exit(1);
80
+ }
81
+
82
+ validateDate(content.releaseDate);
83
+ const releaseDate = new Date(content.releaseDate);
84
+ const template = this.getSecurityPostReleaseTemplate();
85
+ const data = {
86
+ // TODO: read from pre-sec-release
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,68 @@ 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
+ const cveId = report.cveIds?.join(', ');
209
+ if (!cveId) {
210
+ this.cli.error(`CVE ID for vulnerability ${report.link} ${report.title} not found`);
211
+ process.exit(1);
212
+ }
213
+ template += `## ${report.title} (${cveId}) - (${report.severity.rating})\n\n`;
214
+ if (!report.summary) {
215
+ this.cli.error(`Summary missing for vulnerability ${report.link} ` +
216
+ `${report.title}. Please create it before continuing.`);
217
+ process.exit(1);
218
+ }
219
+
220
+ template += `${report.summary}\n\n`;
221
+ const releaseLines = report.affectedVersions.join(', ');
222
+ template += `Impact:\n\n- This vulnerability affects all users\
223
+ in active release lines: ${releaseLines}\n\n`;
224
+ if (!report.patchAuthors) {
225
+ this.cli.error(`Missing patch author for vulnerability ${report.link} ${report.title}`);
226
+ process.exit(1);
227
+ }
228
+ template += `Thank you, to ${report.reporter} for reporting this vulnerability\
229
+ and thank you ${report.patchAuthors.join(' and ')} for fixing it.\n\n`;
230
+ }
231
+ return template;
88
232
  }
89
233
 
90
- getOpenSSLUpdateTemplate(openSSLUpdate) {
91
- if (openSSLUpdate) {
92
- return '\n## OpenSSL Security updates\n\n' +
93
- 'This security release includes OpenSSL security updates\n';
234
+ getDependencyUpdatesTemplate(dependencyUpdates) {
235
+ if (!dependencyUpdates) return '';
236
+ let template = 'This security release includes the following dependency' +
237
+ ' updates to address public vulnerabilities:\n\n';
238
+ for (const dependencyUpdate of Object.values(dependencyUpdates)) {
239
+ for (const dependency of dependencyUpdate) {
240
+ const title = dependency.title.substring(dependency.title.indexOf(':') + ':'.length).trim();
241
+ template += `- ${title}\
242
+ on ${dependency.affectedVersions.join(', ')}\n`;
243
+ }
94
244
  }
95
- return '';
245
+ return template;
246
+ }
247
+
248
+ async getDownloadsTemplate(affectedVersions) {
249
+ let template = '';
250
+ const versionsToBeReleased = (await nv('supported')).filter(
251
+ (v) => affectedVersions.split(', ').includes(`${v.major}.x`)
252
+ );
253
+ for (const version of versionsToBeReleased) {
254
+ const v = `v${version.major}.${version.minor}.${Number(version.patch) + 1}`;
255
+ template += `- [Node.js ${v}](/blog/release/${v}/)\n`;
256
+ }
257
+
258
+ return template;
96
259
  }
97
260
 
98
261
  getSlug(releaseDate) {
@@ -179,4 +342,14 @@ export default class SecurityBlog {
179
342
  'utf-8'
180
343
  );
181
344
  }
345
+
346
+ getSecurityPostReleaseTemplate() {
347
+ return fs.readFileSync(
348
+ new URL(
349
+ './github/templates/security-post-release.md',
350
+ import.meta.url
351
+ ),
352
+ 'utf-8'
353
+ );
354
+ }
182
355
  }
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.1",
4
4
  "description": "Utilities for Node.js core collaborators",
5
5
  "type": "module",
6
6
  "engines": {
@@ -34,8 +34,8 @@
34
34
  ],
35
35
  "license": "MIT",
36
36
  "dependencies": {
37
- "@listr2/prompt-adapter-enquirer": "^2.0.8",
38
- "@node-core/caritat": "^1.3.1",
37
+ "@listr2/prompt-adapter-enquirer": "^2.0.10",
38
+ "@node-core/caritat": "^1.6.0",
39
39
  "@pkgjs/nv": "^0.2.2",
40
40
  "branch-diff": "^3.0.4",
41
41
  "chalk": "^5.3.0",
@@ -44,26 +44,26 @@
44
44
  "clipboardy": "^4.0.0",
45
45
  "core-validate-commit": "^4.0.0",
46
46
  "figures": "^6.1.0",
47
- "ghauth": "^6.0.4",
48
- "inquirer": "^9.2.22",
47
+ "ghauth": "^6.0.5",
48
+ "inquirer": "^9.3.2",
49
49
  "js-yaml": "^4.1.0",
50
- "listr2": "^8.2.1",
50
+ "listr2": "^8.2.3",
51
51
  "lodash": "^4.17.21",
52
52
  "log-symbols": "^6.0.0",
53
53
  "ora": "^8.0.1",
54
- "replace-in-file": "^7.1.0",
55
- "undici": "^6.18.0",
54
+ "replace-in-file": "^8.0.2",
55
+ "undici": "^6.19.2",
56
56
  "which": "^4.0.0",
57
57
  "yargs": "^17.7.2"
58
58
  },
59
59
  "devDependencies": {
60
60
  "@reporters/github": "^1.7.0",
61
- "c8": "^9.1.0",
61
+ "c8": "^10.1.2",
62
62
  "eslint": "^8.57.0",
63
63
  "eslint-config-standard": "^17.1.0",
64
64
  "eslint-plugin-import": "^2.29.1",
65
65
  "eslint-plugin-n": "^16.6.2",
66
- "eslint-plugin-promise": "^6.1.1",
66
+ "eslint-plugin-promise": "^6.4.0",
67
67
  "sinon": "^18.0.0"
68
68
  }
69
69
  }