Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: request cve automatically #777

Merged
merged 12 commits into from
Apr 4, 2024
21 changes: 20 additions & 1 deletion components/git/security.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ const securityOptions = {
'notify-pre-release': {
describe: 'Notify the community about the security release',
type: 'boolean'
},
'request-cve': {
describe: 'Request CVEs for a security release',
type: 'boolean'
}
};

Expand Down Expand Up @@ -60,6 +64,11 @@ export function builder(yargs) {
).example(
'git node security --notify-pre-release' +
'Notifies the community about the security release'
)
.example(
'git node security --request-cve',
'Request CVEs for a security release of Node.js based on' +
' the next-security-release/vulnerabilities.json'
);
}

Expand All @@ -82,6 +91,9 @@ export function handler(argv) {
if (argv['notify-pre-release']) {
return notifyPreRelease(argv);
}
if (argv['request-cve']) {
return requestCVEs(argv);
}
yargsInstance.showHelp();
}

Expand Down Expand Up @@ -116,7 +128,14 @@ async function createPreRelease() {
return preRelease.createPreRelease();
}

async function startSecurityRelease() {
async function requestCVEs() {
const logStream = process.stdout.isTTY ? process.stdout : process.stderr;
const cli = new CLI(logStream);
const hackerOneCve = new UpdateSecurityRelease(cli);
return hackerOneCve.requestCVEs();
}

async function startSecurityRelease(argv) {
const logStream = process.stdout.isTTY ? process.stdout : process.stderr;
const cli = new CLI(logStream);
const release = new SecurityReleaseSteward(cli);
Expand Down
23 changes: 7 additions & 16 deletions lib/prepare_security.js
Original file line number Diff line number Diff line change
Expand Up @@ -173,23 +173,15 @@ class PrepareSecurityRelease {

for (const report of reports.data) {
const {
id, attributes: { title, cve_ids, created_at },
id, attributes: { title, cve_ids },
relationships: { severity, weakness, reporter }
} = report;
const link = `https://hackerone.com/reports/${id}`;
let reportSeverity = {
rating: '',
cvss_vector_string: '',
weakness_id: ''
const reportSeverity = {
rating: severity?.data?.attributes?.rating || '',
cvss_vector_string: severity?.data?.attributes?.cvss_vector_string || '',
weakness_id: weakness?.data?.id || ''
};
if (severity?.data?.attributes?.cvss_vector_string) {
const { cvss_vector_string, rating } = severity.data.attributes;
reportSeverity = {
cvss_vector_string,
rating,
weakness_id: weakness?.data?.id
};
}

cli.separator();
cli.info(`Report: ${link} - ${title} (${reportSeverity?.rating})`);
Expand All @@ -209,13 +201,12 @@ class PrepareSecurityRelease {
selectedReports.push({
id,
title,
cve_ids,
cveIds: cve_ids,
severity: reportSeverity,
summary: summaryContent ?? '',
affectedVersions: versions.split(',').map((v) => v.replace('v', '').trim()),
link,
reporter: reporter.data.attributes.username,
created_at // when we request CVE we need to input vulnerability_discovered_at
reporter: reporter.data.attributes.username
});
}
return selectedReports;
Expand Down
43 changes: 43 additions & 0 deletions lib/request.js
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,49 @@ export default class Request {
return this.json(url, options);
}

async getPrograms() {
const url = 'https://api.hackerone.com/v1/me/programs';
const options = {
method: 'GET',
headers: {
Authorization: `Basic ${this.credentials.h1}`,
'User-Agent': 'node-core-utils',
Accept: 'application/json'
}
};
return this.json(url, options);
}

async requestCVE(programId, opts) {
const url = `https://api.hackerone.com/v1/programs/${programId}/cve_requests`;
const options = {
method: 'POST',
headers: {
Authorization: `Basic ${this.credentials.h1}`,
'User-Agent': 'node-core-utils',
'Content-Type': 'application/json',
Accept: 'application/json'
},
body: JSON.stringify(opts)
};
return this.json(url, options);
}

async updateReportCVE(reportId, opts) {
const url = `https://api.hackerone.com/v1/reports/${reportId}/cves`;
const options = {
method: 'PUT',
headers: {
Authorization: `Basic ${this.credentials.h1}`,
'User-Agent': 'node-core-utils',
Accept: 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify(opts)
};
return this.json(url, options);
}

async getReport(reportId) {
const url = `https://api.hackerone.com/v1/reports/${reportId}`;
const options = {
Expand Down
182 changes: 175 additions & 7 deletions lib/update_security_release.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import fs from 'node:fs';
import path from 'node:path';
import auth from './auth.js';
import Request from './request.js';
import nv from '@pkgjs/nv';

export default class UpdateSecurityRelease {
repository = NEXT_SECURITY_RELEASE_REPOSITORY;
Expand All @@ -32,7 +33,7 @@ export default class UpdateSecurityRelease {
checkoutOnSecurityReleaseBranch(cli, this.repository);

// update the release date in the vulnerabilities.json file
const updatedVulnerabilitiesFiles = await this.updateVulnerabilitiesJSON(releaseDate, { cli });
const updatedVulnerabilitiesFiles = await this.updateJSONReleaseDate(releaseDate, { cli });

const commitMessage = `chore: update the release date to ${releaseDate}`;
commitAndPushVulnerabilitiesJSON(updatedVulnerabilitiesFiles,
Expand All @@ -56,7 +57,7 @@ export default class UpdateSecurityRelease {
NEXT_SECURITY_RELEASE_FOLDER, 'vulnerabilities.json');
}

async updateVulnerabilitiesJSON(releaseDate) {
async updateJSONReleaseDate(releaseDate) {
const vulnerabilitiesJSONPath = this.getVulnerabilitiesJSONPath();
const content = this.readVulnerabilitiesJSON(vulnerabilitiesJSONPath);
content.releaseDate = releaseDate;
Expand All @@ -80,9 +81,16 @@ export default class UpdateSecurityRelease {

// get h1 report
const { data: report } = await req.getReport(reportId);
const { id, attributes: { title, cve_ids }, relationships: { severity, reporter } } = report;
// if severity is not set on h1, set it to TBD
const reportLevel = severity ? severity.data.attributes.rating : 'TBD';
const {
id, attributes: { title, cve_ids },
relationships: { severity, reporter, weakness }
} = report;

const reportSeverity = {
rating: severity?.data?.attributes?.rating || '',
cvss_vector_string: severity?.data?.attributes?.cvss_vector_string || '',
weakness_id: weakness?.data?.id || ''
};

// get the affected versions
const supportedVersions = await getSupportedVersions();
Expand All @@ -97,8 +105,9 @@ export default class UpdateSecurityRelease {
const entry = {
id,
title,
cve_ids,
severity: reportLevel,
link: `https://hackerone.com/reports/${id}`,
cveIds: cve_ids,
severity: reportSeverity,
summary: summaryContent ?? '',
affectedVersions: versions.split(',').map((v) => v.replace('v', '').trim()),
reporter: reporter.data.attributes.username
Expand Down Expand Up @@ -135,4 +144,163 @@ export default class UpdateSecurityRelease {
commitMessage, { cli, repository: this.repository });
cli.ok('Done!');
}

async requestCVEs() {
const credentials = await auth({
github: true,
h1: true
});
const vulnerabilitiesJSONPath = this.getVulnerabilitiesJSONPath();
const content = this.readVulnerabilitiesJSON(vulnerabilitiesJSONPath);
const { reports } = content;
const req = new Request(credentials);
const programId = await this.getNodeProgramId(req);
const cves = await this.promptCVECreation(req, reports, programId);
this.assignCVEtoReport(cves, reports);
this.updateVulnerabilitiesJSON(content, vulnerabilitiesJSONPath);
this.updateHackonerReportCve(req, reports);
}

assignCVEtoReport(cves, reports) {
for (const cve of cves) {
const report = reports.find(report => report.id === cve.reportId);
report.cveIds = [cve.cve_identifier];
}
}

async updateHackonerReportCve(req, reports) {
for (const report of reports) {
const { id, cveIds } = report;
this.cli.startSpinner(`Updating report ${id} with CVEs ${cveIds}..`);
const body = {
data: {
type: 'report-cves',
attributes: {
cve_ids: cveIds
}
}
};
const response = await req.updateReportCVE(id, body);
if (response.errors) {
this.cli.error(`Error updating report ${id}`);
this.cli.error(JSON.stringify(response.errors, null, 2));
}
this.cli.stopSpinner(`Done updating report ${id} with CVEs ${cveIds}..`);
}
}

updateVulnerabilitiesJSON(content, vulnerabilitiesJSONPath) {
this.cli.startSpinner(`Updating vulnerabilities.json from\
${vulnerabilitiesJSONPath}..`);
const filePath = path.resolve(vulnerabilitiesJSONPath);
fs.writeFileSync(filePath, JSON.stringify(content, null, 2));
// push the changes to the repository
commitAndPushVulnerabilitiesJSON(filePath,
'chore: updated vulnerabilities.json with CVEs',
{ cli: this.cli, repository: this.repository });
this.cli.stopSpinner(`Done updating vulnerabilities.json from ${filePath}`);
}

async promptCVECreation(req, reports, programId) {
const supportedVersions = (await nv('supported'));
const cves = [];
for (const report of reports) {
const { id, summary, title, affectedVersions, cveIds, link } = report;
// skip if already has a CVE
// risky because the CVE associated might be
// mentioned in the report and not requested by Node
if (cveIds?.length) continue;

let severity = report.severity;

if (!severity.cvss_vector_string || !severity.weakness_id) {
try {
const h1Report = await req.getReport(id);
if (!h1Report.data.relationships.severity?.data.attributes.cvss_vector_string) {
throw new Error('No severity found');
}
severity = {
weakness_id: h1Report.data.relationships.weakness?.data.id,
cvss_vector_string:
h1Report.data.relationships.severity?.data.attributes.cvss_vector_string,
rating: h1Report.data.relationships.severity?.data.attributes.rating
};
} catch (error) {
this.cli.error(`Couldnt not retrieve severity from report ${id}, skipping...`);
continue;
}
}

const { cvss_vector_string, weakness_id } = severity;

const create = await this.cli.prompt(
`Request a CVE for: \n
Title: ${title}\n
Link: ${link}\n
Affected versions: ${affectedVersions.join(', ')}\n
Vector: ${cvss_vector_string}\n
Summary: ${summary}\n`,
{ defaultAnswer: true });

if (!create) continue;

const body = {
data: {
type: 'cve-request',
attributes: {
team_handle: 'nodejs-team',
versions: await this.formatAffected(affectedVersions, supportedVersions),
metrics: [
{
vectorString: cvss_vector_string
}
],
weakness_id: Number(weakness_id),
description: title,
vulnerability_discovered_at: new Date().toISOString()
}
}
};
const { data } = await req.requestCVE(programId, body);
if (data.errors) {
this.cli.error(`Error requesting CVE for report ${id}`);
this.cli.error(JSON.stringify(data.errors, null, 2));
continue;
}
const { cve_identifier } = data.attributes;
cves.push({ cve_identifier, reportId: id });
}
return cves;
}

async getNodeProgramId(req) {
const programs = await req.getPrograms();
const { data } = programs;
for (const program of data) {
const { attributes } = program;
if (attributes.handle === 'nodejs') {
return program.id;
}
}
}

async formatAffected(affectedVersions, supportedVersions) {
const result = [];
for (const affectedVersion of affectedVersions) {
const major = affectedVersion.split('.')[0];
const latest = supportedVersions.find((v) => v.major === Number(major)).version;
const version = await this.cli.prompt(
`What is the affected version (<=) for release line ${affectedVersion}?`,
{ questionType: 'input', defaultAnswer: latest });
result.push({
vendor: 'nodejs',
product: 'node',
func: '<=',
version,
versionType: 'semver',
affected: true
});
}
return result;
}
}
Loading