Skip to content

Commit

Permalink
feat: add git node security --cleanup (#833)
Browse files Browse the repository at this point in the history
* feat: add git node security --finish

feat: use SecurityRelease as base class

* fixup! rename to cleanup
  • Loading branch information
RafaelGSS authored Aug 14, 2024
1 parent 61586c9 commit 871a16f
Show file tree
Hide file tree
Showing 6 changed files with 199 additions and 92 deletions.
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

0 comments on commit 871a16f

Please sign in to comment.