diff --git a/src/cli/commands/test/formatters/format-reachability.ts b/src/cli/commands/test/formatters/format-reachability.ts index 20c5b0b688b..a9b3cc8f73a 100644 --- a/src/cli/commands/test/formatters/format-reachability.ts +++ b/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 }; @@ -59,3 +70,38 @@ export function summariseReachableVulns( return ''; } + +function getDistinctReachablePaths( + reachablePaths: CallPath[], + maxPathCount: number, +): string[] { + const uniquePaths = new Set(); + 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}`; +} diff --git a/src/cli/commands/test/formatters/remediation-based-format-issues.ts b/src/cli/commands/test/formatters/remediation-based-format-issues.ts index 0faa31a3a07..14a2f6d3bf2 100644 --- a/src/cli/commands/test/formatters/remediation-based-format-issues.ts +++ b/src/cli/commands/test/formatters/remediation-based-format-issues.ts @@ -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[], @@ -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, @@ -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') { @@ -249,6 +248,7 @@ function thisUpgradeFixes( basicVulnInfo[id].note, [], basicVulnInfo[id].reachability, + basicVulnInfo[id].sampleReachablePaths, ), ) .join('\n'); @@ -429,6 +429,7 @@ export function formatIssue( note: string | false, legalInstructions?: LegalInstruction[], reachability?: REACHABILITY, + sampleReachablePaths?: SampleReachablePaths, ): string { const severitiesColourMapping = { low: { @@ -455,7 +456,7 @@ export function formatIssue( } let reachabilityText = ''; if (reachability) { - reachabilityText = `${formatReachability(reachability)}`; + reachabilityText = formatReachability(reachability); } let introducedBy = ''; @@ -485,6 +486,11 @@ export function formatIssue( )} other path(s)`; } } + const reachableVia = formatReachablePaths( + sampleReachablePaths, + MAX_REACHABLE_PATHS, + reachablePathsTemplate, + ); return ( severitiesColourMapping[severity].colorFunc( @@ -495,6 +501,7 @@ export function formatIssue( reachabilityText + `[${config.ROOT}/vuln/${id}]` + name + + reachableVia + introducedBy + (legalLicenseInstructionsText ? `${chalk.bold( @@ -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; +} diff --git a/src/cli/commands/test/formatters/types.ts b/src/cli/commands/test/formatters/types.ts new file mode 100644 index 00000000000..dc19c2b23f4 --- /dev/null +++ b/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[]; +} diff --git a/src/lib/snyk-test/legacy.ts b/src/lib/snyk-test/legacy.ts index e698a04b5b3..92d0696046d 100644 --- a/src/lib/snyk-test/legacy.ts +++ b/src/lib/snyk-test/legacy.ts @@ -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; @@ -106,6 +118,8 @@ interface AnnotatedIssue extends IssueData { patch?: any; note?: string | false; publicationTime?: string; + + reachablePaths?: ReachablePaths; } // Mixin, to be added to GroupedVuln / AnnotatedIssue diff --git a/test/reachable-vulns.test.ts b/test/reachable-vulns.test.ts index c2c1bdc53c8..2c7c3d60626 100644 --- a/test/reachable-vulns.test.ts +++ b/test/reachable-vulns.test.ts @@ -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 { @@ -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');