diff --git a/src/lib/plugins/get-deps-from-plugin.ts b/src/lib/plugins/get-deps-from-plugin.ts index 57240107f6e..79e91696e06 100644 --- a/src/lib/plugins/get-deps-from-plugin.ts +++ b/src/lib/plugins/get-deps-from-plugin.ts @@ -1,4 +1,6 @@ import * as debugModule from 'debug'; +import * as pathLib from 'path'; +import chalk from 'chalk'; import { legacyPlugin as pluginApi } from '@snyk/cli-interface'; import { find } from '../find-files'; import { Options, TestOptions, MonitorOptions } from '../types'; @@ -17,6 +19,7 @@ import analytics = require('../analytics'); import { convertSingleResultToMultiCustom } from './convert-single-splugin-res-to-multi-custom'; import { convertMultiResultToMultiCustom } from './convert-multi-plugin-res-to-multi-custom'; import { processYarnWorkspaces } from './nodejs-plugin/yarn-workspaces-parser'; +import { ScannedProject } from '@snyk/cli-interface/legacy/common'; const debug = debugModule('snyk-test'); @@ -42,7 +45,7 @@ export async function getDepsFromPlugin( const scanType = options.yarnWorkspaces ? 'yarnWorkspaces' : 'allProjects'; const levelsDeep = options.detectionDepth; const ignore = options.exclude ? options.exclude.split(',') : []; - const { files: targetFiles } = await find( + const { files: targetFiles, allFilesFound } = await find( root, ignore, multiProjectProcessors[scanType].files, @@ -62,8 +65,9 @@ export async function getDepsFromPlugin( options, targetFiles, ); + const scannedProjects = inspectRes.scannedProjects; const analyticData = { - scannedProjects: inspectRes.scannedProjects.length, + scannedProjects: scannedProjects.length, targetFiles, packageManagers: targetFiles.map((file) => detectPackageManagerFromFile(file), @@ -72,6 +76,18 @@ export async function getDepsFromPlugin( ignore, }; analytics.add(scanType, analyticData); + debug( + `Found ${scannedProjects.length} projects from ${allFilesFound.length} detected manifests`, + ); + const userWarningMessage = warnSomeGradleManifestsNotScanned( + scannedProjects, + allFilesFound, + root, + ); + + if (!options.json && userWarningMessage) { + console.warn(chalk.bold.red(userWarningMessage)); + } return inspectRes; } @@ -105,3 +121,35 @@ export async function getDepsFromPlugin( ); return convertMultiResultToMultiCustom(inspectRes, options.packageManager); } + +export function warnSomeGradleManifestsNotScanned( + scannedProjects: ScannedProject[], + allFilesFound: string[], + root: string, +): string | null { + const gradleTargetFilesFilter = (targetFile) => + targetFile && + (targetFile.endsWith('build.gradle') || + targetFile.endsWith('build.gradle.kts')); + const scannedGradleFiles = scannedProjects + .map((p) => { + const targetFile = p.meta?.targetFile || p.targetFile; + return targetFile ? pathLib.resolve(root, targetFile) : null; + }) + .filter(gradleTargetFilesFilter); + const detectedGradleFiles = allFilesFound.filter(gradleTargetFilesFilter); + const diff = detectedGradleFiles.filter( + (file) => !scannedGradleFiles.includes(file), + ); + + debug( + `These Gradle manifests did not return any dependency results:\n${diff.join( + ',\n', + )}`, + ); + + if (diff.length > 0) { + return `✗ ${diff.length}/${detectedGradleFiles.length} detected Gradle manifests did not return dependencies.\nThey may have errored or were not included as part of a multi-project build. You may need to scan them individually with --file=path/to/file. Run with \`-d\` for more info.`; + } + return null; +} diff --git a/test/acceptance/cli-test/cli-test.all-projects.spec.ts b/test/acceptance/cli-test/cli-test.all-projects.spec.ts index 90862be8034..2e9a07a69f0 100644 --- a/test/acceptance/cli-test/cli-test.all-projects.spec.ts +++ b/test/acceptance/cli-test/cli-test.all-projects.spec.ts @@ -4,40 +4,112 @@ import * as depGraphLib from '@snyk/dep-graph'; import { CommandResult } from '../../../src/cli/commands/types'; import { AcceptanceTests } from './cli-test.acceptance.test'; import { getWorkspaceJSON } from '../workspace-helper'; +import * as getDepsFromPlugin from '../../../src/lib/plugins/get-deps-from-plugin'; + +const simpleGradleGraph = depGraphLib.createFromJSON({ + schemaVersion: '1.2.0', + pkgManager: { + name: 'gradle', + }, + pkgs: [ + { + id: 'gradle-monorepo@0.0.0', + info: { + name: 'gradle-monorepo', + version: '0.0.0', + }, + }, + ], + graph: { + rootNodeId: 'root-node', + nodes: [ + { + nodeId: 'root-node', + pkgId: 'gradle-monorepo@0.0.0', + deps: [], + }, + ], + }, +}); export const AllProjectsTests: AcceptanceTests = { language: 'Mixed', tests: { - '`test kotlin-monorepo --all-projects` scans kotlin files': ( + '`test gradle-with-orphaned-build-file --all-projects` warns user': ( params, utils, ) => async (t) => { utils.chdirWorkspaces(); - const simpleGradleGraph = depGraphLib.createFromJSON({ - schemaVersion: '1.2.0', - pkgManager: { - name: 'gradle', - }, - pkgs: [ - { - id: 'gradle-monorepo@0.0.0', - info: { - name: 'gradle-monorepo', - version: '0.0.0', - }, - }, - ], - graph: { - rootNodeId: 'root-node', - nodes: [ - { - nodeId: 'root-node', - pkgId: 'gradle-monorepo@0.0.0', - deps: [], + const plugin = { + async inspect() { + return { + plugin: { + name: 'bundled:gradle', + runtime: 'unknown', + meta: {}, }, - ], + scannedProjects: [ + { + meta: { + gradleProjectName: 'root-proj', + versionBuildInfo: { + gradleVersion: '6.5', + }, + targetFile: 'build.gradle', + }, + depGraph: simpleGradleGraph, + }, + { + meta: { + gradleProjectName: 'root-proj/subproj', + versionBuildInfo: { + gradleVersion: '6.5', + }, + targetFile: 'subproj/build.gradle', + }, + depGraph: simpleGradleGraph, + }, + ], + }; }, - }); + }; + const loadPlugin = sinon.stub(params.plugins, 'loadPlugin'); + t.teardown(loadPlugin.restore); + loadPlugin.withArgs('gradle').returns(plugin); + loadPlugin.callThrough(); + // read data from console.log + let stdoutMessages = ''; + const stubConsoleLog = (msg: string) => (stdoutMessages += msg); + const stubbedConsole = sinon + .stub(console, 'warn') + .callsFake(stubConsoleLog); + const result: CommandResult = await params.cli.test( + 'gradle-with-orphaned-build-file', + { + allProjects: true, + detectionDepth: 3, + }, + ); + t.same( + stdoutMessages, + '✗ 1/3 detected Gradle manifests did not return dependencies.\n' + + 'They may have errored or were not included as part of a multi-project build. You may need to scan them individually with --file=path/to/file. Run with `-d` for more info.', + ); + stubbedConsole.restore(); + t.ok(stubbedConsole.calledOnce); + t.ok(loadPlugin.withArgs('gradle').calledOnce, 'calls gradle plugin'); + + t.match( + result.getDisplayResults(), + 'Tested 2 projects', + 'Detected 2 projects', + ); + }, + '`test kotlin-monorepo --all-projects` scans kotlin files': ( + params, + utils, + ) => async (t) => { + utils.chdirWorkspaces(); const plugin = { async inspect() { return { diff --git a/test/acceptance/workspaces/gradle-with-orphaned-build-file/build.gradle b/test/acceptance/workspaces/gradle-with-orphaned-build-file/build.gradle new file mode 100644 index 00000000000..e69de29bb2d diff --git a/test/acceptance/workspaces/gradle-with-orphaned-build-file/orphaned/build.gradle b/test/acceptance/workspaces/gradle-with-orphaned-build-file/orphaned/build.gradle new file mode 100644 index 00000000000..e69de29bb2d diff --git a/test/acceptance/workspaces/gradle-with-orphaned-build-file/settings.gradle b/test/acceptance/workspaces/gradle-with-orphaned-build-file/settings.gradle new file mode 100644 index 00000000000..90289e8a32b --- /dev/null +++ b/test/acceptance/workspaces/gradle-with-orphaned-build-file/settings.gradle @@ -0,0 +1,5 @@ +rootProject.name = 'root-proj' + +include 'subproj' + + diff --git a/test/acceptance/workspaces/gradle-with-orphaned-build-file/subproj/build.gradle b/test/acceptance/workspaces/gradle-with-orphaned-build-file/subproj/build.gradle new file mode 100644 index 00000000000..301e7be03bc --- /dev/null +++ b/test/acceptance/workspaces/gradle-with-orphaned-build-file/subproj/build.gradle @@ -0,0 +1,51 @@ +apply plugin: 'java' +apply plugin: 'maven' + +group = 'com.github.jitpack' + +sourceCompatibility = 1.8 // java 8 +targetCompatibility = 1.8 + +repositories { + mavenCentral() +} + +dependencies { + // Gradle 3+ will not pick up "compile" dependencies for "compileOnly" + // Gradle 2 will, so for configuration-matching tests we use "runtime" + runtime 'com.google.guava:guava:18.0' + runtime 'batik:batik-dom:1.6' + runtime 'commons-discovery:commons-discovery:0.2' + compileOnly 'axis:axis:1.3' + runtime 'com.android.tools.build:builder:2.3.0' +} + +task sourcesJar(type: Jar, dependsOn: classes) { + classifier = 'sources' + from sourceSets.main.allSource +} + +task javadocJar(type: Jar, dependsOn: javadoc) { + classifier = 'javadoc' + from javadoc.destinationDir +} + +artifacts { + archives sourcesJar + archives javadocJar +} + +// To specify a license in the pom: +install { + repositories.mavenInstaller { + pom.project { + licenses { + license { + name 'The Apache Software License, Version 2.0' + url 'http://www.apache.org/licenses/LICENSE-2.0.txt' + distribution 'repo' + } + } + } + } +}