Skip to content

Commit

Permalink
feat: Render the actionable advice via new formatter
Browse files Browse the repository at this point in the history
  • Loading branch information
lili2311 committed Aug 3, 2019
1 parent 0007d0f commit 3803df8
Show file tree
Hide file tree
Showing 8 changed files with 1,074 additions and 16 deletions.
3 changes: 2 additions & 1 deletion src/cli/commands/test/formatters/legacy-format-issue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ import * as config from '../../../../lib/config';
import {Options, TestOptions} from '../../../../lib/types';
import {isLocalFolder} from '../../../../lib/detect';
import { WIZARD_SUPPORTED_PACKAGE_MANAGERS } from '../../../../lib/package-managers';
import { GroupedVuln } from '../../../../lib/snyk-test/legacy';

export function formatIssues(vuln, options: Options & TestOptions) {
export function formatIssues(vuln: GroupedVuln, options: Options & TestOptions) {
const vulnID = vuln.list[0].id;
const packageManager = options.packageManager;
const localPackageTest = isLocalFolder(options.path);
Expand Down
150 changes: 150 additions & 0 deletions src/cli/commands/test/formatters/remediation-based-format-issues.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
import * as _ from 'lodash';
import chalk from 'chalk';
import { TestOptions } from '../../../../lib/types';
import { RemediationResult, PatchRemediation,
DependencyUpdates, IssueData, SEVERITY, GroupedVuln } from '../../../../lib/snyk-test/legacy';

interface BasicVulnInfo {
title: string;
severity: SEVERITY;
isNew: boolean;
name: string;
version: string;
fixedIn: string[];
}

export function formatIssuesWithRemediation(
vulns: GroupedVuln[],
remediationInfo: RemediationResult,
options: TestOptions,
): string[] {

const basicVulnInfo: {
[name: string]: BasicVulnInfo,
} = {};

for (const vuln of vulns) {
basicVulnInfo[vuln.metadata.id] = {
title: vuln.title,
severity: vuln.severity,
isNew: vuln.isNew,
name: vuln.name,
version: vuln.version,
fixedIn: vuln.fixedIn,
};
}
const results = [chalk.bold.white('Remediation advice')];

const upgradeTextArray = constructUpgradesText(remediationInfo.upgrade, basicVulnInfo);
if (upgradeTextArray.length > 0) {
results.push(upgradeTextArray.join('\n'));
}

const patchedTextArray = constructPatchesText(remediationInfo.patch, basicVulnInfo);

if (patchedTextArray.length > 0) {
results.push(patchedTextArray.join('\n'));
}

const unfixableIssuesTextArray = constructUnfixableText(remediationInfo.unresolved);

if (unfixableIssuesTextArray.length > 0) {
results.push(unfixableIssuesTextArray.join('\n'));
}

return results;
}

function constructPatchesText(
patches: {
[name: string]: PatchRemediation;
},
basicVulnInfo: {
[name: string]: BasicVulnInfo;
},
): string[] {

if (!(Object.keys(patches).length > 0)) {
return [];
}
const patchedTextArray = [chalk.bold.green('Patchable issues:')];
for (const id of Object.keys(patches)) {
// todo: add vulnToPatch package name
const packageAtVersion = `${basicVulnInfo[id].name}@${basicVulnInfo[id].version}`;
const patchedText = `\n Patch available for ${chalk.bold.whiteBright(packageAtVersion)}\n`;
const thisPatchFixes =
formatIssue(id, basicVulnInfo[id].title, basicVulnInfo[id].severity, basicVulnInfo[id].isNew);
patchedTextArray.push(patchedText + thisPatchFixes);
}

return patchedTextArray;
}

function constructUpgradesText(
upgrades: DependencyUpdates,
basicVulnInfo: {
[name: string]: BasicVulnInfo;
},
): string[] {

if (!(Object.keys(upgrades).length > 0)) {
return [];
}

const upgradeTextArray = [chalk.bold.green('Upgradable Issues:')];
for (const upgrade of Object.keys(upgrades)) {
const upgradeDepTo = _.get(upgrades, [upgrade, 'upgradeTo']);
const vulnIds = _.get(upgrades, [upgrade, 'vulns']);
const upgradeText =
`\n Upgrade ${chalk.bold.whiteBright(upgrade)} to ${chalk.bold.whiteBright(upgradeDepTo)} to fix\n`;
const thisUpgradeFixes = vulnIds
.map((id) => formatIssue(
id,
basicVulnInfo[id].title, basicVulnInfo[id].severity, basicVulnInfo[id].isNew))
.join('\n');
upgradeTextArray.push(upgradeText + thisUpgradeFixes);
}
return upgradeTextArray;
}

function constructUnfixableText(unresolved: IssueData[]) {
if (!(unresolved.length > 0)) {
return [];
}
const unfixableIssuesTextArray = [chalk.bold.white('Non-fixable issues:')];
for (const issue of unresolved) {
const packageNameAtVersion = chalk.bold.whiteBright(`\n ${issue.packageName}@${issue.version} \n`);
unfixableIssuesTextArray
.push(packageNameAtVersion + formatIssue(issue.id, issue.title, issue.severity, issue.isNew));
}

return unfixableIssuesTextArray;
}

function formatIssue(id: string, title: string, severity: SEVERITY, isNew: boolean): string {
const severitiesColourMapping = {
low: {
colorFunc(text) {
return chalk.blueBright(text);
},
},
medium: {
colorFunc(text) {
return chalk.yellowBright(text);
},
},
high: {
colorFunc(text) {
return chalk.redBright(text);
},
},
};
const newBadge = isNew ? ' (new)' : '';
return severitiesColourMapping[severity].colorFunc(
` ✗ ${chalk.bold(title)}${newBadge} [${titleCaseText(severity)} Severity]`,
) + `[${id}] `;
}

function titleCaseText(text) {
return text[0].toUpperCase() + text.slice(1);
}
26 changes: 18 additions & 8 deletions src/cli/commands/test/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,10 @@ import * as Debug from 'debug';
import {Options, TestOptions} from '../../../lib/types';
import {isLocalFolder} from '../../../lib/detect';
import { MethodArgs } from '../../args';
import { LegacyVulnApiResult } from '../../../lib/snyk-test/legacy';
import { LegacyVulnApiResult, SEVERITY, GroupedVuln, VulnMetaData } from '../../../lib/snyk-test/legacy';
import { formatIssues } from './formatters/legacy-format-issue';
import { WIZARD_SUPPORTED_PACKAGE_MANAGERS } from '../../../lib/package-managers';
import { formatIssuesWithRemediation } from './formatters/remediation-based-format-issues';

const debug = Debug('snyk');
const SEPARATOR = '\n-------------------------------------------------------\n';
Expand Down Expand Up @@ -259,7 +260,7 @@ function displayResult(res, options: Options & TestOptions) {
'your CI/test.' : '';
// user tested a package@version and got 0 vulns back, but there were dev deps
// to consider
const snykPackageTestTip: string = !(localPackageTest || options.dev) ?
const snykPackageTestTip: string = !(options.docker || localPackageTest || options.dev) ?
'\n\nTip: Snyk only tests production dependencies by default. You can try re-running with the `--dev` flag.' : '';
return (
prefix + meta + summaryOKText + multiProjAdvice + (
Expand Down Expand Up @@ -314,7 +315,7 @@ function displayResult(res, options: Options & TestOptions) {
}

const vulns = res.vulnerabilities || [];
const groupedVulns = groupVulnerabilities(vulns);
const groupedVulns: GroupedVuln[] = groupVulnerabilities(vulns);
const sortedGroupedVulns = _.orderBy(
groupedVulns,
['metadata.severityValue', 'metadata.name'],
Expand All @@ -325,7 +326,12 @@ function displayResult(res, options: Options & TestOptions) {
const binariesSortedGroupedVulns = sortedGroupedVulns
.filter((vuln) => (vuln.metadata.packageManager === 'upstream'));

const groupedVulnInfoOutput = filteredSortedGroupedVulns.map((vuln) => formatIssues(vuln, options));
let groupedVulnInfoOutput;
if (res.remediation) {
groupedVulnInfoOutput = formatIssuesWithRemediation(filteredSortedGroupedVulns, res.remediation, options);
} else {
groupedVulnInfoOutput = filteredSortedGroupedVulns.map((vuln) => formatIssues(vuln, options));
}

const groupedDockerBinariesVulnInfoOutput = (res.docker && binariesSortedGroupedVulns.length) ?
formatDockerBinariesIssues(binariesSortedGroupedVulns, res.docker.binariesVulns, options) : [];
Expand All @@ -334,7 +340,8 @@ function displayResult(res, options: Options & TestOptions) {
groupedVulnInfoOutput.join('\n\n') + '\n\n' +
groupedDockerBinariesVulnInfoOutput.join('\n\n') + '\n\n' + meta + summary;

return prefix + body + multiProjAdvice + dockerAdvice + dockerSuggestion;
const ignoredIssues = '';
return prefix + body + multiProjAdvice + ignoredIssues + dockerAdvice + dockerSuggestion;
}

function formatDockerBinariesIssues(
Expand Down Expand Up @@ -453,7 +460,7 @@ function isVulnFixable(vuln) {
return vuln.isUpgradable || vuln.isPatchable;
}

function groupVulnerabilities(vulns) {
function groupVulnerabilities(vulns): GroupedVuln[] {
return vulns.reduce((map, curr) => {
if (!map[curr.id]) {
map[curr.id] = {};
Expand All @@ -464,8 +471,11 @@ function groupVulnerabilities(vulns) {
// Extra added fields for ease of handling
map[curr.id].title = curr.title;
map[curr.id].note = curr.note;
map[curr.id].severity = curr.severity;
map[curr.id].severity = curr.severity as SEVERITY;
map[curr.id].isNew = isNewVuln(curr);
map[curr.id].name = curr.name;
map[curr.id].version = curr.version;
map[curr.id].fixedIn = curr.fixedIn;
map[curr.id].dockerfileInstruction = curr.dockerfileInstruction;
map[curr.id].dockerBaseImage = curr.dockerBaseImage;
map[curr.id].nearestFixedInVersion = curr.nearestFixedInVersion;
Expand All @@ -490,7 +500,7 @@ function isNewVuln(vuln) {
return publicationTime > Date.now() - MONTH;
}

function metadataForVuln(vuln) {
function metadataForVuln(vuln): VulnMetaData {
return {
id: vuln.id,
title: vuln.title,
Expand Down
49 changes: 44 additions & 5 deletions src/lib/snyk-test/legacy.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import * as _ from 'lodash';
import * as depGraphLib from '@snyk/dep-graph';
import { SupportedPackageManagers } from '../package-managers';

interface Pkg {
name: string;
Expand All @@ -13,9 +14,46 @@ interface Patch {
modificationTime: string;
}

interface IssueData {
export enum SEVERITY {
LOW = 'low',
MEDIUM = 'medium',
HIGH = 'high',
}

export interface VulnMetaData {
id: string;
title: string;
description: string;
type: 'license' | 'vuln';
name: string;
info: string;
severity: SEVERITY;
severityValue: number;
isNew: boolean;
version: string;
packageManager: SupportedPackageManagers | 'upstream';
}

export interface GroupedVuln {
list: AnnotatedIssue[];
metadata: VulnMetaData;
isIgnored: boolean;
title: string;
note: string;
severity: SEVERITY;
isNew: boolean;
name: string;
version: string;
fixedIn: string[];
dockerfileInstruction: string;
dockerBaseImage: string;
nearestFixedInVersion: string;
}

export interface IssueData {
id: string;
packageName: string;
version: string;
moduleName?: string;
below: string; // Vulnerable below version
semver: {
Expand All @@ -26,11 +64,12 @@ interface IssueData {
}
};
patches: Patch[];
isNew: boolean;
description: string;
title: string;
severity: SEVERITY;
}

type Severity = 'low' | 'medium' | 'high';

interface AnnotatedIssue extends IssueData {
credit: any;
name: string;
Expand All @@ -40,7 +79,7 @@ interface AnnotatedIssue extends IssueData {
isUpgradable: boolean;
isPatchable: boolean;
nearestFixedInVersion?: string;
severity: Severity;
severity: SEVERITY;

// These fields present for "node_module" based scans to allow remediation
bundled?: any;
Expand All @@ -52,7 +91,7 @@ interface AnnotatedIssue extends IssueData {
dockerBaseImage?: any;

type?: 'license';
title?: string;
title: string;
patch?: any;
note?: any;
publicationTime?: string;
Expand Down
4 changes: 2 additions & 2 deletions src/lib/snyk-test/run-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,10 +111,10 @@ async function runTest(packageManager: SupportedPackageManagers,
}
}
}

// TODO: is this needed? we filter on the other side already based on policy
// this will move to be filtered server side soon & it will support `'ignore-policy'`
analytics.add('vulns-pre-policy', res.vulnerabilities.length);
res.filesystemPolicy = !!payloadPolicy;
// TODO: is this needed? we filter on the other side already based on policy
if (!options['ignore-policy']) {
res.policy = res.policy || payloadPolicy as string;
const policy = await snyk.policy.loadFromText(res.policy);
Expand Down
33 changes: 33 additions & 0 deletions test/acceptance/display-test-results.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import * as tap from 'tap';
import * as sinon from 'sinon';
import * as _ from 'lodash';
import * as fs from 'fs';

// tslint:disable-next-line:no-var-requires
const snykTest = require('../../src/cli/commands/test');
import * as snyk from '../../src/lib';

const {test} = tap;
(tap as any).runOnly = false; // <- for debug. set to true, and replace a test to only(..)

test('`test ruby-app` remediation displayed', async (t) => {
chdirWorkspaces();
const stubbedResponse = JSON.parse(
fs.readFileSync(__dirname + '/workspaces/ruby-app/test-graph-response-with-remediation.json', 'utf8'),
);
const snykTestStub = sinon.stub(snyk, 'test').returns(stubbedResponse);
try {
await snykTest('ruby-app');
} catch (error) {
const res = error.message;
t.match(res, 'Upgrade rack@1.6.5 to rack@1.6.11 to fix', 'upgrade advice displayed');
t.match(res, 'Tested 3 dependencies for known issues, found 6 issues, 8 vulnerable paths.');
}

snykTestStub.restore();
t.end();
});

function chdirWorkspaces(subdir: string = '') {
process.chdir(__dirname + '/workspaces' + (subdir ? '/' + subdir : ''));
}

0 comments on commit 3803df8

Please sign in to comment.