Skip to content

Commit

Permalink
feat: iac test directory support
Browse files Browse the repository at this point in the history
  • Loading branch information
Almog Ben-David committed Oct 15, 2020
1 parent c2815f6 commit 0691d66
Show file tree
Hide file tree
Showing 27 changed files with 805 additions and 194 deletions.
3 changes: 1 addition & 2 deletions .github/CODEOWNERS
Validating CODEOWNERS rules …
Expand Up @@ -10,8 +10,7 @@ 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
test/acceptance/cli-test/cli-test.iac-k8s.utils.ts @snyk/cloudconfig
test/acceptance/cli-test/iac/ @snyk/cloudconfig
test/fixtures/iac/* @snyk/cloudconfig
test/smoke/spec/iac/* @snyk/cloudconfig
src/lib/errors/invalid-iac-file.ts @snyk/cloudconfig
Expand Down
@@ -1,13 +1,15 @@
import { TestOptions } from '../../../../lib/types';
import { Options, TestOptions } from '../../../../lib/types';

export function summariseVulnerableResults(
vulnerableResults,
options: TestOptions,
options: Options & TestOptions,
): string {
const vulnsLength = vulnerableResults.length;
if (vulnsLength) {
if (options.showVulnPaths) {
return `, ${vulnsLength} contained vulnerable paths.`;
return `, ${vulnsLength} contained ${
options.iac ? 'issues' : 'vulnerable paths'
}.`;
}
return `, ${vulnsLength} had issues.`;
}
Expand Down
15 changes: 15 additions & 0 deletions src/cli/commands/test/iac-output.ts
@@ -1,5 +1,6 @@
import chalk from 'chalk';
import * as Debug from 'debug';
import * as pathLib from 'path';
import {
IacTestResponse,
AnnotatedIacIssue,
Expand All @@ -9,6 +10,7 @@ import { printPath } from './formatters/remediation-based-format-issues';
import { titleCaseText } from './formatters/legacy-format-issue';
import * as sarif from 'sarif';
import { SEVERITY } from '../../../lib/snyk-test/legacy';
import { IacFileInDirectory } from '../../../lib/types';
import upperFirst = require('lodash/upperFirst');
const debug = Debug('iac-output');

Expand Down Expand Up @@ -109,6 +111,19 @@ export function getIacDisplayedOutput(
return prefix + body;
}

export function getIacDisplayErrorFileOutput(
iacFileResult: IacFileInDirectory,
): string {
const fileName = pathLib.basename(iacFileResult.filePath);
return `
-------------------------------------------------------
Testing ${fileName}...
${iacFileResult.failureReason}`;
}

export function capitalizePackageManager(type) {
switch (type) {
case 'k8sconfig': {
Expand Down
32 changes: 28 additions & 4 deletions src/cli/commands/test/index.ts
Expand Up @@ -8,6 +8,7 @@ import { isCI } from '../../../lib/is-ci';
import { apiTokenExists, getDockerToken } from '../../../lib/api-token';
import { FAIL_ON, FailOn, SEVERITIES } from '../../../lib/snyk-test/common';
import * as Debug from 'debug';
import * as pathLib from 'path';
import {
Options,
ShowVulnPaths,
Expand Down Expand Up @@ -48,7 +49,11 @@ import {
summariseVulnerableResults,
} from './formatters';
import * as utils from './utils';
import { getIacDisplayedOutput, createSarifOutputForIac } from './iac-output';
import {
getIacDisplayedOutput,
getIacDisplayErrorFileOutput,
createSarifOutputForIac,
} from './iac-output';
import { getEcosystemForTest, testEcosystem } from '../../../lib/ecosystems';
import { isMultiProjectScan } from '../../../lib/is-multi-project-scan';
import { createSarifOutputForContainers } from './sarif-output';
Expand Down Expand Up @@ -141,6 +146,9 @@ async function test(...args: MethodArgs): Promise<TestCommandResult> {

try {
res = await snyk.test(path, testOpts);
if (testOpts.iacDirFiles) {
options.iacDirFiles = testOpts.iacDirFiles;
}
} catch (error) {
// Possible error cases:
// - the test found some vulns. `error.message` is a
Expand Down Expand Up @@ -284,13 +292,27 @@ async function test(...args: MethodArgs): Promise<TestCommandResult> {
}

let summaryMessage = '';
let errorResultsLength = errorResults.length;

if (options.iac && options.iacDirFiles) {
const iacDirFilesErrors = options.iacDirFiles?.filter(
(iacFile) => iacFile.failureReason,
);
errorResultsLength = iacDirFilesErrors?.length || errorResults.length;

if (iacDirFilesErrors) {
for (const iacFileError of iacDirFilesErrors) {
response += chalk.bold.red(getIacDisplayErrorFileOutput(iacFileError));
}
}
}

if (results.length > 1) {
const projects = results.length === 1 ? 'project' : 'projects';
summaryMessage =
`\n\n\nTested ${results.length} ${projects}` +
summariseVulnerableResults(vulnerableResults, options) +
summariseErrorResults(errorResults.length) +
summariseErrorResults(errorResultsLength) +
'\n';
}

Expand Down Expand Up @@ -435,8 +457,8 @@ function displayResult(
(res.packageManager as SupportedProjectTypes) || options.packageManager;
const localPackageTest = isLocalFolder(options.path);
let testingPath = options.path;
if (options.iac && options.file) {
testingPath = options.file;
if (options.iac && options.iacDirFiles && res.targetFile) {
testingPath = pathLib.basename(res.targetFile);
}
const prefix = chalk.bold.white('\nTesting ' + testingPath + '...\n\n');

Expand All @@ -453,6 +475,8 @@ function displayResult(

if (res.dependencyCount) {
pathOrDepsText += res.dependencyCount + ' dependencies';
} else if (options.iacDirFiles && res.targetFile) {
pathOrDepsText += pathLib.basename(res.targetFile);
} else {
pathOrDepsText += options.path;
}
Expand Down
54 changes: 2 additions & 52 deletions src/lib/detect.ts
Expand Up @@ -2,18 +2,8 @@ import * as fs from 'fs';
import * as pathLib from 'path';
import * as debugLib from 'debug';
import * as _ from 'lodash';
import { IllegalIacFileError, NoSupportedManifestsFoundError } from './errors';
import { NoSupportedManifestsFoundError } from './errors';
import { SupportedPackageManagers } from './package-managers';
import {
validateK8sFile,
makeValidateTerraformRequest,
} from './iac/iac-parser';
import { projectTypeByFileType, IacProjectType } from './iac/constants';
import {
SupportLocalFileOnlyIacError,
UnsupportedOptionFileIacError,
} from './errors/unsupported-options-iac-error';
import { IllegalTerraformFileError } from './errors/invalid-iac-file';

const debug = debugLib('snyk-detect');

Expand Down Expand Up @@ -150,48 +140,8 @@ export function detectPackageManager(root: string, options) {
return packageManager;
}

export async function isIacProject(root: string, options): Promise<string> {
if (options.file) {
debug('Iac - --file specified ' + options.file);
throw UnsupportedOptionFileIacError(options.file);
}

if (isLocalFolder(root)) {
debug('Iac - folder case ' + root);
throw SupportLocalFileOnlyIacError();
}

if (localFileSuppliedButNotFound(root, '.') || !fs.existsSync(root)) {
throw SupportLocalFileOnlyIacError();
}

const filePath = pathLib.resolve(root, '.');
const fileContent = fs.readFileSync(filePath, 'utf-8');
const fileType = root.substr(root.lastIndexOf('.') + 1);
const projectType = projectTypeByFileType[fileType];
switch (projectType) {
case IacProjectType.K8S:
validateK8sFile(fileContent, filePath, root);
break;
case IacProjectType.TERRAFORM: {
const {
isValidTerraformFile,
reason,
} = await makeValidateTerraformRequest(fileContent);
if (!isValidTerraformFile) {
throw IllegalTerraformFileError([root], reason);
}
break;
}
default:
throw IllegalIacFileError([root]);
}

return projectType;
}

// User supplied a "local" file, but that file doesn't exist
function localFileSuppliedButNotFound(root, file) {
export function localFileSuppliedButNotFound(root, file) {
return (
file && fs.existsSync(root) && !fs.existsSync(pathLib.resolve(root, file))
);
Expand Down
4 changes: 3 additions & 1 deletion src/lib/errors/index.ts
Expand Up @@ -21,5 +21,7 @@ export { UnsupportedOptionCombinationError } from './unsupported-option-combinat
export { FeatureNotSupportedByPackageManagerError } from './feature-not-supported-by-package-manager-error';
export {
NotSupportedIacFileError,
IllegalIacFileError,
NotSupportedIacFileErrorMsg,
IllegalIacFileErrorMsg,
NotSupportedIacAllProjects,
} from './invalid-iac-file';
53 changes: 38 additions & 15 deletions src/lib/errors/invalid-iac-file.ts
@@ -1,34 +1,47 @@
import chalk from 'chalk';
import { CustomError } from './custom-error';

export function NotSupportedIacFileError(atLocations: string[]) {
const locationsStr = atLocations.join(', ');
const errorMsg =
export function NotSupportedIacFileErrorMsg(fileName: string): string {
return (
'Not supported infrastructure as code target files in ' +
locationsStr +
fileName +
'.\nPlease see our documentation for supported target files (currently we support Kubernetes files only): ' +
chalk.underline(
'https://support.snyk.io/hc/en-us/articles/360006368877-Scan-and-fix-security-issues-in-your-Kubernetes-configuration-files',
) +
' and make sure you are in the right directory.';

const error = new CustomError(errorMsg);
error.code = 422;
error.userMessage = errorMsg;
return error;
' and make sure you are in the right directory.'
);
}

export function IllegalIacFileError(atLocations: string[]): CustomError {
const locationsStr = atLocations.join(', ');
const errorMsg =
export function IllegalIacFileErrorMsg(fileName: string): string {
return (
'Illegal infrastructure as code target file ' +
locationsStr +
fileName +
'.\nPlease see our documentation for supported target files (currently we support Kubernetes files only): ' +
chalk.underline(
'https://support.snyk.io/hc/en-us/articles/360006368877-Scan-and-fix-security-issues-in-your-Kubernetes-configuration-files',
) +
' and make sure you are in the right directory.';
' and make sure you are in the right directory.'
);
}

export function NotSupportedIacFileError(fileName: string): CustomError {
const errorMsg = NotSupportedIacFileErrorMsg(fileName);
const error = new CustomError(errorMsg);
error.code = 422;
error.userMessage = errorMsg;
return error;
}

export function IllegalIacCustomError(fileName: string): CustomError {
const errorMsg = IllegalIacFileErrorMsg(fileName);
const error = new CustomError(errorMsg);
error.code = 422;
error.userMessage = errorMsg;
return error;
}

export function InvalidK8SFileError(errorMsg: string): CustomError {
const error = new CustomError(errorMsg);
error.code = 422;
error.userMessage = errorMsg;
Expand All @@ -53,3 +66,13 @@ export function IllegalTerraformFileError(
error.userMessage = errorMsg;
return error;
}

export function NotSupportedIacAllProjects(path: string): CustomError {
const errorMsg =
`Infrastructure as Code test does not support the "--all-projects" flag.\n` +
`Directories with multiple IaC files can be tested using "snyk iac test ${path}" command.`;
const error = new CustomError(errorMsg);
error.code = 422;
error.userMessage = errorMsg;
return error;
}
4 changes: 4 additions & 0 deletions src/lib/errors/unsupported-options-iac-error.ts
Expand Up @@ -8,3 +8,7 @@ export function UnsupportedOptionFileIacError(path: string): string {
'Check other options by running snyk iac --help'
);
}

export function IacDirectoryWithoutAnyIacFileError(): string {
return 'iac test support single valid file or a directory of valid files';
}
12 changes: 11 additions & 1 deletion src/lib/iac/constants.ts
@@ -1,14 +1,19 @@
export type IacProjectTypes = 'k8sconfig' | 'terraformconfig';
export type IacProjectTypes =
| 'k8sconfig'
| 'terraformconfig'
| 'multiiacconfig';
export type IacFileTypes = 'yaml' | 'yml' | 'json' | 'tf';

export enum IacProjectType {
K8S = 'k8sconfig',
TERRAFORM = 'terraformconfig',
MULTI_IAC = 'multiiacconfig',
}

export const TEST_SUPPORTED_IAC_PROJECTS: IacProjectTypes[] = [
IacProjectType.K8S,
IacProjectType.TERRAFORM,
IacProjectType.MULTI_IAC,
];

export const projectTypeByFileType = {
Expand All @@ -24,3 +29,8 @@ export type IacValidateTerraformResponse = {
reason: string;
};
};

export interface IacValidationResponse {
isValidFile: boolean;
reason: string;
}

0 comments on commit 0691d66

Please sign in to comment.