|
| 1 | +# This policy uses the Sentinel tfplan import to require that all EC2 instances |
| 2 | +# have all mandatory tags. |
| 3 | + |
| 4 | +# Note that the comparison is case-sensitive since AWS tags are case-sensitive. |
| 5 | + |
| 6 | +##### Imports ##### |
| 7 | + |
| 8 | +import "tfplan" |
| 9 | +import "strings" |
| 10 | +import "types" |
| 11 | + |
| 12 | +##### Functions ##### |
| 13 | + |
| 14 | +# Find all resources of a specific type from all modules using the tfplan import |
| 15 | +find_resources_from_plan = func(type) { |
| 16 | + |
| 17 | + resources = {} |
| 18 | + |
| 19 | + # Iterate over all modules in the tfplan import |
| 20 | + for tfplan.module_paths as path { |
| 21 | + # Iterate over the named resources of desired type in the module |
| 22 | + for tfplan.module(path).resources[type] else {} as name, instances { |
| 23 | + # Iterate over resource instances |
| 24 | + for instances as index, r { |
| 25 | + |
| 26 | + # Get the address of the instance |
| 27 | + if length(path) == 0 { |
| 28 | + # root module |
| 29 | + address = type + "." + name + "[" + string(index) + "]" |
| 30 | + } else { |
| 31 | + # non-root module |
| 32 | + address = "module." + strings.join(path, ".module.") + "." + |
| 33 | + type + "." + name + "[" + string(index) + "]" |
| 34 | + } |
| 35 | + |
| 36 | + # Add the instance to resources map, setting the key to the address |
| 37 | + resources[address] = r |
| 38 | + } |
| 39 | + } |
| 40 | + } |
| 41 | + |
| 42 | + return resources |
| 43 | +} |
| 44 | + |
| 45 | +# Validate that all instances of specified type have a specified top-level |
| 46 | +# attribute that contains all members of a given list |
| 47 | +validate_attribute_contains_list = func(type, attribute, required_values) { |
| 48 | + |
| 49 | + validated = true |
| 50 | + |
| 51 | + # Get all resource instances of the specified type |
| 52 | + resource_instances = find_resources_from_plan(type) |
| 53 | + |
| 54 | + # Loop through the resource instances |
| 55 | + for resource_instances as address, r { |
| 56 | + |
| 57 | + # Skip resource instances that are being destroyed |
| 58 | + # to avoid unnecessary policy violations. |
| 59 | + # Used to be: if length(r.diff) == 0 |
| 60 | + if r.destroy and not r.requires_new { |
| 61 | + print("Skipping resource", address, "that is being destroyed.") |
| 62 | + continue |
| 63 | + } |
| 64 | + |
| 65 | + # Determine if the attribute is computed |
| 66 | + # We check "attribute.%" and "attribute.#" because an |
| 67 | + # attribute of type map or list won't show up in the diff |
| 68 | + if (r.diff[attribute + ".%"].computed else false) or |
| 69 | + (r.diff[attribute + ".#"].computed else false) { |
| 70 | + print("Resource", address, "has attribute", attribute, |
| 71 | + "that is computed.") |
| 72 | + # If you want computed values to cause the policy to fail, |
| 73 | + # uncomment the next line. |
| 74 | + # validated = false |
| 75 | + } else { |
| 76 | + # Validate that the attribute is a list or a map |
| 77 | + # but first check if r.applied[attribute] exists |
| 78 | + if r.applied[attribute] else null is not null and |
| 79 | + (types.type_of(r.applied[attribute]) is "list" or |
| 80 | + types.type_of(r.applied[attribute]) is "map") { |
| 81 | + |
| 82 | + # Evaluate each member of required_values list |
| 83 | + for required_values as rv { |
| 84 | + if r.applied[attribute] not contains rv { |
| 85 | + print("Resource", address, "has attribute", attribute, |
| 86 | + "that is missing required value", rv, "from the list:", |
| 87 | + required_values) |
| 88 | + validated = false |
| 89 | + } // end rv |
| 90 | + } // end required_values |
| 91 | + |
| 92 | + } else { |
| 93 | + print("Resource", address, "is missing attribute", attribute, |
| 94 | + "or it is not a list or a map") |
| 95 | + validated = false |
| 96 | + } // end check that attribute is list or map |
| 97 | + |
| 98 | + } // end computed check |
| 99 | + } // end resource instances |
| 100 | + |
| 101 | + return validated |
| 102 | +} |
| 103 | + |
| 104 | +### List of mandatory tags ### |
| 105 | +mandatory_tags = [ |
| 106 | + "Department", |
| 107 | + "Billable", |
| 108 | +] |
| 109 | + |
| 110 | +### Rules ### |
| 111 | + |
| 112 | +# Call the validation function |
| 113 | +tags_validated = validate_attribute_contains_list("aws_instance", |
| 114 | + "tags", mandatory_tags) |
| 115 | + |
| 116 | +#Main rule |
| 117 | +main = rule { |
| 118 | + tags_validated |
| 119 | +} |
0 commit comments