Skip to content

Commit 3ca222c

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 5e11c20 commit 3ca222c

53 files changed

Lines changed: 3114 additions & 2930 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
@@ -202,6 +202,7 @@ def __post_init__(self) -> None:
202202
name
203203
for name, resource in self.resources.items()
204204
if resource.type.endswith("::MODULE")
205+
or resource.type.startswith("AWS::Serverless::")
205206
),
206207
)
207208

@@ -582,6 +583,136 @@ def _init_transforms(transforms: Any) -> Transforms:
582583
return Transforms([])
583584

584585

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

731+
# Inject synthetic resources for SAM implicit APIs.
732+
# When a SAM Function has an Api event without an explicit RestApiId,
733+
# SAM generates "ServerlessRestApi" (AWS::Serverless::Api).
734+
# Similarly for HttpApi events -> "ServerlessHttpApi".
735+
if cfn.has_serverless_transform():
736+
_inject_sam_implicit_resources(cfn.template.get("Resources", {}), resources)
737+
600738
transforms = _init_transforms(cfn.template.get("Transform", []))
601739

602740
try:

0 commit comments

Comments
 (0)