Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add git node security --cleanup #833

Merged
merged 3 commits into from
Aug 14, 2024
Merged
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
17 changes: 17 additions & 0 deletions components/git/security.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@ const securityOptions = {
'post-release': {
describe: 'Create the post-release announcement',
type: 'boolean'
},
cleanup: {
describe: 'cleanup the security release.',
type: 'boolean'
}
};

Expand Down Expand Up @@ -81,6 +85,9 @@ export function builder(yargs) {
).example(
'git node security --post-release',
'Create the post-release announcement on the Nodejs.org repo'
).example(
'git node security --cleanup',
'Cleanup the security release. Merge the PR and close H1 reports'
);
}

Expand Down Expand Up @@ -112,6 +119,9 @@ export function handler(argv) {
if (argv['post-release']) {
return createPostRelease(argv);
}
if (argv.cleanup) {
return cleanupSecurityRelease(argv);
}
yargsInstance.showHelp();
}

Expand Down Expand Up @@ -167,6 +177,13 @@ async function startSecurityRelease() {
return release.start();
}

async function cleanupSecurityRelease() {
const logStream = process.stdout.isTTY ? process.stdout : process.stderr;
const cli = new CLI(logStream);
const release = new PrepareSecurityRelease(cli);
return release.cleanup();
}

async function syncSecurityRelease(argv) {
const logStream = process.stdout.isTTY ? process.stdout : process.stderr;
const cli = new CLI(logStream);
Expand Down
69 changes: 60 additions & 9 deletions lib/prepare_security.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,18 @@ import Request from './request.js';
import {
NEXT_SECURITY_RELEASE_BRANCH,
NEXT_SECURITY_RELEASE_FOLDER,
NEXT_SECURITY_RELEASE_REPOSITORY,
checkoutOnSecurityReleaseBranch,
commitAndPushVulnerabilitiesJSON,
validateDate,
promptDependencies,
getSupportedVersions,
pickReport
pickReport,
SecurityRelease
} from './security-release/security-release.js';
import _ from 'lodash';

export default class PrepareSecurityRelease {
repository = NEXT_SECURITY_RELEASE_REPOSITORY;
export default class PrepareSecurityRelease extends SecurityRelease {
title = 'Next Security Release';
constructor(cli) {
this.cli = cli;
}

async start() {
const credentials = await auth({
Expand All @@ -44,6 +40,27 @@ export default class PrepareSecurityRelease {
this.cli.ok('Done!');
}

async cleanup() {
const credentials = await auth({
github: true,
h1: true
});

this.req = new Request(credentials);
const vulnerabilityJSON = this.readVulnerabilitiesJSON();
this.cli.info('Closing and request disclosure to HackerOne reports');
await this.closeAndRequestDisclosure(vulnerabilityJSON.reports);

this.cli.info('Closing pull requests');
// For now, close the ones with vN.x label
await this.closePRWithLabel(this.getAffectedVersions(vulnerabilityJSON));
this.cli.info(`Merge pull request with:
- git checkout main
- git merge --squash ${NEXT_SECURITY_RELEASE_BRANCH}
- git push origin main`);
this.cli.ok('Done!');
}

async startVulnerabilitiesJSONCreation(releaseDate, content) {
// checkout on the next-security-release branch
checkoutOnSecurityReleaseBranch(this.cli, this.repository);
Expand Down Expand Up @@ -163,9 +180,9 @@ export default class PrepareSecurityRelease {

const folderPath = path.join(process.cwd(), NEXT_SECURITY_RELEASE_FOLDER);
try {
await fs.accessSync(folderPath);
fs.accessSync(folderPath);
} catch (error) {
await fs.mkdirSync(folderPath, { recursive: true });
fs.mkdirSync(folderPath, { recursive: true });
}

const fullPath = path.join(folderPath, 'vulnerabilities.json');
Expand Down Expand Up @@ -254,4 +271,38 @@ export default class PrepareSecurityRelease {
}
return deps;
}

async closeAndRequestDisclosure(jsonReports) {
this.cli.startSpinner('Closing HackerOne reports');
for (const report of jsonReports) {
this.cli.updateSpinner(`Closing report ${report.id}...`);
await this.req.updateReportState(
report.id,
'resolved',
'Closing as resolved'
);

this.cli.updateSpinner(`Requesting disclosure to report ${report.id}...`);
await this.req.requestDisclosure(report.id);
}
this.cli.stopSpinner('Done closing H1 Reports and requesting disclosure');
}

async closePRWithLabel(labels) {
if (typeof labels === 'string') {
labels = [labels];
}

const url = 'https://github.com/nodejs-private/node-private/pulls';
this.cli.startSpinner('Closing GitHub Pull Requests...');
// At this point, GitHub does not provide filters through their REST API
const prs = this.req.getPullRequest(url);
for (const pr of prs) {
if (pr.labels.some((l) => labels.includes(l))) {
this.cli.updateSpinner(`Closing Pull Request: ${pr.id}`);
await this.req.closePullRequest(pr.id);
}
}
this.cli.startSpinner('Closed GitHub Pull Requests.');
}
}
59 changes: 59 additions & 0 deletions lib/request.js
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,22 @@ export default class Request {
return this.json(url, options);
}

async closePullRequest({ owner, repo }) {
const url = `https://api.github.com/repos/${owner}/${repo}/pulls`;
const options = {
method: 'POST',
headers: {
Authorization: `Basic ${this.credentials.github}`,
'User-Agent': 'node-core-utils',
Accept: 'application/vnd.github+json'
},
body: JSON.stringify({
state: 'closed'
})
};
return this.json(url, options);
}

async gql(name, variables, path) {
const query = this.loadQuery(name);
if (path) {
Expand Down Expand Up @@ -201,6 +217,49 @@ export default class Request {
return this.json(url, options);
}

async updateReportState(reportId, state, message) {
const url = `https://api.hackerone.com/v1/reports/${reportId}/state_changes`;
const options = {
method: 'POST',
headers: {
Authorization: `Basic ${this.credentials.h1}`,
'User-Agent': 'node-core-utils',
Accept: 'application/json'
},
body: JSON.stringify({
data: {
type: 'state-change',
attributes: {
message,
state
}
}
})
};
return this.json(url, options);
}

async requestDisclosure(reportId) {
const url = `https://api.hackerone.com/v1/reports/${reportId}/disclosure_requests`;
const options = {
method: 'POST',
headers: {
Authorization: `Basic ${this.credentials.h1}`,
'User-Agent': 'node-core-utils',
Accept: 'application/json'
},
body: JSON.stringify({
data: {
attributes: {
// default to limited version
substate: 'no-content'
}
}
})
};
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) {
Expand Down
53 changes: 53 additions & 0 deletions lib/security-release/security-release.js
Original file line number Diff line number Diff line change
Expand Up @@ -210,3 +210,56 @@ export async function pickReport(report, { cli, req }) {
reporter: reporter.data.attributes.username
};
}

export class SecurityRelease {
constructor(cli, repository = NEXT_SECURITY_RELEASE_REPOSITORY) {
this.cli = cli;
this.repository = repository;
}

readVulnerabilitiesJSON(vulnerabilitiesJSONPath = this.getVulnerabilitiesJSONPath()) {
const exists = fs.existsSync(vulnerabilitiesJSONPath);

if (!exists) {
this.cli.error(`The file vulnerabilities.json does not exist at ${vulnerabilitiesJSONPath}`);
process.exit(1);
}

return JSON.parse(fs.readFileSync(vulnerabilitiesJSONPath, 'utf8'));
}

getVulnerabilitiesJSONPath() {
return path.join(process.cwd(),
NEXT_SECURITY_RELEASE_FOLDER, 'vulnerabilities.json');
}

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,
'chore: updated vulnerabilities.json',
{ cli: this.cli, repository: this.repository });
this.cli.stopSpinner(`Done updating vulnerabilities.json from ${vulnerabilitiesJSONPath}`);
} catch (error) {
this.cli.error('Error updating vulnerabilities.json');
this.cli.error(error);
}
}

getAffectedVersions(content) {
const affectedVersions = new Set();
for (const report of Object.values(content.reports)) {
for (const affectedVersion of report.affectedVersions) {
affectedVersions.add(affectedVersion);
}
}
const parseToNumber = str => +(str.match(/[\d.]+/g)[0]);
return Array.from(affectedVersions)
.sort((a, b) => {
return parseToNumber(a) > parseToNumber(b) ? -1 : 1;
})
.join(', ');
}
}
46 changes: 4 additions & 42 deletions lib/security_blog.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,17 @@ import _ from 'lodash';
import nv from '@pkgjs/nv';
import {
PLACEHOLDERS,
getVulnerabilitiesJSON,
checkoutOnSecurityReleaseBranch,
NEXT_SECURITY_RELEASE_REPOSITORY,
validateDate,
commitAndPushVulnerabilitiesJSON,
NEXT_SECURITY_RELEASE_FOLDER
SecurityRelease
} from './security-release/security-release.js';
import auth from './auth.js';
import Request from './request.js';

const kChanged = Symbol('changed');

export default class SecurityBlog {
repository = NEXT_SECURITY_RELEASE_REPOSITORY;
export default class SecurityBlog extends SecurityRelease {
req;
constructor(cli) {
this.cli = cli;
}

async createPreRelease() {
const { cli } = this;
Expand All @@ -30,7 +23,7 @@ export default class SecurityBlog {
checkoutOnSecurityReleaseBranch(cli, this.repository);

// read vulnerabilities JSON file
const content = getVulnerabilitiesJSON(cli);
const content = this.readVulnerabilitiesJSON();
// validate the release date read from vulnerabilities JSON
if (!content.releaseDate) {
cli.error('Release date is not set in vulnerabilities.json,' +
Expand Down Expand Up @@ -72,7 +65,7 @@ export default class SecurityBlog {
checkoutOnSecurityReleaseBranch(cli, this.repository);

// read vulnerabilities JSON file
const content = getVulnerabilitiesJSON(cli);
const content = this.readVulnerabilitiesJSON(cli);
if (!content.releaseDate) {
cli.error('Release date is not set in vulnerabilities.json,' +
' run `git node security --update-date=YYYY/MM/DD` to set the release date.');
Expand Down Expand Up @@ -113,22 +106,6 @@ export default class SecurityBlog {
this.updateVulnerabilitiesJSON(content);
}

updateVulnerabilitiesJSON(content) {
try {
this.cli.info('Updating vulnerabilities.json');
const vulnerabilitiesJSONPath = path.join(process.cwd(),
NEXT_SECURITY_RELEASE_FOLDER, 'vulnerabilities.json');
fs.writeFileSync(vulnerabilitiesJSONPath, JSON.stringify(content, null, 2));
const commitMessage = 'chore: updated vulnerabilities.json';
commitAndPushVulnerabilitiesJSON(vulnerabilitiesJSONPath,
commitMessage,
{ cli: this.cli, repository: this.repository });
} catch (error) {
this.cli.error('Error updating vulnerabilities.json');
this.cli.error(error);
}
}

async promptExistingPreRelease(cli) {
const pathPreRelease = await cli.prompt(
'Please provide the path of the existing pre-release announcement:', {
Expand Down Expand Up @@ -324,21 +301,6 @@ export default class SecurityBlog {
return text.join('\n');
}

getAffectedVersions(content) {
const affectedVersions = new Set();
for (const report of Object.values(content.reports)) {
for (const affectedVersion of report.affectedVersions) {
affectedVersions.add(affectedVersion);
}
}
const parseToNumber = str => +(str.match(/[\d.]+/g)[0]);
return Array.from(affectedVersions)
.sort((a, b) => {
return parseToNumber(a) > parseToNumber(b) ? -1 : 1;
})
.join(', ');
}

getSecurityPreReleaseTemplate() {
return fs.readFileSync(
new URL(
Expand Down
Loading
Loading