@@ -2,6 +2,16 @@ import nv from '@pkgjs/nv';
22import auth from './auth.js' ;
33import Request from './request.js' ;
44import fs from 'node:fs' ;
5+ import { runSync } from './run.js' ;
6+ import path from 'node:path' ;
7+
8+ export const PLACEHOLDERS = {
9+ releaseDate : '%RELEASE_DATE%' ,
10+ vulnerabilitiesPRURL : '%VULNERABILITIES_PR_URL%' ,
11+ preReleasePrivate : '%PRE_RELEASE_PRIV%' ,
12+ postReleasePrivate : '%POS_RELEASE_PRIV%' ,
13+ affectedLines : '%AFFECTED_LINES%'
14+ } ;
515
616export default class SecurityReleaseSteward {
717 constructor ( cli ) {
@@ -16,31 +26,100 @@ export default class SecurityReleaseSteward {
1626 } ) ;
1727
1828 const req = new Request ( credentials ) ;
19- const create = await cli . prompt (
20- 'Create the Next Security Release issue?' ,
21- { defaultAnswer : true } ) ;
22- if ( create ) {
23- const issue = new SecurityReleaseIssue ( req ) ;
24- const content = await issue . buildIssue ( cli ) ;
25- const data = await req . createIssue ( 'Next Security Release' , content , {
26- owner : 'nodejs-private' ,
27- repo : 'node-private'
28- } ) ;
29- if ( data . html_url ) {
30- cli . ok ( 'Created: ' + data . html_url ) ;
31- } else {
32- cli . error ( data ) ;
33- }
29+ const release = new PrepareSecurityRelease ( req ) ;
30+ const releaseDate = await release . promptReleaseDate ( cli ) ;
31+ let securityReleasePRUrl = PLACEHOLDERS . vulnerabilitiesPRURL ;
32+
33+ const createVulnerabilitiesJSON = await release . promptVulnerabilitiesJSON ( cli ) ;
34+ if ( createVulnerabilitiesJSON ) {
35+ securityReleasePRUrl = await this . createVulnerabilitiesJSON ( req , release , { cli } ) ;
3436 }
37+
38+ const createIssue = await release . promptCreateRelaseIssue ( cli ) ;
39+
40+ if ( createIssue ) {
41+ const { content } = release . buildIssue ( releaseDate , securityReleasePRUrl ) ;
42+ await release . createIssue ( content , { cli } ) ;
43+ } ;
44+
45+ cli . ok ( 'Done!' ) ;
46+ }
47+
48+ async createVulnerabilitiesJSON ( req , release , { cli } ) {
49+ // checkout on the next-security-release branch
50+ release . checkoutOnSecurityReleaseBranch ( cli ) ;
51+
52+ // choose the reports to include in the security release
53+ const reports = await release . chooseReports ( cli ) ;
54+
55+ // create the vulnerabilities.json file in the security-release repo
56+ const filePath = await release . createVulnerabilitiesJSON ( reports , { cli } ) ;
57+
58+ // review the vulnerabilities.json file
59+ const review = await release . promptReviewVulnerabilitiesJSON ( cli ) ;
60+
61+ if ( ! review ) {
62+ cli . info ( `To push the vulnerabilities.json file run:
63+ - git add ${ filePath }
64+ - git commit -m "chore: create vulnerabilities.json for next security release"
65+ - git push -u origin next-security-release
66+ - open a PR on ${ release . repository . owner } /${ release . repository . repo } ` ) ;
67+ return ;
68+ } ;
69+
70+ // commit and push the vulnerabilities.json file
71+ release . commitAndPushVulnerabilitiesJSON ( filePath , cli ) ;
72+
73+ const createPr = await release . promptCreatePR ( cli ) ;
74+
75+ if ( ! createPr ) return ;
76+
77+ // create pr on the security-release repo
78+ return release . createPullRequest ( req , { cli } ) ;
3579 }
3680}
3781
38- class SecurityReleaseIssue {
39- constructor ( req ) {
82+ class PrepareSecurityRelease {
83+ repository = {
84+ owner : 'nodejs-private' ,
85+ repo : 'security-release'
86+ } ;
87+
88+ title = 'Next Security Release' ;
89+ nextSecurityReleaseBranch = 'next-security-release' ;
90+
91+ constructor ( req , repository ) {
4092 this . req = req ;
41- this . content = '' ;
42- this . title = 'Next Security Release' ;
43- this . affectedLines = { } ;
93+ if ( repository ) {
94+ this . repository = repository ;
95+ }
96+ }
97+
98+ promptCreatePR ( cli ) {
99+ return cli . prompt (
100+ 'Create the Next Security Release PR?' ,
101+ { defaultAnswer : true } ) ;
102+ }
103+
104+ checkRemote ( cli ) {
105+ const remote = runSync ( 'git' , [ 'ls-remote' , '--get-url' , 'origin' ] ) . trim ( ) ;
106+ const { owner, repo } = this . repository ;
107+ const securityReleaseOrigin = `https://github.com/${ owner } /${ repo } .git` ;
108+
109+ if ( remote !== securityReleaseOrigin ) {
110+ cli . error ( `Wrong repository! It should be ${ securityReleaseOrigin } ` ) ;
111+ process . exit ( 1 ) ;
112+ }
113+ }
114+
115+ commitAndPushVulnerabilitiesJSON ( filePath , cli ) {
116+ this . checkRemote ( cli ) ;
117+
118+ runSync ( 'git' , [ 'add' , filePath ] ) ;
119+ const commitMessage = 'chore: create vulnerabilities.json for next security release' ;
120+ runSync ( 'git' , [ 'commit' , '-m' , commitMessage ] ) ;
121+ runSync ( 'git' , [ 'push' , '-u' , 'origin' , 'next-security-release' ] ) ;
122+ cli . ok ( `Pushed commit: ${ commitMessage } to ${ this . nextSecurityReleaseBranch } ` ) ;
44123 }
45124
46125 getSecurityIssueTemplate ( ) {
@@ -53,31 +132,58 @@ class SecurityReleaseIssue {
53132 ) ;
54133 }
55134
56- async buildIssue ( cli ) {
57- this . content = this . getSecurityIssueTemplate ( ) ;
58- cli . info ( 'Getting triaged H1 reports...' ) ;
59- const reports = await this . req . getTriagedReports ( ) ;
60- await this . fillReports ( cli , reports ) ;
61-
62- this . fillAffectedLines ( Object . keys ( this . affectedLines ) ) ;
63-
64- const target = await cli . prompt ( 'Enter target date in YYYY-MM-DD format:' , {
135+ async promptReleaseDate ( cli ) {
136+ return cli . prompt ( 'Enter target release date in YYYY-MM-DD format:' , {
65137 questionType : 'input' ,
66138 defaultAnswer : 'TBD'
67139 } ) ;
68- this . fillTargetDate ( target ) ;
140+ }
141+
142+ async promptVulnerabilitiesJSON ( cli ) {
143+ return cli . prompt (
144+ 'Create the vulnerabilities.json?' ,
145+ { defaultAnswer : true } ) ;
146+ }
147+
148+ async promptCreateRelaseIssue ( cli ) {
149+ return cli . prompt (
150+ 'Create the Next Security Release issue?' ,
151+ { defaultAnswer : true } ) ;
152+ }
69153
70- return this . content ;
154+ async promptReviewVulnerabilitiesJSON ( cli ) {
155+ return cli . prompt (
156+ 'Please review vulnerabilities.json and press enter to proceed.' ,
157+ { defaultAnswer : true } ) ;
71158 }
72159
73- async fillReports ( cli , reports ) {
160+ buildIssue ( releaseDate , securityReleasePRUrl ) {
161+ const template = this . getSecurityIssueTemplate ( ) ;
162+ const content = template . replace ( PLACEHOLDERS . releaseDate , releaseDate )
163+ . replace ( PLACEHOLDERS . vulnerabilitiesPRURL , securityReleasePRUrl ) ;
164+ return { releaseDate, content, securityReleasePRUrl } ;
165+ }
166+
167+ async createIssue ( content , { cli } ) {
168+ const data = await this . req . createIssue ( this . title , content , this . repository ) ;
169+ if ( data . html_url ) {
170+ cli . ok ( 'Created: ' + data . html_url ) ;
171+ } else {
172+ cli . error ( data ) ;
173+ process . exit ( 1 ) ;
174+ }
175+ }
176+
177+ async chooseReports ( cli ) {
178+ cli . info ( 'Getting triaged H1 reports...' ) ;
179+ const reports = await this . req . getTriagedReports ( ) ;
74180 const supportedVersions = ( await nv ( 'supported' ) )
75181 . map ( ( v ) => v . versionName + '.x' )
76182 . join ( ',' ) ;
183+ const selectedReports = [ ] ;
77184
78- let reportsContent = '' ;
79185 for ( const report of reports . data ) {
80- const { id, attributes : { title } , relationships : { severity } } = report ;
186+ const { id, attributes : { title, cve_ids } , relationships : { severity } } = report ;
81187 const reportLevel = severity ? severity . data . attributes . rating : 'TBD' ;
82188 cli . separator ( ) ;
83189 cli . info ( `Report: ${ id } - ${ title } (${ reportLevel } )` ) ;
@@ -88,30 +194,90 @@ class SecurityReleaseIssue {
88194 continue ;
89195 }
90196
91- reportsContent +=
92- ` * **[${ id } ](https://hackerone.com/bugs?subject=nodejs&report_id=${ id } ) - ${ title } (TBD) - (${ reportLevel } )**\n` ;
93197 const versions = await cli . prompt ( 'Which active release lines this report affects?' , {
94198 questionType : 'input' ,
95199 defaultAnswer : supportedVersions
96200 } ) ;
97- for ( const v of versions . split ( ',' ) ) {
98- if ( ! this . affectedLines [ v ] ) this . affectedLines [ v ] = true ;
99- reportsContent += ` * ${ v } - TBD\n` ;
100- }
201+ const summaryContent = await this . getSummary ( id ) ;
202+
203+ selectedReports . push ( {
204+ id,
205+ title,
206+ cve_ids,
207+ severity : reportLevel ,
208+ summary : summaryContent ?? '' ,
209+ affectedVersions : versions . split ( ',' ) . map ( ( v ) => v . replace ( 'v' , '' ) . trim ( ) )
210+ } ) ;
101211 }
102- this . content = this . content . replace ( '%REPORTS%' , reportsContent ) ;
212+ return selectedReports ;
213+ }
214+
215+ async getSummary ( reportId ) {
216+ const { data } = await this . req . getReport ( reportId ) ;
217+ const summaryList = data ?. relationships ?. summaries ?. data ;
218+ if ( ! summaryList ?. length ) return ;
219+ const summaries = summaryList . filter ( ( summary ) => summary ?. attributes ?. category === 'team' ) ;
220+ if ( ! summaries ?. length ) return ;
221+ return summaries ?. [ 0 ] . attributes ?. content ;
103222 }
104223
105- fillAffectedLines ( affectedLines ) {
106- let affected = '' ;
107- for ( const line of affectedLines ) {
108- affected += ` * ${ line } - TBD\n` ;
224+ checkoutOnSecurityReleaseBranch ( cli ) {
225+ this . checkRemote ( cli ) ;
226+ const currentBranch = runSync ( 'git' , [ 'branch' , '--show-current' ] ) . trim ( ) ;
227+ cli . info ( `Current branch: ${ currentBranch } ` ) ;
228+
229+ if ( currentBranch !== this . nextSecurityReleaseBranch ) {
230+ runSync ( 'git' , [ 'checkout' , '-B' , this . nextSecurityReleaseBranch ] ) ;
231+ cli . ok ( `Checkout on branch: ${ this . nextSecurityReleaseBranch } ` ) ;
232+ } ;
233+ }
234+
235+ async createVulnerabilitiesJSON ( reports , { cli } ) {
236+ cli . separator ( 'Creating vulnerabilities.json...' ) ;
237+ const file = JSON . stringify ( {
238+ reports
239+ } , null , 2 ) ;
240+
241+ const folderPath = path . join ( process . cwd ( ) , 'security-release' , 'next-security-release' ) ;
242+ try {
243+ await fs . accessSync ( folderPath ) ;
244+ } catch ( error ) {
245+ await fs . mkdirSync ( folderPath , { recursive : true } ) ;
109246 }
110- this . content =
111- this . content . replace ( '%AFFECTED_LINES%' , affected ) ;
247+
248+ const fullPath = path . join ( folderPath , 'vulnerabilities.json' ) ;
249+ fs . writeFileSync ( fullPath , file ) ;
250+ cli . ok ( `Created ${ fullPath } ` ) ;
251+
252+ return fullPath ;
112253 }
113254
114- fillTargetDate ( date ) {
115- this . content = this . content . replace ( '%RELEASE_DATE%' , date ) ;
255+ async createPullRequest ( req , { cli } ) {
256+ const { owner, repo } = this . repository ;
257+ const response = await req . createPullRequest (
258+ this . title ,
259+ 'List of vulnerabilities to be included in the next security release' ,
260+ {
261+ owner,
262+ repo,
263+ base : 'main' ,
264+ head : 'next-security-release'
265+ }
266+
267+ ) ;
268+ const url = response ?. html_url ;
269+ if ( url ) {
270+ cli . ok ( 'Created: ' + url ) ;
271+ return url ;
272+ } else {
273+ if ( response ?. errors ) {
274+ for ( const error of response . errors ) {
275+ cli . error ( error . message ) ;
276+ }
277+ } else {
278+ cli . error ( response ) ;
279+ }
280+ process . exit ( 1 ) ;
281+ }
116282 }
117283}
0 commit comments