Skip to content

Commit 59c0381

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

File tree

4 files changed

+105
-13
lines changed

4 files changed

+105
-13
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: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,20 +14,30 @@ export default class CherryPick {
1414
upstream,
1515
gpgSign,
1616
lint,
17-
includeCVE
17+
includeCVE,
18+
cveIds,
19+
vulnCveMap
1820
} = {}) {
1921
this.prid = prid;
2022
this.cli = cli;
2123
this.dir = dir;
2224
this.upstream = upstream;
2325
this.gpgSign = gpgSign;
24-
this.options = { owner, repo, lint, includeCVE };
26+
this.options = { owner, repo, lint, includeCVE, cveIds, vulnCveMap };
2527
}
2628

2729
get includeCVE() {
2830
return this.options.includeCVE ?? false;
2931
}
3032

33+
get cveIds() {
34+
return this.options.cveIds ?? null;
35+
}
36+
37+
get vulnCveMap() {
38+
return this.options.vulnCveMap ?? null;
39+
}
40+
3141
get owner() {
3242
return this.options.owner || 'nodejs';
3343
}

lib/landing_session.js

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -345,12 +345,39 @@ 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-
);
353-
amended.push('CVE-ID: ' + cveID);
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+
// Fallback: check if the original commit has a PR-URL trailer
354+
// and use it to look up CVE-IDs from the vulnerabilities map
355+
if (this.vulnCveMap) {
356+
const prUrlMatch = original.match(PR_RE);
357+
if (prUrlMatch) {
358+
const prUrl = prUrlMatch[1];
359+
const cveIds = this.vulnCveMap.get(prUrl);
360+
if (cveIds && cveIds.length > 0) {
361+
cveID = cveIds.join(', ');
362+
cli.ok(`Using CVE-ID from backport PR-URL (${prUrl}): ${cveID}`);
363+
}
364+
}
365+
}
366+
367+
// Fall back to prompt if still not found
368+
if (!cveID) {
369+
cveID = await cli.prompt(
370+
'Git found no CVE-ID trailer in the original commit message. ' +
371+
'Please, provide the CVE-ID or leave it empty',
372+
{ questionType: 'input', defaultAnswer: 'CVE-2026-XXXXX' }
373+
);
374+
}
375+
}
376+
// Some commits might not address a vulnerability, but it is necessary
377+
// for the security release to happen.
378+
if (cveID !== '') {
379+
amended.push('CVE-ID: ' + cveID);
380+
}
354381
}
355382

356383
const message = amended.join('\n');

lib/prepare_release.js

Lines changed: 52 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,62 @@ export default class ReleasePreparation extends Session {
6367
return false;
6468
}
6569

70+
const vulnCveMap = new Map();
71+
if (this.isSecurityRelease && this.securityReleaseRepo) {
72+
const vulnPath = path.join(
73+
this.securityReleaseRepo,
74+
'security-release',
75+
'next-security-release',
76+
'vulnerabilities.json'
77+
);
78+
79+
if (!existsSync(vulnPath)) {
80+
cli.error(`vulnerabilities.json not found at ${vulnPath}. ` +
81+
'Skipping CVE auto-population.');
82+
cli.warn('PRs will require manual CVE-ID entry.');
83+
} else {
84+
try {
85+
cli.startSpinner(`Reading vulnerabilities.json from ${vulnPath}..`);
86+
const vulnData = JSON.parse(readFileSync(vulnPath, 'utf-8'));
87+
cli.stopSpinner(`Done reading vulnerabilities.json from ${vulnPath}`);
88+
89+
if (vulnData.reports && Array.isArray(vulnData.reports)) {
90+
vulnData.reports.forEach(report => {
91+
if (report.prURL && report.cveIds && report.cveIds.length > 0) {
92+
vulnCveMap.set(report.prURL, report.cveIds);
93+
}
94+
});
95+
}
96+
cli.ok(`Loaded ${vulnCveMap.size} CVE mappings from vulnerabilities.json`);
97+
} catch (err) {
98+
cli.error(`Failed to read vulnerabilities.json: ${err.message}`);
99+
cli.warn('Continuing without CVE auto-population.');
100+
}
101+
}
102+
}
103+
66104
for (const pr of prs) {
67105
if (pr.mergeable !== 'MERGEABLE') {
68106
this.warnForNonMergeablePR(pr);
69107
}
108+
109+
// Look up CVE-IDs from vulnerabilities.json
110+
const prUrl = `https://github.com/${this.owner}/${this.repo}/pull/${pr.number}`;
111+
const cveIds = vulnCveMap.get(prUrl);
112+
113+
if (!cveIds || cveIds.length === 0) {
114+
cli.warn(`No CVE-IDs found in vulnerabilities.json for ${prUrl}`);
115+
}
116+
70117
const cp = new CherryPick(pr.number, this.dir, cli, {
71118
owner: this.owner,
72119
repo: this.repo,
73120
gpgSign: this.gpgSign,
74121
upstream: this.isSecurityRelease ? `https://${this.username}:${this.config.token}@github.com/${this.owner}/${this.repo}.git` : this.upstream,
75122
lint: false,
76-
includeCVE: true
123+
includeCVE: true,
124+
cveIds: cveIds || null,
125+
vulnCveMap
77126
});
78127
const success = await cp.start();
79128
if (!success) {

0 commit comments

Comments
 (0)