From 02e768114709b252d9743697c491d5fa5b19c21d Mon Sep 17 00:00:00 2001 From: Ivan Stanev Date: Thu, 1 Oct 2020 23:28:06 +0100 Subject: [PATCH] feat: container static scanning in monitor command 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". --- src/cli/commands/monitor/index.ts | 20 ++++ src/lib/ecosystems/index.ts | 1 + src/lib/ecosystems/monitor.ts | 177 ++++++++++++++++++++++++++++++ src/lib/ecosystems/types.ts | 34 ++++++ 4 files changed, 232 insertions(+) create mode 100644 src/lib/ecosystems/monitor.ts diff --git a/src/cli/commands/monitor/index.ts b/src/cli/commands/monitor/index.ts index ad5740140e..b2e05b5b98 100644 --- a/src/cli/commands/monitor/index.ts +++ b/src/cli/commands/monitor/index.ts @@ -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'); @@ -95,6 +97,24 @@ async function monitor(...args0: MethodArgs): Promise { } } + 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}...`); diff --git a/src/lib/ecosystems/index.ts b/src/lib/ecosystems/index.ts index 01f5c0603c..9ffd905d3f 100644 --- a/src/lib/ecosystems/index.ts +++ b/src/lib/ecosystems/index.ts @@ -2,6 +2,7 @@ import { Options } from '../types'; import { Ecosystem } from './types'; export { testEcosystem } from './test'; +export { monitorEcosystem } from './monitor'; export { getPlugin } from './plugins'; /** diff --git a/src/lib/ecosystems/monitor.ts b/src/lib/ecosystems/monitor.ts new file mode 100644 index 0000000000..b4a784d579 --- /dev/null +++ b/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 { + // 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( + 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, + monitorResults: EcosystemMonitorResult[], + errors: EcosystemMonitorError[], + options: Options, +): Promise { + 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); +} diff --git a/src/lib/ecosystems/types.ts b/src/lib/ecosystems/types.ts index 9dcd9e0dae..c0cdffa9cc 100644 --- a/src/lib/ecosystems/types.ts +++ b/src/lib/ecosystems/types.ts @@ -82,3 +82,37 @@ export interface EcosystemPlugin { options: Options, ) => Promise; } + +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'; +}