@node-core/utils 5.14.1 → 5.16.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/README.md CHANGED
@@ -47,7 +47,7 @@ If you would prefer to build from the source, install and link:
47
47
  ```
48
48
  git clone git@github.com:nodejs/node-core-utils.git
49
49
  cd node-core-utils
50
- npm install
50
+ npm ci
51
51
  npm link
52
52
  ```
53
53
 
@@ -89,6 +89,14 @@ After the token is generated, create an rc file with the following content:
89
89
  Note: you could use `ncu-config` to configure these variables, but it's not
90
90
  recommended to leave your tokens in your command line history.
91
91
 
92
+ If you have `gpg` installed and setup on your local machine, it is recommended
93
+ to store an encrypted version of this file:
94
+
95
+ ```console
96
+ $ gpg --default-recipient-self --encrypt ~/.ncurc
97
+ $ rm ~/.ncurc
98
+ ```
99
+
92
100
  ### Setting up Jenkins credentials
93
101
 
94
102
  The `git-node` and `ncu-ci` commands need to query the Node.js Jenkins API for
@@ -104,8 +112,9 @@ To obtain the Jenkins API token
104
112
  3. Enter an identifiable name (for example, `node-core-utils`) for this
105
113
  token in the inbox that appears, and click `GENERATE`.
106
114
  4. Copy the generated token.
107
- 5. Add it into your `ncurc` file (`~/.ncurc` or `$XDG_CONFIG_HOME/ncurc`)
108
- with `jenkins_token` as key, like this:
115
+ 5. Add it into your `ncurc` file (`~/.ncurc` or `$XDG_CONFIG_HOME/ncurc`, or
116
+ `~/.ncurc.gpg` or `$XDG_CONFIG_HOME/ncurc.gpg`) with `jenkins_token` as key,
117
+ like this:
109
118
 
110
119
  ```json
111
120
  {
@@ -125,6 +134,7 @@ Put the following entries into your
125
134
  ```
126
135
  # node-core-utils configuration file
127
136
  .ncurc
137
+ .ncurc.gpg
128
138
  # node-core-utils working directory
129
139
  .ncu
130
140
  ```
package/bin/ncu-ci.js CHANGED
@@ -111,8 +111,8 @@ const args = yargs(hideBin(process.argv))
111
111
  builder: (yargs) => {
112
112
  yargs
113
113
  .positional('prid', {
114
- describe: 'ID of the PR',
115
- type: 'number'
114
+ describe: 'ID of the PR or URL to the PR or its head commit',
115
+ type: 'string'
116
116
  })
117
117
  .option('certify-safe', {
118
118
  describe: 'SHA of the commit that is expected to be at the tip of the PR head. ' +
@@ -574,6 +574,15 @@ async function main(command, argv) {
574
574
  // Prepare queue.
575
575
  switch (command) {
576
576
  case 'run': {
577
+ const maybeURL = URL.parse(argv.prid);
578
+ if (maybeURL?.host === 'github.com') {
579
+ const [, owner, repo, , prid, , commit_sha] = maybeURL.pathname.split('/');
580
+ argv.owner ||= owner;
581
+ argv.repo ||= repo;
582
+ argv.certifySafe ||= commit_sha;
583
+ argv.prid = prid;
584
+ }
585
+ argv.prid = Number(argv.prid);
577
586
  const jobRunner = new RunPRJobCommand(cli, request, argv);
578
587
  return jobRunner.start();
579
588
  }
package/lib/config.js CHANGED
@@ -2,6 +2,8 @@ import path from 'node:path';
2
2
  import os from 'node:os';
3
3
 
4
4
  import { readJson, writeJson } from './file.js';
5
+ import { existsSync, mkdtempSync, rmSync } from 'node:fs';
6
+ import { spawnSync } from 'node:child_process';
5
7
 
6
8
  export const GLOBAL_CONFIG = Symbol('globalConfig');
7
9
  export const PROJECT_CONFIG = Symbol('projectConfig');
@@ -25,6 +27,14 @@ export function getMergedConfig(dir, home) {
25
27
 
26
28
  export function getConfig(configType, dir) {
27
29
  const configPath = getConfigPath(configType, dir);
30
+ const encryptedConfigPath = configPath + '.gpg';
31
+ if (existsSync(encryptedConfigPath)) {
32
+ const { status, stdout } =
33
+ spawnSync('gpg', ['--decrypt', encryptedConfigPath]);
34
+ if (status === 0) {
35
+ return JSON.parse(stdout.toString('utf-8'));
36
+ }
37
+ }
28
38
  try {
29
39
  return readJson(configPath);
30
40
  } catch (cause) {
@@ -51,13 +61,31 @@ export function getConfigPath(configType, dir) {
51
61
  };
52
62
 
53
63
  export function writeConfig(configType, obj, dir) {
54
- writeJson(getConfigPath(configType, dir), obj);
64
+ const configPath = getConfigPath(configType, dir);
65
+ const encryptedConfigPath = configPath + '.gpg';
66
+ if (existsSync(encryptedConfigPath)) {
67
+ const tmpDir = mkdtempSync(path.join(os.tmpdir(), 'ncurc-'));
68
+ const tmpFile = path.join(tmpDir, 'config.json');
69
+ try {
70
+ writeJson(tmpFile, obj);
71
+ const { status } = spawnSync('gpg',
72
+ ['--default-recipient-self', '--yes', '--encrypt', '--output', encryptedConfigPath, tmpFile]
73
+ );
74
+ if (status !== 0) {
75
+ throw new Error('Failed to encrypt config file: ' + encryptedConfigPath);
76
+ }
77
+ } finally {
78
+ rmSync(tmpDir, { recursive: true, force: true });
79
+ }
80
+ return encryptedConfigPath;
81
+ }
82
+ writeJson(configPath, obj);
83
+ return configPath;
55
84
  };
56
85
 
57
86
  export function updateConfig(configType, obj, dir) {
58
87
  const config = getConfig(configType, dir);
59
- const configPath = getConfigPath(configType, dir);
60
- writeJson(configPath, Object.assign(config, obj));
88
+ writeConfig(configType, Object.assign(config, obj), dir);
61
89
  };
62
90
 
63
91
  export function getHomeDir(home) {
@@ -55,6 +55,22 @@ export default class PrepareSecurityRelease extends SecurityRelease {
55
55
  // For now, close the ones with Security Release label
56
56
  await this.closePRWithLabel('Security Release');
57
57
 
58
+ if (vulnerabilityJSON.buildIssue) {
59
+ this.cli.info('Commenting on nodejs/build issue');
60
+ await this.req.commentIssue(
61
+ vulnerabilityJSON.buildIssue,
62
+ 'Security release is out'
63
+ );
64
+ }
65
+
66
+ if (vulnerabilityJSON.dockerIssue) {
67
+ this.cli.info('Commenting on nodejs/docker-node issue');
68
+ await this.req.commentIssue(
69
+ vulnerabilityJSON.dockerIssue,
70
+ 'Security release is out'
71
+ );
72
+ }
73
+
58
74
  const updateFolder = await this.cli.prompt(
59
75
  `Would you like to update the next-security-release folder to ${
60
76
  vulnerabilityJSON.releaseDate}?`,
package/lib/request.js CHANGED
@@ -81,6 +81,23 @@ export default class Request {
81
81
  return this.json(url, options);
82
82
  }
83
83
 
84
+ async commentIssue(fullUrl, comment) {
85
+ const commentUrl = fullUrl.replace('https://github.com/', 'https://api.github.com/repos/') +
86
+ '/comments';
87
+ const options = {
88
+ method: 'POST',
89
+ headers: {
90
+ Authorization: `Basic ${this.credentials.github}`,
91
+ 'User-Agent': 'node-core-utils',
92
+ Accept: 'application/vnd.github+json'
93
+ },
94
+ body: JSON.stringify({
95
+ body: comment,
96
+ })
97
+ };
98
+ return this.json(commentUrl, options);
99
+ }
100
+
84
101
  async getPullRequest(fullUrl) {
85
102
  const prUrl = fullUrl.replace('https://github.com/', 'https://api.github.com/repos/').replace('pull', 'pulls');
86
103
  const options = {
@@ -1,9 +1,12 @@
1
+ import fs from 'node:fs';
1
2
  import {
2
3
  NEXT_SECURITY_RELEASE_REPOSITORY,
3
4
  checkoutOnSecurityReleaseBranch,
4
5
  getVulnerabilitiesJSON,
6
+ getVulnerabilitiesJSONPath,
5
7
  validateDate,
6
8
  formatDateToYYYYMMDD,
9
+ commitAndPushVulnerabilitiesJSON,
7
10
  createIssue
8
11
  } from './security-release/security-release.js';
9
12
  import auth from './auth.js';
@@ -40,10 +43,21 @@ export default class SecurityAnnouncement {
40
43
  validateDate(content.releaseDate);
41
44
  const releaseDate = new Date(content.releaseDate);
42
45
 
43
- await Promise.all([
46
+ const [dockerIssue, buildIssue] = await Promise.all([
44
47
  this.createDockerNodeIssue(releaseDate),
45
48
  this.createBuildWGIssue(releaseDate)
46
49
  ]);
50
+
51
+ content.buildIssue = buildIssue;
52
+ content.dockerIssue = dockerIssue;
53
+
54
+ const vulnerabilitiesJSONPath = getVulnerabilitiesJSONPath();
55
+ fs.writeFileSync(vulnerabilitiesJSONPath, JSON.stringify(content, null, 2));
56
+ const commitMessage = 'chore: add build and docker issue link';
57
+ commitAndPushVulnerabilitiesJSON([vulnerabilitiesJSONPath],
58
+ commitMessage, { cli: this.cli, repository: this.repository });
59
+
60
+ this.cli.ok('Added docker and build issue in vulnerabilities.json');
47
61
  }
48
62
 
49
63
  async createBuildWGIssue(releaseDate) {
@@ -53,7 +67,7 @@ export default class SecurityAnnouncement {
53
67
  };
54
68
 
55
69
  const { title, content } = this.createPreleaseAnnouncementIssue(releaseDate, 'build');
56
- await createIssue(title, content, repository, { cli: this.cli, req: this.req });
70
+ return createIssue(title, content, repository, { cli: this.cli, req: this.req });
57
71
  }
58
72
 
59
73
  createPreleaseAnnouncementIssue(releaseDate, team) {
@@ -71,6 +85,6 @@ export default class SecurityAnnouncement {
71
85
  };
72
86
 
73
87
  const { title, content } = this.createPreleaseAnnouncementIssue(releaseDate, 'docker');
74
- await createIssue(title, content, repository, { cli: this.cli, req: this.req });
88
+ return createIssue(title, content, repository, { cli: this.cli, req: this.req });
75
89
  }
76
90
  }
@@ -107,6 +107,12 @@ export function getVulnerabilitiesJSON(cli) {
107
107
  return file;
108
108
  }
109
109
 
110
+ export function getVulnerabilitiesJSONPath() {
111
+ const vulnerabilitiesJSONPath = path.join(process.cwd(),
112
+ NEXT_SECURITY_RELEASE_FOLDER, 'vulnerabilities.json');
113
+ return vulnerabilitiesJSONPath;
114
+ }
115
+
110
116
  export function validateDate(releaseDate) {
111
117
  const value = new Date(releaseDate).valueOf();
112
118
  if (Number.isNaN(value) || value < 0) {
@@ -135,6 +141,7 @@ export async function createIssue(title, content, repository, { cli, req }) {
135
141
  const data = await req.createIssue(title, content, repository);
136
142
  if (data.html_url) {
137
143
  cli.ok(`Created: ${data.html_url}`);
144
+ return data.html_url;
138
145
  } else {
139
146
  cli.error(data);
140
147
  process.exit(1);
@@ -6,7 +6,8 @@ import {
6
6
  PLACEHOLDERS,
7
7
  checkoutOnSecurityReleaseBranch,
8
8
  validateDate,
9
- SecurityRelease
9
+ SecurityRelease,
10
+ commitAndPushVulnerabilitiesJSON,
10
11
  } from './security-release/security-release.js';
11
12
  import auth from './auth.js';
12
13
  import Request from './request.js';
@@ -56,16 +57,41 @@ export default class SecurityBlog extends SecurityRelease {
56
57
  const endDate = new Date(data.annoucementDate);
57
58
  endDate.setDate(endDate.getDate() + 7);
58
59
 
60
+ const link = `https://nodejs.org/en/blog/vulnerability/${fileName}`;
59
61
  this.updateWebsiteBanner(site, {
60
62
  startDate: data.annoucementDate,
61
63
  endDate: endDate.toISOString(),
62
64
  text: `New security releases to be made available ${data.releaseDate}`,
63
- link: `https://nodejs.org/en/blog/vulnerability/${fileName}`,
65
+ link,
64
66
  type: 'warning'
65
67
  });
66
-
67
68
  fs.writeFileSync(file, preRelease);
69
+
68
70
  cli.ok(`Announcement file created and banner has been updated. Folder: ${nodejsOrgFolder}`);
71
+ await this.updateAnnouncementLink(link);
72
+ }
73
+
74
+ async updateAnnouncementLink(link) {
75
+ const vulnerabilitiesJSONPath = this.getVulnerabilitiesJSONPath();
76
+ const content = this.readVulnerabilitiesJSON(vulnerabilitiesJSONPath);
77
+ let shouldCommit = false;
78
+ for (let i = 0; i < content.reports.length; ++i) {
79
+ if (content.reports[i].announcement !== link) {
80
+ content.reports[i].announcement = link;
81
+ shouldCommit = true;
82
+ }
83
+ };
84
+
85
+ if (shouldCommit) {
86
+ fs.writeFileSync(vulnerabilitiesJSONPath, JSON.stringify(content, null, 2));
87
+ const commitMessage = 'chore: add announcement link';
88
+ commitAndPushVulnerabilitiesJSON([vulnerabilitiesJSONPath],
89
+ commitMessage, { cli: this.cli, repository: this.repository });
90
+
91
+ this.cli.ok('Updated the announcement link in vulnerabilities.json');
92
+ }
93
+
94
+ return [vulnerabilitiesJSONPath];
69
95
  }
70
96
 
71
97
  async createPostRelease(nodejsOrgFolder) {
@@ -152,6 +152,7 @@ export default class UpdateSecurityRelease extends SecurityRelease {
152
152
  for (const cve of cves) {
153
153
  const report = reports.find(report => report.id === cve.reportId);
154
154
  report.cveIds = [cve.cve_identifier];
155
+ report.patchedVersions = cve.patchedVersions;
155
156
  }
156
157
  }
157
158
 
@@ -219,12 +220,14 @@ Summary: ${summary}\n`,
219
220
 
220
221
  if (!create) continue;
221
222
 
223
+ const { h1AffectedVersions, patchedVersions } =
224
+ await this.calculateVersions(affectedVersions, supportedVersions);
222
225
  const body = {
223
226
  data: {
224
227
  type: 'cve-request',
225
228
  attributes: {
226
229
  team_handle: 'nodejs-team',
227
- versions: await this.formatAffected(affectedVersions, supportedVersions),
230
+ versions: h1AffectedVersions,
228
231
  metrics: [
229
232
  {
230
233
  vectorString: cvss_vector_string
@@ -246,7 +249,7 @@ Summary: ${summary}\n`,
246
249
  continue;
247
250
  }
248
251
  const { cve_identifier } = data.attributes;
249
- cves.push({ cve_identifier, reportId: id });
252
+ cves.push({ cve_identifier, reportId: id, patchedVersions });
250
253
  }
251
254
  return cves;
252
255
  }
@@ -262,15 +265,23 @@ Summary: ${summary}\n`,
262
265
  }
263
266
  }
264
267
 
265
- async formatAffected(affectedVersions, supportedVersions) {
266
- const result = [];
268
+ async calculateVersions(affectedVersions, supportedVersions) {
269
+ const h1AffectedVersions = [];
270
+ const patchedVersions = [];
267
271
  for (const affectedVersion of affectedVersions) {
268
272
  const major = affectedVersion.split('.')[0];
269
273
  const latest = supportedVersions.find((v) => v.major === Number(major)).version;
270
274
  const version = await this.cli.prompt(
271
275
  `What is the affected version (<=) for release line ${affectedVersion}?`,
272
276
  { questionType: 'input', defaultAnswer: latest });
273
- result.push({
277
+
278
+ const nextPatchVersion = parseInt(version.split('.')[2]) + 1;
279
+ const patchedVersion = await this.cli.prompt(
280
+ `What is the patched version (>=) for release line ${affectedVersion}?`,
281
+ { questionType: 'input', defaultAnswer: nextPatchVersion });
282
+
283
+ patchedVersions.push(patchedVersion);
284
+ h1AffectedVersions.push({
274
285
  vendor: 'nodejs',
275
286
  product: 'node',
276
287
  func: '<=',
@@ -279,6 +290,6 @@ Summary: ${summary}\n`,
279
290
  affected: true
280
291
  });
281
292
  }
282
- return result;
293
+ return { h1AffectedVersions, patchedVersions };
283
294
  }
284
295
  }
@@ -29,6 +29,7 @@ export default class VotingSession extends Session {
29
29
  this.abstain = abstain;
30
30
  this.closeVote = argv['decrypt-key-part'];
31
31
  this.postComment = argv['post-comment'];
32
+ this.gpgSign = argv['gpg-sign'];
32
33
  }
33
34
 
34
35
  get argv() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@node-core/utils",
3
- "version": "5.14.1",
3
+ "version": "5.16.0",
4
4
  "description": "Utilities for Node.js core collaborators",
5
5
  "type": "module",
6
6
  "engines": {
@@ -68,6 +68,6 @@
68
68
  "eslint-plugin-promise": "^7.2.1",
69
69
  "globals": "^16.0.0",
70
70
  "neostandard": "^0.12.1",
71
- "sinon": "^20.0.0"
71
+ "sinon": "^21.0.0"
72
72
  }
73
73
  }