diff --git a/lib/index.ts b/lib/index.ts index 52350e0..f3f749c 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -7,10 +7,12 @@ import { MissingSubProjectError } from './errors'; import * as chalk from 'chalk'; import { DepGraphBuilder, PkgManager, DepGraph } from '@snyk/dep-graph'; import { legacyCommon, legacyPlugin as api } from '@snyk/cli-interface'; +import * as javaCallGraphBuilder from '@snyk/java-call-graph-builder'; import debugModule = require('debug'); import { findCycles } from './find-cycles'; type ScannedProject = legacyCommon.ScannedProject; +type CallGraph = legacyCommon.CallGraph; // To enable debugging output, use `snyk -d` let logger: debugModule.Debugger | null = null; @@ -59,6 +61,7 @@ export interface GradleInspectOptions { // Gradle process just never exits, from the Node's standpoint. // Leaving default usage `--no-daemon`, because of backwards compatibility daemon?: boolean; + reachableVulns?: boolean; } type Options = api.InspectOptions & GradleInspectOptions; @@ -105,6 +108,17 @@ export async function inspect( targetFile: targetFileFilteredForCompatibility(targetFile), meta: {}, }; + + let callGraph: CallGraph | undefined; + const targetPath = path.join(root, targetFile); + if (options.reachableVulns) { + debugLog(`getting call graph from path ${targetPath}`); + callGraph = await javaCallGraphBuilder.getCallGraphGradle( + path.dirname(targetPath), + ); + debugLog('got call graph successfully'); + } + if (api.isMultiSubProject(options)) { if (subProject) { throw new Error( @@ -120,6 +134,7 @@ export async function inspect( return { plugin, scannedProjects, + callGraph, }; } const depGraphAndDepRootNames = await getAllDepsOneProject( @@ -136,6 +151,7 @@ export async function inspect( return { plugin, package: null, //TODO @boost: delete me once cli-interface makes it optional + callGraph, dependencyGraph: depGraphAndDepRootNames.depGraph, meta: { gradleProjectName: depGraphAndDepRootNames.gradleProjectName, diff --git a/package.json b/package.json index 1445d21..529f529 100644 --- a/package.json +++ b/package.json @@ -47,6 +47,7 @@ "dependencies": { "@snyk/cli-interface": "2.9.1", "@snyk/dep-graph": "^1.19.4", + "@snyk/java-call-graph-builder": "^1.14.0", "@types/debug": "^4.1.4", "chalk": "^3.0.0", "debug": "^4.1.1", diff --git a/test/fixtures/call-graphs/simple.json b/test/fixtures/call-graphs/simple.json new file mode 100644 index 0000000..72c2476 --- /dev/null +++ b/test/fixtures/call-graphs/simple.json @@ -0,0 +1,33 @@ +{ + "options": { + "directed": true, + "multigraph": false, + "compound": false + }, + "nodes": [ + { + "v": "com.ibm.wala.FakeRootClass:fakeRootMethod", + "value": { + "className": "com.ibm.wala.FakeRootClass", + "functionName": "fakeRootMethod" + } + }, + { + "v": "ch.qos.logback.classic.net.SocketNode:run", + "value": { + "className": "ch.qos.logback.classic.net.SocketNode", + "functionName": "run" + } + } + ], + "edges": [ + { + "v": "com.ibm.wala.FakeRootClass:fakeRootMethod", + "w": "com.ibm.wala.FakeRootClass:fakeWorldClinit" + }, + { + "v": "ch.qos.logback.classic.net.SocketNode", + "w": "com.ibm.wala.FakeRootClass:fakeWorldClinit" + } + ] +} diff --git a/test/fixtures/java-reachability-playground/build.gradle b/test/fixtures/java-reachability-playground/build.gradle new file mode 100644 index 0000000..3782842 --- /dev/null +++ b/test/fixtures/java-reachability-playground/build.gradle @@ -0,0 +1,42 @@ +/* + * This file was generated by the Gradle 'init' task. + */ + +plugins { + id 'java' + id 'maven-publish' + id 'application' +} + +apply plugin : "java" +ext { + javaMainClass = "Unzipper" +} + +application { + mainClassName = javaMainClass +} + +repositories { + mavenLocal() + maven { + url = uri('https://repo.maven.apache.org/maven2') + } +} + +dependencies { + implementation 'commons-collections:commons-collections:3.2.1' + implementation 'org.nd4j:nd4j-common:1.0.0-beta2' +} + +group = 'org.example' +version = '1.0-SNAPSHOT' +sourceCompatibility = '1.8' + +publishing { + publications { + maven(MavenPublication) { + from(components.java) + } + } +} diff --git a/test/system/plugin.test.ts b/test/system/plugin.test.ts index 9289e9a..dc644a8 100644 --- a/test/system/plugin.test.ts +++ b/test/system/plugin.test.ts @@ -3,6 +3,10 @@ import { fixtureDir } from '../common'; import { test } from 'tap'; import { inspect } from '../../lib'; import * as subProcess from '../../lib/sub-process'; +import * as fs from 'fs'; +import * as sinon from 'sinon'; +import * as javaCallGraphBuilder from '@snyk/java-call-graph-builder'; +import { CallGraph } from '@snyk/cli-interface/legacy/common'; const rootNoWrapper = fixtureDir('no wrapper'); const GRADLE_VERSION = process.env.GRADLE_VERSION; @@ -21,6 +25,40 @@ test('run inspect()', async (t) => { ); }); +test('run inspect() with reachableVulns', async (t) => { + const gradleCallGraph = JSON.parse( + fs.readFileSync( + path.join(fixtureDir('call-graphs'), 'simple.json'), + 'utf-8', + ), + ); + const javaCallGraphBuilderStub = sinon + .stub(javaCallGraphBuilder, 'getCallGraphGradle') + .resolves(gradleCallGraph as CallGraph); + const result = await inspect('.', path.join(rootNoWrapper, 'build.gradle'), { + reachableVulns: true, + }); + const pkgs = result.dependencyGraph.getDepPkgs(); + const nodeIds: string[] = []; + Object.keys(pkgs).forEach((id) => { + nodeIds.push(`${pkgs[id].name}@${pkgs[id].version}`); + }); + + t.ok( + nodeIds.indexOf('com.android.tools:annotations@25.3.0') !== -1, + 'correct version found', + ); + t.ok(javaCallGraphBuilderStub.calledOnce, 'called to the call graph builder'); + t.ok( + javaCallGraphBuilderStub.calledWith(path.join('.', rootNoWrapper)), + 'call graph builder was called with the correct path', + ); + t.same(gradleCallGraph, result.callGraph, 'returns expected callgraph'); + t.teardown(() => { + javaCallGraphBuilderStub.restore(); + }); +}); + test('multi-config: both compile and runtime deps picked up by default', async (t) => { const result = await inspect( '.',