Skip to content

Commit

Permalink
feat: monitor all-sub-projecs
Browse files Browse the repository at this point in the history
  • Loading branch information
Konstantin Yegupov committed Apr 5, 2019
1 parent 689cf1b commit d12167d
Show file tree
Hide file tree
Showing 6 changed files with 175 additions and 82 deletions.
129 changes: 85 additions & 44 deletions src/cli/commands/monitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,21 +15,26 @@ import * as plugins from '../../lib/plugins';
import ModuleInfo = require('../../lib/module-info'); // TODO(kyegupov): fix import
import * as docker from '../../lib/docker-promotion';
import { MonitorError } from '../../lib/monitor';
import {SingleDepRootResult, MultiDepRootsResult, isMultiResult} from '../../lib/types';

const SEPARATOR = '\n-------------------------------------------------------\n';

// TODO(kyegupov): catch accessing ['undefined-properties'] via noImplicitAny
interface MonitorOptions {
id?: string;
docker?: boolean;
file?: string;
policy?: string;
json?: boolean;
'all-sub-projects'?: boolean;
'all-sub-projects'?: boolean; // Corresponds to multiDepRoot in plugins
'project-name'?: string;
}

interface GoodResult {
ok: true;
data: string;
path: string;
subProjectName?: string;
}

interface BadResult {
Expand All @@ -38,7 +43,15 @@ interface BadResult {
path: string;
}

async function monitor(...args0: any[]) {
// This is used instead of `let x; try { x = await ... } catch { cleanup }` to avoid
// declaring the type of x as possibly undefined.
async function promiseOrCleanup<T>(p: Promise<T>, cleanup: () => void): Promise<T> {
return p.catch((error) => { cleanup(); throw error; });
}

// Returns an array of Registry responses (one per every sub-project scanned), a single response,
// or an error message.
async function monitor(...args0: any[]): Promise<any> {
let args = [...args0];
let options: MonitorOptions = {};
const results: Array<GoodResult | BadResult> = [];
Expand All @@ -57,14 +70,12 @@ async function monitor(...args0: any[]) {
snyk.id = options.id;
}

// This is a temporary check for gradual rollout of subprojects scanning
// TODO: delete once supported for monitor
if (options['all-sub-projects']) {
throw new Error('`--all-sub-projects` is currently not supported for `snyk monitor`');
if (options['all-sub-projects'] && options['project-name']) {
throw new Error('`--all-sub-projects` is currently not compatible with `--project-name`');
}

await apiTokenExists('snyk monitor');
// Part 1: every argument is a scan target; run scans sequentially
// Part 1: every argument is a scan target; process them sequentially
for (const path of args) {
try {
const exists = await fs.exists(path);
Expand Down Expand Up @@ -96,17 +107,20 @@ async function monitor(...args0: any[]) {

await spinner(analyzingDepsSpinnerLabel);

let info;
try {
info = await moduleInfo.inspect(path, targetFile, options);
await spinner.clear(analyzingDepsSpinnerLabel)(info);
} catch (error) {
spinner.clear(analyzingDepsSpinnerLabel)();
throw error;
}
// Scan the project dependencies via a plugin

const pluginOptions = plugins.getPluginOptions(packageManager, options);

// TODO: the type should depend on multiDepRoots flag
const inspectResult: SingleDepRootResult|MultiDepRootsResult = await promiseOrCleanup(
moduleInfo.inspect(path, targetFile, { ...options, ...pluginOptions }),
spinner.clear(analyzingDepsSpinnerLabel));

await spinner.clear(analyzingDepsSpinnerLabel)(inspectResult);

await spinner(postingMonitorSpinnerLabel);
if (_.get(info, 'plugin.packageManager')) {
packageManager = info.plugin.packageManager;
if (inspectResult.plugin.packageManager) {
packageManager = inspectResult.plugin.packageManager;
}
const meta = {
'method': 'cli',
Expand All @@ -115,42 +129,68 @@ async function monitor(...args0: any[]) {
'project-name': options['project-name'] || config.PROJECT_NAME,
'isDocker': !!options.docker,
};
let res;
try {
res = await (snyk.monitor as any as (path, meta, info) => Promise<any>)(path, meta, info);
spinner.clear(postingMonitorSpinnerLabel)(res);
} catch (error) {
spinner.clear(postingMonitorSpinnerLabel)();
throw error;

// We send results from "all-sub-projects" scanning as different Monitor objects

// SingleDepRootResult is a legacy format understood by Registry, so we have to convert
// a MultiDepRootsResult to an array of these.

let perDepRootResults: SingleDepRootResult[] = [];
if (isMultiResult(inspectResult)) {
perDepRootResults = inspectResult.depRoots.map(
(depRoot) => ({plugin: inspectResult.plugin, package: depRoot.depTree}));
} else {
perDepRootResults = [inspectResult];
}
res.path = path;
const endpoint = url.parse(config.API);
let leader = '';
if (res.org) {
leader = '/org/' + res.org;

// Post the project dependencies to the Registry
const monOutputs: string[] = [];
for (const depRootDeps of perDepRootResults) {
// TODO(kyegupov): make snyk.monitor typed by converting src/lib/index.js to TS
const snykMonitor = snyk.monitor as any as (path, meta, depRootDeps) => Promise<any>;
const res = await promiseOrCleanup(
snykMonitor(path, meta, depRootDeps),
spinner.clear(postingMonitorSpinnerLabel));

await spinner.clear(postingMonitorSpinnerLabel)(res);

res.path = path;
const endpoint = url.parse(config.API);
let leader = '';
if (res.org) {
leader = '/org/' + res.org;
}
endpoint.pathname = leader + '/manage';
const manageUrl = url.format(endpoint);

endpoint.pathname = leader + '/monitor/' + res.id;
const subProjectName = ((inspectResult as MultiDepRootsResult).depRoots)
? depRootDeps.package.name
: undefined;
const monOutput = formatMonitorOutput(
packageManager,
res,
manageUrl,
options,
subProjectName,
);
results.push({ok: true, data: monOutput, path, subProjectName});
}
endpoint.pathname = leader + '/manage';
const manageUrl = url.format(endpoint);

endpoint.pathname = leader + '/monitor/' + res.id;
const monOutput = formatMonitorOutput(
packageManager,
res,
manageUrl,
options,
);
// push a good result
results.push({ok: true, data: monOutput, path});
} catch (err) {
// push this error, the loop continues
results.push({ok: false, data: err, path});
}
}
// Part 2: having collected the results, format them for shipping to the Registry
// Part 2: process the output from the Registry
if (options.json) {
let dataToSend = results.map((result) => {
if (result.ok) {
return JSON.parse(result.data);
const jsonData = JSON.parse(result.data);
if (result.subProjectName) {
jsonData.subProjectName = result.subProjectName;
}
return jsonData;
}
return {ok: false, error: result.data.message, path: result.path};
});
Expand Down Expand Up @@ -185,9 +225,10 @@ async function monitor(...args0: any[]) {
throw new Error(output);
}

function formatMonitorOutput(packageManager, res, manageUrl, options) {
function formatMonitorOutput(packageManager, res, manageUrl, options, subProjectName?: string) {
const issues = res.licensesPolicy ? 'issues' : 'vulnerabilities';
let strOutput = chalk.bold.white('\nMonitoring ' + res.path + '...\n\n') +
const humanReadableName = subProjectName ? `${res.path} (${subProjectName})` : res.path;
let strOutput = chalk.bold.white('\nMonitoring ' + humanReadableName + '...\n\n') +
(packageManager === 'yarn' ?
'A yarn.lock file was detected - continuing as a Yarn project.\n' : '') +
'Explore this snapshot at ' + res.uri + '\n\n' +
Expand Down
3 changes: 2 additions & 1 deletion src/lib/monitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import * as os from 'os';
import * as _ from 'lodash';
import * as isCI from './is-ci';
import * as analytics from './analytics';
import { SingleDepRootResult } from './types';

export class MonitorError extends Error {
public code?: number;
Expand Down Expand Up @@ -41,7 +42,7 @@ interface Meta {
projectName: string;
}

function monitor(root, meta, info) {
function monitor(root, meta, info: SingleDepRootResult) {
const pkg = info.package;
const pluginMeta = info.plugin;
let policyPath = meta['policy-path'];
Expand Down
34 changes: 1 addition & 33 deletions src/lib/snyk-test/run-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import spinner = require('../spinner');
import common = require('./common');
import gemfileLockToDependencies = require('../../lib/plugins/rubygems/gemfile-lock-to-dependencies');
import {convertTestDepGraphResultToLegacy, AnnotatedIssue} from './legacy';
import {Package, DepRoot} from '../types';

// tslint:disable-next-line:no-var-requires
const debug = require('debug')('snyk');
Expand All @@ -40,39 +41,6 @@ interface Payload {
qs?: object | null;
}

interface PluginMetadata {
name: string;
packageFormatVersion?: string;
packageManager: string;
imageLayers?: any;
targetFile?: string; // this is wrong (because Shaun said it)
}

interface DepDict {
[name: string]: DepTree;
}

interface DepTree {
name: string;
version: string;
dependencies?: DepDict;
packageFormatVersion?: string;
docker?: any;
files?: any;
targetFile?: string;
}

interface DepRoot {
depTree: DepTree; // to be soon replaced with depGraph
targetFile?: string;
}

interface Package {
plugin: PluginMetadata;
depRoots?: DepRoot[]; // currently only returned by gradle
package?: DepTree;
}

async function runTest(packageManager: string, root: string, options): Promise<object[]> {
const policyLocations = [options['policy-path'] || root];
// TODO: why hasDevDependencies is always false?
Expand Down
54 changes: 54 additions & 0 deletions src/lib/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import * as depGraphLib from '@snyk/dep-graph';

// TODO(kyegupov): use a shared repository snyk-cli-interface

export interface PluginMetadata {
name: string;
packageFormatVersion?: string;
packageManager: string;
imageLayers?: any;
targetFile?: string; // this is wrong (because Shaun said it)
runtime?: any;
dockerImageId: any;
}

export interface DepDict {
[name: string]: DepTree;
}

export interface DepTree {
name: string;
version: string;
dependencies?: DepDict;
packageFormatVersion?: string;
docker?: any;
files?: any;
targetFile?: string;
}

export interface DepRoot {
depTree: DepTree; // to be soon replaced with depGraph
targetFile?: string;
}

export interface Package {
plugin: PluginMetadata;
depRoots?: DepRoot[]; // currently only returned by gradle
package?: DepTree;
}

// Legacy result type. Will be deprecated soon.
export interface SingleDepRootResult {
plugin: PluginMetadata;
package: DepTree;
}

export interface MultiDepRootsResult {
plugin: PluginMetadata;
depRoots: DepRoot[];
}

// https://www.typescriptlang.org/docs/handbook/advanced-types.html#user-defined-type-guards
export function isMultiResult(pet: SingleDepRootResult | MultiDepRootsResult): pet is MultiDepRootsResult {
return !!(pet as MultiDepRootsResult).depRoots;
}
35 changes: 32 additions & 3 deletions test/acceptance/cli.acceptance.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -566,7 +566,6 @@ test('`test gradle-app --all-sub-projects` sends `multiDepRoots` argument to plu
const res = await cli.test('gradle-app', {
'all-sub-projects': true,
});
const meta = res.slice(res.indexOf('Organisation:')).split('\n');
t.true(spyPlugin.args[0][2].multiDepRoots);
});

Expand Down Expand Up @@ -2314,6 +2313,36 @@ test('`monitor gradle-app`', async (t) => {
});

test('`monitor gradle-app --all-sub-projects`', async (t) => {
t.plan(4);
chdirWorkspaces();
const plugin = {
async inspect() {
return {
plugin: {},
package: {},
};
},
};
const spyPlugin = sinon.spy(plugin, 'inspect');
const loadPlugin = sinon.stub(plugins, 'loadPlugin');
t.teardown(loadPlugin.restore);
loadPlugin.withArgs('gradle').returns(plugin);

await cli.monitor('gradle-app', {'all-sub-projects': true});
t.true(spyPlugin.args[0][2].multiDepRoots);

const req = server.popRequest();
t.equal(req.method, 'PUT', 'makes PUT request');
t.match(req.url, '/monitor/gradle', 'puts at correct url');
t.same(spyPlugin.getCall(0).args,
['gradle-app', 'build.gradle', {
"all-sub-projects": true,
"multiDepRoots": true,
"args": null,
}], 'calls gradle plugin');
});

test('`monitor gradle-app --all-sub-projects --project-name`', async (t) => {
t.plan(2);
chdirWorkspaces();
const plugin = {
Expand All @@ -2330,9 +2359,9 @@ test('`monitor gradle-app --all-sub-projects`', async (t) => {
loadPlugin.withArgs('gradle').returns(plugin);

try {
await cli.monitor('gradle-app', {'all-sub-projects': true});
await cli.monitor('gradle-app', {'all-sub-projects': true, 'project-name': 'frumpus'});
} catch (e) {
t.contains(e, /not supported/);
t.contains(e, /is currently not compatible/);
}

t.true(spyPlugin.notCalled, "`inspect` method wasn't called");
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit d12167d

Please sign in to comment.