|
| 1 | +import fs from 'node:fs'; |
| 2 | +import path from 'node:path'; |
| 3 | +import _ from 'lodash'; |
| 4 | +import { |
| 5 | + PLACEHOLDERS, |
| 6 | + getVulnerabilitiesJSON, |
| 7 | + checkoutOnSecurityReleaseBranch, |
| 8 | + NEXT_SECURITY_RELEASE_REPOSITORY, |
| 9 | + validateDate |
| 10 | +} from './security-release/security-release.js'; |
| 11 | + |
| 12 | +export default class SecurityBlog { |
| 13 | + repository = NEXT_SECURITY_RELEASE_REPOSITORY; |
| 14 | + constructor(cli) { |
| 15 | + this.cli = cli; |
| 16 | + } |
| 17 | + |
| 18 | + async createPreRelease() { |
| 19 | + const { cli } = this; |
| 20 | + |
| 21 | + // checkout on security release branch |
| 22 | + checkoutOnSecurityReleaseBranch(cli, this.repository); |
| 23 | + |
| 24 | + // read vulnerabilities JSON file |
| 25 | + const content = getVulnerabilitiesJSON(cli); |
| 26 | + // validate the release date read from vulnerabilities JSON |
| 27 | + if (!content.releaseDate) { |
| 28 | + cli.error('Release date is not set in vulnerabilities.json,' + |
| 29 | + ' run `git node security --update-date=YYYY/MM/DD` to set the release date.'); |
| 30 | + process.exit(1); |
| 31 | + } |
| 32 | + |
| 33 | + validateDate(content.releaseDate); |
| 34 | + const releaseDate = new Date(content.releaseDate); |
| 35 | + |
| 36 | + const template = this.getSecurityPreReleaseTemplate(); |
| 37 | + const data = { |
| 38 | + annoucementDate: await this.getAnnouncementDate(cli), |
| 39 | + releaseDate: this.formatReleaseDate(releaseDate), |
| 40 | + affectedVersions: this.getAffectedVersions(content), |
| 41 | + vulnerabilities: this.getVulnerabilities(content), |
| 42 | + slug: this.getSlug(releaseDate), |
| 43 | + impact: this.getImpact(content), |
| 44 | + openSSLUpdate: await this.promptOpenSSLUpdate(cli) |
| 45 | + }; |
| 46 | + const month = releaseDate.toLocaleString('en-US', { month: 'long' }).toLowerCase(); |
| 47 | + const year = releaseDate.getFullYear(); |
| 48 | + const fileName = `${month}-${year}-security-releases.md`; |
| 49 | + const preRelease = this.buildPreRelease(template, data); |
| 50 | + const file = path.join(process.cwd(), fileName); |
| 51 | + fs.writeFileSync(file, preRelease); |
| 52 | + cli.ok(`Pre-release announcement file created at ${file}`); |
| 53 | + } |
| 54 | + |
| 55 | + promptOpenSSLUpdate(cli) { |
| 56 | + return cli.prompt('Does this security release containt OpenSSL updates?', { |
| 57 | + defaultAnswer: true |
| 58 | + }); |
| 59 | + } |
| 60 | + |
| 61 | + formatReleaseDate(releaseDate) { |
| 62 | + const options = { |
| 63 | + weekday: 'long', |
| 64 | + month: 'long', |
| 65 | + day: 'numeric', |
| 66 | + year: 'numeric' |
| 67 | + }; |
| 68 | + return releaseDate.toLocaleDateString('en-US', options); |
| 69 | + } |
| 70 | + |
| 71 | + buildPreRelease(template, data) { |
| 72 | + const { |
| 73 | + annoucementDate, |
| 74 | + releaseDate, |
| 75 | + affectedVersions, |
| 76 | + vulnerabilities, |
| 77 | + slug, |
| 78 | + impact, |
| 79 | + openSSLUpdate |
| 80 | + } = data; |
| 81 | + return template.replaceAll(PLACEHOLDERS.annoucementDate, annoucementDate) |
| 82 | + .replaceAll(PLACEHOLDERS.slug, slug) |
| 83 | + .replaceAll(PLACEHOLDERS.affectedVersions, affectedVersions) |
| 84 | + .replaceAll(PLACEHOLDERS.vulnerabilities, vulnerabilities) |
| 85 | + .replaceAll(PLACEHOLDERS.releaseDate, releaseDate) |
| 86 | + .replaceAll(PLACEHOLDERS.impact, impact) |
| 87 | + .replaceAll(PLACEHOLDERS.openSSLUpdate, this.getOpenSSLUpdateTemplate(openSSLUpdate)); |
| 88 | + } |
| 89 | + |
| 90 | + getOpenSSLUpdateTemplate(openSSLUpdate) { |
| 91 | + if (openSSLUpdate) { |
| 92 | + return '\n## OpenSSL Security updates\n\n' + |
| 93 | + 'This security release includes OpenSSL security updates\n'; |
| 94 | + } |
| 95 | + return ''; |
| 96 | + } |
| 97 | + |
| 98 | + getSlug(releaseDate) { |
| 99 | + const month = releaseDate.toLocaleString('en-US', { month: 'long' }); |
| 100 | + const year = releaseDate.getFullYear(); |
| 101 | + return `${month.toLocaleLowerCase()}-${year}-security-releases`; |
| 102 | + } |
| 103 | + |
| 104 | + async getAnnouncementDate(cli) { |
| 105 | + try { |
| 106 | + const date = await this.promptAnnouncementDate(cli); |
| 107 | + validateDate(date); |
| 108 | + return new Date(date).toISOString(); |
| 109 | + } catch (error) { |
| 110 | + return PLACEHOLDERS.annoucementDate; |
| 111 | + } |
| 112 | + } |
| 113 | + |
| 114 | + promptAnnouncementDate(cli) { |
| 115 | + return cli.prompt('When is the security release going to be announced? ' + |
| 116 | + 'Enter in YYYY-MM-DD format:', { |
| 117 | + questionType: 'input', |
| 118 | + defaultAnswer: PLACEHOLDERS.annoucementDate |
| 119 | + }); |
| 120 | + } |
| 121 | + |
| 122 | + getImpact(content) { |
| 123 | + const impact = content.reports.reduce((acc, report) => { |
| 124 | + for (const affectedVersion of report.affectedVersions) { |
| 125 | + if (acc[affectedVersion]) { |
| 126 | + acc[affectedVersion].push(report); |
| 127 | + } else { |
| 128 | + acc[affectedVersion] = [report]; |
| 129 | + } |
| 130 | + } |
| 131 | + return acc; |
| 132 | + }, {}); |
| 133 | + |
| 134 | + const impactText = []; |
| 135 | + for (const [key, value] of Object.entries(impact)) { |
| 136 | + const groupedByRating = Object.values(_.groupBy(value, 'severity.rating')) |
| 137 | + .map(severity => { |
| 138 | + const firstSeverityRating = severity[0].severity.rating.toLocaleLowerCase(); |
| 139 | + return `${severity.length} ${firstSeverityRating} severity issues`; |
| 140 | + }).join(', '); |
| 141 | + |
| 142 | + impactText.push(`The ${key} release line of Node.js is vulnerable to ${groupedByRating}.`); |
| 143 | + } |
| 144 | + |
| 145 | + return impactText.join('\n'); |
| 146 | + } |
| 147 | + |
| 148 | + getVulnerabilities(content) { |
| 149 | + const grouped = _.groupBy(content.reports, 'severity.rating'); |
| 150 | + const text = []; |
| 151 | + for (const [key, value] of Object.entries(grouped)) { |
| 152 | + text.push(`* ${value.length} ${key.toLocaleLowerCase()} severity issues.`); |
| 153 | + } |
| 154 | + return text.join('\n'); |
| 155 | + } |
| 156 | + |
| 157 | + getAffectedVersions(content) { |
| 158 | + const affectedVersions = new Set(); |
| 159 | + for (const report of Object.values(content.reports)) { |
| 160 | + for (const affectedVersion of report.affectedVersions) { |
| 161 | + affectedVersions.add(affectedVersion); |
| 162 | + } |
| 163 | + } |
| 164 | + return Array.from(affectedVersions).join(', '); |
| 165 | + } |
| 166 | + |
| 167 | + getSecurityPreReleaseTemplate() { |
| 168 | + return fs.readFileSync( |
| 169 | + new URL( |
| 170 | + './github/templates/security-pre-release.md', |
| 171 | + import.meta.url |
| 172 | + ), |
| 173 | + 'utf-8' |
| 174 | + ); |
| 175 | + } |
| 176 | +} |
0 commit comments