Skip to content

Commit

Permalink
Upgrade more rules to v1 (#3243)
Browse files Browse the repository at this point in the history
* Upgrade more rules to v1
* Add more frozen sets to helpers
  • Loading branch information
kddejong committed May 14, 2024
1 parent d4eda4d commit 523e762
Show file tree
Hide file tree
Showing 15 changed files with 453 additions and 358 deletions.
193 changes: 113 additions & 80 deletions src/cfnlint/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -180,22 +180,30 @@
FUNCTION_BASE64 = "Fn::Base64"
FUNCTION_FOR_EACH = re.compile(r"^Fn::ForEach::[a-zA-Z0-9]+$")

FUNCTION_CONDITIONS = [FUNCTION_AND, FUNCTION_OR, FUNCTION_NOT, FUNCTION_EQUALS]
FUNCTION_CONDITIONS = frozenset(
[FUNCTION_AND, FUNCTION_OR, FUNCTION_NOT, FUNCTION_EQUALS]
)

PSEUDOPARAMS_SINGLE = [
"AWS::AccountId",
"AWS::Partition",
"AWS::Region",
"AWS::StackId",
"AWS::StackName",
"AWS::URLSuffix",
]
PSEUDOPARAMS_SINGLE = frozenset(
[
"AWS::AccountId",
"AWS::Partition",
"AWS::Region",
"AWS::StackId",
"AWS::StackName",
"AWS::URLSuffix",
]
)

PSEUDOPARAMS_MULTIPLE = [
"AWS::NotificationARNs",
]
PSEUDOPARAMS_MULTIPLE = frozenset(
[
"AWS::NotificationARNs",
]
)

PSEUDOPARAMS = ["AWS::NoValue"] + PSEUDOPARAMS_SINGLE + PSEUDOPARAMS_MULTIPLE
PSEUDOPARAMS = frozenset(
["AWS::NoValue"] + list(PSEUDOPARAMS_SINGLE) + list(PSEUDOPARAMS_MULTIPLE)
)

LIMITS = {
"Mappings": {"number": 200, "attributes": 200, "name": 255}, # in characters
Expand All @@ -214,73 +222,98 @@
"threshold": 0.9, # for rules about approaching the other limit values
}

valid_snapshot_types = [
"AWS::EC2::Volume",
"AWS::ElastiCache::CacheCluster",
"AWS::ElastiCache::ReplicationGroup",
"AWS::Neptune::DBCluster",
"AWS::RDS::DBCluster",
"AWS::RDS::DBInstance",
"AWS::Redshift::Cluster",
]

VALID_PARAMETER_TYPES_SINGLE = [
"AWS::EC2::AvailabilityZone::Name",
"AWS::EC2::Image::Id",
"AWS::EC2::Instance::Id",
"AWS::EC2::KeyPair::KeyName",
"AWS::EC2::SecurityGroup::GroupName",
"AWS::EC2::SecurityGroup::Id",
"AWS::EC2::Subnet::Id",
"AWS::EC2::VPC::Id",
"AWS::EC2::Volume::Id",
"AWS::Route53::HostedZone::Id",
"AWS::SSM::Parameter::Name",
"Number",
"String",
"AWS::SSM::Parameter::Value<AWS::EC2::AvailabilityZone::Name>",
"AWS::SSM::Parameter::Value<AWS::EC2::Image::Id>",
"AWS::SSM::Parameter::Value<AWS::EC2::Instance::Id>",
"AWS::SSM::Parameter::Value<AWS::EC2::KeyPair::KeyName>",
"AWS::SSM::Parameter::Value<AWS::EC2::SecurityGroup::GroupName>",
"AWS::SSM::Parameter::Value<AWS::EC2::SecurityGroup::Id>",
"AWS::SSM::Parameter::Value<AWS::EC2::Subnet::Id>",
"AWS::SSM::Parameter::Value<AWS::EC2::VPC::Id>",
"AWS::SSM::Parameter::Value<AWS::EC2::Volume::Id>",
"AWS::SSM::Parameter::Value<AWS::Route53::HostedZone::Id>",
"AWS::SSM::Parameter::Value<AWS::SSM::Parameter::Name>",
"AWS::SSM::Parameter::Value<Number>",
"AWS::SSM::Parameter::Value<String>",
]

VALID_PARAMETER_TYPES_LIST = [
"CommaDelimitedList",
"List<AWS::EC2::AvailabilityZone::Name>",
"List<AWS::EC2::Image::Id>",
"List<AWS::EC2::Instance::Id>",
"List<AWS::EC2::SecurityGroup::GroupName>",
"List<AWS::EC2::SecurityGroup::Id>",
"List<AWS::EC2::Subnet::Id>",
"List<AWS::EC2::VPC::Id>",
"List<AWS::EC2::Volume::Id>",
"List<AWS::Route53::HostedZone::Id>",
"List<Number>",
"List<String>",
"AWS::SSM::Parameter::Value<CommaDelimitedList>",
"AWS::SSM::Parameter::Value<List<AWS::EC2::AvailabilityZone::Name>>",
"AWS::SSM::Parameter::Value<List<AWS::EC2::Image::Id>>",
"AWS::SSM::Parameter::Value<List<AWS::EC2::Instance::Id>>",
"AWS::SSM::Parameter::Value<List<AWS::EC2::SecurityGroup::GroupName>>",
"AWS::SSM::Parameter::Value<List<AWS::EC2::SecurityGroup::Id>>",
"AWS::SSM::Parameter::Value<List<AWS::EC2::Subnet::Id>>",
"AWS::SSM::Parameter::Value<List<AWS::EC2::VPC::Id>>",
"AWS::SSM::Parameter::Value<List<AWS::EC2::Volume::Id>>",
"AWS::SSM::Parameter::Value<List<AWS::Route53::HostedZone::Id>>",
"AWS::SSM::Parameter::Value<List<Number>>",
"AWS::SSM::Parameter::Value<List<String>>",
]

VALID_PARAMETER_TYPES = VALID_PARAMETER_TYPES_SINGLE + VALID_PARAMETER_TYPES_LIST
valid_snapshot_types = frozenset(
[
"AWS::EC2::Volume",
"AWS::ElastiCache::CacheCluster",
"AWS::ElastiCache::ReplicationGroup",
"AWS::Neptune::DBCluster",
"AWS::RDS::DBCluster",
"AWS::RDS::DBInstance",
"AWS::Redshift::Cluster",
]
)

VALID_PARAMETER_TYPES_SINGLE = frozenset(
[
"AWS::EC2::AvailabilityZone::Name",
"AWS::EC2::Image::Id",
"AWS::EC2::Instance::Id",
"AWS::EC2::KeyPair::KeyName",
"AWS::EC2::SecurityGroup::GroupName",
"AWS::EC2::SecurityGroup::Id",
"AWS::EC2::Subnet::Id",
"AWS::EC2::VPC::Id",
"AWS::EC2::Volume::Id",
"AWS::Route53::HostedZone::Id",
"AWS::SSM::Parameter::Name",
"Number",
"String",
"AWS::SSM::Parameter::Value<AWS::EC2::AvailabilityZone::Name>",
"AWS::SSM::Parameter::Value<AWS::EC2::Image::Id>",
"AWS::SSM::Parameter::Value<AWS::EC2::Instance::Id>",
"AWS::SSM::Parameter::Value<AWS::EC2::KeyPair::KeyName>",
"AWS::SSM::Parameter::Value<AWS::EC2::SecurityGroup::GroupName>",
"AWS::SSM::Parameter::Value<AWS::EC2::SecurityGroup::Id>",
"AWS::SSM::Parameter::Value<AWS::EC2::Subnet::Id>",
"AWS::SSM::Parameter::Value<AWS::EC2::VPC::Id>",
"AWS::SSM::Parameter::Value<AWS::EC2::Volume::Id>",
"AWS::SSM::Parameter::Value<AWS::Route53::HostedZone::Id>",
"AWS::SSM::Parameter::Value<AWS::SSM::Parameter::Name>",
"AWS::SSM::Parameter::Value<Number>",
"AWS::SSM::Parameter::Value<String>",
]
)

VALID_PARAMETER_TYPES_LIST = frozenset(
[
"CommaDelimitedList",
"List<AWS::EC2::AvailabilityZone::Name>",
"List<AWS::EC2::Image::Id>",
"List<AWS::EC2::Instance::Id>",
"List<AWS::EC2::SecurityGroup::GroupName>",
"List<AWS::EC2::SecurityGroup::Id>",
"List<AWS::EC2::Subnet::Id>",
"List<AWS::EC2::VPC::Id>",
"List<AWS::EC2::Volume::Id>",
"List<AWS::Route53::HostedZone::Id>",
"List<Number>",
"List<String>",
"AWS::SSM::Parameter::Value<CommaDelimitedList>",
"AWS::SSM::Parameter::Value<List<AWS::EC2::AvailabilityZone::Name>>",
"AWS::SSM::Parameter::Value<List<AWS::EC2::Image::Id>>",
"AWS::SSM::Parameter::Value<List<AWS::EC2::Instance::Id>>",
"AWS::SSM::Parameter::Value<List<AWS::EC2::SecurityGroup::GroupName>>",
"AWS::SSM::Parameter::Value<List<AWS::EC2::SecurityGroup::Id>>",
"AWS::SSM::Parameter::Value<List<AWS::EC2::Subnet::Id>>",
"AWS::SSM::Parameter::Value<List<AWS::EC2::VPC::Id>>",
"AWS::SSM::Parameter::Value<List<AWS::EC2::Volume::Id>>",
"AWS::SSM::Parameter::Value<List<AWS::Route53::HostedZone::Id>>",
"AWS::SSM::Parameter::Value<List<Number>>",
"AWS::SSM::Parameter::Value<List<String>>",
]
)

TEMPLATED_PROPERTY_CFN_PATHS = frozenset(
[
"Resources/AWS::ApiGateway::RestApi/Properties/BodyS3Location",
"Resources/AWS::Lambda::Function/Properties/Code",
"Resources/AWS::Lambda::LayerVersion/Properties/Content",
"Resources/AWS::ElasticBeanstalk::ApplicationVersion/Properties/SourceBundle",
"Resources/AWS::StepFunctions::StateMachine/Properties/DefinitionS3Location",
"Resources/AWS::AppSync::GraphQLSchema/Properties/DefinitionS3Location",
"Resources/AWS::AppSync::Resolver/Properties/RequestMappingTemplateS3Location",
"Resources/AWS::AppSync::Resolver/Properties/ResponseMappingTemplateS3Location",
"Resources/AWS::AppSync::FunctionConfiguration/Properties/RequestMappingTemplateS3Location",
"Resources/AWS::AppSync::FunctionConfiguration/Properties/ResponseMappingTemplateS3Location",
"Resources/AWS::CloudFormation::Stack/Properties/TemplateURL",
"Resources/AWS::CodeCommit::Repository/Properties/Code/S3",
]
)

VALID_PARAMETER_TYPES = list(VALID_PARAMETER_TYPES_SINGLE) + list(
VALID_PARAMETER_TYPES_LIST
)

BOOLEAN_STRINGS_TRUE = frozenset(["true", "True"])
BOOLEAN_STRINGS_FALSE = frozenset(["false", "False"])
Expand Down
2 changes: 1 addition & 1 deletion src/cfnlint/jsonschema/_keywords.py
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,7 @@ def exclusiveMinimum(

if t_instance <= m:
yield ValidationError(
f"{instance!r} is less than or equal to " f"the minimum of {m!r}",
f"{instance!r} is less than or equal to the minimum of {m!r}",
)


Expand Down
3 changes: 2 additions & 1 deletion src/cfnlint/jsonschema/_keywords_cfn.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import regex as re

import cfnlint.jsonschema._keywords as validators_standard
from cfnlint.helpers import BOOLEAN_STRINGS
from cfnlint.jsonschema import ValidationError, Validator
from cfnlint.jsonschema._typing import V, ValidationResult
from cfnlint.jsonschema._utils import ensure_list
Expand Down Expand Up @@ -108,7 +109,7 @@ def _raw_type(validator: Validator, tS: Any, instance: Any) -> bool:
if "boolean" == tS:
if validator.is_type(instance, "boolean"):
return True
if instance in ["true", "false", "True", "False"]:
if instance in list(BOOLEAN_STRINGS):
return True
return False

Expand Down
3 changes: 1 addition & 2 deletions src/cfnlint/jsonschema/_resolvers_cfn.py
Original file line number Diff line number Diff line change
Expand Up @@ -243,8 +243,7 @@ def _sub_string(validator: Validator, string: str) -> ResolutionResult:
sub_regex = re.compile(r"(\${([^!].*?)})")

def _replace(matchobj):
if matchobj.group(2) in validator.context.ref_values:
value = validator.context.ref_values[matchobj.group(2)]
for value, _ in validator.context.ref_value(matchobj.group(2).strip()):
if not isinstance(value, (str, int, float, bool)):
raise ValueError(f"Parameter {matchobj.group(2)!r} has wrong type")
return value
Expand Down
29 changes: 20 additions & 9 deletions src/cfnlint/jsonschema/validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -144,10 +144,19 @@ def is_valid(self, instance: Any) -> bool:
def resolve_value(self, instance: Any) -> ResolutionResult:
if self.is_type(instance, "object"):
if len(instance) == 1:
for k, v in instance.items():
if k in self.fn_resolvers:
for value, v, _ in self.resolve_value(v):
yield from self.fn_resolvers[k](v, value)
for key, value in instance.items():
if key in self.fn_resolvers:
for (
resolved_value,
validator,
resolve_errs,
) in self.resolve_value(value):
if resolve_errs:
continue
yield from self.fn_resolvers[key](
validator, resolved_value # type: ignore
)

return

# The return type is a Protocol and we are returning an instance
Expand Down Expand Up @@ -242,15 +251,12 @@ def descend(
schema_path: str | None = None,
property_path: str | int | None = None,
) -> ValidationResult:
cfn_path = self.cfn_path.copy()
if property_path:
cfn_path.append(property_path)
for error in self.evolve(
schema=schema,
context=self.context.evolve(
path=path,
),
cfn_path=cfn_path,
cfn_path=deque([property_path]) if property_path else deque([]),
).iter_errors(instance):
if path is not None:
error.path.appendleft(path)
Expand All @@ -273,7 +279,12 @@ def evolve(self, **kwargs) -> "Validator":
cls = self.__class__
for f in fields(Validator):
if f.init:
kwargs.setdefault(f.name, getattr(self, f.name))
if f.name == "cfn_path":
cfn_path = self.cfn_path.copy()
cfn_path.extend(kwargs.get(f.name, []))
kwargs["cfn_path"] = cfn_path
else:
kwargs.setdefault(f.name, getattr(self, f.name))

return cls(**kwargs)

Expand Down
2 changes: 1 addition & 1 deletion src/cfnlint/rules/conditions/Configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ def schema(self):
def cfnconditions(self, validator: Validator, conditions, instance: Any, schema):
validator = validator.evolve(
context=validator.context.evolve(
functions=FUNCTION_CONDITIONS + ["Condition"],
functions=list(FUNCTION_CONDITIONS) + ["Condition"],
resources={},
strict_types=False,
),
Expand Down
18 changes: 11 additions & 7 deletions src/cfnlint/rules/functions/Sub.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,23 +78,27 @@ def _clean_error(
err.validator = self.fn.py
return err

def _validate_string(self, validator: Validator, instance: Any) -> ValidationResult:
def _validate_string(
self, validator: Validator, key: str, instance: Any
) -> ValidationResult:
params = re.findall(REGEX_SUB_PARAMETERS, instance)
validator = validator.evolve(
context=validator.context.evolve(
functions=self._functions,
)
),
cfn_path=deque([key]),
)
for param in params:
param = param.strip()
if "." in param:
for err in validator.descend(
{"Fn::GetAtt": param},
{"type": ["string"]},
instance={"Fn::GetAtt": param},
schema={"type": ["string"]},
path=key,
):
yield self._clean_error(err, {"Fn::GetAtt": param}, param)
else:
for err in validator.descend({"Ref": param}, {"type": ["string"]}):
for err in validator.descend({"Ref": param}, {"type": ["string"]}, key):
yield self._clean_error(err, {"Ref": param}, param)

def fn_sub(
Expand All @@ -105,7 +109,7 @@ def fn_sub(
)
yield from super().validate(validator, s, instance, schema)

_, value = self.key_value(instance)
key, value = self.key_value(instance)
if validator.is_type(value, "array"):
if len(value) != 2:
return
Expand All @@ -121,7 +125,7 @@ def fn_sub(
else:
return

errs = list(self._validate_string(validator_string, value))
errs = list(self._validate_string(validator_string, key, value))
if errs:
yield from iter(errs)
return
Expand Down
Loading

0 comments on commit 523e762

Please sign in to comment.