Skip to content

Commit

Permalink
Merge pull request #3116 from snyk/feat/add-var-file-flag-iac-test-all
Browse files Browse the repository at this point in the history
feat: Add '--var-file' flag to iac test for loading external TF variable definition files [CFG-1663]
  • Loading branch information
ipapast authored Apr 11, 2022
2 parents 852fee9 + 141b6c0 commit b0ca60b
Show file tree
Hide file tree
Showing 8 changed files with 114 additions and 9 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ const keys: (keyof IaCTestFlags)[] = [
'project-lifecycle',
'project-business-criticality',
'target-reference',
'var-file',
// PolicyOptions
'ignore-policy',
'policy-path',
Expand Down
38 changes: 37 additions & 1 deletion src/cli/commands/test/iac-local-execution/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { existsSync } from 'fs';
import { isLocalFolder } from '../../../../lib/detect';
import {
EngineType,
IaCErrorCodes,
IacFileParsed,
IacFileParseFailure,
IaCTestFlags,
Expand Down Expand Up @@ -32,6 +34,9 @@ import {
getAllDirectoriesForPath,
getFilesForDirectory,
} from './directory-loader';
import { CustomError } from '../../../../lib/errors';
import { getErrorStringCode } from './error-utils';
import { FeatureFlagError } from './assert-iac-options-flag';

// this method executes the local processing engine and then formats the results to adapt with the CLI output.
// this flow is the default GA flow for IAC scanning.
Expand Down Expand Up @@ -77,6 +82,13 @@ export async function test(
pathToScan,
currentDirectory,
);
if (
currentDirectory === pathToScan &&
shouldLoadVarDefinitionsFile(options, isTFVarSupportEnabled)
) {
const varDefinitionsFilePath = options['var-file'];
filePathsInDirectory.push(varDefinitionsFilePath);
}
const filesToParse = await loadContentForFiles(filePathsInDirectory);
const { parsedFiles, failedFiles } = await parseFiles(
filesToParse,
Expand Down Expand Up @@ -107,7 +119,6 @@ export async function test(
);
}

// TODO: decide if this should go into scanFiles or stay here
const scannedFiles = await scanFiles(allParsedFiles);
const resultsWithCustomSeverities = await applyCustomSeverities(
scannedFiles,
Expand Down Expand Up @@ -176,3 +187,28 @@ function parseAttributes(options: IaCTestFlags) {
return generateProjectAttributes(options);
}
}

function shouldLoadVarDefinitionsFile(
options: IaCTestFlags,
isTFVarSupportEnabled = false,
): options is IaCTestFlags & { 'var-file': string } {
if (options['var-file']) {
if (!isTFVarSupportEnabled) {
throw new FeatureFlagError('var-file', 'iacTerraformVarSupport');
}
if (!existsSync(options['var-file'])) {
throw new InvalidVarFilePath(options['var-file']);
}
return true;
}
return false;
}

export class InvalidVarFilePath extends CustomError {
constructor(path: string, message?: string) {
super(message || 'Invalid path to variable definitions file');
this.code = IaCErrorCodes.InvalidVarFilePath;
this.strCode = getErrorStringCode(this.code);
this.userMessage = `We were unable to locate a variable definitions file at: "${path}". The file at the provided path does not exist`;
}
}
2 changes: 2 additions & 0 deletions src/cli/commands/test/iac-local-execution/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,7 @@ export type IaCTestFlags = Pick<
| 'sarif'
| 'report'
| 'target-reference'
| 'var-file'

// PolicyOptions
| 'ignore-policy'
Expand Down Expand Up @@ -298,6 +299,7 @@ export enum IaCErrorCodes {
FailedToExtractCustomRulesError = 1003,
InvalidCustomRules = 1004,
InvalidCustomRulesPath = 1005,
InvalidVarFilePath = 1006,

// file-loader errors
NoFilesToScanError = 1010,
Expand Down
1 change: 1 addition & 0 deletions src/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ export interface Options {
'no-markdown'?: boolean;
'max-depth'?: number;
report?: boolean;
'var-file'?: string;
}

// TODO(kyegupov): catch accessing ['undefined-properties'] via noImplicitAny
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,16 @@ resource "aws_security_group_rule" "egress" {
cidr_blocks = [var.remote_user_addr]
security_group_id = aws_security_group.allow.id
}

resource "aws_security_group" "allow_ssh_external_var_file" {
name = "allow_ssh"
description = "Allow SSH inbound from anywhere"
vpc_id = "${aws_vpc.main.id}"

ingress {
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = var.remote_user_addr_external_var_file
}
}
4 changes: 4 additions & 0 deletions test/fixtures/iac/terraform/vars.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
variable "remote_user_addr_external_var_file" {
type = list(string)
default = ["0.0.0.0/0"]
}
7 changes: 3 additions & 4 deletions test/jest/acceptance/iac/test-directory.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,6 @@ describe('Directory scan', () => {

it('scans all files in a directory with Kubernetes files', async () => {
const { stdout, exitCode } = await run(`snyk iac test ./iac/kubernetes/`);
expect(exitCode).toBe(1);

expect(stdout).toContain('Testing pod-privileged.yaml'); //directory scan shows relative path to cwd in output
expect(stdout).toContain('Testing pod-privileged-multi.yaml');
expect(stdout).toContain('Testing pod-valid.json');
Expand All @@ -28,11 +26,11 @@ describe('Directory scan', () => {
expect(stdout).toContain(
'Tested 3 projects, 3 contained issues. Failed to test 1 project.',
);
expect(exitCode).toBe(1);
});

it('scans all files in a directory with a mix of IaC files', async () => {
const { stdout, exitCode } = await run(`snyk iac test ./iac/`);
expect(exitCode).toBe(1);
//directory scan shows relative path to cwd in output
// here we assert just on the filename to avoid the different slashes (/) for Unix/Windows on the CI runner
expect(stdout).toContain('pod-privileged.yaml');
Expand All @@ -47,8 +45,9 @@ describe('Directory scan', () => {
expect(stdout).toContain('Failed to parse YAML file');
expect(stdout).toContain('Failed to parse JSON file');
expect(stdout).toContain(
'22 projects, 15 contained issues. Failed to test 8 projects.',
'23 projects, 15 contained issues. Failed to test 8 projects.',
);
expect(exitCode).toBe(1);
});

it('filters out issues when using severity threshold', async () => {
Expand Down
57 changes: 53 additions & 4 deletions test/jest/acceptance/iac/test-terraform-var-deref.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { startMockServer, isValidJSONString } from './helpers';
import { isValidJSONString, startMockServer } from './helpers';
import * as path from 'path';

jest.setTimeout(50000);
Expand All @@ -23,9 +23,6 @@ describe('Terraform Language Support', () => {
`snyk iac test ./iac/terraform/var_deref`,
);

// expect exitCode to be 0 or 1
expect(exitCode).toBeLessThanOrEqual(1);

expect(stdout).toContain('Testing sg_open_ssh.tf...');
expect(stdout.match(/✗ Security Group allows open ingress/g)).toBeNull();
expect(stdout).toContain('Tested sg_open_ssh.tf for known issues');
Expand All @@ -40,6 +37,17 @@ describe('Terraform Language Support', () => {
'sg_open_ssh.tf',
)} for known issues`,
);
// expect exitCode to be 0 or 1
expect(exitCode).toBeLessThanOrEqual(1);
});
it('returns an error if var-file flag is used', async () => {
const { stdout, exitCode } = await run(
`snyk iac test ./iac/terraform/var_deref --var-file=path/to/var-file.tfvars`,
);
expect(stdout).toMatch(
'Flag "--var-file" is only supported if feature flag "iacTerraformVarSupport" is enabled. To enable it, please contact Snyk support.',
);
expect(exitCode).toBe(2);
});

it('returns error if empty Terraform file', async () => {
Expand Down Expand Up @@ -248,6 +256,47 @@ describe('Terraform Language Support', () => {
});
});

describe('with the --var-file flag', () => {
it('picks up the file and dereferences the variable context for the right directory (pathToScan)', async () => {
const { stdout, exitCode } = await run(
`snyk iac test --org=tf-lang-support ./iac/terraform/var_deref/nested_var_deref --var-file=./iac/terraform/vars.tf`,
);
expect(stdout).toContain(
`Testing ${path.relative(
'./iac/terraform/var_deref/nested_var_deref',
'./iac/terraform/vars.tf',
)}`,
);
expect(stdout).toContain(
'introduced by input > resource > aws_security_group[allow_ssh_external_var_file] > ingress\n',
);
expect(
stdout.match(
/Project path: {6}.\/iac\/terraform\/var_deref\/nested_var_deref/g,
),
).toHaveLength(3);
expect(stdout.match(/Project path: {6}.\/iac\/terraform$/g)).toBeNull();
expect(exitCode).toBe(1);
});
it('returns error if the file does not exist', async () => {
const { stdout, exitCode } = await run(
`snyk iac test --org=tf-lang-support ./iac/terraform/var_deref --var-file=./iac/terraform/non-existent.tfvars`,
);
expect(stdout).toContain(
'We were unable to locate a variable definitions file at: "./iac/terraform/non-existent.tfvars". The file at the provided path does not exist',
);
expect(exitCode).toBe(2);
});
it('will not parse the external file if it is invalid', async () => {
const { stdout, exitCode } = await run(
`snyk iac test --org=tf-lang-support ./iac/terraform/var_deref --var-file=./iac/terraform/sg_open_ssh_invalid_hcl2.tf`,
);
expect(stdout).toContain('Testing sg_open_ssh_invalid_hcl2.tf...');
expect(stdout).toContain('Failed to parse Terraform file');
expect(exitCode).toBe(1);
});
});

describe('other functions', () => {
it('is backwards compatible without variable dereferencing', async () => {
const { stdout, exitCode } = await run(
Expand Down

0 comments on commit b0ca60b

Please sign in to comment.