Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Upgrade more rules to v1 #3243

Merged
merged 2 commits into from
May 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 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

Check warning on line 155 in src/cfnlint/jsonschema/validators.py

View check run for this annotation

Codecov / codecov/patch

src/cfnlint/jsonschema/validators.py#L155

Added line #L155 was not covered by tests
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 @@
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 @@
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
Loading