diff --git a/help/commands-docs/iac.md b/help/commands-docs/iac.md index aefbc75db3a..7a05a26387a 100644 --- a/help/commands-docs/iac.md +++ b/help/commands-docs/iac.md @@ -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=`: + 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) \ No newline at end of file diff --git a/src/cli/commands/test/iac-local-execution/assert-iac-options-flag.ts b/src/cli/commands/test/iac-local-execution/assert-iac-options-flag.ts index f4dc1dd55a1..3783fa67adc 100644 --- a/src/cli/commands/test/iac-local-execution/assert-iac-options-flag.ts +++ b/src/cli/commands/test/iac-local-execution/assert-iac-options-flag.ts @@ -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', @@ -18,6 +18,7 @@ const keys: (keyof IaCTestFlags)[] = [ 'help', 'q', 'quiet', + 'scan', ]; const allowed = new Set(keys); @@ -25,17 +26,34 @@ 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 @@ -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); + } } 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 81e3f233a67..fff05b3863a 100644 --- a/src/cli/commands/test/iac-local-execution/file-parser.ts +++ b/src/cli/commands/test/iac-local-execution/file-parser.ts @@ -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 { 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; @@ -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': @@ -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); diff --git a/src/cli/commands/test/iac-local-execution/index.ts b/src/cli/commands/test/iac-local-execution/index.ts index 08f46a0765e..d7743f4780f 100644 --- a/src/cli/commands/test/iac-local-execution/index.ts +++ b/src/cli/commands/test/iac-local-execution/index.ts @@ -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( diff --git a/src/cli/commands/test/iac-local-execution/types.ts b/src/cli/commands/test/iac-local-execution/types.ts index f9869543346..f460d9c22b2 100644 --- a/src/cli/commands/test/iac-local-execution/types.ts +++ b/src/cli/commands/test/iac-local-execution/types.ts @@ -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 @@ -233,4 +243,5 @@ export enum IaCErrorCodes { // assert-iac-options-flag FlagError = 1090, + FlagValueError = 1091, } diff --git a/test/jest/unit/iac-unit-tests/assert-iac-options-flag.spec.ts b/test/jest/unit/iac-unit-tests/assert-iac-options-flag.spec.ts index 632187e9658..43382c435fe 100644 --- a/test/jest/unit/iac-unit-tests/assert-iac-options-flag.spec.ts +++ b/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']; @@ -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); + }, + ); + }); }); diff --git a/test/smoke/spec/iac/snyk_test_local_exec_spec.sh b/test/smoke/spec/iac/snyk_test_local_exec_spec.sh index 9f028d33157..7929bf52a74 100644 --- a/test/smoke/spec/iac/snyk_test_local_exec_spec.sh +++ b/test/smoke/spec/iac/snyk_test_local_exec_spec.sh @@ -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" @@ -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