@node-core/utils 4.3.0 → 4.4.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,5 +1,8 @@
1
1
  import CLI from '../../lib/cli.js';
2
2
  import SecurityReleaseSteward from '../../lib/prepare_security.js';
3
+ import UpdateSecurityRelease from '../../lib/update_security_release.js';
4
+ import SecurityBlog from '../../lib/security_blog.js';
5
+ import SecurityAnnouncement from '../../lib/security-announcement.js';
3
6
 
4
7
  export const command = 'security [options]';
5
8
  export const describe = 'Manage an in-progress security release or start a new one.';
@@ -8,6 +11,26 @@ const securityOptions = {
8
11
  start: {
9
12
  describe: 'Start security release process',
10
13
  type: 'boolean'
14
+ },
15
+ 'update-date': {
16
+ describe: 'Updates the target date of the security release',
17
+ type: 'string'
18
+ },
19
+ 'add-report': {
20
+ describe: 'Extracts data from HackerOne report and adds it into vulnerabilities.json',
21
+ type: 'string'
22
+ },
23
+ 'remove-report': {
24
+ describe: 'Removes a report from vulnerabilities.json',
25
+ type: 'string'
26
+ },
27
+ 'pre-release': {
28
+ describe: 'Create the pre-release announcement',
29
+ type: 'boolean'
30
+ },
31
+ 'notify-pre-release': {
32
+ describe: 'Notify the community about the security release',
33
+ type: 'boolean'
11
34
  }
12
35
  };
13
36
 
@@ -15,21 +38,94 @@ let yargsInstance;
15
38
 
16
39
  export function builder(yargs) {
17
40
  yargsInstance = yargs;
18
- return yargs.options(securityOptions).example(
19
- 'git node security --start',
20
- 'Prepare a security release of Node.js');
41
+ return yargs.options(securityOptions)
42
+ .example(
43
+ 'git node security --start',
44
+ 'Prepare a security release of Node.js')
45
+ .example(
46
+ 'git node security --update-date=YYYY/MM/DD',
47
+ 'Updates the target date of the security release'
48
+ )
49
+ .example(
50
+ 'git node security --add-report=H1-ID',
51
+ 'Fetches HackerOne report based on ID provided and adds it into vulnerabilities.json'
52
+ )
53
+ .example(
54
+ 'git node security --remove-report=H1-ID',
55
+ 'Removes the Hackerone report based on ID provided from vulnerabilities.json'
56
+ )
57
+ .example(
58
+ 'git node security --pre-release' +
59
+ 'Create the pre-release announcement on the Nodejs.org repo'
60
+ ).example(
61
+ 'git node security --notify-pre-release' +
62
+ 'Notifies the community about the security release'
63
+ );
21
64
  }
22
65
 
23
66
  export function handler(argv) {
24
67
  if (argv.start) {
25
68
  return startSecurityRelease(argv);
26
69
  }
70
+ if (argv['update-date']) {
71
+ return updateReleaseDate(argv);
72
+ }
73
+ if (argv['pre-release']) {
74
+ return createPreRelease(argv);
75
+ }
76
+ if (argv['add-report']) {
77
+ return addReport(argv);
78
+ }
79
+ if (argv['remove-report']) {
80
+ return removeReport(argv);
81
+ }
82
+ if (argv['notify-pre-release']) {
83
+ return notifyPreRelease(argv);
84
+ }
27
85
  yargsInstance.showHelp();
28
86
  }
29
87
 
30
- async function startSecurityRelease(argv) {
88
+ async function removeReport(argv) {
89
+ const reportId = argv['remove-report'];
90
+ const logStream = process.stdout.isTTY ? process.stdout : process.stderr;
91
+ const cli = new CLI(logStream);
92
+ const update = new UpdateSecurityRelease(cli);
93
+ return update.removeReport(reportId);
94
+ }
95
+
96
+ async function addReport(argv) {
97
+ const reportId = argv['add-report'];
98
+ const logStream = process.stdout.isTTY ? process.stdout : process.stderr;
99
+ const cli = new CLI(logStream);
100
+ const update = new UpdateSecurityRelease(cli);
101
+ return update.addReport(reportId);
102
+ }
103
+
104
+ async function updateReleaseDate(argv) {
105
+ const releaseDate = argv['update-date'];
106
+ const logStream = process.stdout.isTTY ? process.stdout : process.stderr;
107
+ const cli = new CLI(logStream);
108
+ const update = new UpdateSecurityRelease(cli);
109
+ return update.updateReleaseDate(releaseDate);
110
+ }
111
+
112
+ async function createPreRelease() {
113
+ const logStream = process.stdout.isTTY ? process.stdout : process.stderr;
114
+ const cli = new CLI(logStream);
115
+ const preRelease = new SecurityBlog(cli);
116
+ return preRelease.createPreRelease();
117
+ }
118
+
119
+ async function startSecurityRelease() {
31
120
  const logStream = process.stdout.isTTY ? process.stdout : process.stderr;
32
121
  const cli = new CLI(logStream);
33
122
  const release = new SecurityReleaseSteward(cli);
34
123
  return release.start();
35
124
  }
125
+
126
+ async function notifyPreRelease() {
127
+ const logStream = process.stdout.isTTY ? process.stdout : process.stderr;
128
+ const cli = new CLI(logStream);
129
+ const preRelease = new SecurityAnnouncement(cli);
130
+ return preRelease.notifyPreRelease();
131
+ }
@@ -0,0 +1,30 @@
1
+ ---
2
+ date: %ANNOUNCEMENT_DATE%
3
+ category: vulnerability
4
+ title: %RELEASE_DATE% Security Releases
5
+ slug: %SLUG%
6
+ layout: blog-post
7
+ author: The Node.js Project
8
+ ---
9
+
10
+ # Summary
11
+
12
+ The Node.js project will release new versions of the %AFFECTED_VERSIONS%
13
+ releases lines on or shortly after, %RELEASE_DATE% in order to address:
14
+
15
+ %VULNERABILITIES%
16
+ %OPENSSL_UPDATES%
17
+ ## Impact
18
+
19
+ %IMPACT%
20
+
21
+ ## Release timing
22
+
23
+ Releases will be available on, or shortly after, %RELEASE_DATE%.
24
+
25
+ ## Contact and future updates
26
+
27
+ The current Node.js security policy can be found at https://nodejs.org/en/security/.
28
+ 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.
29
+
30
+ 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.
@@ -1,19 +1,21 @@
1
1
  import nv from '@pkgjs/nv';
2
- import auth from './auth.js';
3
- import Request from './request.js';
4
2
  import fs from 'node:fs';
5
- import { runSync } from './run.js';
6
3
  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
- };
4
+ import auth from './auth.js';
5
+ import Request from './request.js';
6
+ import {
7
+ NEXT_SECURITY_RELEASE_BRANCH,
8
+ NEXT_SECURITY_RELEASE_FOLDER,
9
+ NEXT_SECURITY_RELEASE_REPOSITORY,
10
+ PLACEHOLDERS,
11
+ checkoutOnSecurityReleaseBranch,
12
+ commitAndPushVulnerabilitiesJSON,
13
+ getSummary,
14
+ validateDate
15
+ } from './security-release/security-release.js';
15
16
 
16
17
  export default class SecurityReleaseSteward {
18
+ repository = NEXT_SECURITY_RELEASE_REPOSITORY;
17
19
  constructor(cli) {
18
20
  this.cli = cli;
19
21
  }
@@ -28,9 +30,10 @@ export default class SecurityReleaseSteward {
28
30
  const req = new Request(credentials);
29
31
  const release = new PrepareSecurityRelease(req);
30
32
  const releaseDate = await release.promptReleaseDate(cli);
31
- let securityReleasePRUrl = PLACEHOLDERS.vulnerabilitiesPRURL;
32
-
33
+ validateDate(releaseDate);
33
34
  const createVulnerabilitiesJSON = await release.promptVulnerabilitiesJSON(cli);
35
+
36
+ let securityReleasePRUrl;
34
37
  if (createVulnerabilitiesJSON) {
35
38
  securityReleasePRUrl = await this.createVulnerabilitiesJSON(req, release, { cli });
36
39
  }
@@ -38,7 +41,7 @@ export default class SecurityReleaseSteward {
38
41
  const createIssue = await release.promptCreateRelaseIssue(cli);
39
42
 
40
43
  if (createIssue) {
41
- const { content } = release.buildIssue(releaseDate, securityReleasePRUrl);
44
+ const content = await release.buildIssue(releaseDate, securityReleasePRUrl);
42
45
  await release.createIssue(content, { cli });
43
46
  };
44
47
 
@@ -47,7 +50,7 @@ export default class SecurityReleaseSteward {
47
50
 
48
51
  async createVulnerabilitiesJSON(req, release, { cli }) {
49
52
  // checkout on the next-security-release branch
50
- release.checkoutOnSecurityReleaseBranch(cli);
53
+ checkoutOnSecurityReleaseBranch(cli, this.repository);
51
54
 
52
55
  // choose the reports to include in the security release
53
56
  const reports = await release.chooseReports(cli);
@@ -62,13 +65,14 @@ export default class SecurityReleaseSteward {
62
65
  cli.info(`To push the vulnerabilities.json file run:
63
66
  - git add ${filePath}
64
67
  - git commit -m "chore: create vulnerabilities.json for next security release"
65
- - git push -u origin next-security-release
68
+ - git push -u origin ${NEXT_SECURITY_RELEASE_BRANCH}
66
69
  - open a PR on ${release.repository.owner}/${release.repository.repo}`);
67
70
  return;
68
71
  };
69
72
 
70
73
  // commit and push the vulnerabilities.json file
71
- release.commitAndPushVulnerabilitiesJSON(filePath, cli);
74
+ const commitMessage = 'chore: create vulnerabilities.json for next security release';
75
+ commitAndPushVulnerabilitiesJSON(filePath, commitMessage, { cli, repository: this.repository });
72
76
 
73
77
  const createPr = await release.promptCreatePR(cli);
74
78
 
@@ -80,13 +84,8 @@ export default class SecurityReleaseSteward {
80
84
  }
81
85
 
82
86
  class PrepareSecurityRelease {
83
- repository = {
84
- owner: 'nodejs-private',
85
- repo: 'security-release'
86
- };
87
-
87
+ repository = NEXT_SECURITY_RELEASE_REPOSITORY;
88
88
  title = 'Next Security Release';
89
- nextSecurityReleaseBranch = 'next-security-release';
90
89
 
91
90
  constructor(req, repository) {
92
91
  this.req = req;
@@ -101,41 +100,31 @@ class PrepareSecurityRelease {
101
100
  { defaultAnswer: true });
102
101
  }
103
102
 
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);
103
+ async getSecurityIssueTemplate() {
104
+ const url = 'https://raw.githubusercontent.com/nodejs/node/main/doc/contributing/security-release-process.md';
105
+ try {
106
+ // fetch document from nodejs/node main so we dont need to keep a copy
107
+ const response = await fetch(url);
108
+ const body = await response.text();
109
+ // remove everything before the Planning section
110
+ const index = body.indexOf('## Planning');
111
+ if (index !== -1) {
112
+ return body.substring(index);
113
+ }
114
+ return body;
115
+ } catch (error) {
116
+ this.cli.error(`Could not retrieve the security issue template from ${url}`);
112
117
  }
113
118
  }
114
119
 
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
120
  async promptReleaseDate(cli) {
136
- return cli.prompt('Enter target release date in YYYY-MM-DD format:', {
121
+ const nextWeekDate = new Date();
122
+ nextWeekDate.setDate(nextWeekDate.getDate() + 7);
123
+ // Format the date as YYYY/MM/DD
124
+ const formattedDate = nextWeekDate.toISOString().slice(0, 10).replace(/-/g, '/');
125
+ return cli.prompt('Enter target release date in YYYY/MM/DD format:', {
137
126
  questionType: 'input',
138
- defaultAnswer: 'TBD'
127
+ defaultAnswer: formattedDate
139
128
  });
140
129
  }
141
130
 
@@ -157,17 +146,17 @@ class PrepareSecurityRelease {
157
146
  { defaultAnswer: true });
158
147
  }
159
148
 
160
- buildIssue(releaseDate, securityReleasePRUrl) {
161
- const template = this.getSecurityIssueTemplate();
149
+ async buildIssue(releaseDate, securityReleasePRUrl = PLACEHOLDERS.vulnerabilitiesPRURL) {
150
+ const template = await this.getSecurityIssueTemplate();
162
151
  const content = template.replace(PLACEHOLDERS.releaseDate, releaseDate)
163
152
  .replace(PLACEHOLDERS.vulnerabilitiesPRURL, securityReleasePRUrl);
164
- return { releaseDate, content, securityReleasePRUrl };
153
+ return content;
165
154
  }
166
155
 
167
156
  async createIssue(content, { cli }) {
168
157
  const data = await this.req.createIssue(this.title, content, this.repository);
169
158
  if (data.html_url) {
170
- cli.ok('Created: ' + data.html_url);
159
+ cli.ok(`Created: ${data.html_url}`);
171
160
  } else {
172
161
  cli.error(data);
173
162
  process.exit(1);
@@ -178,15 +167,32 @@ class PrepareSecurityRelease {
178
167
  cli.info('Getting triaged H1 reports...');
179
168
  const reports = await this.req.getTriagedReports();
180
169
  const supportedVersions = (await nv('supported'))
181
- .map((v) => v.versionName + '.x')
170
+ .map((v) => `${v.versionName}.x`)
182
171
  .join(',');
183
172
  const selectedReports = [];
184
173
 
185
174
  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';
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
+
188
194
  cli.separator();
189
- cli.info(`Report: ${id} - ${title} (${reportLevel})`);
195
+ cli.info(`Report: ${link} - ${title} (${reportSeverity?.rating})`);
190
196
  const include = await cli.prompt(
191
197
  'Would you like to include this report to the next security release?',
192
198
  { defaultAnswer: true });
@@ -198,47 +204,30 @@ class PrepareSecurityRelease {
198
204
  questionType: 'input',
199
205
  defaultAnswer: supportedVersions
200
206
  });
201
- const summaryContent = await this.getSummary(id);
207
+ const summaryContent = await getSummary(id, this.req);
202
208
 
203
209
  selectedReports.push({
204
210
  id,
205
211
  title,
206
212
  cve_ids,
207
- severity: reportLevel,
213
+ severity: reportSeverity,
208
214
  summary: summaryContent ?? '',
209
- affectedVersions: versions.split(',').map((v) => v.replace('v', '').trim())
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
210
219
  });
211
220
  }
212
221
  return selectedReports;
213
222
  }
214
223
 
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
224
  async createVulnerabilitiesJSON(reports, { cli }) {
236
225
  cli.separator('Creating vulnerabilities.json...');
237
226
  const file = JSON.stringify({
238
227
  reports
239
228
  }, null, 2);
240
229
 
241
- const folderPath = path.join(process.cwd(), 'security-release', 'next-security-release');
230
+ const folderPath = path.join(process.cwd(), NEXT_SECURITY_RELEASE_FOLDER);
242
231
  try {
243
232
  await fs.accessSync(folderPath);
244
233
  } catch (error) {
@@ -267,17 +256,16 @@ class PrepareSecurityRelease {
267
256
  );
268
257
  const url = response?.html_url;
269
258
  if (url) {
270
- cli.ok('Created: ' + url);
259
+ cli.ok(`Created: ${url}`);
271
260
  return url;
272
- } else {
273
- if (response?.errors) {
274
- for (const error of response.errors) {
275
- cli.error(error.message);
276
- }
277
- } else {
278
- cli.error(response);
261
+ }
262
+ if (response?.errors) {
263
+ for (const error of response.errors) {
264
+ cli.error(error.message);
279
265
  }
280
- process.exit(1);
266
+ } else {
267
+ cli.error(response);
281
268
  }
269
+ process.exit(1);
282
270
  }
283
271
  }
@@ -0,0 +1,83 @@
1
+ import {
2
+ NEXT_SECURITY_RELEASE_REPOSITORY,
3
+ checkoutOnSecurityReleaseBranch,
4
+ getVulnerabilitiesJSON,
5
+ validateDate,
6
+ formatDateToYYYYMMDD
7
+ } from './security-release/security-release.js';
8
+ import auth from './auth.js';
9
+ import Request from './request.js';
10
+
11
+ export default class SecurityAnnouncement {
12
+ repository = NEXT_SECURITY_RELEASE_REPOSITORY;
13
+ req;
14
+ constructor(cli) {
15
+ this.cli = cli;
16
+ }
17
+
18
+ async notifyPreRelease() {
19
+ const { cli } = this;
20
+
21
+ const credentials = await auth({
22
+ github: true,
23
+ h1: true
24
+ });
25
+
26
+ this.req = new Request(credentials);
27
+
28
+ // checkout on security release branch
29
+ checkoutOnSecurityReleaseBranch(cli, this.repository);
30
+ // read vulnerabilities JSON file
31
+ const content = getVulnerabilitiesJSON(cli);
32
+ // validate the release date read from vulnerabilities JSON
33
+ if (!content.releaseDate) {
34
+ cli.error('Release date is not set in vulnerabilities.json,' +
35
+ ' run `git node security --update-date=YYYY/MM/DD` to set the release date.');
36
+ process.exit(1);
37
+ }
38
+
39
+ validateDate(content.releaseDate);
40
+ const releaseDate = new Date(content.releaseDate);
41
+
42
+ await Promise.all([this.createDockerNodeIssue(releaseDate),
43
+ this.createBuildWGIssue(releaseDate)]);
44
+ }
45
+
46
+ async createBuildWGIssue(releaseDate) {
47
+ const repository = {
48
+ owner: 'nodejs',
49
+ repo: 'build'
50
+ };
51
+
52
+ const { title, content } = this.createPreleaseAnnouncementIssue(releaseDate, 'build');
53
+ await this.createIssue(title, content, repository);
54
+ }
55
+
56
+ createPreleaseAnnouncementIssue(releaseDate, team) {
57
+ const title = `[NEXT-SECURITY-RELEASE] Heads up on upcoming Node.js\
58
+ security release ${formatDateToYYYYMMDD(releaseDate)}`;
59
+ const content = 'As per security release workflow,' +
60
+ ` creating issue to give the ${team} team a heads up.`;
61
+ return { title, content };
62
+ }
63
+
64
+ async createDockerNodeIssue(releaseDate) {
65
+ const repository = {
66
+ owner: 'nodejs',
67
+ repo: 'docker-node'
68
+ };
69
+
70
+ 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
+ }
82
+ }
83
+ }
@@ -0,0 +1,109 @@
1
+ import { runSync } from '../run.js';
2
+ import nv from '@pkgjs/nv';
3
+ import fs from 'node:fs';
4
+ import path from 'node:path';
5
+
6
+ export const NEXT_SECURITY_RELEASE_BRANCH = 'next-security-release';
7
+ export const NEXT_SECURITY_RELEASE_FOLDER = 'security-release/next-security-release';
8
+
9
+ export const NEXT_SECURITY_RELEASE_REPOSITORY = {
10
+ owner: 'nodejs-private',
11
+ repo: 'security-release'
12
+ };
13
+
14
+ export const PLACEHOLDERS = {
15
+ releaseDate: '%RELEASE_DATE%',
16
+ vulnerabilitiesPRURL: '%VULNERABILITIES_PR_URL%',
17
+ preReleasePrivate: '%PRE_RELEASE_PRIV%',
18
+ postReleasePrivate: '%POS_RELEASE_PRIV%',
19
+ affectedLines: '%AFFECTED_LINES%',
20
+ annoucementDate: '%ANNOUNCEMENT_DATE%',
21
+ slug: '%SLUG%',
22
+ affectedVersions: '%AFFECTED_VERSIONS%',
23
+ openSSLUpdate: '%OPENSSL_UPDATES%',
24
+ impact: '%IMPACT%',
25
+ vulnerabilities: '%VULNERABILITIES%'
26
+ };
27
+
28
+ export function checkRemote(cli, repository) {
29
+ const remote = runSync('git', ['ls-remote', '--get-url', 'origin']).trim();
30
+ const { owner, repo } = repository;
31
+ const securityReleaseOrigin = [
32
+ `https://github.com/${owner}/${repo}.git`,
33
+ `git@github.com:${owner}/${repo}.git`
34
+ ];
35
+
36
+ if (!securityReleaseOrigin.includes(remote)) {
37
+ cli.error(`Wrong repository! It should be ${securityReleaseOrigin}`);
38
+ process.exit(1);
39
+ }
40
+ }
41
+
42
+ export function checkoutOnSecurityReleaseBranch(cli, repository) {
43
+ checkRemote(cli, repository);
44
+ const currentBranch = runSync('git', ['branch', '--show-current']).trim();
45
+ cli.info(`Current branch: ${currentBranch} `);
46
+
47
+ if (currentBranch !== NEXT_SECURITY_RELEASE_BRANCH) {
48
+ runSync('git', ['checkout', '-B', NEXT_SECURITY_RELEASE_BRANCH]);
49
+ cli.ok(`Checkout on branch: ${NEXT_SECURITY_RELEASE_BRANCH} `);
50
+ };
51
+ }
52
+
53
+ export function commitAndPushVulnerabilitiesJSON(filePath, commitMessage, { cli, repository }) {
54
+ checkRemote(cli, repository);
55
+
56
+ if (Array.isArray(filePath)) {
57
+ for (const path of filePath) {
58
+ runSync('git', ['add', path]);
59
+ }
60
+ } else {
61
+ runSync('git', ['add', filePath]);
62
+ }
63
+
64
+ runSync('git', ['commit', '-m', commitMessage]);
65
+ runSync('git', ['push', '-u', 'origin', NEXT_SECURITY_RELEASE_BRANCH]);
66
+ cli.ok(`Pushed commit: ${commitMessage} to ${NEXT_SECURITY_RELEASE_BRANCH}`);
67
+ }
68
+
69
+ export async function getSupportedVersions() {
70
+ const supportedVersions = (await nv('supported'))
71
+ .map((v) => `${v.versionName}.x`)
72
+ .join(',');
73
+ return supportedVersions;
74
+ }
75
+
76
+ export async function getSummary(reportId, req) {
77
+ const { data } = await req.getReport(reportId);
78
+ const summaryList = data?.relationships?.summaries?.data;
79
+ if (!summaryList?.length) return;
80
+ const summaries = summaryList.filter((summary) => summary?.attributes?.category === 'team');
81
+ if (!summaries?.length) return;
82
+ return summaries?.[0].attributes?.content;
83
+ }
84
+
85
+ export function getVulnerabilitiesJSON(cli) {
86
+ const vulnerabilitiesJSONPath = path.join(process.cwd(),
87
+ NEXT_SECURITY_RELEASE_FOLDER, 'vulnerabilities.json');
88
+ cli.startSpinner(`Reading vulnerabilities.json from ${vulnerabilitiesJSONPath}..`);
89
+ const file = JSON.parse(fs.readFileSync(vulnerabilitiesJSONPath, 'utf-8'));
90
+ cli.stopSpinner(`Done reading vulnerabilities.json from ${vulnerabilitiesJSONPath}`);
91
+ return file;
92
+ }
93
+
94
+ export function validateDate(releaseDate) {
95
+ const value = new Date(releaseDate).valueOf();
96
+ if (Number.isNaN(value) || value < 0) {
97
+ throw new Error('Invalid date format');
98
+ }
99
+ }
100
+
101
+ export function formatDateToYYYYMMDD(date) {
102
+ // Get year, month, and day
103
+ const year = date.getFullYear();
104
+ const month = String(date.getMonth() + 1).padStart(2, '0'); // Months are zero-based
105
+ const day = String(date.getDate()).padStart(2, '0');
106
+
107
+ // Concatenate year, month, and day with slashes
108
+ return `${year}/${month}/${day}`;
109
+ }
@@ -0,0 +1,182 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import _ from 'lodash';
4
+ import {
5
+ PLACEHOLDERS,
6
+ getVulnerabilitiesJSON,
7
+ checkoutOnSecurityReleaseBranch,
8
+ NEXT_SECURITY_RELEASE_REPOSITORY,
9
+ validateDate
10
+ } from './security-release/security-release.js';
11
+
12
+ export default class SecurityBlog {
13
+ repository = NEXT_SECURITY_RELEASE_REPOSITORY;
14
+ constructor(cli) {
15
+ this.cli = cli;
16
+ }
17
+
18
+ async createPreRelease() {
19
+ const { cli } = this;
20
+
21
+ // checkout on security release branch
22
+ checkoutOnSecurityReleaseBranch(cli, this.repository);
23
+
24
+ // read vulnerabilities JSON file
25
+ const content = getVulnerabilitiesJSON(cli);
26
+ // validate the release date read from vulnerabilities JSON
27
+ if (!content.releaseDate) {
28
+ cli.error('Release date is not set in vulnerabilities.json,' +
29
+ ' run `git node security --update-date=YYYY/MM/DD` to set the release date.');
30
+ process.exit(1);
31
+ }
32
+
33
+ validateDate(content.releaseDate);
34
+ const releaseDate = new Date(content.releaseDate);
35
+
36
+ const template = this.getSecurityPreReleaseTemplate();
37
+ const data = {
38
+ annoucementDate: await this.getAnnouncementDate(cli),
39
+ releaseDate: this.formatReleaseDate(releaseDate),
40
+ affectedVersions: this.getAffectedVersions(content),
41
+ vulnerabilities: this.getVulnerabilities(content),
42
+ slug: this.getSlug(releaseDate),
43
+ impact: this.getImpact(content),
44
+ openSSLUpdate: await this.promptOpenSSLUpdate(cli)
45
+ };
46
+ const month = releaseDate.toLocaleString('en-US', { month: 'long' }).toLowerCase();
47
+ const year = releaseDate.getFullYear();
48
+ const fileName = `${month}-${year}-security-releases.md`;
49
+ const preRelease = this.buildPreRelease(template, data);
50
+ const file = path.join(process.cwd(), fileName);
51
+ fs.writeFileSync(file, preRelease);
52
+ cli.ok(`Pre-release announcement file created at ${file}`);
53
+ }
54
+
55
+ promptOpenSSLUpdate(cli) {
56
+ return cli.prompt('Does this security release containt OpenSSL updates?', {
57
+ defaultAnswer: true
58
+ });
59
+ }
60
+
61
+ formatReleaseDate(releaseDate) {
62
+ const options = {
63
+ weekday: 'long',
64
+ month: 'long',
65
+ day: 'numeric',
66
+ year: 'numeric'
67
+ };
68
+ return releaseDate.toLocaleDateString('en-US', options);
69
+ }
70
+
71
+ buildPreRelease(template, data) {
72
+ const {
73
+ annoucementDate,
74
+ releaseDate,
75
+ affectedVersions,
76
+ vulnerabilities,
77
+ slug,
78
+ impact,
79
+ openSSLUpdate
80
+ } = data;
81
+ return template.replaceAll(PLACEHOLDERS.annoucementDate, annoucementDate)
82
+ .replaceAll(PLACEHOLDERS.slug, slug)
83
+ .replaceAll(PLACEHOLDERS.affectedVersions, affectedVersions)
84
+ .replaceAll(PLACEHOLDERS.vulnerabilities, vulnerabilities)
85
+ .replaceAll(PLACEHOLDERS.releaseDate, releaseDate)
86
+ .replaceAll(PLACEHOLDERS.impact, impact)
87
+ .replaceAll(PLACEHOLDERS.openSSLUpdate, this.getOpenSSLUpdateTemplate(openSSLUpdate));
88
+ }
89
+
90
+ getOpenSSLUpdateTemplate(openSSLUpdate) {
91
+ if (openSSLUpdate) {
92
+ return '\n## OpenSSL Security updates\n\n' +
93
+ 'This security release includes OpenSSL security updates\n';
94
+ }
95
+ return '';
96
+ }
97
+
98
+ getSlug(releaseDate) {
99
+ const month = releaseDate.toLocaleString('en-US', { month: 'long' });
100
+ const year = releaseDate.getFullYear();
101
+ return `${month.toLocaleLowerCase()}-${year}-security-releases`;
102
+ }
103
+
104
+ async getAnnouncementDate(cli) {
105
+ try {
106
+ const date = await this.promptAnnouncementDate(cli);
107
+ validateDate(date);
108
+ return new Date(date).toISOString();
109
+ } catch (error) {
110
+ return PLACEHOLDERS.annoucementDate;
111
+ }
112
+ }
113
+
114
+ promptAnnouncementDate(cli) {
115
+ const today = new Date().toISOString().substring(0, 10).replace(/-/g, '/');
116
+ return cli.prompt('When is the security release going to be announced? ' +
117
+ 'Enter in YYYY/MM/DD format:', {
118
+ questionType: 'input',
119
+ defaultAnswer: today
120
+ });
121
+ }
122
+
123
+ getImpact(content) {
124
+ const impact = content.reports.reduce((acc, report) => {
125
+ for (const affectedVersion of report.affectedVersions) {
126
+ if (acc[affectedVersion]) {
127
+ acc[affectedVersion].push(report);
128
+ } else {
129
+ acc[affectedVersion] = [report];
130
+ }
131
+ }
132
+ return acc;
133
+ }, {});
134
+
135
+ const impactText = [];
136
+ for (const [key, value] of Object.entries(impact)) {
137
+ const groupedByRating = Object.values(_.groupBy(value, 'severity.rating'))
138
+ .map(severity => {
139
+ if (!severity[0]?.severity?.rating) {
140
+ this.cli.error(`severity.rating not found for the report ${severity[0].id}. \
141
+ Please add it manually before continuing.`);
142
+ process.exit(1);
143
+ }
144
+ const firstSeverityRating = severity[0].severity.rating.toLocaleLowerCase();
145
+ return `${severity.length} ${firstSeverityRating} severity issues`;
146
+ }).join(', ');
147
+
148
+ impactText.push(`The ${key} release line of Node.js is vulnerable to ${groupedByRating}.`);
149
+ }
150
+
151
+ return impactText.join('\n');
152
+ }
153
+
154
+ getVulnerabilities(content) {
155
+ const grouped = _.groupBy(content.reports, 'severity.rating');
156
+ const text = [];
157
+ for (const [key, value] of Object.entries(grouped)) {
158
+ text.push(`- ${value.length} ${key.toLocaleLowerCase()} severity issues.`);
159
+ }
160
+ return text.join('\n');
161
+ }
162
+
163
+ getAffectedVersions(content) {
164
+ const affectedVersions = new Set();
165
+ for (const report of Object.values(content.reports)) {
166
+ for (const affectedVersion of report.affectedVersions) {
167
+ affectedVersions.add(affectedVersion);
168
+ }
169
+ }
170
+ return Array.from(affectedVersions).join(', ');
171
+ }
172
+
173
+ getSecurityPreReleaseTemplate() {
174
+ return fs.readFileSync(
175
+ new URL(
176
+ './github/templates/security-pre-release.md',
177
+ import.meta.url
178
+ ),
179
+ 'utf-8'
180
+ );
181
+ }
182
+ }
@@ -0,0 +1,138 @@
1
+ import {
2
+ NEXT_SECURITY_RELEASE_FOLDER,
3
+ NEXT_SECURITY_RELEASE_REPOSITORY,
4
+ checkoutOnSecurityReleaseBranch,
5
+ commitAndPushVulnerabilitiesJSON,
6
+ getSupportedVersions,
7
+ getSummary,
8
+ validateDate
9
+ } from './security-release/security-release.js';
10
+ import fs from 'node:fs';
11
+ import path from 'node:path';
12
+ import auth from './auth.js';
13
+ import Request from './request.js';
14
+
15
+ export default class UpdateSecurityRelease {
16
+ repository = NEXT_SECURITY_RELEASE_REPOSITORY;
17
+ constructor(cli) {
18
+ this.cli = cli;
19
+ }
20
+
21
+ async updateReleaseDate(releaseDate) {
22
+ const { cli } = this;
23
+
24
+ try {
25
+ validateDate(releaseDate);
26
+ } catch (error) {
27
+ cli.error('Invalid date format. Please use the format yyyy/mm/dd.');
28
+ process.exit(1);
29
+ }
30
+
31
+ // checkout on the next-security-release branch
32
+ checkoutOnSecurityReleaseBranch(cli, this.repository);
33
+
34
+ // update the release date in the vulnerabilities.json file
35
+ const updatedVulnerabilitiesFiles = await this.updateVulnerabilitiesJSON(releaseDate, { cli });
36
+
37
+ const commitMessage = `chore: update the release date to ${releaseDate}`;
38
+ commitAndPushVulnerabilitiesJSON(updatedVulnerabilitiesFiles,
39
+ commitMessage, { cli, repository: this.repository });
40
+ cli.ok('Done!');
41
+ }
42
+
43
+ readVulnerabilitiesJSON(vulnerabilitiesJSONPath) {
44
+ const exists = fs.existsSync(vulnerabilitiesJSONPath);
45
+
46
+ if (!exists) {
47
+ this.cli.error(`The file vulnerabilities.json does not exist at ${vulnerabilitiesJSONPath}`);
48
+ process.exit(1);
49
+ }
50
+
51
+ return JSON.parse(fs.readFileSync(vulnerabilitiesJSONPath, 'utf8'));
52
+ }
53
+
54
+ getVulnerabilitiesJSONPath() {
55
+ return path.join(process.cwd(),
56
+ NEXT_SECURITY_RELEASE_FOLDER, 'vulnerabilities.json');
57
+ }
58
+
59
+ async updateVulnerabilitiesJSON(releaseDate) {
60
+ const vulnerabilitiesJSONPath = this.getVulnerabilitiesJSONPath();
61
+ const content = this.readVulnerabilitiesJSON(vulnerabilitiesJSONPath);
62
+ content.releaseDate = releaseDate;
63
+
64
+ fs.writeFileSync(vulnerabilitiesJSONPath, JSON.stringify(content, null, 2));
65
+
66
+ this.cli.ok(`Updated the release date in vulnerabilities.json: ${releaseDate}`);
67
+ return [vulnerabilitiesJSONPath];
68
+ }
69
+
70
+ async addReport(reportId) {
71
+ const { cli } = this;
72
+ const credentials = await auth({
73
+ github: true,
74
+ h1: true
75
+ });
76
+
77
+ const req = new Request(credentials);
78
+ // checkout on the next-security-release branch
79
+ checkoutOnSecurityReleaseBranch(cli, this.repository);
80
+
81
+ // get h1 report
82
+ 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
+ };
106
+
107
+ const vulnerabilitiesJSONPath = this.getVulnerabilitiesJSONPath();
108
+ const content = this.readVulnerabilitiesJSON(vulnerabilitiesJSONPath);
109
+ content.reports.push(entry);
110
+ 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`;
113
+ commitAndPushVulnerabilitiesJSON(vulnerabilitiesJSONPath,
114
+ commitMessage, { cli, repository: this.repository });
115
+ cli.ok('Done!');
116
+ }
117
+
118
+ removeReport(reportId) {
119
+ const { cli } = this;
120
+ // checkout on the next-security-release branch
121
+ checkoutOnSecurityReleaseBranch(cli, this.repository);
122
+ const vulnerabilitiesJSONPath = this.getVulnerabilitiesJSONPath();
123
+ const content = this.readVulnerabilitiesJSON(vulnerabilitiesJSONPath);
124
+ const found = content.reports.some((report) => report.id === reportId);
125
+ if (!found) {
126
+ cli.error(`Report with id ${reportId} not found in vulnerabilities.json`);
127
+ process.exit(1);
128
+ }
129
+ content.reports = content.reports.filter((report) => report.id !== reportId);
130
+ fs.writeFileSync(vulnerabilitiesJSONPath, JSON.stringify(content, null, 2));
131
+ this.cli.ok(`Updated vulnerabilities.json with the report: ${reportId}`);
132
+
133
+ const commitMessage = `chore: remove report ${reportId} from vulnerabilities.json`;
134
+ commitAndPushVulnerabilitiesJSON(vulnerabilitiesJSONPath,
135
+ commitMessage, { cli, repository: this.repository });
136
+ cli.ok('Done!');
137
+ }
138
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@node-core/utils",
3
- "version": "4.3.0",
3
+ "version": "4.4.0",
4
4
  "description": "Utilities for Node.js core collaborators",
5
5
  "type": "module",
6
6
  "engines": {
@@ -1,98 +0,0 @@
1
- ## Planning
2
-
3
- * [X] Open an [issue](https://github.com/nodejs-private/node-private) titled
4
- `Next Security Release`, and put this checklist in the description.
5
-
6
- * [ ] Get agreement on the [list of vulnerabilities](%VULNERABILITIES_PR_URL%) to be addressed.
7
-
8
- * [ ] PR release announcements in [private](https://github.com/nodejs-private/nodejs.org-private):
9
- * [ ] pre-release: %PRE_RELEASE_PRIV%
10
- * [ ] post-release: %POS_RELEASE_PRIV%
11
- * List vulnerabilities in order of descending severity
12
- * Use the "summary" feature in HackerOne to sync post-release content
13
- and CVE requests. Example [2038134](https://hackerone.com/bugs?subject=nodejs\&report_id=2038134)
14
- * Ask the HackerOne reporter if they would like to be credited on the
15
- security release blog page
16
-
17
- * [ ] Get agreement on the planned date for the release: %RELEASE_DATE%
18
-
19
- * [ ] Get release team volunteers for all affected lines:
20
- %AFFECTED_LINES%
21
-
22
- ## Announcement (one week in advance of the planned release)
23
-
24
- * [ ] Check that all vulnerabilities are ready for release integration:
25
- * PRs against all affected release lines or cherry-pick clean
26
- * PRs with breaking changes have a
27
- [--security-revert](#Adding-a-security-revert-option) option if possible.
28
- * Approved
29
- * (optional) Approved by the reporter
30
- * Build and send the binary to the reporter according to its architecture
31
- and ask for a review. This step is important to avoid insufficient fixes
32
- between Security Releases.
33
- * Have CVEs
34
- * Make sure that dependent libraries have CVEs for their issues. We should
35
- only create CVEs for vulnerabilities in Node.js itself. This is to avoid
36
- having duplicate CVEs for the same vulnerability.
37
- * Described in the pre/post announcements
38
-
39
- * [ ] Pre-release announcement to nodejs.org blog: TBD
40
- (Re-PR the pre-approved branch from nodejs-private/nodejs.org-private to
41
- nodejs/nodejs.org)
42
-
43
- * [ ] Pre-release announcement [email](https://groups.google.com/forum/#!forum/nodejs-sec): TBD
44
- * Subject: `Node.js security updates for all active release lines, Month Year`
45
-
46
- * [ ] CC `oss-security@lists.openwall.com` on pre-release
47
- * [ ] Forward the email you receive to `oss-security@lists.openwall.com`.
48
-
49
- * [ ] Create a new issue in [nodejs/tweet](https://github.com/nodejs/tweet/issues)
50
-
51
- * [ ] Request releaser(s) to start integrating the PRs to be released.
52
-
53
- * [ ] Notify [docker-node](https://github.com/nodejs/docker-node/issues) of upcoming security release date: TBD
54
-
55
- * [ ] Notify build-wg of upcoming security release date by opening an issue
56
- in [nodejs/build](https://github.com/nodejs/build/issues) to request WG members are available to fix any CI issues: TBD
57
-
58
- ## Release day
59
-
60
- * [ ] [Lock CI](https://github.com/nodejs/build/blob/HEAD/doc/jenkins-guide.md#before-the-release)
61
-
62
- * [ ] The releaser(s) run the release process to completion.
63
-
64
- * [ ] [Unlock CI](https://github.com/nodejs/build/blob/HEAD/doc/jenkins-guide.md#after-the-release)
65
-
66
- * [ ] Post-release announcement to Nodejs.org blog:
67
- * (Re-PR the pre-approved branch from nodejs-private/nodejs.org-private to
68
- nodejs/nodejs.org)
69
-
70
- * [ ] Post-release announcement in reply email: TBD
71
-
72
- * [ ] Notify `#nodejs-social` about the release.
73
-
74
- * [ ] Comment in [docker-node][] issue that release is ready for integration.
75
- The docker-node team will build and release docker image updates.
76
-
77
- * [ ] For every H1 report resolved:
78
- * Close as Resolved
79
- * Request Disclosure
80
- * Request publication of H1 CVE requests
81
- * (Check that the "Version Fixed" field in the CVE is correct, and provide
82
- links to the release blogs in the "Public Reference" section)
83
-
84
- * [ ] PR machine-readable JSON descriptions of the vulnerabilities to the
85
- [core](https://github.com/nodejs/security-wg/tree/HEAD/vuln/core)
86
- vulnerability DB.
87
- * For each vulnerability add a `#.json` file, one can copy an existing
88
- [json](https://github.com/nodejs/security-wg/blob/0d82062d917cb9ddab88f910559469b2b13812bf/vuln/core/78.json)
89
- file, and increment the latest created file number and use that as the name
90
- of the new file to be added. For example, `79.json`.
91
-
92
- * [ ] Close this issue
93
-
94
- * [ ] Make sure the PRs for the vulnerabilities are closed.
95
-
96
- * [ ] PR in that you stewarded the release in
97
- [Security release stewards](https://github.com/nodejs/node/blob/HEAD/doc/contributing/security-release-process.md#security-release-stewards).
98
- If necessary add the next rotation of the steward rotation.