From 188c14025431666299fa44162dd6e4088f6ab0ef Mon Sep 17 00:00:00 2001 From: tal66 <77445020+tal66@users.noreply.github.com> Date: Tue, 11 Apr 2023 17:20:52 +0300 Subject: [PATCH 1/3] New rule for Lambda zip deployment --- .../lmbd/ZipPackageRequiredProperties.py | 64 +++++++++++++++++++ .../lambda/zipfile_required_properties.yaml | 32 ++++++++++ .../resources/lambda/required_properties.yaml | 31 +++++++++ .../test_zip_package_required_properties.py | 29 +++++++++ 4 files changed, 156 insertions(+) create mode 100644 src/cfnlint/rules/resources/lmbd/ZipPackageRequiredProperties.py create mode 100644 test/fixtures/templates/bad/resources/lambda/zipfile_required_properties.yaml create mode 100644 test/fixtures/templates/good/resources/lambda/required_properties.yaml create mode 100644 test/unit/rules/resources/lmbd/test_zip_package_required_properties.py diff --git a/src/cfnlint/rules/resources/lmbd/ZipPackageRequiredProperties.py b/src/cfnlint/rules/resources/lmbd/ZipPackageRequiredProperties.py new file mode 100644 index 0000000000..681eda189c --- /dev/null +++ b/src/cfnlint/rules/resources/lmbd/ZipPackageRequiredProperties.py @@ -0,0 +1,64 @@ +""" +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: MIT-0 +""" +from cfnlint.rules import CloudFormationLintRule +from cfnlint.rules import RuleMatch + + +class ZipPackageRequiredProperties(CloudFormationLintRule): + id = "W2533" + shortdesc = ( + "Check required properties for Lambda if the deployment package is a .zip file" + ) + description = ( + "When the package type is Zip, " + "you must also specify the `handler` and `runtime` properties." + ) + source_url = "https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-lambda-function.html" + tags = ["resources", "lambda"] + + def match(self, cfn): + """Basic Rule Matching""" + matches = [] + required_properties = [ + "Handler", + "Runtime", + ] # required if package is a .zip file + + resources = cfn.get_resources( + ["AWS::Lambda::Function", "AWS::Serverless::Function"] + ) + for resource_name, resource in resources.items(): + properties = resource.get("Properties") + if not properties: + continue + + path = ["Resources", resource_name, "Properties"] + + is_zip_deployment = True + if properties.get("PackageType") == "Zip": + path.append("PackageType") + elif properties.get("Code", {}).get("ZipFile") or properties.get( + "Code", {} + ).get("S3Key", "").endswith(".zip"): + path.append("Code") + else: + is_zip_deployment = False + + if not is_zip_deployment: + continue + + # check required properties for zip deployment + missing_properties = [] + for p in required_properties: + if properties.get(p) is None: + missing_properties.append(p) + + if len(missing_properties) > 0: + message = ( + "Missing required properties for Lambda zip file deployment: {0}" + ) + matches.append(RuleMatch(path, message.format(missing_properties))) + + return matches diff --git a/test/fixtures/templates/bad/resources/lambda/zipfile_required_properties.yaml b/test/fixtures/templates/bad/resources/lambda/zipfile_required_properties.yaml new file mode 100644 index 0000000000..806ac99f80 --- /dev/null +++ b/test/fixtures/templates/bad/resources/lambda/zipfile_required_properties.yaml @@ -0,0 +1,32 @@ +AWSTemplateFormatVersion: '2010-09-09' +Description: Missing properties. +Resources: + Function: + Type: AWS::Lambda::Function + Properties: +# Handler: index.handler + Role: arn:aws:iam::123456789012:role/lambda-role + Code: + S3Bucket: my-bucket + S3Key: function.zip +# Runtime: nodejs16.x + Function2: + Type: AWS::Lambda::Function + Properties: +# Handler: index.handler + Role: arn:aws:iam::123456789012:role/lambda-role + Code: + S3Bucket: my-bucket + S3Key: function.zip + Runtime: python3.9 + PackageType: Zip + Function3: + Type: AWS::Lambda::Function + Properties: + Handler: index.handler + Role: arn:aws:iam::123456789012:role/lambda-role + Code: + ZipFile: | + var aws = require('aws-sdk') + exports.handler = function(event, context) {} +# Runtime: nodejs16.x \ No newline at end of file diff --git a/test/fixtures/templates/good/resources/lambda/required_properties.yaml b/test/fixtures/templates/good/resources/lambda/required_properties.yaml new file mode 100644 index 0000000000..8ae54fd04a --- /dev/null +++ b/test/fixtures/templates/good/resources/lambda/required_properties.yaml @@ -0,0 +1,31 @@ +AWSTemplateFormatVersion: '2010-09-09' +Description: Properties. +Resources: + Function1: + Type: AWS::Lambda::Function + Properties: + Handler: index.handler + Role: arn:aws:iam::123456789012:role/lambda-role + Code: + S3Bucket: my-bucket + S3Key: function.zip + Runtime: python3.9 + Function2: + Type: AWS::Lambda::Function + Properties: + Handler: index.handler + Role: arn:aws:iam::123456789012:role/lambda-role + Code: + ZipFile: | + var aws = require('aws-sdk') + exports.handler = function(event, context) {} + Runtime: nodejs16.x + Function3: + Type: AWS::Lambda::Function + Properties: + Handler: index.handler + Role: arn:aws:iam::123456789012:role/lambda-role + Code: + ImageUri : 111122223333.dkr.ecr.us-east-1.amazonaws.com/hello-world:latest + Runtime: nodejs16.x + PackageType: Image \ No newline at end of file diff --git a/test/unit/rules/resources/lmbd/test_zip_package_required_properties.py b/test/unit/rules/resources/lmbd/test_zip_package_required_properties.py new file mode 100644 index 0000000000..23613937d5 --- /dev/null +++ b/test/unit/rules/resources/lmbd/test_zip_package_required_properties.py @@ -0,0 +1,29 @@ +""" +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: MIT-0 +""" +from test.unit.rules import BaseRuleTestCase + +from cfnlint.rules.resources.lmbd.ZipPackageRequiredProperties import ( + ZipPackageRequiredProperties, +) + + +class TestZipPackageRequiredProperties(BaseRuleTestCase): + """Test template parameter configurations""" + + def setUp(self): + super(TestZipPackageRequiredProperties, self).setUp() + self.collection.register(ZipPackageRequiredProperties()) + self.success_templates = [ + "test/fixtures/templates/good/resources/lambda/required_properties.yaml" + ] + + def test_file_positive(self): + self.helper_file_positive() + + def test_file_negative(self): + self.helper_file_negative( + "test/fixtures/templates/bad/resources/lambda/zipfile_required_properties.yaml", + err_count=3, + ) From a9d28e77543f2ff76183390b4702b3ea795f53b2 Mon Sep 17 00:00:00 2001 From: tal66 <77445020+tal66@users.noreply.github.com> Date: Wed, 3 May 2023 03:01:22 +0300 Subject: [PATCH 2/3] code rev --- .../lmbd/ZipPackageRequiredProperties.py | 65 +++++++++++-------- ...operties.yaml => required_properties.yaml} | 2 +- .../resources/lambda/required_properties.yaml | 10 ++- .../test_zip_package_required_properties.py | 4 +- 4 files changed, 45 insertions(+), 36 deletions(-) rename test/fixtures/templates/bad/resources/lambda/{zipfile_required_properties.yaml => required_properties.yaml} (98%) diff --git a/src/cfnlint/rules/resources/lmbd/ZipPackageRequiredProperties.py b/src/cfnlint/rules/resources/lmbd/ZipPackageRequiredProperties.py index 681eda189c..b59cfaa6eb 100644 --- a/src/cfnlint/rules/resources/lmbd/ZipPackageRequiredProperties.py +++ b/src/cfnlint/rules/resources/lmbd/ZipPackageRequiredProperties.py @@ -19,46 +19,57 @@ class ZipPackageRequiredProperties(CloudFormationLintRule): tags = ["resources", "lambda"] def match(self, cfn): - """Basic Rule Matching""" matches = [] required_properties = [ "Handler", "Runtime", ] # required if package is a .zip file - resources = cfn.get_resources( - ["AWS::Lambda::Function", "AWS::Serverless::Function"] - ) + resources = cfn.get_resources(["AWS::Lambda::Function"]) + for resource_name, resource in resources.items(): properties = resource.get("Properties") - if not properties: + if not isinstance(properties, dict): continue - path = ["Resources", resource_name, "Properties"] + for scenario in cfn.get_object_without_conditions( + properties, ["PackageType", "Code", "Handler", "Runtime"] + ): + props = scenario.get("Object") + path = ["Resources", resource_name, "Properties"] - is_zip_deployment = True - if properties.get("PackageType") == "Zip": - path.append("PackageType") - elif properties.get("Code", {}).get("ZipFile") or properties.get( - "Code", {} - ).get("S3Key", "").endswith(".zip"): - path.append("Code") - else: - is_zip_deployment = False + # check is zip deployment + is_zip_deployment = True + code = props.get("Code") - if not is_zip_deployment: - continue + if props.get("PackageType") == "Zip": + path.append("PackageType") + elif isinstance(code, dict) and (code.get("ZipFile") or code.get("S3Key")): + path.append("Code") + else: + is_zip_deployment = False + + if not is_zip_deployment: + continue - # check required properties for zip deployment - missing_properties = [] - for p in required_properties: - if properties.get(p) is None: - missing_properties.append(p) + # check required properties for zip deployment + missing_properties = [] + for p in required_properties: + if props.get(p) is None: + missing_properties.append(p) - if len(missing_properties) > 0: - message = ( - "Missing required properties for Lambda zip file deployment: {0}" - ) - matches.append(RuleMatch(path, message.format(missing_properties))) + if len(missing_properties) > 0: + message = "Properties {0} missing for zip file deployment at {1}" + matches.append( + RuleMatch( + path, + message.format( + missing_properties, + "/".join( + map(str, ["Resources", resource_name, "Properties"]) + ), + ), + ) + ) return matches diff --git a/test/fixtures/templates/bad/resources/lambda/zipfile_required_properties.yaml b/test/fixtures/templates/bad/resources/lambda/required_properties.yaml similarity index 98% rename from test/fixtures/templates/bad/resources/lambda/zipfile_required_properties.yaml rename to test/fixtures/templates/bad/resources/lambda/required_properties.yaml index 806ac99f80..c087056343 100644 --- a/test/fixtures/templates/bad/resources/lambda/zipfile_required_properties.yaml +++ b/test/fixtures/templates/bad/resources/lambda/required_properties.yaml @@ -1,7 +1,7 @@ AWSTemplateFormatVersion: '2010-09-09' Description: Missing properties. Resources: - Function: + Function1: Type: AWS::Lambda::Function Properties: # Handler: index.handler diff --git a/test/fixtures/templates/good/resources/lambda/required_properties.yaml b/test/fixtures/templates/good/resources/lambda/required_properties.yaml index 8ae54fd04a..36823a575f 100644 --- a/test/fixtures/templates/good/resources/lambda/required_properties.yaml +++ b/test/fixtures/templates/good/resources/lambda/required_properties.yaml @@ -1,7 +1,7 @@ AWSTemplateFormatVersion: '2010-09-09' -Description: Properties. +Description: Required Properties. Resources: - Function1: + Function1: # S3 key to deployment package Type: AWS::Lambda::Function Properties: Handler: index.handler @@ -10,7 +10,7 @@ Resources: S3Bucket: my-bucket S3Key: function.zip Runtime: python3.9 - Function2: + Function2: # source inline (zipped by CloudFormation) Type: AWS::Lambda::Function Properties: Handler: index.handler @@ -20,12 +20,10 @@ Resources: var aws = require('aws-sdk') exports.handler = function(event, context) {} Runtime: nodejs16.x - Function3: + Function3: # image deployment Type: AWS::Lambda::Function Properties: - Handler: index.handler Role: arn:aws:iam::123456789012:role/lambda-role Code: ImageUri : 111122223333.dkr.ecr.us-east-1.amazonaws.com/hello-world:latest - Runtime: nodejs16.x PackageType: Image \ No newline at end of file diff --git a/test/unit/rules/resources/lmbd/test_zip_package_required_properties.py b/test/unit/rules/resources/lmbd/test_zip_package_required_properties.py index 23613937d5..409b41beea 100644 --- a/test/unit/rules/resources/lmbd/test_zip_package_required_properties.py +++ b/test/unit/rules/resources/lmbd/test_zip_package_required_properties.py @@ -10,7 +10,7 @@ class TestZipPackageRequiredProperties(BaseRuleTestCase): - """Test template parameter configurations""" + """Test required properties""" def setUp(self): super(TestZipPackageRequiredProperties, self).setUp() @@ -24,6 +24,6 @@ def test_file_positive(self): def test_file_negative(self): self.helper_file_negative( - "test/fixtures/templates/bad/resources/lambda/zipfile_required_properties.yaml", + "test/fixtures/templates/bad/resources/lambda/required_properties.yaml", err_count=3, ) From 0b9183d2989368ed4a22a883af7c2cf831aa683a Mon Sep 17 00:00:00 2001 From: tal66 <77445020+tal66@users.noreply.github.com> Date: Fri, 5 May 2023 14:13:05 +0300 Subject: [PATCH 3/3] style, --update-documentation --- docs/rules.md | 3 ++- .../rules/resources/lmbd/ZipPackageRequiredProperties.py | 7 ++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/docs/rules.md b/docs/rules.md index be054ba619..42f0b7c160 100644 --- a/docs/rules.md +++ b/docs/rules.md @@ -46,7 +46,7 @@ To include these rules, use the `-e/include-experimental` argument when running ## Rules (_This documentation is generated by running `cfn-lint --update-documentation`, do not alter this manually_) -The following **152** rules are applied by this linter: +The following **153** rules are applied by this linter: | Rule ID | Title | Description | Config
(Name:Type:Default) | Source | Tags | | -------- | ----- | ----------- | ---------- | ------ | ---- | @@ -191,6 +191,7 @@ The following **152** rules are applied by this linter: | [W2510](../src/cfnlint/rules/parameters/LambdaMemorySize.py) | Parameter Memory Size attributes should have max and min | Check if a parameter that is used for Lambda memory size should have a min and max size that matches Lambda constraints | | [Source](https://docs.aws.amazon.com/lambda/latest/dg/API_CreateFunction.html#SSS-CreateFunction-request-MemorySize) | `parameters`,`lambda` | | [W2511](../src/cfnlint/rules/resources/iam/PolicyVersion.py) | Check IAM Resource Policies syntax | See if the elements inside an IAM Resource policy are configured correctly. | | [Source](https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements.html) | `properties`,`iam` | | [W2531](../src/cfnlint/rules/resources/lmbd/DeprecatedRuntimeEol.py) | Check if EOL Lambda Function Runtimes are used | Check if an EOL Lambda Runtime is specified and give a warning if used. | | [Source](https://docs.aws.amazon.com/lambda/latest/dg/runtime-support-policy.html) | `resources`,`lambda`,`runtime` | +| [W2533](../src/cfnlint/rules/resources/lmbd/ZipPackageRequiredProperties.py) | Check required properties for Lambda if the deployment package is a .zip file | When the package type is Zip, you must also specify the `handler` and `runtime` properties. | | [Source](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-lambda-function.html) | `resources`,`lambda` | | [W3002](../src/cfnlint/rules/resources/properties/PropertiesTemplated.py) | Warn when properties are configured to only work with the package command | Some properties can be configured to only work with the CloudFormationpackage command. Warn when this is the case so user is aware. | | [Source](https://docs.aws.amazon.com/cli/latest/reference/cloudformation/package.html) | `resources` | | [W3005](../src/cfnlint/rules/resources/DependsOnObsolete.py) | Check obsolete DependsOn configuration for Resources | Check if DependsOn is specified if not needed. A Ref or a Fn::GetAtt already is an implicit dependency. | | [Source](https://aws.amazon.com/blogs/devops/optimize-aws-cloudformation-templates/) | `resources`,`dependson`,`ref`,`getatt` | | [W3010](../src/cfnlint/rules/resources/properties/AvailabilityZone.py) | Availability Zone Parameters should not be hardcoded | Check if an Availability Zone property is hardcoded. | | [Source](https://github.com/aws-cloudformation/cfn-python-lint) | `parameters`,`availabilityzone` | diff --git a/src/cfnlint/rules/resources/lmbd/ZipPackageRequiredProperties.py b/src/cfnlint/rules/resources/lmbd/ZipPackageRequiredProperties.py index b59cfaa6eb..6254b82f28 100644 --- a/src/cfnlint/rules/resources/lmbd/ZipPackageRequiredProperties.py +++ b/src/cfnlint/rules/resources/lmbd/ZipPackageRequiredProperties.py @@ -2,8 +2,7 @@ Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. SPDX-License-Identifier: MIT-0 """ -from cfnlint.rules import CloudFormationLintRule -from cfnlint.rules import RuleMatch +from cfnlint.rules import CloudFormationLintRule, RuleMatch class ZipPackageRequiredProperties(CloudFormationLintRule): @@ -44,7 +43,9 @@ def match(self, cfn): if props.get("PackageType") == "Zip": path.append("PackageType") - elif isinstance(code, dict) and (code.get("ZipFile") or code.get("S3Key")): + elif isinstance(code, dict) and ( + code.get("ZipFile") or code.get("S3Key") + ): path.append("Code") else: is_zip_deployment = False