import argparse
import ast
import inspect
import json
import sys
import textwrap
from pathlib import Path
def pointer_to_attr(pointer: str) -> str:
return ".".join(pointer.split("/")[2:])
def extract_getatt_additions(region: str = "us-east-1") -> dict:
from cfnlint.schema import PROVIDER_SCHEMA_MANAGER
from cfnlint.schema._getatts import _all_property_types, _exceptions
additions: dict[str, list[str]] = {}
for type_name in sorted(set(_all_property_types) | set(_exceptions)):
try:
getatts = PROVIDER_SCHEMA_MANAGER.get_type_getatts(type_name, region)
except Exception as e: print(f" warning: skipping {type_name}: {e}", file=sys.stderr)
continue
full_attrs = set(getatts.keys())
try:
schema = PROVIDER_SCHEMA_MANAGER.get_resource_schema(region, type_name)
read_only = {pointer_to_attr(p) for p in schema.schema.get("readOnlyProperties", [])}
except Exception: read_only = set()
delta = sorted(full_attrs - read_only)
if delta:
additions[type_name] = delta
return additions
def extract_retention_requirements() -> dict:
from cfnlint.rules.resources.RetentionPeriodOnResourceTypesWithAutoExpiringContent import (
RetentionPeriodOnResourceTypesWithAutoExpiringContent,
)
rule = RetentionPeriodOnResourceTypesWithAutoExpiringContent()
requirements: dict[str, list[str]] = {}
for type_name, properties in rule._properties.items():
attrs = [p["Attribute"] for p in properties if p.get("Attribute")]
if attrs:
requirements[type_name] = attrs
return requirements
def extract_codepipeline_artifact_counts() -> dict:
from cfnlint.rules.resources.codepipeline.PipelineArtifactCounts import (
PipelineArtifactCounts,
)
counts = PipelineArtifactCounts()._artifact_counts
flat: dict[str, dict] = {}
for owner, categories in counts.items():
for category, providers in categories.items():
for provider, spec in providers.items():
inputs = spec.get("InputArtifacts", {})
outputs = spec.get("OutputArtifacts", {})
flat[f"{owner}/{category}/{provider}"] = {
"min_input": inputs.get("minItems", 0),
"max_input": inputs["maxItems"],
"min_output": outputs.get("minItems", 0),
"max_output": outputs["maxItems"],
}
return flat
def extract_local_list(function, variable_name: str) -> list[str]:
syntax_tree = ast.parse(textwrap.dedent(inspect.getsource(function)))
matches: list[list[str]] = []
for node in ast.walk(syntax_tree):
if not isinstance(node, (ast.Assign, ast.AnnAssign)):
continue
targets = node.targets if isinstance(node, ast.Assign) else [node.target]
if not any(isinstance(target, ast.Name) and target.id == variable_name for target in targets):
continue
value = ast.literal_eval(node.value)
if not isinstance(value, list) or not all(isinstance(item, str) for item in value):
raise ValueError(f"{variable_name} must be a literal string list")
matches.append(value)
if len(matches) != 1:
raise ValueError(f"expected one {variable_name} assignment, found {len(matches)}")
return matches[0]
def extract_enum_list(function) -> list[str]:
syntax_tree = ast.parse(textwrap.dedent(inspect.getsource(function)))
matches: list[list[str]] = []
for node in ast.walk(syntax_tree):
if not isinstance(node, ast.Dict):
continue
for key, value in zip(node.keys, node.values):
if not isinstance(key, ast.Constant) or key.value != "enum":
continue
enum_values = ast.literal_eval(value)
if not isinstance(enum_values, list) or not all(isinstance(item, str) for item in enum_values):
raise ValueError("enum must be a literal string list")
matches.append(enum_values)
if len(matches) != 1:
raise ValueError(f"expected one enum list, found {len(matches)}")
return matches[0]
def extract_membership_string_list(function) -> list[str]:
syntax_tree = ast.parse(textwrap.dedent(inspect.getsource(function)))
matches: list[list[str]] = []
for node in ast.walk(syntax_tree):
if not isinstance(node, ast.Compare) or not any(isinstance(operator, (ast.In, ast.NotIn)) for operator in node.ops):
continue
for comparator in node.comparators:
if not isinstance(comparator, (ast.List, ast.Tuple, ast.Set)):
continue
values = ast.literal_eval(comparator)
if not isinstance(values, (list, tuple, set)) or not all(isinstance(item, str) for item in values):
raise ValueError("membership values must be literal strings")
matches.append(list(values))
if len(matches) != 1:
raise ValueError(f"expected one membership string list, found {len(matches)}")
return matches[0]
def extract_startswith_predicates(function) -> tuple[list[str], list[str]]:
syntax_tree = ast.parse(textwrap.dedent(inspect.getsource(function)))
list_prefixes: list[list[str]] = []
direct_prefixes: list[str] = []
for node in ast.walk(syntax_tree):
if isinstance(node, ast.GeneratorExp):
for generator in node.generators:
if not isinstance(generator.iter, (ast.List, ast.Tuple, ast.Set)):
continue
values = ast.literal_eval(generator.iter)
if isinstance(values, (list, tuple, set)) and all(isinstance(item, str) for item in values):
list_prefixes.append(list(values))
if (
isinstance(node, ast.Call)
and isinstance(node.func, ast.Attribute)
and node.func.attr == "startswith"
and node.args
and isinstance(node.args[0], ast.Constant)
and isinstance(node.args[0].value, str)
):
direct_prefixes.append(node.args[0].value)
if len(list_prefixes) > 1:
raise ValueError(f"expected at most one startswith prefix list, found {len(list_prefixes)}")
return (list_prefixes[0] if list_prefixes else []), direct_prefixes
def extract_previous_generation_routes(rule_class) -> list[str]:
syntax_tree = ast.parse(textwrap.dedent(inspect.getsource(rule_class.match)))
routes: list[str] = []
for node in ast.walk(syntax_tree):
if not isinstance(node, ast.For) or not isinstance(node.iter, (ast.List, ast.Tuple)):
continue
try:
route_tuples = ast.literal_eval(node.iter)
except (ValueError, TypeError):
continue
if not isinstance(route_tuples, list) or not route_tuples:
continue
if all(isinstance(route, tuple) and len(route) == 2 for route in route_tuples):
routes.extend(f"Resources/{resource_type}/Properties/{property}" for resource_type, property in route_tuples)
elif all(isinstance(route, tuple) and len(route) == 3 for route in route_tuples):
routes.extend(
f"Resources/{resource_type}/Properties/{parent}/{property}"
for resource_type, parent, property in route_tuples
)
if not routes:
raise ValueError("previous-generation instance rule has no literal routes")
return routes
def extract_previous_generation_pattern(rule_class) -> str:
check = rule_class._PreviousGenerationInstanceType__is_previous_generation_instance_type
syntax_tree = ast.parse(textwrap.dedent(inspect.getsource(check)))
matches: list[str] = []
for node in ast.walk(syntax_tree):
if not isinstance(node, ast.Call) or not isinstance(node.func, ast.Attribute) or node.func.attr != "search":
continue
if node.args and isinstance(node.args[0], ast.Constant) and isinstance(node.args[0].value, str):
matches.append(node.args[0].value)
if len(matches) != 1:
raise ValueError(f"expected one previous-generation regex, found {len(matches)}")
return matches[0]
def extract_update_policy_resource_types(cfn_lint_root: Path) -> list[str]:
schema_path = cfn_lint_root / "src/cfnlint/data/schemas/other/resources/update_policy.json"
schema = json.loads(schema_path.read_text())
matches: list[list[str]] = []
for clause in schema.get("allOf", []):
candidate = clause.get("if", {}).get("properties", {}).get("Type", {}).get("enum")
if isinstance(candidate, list) and all(isinstance(item, str) for item in candidate):
matches.append(candidate)
if not matches:
raise ValueError("UpdatePolicy schema has no resource-type enum")
largest_size = max(len(candidate) for candidate in matches)
largest_matches = [candidate for candidate in matches if len(candidate) == largest_size]
if len(largest_matches) != 1:
raise ValueError(f"expected one complete UpdatePolicy resource-type enum, found {len(largest_matches)}")
return largest_matches[0]
def extract_rule_tables(cfn_lint_root: Path) -> dict:
from cfnlint import helpers
from cfnlint.rules.parameters.DynamicReferenceSecret import DynamicReferenceSecret
from cfnlint.rules.resources.PreviousGenerationInstanceType import PreviousGenerationInstanceType
from cfnlint.rules.resources.apigateway.RestApiMixingDefinitions import RestApiMixingDefinitions
from cfnlint.rules.resources.ectwo.EbsIopsIgnored import EbsIopsIgnored, _no_iops_volume_types
from cfnlint.rules.resources.iam.ResourcePolicy import ResourcePolicy
from cfnlint.rules.resources.iam.RoleArnPattern import RoleArnPattern
from cfnlint.rules.resources.lmbd.SnapStartEnabled import SnapStartEnabled
from cfnlint.rules.resources.lmbd.SnapStartSupported import SnapStartSupported
from cfnlint.rules.resources.lmbd.ZipPackageRequiredProperties import ZipPackageRequiredProperties
from cfnlint.rules.resources.properties.ImageId import ImageId
from cfnlint.rules.resources.properties.Password import Password
image_id_rule = ImageId()
snapstart_rule = SnapStartSupported()
api_gateway_rule = RestApiMixingDefinitions()
snapstart_runtime_prefixes, snapstart_unsupported_runtime_prefixes = extract_startswith_predicates(
snapstart_rule._is_runtime_valid
)
snapstart_recommendation_list_prefixes, snapstart_recommendation_runtime_prefixes = extract_startswith_predicates(
SnapStartEnabled.validate
)
if snapstart_recommendation_list_prefixes:
raise ValueError("SnapStart recommendation predicate unexpectedly uses a prefix list")
return {
"api_gateway_mixing_resource_types": api_gateway_rule._mix_types,
"ebs_iops_ignored_volume_types": sorted(_no_iops_volume_types),
"ebs_iops_property_paths": EbsIopsIgnored().keywords,
"iam_role_arn_property_paths": RoleArnPattern().keywords,
"image_id_parameter_types": extract_enum_list(image_id_rule.validate),
"image_id_property_paths": image_id_rule.keywords,
"lambda_zip_required_properties": extract_local_list(
ZipPackageRequiredProperties.match, "required_properties"
),
"package_property_paths": sorted(helpers.TEMPLATED_PROPERTY_CFN_PATHS),
"password_property_names": extract_local_list(Password.match, "password_properties"),
"previous_generation_instance_pattern": extract_previous_generation_pattern(PreviousGenerationInstanceType),
"previous_generation_instance_property_paths": extract_previous_generation_routes(
PreviousGenerationInstanceType
),
"resource_policy_paths": ResourcePolicy().keywords,
"secret_dynamic_reference_property_paths": DynamicReferenceSecret().keywords,
"snapstart_recommendation_excluded_runtimes": extract_membership_string_list(SnapStartEnabled.validate),
"snapstart_recommendation_runtime_prefixes": snapstart_recommendation_runtime_prefixes,
"snapstart_runtime_prefixes": snapstart_runtime_prefixes,
"snapstart_supported_regions": snapstart_rule.regions,
"snapstart_unsupported_runtime_prefixes": snapstart_unsupported_runtime_prefixes,
"snapstart_unsupported_runtimes": extract_membership_string_list(snapstart_rule._is_runtime_valid),
"snapshot_capable_resource_types": sorted(helpers.valid_snapshot_types),
"update_policy_resource_types": extract_update_policy_resource_types(cfn_lint_root),
"valid_parameter_types": sorted(helpers.VALID_PARAMETER_TYPES),
}
def write_json(path: Path, key: str, value) -> None:
path.write_text(json.dumps({key: value}, indent=2, sort_keys=True) + "\n")
print(f" wrote {path.name} ({len(value)} entries)")
def main() -> int:
parser = argparse.ArgumentParser(description="Extract data tables from cfn-lint Python rule code")
parser.add_argument("--cfn-lint-root", required=True, type=Path, help="Path to the cfn-lint repo root")
parser.add_argument("--out", required=True, type=Path, help="Output directory (generated data dir)")
args = parser.parse_args()
src = args.cfn_lint_root / "src"
if not src.exists():
print(f"error: cfn-lint src not found at {src}", file=sys.stderr)
return 2
sys.path.insert(0, str(src))
args.out.mkdir(parents=True, exist_ok=True)
print(f"Extracting cfn-lint data tables from {args.cfn_lint_root}")
write_json(args.out / "getatt_additions.json", "getatt_additions", extract_getatt_additions())
write_json(
args.out / "retention_period_requirements.json",
"retention_period_requirements",
extract_retention_requirements(),
)
write_json(
args.out / "codepipeline_action_artifact_counts.json",
"codepipeline_action_artifact_counts",
extract_codepipeline_artifact_counts(),
)
write_json(
args.out / "cfnlint_rule_tables.json",
"cfnlint_rule_tables",
extract_rule_tables(args.cfn_lint_root),
)
print("Done")
return 0
if __name__ == "__main__":
sys.exit(main())