-
Notifications
You must be signed in to change notification settings - Fork 13
/
Copy pathenforce-module-use-policy.rego
81 lines (68 loc) · 3.43 KB
/
enforce-module-use-policy.rego
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
package spacelift
import future.keywords.in
# Note: This policy requires the configuration of your terraform state to be provided. In this policy,
# we reference this via `input.third_party_metadata.custom.configuration` (line 56 below)
# which is provided to the policy if you follow the instructions documented here:
# https://docs.spacelift.io/concepts/policy/terraform-plan-policy#example-exposing-terraform-configuration-to-the-plan-policy
# TODO: Upgrade to take into account approved versions of the approved modules, and implement
# ability to warn on deprecated versions, and deny on no longer supported versions!
# This is a map of resource types and the list of modules which are
# approved to be used to create them. Note, you do not need to allow explicitly
# any "wrapper" modules... this is checking the immediate module parent of the resource itself
# Some example resources and approved module(s), you of course can specify your spacelift.io hosted modules
controlled_resource_types := {
"aws_s3_bucket": ["terraform-aws-modules/s3-bucket/aws"],
"aws_s3_bucket_acl": ["terraform-aws-modules/s3-bucket/aws"],
"aws_s3_bucket_website_configuration": ["terraform-aws-modules/s3-bucket/aws"],
}
# Deny ability to create the resource directly (aka not in a module we identify)
deny[reason] {
resource := input.terraform.resource_changes[_]
actions := {"create", "update"}
actions[resource.change.actions[_]]
module_source = controlled_resource_types[resource.type]
not resource.module_address
reason := sprintf(
"Resource '%s' cannot be created directly. Module(s) '%s' must be used instead",
[resource.address, concat("', '", controlled_resource_types[resource.type])],
)
}
# Deny ability to create the resource in an unapproved module
deny[failed_reasons] {
# Did any of the resources fail to pass?
count(invalid_resources[_]) > 0
# Build a list of reasons for each failure
failed_reasons := [sprintf(
"Resource '%s' in top level module named '%s' is being created with the incorrect terraform module '%s'. Module(s) '%s' must be used instead.",
[reason.resource_type, reason.top_level_module_name, reason.resource_module_name, concat("', '", controlled_resource_types[reason.resource_type])],
) |
reason := invalid_resources[_]
][_]
}
# Walk the "configuration" tree and find all the resources which appear in our
# "controlled_resource_types"
controlled_resources[resource] {
# Recursively walk the module hierarchy
[path, module_ref] := walk(input.third_party_metadata.custom.configuration)
# Filter out only objects that are modules, i.e. have a source property
source := module_ref.source
# Get all resources created in the module and their types
resources := module_ref.module.resources
resource_type := resources[_].type
# Filter out resources that are considered controlled
resource_type in object.keys(controlled_resource_types)
# Create an object with the module and the resource type,
# this is a set so duplicates will be removed based on this object
resource := {
"resource_module_name": source,
"top_level_module_name": path[2],
"resource_type": resource_type,
"resource_module_version_constraint": module_ref.version_constraint,
}
}
# When the controlled resources are collected, iterate through them and
# see if they comply
invalid_resources[resource_instance] {
resource_instance := controlled_resources[_]
not resource_instance.resource_module_name in controlled_resource_types[resource_instance.resource_type]
}