@node-core/utils 5.14.1 → 5.15.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
@@ -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 } 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) {
@@ -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.15.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
  }