diff --git a/src/cli/commands/test/iac-local-execution/parsers/terraform-plan-parser.ts b/src/cli/commands/test/iac-local-execution/parsers/terraform-plan-parser.ts index 30cc4cc355..096d7fe485 100644 --- a/src/cli/commands/test/iac-local-execution/parsers/terraform-plan-parser.ts +++ b/src/cli/commands/test/iac-local-execution/parsers/terraform-plan-parser.ts @@ -10,6 +10,8 @@ import { TerraformScanInput, TerraformPlanResourceChange, IaCErrorCodes, + TerraformPlanReferencedResource, + TerraformPlanExpression, } from '../types'; import { CustomError } from '../../../../../lib/errors'; import { getErrorStringCode } from '../error-utils'; @@ -34,6 +36,27 @@ function terraformPlanReducer( return scanInput; } +function getExpressions( + expressions: Record, +): Record { + const result: Record = {}; + // expressions can be nested. we are only doing 1 depth to resolve top level depenencies + for (const key of Object.keys(expressions)) { + const referenceKey = getReference(expressions[key]); + if (referenceKey) { + result[key] = referenceKey; + } + } + return result; +} + +// this is very naive implementation +// the referenences can be composed of number of keys +// we only going to use the first reference for time being +function getReference(value: TerraformPlanExpression): string { + return value.references?.[0]; +} + function getResourceName(index: string | number, name: string): string { return index !== undefined ? `${name}["${index}"]` : name; } @@ -70,24 +93,67 @@ function isValidResourceActions( }); } +function referencedResourcesResolver( + scanInput: TerraformScanInput, + resources: TerraformPlanReferencedResource[], +): TerraformScanInput { + // check root module for references in first depth of attributes + for (const resource of resources) { + const { type, name, mode, index, expressions } = resource; + + // don't care about references in data sources for time being + if (mode == 'data') { + continue; + } + const inputKey: keyof TerraformScanInput = 'resource'; + + // only update the references in resources that have some resolved attributes already + const resolvedResource: any = + scanInput[inputKey]?.[type]?.[getResourceName(index, name)]; + if (resolvedResource) { + const resourceExpressions = getExpressions(expressions); + for (const key of Object.keys(resourceExpressions)) { + // only add non existing attributes. If we already have resolved value do not overwrite it with reference + if (!resolvedResource[key]) { + resolvedResource[key] = resourceExpressions[key]; + } + } + scanInput[inputKey][type][ + getResourceName(index, name) + ] = resolvedResource; + } + } + + return scanInput; +} + function extractResourceChanges( terraformPlanJson: TerraformPlanJson, ): Array { return terraformPlanJson.resource_changes || []; } +function extractReferencedResources( + terraformPlanJson: TerraformPlanJson, +): Array { + return terraformPlanJson.configuration?.root_module?.resources || []; +} + function extractResourcesForScan( terraformPlanJson: TerraformPlanJson, isFullScan = false, ): TerraformScanInput { const resourceChanges = extractResourceChanges(terraformPlanJson); - return resourceChanges.reduce( + const scanInput = resourceChanges.reduce( (memo, curr) => resourceChangeReducer(memo, curr, isFullScan), { resource: {}, data: {}, }, ); + + const referencedResources = extractReferencedResources(terraformPlanJson); + return referencedResourcesResolver(scanInput, referencedResources); } export function isTerraformPlan(terraformPlanJson: TerraformPlanJson): boolean { diff --git a/src/cli/commands/test/iac-local-execution/types.ts b/src/cli/commands/test/iac-local-execution/types.ts index 5a6e2a581a..aa42a93667 100644 --- a/src/cli/commands/test/iac-local-execution/types.ts +++ b/src/cli/commands/test/iac-local-execution/types.ts @@ -225,7 +225,21 @@ export interface TerraformPlanResourceChange export interface TerraformPlanJson { // there are more values, but these are the required ones for us to scan resource_changes: Array; + configuration: { + root_module: { + resources: Array; + }; + }; +} + +export interface TerraformPlanReferencedResource extends TerraformPlanResource { + expressions: Record; } + +export interface TerraformPlanExpression { + references: Array; +} + export interface TerraformScanInput { // within the resource field, resources are stored: [type] => [name] => [values] resource: Record>; diff --git a/test/fixtures/iac/terraform-plan/expected-parser-results/delta-scan/tf-plan-create.resources.json b/test/fixtures/iac/terraform-plan/expected-parser-results/delta-scan/tf-plan-create.resources.json index 835ded969e..8ebf7cf9b2 100644 --- a/test/fixtures/iac/terraform-plan/expected-parser-results/delta-scan/tf-plan-create.resources.json +++ b/test/fixtures/iac/terraform-plan/expected-parser-results/delta-scan/tf-plan-create.resources.json @@ -53,6 +53,7 @@ "queued_timeout": 480, "secondary_artifacts": [], "secondary_sources": [], + "service_role": "aws_iam_role.terra_ci_job", "source": [ { "auth": [], @@ -97,10 +98,12 @@ "aws_iam_role_policy": { "terra_ci_job": { "name_prefix": null, + "policy": "data.aws_caller_identity.current", "role": "terra_ci_job" }, "terra_ci_runner": { "name_prefix": null, + "policy": "aws_codebuild_project.terra_ci", "role": "terra_ci_runner" } }, @@ -146,6 +149,7 @@ "terra_ci_runner": { "definition": "{\n \"Comment\": \"Run Terragrunt Jobs\",\n \"StartAt\": \"OnBranch?\",\n \"States\": {\n \"OnBranch?\": {\n \"Type\": \"Choice\",\n \"Choices\": [\n {\n \"Variable\": \"$.build.sourceversion\",\n \"IsPresent\": true,\n \"Next\": \"PlanBranch\"\n }\n ],\n \"Default\": \"Plan\"\n },\n \"Plan\": {\n \"Type\": \"Task\",\n \"Resource\": \"arn:aws:states:::codebuild:startBuild.sync\",\n \"Parameters\": {\n \"ProjectName\": \"terra-ci-runner\",\n \"EnvironmentVariablesOverride\": [\n {\n \"Name\": \"TERRA_CI_BUILD_NAME\",\n \"Value.$\": \"$$.Execution.Name\"\n },\n {\n \"Name\": \"TERRA_CI_RESOURCE\",\n \"Value.$\": \"$.build.environment.terra_ci_resource\"\n }\n ]\n },\n \"End\": true\n },\n \"PlanBranch\": {\n \"Type\": \"Task\",\n \"Resource\": \"arn:aws:states:::codebuild:startBuild.sync\",\n \"Parameters\": {\n \"ProjectName\": \"terra-ci-runner\",\n \"SourceVersion.$\": \"$.build.sourceversion\",\n \"EnvironmentVariablesOverride\": [\n {\n \"Name\": \"TERRA_CI_RESOURCE\",\n \"Value.$\": \"$.build.environment.terra_ci_resource\"\n }\n ]\n },\n \"End\": true\n }\n }\n}\n", "name": "terra-ci-runner", + "role_arn": "aws_iam_role.terra_ci_runner", "tags": null, "type": "STANDARD" } diff --git a/test/fixtures/iac/terraform-plan/expected-parser-results/full-scan/tf-plan-create.resources.json b/test/fixtures/iac/terraform-plan/expected-parser-results/full-scan/tf-plan-create.resources.json index 835ded969e..8ebf7cf9b2 100644 --- a/test/fixtures/iac/terraform-plan/expected-parser-results/full-scan/tf-plan-create.resources.json +++ b/test/fixtures/iac/terraform-plan/expected-parser-results/full-scan/tf-plan-create.resources.json @@ -53,6 +53,7 @@ "queued_timeout": 480, "secondary_artifacts": [], "secondary_sources": [], + "service_role": "aws_iam_role.terra_ci_job", "source": [ { "auth": [], @@ -97,10 +98,12 @@ "aws_iam_role_policy": { "terra_ci_job": { "name_prefix": null, + "policy": "data.aws_caller_identity.current", "role": "terra_ci_job" }, "terra_ci_runner": { "name_prefix": null, + "policy": "aws_codebuild_project.terra_ci", "role": "terra_ci_runner" } }, @@ -146,6 +149,7 @@ "terra_ci_runner": { "definition": "{\n \"Comment\": \"Run Terragrunt Jobs\",\n \"StartAt\": \"OnBranch?\",\n \"States\": {\n \"OnBranch?\": {\n \"Type\": \"Choice\",\n \"Choices\": [\n {\n \"Variable\": \"$.build.sourceversion\",\n \"IsPresent\": true,\n \"Next\": \"PlanBranch\"\n }\n ],\n \"Default\": \"Plan\"\n },\n \"Plan\": {\n \"Type\": \"Task\",\n \"Resource\": \"arn:aws:states:::codebuild:startBuild.sync\",\n \"Parameters\": {\n \"ProjectName\": \"terra-ci-runner\",\n \"EnvironmentVariablesOverride\": [\n {\n \"Name\": \"TERRA_CI_BUILD_NAME\",\n \"Value.$\": \"$$.Execution.Name\"\n },\n {\n \"Name\": \"TERRA_CI_RESOURCE\",\n \"Value.$\": \"$.build.environment.terra_ci_resource\"\n }\n ]\n },\n \"End\": true\n },\n \"PlanBranch\": {\n \"Type\": \"Task\",\n \"Resource\": \"arn:aws:states:::codebuild:startBuild.sync\",\n \"Parameters\": {\n \"ProjectName\": \"terra-ci-runner\",\n \"SourceVersion.$\": \"$.build.sourceversion\",\n \"EnvironmentVariablesOverride\": [\n {\n \"Name\": \"TERRA_CI_RESOURCE\",\n \"Value.$\": \"$.build.environment.terra_ci_resource\"\n }\n ]\n },\n \"End\": true\n }\n }\n}\n", "name": "terra-ci-runner", + "role_arn": "aws_iam_role.terra_ci_runner", "tags": null, "type": "STANDARD" } diff --git a/test/fixtures/iac/terraform-plan/expected-parser-results/full-scan/tf-plan-v4.resources.json b/test/fixtures/iac/terraform-plan/expected-parser-results/full-scan/tf-plan-v4.resources.json new file mode 100644 index 0000000000..67e21f830a --- /dev/null +++ b/test/fixtures/iac/terraform-plan/expected-parser-results/full-scan/tf-plan-v4.resources.json @@ -0,0 +1,41 @@ +{ + "data": {}, + "resource": { + "aws_s3_bucket": { + "denied": { + "bucket": "denied", + "bucket_prefix": null, + "force_destroy": false, + "tags": null + }, + "duh": { + "bucket": "duh", + "bucket_prefix": null, + "force_destroy": false, + "tags": null + }, + "logging2": { + "bucket": "logging2", + "bucket_prefix": null, + "force_destroy": false, + "tags": null + } + }, + "aws_s3_bucket_logging": { + "example": { + "bucket": "aws_s3_bucket.logging2.id", + "target_bucket": "aws_s3_bucket.duh.id", + "expected_bucket_owner": null, + "target_grant": [], + "target_prefix": "log/" + }, + "example2": { + "bucket": "aws_s3_bucket.logging2.id", + "target_bucket": "aws_s3_bucket.duh.id", + "expected_bucket_owner": null, + "target_grant": [], + "target_prefix": "log/" + } + } + } +} \ No newline at end of file diff --git a/test/jest/acceptance/iac/test-directory.spec.ts b/test/jest/acceptance/iac/test-directory.spec.ts index 7785b39fa4..a8d3d3b5b5 100644 --- a/test/jest/acceptance/iac/test-directory.spec.ts +++ b/test/jest/acceptance/iac/test-directory.spec.ts @@ -47,7 +47,7 @@ describe('Directory scan', () => { expect(stdout).toContain('Failed to parse YAML file'); expect(stdout).toContain('Failed to parse JSON file'); expect(stdout).toContain( - '21 projects, 15 contained issues. Failed to test 8 projects.', + '22 projects, 16 contained issues. Failed to test 8 projects.', ); }); diff --git a/test/jest/unit/iac/terraform-plan-parser.spec.ts b/test/jest/unit/iac/terraform-plan-parser.spec.ts index be758560d4..15c49bd984 100644 --- a/test/jest/unit/iac/terraform-plan-parser.spec.ts +++ b/test/jest/unit/iac/terraform-plan-parser.spec.ts @@ -19,6 +19,7 @@ describe('tryParsingTerraformPlan', () => { 2. Plan which deletes resources 3. Plan which doesn't do anything 4. Plan which updates resources + 5. Plan from the Terraform v4 provider These tests validate that the correct resources are being extracted, based on the give scan mode (Full/Delta). These tests do not cover scanning for finding vulnerabilites, but only for the resource extraction logic. **/