Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 24 additions & 4 deletions components/git/security.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import CLI from '../../lib/cli.js';
import SecurityReleaseSteward from '../../lib/prepare_security.js';
import UpdateSecurityRelease from '../../lib/update_security_release.js';
import SecurityBlog from '../../lib/security_blog.js';

export const command = 'security [options]';
export const describe = 'Manage an in-progress security release or start a new one.';
Expand All @@ -21,18 +22,23 @@ const securityOptions = {
'remove-report': {
describe: 'Removes a report from vulnerabilities.json',
type: 'string'
},
'pre-release': {
describe: 'Create the pre-release announcement',
type: 'boolean'
}
};

let yargsInstance;

export function builder(yargs) {
yargsInstance = yargs;
return yargs.options(securityOptions).example(
'git node security --start',
'Prepare a security release of Node.js')
return yargs.options(securityOptions)
.example(
'git node security --start',
'Prepare a security release of Node.js')
.example(
'git node security --update-date=31/12/2023',
'git node security --update-date=YYYY/MM/DD',
'Updates the target date of the security release'
)
.example(
Expand All @@ -42,6 +48,10 @@ export function builder(yargs) {
.example(
'git node security --remove-report=H1-ID',
'Removes the Hackerone report based on ID provided from vulnerabilities.json'
)
.example(
'git node security --pre-release' +
'Create the pre-release announcement on the Nodejs.org repo'
);
}

Expand All @@ -52,6 +62,9 @@ export function handler(argv) {
if (argv['update-date']) {
return updateReleaseDate(argv);
}
if (argv['pre-release']) {
return createPreRelease(argv);
}
if (argv['add-report']) {
return addReport(argv);
}
Expand Down Expand Up @@ -85,6 +98,13 @@ async function updateReleaseDate(argv) {
return update.updateReleaseDate(releaseDate);
}

async function createPreRelease() {
const logStream = process.stdout.isTTY ? process.stdout : process.stderr;
const cli = new CLI(logStream);
const preRelease = new SecurityBlog(cli);
return preRelease.createPreRelease();
}

async function startSecurityRelease() {
const logStream = process.stdout.isTTY ? process.stdout : process.stderr;
const cli = new CLI(logStream);
Expand Down
13 changes: 11 additions & 2 deletions docs/git-node.md
Original file line number Diff line number Diff line change
Expand Up @@ -457,13 +457,22 @@ This command creates the Next Security Issue in Node.js private repository
following the [Security Release Process][] document.
It will retrieve all the triaged HackerOne reports and add creates the `vulnerabilities.json`.

### `git node security --update-date=target`
### `git node security --update-date=YYYY/MM/DD`

This command updates the `vulnerabilities.json` with target date of the security release.
Example:

```sh
git node security --update-date=16/12/2023
git node security --update-date=2023/12/31
```

### `git node security --pre-release`

This command creates a pre-release announcement for the security release.
Example:

```sh
git node security --pre-release
```

### `git node security --add-report=report-id`
Expand Down
29 changes: 29 additions & 0 deletions lib/github/templates/security-pre-release.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
---
date: %ANNOUNCEMENT_DATE%
category: vulnerability
title: %RELEASE_DATE% Security Releases
slug: %SLUG%
layout: blog-post.hbs
author: The Node.js Project
---

# 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%
%OPENSSL_UPDATES%
## 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 <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.

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.
26 changes: 25 additions & 1 deletion lib/security-release/security-release.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { runSync } from '../run.js';
import nv from '@pkgjs/nv';
import fs from 'node:fs';
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';
Expand All @@ -14,7 +16,13 @@ export const PLACEHOLDERS = {
vulnerabilitiesPRURL: '%VULNERABILITIES_PR_URL%',
preReleasePrivate: '%PRE_RELEASE_PRIV%',
postReleasePrivate: '%POS_RELEASE_PRIV%',
affectedLines: '%AFFECTED_LINES%'
affectedLines: '%AFFECTED_LINES%',
annoucementDate: '%ANNOUNCEMENT_DATE%',
slug: '%SLUG%',
affectedVersions: '%AFFECTED_VERSIONS%',
openSSLUpdate: '%OPENSSL_UPDATES%',
impact: '%IMPACT%',
vulnerabilities: '%VULNERABILITIES%'
};

export function checkRemote(cli, repository) {
Expand Down Expand Up @@ -73,3 +81,19 @@ export async function getSummary(reportId, req) {
if (!summaries?.length) return;
return summaries?.[0].attributes?.content;
}

export function getVulnerabilitiesJSON(cli) {
const vulnerabilitiesJSONPath = path.join(process.cwd(),
NEXT_SECURITY_RELEASE_FOLDER, 'vulnerabilities.json');
cli.startSpinner(`Reading vulnerabilities.json from ${vulnerabilitiesJSONPath}..`);
const file = JSON.parse(fs.readFileSync(vulnerabilitiesJSONPath, 'utf-8'));
cli.stopSpinner(`Done reading vulnerabilities.json from ${vulnerabilitiesJSONPath}`);
return file;
}

export function validateDate(releaseDate) {
const value = new Date(releaseDate).valueOf();
if (Number.isNaN(value) || value < 0) {
throw new Error('Invalid date format');
}
}
176 changes: 176 additions & 0 deletions lib/security_blog.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
import fs from 'node:fs';
import path from 'node:path';
import _ from 'lodash';
import {
PLACEHOLDERS,
getVulnerabilitiesJSON,
checkoutOnSecurityReleaseBranch,
NEXT_SECURITY_RELEASE_REPOSITORY,
validateDate
} from './security-release/security-release.js';

export default class SecurityBlog {
repository = NEXT_SECURITY_RELEASE_REPOSITORY;
constructor(cli) {
this.cli = cli;
}

async createPreRelease() {
const { cli } = this;

// checkout on security release branch
checkoutOnSecurityReleaseBranch(cli, this.repository);

// read vulnerabilities JSON file
const content = getVulnerabilitiesJSON(cli);
// validate the release date read from vulnerabilities JSON
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.getSecurityPreReleaseTemplate();
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)
};
const month = releaseDate.toLocaleString('en-US', { month: 'long' }).toLowerCase();
const year = releaseDate.getFullYear();
const fileName = `${month}-${year}-security-releases.md`;
const preRelease = this.buildPreRelease(template, data);
const file = path.join(process.cwd(), fileName);
fs.writeFileSync(file, preRelease);
cli.ok(`Pre-release announcement file created at ${file}`);
}

promptOpenSSLUpdate(cli) {
return cli.prompt('Does this security release containt OpenSSL updates?', {
defaultAnswer: true
});
}

formatReleaseDate(releaseDate) {
const options = {
weekday: 'long',
month: 'long',
day: 'numeric',
year: 'numeric'
};
return releaseDate.toLocaleDateString('en-US', options);
}

buildPreRelease(template, data) {
const {
annoucementDate,
releaseDate,
affectedVersions,
vulnerabilities,
slug,
impact,
openSSLUpdate
} = 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));
}

getOpenSSLUpdateTemplate(openSSLUpdate) {
if (openSSLUpdate) {
return '\n## OpenSSL Security updates\n\n' +
'This security release includes OpenSSL security updates\n';
}
return '';
}

getSlug(releaseDate) {
const month = releaseDate.toLocaleString('en-US', { month: 'long' });
const year = releaseDate.getFullYear();
return `${month.toLocaleLowerCase()}-${year}-security-releases`;
}

async getAnnouncementDate(cli) {
try {
const date = await this.promptAnnouncementDate(cli);
validateDate(date);
return new Date(date).toISOString();
} catch (error) {
return PLACEHOLDERS.annoucementDate;
}
}

promptAnnouncementDate(cli) {
return cli.prompt('When is the security release going to be announced? ' +
'Enter in YYYY-MM-DD format:', {
questionType: 'input',
defaultAnswer: PLACEHOLDERS.annoucementDate
});
}

getImpact(content) {
const impact = content.reports.reduce((acc, report) => {
for (const affectedVersion of report.affectedVersions) {
if (acc[affectedVersion]) {
acc[affectedVersion].push(report);
} else {
acc[affectedVersion] = [report];
}
}
return acc;
}, {});

const impactText = [];
for (const [key, value] of Object.entries(impact)) {
const groupedByRating = Object.values(_.groupBy(value, 'severity.rating'))
.map(severity => {
const firstSeverityRating = severity[0].severity.rating.toLocaleLowerCase();
return `${severity.length} ${firstSeverityRating} severity issues`;
}).join(', ');

impactText.push(`The ${key} release line of Node.js is vulnerable to ${groupedByRating}.`);
}

return impactText.join('\n');
}

getVulnerabilities(content) {
const grouped = _.groupBy(content.reports, 'severity.rating');
const text = [];
for (const [key, value] of Object.entries(grouped)) {
text.push(`* ${value.length} ${key.toLocaleLowerCase()} severity issues.`);
}
return text.join('\n');
}

getAffectedVersions(content) {
const affectedVersions = new Set();
for (const report of Object.values(content.reports)) {
for (const affectedVersion of report.affectedVersions) {
affectedVersions.add(affectedVersion);
}
}
return Array.from(affectedVersions).join(', ');
}

getSecurityPreReleaseTemplate() {
return fs.readFileSync(
new URL(
'./github/templates/security-pre-release.md',
import.meta.url
),
'utf-8'
);
}
}
11 changes: 4 additions & 7 deletions lib/update_security_release.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import {
checkoutOnSecurityReleaseBranch,
commitAndPushVulnerabilitiesJSON,
getSupportedVersions,
getSummary
getSummary,
validateDate
} from './security-release/security-release.js';
import fs from 'node:fs';
import path from 'node:path';
Expand All @@ -21,13 +22,9 @@ export default class UpdateSecurityRelease {
const { cli } = this;

try {
const [day, month, year] = releaseDate.split('/');
const value = new Date(`${month}/${day}/${year}`).valueOf();
if (Number.isNaN(value) || value < 0) {
throw new Error('Invalid date format');
}
validateDate(releaseDate);
} catch (error) {
cli.error('Invalid date format. Please use the format dd/mm/yyyy.');
cli.error('Invalid date format. Please use the format yyyy/mm/dd.');
process.exit(1);
}

Expand Down