Skip to content

Commit

Permalink
feat(terraform_plan): Expose field changes to python checks (#5112)
Browse files Browse the repository at this point in the history
* first pass at adding functionality to detect changed fields in a terraform resource

* no cool one-liners with relevant_attributes, apparently that is not what I thought it was. back to basic diffing

* debug

* ofc they make dicts None instead of empty when they are empty

* remove whitespace

* pr feedback, hopefully a test

* docs

* import hopefully properly

* lint

* mypy does not seem to understand how i made lines 256-259 safe

* i still have failed to figure out my imports :upside_down:

* files not classes?

* fix tests and mypy

* s/each/resource/g

---------

Co-authored-by: Anton Grübel <[email protected]>
  • Loading branch information
tarfeef101 and gruebel authored Jun 6, 2023
1 parent 15876b4 commit 861c675
Show file tree
Hide file tree
Showing 5 changed files with 333 additions and 12 deletions.
27 changes: 20 additions & 7 deletions checkov/terraform/plan_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,14 @@

from checkov.common.graph.graph_builder import CustomAttributes
from checkov.common.parsers.node import ListNode
from checkov.common.util.consts import LINE_FIELD_NAMES
from checkov.common.util.type_forcers import force_list
from checkov.terraform.context_parsers.tf_plan import parse

SIMPLE_TYPES = (str, int, float, bool)
TF_PLAN_RESOURCE_ADDRESS = "__address__"
TF_PLAN_RESOURCE_CHANGE_ACTIONS = "__change_actions__"
TF_PLAN_RESOURCE_CHANGE_KEYS = "__change_keys__"

RESOURCE_TYPES_JSONIFY = {
"aws_batch_job_definition": "container_properties",
Expand Down Expand Up @@ -129,7 +131,6 @@ def _prepare_resource_block(
:param resource: tf planned_values resource block
:param conf: tf configuration resource block
:param resource_changes: tf resource_changes block
:returns:
- resource_block: a list of strings representing the header columns
- prepared: whether conditions met to prepare data
Expand Down Expand Up @@ -161,6 +162,7 @@ def _prepare_resource_block(
changes = resource_changes.get(resource_address) # type:ignore[arg-type] # becaus eit can be None
if changes:
resource_conf[TF_PLAN_RESOURCE_CHANGE_ACTIONS] = changes.get("change", {}).get("actions") or []
resource_conf[TF_PLAN_RESOURCE_CHANGE_KEYS] = changes.get(TF_PLAN_RESOURCE_CHANGE_KEYS) or []

resource_block[resource_type][resource.get("name", "default")] = resource_conf
prepared = True
Expand All @@ -186,7 +188,7 @@ def _find_child_modules(
nested_blocks = _find_child_modules(
child_modules=nested_child_modules,
resource_changes=resource_changes,
root_module_conf=root_module_conf
root_module_conf=root_module_conf,
)
for block_type, resource_blocks in nested_blocks.items():
blocks[block_type].extend(resource_blocks)
Expand Down Expand Up @@ -238,13 +240,24 @@ def _get_resource_changes(template: dict[str, Any]) -> dict[str, dict[str, Any]]
"""Returns a resource address to resource changes dict"""

resource_changes_map = {}

resource_changes = template.get("resource_changes")

if resource_changes and isinstance(resource_changes, list):
resource_changes_map = {
change.get("address", ""): change
for change in resource_changes
}
for resource in resource_changes:
resource_changes_map[resource["address"]] = resource
changes = []

# before + after are None when resources are created/destroyed, so make them safe
change_before = resource["change"]["before"] or {}
change_after = resource["change"]["after"] or {}

for field, value in change_before.items():
if field in LINE_FIELD_NAMES:
continue # don't care about line #s
if value != change_after.get(field):
changes.append(field)

resource_changes_map[resource["address"]][TF_PLAN_RESOURCE_CHANGE_KEYS] = changes

return resource_changes_map

Expand Down
16 changes: 16 additions & 0 deletions docs/7.Scan Examples/Terraform Plan Scanning.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,22 @@ Ex. YAML
value: delete
```
### Changed resource fields
To write a check conditional on whether or not a specific field has changed, one can access the changed fields via the attribute `TF_PLAN_RESOURCE_CHANGE_KEYS` (a list of changed keys).

Ex Python
```python
from checkov.terraform.plan_parser import TF_PLAN_RESOURCE_CHANGE_ACTIONS, TF_PLAN_RESOURCE_CHANGE_KEYS
def scan_resource_conf(self, conf: dict[str, Any]) -> CheckResult:
actions = conf.get(TF_PLAN_RESOURCE_CHANGE_ACTIONS)
if isinstance(actions, list) and "update" in actions:
if "protocol" in conf.get(TF_PLAN_RESOURCE_CHANGE_KEYS):
return CheckResult.FAILED
return CheckResult.PASSED
```

## Combining Plan and Terraform scans
Plan file scans can be enriched with the Terraform files to improve outputs, add skip comments and expand coverage. Note that these will increase scan times.

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
from __future__ import annotations

from typing import Any

from checkov.common.models.enums import CheckResult, CheckCategories
from checkov.terraform.checks.resource.base_resource_check import BaseResourceCheck
from checkov.terraform.plan_parser import TF_PLAN_RESOURCE_CHANGE_ACTIONS, TF_PLAN_RESOURCE_CHANGE_KEYS


class SecurityGroupRuleProtocolChanged(BaseResourceCheck):
def __init__(self) -> None:
name = "Ensure security group rule protocol is not being changed"
id = "CUSTOM_CHANGE_1"
supported_resources = ("aws_security_group_rule",)
categories = (CheckCategories.GENERAL_SECURITY,)
super().__init__(name=name, id=id, categories=categories, supported_resources=supported_resources)

def scan_resource_conf(self, conf: dict[str, Any]) -> CheckResult:
actions = conf.get(TF_PLAN_RESOURCE_CHANGE_ACTIONS)
if isinstance(actions, list) and "update" in actions:
if "protocol" in conf.get(TF_PLAN_RESOURCE_CHANGE_KEYS):
self.details.append("some great details")
return CheckResult.FAILED
return CheckResult.PASSED


check = SecurityGroupRuleProtocolChanged()
228 changes: 228 additions & 0 deletions tests/terraform/runner/resources/plan_change_keys/tfplan.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
{
"format_version": "1.1",
"terraform_version": "1.4.6",
"planned_values":
{
"root_module":
{
"resources":
[
{
"address": "aws_security_group_rule.foo",
"mode": "managed",
"type": "aws_security_group_rule",
"name": "foo",
"provider_name": "registry.terraform.io/hashicorp/aws",
"schema_version": 2,
"values": {
"cidr_blocks": null,
"description": "foo",
"from_port": 5432,
"ipv6_cidr_blocks": null,
"prefix_list_ids": [],
"protocol": "tcp",
"security_group_id": "sg-547cc4cd5f94bb695",
"self": false,
"source_security_group_id": "sg-8d21ab5963b0e7917",
"timeouts": null,
"to_port": 5433,
"type": "ingress"
},
"sensitive_values": {
"prefix_list_ids": []
}
},
{
"address": "aws_security_group_rule.bar",
"mode": "managed",
"type": "aws_security_group_rule",
"name": "bar",
"provider_name": "registry.terraform.io/hashicorp/aws",
"schema_version": 2,
"values": {
"cidr_blocks": null,
"description": "foo",
"from_port": 8888,
"ipv6_cidr_blocks": null,
"prefix_list_ids": [],
"protocol": "udp",
"security_group_id": "sg-547cc4cd5f94bb696",
"self": false,
"source_security_group_id": "sg-8d21ab5963b0e7917",
"timeouts": null,
"to_port": 8888,
"type": "ingress"
},
"sensitive_values": {
"prefix_list_ids": []
}
}
]
}
},
"resource_changes":
[
{
"address": "aws_security_group_rule.foo",
"mode": "managed",
"type": "aws_security_group_rule",
"name": "foo",
"provider_name": "registry.terraform.io/hashicorp/aws",
"schema_version": 2,
"change":
{
"actions":
[
"update"
],
"before": {
"cidr_blocks": [],
"description": "foo",
"from_port": 5432,
"id": "sgrule-88888888",
"ipv6_cidr_blocks": [],
"prefix_list_ids": [],
"protocol": "tcp",
"security_group_id": "sg-547cc4cd5f94bb695",
"security_group_rule_id": null,
"self": false,
"source_security_group_id": "sg-8d21ab5963b0e7917",
"timeouts": null,
"to_port": 5432,
"type": "ingress"
},
"after": {
"cidr_blocks": [],
"description": "foo",
"from_port": 5432,
"id": "sgrule-88888888",
"ipv6_cidr_blocks": [],
"prefix_list_ids": [],
"protocol": "tcp",
"security_group_id": "sg-547cc4cd5f94bb695",
"security_group_rule_id": "sgr-5a4b695164c564a8f",
"self": false,
"source_security_group_id": "sg-8d21ab5963b0e7917",
"timeouts": null,
"to_port": 5433,
"type": "ingress"
},
"after_unknown": {},
"before_sensitive": {
"cidr_blocks": [],
"ipv6_cidr_blocks": [],
"prefix_list_ids": []
},
"after_sensitive": {
"cidr_blocks": [],
"ipv6_cidr_blocks": [],
"prefix_list_ids": []
}
}
},
{
"address": "aws_security_group_rule.bar",
"mode": "managed",
"type": "aws_security_group_rule",
"name": "bar",
"provider_name": "registry.terraform.io/hashicorp/aws",
"schema_version": 2,
"change":
{
"actions":
[
"update"
],
"before": {
"cidr_blocks": [],
"description": "bar",
"from_port": 8888,
"id": "sgrule-88888888",
"ipv6_cidr_blocks": [],
"prefix_list_ids": [],
"protocol": "tcp",
"security_group_id": "sg-547cc4cd5f94bb696",
"security_group_rule_id": null,
"self": false,
"source_security_group_id": "sg-8d21ab5963b0e7917",
"timeouts": null,
"to_port": 8888,
"type": "ingress"
},
"after": {
"cidr_blocks": [],
"description": "bar",
"from_port": 8888,
"id": "sgrule-88888888",
"ipv6_cidr_blocks": [],
"prefix_list_ids": [],
"protocol": "udp",
"security_group_id": "sg-547cc4cd5f94bb696",
"security_group_rule_id": "sgr-5a4b695164c564a90",
"self": false,
"source_security_group_id": "sg-8d21ab5963b0e7917",
"timeouts": null,
"to_port": 8888,
"type": "ingress"
},
"after_unknown": {},
"before_sensitive": {
"cidr_blocks": [],
"ipv6_cidr_blocks": [],
"prefix_list_ids": []
},
"after_sensitive": {
"cidr_blocks": [],
"ipv6_cidr_blocks": [],
"prefix_list_ids": []
}
}
}
],
"configuration":
{
"provider_config":
{
"aws":
{
"name": "aws"
}
},
"root_module":
{
"resources":
[
{
"address": "aws_security_group_rule.foo",
"mode": "managed",
"type": "aws_security_group_rule",
"name": "foo",
"provider_config_key": "aws",
"expressions":
{
"name":
{
"constant_value": "foo"
}
},
"schema_version": 2
},
{
"address": "aws_security_group_rule.bar",
"mode": "managed",
"type": "aws_security_group_rule",
"name": "bar",
"provider_config_key": "aws",
"expressions":
{
"name":
{
"constant_value": "bar"
}
},
"schema_version": 2
}
]
}
}
}
Loading

0 comments on commit 861c675

Please sign in to comment.