Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: tf plan full scan flag support #1865

Merged
merged 1 commit into from May 4, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
7 changes: 7 additions & 0 deletions help/commands-docs/iac.md
Expand Up @@ -48,3 +48,10 @@ Find security issues in your Infrastructure as Code files.
(only in `test` command)
Enable an experimental feature to scan configuration files locally on your machine.
This feature also gives you the ability to scan terraform plan JSON files.

- `--scan=`<TERRAFORM_PLAN_SCAN_MODE>:
Dedicated flag for Terraform plan scanning modes (available only under `--experimental` mode).
It enables to control whether the scan should analyse the full final state (e.g. `planned-values`), or the proposed changes only (e.g. `resource-changes`).
Default: If the `--scan` flag is not provided it would scan the proposed changes only by default.
Example #1: `--scan=planned-values` (full state scan)
Example #2: `--scan=resource-changes` (proposed changes scan)
@@ -1,6 +1,6 @@
import { CustomError } from '../../../../lib/errors';
import { args } from '../../../args';
import { IaCErrorCodes, IaCTestFlags } from './types';
import { IaCErrorCodes, IaCTestFlags, TerraformPlanScanMode } from './types';

const keys: (keyof IaCTestFlags)[] = [
'debug',
Expand All @@ -18,24 +18,42 @@ const keys: (keyof IaCTestFlags)[] = [
'help',
'q',
'quiet',
'scan',
];
const allowed = new Set<string>(keys);

function camelcaseToDash(key: string) {
return key.replace(/[A-Z]/g, (m) => '-' + m.toLowerCase());
}

function getFlagName(key: string) {
const dashes = key.length === 1 ? '-' : '--';
const flag = camelcaseToDash(key);
return `${dashes}${flag}`;
}

class FlagError extends CustomError {
constructor(key: string) {
const dashes = key.length === 1 ? '-' : '--';
const flag = camelcaseToDash(key);
const msg = `Unsupported flag "${dashes}${flag}" provided. Run snyk iac test --help for supported flags.`;
const flag = getFlagName(key);
const msg = `Unsupported flag "${flag}" provided. Run snyk iac test --help for supported flags.`;
super(msg);
this.code = IaCErrorCodes.FlagError;
this.userMessage = msg;
}
}

export class FlagValueError extends CustomError {
constructor(key: string, value: string) {
const flag = getFlagName(key);
const msg = `Unsupported value "${value}" provided to flag "${flag}".\nSupported values are: ${SUPPORTED_TF_PLAN_SCAN_MODES.join(
', ',
)}`;
super(msg);
this.code = IaCErrorCodes.FlagValueError;
this.userMessage = msg;
}
}

/**
* Validates the command line flags passed to the snyk iac test
* command. The current argument parsing is very permissive and
Expand All @@ -58,4 +76,23 @@ export function assertIaCOptionsFlags(argv: string[]) {
throw new FlagError(key);
}
}

if (parsed.options.scan) {
assertTerraformPlanModes(parsed.options.scan as string);
}
}

const SUPPORTED_TF_PLAN_SCAN_MODES = [
TerraformPlanScanMode.DeltaScan,
TerraformPlanScanMode.FullScan,
];

function assertTerraformPlanModes(scanModeArgValue: string) {
if (
!SUPPORTED_TF_PLAN_SCAN_MODES.includes(
scanModeArgValue as TerraformPlanScanMode,
)
) {
throw new FlagValueError('scan', scanModeArgValue);
}
}
14 changes: 11 additions & 3 deletions src/cli/commands/test/iac-local-execution/file-parser.ts
Expand Up @@ -16,18 +16,21 @@ import {
ParsingResults,
IacFileParseFailure,
IaCErrorCodes,
IaCTestFlags,
TerraformPlanScanMode,
} from './types';
import * as analytics from '../../../../lib/analytics';
import { CustomError } from '../../../../lib/errors';

export async function parseFiles(
filesData: IacFileData[],
options: IaCTestFlags = {},
): Promise<ParsingResults> {
const parsedFiles: IacFileParsed[] = [];
const failedFiles: IacFileParseFailure[] = [];
for (const fileData of filesData) {
try {
parsedFiles.push(...tryParseIacFile(fileData));
parsedFiles.push(...tryParseIacFile(fileData, options));
} catch (err) {
if (filesData.length === 1) {
throw err;
Expand Down Expand Up @@ -75,7 +78,10 @@ function parseYAMLOrJSONFileData(fileData: IacFileData): any[] {
return yamlDocuments;
}

export function tryParseIacFile(fileData: IacFileData): IacFileParsed[] {
export function tryParseIacFile(
fileData: IacFileData,
options: IaCTestFlags = {},
): IacFileParsed[] {
analytics.add('iac-terraform-plan', false);
switch (fileData.fileType) {
case 'yaml':
Expand All @@ -89,7 +95,9 @@ export function tryParseIacFile(fileData: IacFileData): IacFileParsed[] {
// but the Terraform plan can only have one
if (parsedIacFile.length === 1 && isTerraformPlan(parsedIacFile[0])) {
analytics.add('iac-terraform-plan', true);
return tryParsingTerraformPlan(fileData, parsedIacFile[0]);
return tryParsingTerraformPlan(fileData, parsedIacFile[0], {
isFullScan: options.scan === TerraformPlanScanMode.FullScan,
});
} else {
try {
return tryParsingKubernetesFile(fileData, parsedIacFile);
Expand Down
2 changes: 1 addition & 1 deletion src/cli/commands/test/iac-local-execution/index.ts
Expand Up @@ -30,7 +30,7 @@ export async function test(
}> {
await initLocalCache();
const filesToParse = await loadFiles(pathToScan, options);
const { parsedFiles, failedFiles } = await parseFiles(filesToParse);
const { parsedFiles, failedFiles } = await parseFiles(filesToParse, options);
const scannedFiles = await scanFiles(parsedFiles);
const iacOrgSettings = await getIacOrgSettings();
const resultsWithCustomSeverities = await applyCustomSeverities(
Expand Down
13 changes: 12 additions & 1 deletion src/cli/commands/test/iac-local-execution/types.ts
Expand Up @@ -130,7 +130,17 @@ export type IaCTestFlags = Pick<
help?: 'help';
q?: boolean;
quiet?: boolean;
};
} & TerraformPlanFlags;

// Flags specific for Terraform plan scanning
interface TerraformPlanFlags {
scan?: TerraformPlanScanMode;
}

export enum TerraformPlanScanMode {
DeltaScan = 'resource-changes', // default value
FullScan = 'planned-values',
}

// Includes all IaCTestOptions plus additional properties
// that are added at runtime and not part of the parsed
Expand Down Expand Up @@ -233,4 +243,5 @@ export enum IaCErrorCodes {

// assert-iac-options-flag
FlagError = 1090,
FlagValueError = 1091,
}
33 changes: 32 additions & 1 deletion test/jest/unit/iac-unit-tests/assert-iac-options-flag.spec.ts
@@ -1,4 +1,7 @@
import { assertIaCOptionsFlags } from '../../../../src/cli/commands/test/iac-local-execution/assert-iac-options-flag';
import {
assertIaCOptionsFlags,
FlagValueError,
} from '../../../../src/cli/commands/test/iac-local-execution/assert-iac-options-flag';

describe('assertIaCOptionsFlags()', () => {
const command = ['node', 'cli', 'iac', 'test'];
Expand Down Expand Up @@ -44,4 +47,32 @@ describe('assertIaCOptionsFlags()', () => {
assertIaCOptionsFlags([...command, ...options, ...files]),
).toThrow();
});

describe('Terraform plan scan modes', () => {
it('throws an error if the scan flag has no value', () => {
const options = ['--scan'];
expect(() =>
assertIaCOptionsFlags([...command, ...options, ...files]),
).toThrow(FlagValueError);
});

it('throws an error if the scan flag has an unsupported value', () => {
const options = ['--scan=rsrce-changes'];
expect(() =>
assertIaCOptionsFlags([...command, ...options, ...files]),
).toThrow(FlagValueError);
});

it.each([
['--scan=resource-changes', 'delta-scan'],
['--scan=planned-values', 'full-scan'],
])(
'does not throw an error if the scan flag has a valid value of %s',
(options) => {
expect(() =>
assertIaCOptionsFlags([...command, ...options, ...files]),
).not.toThrow(FlagValueError);
},
);
});
});
31 changes: 28 additions & 3 deletions test/smoke/spec/iac/snyk_test_local_exec_spec.sh
Expand Up @@ -236,9 +236,8 @@ Describe "Snyk iac test --experimental command"
The output should include "tf-plan.json for known issues, found"
End

# The test below should be enabled once we add the full scan flag
xIt "finds issues in a Terraform plan file - full scan flag"
When run snyk iac test ../fixtures/iac/terraform-plan/tf-plan.json --experimental
It "finds issues in a Terraform plan file - full scan flag"
When run snyk iac test ../fixtures/iac/terraform-plan/tf-plan.json --experimental --scan=planned-values
The status should equal 1 # issues found
The output should include "Testing tf-plan.json"

Expand All @@ -250,5 +249,31 @@ Describe "Snyk iac test --experimental command"

The output should include "tf-plan.json for known issues, found"
End

It "finds issues in a Terraform plan file - explicit delta scan with flag"
When run snyk iac test ../fixtures/iac/terraform-plan/tf-plan.json --experimental --scan=resource-changes
The status should equal 1 # issues found
The output should include "Testing tf-plan.json"

# Outputs issues
The output should include "Infrastructure as code issues:"
# Root module
The output should include "✗ "
The output should include " introduced by"

The output should include "tf-plan.json for known issues, found"
End

It "errors when a wrong value is passed to the --scan flag"
When run snyk iac test ../fixtures/iac/terraform-plan/tf-plan.json --experimental --scan=rsrc-changes
The status should equal 2 # failure
The output should include "Unsupported value"
End

It "errors when no value is provided to the --scan flag"
When run snyk iac test ../fixtures/iac/terraform-plan/tf-plan.json --experimental --scan
The status should equal 2 # failure
The output should include "Unsupported value"
End
End
End