Skip to content

Commit

Permalink
feat: container static scanning in monitor command
Browse files Browse the repository at this point in the history
The CLI no longer scans container images by using Docker.
Distroless scanning is also enabled by default.

This is enabled for both "snyk monitor --docker" and "snyk container monitor".
  • Loading branch information
ivanstanev committed Oct 15, 2020
1 parent 800b57b commit e18e4d4
Show file tree
Hide file tree
Showing 4 changed files with 232 additions and 0 deletions.
20 changes: 20 additions & 0 deletions src/cli/commands/monitor/index.ts
Expand Up @@ -37,6 +37,8 @@ import { PluginMetadata } from '@snyk/cli-interface/legacy/plugin';
import { getContributors } from '../../../lib/monitor/dev-count-analysis';
import { FailedToRunTestError, MonitorError } from '../../../lib/errors';
import { isMultiProjectScan } from '../../../lib/is-multi-project-scan';
import { getEcosystem, monitorEcosystem } from '../../../lib/ecosystems';
import { getFormattedMonitorOutput } from '../../../lib/ecosystems/monitor';

const SEPARATOR = '\n-------------------------------------------------------\n';
const debug = Debug('snyk');
Expand Down Expand Up @@ -95,6 +97,24 @@ async function monitor(...args0: MethodArgs): Promise<any> {
}
}

const ecosystem = getEcosystem(options);
if (ecosystem) {
const commandResult = await monitorEcosystem(
ecosystem,
args as string[],
options,
);

const [monitorResults, monitorErrors] = commandResult;

return await getFormattedMonitorOutput(
results,
monitorResults,
monitorErrors,
options,
);
}

// Part 1: every argument is a scan target; process them sequentially
for (const path of args as string[]) {
debug(`Processing ${path}...`);
Expand Down
1 change: 1 addition & 0 deletions src/lib/ecosystems/index.ts
Expand Up @@ -2,6 +2,7 @@ import { Options } from '../types';
import { Ecosystem } from './types';

export { testEcosystem } from './test';
export { monitorEcosystem } from './monitor';
export { getPlugin } from './plugins';

/**
Expand Down
177 changes: 177 additions & 0 deletions src/lib/ecosystems/monitor.ts
@@ -0,0 +1,177 @@
import { InspectResult } from '@snyk/cli-interface/legacy/plugin';
import chalk from 'chalk';

import * as snyk from '../index';
import * as config from '../config';
import { isCI } from '../is-ci';
import { makeRequest } from '../request/promise';
import { MonitorResult, Options } from '../types';
import * as spinner from '../../lib/spinner';
import { getPlugin } from './plugins';
import { BadResult, GoodResult } from '../../cli/commands/monitor/types';
import { formatMonitorOutput } from '../../cli/commands/monitor/formatters/format-monitor-response';
import { getExtraProjectCount } from '../plugins/get-extra-project-count';
import { MonitorError } from '../errors';
import {
Ecosystem,
ScanResult,
EcosystemMonitorResult,
EcosystemMonitorError,
MonitorDependenciesRequest,
MonitorDependenciesResponse,
} from './types';
import { findAndLoadPolicyForScanResult } from './policy';

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

export async function monitorEcosystem(
ecosystem: Ecosystem,
paths: string[],
options: Options,
): Promise<[EcosystemMonitorResult[], EcosystemMonitorError[]]> {
const plugin = getPlugin(ecosystem);
const scanResultsByPath: { [dir: string]: ScanResult[] } = {};
for (const path of paths) {
await spinner(`Analyzing dependencies in ${path}`);
options.path = path;
const pluginResponse = await plugin.scan(options);
scanResultsByPath[path] = pluginResponse.scanResults;
}
const [monitorResults, errors] = await monitorDependencies(
scanResultsByPath,
options,
);
return [monitorResults, errors];
}

async function generateMonitorDependenciesRequest(
scanResult: ScanResult,
options: Options,
): Promise<MonitorDependenciesRequest> {
// WARNING! This mutates the payload. The project name logic should be handled in the plugin.
scanResult.name =
options['project-name'] || config.PROJECT_NAME || scanResult.name;
// WARNING! This mutates the payload. Policy logic should be in the plugin.
const policy = await findAndLoadPolicyForScanResult(scanResult, options);
if (policy !== undefined) {
scanResult.policy = policy.toString();
}

return {
scanResult,
method: 'cli',
projectName: options['project-name'] || config.PROJECT_NAME || undefined,
};
}

async function monitorDependencies(
scans: {
[dir: string]: ScanResult[];
},
options: Options,
): Promise<[EcosystemMonitorResult[], EcosystemMonitorError[]]> {
const results: EcosystemMonitorResult[] = [];
const errors: EcosystemMonitorError[] = [];
for (const [path, scanResults] of Object.entries(scans)) {
await spinner(`Monitoring dependencies in ${path}`);
for (const scanResult of scanResults) {
const monitorDependenciesRequest = await generateMonitorDependenciesRequest(
scanResult,
options,
);

const payload = {
method: 'PUT',
url: `${config.API}/monitor-dependencies`,
json: true,
headers: {
'x-is-ci': isCI(),
authorization: 'token ' + snyk.api,
},
body: monitorDependenciesRequest,
};
try {
const response = await makeRequest<MonitorDependenciesResponse>(
payload,
);
results.push({
...response,
path,
scanResult,
});
} catch (error) {
if (error.code >= 400 && error.code < 500) {
throw new Error(error.message);
}
errors.push({
error: 'Could not monitor dependencies in ' + path,
path,
scanResult,
});
}
}
}
spinner.clearAll();
return [results, errors];
}

export async function getFormattedMonitorOutput(
results: Array<GoodResult | BadResult>,
monitorResults: EcosystemMonitorResult[],
errors: EcosystemMonitorError[],
options: Options,
): Promise<string> {
for (const monitorResult of monitorResults) {
const monOutput = formatMonitorOutput(
monitorResult.scanResult.identity.type,
monitorResult as MonitorResult,
options,
monitorResult.projectName,
await getExtraProjectCount(
monitorResult.path,
options,
// TODO: Fix to pass the old "inspectResult.plugin.meta.allSubProjectNames", which ecosystem uses this?
// "allSubProjectNames" can become a Fact returned by a plugin.
{} as InspectResult,
),
);
results.push({
ok: true,
data: monOutput,
path: monitorResult.path,
projectName: monitorResult.id,
});
}
for (const monitorError of errors) {
results.push({
ok: false,
data: new MonitorError(500, monitorError),
path: monitorError.path,
});
}

const outputString = results
.map((res) => {
if (res.ok) {
return res.data;
}

const errorMessage =
res.data && res.data.userMessage
? chalk.bold.red(res.data.userMessage)
: res.data
? res.data.message
: 'Unknown error occurred.';

return (
chalk.bold.white('\nMonitoring ' + res.path + '...\n\n') + errorMessage
);
})
.join('\n' + SEPARATOR);

if (results.every((res) => res.ok)) {
return outputString;
}

throw new Error(outputString);
}
34 changes: 34 additions & 0 deletions src/lib/ecosystems/types.ts
Expand Up @@ -82,3 +82,37 @@ export interface EcosystemPlugin {
options: Options,
) => Promise<string>;
}

export interface EcosystemMonitorError {
error: string;
path: string;
scanResult: ScanResult;
}

export interface MonitorDependenciesResponse {
ok: boolean;
org: string;
id: string;
isMonitored: boolean;
licensesPolicy: any;
uri: string;
trialStarted: boolean;
path: string;
projectName: string;
}

export interface EcosystemMonitorResult extends MonitorDependenciesResponse {
scanResult: ScanResult;
}

export interface MonitorDependenciesRequest {
scanResult: ScanResult;

/**
* If provided, overrides the default project name (usually equivalent to the root package).
* @deprecated Must not be set by new code! Prefer to set the "scanResult.name" within your plugin!
*/
projectName?: string;
policy?: string;
method?: 'cli';
}

0 comments on commit e18e4d4

Please sign in to comment.