Skip to content

Commit

Permalink
Merge pull request #1140 from snyk/feat/support-cloud-config
Browse files Browse the repository at this point in the history
feat: add Cloud Config support
  • Loading branch information
orkamara committed Jun 29, 2020
2 parents d8dddaf + 09f330c commit 5a22e6a
Show file tree
Hide file tree
Showing 27 changed files with 1,311 additions and 101 deletions.
8 changes: 8 additions & 0 deletions .github/CODEOWNERS
Validating CODEOWNERS rules …
Expand Up @@ -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
24 changes: 24 additions & 0 deletions help/iac.txt
@@ -0,0 +1,24 @@
Usage:

$ snyk iac [command] [options] --file=<path>

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=<string> ................. Specify a custom Snyk project name.
--policy-path=<path> .................... Manually pass a path to a snyk policy file.
--severity-threshold=<low|medium|high>... 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
51 changes: 29 additions & 22 deletions src/cli/commands/test/formatters/format-test-meta.ts
Expand Up @@ -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
Expand Down Expand Up @@ -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');
Expand Down
60 changes: 60 additions & 0 deletions 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;
}
48 changes: 44 additions & 4 deletions src/cli/commands/test/index.ts
Expand Up @@ -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,
Expand All @@ -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';
Expand Down Expand Up @@ -156,7 +158,11 @@ async function test(...args: MethodArgs): Promise<TestCommandResult> {
}

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;
Expand All @@ -165,7 +171,9 @@ async function test(...args: MethodArgs): Promise<TestCommandResult> {
// 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
Expand Down Expand Up @@ -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') {
Expand Down Expand Up @@ -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')) {
Expand Down Expand Up @@ -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,
Expand Down
8 changes: 8 additions & 0 deletions src/cli/modes.ts
Expand Up @@ -13,6 +13,14 @@ const modes: Record<string, ModeData> = {
args['docker'] = true;
args['experimental'] = true;

return args;
},
},
iac: {
allowedCommands: ['test'],
config: (args): [] => {
args['iac'] = true;

return args;
},
},
Expand Down
2 changes: 0 additions & 2 deletions src/lib/cloud-config-projects.ts

This file was deleted.

25 changes: 25 additions & 0 deletions src/lib/detect.ts
Expand Up @@ -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');

Expand Down Expand Up @@ -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 (
Expand Down
4 changes: 4 additions & 0 deletions src/lib/errors/index.ts
Expand Up @@ -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';
38 changes: 38 additions & 0 deletions 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;
}

0 comments on commit 5a22e6a

Please sign in to comment.