From 3545e2857be67dfd76d54cc78456953b1769cfad Mon Sep 17 00:00:00 2001 From: RafaelGSS Date: Wed, 10 Jun 2026 12:36:06 -0300 Subject: [PATCH 1/2] feat: add confirmation steps to any write operation --- docs/git-node.md | 6 ++ lib/prepare_security.js | 55 ++++++++++-- lib/security-announcement.js | 15 ++-- lib/security-release/security-release.js | 110 +++++++++++++++++++---- lib/security_blog.js | 41 ++++++--- lib/update_security_release.js | 61 +++++++++---- 6 files changed, 234 insertions(+), 54 deletions(-) diff --git a/docs/git-node.md b/docs/git-node.md index f36be7c5..bb5642e0 100644 --- a/docs/git-node.md +++ b/docs/git-node.md @@ -440,6 +440,12 @@ $ git node vote \ Manage or starts a security release process. +Every `git node security` action asks for permission before each mutating step. +Read-only operations, such as reading files or fetching report data, run without +confirmation. Each confirmation names the command or service action that will +write state and explains the side effect, such as writing files, committing and +pushing changes, creating issues, or updating HackerOne reports. + ### Prerequisites diff --git a/lib/prepare_security.js b/lib/prepare_security.js index 82271a2d..8bddc1cd 100644 --- a/lib/prepare_security.js +++ b/lib/prepare_security.js @@ -12,6 +12,8 @@ import { getSupportedVersions, getReportSeverity, pickReport, + confirmSecurityStep, + writeSecurityFile, SecurityRelease } from './security-release/security-release.js'; import _ from 'lodash'; @@ -72,6 +74,11 @@ export default class PrepareSecurityRelease extends SecurityRelease { if (vulnerabilityJSON.buildIssue) { this.cli.info('Commenting on nodejs/build issue'); + await confirmSecurityStep( + this.cli, + `comment on GitHub issue \`${vulnerabilityJSON.buildIssue}\``, + 'This posts that the security release is out on the build tracking issue.' + ); await this.req.commentIssue( vulnerabilityJSON.buildIssue, 'Security release is out' @@ -80,6 +87,11 @@ export default class PrepareSecurityRelease extends SecurityRelease { if (vulnerabilityJSON.dockerIssue) { this.cli.info('Commenting on nodejs/docker-node issue'); + await confirmSecurityStep( + this.cli, + `comment on GitHub issue \`${vulnerabilityJSON.dockerIssue}\``, + 'This posts that the security release is out on the docker-node tracking issue.' + ); await this.req.commentIssue( vulnerabilityJSON.dockerIssue, 'Security release is out' @@ -91,11 +103,11 @@ export default class PrepareSecurityRelease extends SecurityRelease { vulnerabilityJSON.releaseDate}?`, { defaultAnswer: true }); if (updateFolder) { - this.updateReleaseFolder( + await this.updateReleaseFolder( vulnerabilityJSON.releaseDate.replaceAll('/', '-') ); const securityReleaseFolder = path.join(process.cwd(), 'security-release'); - commitAndPushVulnerabilitiesJSON( + await commitAndPushVulnerabilitiesJSON( securityReleaseFolder, 'chore: change next-security-release folder', { cli: this.cli, repository: this.repository } @@ -110,7 +122,7 @@ export default class PrepareSecurityRelease extends SecurityRelease { async startVulnerabilitiesJSONCreation(releaseDate, content, excludedReports = []) { // checkout on the next-security-release branch - checkoutOnSecurityReleaseBranch(this.cli, this.repository); + await checkoutOnSecurityReleaseBranch(this.cli, this.repository); // choose the reports to include in the security release const reports = await this.chooseReports(excludedReports); @@ -134,7 +146,7 @@ export default class PrepareSecurityRelease extends SecurityRelease { // commit and push the vulnerabilities.json file const commitMessage = 'chore: create vulnerabilities.json for next security release'; - commitAndPushVulnerabilitiesJSON(filePath, + await commitAndPushVulnerabilitiesJSON(filePath, commitMessage, { cli: this.cli, repository: this.repository }); @@ -270,10 +282,19 @@ export default class PrepareSecurityRelease extends SecurityRelease { }, null, 2) + '\n'; const folderPath = path.resolve(NEXT_SECURITY_RELEASE_FOLDER); - await fs.promises.mkdir(folderPath, { recursive: true }); - const fullPath = path.join(folderPath, 'vulnerabilities.json'); - await fs.promises.writeFile(fullPath, fileContent); + await confirmSecurityStep( + this.cli, + `create directory \`${folderPath}\``, + 'This creates the security release folder if it does not already exist.' + ); + await fs.promises.mkdir(folderPath, { recursive: true }); + await writeSecurityFile( + this.cli, + fullPath, + fileContent, + 'This creates vulnerabilities.json for the next security release.' + ); this.cli.stopSpinner(`Created ${fullPath}`); return fullPath; @@ -281,6 +302,11 @@ export default class PrepareSecurityRelease extends SecurityRelease { async createPullRequest(content) { const { owner, repo } = this.repository; + await confirmSecurityStep( + this.cli, + `create GitHub pull request \`${owner}/${repo}: ${this.title}\``, + `This opens a pull request from ${NEXT_SECURITY_RELEASE_BRANCH} to main.` + ); const response = await this.req.createPullRequest( this.title, content ?? 'List of vulnerabilities to be included in the next security release', @@ -361,6 +387,11 @@ export default class PrepareSecurityRelease extends SecurityRelease { this.cli.startSpinner('Closing HackerOne reports'); for (const report of jsonReports) { this.cli.updateSpinner(`Closing report ${report.id}...`); + await confirmSecurityStep( + this.cli, + `resolve HackerOne report \`${report.id}\``, + 'This marks the HackerOne report as resolved.' + ); await this.req.updateReportState( report.id, 'resolved', @@ -368,6 +399,11 @@ export default class PrepareSecurityRelease extends SecurityRelease { ); this.cli.updateSpinner(`Requesting disclosure to report ${report.id}...`); + await confirmSecurityStep( + this.cli, + `request disclosure for HackerOne report \`${report.id}\``, + 'This asks HackerOne to disclose the resolved report.' + ); await this.req.requestDisclosure(report.id); } this.cli.stopSpinner('Done closing H1 Reports and requesting disclosure'); @@ -385,6 +421,11 @@ export default class PrepareSecurityRelease extends SecurityRelease { for (const pr of prs) { if (pr.labels.some((l) => labels.includes(l.name))) { this.cli.updateSpinner(`Closing Pull Request: ${pr.number}`); + await confirmSecurityStep( + this.cli, + `close GitHub pull request \`nodejs-private/node-private#${pr.number}\``, + 'This closes a pull request labeled for the security release.' + ); await this.req.closePullRequest(pr.number, { owner: 'nodejs-private', repo: 'node-private' }); } diff --git a/lib/security-announcement.js b/lib/security-announcement.js index eeab460a..f12bad6b 100644 --- a/lib/security-announcement.js +++ b/lib/security-announcement.js @@ -1,4 +1,3 @@ -import fs from 'node:fs'; import { NEXT_SECURITY_RELEASE_REPOSITORY, checkoutOnSecurityReleaseBranch, @@ -7,7 +6,8 @@ import { validateDate, formatDateToYYYYMMDD, commitAndPushVulnerabilitiesJSON, - createIssue + createIssue, + writeSecurityFile } from './security-release/security-release.js'; import auth from './auth.js'; import Request from './request.js'; @@ -30,7 +30,7 @@ export default class SecurityAnnouncement { this.req = new Request(credentials); // checkout on security release branch - checkoutOnSecurityReleaseBranch(cli, this.repository); + await checkoutOnSecurityReleaseBranch(cli, this.repository); // read vulnerabilities JSON file const content = getVulnerabilitiesJSON(cli); // validate the release date read from vulnerabilities JSON @@ -52,9 +52,14 @@ export default class SecurityAnnouncement { content.dockerIssue = dockerIssue; const vulnerabilitiesJSONPath = getVulnerabilitiesJSONPath(); - fs.writeFileSync(vulnerabilitiesJSONPath, JSON.stringify(content, null, 2)); + await writeSecurityFile( + cli, + vulnerabilitiesJSONPath, + JSON.stringify(content, null, 2), + 'This records the build and docker tracking issue links in vulnerabilities.json.' + ); const commitMessage = 'chore: add build and docker issue link'; - commitAndPushVulnerabilitiesJSON([vulnerabilitiesJSONPath], + await commitAndPushVulnerabilitiesJSON([vulnerabilitiesJSONPath], commitMessage, { cli: this.cli, repository: this.repository }); this.cli.ok('Added docker and build issue in vulnerabilities.json'); diff --git a/lib/security-release/security-release.js b/lib/security-release/security-release.js index 9c788fc9..7bb2678a 100644 --- a/lib/security-release/security-release.js +++ b/lib/security-release/security-release.js @@ -30,7 +30,36 @@ export const PLACEHOLDERS = { downloads: '%DOWNLOADS%' }; -export function checkRemote(cli, repository) { +function formatCommand(command, args) { + return [command, ...args].join(' '); +} + +export async function confirmSecurityStep(cli, action, detail) { + const lines = [`Allow action: ${action}?`]; + if (detail) { + lines.push('', detail); + } + const message = lines.join('\n'); + + const allowed = await cli.prompt(message, { defaultAnswer: false }); + if (!allowed) { + cli.info(`Aborted: ${action}.`); + process.exit(0); + } +} + +export async function runSecurityGitCommand(cli, args, detail) { + const command = formatCommand('git', args); + await confirmSecurityStep(cli, `run \`${command}\``, detail); + return runSync('git', args); +} + +export async function writeSecurityFile(cli, filePath, content, detail) { + await confirmSecurityStep(cli, `write \`${filePath}\``, detail); + return fs.writeFileSync(filePath, content); +} + +export async function checkRemote(cli, repository) { const remote = runSync('git', ['ls-remote', '--get-url', 'origin']).trim(); const { owner, repo } = repository; const securityReleaseOrigin = [ @@ -44,26 +73,42 @@ export function checkRemote(cli, repository) { } } -export function checkoutOnSecurityReleaseBranch(cli, repository) { - checkRemote(cli, repository); +export async function checkoutOnSecurityReleaseBranch(cli, repository) { + await checkRemote(cli, repository); const currentBranch = runSync('git', ['branch', '--show-current']).trim(); cli.info(`Current branch: ${currentBranch} `); if (currentBranch !== NEXT_SECURITY_RELEASE_BRANCH) { - runSync('git', ['checkout', '-B', NEXT_SECURITY_RELEASE_BRANCH]); + await runSecurityGitCommand( + cli, + ['checkout', '-B', NEXT_SECURITY_RELEASE_BRANCH], + `This checks out or recreates the ${NEXT_SECURITY_RELEASE_BRANCH} branch locally.` + ); cli.ok(`Checkout on branch: ${NEXT_SECURITY_RELEASE_BRANCH} `); }; } -export function commitAndPushVulnerabilitiesJSON(filePath, commitMessage, { cli, repository }) { - checkRemote(cli, repository); +export async function commitAndPushVulnerabilitiesJSON( + filePath, + commitMessage, + { cli, repository } +) { + await checkRemote(cli, repository); if (Array.isArray(filePath)) { - for (const path of filePath) { - runSync('git', ['add', path]); + for (const currentPath of filePath) { + await runSecurityGitCommand( + cli, + ['add', currentPath], + `This stages ${currentPath} for the security release commit.` + ); } } else { - runSync('git', ['add', filePath]); + await runSecurityGitCommand( + cli, + ['add', filePath], + `This stages ${filePath} for the security release commit.` + ); } const staged = runSync('git', ['diff', '--name-only', '--cached']).trim(); @@ -72,15 +117,31 @@ export function commitAndPushVulnerabilitiesJSON(filePath, commitMessage, { cli, return; } - runSync('git', ['commit', '-m', commitMessage]); + await runSecurityGitCommand( + cli, + ['commit', '-m', commitMessage], + `This creates a local commit with message: ${commitMessage}` + ); try { - runSync('git', ['push', '-u', 'origin', NEXT_SECURITY_RELEASE_BRANCH]); + await runSecurityGitCommand( + cli, + ['push', '-u', 'origin', NEXT_SECURITY_RELEASE_BRANCH], + `This pushes the security release branch to origin/${NEXT_SECURITY_RELEASE_BRANCH}.` + ); } catch (error) { cli.warn('Rebasing...'); // try to pull rebase and push again - runSync('git', ['pull', 'origin', NEXT_SECURITY_RELEASE_BRANCH, '--rebase']); - runSync('git', ['push', '-u', 'origin', NEXT_SECURITY_RELEASE_BRANCH]); + await runSecurityGitCommand( + cli, + ['pull', 'origin', NEXT_SECURITY_RELEASE_BRANCH, '--rebase'], + `This rebases local changes on origin/${NEXT_SECURITY_RELEASE_BRANCH}.` + ); + await runSecurityGitCommand( + cli, + ['push', '-u', 'origin', NEXT_SECURITY_RELEASE_BRANCH], + `This retries pushing the security release branch to origin/${NEXT_SECURITY_RELEASE_BRANCH}.` + ); } cli.ok(`Pushed commit: ${commitMessage} to ${NEXT_SECURITY_RELEASE_BRANCH}`); } @@ -150,6 +211,11 @@ export function promptDependencies(cli) { } export async function createIssue(title, content, repository, { cli, req }) { + await confirmSecurityStep( + cli, + `create GitHub issue \`${repository.owner}/${repository.repo}: ${title}\``, + `This creates an issue in ${repository.owner}/${repository.repo}.` + ); const data = await req.createIssue(title, content, repository); if (data.html_url) { cli.ok(`Created: ${data.html_url}`); @@ -252,20 +318,30 @@ export class SecurityRelease { NEXT_SECURITY_RELEASE_FOLDER, 'vulnerabilities.json'); } - updateReleaseFolder(releaseDate) { + async updateReleaseFolder(releaseDate) { const folder = path.join(process.cwd(), NEXT_SECURITY_RELEASE_FOLDER); const newFolder = path.join(process.cwd(), 'security-release', releaseDate); + await confirmSecurityStep( + this.cli, + `rename \`${folder}\` to \`${newFolder}\``, + 'This moves the next-security-release folder to the dated release folder.' + ); fs.renameSync(folder, newFolder); return newFolder; } - updateVulnerabilitiesJSON(content) { + async updateVulnerabilitiesJSON(content) { try { const vulnerabilitiesJSONPath = this.getVulnerabilitiesJSONPath(); this.cli.startSpinner(`Updating vulnerabilities.json from ${vulnerabilitiesJSONPath}...`); - fs.writeFileSync(vulnerabilitiesJSONPath, JSON.stringify(content, null, 2)); - commitAndPushVulnerabilitiesJSON(vulnerabilitiesJSONPath, + await writeSecurityFile( + this.cli, + vulnerabilitiesJSONPath, + JSON.stringify(content, null, 2), + 'This updates vulnerabilities.json with the latest security release data.' + ); + await commitAndPushVulnerabilitiesJSON(vulnerabilitiesJSONPath, 'chore: updated vulnerabilities.json', { cli: this.cli, repository: this.repository }); this.cli.stopSpinner(`Done updating vulnerabilities.json from ${vulnerabilitiesJSONPath}`); diff --git a/lib/security_blog.js b/lib/security_blog.js index b424a5bc..d52abad2 100644 --- a/lib/security_blog.js +++ b/lib/security_blog.js @@ -8,6 +8,7 @@ import { SecurityRelease, commitAndPushVulnerabilitiesJSON, getHighestSeverityAnnouncement, + writeSecurityFile, } from './security-release/security-release.js'; import auth from './auth.js'; import Request from './request.js'; @@ -19,7 +20,7 @@ export default class SecurityBlog extends SecurityRelease { const { cli } = this; // checkout on security release branch - checkoutOnSecurityReleaseBranch(cli, this.repository); + await checkoutOnSecurityReleaseBranch(cli, this.repository); // read vulnerabilities JSON file const content = this.readVulnerabilitiesJSON(); @@ -57,14 +58,19 @@ export default class SecurityBlog extends SecurityRelease { endDate.setDate(endDate.getDate() + 7); const link = `https://nodejs.org/en/blog/vulnerability/${fileName}`; - this.updateWebsiteBanner(site, { + await this.updateWebsiteBanner(site, { startDate: data.annoucementDate, endDate: endDate.toISOString(), text: `New security releases to be made available ${data.releaseDate}`, link, type: 'warning' }); - fs.writeFileSync(file, preRelease); + await writeSecurityFile( + cli, + file, + preRelease, + 'This writes the pre-release vulnerability announcement post.' + ); cli.ok(`Announcement file created and banner has been updated. Folder: ${nodejsOrgFolder}`); await this.updateAnnouncementLink(link); @@ -82,9 +88,14 @@ export default class SecurityBlog extends SecurityRelease { }; if (shouldCommit) { - fs.writeFileSync(vulnerabilitiesJSONPath, JSON.stringify(content, null, 2)); + await writeSecurityFile( + this.cli, + vulnerabilitiesJSONPath, + JSON.stringify(content, null, 2), + 'This writes the announcement link to each report in vulnerabilities.json.' + ); const commitMessage = 'chore: add announcement link'; - commitAndPushVulnerabilitiesJSON([vulnerabilitiesJSONPath], + await commitAndPushVulnerabilitiesJSON([vulnerabilitiesJSONPath], commitMessage, { cli: this.cli, repository: this.repository }); this.cli.ok('Updated the announcement link in vulnerabilities.json'); @@ -103,7 +114,7 @@ export default class SecurityBlog extends SecurityRelease { this.req = new Request(credentials); // checkout on security release branch - checkoutOnSecurityReleaseBranch(cli, this.repository); + await checkoutOnSecurityReleaseBranch(cli, this.repository); // read vulnerabilities JSON file const content = this.readVulnerabilitiesJSON(); @@ -147,13 +158,18 @@ export default class SecurityBlog extends SecurityRelease { const month = releaseDate.toLocaleString('en-US', { month: 'long' }); const capitalizedMonth = month[0].toUpperCase() + month.slice(1); - this.updateWebsiteBanner(pathToBannerJson, { + await this.updateWebsiteBanner(pathToBannerJson, { startDate: releaseDate, endDate, text: `${capitalizedMonth} Security Release is available` }); - fs.writeFileSync(preReleasePath, updatedContent); + await writeSecurityFile( + cli, + preReleasePath, + updatedContent, + 'This writes the post-release vulnerability announcement post.' + ); cli.ok(`Announcement file and banner has been updated. Folder: ${nodejsOrgFolder}`); } @@ -173,7 +189,7 @@ export default class SecurityBlog extends SecurityRelease { }); } - updateWebsiteBanner(siteJsonPath, content) { + async updateWebsiteBanner(siteJsonPath, content) { const siteJson = JSON.parse(fs.readFileSync(siteJsonPath)); const currentValue = siteJson.websiteBanners.index; @@ -184,7 +200,12 @@ export default class SecurityBlog extends SecurityRelease { link: content.link ?? currentValue.link, type: content.type ?? currentValue.type }; - fs.writeFileSync(siteJsonPath, JSON.stringify(siteJson, null, 2) + '\n'); + await writeSecurityFile( + this.cli, + siteJsonPath, + JSON.stringify(siteJson, null, 2) + '\n', + 'This writes the updated security release website banner.' + ); } formatReleaseDate(releaseDate) { diff --git a/lib/update_security_release.js b/lib/update_security_release.js index 8125e648..7dc710f9 100644 --- a/lib/update_security_release.js +++ b/lib/update_security_release.js @@ -6,9 +6,10 @@ import { pickReport, getReportSeverity, getSummary, + confirmSecurityStep, + writeSecurityFile, SecurityRelease } from './security-release/security-release.js'; -import fs from 'node:fs'; import auth from './auth.js'; import Request from './request.js'; import nv from '@pkgjs/nv'; @@ -16,7 +17,7 @@ import semver from 'semver'; export default class UpdateSecurityRelease extends SecurityRelease { async sync() { - checkRemote(this.cli, this.repository); + await checkRemote(this.cli, this.repository); const content = this.readVulnerabilitiesJSON(); const credentials = await auth({ @@ -46,10 +47,15 @@ export default class UpdateSecurityRelease extends SecurityRelease { }; } const vulnerabilitiesJSONPath = this.getVulnerabilitiesJSONPath(); - fs.writeFileSync(vulnerabilitiesJSONPath, JSON.stringify(content, null, 2)); + await writeSecurityFile( + this.cli, + vulnerabilitiesJSONPath, + JSON.stringify(content, null, 2), + 'This writes synchronized HackerOne report data to vulnerabilities.json.' + ); const commitMessage = 'chore: git node security --sync'; - commitAndPushVulnerabilitiesJSON([vulnerabilitiesJSONPath], + await commitAndPushVulnerabilitiesJSON([vulnerabilitiesJSONPath], commitMessage, { cli: this.cli, repository: this.repository }); this.cli.ok('Synced vulnerabilities.json with HackerOne'); } @@ -65,13 +71,13 @@ export default class UpdateSecurityRelease extends SecurityRelease { } // checkout on the next-security-release branch - checkoutOnSecurityReleaseBranch(cli, this.repository); + await checkoutOnSecurityReleaseBranch(cli, this.repository); // update the release date in the vulnerabilities.json file const updatedVulnerabilitiesFiles = await this.updateJSONReleaseDate(releaseDate, { cli }); const commitMessage = `chore: update the release date to ${releaseDate}`; - commitAndPushVulnerabilitiesJSON(updatedVulnerabilitiesFiles, + await commitAndPushVulnerabilitiesJSON(updatedVulnerabilitiesFiles, commitMessage, { cli, repository: this.repository }); cli.ok('Done!'); } @@ -81,7 +87,12 @@ export default class UpdateSecurityRelease extends SecurityRelease { const content = this.readVulnerabilitiesJSON(vulnerabilitiesJSONPath); content.releaseDate = releaseDate; - fs.writeFileSync(vulnerabilitiesJSONPath, JSON.stringify(content, null, 2)); + await writeSecurityFile( + this.cli, + vulnerabilitiesJSONPath, + JSON.stringify(content, null, 2), + `This sets releaseDate to ${releaseDate} in vulnerabilities.json.` + ); this.cli.ok(`Updated the release date in vulnerabilities.json: ${releaseDate}`); return [vulnerabilitiesJSONPath]; @@ -95,7 +106,7 @@ export default class UpdateSecurityRelease extends SecurityRelease { const req = new Request(credentials); // checkout on the next-security-release branch - checkoutOnSecurityReleaseBranch(this.cli, this.repository); + await checkoutOnSecurityReleaseBranch(this.cli, this.repository); // get h1 report const { data: report } = await req.getReport(reportId); @@ -104,18 +115,23 @@ export default class UpdateSecurityRelease extends SecurityRelease { const vulnerabilitiesJSONPath = this.getVulnerabilitiesJSONPath(); const content = this.readVulnerabilitiesJSON(vulnerabilitiesJSONPath); content.reports.push(entry); - fs.writeFileSync(vulnerabilitiesJSONPath, JSON.stringify(content, null, 2)); + await writeSecurityFile( + this.cli, + vulnerabilitiesJSONPath, + JSON.stringify(content, null, 2), + `This appends HackerOne report ${entry.id} to vulnerabilities.json.` + ); this.cli.ok(`Updated vulnerabilities.json with the report: ${entry.id}`); const commitMessage = `chore: added report ${entry.id} to vulnerabilities.json`; - commitAndPushVulnerabilitiesJSON(vulnerabilitiesJSONPath, + await commitAndPushVulnerabilitiesJSON(vulnerabilitiesJSONPath, commitMessage, { cli: this.cli, repository: this.repository }); this.cli.ok('Done!'); } - removeReport(reportId) { + async removeReport(reportId) { const { cli } = this; // checkout on the next-security-release branch - checkoutOnSecurityReleaseBranch(cli, this.repository); + await checkoutOnSecurityReleaseBranch(cli, this.repository); const vulnerabilitiesJSONPath = this.getVulnerabilitiesJSONPath(); const content = this.readVulnerabilitiesJSON(vulnerabilitiesJSONPath); const found = content.reports.some((report) => report.id === reportId); @@ -124,11 +140,16 @@ export default class UpdateSecurityRelease extends SecurityRelease { process.exit(1); } content.reports = content.reports.filter((report) => report.id !== reportId); - fs.writeFileSync(vulnerabilitiesJSONPath, JSON.stringify(content, null, 2)); + await writeSecurityFile( + this.cli, + vulnerabilitiesJSONPath, + JSON.stringify(content, null, 2), + `This removes HackerOne report ${reportId} from vulnerabilities.json.` + ); this.cli.ok(`Updated vulnerabilities.json with the report: ${reportId}`); const commitMessage = `chore: remove report ${reportId} from vulnerabilities.json`; - commitAndPushVulnerabilitiesJSON(vulnerabilitiesJSONPath, + await commitAndPushVulnerabilitiesJSON(vulnerabilitiesJSONPath, commitMessage, { cli, repository: this.repository }); cli.ok('Done!'); } @@ -170,6 +191,11 @@ export default class UpdateSecurityRelease extends SecurityRelease { async updateHackonerReportCve(req, report) { const { id, cveIds } = report; + await confirmSecurityStep( + this.cli, + `update HackerOne report \`${id}\` with CVEs ${cveIds}`, + 'This writes the assigned CVE IDs back to the HackerOne report.' + ); this.cli.startSpinner(`Updating report ${id} with CVEs ${cveIds}..`); const body = { data: { @@ -252,6 +278,11 @@ Summary: ${summary}\n`, } } }; + await confirmSecurityStep( + this.cli, + `request CVE for HackerOne report \`${report.id}\``, + 'This submits a CVE request to HackerOne for the selected report.' + ); const response = await req.requestCVE(programId, body); if (response.errors) { this.cli.error(`Error requesting CVE for report ${id}`); @@ -261,7 +292,7 @@ Summary: ${summary}\n`, const { cve_identifier } = response.data.attributes; report.cveIds = [cve_identifier]; report.patchedVersions = patchedVersions; - this.updateVulnerabilitiesJSON(content); + await this.updateVulnerabilitiesJSON(content); await this.updateHackonerReportCve(req, report); } } From 4f8376e3d981aabf7cd33fe2403b66d54ecf2e2e Mon Sep 17 00:00:00 2001 From: RafaelGSS Date: Wed, 10 Jun 2026 18:11:42 -0300 Subject: [PATCH 2/2] fixup! feat: add confirmation steps to any write operation --- lib/security-release/security-release.js | 20 +++++--------------- 1 file changed, 5 insertions(+), 15 deletions(-) diff --git a/lib/security-release/security-release.js b/lib/security-release/security-release.js index 7bb2678a..fbe29d1f 100644 --- a/lib/security-release/security-release.js +++ b/lib/security-release/security-release.js @@ -35,16 +35,14 @@ function formatCommand(command, args) { } export async function confirmSecurityStep(cli, action, detail) { - const lines = [`Allow action: ${action}?`]; + let message = `Allow action: ${action}?`; if (detail) { - lines.push('', detail); + message += `\n\n${detail}`; } - const message = lines.join('\n'); const allowed = await cli.prompt(message, { defaultAnswer: false }); if (!allowed) { - cli.info(`Aborted: ${action}.`); - process.exit(0); + throw new Error(`Aborted: ${action}.`); } } @@ -97,18 +95,10 @@ export async function commitAndPushVulnerabilitiesJSON( if (Array.isArray(filePath)) { for (const currentPath of filePath) { - await runSecurityGitCommand( - cli, - ['add', currentPath], - `This stages ${currentPath} for the security release commit.` - ); + runSync('git', ['add', currentPath]); } } else { - await runSecurityGitCommand( - cli, - ['add', filePath], - `This stages ${filePath} for the security release commit.` - ); + runSync('git', ['add', filePath]); } const staged = runSync('git', ['diff', '--name-only', '--cached']).trim();