diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index dd010f9f5a8..1696c543308 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -3,3 +3,11 @@ README.md @snyk/content @snyk/boost src/cli/commands/test/formatters/format-reachability.ts @snyk/flow 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 +test/acceptance/cli-test/cli-test.iac-k8s.spec.ts @snyk/cloudconfig \ No newline at end of file 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/commands/test/formatters/format-test-meta.ts b/src/cli/commands/test/formatters/format-test-meta.ts index ec26b3aa975..03b8056f93f 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 { IacTestResult } from '../../../../lib/snyk-test/iac-test-result'; export function formatTestMeta( - res: TestResult, + res: TestResult | IacTestResult, 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/iac-output.ts b/src/cli/commands/test/iac-output.ts new file mode 100644 index 00000000000..4f895f3726f --- /dev/null +++ b/src/cli/commands/test/iac-output.ts @@ -0,0 +1,60 @@ +import chalk from 'chalk'; +import * as Debug from 'debug'; +import { Options, TestOptions } from '../../../lib/types'; +import { IacTestResult } from '../../../lib/snyk-test/iac-test-result'; +import { getSeverityValue } from './formatters'; +import { formatIssue } from './formatters/remediation-based-format-issues'; +import { AnnotatedIacIssue } from '../../../lib/snyk-test/iac-test-result'; + +const debug = Debug('iac-output'); + +export function getIacDisplayedOutput( + res: IacTestResult, + 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: 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]; + issuesTextArray.push( + formatIssue( + issue.id, + issue.title, + issue.severity, + NotNew, + issue.subType, + path, + testOptions, + NoNote, + ), + ); + }); + + const issuesInfoOutput: string[] = []; + debug(`Iac 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/index.ts b/src/cli/commands/test/index.ts index dc6909bea78..97ad01daa1c 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 { IacTestResult } from '../../../lib/snyk-test/iac-test-result'; import { SupportedPackageManagers, WIZARD_SUPPORTED_PACKAGE_MANAGERS, @@ -43,6 +44,7 @@ import { summariseVulnerableResults, } from './formatters'; import * as utils from './utils'; +import { getIacDisplayedOutput } from './iac-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) + : createErrorMappedResultsForJsonOutputForIac(results); // backwards compat - strip array IFF only one result const dataToSend = errorMappedResults.length === 1 @@ -295,6 +303,25 @@ function createErrorMappedResultsForJsonOutput(results) { return errorMappedResults; } +function createErrorMappedResultsForJsonOutputForIac(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 getIacDisplayedOutput( + (res as any) as IacTestResult, + 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/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/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/detect.ts b/src/lib/detect.ts index fcf4697a994..4605d5f0468 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 './iac/iac-parser'; const debug = debugLib('snyk-detect'); @@ -137,6 +138,30 @@ export function detectPackageManager(root: string, options) { return packageManager; } +export function isIacProject(root: string, options): string { + if (!isLocalFolder(root)) { + debug('Iac - repo case ' + root); + throw "iac option doesn't support lookup as repo"; + } + + if (!options.file) { + debug('Iac - 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..c5a8479b68f 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 { + NotSupportedIacFileError, + IllegalIacFileError, +} from './invalid-iac-file'; diff --git a/src/lib/errors/invalid-iac-file.ts b/src/lib/errors/invalid-iac-file.ts new file mode 100644 index 00000000000..eda6d979795 --- /dev/null +++ b/src/lib/errors/invalid-iac-file.ts @@ -0,0 +1,38 @@ +import chalk from 'chalk'; +import { CustomError } from './custom-error'; + +export function NotSupportedIacFileError(atLocations: string[]) { + const locationsStr = atLocations.join(', '); + const errorMsg = + 'Not supported infrastruction as code 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 IllegalIacFileError(atLocations: string[]): CustomError { + const locationsStr = atLocations.join(', '); + const errorMsg = + 'Illegal infrastruction as code 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; +} diff --git a/src/lib/iac/iac-parser.ts b/src/lib/iac/iac-parser.ts new file mode 100644 index 00000000000..943a59d91ff --- /dev/null +++ b/src/lib/iac/iac-parser.ts @@ -0,0 +1,90 @@ +//TODO(orka): take out into a new lib +import * as YAML from 'js-yaml'; +import * as debugLib from 'debug'; +import { IllegalIacFileError, NotSupportedIacFileError } 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 iac config as a YAML'); + } + break; + case 'json': + try { + const objectsArr: any[] = []; + objectsArr.push(JSON.parse(fileContent)); + return objectsArr; + } catch (e) { + debug('Failed to parse iac config as a JSON'); + } + break; + default: + debug(`Unsupported iac 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 IllegalIacFileError([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 IllegalIacFileError([root]); + } + } + } + + if (numOfSupportedKeyDocs === 0) { + 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/iac-test-result.ts b/src/lib/snyk-test/iac-test-result.ts new file mode 100644 index 00000000000..a4ade59c325 --- /dev/null +++ b/src/lib/snyk-test/iac-test-result.ts @@ -0,0 +1,23 @@ +import { BasicResultData, SEVERITY } from './legacy'; + +export interface AnnotatedIacIssue { + id: string; + title: string; + description: string; + severity: SEVERITY; + isIgnored: boolean; + cloudConfigPath: string[]; + type: string; + subType: string; +} + +export interface IacTestResult extends BasicResultData { + targetFile: string; + projectName: string; + displayTargetFile: string; // used for display only + foundProjectCount: number; + result: { + cloudConfigResults: AnnotatedIacIssue[]; + projectType: string; + }; +} diff --git a/src/lib/snyk-test/index.js b/src/lib/snyk-test/index.js index eed58c3aff2..b0b52971616 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 iacProjects = require('../iac/iac-projects'); +const { + UnsupportedPackageManagerError, + NoSupportedIacFileError, +} = 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.isIacProject(root, options); + } else { + options.packageManager = detect.detectPackageManager(root, options); + } } return run(root, options).then((results) => { for (const res of results) { @@ -49,6 +57,14 @@ function executeTest(root, options) { } function run(root, options) { + if (options.iac) { + const projectType = options.packageManager; + if (!iacProjects.TEST_SUPPORTED_IAC_PROJECTS.includes(projectType)) { + throw new NoSupportedIacFileError(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 e698a04b5b3..3972e8c6e94 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 { @@ -121,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; @@ -279,7 +283,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/payload-schema.ts b/src/lib/snyk-test/payload-schema.ts new file mode 100644 index 00000000000..9ffbb9729b0 --- /dev/null +++ b/src/lib/snyk-test/payload-schema.ts @@ -0,0 +1,23 @@ +//TODO(orka): future - change this file +import { IacProjectTypes } from '../iac/iac-projects'; + +interface Scan { + type: string; + targetFile: string; + data: any; +} + +export interface IacFile { + fileContent: string; + fileType: 'yaml' | 'yml' | 'json'; +} + +export interface IacScan extends Scan { + type: IacProjectTypes; + targetFile: string; + data: IacFile; + targetFileRelativePath: string; + originalProjectName: string; + policy: string; + projectNameOverride?: string; +} diff --git a/src/lib/snyk-test/run-iac-test.ts b/src/lib/snyk-test/run-iac-test.ts new file mode 100644 index 00000000000..5019dc1b773 --- /dev/null +++ b/src/lib/snyk-test/run-iac-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 { 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 { IacScan } from './payload-schema'; +import { SEVERITY } from './legacy'; +import * as pathLib from 'path'; + +export async function parseIacTestResult( + res: IacTestResult, + 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 assembleIacLocalPayloads( + 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: IacScan = { + 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 74b653276e2..cea2a91f1dc 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 { IacScan } from './payload-schema'; import { TestResult, DockerIssue, AnnotatedIssue, - LegacyVulnApiResult, TestDepGraphResponse, convertTestDepGraphResultToLegacy, + LegacyVulnApiResult, } from './legacy'; +import { IacTestResult } from './iac-test-result'; import { AuthFailedError, InternalServerError, @@ -49,60 +51,45 @@ import { getSubProjectCount } from '../plugins/get-sub-project-count'; import { serializeCallGraphWithMetrics } from '../reachable-vulns'; import { validateOptions } from '../options-validator'; import { findAndLoadPolicy } from '../policy'; +import { assembleIacLocalPayloads, parseIacTestResult } from './run-iac-test'; +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, +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; + for (const payload of payloads) { + await spinner(spinnerLbl); + if (options.iac) { + const iacScan: IacScan = payload.body as IacScan; + analytics.add('iac type', !!iacScan.type); + const res = (await sendTestPayload(payload)) as IacTestResult; + + const projectName = + iacScan.projectNameOverride || iacScan.originalProjectName; + const result = await parseIacTestResult( + res, + iacScan.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; - const targetFile = payload.body && payload.body.targetFile; + depGraph && + depGraph.pkgManager && + (depGraph.pkgManager.name as SupportedProjectTypes); + const targetFile = payloadBody && payloadBody.targetFile; const projectName = _.get(payload, 'body.projectNameOverride') || _.get(payload, 'body.originalProjectName'); @@ -110,15 +97,14 @@ async function runTest( const displayTargetFile = _.get(payload, 'body.displayTargetFile'); let dockerfilePackages; if ( - payload.body && - payload.body.docker && - payload.body.docker.dockerfilePackages + payloadBody && + payloadBody.docker && + payloadBody.docker.dockerfilePackages ) { - dockerfilePackages = payload.body.docker.dockerfilePackages; + dockerfilePackages = payloadBody.docker.dockerfilePackages; } - await spinner(spinnerLbl); analytics.add('depGraph', !!depGraph); - analytics.add('isDocker', !!(payload.body && payload.body.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; @@ -141,7 +127,20 @@ async function runTest( displayTargetFile, }); } - return results; + } + 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 @@ -173,7 +172,7 @@ async function runTest( async function parseRes( depGraph: depGraphLib.DepGraph | undefined, - pkgManager: string | undefined, + pkgManager: SupportedProjectTypes | undefined, res: LegacyVulnApiResult, options: Options & TestOptions, payload: Payload, @@ -259,7 +258,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) => { @@ -326,12 +325,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'); @@ -339,6 +344,9 @@ async function assembleLocalPayloads( const payloads: Payload[] = []; await spinner(spinnerLbl); + if (options.iac) { + return assembleIacLocalPayloads(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 new file mode 100644 index 00000000000..23b39c131d6 --- /dev/null +++ b/src/lib/snyk-test/types.ts @@ -0,0 +1,37 @@ +import * as depGraphLib from '@snyk/dep-graph'; +import { GitTarget, ContainerTarget } from '../project-metadata/types'; +import { DepTree } from '../types'; +import { IacScan } from './payload-schema'; + +export 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 | IacScan; + qs?: object | null; + modules?: DepTreeFromResolveDeps; +} diff --git a/src/lib/types.ts b/src/lib/types.ts index d19948468dd..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-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'; @@ -41,6 +41,7 @@ export interface Options { org?: string | null; path: string; docker?: boolean; + iac?: boolean; file?: string; policy?: string; json?: boolean; @@ -124,6 +125,4 @@ export interface SpinnerOptions { cleanup?: any; } -export type SupportedProjectTypes = - | SupportedCloudConfigs - | SupportedPackageManagers; +export type SupportedProjectTypes = IacProjectTypes | SupportedPackageManagers; 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 + } +}