Skip to content

Commit

Permalink
feat: resolve single depth of references
Browse files Browse the repository at this point in the history
  • Loading branch information
teodora-sandu committed Feb 24, 2022
1 parent 4c4171c commit e8d445c
Show file tree
Hide file tree
Showing 7 changed files with 132 additions and 2 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import {
TerraformScanInput,
TerraformPlanResourceChange,
IaCErrorCodes,
TerraformPlanReferencedResource,
TerraformPlanExpression,
} from '../types';
import { CustomError } from '../../../../../lib/errors';
import { getErrorStringCode } from '../error-utils';
Expand All @@ -34,6 +36,27 @@ function terraformPlanReducer(
return scanInput;
}

function getExpressions(
expressions: Record<string, TerraformPlanExpression>,
): Record<string, string> {
const result: Record<string, string> = {};
// 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;
}
Expand Down Expand Up @@ -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<TerraformPlanResourceChange> {
return terraformPlanJson.resource_changes || [];
}

function extractReferencedResources(
terraformPlanJson: TerraformPlanJson,
): Array<TerraformPlanReferencedResource> {
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 {
Expand Down
14 changes: 14 additions & 0 deletions src/cli/commands/test/iac-local-execution/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<TerraformPlanResourceChange>;
configuration: {
root_module: {
resources: Array<TerraformPlanReferencedResource>;
};
};
}

export interface TerraformPlanReferencedResource extends TerraformPlanResource {
expressions: Record<string, TerraformPlanExpression>;
}

export interface TerraformPlanExpression {
references: Array<string>;
}

export interface TerraformScanInput {
// within the resource field, resources are stored: [type] => [name] => [values]
resource: Record<string, Record<string, unknown>>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
"queued_timeout": 480,
"secondary_artifacts": [],
"secondary_sources": [],
"service_role": "aws_iam_role.terra_ci_job",
"source": [
{
"auth": [],
Expand Down Expand Up @@ -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"
}
},
Expand Down Expand Up @@ -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"
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
"queued_timeout": 480,
"secondary_artifacts": [],
"secondary_sources": [],
"service_role": "aws_iam_role.terra_ci_job",
"source": [
{
"auth": [],
Expand Down Expand Up @@ -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"
}
},
Expand Down Expand Up @@ -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"
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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/"
}
}
}
}
2 changes: 1 addition & 1 deletion test/jest/acceptance/iac/test-directory.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.',
);
});

Expand Down
1 change: 1 addition & 0 deletions test/jest/unit/iac/terraform-plan-parser.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
**/
Expand Down

0 comments on commit e8d445c

Please sign in to comment.