Skip to content

Commit

Permalink
feat: tf plan full scan flag support
Browse files Browse the repository at this point in the history
  • Loading branch information
rontalx committed May 2, 2021
1 parent c58f026 commit 8800697
Show file tree
Hide file tree
Showing 7 changed files with 132 additions and 13 deletions.
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

0 comments on commit 8800697

Please sign in to comment.