Skip to content

Commit dd6fdfc

Browse files
committed
feat(security): add include-all report selection mode
Signed-off-by: RafaelGSS <rafael.nunu@hotmail.com>
1 parent 1f46846 commit dd6fdfc

2 files changed

Lines changed: 398 additions & 9 deletions

File tree

lib/prepare_security.js

Lines changed: 180 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
promptDependencies,
1212
getSupportedVersions,
1313
getReportSeverity,
14+
getSummary,
1415
pickReport,
1516
confirmSecurityStep,
1617
writeSecurityFile,
@@ -81,6 +82,75 @@ export function getNextTuesdayReleaseDateChoices(fromDate = new Date(), count =
8182
return choices;
8283
}
8384

85+
function getReportPRURL(report) {
86+
const customFieldValues = report.relationships.custom_field_values?.data ?? [];
87+
return customFieldValues[0]?.attributes?.value ?? '';
88+
}
89+
90+
export function buildIncludedTriagedReport(report, options = {}) {
91+
const {
92+
affectedVersions = '',
93+
patchAuthors = [],
94+
prURL = getReportPRURL(report)
95+
} = options;
96+
const {
97+
id,
98+
attributes: { title, cve_ids = [] },
99+
relationships: { reporter }
100+
} = report;
101+
const link = `https://hackerone.com/reports/${id}`;
102+
const summaryContent = getSummary(report);
103+
104+
return {
105+
id,
106+
title,
107+
cveIds: cve_ids,
108+
severity: getReportSeverity(report),
109+
summary: summaryContent ?? '',
110+
patchAuthors,
111+
prURL,
112+
affectedVersions: affectedVersions
113+
.split(',')
114+
.map((v) => v.replace('v', '').trim())
115+
.filter(Boolean),
116+
link,
117+
reporter: reporter?.data?.attributes?.username ?? ''
118+
};
119+
}
120+
121+
export function getMissingReportInformation(report) {
122+
const missing = [];
123+
124+
if (!report.severity?.rating) missing.push('severity rating');
125+
if (!report.severity?.cvss_vector_string) missing.push('CVSS vector');
126+
if (!report.severity?.weakness_id) missing.push('weakness ID');
127+
if (!report.summary) missing.push('team summary');
128+
if (!report.prURL) missing.push('PR URL');
129+
if (!report.patchAuthors?.length) missing.push('patch authors');
130+
if (!report.affectedVersions?.length) missing.push('affected versions');
131+
132+
return missing;
133+
}
134+
135+
export function groupMissingReportInformation(reports) {
136+
const grouped = new Map();
137+
138+
for (const report of reports) {
139+
for (const field of report.missing) {
140+
const current = grouped.get(field) ?? [];
141+
current.push(report);
142+
grouped.set(field, current);
143+
}
144+
}
145+
146+
return Array.from(grouped.entries())
147+
.map(([field, fieldReports]) => ({
148+
field,
149+
reports: fieldReports
150+
}))
151+
.sort((a, b) => b.reports.length - a.reports.length);
152+
}
153+
84154
export default class PrepareSecurityRelease extends SecurityRelease {
85155
title = 'Next Security Release';
86156

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

95165
let excludedReports = [];
96-
const showTriaged = await this.promptShowTriagedWithoutPR();
97-
if (showTriaged) {
98-
excludedReports = await this.showTriagedReportsWithoutPR();
99-
}
100-
101166
const releaseDate = await this.promptReleaseDate();
102167
if (releaseDate !== 'TBD') {
103168
validateDate(releaseDate);
@@ -106,8 +171,15 @@ export default class PrepareSecurityRelease extends SecurityRelease {
106171

107172
const content = await this.buildDescription(releaseDate);
108173
if (createVulnerabilitiesJSON) {
174+
const reportSelectionMode = await this.promptReportSelectionMode();
175+
if (reportSelectionMode === 'review') {
176+
const showTriaged = await this.promptShowTriagedWithoutPR();
177+
if (showTriaged) {
178+
excludedReports = await this.showTriagedReportsWithoutPR();
179+
}
180+
}
109181
await this.startVulnerabilitiesJSONCreation(
110-
releaseDate, content, excludedReports);
182+
releaseDate, content, excludedReports, reportSelectionMode);
111183
}
112184

113185
this.cli.ok('Done!');
@@ -176,12 +248,19 @@ export default class PrepareSecurityRelease extends SecurityRelease {
176248
this.cli.ok('Done!');
177249
}
178250

179-
async startVulnerabilitiesJSONCreation(releaseDate, content, excludedReports = []) {
251+
async startVulnerabilitiesJSONCreation(
252+
releaseDate,
253+
content,
254+
excludedReports = [],
255+
reportSelectionMode = 'review'
256+
) {
180257
// checkout on the next-security-release branch
181258
await checkoutOnSecurityReleaseBranch(this.cli, this.repository);
182259

183260
// choose the reports to include in the security release
184-
const reports = await this.chooseReports(excludedReports);
261+
const reports = reportSelectionMode === 'include-all'
262+
? await this.includeAllTriagedReports(excludedReports)
263+
: await this.chooseReports(excludedReports);
185264
const depUpdates = await this.getDependencyUpdates();
186265
const deps = _.groupBy(depUpdates, 'name');
187266

@@ -252,6 +331,25 @@ export default class PrepareSecurityRelease extends SecurityRelease {
252331
{ defaultAnswer: true });
253332
}
254333

334+
async promptReportSelectionMode() {
335+
return this.cli.promptSelect(
336+
'How would you like to choose reports for the next security release?',
337+
[
338+
{
339+
name: 'Review each triaged report',
340+
value: 'review',
341+
description: 'Iterate over reports and fill missing details interactively'
342+
},
343+
{
344+
name: 'Include all triaged reports',
345+
value: 'include-all',
346+
description: 'Add every triaged report and summarize missing information'
347+
}
348+
],
349+
{ defaultAnswer: 'review' }
350+
);
351+
}
352+
255353
async promptCreateRelaseIssue() {
256354
return this.cli.prompt(
257355
'Create the Next Security Release issue?',
@@ -326,6 +424,80 @@ export default class PrepareSecurityRelease extends SecurityRelease {
326424
return selectedReports;
327425
}
328426

427+
async includeAllTriagedReports(excludedReports = []) {
428+
this.cli.info('Getting triaged H1 reports...');
429+
const reports = await this.req.getTriagedReports();
430+
const supportedVersions = await getSupportedVersions();
431+
const selectedReports = [];
432+
const missingInformation = [];
433+
434+
for (const report of reports.data) {
435+
if (excludedReports.includes(report.id)) continue;
436+
437+
const reportData = await this.buildIncludedTriagedReport(
438+
report,
439+
supportedVersions
440+
);
441+
selectedReports.push(reportData);
442+
443+
const missing = getMissingReportInformation(reportData);
444+
if (missing.length) {
445+
missingInformation.push({
446+
id: reportData.id,
447+
title: reportData.title,
448+
link: reportData.link,
449+
missing
450+
});
451+
}
452+
}
453+
454+
this.displayMissingReportInformationSummary(missingInformation);
455+
return selectedReports;
456+
}
457+
458+
async buildIncludedTriagedReport(report, supportedVersions) {
459+
const prURL = getReportPRURL(report);
460+
const patchAuthors = await this.getPatchAuthorsFromPR(prURL);
461+
return buildIncludedTriagedReport(report, {
462+
affectedVersions: supportedVersions,
463+
patchAuthors,
464+
prURL
465+
});
466+
}
467+
468+
async getPatchAuthorsFromPR(prURL) {
469+
if (!prURL) return [];
470+
471+
try {
472+
const { user } = await this.req.getPullRequest(prURL);
473+
return user?.login ? [user.login] : [];
474+
} catch (error) {
475+
this.cli.warn(`Could not fetch patch author from ${prURL}`);
476+
this.cli.error(error);
477+
return [];
478+
}
479+
}
480+
481+
displayMissingReportInformationSummary(reports) {
482+
if (!reports.length) {
483+
this.cli.ok('All included reports have the expected information.');
484+
return;
485+
}
486+
487+
const grouped = groupMissingReportInformation(reports);
488+
this.cli.warn(
489+
`${reports.length} included reports are missing information:`
490+
);
491+
for (const { field, reports: fieldReports } of grouped) {
492+
const reportList = fieldReports
493+
.map(({ id }) => `H1 #${id}`)
494+
.join(', ');
495+
this.cli.info(
496+
`- ${field} (${fieldReports.length}): ${reportList}`
497+
);
498+
}
499+
}
500+
329501
async createVulnerabilitiesJSON(reports, dependencies, releaseDate) {
330502
this.cli.startSpinner('Creating vulnerabilities.json...');
331503
const fileContent = JSON.stringify({

0 commit comments

Comments
 (0)