@JiJo333 I wrote a couple of custom functions in case you wanted an alternate option:
##### Imports #####
import "tfplan/v2" as tfplan
import "tfplan-functions" as plan
import "strings"
import "types"
### filter_map_attribute_key_contains_items_in_list ###
# Filter a list of resources to those with a specified map
# attribute (attr) key (key) that contains items in a given list of forbidden values (forbidden).
# Resources should be derived by applying filters to tfplan.resource_changes.
# Set prtmsg to `true` (without quotes) if you want to print violation messages.
# If you want to allow null, include "null" in the list (forbidden).
filter_map_attribute_key_contains_items_in_list = func(resources, attr, key, forbidden, prtmsg) {
violators = {}
messages = {}
for resources as address, rc {
# Evaluate the value (vals) of the attribute
vals = plan.evaluate_attribute(rc, attr) else null
# Check if the attribute value is a map or is null.
if not (types.type_of(vals) is "map" or types.type_of(vals) is "null") {
# Add the resource and a warning message to the violators list
message = "Resource '" + plan.to_string(address) + " has attribute '" + plan.to_string(attr) +
"' with a value of '" +
plan.to_string(vals) +
"' that is not a map or null"
violators[address] = rc
messages[address] = message
if prtmsg {
print(message)
}
} else if vals is null {
if "null" not in forbidden {
# Add the resource and a warning message to the violators list
message = "Resource '" + plan.to_string(address) + "' has attribute '" + plan.to_string(attr) +
"' with a value of 'null' that is not in the forbidden list: " +
plan.to_string(forbidden)
violators[address] = rc
messages[address] = message
if prtmsg {
print(message)
}
}
} else {
# Check if vals contains the desired key
key_value = vals[key]
key_exists = key_value else null
if types.type_of(key_exists) is "null" {
message = "Resource '" + plan.to_string(address) + "' has attribute '" + plan.to_string(attr) +
"' that does not contain the key '" +
plan.to_string(key) +
"'"
violators[address] = rc
messages[address] = message
if prtmsg {
print(message)
}
} else {
forbidden_values = []
if types.type_of(key_value) not in ["list", "map"] {
if key_value in forbidden {
append(forbidden_values, key_value)
}
} else {
for key_value as v {
if v in forbidden {
append(forbidden_values, v)
} // end if v not forbidden
} // end for vals
}
if length(forbidden_values) > 0 {
# Build warning message when vals is a map
message = "Resource '" + plan.to_string(address) + "' has attribute '" + plan.to_string(attr) +
"' with key '" +
plan.to_string(key) +
"' containing items " +
plan.to_string(forbidden_values) +
" that are in the forbidden list: " +
plan.to_string(forbidden)
# Add the resource and a warning message to the violators list
violators[address] = rc
messages[address] = message
if prtmsg {
print(message)
}
} // end length(forbidden_values)
}
} // end if null
} // end for
return {"resources": violators, "messages": messages}
}
### filter_map_attribute_key_contains_items_not_in_list ###
# Filter a list of resources to those with a specified map
# attribute (attr) key (key) that contains items not in a given list of allowed values (allowed).
# Resources should be derived by applying filters to tfplan.resource_changes.
# Set prtmsg to `true` (without quotes) if you want to print violation messages.
# If you want to allow null, include "null" in the list (allowed).
filter_map_attribute_key_contains_items_not_in_list = func(resources, attr, key, allowed, prtmsg) {
violators = {}
messages = {}
for resources as address, rc {
# Evaluate the value (vals) of the attribute
vals = plan.evaluate_attribute(rc, attr) else null
# Check if the attribute value is a map or is null.
if not (types.type_of(vals) is "map" or types.type_of(vals) is "null") {
# Add the resource and a warning message to the violators list
message = "Resource '" + plan.to_string(address) + " has attribute '" + plan.to_string(attr) +
"' with a value of '" +
plan.to_string(vals) +
"' that is not a map or null"
violators[address] = rc
messages[address] = message
if prtmsg {
print(message)
}
} else if vals is null {
if "null" not in allowed {
# Add the resource and a warning message to the violators list
message = "Resource '" + plan.to_string(address) + "' has attribute '" + plan.to_string(attr) +
"' with a value of 'null' that is not in the allowed list: " +
plan.to_string(allowed)
violators[address] = rc
messages[address] = message
if prtmsg {
print(message)
}
}
} else {
# Check if vals contains the desired key
key_value = vals[key]
key_exists = key_value else null
if types.type_of(key_exists) is "null" {
message = "Resource '" + plan.to_string(address) + "' has attribute '" + plan.to_string(attr) +
"' that does not contain the key '" +
plan.to_string(key) +
"'"
violators[address] = rc
messages[address] = message
if prtmsg {
print(message)
}
} else {
forbidden_values = []
if types.type_of(key_value) not in ["list", "map"] {
if key_value not in allowed {
append(forbidden_values, key_value)
}
} else {
for key_value as v {
if v not in allowed {
append(forbidden_values, v)
} // end if v not allowed
} // end for vals
}
if length(forbidden_values) > 0 {
# Build warning message when vals is a map
message = "Resource '" + plan.to_string(address) + "' has attribute '" + plan.to_string(attr) +
"' with key '" +
plan.to_string(key) +
"' containing items " +
plan.to_string(forbidden_values) +
" that are not in the allowed list: " +
plan.to_string(allowed)
# Add the resource and a warning message to the violators list
violators[address] = rc
messages[address] = message
if prtmsg {
print(message)
}
} // end length(forbidden_values)
}
} // end if null
} // end for
return {"resources": violators, "messages": messages}
}
Then to call the functions, you would do something like:
# This policy requires that certain tags with valid values are present on given object types
import "custom-functions" as custom
import "tfplan-functions" as plan
# Map of resources that are required to have the specified mandatory name/value tags.
param resource_type_mandatory_tags default {
"azurerm_resource_group": {
"app-id": [
"1234",
"567",
],
},
}
# Loop through each object type, finding any resources that do not have a valid value for each mandatory tag.
resourceMissingTagsCount = 0
for resource_type_mandatory_tags as resourceType, mandatoryTags {
for mandatoryTags as key, allowedValues {
allResources = plan.find_resources(resourceType)
resourcesWithoutValidTags = custom.filter_map_attribute_key_contains_items_not_in_list(allResources,
"tags", key, allowedValues, true)
resourceMissingTagsCount += length(resourcesWithoutValidTags["resources"])
}
}
# Main rule
main = rule {
resourceMissingTagsCount is 0
}
You’ll need to import the functions in your policy set:
module "custom-functions" {
source = "../../../custom-functions/tfplan-functions.sentinel"
}