Skip to content

Commit

Permalink
Merge pull request #1355 from snyk/feat/log-unprocessed-manifests
Browse files Browse the repository at this point in the history
feat: Detect and log orphaned or errored gradle project scans by comparing detected files vs scanned.
  • Loading branch information
lili2311 committed Sep 1, 2020
2 parents 6e168de + d323180 commit a99ac29
Show file tree
Hide file tree
Showing 12 changed files with 300 additions and 50 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Expand Up @@ -3,7 +3,7 @@ config.local.json
/node_modules/
local.log
/patches/
/dist
**/dist
tmp
.DS_Store
/package-lock.json
Expand All @@ -24,3 +24,4 @@ report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
/test/acceptance/workspaces/**/project/
/test/acceptance/workspaces/**/target/
test/acceptance/workspaces/**/.gradle
test/**/.gradle
2 changes: 1 addition & 1 deletion package.json
Expand Up @@ -53,7 +53,7 @@
"author": "snyk.io",
"license": "Apache-2.0",
"dependencies": {
"@snyk/cli-interface": "2.8.1",
"@snyk/cli-interface": "2.9.0",
"@snyk/dep-graph": "1.19.3",
"@snyk/gemfile": "1.2.0",
"@snyk/graphlib": "2.1.9-patch",
Expand Down
49 changes: 40 additions & 9 deletions src/lib/find-files.ts
Expand Up @@ -39,6 +39,11 @@ export async function getStats(path: string): Promise<fs.Stats> {
});
}

interface FindFilesRes {
files: string[];
allFilesFound: string[];
}

/**
* Find all files in given search path. Returns paths to files found.
*
Expand All @@ -52,34 +57,50 @@ export async function find(
ignore: string[] = [],
filter: string[] = [],
levelsDeep = 4,
): Promise<string[]> {
): Promise<FindFilesRes> {
const found: string[] = [];
const foundAll: string[] = [];

// ensure we ignore find against node_modules path.
if (path.endsWith('node_modules')) {
return found;
return { files: found, allFilesFound: foundAll };
}
// ensure node_modules is always ignored
if (!ignore.includes('node_modules')) {
ignore.push('node_modules');
}
try {
if (levelsDeep < 0) {
return found;
return { files: found, allFilesFound: foundAll };
} else {
levelsDeep--;
}
const fileStats = await getStats(path);
if (fileStats.isDirectory()) {
const files = await findInDirectory(path, ignore, filter, levelsDeep);
const { files, allFilesFound } = await findInDirectory(
path,
ignore,
filter,
levelsDeep,
);
found.push(...files);
foundAll.push(...allFilesFound);
} else if (fileStats.isFile()) {
const fileFound = findFile(path, filter);
if (fileFound) {
found.push(fileFound);
foundAll.push(fileFound);
}
}

return filterForDefaultManifests(found);
const filteredOutFiles = foundAll.filter((f) => !found.includes(f));
if (filteredOutFiles.length) {
debug(
`Filtered out ${filteredOutFiles.length}/${
foundAll.length
} files: ${foundAll.join(', ')}`,
);
}
return { files: filterForDefaultManifests(found), allFilesFound: foundAll };
} catch (err) {
throw new Error(`Error finding files in path '${path}'.\n${err.message}`);
}
Expand All @@ -102,20 +123,30 @@ async function findInDirectory(
ignore: string[] = [],
filter: string[] = [],
levelsDeep = 4,
): Promise<string[]> {
): Promise<FindFilesRes> {
const files = await readDirectory(path);
const toFind = files
.filter((file) => !ignore.includes(file))
.map((file) => {
const resolvedPath = pathLib.resolve(path, file);
if (!fs.existsSync(resolvedPath)) {
debug('File does not seem to exist, skipping: ', file);
return [];
return { files: [], allFilesFound: [] };
}
return find(resolvedPath, ignore, filter, levelsDeep);
});

const found = await Promise.all(toFind);
return Array.prototype.concat.apply([], found);
return {
files: Array.prototype.concat.apply(
[],
found.map((f) => f.files),
),
allFilesFound: Array.prototype.concat.apply(
[],
found.map((f) => f.allFilesFound),
),
};
}

function filterForDefaultManifests(files: string[]): string[] {
Expand Down
52 changes: 50 additions & 2 deletions 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';
Expand All @@ -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');

Expand All @@ -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 targetFiles = await find(
const { files: targetFiles, allFilesFound } = await find(
root,
ignore,
multiProjectProcessors[scanType].files,
Expand All @@ -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),
Expand All @@ -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;
}

Expand Down Expand Up @@ -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;
}
6 changes: 5 additions & 1 deletion src/lib/plugins/get-extra-project-count.ts
Expand Up @@ -19,7 +19,11 @@ export async function getExtraProjectCount(
return inspectResult.plugin.meta.allSubProjectNames.length;
}
try {
const extraTargetFiles = await find(root, [], AUTO_DETECTABLE_FILES);
const { files: extraTargetFiles } = await find(
root,
[],
AUTO_DETECTABLE_FILES,
);
const foundProjectsCount =
extraTargetFiles.length > 1 ? extraTargetFiles.length - 1 : undefined;
return foundProjectsCount;
Expand Down
2 changes: 1 addition & 1 deletion src/lib/plugins/nodejs-plugin/npm-lock-parser.ts
Expand Up @@ -58,6 +58,6 @@ export async function parse(
strictOutOfSync,
);
} finally {
await spinner.clear(resolveModuleSpinnerLabel);
await spinner.clear<void>(resolveModuleSpinnerLabel)();
}
}
120 changes: 96 additions & 24 deletions test/acceptance/cli-test/cli-test.all-projects.spec.ts
Expand Up @@ -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 {
Expand Down
Empty file.
Empty file.
@@ -0,0 +1,5 @@
rootProject.name = 'root-proj'

include 'subproj'


0 comments on commit a99ac29

Please sign in to comment.