Skip to content

Commit 6409d21

Browse files
committed
Remove SAM translator dependency, validate SAM templates via schemas
Replace the aws-sam-translator runtime dependency with direct JSON Schema validation of SAM resource types. SAM templates are now validated against schemas from the enhanced schemas service using the same provider schema infrastructure as CloudFormation resources. - Remove _sam.py transform module and aws-sam-translator dependency - Add _sam_globals.py to merge Globals section into SAM resources - Add E3724 rule for Globals section validation - Add E3066 rule for SAM resource attributes (Connectors, IgnoreGlobals) - Extend Lambda rules to also match AWS::Serverless::Function paths - Treat SAM resources as module-like for sub-resource Ref/GetAtt wildcard - Handle unresolvable schema pointers gracefully in GetAtt validation - Inject SAM implicit resources (roles, versions, aliases, stages, etc.) Fixes #4556
1 parent 5faa41e commit 6409d21

53 files changed

Lines changed: 3114 additions & 2942 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/maintenance-v1.yaml

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,16 +20,13 @@ jobs:
2020
python-version: 3.13
2121
- id: maintenance
2222
run: |
23-
latest_sam_cli=`curl -s https://api.github.com/repos/aws/aws-sam-cli/releases/latest | jq -r .tag_name | cut -c 2-`
24-
latest=`curl "https://pypi.org/pypi/aws-sam-cli/$latest_sam_cli/json" -s | jq -r '.info.requires_dist[] | select(contains("aws-sam-translator"))' | cut -c 21-`
25-
sed -i -E "s/aws-sam-translator>=[0-9.]+/aws-sam-translator>=$latest/" requirements/base.txt
2623
pip install -e .
24+
pip install -r requirements/dev.txt
2725
cfn-lint --update-iam-policies
2826
cfn-lint --update-specs --force
2927
scripts/update_specs_from_pricing.py
3028
scripts/update_schemas_from_aws_api.py
3129
cfn-lint --update-documentation
32-
scripts/update_serverless_aws_policies.py
3330
echo "date=$(date +'%Y-%m-%d')" >> $GITHUB_OUTPUT
3431
env:
3532
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}

pyproject.toml

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -183,10 +183,6 @@ ignore_missing_imports = true
183183
module = "importlib_resources.*"
184184
ignore_missing_imports = true
185185

186-
[[tool.mypy.overrides]]
187-
module = "samtranslator.*"
188-
ignore_missing_imports = true
189-
190186
[[tool.mypy.overrides]]
191187
module = "sarif_om.*"
192188
ignore_missing_imports = true

requirements/base.txt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
pyyaml>=6.0.3
2-
aws-sam-translator>=1.111.0
32
jsonpatch
43
networkx>=2.4,<4
54
sympy>=1.14.0

requirements/dev.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@ coverage>=7.2.1
33
coverage[toml]
44
pydot
55
defusedxml
6+
boto3

scripts/update_serverless_aws_policies.py

Lines changed: 0 additions & 42 deletions
This file was deleted.

src/cfnlint/context/context.py

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,7 @@ def module_names(self) -> tuple[str, ...]:
198198
name
199199
for name, resource in self.resources.items()
200200
if resource.type.endswith("::MODULE")
201+
or resource.type.startswith("AWS::Serverless::")
201202
)
202203

203204
def evolve(self, **kwargs) -> "Context":
@@ -577,6 +578,136 @@ def _init_transforms(transforms: Any) -> Transforms:
577578
return Transforms([])
578579

579580

581+
def _inject(
582+
resources: dict[str, Resource], logical_id: str, resource_type: str
583+
) -> None:
584+
"""Add a synthetic resource if it doesn't already exist."""
585+
if logical_id not in resources:
586+
try:
587+
resources[logical_id] = Resource({"Type": resource_type})
588+
except ValueError:
589+
pass
590+
591+
592+
def _inject_sam_implicit_resources(
593+
template_resources: Any, resources: dict[str, Resource]
594+
) -> None:
595+
"""Add synthetic resources for SAM implicit APIs and generated roles.
596+
597+
SAM auto-generates these when Functions have Api/HttpApi events
598+
without explicit RestApiId/ApiId references, and IAM Roles when
599+
no explicit Role property is set.
600+
"""
601+
if not isinstance(template_resources, dict):
602+
return
603+
604+
needs_rest_api = False
605+
needs_http_api = False
606+
607+
for resource_id, resource in template_resources.items():
608+
if not isinstance(resource, dict):
609+
continue
610+
resource_type = resource.get("Type")
611+
props = resource.get("Properties", {})
612+
if not isinstance(props, dict):
613+
props = {}
614+
615+
# SAM Functions/StateMachines without explicit Role get a generated Role
616+
if resource_type in (
617+
"AWS::Serverless::Function",
618+
"AWS::Serverless::StateMachine",
619+
):
620+
if "Role" not in props:
621+
_inject(resources, f"{resource_id}Role", "AWS::IAM::Role")
622+
623+
if resource_type == "AWS::Serverless::Function":
624+
# Version/Alias when AutoPublishAlias or DeploymentPreference
625+
has_alias = "AutoPublishAlias" in props or "DeploymentPreference" in props
626+
if has_alias:
627+
for suffix, rtype in (
628+
(f"{resource_id}.Version", "AWS::Lambda::Version"),
629+
(f"{resource_id}.Alias", "AWS::Lambda::Alias"),
630+
):
631+
if suffix not in resources:
632+
try:
633+
resources[suffix] = Resource({"Type": rtype})
634+
except ValueError:
635+
pass
636+
637+
# Url when FunctionUrlConfig is set
638+
if "FunctionUrlConfig" in props:
639+
_inject(resources, f"{resource_id}Url", "AWS::Lambda::Url")
640+
641+
# DeploymentPreference generates CodeDeploy resources
642+
dp = props.get("DeploymentPreference", {})
643+
if isinstance(dp, dict) and dp.get("Enabled", True):
644+
_inject(
645+
resources,
646+
"ServerlessDeploymentApplication",
647+
"AWS::CodeDeploy::Application",
648+
)
649+
_inject(
650+
resources,
651+
f"{resource_id}DeploymentGroup",
652+
"AWS::CodeDeploy::DeploymentGroup",
653+
)
654+
if "Role" not in dp:
655+
_inject(resources, "CodeDeployServiceRole", "AWS::IAM::Role")
656+
657+
# Per-event permissions and implicit API detection
658+
events = props.get("Events", {})
659+
if isinstance(events, dict):
660+
for event_name, event in events.items():
661+
if not isinstance(event, dict):
662+
continue
663+
_inject(
664+
resources,
665+
f"{resource_id}{event_name}Permission",
666+
"AWS::Lambda::Permission",
667+
)
668+
event_type = event.get("Type")
669+
if event_type == "Api":
670+
event_props = event.get("Properties", {})
671+
if (
672+
not isinstance(event_props, dict)
673+
or "RestApiId" not in event_props
674+
):
675+
needs_rest_api = True
676+
elif event_type == "HttpApi":
677+
event_props = event.get("Properties", {})
678+
if (
679+
not isinstance(event_props, dict)
680+
or "ApiId" not in event_props
681+
):
682+
needs_http_api = True
683+
684+
if resource_type == "AWS::Serverless::Api":
685+
_inject(resources, f"{resource_id}Stage", "AWS::ApiGateway::Stage")
686+
if "Domain" in props:
687+
_inject(
688+
resources,
689+
f"{resource_id}DomainName",
690+
"AWS::ApiGateway::DomainName",
691+
)
692+
if "Auth" in props:
693+
_inject(
694+
resources,
695+
f"{resource_id}UsagePlan",
696+
"AWS::ApiGateway::UsagePlan",
697+
)
698+
699+
if resource_type == "AWS::Serverless::HttpApi":
700+
_inject(resources, f"{resource_id}Stage", "AWS::ApiGatewayV2::Stage")
701+
702+
if needs_rest_api:
703+
_inject(resources, "ServerlessRestApi", "AWS::Serverless::Api")
704+
_inject(resources, "ServerlessRestApiStage", "AWS::ApiGateway::Stage")
705+
706+
if needs_http_api:
707+
_inject(resources, "ServerlessHttpApi", "AWS::Serverless::HttpApi")
708+
_inject(resources, "ServerlessHttpApiStage", "AWS::ApiGatewayV2::Stage")
709+
710+
580711
def create_context_for_template(
581712
cfn: Template,
582713
) -> "Context":
@@ -592,6 +723,13 @@ def create_context_for_template(
592723
except (ValueError, AttributeError):
593724
pass
594725

726+
# Inject synthetic resources for SAM implicit APIs.
727+
# When a SAM Function has an Api event without an explicit RestApiId,
728+
# SAM generates "ServerlessRestApi" (AWS::Serverless::Api).
729+
# Similarly for HttpApi events -> "ServerlessHttpApi".
730+
if cfn.has_serverless_transform():
731+
_inject_sam_implicit_resources(cfn.template.get("Resources", {}), resources)
732+
595733
transforms = _init_transforms(cfn.template.get("Transform", []))
596734

597735
try:

0 commit comments

Comments
 (0)