diff --git a/components/git/security.js b/components/git/security.js new file mode 100644 index 00000000..d11e5bf5 --- /dev/null +++ b/components/git/security.js @@ -0,0 +1,35 @@ +import CLI from '../../lib/cli.js'; +import SecurityReleaseSteward from '../../lib/prepare_security.js'; + +export const command = 'security [options]'; +export const describe = 'Manage an in-progress security release or start a new one.'; + +const securityOptions = { + start: { + describe: 'Start security release process', + type: 'boolean' + } +}; + +let yargsInstance; + +export function builder(yargs) { + yargsInstance = yargs; + return yargs.options(securityOptions).example( + 'git node security --start', + 'Prepare a security release of Node.js'); +} + +export function handler(argv) { + if (argv.start) { + return startSecurityRelease(argv); + } + yargsInstance.showHelp(); +} + +async function startSecurityRelease(argv) { + const logStream = process.stdout.isTTY ? process.stdout : process.stderr; + const cli = new CLI(logStream); + const release = new SecurityReleaseSteward(cli); + return release.start(); +} diff --git a/docs/git-node.md b/docs/git-node.md index 2d0f3bac..26d2d482 100644 --- a/docs/git-node.md +++ b/docs/git-node.md @@ -427,6 +427,32 @@ $ git node vote \ ============================================================================== ``` +## `git node security` + +Manage or starts a security release process. + + + +### Prerequisites + +It's necessary to set up `.ncurc` with HackerOne keys: + +```console +$ ncu-config --global set h1_token $H1_TOKEN +$ ncu-config --global set h1_username $H1_TOKEN +``` + +- `h1_token`: HackerOne Organization API Token, preferable with read-only + access. +- `h1_username`: HackerOne API Token username. + +### `git node security --start` + +This command creates the Next Security Issue in Node.js private repository +following the [Security Release Process][] document. +It will retrieve all the triaged HackerOne reports and add them to the list +with the affected release line. + ## `git node status` Return status and information about the current git-node land session. Shows the following information: @@ -488,3 +514,4 @@ $ git node wpt url --commit=43feb7f612fe9160639e09a47933a29834904d69 ``` [node.js abi version registry]: https://github.com/nodejs/node/blob/main/doc/abi_version_registry.json +[Security Release Process]: https://github.com/nodejs/node/blob/main/doc/contributing/security-release-process.md diff --git a/lib/auth.js b/lib/auth.js index c13ef196..a0cf5b4a 100644 --- a/lib/auth.js +++ b/lib/auth.js @@ -107,6 +107,20 @@ async function auth( check(username, jenkins_token); result.jenkins = encode(username, jenkins_token); } + + if (options.h1) { + const { h1_username, h1_token } = getMergedConfig(); + if (!h1_username || !h1_token) { + errorExit( + 'Get your HackerOne API token in ' + + 'https://docs.hackerone.com/organizations/api-tokens.html ' + + 'and run the following command to add it to your ncu config: ' + + 'ncu-config --global set h1_token TOKEN or ' + + 'ncu-config --global set h1_username USERNAME' + ); + }; + result.h1 = encode(h1_username, h1_token); + } return result; } diff --git a/lib/github/templates/next-security-release.md b/lib/github/templates/next-security-release.md new file mode 100644 index 00000000..ae5d6176 --- /dev/null +++ b/lib/github/templates/next-security-release.md @@ -0,0 +1,97 @@ +## Planning + +* [X] Open an [issue](https://github.com/nodejs-private/node-private) titled + `Next Security Release`, and put this checklist in the description. + +* [ ] Get agreement on the list of vulnerabilities to be addressed: +%REPORTS% + +* [ ] PR release announcements in [private](https://github.com/nodejs-private/nodejs.org-private): + * [ ] pre-release: %PRE_RELEASE_PRIV% + * [ ] post-release: %POS_RELEASE_PRIV% + * List vulnerabilities in order of descending severity + * Ask the HackerOne reporter if they would like to be credited on the + security release blog page + +* [ ] Get agreement on the planned date for the release: %RELEASE_DATE% + +* [ ] Get release team volunteers for all affected lines: +%AFFECTED_LINES% + +## Announcement (one week in advance of the planned release) + +* [ ] Verify that GitHub Actions are working as normal: . + +* [ ] Check that all vulnerabilities are ready for release integration: + * PRs against all affected release lines or cherry-pick clean + * Approved + * (optional) Approved by the reporter + * Build and send the binary to the reporter according to its architecture + and ask for a review. This step is important to avoid insufficient fixes + between Security Releases. + * Have CVEs + * Make sure that dependent libraries have CVEs for their issues. We should + only create CVEs for vulnerabilities in Node.js itself. This is to avoid + having duplicate CVEs for the same vulnerability. + * Described in the pre/post announcements + +* [ ] Pre-release announcement to nodejs.org blog: TBD + (Re-PR the pre-approved branch from nodejs-private/nodejs.org-private to + nodejs/nodejs.org) + +* [ ] Pre-release announcement [email](https://groups.google.com/forum/#!forum/nodejs-sec): TBD + * Subject: `Node.js security updates for all active release lines, Month Year` + +* [ ] CC `oss-security@lists.openwall.com` on pre-release + * [ ] Forward the email you receive to `oss-security@lists.openwall.com`. + +* [ ] Create a new issue in [nodejs/tweet](https://github.com/nodejs/tweet/issues) + +* [ ] Request releaser(s) to start integrating the PRs to be released. + +* [ ] Notify [docker-node](https://github.com/nodejs/docker-node/issues) of upcoming security release date: TBD + +* [ ] Notify build-wg of upcoming security release date by opening an issue + in [nodejs/build](https://github.com/nodejs/build/issues) to request WG members are available to fix any CI issues: TBD + +## Release day + +* [ ] [Lock CI](https://github.com/nodejs/build/blob/HEAD/doc/jenkins-guide.md#before-the-release) + +* [ ] The releaser(s) run the release process to completion. + +* [ ] [Unlock CI](https://github.com/nodejs/build/blob/HEAD/doc/jenkins-guide.md#after-the-release) + +* [ ] Post-release announcement to Nodejs.org blog: https://github.com/nodejs/nodejs.org/pull/5447 + * (Re-PR the pre-approved branch from nodejs-private/nodejs.org-private to + nodejs/nodejs.org) + +* [ ] Post-release announcement in reply email: TBD + +* [ ] Create a new issue in nodejs/tweet + +* [ ] Comment in [docker-node][] issue that release is ready for integration. + The docker-node team will build and release docker image updates. + +* [ ] For every H1 report resolved: + * Close as Resolved + * Request Disclosure + * Request publication of H1 CVE requests + * (Check that the "Version Fixed" field in the CVE is correct, and provide + links to the release blogs in the "Public Reference" section) + +* [ ] PR machine-readable JSON descriptions of the vulnerabilities to the + [core](https://github.com/nodejs/security-wg/tree/HEAD/vuln/core) + vulnerability DB. https://github.com/nodejs/security-wg/pull/1029 + * For each vulnerability add a `#.json` file, one can copy an existing + [json](https://github.com/nodejs/security-wg/blob/0d82062d917cb9ddab88f910559469b2b13812bf/vuln/core/78.json) + file, and increment the latest created file number and use that as the name + of the new file to be added. For example, `79.json`. + +* [ ] Close this issue + +* [ ] Make sure the PRs for the vulnerabilities are closed. + +* [ ] PR in that you stewarded the release in + [Security release stewards](https://github.com/nodejs/node/blob/HEAD/doc/contributing/security-release-process.md#security-release-stewards). + If necessary add the next rotation of the steward rotation. diff --git a/lib/prepare_security.js b/lib/prepare_security.js new file mode 100644 index 00000000..68a7d942 --- /dev/null +++ b/lib/prepare_security.js @@ -0,0 +1,117 @@ +import nv from '@pkgjs/nv'; +import auth from './auth.js'; +import Request from './request.js'; +import fs from 'node:fs'; + +export default class SecurityReleaseSteward { + constructor(cli) { + this.cli = cli; + } + + async start() { + const { cli } = this; + const credentials = await auth({ + github: true, + h1: true + }); + + const req = new Request(credentials); + const create = await cli.prompt( + 'Create the Next Security Release issue?', + { defaultAnswer: true }); + if (create) { + const issue = new SecurityReleaseIssue(req); + const content = await issue.buildIssue(cli); + const data = await req.createIssue('Next Security Release', content, { + owner: 'nodejs-private', + repo: 'node-private' + }); + if (data.html_url) { + cli.ok('Created: ' + data.html_url); + } else { + cli.error(data); + } + } + } +} + +class SecurityReleaseIssue { + constructor(req) { + this.req = req; + this.content = ''; + this.title = 'Next Security Release'; + this.affectedLines = {}; + } + + getSecurityIssueTemplate() { + return fs.readFileSync( + new URL( + './github/templates/next-security-release.md', + import.meta.url + ), + 'utf-8' + ); + } + + async buildIssue(cli) { + this.content = this.getSecurityIssueTemplate(); + cli.info('Getting triaged H1 reports...'); + const reports = await this.req.getTriagedReports(); + await this.fillReports(cli, reports); + + this.fillAffectedLines(Object.keys(this.affectedLines)); + + const target = await cli.prompt('Enter target date in YYYY-MM-DD format:', { + questionType: 'input', + defaultAnswer: 'TBD' + }); + this.fillTargetDate(target); + + return this.content; + } + + async fillReports(cli, reports) { + const supportedVersions = (await nv('supported')) + .map((v) => v.versionName + '.x') + .join(','); + + let reportsContent = ''; + for (const report of reports.data) { + const { id, attributes: { title }, relationships: { severity } } = report; + const reportLevel = severity.data.attributes.rating; + cli.separator(); + cli.info(`Report: ${id} - ${title} (${reportLevel})`); + const include = await cli.prompt( + 'Would you like to include this report to the next security release?', + { defaultAnswer: true }); + if (!include) { + continue; + } + + reportsContent += + ` * **[${id}](https://hackerone.com/bugs?subject=nodejs&report_id=${id}) - ${title} (TBD) - (${reportLevel})**\n`; + const versions = await cli.prompt('Which active release lines this report affects?', { + questionType: 'input', + defaultAnswer: supportedVersions + }); + for (const v of versions.split(',')) { + if (!this.affectedLines[v]) this.affectedLines[v] = true; + reportsContent += ` * ${v} - TBD\n`; + } + } + this.content = this.content.replace('%REPORTS%', reportsContent); + } + + fillAffectedLines(affectedLines) { + let affected = ''; + for (const line of affectedLines) { + affected += ` * ${line} - TBD\n`; + } + this.content = + this.content.replace('%AFFECTED_LINES%', affected); + } + + fillTargetDate(date) { + this.content = this.content.replace('%RELEASE_DATE%', date); + } +} diff --git a/lib/request.js b/lib/request.js index e15d3904..3e5f02d5 100644 --- a/lib/request.js +++ b/lib/request.js @@ -60,6 +60,23 @@ export default class Request { } } + async createIssue(title, body, { owner, repo }) { + const url = `https://api.github.com/repos/${owner}/${repo}/issues`; + const options = { + method: 'POST', + headers: { + Authorization: `Basic ${this.credentials.github}`, + 'User-Agent': 'node-core-utils', + Accept: 'application/vnd.github+json' + }, + body: JSON.stringify({ + title, + body + }) + }; + return this.json(url, options); + } + async gql(name, variables, path) { const query = this.loadQuery(name); if (path) { @@ -83,6 +100,19 @@ export default class Request { }; } + async getTriagedReports() { + const url = 'https://api.hackerone.com/v1/reports?filter[program][]=nodejs&filter[state][]=triaged'; + const options = { + method: 'GET', + headers: { + Authorization: `Basic ${this.credentials.h1}`, + 'User-Agent': 'node-core-utils', + Accept: 'application/json' + } + }; + return this.json(url, options); + } + // This is for github v4 API queries, for other types of queries // use .text or .json async query(query, variables) { diff --git a/package.json b/package.json index afb6fd79..e1259e95 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,7 @@ "branch-diff": "^2.1.4", "chalk": "^5.3.0", "changelog-maker": "^3.2.4", + "@pkgjs/nv": "^0.2.1", "cheerio": "^1.0.0-rc.12", "clipboardy": "^3.0.0", "core-validate-commit": "^4.0.0",