"""
Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
SPDX-License-Identifier: MIT-0
"""
import cfnlint.helpers
from cfnlint.helpers import RESOURCE_SPECS
from cfnlint.rules import CloudFormationLintRule, RuleMatch
class ValueRefGetAtt(CloudFormationLintRule):
"""Check if Resource Properties are correct"""
id = "E3008"
shortdesc = "Check values of properties for valid Refs and GetAtts"
description = "Checks resource properties for Ref and GetAtt values"
tags = ["resources", "ref", "getatt"]
def initialize(self, cfn):
"""Initialize the rule"""
for resource_type_spec in RESOURCE_SPECS.get(cfn.regions[0]).get(
"ResourceTypes"
):
self.resource_property_types.append(resource_type_spec)
for property_type_spec in RESOURCE_SPECS.get(cfn.regions[0]).get(
"PropertyTypes"
):
self.resource_sub_property_types.append(property_type_spec)
def is_value_a_list(self, path, property_name):
"""
Determines if the value checked is a list or a value in a list
We need to handle conditions in the path that could be nested, etc.
['Resources', 'LoadBalancer', 'Properties', 'Subnets', 'Fn::If', 2, 'Fn::If', 2]
Numbers preceeded by a Fn::If should be removed and check repeated.
"""
if path[-1] != property_name:
# Property doesn't match the property name
# Check if its a number and a condition
if isinstance(path[-1], int) and path[-2] == "Fn::If":
return self.is_value_a_list(path[:-2], property_name)
return False
return True
def check_value_ref(self, value, path, **kwargs):
"""Check Ref"""
matches = []
cfn = kwargs.get("cfn")
value_specs = kwargs.get("value_specs", {}).get("Ref")
list_value_specs = kwargs.get("list_value_specs", {}).get("Ref")
property_type = kwargs.get("property_type")
property_name = kwargs.get("property_name")
if (
path[-1] == "Ref"
and property_type == "List"
and self.is_value_a_list(path[:-1], property_name)
):
specs = list_value_specs
else:
specs = value_specs
if not specs:
# If no Ref's are specified, just skip
# Opposite of GetAtt you will always have a Ref to a Parameter so if this is
# None it just hasn't been defined and we can skip
return matches
if value in cfn.template.get("Parameters", {}):
param = cfn.template.get("Parameters").get(value, {})
parameter_type = param.get("Type")
valid_parameter_types = []
for parameter in specs.get("Parameters"):
for param_type in (
RESOURCE_SPECS.get(cfn.regions[0])
.get("ParameterTypes")
.get(parameter)
):
valid_parameter_types.append(param_type)
if not specs.get("Parameters"):
message = 'Property "{0}" has no valid Refs to Parameters at {1}'
matches.append(
RuleMatch(
path, message.format(property_name, "/".join(map(str, path)))
)
)
elif parameter_type not in valid_parameter_types:
message = 'Property "{0}" can Ref to parameter of types [{1}] at {2}'
matches.append(
RuleMatch(
path,
message.format(
property_name,
", ".join(map(str, valid_parameter_types)),
"/".join(map(str, path)),
),
)
)
if value in cfn.template.get("Resources", {}):
resource = cfn.template.get("Resources").get(value, {})
resource_type = resource.get("Type")
if not specs.get("Resources"):
message = 'Property "{0}" has no valid Refs to Resources at {1}'
matches.append(
RuleMatch(
path, message.format(property_name, "/".join(map(str, path)))
)
)
elif resource_type not in specs.get("Resources"):
message = 'Property "{0}" can Ref to resources of types [{1}] at {2}'
matches.append(
RuleMatch(
path,
message.format(
property_name,
", ".join(map(str, specs.get("Resources"))),
"/".join(map(str, path)),
),
)
)
return matches
def check_value_getatt(self, value, path, **kwargs):
"""Check GetAtt"""
matches = []
cfn = kwargs.get("cfn")
value_specs = kwargs.get("value_specs", {}).get("GetAtt")
list_value_specs = kwargs.get("list_value_specs", {}).get("GetAtt")
property_type = kwargs.get("property_type")
property_name = kwargs.get("property_name")
# You can sometimes get a list or a string with . in it
if isinstance(value, list):
resource_name = value[0]
if len(value[1:]) == 1:
resource_attribute = value[1].split(".")
else:
resource_attribute = value[1:]
elif isinstance(value, str):
resource_name = value.split(".")[0]
resource_attribute = value.split(".")[1:]
is_value_a_list = self.is_value_a_list(path[:-1], property_name)
if path[-1] == "Fn::GetAtt" and property_type == "List" and is_value_a_list:
specs = list_value_specs
else:
specs = value_specs
resource_type = (
cfn.template.get("Resources", {}).get(resource_name, {}).get("Type")
)
if cfnlint.helpers.is_custom_resource(resource_type):
# A custom resource voids the spec. Move on
return matches
if (
resource_type
in [
"AWS::CloudFormation::Stack",
"AWS::ServiceCatalog::CloudFormationProvisionedProduct",
]
and resource_attribute[0] == "Outputs"
):
# Nested Stack Outputs
# if its a string type we are good and return matches
# if its a list its a failure as Outputs can only be strings
if is_value_a_list and property_type == "List":
message = (
"CloudFormation stack outputs need to be strings not lists at {0}"
)
matches.append(
RuleMatch(path, message.format("/".join(map(str, path))))
)
return matches
if specs is None:
# GetAtt specs aren't specified skip
return matches
if not specs:
# GetAtt is specified but empty so there are no valid options
message = 'Property "{0}" has no valid Fn::GetAtt options at {1}'
matches.append(
RuleMatch(path, message.format(property_name, "/".join(map(str, path))))
)
return matches
if resource_type not in specs:
message = (
'Property "{0}" can Fn::GetAtt to a resource of types [{1}] at {2}'
)
matches.append(
RuleMatch(
path,
message.format(
property_name,
", ".join(map(str, specs)),
"/".join(map(str, path)),
),
)
)
elif isinstance(specs[resource_type], list):
found = False
for allowed_att in specs[resource_type]:
if ".".join(map(str, resource_attribute)) == allowed_att:
found = True
if not found:
message = (
'Property "{0}" can Fn::GetAtt to a resource attribute "{1}" at {2}'
)
matches.append(
RuleMatch(
path,
message.format(
property_name,
specs[resource_type],
"/".join(map(str, path)),
),
)
)
elif ".".join(map(str, resource_attribute)) != specs[resource_type]:
message = (
'Property "{0}" can Fn::GetAtt to a resource attribute "{1}" at {2}'
)
matches.append(
RuleMatch(
path,
message.format(
property_name, specs[resource_type], "/".join(map(str, path))
),
)
)
return matches
def _get_value_specs(self, value_type, region):
value_specs = RESOURCE_SPECS.get(region).get("ValueTypes").get(value_type, {})
if value_specs == "CACHED":
value_specs = (
RESOURCE_SPECS.get("us-east-1").get("ValueTypes").get(value_type, {})
)
return value_specs
def check(self, cfn, properties, value_specs, property_specs, path):
"""Check itself"""
matches = []
for p_value, p_path in properties.items_safe(path[:]):
for prop in p_value:
if prop in value_specs:
value = value_specs.get(prop).get("Value", {})
if value:
value_type = value.get("ValueType", "")
list_value_type = value.get("ListValueType", "")
property_type = (
property_specs.get("Properties").get(prop).get("Type")
)
value_type_specs = self._get_value_specs(
value_type, cfn.regions[0]
)
list_value_specs = self._get_value_specs(
list_value_type, cfn.regions[0]
)
matches.extend(
cfn.check_value(
p_value,
prop,
p_path,
check_ref=self.check_value_ref,
check_get_att=self.check_value_getatt,
value_specs=value_type_specs,
list_value_specs=list_value_specs,
cfn=cfn,
property_type=property_type,
property_name=prop,
)
)
return matches
def match_resource_sub_properties(self, properties, property_type, path, cfn):
"""Match for sub properties"""
matches = []
specs = (
RESOURCE_SPECS.get(cfn.regions[0])
.get("PropertyTypes")
.get(property_type, {})
.get("Properties", {})
)
property_specs = (
RESOURCE_SPECS.get(cfn.regions[0]).get("PropertyTypes").get(property_type)
)
matches.extend(self.check(cfn, properties, specs, property_specs, path))
return matches
def match_resource_properties(self, properties, resource_type, path, cfn):
"""Check CloudFormation Properties"""
matches = []
specs = (
RESOURCE_SPECS.get(cfn.regions[0])
.get("ResourceTypes")
.get(resource_type, {})
.get("Properties", {})
)
resource_specs = (
RESOURCE_SPECS.get(cfn.regions[0]).get("ResourceTypes").get(resource_type)
)
matches.extend(self.check(cfn, properties, specs, resource_specs, path))
return matches