Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 14 additions & 5 deletions docs/git-node.md
Original file line number Diff line number Diff line change
Expand Up @@ -577,11 +577,13 @@ $ 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.
Every `git node security` action asks for permission before each step that
changes remote state, such as creating issues or pull requests, or updating
HackerOne reports. Local changes run without confirmation: files are written
and staged, and the commit message is pre-filled in `.git/MERGE_MSG`, so it is
picked up by `git commit` and editors such as VS Code. Apart from `--start`,
which pushes its draft through the GitHub API, the CLI never commits or pushes;
review the staged diff, then commit and push `next-security-release` yourself.

<a id="git-node-security-prerequisites"></a>

Expand Down Expand Up @@ -609,6 +611,13 @@ is configured with, not your local git credentials. That token needs:
pull request), plus **Pull requests: Read** on `nodejs-private/node-private`
to look up patch authors.

`git node security --request-cve` lists the reports without a CVE and asks for
a single confirmation. It then requests a CVE for each report and adds the CVE
ID to the HackerOne report right away, without further prompts.
`vulnerabilities.json` is updated and staged last, so if that step fails or the
run is interrupted, `git node security --sync` can still fetch the CVE IDs from
HackerOne.

#### Optional: reserve CVEs through the OpenJS Foundation CNA

By default `--request-cve` reserves CVE ids through HackerOne, which acts as
Expand Down
6 changes: 3 additions & 3 deletions lib/prepare_security.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import {
import {
NEXT_SECURITY_RELEASE_BRANCH,
NEXT_SECURITY_RELEASE_FOLDER,
commitAndPushVulnerabilitiesJSON,
stageSecurityReleaseChanges,
validateDate,
promptDependencies,
getSupportedVersions,
Expand Down Expand Up @@ -195,11 +195,11 @@ export default class PrepareSecurityRelease extends SecurityRelease {
vulnerabilityJSON.releaseDate}?`,
{ defaultAnswer: true });
if (updateFolder) {
await this.updateReleaseFolder(
this.updateReleaseFolder(
vulnerabilityJSON.releaseDate.replaceAll('/', '-')
);
const securityReleaseFolder = path.join(process.cwd(), 'security-release');
await commitAndPushVulnerabilitiesJSON(
await stageSecurityReleaseChanges(
securityReleaseFolder,
'chore: change next-security-release folder',
{ cli: this.cli, repository: this.repository }
Expand Down
15 changes: 5 additions & 10 deletions lib/security-announcement.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import fs from 'node:fs';
import {
NEXT_SECURITY_RELEASE_REPOSITORY,
checkoutOnSecurityReleaseBranch,
getVulnerabilitiesJSON,
getVulnerabilitiesJSONPath,
validateDate,
formatDateToYYYYMMDD,
commitAndPushVulnerabilitiesJSON,
createIssue,
writeSecurityFile
stageSecurityReleaseChanges,
createIssue
} from './security-release/security-release.js';
import auth from './auth.js';
import Request from './request.js';
Expand Down Expand Up @@ -52,14 +52,9 @@ export default class SecurityAnnouncement {
content.dockerIssue = dockerIssue;

const vulnerabilitiesJSONPath = getVulnerabilitiesJSONPath();
await writeSecurityFile(
cli,
vulnerabilitiesJSONPath,
JSON.stringify(content, null, 2),
'This records the build and docker tracking issue links in vulnerabilities.json.'
);
fs.writeFileSync(vulnerabilitiesJSONPath, JSON.stringify(content, null, 2));
const commitMessage = 'chore: add build and docker issue link';
await commitAndPushVulnerabilitiesJSON([vulnerabilitiesJSONPath],
await stageSecurityReleaseChanges([vulnerabilitiesJSONPath],
commitMessage, { cli: this.cli, repository: this.repository });

this.cli.ok('Added docker and build issue in vulnerabilities.json');
Expand Down
101 changes: 30 additions & 71 deletions lib/security-release/security-release.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,6 @@ export const PLACEHOLDERS = {
downloads: '%DOWNLOADS%'
};

function formatCommand(command, args) {
return [command, ...args].join(' ');
}

export async function confirmSecurityStep(cli, action, detail) {
let message = `Allow action: ${action}?`;
if (detail) {
Expand All @@ -48,17 +44,6 @@ export async function confirmSecurityStep(cli, action, detail) {
}
}

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;
Expand Down Expand Up @@ -93,24 +78,33 @@ export async function checkoutOnSecurityReleaseBranch(cli, repository) {
} else {
args = ['checkout', '-b', NEXT_SECURITY_RELEASE_BRANCH];
}
await runSecurityGitCommand(
cli,
args,
`This switches to ${NEXT_SECURITY_RELEASE_BRANCH} without resetting an existing branch.`
);
runSync('git', args);
cli.ok(`Checkout on branch: ${NEXT_SECURITY_RELEASE_BRANCH} `);
}
}

export async function commitAndPushVulnerabilitiesJSON(
function writeCommitMessage(commitMessage) {
const messagePath = runSync('git', ['rev-parse', '--git-path', 'MERGE_MSG']).trim();
let existing = '';
try {
existing = fs.readFileSync(messagePath, 'utf8').trim();
} catch (error) {
if (error.code !== 'ENOENT') throw error;
}
if (existing.split('\n').includes(commitMessage)) return;
fs.writeFileSync(messagePath,
existing ? `${existing}\n${commitMessage}\n` : `${commitMessage}\n`);
}

export async function stageSecurityReleaseChanges(
filePath,
commitMessage,
{ cli, repository }
) {
await checkRemote(cli, repository);

const files = (Array.isArray(filePath) ? filePath : [filePath])
.map((file) => path.resolve(file));
const filePaths = Array.isArray(filePath) ? filePath : [filePath];
const files = filePaths.map((file) => path.resolve(file));
const stagedFiles = runSync('git', ['diff', '--cached', '--name-only', '-z'])
.split('\0').filter(Boolean);
const unrelated = stagedFiles.filter((file) => {
Expand All @@ -125,47 +119,22 @@ export async function commitAndPushVulnerabilitiesJSON(
);
}

if (Array.isArray(filePath)) {
for (const currentPath of filePath) {
runSync('git', ['add', currentPath]);
}
} else {
runSync('git', ['add', filePath]);
}
runSync('git', ['add', '--', ...filePaths]);

const staged = runSync('git', ['diff', '--name-only', '--cached']).trim();
if (!staged) {
cli.ok('No changes to commit');
cli.ok('No changes to stage');
return;
}

await runSecurityGitCommand(
cli,
['commit', '-m', commitMessage],
`This creates a local commit with message: ${commitMessage}`
);

try {
await runSecurityGitCommand(
cli,
['push', '-u', 'origin', NEXT_SECURITY_RELEASE_BRANCH],
`This pushes the security release branch to origin/${NEXT_SECURITY_RELEASE_BRANCH}.`
);
} catch {
cli.warn('Rebasing...');
// try to pull rebase and push again
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}`);
writeCommitMessage(commitMessage);
cli.ok(`Staged changes with commit message: ${commitMessage}`);
cli.log(runSync('git', ['diff', '--cached', '--stat']).trimEnd());
cli.info(`Next steps:
- git diff --cached # review the staged changes
- git commit # edit the pre-filled message (or --no-edit to keep it)
- git push -u origin ${NEXT_SECURITY_RELEASE_BRANCH}
To discard, run \`git reset\` (it also clears the pre-filled message).`);
}

export async function getSupportedVersions() {
Expand Down Expand Up @@ -398,15 +367,10 @@ export class SecurityRelease {
NEXT_SECURITY_RELEASE_FOLDER, 'vulnerabilities.json');
}

async updateReleaseFolder(releaseDate) {
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;
}
Expand All @@ -415,13 +379,8 @@ export class SecurityRelease {
try {
const vulnerabilitiesJSONPath = this.getVulnerabilitiesJSONPath();
this.cli.startSpinner(`Updating vulnerabilities.json from ${vulnerabilitiesJSONPath}...`);
await writeSecurityFile(
this.cli,
vulnerabilitiesJSONPath,
JSON.stringify(content, null, 2),
'This updates vulnerabilities.json with the latest security release data.'
);
await commitAndPushVulnerabilitiesJSON(vulnerabilitiesJSONPath,
fs.writeFileSync(vulnerabilitiesJSONPath, JSON.stringify(content, null, 2));
await stageSecurityReleaseChanges(vulnerabilitiesJSONPath,
'chore: updated vulnerabilities.json',
{ cli: this.cli, repository: this.repository });
this.cli.stopSpinner(`Done updating vulnerabilities.json from ${vulnerabilitiesJSONPath}`);
Expand Down
33 changes: 6 additions & 27 deletions lib/security_blog.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,10 @@ import {
checkoutOnSecurityReleaseBranch,
validateDate,
SecurityRelease,
commitAndPushVulnerabilitiesJSON,
stageSecurityReleaseChanges,
getAffectedVersionLines,
getDependencyUpdates,
getHighestSeverityAnnouncement,
writeSecurityFile,
} from './security-release/security-release.js';
import auth from './auth.js';
import Request from './request.js';
Expand Down Expand Up @@ -69,12 +68,7 @@ export default class SecurityBlog extends SecurityRelease {
link,
type: 'warning'
});
await writeSecurityFile(
cli,
file,
preRelease,
'This writes the pre-release vulnerability announcement post.'
);
fs.writeFileSync(file, preRelease);

cli.ok(`Announcement file created and banner has been updated. Folder: ${nodejsOrgFolder}`);
await this.updateAnnouncementLink(link);
Expand All @@ -89,14 +83,9 @@ export default class SecurityBlog extends SecurityRelease {
}

content.announcement = link;
await writeSecurityFile(
this.cli,
vulnerabilitiesJSONPath,
JSON.stringify(content, null, 2),
'This writes the announcement link to vulnerabilities.json.'
);
fs.writeFileSync(vulnerabilitiesJSONPath, JSON.stringify(content, null, 2));
const commitMessage = 'chore: add announcement link';
await commitAndPushVulnerabilitiesJSON([vulnerabilitiesJSONPath],
await stageSecurityReleaseChanges([vulnerabilitiesJSONPath],
commitMessage, { cli: this.cli, repository: this.repository });

this.cli.ok('Updated the announcement link in vulnerabilities.json');
Expand Down Expand Up @@ -163,12 +152,7 @@ export default class SecurityBlog extends SecurityRelease {
text: `${capitalizedMonth} Security Release is available`
});

await writeSecurityFile(
cli,
preReleasePath,
updatedContent,
'This writes the post-release vulnerability announcement post.'
);
fs.writeFileSync(preReleasePath, updatedContent);
cli.ok(`Announcement file and banner has been updated. Folder: ${nodejsOrgFolder}`);
}

Expand Down Expand Up @@ -199,12 +183,7 @@ export default class SecurityBlog extends SecurityRelease {
link: content.link ?? currentValue.link,
type: content.type ?? currentValue.type
};
await writeSecurityFile(
this.cli,
siteJsonPath,
JSON.stringify(siteJson, null, 2) + '\n',
'This writes the updated security release website banner.'
);
fs.writeFileSync(siteJsonPath, JSON.stringify(siteJson, null, 2) + '\n');
}

formatReleaseDate(releaseDate) {
Expand Down
Loading
Loading