Skip to content

Commit

Permalink
feat: support maven reachable vulnerabilities for snyk test
Browse files Browse the repository at this point in the history
Will send a call graph to registry if the --reachable-vulns flag
is being passed.

The function call graph is generated by the maven plugin.

This is only supported behind the reachableVulns feature flag for
the maven package manager.
  • Loading branch information
Dar Malovani committed Apr 21, 2020
1 parent f7097e3 commit 8b805e1
Show file tree
Hide file tree
Showing 17 changed files with 445 additions and 3 deletions.
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@
"author": "snyk.io",
"license": "Apache-2.0",
"dependencies": {
"@snyk/cli-interface": "2.3.2",
"@snyk/cli-interface": "^2.4.0",
"@snyk/configstore": "^3.2.0-rc1",
"@snyk/dep-graph": "1.16.1",
"@snyk/gemfile": "1.2.0",
Expand All @@ -69,6 +69,7 @@
"diff": "^4.0.1",
"git-url-parse": "11.1.2",
"glob": "^7.1.3",
"graphlib": "^2.1.8",
"inquirer": "^6.2.2",
"lodash": "^4.17.14",
"needle": "^2.2.4",
Expand All @@ -82,7 +83,7 @@
"snyk-go-plugin": "1.13.0",
"snyk-gradle-plugin": "3.2.5",
"snyk-module": "1.9.1",
"snyk-mvn-plugin": "2.9.0",
"snyk-mvn-plugin": "2.10.0",
"snyk-nodejs-lockfile-parser": "1.18.0",
"snyk-nuget-plugin": "1.16.0",
"snyk-php-plugin": "1.7.0",
Expand Down
1 change: 1 addition & 0 deletions src/cli/args.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,7 @@ export function args(rawArgv: string[]): Args {
'fail-on',
'all-projects',
'detection-depth',
'reachable-vulns',
]) {
if (argv[dashedArg]) {
const camelCased = dashToCamelCase(dashedArg);
Expand Down
11 changes: 11 additions & 0 deletions src/lib/errors/feature-not-supported-by-package-manager-error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { CustomError } from './custom-error';
import { SupportedPackageManagers } from '../package-managers';

export class FeatureNotSupportedByPackageManagerError extends CustomError {
constructor(feature: string, packageManager: SupportedPackageManagers) {
super(`Unsupported package manager ${packageManager} for ${feature}.`);
this.code = 422;

this.userMessage = `'${feature}' is not supported for package manager '${packageManager}'.`;
}
}
1 change: 1 addition & 0 deletions src/lib/errors/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,4 @@ export { AuthFailedError } from './authentication-failed-error';
export { OptionMissingErrorError } from './option-missing-error';
export { ExcludeFlagBadInputError } from './exclude-flag-bad-input';
export { UnsupportedOptionCombinationError } from './unsupported-option-combination-error';
export { FeatureNotSupportedByPackageManagerError } from './feature-not-supported-by-package-manager-error';
3 changes: 3 additions & 0 deletions src/lib/package-managers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,6 @@ export const GRAPH_SUPPORTED_PACKAGE_MANAGERS: SupportedPackageManagers[] = [
export const PINNING_SUPPORTED_PACKAGE_MANAGERS: SupportedPackageManagers[] = [
'pip',
];
export const REACHABLE_VULNS_SUPPORTED_PACKAGE_MANAGERS: SupportedPackageManagers[] = [
'maven',
];
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export function convertSingleResultToMultiCustom(
if (!inspectRes.package.targetFile && inspectRes.plugin) {
inspectRes.package.targetFile = inspectRes.plugin.targetFile;
}
const { plugin, meta, package: depTree } = inspectRes;
const { plugin, meta, package: depTree, callGraph } = inspectRes;

if (!depTree.targetFile && plugin) {
depTree.targetFile = plugin.targetFile;
Expand All @@ -21,6 +21,7 @@ export function convertSingleResultToMultiCustom(
{
plugin: plugin as any,
depTree,
callGraph,
meta,
targetFile: plugin.targetFile,
packageManager:
Expand Down
2 changes: 2 additions & 0 deletions src/lib/plugins/get-multi-plugin-result.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,14 @@ import { getSinglePluginResult } from './get-single-plugin-result';
import { convertSingleResultToMultiCustom } from './convert-single-splugin-res-to-multi-custom';
import { convertMultiResultToMultiCustom } from './convert-multi-plugin-res-to-multi-custom';
import { PluginMetadata } from '@snyk/cli-interface/legacy/plugin';
import { CallGraph } from '@snyk/cli-interface/legacy/common';

const debug = debugModule('snyk-test');
export interface ScannedProjectCustom
extends cliInterface.legacyCommon.ScannedProject {
packageManager: SupportedPackageManagers;
plugin: PluginMetadata;
callGraph?: CallGraph;
}
export interface MultiProjectResultCustom
extends cliInterface.legacyPlugin.MultiProjectResult {
Expand Down
59 changes: 59 additions & 0 deletions src/lib/reachable-vulns.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import * as graphlib from 'graphlib';
import { CallGraph } from '@snyk/cli-interface/legacy/common';

import {
REACHABLE_VULNS_SUPPORTED_PACKAGE_MANAGERS,
SupportedPackageManagers,
} from './package-managers';
import { isFeatureFlagSupportedForOrg } from './feature-flags';
import {
AuthFailedError,
FeatureNotSupportedByPackageManagerError,
UnsupportedFeatureFlagError,
} from './errors';

const featureFlag = 'reachableVulns';

export function serializeCallGraphWithMetrics(
callGraph: CallGraph,
): {
callGraph: any;
nodeCount: number;
edgeCount: number;
} {
return {
callGraph: graphlib.json.write(callGraph),
nodeCount: callGraph.nodeCount(),
edgeCount: callGraph.edgeCount(),
};
}

export async function validatePayload(
packageManager: SupportedPackageManagers,
org: any,
): Promise<boolean> {
if (!REACHABLE_VULNS_SUPPORTED_PACKAGE_MANAGERS.includes(packageManager)) {
throw new FeatureNotSupportedByPackageManagerError(
'Reachable vulns',
packageManager,
);
}
const reachableVulnsSupportedRes = await isFeatureFlagSupportedForOrg(
featureFlag,
org,
);

if (reachableVulnsSupportedRes.code === 401) {
throw AuthFailedError(
reachableVulnsSupportedRes.error,
reachableVulnsSupportedRes.code,
);
}
if (reachableVulnsSupportedRes.userMessage) {
throw new UnsupportedFeatureFlagError(
featureFlag,
reachableVulnsSupportedRes.userMessage,
);
}
return true;
}
23 changes: 23 additions & 0 deletions src/lib/snyk-test/run-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
FailedToGetVulnerabilitiesError,
FailedToGetVulnsFromUnavailableResource,
FailedToRunTestError,
UnsupportedFeatureFlagError,
} from '../errors';
import * as snyk from '../';
import { isCI } from '../is-ci';
Expand All @@ -41,6 +42,8 @@ import request = require('../request');
import spinner = require('../spinner');
import { extractPackageManager } from '../plugins/extract-package-manager';
import { getSubProjectCount } from '../plugins/get-sub-project-count';
import { serializeCallGraphWithMetrics } from '../reachable-vulns';
import { validateOptions } from './validation';

const debug = debugModule('snyk');

Expand All @@ -53,6 +56,7 @@ interface DepTreeFromResolveDeps extends DepTree {

interface PayloadBody {
depGraph?: depGraphLib.DepGraph; // missing for legacy endpoint (options.vulnEndpoint)
callGraph?: any;
policy: string;
targetFile?: string;
targetFileRelativePath?: string;
Expand Down Expand Up @@ -86,6 +90,7 @@ async function runTest(
const results: TestResult[] = [];
const spinnerLbl = 'Querying vulnerabilities database...';
try {
await validateOptions(options);
const payloads = await assemblePayloads(root, options);
for (const payload of payloads) {
const payloadPolicy = payload.body && payload.body.policy;
Expand Down Expand Up @@ -260,6 +265,10 @@ function handleTestHttpErrorResponse(res, body) {
err = AuthFailedError(userMessage, statusCode);
err.innerError = body.stack;
break;
case 405:
err = new UnsupportedFeatureFlagError('reachableVulns');
err.innerError = body.stack;
break;
case 500:
err = new InternalServerError(userMessage);
err.innerError = body.stack;
Expand Down Expand Up @@ -436,6 +445,20 @@ async function assembleLocalPayloads(
body.depGraph = depGraph;
}

if (scannedProject.callGraph) {
const {
callGraph,
nodeCount,
edgeCount,
} = serializeCallGraphWithMetrics(scannedProject.callGraph);
debug(
`Adding call graph to payload, node count: ${nodeCount}, edge count: ${edgeCount}`,
);
analytics.add('callGraphNodeCount', nodeCount);
analytics.add('callGraphEdgeCount', edgeCount);
body.callGraph = callGraph;
}

const payload: Payload = {
method: 'POST',
url: config.API + (options.vulnEndpoint || '/test-dep-graph'),
Expand Down
12 changes: 12 additions & 0 deletions src/lib/snyk-test/validation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { Options, TestOptions } from '../types';
import * as config from '../config';
import { SupportedPackageManagers } from '../package-managers';
import * as reachableVulns from '../reachable-vulns';

export async function validateOptions(options: Options & TestOptions) {
if (options.reachableVulns) {
const pkgManager = options.packageManager as SupportedPackageManagers;
const org = options.org || config.org;
await reachableVulns.validatePayload(pkgManager, org);
}
}
1 change: 1 addition & 0 deletions src/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export interface TestOptions {
'prune-repeated-subdependencies'?: boolean;
showVulnPaths: ShowVulnPaths;
failOn?: FailOn;
reachableVulns?: boolean;
}
export interface ProtectOptions {
loose: boolean;
Expand Down
94 changes: 94 additions & 0 deletions test/acceptance/cli-reachable-vulns.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import * as tap from 'tap';
import * as cli from '../../src/cli/commands';
import { fakeServer } from './fake-server';

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

const port = (process.env.PORT = process.env.SNYK_PORT = '12345');
process.env.SNYK_API = 'http://localhost:' + port + '/api/v1';
process.env.SNYK_HOST = 'http://localhost:' + port;
process.env.LOG_LEVEL = '0';
const apiKey = '123456789';
let oldkey;
let oldendpoint;
const server = fakeServer(process.env.SNYK_API, apiKey);
const before = tap.runOnly ? only : test;
const after = tap.runOnly ? only : test;

// @later: remove this config stuff.
// Was copied straight from ../src/cli-server.js
before('setup', async (t) => {
t.plan(3);
let key = await cli.config('get', 'api');
oldkey = key;
t.pass('existing user config captured');

key = await cli.config('get', 'endpoint');
oldendpoint = key;
t.pass('existing user endpoint captured');

await new Promise((resolve) => {
server.listen(port, resolve);
});
t.pass('started demo server');
t.end();
});

// @later: remove this config stuff.
// Was copied straight from ../src/cli-server.js
before('prime config', async (t) => {
await cli.config('set', 'api=' + apiKey);
t.pass('api token set');
await cli.config('unset', 'endpoint');
t.pass('endpoint removed');
t.end();
});

test('test vulnerable project with --reachable-vulns not supported package manager', async (t) => {
try {
await cli.test('gradle', {
reachableVulns: true,
});
t.fail('expected test to throw exception');
} catch (err) {
t.equal(err.code, 422, 'should throw exception');
t.equal(
err.userMessage,
`'Reachable vulns' is not supported for package manager 'npm'.`,
'correct user message',
);
}
});

// @later: try and remove this config stuff
// Was copied straight from ../src/cli-server.js
after('teardown', async (t) => {
t.plan(4);

delete process.env.SNYK_API;
delete process.env.SNYK_HOST;
delete process.env.SNYK_PORT;
t.notOk(process.env.SNYK_PORT, 'fake env values cleared');

await new Promise((resolve) => {
server.close(resolve);
});
t.pass('server shutdown');
let key = 'set';
let value = 'api=' + oldkey;
if (!oldkey) {
key = 'unset';
value = 'api';
}
await cli.config(key, value);
t.pass('user config restored');
if (oldendpoint) {
await cli.config('endpoint', oldendpoint);
t.pass('user endpoint restored');
t.end();
} else {
t.pass('no endpoint');
t.end();
}
});
Loading

0 comments on commit 8b805e1

Please sign in to comment.