Skip to content

Changes related to scanning EKS Blueprint Terraform plan #2

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 16 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
.DS_Store
._.DS_Store
**/.DS_Store
**/._.DS_Store
76 changes: 76 additions & 0 deletions policy-as-code/OPA/policy/aws/blueprints/eks/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
Amazon EKS Blueprint for Terraform contains a collection of Amazon EKS cluster patterns implemented in Terraform that demonstrates how fast and easy it is for customers to adopt Amazon EKS. The Open Policy Agent is an open-source, general purpose policy engine that unifies policy enforcement across the stack. Using Open Policy Agent (OPA) to scan your infrastructure as code and within your Kubernetes cluster is a smart and effective way to ensure the security and compliance of your environment. <br/>
This repo contains below OPA Rego policies for the EKS Blueprint for Terraform project:

1. eks-blueprint-control-logs: Check if the EKS control plane has valid logs enabled.
2. eks-blueprint-private-endpoint: Check if the EKS cluster uses private endpoints
3. eks-blueprint-security-group: Check if the EKS Cluster has security group defined
4. eks-blueprint-selfmanaged-security-group: Check if self managed cluster nodes have security groups defined
5. eks-blueprint-disk-size: Check if the EKS Cluster node groups have disk_size parameter configured

# Pre-requisites

- [Install AWS CLI version 2](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html) on your local machine
- [Install Open Policy Agent](https://github.com/open-policy-agent/opa) from the latest release
- [Install Terraform](https://developer.hashicorp.com/terraform/tutorials/aws-get-started/install-cli)

# Evaluate EKS Blueprint for Terraform plan with OPA

You can git clone the two repos, one containing the EKS Blueprint terraform configuration and this one containing the OPA Rego policies which would be used to scan the terraform plans
```
git clone https://github.com/aws-ia/terraform-aws-eks-blueprints.git
git clone https://github.com/gpmattoo/aws-infra-policy-as-code-with-terraform.git
```

You can initialize and run the terraform plan command
```
terraform -chdir=terraform-aws-eks-blueprints/patterns/fargate-serverless/ init
terraform -chdir=terraform-aws-eks-blueprints/patterns/fargate-serverless/ plan --out tfplan.binary
terraform -chdir=terraform-aws-eks-blueprints/patterns/fargate-serverless/ show -json tfplan.binary > tfplan.json

cat tfplan.json
```

Here is the OPA policy execution to evaluate the terraform plan from fargate-serverless pattern of EKS Blueprint

```
opa eval -i /Users/gpmattoo/devOpsRepos/aws-infra-policy-as-code-with-terraform/policy-as-code/OPA/policy/aws/blueprints/eks/fargate-serverless-tfplan.json -d /Users/gpmattoo/devOpsRepos/aws-infra-policy-as-code-with-terraform/policy-as-code/OPA/policy/aws/blueprints/eks/eks-blueprint-control-logs.rego -d common.utils.rego "data.aws.blueprints.eks.controllogs.deny"
```
and here is the output of above policy execution:
```
{
"result": [
{
"expressions": [
{
"value": [],
"text": "data.aws.blueprints.eks.controllogs.deny",
"location": {
"row": 1,
"col": 1
}
}
]
}
]
}
```

# Testing EKS Blueprint OPA Rego Policies
```
opa test <path_to_opa_control> <path_to_common_utils.rego> -v
```
For instance:
```
cd ~/aws-infra-policy-as-code-with-terraform/policy-as-code/OPA/policy/aws/blueprints/eks
opa test eks-blueprint-control-logs* common.utils.rego -v
```

# EKS Blueprint OPA Rego Test Coverage
```
opa test <path_to_opa_control> <path_to_common_utils.rego> -v --coverage
```
For instance:
```
cd ~/aws-infra-policy-as-code-with-terraform/policy-as-code/OPA/policy/aws/blueprints/eks
opa test eks-blueprint-control-logs* common.utils.rego -v --coverage
```
213 changes: 213 additions & 0 deletions policy-as-code/OPA/policy/aws/blueprints/eks/common.utils.rego
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
package utils

# Checks if action is create or update
# Common path: resource.change.actions
is_create_or_update(change_actions) {
change_actions[count(change_actions) - 1] == ["create", "update"][_]
}

# Checks of resource is being created or updated
is_resource_create_or_update(resource) {
is_create_or_update(resource.change.actions)
}

# Creates an array with all falsey values removed.
# The values false, null, 0, "", {} and [] are considered falsey.
compact(array) = output {
output := [value |
value := array[_]
not is_null(value)
not value == false
not value == ""
not value == 0
not value == []
not value == {}
]
}

# Checks if `match` value matches to all items in array.
every(array, match) {
count([value |
value := array[_]
value == match
]) == count(array)
} else = false {
true
}

# Gets the value at path of object.
get(object, path) = output {
[obj_path, value] = walk(object)
path_array := to_path(path)
obj_path == path_array
output := value
}

# Gets the value at path of object.
# If the resolved value is undefined,
# the default_value is returned in its place.
get_or_default(object, path, default_value) = output {
output := get(object, path)
} else = output {
output := default_value
}

# Checks if path exists on object
has(object, path) {
[obj_path, value] = walk(object)
obj_path == to_path(path)
} else = false {
true
}

# Checks if value exists in array
includes(array, value) {
value == array[_]
} else = false {
true
}

# Gets index of object in array that matches provided fraction object
index_by(array, fraction) = output {
some i
item = array[i]
is_fraction(item, fraction)
output := i
} else = output {
output := -1
}

# Gets index of value in array
index_of(array, value) = output {
some i
item = array[i]
item == value
output := i
} else = output {
output := -1
}

# Checks if value is null or false
is_null_or_false(value) {
is_null(value)
} else {
value == false
} else = false {
true
}

# Checks if object matches fraction
is_fraction(object, fraction) {
search_keys = keys(fraction)
count({key |
key = search_keys[_]
object[key] == fraction[key]
}) == count(search_keys)
} else = false {
true
}

# Gets the keys of object
keys(object) = output {
output := {key |
[path, value] = walk(object)
key := path[0]
}
}

# Gets the size of collection
size(collection) = output {
is_string(collection)
output := count(collection)
} else = output {
output := count(keys(collection))
}

# Converts set to an array
to_array(set) = output {
output := [value |
value := set[_]
]
}

# Converts array to a set
to_set(array) = output {
output := {value |
value := array[_]
}
}

_parse_array_index(value) = output {
contains(value, "[")
number_string := substring(value, 1, count(value) - 2)
output = try_to_number(number_string)
} else = output {
output = value
}

# Converts string path to a path array
to_path(path) = output_array {
output_array := [value |
part := split(path, ".")[_]
value := _parse_array_index(part)
]
}

# Attempts to converts string to a number
try_to_number(string) = out {
out := to_number(string)
} else = out {
out := string
}

is_resource_of_type(resource, service) {
resource.mode == "managed"
contains(resource.type, service)
resource.change.actions[count(resource.change.actions) - 1] != "delete"
}

# Checks if service resource exists in the plan
find_service_resource(plan, service) = result {
result := [ x.address | x := plan.resource_changes[_]; is_resource_of_type(x, service)]
}

# Checks if arrays is null or empty
is_array_null_or_empty(value) {
is_null(value)
} else {
size(value) = 0
} else = false {
true
}

# Check if an array contains specified value
contains_element(array, value) {
array[_] = value
} else = false {
true
}

# find configuration entries for resource
find_configuration_resource(plan, resource) = cfgresource{
# case where there is no module
not resource.module_address
some ssm_resource
plan.configuration.root_module.resources[ssm_resource].address == resource.address
cfgresource := plan.configuration.root_module.resources[ssm_resource]
} else = cfgresource{
some ssm_resource
# case with module (or nested modules)
base_path := "configuration.root_module"
# get module_address and split with "."
module_address_list := split(resource.module_address, ".")
# list comprehention to keep only modules names not "module." entries
nested_module_path := [ path | module_address_list[i] != "module"; path := module_address_list[i] ]
# rebuild path for configuration section
temp_path := concat(".", [ path2 | nested_module_path[i] ; path2 := sprintf("module_calls.%s.module",[nested_module_path[i]])])
temp_path2 := concat(".", [base_path, temp_path])

# search input object starting at root_module
myobj := data.utils.get(plan, temp_path2)
concat(".", [resource.module_address, myobj.resources[ssm_resource].address]) == resource.address
cfgresource := myobj.resources[ssm_resource]
}
Loading