Skip to content

Commit

Permalink
feat: show reachable paths for supported projects
Browse files Browse the repository at this point in the history
  • Loading branch information
muscar committed Jun 29, 2020
1 parent d8dddaf commit b91c3f1
Show file tree
Hide file tree
Showing 5 changed files with 216 additions and 32 deletions.
48 changes: 47 additions & 1 deletion src/cli/commands/test/formatters/format-reachability.ts
@@ -1,7 +1,18 @@
import * as wrap from 'wrap-ansi';
import chalk from 'chalk';

import { AnnotatedIssue, REACHABILITY } from '../../../../lib/snyk-test/legacy';
import {
AnnotatedIssue,
CallPath,
REACHABILITY,
} from '../../../../lib/snyk-test/legacy';
import { SampleReachablePaths } from './types';

// Number of function names to show in the beginning of an abbreviated code path
const LEADING_PATH_ELEMENTS = 2;

// Number of function names to show in the end of an abbreviated code path
const TRAILING_PATH_ELEMENTS = 2;

const reachabilityLevels: {
[key in REACHABILITY]: { color: Function; text: string };
Expand Down Expand Up @@ -59,3 +70,38 @@ export function summariseReachableVulns(

return '';
}

function getDistinctReachablePaths(
reachablePaths: CallPath[],
maxPathCount: number,
): string[] {
const uniquePaths = new Set<string>();
for (const path of reachablePaths) {
if (uniquePaths.size >= maxPathCount) {
break;
}
uniquePaths.add(formatReachablePath(path));
}
return Array.from(uniquePaths.values());
}

export function formatReachablePaths(
sampleReachablePaths: SampleReachablePaths | undefined,
maxPathCount: number,
template: (samplePaths: string[], extraPathsCount: number) => string,
): string {
const paths = sampleReachablePaths?.paths || [];
const pathCount = sampleReachablePaths?.pathCount || 0;
const distinctPaths = getDistinctReachablePaths(paths, maxPathCount);
const extraPaths = pathCount - distinctPaths.length;

return template(distinctPaths, extraPaths);
}

export function formatReachablePath(path: CallPath): string {
const head = path.slice(0, LEADING_PATH_ELEMENTS).join('>');
const tail = path
.slice(path.length - TRAILING_PATH_ELEMENTS, path.length)
.join('>');
return `${head} > ... > ${tail}`;
}
91 changes: 60 additions & 31 deletions src/cli/commands/test/formatters/remediation-based-format-issues.ts
Expand Up @@ -2,44 +2,32 @@ import chalk from 'chalk';
import * as config from '../../../../lib/config';
import { TestOptions } from '../../../../lib/types';
import {
RemediationChanges,
PatchRemediation,
DependencyPins,
DependencyUpdates,
IssueData,
SEVERITY,
GroupedVuln,
DependencyPins,
UpgradeRemediation,
PinRemediation,
IssueData,
LegalInstruction,
PatchRemediation,
PinRemediation,
REACHABILITY,
RemediationChanges,
SEVERITY,
UpgradeRemediation,
} from '../../../../lib/snyk-test/legacy';
import { SEVERITIES } from '../../../../lib/snyk-test/common';
import { formatLegalInstructions } from './legal-license-instructions';
import { formatReachability } from './format-reachability';

interface BasicVulnInfo {
type: string;
title: string;
severity: SEVERITY;
isNew: boolean;
name: string;
version: string;
fixedIn: string[];
legalInstructions?: LegalInstruction[];
paths: string[][];
note: string | false;
reachability?: REACHABILITY;
}

interface TopLevelPackageUpgrade {
name: string;
version: string;
}
import {
formatReachability,
formatReachablePaths,
} from './format-reachability';
import {
BasicVulnInfo,
SampleReachablePaths,
UpgradesByAffectedPackage,
} from './types';

interface UpgradesByAffectedPackage {
[pkgNameAndVersion: string]: TopLevelPackageUpgrade[];
}
// How many reachable paths to show in the output
const MAX_REACHABLE_PATHS = 2;

export function formatIssuesWithRemediation(
vulns: GroupedVuln[],
Expand All @@ -55,6 +43,16 @@ export function formatIssuesWithRemediation(
} = {};

for (const vuln of vulns) {
const allReachablePaths: SampleReachablePaths = { pathCount: 0, paths: [] };
for (const issue of vuln.list) {
const issueReachablePaths = issue.reachablePaths?.reachablePaths || [];
for (const functionReachablePaths of issueReachablePaths) {
allReachablePaths.paths = allReachablePaths.paths.concat(
functionReachablePaths.callPaths,
);
allReachablePaths.pathCount += functionReachablePaths.callPaths.length;
}
}
const vulnData = {
title: vuln.title,
severity: vuln.severity,
Expand All @@ -67,6 +65,7 @@ export function formatIssuesWithRemediation(
legalInstructions: vuln.legalInstructionsArray,
paths: vuln.list.map((v) => v.from),
reachability: vuln.reachability,
sampleReachablePaths: allReachablePaths,
};

if (vulnData.type === 'license') {
Expand Down Expand Up @@ -249,6 +248,7 @@ function thisUpgradeFixes(
basicVulnInfo[id].note,
[],
basicVulnInfo[id].reachability,
basicVulnInfo[id].sampleReachablePaths,
),
)
.join('\n');
Expand Down Expand Up @@ -429,6 +429,7 @@ export function formatIssue(
note: string | false,
legalInstructions?: LegalInstruction[],
reachability?: REACHABILITY,
sampleReachablePaths?: SampleReachablePaths,
): string {
const severitiesColourMapping = {
low: {
Expand All @@ -455,7 +456,7 @@ export function formatIssue(
}
let reachabilityText = '';
if (reachability) {
reachabilityText = `${formatReachability(reachability)}`;
reachabilityText = formatReachability(reachability);
}

let introducedBy = '';
Expand Down Expand Up @@ -485,6 +486,11 @@ export function formatIssue(
)} other path(s)`;
}
}
const reachableVia = formatReachablePaths(
sampleReachablePaths,
MAX_REACHABLE_PATHS,
reachablePathsTemplate,
);

return (
severitiesColourMapping[severity].colorFunc(
Expand All @@ -495,6 +501,7 @@ export function formatIssue(
reachabilityText +
`[${config.ROOT}/vuln/${id}]` +
name +
reachableVia +
introducedBy +
(legalLicenseInstructionsText
? `${chalk.bold(
Expand All @@ -508,3 +515,25 @@ export function formatIssue(
function titleCaseText(text) {
return text[0].toUpperCase() + text.slice(1);
}

function reachablePathsTemplate(
samplePaths: string[],
extraPathsCount: number,
): string {
if (samplePaths.length === 0 && extraPathsCount === 0) {
return '';
}
if (samplePaths.length === 0) {
return `\n reachable via at least ${extraPathsCount} paths`;
}
let reachableVia = '\n reachable via:\n';
for (const p of samplePaths) {
reachableVia += ` ${p}\n`;
}
if (extraPathsCount > 0) {
reachableVia += ` and at least ${chalk.cyanBright(
'' + extraPathsCount,
)} other path(s)`;
}
return reachableVia;
}
35 changes: 35 additions & 0 deletions src/cli/commands/test/formatters/types.ts
@@ -0,0 +1,35 @@
import {
CallPath,
LegalInstruction,
REACHABILITY,
SEVERITY,
} from '../../../../lib/snyk-test/legacy';

export interface SampleReachablePaths {
pathCount: number;
paths: CallPath[];
}

export interface BasicVulnInfo {
type: string;
title: string;
severity: SEVERITY;
isNew: boolean;
name: string;
version: string;
fixedIn: string[];
legalInstructions?: LegalInstruction[];
paths: string[][];
note: string | false;
reachability?: REACHABILITY;
sampleReachablePaths?: SampleReachablePaths;
}

interface TopLevelPackageUpgrade {
name: string;
version: string;
}

export interface UpgradesByAffectedPackage {
[pkgNameAndVersion: string]: TopLevelPackageUpgrade[];
}
14 changes: 14 additions & 0 deletions src/lib/snyk-test/legacy.ts
Expand Up @@ -85,6 +85,18 @@ export interface IssueData {
reachability?: REACHABILITY;
}

export type CallPath = string[];

export interface ReachableFunctionPaths {
functionName: string;
callPaths: CallPath[];
}

export interface ReachablePaths {
pathCount: number;
reachablePaths: ReachableFunctionPaths[];
}

interface AnnotatedIssue extends IssueData {
credit: any;
name: string;
Expand All @@ -106,6 +118,8 @@ interface AnnotatedIssue extends IssueData {
patch?: any;
note?: string | false;
publicationTime?: string;

reachablePaths?: ReachablePaths;
}

// Mixin, to be added to GroupedVuln / AnnotatedIssue
Expand Down
60 changes: 60 additions & 0 deletions test/reachable-vulns.test.ts
Expand Up @@ -5,6 +5,8 @@ import {
formatReachability,
summariseReachableVulns,
getReachabilityText,
formatReachablePaths,
formatReachablePath,
} from '../src/cli/commands/test/formatters/format-reachability';
import { AnnotatedIssue, REACHABILITY } from '../src/lib/snyk-test/legacy';
import {
Expand Down Expand Up @@ -105,6 +107,64 @@ test('formatReachabilitySummaryText', (t) => {
t.end();
});

test('formatReachablePaths', (t) => {
function reachablePathsTemplate(
samplePaths: string[],
extraPathsCount: number,
): string {
if (samplePaths.length === 0) {
return `\n reachable via at least ${extraPathsCount} paths`;
}
let reachableVia = '\n reachable via:\n';
for (const p of samplePaths) {
reachableVia += ` ${p}\n`;
}
if (extraPathsCount > 0) {
reachableVia += ` and at least ${extraPathsCount} other path(s)`;
}
return reachableVia;
}

const noReachablePaths = {
pathCount: 0,
paths: [],
};

const reachablePaths = {
pathCount: 3,
paths: [
['f', 'g', 'h', 'i', 'j', 'vulnFunc1'],
['k', 'l', 'm', 'n', 'o', 'vulnFunc1'],
['p', 'q', 'r', 's', 't', 'vulnFunc2'],
],
};

t.equal(
formatReachablePaths(reachablePaths, 0, reachablePathsTemplate),
reachablePathsTemplate([], 3),
);

t.equal(
formatReachablePaths(reachablePaths, 2, reachablePathsTemplate),
reachablePathsTemplate(
reachablePaths.paths.slice(0, 2).map(formatReachablePath),
1,
),
);

t.equal(
formatReachablePaths(reachablePaths, 5, reachablePathsTemplate),
reachablePathsTemplate(reachablePaths.paths.map(formatReachablePath), 0),
);

t.equal(
formatReachablePaths(noReachablePaths, 2, reachablePathsTemplate),
reachablePathsTemplate([], 0),
);

t.end();
});

test('validatePayload - not supported package manager', async (t) => {
const pkgManagers = Object.keys(SUPPORTED_PACKAGE_MANAGER_NAME);
const mavenIndex = pkgManagers.indexOf('maven');
Expand Down

0 comments on commit b91c3f1

Please sign in to comment.