diff --git a/src/Audit/AuditOSSIndex.ts b/src/Audit/AuditOSSIndex.ts index 5ef0c104..98cffabc 100644 --- a/src/Audit/AuditOSSIndex.ts +++ b/src/Audit/AuditOSSIndex.ts @@ -13,172 +13,34 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { OssIndexServerResult, Vulnerability } from '../Types/OssIndexServerResult'; -import chalk from 'chalk'; -import * as builder from 'xmlbuilder'; +import { OssIndexServerResult } from '../Types/OssIndexServerResult'; +import { Formatter, getNumberOfVulnerablePackagesFromResults } from './Formatters/Formatter'; +import { JsonFormatter } from './Formatters/JsonFormatter'; +import { TextFormatter } from './Formatters/TextFormatter'; +import { XmlFormatter } from './Formatters/XmlFormatter'; export class AuditOSSIndex { - constructor(readonly quiet: boolean = false, readonly json: boolean = false, readonly xml: boolean = false) {} + private formatter: Formatter; - public auditResults(results: Array): boolean { - if (this.json) { - return this.printJson(results); - } - if (this.xml) { - return this.printJUnitXML(results); - } - - const total = results.length; - results = results.sort((a, b) => { - return a.coordinates < b.coordinates ? -1 : 1; - }); - - console.log(); - console.group(); - this.printLine('Sonabot here, beep boop beep boop, here are your Sonatype OSS Index results:'); - this.suggestIncludeDevDeps(total); - console.groupEnd(); - console.log(); - - this.printLine('-'.repeat(process.stdout.columns)); - - let isVulnerable = false; - - results.forEach((x: OssIndexServerResult, i: number) => { - if (x.vulnerabilities && x.vulnerabilities.length > 0) { - isVulnerable = true; - this.printVulnerability(i, total, x); - } else { - this.printLine(chalk.keyword('green')(`[${i + 1}/${total}] - ${x.toAuditLog()}`)); - } - }); - - this.printLine('-'.repeat(process.stdout.columns)); - - return isVulnerable; - } - - private printJson(results: Array): boolean { - console.log(JSON.stringify(results, null, 2)); - - if (this.getNumberOfVulnerablePackagesFromResults(results) > 0) { - return true; - } - return false; - } - - private printJUnitXML(results: Array): boolean { - const testsuite = builder.create('testsuite'); - testsuite.att('tests', results.length); - testsuite.att('timestamp', new Date().toISOString()); - testsuite.att('failures', this.getNumberOfVulnerablePackagesFromResults(results)); - - for (let i = 0; i < results.length; i++) { - const testcase = testsuite.ele('testcase', { classname: results[i].coordinates, name: results[i].coordinates }); - const vulns = results[i].vulnerabilities; - - if (vulns) { - if (vulns.length > 0) { - const failure = testcase.ele('failure'); - let failureText = ''; - for (let j = 0; j < vulns.length; j++) { - failureText += this.getVulnerabilityForXmlBlock(vulns[j]) + '\n'; - } - failure.text(failureText); - failure.att('type', 'Vulnerability detected'); - } - } - } - - const xml = testsuite.end({ pretty: true }); - - console.log(xml); - - if (this.getNumberOfVulnerablePackagesFromResults(results) > 0) { - return true; + constructor(readonly quiet: boolean = false, readonly json: boolean = false, readonly xml: boolean = false) { + if (json) { + this.formatter = new JsonFormatter(); + } else if (xml) { + this.formatter = new XmlFormatter(); + } else { + this.formatter = new TextFormatter(quiet); } - return false; - } - - private getNumberOfVulnerablePackagesFromResults(results: Array): number { - return results.filter((x) => { - return x.vulnerabilities && x.vulnerabilities?.length > 0; - }).length; } - private getVulnerabilityForXmlBlock(vuln: Vulnerability): string { - let vulnBlock = ''; - vulnBlock += `Vulnerability Title: ${vuln.title}\n`; - vulnBlock += `ID: ${vuln.id}\n`; - vulnBlock += `Description: ${vuln.description}\n`; - vulnBlock += `CVSS Score: ${vuln.cvssScore}\n`; - vulnBlock += `CVSS Vector: ${vuln.cvssVector}\n`; - vulnBlock += `CVE: ${vuln.cve}\n`; - vulnBlock += `Reference: ${vuln.reference}\n`; - - return vulnBlock; - } - - private getColorFromMaxScore(maxScore: number, defaultColor = 'chartreuse'): string { - if (maxScore > 8) { - defaultColor = 'red'; - } else if (maxScore > 6) { - defaultColor = 'orange'; - } else if (maxScore > 4) { - defaultColor = 'yellow'; - } - return defaultColor; - } - - private printVulnerability(i: number, total: number, result: OssIndexServerResult): void { - if (!result.vulnerabilities) { - return; - } - const maxScore: number = Math.max( - ...result.vulnerabilities.map((x: Vulnerability) => { - return +x.cvssScore; - }), - ); - const printVuln = (x: Array): void => { - x.forEach((y: Vulnerability) => { - const color: string = this.getColorFromMaxScore(+y.cvssScore); - console.group(); - console.log(chalk.keyword(color)(`Vulnerability Title: `), `${y.title}`); - console.log(chalk.keyword(color)(`ID: `), `${y.id}`); - console.log(chalk.keyword(color)(`Description: `), `${y.description}`); - console.log(chalk.keyword(color)(`CVSS Score: `), `${y.cvssScore}`); - console.log(chalk.keyword(color)(`CVSS Vector: `), `${y.cvssVector}`); - console.log(chalk.keyword(color)(`CVE: `), `${y.cve}`); - console.log(chalk.keyword(color)(`Reference: `), `${y.reference}`); - console.log(); - console.groupEnd(); + public auditResults(results: Array): boolean { + if (this.quiet) { + results = results.filter((x) => { + return x.vulnerabilities && x.vulnerabilities?.length > 0; }); - }; - - console.log( - chalk.keyword(this.getColorFromMaxScore(maxScore)).bold(`[${i + 1}/${total}] - ${result.toAuditLog()}`), - ); - console.log(); - result.vulnerabilities && - printVuln( - result.vulnerabilities.sort((x, y) => { - return +y.cvssScore - +x.cvssScore; - }), - ); - } - - private printLine(line: any): void { - if (!this.quiet) { - console.log(line); } - } - private suggestIncludeDevDeps(total: number) { - this.printLine(`Total dependencies audited: ${total}`); - if (total == 0) { - this.printLine( - `We noticed you had 0 dependencies, we exclude devDependencies by default, try running with --dev if you want to include those as well`, - ); - } + this.formatter.printAuditResults(results); + + return getNumberOfVulnerablePackagesFromResults(results) > 0; } } diff --git a/src/Audit/Formatters/Formatter.ts b/src/Audit/Formatters/Formatter.ts new file mode 100644 index 00000000..069538cb --- /dev/null +++ b/src/Audit/Formatters/Formatter.ts @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2020-present Sonatype, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { OssIndexServerResult } from '../../Types/OssIndexServerResult'; + +export interface Formatter { + printAuditResults(list: Array): void; +} + +export const getNumberOfVulnerablePackagesFromResults = (results: Array): number => { + return results.filter((x) => { + return x.vulnerabilities && x.vulnerabilities?.length > 0; + }).length; +}; diff --git a/src/Audit/Formatters/JsonFormatter.ts b/src/Audit/Formatters/JsonFormatter.ts new file mode 100644 index 00000000..a136aa37 --- /dev/null +++ b/src/Audit/Formatters/JsonFormatter.ts @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2020-present Sonatype, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { Formatter } from './Formatter'; +import { OssIndexServerResult } from '../../Types/OssIndexServerResult'; + +export class JsonFormatter implements Formatter { + public printAuditResults(list: Array) { + console.log(JSON.stringify(list, null, 2)); + } +} diff --git a/src/Audit/Formatters/TextFormatter.ts b/src/Audit/Formatters/TextFormatter.ts new file mode 100644 index 00000000..5618f84c --- /dev/null +++ b/src/Audit/Formatters/TextFormatter.ts @@ -0,0 +1,111 @@ +/* + * Copyright (c) 2020-present Sonatype, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { Formatter } from './Formatter'; +import { OssIndexServerResult, Vulnerability } from '../../Types/OssIndexServerResult'; +import chalk = require('chalk'); + +export class TextFormatter implements Formatter { + constructor(readonly quiet: boolean = false) {} + + public printAuditResults(list: Array) { + const total = list.length; + list = list.sort((a, b) => { + return a.coordinates < b.coordinates ? -1 : 1; + }); + + console.log(); + console.group(); + this.printLine('Sonabot here, beep boop beep boop, here are your Sonatype OSS Index results:'); + this.suggestIncludeDevDeps(total); + console.groupEnd(); + console.log(); + + this.printLine('-'.repeat(process.stdout.columns)); + + list.forEach((x: OssIndexServerResult, i: number) => { + if (x.vulnerabilities && x.vulnerabilities.length > 0) { + this.printVulnerability(i, total, x); + } else { + this.printLine(chalk.keyword('green')(`[${i + 1}/${total}] - ${x.toAuditLog()}`)); + } + }); + + this.printLine('-'.repeat(process.stdout.columns)); + } + + private getColorFromMaxScore(maxScore: number, defaultColor = 'chartreuse'): string { + if (maxScore > 8) { + defaultColor = 'red'; + } else if (maxScore > 6) { + defaultColor = 'orange'; + } else if (maxScore > 4) { + defaultColor = 'yellow'; + } + return defaultColor; + } + + private printVulnerability(i: number, total: number, result: OssIndexServerResult): void { + if (!result.vulnerabilities) { + return; + } + const maxScore: number = Math.max( + ...result.vulnerabilities.map((x: Vulnerability) => { + return +x.cvssScore; + }), + ); + const printVuln = (x: Array): void => { + x.forEach((y: Vulnerability) => { + const color: string = this.getColorFromMaxScore(+y.cvssScore); + console.group(); + console.log(chalk.keyword(color)(`Vulnerability Title: `), `${y.title}`); + console.log(chalk.keyword(color)(`ID: `), `${y.id}`); + console.log(chalk.keyword(color)(`Description: `), `${y.description}`); + console.log(chalk.keyword(color)(`CVSS Score: `), `${y.cvssScore}`); + console.log(chalk.keyword(color)(`CVSS Vector: `), `${y.cvssVector}`); + console.log(chalk.keyword(color)(`CVE: `), `${y.cve}`); + console.log(chalk.keyword(color)(`Reference: `), `${y.reference}`); + console.log(); + console.groupEnd(); + }); + }; + + console.log( + chalk.keyword(this.getColorFromMaxScore(maxScore)).bold(`[${i + 1}/${total}] - ${result.toAuditLog()}`), + ); + console.log(); + result.vulnerabilities && + printVuln( + result.vulnerabilities.sort((x, y) => { + return +y.cvssScore - +x.cvssScore; + }), + ); + } + + private printLine(line: any): void { + if (!this.quiet) { + console.log(line); + } + } + + private suggestIncludeDevDeps(total: number) { + this.printLine(`Total dependencies audited: ${total}`); + if (total == 0) { + this.printLine( + `We noticed you had 0 dependencies, we exclude devDependencies by default, try running with --dev if you want to include those as well`, + ); + } + } +} diff --git a/src/Audit/Formatters/XmlFormatter.ts b/src/Audit/Formatters/XmlFormatter.ts new file mode 100644 index 00000000..0373b02e --- /dev/null +++ b/src/Audit/Formatters/XmlFormatter.ts @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2020-present Sonatype, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import * as builder from 'xmlbuilder'; + +import { Formatter, getNumberOfVulnerablePackagesFromResults } from './Formatter'; +import { OssIndexServerResult, Vulnerability } from '../../Types/OssIndexServerResult'; + +export class XmlFormatter implements Formatter { + public printAuditResults(list: Array) { + const testsuite = builder.create('testsuite'); + testsuite.att('tests', list.length); + testsuite.att('timestamp', new Date().toISOString()); + testsuite.att('failures', getNumberOfVulnerablePackagesFromResults(list)); + + for (let i = 0; i < list.length; i++) { + const testcase = testsuite.ele('testcase', { classname: list[i].coordinates, name: list[i].coordinates }); + const vulns = list[i].vulnerabilities; + + if (vulns) { + if (vulns.length > 0) { + const failure = testcase.ele('failure'); + let failureText = ''; + for (let j = 0; j < vulns.length; j++) { + failureText += this.getVulnerabilityForXmlBlock(vulns[j]) + '\n'; + } + failure.text(failureText); + failure.att('type', 'Vulnerability detected'); + } + } + } + + const xml = testsuite.end({ pretty: true }); + + console.log(xml); + } + + private getVulnerabilityForXmlBlock(vuln: Vulnerability): string { + let vulnBlock = ''; + vulnBlock += `Vulnerability Title: ${vuln.title}\n`; + vulnBlock += `ID: ${vuln.id}\n`; + vulnBlock += `Description: ${vuln.description}\n`; + vulnBlock += `CVSS Score: ${vuln.cvssScore}\n`; + vulnBlock += `CVSS Vector: ${vuln.cvssVector}\n`; + vulnBlock += `CVE: ${vuln.cve}\n`; + vulnBlock += `Reference: ${vuln.reference}\n`; + + return vulnBlock; + } +}