From 65b9f3d7b6f46fa75a65f843ccd458367631fbfc Mon Sep 17 00:00:00 2001 From: marco-ippolito Date: Tue, 30 Jan 2024 09:51:15 +0100 Subject: [PATCH 1/8] feat: post-release blogpost --- components/git/security.js | 33 +++-- lib/github/templates/security-pos-release.md | 38 ++++++ lib/security-release/security-release.js | 5 +- lib/security_blog.js | 131 ++++++++++++++++++- 4 files changed, 192 insertions(+), 15 deletions(-) create mode 100644 lib/github/templates/security-pos-release.md diff --git a/components/git/security.js b/components/git/security.js index 74fe1de9..3a0296d3 100644 --- a/components/git/security.js +++ b/components/git/security.js @@ -35,6 +35,10 @@ const securityOptions = { 'request-cve': { describe: 'Request CVEs for a security release', type: 'boolean' + }, + 'post-release': { + describe: 'Create the post-release announcement', + type: 'boolean' } }; @@ -45,20 +49,17 @@ export function builder(yargs) { return yargs.options(securityOptions) .example( 'git node security --start', - 'Prepare a security release of Node.js') - .example( + 'Prepare a security release of Node.js' + ).example( 'git node security --update-date=YYYY/MM/DD', 'Updates the target date of the security release' - ) - .example( + ).example( 'git node security --add-report=H1-ID', 'Fetches HackerOne report based on ID provided and adds it into vulnerabilities.json' - ) - .example( + ).example( 'git node security --remove-report=H1-ID', 'Removes the Hackerone report based on ID provided from vulnerabilities.json' - ) - .example( + ).example( 'git node security --pre-release' + 'Create the pre-release announcement on the Nodejs.org repo' ).example( @@ -69,6 +70,10 @@ export function builder(yargs) { 'git node security --request-cve', 'Request CVEs for a security release of Node.js based on' + ' the next-security-release/vulnerabilities.json' + ) + .example( + 'git node security --post-release' + + 'Create the post-release announcement on the Nodejs.org repo' ); } @@ -94,6 +99,9 @@ export function handler(argv) { if (argv['request-cve']) { return requestCVEs(argv); } + if (argv['post-release']) { + return createPostRelease(argv); + } yargsInstance.showHelp(); } @@ -135,7 +143,14 @@ async function requestCVEs() { return hackerOneCve.requestCVEs(); } -async function startSecurityRelease(argv) { +async function createPostRelease() { + const logStream = process.stdout.isTTY ? process.stdout : process.stderr; + const cli = new CLI(logStream); + const blog = new SecurityBlog(cli); + return blog.createPostRelease(); +} + +async function startSecurityRelease() { const logStream = process.stdout.isTTY ? process.stdout : process.stderr; const cli = new CLI(logStream); const release = new SecurityReleaseSteward(cli); diff --git a/lib/github/templates/security-pos-release.md b/lib/github/templates/security-pos-release.md new file mode 100644 index 00000000..208054d8 --- /dev/null +++ b/lib/github/templates/security-pos-release.md @@ -0,0 +1,38 @@ +--- +date: %ANNOUNCEMENT_DATE% +category: vulnerability +title: %RELEASE_DATE% Security Releases +slug: %SLUG% +layout: blog-post.hbs +author: %AUTHOR% +--- + +## Security releases available + +Updates are now available for the %AFFECTED_VERSIONS% Node.js release lines for the +following issues. +%DEPENDENCY_UPDATES% +%OPENSSL_UPDATES% +%REPORTS% +--- + +# Summary + +The Node.js project will release new versions of the %AFFECTED_VERSIONS% +releases lines on or shortly after, %RELEASE_DATE% in order to address: + +%VULNERABILITIES% + +## Impact + +%IMPACT% + +## Release timing + +Releases will be available on, or shortly after, %RELEASE_DATE%. + +## Contact and future updates + +The current Node.js security policy can be found at . Please follow the process outlined in if you wish to report a vulnerability in Node.js. + +Subscribe to the low-volume announcement-only nodejs-sec mailing list at to stay up to date on security vulnerabilities and security-related releases of Node.js and the projects maintained in the nodejs GitHub organization. diff --git a/lib/security-release/security-release.js b/lib/security-release/security-release.js index 00e6ddc3..88067154 100644 --- a/lib/security-release/security-release.js +++ b/lib/security-release/security-release.js @@ -22,7 +22,10 @@ export const PLACEHOLDERS = { affectedVersions: '%AFFECTED_VERSIONS%', openSSLUpdate: '%OPENSSL_UPDATES%', impact: '%IMPACT%', - vulnerabilities: '%VULNERABILITIES%' + vulnerabilities: '%VULNERABILITIES%', + reports: '%REPORTS%', + author: '%AUTHOR%', + dependencyUpdates: '%DEPENDENCY_UPDATES%' }; export function checkRemote(cli, repository) { diff --git a/lib/security_blog.js b/lib/security_blog.js index 89ddeb77..0af2f57b 100644 --- a/lib/security_blog.js +++ b/lib/security_blog.js @@ -52,12 +52,64 @@ export default class SecurityBlog { cli.ok(`Pre-release announcement file created at ${file}`); } + async createPostRelease() { + const { cli } = this; + + // checkout on security release branch + checkoutOnSecurityReleaseBranch(cli, this.repository); + + // read vulnerabilities JSON file + const content = getVulnerabilitiesJSON(cli); + if (!content.releaseDate) { + cli.error('Release date is not set in vulnerabilities.json,' + + ' run `git node security --update-date=YYYY/MM/DD` to set the release date.'); + process.exit(1); + } + + validateDate(content.releaseDate); + const releaseDate = new Date(content.releaseDate); + const template = this.getSecurityPosReleaseTemplate(); + const data = { + annoucementDate: await this.getAnnouncementDate(cli), + releaseDate: this.formatReleaseDate(releaseDate), + affectedVersions: this.getAffectedVersions(content), + vulnerabilities: this.getVulnerabilities(content), + slug: this.getSlug(releaseDate), + impact: this.getImpact(content), + openSSLUpdate: await this.promptOpenSSLUpdate(cli), + author: await this.promptAuthor(cli), + reports: content.reports, + dependencyUpdates: await this.promptDependencyUpdates(cli) + }; + const month = releaseDate.toLocaleString('en-US', { month: 'long' }).toLowerCase(); + const year = releaseDate.getFullYear(); + const fileName = `${month}-${year}-security-releases.md`; + const posRelease = await this.buildPosRelease(template, data); + const file = path.join(process.cwd(), fileName); + fs.writeFileSync(file, posRelease); + cli.ok(`Pos-release announcement file created at ${file}`); + } + + promptDependencyUpdates(cli) { + return cli.prompt('Does this security release contain dependency updates?', { + defaultAnswer: true + }); + } + promptOpenSSLUpdate(cli) { return cli.prompt('Does this security release containt OpenSSL updates?', { defaultAnswer: true }); } + promptAuthor(cli) { + return cli.prompt('Who is the author of this security release? If multiple' + + ' use & as separator', { + questionType: 'input', + defaultAnswer: PLACEHOLDERS.author + }); + } + formatReleaseDate(releaseDate) { const options = { weekday: 'long', @@ -87,12 +139,71 @@ export default class SecurityBlog { .replaceAll(PLACEHOLDERS.openSSLUpdate, this.getOpenSSLUpdateTemplate(openSSLUpdate)); } - getOpenSSLUpdateTemplate(openSSLUpdate) { - if (openSSLUpdate) { - return '\n## OpenSSL Security updates\n\n' + - 'This security release includes OpenSSL security updates\n'; + async buildPosRelease(template, data) { + const { + annoucementDate, + releaseDate, + affectedVersions, + vulnerabilities, + slug, + impact, + openSSLUpdate, + author, + reports, + dependencyUpdates + } = data; + return template.replaceAll(PLACEHOLDERS.annoucementDate, annoucementDate) + .replaceAll(PLACEHOLDERS.slug, slug) + .replaceAll(PLACEHOLDERS.affectedVersions, affectedVersions) + .replaceAll(PLACEHOLDERS.vulnerabilities, vulnerabilities) + .replaceAll(PLACEHOLDERS.releaseDate, releaseDate) + .replaceAll(PLACEHOLDERS.impact, impact) + .replaceAll(PLACEHOLDERS.openSSLUpdate, this.getOpenSSLUpdateTemplate(openSSLUpdate)) + .replaceAll(PLACEHOLDERS.author, author) + .replaceAll(PLACEHOLDERS.reports, await this.getReportsTemplate(reports)) + .replaceAll(PLACEHOLDERS.dependencyUpdates, + await this.getDependencyUpdatesTemplate(dependencyUpdates)); + } + + async getReportsTemplate(reports) { + let template = ''; + for (const report of reports) { + let cveId = report.cve_ids.join(', '); + if (!cveId) { + cveId = await this.cli.prompt(`What is the CVE ID for vulnerability https://hackerone.com/reports/${report.id} ${report.title}?`, { + questionType: 'input', + defaultAnswer: 'TBD' + }); + } + template += `\n## ${report.title} (${cveId}) - (${report.severity.rating})\n\n`; + template += `${report.summary}\n\n`; + const releaseLines = report.affectedVersions.join(', '); + template += `Impact:\n\n- This vulnerability affects all users\ + in active release lines: ${releaseLines}\n\n`; + let contributor = report.contributor; + if (!contributor) { + contributor = await this.cli.prompt(`Who fixed vulnerability https://hackerone.com/reports/${report.id} ${report.title}? If multiple use & as separator`, { + questionType: 'input', + defaultAnswer: 'TBD' + }); + } + template += `Thank you, to ${report.reporter} for reporting this vulnerability\ +and thank you ${contributor} for fixing it.\n`; } - return ''; + return template; + } + + async getDependencyUpdatesTemplate(dependencyUpdates) { + if (!dependencyUpdates) return ''; + const template = 'This security release includes the following dependency' + + ' updates to address public vulnerabilities:\n'; + return template; + } + + getOpenSSLUpdateTemplate(openSSLUpdate) { + if (!openSSLUpdate) return ''; + return '## OpenSSL Security updates\n\n' + + 'This security release includes OpenSSL security updates'; } getSlug(releaseDate) { @@ -179,4 +290,14 @@ export default class SecurityBlog { 'utf-8' ); } + + getSecurityPosReleaseTemplate() { + return fs.readFileSync( + new URL( + './github/templates/security-pos-release.md', + import.meta.url + ), + 'utf-8' + ); + } } From 8cb42e871569f2931c1cd5340b12e6a9b92cde82 Mon Sep 17 00:00:00 2001 From: marco-ippolito Date: Mon, 25 Mar 2024 18:06:39 +0100 Subject: [PATCH 2/8] fix: appending on existing pre-release --- lib/github/templates/security-pos-release.md | 20 --------- lib/security_blog.js | 47 ++++++++++++++++---- 2 files changed, 38 insertions(+), 29 deletions(-) diff --git a/lib/github/templates/security-pos-release.md b/lib/github/templates/security-pos-release.md index 208054d8..a97dfcd6 100644 --- a/lib/github/templates/security-pos-release.md +++ b/lib/github/templates/security-pos-release.md @@ -16,23 +16,3 @@ following issues. %REPORTS% --- -# Summary - -The Node.js project will release new versions of the %AFFECTED_VERSIONS% -releases lines on or shortly after, %RELEASE_DATE% in order to address: - -%VULNERABILITIES% - -## Impact - -%IMPACT% - -## Release timing - -Releases will be available on, or shortly after, %RELEASE_DATE%. - -## Contact and future updates - -The current Node.js security policy can be found at . Please follow the process outlined in if you wish to report a vulnerability in Node.js. - -Subscribe to the low-volume announcement-only nodejs-sec mailing list at to stay up to date on security vulnerabilities and security-related releases of Node.js and the projects maintained in the nodejs GitHub organization. diff --git a/lib/security_blog.js b/lib/security_blog.js index 0af2f57b..864d892d 100644 --- a/lib/security_blog.js +++ b/lib/security_blog.js @@ -75,19 +75,39 @@ export default class SecurityBlog { affectedVersions: this.getAffectedVersions(content), vulnerabilities: this.getVulnerabilities(content), slug: this.getSlug(releaseDate), - impact: this.getImpact(content), openSSLUpdate: await this.promptOpenSSLUpdate(cli), author: await this.promptAuthor(cli), reports: content.reports, dependencyUpdates: await this.promptDependencyUpdates(cli) }; - const month = releaseDate.toLocaleString('en-US', { month: 'long' }).toLowerCase(); - const year = releaseDate.getFullYear(); - const fileName = `${month}-${year}-security-releases.md`; const posRelease = await this.buildPosRelease(template, data); - const file = path.join(process.cwd(), fileName); - fs.writeFileSync(file, posRelease); - cli.ok(`Pos-release announcement file created at ${file}`); + + const pathPreRelease = await this.promptExistingPreRelease(cli); + // read the existing pre-release announcement + let preReleaseContent = fs.readFileSync(pathPreRelease, 'utf-8'); + // cut the part before summary + const preSummary = preReleaseContent.indexOf('# Summary'); + if (preSummary !== -1) { + preReleaseContent = preReleaseContent.substring(preSummary); + } + + const updatedContent = posRelease + preReleaseContent; + + fs.writeFileSync(pathPreRelease, updatedContent); + cli.ok(`Pos-release announcement file updated at ${pathPreRelease}`); + } + + async promptExistingPreRelease(cli) { + const pathPreRelease = await cli.prompt( + 'Please provide the path of the existing pre-release announcement:', { + questionType: 'input', + defaultAnswer: '' + }); + + if (!pathPreRelease || !fs.existsSync(path.resolve(pathPreRelease))) { + return this.promptExistingPreRelease(cli); + } + return pathPreRelease; } promptDependencyUpdates(cli) { @@ -170,12 +190,21 @@ export default class SecurityBlog { for (const report of reports) { let cveId = report.cve_ids.join(', '); if (!cveId) { + // TODO(@marco-ippolito): fetch the CVE ID from hackerone cveId = await this.cli.prompt(`What is the CVE ID for vulnerability https://hackerone.com/reports/${report.id} ${report.title}?`, { questionType: 'input', defaultAnswer: 'TBD' }); + // TODO(@marco-ippolito): save the cve_id in the vulnerabilities JSON + report.cve_ids = [cveId]; } template += `\n## ${report.title} (${cveId}) - (${report.severity.rating})\n\n`; + if (!report.summary) { + // TODO(@marco-ippolito): fetch the summary + // from hackerone and update the vulnerabilities JSON + this.cli.warn(`Summary is missing for vulnerability:\ + ${report.link}. Please add it manually.`); + } template += `${report.summary}\n\n`; const releaseLines = report.affectedVersions.join(', '); template += `Impact:\n\n- This vulnerability affects all users\ @@ -202,8 +231,8 @@ and thank you ${contributor} for fixing it.\n`; getOpenSSLUpdateTemplate(openSSLUpdate) { if (!openSSLUpdate) return ''; - return '## OpenSSL Security updates\n\n' + - 'This security release includes OpenSSL security updates'; + return '\n## OpenSSL Security updates\n\n' + + 'This security release includes OpenSSL security updates\n'; } getSlug(releaseDate) { From aeef518693a1d96646a32608af6dc92d2d646567 Mon Sep 17 00:00:00 2001 From: marco-ippolito Date: Tue, 26 Mar 2024 08:45:04 +0100 Subject: [PATCH 3/8] fix: reworded to post release --- components/git/security.js | 6 ++---- ...ity-pos-release.md => security-post-release.md} | 0 lib/security_blog.js | 14 +++++++------- 3 files changed, 9 insertions(+), 11 deletions(-) rename lib/github/templates/{security-pos-release.md => security-post-release.md} (100%) diff --git a/components/git/security.js b/components/git/security.js index 3a0296d3..bdb4b8f7 100644 --- a/components/git/security.js +++ b/components/git/security.js @@ -65,13 +65,11 @@ export function builder(yargs) { ).example( 'git node security --notify-pre-release' + 'Notifies the community about the security release' - ) - .example( + ).example( 'git node security --request-cve', 'Request CVEs for a security release of Node.js based on' + ' the next-security-release/vulnerabilities.json' - ) - .example( + ).example( 'git node security --post-release' + 'Create the post-release announcement on the Nodejs.org repo' ); diff --git a/lib/github/templates/security-pos-release.md b/lib/github/templates/security-post-release.md similarity index 100% rename from lib/github/templates/security-pos-release.md rename to lib/github/templates/security-post-release.md diff --git a/lib/security_blog.js b/lib/security_blog.js index 864d892d..1371c87c 100644 --- a/lib/security_blog.js +++ b/lib/security_blog.js @@ -68,7 +68,7 @@ export default class SecurityBlog { validateDate(content.releaseDate); const releaseDate = new Date(content.releaseDate); - const template = this.getSecurityPosReleaseTemplate(); + const template = this.getSecurityPostReleaseTemplate(); const data = { annoucementDate: await this.getAnnouncementDate(cli), releaseDate: this.formatReleaseDate(releaseDate), @@ -80,7 +80,7 @@ export default class SecurityBlog { reports: content.reports, dependencyUpdates: await this.promptDependencyUpdates(cli) }; - const posRelease = await this.buildPosRelease(template, data); + const postReleaseContent = await this.buildPostRelease(template, data); const pathPreRelease = await this.promptExistingPreRelease(cli); // read the existing pre-release announcement @@ -91,10 +91,10 @@ export default class SecurityBlog { preReleaseContent = preReleaseContent.substring(preSummary); } - const updatedContent = posRelease + preReleaseContent; + const updatedContent = postReleaseContent + preReleaseContent; fs.writeFileSync(pathPreRelease, updatedContent); - cli.ok(`Pos-release announcement file updated at ${pathPreRelease}`); + cli.ok(`Post-release announcement file updated at ${pathPreRelease}`); } async promptExistingPreRelease(cli) { @@ -159,7 +159,7 @@ export default class SecurityBlog { .replaceAll(PLACEHOLDERS.openSSLUpdate, this.getOpenSSLUpdateTemplate(openSSLUpdate)); } - async buildPosRelease(template, data) { + async buildPostRelease(template, data) { const { annoucementDate, releaseDate, @@ -320,10 +320,10 @@ and thank you ${contributor} for fixing it.\n`; ); } - getSecurityPosReleaseTemplate() { + getSecurityPostReleaseTemplate() { return fs.readFileSync( new URL( - './github/templates/security-pos-release.md', + './github/templates/security-post-release.md', import.meta.url ), 'utf-8' From 49c878ad735b5c7167d03d7d4c0a5e281285bc16 Mon Sep 17 00:00:00 2001 From: marco-ippolito Date: Tue, 26 Mar 2024 09:45:14 +0100 Subject: [PATCH 4/8] feat: update vulnerabilities.json --- lib/security_blog.js | 66 ++++++++++++++++++++++++++++++++++---------- 1 file changed, 52 insertions(+), 14 deletions(-) diff --git a/lib/security_blog.js b/lib/security_blog.js index 1371c87c..49b834f6 100644 --- a/lib/security_blog.js +++ b/lib/security_blog.js @@ -6,11 +6,17 @@ import { getVulnerabilitiesJSON, checkoutOnSecurityReleaseBranch, NEXT_SECURITY_RELEASE_REPOSITORY, - validateDate + validateDate, + getSummary, + commitAndPushVulnerabilitiesJSON, + NEXT_SECURITY_RELEASE_FOLDER } from './security-release/security-release.js'; +import auth from './auth.js'; +import Request from './request.js'; export default class SecurityBlog { repository = NEXT_SECURITY_RELEASE_REPOSITORY; + req; constructor(cli) { this.cli = cli; } @@ -54,6 +60,12 @@ export default class SecurityBlog { async createPostRelease() { const { cli } = this; + const credentials = await auth({ + github: true, + h1: true + }); + + this.req = new Request(credentials); // checkout on security release branch checkoutOnSecurityReleaseBranch(cli, this.repository); @@ -77,10 +89,9 @@ export default class SecurityBlog { slug: this.getSlug(releaseDate), openSSLUpdate: await this.promptOpenSSLUpdate(cli), author: await this.promptAuthor(cli), - reports: content.reports, dependencyUpdates: await this.promptDependencyUpdates(cli) }; - const postReleaseContent = await this.buildPostRelease(template, data); + const postReleaseContent = await this.buildPostRelease(template, data, content); const pathPreRelease = await this.promptExistingPreRelease(cli); // read the existing pre-release announcement @@ -95,6 +106,23 @@ export default class SecurityBlog { fs.writeFileSync(pathPreRelease, updatedContent); cli.ok(`Post-release announcement file updated at ${pathPreRelease}`); + this.updateVulnerabilitiesJSON(content); + } + + updateVulnerabilitiesJSON(content) { + try { + this.cli.info('Updating vulnerabilities.json'); + const vulnerabilitiesJSONPath = path.join(process.cwd(), + NEXT_SECURITY_RELEASE_FOLDER, 'vulnerabilities.json'); + fs.writeFileSync(vulnerabilitiesJSONPath, JSON.stringify(content, null, 2)); + const commitMessage = 'chore: updated vulnerabilities.json'; + commitAndPushVulnerabilitiesJSON(vulnerabilitiesJSONPath, + commitMessage, + { cli: this.cli, repository: this.repository }); + } catch (error) { + this.cli.error('Error updating vulnerabilities.json'); + this.cli.error(error); + } } async promptExistingPreRelease(cli) { @@ -159,7 +187,7 @@ export default class SecurityBlog { .replaceAll(PLACEHOLDERS.openSSLUpdate, this.getOpenSSLUpdateTemplate(openSSLUpdate)); } - async buildPostRelease(template, data) { + async buildPostRelease(template, data, content) { const { annoucementDate, releaseDate, @@ -169,7 +197,6 @@ export default class SecurityBlog { impact, openSSLUpdate, author, - reports, dependencyUpdates } = data; return template.replaceAll(PLACEHOLDERS.annoucementDate, annoucementDate) @@ -180,30 +207,41 @@ export default class SecurityBlog { .replaceAll(PLACEHOLDERS.impact, impact) .replaceAll(PLACEHOLDERS.openSSLUpdate, this.getOpenSSLUpdateTemplate(openSSLUpdate)) .replaceAll(PLACEHOLDERS.author, author) - .replaceAll(PLACEHOLDERS.reports, await this.getReportsTemplate(reports)) + .replaceAll(PLACEHOLDERS.reports, await this.getReportsTemplate(content)) .replaceAll(PLACEHOLDERS.dependencyUpdates, await this.getDependencyUpdatesTemplate(dependencyUpdates)); } - async getReportsTemplate(reports) { + async getReportsTemplate(content) { + const reports = content.reports; let template = ''; for (const report of reports) { - let cveId = report.cve_ids.join(', '); + let cveId = report.cve_ids?.join(', '); if (!cveId) { - // TODO(@marco-ippolito): fetch the CVE ID from hackerone + // ask for the CVE ID + // it should have been created with the step `--request-cve` cveId = await this.cli.prompt(`What is the CVE ID for vulnerability https://hackerone.com/reports/${report.id} ${report.title}?`, { questionType: 'input', defaultAnswer: 'TBD' }); - // TODO(@marco-ippolito): save the cve_id in the vulnerabilities JSON report.cve_ids = [cveId]; } template += `\n## ${report.title} (${cveId}) - (${report.severity.rating})\n\n`; if (!report.summary) { - // TODO(@marco-ippolito): fetch the summary - // from hackerone and update the vulnerabilities JSON - this.cli.warn(`Summary is missing for vulnerability:\ - ${report.link}. Please add it manually.`); + const fetchIt = await this.cli.prompt(`Summary missing for vulnerability https://hackerone.com/reports/${report.id} ${report.title}.\ + Do you want to try fetch it from HackerOne??`, { + questionType: 'confirm', + defaultAnswer: true + }); + + if (fetchIt) { + report.summary = await getSummary(report.id, this.req); + } + + if (!report.summary) { + this.cli.error(`Summary missing for vulnerability https://hackerone.com/reports/${report.id} ${report.title}. Please create it before continuing.`); + process.exit(1); + } } template += `${report.summary}\n\n`; const releaseLines = report.affectedVersions.join(', '); From 2fe2b9b7cc0d603f6935059fa5eb97bda8f50328 Mon Sep 17 00:00:00 2001 From: marco-ippolito Date: Tue, 26 Mar 2024 10:17:52 +0100 Subject: [PATCH 5/8] fix: if no changes dont update --- lib/security_blog.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/lib/security_blog.js b/lib/security_blog.js index 49b834f6..c56a6b71 100644 --- a/lib/security_blog.js +++ b/lib/security_blog.js @@ -14,6 +14,8 @@ import { import auth from './auth.js'; import Request from './request.js'; +const kChanged = Symbol('changed'); + export default class SecurityBlog { repository = NEXT_SECURITY_RELEASE_REPOSITORY; req; @@ -106,6 +108,9 @@ export default class SecurityBlog { fs.writeFileSync(pathPreRelease, updatedContent); cli.ok(`Post-release announcement file updated at ${pathPreRelease}`); + + // if the vulnerabilities.json has been changed, update the file + if (!content[kChanged]) return; this.updateVulnerabilitiesJSON(content); } @@ -225,6 +230,7 @@ export default class SecurityBlog { defaultAnswer: 'TBD' }); report.cve_ids = [cveId]; + content[kChanged] = true; } template += `\n## ${report.title} (${cveId}) - (${report.severity.rating})\n\n`; if (!report.summary) { @@ -236,6 +242,7 @@ export default class SecurityBlog { if (fetchIt) { report.summary = await getSummary(report.id, this.req); + content[kChanged] = true; } if (!report.summary) { From 21c2e655650bfcbc115289e1931d33ed1fb46076 Mon Sep 17 00:00:00 2001 From: marco-ippolito Date: Fri, 19 Apr 2024 10:47:36 +0200 Subject: [PATCH 6/8] fix: comments from release --- lib/github/templates/security-post-release.md | 3 +-- lib/github/templates/security-pre-release.md | 6 +++--- lib/security_blog.js | 2 +- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/lib/github/templates/security-post-release.md b/lib/github/templates/security-post-release.md index a97dfcd6..f7d653b4 100644 --- a/lib/github/templates/security-post-release.md +++ b/lib/github/templates/security-post-release.md @@ -3,7 +3,7 @@ date: %ANNOUNCEMENT_DATE% category: vulnerability title: %RELEASE_DATE% Security Releases slug: %SLUG% -layout: blog-post.hbs +layout: blog-post author: %AUTHOR% --- @@ -15,4 +15,3 @@ following issues. %OPENSSL_UPDATES% %REPORTS% --- - diff --git a/lib/github/templates/security-pre-release.md b/lib/github/templates/security-pre-release.md index a05507dc..26a809ce 100644 --- a/lib/github/templates/security-pre-release.md +++ b/lib/github/templates/security-pre-release.md @@ -24,7 +24,7 @@ Releases will be available on, or shortly after, %RELEASE_DATE%. ## Contact and future updates -The current Node.js security policy can be found at https://nodejs.org/en/security/. -Please follow the process outlined in https://github.com/nodejs/node/blob/master/SECURITY.md if you wish to report a vulnerability in Node.js. +The current Node.js security policy can be found at . +Please follow the process outlined in if you wish to report a vulnerability in Node.js. -Subscribe to the low-volume announcement-only nodejs-sec mailing list at https://groups.google.com/forum/#!forum/nodejs-sec to stay up to date on security vulnerabilities and security-related releases of Node.js and the projects maintained in the nodejs GitHub organization. +Subscribe to the low-volume announcement-only nodejs-sec mailing list at to stay up to date on security vulnerabilities and security-related releases of Node.js and the projects maintained in the nodejs GitHub organization. diff --git a/lib/security_blog.js b/lib/security_blog.js index c56a6b71..de94b8d1 100644 --- a/lib/security_blog.js +++ b/lib/security_blog.js @@ -262,7 +262,7 @@ export default class SecurityBlog { }); } template += `Thank you, to ${report.reporter} for reporting this vulnerability\ -and thank you ${contributor} for fixing it.\n`; + and thank you ${contributor} for fixing it.\n`; } return template; } From f65c680f365e446b29340ae247d9c20272da3ebb Mon Sep 17 00:00:00 2001 From: marco-ippolito Date: Fri, 19 Apr 2024 11:43:24 +0200 Subject: [PATCH 7/8] fix: improved strcture --- lib/github/templates/security-post-release.md | 6 +- lib/github/templates/security-pre-release.md | 2 +- lib/security-release/security-release.js | 13 ++-- lib/security_blog.js | 59 ++++++++----------- 4 files changed, 38 insertions(+), 42 deletions(-) diff --git a/lib/github/templates/security-post-release.md b/lib/github/templates/security-post-release.md index f7d653b4..e7a04016 100644 --- a/lib/github/templates/security-post-release.md +++ b/lib/github/templates/security-post-release.md @@ -12,6 +12,8 @@ author: %AUTHOR% Updates are now available for the %AFFECTED_VERSIONS% Node.js release lines for the following issues. %DEPENDENCY_UPDATES% -%OPENSSL_UPDATES% %REPORTS% ---- + +## Downloads and release details + +%DOWNLOADS% diff --git a/lib/github/templates/security-pre-release.md b/lib/github/templates/security-pre-release.md index 26a809ce..a2f91e54 100644 --- a/lib/github/templates/security-pre-release.md +++ b/lib/github/templates/security-pre-release.md @@ -13,7 +13,7 @@ The Node.js project will release new versions of the %AFFECTED_VERSIONS% releases lines on or shortly after, %RELEASE_DATE% in order to address: %VULNERABILITIES% -%OPENSSL_UPDATES% + ## Impact %IMPACT% diff --git a/lib/security-release/security-release.js b/lib/security-release/security-release.js index 88067154..570d8fb5 100644 --- a/lib/security-release/security-release.js +++ b/lib/security-release/security-release.js @@ -6,9 +6,14 @@ import path from 'node:path'; export const NEXT_SECURITY_RELEASE_BRANCH = 'next-security-release'; export const NEXT_SECURITY_RELEASE_FOLDER = 'security-release/next-security-release'; +// export const NEXT_SECURITY_RELEASE_REPOSITORY = { +// owner: 'nodejs-private', +// repo: 'security-release' +// }; + export const NEXT_SECURITY_RELEASE_REPOSITORY = { - owner: 'nodejs-private', - repo: 'security-release' + owner: 'marco-ippolito', + repo: 'test-repo-security-release' }; export const PLACEHOLDERS = { @@ -20,12 +25,12 @@ export const PLACEHOLDERS = { annoucementDate: '%ANNOUNCEMENT_DATE%', slug: '%SLUG%', affectedVersions: '%AFFECTED_VERSIONS%', - openSSLUpdate: '%OPENSSL_UPDATES%', impact: '%IMPACT%', vulnerabilities: '%VULNERABILITIES%', reports: '%REPORTS%', author: '%AUTHOR%', - dependencyUpdates: '%DEPENDENCY_UPDATES%' + dependencyUpdates: '%DEPENDENCY_UPDATES%', + downloads: '%DOWNLOADS%' }; export function checkRemote(cli, repository) { diff --git a/lib/security_blog.js b/lib/security_blog.js index de94b8d1..d952fdba 100644 --- a/lib/security_blog.js +++ b/lib/security_blog.js @@ -48,8 +48,7 @@ export default class SecurityBlog { affectedVersions: this.getAffectedVersions(content), vulnerabilities: this.getVulnerabilities(content), slug: this.getSlug(releaseDate), - impact: this.getImpact(content), - openSSLUpdate: await this.promptOpenSSLUpdate(cli) + impact: this.getImpact(content) }; const month = releaseDate.toLocaleString('en-US', { month: 'long' }).toLowerCase(); const year = releaseDate.getFullYear(); @@ -89,9 +88,8 @@ export default class SecurityBlog { affectedVersions: this.getAffectedVersions(content), vulnerabilities: this.getVulnerabilities(content), slug: this.getSlug(releaseDate), - openSSLUpdate: await this.promptOpenSSLUpdate(cli), author: await this.promptAuthor(cli), - dependencyUpdates: await this.promptDependencyUpdates(cli) + dependencyUpdates: content.dependencies }; const postReleaseContent = await this.buildPostRelease(template, data, content); @@ -143,18 +141,6 @@ export default class SecurityBlog { return pathPreRelease; } - promptDependencyUpdates(cli) { - return cli.prompt('Does this security release contain dependency updates?', { - defaultAnswer: true - }); - } - - promptOpenSSLUpdate(cli) { - return cli.prompt('Does this security release containt OpenSSL updates?', { - defaultAnswer: true - }); - } - promptAuthor(cli) { return cli.prompt('Who is the author of this security release? If multiple' + ' use & as separator', { @@ -180,16 +166,14 @@ export default class SecurityBlog { affectedVersions, vulnerabilities, slug, - impact, - openSSLUpdate + impact } = data; return template.replaceAll(PLACEHOLDERS.annoucementDate, annoucementDate) .replaceAll(PLACEHOLDERS.slug, slug) .replaceAll(PLACEHOLDERS.affectedVersions, affectedVersions) .replaceAll(PLACEHOLDERS.vulnerabilities, vulnerabilities) .replaceAll(PLACEHOLDERS.releaseDate, releaseDate) - .replaceAll(PLACEHOLDERS.impact, impact) - .replaceAll(PLACEHOLDERS.openSSLUpdate, this.getOpenSSLUpdateTemplate(openSSLUpdate)); + .replaceAll(PLACEHOLDERS.impact, impact); } async buildPostRelease(template, data, content) { @@ -200,7 +184,6 @@ export default class SecurityBlog { vulnerabilities, slug, impact, - openSSLUpdate, author, dependencyUpdates } = data; @@ -210,11 +193,11 @@ export default class SecurityBlog { .replaceAll(PLACEHOLDERS.vulnerabilities, vulnerabilities) .replaceAll(PLACEHOLDERS.releaseDate, releaseDate) .replaceAll(PLACEHOLDERS.impact, impact) - .replaceAll(PLACEHOLDERS.openSSLUpdate, this.getOpenSSLUpdateTemplate(openSSLUpdate)) .replaceAll(PLACEHOLDERS.author, author) .replaceAll(PLACEHOLDERS.reports, await this.getReportsTemplate(content)) .replaceAll(PLACEHOLDERS.dependencyUpdates, - await this.getDependencyUpdatesTemplate(dependencyUpdates)); + this.getDependencyUpdatesTemplate(dependencyUpdates)) + .replaceAll(PLACEHOLDERS.downloads, this.getDownloadsTemplate()); } async getReportsTemplate(content) { @@ -232,7 +215,7 @@ export default class SecurityBlog { report.cve_ids = [cveId]; content[kChanged] = true; } - template += `\n## ${report.title} (${cveId}) - (${report.severity.rating})\n\n`; + template += `## ${report.title} (${cveId}) - (${report.severity.rating})\n\n`; if (!report.summary) { const fetchIt = await this.cli.prompt(`Summary missing for vulnerability https://hackerone.com/reports/${report.id} ${report.title}.\ Do you want to try fetch it from HackerOne??`, { @@ -254,30 +237,36 @@ export default class SecurityBlog { const releaseLines = report.affectedVersions.join(', '); template += `Impact:\n\n- This vulnerability affects all users\ in active release lines: ${releaseLines}\n\n`; - let contributor = report.contributor; - if (!contributor) { - contributor = await this.cli.prompt(`Who fixed vulnerability https://hackerone.com/reports/${report.id} ${report.title}? If multiple use & as separator`, { + if (!report.patchAuthors) { + const author = await this.cli.prompt(`Who fixed vulnerability https://hackerone.com/reports/${report.id} ${report.title}? If multiple use & as separator`, { questionType: 'input', defaultAnswer: 'TBD' }); + report.patchAuthors = author.split('&').map((p) => p.trim()); + content[kChanged] = true; } template += `Thank you, to ${report.reporter} for reporting this vulnerability\ - and thank you ${contributor} for fixing it.\n`; + and thank you ${report.patchAuthors.join(' and ')} for fixing it.\n\n`; } return template; } - async getDependencyUpdatesTemplate(dependencyUpdates) { + getDependencyUpdatesTemplate(dependencyUpdates) { if (!dependencyUpdates) return ''; - const template = 'This security release includes the following dependency' + - ' updates to address public vulnerabilities:\n'; + let template = 'This security release includes the following dependency' + + ' updates to address public vulnerabilities:\n\n'; + for (const dependencyUpdate of Object.values(dependencyUpdates)) { + for (const dependency of dependencyUpdate) { + const title = dependency.title.substring(dependency.title.indexOf(':') + ':'.length).trim(); + template += `- ${title}\ + on ${dependency.affectedVersions.join(', ')}\n`; + } + } return template; } - getOpenSSLUpdateTemplate(openSSLUpdate) { - if (!openSSLUpdate) return ''; - return '\n## OpenSSL Security updates\n\n' + - 'This security release includes OpenSSL security updates\n'; + getDownloadsTemplate() { + return ''; } getSlug(releaseDate) { From 585007a651984ca446e580f65fff04bd2e9a60da Mon Sep 17 00:00:00 2001 From: marco-ippolito Date: Fri, 19 Apr 2024 11:56:25 +0200 Subject: [PATCH 8/8] fix: download section --- lib/github/templates/security-post-release.md | 3 +-- lib/security-release/security-release.js | 9 ++------- lib/security_blog.js | 16 +++++++++++++--- 3 files changed, 16 insertions(+), 12 deletions(-) diff --git a/lib/github/templates/security-post-release.md b/lib/github/templates/security-post-release.md index e7a04016..39b15008 100644 --- a/lib/github/templates/security-post-release.md +++ b/lib/github/templates/security-post-release.md @@ -1,7 +1,7 @@ --- date: %ANNOUNCEMENT_DATE% category: vulnerability -title: %RELEASE_DATE% Security Releases +title: %RELEASE_DATE% Security Releases slug: %SLUG% layout: blog-post author: %AUTHOR% @@ -13,7 +13,6 @@ Updates are now available for the %AFFECTED_VERSIONS% Node.js release lines for following issues. %DEPENDENCY_UPDATES% %REPORTS% - ## Downloads and release details %DOWNLOADS% diff --git a/lib/security-release/security-release.js b/lib/security-release/security-release.js index 570d8fb5..dddec4f9 100644 --- a/lib/security-release/security-release.js +++ b/lib/security-release/security-release.js @@ -6,14 +6,9 @@ import path from 'node:path'; export const NEXT_SECURITY_RELEASE_BRANCH = 'next-security-release'; export const NEXT_SECURITY_RELEASE_FOLDER = 'security-release/next-security-release'; -// export const NEXT_SECURITY_RELEASE_REPOSITORY = { -// owner: 'nodejs-private', -// repo: 'security-release' -// }; - export const NEXT_SECURITY_RELEASE_REPOSITORY = { - owner: 'marco-ippolito', - repo: 'test-repo-security-release' + owner: 'nodejs-private', + repo: 'security-release' }; export const PLACEHOLDERS = { diff --git a/lib/security_blog.js b/lib/security_blog.js index d952fdba..217778a2 100644 --- a/lib/security_blog.js +++ b/lib/security_blog.js @@ -1,6 +1,7 @@ import fs from 'node:fs'; import path from 'node:path'; import _ from 'lodash'; +import nv from '@pkgjs/nv'; import { PLACEHOLDERS, getVulnerabilitiesJSON, @@ -197,7 +198,7 @@ export default class SecurityBlog { .replaceAll(PLACEHOLDERS.reports, await this.getReportsTemplate(content)) .replaceAll(PLACEHOLDERS.dependencyUpdates, this.getDependencyUpdatesTemplate(dependencyUpdates)) - .replaceAll(PLACEHOLDERS.downloads, this.getDownloadsTemplate()); + .replaceAll(PLACEHOLDERS.downloads, await this.getDownloadsTemplate(affectedVersions)); } async getReportsTemplate(content) { @@ -265,8 +266,17 @@ export default class SecurityBlog { return template; } - getDownloadsTemplate() { - return ''; + async getDownloadsTemplate(affectedVersions) { + let template = ''; + const versionsToBeReleased = (await nv('supported')).filter( + (v) => affectedVersions.split(', ').includes(`${v.major}.x`) + ); + for (const version of versionsToBeReleased) { + const v = `v${version.major}.${version.minor}.${Number(version.patch) + 1}`; + template += `- [Node.js ${v}](/blog/release/${v}/)\n`; + } + + return template; } getSlug(releaseDate) {