From eedd239df648c5b9098e69487a3771a6fae23f99 Mon Sep 17 00:00:00 2001 From: Ilianna Papastefanou Date: Fri, 29 Apr 2022 16:09:44 +0100 Subject: [PATCH] feat: enable TF Vars Support for all This commits enables the terraform variable dereferencing flow to be the default one as we go into GA. Now everyone will benefit from getting issues based on their variables values, without having to enable a FF. [CFG-1785] test: merge tests after removing FF --- .../test/iac/local-execution/file-parser.ts | 21 +- .../test/iac/local-execution/index.ts | 16 +- test/acceptance/fake-server.ts | 8 - test/jest/acceptance/iac/iac-output.spec.ts | 12 +- .../acceptance/iac/test-directory.spec.ts | 2 +- .../iac/test-terraform-var-deref.spec.ts | 416 ------------------ .../acceptance/iac/test-terraform.spec.ts | 252 +++++++++-- test/jest/unit/iac/file-parser.spec.ts | 18 +- test/jest/unit/iac/index.spec.ts | 15 +- 9 files changed, 231 insertions(+), 529 deletions(-) delete mode 100644 test/jest/acceptance/iac/test-terraform-var-deref.spec.ts diff --git a/src/cli/commands/test/iac/local-execution/file-parser.ts b/src/cli/commands/test/iac/local-execution/file-parser.ts index f90e972e75..d5b5cf907b 100644 --- a/src/cli/commands/test/iac/local-execution/file-parser.ts +++ b/src/cli/commands/test/iac/local-execution/file-parser.ts @@ -27,28 +27,21 @@ import hclToJsonV2 from './parsers/hcl-to-json-v2'; import { IacProjectType } from '../../../../../lib/iac/constants'; import * as Debug from 'debug'; + const debug = Debug('snyk-test'); export async function parseFiles( filesData: IacFileData[], options: IaCTestFlags = {}, - isTFVarSupportEnabled = false, ): Promise { let tfFileData: IacFileData[] = []; let nonTfFileData: IacFileData[] = []; - - if (!isTFVarSupportEnabled) { - nonTfFileData = filesData.filter((fileData) => - ['tf', 'json', 'yaml', 'yml'].includes(fileData.fileType), - ); - } else { - tfFileData = filesData.filter((fileData) => - VALID_TERRAFORM_FILE_TYPES.includes(fileData.fileType), - ); - nonTfFileData = filesData.filter( - (fileData) => !VALID_TERRAFORM_FILE_TYPES.includes(fileData.fileType), - ); - } + tfFileData = filesData.filter((fileData) => + VALID_TERRAFORM_FILE_TYPES.includes(fileData.fileType), + ); + nonTfFileData = filesData.filter( + (fileData) => !VALID_TERRAFORM_FILE_TYPES.includes(fileData.fileType), + ); let { parsedFiles, failedFiles } = parseNonTerraformFiles( nonTfFileData, diff --git a/src/cli/commands/test/iac/local-execution/index.ts b/src/cli/commands/test/iac/local-execution/index.ts index 1b498c4751..910abcf718 100644 --- a/src/cli/commands/test/iac/local-execution/index.ts +++ b/src/cli/commands/test/iac/local-execution/index.ts @@ -22,7 +22,6 @@ import { trackUsage, } from './measurable-methods'; import { findAndLoadPolicy } from '../../../../../lib/policy'; -import { isFeatureFlagSupportedForOrg } from '../../../../../lib/feature-flags'; import { NoFilesToScanError } from './file-loader'; import { processResults } from './process-results'; import { generateProjectAttributes, generateTags } from '../../../monitor'; @@ -32,7 +31,6 @@ import { } 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. @@ -50,13 +48,6 @@ export async function test( const policy = await findAndLoadPolicy(pathToScan, 'iac', options); - const isTFVarSupportEnabled = ( - await isFeatureFlagSupportedForOrg( - 'iacTerraformVarSupport', - iacOrgSettings.meta.org, - ) - ).ok; - let allParsedFiles: IacFileParsed[] = [], allFailedFiles: IacFileParseFailure[] = []; const allDirectories = getAllDirectoriesForPath( @@ -73,7 +64,7 @@ export async function test( ); if ( currentDirectory === pathToScan && - shouldLoadVarDefinitionsFile(options, isTFVarSupportEnabled) + shouldLoadVarDefinitionsFile(options) ) { const varDefinitionsFilePath = options['var-file']; filePathsInDirectory.push(varDefinitionsFilePath); @@ -82,7 +73,6 @@ export async function test( const { parsedFiles, failedFiles } = await parseFiles( filesToParse, options, - isTFVarSupportEnabled, ); allParsedFiles = allParsedFiles.concat(parsedFiles); allFailedFiles = allFailedFiles.concat(failedFiles); @@ -182,12 +172,8 @@ function parseAttributes(options: IaCTestFlags) { 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']); } diff --git a/test/acceptance/fake-server.ts b/test/acceptance/fake-server.ts index 0da8b957c2..c926de8c8d 100644 --- a/test/acceptance/fake-server.ts +++ b/test/acceptance/fake-server.ts @@ -9,7 +9,6 @@ import { getFixturePath } from '../jest/util/getFixturePath'; const featureFlagDefaults = (): Map => { return new Map([ ['cliFailFast', false], - ['iacTerraformVarSupport', false], ['iacCliOutputRelease', false], ]); }; @@ -341,13 +340,6 @@ export const fakeServer = (basePath: string, snykToken: string): FakeServer => { return; } - if (org === 'tf-lang-support' && flag === 'iacTerraformVarSupport') { - res.send({ - ok: true, - }); - return; - } - if (featureFlags.has(flag)) { const ffEnabled = featureFlags.get(flag); if (ffEnabled) { diff --git a/test/jest/acceptance/iac/iac-output.spec.ts b/test/jest/acceptance/iac/iac-output.spec.ts index 563734fc04..cd338beef8 100644 --- a/test/jest/acceptance/iac/iac-output.spec.ts +++ b/test/jest/acceptance/iac/iac-output.spec.ts @@ -325,34 +325,32 @@ Project path: ${filePath} expect(stdout).toContain( 'Tested sg_open_ssh.tf for known issues, found 1 issues', ); - expect(stdout).toContain( - 'Tested vars.tf for known issues, found 0 issues', - ); + expect(stdout).toContain('Tested vars.tf for known issues, found'); expect(stdout).toContain( `Tested ${pathLib.join( 'var_deref', 'sg_open_ssh.tf', - )} for known issues, found 0 issues`, + )} for known issues, found`, ); expect(stdout).toContain( `Tested ${pathLib.join( 'var_deref', 'variables.tf', - )} for known issues, found 0 issues`, + )} for known issues, found`, ); expect(stdout).toContain( `Tested ${pathLib.join( 'var_deref', 'nested_var_deref', 'sg_open_ssh.tf', - )} for known issues, found 0 issues`, + )} for known issues, found`, ); expect(stdout).toContain( `Tested ${pathLib.join( 'var_deref', 'nested_var_deref', 'variables.tf', - )} for known issues, found 0 issues`, + )} for known issues, found`, ); }); diff --git a/test/jest/acceptance/iac/test-directory.spec.ts b/test/jest/acceptance/iac/test-directory.spec.ts index 1c4ed09f56..9aa69ce709 100644 --- a/test/jest/acceptance/iac/test-directory.spec.ts +++ b/test/jest/acceptance/iac/test-directory.spec.ts @@ -49,7 +49,7 @@ describe('Directory scan', () => { expect(stdout).toContain('Failed to parse YAML file'); expect(stdout).toContain('Failed to parse JSON file'); expect(stdout).toContain( - '28 projects, 20 contained issues. Failed to test 5 projects.', + '28 projects, 22 contained issues. Failed to test 5 projects.', ); expect(exitCode).toBe(1); }); diff --git a/test/jest/acceptance/iac/test-terraform-var-deref.spec.ts b/test/jest/acceptance/iac/test-terraform-var-deref.spec.ts deleted file mode 100644 index 704eade43e..0000000000 --- a/test/jest/acceptance/iac/test-terraform-var-deref.spec.ts +++ /dev/null @@ -1,416 +0,0 @@ -import { isValidJSONString, startMockServer } from './helpers'; -import * as path from 'path'; -import { FakeServer } from '../../../acceptance/fake-server'; - -jest.setTimeout(50000); - -describe('Terraform Language Support', () => { - let server: FakeServer; - let run: ( - cmd: string, - ) => Promise<{ stdout: string; stderr: string; exitCode: number }>; - let teardown: () => void; - - beforeAll(async () => { - ({ server, run, teardown } = await startMockServer()); - - server.setFeatureFlag('iacTerraformVarSupport', true); - }); - - afterAll(async () => teardown()); - - describe('without feature flag', () => { - beforeAll(() => { - server.setFeatureFlag('iacTerraformVarSupport', false); - }); - - afterAll(() => { - server.setFeatureFlag('iacTerraformVarSupport', true); - }); - - it('does not dereference variables', async () => { - const { stdout, exitCode } = await run( - `snyk iac test ./iac/terraform/var_deref`, - ); - - 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'); - - expect(stdout).toContain( - `Testing ${path.join('nested_var_deref', 'sg_open_ssh.tf')}...`, - ); - expect(stdout.match(/✗ Rule allows open egress/g)).toBeNull(); - expect(stdout).toContain( - `Tested ${path.join( - 'nested_var_deref', - '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 () => { - const { exitCode } = await run( - `snyk iac test ./iac/terraform/empty_file.tf`, - ); - - expect(exitCode).toBe(3); - }); - - it('skips non-supported IaC files in a directory', async () => { - const { exitCode, stdout } = await run( - `snyk iac test ./iac/terraform/var_deref`, - ); - - expect(stdout).not.toContain(`a.auto.tfvars`); - expect(stdout).not.toContain(`Unsupported file extension`); - expect(exitCode).toBe(0); - }); - }); - - describe('with feature flag', () => { - describe('single files', () => { - // TODO: can be merged with the existing test-terraform.spec.ts when the flag is removed - it('finds issues in Terraform file', async () => { - const { stdout, exitCode } = await run( - `snyk iac test iac/terraform/var_deref/sg_open_ssh.tf`, - ); - - expect(stdout).toContain( - 'Testing iac/terraform/var_deref/sg_open_ssh.tf...', - ); - expect( - stdout.match(/✗ Security Group allows open ingress/g), - ).toHaveLength(1); - expect(stdout).toContain( - 'Tested iac/terraform/var_deref/sg_open_ssh.tf for known issues', - ); - expect(exitCode).toBe(1); - }); - - it('returns error empty Terraform file', async () => { - const { exitCode } = await run( - `snyk iac test ./iac/terraform/empty_file.tf`, - ); - expect(exitCode).toBe(3); - }); - }); - describe('single non-terraform files', () => { - it('finds issues in a Terraform plan file', async () => { - const { stdout, exitCode } = await run( - `snyk iac test ./iac/terraform-plan/tf-plan-create.json`, - ); - expect(stdout).toContain( - 'Testing ./iac/terraform-plan/tf-plan-create.json', - ); - expect(stdout).toContain('Infrastructure as code issues:'); - expect(stdout).toContain('✗ S3 bucket versioning disabled'); - expect(stdout).toContain( - 'resource > aws_s3_bucket[terra_ci] > versioning > enabled', - ); - expect(exitCode).toBe(1); - }); - it('finds issues in CloudFormation YAML file', async () => { - const { stdout, exitCode } = await run( - `snyk iac test ./iac/cloudformation/aurora-valid.yml`, - ); - expect(stdout).toContain( - 'Testing ./iac/cloudformation/aurora-valid.yml', - ); - expect(stdout).toContain('Infrastructure as code issues:'); - expect(stdout).toContain( - '✗ SNS topic is not encrypted with customer managed key', - ); - expect(stdout).toContain( - '[DocId: 0] > Resources[DatabaseAlarmTopic] > Properties > KmsMasterKeyId', - ); - expect(exitCode).toBe(1); - }); - - it('finds issues in CloudFormation JSON file', async () => { - const { stdout, exitCode } = await run( - `snyk iac test ./iac/cloudformation/fargate-valid.json`, - ); - expect(stdout).toContain( - 'Testing ./iac/cloudformation/fargate-valid.json', - ); - expect(stdout).toContain('Infrastructure as code issues:'); - expect(stdout).toContain( - '✗ S3 restrict public bucket control is disabled', - ); - expect(stdout).toContain( - 'Resources[CodePipelineArtifactBucket] > Properties > PublicAccessBlockConfiguration > RestrictPublicBuckets', - ); - expect(exitCode).toBe(1); - }); - - it('finds issues in ARM JSON file', async () => { - const { stdout, exitCode } = await run( - `snyk iac test ./iac/arm/rule_test.json`, - ); - expect(stdout).toContain('Testing ./iac/arm/rule_test.json'); - expect(stdout).toContain('Infrastructure as code issues:'); - expect(stdout).toContain( - '✗ Azure Firewall Network Rule Collection allows public access', - ); - expect(stdout).toContain( - 'resources[1] > properties > networkRuleCollections[0] > properties > rules[0] > sourceAddresses', - ); - expect(exitCode).toBe(1); - }); - it('finds issues in Kubernetes YAML file', async () => { - const { stdout, exitCode } = await run( - `snyk iac test ./iac/kubernetes/pod-privileged.yaml`, - ); - expect(stdout).toContain( - 'Testing ./iac/kubernetes/pod-privileged.yaml', - ); - expect(stdout).toContain('Infrastructure as code issues:'); - expect(stdout).toContain('✗ Privileged container'); - expect(stdout).toContain( - '[DocId: 0] > input > spec > containers[example] > securityContext > privileged', - ); - expect(exitCode).toBe(1); - }); - - it('finds issues in Kubernetes YAML multi file', async () => { - const { stdout, exitCode } = await run( - `snyk iac test ./iac/kubernetes/pod-privileged-multi.yaml`, - ); - expect(stdout).toContain( - 'Testing ./iac/kubernetes/pod-privileged-multi.yaml', - ); - expect(stdout).toContain('Infrastructure as code issues:'); - expect(stdout).toContain('✗ Privileged container'); - expect(stdout).toContain( - '[DocId: 0] > input > spec > containers[example] > securityContext > privileged', - ); - expect(exitCode).toBe(1); - }); - }); - }); - - describe('directories', () => { - it('dereferences variables in nested directories', async () => { - const { stdout, exitCode } = await run( - `snyk iac test ./iac/terraform/var_deref`, - ); - - expect(stdout).toContain('Testing sg_open_ssh.tf...'); - expect( - stdout.match(/✗ Security Group allows open ingress/g), - ).toHaveLength(5); - expect(stdout).toContain('Tested sg_open_ssh.tf for known issues'); - - expect(stdout).toContain( - `Testing ${path.join('nested_var_deref', 'sg_open_ssh.tf')}...`, - ); - expect(stdout.match(/✗ Rule allows open egress/g)).toHaveLength(1); - expect(stdout).toContain( - `Tested ${path.join( - 'nested_var_deref', - 'sg_open_ssh.tf', - )} for known issues`, - ); - expect(exitCode).toBe(1); - }); - - it('scans a mix of IaC files in nested directories', async () => { - const { stdout, exitCode } = await run(`snyk iac test ./iac`); - - expect(stdout).toContain( - `Testing ${path.join('kubernetes', 'pod-privileged.yaml')}`, - ); - expect(stdout).toContain( - `Tested ${path.join( - 'kubernetes', - 'pod-privileged.yaml', - )} for known issues`, - ); - - expect(stdout).toContain( - `Testing ${path.join('terraform', 'var_deref', 'sg_open_ssh.tf')}`, - ); - expect( - stdout.match(/✗ Security Group allows open ingress/g), - ).toHaveLength(9); - expect(stdout).toContain( - `Tested ${path.join( - 'terraform', - 'var_deref', - 'sg_open_ssh.tf', - )} for known issues`, - ); - - expect(stdout).toContain( - `Testing ${path.join( - 'terraform', - 'var_deref', - 'nested_var_deref', - 'sg_open_ssh.tf', - )}...`, - ); - expect(stdout.match(/✗ Rule allows open egress/g)).toHaveLength(1); - expect(stdout).toContain( - `Tested ${path.join( - 'terraform', - 'var_deref', - 'nested_var_deref', - 'sg_open_ssh.tf', - )} for known issues`, - ); - expect(exitCode).toBe(1); - }); - }); - - 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( - `snyk iac test iac/terraform/sg_open_ssh.tf`, - ); - - expect(stdout).toContain('Testing iac/terraform/sg_open_ssh.tf...'); - expect( - stdout.match(/✗ Security Group allows open ingress/g), - ).toHaveLength(1); - expect(stdout).toContain( - 'Tested iac/terraform/sg_open_ssh.tf for known issues', - ); - expect(exitCode).toBe(1); - }); - - it('filters out issues when using severity threshold', async () => { - const { stdout, exitCode } = await run( - `snyk iac test iac/terraform/sg_open_ssh.tf --severity-threshold=high`, - ); - - expect(stdout).toContain('Testing iac/terraform/sg_open_ssh.tf...'); - expect(stdout.match(/✗ Security Group allows open ingress/g)).toBeNull(); - expect(stdout).toContain( - 'Tested iac/terraform/sg_open_ssh.tf for known issues', - ); - // expect exitCode to be 0 or 1 - expect(exitCode).toBeLessThanOrEqual(1); - }); - - it('filters out issues when using detection depth', async () => { - const { stdout, exitCode } = await run( - `snyk iac test ./iac/terraform/ --detection-depth=1`, - ); - - expect(stdout).toContain( - `Testing ${path.join('var_deref', 'sg_open_ssh.tf')}`, - ); - expect(stdout).toContain( - `Tested ${path.join('var_deref', 'sg_open_ssh.tf')} for known issues`, - ); - expect(stdout).toContain(`Testing ${path.join('sg_open_ssh.tf')}`); - expect(stdout).toContain('Tested sg_open_ssh.tf for known issues'); - expect( - stdout.match(/✗ Security Group allows open ingress/g), - ).toHaveLength(6); - - // Check that we didn't scan directories with depth > 2 - expect(stdout).not.toContain( - `Testing ${path.join( - 'var_deref', - 'nested_var_deref', - 'sg_open_ssh.tf', - )}...`, - ); - expect(stdout.match(/✗ Rule allows open egress/g)).toBeNull(); - expect(stdout).not.toContain( - `Tested ${path.join( - 'var_deref', - 'nested_var_deref', - 'sg_open_ssh.tf', - )} for known issues`, - ); - expect(exitCode).toBe(1); - }); - - it('outputs an error for files with invalid HCL2', async () => { - const { stdout, exitCode } = await run( - `snyk iac test ./iac/terraform/sg_open_ssh_invalid_hcl2.tf`, - ); - - expect(stdout).toContain('We were unable to parse the Terraform file'); - expect(exitCode).toBe(2); - }); - - it('outputs the expected text when running with --sarif flag', async () => { - const { stdout, exitCode } = await run( - `snyk iac test ./iac/terraform/sg_open_ssh.tf --sarif`, - ); - - expect(isValidJSONString(stdout)).toBe(true); - expect(stdout).toContain('"id": "SNYK-CC-TF-1",'); - expect(stdout).toContain('"ruleId": "SNYK-CC-TF-1",'); - expect(exitCode).toBe(1); - }); - - it('outputs the expected text when running with --json flag', async () => { - const { stdout, exitCode } = await run( - `snyk iac test ./iac/terraform/sg_open_ssh.tf --json`, - ); - - expect(isValidJSONString(stdout)).toBe(true); - expect(stdout).toContain('"id": "SNYK-CC-TF-1",'); - expect(stdout).toContain('"packageManager": "terraformconfig",'); - expect(stdout).toContain('"projectType": "terraformconfig",'); - expect(exitCode).toBe(1); - }); - }); -}); diff --git a/test/jest/acceptance/iac/test-terraform.spec.ts b/test/jest/acceptance/iac/test-terraform.spec.ts index f11504f105..b452ecc56e 100644 --- a/test/jest/acceptance/iac/test-terraform.spec.ts +++ b/test/jest/acceptance/iac/test-terraform.spec.ts @@ -1,8 +1,9 @@ -import { startMockServer, isValidJSONString } from './helpers'; +import { isValidJSONString, startMockServer } from './helpers'; +import * as path from 'path'; jest.setTimeout(50000); -describe('Terraform single file scan', () => { +describe('Terraform', () => { let run: ( cmd: string, ) => Promise<{ stdout: string; stderr: string; exitCode: number }>; @@ -16,57 +17,214 @@ describe('Terraform single file scan', () => { afterAll(async () => teardown()); - it('finds issues in Terraform file', async () => { - const { stdout, exitCode } = await run( - `snyk iac test ./iac/terraform/sg_open_ssh.tf`, - ); - expect(stdout).toContain('Testing ./iac/terraform/sg_open_ssh.tf'); - expect(stdout).toContain('Infrastructure as code issues:'); - expect(stdout).toContain('✗ Security Group allows open ingress'); - expect(stdout).toContain( - ' input > resource > aws_security_group[allow_ssh] > ingress', - ); - expect(exitCode).toBe(1); - }); + describe('Terraform single file scans', () => { + it('finds issues in Terraform file', async () => { + const { stdout, exitCode } = await run( + `snyk iac test ./iac/terraform/sg_open_ssh.tf`, + ); + expect(stdout).toContain('Testing ./iac/terraform/sg_open_ssh.tf'); + expect(stdout).toContain('Infrastructure as code issues:'); + expect(stdout).toContain('✗ Security Group allows open ingress'); + expect(stdout).toContain( + ' input > resource > aws_security_group[allow_ssh] > ingress', + ); + expect(exitCode).toBe(1); + }); - it('filters out issues when using severity threshold', async () => { - const { stdout, exitCode } = await run( - `snyk iac test ./iac/terraform/sg_open_ssh.tf --severity-threshold=high`, - ); + it('filters out issues when using severity threshold', async () => { + const { stdout, exitCode } = await run( + `snyk iac test ./iac/terraform/sg_open_ssh.tf --severity-threshold=high`, + ); - expect(stdout).toContain('Infrastructure as code issues:'); - expect(stdout).toContain( - 'Tested ./iac/terraform/sg_open_ssh.tf for known issues, found 0 issues', - ); - expect(exitCode).toBe(0); - }); + expect(stdout).toContain('Infrastructure as code issues:'); + expect(stdout).toContain( + 'Tested ./iac/terraform/sg_open_ssh.tf for known issues, found 0 issues', + ); + expect(exitCode).toBe(0); + }); + + it('outputs an error for files with invalid HCL2', async () => { + const { stdout, exitCode } = await run( + `snyk iac test ./iac/terraform/sg_open_ssh_invalid_hcl2.tf`, + ); + expect(stdout).toContain('We were unable to parse the Terraform file'); + expect(exitCode).toBe(2); + }); - it('outputs an error for files with invalid HCL2', async () => { - const { stdout, exitCode } = await run( - `snyk iac test ./iac/terraform/sg_open_ssh_invalid_hcl2.tf`, - ); - expect(stdout).toContain('We were unable to parse the Terraform file'); - expect(exitCode).toBe(2); + it('outputs the expected text when running with --sarif flag', async () => { + const { stdout, exitCode } = await run( + `snyk iac test ./iac/terraform/sg_open_ssh.tf --sarif`, + ); + expect(isValidJSONString(stdout)).toBe(true); + expect(stdout).toContain('"id": "SNYK-CC-TF-1",'); + expect(stdout).toContain('"ruleId": "SNYK-CC-TF-1",'); + expect(exitCode).toBe(1); + }); + + it('outputs the expected text when running with --json flag', async () => { + const { stdout, exitCode } = await run( + `snyk iac test ./iac/terraform/sg_open_ssh.tf --json`, + ); + expect(isValidJSONString(stdout)).toBe(true); + expect(stdout).toContain('"id": "SNYK-CC-TF-1",'); + expect(stdout).toContain('"packageManager": "terraformconfig",'); + expect(stdout).toContain('"projectType": "terraformconfig",'); + expect(exitCode).toBe(1); + }); + it('returns error empty Terraform file', async () => { + const { exitCode } = await run( + `snyk iac test ./iac/terraform/empty_file.tf`, + ); + expect(exitCode).toBe(3); + }); }); - it('outputs the expected text when running with --sarif flag', async () => { - const { stdout, exitCode } = await run( - `snyk iac test ./iac/terraform/sg_open_ssh.tf --sarif`, - ); - expect(isValidJSONString(stdout)).toBe(true); - expect(stdout).toContain('"id": "SNYK-CC-TF-1",'); - expect(stdout).toContain('"ruleId": "SNYK-CC-TF-1",'); - expect(exitCode).toBe(1); + describe('Terraform directories', () => { + it('dereferences variables in nested directories', async () => { + const { stdout, exitCode } = await run( + `snyk iac test ./iac/terraform/var_deref`, + ); + + expect(stdout).toContain('Testing sg_open_ssh.tf...'); + expect( + stdout.match(/✗ Security Group allows open ingress/g), + ).toHaveLength(5); + expect(stdout).toContain('Tested sg_open_ssh.tf for known issues'); + + expect(stdout).toContain( + `Testing ${path.join('nested_var_deref', 'sg_open_ssh.tf')}...`, + ); + expect(stdout.match(/✗ Rule allows open egress/g)).toHaveLength(1); + expect(stdout).toContain( + `Tested ${path.join( + 'nested_var_deref', + 'sg_open_ssh.tf', + )} for known issues`, + ); + expect(exitCode).toBe(1); + }); + + it('scans a mix of IaC files in nested directories', async () => { + const { stdout, exitCode } = await run(`snyk iac test ./iac`); + + expect(stdout).toContain( + `Testing ${path.join('kubernetes', 'pod-privileged.yaml')}`, + ); + expect(stdout).toContain( + `Tested ${path.join( + 'kubernetes', + 'pod-privileged.yaml', + )} for known issues`, + ); + + expect(stdout).toContain( + `Testing ${path.join('terraform', 'var_deref', 'sg_open_ssh.tf')}`, + ); + expect( + stdout.match(/✗ Security Group allows open ingress/g), + ).toHaveLength(9); + expect(stdout).toContain( + `Tested ${path.join( + 'terraform', + 'var_deref', + 'sg_open_ssh.tf', + )} for known issues`, + ); + + expect(stdout).toContain( + `Testing ${path.join( + 'terraform', + 'var_deref', + 'nested_var_deref', + 'sg_open_ssh.tf', + )}...`, + ); + expect(stdout.match(/✗ Rule allows open egress/g)).toHaveLength(1); + expect(stdout).toContain( + `Tested ${path.join( + 'terraform', + 'var_deref', + 'nested_var_deref', + 'sg_open_ssh.tf', + )} for known issues`, + ); + expect(exitCode).toBe(1); + }); + it('filters out issues when using detection depth', async () => { + const { stdout, exitCode } = await run( + `snyk iac test ./iac/terraform/ --detection-depth=1`, + ); + + expect(stdout).toContain( + `Testing ${path.join('var_deref', 'sg_open_ssh.tf')}`, + ); + expect(stdout).toContain( + `Tested ${path.join('var_deref', 'sg_open_ssh.tf')} for known issues`, + ); + expect(stdout).toContain(`Testing ${path.join('sg_open_ssh.tf')}`); + expect(stdout).toContain('Tested sg_open_ssh.tf for known issues'); + expect( + stdout.match(/✗ Security Group allows open ingress/g), + ).toHaveLength(6); + + // Check that we didn't scan directories with depth > 2 + expect(stdout).not.toContain( + `Testing ${path.join( + 'var_deref', + 'nested_var_deref', + 'sg_open_ssh.tf', + )}...`, + ); + expect(stdout.match(/✗ Rule allows open egress/g)).toBeNull(); + expect(stdout).not.toContain( + `Tested ${path.join( + 'var_deref', + 'nested_var_deref', + 'sg_open_ssh.tf', + )} for known issues`, + ); + expect(exitCode).toBe(1); + }); }); - it('outputs the expected text when running with --json flag', async () => { - const { stdout, exitCode } = await run( - `snyk iac test ./iac/terraform/sg_open_ssh.tf --json`, - ); - expect(isValidJSONString(stdout)).toBe(true); - expect(stdout).toContain('"id": "SNYK-CC-TF-1",'); - expect(stdout).toContain('"packageManager": "terraformconfig",'); - expect(stdout).toContain('"projectType": "terraformconfig",'); - expect(exitCode).toBe(1); + 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 ./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 ./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 ./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); + }); }); }); diff --git a/test/jest/unit/iac/file-parser.spec.ts b/test/jest/unit/iac/file-parser.spec.ts index d6c3d72f7d..2113e89334 100644 --- a/test/jest/unit/iac/file-parser.spec.ts +++ b/test/jest/unit/iac/file-parser.spec.ts @@ -61,18 +61,18 @@ describe('parseFiles', () => { expect(parsedFiles.length).toEqual(9); expect(parsedFiles[0]).toEqual(expectedKubernetesYamlParsingResult); expect(parsedFiles[1]).toEqual(expectedKubernetesJsonParsingResult); - expect(parsedFiles[2]).toEqual(expectedTerraformParsingResult); - expect(parsedFiles[3]).toEqual(expectedTerraformJsonParsingResult); - expect(parsedFiles[4]).toEqual( + expect(parsedFiles[2]).toEqual(expectedTerraformJsonParsingResult); + expect(parsedFiles[3]).toEqual( expectedMultipleKubernetesYamlsParsingResult, ); - expect(parsedFiles[5]).toEqual({ + expect(parsedFiles[4]).toEqual({ ...expectedMultipleKubernetesYamlsParsingResult, docId: 2, // doc, the 2nd doc, is an empty doc, which is ignored. There is also an ignored docId:3. }); - expect(parsedFiles[6]).toEqual(expectedCloudFormationYAMLParsingResult); - expect(parsedFiles[7]).toEqual(expectedCloudFormationJSONParsingResult); - expect(parsedFiles[8]).toEqual(expectedArmParsingResult); + expect(parsedFiles[5]).toEqual(expectedCloudFormationYAMLParsingResult); + expect(parsedFiles[6]).toEqual(expectedCloudFormationJSONParsingResult); + expect(parsedFiles[7]).toEqual(expectedArmParsingResult); + expect(parsedFiles[8]).toEqual(expectedTerraformParsingResult); expect(failedFiles.length).toEqual(0); }); @@ -89,7 +89,7 @@ describe('parseFiles', () => { expect(failedFiles.length).toEqual(0); }); - it('does not include the unsupported file types in failed files list', async () => { + it('includes unsupported file types in failed files', async () => { const { parsedFiles, failedFiles } = await parseFiles([ { fileContent: 'file.java', @@ -98,7 +98,7 @@ describe('parseFiles', () => { }, ]); expect(parsedFiles.length).toEqual(0); - expect(failedFiles.length).toEqual(0); + expect(failedFiles.length).toEqual(1); }); it('throws an error for invalid JSON file types', async () => { diff --git a/test/jest/unit/iac/index.spec.ts b/test/jest/unit/iac/index.spec.ts index 01d63adfb6..56af1be690 100644 --- a/test/jest/unit/iac/index.spec.ts +++ b/test/jest/unit/iac/index.spec.ts @@ -1,9 +1,5 @@ jest.mock('../../../../src/cli/commands/test/iac/local-execution/local-cache'); jest.mock('../../../../src/cli/commands/test/iac/local-execution/file-loader'); -const isFeatureFlagSupportedForOrgStub = jest.fn(); -jest.mock('../../../../src/lib/feature-flags', () => ({ - isFeatureFlagSupportedForOrg: isFeatureFlagSupportedForOrgStub, -})); const parseFilesStub = jest.fn(); jest.mock( '../../../../src/cli/commands/test/iac/local-execution/file-parser', @@ -50,12 +46,13 @@ jest.mock( import { test } from '../../../../src/cli/commands/test/iac/local-execution/'; import { + EngineType, IacFileParsed, IaCTestFlags, RulesOrigin, } from '../../../../src/cli/commands/test/iac/local-execution/types'; import { IacProjectType } from '../../../../src/lib/iac/constants'; -import { EngineType } from '../../../../src/cli/commands/test/iac/local-execution/types'; + const parsedFiles: IacFileParsed[] = [ { engineType: EngineType.Terraform, @@ -79,10 +76,7 @@ const failedFiles: IacFileParsed[] = [ ]; describe('test()', () => { - describe.each([ - [{ iacTerraformVarSupport: false }], - [{ iacTerraformVarSupport: true }], - ])('With TF language support feature flag set to %p', (featureFlags) => { + describe('parsing', () => { const iacOrgSettings = { meta: { isPrivate: false, @@ -99,9 +93,6 @@ describe('test()', () => { }; beforeAll(() => { - isFeatureFlagSupportedForOrgStub.mockImplementation((flag) => - Promise.resolve({ ok: featureFlags[flag] ?? true }), - ); getAllDirectoriesForPathStub.mockImplementation(() => ['./storage']); parseFilesStub.mockImplementation(() => ({ parsedFiles,