Repository: aws-cloudformation/cfn-lint

Issue:
New rule for package command and Lambda
### Is this feature request related to a new rule or cfn-lint capabilities?

_No response_

### Describe the feature you'd like to request

Error message when using the package command and Lambda: `Runtime and Handler are mandatory parameters for functions created with deployment packages. `

### Describe the solution you'd like

A new warning or information rule that determines if you are using the package command and alerts that these parameters are required.

### Additional context

_No response_

### Is this something that you'd be interested in working on?

- [X] 👋 I may be able to implement this feature request

### Would this feature include a breaking change?

- [ ] ⚠️ This feature might incur a breaking change


Discussion:

Resolving patch:
diff --git a/src/cfnlint/rules/resources/lmbd/ZipPackageRequiredProperties.py b/src/cfnlint/rules/resources/lmbd/ZipPackageRequiredProperties.py
new file mode 100644
--- /dev/null
+++ b/src/cfnlint/rules/resources/lmbd/ZipPackageRequiredProperties.py
@@ -0,0 +1,76 @@
+"""
+Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+SPDX-License-Identifier: MIT-0
+"""
+from cfnlint.rules import CloudFormationLintRule, 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):
+        matches = []
+        required_properties = [
+            "Handler",
+            "Runtime",
+        ]  # required if package is a .zip file
+
+        resources = cfn.get_resources(["AWS::Lambda::Function"])
+
+        for resource_name, resource in resources.items():
+            properties = resource.get("Properties")
+            if not isinstance(properties, dict):
+                continue
+
+            for scenario in cfn.get_object_without_conditions(
+                properties, ["PackageType", "Code", "Handler", "Runtime"]
+            ):
+                props = scenario.get("Object")
+                path = ["Resources", resource_name, "Properties"]
+
+                # check is zip deployment
+                is_zip_deployment = True
+                code = props.get("Code")
+
+                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 props.get(p) is None:
+                        missing_properties.append(p)
+
+                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


Test patch:
diff --git a/test/fixtures/templates/bad/resources/lambda/required_properties.yaml b/test/fixtures/templates/bad/resources/lambda/required_properties.yaml
new file mode 100644
--- /dev/null
+++ b/test/fixtures/templates/bad/resources/lambda/required_properties.yaml
@@ -0,0 +1,32 @@
+AWSTemplateFormatVersion: '2010-09-09'
+Description: Missing 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: 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
--- /dev/null
+++ b/test/fixtures/templates/good/resources/lambda/required_properties.yaml
@@ -0,0 +1,29 @@
+AWSTemplateFormatVersion: '2010-09-09'
+Description: Required Properties.
+Resources:
+  Function1: # S3 key to deployment package
+    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: # source inline (zipped by CloudFormation)
+    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: # image deployment
+    Type: AWS::Lambda::Function
+    Properties:
+      Role: arn:aws:iam::123456789012:role/lambda-role
+      Code:
+        ImageUri : 111122223333.dkr.ecr.us-east-1.amazonaws.com/hello-world:latest
+      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
--- /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 required properties"""
+
+    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/required_properties.yaml",
+            err_count=3,
+        )