Skip to content

Commit

Permalink
refactor, feat: patch authors and rebase when pushing (#797)
Browse files Browse the repository at this point in the history
* refactor: renamed createVulnerabilitiesJSON

* refactor: merge into one class

* refactor: create pickReport function to avoid duplication

* refactor: remove confusing cli and req inputs

* feat: add rebase when pushing

* feat: add patch authors
  • Loading branch information
marco-ippolito committed Apr 19, 2024
1 parent b5d99ca commit 78ad337
Show file tree
Hide file tree
Showing 5 changed files with 171 additions and 187 deletions.
4 changes: 2 additions & 2 deletions components/git/security.js
@@ -1,5 +1,5 @@
import CLI from '../../lib/cli.js';
import SecurityReleaseSteward from '../../lib/prepare_security.js';
import PrepareSecurityRelease from '../../lib/prepare_security.js';
import UpdateSecurityRelease from '../../lib/update_security_release.js';
import SecurityBlog from '../../lib/security_blog.js';
import SecurityAnnouncement from '../../lib/security-announcement.js';
Expand Down Expand Up @@ -138,7 +138,7 @@ async function requestCVEs() {
async function startSecurityRelease(argv) {
const logStream = process.stdout.isTTY ? process.stdout : process.stderr;
const cli = new CLI(logStream);
const release = new SecurityReleaseSteward(cli);
const release = new PrepareSecurityRelease(cli);
return release.start();
}

Expand Down
202 changes: 74 additions & 128 deletions lib/prepare_security.js
@@ -1,4 +1,3 @@
import nv from '@pkgjs/nv';
import fs from 'node:fs';
import path from 'node:path';
import auth from './auth.js';
Expand All @@ -10,100 +9,89 @@ import {
PLACEHOLDERS,
checkoutOnSecurityReleaseBranch,
commitAndPushVulnerabilitiesJSON,
getSummary,
validateDate,
promptDependencies,
getSupportedVersions
getSupportedVersions,
pickReport
} from './security-release/security-release.js';
import _ from 'lodash';

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

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

const req = new Request(credentials);
const release = new PrepareSecurityRelease(req);
const releaseDate = await release.promptReleaseDate(cli);
this.req = new Request(credentials);
const releaseDate = await this.promptReleaseDate();
if (releaseDate !== 'TBD') {
validateDate(releaseDate);
}

const createVulnerabilitiesJSON = await release.promptVulnerabilitiesJSON(cli);
const createVulnerabilitiesJSON = await this.promptVulnerabilitiesJSON();

let securityReleasePRUrl;
if (createVulnerabilitiesJSON) {
securityReleasePRUrl = await this.createVulnerabilitiesJSON(
req, release, releaseDate, { cli });
securityReleasePRUrl = await this.startVulnerabilitiesJSONCreation(releaseDate);
}

const createIssue = await release.promptCreateRelaseIssue(cli);
const createIssue = await this.promptCreateRelaseIssue();

if (createIssue) {
const content = await release.buildIssue(releaseDate, securityReleasePRUrl);
await release.createIssue(content, { cli });
const content = await this.buildIssue(releaseDate, securityReleasePRUrl);
await createIssue(
this.title, content, this.repository, { cli: this.cli, repository: this.repository });
};

cli.ok('Done!');
this.cli.ok('Done!');
}

async createVulnerabilitiesJSON(req, release, releaseDate, { cli }) {
async startVulnerabilitiesJSONCreation(releaseDate) {
// checkout on the next-security-release branch
checkoutOnSecurityReleaseBranch(cli, this.repository);
checkoutOnSecurityReleaseBranch(this.cli, this.repository);

// choose the reports to include in the security release
const reports = await release.chooseReports(cli);
const depUpdates = await release.getDependencyUpdates({ cli });
const reports = await this.chooseReports();
const depUpdates = await this.getDependencyUpdates();
const deps = _.groupBy(depUpdates, 'name');

// create the vulnerabilities.json file in the security-release repo
const filePath = await release.createVulnerabilitiesJSON(reports, deps, releaseDate, { cli });
const filePath = await this.createVulnerabilitiesJSON(reports, deps, releaseDate);

// review the vulnerabilities.json file
const review = await release.promptReviewVulnerabilitiesJSON(cli);
const review = await this.promptReviewVulnerabilitiesJSON();

if (!review) {
cli.info(`To push the vulnerabilities.json file run:
this.cli.info(`To push the vulnerabilities.json file run:
- git add ${filePath}
- git commit -m "chore: create vulnerabilities.json for next security release"
- git push -u origin ${NEXT_SECURITY_RELEASE_BRANCH}
- open a PR on ${release.repository.owner}/${release.repository.repo}`);
- open a PR on ${this.repository.owner}/${this.repository.repo}`);
return;
};

// commit and push the vulnerabilities.json file
const commitMessage = 'chore: create vulnerabilities.json for next security release';
commitAndPushVulnerabilitiesJSON(filePath, commitMessage, { cli, repository: this.repository });
commitAndPushVulnerabilitiesJSON(filePath,
commitMessage,
{ cli: this.cli, repository: this.repository });

const createPr = await release.promptCreatePR(cli);
const createPr = await this.promptCreatePR();

if (!createPr) return;

// create pr on the security-release repo
return release.createPullRequest(req, { cli });
return this.createPullRequest();
}
}

class PrepareSecurityRelease {
repository = NEXT_SECURITY_RELEASE_REPOSITORY;
title = 'Next Security Release';

constructor(req, repository) {
this.req = req;
if (repository) {
this.repository = repository;
}
}

promptCreatePR(cli) {
return cli.prompt(
promptCreatePR() {
return this.cli.prompt(
'Create the Next Security Release PR?',
{ defaultAnswer: true });
}
Expand All @@ -125,31 +113,32 @@ class PrepareSecurityRelease {
}
}

async promptReleaseDate(cli) {
async promptReleaseDate() {
const nextWeekDate = new Date();
nextWeekDate.setDate(nextWeekDate.getDate() + 7);
// Format the date as YYYY/MM/DD
const formattedDate = nextWeekDate.toISOString().slice(0, 10).replace(/-/g, '/');
return cli.prompt('Enter target release date in YYYY/MM/DD format (TBD if not defined yet):', {
questionType: 'input',
defaultAnswer: formattedDate
});
return this.cli.prompt(
'Enter target release date in YYYY/MM/DD format (TBD if not defined yet):', {
questionType: 'input',
defaultAnswer: formattedDate
});
}

async promptVulnerabilitiesJSON(cli) {
return cli.prompt(
async promptVulnerabilitiesJSON() {
return this.cli.prompt(
'Create the vulnerabilities.json?',
{ defaultAnswer: true });
}

async promptCreateRelaseIssue(cli) {
return cli.prompt(
async promptCreateRelaseIssue() {
return this.cli.prompt(
'Create the Next Security Release issue?',
{ defaultAnswer: true });
}

async promptReviewVulnerabilitiesJSON(cli) {
return cli.prompt(
async promptReviewVulnerabilitiesJSON() {
return this.cli.prompt(
'Please review vulnerabilities.json and press enter to proceed.',
{ defaultAnswer: true });
}
Expand All @@ -161,67 +150,21 @@ class PrepareSecurityRelease {
return content;
}

async createIssue(content, { cli }) {
const data = await this.req.createIssue(this.title, content, this.repository);
if (data.html_url) {
cli.ok(`Created: ${data.html_url}`);
} else {
cli.error(data);
process.exit(1);
}
}

async chooseReports(cli) {
cli.info('Getting triaged H1 reports...');
async chooseReports() {
this.cli.info('Getting triaged H1 reports...');
const reports = await this.req.getTriagedReports();
const supportedVersions = (await nv('supported'))
.map((v) => `${v.versionName}.x`)
.join(',');
const selectedReports = [];

for (const report of reports.data) {
const {
id, attributes: { title, cve_ids },
relationships: { severity, weakness, reporter }
} = report;
const link = `https://hackerone.com/reports/${id}`;
const reportSeverity = {
rating: severity?.data?.attributes?.rating || '',
cvss_vector_string: severity?.data?.attributes?.cvss_vector_string || '',
weakness_id: weakness?.data?.id || ''
};

cli.separator();
cli.info(`Report: ${link} - ${title} (${reportSeverity?.rating})`);
const include = await cli.prompt(
'Would you like to include this report to the next security release?',
{ defaultAnswer: true });
if (!include) {
continue;
}

const versions = await cli.prompt('Which active release lines this report affects?', {
questionType: 'input',
defaultAnswer: supportedVersions
});
const summaryContent = await getSummary(id, this.req);

selectedReports.push({
id,
title,
cveIds: cve_ids,
severity: reportSeverity,
summary: summaryContent ?? '',
affectedVersions: versions.split(',').map((v) => v.replace('v', '').trim()),
link,
reporter: reporter.data.attributes.username
});
const rep = await pickReport(report, { cli: this.cli, req: this.req });
if (!rep) continue;
selectedReports.push(rep);
}
return selectedReports;
}

async createVulnerabilitiesJSON(reports, dependencies, releaseDate, { cli }) {
cli.separator('Creating vulnerabilities.json...');
async createVulnerabilitiesJSON(reports, dependencies, releaseDate) {
this.cli.separator('Creating vulnerabilities.json...');
const file = JSON.stringify({
releaseDate,
reports,
Expand All @@ -237,14 +180,14 @@ class PrepareSecurityRelease {

const fullPath = path.join(folderPath, 'vulnerabilities.json');
fs.writeFileSync(fullPath, file);
cli.ok(`Created ${fullPath} `);
this.cli.ok(`Created ${fullPath} `);

return fullPath;
}

async createPullRequest(req, { cli }) {
async createPullRequest() {
const { owner, repo } = this.repository;
const response = await req.createPullRequest(
const response = await this.req.createPullRequest(
this.title,
'List of vulnerabilities to be included in the next security release',
{
Expand All @@ -257,49 +200,52 @@ class PrepareSecurityRelease {
);
const url = response?.html_url;
if (url) {
cli.ok(`Created: ${url}`);
this.cli.ok(`Created: ${url}`);
return url;
}
if (response?.errors) {
for (const error of response.errors) {
cli.error(error.message);
this.cli.error(error.message);
}
} else {
cli.error(response);
this.cli.error(response);
}
process.exit(1);
}

async getDependencyUpdates({ cli }) {
async getDependencyUpdates() {
const deps = [];
cli.log('\n');
cli.separator('Dependency Updates');
const updates = await cli.prompt('Are there dependency updates in this security release?', {
defaultAnswer: true,
questionType: 'confirm'
});
this.cli.log('\n');
this.cli.separator('Dependency Updates');
const updates = await this.cli.prompt('Are there dependency updates in this security release?',
{
defaultAnswer: true,
questionType: 'confirm'
});

if (!updates) return deps;

const supportedVersions = await getSupportedVersions();

let asking = true;
while (asking) {
const dep = await promptDependencies(cli);
const dep = await promptDependencies(this.cli);
if (!dep) {
asking = false;
break;
}

const name = await cli.prompt('What is the name of the dependency that has been updated?', {
defaultAnswer: '',
questionType: 'input'
});
const name = await this.cli.prompt(
'What is the name of the dependency that has been updated?', {
defaultAnswer: '',
questionType: 'input'
});

const versions = await cli.prompt('Which release line does this dependency update affect?', {
defaultAnswer: supportedVersions,
questionType: 'input'
});
const versions = await this.cli.prompt(
'Which release line does this dependency update affect?', {
defaultAnswer: supportedVersions,
questionType: 'input'
});

try {
const prUrl = dep.replace('https://github.com/', 'https://api.github.com/repos/').replace('pull', 'pulls');
Expand All @@ -311,7 +257,7 @@ class PrepareSecurityRelease {
title,
affectedVersions: versions.split(',').map((v) => v.replace('v', '').trim())
});
cli.separator();
this.cli.separator();
} catch (error) {
this.cli.error('Invalid PR url. Please provide a valid PR url.');
this.cli.error(error);
Expand Down

0 comments on commit 78ad337

Please sign in to comment.