"""
Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
SPDX-License-Identifier: MIT-0
"""
import regex as re
# pylint: disable=cyclic-import
import cfnlint.rules
OPERATOR = [
"EQUALS",
"NOT_EQUALS",
"REGEX_MATCH",
"==",
"!=",
"IN",
"NOT_IN",
">",
"<",
">=",
"<=",
"IS DEFINED",
"IS NOT_DEFINED",
]
def CreateCustomRule(
rule_id, resourceType, prop, value, error_message, description, shortdesc, rule_func
):
class CustomRule(cfnlint.rules.CloudFormationLintRule):
def __init__(
self,
rule_id,
resourceType,
prop,
value,
error_message,
description,
shortdesc,
rule_func,
):
super().__init__()
self.resource_property_types.append(resourceType)
self.id = rule_id
self.property_chain = prop.split(".")
self.property_value = value
self.error_message = error_message
self.description = description
self.shortdesc = shortdesc
self.rule_func = rule_func
def _remaining_inset_properties(self, property_chain):
if len(property_chain) > 1:
return property_chain[1:]
return []
def _check_value(self, value, path, property_chain, cfn):
matches = []
if property_chain:
new_property_chain = self._remaining_inset_properties(property_chain)
matches.extend(
cfn.check_value(
value,
property_chain[0],
path,
check_value=self._check_value,
property_chain=new_property_chain,
cfn=cfn,
)
)
return matches
if value is not None:
matches.extend(self.rule_func(value, self.property_value, path))
return matches
def match_resource_properties(self, properties, _, path, cfn):
new_property_chain = self._remaining_inset_properties(self.property_chain)
return cfn.check_value(
properties,
self.property_chain[0],
path,
check_value=self._check_value,
property_chain=new_property_chain,
cfn=cfn,
)
return CustomRule(
rule_id,
resourceType,
prop,
value,
error_message,
description,
shortdesc,
rule_func,
)
def CreateCustomIsDefinedRule(rule_id, resourceType, prop, value, error_message):
class CustomIsDefinedRule(cfnlint.rules.CloudFormationLintRule):
def __init__(
self,
rule_id,
resourceType,
prop,
value,
error_message,
description,
shortdesc,
):
super().__init__()
self.id = rule_id
self.resource_property_types.append(resourceType)
self.property_chain = prop.split(".")
if value == "DEFINED":
self.is_defined = True
elif value == "NOT_DEFINED":
self.is_defined = False
else:
raise ValueError("IS must follow either DEFINED or NOT_DEFINED")
self.error_message = error_message
self.description = description
self.shortdesc = shortdesc
def _split_inset_properties(self, property_chain):
if property_chain:
if len(property_chain) > 1:
return property_chain[0], property_chain[1:]
return property_chain[0], []
return None, []
def _check_value_defined(self, value, path, property_chain, cfn, **_):
matches = []
child_property, new_property_chain = self._split_inset_properties(
property_chain
)
# Specific for !Ref AWS::NoValue, no callback even for pass_if_null=True
if len(property_chain) == 1 and value == {
child_property: {"Ref": "AWS::NoValue"}
}:
matches.append(
cfnlint.rules.RuleMatch(
path, error_message or f"{path} must be defined"
)
)
return matches
if value is None or (
isinstance(value, dict) and value.get("Ref", None) == "AWS::NoValue"
):
matches.append(
cfnlint.rules.RuleMatch(
path, error_message or f"{path} must be defined"
)
)
return matches
if child_property is not None:
matches.extend(
cfn.check_value(
value,
child_property,
path,
check_value=self._check_value_defined,
check_ref=self._check_value_defined,
check_get_att=self._check_value_defined,
check_find_in_map=self._check_value_defined,
check_split=self._check_value_defined,
check_join=self._check_value_defined,
check_import_value=self._check_value_defined,
check_sub=self._check_value_defined,
pass_if_null=True,
property_chain=new_property_chain,
cfn=cfn,
)
)
return matches
def _check_value_not_defined(self, value, path, property_chain, cfn, **_):
matches = []
if value is None:
return matches
if len(property_chain) == 0 and value is not None:
matches.append(
cfnlint.rules.RuleMatch(
path, error_message or f"{path} must not be defined"
)
)
return matches
child_property, new_property_chain = self._split_inset_properties(
property_chain
)
matches.extend(
cfn.check_value(
value,
child_property,
path,
check_value=self._check_value_not_defined,
check_ref=self._check_value_not_defined,
check_get_att=self._check_value_not_defined,
check_find_in_map=self._check_value_not_defined,
check_split=self._check_value_not_defined,
check_join=self._check_value_not_defined,
check_import_value=self._check_value_not_defined,
check_sub=self._check_value_not_defined,
property_chain=new_property_chain,
cfn=cfn,
pass_if_null=True,
)
)
return matches
def match_resource_properties(self, properties, _, path, cfn):
child_property, new_property_chain = self._split_inset_properties(
self.property_chain
)
check_fn = (
self._check_value_defined
if self.is_defined
else self._check_value_not_defined
)
matches = []
# here does nothing when the value is not defined, this is checked separately below
matches.extend(
cfn.check_value(
properties,
child_property,
path,
check_value=check_fn,
check_ref=check_fn,
check_get_att=check_fn,
check_find_in_map=check_fn,
check_split=check_fn,
check_join=check_fn,
check_import_value=check_fn,
check_sub=check_fn,
property_chain=new_property_chain,
cfn=cfn,
pass_if_null=True,
)
)
return matches
return CustomIsDefinedRule(
rule_id,
resourceType,
prop,
value,
error_message,
shortdesc=f"Custom rule to check for value is {value}",
description=f"Created from the custom rules parameter. This rule will check if a property value is {value}",
)
def CreateEqualsRule(rule_id, resourceType, prop, value, error_message):
def rule_func(value, expected_value, path):
matches = []
if str(value).strip().lower() != str(expected_value).strip().lower():
matches.append(
cfnlint.rules.RuleMatch(
path, error_message or "Must equal check failed"
)
)
return matches
return CreateCustomRule(
rule_id,
resourceType,
prop,
value,
error_message,
shortdesc="Custom rule to check for equal values",
description="Created from the custom rules parameter. This rule will check if a property value is equal to the specified value.",
rule_func=rule_func,
)
def CreateNotEqualsRule(rule_id, resourceType, prop, value, error_message):
def rule_func(value, expected_values, path):
matches = []
if str(value).strip().lower() == str(expected_values).strip().lower():
matches.append(
cfnlint.rules.RuleMatch(
path, error_message or "Must not equal check failed"
)
)
return matches
return CreateCustomRule(
rule_id,
resourceType,
prop,
value,
error_message,
shortdesc="Custom rule to check for not equal values",
description="Created from the custom rules parameter. This rule will check if a property value is NOT equal to the specified value.",
rule_func=rule_func,
)
def CreateRegexMatchRule(rule_id, resourceType, prop, value, error_message):
def rule_func(value, expected_values, path):
matches = []
if not re.match(expected_values.strip(), str(value).strip()):
matches.append(
cfnlint.rules.RuleMatch(path, error_message or "Regex does not match")
)
return matches
return CreateCustomRule(
rule_id,
resourceType,
prop,
value,
error_message,
shortdesc="Custom rule to check for regex match",
description="Created from the custom rules parameter. This rule will check if a property value match the provided regex pattern.",
rule_func=rule_func,
)
def CreateGreaterEqualRule(rule_id, resourceType, prop, value, error_message):
def rule_func(value, expected_value, path):
matches = []
if checkInt(str(value).strip()) and checkInt(str(expected_value).strip()):
if int(str(value).strip()) < int(str(expected_value).strip()):
matches.append(
cfnlint.rules.RuleMatch(
path, error_message or "Greater than check failed"
)
)
else:
matches.append(
cfnlint.rules.RuleMatch(
path, error_message or "Given values are not numeric"
)
)
return matches
return CreateCustomRule(
rule_id,
resourceType,
prop,
value,
error_message,
shortdesc="Custom rule to check for if a value is greater than the specified value",
description="Created from the custom rules parameter. This rule will check if a property value is greater than the specified value.",
rule_func=rule_func,
)
def CreateGreaterRule(rule_id, resourceType, prop, value, error_message):
def rule_func(value, expected_value, path):
matches = []
if checkInt(str(value).strip()) and checkInt(str(expected_value).strip()):
if int(str(value).strip()) <= int(str(expected_value).strip()):
matches.append(
cfnlint.rules.RuleMatch(
path, error_message or "Greater than check failed"
)
)
else:
matches.append(
cfnlint.rules.RuleMatch(
path, error_message or "Given values are not numeric"
)
)
return matches
return CreateCustomRule(
rule_id,
resourceType,
prop,
value,
error_message,
shortdesc="Custom rule to check for if a value is greater than the specified value",
description="Created from the custom rules parameter. This rule will check if a property value is greater than the specified value.",
rule_func=rule_func,
)
def CreateLesserRule(rule_id, resourceType, prop, value, error_message):
def rule_func(value, expected_value, path):
matches = []
if checkInt(str(value).strip()) and checkInt(str(expected_value).strip()):
if int(str(value).strip()) >= int(str(expected_value).strip()):
matches.append(
cfnlint.rules.RuleMatch(
path, error_message or "Lesser than check failed"
)
)
else:
matches.append(
cfnlint.rules.RuleMatch(
path, error_message or "Given values are not numeric"
)
)
return matches
return CreateCustomRule(
rule_id,
resourceType,
prop,
value,
error_message,
shortdesc="Custom rule to check for if a value is lesser than the specified value",
description="Created from the custom rules parameter. This rule will check if a property value is lesser than the specified value.",
rule_func=rule_func,
)
def CreateLesserEqualRule(rule_id, resourceType, prop, value, error_message):
def rule_func(value, expected_value, path):
matches = []
if checkInt(str(value).strip()) and checkInt(str(expected_value).strip()):
if int(str(value).strip()) > int(str(expected_value).strip()):
matches.append(
cfnlint.rules.RuleMatch(
path, error_message or "Lesser than check failed"
)
)
else:
matches.append(
cfnlint.rules.RuleMatch(
path, error_message or "Given values are not numeric"
)
)
return matches
return CreateCustomRule(
rule_id,
resourceType,
prop,
value,
error_message,
shortdesc="Custom rule to check for if a value is lesser than the specified value",
description="Created from the custom rules parameter. This rule will check if a property value is lesser than the specified value.",
rule_func=rule_func,
)
def CreateInSetRule(rule_id, resourceType, prop, value, error_message):
def rule_func(value, expected_values, path):
matches = []
if value not in expected_values:
matches.append(
cfnlint.rules.RuleMatch(path, error_message or "In set check failed")
)
return matches
return CreateCustomRule(
rule_id,
resourceType,
prop,
value,
error_message,
shortdesc="Custom rule to check for if a value exists in a list of specified values",
description="Created from the custom rules parameter. This rule will check if a property value exists inside a list of specified values.",
rule_func=rule_func,
)
def CreateNotInSetRule(rule_id, resourceType, prop, value, error_message):
def rule_func(value, expected_values, path):
matches = []
if value in expected_values:
matches.append(
cfnlint.rules.RuleMatch(
path, error_message or "Not in set check failed"
)
)
return matches
return CreateCustomRule(
rule_id,
resourceType,
prop,
value,
error_message,
shortdesc="Custom rule to check for if a value does not exist in a list of specified values",
description="Created from the custom rules parameter. This rule will check if a property value does not exist inside a list of specified values.",
rule_func=rule_func,
)
def CreateInvalidRule(rule_id, operator):
class InvalidRule(cfnlint.rules.CloudFormationLintRule):
def __init__(self, rule_id, operator):
super().__init__()
self.id = rule_id
self.operator = operator
self.description = "Created from the custom rule parameter. This rule is the result of an invalid configuration of a custom rule."
self.shortdesc = "Invalid custom rule configuration"
def match(self, _):
message = '"{0}" not in supported operators: [{1}]'
return [
cfnlint.rules.RuleMatch(
[], message.format(str(self.operator), ", ".join(OPERATOR))
)
]
return InvalidRule(rule_id, operator)
def checkInt(i):
"""Python 2.7 Compatibility - There is no isnumeric() method"""
try:
int(i)
return True
except ValueError:
return False