From 59c038152147a71eec0ccefe2fc78000178628a4 Mon Sep 17 00:00:00 2001 From: RafaelGSS Date: Fri, 2 Jan 2026 12:00:06 -0300 Subject: [PATCH] feat: automatically include cveId trailling Fixes: https://github.com/nodejs-private/security-release/issues/47 --- components/git/release.js | 10 +++++-- lib/cherry_pick.js | 14 ++++++++-- lib/landing_session.js | 39 ++++++++++++++++++++++----- lib/prepare_release.js | 55 ++++++++++++++++++++++++++++++++++++--- 4 files changed, 105 insertions(+), 13 deletions(-) diff --git a/components/git/release.js b/components/git/release.js index 597fe2ed..69265b6d 100644 --- a/components/git/release.js +++ b/components/git/release.js @@ -50,8 +50,14 @@ const releaseOptions = { type: 'boolean' }, security: { - describe: 'Demarcate the new security release as a security release', - type: 'boolean' + describe: 'Demarcate the new security release as a security release. ' + + 'Optionally provide path to security-release repository for CVE auto-population', + type: 'string', + coerce: (arg) => { + // If --security=path is used, return the path + if (arg === '' || arg === true) return true; + return arg; + } }, skipBranchDiff: { describe: 'Skips the initial branch-diff check when preparing releases', diff --git a/lib/cherry_pick.js b/lib/cherry_pick.js index df0503fa..44968d2f 100644 --- a/lib/cherry_pick.js +++ b/lib/cherry_pick.js @@ -14,20 +14,30 @@ export default class CherryPick { upstream, gpgSign, lint, - includeCVE + includeCVE, + cveIds, + vulnCveMap } = {}) { this.prid = prid; this.cli = cli; this.dir = dir; this.upstream = upstream; this.gpgSign = gpgSign; - this.options = { owner, repo, lint, includeCVE }; + this.options = { owner, repo, lint, includeCVE, cveIds, vulnCveMap }; } get includeCVE() { return this.options.includeCVE ?? false; } + get cveIds() { + return this.options.cveIds ?? null; + } + + get vulnCveMap() { + return this.options.vulnCveMap ?? null; + } + get owner() { return this.options.owner || 'nodejs'; } diff --git a/lib/landing_session.js b/lib/landing_session.js index 19944ba4..15b67ed7 100644 --- a/lib/landing_session.js +++ b/lib/landing_session.js @@ -345,12 +345,39 @@ export default class LandingSession extends Session { } if (!containCVETrailer && this.includeCVE) { - const cveID = await cli.prompt( - 'Git found no CVE-ID trailer in the original commit message. ' + - 'Please, provide the CVE-ID', - { questionType: 'input', defaultAnswer: 'CVE-2023-XXXXX' } - ); - amended.push('CVE-ID: ' + cveID); + let cveID; + if (this.cveIds && this.cveIds.length > 0) { + cveID = this.cveIds.join(', '); + cli.ok(`Using CVE-ID from vulnerabilities.json: ${cveID}`); + } else { + // Fallback: check if the original commit has a PR-URL trailer + // and use it to look up CVE-IDs from the vulnerabilities map + if (this.vulnCveMap) { + const prUrlMatch = original.match(PR_RE); + if (prUrlMatch) { + const prUrl = prUrlMatch[1]; + const cveIds = this.vulnCveMap.get(prUrl); + if (cveIds && cveIds.length > 0) { + cveID = cveIds.join(', '); + cli.ok(`Using CVE-ID from backport PR-URL (${prUrl}): ${cveID}`); + } + } + } + + // Fall back to prompt if still not found + if (!cveID) { + cveID = await cli.prompt( + 'Git found no CVE-ID trailer in the original commit message. ' + + 'Please, provide the CVE-ID or leave it empty', + { questionType: 'input', defaultAnswer: 'CVE-2026-XXXXX' } + ); + } + } + // Some commits might not address a vulnerability, but it is necessary + // for the security release to happen. + if (cveID !== '') { + amended.push('CVE-ID: ' + cveID); + } } const message = amended.join('\n'); diff --git a/lib/prepare_release.js b/lib/prepare_release.js index eea137a8..785d5ea0 100644 --- a/lib/prepare_release.js +++ b/lib/prepare_release.js @@ -1,5 +1,5 @@ import path from 'node:path'; -import { promises as fs } from 'node:fs'; +import { promises as fs, existsSync, readFileSync } from 'node:fs'; import semver from 'semver'; import { replaceInFile } from 'replace-in-file'; @@ -21,7 +21,11 @@ const isWindows = process.platform === 'win32'; export default class ReleasePreparation extends Session { constructor(argv, cli, dir) { super(cli, dir); - this.isSecurityRelease = argv.security; + // argv.security can be either: + // - true (boolean) if --security was used without parameter + // - string if --security=path was used + this.isSecurityRelease = !!argv.security; + this.securityReleaseRepo = typeof argv.security === 'string' ? argv.security : null; this.isLTS = false; this.isLTSTransition = argv.startLTS; this.runBranchDiff = !argv.skipBranchDiff; @@ -63,17 +67,62 @@ export default class ReleasePreparation extends Session { return false; } + const vulnCveMap = new Map(); + if (this.isSecurityRelease && this.securityReleaseRepo) { + const vulnPath = path.join( + this.securityReleaseRepo, + 'security-release', + 'next-security-release', + 'vulnerabilities.json' + ); + + if (!existsSync(vulnPath)) { + cli.error(`vulnerabilities.json not found at ${vulnPath}. ` + + 'Skipping CVE auto-population.'); + cli.warn('PRs will require manual CVE-ID entry.'); + } else { + try { + cli.startSpinner(`Reading vulnerabilities.json from ${vulnPath}..`); + const vulnData = JSON.parse(readFileSync(vulnPath, 'utf-8')); + cli.stopSpinner(`Done reading vulnerabilities.json from ${vulnPath}`); + + if (vulnData.reports && Array.isArray(vulnData.reports)) { + vulnData.reports.forEach(report => { + if (report.prURL && report.cveIds && report.cveIds.length > 0) { + vulnCveMap.set(report.prURL, report.cveIds); + } + }); + } + cli.ok(`Loaded ${vulnCveMap.size} CVE mappings from vulnerabilities.json`); + } catch (err) { + cli.error(`Failed to read vulnerabilities.json: ${err.message}`); + cli.warn('Continuing without CVE auto-population.'); + } + } + } + for (const pr of prs) { if (pr.mergeable !== 'MERGEABLE') { this.warnForNonMergeablePR(pr); } + + // Look up CVE-IDs from vulnerabilities.json + const prUrl = `https://github.com/${this.owner}/${this.repo}/pull/${pr.number}`; + const cveIds = vulnCveMap.get(prUrl); + + if (!cveIds || cveIds.length === 0) { + cli.warn(`No CVE-IDs found in vulnerabilities.json for ${prUrl}`); + } + const cp = new CherryPick(pr.number, this.dir, cli, { owner: this.owner, repo: this.repo, gpgSign: this.gpgSign, upstream: this.isSecurityRelease ? `https://${this.username}:${this.config.token}@github.com/${this.owner}/${this.repo}.git` : this.upstream, lint: false, - includeCVE: true + includeCVE: true, + cveIds: cveIds || null, + vulnCveMap }); const success = await cp.start(); if (!success) {