@node-core/utils 4.4.0 → 5.0.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/bin/ncu-ci.js +6 -1
- package/components/git/security.js +22 -3
- package/lib/ci/run_ci.js +12 -2
- package/lib/pr_checker.js +1 -0
- package/lib/prepare_security.js +116 -119
- package/lib/request.js +55 -0
- package/lib/security-announcement.js +9 -16
- package/lib/security-release/security-release.js +85 -1
- package/lib/update_security_release.js +170 -34
- package/package.json +1 -1
package/bin/ncu-ci.js
CHANGED
@@ -113,6 +113,11 @@ const args = yargs(hideBin(process.argv))
|
|
113
113
|
describe: 'ID of the PR',
|
114
114
|
type: 'number'
|
115
115
|
})
|
116
|
+
.positional('certify-safe', {
|
117
|
+
describe: 'If not provided, the command will reject PRs that have ' +
|
118
|
+
'been pushed since the last review',
|
119
|
+
type: 'boolean'
|
120
|
+
})
|
116
121
|
.option('owner', {
|
117
122
|
default: '',
|
118
123
|
describe: 'GitHub repository owner'
|
@@ -291,7 +296,7 @@ class RunPRJobCommand {
|
|
291
296
|
this.cli.setExitCode(1);
|
292
297
|
return;
|
293
298
|
}
|
294
|
-
const jobRunner = new RunPRJob(cli, request, owner, repo, prid);
|
299
|
+
const jobRunner = new RunPRJob(cli, request, owner, repo, prid, this.argv.certifySafe);
|
295
300
|
if (!(await jobRunner.start())) {
|
296
301
|
this.cli.setExitCode(1);
|
297
302
|
process.exitCode = 1;
|
@@ -1,5 +1,5 @@
|
|
1
1
|
import CLI from '../../lib/cli.js';
|
2
|
-
import
|
2
|
+
import PrepareSecurityRelease from '../../lib/prepare_security.js';
|
3
3
|
import UpdateSecurityRelease from '../../lib/update_security_release.js';
|
4
4
|
import SecurityBlog from '../../lib/security_blog.js';
|
5
5
|
import SecurityAnnouncement from '../../lib/security-announcement.js';
|
@@ -31,6 +31,10 @@ const securityOptions = {
|
|
31
31
|
'notify-pre-release': {
|
32
32
|
describe: 'Notify the community about the security release',
|
33
33
|
type: 'boolean'
|
34
|
+
},
|
35
|
+
'request-cve': {
|
36
|
+
describe: 'Request CVEs for a security release',
|
37
|
+
type: 'boolean'
|
34
38
|
}
|
35
39
|
};
|
36
40
|
|
@@ -60,6 +64,11 @@ export function builder(yargs) {
|
|
60
64
|
).example(
|
61
65
|
'git node security --notify-pre-release' +
|
62
66
|
'Notifies the community about the security release'
|
67
|
+
)
|
68
|
+
.example(
|
69
|
+
'git node security --request-cve',
|
70
|
+
'Request CVEs for a security release of Node.js based on' +
|
71
|
+
' the next-security-release/vulnerabilities.json'
|
63
72
|
);
|
64
73
|
}
|
65
74
|
|
@@ -82,6 +91,9 @@ export function handler(argv) {
|
|
82
91
|
if (argv['notify-pre-release']) {
|
83
92
|
return notifyPreRelease(argv);
|
84
93
|
}
|
94
|
+
if (argv['request-cve']) {
|
95
|
+
return requestCVEs(argv);
|
96
|
+
}
|
85
97
|
yargsInstance.showHelp();
|
86
98
|
}
|
87
99
|
|
@@ -116,10 +128,17 @@ async function createPreRelease() {
|
|
116
128
|
return preRelease.createPreRelease();
|
117
129
|
}
|
118
130
|
|
119
|
-
async function
|
131
|
+
async function requestCVEs() {
|
132
|
+
const logStream = process.stdout.isTTY ? process.stdout : process.stderr;
|
133
|
+
const cli = new CLI(logStream);
|
134
|
+
const hackerOneCve = new UpdateSecurityRelease(cli);
|
135
|
+
return hackerOneCve.requestCVEs();
|
136
|
+
}
|
137
|
+
|
138
|
+
async function startSecurityRelease(argv) {
|
120
139
|
const logStream = process.stdout.isTTY ? process.stdout : process.stderr;
|
121
140
|
const cli = new CLI(logStream);
|
122
|
-
const release = new
|
141
|
+
const release = new PrepareSecurityRelease(cli);
|
123
142
|
return release.start();
|
124
143
|
}
|
125
144
|
|
package/lib/ci/run_ci.js
CHANGED
@@ -7,6 +7,7 @@ import {
|
|
7
7
|
} from './ci_type_parser.js';
|
8
8
|
import PRData from '../pr_data.js';
|
9
9
|
import { debuglog } from '../verbosity.js';
|
10
|
+
import PRChecker from '../pr_checker.js';
|
10
11
|
|
11
12
|
export const CI_CRUMB_URL = `https://${CI_DOMAIN}/crumbIssuer/api/json`;
|
12
13
|
const CI_PR_NAME = CI_TYPES.get(CI_TYPES_KEYS.PR).jobName;
|
@@ -16,13 +17,16 @@ const CI_V8_NAME = CI_TYPES.get(CI_TYPES_KEYS.V8).jobName;
|
|
16
17
|
export const CI_V8_URL = `https://${CI_DOMAIN}/job/${CI_V8_NAME}/build`;
|
17
18
|
|
18
19
|
export class RunPRJob {
|
19
|
-
constructor(cli, request, owner, repo, prid) {
|
20
|
+
constructor(cli, request, owner, repo, prid, certifySafe) {
|
20
21
|
this.cli = cli;
|
21
22
|
this.request = request;
|
22
23
|
this.owner = owner;
|
23
24
|
this.repo = repo;
|
24
25
|
this.prid = prid;
|
25
26
|
this.prData = new PRData({ prid, owner, repo }, cli, request);
|
27
|
+
this.certifySafe =
|
28
|
+
certifySafe ||
|
29
|
+
new PRChecker(cli, this.prData, request, {}).checkCommitsAfterReview();
|
26
30
|
}
|
27
31
|
|
28
32
|
async getCrumb() {
|
@@ -62,7 +66,13 @@ export class RunPRJob {
|
|
62
66
|
}
|
63
67
|
|
64
68
|
async start() {
|
65
|
-
const { cli } = this;
|
69
|
+
const { cli, certifySafe } = this;
|
70
|
+
|
71
|
+
if (!certifySafe) {
|
72
|
+
cli.error('Refusing to run CI on potentially unsafe PR');
|
73
|
+
return false;
|
74
|
+
}
|
75
|
+
|
66
76
|
cli.startSpinner('Validating Jenkins credentials');
|
67
77
|
const crumb = await this.getCrumb();
|
68
78
|
|
package/lib/pr_checker.js
CHANGED
package/lib/prepare_security.js
CHANGED
@@ -1,4 +1,3 @@
|
|
1
|
-
import nv from '@pkgjs/nv';
|
2
1
|
import fs from 'node:fs';
|
3
2
|
import path from 'node:path';
|
4
3
|
import auth from './auth.js';
|
@@ -10,92 +9,89 @@ import {
|
|
10
9
|
PLACEHOLDERS,
|
11
10
|
checkoutOnSecurityReleaseBranch,
|
12
11
|
commitAndPushVulnerabilitiesJSON,
|
13
|
-
|
14
|
-
|
12
|
+
validateDate,
|
13
|
+
promptDependencies,
|
14
|
+
getSupportedVersions,
|
15
|
+
pickReport
|
15
16
|
} from './security-release/security-release.js';
|
17
|
+
import _ from 'lodash';
|
16
18
|
|
17
|
-
export default class
|
19
|
+
export default class PrepareSecurityRelease {
|
18
20
|
repository = NEXT_SECURITY_RELEASE_REPOSITORY;
|
21
|
+
title = 'Next Security Release';
|
19
22
|
constructor(cli) {
|
20
23
|
this.cli = cli;
|
21
24
|
}
|
22
25
|
|
23
26
|
async start() {
|
24
|
-
const { cli } = this;
|
25
27
|
const credentials = await auth({
|
26
28
|
github: true,
|
27
29
|
h1: true
|
28
30
|
});
|
29
31
|
|
30
|
-
|
31
|
-
const
|
32
|
-
|
33
|
-
|
34
|
-
|
32
|
+
this.req = new Request(credentials);
|
33
|
+
const releaseDate = await this.promptReleaseDate();
|
34
|
+
if (releaseDate !== 'TBD') {
|
35
|
+
validateDate(releaseDate);
|
36
|
+
}
|
37
|
+
const createVulnerabilitiesJSON = await this.promptVulnerabilitiesJSON();
|
35
38
|
|
36
39
|
let securityReleasePRUrl;
|
37
40
|
if (createVulnerabilitiesJSON) {
|
38
|
-
securityReleasePRUrl = await this.
|
41
|
+
securityReleasePRUrl = await this.startVulnerabilitiesJSONCreation(releaseDate);
|
39
42
|
}
|
40
43
|
|
41
|
-
const createIssue = await
|
44
|
+
const createIssue = await this.promptCreateRelaseIssue();
|
42
45
|
|
43
46
|
if (createIssue) {
|
44
|
-
const content = await
|
45
|
-
await
|
47
|
+
const content = await this.buildIssue(releaseDate, securityReleasePRUrl);
|
48
|
+
await createIssue(
|
49
|
+
this.title, content, this.repository, { cli: this.cli, repository: this.repository });
|
46
50
|
};
|
47
51
|
|
48
|
-
cli.ok('Done!');
|
52
|
+
this.cli.ok('Done!');
|
49
53
|
}
|
50
54
|
|
51
|
-
async
|
55
|
+
async startVulnerabilitiesJSONCreation(releaseDate) {
|
52
56
|
// checkout on the next-security-release branch
|
53
|
-
checkoutOnSecurityReleaseBranch(cli, this.repository);
|
57
|
+
checkoutOnSecurityReleaseBranch(this.cli, this.repository);
|
54
58
|
|
55
59
|
// choose the reports to include in the security release
|
56
|
-
const reports = await
|
60
|
+
const reports = await this.chooseReports();
|
61
|
+
const depUpdates = await this.getDependencyUpdates();
|
62
|
+
const deps = _.groupBy(depUpdates, 'name');
|
57
63
|
|
58
64
|
// create the vulnerabilities.json file in the security-release repo
|
59
|
-
const filePath = await
|
65
|
+
const filePath = await this.createVulnerabilitiesJSON(reports, deps, releaseDate);
|
60
66
|
|
61
67
|
// review the vulnerabilities.json file
|
62
|
-
const review = await
|
68
|
+
const review = await this.promptReviewVulnerabilitiesJSON();
|
63
69
|
|
64
70
|
if (!review) {
|
65
|
-
cli.info(`To push the vulnerabilities.json file run:
|
71
|
+
this.cli.info(`To push the vulnerabilities.json file run:
|
66
72
|
- git add ${filePath}
|
67
73
|
- git commit -m "chore: create vulnerabilities.json for next security release"
|
68
74
|
- git push -u origin ${NEXT_SECURITY_RELEASE_BRANCH}
|
69
|
-
- open a PR on ${
|
75
|
+
- open a PR on ${this.repository.owner}/${this.repository.repo}`);
|
70
76
|
return;
|
71
77
|
};
|
72
78
|
|
73
79
|
// commit and push the vulnerabilities.json file
|
74
80
|
const commitMessage = 'chore: create vulnerabilities.json for next security release';
|
75
|
-
commitAndPushVulnerabilitiesJSON(filePath,
|
81
|
+
commitAndPushVulnerabilitiesJSON(filePath,
|
82
|
+
commitMessage,
|
83
|
+
{ cli: this.cli, repository: this.repository });
|
76
84
|
|
77
|
-
const createPr = await
|
85
|
+
const createPr = await this.promptCreatePR();
|
78
86
|
|
79
87
|
if (!createPr) return;
|
80
88
|
|
81
89
|
// create pr on the security-release repo
|
82
|
-
return
|
83
|
-
}
|
84
|
-
}
|
85
|
-
|
86
|
-
class PrepareSecurityRelease {
|
87
|
-
repository = NEXT_SECURITY_RELEASE_REPOSITORY;
|
88
|
-
title = 'Next Security Release';
|
89
|
-
|
90
|
-
constructor(req, repository) {
|
91
|
-
this.req = req;
|
92
|
-
if (repository) {
|
93
|
-
this.repository = repository;
|
94
|
-
}
|
90
|
+
return this.createPullRequest();
|
95
91
|
}
|
96
92
|
|
97
|
-
promptCreatePR(
|
98
|
-
return cli.prompt(
|
93
|
+
promptCreatePR() {
|
94
|
+
return this.cli.prompt(
|
99
95
|
'Create the Next Security Release PR?',
|
100
96
|
{ defaultAnswer: true });
|
101
97
|
}
|
@@ -117,31 +113,32 @@ class PrepareSecurityRelease {
|
|
117
113
|
}
|
118
114
|
}
|
119
115
|
|
120
|
-
async promptReleaseDate(
|
116
|
+
async promptReleaseDate() {
|
121
117
|
const nextWeekDate = new Date();
|
122
118
|
nextWeekDate.setDate(nextWeekDate.getDate() + 7);
|
123
119
|
// Format the date as YYYY/MM/DD
|
124
120
|
const formattedDate = nextWeekDate.toISOString().slice(0, 10).replace(/-/g, '/');
|
125
|
-
return cli.prompt(
|
126
|
-
|
127
|
-
|
128
|
-
|
121
|
+
return this.cli.prompt(
|
122
|
+
'Enter target release date in YYYY/MM/DD format (TBD if not defined yet):', {
|
123
|
+
questionType: 'input',
|
124
|
+
defaultAnswer: formattedDate
|
125
|
+
});
|
129
126
|
}
|
130
127
|
|
131
|
-
async promptVulnerabilitiesJSON(
|
132
|
-
return cli.prompt(
|
128
|
+
async promptVulnerabilitiesJSON() {
|
129
|
+
return this.cli.prompt(
|
133
130
|
'Create the vulnerabilities.json?',
|
134
131
|
{ defaultAnswer: true });
|
135
132
|
}
|
136
133
|
|
137
|
-
async promptCreateRelaseIssue(
|
138
|
-
return cli.prompt(
|
134
|
+
async promptCreateRelaseIssue() {
|
135
|
+
return this.cli.prompt(
|
139
136
|
'Create the Next Security Release issue?',
|
140
137
|
{ defaultAnswer: true });
|
141
138
|
}
|
142
139
|
|
143
|
-
async promptReviewVulnerabilitiesJSON(
|
144
|
-
return cli.prompt(
|
140
|
+
async promptReviewVulnerabilitiesJSON() {
|
141
|
+
return this.cli.prompt(
|
145
142
|
'Please review vulnerabilities.json and press enter to proceed.',
|
146
143
|
{ defaultAnswer: true });
|
147
144
|
}
|
@@ -153,78 +150,25 @@ class PrepareSecurityRelease {
|
|
153
150
|
return content;
|
154
151
|
}
|
155
152
|
|
156
|
-
async
|
157
|
-
|
158
|
-
if (data.html_url) {
|
159
|
-
cli.ok(`Created: ${data.html_url}`);
|
160
|
-
} else {
|
161
|
-
cli.error(data);
|
162
|
-
process.exit(1);
|
163
|
-
}
|
164
|
-
}
|
165
|
-
|
166
|
-
async chooseReports(cli) {
|
167
|
-
cli.info('Getting triaged H1 reports...');
|
153
|
+
async chooseReports() {
|
154
|
+
this.cli.info('Getting triaged H1 reports...');
|
168
155
|
const reports = await this.req.getTriagedReports();
|
169
|
-
const supportedVersions = (await nv('supported'))
|
170
|
-
.map((v) => `${v.versionName}.x`)
|
171
|
-
.join(',');
|
172
156
|
const selectedReports = [];
|
173
157
|
|
174
158
|
for (const report of reports.data) {
|
175
|
-
const {
|
176
|
-
|
177
|
-
|
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
|
-
|
194
|
-
cli.separator();
|
195
|
-
cli.info(`Report: ${link} - ${title} (${reportSeverity?.rating})`);
|
196
|
-
const include = await cli.prompt(
|
197
|
-
'Would you like to include this report to the next security release?',
|
198
|
-
{ defaultAnswer: true });
|
199
|
-
if (!include) {
|
200
|
-
continue;
|
201
|
-
}
|
202
|
-
|
203
|
-
const versions = await cli.prompt('Which active release lines this report affects?', {
|
204
|
-
questionType: 'input',
|
205
|
-
defaultAnswer: supportedVersions
|
206
|
-
});
|
207
|
-
const summaryContent = await getSummary(id, this.req);
|
208
|
-
|
209
|
-
selectedReports.push({
|
210
|
-
id,
|
211
|
-
title,
|
212
|
-
cve_ids,
|
213
|
-
severity: reportSeverity,
|
214
|
-
summary: summaryContent ?? '',
|
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
|
219
|
-
});
|
159
|
+
const rep = await pickReport(report, { cli: this.cli, req: this.req });
|
160
|
+
if (!rep) continue;
|
161
|
+
selectedReports.push(rep);
|
220
162
|
}
|
221
163
|
return selectedReports;
|
222
164
|
}
|
223
165
|
|
224
|
-
async createVulnerabilitiesJSON(reports,
|
225
|
-
cli.separator('Creating vulnerabilities.json...');
|
166
|
+
async createVulnerabilitiesJSON(reports, dependencies, releaseDate) {
|
167
|
+
this.cli.separator('Creating vulnerabilities.json...');
|
226
168
|
const file = JSON.stringify({
|
227
|
-
|
169
|
+
releaseDate,
|
170
|
+
reports,
|
171
|
+
dependencies
|
228
172
|
}, null, 2);
|
229
173
|
|
230
174
|
const folderPath = path.join(process.cwd(), NEXT_SECURITY_RELEASE_FOLDER);
|
@@ -236,14 +180,14 @@ class PrepareSecurityRelease {
|
|
236
180
|
|
237
181
|
const fullPath = path.join(folderPath, 'vulnerabilities.json');
|
238
182
|
fs.writeFileSync(fullPath, file);
|
239
|
-
cli.ok(`Created ${fullPath} `);
|
183
|
+
this.cli.ok(`Created ${fullPath} `);
|
240
184
|
|
241
185
|
return fullPath;
|
242
186
|
}
|
243
187
|
|
244
|
-
async createPullRequest(
|
188
|
+
async createPullRequest() {
|
245
189
|
const { owner, repo } = this.repository;
|
246
|
-
const response = await req.createPullRequest(
|
190
|
+
const response = await this.req.createPullRequest(
|
247
191
|
this.title,
|
248
192
|
'List of vulnerabilities to be included in the next security release',
|
249
193
|
{
|
@@ -256,16 +200,69 @@ class PrepareSecurityRelease {
|
|
256
200
|
);
|
257
201
|
const url = response?.html_url;
|
258
202
|
if (url) {
|
259
|
-
cli.ok(`Created: ${url}`);
|
203
|
+
this.cli.ok(`Created: ${url}`);
|
260
204
|
return url;
|
261
205
|
}
|
262
206
|
if (response?.errors) {
|
263
207
|
for (const error of response.errors) {
|
264
|
-
cli.error(error.message);
|
208
|
+
this.cli.error(error.message);
|
265
209
|
}
|
266
210
|
} else {
|
267
|
-
cli.error(response);
|
211
|
+
this.cli.error(response);
|
268
212
|
}
|
269
213
|
process.exit(1);
|
270
214
|
}
|
215
|
+
|
216
|
+
async getDependencyUpdates() {
|
217
|
+
const deps = [];
|
218
|
+
this.cli.log('\n');
|
219
|
+
this.cli.separator('Dependency Updates');
|
220
|
+
const updates = await this.cli.prompt('Are there dependency updates in this security release?',
|
221
|
+
{
|
222
|
+
defaultAnswer: true,
|
223
|
+
questionType: 'confirm'
|
224
|
+
});
|
225
|
+
|
226
|
+
if (!updates) return deps;
|
227
|
+
|
228
|
+
const supportedVersions = await getSupportedVersions();
|
229
|
+
|
230
|
+
let asking = true;
|
231
|
+
while (asking) {
|
232
|
+
const dep = await promptDependencies(this.cli);
|
233
|
+
if (!dep) {
|
234
|
+
asking = false;
|
235
|
+
break;
|
236
|
+
}
|
237
|
+
|
238
|
+
const name = await this.cli.prompt(
|
239
|
+
'What is the name of the dependency that has been updated?', {
|
240
|
+
defaultAnswer: '',
|
241
|
+
questionType: 'input'
|
242
|
+
});
|
243
|
+
|
244
|
+
const versions = await this.cli.prompt(
|
245
|
+
'Which release line does this dependency update affect?', {
|
246
|
+
defaultAnswer: supportedVersions,
|
247
|
+
questionType: 'input'
|
248
|
+
});
|
249
|
+
|
250
|
+
try {
|
251
|
+
const prUrl = dep.replace('https://github.com/', 'https://api.github.com/repos/').replace('pull', 'pulls');
|
252
|
+
const res = await this.req.getPullRequest(prUrl);
|
253
|
+
const { html_url, title } = res;
|
254
|
+
deps.push({
|
255
|
+
name,
|
256
|
+
url: html_url,
|
257
|
+
title,
|
258
|
+
affectedVersions: versions.split(',').map((v) => v.replace('v', '').trim())
|
259
|
+
});
|
260
|
+
this.cli.separator();
|
261
|
+
} catch (error) {
|
262
|
+
this.cli.error('Invalid PR url. Please provide a valid PR url.');
|
263
|
+
this.cli.error(error);
|
264
|
+
}
|
265
|
+
}
|
266
|
+
return deps;
|
267
|
+
}
|
271
268
|
}
|
package/lib/request.js
CHANGED
@@ -77,6 +77,18 @@ export default class Request {
|
|
77
77
|
return this.json(url, options);
|
78
78
|
}
|
79
79
|
|
80
|
+
async getPullRequest(url) {
|
81
|
+
const options = {
|
82
|
+
method: 'GET',
|
83
|
+
headers: {
|
84
|
+
Authorization: `Basic ${this.credentials.github}`,
|
85
|
+
'User-Agent': 'node-core-utils',
|
86
|
+
Accept: 'application/vnd.github+json'
|
87
|
+
}
|
88
|
+
};
|
89
|
+
return this.json(url, options);
|
90
|
+
}
|
91
|
+
|
80
92
|
async createPullRequest(title, body, { owner, repo, head, base }) {
|
81
93
|
const url = `https://api.github.com/repos/${owner}/${repo}/pulls`;
|
82
94
|
const options = {
|
@@ -132,6 +144,49 @@ export default class Request {
|
|
132
144
|
return this.json(url, options);
|
133
145
|
}
|
134
146
|
|
147
|
+
async getPrograms() {
|
148
|
+
const url = 'https://api.hackerone.com/v1/me/programs';
|
149
|
+
const options = {
|
150
|
+
method: 'GET',
|
151
|
+
headers: {
|
152
|
+
Authorization: `Basic ${this.credentials.h1}`,
|
153
|
+
'User-Agent': 'node-core-utils',
|
154
|
+
Accept: 'application/json'
|
155
|
+
}
|
156
|
+
};
|
157
|
+
return this.json(url, options);
|
158
|
+
}
|
159
|
+
|
160
|
+
async requestCVE(programId, opts) {
|
161
|
+
const url = `https://api.hackerone.com/v1/programs/${programId}/cve_requests`;
|
162
|
+
const options = {
|
163
|
+
method: 'POST',
|
164
|
+
headers: {
|
165
|
+
Authorization: `Basic ${this.credentials.h1}`,
|
166
|
+
'User-Agent': 'node-core-utils',
|
167
|
+
'Content-Type': 'application/json',
|
168
|
+
Accept: 'application/json'
|
169
|
+
},
|
170
|
+
body: JSON.stringify(opts)
|
171
|
+
};
|
172
|
+
return this.json(url, options);
|
173
|
+
}
|
174
|
+
|
175
|
+
async updateReportCVE(reportId, opts) {
|
176
|
+
const url = `https://api.hackerone.com/v1/reports/${reportId}/cves`;
|
177
|
+
const options = {
|
178
|
+
method: 'PUT',
|
179
|
+
headers: {
|
180
|
+
Authorization: `Basic ${this.credentials.h1}`,
|
181
|
+
'User-Agent': 'node-core-utils',
|
182
|
+
Accept: 'application/json',
|
183
|
+
'Content-Type': 'application/json'
|
184
|
+
},
|
185
|
+
body: JSON.stringify(opts)
|
186
|
+
};
|
187
|
+
return this.json(url, options);
|
188
|
+
}
|
189
|
+
|
135
190
|
async getReport(reportId) {
|
136
191
|
const url = `https://api.hackerone.com/v1/reports/${reportId}`;
|
137
192
|
const options = {
|
@@ -3,7 +3,8 @@ import {
|
|
3
3
|
checkoutOnSecurityReleaseBranch,
|
4
4
|
getVulnerabilitiesJSON,
|
5
5
|
validateDate,
|
6
|
-
formatDateToYYYYMMDD
|
6
|
+
formatDateToYYYYMMDD,
|
7
|
+
createIssue
|
7
8
|
} from './security-release/security-release.js';
|
8
9
|
import auth from './auth.js';
|
9
10
|
import Request from './request.js';
|
@@ -39,8 +40,10 @@ export default class SecurityAnnouncement {
|
|
39
40
|
validateDate(content.releaseDate);
|
40
41
|
const releaseDate = new Date(content.releaseDate);
|
41
42
|
|
42
|
-
await Promise.all([
|
43
|
-
this.
|
43
|
+
await Promise.all([
|
44
|
+
this.createDockerNodeIssue(releaseDate),
|
45
|
+
this.createBuildWGIssue(releaseDate)
|
46
|
+
]);
|
44
47
|
}
|
45
48
|
|
46
49
|
async createBuildWGIssue(releaseDate) {
|
@@ -56,8 +59,8 @@ export default class SecurityAnnouncement {
|
|
56
59
|
createPreleaseAnnouncementIssue(releaseDate, team) {
|
57
60
|
const title = `[NEXT-SECURITY-RELEASE] Heads up on upcoming Node.js\
|
58
61
|
security release ${formatDateToYYYYMMDD(releaseDate)}`;
|
59
|
-
const content =
|
60
|
-
|
62
|
+
const content = `As per security release workflow,\
|
63
|
+
creating issue to give the ${team} team a heads up.`;
|
61
64
|
return { title, content };
|
62
65
|
}
|
63
66
|
|
@@ -68,16 +71,6 @@ export default class SecurityAnnouncement {
|
|
68
71
|
};
|
69
72
|
|
70
73
|
const { title, content } = this.createPreleaseAnnouncementIssue(releaseDate, 'docker');
|
71
|
-
await
|
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
|
-
}
|
74
|
+
await createIssue(title, content, repository, { cli: this.cli, repository: this.repository });
|
82
75
|
}
|
83
76
|
}
|
@@ -61,8 +61,22 @@ export function commitAndPushVulnerabilitiesJSON(filePath, commitMessage, { cli,
|
|
61
61
|
runSync('git', ['add', filePath]);
|
62
62
|
}
|
63
63
|
|
64
|
+
const staged = runSync('git', ['diff', '--name-only', '--cached']).trim();
|
65
|
+
if (!staged) {
|
66
|
+
cli.ok('No changes to commit');
|
67
|
+
return;
|
68
|
+
}
|
69
|
+
|
64
70
|
runSync('git', ['commit', '-m', commitMessage]);
|
65
|
-
|
71
|
+
|
72
|
+
try {
|
73
|
+
runSync('git', ['push', '-u', 'origin', NEXT_SECURITY_RELEASE_BRANCH]);
|
74
|
+
} catch (error) {
|
75
|
+
cli.warn('Rebasing...');
|
76
|
+
// try to pull rebase and push again
|
77
|
+
runSync('git', ['pull', 'origin', NEXT_SECURITY_RELEASE_BRANCH, '--rebase']);
|
78
|
+
runSync('git', ['push', '-u', 'origin', NEXT_SECURITY_RELEASE_BRANCH]);
|
79
|
+
}
|
66
80
|
cli.ok(`Pushed commit: ${commitMessage} to ${NEXT_SECURITY_RELEASE_BRANCH}`);
|
67
81
|
}
|
68
82
|
|
@@ -107,3 +121,73 @@ export function formatDateToYYYYMMDD(date) {
|
|
107
121
|
// Concatenate year, month, and day with slashes
|
108
122
|
return `${year}/${month}/${day}`;
|
109
123
|
}
|
124
|
+
|
125
|
+
export function promptDependencies(cli) {
|
126
|
+
return cli.prompt('Enter the link to the dependency update PR (leave empty to exit): ', {
|
127
|
+
defaultAnswer: '',
|
128
|
+
questionType: 'input'
|
129
|
+
});
|
130
|
+
}
|
131
|
+
|
132
|
+
export async function createIssue(title, content, repository, { cli, req }) {
|
133
|
+
const data = await req.createIssue(title, content, repository);
|
134
|
+
if (data.html_url) {
|
135
|
+
cli.ok(`Created: ${data.html_url}`);
|
136
|
+
} else {
|
137
|
+
cli.error(data);
|
138
|
+
process.exit(1);
|
139
|
+
}
|
140
|
+
}
|
141
|
+
|
142
|
+
export async function pickReport(report, { cli, req }) {
|
143
|
+
const {
|
144
|
+
id, attributes: { title, cve_ids },
|
145
|
+
relationships: { severity, weakness, reporter }
|
146
|
+
} = report;
|
147
|
+
const link = `https://hackerone.com/reports/${id}`;
|
148
|
+
const reportSeverity = {
|
149
|
+
rating: severity?.data?.attributes?.rating || '',
|
150
|
+
cvss_vector_string: severity?.data?.attributes?.cvss_vector_string || '',
|
151
|
+
weakness_id: weakness?.data?.id || ''
|
152
|
+
};
|
153
|
+
|
154
|
+
cli.separator();
|
155
|
+
cli.info(`Report: ${link} - ${title} (${reportSeverity?.rating})`);
|
156
|
+
const include = await cli.prompt(
|
157
|
+
'Would you like to include this report to the next security release?',
|
158
|
+
{ defaultAnswer: true });
|
159
|
+
if (!include) {
|
160
|
+
return;
|
161
|
+
}
|
162
|
+
|
163
|
+
const versions = await cli.prompt('Which active release lines this report affects?', {
|
164
|
+
questionType: 'input',
|
165
|
+
defaultAnswer: await getSupportedVersions()
|
166
|
+
});
|
167
|
+
|
168
|
+
let patchAuthors = await cli.prompt(
|
169
|
+
'Add github username of the authors of the patch (split by comma if multiple)', {
|
170
|
+
questionType: 'input',
|
171
|
+
defaultAnswer: ''
|
172
|
+
});
|
173
|
+
|
174
|
+
if (!patchAuthors) {
|
175
|
+
patchAuthors = [];
|
176
|
+
} else {
|
177
|
+
patchAuthors = patchAuthors.split(',').map((p) => p.trim());
|
178
|
+
}
|
179
|
+
|
180
|
+
const summaryContent = await getSummary(id, req);
|
181
|
+
|
182
|
+
return {
|
183
|
+
id,
|
184
|
+
title,
|
185
|
+
cveIds: cve_ids,
|
186
|
+
severity: reportSeverity,
|
187
|
+
summary: summaryContent ?? '',
|
188
|
+
patchAuthors,
|
189
|
+
affectedVersions: versions.split(',').map((v) => v.replace('v', '').trim()),
|
190
|
+
link,
|
191
|
+
reporter: reporter.data.attributes.username
|
192
|
+
};
|
193
|
+
}
|
@@ -3,14 +3,14 @@ import {
|
|
3
3
|
NEXT_SECURITY_RELEASE_REPOSITORY,
|
4
4
|
checkoutOnSecurityReleaseBranch,
|
5
5
|
commitAndPushVulnerabilitiesJSON,
|
6
|
-
|
7
|
-
|
8
|
-
validateDate
|
6
|
+
validateDate,
|
7
|
+
pickReport
|
9
8
|
} from './security-release/security-release.js';
|
10
9
|
import fs from 'node:fs';
|
11
10
|
import path from 'node:path';
|
12
11
|
import auth from './auth.js';
|
13
12
|
import Request from './request.js';
|
13
|
+
import nv from '@pkgjs/nv';
|
14
14
|
|
15
15
|
export default class UpdateSecurityRelease {
|
16
16
|
repository = NEXT_SECURITY_RELEASE_REPOSITORY;
|
@@ -32,7 +32,7 @@ export default class UpdateSecurityRelease {
|
|
32
32
|
checkoutOnSecurityReleaseBranch(cli, this.repository);
|
33
33
|
|
34
34
|
// update the release date in the vulnerabilities.json file
|
35
|
-
const updatedVulnerabilitiesFiles = await this.
|
35
|
+
const updatedVulnerabilitiesFiles = await this.updateJSONReleaseDate(releaseDate, { cli });
|
36
36
|
|
37
37
|
const commitMessage = `chore: update the release date to ${releaseDate}`;
|
38
38
|
commitAndPushVulnerabilitiesJSON(updatedVulnerabilitiesFiles,
|
@@ -56,7 +56,7 @@ export default class UpdateSecurityRelease {
|
|
56
56
|
NEXT_SECURITY_RELEASE_FOLDER, 'vulnerabilities.json');
|
57
57
|
}
|
58
58
|
|
59
|
-
async
|
59
|
+
async updateJSONReleaseDate(releaseDate) {
|
60
60
|
const vulnerabilitiesJSONPath = this.getVulnerabilitiesJSONPath();
|
61
61
|
const content = this.readVulnerabilitiesJSON(vulnerabilitiesJSONPath);
|
62
62
|
content.releaseDate = releaseDate;
|
@@ -68,7 +68,6 @@ export default class UpdateSecurityRelease {
|
|
68
68
|
}
|
69
69
|
|
70
70
|
async addReport(reportId) {
|
71
|
-
const { cli } = this;
|
72
71
|
const credentials = await auth({
|
73
72
|
github: true,
|
74
73
|
h1: true
|
@@ -76,43 +75,21 @@ export default class UpdateSecurityRelease {
|
|
76
75
|
|
77
76
|
const req = new Request(credentials);
|
78
77
|
// checkout on the next-security-release branch
|
79
|
-
checkoutOnSecurityReleaseBranch(cli, this.repository);
|
78
|
+
checkoutOnSecurityReleaseBranch(this.cli, this.repository);
|
80
79
|
|
81
80
|
// get h1 report
|
82
81
|
const { data: report } = await req.getReport(reportId);
|
83
|
-
const
|
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
|
-
};
|
82
|
+
const entry = await pickReport(report, { cli: this.cli, req });
|
106
83
|
|
107
84
|
const vulnerabilitiesJSONPath = this.getVulnerabilitiesJSONPath();
|
108
85
|
const content = this.readVulnerabilitiesJSON(vulnerabilitiesJSONPath);
|
109
86
|
content.reports.push(entry);
|
110
87
|
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`;
|
88
|
+
this.cli.ok(`Updated vulnerabilities.json with the report: ${entry.id}`);
|
89
|
+
const commitMessage = `chore: added report ${entry.id} to vulnerabilities.json`;
|
113
90
|
commitAndPushVulnerabilitiesJSON(vulnerabilitiesJSONPath,
|
114
|
-
commitMessage, { cli, repository: this.repository });
|
115
|
-
cli.ok('Done!');
|
91
|
+
commitMessage, { cli: this.cli, repository: this.repository });
|
92
|
+
this.cli.ok('Done!');
|
116
93
|
}
|
117
94
|
|
118
95
|
removeReport(reportId) {
|
@@ -135,4 +112,163 @@ export default class UpdateSecurityRelease {
|
|
135
112
|
commitMessage, { cli, repository: this.repository });
|
136
113
|
cli.ok('Done!');
|
137
114
|
}
|
115
|
+
|
116
|
+
async requestCVEs() {
|
117
|
+
const credentials = await auth({
|
118
|
+
github: true,
|
119
|
+
h1: true
|
120
|
+
});
|
121
|
+
const vulnerabilitiesJSONPath = this.getVulnerabilitiesJSONPath();
|
122
|
+
const content = this.readVulnerabilitiesJSON(vulnerabilitiesJSONPath);
|
123
|
+
const { reports } = content;
|
124
|
+
const req = new Request(credentials);
|
125
|
+
const programId = await this.getNodeProgramId(req);
|
126
|
+
const cves = await this.promptCVECreation(req, reports, programId);
|
127
|
+
this.assignCVEtoReport(cves, reports);
|
128
|
+
this.updateVulnerabilitiesJSON(content, vulnerabilitiesJSONPath);
|
129
|
+
this.updateHackonerReportCve(req, reports);
|
130
|
+
}
|
131
|
+
|
132
|
+
assignCVEtoReport(cves, reports) {
|
133
|
+
for (const cve of cves) {
|
134
|
+
const report = reports.find(report => report.id === cve.reportId);
|
135
|
+
report.cveIds = [cve.cve_identifier];
|
136
|
+
}
|
137
|
+
}
|
138
|
+
|
139
|
+
async updateHackonerReportCve(req, reports) {
|
140
|
+
for (const report of reports) {
|
141
|
+
const { id, cveIds } = report;
|
142
|
+
this.cli.startSpinner(`Updating report ${id} with CVEs ${cveIds}..`);
|
143
|
+
const body = {
|
144
|
+
data: {
|
145
|
+
type: 'report-cves',
|
146
|
+
attributes: {
|
147
|
+
cve_ids: cveIds
|
148
|
+
}
|
149
|
+
}
|
150
|
+
};
|
151
|
+
const response = await req.updateReportCVE(id, body);
|
152
|
+
if (response.errors) {
|
153
|
+
this.cli.error(`Error updating report ${id}`);
|
154
|
+
this.cli.error(JSON.stringify(response.errors, null, 2));
|
155
|
+
}
|
156
|
+
this.cli.stopSpinner(`Done updating report ${id} with CVEs ${cveIds}..`);
|
157
|
+
}
|
158
|
+
}
|
159
|
+
|
160
|
+
updateVulnerabilitiesJSON(content, vulnerabilitiesJSONPath) {
|
161
|
+
this.cli.startSpinner(`Updating vulnerabilities.json from\
|
162
|
+
${vulnerabilitiesJSONPath}..`);
|
163
|
+
const filePath = path.resolve(vulnerabilitiesJSONPath);
|
164
|
+
fs.writeFileSync(filePath, JSON.stringify(content, null, 2));
|
165
|
+
// push the changes to the repository
|
166
|
+
commitAndPushVulnerabilitiesJSON(filePath,
|
167
|
+
'chore: updated vulnerabilities.json with CVEs',
|
168
|
+
{ cli: this.cli, repository: this.repository });
|
169
|
+
this.cli.stopSpinner(`Done updating vulnerabilities.json from ${filePath}`);
|
170
|
+
}
|
171
|
+
|
172
|
+
async promptCVECreation(req, reports, programId) {
|
173
|
+
const supportedVersions = (await nv('supported'));
|
174
|
+
const cves = [];
|
175
|
+
for (const report of reports) {
|
176
|
+
const { id, summary, title, affectedVersions, cveIds, link } = report;
|
177
|
+
// skip if already has a CVE
|
178
|
+
// risky because the CVE associated might be
|
179
|
+
// mentioned in the report and not requested by Node
|
180
|
+
if (cveIds?.length) continue;
|
181
|
+
|
182
|
+
let severity = report.severity;
|
183
|
+
|
184
|
+
if (!severity.cvss_vector_string || !severity.weakness_id) {
|
185
|
+
try {
|
186
|
+
const h1Report = await req.getReport(id);
|
187
|
+
if (!h1Report.data.relationships.severity?.data.attributes.cvss_vector_string) {
|
188
|
+
throw new Error('No severity found');
|
189
|
+
}
|
190
|
+
severity = {
|
191
|
+
weakness_id: h1Report.data.relationships.weakness?.data.id,
|
192
|
+
cvss_vector_string:
|
193
|
+
h1Report.data.relationships.severity?.data.attributes.cvss_vector_string,
|
194
|
+
rating: h1Report.data.relationships.severity?.data.attributes.rating
|
195
|
+
};
|
196
|
+
} catch (error) {
|
197
|
+
this.cli.error(`Couldnt not retrieve severity from report ${id}, skipping...`);
|
198
|
+
continue;
|
199
|
+
}
|
200
|
+
}
|
201
|
+
|
202
|
+
const { cvss_vector_string, weakness_id } = severity;
|
203
|
+
|
204
|
+
const create = await this.cli.prompt(
|
205
|
+
`Request a CVE for: \n
|
206
|
+
Title: ${title}\n
|
207
|
+
Link: ${link}\n
|
208
|
+
Affected versions: ${affectedVersions.join(', ')}\n
|
209
|
+
Vector: ${cvss_vector_string}\n
|
210
|
+
Summary: ${summary}\n`,
|
211
|
+
{ defaultAnswer: true });
|
212
|
+
|
213
|
+
if (!create) continue;
|
214
|
+
|
215
|
+
const body = {
|
216
|
+
data: {
|
217
|
+
type: 'cve-request',
|
218
|
+
attributes: {
|
219
|
+
team_handle: 'nodejs-team',
|
220
|
+
versions: await this.formatAffected(affectedVersions, supportedVersions),
|
221
|
+
metrics: [
|
222
|
+
{
|
223
|
+
vectorString: cvss_vector_string
|
224
|
+
}
|
225
|
+
],
|
226
|
+
weakness_id: Number(weakness_id),
|
227
|
+
description: title,
|
228
|
+
vulnerability_discovered_at: new Date().toISOString()
|
229
|
+
}
|
230
|
+
}
|
231
|
+
};
|
232
|
+
const { data } = await req.requestCVE(programId, body);
|
233
|
+
if (data.errors) {
|
234
|
+
this.cli.error(`Error requesting CVE for report ${id}`);
|
235
|
+
this.cli.error(JSON.stringify(data.errors, null, 2));
|
236
|
+
continue;
|
237
|
+
}
|
238
|
+
const { cve_identifier } = data.attributes;
|
239
|
+
cves.push({ cve_identifier, reportId: id });
|
240
|
+
}
|
241
|
+
return cves;
|
242
|
+
}
|
243
|
+
|
244
|
+
async getNodeProgramId(req) {
|
245
|
+
const programs = await req.getPrograms();
|
246
|
+
const { data } = programs;
|
247
|
+
for (const program of data) {
|
248
|
+
const { attributes } = program;
|
249
|
+
if (attributes.handle === 'nodejs') {
|
250
|
+
return program.id;
|
251
|
+
}
|
252
|
+
}
|
253
|
+
}
|
254
|
+
|
255
|
+
async formatAffected(affectedVersions, supportedVersions) {
|
256
|
+
const result = [];
|
257
|
+
for (const affectedVersion of affectedVersions) {
|
258
|
+
const major = affectedVersion.split('.')[0];
|
259
|
+
const latest = supportedVersions.find((v) => v.major === Number(major)).version;
|
260
|
+
const version = await this.cli.prompt(
|
261
|
+
`What is the affected version (<=) for release line ${affectedVersion}?`,
|
262
|
+
{ questionType: 'input', defaultAnswer: latest });
|
263
|
+
result.push({
|
264
|
+
vendor: 'nodejs',
|
265
|
+
product: 'node',
|
266
|
+
func: '<=',
|
267
|
+
version,
|
268
|
+
versionType: 'semver',
|
269
|
+
affected: true
|
270
|
+
});
|
271
|
+
}
|
272
|
+
return result;
|
273
|
+
}
|
138
274
|
}
|