@node-core/utils 5.7.0 → 5.9.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/components/git/security.js +12 -10
- package/lib/prepare_release.js +29 -5
- package/lib/promote_release.js +23 -0
- package/lib/security_blog.js +94 -54
- package/lib/update_security_release.js +4 -1
- package/package.json +2 -2
@@ -29,8 +29,8 @@ const securityOptions = {
|
|
29
29
|
type: 'string'
|
30
30
|
},
|
31
31
|
'pre-release': {
|
32
|
-
describe: 'Create the pre-release announcement',
|
33
|
-
type: '
|
32
|
+
describe: 'Create the pre-release announcement to the given nodejs.org folder',
|
33
|
+
type: 'string'
|
34
34
|
},
|
35
35
|
'notify-pre-release': {
|
36
36
|
describe: 'Notify the community about the security release',
|
@@ -41,8 +41,8 @@ const securityOptions = {
|
|
41
41
|
type: 'boolean'
|
42
42
|
},
|
43
43
|
'post-release': {
|
44
|
-
describe: 'Create the post-release announcement',
|
45
|
-
type: '
|
44
|
+
describe: 'Create the post-release announcement to the given nodejs.org folder',
|
45
|
+
type: 'string'
|
46
46
|
},
|
47
47
|
cleanup: {
|
48
48
|
describe: 'cleanup the security release.',
|
@@ -73,7 +73,7 @@ export function builder(yargs) {
|
|
73
73
|
'git node security --remove-report=H1-ID',
|
74
74
|
'Removes the Hackerone report based on ID provided from vulnerabilities.json'
|
75
75
|
).example(
|
76
|
-
'git node security --pre-release',
|
76
|
+
'git node security --pre-release="../nodejs.org/"',
|
77
77
|
'Create the pre-release announcement on the Nodejs.org repo'
|
78
78
|
).example(
|
79
79
|
'git node security --notify-pre-release',
|
@@ -83,7 +83,7 @@ export function builder(yargs) {
|
|
83
83
|
'Request CVEs for a security release of Node.js based on' +
|
84
84
|
' the next-security-release/vulnerabilities.json'
|
85
85
|
).example(
|
86
|
-
'git node security --post-release',
|
86
|
+
'git node security --post-release="../nodejs.org/"',
|
87
87
|
'Create the post-release announcement on the Nodejs.org repo'
|
88
88
|
).example(
|
89
89
|
'git node security --cleanup',
|
@@ -149,11 +149,12 @@ async function updateReleaseDate(argv) {
|
|
149
149
|
return update.updateReleaseDate(releaseDate);
|
150
150
|
}
|
151
151
|
|
152
|
-
async function createPreRelease() {
|
152
|
+
async function createPreRelease(argv) {
|
153
|
+
const nodejsOrgFolder = argv['pre-release'];
|
153
154
|
const logStream = process.stdout.isTTY ? process.stdout : process.stderr;
|
154
155
|
const cli = new CLI(logStream);
|
155
156
|
const preRelease = new SecurityBlog(cli);
|
156
|
-
return preRelease.createPreRelease();
|
157
|
+
return preRelease.createPreRelease(nodejsOrgFolder);
|
157
158
|
}
|
158
159
|
|
159
160
|
async function requestCVEs() {
|
@@ -163,11 +164,12 @@ async function requestCVEs() {
|
|
163
164
|
return hackerOneCve.requestCVEs();
|
164
165
|
}
|
165
166
|
|
166
|
-
async function createPostRelease() {
|
167
|
+
async function createPostRelease(argv) {
|
168
|
+
const nodejsOrgFolder = argv['post-release'];
|
167
169
|
const logStream = process.stdout.isTTY ? process.stdout : process.stderr;
|
168
170
|
const cli = new CLI(logStream);
|
169
171
|
const blog = new SecurityBlog(cli);
|
170
|
-
return blog.createPostRelease();
|
172
|
+
return blog.createPostRelease(nodejsOrgFolder);
|
171
173
|
}
|
172
174
|
|
173
175
|
async function startSecurityRelease() {
|
package/lib/prepare_release.js
CHANGED
@@ -363,10 +363,26 @@ export default class ReleasePreparation extends Session {
|
|
363
363
|
try {
|
364
364
|
runSync('git', ['rev-parse', tagName]);
|
365
365
|
} catch {
|
366
|
-
this.cli.
|
367
|
-
|
366
|
+
this.cli.error(`Error parsing git ref ${tagName}`);
|
367
|
+
this.cli.startSpinner('Attempting fetching it as a tag\n');
|
368
|
+
try {
|
369
|
+
runSync('git', ['fetch', this.upstream, 'tag', '-n', tagName]);
|
370
|
+
} catch (e) {
|
371
|
+
this.cli.error(`${e.message}\nRun: git remote update`);
|
372
|
+
process.exit(1);
|
373
|
+
}
|
368
374
|
this.cli.stopSpinner(`Tag fetched: ${tagName}`);
|
369
375
|
}
|
376
|
+
try {
|
377
|
+
if (runSync('git', ['describe', '--abbrev=0', '--tags']).trim() !== tagName) {
|
378
|
+
this.cli.warn(`${tagName} is not the closest tag`);
|
379
|
+
}
|
380
|
+
} catch {
|
381
|
+
this.cli.startSpinner(`${tagName} is unreachable from the current HEAD`);
|
382
|
+
runSync('git', ['fetch', '--shallow-exclude', tagName, this.upstream, this.branch]);
|
383
|
+
runSync('git', ['fetch', '--deepen=1', this.upstream, this.branch]);
|
384
|
+
this.cli.stopSpinner('Local clone unshallowed');
|
385
|
+
}
|
370
386
|
return tagName;
|
371
387
|
}
|
372
388
|
|
@@ -755,7 +771,7 @@ export default class ReleasePreparation extends Session {
|
|
755
771
|
}
|
756
772
|
|
757
773
|
async prepareLocalBranch() {
|
758
|
-
const { cli } = this;
|
774
|
+
const { cli, isSecurityRelease } = this;
|
759
775
|
if (this.newVersion) {
|
760
776
|
// If the CLI asked for a specific version:
|
761
777
|
const newVersion = semver.parse(this.newVersion);
|
@@ -779,11 +795,19 @@ export default class ReleasePreparation extends Session {
|
|
779
795
|
// Otherwise, we need to figure out what's the next version number for the
|
780
796
|
// release line of the branch that's currently checked out.
|
781
797
|
const currentBranch = this.getCurrentBranch();
|
782
|
-
const match = /^v(\d+)\.x-staging$/.exec(currentBranch);
|
783
798
|
|
799
|
+
// In security releases vN.x branch is the base
|
800
|
+
let regex = /^v(\d+)\.x-staging$/;
|
801
|
+
let targetBranch = 'vN.x-staging';
|
802
|
+
if (isSecurityRelease) {
|
803
|
+
regex = /^v(\d+)\.x$/;
|
804
|
+
targetBranch = 'vN.x';
|
805
|
+
}
|
806
|
+
|
807
|
+
const match = regex.exec(currentBranch);
|
784
808
|
if (!match) {
|
785
809
|
cli.warn(`Cannot prepare a release from ${currentBranch
|
786
|
-
|
810
|
+
}. Switch to a ${targetBranch} branch before proceeding.`);
|
787
811
|
return;
|
788
812
|
}
|
789
813
|
this.stagingBranch = currentBranch;
|
package/lib/promote_release.js
CHANGED
@@ -131,6 +131,7 @@ export default class ReleasePromotion extends Session {
|
|
131
131
|
throw new Error('Aborted');
|
132
132
|
}
|
133
133
|
await this.secureTagRelease();
|
134
|
+
await this.verifyTagSignature();
|
134
135
|
|
135
136
|
// Set up for next release.
|
136
137
|
cli.startSpinner('Setting up for next release');
|
@@ -223,6 +224,28 @@ export default class ReleasePromotion extends Session {
|
|
223
224
|
this.isLTS ? '=false' : ''} --title=${JSON.stringify(this.releaseTitle)} --notes-file -`);
|
224
225
|
}
|
225
226
|
|
227
|
+
async verifyTagSignature() {
|
228
|
+
const { cli, version } = this;
|
229
|
+
const [needle, haystack] = await Promise.all([forceRunAsync(
|
230
|
+
'git', ['--no-pager',
|
231
|
+
'log', '-1',
|
232
|
+
`refs/tags/v${version}`,
|
233
|
+
'--format=* **%an** <<%ae>>\n `%GF`'
|
234
|
+
], { captureStdout: true }), fs.readFile('README.md')]);
|
235
|
+
if (haystack.includes(needle)) {
|
236
|
+
return;
|
237
|
+
}
|
238
|
+
cli.warn('Tag was signed with an undocumented identity/key pair!');
|
239
|
+
cli.info('Expected to find the following entry in the README:');
|
240
|
+
cli.info(needle);
|
241
|
+
cli.info('If you are using a subkey, it might be OK.');
|
242
|
+
cli.info(`Otherwise consider removing the tag (git tag -d v${version
|
243
|
+
}), check your local config, and start the process over.`);
|
244
|
+
if (!await cli.prompt('Do you want to proceed anyway?', { defaultAnswer: false })) {
|
245
|
+
throw new Error('Aborted');
|
246
|
+
}
|
247
|
+
}
|
248
|
+
|
226
249
|
async verifyPRAttributes() {
|
227
250
|
const { cli, prid, owner, repo, req } = this;
|
228
251
|
|
package/lib/security_blog.js
CHANGED
@@ -11,12 +11,10 @@ import {
|
|
11
11
|
import auth from './auth.js';
|
12
12
|
import Request from './request.js';
|
13
13
|
|
14
|
-
const kChanged = Symbol('changed');
|
15
|
-
|
16
14
|
export default class SecurityBlog extends SecurityRelease {
|
17
15
|
req;
|
18
16
|
|
19
|
-
async createPreRelease() {
|
17
|
+
async createPreRelease(nodejsOrgFolder) {
|
20
18
|
const { cli } = this;
|
21
19
|
|
22
20
|
// checkout on security release branch
|
@@ -45,14 +43,32 @@ export default class SecurityBlog extends SecurityRelease {
|
|
45
43
|
};
|
46
44
|
const month = releaseDate.toLocaleString('en-US', { month: 'long' }).toLowerCase();
|
47
45
|
const year = releaseDate.getFullYear();
|
48
|
-
const fileName = `${month}-${year}-security-releases
|
46
|
+
const fileName = `${month}-${year}-security-releases`;
|
47
|
+
const fileNameExt = fileName + '.md';
|
49
48
|
const preRelease = this.buildPreRelease(template, data);
|
50
|
-
|
49
|
+
|
50
|
+
const pathToBlogPosts = 'apps/site/pages/en/blog/vulnerability';
|
51
|
+
const pathToBannerJson = 'apps/site/site.json';
|
52
|
+
|
53
|
+
const file = path.resolve(process.cwd(), nodejsOrgFolder, pathToBlogPosts, fileNameExt);
|
54
|
+
const site = path.resolve(process.cwd(), nodejsOrgFolder, pathToBannerJson);
|
55
|
+
|
56
|
+
const endDate = new Date(data.annoucementDate);
|
57
|
+
endDate.setDate(endDate.getDate() + 7);
|
58
|
+
|
59
|
+
this.updateWebsiteBanner(site, {
|
60
|
+
startDate: data.annoucementDate,
|
61
|
+
endDate: endDate.toISOString(),
|
62
|
+
text: `New security releases to be made available ${data.releaseDate}`,
|
63
|
+
link: `https://nodejs.org/en/blog/vulnerability/${fileName}`,
|
64
|
+
type: 'warning'
|
65
|
+
});
|
66
|
+
|
51
67
|
fs.writeFileSync(file, preRelease);
|
52
|
-
cli.ok(`
|
68
|
+
cli.ok(`Announcement file created and banner has been updated. Folder: ${nodejsOrgFolder}`);
|
53
69
|
}
|
54
70
|
|
55
|
-
async createPostRelease() {
|
71
|
+
async createPostRelease(nodejsOrgFolder) {
|
56
72
|
const { cli } = this;
|
57
73
|
const credentials = await auth({
|
58
74
|
github: true,
|
@@ -65,7 +81,7 @@ export default class SecurityBlog extends SecurityRelease {
|
|
65
81
|
checkoutOnSecurityReleaseBranch(cli, this.repository);
|
66
82
|
|
67
83
|
// read vulnerabilities JSON file
|
68
|
-
const content = this.readVulnerabilitiesJSON(
|
84
|
+
const content = this.readVulnerabilitiesJSON();
|
69
85
|
if (!content.releaseDate) {
|
70
86
|
cli.error('Release date is not set in vulnerabilities.json,' +
|
71
87
|
' run `git node security --update-date=YYYY/MM/DD` to set the release date.');
|
@@ -76,47 +92,54 @@ export default class SecurityBlog extends SecurityRelease {
|
|
76
92
|
const releaseDate = new Date(content.releaseDate);
|
77
93
|
const template = this.getSecurityPostReleaseTemplate();
|
78
94
|
const data = {
|
79
|
-
|
80
|
-
annoucementDate: await this.getAnnouncementDate(cli),
|
95
|
+
annoucementDate: releaseDate.toISOString(),
|
81
96
|
releaseDate: this.formatReleaseDate(releaseDate),
|
82
97
|
affectedVersions: this.getAffectedVersions(content),
|
83
98
|
vulnerabilities: this.getVulnerabilities(content),
|
84
99
|
slug: this.getSlug(releaseDate),
|
85
|
-
author:
|
100
|
+
author: 'The Node.js Project',
|
86
101
|
dependencyUpdates: content.dependencies
|
87
102
|
};
|
88
|
-
const postReleaseContent = await this.buildPostRelease(template, data, content);
|
89
103
|
|
90
|
-
const
|
91
|
-
|
92
|
-
|
104
|
+
const pathToBlogPosts = path.resolve(nodejsOrgFolder, 'apps/site/pages/en/blog/release');
|
105
|
+
const pathToBannerJson = path.resolve(nodejsOrgFolder, 'apps/site/site.json');
|
106
|
+
|
107
|
+
const preReleasePath = path.resolve(pathToBlogPosts, data.slug + '.md');
|
108
|
+
let preReleaseContent = this.findExistingPreRelease(preReleasePath);
|
109
|
+
if (!preReleaseContent) {
|
110
|
+
cli.error(`Existing pre-release not found! Path: ${preReleasePath} `);
|
111
|
+
process.exit(1);
|
112
|
+
}
|
113
|
+
|
114
|
+
const postReleaseContent = await this.buildPostRelease(template, data, content);
|
93
115
|
// cut the part before summary
|
94
116
|
const preSummary = preReleaseContent.indexOf('# Summary');
|
95
117
|
if (preSummary !== -1) {
|
96
118
|
preReleaseContent = preReleaseContent.substring(preSummary);
|
97
119
|
}
|
98
|
-
|
99
120
|
const updatedContent = postReleaseContent + preReleaseContent;
|
100
121
|
|
101
|
-
|
102
|
-
|
122
|
+
const endDate = new Date(data.annoucementDate);
|
123
|
+
endDate.setDate(endDate.getDate() + 7);
|
124
|
+
const month = releaseDate.toLocaleString('en-US', { month: 'long' });
|
125
|
+
const capitalizedMonth = month[0].toUpperCase() + month.slice(1);
|
103
126
|
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
127
|
+
this.updateWebsiteBanner(pathToBannerJson, {
|
128
|
+
startDate: releaseDate,
|
129
|
+
endDate,
|
130
|
+
text: `${capitalizedMonth} Security Release is available`
|
131
|
+
});
|
108
132
|
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
questionType: 'input',
|
113
|
-
defaultAnswer: ''
|
114
|
-
});
|
133
|
+
fs.writeFileSync(preReleasePath, updatedContent);
|
134
|
+
cli.ok(`Announcement file and banner has been updated. Folder: ${nodejsOrgFolder}`);
|
135
|
+
}
|
115
136
|
|
116
|
-
|
117
|
-
|
137
|
+
findExistingPreRelease(filepath) {
|
138
|
+
if (!fs.existsSync(filepath)) {
|
139
|
+
return null;
|
118
140
|
}
|
119
|
-
|
141
|
+
|
142
|
+
return fs.readFileSync(filepath, 'utf-8');
|
120
143
|
}
|
121
144
|
|
122
145
|
promptAuthor(cli) {
|
@@ -127,6 +150,20 @@ export default class SecurityBlog extends SecurityRelease {
|
|
127
150
|
});
|
128
151
|
}
|
129
152
|
|
153
|
+
updateWebsiteBanner(siteJsonPath, content) {
|
154
|
+
const siteJson = JSON.parse(fs.readFileSync(siteJsonPath));
|
155
|
+
|
156
|
+
const currentValue = siteJson.websiteBanners.index;
|
157
|
+
siteJson.websiteBanners.index = {
|
158
|
+
startDate: content.startDate ?? currentValue.startDate,
|
159
|
+
endDate: content.endDate ?? currentValue.endDate,
|
160
|
+
text: content.text ?? currentValue.text,
|
161
|
+
link: content.link ?? currentValue.link,
|
162
|
+
type: content.type ?? currentValue.type
|
163
|
+
};
|
164
|
+
fs.writeFileSync(siteJsonPath, JSON.stringify(siteJson, null, 2) + '\n');
|
165
|
+
}
|
166
|
+
|
130
167
|
formatReleaseDate(releaseDate) {
|
131
168
|
const options = {
|
132
169
|
weekday: 'long',
|
@@ -262,34 +299,37 @@ export default class SecurityBlog extends SecurityRelease {
|
|
262
299
|
}
|
263
300
|
|
264
301
|
getImpact(content) {
|
265
|
-
const impact =
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
acc[affectedVersion] = [report];
|
271
|
-
}
|
302
|
+
const impact = new Map();
|
303
|
+
for (const report of content.reports) {
|
304
|
+
for (const version of report.affectedVersions) {
|
305
|
+
if (!impact.has(version)) impact.set(version, []);
|
306
|
+
impact.get(version).push(report);
|
272
307
|
}
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
308
|
+
}
|
309
|
+
|
310
|
+
const result = Array.from(impact.entries())
|
311
|
+
.sort(([a], [b]) => b.localeCompare(a)) // DESC
|
312
|
+
.map(([version, reports]) => {
|
313
|
+
const severityCount = new Map();
|
314
|
+
|
315
|
+
for (const report of reports) {
|
316
|
+
const rating = report.severity.rating?.toLowerCase();
|
317
|
+
if (!rating) {
|
318
|
+
this.cli.error(`severity.rating not found for report ${report.id}.`);
|
283
319
|
process.exit(1);
|
284
320
|
}
|
285
|
-
|
286
|
-
|
287
|
-
}).join(', ');
|
321
|
+
severityCount.set(rating, (severityCount.get(rating) || 0) + 1);
|
322
|
+
}
|
288
323
|
|
289
|
-
|
290
|
-
|
324
|
+
const groupedByRating = Array.from(severityCount.entries())
|
325
|
+
.map(([rating, count]) => `${count} ${rating} severity issues`)
|
326
|
+
.join(', ');
|
327
|
+
|
328
|
+
return `The ${version} release line of Node.js is vulnerable to ${groupedByRating}.`;
|
329
|
+
})
|
330
|
+
.join('\n');
|
291
331
|
|
292
|
-
return
|
332
|
+
return result;
|
293
333
|
}
|
294
334
|
|
295
335
|
getVulnerabilities(content) {
|
@@ -226,8 +226,11 @@ Summary: ${summary}\n`,
|
|
226
226
|
vectorString: cvss_vector_string
|
227
227
|
}
|
228
228
|
],
|
229
|
+
auto_submit_on_publicly_disclosing_report: true,
|
230
|
+
references: ['https://nodejs.org/en/blog/vulnerability'],
|
231
|
+
report_id: report.id,
|
229
232
|
weakness_id: Number(weakness_id),
|
230
|
-
description:
|
233
|
+
description: report.summary,
|
231
234
|
vulnerability_discovered_at: new Date().toISOString()
|
232
235
|
}
|
233
236
|
}
|
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "@node-core/utils",
|
3
|
-
"version": "5.
|
3
|
+
"version": "5.9.0",
|
4
4
|
"description": "Utilities for Node.js core collaborators",
|
5
5
|
"type": "module",
|
6
6
|
"engines": {
|
@@ -40,7 +40,7 @@
|
|
40
40
|
"@pkgjs/nv": "^0.2.2",
|
41
41
|
"branch-diff": "^3.1.1",
|
42
42
|
"chalk": "^5.3.0",
|
43
|
-
"changelog-maker": "^4.
|
43
|
+
"changelog-maker": "^4.3.1",
|
44
44
|
"cheerio": "^1.0.0",
|
45
45
|
"clipboardy": "^4.0.0",
|
46
46
|
"core-validate-commit": "^4.1.0",
|