@@ -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+
84154export 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