Skip to content

Commit 7aaf70c

Browse files
committed
feat: automatically include cveId trailling
Fixes: nodejs-private/security-release#47
1 parent 5d72b3e commit 7aaf70c

File tree

4 files changed

+80
-12
lines changed

4 files changed

+80
-12
lines changed

components/git/release.js

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,14 @@ const releaseOptions = {
5050
type: 'boolean'
5151
},
5252
security: {
53-
describe: 'Demarcate the new security release as a security release',
54-
type: 'boolean'
53+
describe: 'Demarcate the new security release as a security release. ' +
54+
'Optionally provide path to security-release repository for CVE auto-population',
55+
type: 'string',
56+
coerce: (arg) => {
57+
// If --security=path is used, return the path
58+
if (arg === '' || arg === true) return true;
59+
return arg;
60+
}
5561
},
5662
skipBranchDiff: {
5763
describe: 'Skips the initial branch-diff check when preparing releases',

lib/cherry_pick.js

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,20 +14,25 @@ export default class CherryPick {
1414
upstream,
1515
gpgSign,
1616
lint,
17-
includeCVE
17+
includeCVE,
18+
cveIds
1819
} = {}) {
1920
this.prid = prid;
2021
this.cli = cli;
2122
this.dir = dir;
2223
this.upstream = upstream;
2324
this.gpgSign = gpgSign;
24-
this.options = { owner, repo, lint, includeCVE };
25+
this.options = { owner, repo, lint, includeCVE, cveIds };
2526
}
2627

2728
get includeCVE() {
2829
return this.options.includeCVE ?? false;
2930
}
3031

32+
get cveIds() {
33+
return this.options.cveIds ?? null;
34+
}
35+
3136
get owner() {
3237
return this.options.owner || 'nodejs';
3338
}

lib/landing_session.js

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -345,11 +345,18 @@ export default class LandingSession extends Session {
345345
}
346346

347347
if (!containCVETrailer && this.includeCVE) {
348-
const cveID = await cli.prompt(
349-
'Git found no CVE-ID trailer in the original commit message. ' +
350-
'Please, provide the CVE-ID',
351-
{ questionType: 'input', defaultAnswer: 'CVE-2023-XXXXX' }
352-
);
348+
let cveID;
349+
if (this.cveIds && this.cveIds.length > 0) {
350+
cveID = this.cveIds.join(', ');
351+
cli.ok(`Using CVE-ID from vulnerabilities.json: ${cveID}`);
352+
} else {
353+
// Fall back to prompt if not found
354+
cveID = await cli.prompt(
355+
'Git found no CVE-ID trailer in the original commit message. ' +
356+
'Please, provide the CVE-ID',
357+
{ questionType: 'input', defaultAnswer: 'CVE-2026-XXXXX' }
358+
);
359+
}
353360
amended.push('CVE-ID: ' + cveID);
354361
}
355362

lib/prepare_release.js

Lines changed: 53 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import path from 'node:path';
2-
import { promises as fs } from 'node:fs';
2+
import { promises as fs, existsSync, readFileSync } from 'node:fs';
33

44
import semver from 'semver';
55
import { replaceInFile } from 'replace-in-file';
@@ -21,7 +21,11 @@ const isWindows = process.platform === 'win32';
2121
export default class ReleasePreparation extends Session {
2222
constructor(argv, cli, dir) {
2323
super(cli, dir);
24-
this.isSecurityRelease = argv.security;
24+
// argv.security can be either:
25+
// - true (boolean) if --security was used without parameter
26+
// - string if --security=path was used
27+
this.isSecurityRelease = !!argv.security;
28+
this.securityReleaseRepo = typeof argv.security === 'string' ? argv.security : null;
2529
this.isLTS = false;
2630
this.isLTSTransition = argv.startLTS;
2731
this.runBranchDiff = !argv.skipBranchDiff;
@@ -63,17 +67,63 @@ export default class ReleasePreparation extends Session {
6367
return false;
6468
}
6569

70+
// Read vulnerabilities.json and create PR URL -> CVE-IDs mapping
71+
let vulnCveMap = new Map();
72+
if (this.isSecurityRelease && this.securityReleaseRepo) {
73+
// Construct path to vulnerabilities.json within the security-release repo
74+
const vulnPath = path.join(
75+
this.securityReleaseRepo,
76+
'security-release',
77+
'next-security-release',
78+
'vulnerabilities.json'
79+
);
80+
81+
if (!existsSync(vulnPath)) {
82+
cli.error(`vulnerabilities.json not found at ${vulnPath}. ` +
83+
'Skipping CVE auto-population.');
84+
cli.warn('PRs will require manual CVE-ID entry.');
85+
} else {
86+
try {
87+
cli.startSpinner(`Reading vulnerabilities.json from ${vulnPath}..`);
88+
const vulnData = JSON.parse(readFileSync(vulnPath, 'utf-8'));
89+
cli.stopSpinner(`Done reading vulnerabilities.json from ${vulnPath}`);
90+
91+
if (vulnData.reports && Array.isArray(vulnData.reports)) {
92+
vulnData.reports.forEach(report => {
93+
if (report.prURL && report.cveIds && report.cveIds.length > 0) {
94+
vulnCveMap.set(report.prURL, report.cveIds);
95+
}
96+
});
97+
}
98+
cli.ok(`Loaded ${vulnCveMap.size} CVE mappings from vulnerabilities.json`);
99+
} catch (err) {
100+
cli.error(`Failed to read vulnerabilities.json: ${err.message}`);
101+
cli.warn('Continuing without CVE auto-population.');
102+
}
103+
}
104+
}
105+
66106
for (const pr of prs) {
67107
if (pr.mergeable !== 'MERGEABLE') {
68108
this.warnForNonMergeablePR(pr);
69109
}
110+
111+
// Look up CVE-IDs from vulnerabilities.json
112+
const prUrl = `https://github.com/${this.owner}/${this.repo}/pull/${pr.number}`;
113+
const cveIds = vulnCveMap.get(prUrl);
114+
115+
if (!cveIds || cveIds.length === 0) {
116+
cli.warn(`No CVE-IDs found in vulnerabilities.json for ${prUrl}`);
117+
}
118+
70119
const cp = new CherryPick(pr.number, this.dir, cli, {
71120
owner: this.owner,
72121
repo: this.repo,
73122
gpgSign: this.gpgSign,
74123
upstream: this.isSecurityRelease ? `https://${this.username}:${this.config.token}@github.com/${this.owner}/${this.repo}.git` : this.upstream,
75124
lint: false,
76-
includeCVE: true
125+
includeCVE: true,
126+
cveIds: cveIds || null
77127
});
78128
const success = await cp.start();
79129
if (!success) {

0 commit comments

Comments
 (0)