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
188 changes: 180 additions & 8 deletions lib/prepare_security.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
promptDependencies,
getSupportedVersions,
getReportSeverity,
getSummary,
pickReport,
confirmSecurityStep,
writeSecurityFile,
Expand Down Expand Up @@ -81,6 +82,75 @@ export function getNextTuesdayReleaseDateChoices(fromDate = new Date(), count =
return choices;
}

function getReportPRURL(report) {
const customFieldValues = report.relationships.custom_field_values?.data ?? [];
return customFieldValues[0]?.attributes?.value ?? '';
}

export function buildIncludedTriagedReport(report, options = {}) {
const {
affectedVersions = '',
patchAuthors = [],
prURL = getReportPRURL(report)
} = options;
const {
id,
attributes: { title, cve_ids = [] },
relationships: { reporter }
} = report;
const link = `https://hackerone.com/reports/${id}`;
const summaryContent = getSummary(report);

return {
id,
title,
cveIds: cve_ids,
severity: getReportSeverity(report),
summary: summaryContent ?? '',
patchAuthors,
prURL,
affectedVersions: affectedVersions
.split(',')
.map((v) => v.replace('v', '').trim())
.filter(Boolean),
link,
reporter: reporter?.data?.attributes?.username ?? ''
};
}

export function getMissingReportInformation(report) {
const missing = [];

if (!report.severity?.rating) missing.push('severity rating');
if (!report.severity?.cvss_vector_string) missing.push('CVSS vector');
if (!report.severity?.weakness_id) missing.push('weakness ID');
if (!report.summary) missing.push('team summary');
if (!report.prURL) missing.push('PR URL');
if (!report.patchAuthors?.length) missing.push('patch authors');
if (!report.affectedVersions?.length) missing.push('affected versions');

return missing;
}

export function groupMissingReportInformation(reports) {
const grouped = new Map();

for (const report of reports) {
for (const field of report.missing) {
const current = grouped.get(field) ?? [];
current.push(report);
grouped.set(field, current);
}
}

return Array.from(grouped.entries())
.map(([field, fieldReports]) => ({
field,
reports: fieldReports
}))
.sort((a, b) => b.reports.length - a.reports.length);
}

export default class PrepareSecurityRelease extends SecurityRelease {
title = 'Next Security Release';

Expand All @@ -93,11 +163,6 @@ export default class PrepareSecurityRelease extends SecurityRelease {
this.req = new Request(credentials);

let excludedReports = [];
const showTriaged = await this.promptShowTriagedWithoutPR();
if (showTriaged) {
excludedReports = await this.showTriagedReportsWithoutPR();
}

const releaseDate = await this.promptReleaseDate();
if (releaseDate !== 'TBD') {
validateDate(releaseDate);
Expand All @@ -106,8 +171,15 @@ export default class PrepareSecurityRelease extends SecurityRelease {

const content = await this.buildDescription(releaseDate);
if (createVulnerabilitiesJSON) {
const reportSelectionMode = await this.promptReportSelectionMode();
if (reportSelectionMode === 'review') {
const showTriaged = await this.promptShowTriagedWithoutPR();
if (showTriaged) {
excludedReports = await this.showTriagedReportsWithoutPR();
}
}
await this.startVulnerabilitiesJSONCreation(
releaseDate, content, excludedReports);
releaseDate, content, excludedReports, reportSelectionMode);
}

this.cli.ok('Done!');
Expand Down Expand Up @@ -176,12 +248,19 @@ export default class PrepareSecurityRelease extends SecurityRelease {
this.cli.ok('Done!');
}

async startVulnerabilitiesJSONCreation(releaseDate, content, excludedReports = []) {
async startVulnerabilitiesJSONCreation(
releaseDate,
content,
excludedReports = [],
reportSelectionMode = 'review'
) {
// checkout on the next-security-release branch
await checkoutOnSecurityReleaseBranch(this.cli, this.repository);

// choose the reports to include in the security release
const reports = await this.chooseReports(excludedReports);
const reports = reportSelectionMode === 'include-all'
? await this.includeAllTriagedReports(excludedReports)
: await this.chooseReports(excludedReports);
const depUpdates = await this.getDependencyUpdates();
const deps = _.groupBy(depUpdates, 'name');

Expand Down Expand Up @@ -252,6 +331,25 @@ export default class PrepareSecurityRelease extends SecurityRelease {
{ defaultAnswer: true });
}

async promptReportSelectionMode() {
return this.cli.promptSelect(
'How would you like to choose reports for the next security release?',
[
{
name: 'Review each triaged report',
value: 'review',
description: 'Iterate over reports and fill missing details interactively'
},
{
name: 'Include all triaged reports',
value: 'include-all',
description: 'Add every triaged report and summarize missing information'
}
],
{ defaultAnswer: 'review' }
);
}

async promptCreateRelaseIssue() {
return this.cli.prompt(
'Create the Next Security Release issue?',
Expand Down Expand Up @@ -326,6 +424,80 @@ export default class PrepareSecurityRelease extends SecurityRelease {
return selectedReports;
}

async includeAllTriagedReports(excludedReports = []) {
this.cli.info('Getting triaged H1 reports...');
const reports = await this.req.getTriagedReports();
const supportedVersions = await getSupportedVersions();
const selectedReports = [];
const missingInformation = [];

for (const report of reports.data) {
if (excludedReports.includes(report.id)) continue;

const reportData = await this.buildIncludedTriagedReport(
report,
supportedVersions
);
selectedReports.push(reportData);

const missing = getMissingReportInformation(reportData);
if (missing.length) {
missingInformation.push({
id: reportData.id,
title: reportData.title,
link: reportData.link,
missing
});
}
}

this.displayMissingReportInformationSummary(missingInformation);
return selectedReports;
}

async buildIncludedTriagedReport(report, supportedVersions) {
const prURL = getReportPRURL(report);
const patchAuthors = await this.getPatchAuthorsFromPR(prURL);
return buildIncludedTriagedReport(report, {
affectedVersions: supportedVersions,
patchAuthors,
prURL
});
}

async getPatchAuthorsFromPR(prURL) {
if (!prURL) return [];

try {
const { user } = await this.req.getPullRequest(prURL);
return user?.login ? [user.login] : [];
} catch (error) {
this.cli.warn(`Could not fetch patch author from ${prURL}`);
this.cli.error(error);
return [];
}
}

displayMissingReportInformationSummary(reports) {
if (!reports.length) {
this.cli.ok('All included reports have the expected information.');
return;
}

const grouped = groupMissingReportInformation(reports);
this.cli.warn(
`${reports.length} included reports are missing information:`
);
for (const { field, reports: fieldReports } of grouped) {
const reportList = fieldReports
.map(({ id }) => `H1 #${id}`)
.join(', ');
this.cli.info(
`- ${field} (${fieldReports.length}): ${reportList}`
);
}
}

async createVulnerabilitiesJSON(reports, dependencies, releaseDate) {
this.cli.startSpinner('Creating vulnerabilities.json...');
const fileContent = JSON.stringify({
Expand Down
Loading
Loading