From 430745a4d9f09b07bac3fe607040be5e68d5e95d Mon Sep 17 00:00:00 2001 From: orkamara Date: Mon, 25 May 2020 09:44:49 +0300 Subject: [PATCH 1/8] chore: snyk-test types + use SupportedProjectTypes --- src/lib/cloud-config-projects.ts | 2 - src/lib/cloud-config/cloud-config-projects.ts | 5 ++ src/lib/snyk-test/legacy.ts | 3 +- src/lib/snyk-test/run-test.ts | 36 +------------- src/lib/snyk-test/types.ts | 47 +++++++++++++++++++ src/lib/types.ts | 2 +- 6 files changed, 57 insertions(+), 38 deletions(-) delete mode 100644 src/lib/cloud-config-projects.ts create mode 100644 src/lib/cloud-config/cloud-config-projects.ts create mode 100644 src/lib/snyk-test/types.ts diff --git a/src/lib/cloud-config-projects.ts b/src/lib/cloud-config-projects.ts deleted file mode 100644 index b82c763f380..00000000000 --- a/src/lib/cloud-config-projects.ts +++ /dev/null @@ -1,2 +0,0 @@ -//TODO (ork): add 'k8sconfig' | 'helmconfig' -export type SupportedCloudConfigs = 'undefined'; diff --git a/src/lib/cloud-config/cloud-config-projects.ts b/src/lib/cloud-config/cloud-config-projects.ts new file mode 100644 index 00000000000..09235319bbb --- /dev/null +++ b/src/lib/cloud-config/cloud-config-projects.ts @@ -0,0 +1,5 @@ +export type SupportedCloudConfigs = 'k8sconfig' | 'helmconfig'; + +export const TEST_SUPPORTED_CLOUD_CONFIG_PROJECTS: SupportedCloudConfigs[] = [ + 'k8sconfig', +]; diff --git a/src/lib/snyk-test/legacy.ts b/src/lib/snyk-test/legacy.ts index e698a04b5b3..bea26b8f59f 100644 --- a/src/lib/snyk-test/legacy.ts +++ b/src/lib/snyk-test/legacy.ts @@ -1,6 +1,7 @@ import * as _ from '@snyk/lodash'; import * as depGraphLib from '@snyk/dep-graph'; import { SupportedPackageManagers } from '../package-managers'; +import { SupportedProjectTypes } from '../types'; import { SEVERITIES } from './common'; interface Pkg { @@ -279,7 +280,7 @@ export interface RemediationChanges { function convertTestDepGraphResultToLegacy( res: TestDepGraphResponse, depGraph: depGraphLib.DepGraph, - packageManager: string, + packageManager: SupportedProjectTypes | undefined, severityThreshold?: SEVERITY, ): LegacyVulnApiResult { const result = res.result; diff --git a/src/lib/snyk-test/run-test.ts b/src/lib/snyk-test/run-test.ts index 74b653276e2..308e9788b60 100644 --- a/src/lib/snyk-test/run-test.ts +++ b/src/lib/snyk-test/run-test.ts @@ -49,44 +49,12 @@ import { getSubProjectCount } from '../plugins/get-sub-project-count'; import { serializeCallGraphWithMetrics } from '../reachable-vulns'; import { validateOptions } from '../options-validator'; import { findAndLoadPolicy } from '../policy'; +import { Payload, PayloadBody, DepTreeFromResolveDeps } from './types'; const debug = debugModule('snyk'); export = runTest; -interface DepTreeFromResolveDeps extends DepTree { - numDependencies: number; - pluck: any; -} - -interface PayloadBody { - depGraph?: depGraphLib.DepGraph; // missing for legacy endpoint (options.vulnEndpoint) - callGraph?: any; - policy?: string; - targetFile?: string; - targetFileRelativePath?: string; - projectNameOverride?: string; - hasDevDependencies?: boolean; - originalProjectName?: string; // used only for display - foundProjectCount?: number; // used only for display - docker?: any; - displayTargetFile?: string; - target?: GitTarget | ContainerTarget | null; -} - -interface Payload { - method: string; - url: string; - json: boolean; - headers: { - 'x-is-ci': boolean; - authorization: string; - }; - body?: PayloadBody; - qs?: object | null; - modules?: DepTreeFromResolveDeps; -} - async function runTest( projectType: SupportedProjectTypes | undefined, root: string, @@ -173,7 +141,7 @@ async function runTest( async function parseRes( depGraph: depGraphLib.DepGraph | undefined, - pkgManager: string | undefined, + pkgManager: SupportedProjectTypes | undefined, res: LegacyVulnApiResult, options: Options & TestOptions, payload: Payload, diff --git a/src/lib/snyk-test/types.ts b/src/lib/snyk-test/types.ts new file mode 100644 index 00000000000..0d2eb036b3e --- /dev/null +++ b/src/lib/snyk-test/types.ts @@ -0,0 +1,47 @@ +import * as depGraphLib from '@snyk/dep-graph'; +import { GitTarget, ContainerTarget } from '../project-metadata/types'; +import { DepTree } from '../types'; + +interface PayloadBody { + depGraph?: depGraphLib.DepGraph; // missing for legacy endpoint (options.vulnEndpoint) + callGraph?: any; + policy?: string; + targetFile?: string; + targetFileRelativePath?: string; + projectNameOverride?: string; + hasDevDependencies?: boolean; + originalProjectName?: string; // used only for display + foundProjectCount?: number; // used only for display + docker?: any; + displayTargetFile?: string; + target?: GitTarget | ContainerTarget | null; +} + +export interface DepTreeFromResolveDeps extends DepTree { + numDependencies: number; + pluck: any; +} + +export interface Payload { + method: string; + url: string; + json: boolean; + headers: { + 'x-is-ci': boolean; + authorization: string; + }; + body?: PayloadBody | CloudConfigPayloadBody; + qs?: object | null; + modules?: DepTreeFromResolveDeps; +} + +export interface CloudConfigPayloadBody { + policy: string; + targetFile?: string; + targetFileRelativePath?: string; + originalProjectName?: string; // used only for display + foundProjectCount?: number; // used only for display + displayTargetFile?: string; + target?: GitTarget | ContainerTarget | null; + fileContent: string; +} diff --git a/src/lib/types.ts b/src/lib/types.ts index d19948468dd..01a3ef21d58 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -1,5 +1,5 @@ import { SupportedPackageManagers } from './package-managers'; -import { SupportedCloudConfigs } from './cloud-config-projects'; +import { SupportedCloudConfigs } from './cloud-config/cloud-config-projects'; import { legacyCommon as legacyApi } from '@snyk/cli-interface'; import { SEVERITY } from './snyk-test/legacy'; import { FailOn } from './snyk-test/common'; From 17170c01d5a5247c2748e2a1548234a678286695 Mon Sep 17 00:00:00 2001 From: orkamara Date: Wed, 27 May 2020 14:40:46 +0300 Subject: [PATCH 2/8] feat: add new iac (Infra as Code) mode --- help/iac.txt | 24 ++++++++++++++++++++++++ src/cli/modes.ts | 8 ++++++++ src/lib/types.ts | 1 + 3 files changed, 33 insertions(+) create mode 100644 help/iac.txt diff --git a/help/iac.txt b/help/iac.txt new file mode 100644 index 00000000000..31bc6da7955 --- /dev/null +++ b/help/iac.txt @@ -0,0 +1,24 @@ +Usage: + + $ snyk iac [command] [options] --file= + +Find security issues in your Infrastructure as Code files. + +Commands: + + test ............... Test for any known issue. + +Options: + + -h, --help + --json .................................. Return results in JSON format. + --project-name= ................. Specify a custom Snyk project name. + --policy-path= .................... Manually pass a path to a snyk policy file. + --severity-threshold=... Only report issues of provided level or higher. + +Examples: + + $ snyk iac test /path/to/Kubernetes.yaml + + +For more information see https://support.snyk.io/hc/en-us/categories/360001342678-Infrastructure-as-code diff --git a/src/cli/modes.ts b/src/cli/modes.ts index fa7358dd8db..b050f821f11 100644 --- a/src/cli/modes.ts +++ b/src/cli/modes.ts @@ -13,6 +13,14 @@ const modes: Record = { args['docker'] = true; args['experimental'] = true; + return args; + }, + }, + iac: { + allowedCommands: ['test'], + config: (args): [] => { + args['iac'] = true; + return args; }, }, diff --git a/src/lib/types.ts b/src/lib/types.ts index 01a3ef21d58..73f64a0ca33 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -41,6 +41,7 @@ export interface Options { org?: string | null; path: string; docker?: boolean; + iac?: boolean; file?: string; policy?: string; json?: boolean; From 9fe44b2f4a0080d570287aaf0c23951c333e7175 Mon Sep 17 00:00:00 2001 From: orkamara Date: Wed, 27 May 2020 15:00:57 +0300 Subject: [PATCH 3/8] feat: add Kubernetes configs detection logic --- src/lib/cloud-config/cloud-config-parser.ts | 93 +++++++++++++++++++++ src/lib/detect.ts | 25 ++++++ src/lib/errors/index.ts | 4 + src/lib/errors/invalid-cloud-config-file.ts | 40 +++++++++ 4 files changed, 162 insertions(+) create mode 100644 src/lib/cloud-config/cloud-config-parser.ts create mode 100644 src/lib/errors/invalid-cloud-config-file.ts diff --git a/src/lib/cloud-config/cloud-config-parser.ts b/src/lib/cloud-config/cloud-config-parser.ts new file mode 100644 index 00000000000..4a1cdac6266 --- /dev/null +++ b/src/lib/cloud-config/cloud-config-parser.ts @@ -0,0 +1,93 @@ +//TODO(orka): take out into a new lib +import * as YAML from 'js-yaml'; +import * as debugLib from 'debug'; +import { + IllegalCloudConfigFileError, + NotSupportedCloudConfigFileError, +} from './../errors'; + +const debug = debugLib('snyk-detect'); + +const mandatoryKeysForSupportedK8sKinds = { + deployment: ['apiVersion', 'metadata', 'spec'], + pod: ['apiVersion', 'metadata', 'spec'], + service: ['apiVersion', 'metadata', 'spec'], + podsecuritypolicy: ['apiVersion', 'metadata', 'spec'], + networkpolicy: ['apiVersion', 'metadata', 'spec'], +}; + +function getFileType(filePath: string): string { + const filePathSplit = filePath.split('.'); + return filePathSplit[filePathSplit.length - 1].toLowerCase(); +} + +function parseYamlOrJson(fileContent: string, filePath: string): any { + const fileType = getFileType(filePath); + switch (fileType) { + case 'yaml': + case 'yml': + try { + return YAML.safeLoadAll(fileContent); + } catch (e) { + debug('Failed to parse cloud config as a YAML'); + } + break; + case 'json': + try { + const objectsArr: any[] = []; + objectsArr.push(JSON.parse(fileContent)); + return objectsArr; + } catch (e) { + debug('Failed to parse cloud config as a JSON'); + } + break; + default: + debug(`Unsupported cloud config file type (${fileType})`); + } + return undefined; +} + +// This function validates that there is at least one valid doc with a k8s object kind. +// A valid k8s object has a kind key (.kind) from the keys of `mandatoryKeysForSupportedK8sKinds` +// and all of the keys from `mandatoryKeysForSupportedK8sKinds[kind]`. +// If there is a doc with a supported kind, but invalid, we should fail +// The function return true if the yaml is a valid k8s one, or false otherwise +export function validateK8sFile( + fileContent: string, + filePath: string, + root: string, +) { + const k8sObjects: any[] = parseYamlOrJson(fileContent, filePath); + if (!k8sObjects) { + throw IllegalCloudConfigFileError([root]); + } + + let numOfSupportedKeyDocs = 0; + for (let i = 0; i < k8sObjects.length; i++) { + const k8sObject = k8sObjects[i]; + if (!k8sObject || !k8sObject.kind) { + continue; + } + + const kind = k8sObject.kind.toLowerCase(); + if (!Object.keys(mandatoryKeysForSupportedK8sKinds).includes(kind)) { + continue; + } + + numOfSupportedKeyDocs++; + + for (let i = 0; i < mandatoryKeysForSupportedK8sKinds[kind].length; i++) { + const key = mandatoryKeysForSupportedK8sKinds[kind][i]; + if (!k8sObject[key]) { + debug(`Missing key (${key}) from supported k8s object kind (${kind})`); + throw IllegalCloudConfigFileError([root]); + } + } + } + + if (numOfSupportedKeyDocs === 0) { + throw NotSupportedCloudConfigFileError([root]); + } + + debug(`k8s config found (${filePath})`); +} diff --git a/src/lib/detect.ts b/src/lib/detect.ts index 643fcba40a0..3ffea485ee1 100644 --- a/src/lib/detect.ts +++ b/src/lib/detect.ts @@ -4,6 +4,7 @@ import * as debugLib from 'debug'; import * as _ from '@snyk/lodash'; import { NoSupportedManifestsFoundError } from './errors'; import { SupportedPackageManagers } from './package-managers'; +import { validateK8sFile } from './cloud-config/cloud-config-parser'; const debug = debugLib('snyk-detect'); @@ -137,6 +138,30 @@ export function detectPackageManager(root: string, options) { return packageManager; } +export function isCloudConfigProject(root: string, options): string { + if (!isLocalFolder(root)) { + debug('Cloud Config - repo case ' + root); + throw "iac option doesn't support lookup as repo"; + } + + if (!options.file) { + debug('Cloud Config - no file specified ' + root); + throw 'iac option works only with specified files'; + } + + if (localFileSuppliedButNotFound(root, options.file)) { + throw new Error( + 'Could not find the specified file: ' + + options.file + + '\nPlease check that it exists and try again.', + ); + } + const filePath = pathLib.resolve(root, options.file); + const fileContent = fs.readFileSync(filePath, 'utf-8'); + validateK8sFile(fileContent, filePath, root); + return 'k8sconfig'; +} + // User supplied a "local" file, but that file doesn't exist function localFileSuppliedButNotFound(root, file) { return ( diff --git a/src/lib/errors/index.ts b/src/lib/errors/index.ts index adfa47de0a8..2bc2a7457dd 100644 --- a/src/lib/errors/index.ts +++ b/src/lib/errors/index.ts @@ -19,3 +19,7 @@ export { OptionMissingErrorError } from './option-missing-error'; export { ExcludeFlagBadInputError } from './exclude-flag-bad-input'; export { UnsupportedOptionCombinationError } from './unsupported-option-combination-error'; export { FeatureNotSupportedByPackageManagerError } from './feature-not-supported-by-package-manager-error'; +export { + NotSupportedCloudConfigFileError, + IllegalCloudConfigFileError, +} from './invalid-cloud-config-file'; diff --git a/src/lib/errors/invalid-cloud-config-file.ts b/src/lib/errors/invalid-cloud-config-file.ts new file mode 100644 index 00000000000..d8497bfba80 --- /dev/null +++ b/src/lib/errors/invalid-cloud-config-file.ts @@ -0,0 +1,40 @@ +import chalk from 'chalk'; +import { CustomError } from './custom-error'; + +export function NotSupportedCloudConfigFileError(atLocations: string[]) { + const locationsStr = atLocations.join(', '); + const errorMsg = + 'Not supported Cloud Config target files in ' + + locationsStr + + '.\nPlease see our documentation for supported languages and ' + + 'target files: ' + + chalk.underline( + 'https://support.snyk.io/hc/en-us/articles/360000911957-Language-support', + ) + + ' and make sure you are in the right directory.'; + + const error = new CustomError(errorMsg); + error.code = 422; + error.userMessage = errorMsg; + return error; +} + +export function IllegalCloudConfigFileError( + atLocations: string[], +): CustomError { + const locationsStr = atLocations.join(', '); + const errorMsg = + 'Illegal Cloud Config target file ' + + locationsStr + + '.\nPlease see our documentation for supported languages and ' + + 'target files: ' + + chalk.underline( + 'https://support.snyk.io/hc/en-us/articles/360000911957-Language-support', + ) + + ' and make sure you are in the right directory.'; + + const error = new CustomError(errorMsg); + error.code = 422; + error.userMessage = errorMsg; + return error; +} From 501b58b32da237fd93ade3fc6f41ab144b9cab50 Mon Sep 17 00:00:00 2001 From: orkamara Date: Mon, 22 Jun 2020 13:37:22 +0300 Subject: [PATCH 4/8] chore: refactoring runTest() --- src/lib/snyk-test/run-test.ts | 41 ++++++++++++++++++++++++----------- 1 file changed, 28 insertions(+), 13 deletions(-) diff --git a/src/lib/snyk-test/run-test.ts b/src/lib/snyk-test/run-test.ts index 308e9788b60..e61ca4ca140 100644 --- a/src/lib/snyk-test/run-test.ts +++ b/src/lib/snyk-test/run-test.ts @@ -55,22 +55,25 @@ const debug = debugModule('snyk'); export = runTest; -async function runTest( - projectType: SupportedProjectTypes | undefined, +async function sendAndParseResults( + payloads: Payload[], + spinnerLbl: string, root: string, options: Options & TestOptions, ): Promise { const results: TestResult[] = []; - const spinnerLbl = 'Querying vulnerabilities database...'; - try { - await validateOptions(options, options.packageManager); - const payloads = await assemblePayloads(root, options); + for (const payload of payloads) { const payloadPolicy = payload.body && payload.body.policy; const depGraph = payload.body && payload.body.depGraph; + const depGraphPayload: PayloadBody = payload.body as PayloadBody; + const payloadPolicy = depGraphPayload && depGraphPayload.policy; + const depGraph = depGraphPayload && depGraphPayload.depGraph; const pkgManager = - depGraph && depGraph.pkgManager && depGraph.pkgManager.name; - const targetFile = payload.body && payload.body.targetFile; + depGraph && + depGraph.pkgManager && + (depGraph.pkgManager.name as SupportedProjectTypes); + const targetFile = depGraphPayload && depGraphPayload.targetFile; const projectName = _.get(payload, 'body.projectNameOverride') || _.get(payload, 'body.originalProjectName'); @@ -78,15 +81,15 @@ async function runTest( const displayTargetFile = _.get(payload, 'body.displayTargetFile'); let dockerfilePackages; if ( - payload.body && - payload.body.docker && - payload.body.docker.dockerfilePackages + depGraphPayload && + depGraphPayload.docker && + depGraphPayload.docker.dockerfilePackages ) { - dockerfilePackages = payload.body.docker.dockerfilePackages; + dockerfilePackages = depGraphPayload.docker.dockerfilePackages; } await spinner(spinnerLbl); analytics.add('depGraph', !!depGraph); - analytics.add('isDocker', !!(payload.body && payload.body.docker)); + analytics.add('isDocker', !!(depGraphPayload && depGraphPayload.docker)); // Type assertion might be a lie, but we are correcting that below const res = (await sendTestPayload(payload)) as LegacyVulnApiResult; @@ -110,6 +113,18 @@ async function runTest( }); } return results; +} + +async function runTest( + projectType: SupportedProjectTypes | undefined, + root: string, + options: Options & TestOptions, +): Promise { + const spinnerLbl = 'Querying vulnerabilities database...'; + try { + await validateOptions(options, options.packageManager); + const payloads = await assemblePayloads(root, options); + return await sendAndParseResults(payloads, spinnerLbl, root, options); } catch (error) { debug('Error running test', { error }); // handling denial from registry because of the feature flag From 5dc2c44e35fb2bc300a67ea08fa14b41fe70126e Mon Sep 17 00:00:00 2001 From: orkamara Date: Mon, 22 Jun 2020 13:38:19 +0300 Subject: [PATCH 5/8] feat: support IaC configs test (K8s only) --- src/cli/commands/test/cloud-config-output.ts | 60 +++++++++++++ .../test/formatters/format-test-meta.ts | 51 ++++++----- src/cli/commands/test/index.ts | 48 ++++++++++- src/lib/snyk-test/cloud-config-test-result.ts | 23 +++++ src/lib/snyk-test/index.js | 24 +++++- src/lib/snyk-test/legacy.ts | 19 ++-- src/lib/snyk-test/payload-schema.ts | 23 +++++ src/lib/snyk-test/run-cloud-config-test.ts | 86 +++++++++++++++++++ src/lib/snyk-test/run-test.ts | 69 ++++++++++----- src/lib/snyk-test/types.ts | 16 +--- 10 files changed, 350 insertions(+), 69 deletions(-) create mode 100644 src/cli/commands/test/cloud-config-output.ts create mode 100644 src/lib/snyk-test/cloud-config-test-result.ts create mode 100644 src/lib/snyk-test/payload-schema.ts create mode 100644 src/lib/snyk-test/run-cloud-config-test.ts diff --git a/src/cli/commands/test/cloud-config-output.ts b/src/cli/commands/test/cloud-config-output.ts new file mode 100644 index 00000000000..6b607806ba2 --- /dev/null +++ b/src/cli/commands/test/cloud-config-output.ts @@ -0,0 +1,60 @@ +import chalk from 'chalk'; +import * as Debug from 'debug'; +import { Options, TestOptions } from '../../../lib/types'; +import { CloudConfigTestResult } from '../../../lib/snyk-test/cloud-config-test-result'; +import { getSeverityValue } from './formatters'; +import { formatIssue } from './formatters/remediation-based-format-issues'; +import { AnnotatedCloudConfigIssue } from '../../../lib/snyk-test/cloud-config-test-result'; + +const debug = Debug('cloud-config-output'); + +export function getCloudConfigDisplayedOutput( + res: CloudConfigTestResult, + testOptions: Options & TestOptions, + testedInfoText: string, + meta: string, + prefix: string, +): string { + const issuesTextArray = [ + chalk.bold.white('\nInfrastructure as code issues:'), + ]; + + const NoNote = false; + const NotNew = false; + + const issues: AnnotatedCloudConfigIssue[] = res.result.cloudConfigResults; + debug(`iac display output - ${issues.length} issues`); + + issues + .sort((a, b) => getSeverityValue(b.severity) - getSeverityValue(a.severity)) + .forEach((issue) => { + const path: string[][] = [issue.cloudConfigPath]; + issuesTextArray.push( + formatIssue( + issue.id, + issue.title, + issue.severity, + NotNew, + issue.subType, + path, + testOptions, + NoNote, + ), + ); + }); + + const issuesInfoOutput: string[] = []; + debug(`Cloud Config display output - ${issuesTextArray.length} issues text`); + if (issuesTextArray.length > 0) { + issuesInfoOutput.push(issuesTextArray.join('\n')); + } + + let body = issuesInfoOutput.join('\n\n') + '\n\n' + meta; + + const vulnCountText = `found ${issues.length} issues`; + const summary = testedInfoText + ', ' + chalk.red.bold(vulnCountText); + + body = body + '\n\n' + summary; + + return prefix + body; +} diff --git a/src/cli/commands/test/formatters/format-test-meta.ts b/src/cli/commands/test/formatters/format-test-meta.ts index ec26b3aa975..37ea1f037b4 100644 --- a/src/cli/commands/test/formatters/format-test-meta.ts +++ b/src/cli/commands/test/formatters/format-test-meta.ts @@ -2,9 +2,10 @@ import chalk from 'chalk'; import { rightPadWithSpaces } from '../../../../lib/right-pad'; import { TestOptions, Options } from '../../../../lib/types'; import { TestResult } from '../../../../lib/snyk-test/legacy'; +import { CloudConfigTestResult } from '../../../../lib/snyk-test/cloud-config-test-result'; export function formatTestMeta( - res: TestResult, + res: TestResult | CloudConfigTestResult, options: Options & TestOptions, ): string { const padToLength = 19; // chars to align @@ -41,31 +42,37 @@ export function formatTestMeta( options.path, ); } - if (res.docker && res.docker.baseImage) { - meta.push( - chalk.bold(rightPadWithSpaces('Base image: ', padToLength)) + - res.docker.baseImage, - ); - } + if (res.payloadType !== 'k8sconfig') { + const legacyRes: TestResult = res as TestResult; + if (legacyRes.docker && legacyRes.docker.baseImage) { + meta.push( + chalk.bold(rightPadWithSpaces('Base image: ', padToLength)) + + legacyRes.docker.baseImage, + ); + } - if (res.filesystemPolicy) { - meta.push( - chalk.bold(rightPadWithSpaces('Local Snyk policy: ', padToLength)) + - chalk.green('found'), - ); - if (res.ignoreSettings && res.ignoreSettings.disregardFilesystemIgnores) { + if (legacyRes.filesystemPolicy) { meta.push( - chalk.bold( - rightPadWithSpaces('Local Snyk policy ignored: ', padToLength), - ) + chalk.red('yes'), + chalk.bold(rightPadWithSpaces('Local Snyk policy: ', padToLength)) + + chalk.green('found'), + ); + if ( + legacyRes.ignoreSettings && + legacyRes.ignoreSettings.disregardFilesystemIgnores + ) { + meta.push( + chalk.bold( + rightPadWithSpaces('Local Snyk policy ignored: ', padToLength), + ) + chalk.red('yes'), + ); + } + } + if (legacyRes.licensesPolicy) { + meta.push( + chalk.bold(rightPadWithSpaces('Licenses: ', padToLength)) + + chalk.green('enabled'), ); } - } - if (res.licensesPolicy) { - meta.push( - chalk.bold(rightPadWithSpaces('Licenses: ', padToLength)) + - chalk.green('enabled'), - ); } return meta.join('\n'); diff --git a/src/cli/commands/test/index.ts b/src/cli/commands/test/index.ts index dc6909bea78..beed128a18f 100644 --- a/src/cli/commands/test/index.ts +++ b/src/cli/commands/test/index.ts @@ -24,6 +24,7 @@ import { TestResult, VulnMetaData, } from '../../../lib/snyk-test/legacy'; +import { CloudConfigTestResult } from '../../../lib/snyk-test/cloud-config-test-result'; import { SupportedPackageManagers, WIZARD_SUPPORTED_PACKAGE_MANAGERS, @@ -43,6 +44,7 @@ import { summariseVulnerableResults, } from './formatters'; import * as utils from './utils'; +import { getCloudConfigDisplayedOutput } from './cloud-config-output'; const debug = Debug('snyk-test'); const SEPARATOR = '\n-------------------------------------------------------\n'; @@ -156,7 +158,11 @@ async function test(...args: MethodArgs): Promise { } const vulnerableResults = results.filter( - (res) => res.vulnerabilities && res.vulnerabilities.length, + (res) => + (res.vulnerabilities && res.vulnerabilities.length) || + (res.result && + res.result.cloudConfigResults && + res.result.cloudConfigResults.length), ); const errorResults = results.filter((res) => res instanceof Error); const notSuccess = errorResults.length > 0; @@ -165,7 +171,9 @@ async function test(...args: MethodArgs): Promise { // resultOptions is now an array of 1 or more options used for // the tests results is now an array of 1 or more test results // values depend on `options.json` value - string or object - const errorMappedResults = createErrorMappedResultsForJsonOutput(results); + const errorMappedResults = !options.iac + ? createErrorMappedResultsForJsonOutput(results) + : createErrorMappedResultsForJsonOutputForCloudConfig(results); // backwards compat - strip array IFF only one result const dataToSend = errorMappedResults.length === 1 @@ -295,6 +303,25 @@ function createErrorMappedResultsForJsonOutput(results) { return errorMappedResults; } +function createErrorMappedResultsForJsonOutputForCloudConfig(results) { + const errorMappedResults = results.map((result) => { + // add json for when thrown exception + if (result instanceof Error) { + return { + ok: false, + error: result.message, + path: (result as any).path, + }; + } + const res = { ...result, ...result.result }; + delete res.result; + delete res.meta; + return res; + }); + + return errorMappedResults; +} + function shouldFail(vulnerableResults: any[], failOn: FailOn) { // find reasons not to fail if (failOn === 'all') { @@ -378,7 +405,10 @@ function displayResult( if (res instanceof Error) { return prefix + res.message; } - const issuesText = res.licensesPolicy ? 'issues' : 'vulnerabilities'; + const issuesText = + res.licensesPolicy || projectType === 'k8sconfig' + ? 'issues' + : 'vulnerabilities'; let pathOrDepsText = ''; if (res.hasOwnProperty('dependencyCount')) { @@ -431,10 +461,20 @@ function displayResult( ); } + if (res.packageManager === 'k8sconfig') { + return getCloudConfigDisplayedOutput( + (res as any) as CloudConfigTestResult, + options, + testedInfoText, + meta, + prefix, + ); + } + // NOT OK => We found some vulns, let's format the vulns info return getDisplayedOutput( - res, + res as TestResult, options, testedInfoText, localPackageTest, diff --git a/src/lib/snyk-test/cloud-config-test-result.ts b/src/lib/snyk-test/cloud-config-test-result.ts new file mode 100644 index 00000000000..a92fd43f206 --- /dev/null +++ b/src/lib/snyk-test/cloud-config-test-result.ts @@ -0,0 +1,23 @@ +import { BasicResultData, SEVERITY } from './legacy'; + +export interface AnnotatedCloudConfigIssue { + id: string; + title: string; + description: string; + severity: SEVERITY; + isIgnored: boolean; + cloudConfigPath: string[]; + type: string; + subType: string; +} + +export interface CloudConfigTestResult extends BasicResultData { + targetFile: string; + projectName: string; + displayTargetFile: string; // used for display only + foundProjectCount: number; + result: { + cloudConfigResults: AnnotatedCloudConfigIssue[]; + projectType: string; + }; +} diff --git a/src/lib/snyk-test/index.js b/src/lib/snyk-test/index.js index eed58c3aff2..679b76b1fca 100644 --- a/src/lib/snyk-test/index.js +++ b/src/lib/snyk-test/index.js @@ -4,7 +4,11 @@ const detect = require('../detect'); const runTest = require('./run-test'); const chalk = require('chalk'); const pm = require('../package-managers'); -const { UnsupportedPackageManagerError } = require('../errors'); +const cloudConfigProjects = require('../cloud-config/cloud-config-projects'); +const { + UnsupportedPackageManagerError, + NoSupportedCloudConfigFileError, +} = require('../errors'); async function test(root, options, callback) { if (typeof options === 'function') { @@ -28,7 +32,11 @@ async function test(root, options, callback) { function executeTest(root, options) { try { if (!options.allProjects) { - options.packageManager = detect.detectPackageManager(root, options); + if (options.iac) { + options.packageManager = detect.isCloudConfigProject(root, options); + } else { + options.packageManager = detect.detectPackageManager(root, options); + } } return run(root, options).then((results) => { for (const res of results) { @@ -49,6 +57,18 @@ function executeTest(root, options) { } function run(root, options) { + if (options.iac) { + const projectType = options.packageManager; + if ( + !cloudConfigProjects.TEST_SUPPORTED_CLOUD_CONFIG_PROJECTS.includes( + projectType, + ) + ) { + throw new NoSupportedCloudConfigFileError(projectType); + } + return runTest(projectType, root, options); + } + const packageManager = options.packageManager; if ( !( diff --git a/src/lib/snyk-test/legacy.ts b/src/lib/snyk-test/legacy.ts index bea26b8f59f..3972e8c6e94 100644 --- a/src/lib/snyk-test/legacy.ts +++ b/src/lib/snyk-test/legacy.ts @@ -122,24 +122,27 @@ export interface IgnoreSettings { disregardFilesystemIgnores: boolean; } -export interface LegacyVulnApiResult { - vulnerabilities: AnnotatedIssue[]; +export interface BasicResultData { ok: boolean; - dependencyCount: number; + payloadType?: string; org: string; - policy: string; isPrivate: boolean; + summary: string; + packageManager?: SupportedProjectTypes; + severityThreshold?: string; +} + +export interface LegacyVulnApiResult extends BasicResultData { + vulnerabilities: AnnotatedIssue[]; + dependencyCount: number; + policy: string; licensesPolicy: object | null; - packageManager: string; ignoreSettings: IgnoreSettings | null; - summary: string; docker?: { baseImage?: any; binariesVulns?: unknown; baseImageRemediation?: BaseImageRemediation; }; - severityThreshold?: string; - filesystemPolicy?: boolean; uniqueCount?: any; remediation?: RemediationChanges; diff --git a/src/lib/snyk-test/payload-schema.ts b/src/lib/snyk-test/payload-schema.ts new file mode 100644 index 00000000000..960623ac147 --- /dev/null +++ b/src/lib/snyk-test/payload-schema.ts @@ -0,0 +1,23 @@ +//TODO(orka): future - change this file +import { SupportedCloudConfigs } from '../cloud-config/cloud-config-projects'; + +interface Scan { + type: string; + targetFile: string; + data: any; +} + +export interface CloudConfigFile { + fileContent: string; + fileType: 'yaml' | 'yml' | 'json'; +} + +export interface CloudConfigScan extends Scan { + type: SupportedCloudConfigs; + targetFile: string; + data: CloudConfigFile; + targetFileRelativePath: string; + originalProjectName: string; + policy: string; + projectNameOverride?: string; +} diff --git a/src/lib/snyk-test/run-cloud-config-test.ts b/src/lib/snyk-test/run-cloud-config-test.ts new file mode 100644 index 00000000000..61d5a45358f --- /dev/null +++ b/src/lib/snyk-test/run-cloud-config-test.ts @@ -0,0 +1,86 @@ +import * as fs from 'fs'; +import * as path from 'path'; +import * as pathUtil from 'path'; +import { TestResult } from './legacy'; +import { CloudConfigTestResult } from './cloud-config-test-result'; +import * as snyk from '..'; +import { isCI } from '../is-ci'; +import * as common from './common'; +import * as config from '../config'; +import { Options, TestOptions } from '../types'; +import { Payload } from './types'; +import { CloudConfigScan } from './payload-schema'; +import { SEVERITY } from './legacy'; +import * as pathLib from 'path'; + +export async function parseCloudConfigRes( + res: CloudConfigTestResult, + targetFile: string | undefined, + projectName: any, + severityThreshold?: SEVERITY, + //TODO(orka): future - return a proper type +): Promise { + const meta = (res as any).meta || {}; + + severityThreshold = + severityThreshold === SEVERITY.LOW ? undefined : severityThreshold; + + return { + ...res, + vulnerabilities: [], + dependencyCount: 0, + licensesPolicy: null, + ignoreSettings: null, + targetFile, + projectName, + org: meta.org, + policy: meta.policy, + isPrivate: !meta.isPublic, + severityThreshold, + }; +} + +export async function assembleCloudConfigLocalPayloads( + root: string, + options: Options & TestOptions, +): Promise { + const payloads: Payload[] = []; + if (!options.file) { + return payloads; + } + // Forcing options.path to be a string as pathUtil requires is to be stringified + const targetFile = pathLib.resolve(root, options.file); + const targetFileRelativePath = targetFile + ? pathUtil.join(pathUtil.resolve(`${options.path}`), targetFile) + : ''; + + const fileContent = fs.readFileSync(targetFile, 'utf8'); + const body: CloudConfigScan = { + data: { + fileContent, + fileType: 'yaml', + }, + targetFile: options.file, + type: 'k8sconfig', + //TODO(orka): future - support policy + policy: '', + targetFileRelativePath: `${targetFileRelativePath}`, // Forcing string + originalProjectName: path.basename(path.dirname(targetFile)), + projectNameOverride: options.projectName, + }; + + const payload: Payload = { + method: 'POST', + url: config.API + (options.vulnEndpoint || '/test-iac'), + json: true, + headers: { + 'x-is-ci': isCI(), + authorization: 'token ' + (snyk as any).api, + }, + qs: common.assembleQueryString(options), + body, + }; + + payloads.push(payload); + return payloads; +} diff --git a/src/lib/snyk-test/run-test.ts b/src/lib/snyk-test/run-test.ts index e61ca4ca140..00a56beb83b 100644 --- a/src/lib/snyk-test/run-test.ts +++ b/src/lib/snyk-test/run-test.ts @@ -5,15 +5,17 @@ import * as debugModule from 'debug'; import * as pathUtil from 'path'; import { parsePackageString as moduleToObject } from 'snyk-module'; import * as depGraphLib from '@snyk/dep-graph'; +import { CloudConfigScan } from './payload-schema'; import { TestResult, DockerIssue, AnnotatedIssue, - LegacyVulnApiResult, TestDepGraphResponse, convertTestDepGraphResultToLegacy, + LegacyVulnApiResult, } from './legacy'; +import { CloudConfigTestResult } from './cloud-config-test-result'; import { AuthFailedError, InternalServerError, @@ -49,6 +51,10 @@ import { getSubProjectCount } from '../plugins/get-sub-project-count'; import { serializeCallGraphWithMetrics } from '../reachable-vulns'; import { validateOptions } from '../options-validator'; import { findAndLoadPolicy } from '../policy'; +import { + assembleCloudConfigLocalPayloads, + parseCloudConfigRes, +} from './run-cloud-config-test'; import { Payload, PayloadBody, DepTreeFromResolveDeps } from './types'; const debug = debugModule('snyk'); @@ -62,18 +68,32 @@ async function sendAndParseResults( options: Options & TestOptions, ): Promise { const results: TestResult[] = []; + for (const payload of payloads) { + await spinner(spinnerLbl); + if (options.iac) { + const cloudConfigScan: CloudConfigScan = payload.body as CloudConfigScan; + analytics.add('iac type', !!cloudConfigScan.type); + const res = (await sendTestPayload(payload)) as CloudConfigTestResult; - for (const payload of payloads) { - const payloadPolicy = payload.body && payload.body.policy; - const depGraph = payload.body && payload.body.depGraph; - const depGraphPayload: PayloadBody = payload.body as PayloadBody; - const payloadPolicy = depGraphPayload && depGraphPayload.policy; - const depGraph = depGraphPayload && depGraphPayload.depGraph; + const projectName = + cloudConfigScan.projectNameOverride || + cloudConfigScan.originalProjectName; + const result = await parseCloudConfigRes( + res, + cloudConfigScan.targetFile, + projectName, + options.severityThreshold, + ); + results.push(result); + } else { + const payloadBody: PayloadBody = payload.body as PayloadBody; + const payloadPolicy = payloadBody && payloadBody.policy; + const depGraph = payloadBody && payloadBody.depGraph; const pkgManager = depGraph && depGraph.pkgManager && (depGraph.pkgManager.name as SupportedProjectTypes); - const targetFile = depGraphPayload && depGraphPayload.targetFile; + const targetFile = payloadBody && payloadBody.targetFile; const projectName = _.get(payload, 'body.projectNameOverride') || _.get(payload, 'body.originalProjectName'); @@ -81,15 +101,14 @@ async function sendAndParseResults( const displayTargetFile = _.get(payload, 'body.displayTargetFile'); let dockerfilePackages; if ( - depGraphPayload && - depGraphPayload.docker && - depGraphPayload.docker.dockerfilePackages + payloadBody && + payloadBody.docker && + payloadBody.docker.dockerfilePackages ) { - dockerfilePackages = depGraphPayload.docker.dockerfilePackages; + dockerfilePackages = payloadBody.docker.dockerfilePackages; } - await spinner(spinnerLbl); analytics.add('depGraph', !!depGraph); - analytics.add('isDocker', !!(depGraphPayload && depGraphPayload.docker)); + analytics.add('isDocker', !!(payloadBody && payloadBody.docker)); // Type assertion might be a lie, but we are correcting that below const res = (await sendTestPayload(payload)) as LegacyVulnApiResult; @@ -112,7 +131,8 @@ async function sendAndParseResults( displayTargetFile, }); } - return results; + } + return results; } async function runTest( @@ -242,7 +262,7 @@ async function parseRes( function sendTestPayload( payload: Payload, -): Promise { +): Promise { const filesystemPolicy = payload.body && !!payload.body.policy; return new Promise((resolve, reject) => { request(payload, (error, res, body) => { @@ -309,12 +329,18 @@ async function assembleLocalPayloads( options: Options & TestOptions & PolicyOptions, ): Promise { // For --all-projects packageManager is yet undefined here. Use 'all' - const analysisType = - (options.docker ? 'docker' : options.packageManager) || 'all'; + let analysisTypeText = 'all dependencies for '; + if (options.docker) { + analysisTypeText = 'docker dependencies for '; + } else if (options.iac) { + analysisTypeText = 'Infrastruction as code configurations for '; + } else if (options.packageManager) { + analysisTypeText = options.packageManager + ' dependencies for '; + } + const spinnerLbl = 'Analyzing ' + - analysisType + - ' dependencies for ' + + analysisTypeText + (path.relative('.', path.join(root, options.file || '')) || path.relative('..', '.') + ' project dir'); @@ -322,6 +348,9 @@ async function assembleLocalPayloads( const payloads: Payload[] = []; await spinner(spinnerLbl); + if (options.iac) { + return assembleCloudConfigLocalPayloads(root, options); + } const deps = await getDepsFromPlugin(root, options); analytics.add('pluginName', deps.plugin.name); const javaVersion = _.get( diff --git a/src/lib/snyk-test/types.ts b/src/lib/snyk-test/types.ts index 0d2eb036b3e..6501b658f87 100644 --- a/src/lib/snyk-test/types.ts +++ b/src/lib/snyk-test/types.ts @@ -1,8 +1,9 @@ import * as depGraphLib from '@snyk/dep-graph'; import { GitTarget, ContainerTarget } from '../project-metadata/types'; import { DepTree } from '../types'; +import { CloudConfigScan } from './payload-schema'; -interface PayloadBody { +export interface PayloadBody { depGraph?: depGraphLib.DepGraph; // missing for legacy endpoint (options.vulnEndpoint) callGraph?: any; policy?: string; @@ -30,18 +31,7 @@ export interface Payload { 'x-is-ci': boolean; authorization: string; }; - body?: PayloadBody | CloudConfigPayloadBody; + body?: PayloadBody | CloudConfigScan; qs?: object | null; modules?: DepTreeFromResolveDeps; } - -export interface CloudConfigPayloadBody { - policy: string; - targetFile?: string; - targetFileRelativePath?: string; - originalProjectName?: string; // used only for display - foundProjectCount?: number; // used only for display - displayTargetFile?: string; - target?: GitTarget | ContainerTarget | null; - fileContent: string; -} From 1953c8dfe72cc1600c3b2be53b41b9f122fce791 Mon Sep 17 00:00:00 2001 From: orkamara Date: Wed, 24 Jun 2020 09:28:58 +0300 Subject: [PATCH 6/8] chore: rename Cloud Config to IaC --- .../test/formatters/format-test-meta.ts | 4 +-- .../{cloud-config-output.ts => iac-output.ts} | 16 +++++------ src/cli/commands/test/index.ts | 12 ++++----- src/lib/cloud-config/cloud-config-projects.ts | 5 ---- src/lib/detect.ts | 8 +++--- src/lib/errors/index.ts | 6 ++--- ...oud-config-file.ts => invalid-iac-file.ts} | 10 +++---- .../iac-parser.ts} | 17 +++++------- src/lib/iac/iac-projects.ts | 3 +++ ...nfig-test-result.ts => iac-test-result.ts} | 8 +++--- src/lib/snyk-test/index.js | 14 ++++------ src/lib/snyk-test/payload-schema.ts | 10 +++---- ...n-cloud-config-test.ts => run-iac-test.ts} | 12 ++++----- src/lib/snyk-test/run-test.ts | 27 +++++++++---------- src/lib/snyk-test/types.ts | 4 +-- src/lib/types.ts | 6 ++--- 16 files changed, 73 insertions(+), 89 deletions(-) rename src/cli/commands/test/{cloud-config-output.ts => iac-output.ts} (71%) delete mode 100644 src/lib/cloud-config/cloud-config-projects.ts rename src/lib/errors/{invalid-cloud-config-file.ts => invalid-iac-file.ts} (79%) rename src/lib/{cloud-config/cloud-config-parser.ts => iac/iac-parser.ts} (84%) create mode 100644 src/lib/iac/iac-projects.ts rename src/lib/snyk-test/{cloud-config-test-result.ts => iac-test-result.ts} (65%) rename src/lib/snyk-test/{run-cloud-config-test.ts => run-iac-test.ts} (88%) diff --git a/src/cli/commands/test/formatters/format-test-meta.ts b/src/cli/commands/test/formatters/format-test-meta.ts index 37ea1f037b4..03b8056f93f 100644 --- a/src/cli/commands/test/formatters/format-test-meta.ts +++ b/src/cli/commands/test/formatters/format-test-meta.ts @@ -2,10 +2,10 @@ import chalk from 'chalk'; import { rightPadWithSpaces } from '../../../../lib/right-pad'; import { TestOptions, Options } from '../../../../lib/types'; import { TestResult } from '../../../../lib/snyk-test/legacy'; -import { CloudConfigTestResult } from '../../../../lib/snyk-test/cloud-config-test-result'; +import { IacTestResult } from '../../../../lib/snyk-test/iac-test-result'; export function formatTestMeta( - res: TestResult | CloudConfigTestResult, + res: TestResult | IacTestResult, options: Options & TestOptions, ): string { const padToLength = 19; // chars to align diff --git a/src/cli/commands/test/cloud-config-output.ts b/src/cli/commands/test/iac-output.ts similarity index 71% rename from src/cli/commands/test/cloud-config-output.ts rename to src/cli/commands/test/iac-output.ts index 6b607806ba2..0357b5e9856 100644 --- a/src/cli/commands/test/cloud-config-output.ts +++ b/src/cli/commands/test/iac-output.ts @@ -1,15 +1,15 @@ import chalk from 'chalk'; import * as Debug from 'debug'; import { Options, TestOptions } from '../../../lib/types'; -import { CloudConfigTestResult } from '../../../lib/snyk-test/cloud-config-test-result'; +import { IacTestResult } from '../../../lib/snyk-test/iac-test-result'; import { getSeverityValue } from './formatters'; import { formatIssue } from './formatters/remediation-based-format-issues'; -import { AnnotatedCloudConfigIssue } from '../../../lib/snyk-test/cloud-config-test-result'; +import { AnnotatedIacIssue } from '../../../lib/snyk-test/iac-test-result'; -const debug = Debug('cloud-config-output'); +const debug = Debug('iac-output'); -export function getCloudConfigDisplayedOutput( - res: CloudConfigTestResult, +export function getIacDisplayedOutput( + res: IacTestResult, testOptions: Options & TestOptions, testedInfoText: string, meta: string, @@ -22,13 +22,13 @@ export function getCloudConfigDisplayedOutput( const NoNote = false; const NotNew = false; - const issues: AnnotatedCloudConfigIssue[] = res.result.cloudConfigResults; + const issues: AnnotatedIacIssue[] = res.result.cloudConfigResults; debug(`iac display output - ${issues.length} issues`); issues .sort((a, b) => getSeverityValue(b.severity) - getSeverityValue(a.severity)) .forEach((issue) => { - const path: string[][] = [issue.cloudConfigPath]; + const path: string[][] = [issue.iacPath]; issuesTextArray.push( formatIssue( issue.id, @@ -44,7 +44,7 @@ export function getCloudConfigDisplayedOutput( }); const issuesInfoOutput: string[] = []; - debug(`Cloud Config display output - ${issuesTextArray.length} issues text`); + debug(`Iac display output - ${issuesTextArray.length} issues text`); if (issuesTextArray.length > 0) { issuesInfoOutput.push(issuesTextArray.join('\n')); } diff --git a/src/cli/commands/test/index.ts b/src/cli/commands/test/index.ts index beed128a18f..97ad01daa1c 100644 --- a/src/cli/commands/test/index.ts +++ b/src/cli/commands/test/index.ts @@ -24,7 +24,7 @@ import { TestResult, VulnMetaData, } from '../../../lib/snyk-test/legacy'; -import { CloudConfigTestResult } from '../../../lib/snyk-test/cloud-config-test-result'; +import { IacTestResult } from '../../../lib/snyk-test/iac-test-result'; import { SupportedPackageManagers, WIZARD_SUPPORTED_PACKAGE_MANAGERS, @@ -44,7 +44,7 @@ import { summariseVulnerableResults, } from './formatters'; import * as utils from './utils'; -import { getCloudConfigDisplayedOutput } from './cloud-config-output'; +import { getIacDisplayedOutput } from './iac-output'; const debug = Debug('snyk-test'); const SEPARATOR = '\n-------------------------------------------------------\n'; @@ -173,7 +173,7 @@ async function test(...args: MethodArgs): Promise { // values depend on `options.json` value - string or object const errorMappedResults = !options.iac ? createErrorMappedResultsForJsonOutput(results) - : createErrorMappedResultsForJsonOutputForCloudConfig(results); + : createErrorMappedResultsForJsonOutputForIac(results); // backwards compat - strip array IFF only one result const dataToSend = errorMappedResults.length === 1 @@ -303,7 +303,7 @@ function createErrorMappedResultsForJsonOutput(results) { return errorMappedResults; } -function createErrorMappedResultsForJsonOutputForCloudConfig(results) { +function createErrorMappedResultsForJsonOutputForIac(results) { const errorMappedResults = results.map((result) => { // add json for when thrown exception if (result instanceof Error) { @@ -462,8 +462,8 @@ function displayResult( } if (res.packageManager === 'k8sconfig') { - return getCloudConfigDisplayedOutput( - (res as any) as CloudConfigTestResult, + return getIacDisplayedOutput( + (res as any) as IacTestResult, options, testedInfoText, meta, diff --git a/src/lib/cloud-config/cloud-config-projects.ts b/src/lib/cloud-config/cloud-config-projects.ts deleted file mode 100644 index 09235319bbb..00000000000 --- a/src/lib/cloud-config/cloud-config-projects.ts +++ /dev/null @@ -1,5 +0,0 @@ -export type SupportedCloudConfigs = 'k8sconfig' | 'helmconfig'; - -export const TEST_SUPPORTED_CLOUD_CONFIG_PROJECTS: SupportedCloudConfigs[] = [ - 'k8sconfig', -]; diff --git a/src/lib/detect.ts b/src/lib/detect.ts index 3ffea485ee1..5746b710fb7 100644 --- a/src/lib/detect.ts +++ b/src/lib/detect.ts @@ -4,7 +4,7 @@ import * as debugLib from 'debug'; import * as _ from '@snyk/lodash'; import { NoSupportedManifestsFoundError } from './errors'; import { SupportedPackageManagers } from './package-managers'; -import { validateK8sFile } from './cloud-config/cloud-config-parser'; +import { validateK8sFile } from './iac/iac-parser'; const debug = debugLib('snyk-detect'); @@ -138,14 +138,14 @@ export function detectPackageManager(root: string, options) { return packageManager; } -export function isCloudConfigProject(root: string, options): string { +export function isIacProject(root: string, options): string { if (!isLocalFolder(root)) { - debug('Cloud Config - repo case ' + root); + debug('Iac - repo case ' + root); throw "iac option doesn't support lookup as repo"; } if (!options.file) { - debug('Cloud Config - no file specified ' + root); + debug('Iac - no file specified ' + root); throw 'iac option works only with specified files'; } diff --git a/src/lib/errors/index.ts b/src/lib/errors/index.ts index 2bc2a7457dd..c5a8479b68f 100644 --- a/src/lib/errors/index.ts +++ b/src/lib/errors/index.ts @@ -20,6 +20,6 @@ export { ExcludeFlagBadInputError } from './exclude-flag-bad-input'; export { UnsupportedOptionCombinationError } from './unsupported-option-combination-error'; export { FeatureNotSupportedByPackageManagerError } from './feature-not-supported-by-package-manager-error'; export { - NotSupportedCloudConfigFileError, - IllegalCloudConfigFileError, -} from './invalid-cloud-config-file'; + NotSupportedIacFileError, + IllegalIacFileError, +} from './invalid-iac-file'; diff --git a/src/lib/errors/invalid-cloud-config-file.ts b/src/lib/errors/invalid-iac-file.ts similarity index 79% rename from src/lib/errors/invalid-cloud-config-file.ts rename to src/lib/errors/invalid-iac-file.ts index d8497bfba80..eda6d979795 100644 --- a/src/lib/errors/invalid-cloud-config-file.ts +++ b/src/lib/errors/invalid-iac-file.ts @@ -1,10 +1,10 @@ import chalk from 'chalk'; import { CustomError } from './custom-error'; -export function NotSupportedCloudConfigFileError(atLocations: string[]) { +export function NotSupportedIacFileError(atLocations: string[]) { const locationsStr = atLocations.join(', '); const errorMsg = - 'Not supported Cloud Config target files in ' + + 'Not supported infrastruction as code target files in ' + locationsStr + '.\nPlease see our documentation for supported languages and ' + 'target files: ' + @@ -19,12 +19,10 @@ export function NotSupportedCloudConfigFileError(atLocations: string[]) { return error; } -export function IllegalCloudConfigFileError( - atLocations: string[], -): CustomError { +export function IllegalIacFileError(atLocations: string[]): CustomError { const locationsStr = atLocations.join(', '); const errorMsg = - 'Illegal Cloud Config target file ' + + 'Illegal infrastruction as code target file ' + locationsStr + '.\nPlease see our documentation for supported languages and ' + 'target files: ' + diff --git a/src/lib/cloud-config/cloud-config-parser.ts b/src/lib/iac/iac-parser.ts similarity index 84% rename from src/lib/cloud-config/cloud-config-parser.ts rename to src/lib/iac/iac-parser.ts index 4a1cdac6266..943a59d91ff 100644 --- a/src/lib/cloud-config/cloud-config-parser.ts +++ b/src/lib/iac/iac-parser.ts @@ -1,10 +1,7 @@ //TODO(orka): take out into a new lib import * as YAML from 'js-yaml'; import * as debugLib from 'debug'; -import { - IllegalCloudConfigFileError, - NotSupportedCloudConfigFileError, -} from './../errors'; +import { IllegalIacFileError, NotSupportedIacFileError } from '../errors'; const debug = debugLib('snyk-detect'); @@ -29,7 +26,7 @@ function parseYamlOrJson(fileContent: string, filePath: string): any { try { return YAML.safeLoadAll(fileContent); } catch (e) { - debug('Failed to parse cloud config as a YAML'); + debug('Failed to parse iac config as a YAML'); } break; case 'json': @@ -38,11 +35,11 @@ function parseYamlOrJson(fileContent: string, filePath: string): any { objectsArr.push(JSON.parse(fileContent)); return objectsArr; } catch (e) { - debug('Failed to parse cloud config as a JSON'); + debug('Failed to parse iac config as a JSON'); } break; default: - debug(`Unsupported cloud config file type (${fileType})`); + debug(`Unsupported iac config file type (${fileType})`); } return undefined; } @@ -59,7 +56,7 @@ export function validateK8sFile( ) { const k8sObjects: any[] = parseYamlOrJson(fileContent, filePath); if (!k8sObjects) { - throw IllegalCloudConfigFileError([root]); + throw IllegalIacFileError([root]); } let numOfSupportedKeyDocs = 0; @@ -80,13 +77,13 @@ export function validateK8sFile( const key = mandatoryKeysForSupportedK8sKinds[kind][i]; if (!k8sObject[key]) { debug(`Missing key (${key}) from supported k8s object kind (${kind})`); - throw IllegalCloudConfigFileError([root]); + throw IllegalIacFileError([root]); } } } if (numOfSupportedKeyDocs === 0) { - throw NotSupportedCloudConfigFileError([root]); + throw NotSupportedIacFileError([root]); } debug(`k8s config found (${filePath})`); diff --git a/src/lib/iac/iac-projects.ts b/src/lib/iac/iac-projects.ts new file mode 100644 index 00000000000..7657c0857a5 --- /dev/null +++ b/src/lib/iac/iac-projects.ts @@ -0,0 +1,3 @@ +export type IacProjectTypes = 'k8sconfig' | 'helmconfig'; + +export const TEST_SUPPORTED_IAC_PROJECTS: IacProjectTypes[] = ['k8sconfig']; diff --git a/src/lib/snyk-test/cloud-config-test-result.ts b/src/lib/snyk-test/iac-test-result.ts similarity index 65% rename from src/lib/snyk-test/cloud-config-test-result.ts rename to src/lib/snyk-test/iac-test-result.ts index a92fd43f206..f49e45ad8f7 100644 --- a/src/lib/snyk-test/cloud-config-test-result.ts +++ b/src/lib/snyk-test/iac-test-result.ts @@ -1,23 +1,23 @@ import { BasicResultData, SEVERITY } from './legacy'; -export interface AnnotatedCloudConfigIssue { +export interface AnnotatedIacIssue { id: string; title: string; description: string; severity: SEVERITY; isIgnored: boolean; - cloudConfigPath: string[]; + iacPath: string[]; type: string; subType: string; } -export interface CloudConfigTestResult extends BasicResultData { +export interface IacTestResult extends BasicResultData { targetFile: string; projectName: string; displayTargetFile: string; // used for display only foundProjectCount: number; result: { - cloudConfigResults: AnnotatedCloudConfigIssue[]; + cloudConfigResults: AnnotatedIacIssue[]; projectType: string; }; } diff --git a/src/lib/snyk-test/index.js b/src/lib/snyk-test/index.js index 679b76b1fca..b0b52971616 100644 --- a/src/lib/snyk-test/index.js +++ b/src/lib/snyk-test/index.js @@ -4,10 +4,10 @@ const detect = require('../detect'); const runTest = require('./run-test'); const chalk = require('chalk'); const pm = require('../package-managers'); -const cloudConfigProjects = require('../cloud-config/cloud-config-projects'); +const iacProjects = require('../iac/iac-projects'); const { UnsupportedPackageManagerError, - NoSupportedCloudConfigFileError, + NoSupportedIacFileError, } = require('../errors'); async function test(root, options, callback) { @@ -33,7 +33,7 @@ function executeTest(root, options) { try { if (!options.allProjects) { if (options.iac) { - options.packageManager = detect.isCloudConfigProject(root, options); + options.packageManager = detect.isIacProject(root, options); } else { options.packageManager = detect.detectPackageManager(root, options); } @@ -59,12 +59,8 @@ function executeTest(root, options) { function run(root, options) { if (options.iac) { const projectType = options.packageManager; - if ( - !cloudConfigProjects.TEST_SUPPORTED_CLOUD_CONFIG_PROJECTS.includes( - projectType, - ) - ) { - throw new NoSupportedCloudConfigFileError(projectType); + if (!iacProjects.TEST_SUPPORTED_IAC_PROJECTS.includes(projectType)) { + throw new NoSupportedIacFileError(projectType); } return runTest(projectType, root, options); } diff --git a/src/lib/snyk-test/payload-schema.ts b/src/lib/snyk-test/payload-schema.ts index 960623ac147..9ffbb9729b0 100644 --- a/src/lib/snyk-test/payload-schema.ts +++ b/src/lib/snyk-test/payload-schema.ts @@ -1,5 +1,5 @@ //TODO(orka): future - change this file -import { SupportedCloudConfigs } from '../cloud-config/cloud-config-projects'; +import { IacProjectTypes } from '../iac/iac-projects'; interface Scan { type: string; @@ -7,15 +7,15 @@ interface Scan { data: any; } -export interface CloudConfigFile { +export interface IacFile { fileContent: string; fileType: 'yaml' | 'yml' | 'json'; } -export interface CloudConfigScan extends Scan { - type: SupportedCloudConfigs; +export interface IacScan extends Scan { + type: IacProjectTypes; targetFile: string; - data: CloudConfigFile; + data: IacFile; targetFileRelativePath: string; originalProjectName: string; policy: string; diff --git a/src/lib/snyk-test/run-cloud-config-test.ts b/src/lib/snyk-test/run-iac-test.ts similarity index 88% rename from src/lib/snyk-test/run-cloud-config-test.ts rename to src/lib/snyk-test/run-iac-test.ts index 61d5a45358f..5019dc1b773 100644 --- a/src/lib/snyk-test/run-cloud-config-test.ts +++ b/src/lib/snyk-test/run-iac-test.ts @@ -2,19 +2,19 @@ import * as fs from 'fs'; import * as path from 'path'; import * as pathUtil from 'path'; import { TestResult } from './legacy'; -import { CloudConfigTestResult } from './cloud-config-test-result'; +import { IacTestResult } from './iac-test-result'; import * as snyk from '..'; import { isCI } from '../is-ci'; import * as common from './common'; import * as config from '../config'; import { Options, TestOptions } from '../types'; import { Payload } from './types'; -import { CloudConfigScan } from './payload-schema'; +import { IacScan } from './payload-schema'; import { SEVERITY } from './legacy'; import * as pathLib from 'path'; -export async function parseCloudConfigRes( - res: CloudConfigTestResult, +export async function parseIacTestResult( + res: IacTestResult, targetFile: string | undefined, projectName: any, severityThreshold?: SEVERITY, @@ -40,7 +40,7 @@ export async function parseCloudConfigRes( }; } -export async function assembleCloudConfigLocalPayloads( +export async function assembleIacLocalPayloads( root: string, options: Options & TestOptions, ): Promise { @@ -55,7 +55,7 @@ export async function assembleCloudConfigLocalPayloads( : ''; const fileContent = fs.readFileSync(targetFile, 'utf8'); - const body: CloudConfigScan = { + const body: IacScan = { data: { fileContent, fileType: 'yaml', diff --git a/src/lib/snyk-test/run-test.ts b/src/lib/snyk-test/run-test.ts index 00a56beb83b..8cb7523451d 100644 --- a/src/lib/snyk-test/run-test.ts +++ b/src/lib/snyk-test/run-test.ts @@ -5,7 +5,7 @@ import * as debugModule from 'debug'; import * as pathUtil from 'path'; import { parsePackageString as moduleToObject } from 'snyk-module'; import * as depGraphLib from '@snyk/dep-graph'; -import { CloudConfigScan } from './payload-schema'; +import { IacScan } from './payload-schema'; import { TestResult, @@ -15,7 +15,7 @@ import { convertTestDepGraphResultToLegacy, LegacyVulnApiResult, } from './legacy'; -import { CloudConfigTestResult } from './cloud-config-test-result'; +import { IacTestResult } from './iac-test-result'; import { AuthFailedError, InternalServerError, @@ -51,10 +51,7 @@ import { getSubProjectCount } from '../plugins/get-sub-project-count'; import { serializeCallGraphWithMetrics } from '../reachable-vulns'; import { validateOptions } from '../options-validator'; import { findAndLoadPolicy } from '../policy'; -import { - assembleCloudConfigLocalPayloads, - parseCloudConfigRes, -} from './run-cloud-config-test'; +import { assembleIacLocalPayloads, parseIacTestResult } from './run-iac-test'; import { Payload, PayloadBody, DepTreeFromResolveDeps } from './types'; const debug = debugModule('snyk'); @@ -71,16 +68,16 @@ async function sendAndParseResults( for (const payload of payloads) { await spinner(spinnerLbl); if (options.iac) { - const cloudConfigScan: CloudConfigScan = payload.body as CloudConfigScan; - analytics.add('iac type', !!cloudConfigScan.type); - const res = (await sendTestPayload(payload)) as CloudConfigTestResult; + const iacScan: IacScan = payload.body as IacScan; + analytics.add('iac type', !!iacScan.type); + const res = (await sendTestPayload(payload)) as IacTestResult; + const targetFile = iacScan.targetFile; const projectName = - cloudConfigScan.projectNameOverride || - cloudConfigScan.originalProjectName; - const result = await parseCloudConfigRes( + iacScan.projectNameOverride || iacScan.originalProjectName; + const result = await parseIacTestResult( res, - cloudConfigScan.targetFile, + iacScan.targetFile, projectName, options.severityThreshold, ); @@ -262,7 +259,7 @@ async function parseRes( function sendTestPayload( payload: Payload, -): Promise { +): Promise { const filesystemPolicy = payload.body && !!payload.body.policy; return new Promise((resolve, reject) => { request(payload, (error, res, body) => { @@ -349,7 +346,7 @@ async function assembleLocalPayloads( await spinner(spinnerLbl); if (options.iac) { - return assembleCloudConfigLocalPayloads(root, options); + return assembleIacLocalPayloads(root, options); } const deps = await getDepsFromPlugin(root, options); analytics.add('pluginName', deps.plugin.name); diff --git a/src/lib/snyk-test/types.ts b/src/lib/snyk-test/types.ts index 6501b658f87..23b39c131d6 100644 --- a/src/lib/snyk-test/types.ts +++ b/src/lib/snyk-test/types.ts @@ -1,7 +1,7 @@ import * as depGraphLib from '@snyk/dep-graph'; import { GitTarget, ContainerTarget } from '../project-metadata/types'; import { DepTree } from '../types'; -import { CloudConfigScan } from './payload-schema'; +import { IacScan } from './payload-schema'; export interface PayloadBody { depGraph?: depGraphLib.DepGraph; // missing for legacy endpoint (options.vulnEndpoint) @@ -31,7 +31,7 @@ export interface Payload { 'x-is-ci': boolean; authorization: string; }; - body?: PayloadBody | CloudConfigScan; + body?: PayloadBody | IacScan; qs?: object | null; modules?: DepTreeFromResolveDeps; } diff --git a/src/lib/types.ts b/src/lib/types.ts index 73f64a0ca33..902f2b700b4 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -1,5 +1,5 @@ import { SupportedPackageManagers } from './package-managers'; -import { SupportedCloudConfigs } from './cloud-config/cloud-config-projects'; +import { IacProjectTypes } from './iac/iac-projects'; import { legacyCommon as legacyApi } from '@snyk/cli-interface'; import { SEVERITY } from './snyk-test/legacy'; import { FailOn } from './snyk-test/common'; @@ -125,6 +125,4 @@ export interface SpinnerOptions { cleanup?: any; } -export type SupportedProjectTypes = - | SupportedCloudConfigs - | SupportedPackageManagers; +export type SupportedProjectTypes = IacProjectTypes | SupportedPackageManagers; From 4a93ca328f5409e044ebe3b8d8422fd9bec5916c Mon Sep 17 00:00:00 2001 From: orkamara Date: Wed, 24 Jun 2020 09:45:50 +0300 Subject: [PATCH 7/8] chore: add code owner (Cloud Config) --- .github/CODEOWNERS | 7 +++++++ src/cli/commands/test/iac-output.ts | 2 +- src/lib/snyk-test/iac-test-result.ts | 2 +- src/lib/snyk-test/run-test.ts | 1 - 4 files changed, 9 insertions(+), 3 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index f266a13442e..f4565655750 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -2,3 +2,10 @@ README.md @snyk/content @snyk/boost help/ @snyk/content @snyk/boost * @snyk/hammer @snyk/boost +help/iac.txt @snyk/cloudconfig +src/cli/commands/test/iac-output.ts @snyk/cloudconfig +src/lib/cloud-config-projects.ts @snyk/cloudconfig +src/lib/iac/ @snyk/cloudconfig +src/lib/snyk-test/iac-test-result.ts @snyk/cloudconfig +src/lib/snyk-test/payload-schema.ts @snyk/cloudconfig +src/lib/snyk-test/run-iac-test.ts @snyk/cloudconfig \ No newline at end of file diff --git a/src/cli/commands/test/iac-output.ts b/src/cli/commands/test/iac-output.ts index 0357b5e9856..4f895f3726f 100644 --- a/src/cli/commands/test/iac-output.ts +++ b/src/cli/commands/test/iac-output.ts @@ -28,7 +28,7 @@ export function getIacDisplayedOutput( issues .sort((a, b) => getSeverityValue(b.severity) - getSeverityValue(a.severity)) .forEach((issue) => { - const path: string[][] = [issue.iacPath]; + const path: string[][] = [issue.cloudConfigPath]; issuesTextArray.push( formatIssue( issue.id, diff --git a/src/lib/snyk-test/iac-test-result.ts b/src/lib/snyk-test/iac-test-result.ts index f49e45ad8f7..a4ade59c325 100644 --- a/src/lib/snyk-test/iac-test-result.ts +++ b/src/lib/snyk-test/iac-test-result.ts @@ -6,7 +6,7 @@ export interface AnnotatedIacIssue { description: string; severity: SEVERITY; isIgnored: boolean; - iacPath: string[]; + cloudConfigPath: string[]; type: string; subType: string; } diff --git a/src/lib/snyk-test/run-test.ts b/src/lib/snyk-test/run-test.ts index 8cb7523451d..cea2a91f1dc 100644 --- a/src/lib/snyk-test/run-test.ts +++ b/src/lib/snyk-test/run-test.ts @@ -72,7 +72,6 @@ async function sendAndParseResults( analytics.add('iac type', !!iacScan.type); const res = (await sendTestPayload(payload)) as IacTestResult; - const targetFile = iacScan.targetFile; const projectName = iacScan.projectNameOverride || iacScan.originalProjectName; const result = await parseIacTestResult( From 09f330c28c230dde8dad5d38cb21a929e31ea227 Mon Sep 17 00:00:00 2001 From: orkamara Date: Thu, 25 Jun 2020 09:00:04 +0300 Subject: [PATCH 8/8] chore: add tests for Iac support --- .github/CODEOWNERS | 3 +- .../cli-test/cli-test.acceptance.test.ts | 2 + .../cli-test/cli-test.iac-k8s.spec.ts | 475 ++++++++++++++++++ test/acceptance/fake-server.ts | 36 ++ .../workspaces/iac-kubernetes/multi-file.yaml | 103 ++++ .../iac-kubernetes/test-iac-high-result.json | 21 + .../test-iac-medium-result.json | 31 ++ .../iac-kubernetes/test-iac-result.json | 41 ++ 8 files changed, 711 insertions(+), 1 deletion(-) create mode 100644 test/acceptance/cli-test/cli-test.iac-k8s.spec.ts create mode 100644 test/acceptance/workspaces/iac-kubernetes/multi-file.yaml create mode 100644 test/acceptance/workspaces/iac-kubernetes/test-iac-high-result.json create mode 100644 test/acceptance/workspaces/iac-kubernetes/test-iac-medium-result.json create mode 100644 test/acceptance/workspaces/iac-kubernetes/test-iac-result.json diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index f4565655750..06fdb7be577 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -8,4 +8,5 @@ src/lib/cloud-config-projects.ts @snyk/cloudconfig src/lib/iac/ @snyk/cloudconfig src/lib/snyk-test/iac-test-result.ts @snyk/cloudconfig src/lib/snyk-test/payload-schema.ts @snyk/cloudconfig -src/lib/snyk-test/run-iac-test.ts @snyk/cloudconfig \ No newline at end of file +src/lib/snyk-test/run-iac-test.ts @snyk/cloudconfig +test/acceptance/cli-test/cli-test.iac-k8s.spec.ts @snyk/cloudconfig \ No newline at end of file diff --git a/test/acceptance/cli-test/cli-test.acceptance.test.ts b/test/acceptance/cli-test/cli-test.acceptance.test.ts index bfd27113df8..e854772dca6 100644 --- a/test/acceptance/cli-test/cli-test.acceptance.test.ts +++ b/test/acceptance/cli-test/cli-test.acceptance.test.ts @@ -25,6 +25,7 @@ import { PythonTests } from './cli-test.python.spec'; import { RubyTests } from './cli-test.ruby.spec'; import { SbtTests } from './cli-test.sbt.spec'; import { YarnTests } from './cli-test.yarn.spec'; +import { IacK8sTests } from './cli-test.iac-k8s.spec'; import { AllProjectsTests } from './cli-test.all-projects.spec'; const languageTests: AcceptanceTests[] = [ @@ -40,6 +41,7 @@ const languageTests: AcceptanceTests[] = [ RubyTests, SbtTests, YarnTests, + IacK8sTests, ]; const { test, only } = tap; diff --git a/test/acceptance/cli-test/cli-test.iac-k8s.spec.ts b/test/acceptance/cli-test/cli-test.iac-k8s.spec.ts new file mode 100644 index 00000000000..d0e906b8d4e --- /dev/null +++ b/test/acceptance/cli-test/cli-test.iac-k8s.spec.ts @@ -0,0 +1,475 @@ +import * as sinon from 'sinon'; +import * as _ from '@snyk/lodash'; +import { getWorkspaceJSON } from '../workspace-helper'; +import { CommandResult } from '../../../src/cli/commands/types'; +// import * as fs from 'fs'; +// import * as path from 'path'; + +import { AcceptanceTests } from './cli-test.acceptance.test'; + +export const IacK8sTests: AcceptanceTests = { + language: 'Iac (Kubernetes)', + tests: { + '`iac test --file=multi.yaml - no issues`': (params, utils) => async ( + t, + ) => { + utils.chdirWorkspaces(); + + await params.cli.test('iac-kubernetes', { + file: 'multi-file.yaml', + iac: true, + }); + + const req = params.server.popRequest(); + t.equal(req.method, 'POST', 'makes POST request'); + t.equal( + req.headers['x-snyk-cli-version'], + params.versionNumber, + 'sends version number', + ); + t.match(req.url, '/test-iac', 'posts to correct url'); + t.equal(req.body.type, 'k8sconfig'); + }, + + '`iac test - no --file`': (params, utils) => async (t) => { + utils.chdirWorkspaces(); + + try { + await params.cli.test('iac-kubernetes', { + iac: true, + }); + t.fail('should have failed'); + } catch (err) { + t.pass('throws err'); + t.match( + err.message, + 'iac option works only with specified files', + 'shows err', + ); + } + }, + + '`iac test - not a real dir`': (params, utils) => async (t) => { + utils.chdirWorkspaces(); + + try { + await params.cli.test('nonono', { + iac: true, + }); + t.fail('should have failed'); + } catch (err) { + t.pass('throws err'); + t.match( + err.message, + "iac option doesn't support lookup as repo", + 'shows err', + ); + } + }, + + '`iac test --file=multi.yaml meta - no issues': (params, utils) => async ( + t, + ) => { + utils.chdirWorkspaces(); + const commandResult: CommandResult = await params.cli.test( + 'iac-kubernetes', + { + file: 'multi-file.yaml', + iac: true, + }, + ); + const res = commandResult.getDisplayResults(); + + const meta = res.slice(res.indexOf('Organization:')).split('\n'); + t.match(meta[0], /Organization:\s+test-org/, 'organization displayed'); + t.match( + meta[1], + /Package manager:\s+k8sconfig/, + 'package manager displayed', + ); + t.match( + meta[2], + /Target file:\s+multi-file.yaml/, + 'target file displayed', + ); + t.match( + meta[3], + /Project name:\s+iac-kubernetes/, + 'project name displayed', + ); + t.match(meta[4], /Open source:\s+no/, 'open source displayed'); + t.match(meta[5], /Project path:\s+iac-kubernetes/, 'path displayed'); + t.notMatch( + meta[5], + /Local Snyk policy:\s+found/, + 'local policy not displayed', + ); + }, + + '`iac test --file=multi.yaml`': (params, utils) => async (t) => { + utils.chdirWorkspaces(); + + params.server.setNextResponse( + getWorkspaceJSON('iac-kubernetes', 'test-iac-result.json'), + ); + + try { + await params.cli.test('iac-kubernetes', { + file: 'multi-file.yaml', + iac: true, + }); + t.fail('should have thrown'); + } catch (err) { + const res = err.message; + + t.match( + res, + 'Tested 0 dependencies for known issues, found 3 issues', + '3 issue', + ); + + const meta = res.slice(res.indexOf('Organization:')).split('\n'); + t.match(meta[0], /Organization:\s+test-org/, 'organization displayed'); + t.match( + meta[1], + /Package manager:\s+k8sconfig/, + 'package manager displayed', + ); + t.match( + meta[2], + /Target file:\s+multi-file.yaml/, + 'target file displayed', + ); + t.match( + meta[3], + /Project name:\s+iac-kubernetes/, + 'project name displayed', + ); + t.match(meta[4], /Open source:\s+no/, 'open source displayed'); + t.match(meta[5], /Project path:\s+iac-kubernetes/, 'path displayed'); + t.notMatch( + meta[5], + /Local Snyk policy:\s+found/, + 'local policy not displayed', + ); + } + }, + + '`iac test --file=multi.yaml --severity-threshold=low`': ( + params, + utils, + ) => async (t) => { + utils.chdirWorkspaces(); + + params.server.setNextResponse( + getWorkspaceJSON('iac-kubernetes', 'test-iac-result.json'), + ); + + try { + await params.cli.test('iac-kubernetes', { + file: 'multi-file.yaml', + iac: true, + severityThreshold: 'low', + }); + t.fail('should have thrown'); + } catch (err) { + const res = err.message; + + t.match( + res, + 'Tested 0 dependencies for known issues, found 3 issues', + '3 issue', + ); + + const meta = res.slice(res.indexOf('Organization:')).split('\n'); + t.match(meta[0], /Organization:\s+test-org/, 'organization displayed'); + t.match( + meta[1], + /Package manager:\s+k8sconfig/, + 'package manager displayed', + ); + t.match( + meta[2], + /Target file:\s+multi-file.yaml/, + 'target file displayed', + ); + t.match( + meta[3], + /Project name:\s+iac-kubernetes/, + 'project name displayed', + ); + t.match(meta[4], /Open source:\s+no/, 'open source displayed'); + t.match(meta[5], /Project path:\s+iac-kubernetes/, 'path displayed'); + t.notMatch( + meta[5], + /Local Snyk policy:\s+found/, + 'local policy not displayed', + ); + } + }, + + '`iac test --file=multi.yaml --severity-threshold=low --json`': ( + params, + utils, + ) => async (t) => { + utils.chdirWorkspaces(); + + params.server.setNextResponse( + getWorkspaceJSON('iac-kubernetes', 'test-iac-result.json'), + ); + + try { + await params.cli.test('iac-kubernetes', { + file: 'multi-file.yaml', + iac: true, + severityThreshold: 'low', + json: true, + }); + t.fail('should have thrown'); + } catch (err) { + const req = params.server.popRequest(); + t.is(req.query.severityThreshold, 'low'); + + const res = JSON.parse(err.message); + + const expected = getWorkspaceJSON( + 'iac-kubernetes', + 'test-iac-result.json', + ); + + t.deepEqual(res.org, 'test-org', 'org is ok'); + t.deepEqual(res.projectType, 'k8sconfig', 'projectType is ok'); + t.deepEqual(res.path, 'iac-kubernetes', 'path is ok'); + t.deepEqual(res.projectName, 'iac-kubernetes', 'projectName is ok'); + t.deepEqual(res.targetFile, 'multi-file.yaml', 'targetFile is ok'); + t.deepEqual(res.dependencyCount, 0, 'dependencyCount is 0'); + t.deepEqual(res.vulnerabilities, [], 'vulnerabilities is empty'); + + t.deepEqual( + _.sortBy(res.cloudConfigResults, 'id'), + _.sortBy(expected.result.cloudConfigResults, 'id'), + 'issues are the same', + ); + } + }, + + '`iac test --file=multi.yaml --severity-threshold=medium`': ( + params, + utils, + ) => async (t) => { + utils.chdirWorkspaces(); + + params.server.setNextResponse( + getWorkspaceJSON('iac-kubernetes', 'test-iac-medium-result.json'), + ); + + try { + await params.cli.test('iac-kubernetes', { + file: 'multi-file.yaml', + iac: true, + severityThreshold: 'medium', + }); + t.fail('should have thrown'); + } catch (err) { + const res = err.message; + + t.match( + res, + 'Tested 0 dependencies for known issues, found 2 issues', + '2 issue', + ); + + const meta = res.slice(res.indexOf('Organization:')).split('\n'); + t.match(meta[0], /Organization:\s+test-org/, 'organization displayed'); + t.match( + meta[1], + /Package manager:\s+k8sconfig/, + 'package manager displayed', + ); + t.match( + meta[2], + /Target file:\s+multi-file.yaml/, + 'target file displayed', + ); + t.match( + meta[3], + /Project name:\s+iac-kubernetes/, + 'project name displayed', + ); + t.match(meta[4], /Open source:\s+no/, 'open source displayed'); + t.match(meta[5], /Project path:\s+iac-kubernetes/, 'path displayed'); + t.notMatch( + meta[5], + /Local Snyk policy:\s+found/, + 'local policy not displayed', + ); + } + }, + + '`iac test --file=multi.yaml --severity-threshold=medium --json`': ( + params, + utils, + ) => async (t) => { + utils.chdirWorkspaces(); + + params.server.setNextResponse( + getWorkspaceJSON('iac-kubernetes', 'test-iac-medium-result.json'), + ); + + try { + await params.cli.test('iac-kubernetes', { + file: 'multi-file.yaml', + iac: true, + severityThreshold: 'medium', + json: true, + }); + t.fail('should have thrown'); + } catch (err) { + const req = params.server.popRequest(); + t.is(req.query.severityThreshold, 'medium'); + + const res = JSON.parse(err.message); + + const expected = getWorkspaceJSON( + 'iac-kubernetes', + 'test-iac-medium-result.json', + ); + + t.deepEqual(res.org, 'test-org', 'org is ok'); + t.deepEqual(res.projectType, 'k8sconfig', 'projectType is ok'); + t.deepEqual(res.path, 'iac-kubernetes', 'path is ok'); + t.deepEqual(res.projectName, 'iac-kubernetes', 'projectName is ok'); + t.deepEqual(res.targetFile, 'multi-file.yaml', 'targetFile is ok'); + t.deepEqual(res.dependencyCount, 0, 'dependencyCount is 0'); + t.deepEqual(res.vulnerabilities, [], 'vulnerabilities is empty'); + + t.deepEqual( + _.sortBy(res.cloudConfigResults, 'id'), + _.sortBy(expected.result.cloudConfigResults, 'id'), + 'issues are the same', + ); + } + }, + + '`iac test --file=multi.yaml --severity-threshold=high`': ( + params, + utils, + ) => async (t) => { + utils.chdirWorkspaces(); + + params.server.setNextResponse( + getWorkspaceJSON('iac-kubernetes', 'test-iac-high-result.json'), + ); + + try { + await params.cli.test('iac-kubernetes', { + file: 'multi-file.yaml', + iac: true, + severityThreshold: 'high', + }); + t.fail('should have thrown'); + } catch (err) { + const res = err.message; + + t.match( + res, + 'Tested 0 dependencies for known issues, found 1 issues', + '1 issue', + ); + + const meta = res.slice(res.indexOf('Organization:')).split('\n'); + t.match(meta[0], /Organization:\s+test-org/, 'organization displayed'); + t.match( + meta[1], + /Package manager:\s+k8sconfig/, + 'package manager displayed', + ); + t.match( + meta[2], + /Target file:\s+multi-file.yaml/, + 'target file displayed', + ); + t.match( + meta[3], + /Project name:\s+iac-kubernetes/, + 'project name displayed', + ); + t.match(meta[4], /Open source:\s+no/, 'open source displayed'); + t.match(meta[5], /Project path:\s+iac-kubernetes/, 'path displayed'); + t.notMatch( + meta[5], + /Local Snyk policy:\s+found/, + 'local policy not displayed', + ); + } + }, + + '`iac test --file=multi.yaml --severity-threshold=high --json`': ( + params, + utils, + ) => async (t) => { + utils.chdirWorkspaces(); + + params.server.setNextResponse( + getWorkspaceJSON('iac-kubernetes', 'test-iac-high-result.json'), + ); + + try { + await params.cli.test('iac-kubernetes', { + file: 'multi-file.yaml', + iac: true, + severityThreshold: 'high', + json: true, + }); + t.fail('should have thrown'); + } catch (err) { + const req = params.server.popRequest(); + t.is(req.query.severityThreshold, 'high'); + + const res = JSON.parse(err.message); + + const expected = getWorkspaceJSON( + 'iac-kubernetes', + 'test-iac-high-result.json', + ); + + t.deepEqual(res.org, 'test-org', 'org is ok'); + t.deepEqual(res.projectType, 'k8sconfig', 'projectType is ok'); + t.deepEqual(res.path, 'iac-kubernetes', 'path is ok'); + t.deepEqual(res.projectName, 'iac-kubernetes', 'projectName is ok'); + t.deepEqual(res.targetFile, 'multi-file.yaml', 'targetFile is ok'); + t.deepEqual(res.dependencyCount, 0, 'dependencyCount is 0'); + t.deepEqual(res.vulnerabilities, [], 'vulnerabilities is empty'); + + t.deepEqual( + _.sortBy(res.cloudConfigResults, 'id'), + _.sortBy(expected.result.cloudConfigResults, 'id'), + 'issues are the same', + ); + } + }, + + '`iac test --file=multi.yaml --json - no issues`': ( + params, + utils, + ) => async (t) => { + utils.chdirWorkspaces(); + const commandResult: CommandResult = await params.cli.test( + 'iac-kubernetes', + { + file: 'multi-file.yaml', + iac: true, + }, + ); + const res: any = JSON.parse((commandResult as any).jsonResult); + + t.deepEqual(res.org, 'test-org', 'org is ok'); + t.deepEqual(res.projectType, 'k8sconfig', 'projectType is ok'); + t.deepEqual(res.path, 'iac-kubernetes', 'path is ok'); + t.deepEqual(res.projectName, 'iac-kubernetes', 'projectName is ok'); + t.deepEqual(res.targetFile, 'multi-file.yaml', 'targetFile is ok'); + t.deepEqual(res.dependencyCount, 0, 'dependencyCount is 0'); + t.deepEqual(res.vulnerabilities, [], 'vulnerabilities is empty'); + }, + }, +}; diff --git a/test/acceptance/fake-server.ts b/test/acceptance/fake-server.ts index c3619f67ebb..33b87359e80 100644 --- a/test/acceptance/fake-server.ts +++ b/test/acceptance/fake-server.ts @@ -1,4 +1,6 @@ import * as restify from 'restify'; +import { AnnotatedIacIssue } from '../../src/lib/snyk-test/iac-test-result'; +import { SEVERITY } from '../../src/lib/snyk-test/legacy'; interface FakeServer extends restify.Server { _reqLog: restify.Request[]; @@ -124,6 +126,40 @@ export function fakeServer(root, apikey) { return next(); }); + server.post(root + '/test-iac', (req, res, next) => { + if (req.query.org && req.query.org === 'missing-org') { + res.status(404); + res.send({ + code: 404, + userMessage: 'cli error message', + }); + return next(); + } + + // const cloudConfigResults = [{ + // id: 'SNYK-CC-K8S-1', + // title: 'Reducing the admission of containers with dropped capabilities', + // description: 'The requiredDropCapabilities property (as part of the Pod Security Policy) provides a whitelist of capabilities that must be dropped from containers (https://docs.docker.com/engine/reference/run/#runtime-privilege-and-linux-capabilities). These capabilities are removed from the default set, and must not be added. It’s recommended to drop all the capabilities (using ALL), or at least to drop NET_RAW (which allows a process to spy on packets on its network / to inject data onto the network).', + // cloudConfigPath: ['[DocId: 2]','input','spec','requiredDropCapabilities'], + // severity: 'high', + // isIgnored: false, + // type: 'k8s', + // subType: 'Deployment', + // }]; + + res.send({ + result: { + projectType: 'k8sconfig', + cloudConfigResults: [], + }, + meta: { + org: 'test-org', + isPublic: false, + }, + }); + return next(); + }); + server.get( root + '/cli-config/feature-flags/:featureFlag', (req, res, next) => { diff --git a/test/acceptance/workspaces/iac-kubernetes/multi-file.yaml b/test/acceptance/workspaces/iac-kubernetes/multi-file.yaml new file mode 100644 index 00000000000..97ae9dfa648 --- /dev/null +++ b/test/acceptance/workspaces/iac-kubernetes/multi-file.yaml @@ -0,0 +1,103 @@ +--- +apiVersion: v1 +kind: Service +metadata: + name: snyky-service + labels: + app.kubernetes.io/name: snyky-service + helm.sh/chart: snyky-service-0.1.0 + app.kubernetes.io/instance: snyky-service + app.kubernetes.io/version: "1.0" + app.kubernetes.io/managed-by: Tiller +spec: + type: ClusterIP + ports: + - port: 8080 + targetPort: http + protocol: TCP + name: http + selector: + app.kubernetes.io/name: snyky-service + app.kubernetes.io/instance: snyky-service + +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: snyky-deployment + labels: + app.kubernetes.io/name: snyky + helm.sh/chart: snyky-0.1.0 + app.kubernetes.io/instance: snyky + app.kubernetes.io/version: "1.0" + app.kubernetes.io/managed-by: Tiller +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: snyky + app.kubernetes.io/instance: snyky + template: + metadata: + labels: + app.kubernetes.io/name: snyky + app.kubernetes.io/instance: snyky + spec: +# hostPID: true + containers: + - name: snyky1 + image: "orka/snyky1:latest" + imagePullPolicy: IfNotPresent + ports: + - name: http + containerPort: 5000 + protocol: TCP + livenessProbe: + httpGet: + path: / + port: http + readinessProbe: + httpGet: + path: / + port: http + resources: + # limits: + # cpu: 100 + # memory: 100 + securityContext: + privileged: true + capabilities: + drop: + - all +# add: +# - CAP_SYS_ADMIN + - name: snyky2 + image: "orka/snyky2:latest" + imagePullPolicy: IfNotPresent + ports: + - name: http + containerPort: 5000 + protocol: TCP + livenessProbe: + httpGet: + path: / + port: http + readinessProbe: + httpGet: + path: / + port: http + resources: + # limits: + # cpu: 100 + # memory: 100 + securityContext: + privileged: true + capabilities: + drop: + - ALL +# add: +# - CAP_SYS_ADMIN + volumes: + - name: dockersock + hostPath: + path: /var/run/docker.sock diff --git a/test/acceptance/workspaces/iac-kubernetes/test-iac-high-result.json b/test/acceptance/workspaces/iac-kubernetes/test-iac-high-result.json new file mode 100644 index 00000000000..19899b94380 --- /dev/null +++ b/test/acceptance/workspaces/iac-kubernetes/test-iac-high-result.json @@ -0,0 +1,21 @@ +{ + "result": { + "projectType": "k8sconfig", + "cloudConfigResults": [ + { + "id": "SNYK-CC-K8S-1", + "title": "Reducing the admission of containers with dropped capabilities", + "description": "The requiredDropCapabilities property (as part of the Pod Security Policy) provides a whitelist of capabilities that must be dropped from containers (https://docs.docker.com/engine/reference/run/#runtime-privilege-and-linux-capabilities). These capabilities are removed from the default set, and must not be added. It’s recommended to drop all the capabilities (using ALL), or at least to drop NET_RAW (which allows a process to spy on packets on its network / to inject data onto the network).", + "cloudConfigPath": ["[DocId: 2]","input","spec","requiredDropCapabilities"], + "severity": "high", + "isIgnored": false, + "type": "k8s", + "subType": "Deployment" + } + ] + }, + "meta": { + "org": "test-org", + "isPublic": false + } +} diff --git a/test/acceptance/workspaces/iac-kubernetes/test-iac-medium-result.json b/test/acceptance/workspaces/iac-kubernetes/test-iac-medium-result.json new file mode 100644 index 00000000000..bde8481bc5a --- /dev/null +++ b/test/acceptance/workspaces/iac-kubernetes/test-iac-medium-result.json @@ -0,0 +1,31 @@ +{ + "result": { + "projectType": "k8sconfig", + "cloudConfigResults": [ + { + "id": "SNYK-CC-K8S-1", + "title": "Reducing the admission of containers with dropped capabilities", + "description": "The requiredDropCapabilities property (as part of the Pod Security Policy) provides a whitelist of capabilities that must be dropped from containers (https://docs.docker.com/engine/reference/run/#runtime-privilege-and-linux-capabilities). These capabilities are removed from the default set, and must not be added. It’s recommended to drop all the capabilities (using ALL), or at least to drop NET_RAW (which allows a process to spy on packets on its network / to inject data onto the network).", + "cloudConfigPath": ["[DocId: 2]","input","spec","requiredDropCapabilities"], + "severity": "high", + "isIgnored": false, + "type": "k8s", + "subType": "Deployment" + }, + { + "id": "SNYK-CC-K8S-2", + "title": "Reducing the admission of containers with dropped capabilities", + "description": "The requiredDropCapabilities property (as part of the Pod Security Policy) provides a whitelist of capabilities that must be dropped from containers (https://docs.docker.com/engine/reference/run/#runtime-privilege-and-linux-capabilities). These capabilities are removed from the default set, and must not be added. It’s recommended to drop all the capabilities (using ALL), or at least to drop NET_RAW (which allows a process to spy on packets on its network / to inject data onto the network).", + "cloudConfigPath": ["[DocId: 2]","input","spec","requiredDropCapabilities"], + "severity": "medium", + "isIgnored": false, + "type": "k8s", + "subType": "NetworkPolicy" + } + ] + }, + "meta": { + "org": "test-org", + "isPublic": false + } +} diff --git a/test/acceptance/workspaces/iac-kubernetes/test-iac-result.json b/test/acceptance/workspaces/iac-kubernetes/test-iac-result.json new file mode 100644 index 00000000000..17f758a9548 --- /dev/null +++ b/test/acceptance/workspaces/iac-kubernetes/test-iac-result.json @@ -0,0 +1,41 @@ +{ + "result": { + "projectType": "k8sconfig", + "cloudConfigResults": [ + { + "id": "SNYK-CC-K8S-1", + "title": "Reducing the admission of containers with dropped capabilities", + "description": "The requiredDropCapabilities property (as part of the Pod Security Policy) provides a whitelist of capabilities that must be dropped from containers (https://docs.docker.com/engine/reference/run/#runtime-privilege-and-linux-capabilities). These capabilities are removed from the default set, and must not be added. It’s recommended to drop all the capabilities (using ALL), or at least to drop NET_RAW (which allows a process to spy on packets on its network / to inject data onto the network).", + "cloudConfigPath": ["[DocId: 2]","input","spec","requiredDropCapabilities"], + "severity": "high", + "isIgnored": false, + "type": "k8s", + "subType": "Deployment" + }, + { + "id": "SNYK-CC-K8S-2", + "title": "Reducing the admission of containers with dropped capabilities", + "description": "The requiredDropCapabilities property (as part of the Pod Security Policy) provides a whitelist of capabilities that must be dropped from containers (https://docs.docker.com/engine/reference/run/#runtime-privilege-and-linux-capabilities). These capabilities are removed from the default set, and must not be added. It’s recommended to drop all the capabilities (using ALL), or at least to drop NET_RAW (which allows a process to spy on packets on its network / to inject data onto the network).", + "cloudConfigPath": ["[DocId: 2]","input","spec","requiredDropCapabilities"], + "severity": "medium", + "isIgnored": false, + "type": "k8s", + "subType": "NetworkPolicy" + }, + { + "id": "SNYK-CC-K8S-3", + "title": "Reducing the admission of containers with dropped capabilities", + "description": "The requiredDropCapabilities property (as part of the Pod Security Policy) provides a whitelist of capabilities that must be dropped from containers (https://docs.docker.com/engine/reference/run/#runtime-privilege-and-linux-capabilities). These capabilities are removed from the default set, and must not be added. It’s recommended to drop all the capabilities (using ALL), or at least to drop NET_RAW (which allows a process to spy on packets on its network / to inject data onto the network).", + "cloudConfigPath": ["[DocId: 2]","input","spec","requiredDropCapabilities"], + "severity": "low", + "isIgnored": false, + "type": "k8s", + "subType": "Service" + } + ] + }, + "meta": { + "org": "test-org", + "isPublic": false + } +}