# Common functions that use the tfplan/v2 import # The filter functions all accept a collection of resource changes, an attribute, # a value or a list of values, and a boolean, prtmsg, which can be true or false # and indicates whether the filter function should print violation messages. # The filter functions return a map consisting of 2 items: # * "resources": a map consisting of resource changes that violate a condition # * "messages": a map of violation messages associated with the resources # Note that both the resources and messages collections are indexed by the # address of the resources, so they will have the same order and length. # The filter functions all call evaluate_attribute() to evaluate attributes # of resources even if nested deep within them. ##### Imports ##### import "tfplan/v2" as tfplan import "strings" import "types" ##### Functions ##### ### find_resources ### # Find all resources of a specific type using the tfplan/v2 import. # Include resources that are not being permanently deleted. # Technically, this returns a map of resource changes. find_resources = func(type) { resources = filter tfplan.resource_changes as address, rc { rc.type is type and rc.mode is "managed" and (rc.change.actions contains "create" or rc.change.actions contains "update" or rc.change.actions contains "read" or (rc.change.actions contains "no-op" and rc.change.after is not null)) } return resources } ### find_resources_by_provider ### # Find all resources for a specific provider using the tfplan/v2 import. # Include resources that are not being permanently deleted. # Technically, this returns a map of resource changes. # Terraform 0.12 and earlier set `rc.provider_name` to short text like "null"; # but Terraform 0.13 and higher set it to something like # "registry.terraform.io/hashicorp/null". # You can pass in the long form or short form. find_resources_by_provider = func(provider) { parsed_provider = strings.split(provider, "/") segment_count = length(parsed_provider) v = strings.split(tfplan.terraform_version, ".") v_major = int(v[1]) # If v_major is 12, we know short form was passed to Sentinel if v_major is 12 { resources = filter tfplan.resource_changes as address, rc { rc.provider_name is provider and rc.mode is "managed" and (rc.change.actions contains "create" or rc.change.actions contains "update" or rc.change.actions contains "read" or (rc.change.actions contains "no-op" and rc.change.after is not null)) } } else { # v_major must be higher than 12 since v2 imports being used # So, we know long form like "registry.terraform.io/hashicorp/null" given if segment_count is 1 { # Function was passed short form, so we want to reduce each occurence of # rc.provider_name to its short form. resources = filter tfplan.resource_changes as address, rc { strings.split(rc.provider_name, "/")[2] is provider and rc.mode is "managed" and (rc.change.actions contains "create" or rc.change.actions contains "update" or rc.change.actions contains "read" or (rc.change.actions contains "no-op" and rc.change.after is not null)) } } else { # Function was passed long form, so we use full rc.provider_name resources = filter tfplan.resource_changes as address, rc { rc.provider_name is provider and rc.mode is "managed" and (rc.change.actions contains "create" or rc.change.actions contains "update" or rc.change.actions contains "read" or (rc.change.actions contains "no-op" and rc.change.after is not null)) } } // end segment_count } // end v_major return resources } ### find_datasources ### # Find all data sources of a specific type using the tfplan/v2 import. # Include data sources that are not being permanently deleted. # Technically, this returns a map of resource changes. find_datasources = func(type) { datasources = filter tfplan.resource_changes as address, rc { rc.type is type and rc.mode is "data" and (rc.change.actions contains "create" or rc.change.actions contains "update" or rc.change.actions contains "read" or (rc.change.actions contains "no-op" and rc.change.after is not null)) } return datasources } ### find_datasources_by_provider ### # Find all data sources for a specific provider using the tfplan/v2 import. # Include data sources that are not being permanently deleted. # Technically, this returns a map of resource changes. # Terraform 0.12 and earlier set `rc.provider_name` to short text like "null"; # but Terraform 0.13 and higher set it to something like # "registry.terraform.io/hashicorp/null". # You can pass in the long form or short form. find_datasources_by_provider = func(provider) { parsed_provider = strings.split(provider, "/") segment_count = length(parsed_provider) v = strings.split(tfplan.terraform_version, ".") v_major = int(v[1]) # If v_major is 12, we know short form was passed to Sentinel if v_major is 12 { datasources = filter tfplan.resource_changes as address, rc { rc.provider_name is provider and rc.mode is "data" and (rc.change.actions contains "create" or rc.change.actions contains "update" or rc.change.actions contains "read" or (rc.change.actions contains "no-op" and rc.change.after is not null)) } } else { # v_major must be higher than 12 since v2 imports being used # So, we know long form like "registry.terraform.io/hashicorp/null" given if segment_count is 1 { # Function was passed short form, so we want to reduce each occurence of # rc.provider_name to its short form. datasources = filter tfplan.resource_changes as address, rc { strings.split(rc.provider_name, "/")[2] is provider and rc.mode is "data" and (rc.change.actions contains "create" or rc.change.actions contains "update" or rc.change.actions contains "read" or (rc.change.actions contains "no-op" and rc.change.after is not null)) } } else { # Function was passed long form, so we use full rc.provider_name datasources = filter tfplan.resource_changes as address, rc { rc.provider_name is provider and rc.mode is "data" and (rc.change.actions contains "create" or rc.change.actions contains "update" or rc.change.actions contains "read" or (rc.change.actions contains "no-op" and rc.change.after is not null)) } } // end segment_count } // end v_major return datasources } ### find_resources_being_destroyed ### # Find all resources being destroyed but not recreated using the tfplan/v2 import. # Technically, this returns a map of resource changes. find_resources_being_destroyed = func() { resources = filter tfplan.resource_changes as address, rc { rc.mode is "managed" and rc.change.actions contains "delete" and not (rc.change.actions contains "create" or rc.change.actions contains "update") } return resources } ### find_datasources_being_destroyed ### # Find all data sources being destroyed but not recreated using the tfplan/v2 import. # Technically, this returns a map of resource changes. find_datasources_being_destroyed = func() { datasources = filter tfplan.resource_changes as address, rc { rc.mode is "data" and rc.change.actions contains "delete" and not (rc.change.actions contains "create" or rc.change.actions contains "update") } return datasources } ### find_blocks ### # Find all blocks of a specific type from a resource using the tfplan/v2 import. # parent should be a single resource or block of a resource or a data source # or a block of a data source. # If parent is a resource, you can pass it in the form rc.change.after or just rc. # child should be a string representing a block of parent # that contains a list of objects. find_blocks = func(parent, child) { # Use parent.change.after if it exists if (types.type_of(parent) is "map" and "change" in keys(parent)) and (types.type_of(parent.change) is "map" and "after" in keys(parent.change)) { if types.type_of(parent.change.after[child] else null) is "list" { return parent.change.after[child] } else { return [] } } else { if types.type_of(parent[child] else null) is "list" { return parent[child] } else { return [] } } } ### to_string ### # Convert objects of unknown type to string # It is used to build messages added to the messages map returned by the # filter functions to_string = func(obj) { case types.type_of(obj) { when "string": return obj when "int", "float", "bool": return string(obj) when "null": return "null" when "undefined": return "undefined" when "list": output = "[" lastIndex = length(obj) - 1 for obj as index, value { if index < lastIndex { output += to_string(value) + ", " } else { output += to_string(value) } } output += "]" return output when "map": output = "{" theKeys = keys(obj) lastIndex = length(theKeys) - 1 for theKeys as index, key { if index < lastIndex { output += to_string(key) + ": " + to_string(obj[key]) + ", " } else { output += to_string(key) + ": " + to_string(obj[key]) } } output += "}" return output else: return "" } } ### evaluate_attribute ### # Evaluates the value of a resource's or block's attribute even if nested. # The resource should be derived by applying filters to tfplan.resource_changes. # It can be given in the initial call in the form rc.change.after or just rc. # If you want to evaluate previous values, pass `rc.change.before` instead of # `rc` since the function converts `rc` by itself to `rc.change.after`. # Indices of lists should be given as 0, 1, 2, and so on. # For example: boot_disk.0.initialize_params.0.image evaluate_attribute = func(r, attribute) { # Split the attribute into a list, using "." as the separator attributes = strings.split(attribute, ".") # Convert numeric strings to integers for indices if attributes[0] matches "^[0-9]+$" { a = int(attributes[0]) # Make sure r is of type list if types.type_of(r) is not "list" { return undefined } } else { a = attributes[0] } # Append the current attribute to the resource instance if (types.type_of(r) is "map" and "change" in keys(r)) and (types.type_of(r.change) is "map" and "after" in keys(r.change)) { new_r = r.change.after[a] else null } else { new_r = r[a] else null } # Process based on length of attributes # being greater than or equal to 1 if length(attributes) > 1 { # Strip first element from attributes attributes = attributes[1:length(attributes)] attribute = strings.join(attributes, ".") # Make recursive call return evaluate_attribute(new_r, attribute) } else { # We reached the end of the attribute and can stop the # recursive calls and return the value of the attribute return new_r } } ### print_violations ### # Prints violations returned by any of the filter functions defined below. # This would normally only be called if the filter function had been called # with prtmsg set to false, which is sometimes done when processing resources # and their blocks. # If the result of a filter function is assigned to a map like violatingIRs, # then you should pass violatingIRs["message"] as the first argument. # The prefix argument is printed before the message of each resource. print_violations = func(messages, prefix) { for messages as address, message { print(prefix, message) } return true } ### filter_attribute_not_in_list ### # Filter a list of resources to those with a specified # attribute (attr) that is 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_attribute_not_in_list = func(resources, attr, allowed, prtmsg) { violators = {} messages = {} for resources as address, rc { # Evaluate the value (v) of the attribute v = evaluate_attribute(rc, attr) else null # Convert null to "null" if v is null { v = "null" } # Check if the value is not in the allowed list if v not in allowed { # Add the resource and a warning message to the violators list message = to_string(address) + " has " + to_string(attr) + " with value " + to_string(v) + " that is not in the allowed list: " + to_string(allowed) violators[address] = rc messages[address] = message if prtmsg { print(message) } } // end if } // end for return {"resources":violators,"messages":messages} } ### filter_attribute_in_list ### # Filter a list of resources to those with a specified # attribute (attr) that is 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 disallow null, include "null" in the list (forbidden). filter_attribute_in_list = func(resources, attr, forbidden, prtmsg) { violators = {} messages = {} for resources as address, rc { # Evaluate the value (v) of the attribute v = evaluate_attribute(rc, attr) else null # Check if the value is null if v is null { v = "null" } # Check if the value is in the forbidden list if v in forbidden { # Add the resource and a warning message to the violators list message = to_string(address) + " has " + to_string(attr) + " with value " + to_string(v) + " that is in the forbidden list: " + to_string(forbidden) violators[address] = rc messages[address] = message if prtmsg { print(message) } } } return {"resources":violators,"messages":messages} } ### filter_attribute_not_contains_list ### # Filter a list of resources to those with a specified # attribute (attr) that does not contain a given list of required values (required). # Set prtmsg to `true` (without quotes) if you want to print violation messages. # Resources should be derived by applying filters to tfplan.resource_changes. filter_attribute_not_contains_list = func(resources, attr, required, prtmsg) { violators = {} messages = {} for resources as address, rc { # Evaluate the value (v) of the attribute v = evaluate_attribute(rc, attr) else null # Check if the value contains the desired allowed list if v is null or not (types.type_of(v) is "list" or types.type_of(v) is "map") { # Add the resource and a warning message to the violators list message = to_string(address) + " has " + to_string(attr) + " that is missing, null, or is not a map or a list. " + "It should have had these items: " + to_string(required) violators[address] = rc messages[address] = message if prtmsg { print(message) } } else { missing_values = [] for required as rv { if v not contains rv { append(missing_values, rv) } // end if } // end for required if length(missing_values) > 0 { # Build warning message when v is a map message = to_string(address) + " has " + to_string(attr) + " with value " + to_string(v) + " that is missing the required items " + to_string(missing_values) + " from the list: " + to_string(required) # Add the resource and warning message to the violators list violators[address] = rc messages[address] = message if prtmsg { print(message) } } // end length(missing_values) } // end else v not null } // end for return {"resources":violators,"messages":messages} } ### filter_attribute_contains_items_from_list ### # Filter a list of resources to those with a specified # attribute (attr) that contains any items from 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 disallow null, include "null" in the list (forbidden). filter_attribute_contains_items_from_list = func(resources, attr, forbidden, prtmsg) { violators = {} messages = {} for resources as address, rc { # Evaluate the value (v) of the attribute v = evaluate_attribute(rc, attr) else null # Check if the value contains the desired allowed list if not (types.type_of(v) is "list" or types.type_of(v) is "map" or types.type_of(v) is "null") { # Add the resource and a warning message to the violators list message = to_string(address) + " has " + to_string(attr) + " with value " + to_string(v) + " that is not a map, a list, or null" violators[address] = rc messages[address] = message if prtmsg { print(message) } } else if v is null { if "null" in forbidden { # Add the resource and a warning message to the violators list message = to_string(address) + " has " + to_string(attr) + " with value null from the forbidden list: " + to_string(forbidden) violators[address] = rc messages[address] = message if prtmsg { print(message) } } } else { forbidden_values = [] for forbidden as fv { if v contains fv { append(forbidden_values, fv) } // end if } // end for forbidden if length(forbidden_values) > 0 { # Build warning message when v is a list message = to_string(address) + " has " + to_string(attr) + " with value " + to_string(v) + " that has items " + to_string(forbidden_values) + " from the forbidden list: " + 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 v not null } // end for return {"resources":violators,"messages":messages} } ### filter_attribute_contains_items_not_in_list ### # Filter a list of resources to those with a specified # attribute (attr) that contains items not in a given list of allowed values. # 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_attribute_contains_items_not_in_list = func(resources, attr, allowed, prtmsg) { violators = {} messages = {} for resources as address, rc { # Evaluate the value (vals) of the attribute vals = evaluate_attribute(rc, attr) else null # Check if the value contains items not in allowed list if not (types.type_of(vals) is "list" or 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 = to_string(address) + " has " + to_string(attr) + " with value " + to_string(vals) + " that is not a map, a list, 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 = to_string(address) + " has " + to_string(attr) + " with value null that is not in the allowed list: " + to_string(allowed) violators[address] = rc messages[address] = message if prtmsg { print(message) } } } else { forbidden_values = [] for vals 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 = to_string(address) + " has " + to_string(attr) + " with value " + to_string(vals) + " with items " + to_string(forbidden_values) + " that are not in the allowed list: " + 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} } ### filter_attribute_is_not_value ### # Filter a list of resources to those with a specified # attribute (attr) that does not have a given value. # Resources should be derived by applying filters to tfplan.resource_changes. # Set prtmsg to `true` (without quotes) if you want to print violation messages. filter_attribute_is_not_value = func(resources, attr, value, prtmsg) { violators = {} messages = {} for resources as address, rc { v = evaluate_attribute(rc, attr) else null if v is null { # Add the resource and a warning message to the violators list message = to_string(address) + " has " + to_string(attr) + " that is null or undefined. " + "It is supposed to be " + to_string(value) violators[address] = rc messages[address] = message if prtmsg { print(message) } } else if v is not value { # Add the resource and a warning message to the violators list message = to_string(address) + " has " + to_string(attr) + " with value " + to_string(v) + " that is not equal to " + to_string(value) violators[address] = rc messages[address] = message if prtmsg { print(message) } } } return {"resources":violators,"messages":messages} } ### filter_attribute_was_not_value ### # Filter a list of resources to those with a specified # attribute (attr) that did not have a given value. # 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 match null, set value to "null". # Note that it this function passes `rc.change.before` instead of `rc` # to the evaluate_attribute() function which converts `rc` to `rc.change.after`. filter_attribute_was_not_value = func(resources, attr, value, prtmsg) { violators = {} messages = {} for resources as address, rc { v = evaluate_attribute(rc.change.before, attr) else null if v is null { # Add the resource and a warning message to the violators list message = to_string(address) + " had " + to_string(attr) + " that was null or undefined. " + "It was supposed to be " + to_string(value) violators[address] = rc messages[address] = message if prtmsg { print(message) } } else if v is not value { # Add the resource and a warning message to the violators list message = to_string(address) + " had " + to_string(attr) + " with value " + to_string(v) + " that was not equal to " + to_string(value) violators[address] = rc messages[address] = message if prtmsg { print(message) } } } return {"resources":violators,"messages":messages} } ### filter_attribute_is_value ### # Filter a list of resources to those with a specified # attribute (attr) that has a given value. # 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 match null, set value to "null". filter_attribute_is_value = func(resources, attr, value, prtmsg) { violators = {} messages = {} for resources as address, rc { v = evaluate_attribute(rc, attr) else null if v is null { v = "null" } if v is value { # Add the resource and a warning message to the violators list message = to_string(address) + " has " + to_string(attr) + " with value " + to_string(v) + " that is not allowed." violators[address] = rc messages[address] = message if prtmsg { print(message) } } } return {"resources":violators,"messages":messages} } ### filter_attribute_was_value ### # Filter a list of resources to those with a specified # attribute (attr) that had a given value. # 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 match null, set value to "null". # Note that it this function passes `rc.change.before` instead of `rc` # to the evaluate_attribute() function which converts `rc` to `rc.change.after`. filter_attribute_was_value = func(resources, attr, value, prtmsg) { violators = {} messages = {} for resources as address, rc { v = evaluate_attribute(rc.change.before, attr) else null if v is null { v = "null" } if v is value { # Add the resource and a warning message to the violators list message = to_string(address) + " had " + to_string(attr) + " with value " + to_string(v) + " that was not allowed." violators[address] = rc messages[address] = message if prtmsg { print(message) } } } return {"resources":violators,"messages":messages} } ### filter_attribute_greater_than_value ### # Filter a list of resources to those with a specified # attribute (attr) that is greater than a given numeric value. # Resources should be derived by applying filters to tfplan.resource_changes. # Set prtmsg to `true` (without quotes) if you want to print violation messages. filter_attribute_greater_than_value = func(resources, attr, value, prtmsg) { violators = {} messages = {} for resources as address, rc { v = evaluate_attribute(rc, attr) else null if float(v) else null is null { # Add the resource and a warning message to the violators list message = to_string(address) + " has " + to_string(attr) + " that is null or undefined. " + "It is supposed to be less " + "than or equal to " + to_string(value) violators[address] = rc messages[address] = message if prtmsg { print(message) } } else if float(v) > value { # Add the resource and a warning message to the violators list message = to_string(address) + " has " + to_string(attr) + " with value " + to_string(v) + " that is greater than " + to_string(value) violators[address] = rc messages[address] = message if prtmsg { print(message) } } } return {"resources":violators,"messages":messages} } ### filter_attribute_greater_than_equal_to_value ### # Filter a list of resources to those with a specified # attribute (attr) that is greater than a given numeric value. # Resources should be derived by applying filters to tfplan.resource_changes. # Set prtmsg to `true` (without quotes) if you want to print violation messages. filter_attribute_greater_than_equal_to_value = func(resources, attr, value, prtmsg) { violators = {} messages = {} for resources as address, rc { v = evaluate_attribute(rc, attr) else null if float(v) else null is null { # Add the resource and a warning message to the violators list message = to_string(address) + " has " + to_string(attr) + " that is null or undefined. " + "It is supposed to be less " + "than " + to_string(value) violators[address] = rc messages[address] = message if prtmsg { print(message) } } else if float(v) >= value { # Add the resource and a warning message to the violators list message = to_string(address) + " has " + to_string(attr) + " with value " + to_string(v) + " that is greater than or equal to " + to_string(value) violators[address] = rc messages[address] = message if prtmsg { print(message) } } } return {"resources":violators,"messages":messages} } ### filter_attribute_less_than_value ### # Filter a list of resources to those with a specified # attribute (attr) that is less than a given numeric value. # Resources should be derived by applying filters to tfplan.resource_changes. # Set prtmsg to `true` (without quotes) if you want to print violation messages. filter_attribute_less_than_value = func(resources, attr, value, prtmsg) { violators = {} messages = {} for resources as address, rc { v = evaluate_attribute(rc, attr) else null if float(v) else null is null { # Add the resource and a warning message to the violators list message = to_string(address) + " has " + to_string(attr) + " that is null or undefined. " + "It is supposed to be greater " + "than or equal to " + to_string(value) violators[address] = rc messages[address] = message if prtmsg { print(message) } } else if float(v) < value { # Add the resource and a warning message to the violators list message = to_string(address) + " has " + to_string(attr) + " with value " + to_string(v) + " that is less than " + to_string(value) violators[address] = rc messages[address] = message if prtmsg { print(message) } } } return {"resources":violators,"messages":messages} } ### filter_attribute_less_than_equal_to_value ### # Filter a list of resources to those with a specified # attribute (attr) that is less than a given numeric value. # Resources should be derived by applying filters to tfplan.resource_changes. # Set prtmsg to `true` (without quotes) if you want to print violation messages. filter_attribute_less_than_equal_to_value = func(resources, attr, value, prtmsg) { violators = {} messages = {} for resources as address, rc { v = evaluate_attribute(rc, attr) else null if float(v) else null is null { # Add the resource and a warning message to the violators list message = to_string(address) + " has " + to_string(attr) + " that is null or undefined. " + "It is supposed to be greater " + "than " + to_string(value) violators[address] = rc messages[address] = message if prtmsg { print(message) } } else if float(v) <= value { # Add the resource and a warning message to the violators list message = to_string(address) + " has " + to_string(attr) + " with value " + to_string(v) + " that is less than or equal to " + to_string(value) violators[address] = rc messages[address] = message if prtmsg { print(message) } } } return {"resources":violators,"messages":messages} } ### filter_attribute_does_not_match_regex ### # Filter a list of resources to those with a specified # attribute (attr) that does not match a regular expression (expr). # 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, set `expr` to "(|null)". filter_attribute_does_not_match_regex = func(resources, attr, expr, prtmsg) { violators = {} messages = {} for resources as address, rc { v = evaluate_attribute(rc, attr) else null if v is null { v = "null" } if v not matches expr { # Add the resource and a warning message to the violators list message = to_string(address) + " has " + to_string(attr) + " with value " + to_string(v) + " that does not match the regex " + to_string(expr) violators[address] = rc messages[address] = message if prtmsg { print(message) } } } return {"resources":violators,"messages":messages} } ### filter_attribute_matches_regex ### # Filter a list of resources to those with a specified # attribute (attr) that matches a regular expression (expr). # 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 match null, set expr to "null". filter_attribute_matches_regex = func(resources, attr, expr, prtmsg) { violators = {} messages = {} for resources as address, rc { v = evaluate_attribute(rc, attr) else null if v is null { v = "null" } if v matches expr { # Add the resource and a warning message to the violators list message = to_string(address) + " has " + to_string(attr) + " with value " + to_string(v) + " that matches the regex " + to_string(expr) violators[address] = rc messages[address] = message if prtmsg { print(message) } } } return {"resources":violators,"messages":messages} } ### filter_attribute_does_not_have_prefix ### # Filter a list of resources to those with a specified # attribute (attr) that does not have a given prefix. # Resources should be derived by applying filters to tfplan.resource_changes. # Set prtmsg to `true` (without quotes) if you want to print violation messages. filter_attribute_does_not_have_prefix = func(resources, attr, prefix, prtmsg) { violators = {} messages = {} for resources as address, rc { v = evaluate_attribute(rc, attr) else null if v is null { # Add the resource and a warning message to the violators list message = to_string(address) + " has " + to_string(attr) + " that is null or undefined. " + "It must have a value that " + "starts with " + to_string(prefix) violators[address] = rc messages[address] = message if prtmsg { print(message) } } else if not strings.has_prefix(v, prefix) { # Add the resource and a warning message to the violators list message = to_string(address) + " has " + to_string(attr) + " with value " + to_string(v) + " that does not have the prefix " + to_string(prefix) violators[address] = rc messages[address] = message if prtmsg { print(message) } } } return {"resources":violators,"messages":messages} } ### filter_attribute_has_prefix ### # Filter a list of resources to those with a specified # attribute (attr) that has a given prefix. # Resources should be derived by applying filters to tfplan.resource_changes. # Set prtmsg to `true` (without quotes) if you want to print violation messages. filter_attribute_has_prefix = func(resources, attr, prefix, prtmsg) { violators = {} messages = {} for resources as address, rc { v = evaluate_attribute(rc, attr) else null if v is null { # Add the resource and a warning message to the violators list message = to_string(address) + " has " + to_string(attr) + " that is null or undefined. " + "It must have a value that " + "does not start with " + to_string(prefix) violators[address] = rc messages[address] = message if prtmsg { print(message) } } else if strings.has_prefix(v, prefix) { # Add the resource and a warning message to the violators list message = to_string(address) + " has " + to_string(attr) + " with value " + to_string(v) + " that has the prefix " + to_string(prefix) violators[address] = rc messages[address] = message if prtmsg { print(message) } } } return {"resources":violators,"messages":messages} } ### filter_attribute_does_not_have_suffix ### # Filter a list of resources to those with a specified # attribute (attr) that does not have a given suffix. # Resources should be derived by applying filters to tfplan.resource_changes. # Set prtmsg to `true` (without quotes) if you want to print violation messages. filter_attribute_does_not_have_suffix = func(resources, attr, suffix, prtmsg) { violators = {} messages = {} for resources as address, rc { v = evaluate_attribute(rc, attr) else null if v is null { # Add the resource and a warning message to the violators list message = to_string(address) + " has " + to_string(attr) + " that is null or undefined. " + "It must have a value that " + "ends with " + to_string(prefix) violators[address] = rc messages[address] = message if prtmsg { print(message) } } else if not strings.has_suffix(v, suffix) { # Add the resource and a warning message to the violators list message = to_string(address) + " has " + to_string(attr) + " with value " + to_string(v) + " that does not have the suffix " + to_string(suffix) violators[address] = rc messages[address] = message if prtmsg { print(message) } } } return {"resources":violators,"messages":messages} } ### filter_attribute_has_suffix ### # Filter a list of resources to those with a specified # attribute (attr) that has a given suffix. # Resources should be derived by applying filters to tfplan.resource_changes. # Set prtmsg to `true` (without quotes) if you want to print violation messages. filter_attribute_has_suffix = func(resources, attr, suffix, prtmsg) { violators = {} messages = {} for resources as address, rc { v = evaluate_attribute(rc, attr) else null if v is null { # Add the resource and a warning message to the violators list message = to_string(address) + " has " + to_string(attr) + " that is null or undefined. " + "It must have a value that " + "does not end with " + to_string(suffix) violators[address] = rc messages[address] = message if prtmsg { print(message) } } else if strings.has_suffix(v, suffix) { # Add the resource and a warning message to the violators list message = to_string(address) + " has " + to_string(attr) + " with value " + to_string(v) + " that has the suffix " + to_string(suffix) violators[address] = rc messages[address] = message if prtmsg { print(message) } } } return {"resources":violators,"messages":messages} } ### filter_attribute_map_key_contains_items_in_list ### # Filter a list of resources to those with a specified map # attribute (attr) with key (key) that contains items in a given list of forbidden values (forbidden). This list should have strings, numbers, or booleans. # The key should either be a simple type like a string or number or a list. # 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 the attribute is null or does not contain the key, no violation is generated. filter_attribute_map_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 = 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 = to_string(address) + " has " + to_string(attr) + " with value " + to_string(vals) + " that is not a map or null" violators[address] = rc messages[address] = message if prtmsg { print(message) } } else if vals is not null { # Check if vals contains the desired key if key in keys(vals) { # Get the value of the key key_value = vals[key] forbidden_values = [] if types.type_of(key_value) not in ["list", "map"] { if key_value in forbidden { append(forbidden_values, key_value) } } else if types.type_of(key_value) is "list" { for key_value as v { if v in forbidden { append(forbidden_values, v) } // end if v forbidden } // end for key_value } else if types.type_of(key_value) is "map" { # Ignore maps } // end key_value type if length(forbidden_values) > 0 { # Build warning message when vals is a map message = to_string(address) + " has " + to_string(attr) + "with value " + to_string(vals) + " with key " + to_string(key) + " containing items " + to_string(forbidden_values) + " that are in the forbidden list: " + 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 not null } // end for return {"resources": violators, "messages": messages} } ### filter_attribute_map_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 the attribute is null or does not contain the key, no violation is generated. filter_attribute_map_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 = 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 = to_string(address) + " has attribute " + to_string(attr) + " with value " + to_string(vals) + " that is not a map or null" violators[address] = rc messages[address] = message if prtmsg { print(message) } } else if vals is not null { # Check if vals contains the desired key if key in keys(vals) { # Get the value of the key key_value = vals[key] forbidden_values = [] if types.type_of(key_value) not in ["list", "map"] { if key_value not in allowed { append(forbidden_values, key_value) } } else if types.type_of(key_value) is "list" { for key_value as v { if v not in allowed { append(forbidden_values, v) } // end if v not allowed } // end for key_value } else if types.type_of(key_value) is "map" { # Ignore maps } // end key_value type if length(forbidden_values) > 0 { # Build warning message when vals is a map message = to_string(address) + " has attribute " + to_string(attr) + " with key " + to_string(key) + " containing items " + to_string(forbidden_values) + " that are not in the allowed list: " + 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 not null } // end for return {"resources": violators, "messages": messages} } ### find_resources with name ### # Find all resources of a specific type & name using the tfplan/v2 import. # Only include resources that are being created or updated. # Technically, this returns a map of resource changes. find_resources_with_name = func(type, name) { resources = filter tfplan.resource_changes as address, rc { rc.type is type and rc.name is name and rc.mode is "managed" and (rc.change.actions contains "create" or rc.change.actions contains "update" or rc.change.actions contains "read" or rc.change.actions contains "no-op") } return resources }