From 834d1bca4912e590ebd933f5155b80ededf2da66 Mon Sep 17 00:00:00 2001 From: Sandiyo Christan <55909152+sandiyochristan@users.noreply.github.com> Date: Thu, 26 Mar 2026 15:03:39 +0530 Subject: [PATCH] feat(awslambda): enrich Function model with inventory fields and add 3 security checks (#10381) Co-authored-by: Daniel Barranquero --- prowler/CHANGELOG.md | 4 +- .../__init__.py | 0 ..._vars_not_encrypted_with_cmk.metadata.json | 41 ++++ ...unction_env_vars_not_encrypted_with_cmk.py | 28 +++ .../__init__.py | 0 ...unction_no_dead_letter_queue.metadata.json | 41 ++++ ...awslambda_function_no_dead_letter_queue.py | 17 ++ .../__init__.py | 0 ...n_using_cross_account_layers.metadata.json | 41 ++++ ...bda_function_using_cross_account_layers.py | 34 +++ .../services/awslambda/awslambda_service.py | 69 +++++- ...on_env_vars_not_encrypted_with_cmk_test.py | 197 +++++++++++++++++ ...mbda_function_no_dead_letter_queue_test.py | 149 +++++++++++++ ...unction_using_cross_account_layers_test.py | 200 ++++++++++++++++++ 14 files changed, 818 insertions(+), 3 deletions(-) create mode 100644 prowler/providers/aws/services/awslambda/awslambda_function_env_vars_not_encrypted_with_cmk/__init__.py create mode 100644 prowler/providers/aws/services/awslambda/awslambda_function_env_vars_not_encrypted_with_cmk/awslambda_function_env_vars_not_encrypted_with_cmk.metadata.json create mode 100644 prowler/providers/aws/services/awslambda/awslambda_function_env_vars_not_encrypted_with_cmk/awslambda_function_env_vars_not_encrypted_with_cmk.py create mode 100644 prowler/providers/aws/services/awslambda/awslambda_function_no_dead_letter_queue/__init__.py create mode 100644 prowler/providers/aws/services/awslambda/awslambda_function_no_dead_letter_queue/awslambda_function_no_dead_letter_queue.metadata.json create mode 100644 prowler/providers/aws/services/awslambda/awslambda_function_no_dead_letter_queue/awslambda_function_no_dead_letter_queue.py create mode 100644 prowler/providers/aws/services/awslambda/awslambda_function_using_cross_account_layers/__init__.py create mode 100644 prowler/providers/aws/services/awslambda/awslambda_function_using_cross_account_layers/awslambda_function_using_cross_account_layers.metadata.json create mode 100644 prowler/providers/aws/services/awslambda/awslambda_function_using_cross_account_layers/awslambda_function_using_cross_account_layers.py create mode 100644 tests/providers/aws/services/awslambda/awslambda_function_env_vars_not_encrypted_with_cmk/awslambda_function_env_vars_not_encrypted_with_cmk_test.py create mode 100644 tests/providers/aws/services/awslambda/awslambda_function_no_dead_letter_queue/awslambda_function_no_dead_letter_queue_test.py create mode 100644 tests/providers/aws/services/awslambda/awslambda_function_using_cross_account_layers/awslambda_function_using_cross_account_layers_test.py diff --git a/prowler/CHANGELOG.md b/prowler/CHANGELOG.md index 3dc22c7b89..47f80f7212 100644 --- a/prowler/CHANGELOG.md +++ b/prowler/CHANGELOG.md @@ -6,10 +6,10 @@ All notable changes to the **Prowler SDK** are documented in this file. ### 🚀 Added -- `apikeys_api_restricted_with_gemini_api` check for GCP provider [(#10280)](https://github.com/prowler-cloud/prowler/pull/10280) -- `gemini_api_disabled` check for GCP provider [(#10280)](https://github.com/prowler-cloud/prowler/pull/10280) +- `apikeys_api_restricted_with_gemini_api` and `gemini_api_disabled`checks for GCP provider [(#10280)](https://github.com/prowler-cloud/prowler/pull/10280) - `cloudfront_distributions_logging_enabled` detects Standard Logging v2 via CloudWatch Log Delivery [(#10090)](https://github.com/prowler-cloud/prowler/pull/10090) - `glue_etl_jobs_no_secrets_in_arguments` check for plaintext secrets in AWS Glue ETL job arguments [(#10368)](https://github.com/prowler-cloud/prowler/pull/10368) +- `awslambda_function_no_dead_letter_queue`, `awslambda_function_using_cross_account_layers`, and `awslambda_function_env_vars_not_encrypted_with_cmk` checks for AWS Lambda [(#10381)](https://github.com/prowler-cloud/prowler/pull/10381) ### 🔄 Changed diff --git a/prowler/providers/aws/services/awslambda/awslambda_function_env_vars_not_encrypted_with_cmk/__init__.py b/prowler/providers/aws/services/awslambda/awslambda_function_env_vars_not_encrypted_with_cmk/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/aws/services/awslambda/awslambda_function_env_vars_not_encrypted_with_cmk/awslambda_function_env_vars_not_encrypted_with_cmk.metadata.json b/prowler/providers/aws/services/awslambda/awslambda_function_env_vars_not_encrypted_with_cmk/awslambda_function_env_vars_not_encrypted_with_cmk.metadata.json new file mode 100644 index 0000000000..82e0fc6979 --- /dev/null +++ b/prowler/providers/aws/services/awslambda/awslambda_function_env_vars_not_encrypted_with_cmk/awslambda_function_env_vars_not_encrypted_with_cmk.metadata.json @@ -0,0 +1,41 @@ +{ + "Provider": "aws", + "CheckID": "awslambda_function_env_vars_not_encrypted_with_cmk", + "CheckTitle": "Lambda function environment variables are encrypted with a customer-managed KMS key", + "CheckType": [ + "Software and Configuration Checks/AWS Security Best Practices", + "Software and Configuration Checks/Industry and Regulatory Standards/AWS Foundational Security Best Practices" + ], + "ServiceName": "awslambda", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "medium", + "ResourceType": "AwsLambdaFunction", + "ResourceGroup": "serverless", + "Description": "**AWS Lambda function** environment variables are encrypted at rest using a **customer-managed KMS key (CMK)** rather than the default AWS-managed Lambda service key.\n\nThe presence of a `KMSKeyArn` on the function configuration indicates CMK-based encryption is active.", + "Risk": "Without a CMK, environment variables are encrypted with an AWS-managed key, removing **customer control** over rotation, auditing, and revocation.\n\nIf variables contain secrets or connection strings, loss of key control weakens **confidentiality** and can fail compliance requirements (PCI-DSS, HIPAA, FedRAMP) that mandate customer-controlled encryption.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://docs.aws.amazon.com/lambda/latest/dg/configuration-envvars.html#configuration-envvars-encryption", + "https://docs.aws.amazon.com/securityhub/latest/userguide/lambda-controls.html", + "https://docs.aws.amazon.com/kms/latest/developerguide/services-lambda.html" + ], + "Remediation": { + "Code": { + "CLI": "aws lambda update-function-configuration --function-name --kms-key-arn ", + "NativeIaC": "```yaml\nAWSTemplateFormatVersion: '2010-09-09'\nResources:\n LambdaFunction:\n Type: AWS::Lambda::Function\n Properties:\n FunctionName: \n Role: \n Handler: index.handler\n Runtime: python3.12\n Code:\n S3Bucket: \n S3Key: \n KmsKeyArn: \n Environment:\n Variables:\n MY_CONFIG: \n```", + "Other": "1. Create or identify a KMS CMK in the same region as the function\n2. Grant the Lambda execution role `kms:Decrypt` and `kms:GenerateDataKey` on the key\n3. In the Lambda console go to Configuration > Environment variables > Edit\n4. Under Encryption configuration, select your CMK\n5. Save — Lambda re-encrypts all environment variables with the chosen key", + "Terraform": "```hcl\nresource \"aws_kms_key\" \"lambda_env\" {\n description = \"Lambda env var encryption key\"\n enable_key_rotation = true\n deletion_window_in_days = 30\n}\n\nresource \"aws_lambda_function\" \"example\" {\n function_name = \"\"\n role = \"\"\n handler = \"index.handler\"\n runtime = \"python3.12\"\n filename = \"\"\n kms_key_arn = aws_kms_key.lambda_env.arn\n\n environment {\n variables = {\n MY_CONFIG = \"\"\n }\n }\n}\n```" + }, + "Recommendation": { + "Text": "Encrypt Lambda environment variables with a customer-managed KMS key to maintain full control over key lifecycle and access.\n- Create a dedicated KMS key per application or per function for blast-radius isolation\n- Enable **automatic key rotation** (`EnableKeyRotation: true`)\n- Grant only the Lambda execution role decrypt access via a key policy condition on `kms:ViaService`\n- Prefer **AWS Secrets Manager** or **SSM Parameter Store (SecureString)** for secrets — environment variables should hold non-secret configuration only", + "Url": "https://hub.prowler.com/check/awslambda_function_env_vars_not_encrypted_with_cmk" + } + }, + "Categories": [ + "encryption" + ], + "DependsOn": [], + "RelatedTo": [], + "Notes": "" +} diff --git a/prowler/providers/aws/services/awslambda/awslambda_function_env_vars_not_encrypted_with_cmk/awslambda_function_env_vars_not_encrypted_with_cmk.py b/prowler/providers/aws/services/awslambda/awslambda_function_env_vars_not_encrypted_with_cmk/awslambda_function_env_vars_not_encrypted_with_cmk.py new file mode 100644 index 0000000000..152859d543 --- /dev/null +++ b/prowler/providers/aws/services/awslambda/awslambda_function_env_vars_not_encrypted_with_cmk/awslambda_function_env_vars_not_encrypted_with_cmk.py @@ -0,0 +1,28 @@ +from prowler.lib.check.models import Check, Check_Report_AWS +from prowler.providers.aws.services.awslambda.awslambda_client import awslambda_client + + +class awslambda_function_env_vars_not_encrypted_with_cmk(Check): + def execute(self): + findings = [] + for function in awslambda_client.functions.values(): + report = Check_Report_AWS(metadata=self.metadata(), resource=function) + if not function.environment: + report.status = "PASS" + report.status_extended = ( + f"Lambda function {function.name} has no environment variables." + ) + elif function.kms_key_arn: + report.status = "PASS" + report.status_extended = ( + f"Lambda function {function.name} environment variables are " + f"encrypted with KMS key {function.kms_key_arn}." + ) + else: + report.status = "FAIL" + report.status_extended = ( + f"Lambda function {function.name} has environment variables " + f"but they are not encrypted with a customer-managed KMS key." + ) + findings.append(report) + return findings diff --git a/prowler/providers/aws/services/awslambda/awslambda_function_no_dead_letter_queue/__init__.py b/prowler/providers/aws/services/awslambda/awslambda_function_no_dead_letter_queue/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/aws/services/awslambda/awslambda_function_no_dead_letter_queue/awslambda_function_no_dead_letter_queue.metadata.json b/prowler/providers/aws/services/awslambda/awslambda_function_no_dead_letter_queue/awslambda_function_no_dead_letter_queue.metadata.json new file mode 100644 index 0000000000..6d7dbfe6d6 --- /dev/null +++ b/prowler/providers/aws/services/awslambda/awslambda_function_no_dead_letter_queue/awslambda_function_no_dead_letter_queue.metadata.json @@ -0,0 +1,41 @@ +{ + "Provider": "aws", + "CheckID": "awslambda_function_no_dead_letter_queue", + "CheckTitle": "Lambda function has a Dead Letter Queue configured", + "CheckType": [ + "Software and Configuration Checks/AWS Security Best Practices", + "Software and Configuration Checks/Industry and Regulatory Standards/AWS Foundational Security Best Practices" + ], + "ServiceName": "awslambda", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "medium", + "ResourceType": "AwsLambdaFunction", + "ResourceGroup": "serverless", + "Description": "**AWS Lambda functions** have a **Dead Letter Queue (DLQ)** configured — an SQS queue or SNS topic that receives records of failed asynchronous invocations.\n\nWithout a DLQ, failed invocations are silently discarded after exhausting retries.", + "Risk": "Without a DLQ, failed asynchronous invocations are permanently lost. This harms **availability** by hiding processing failures, and weakens **integrity** by making it impossible to replay or audit unprocessed events.\n\nIn security-sensitive pipelines (e.g., audit log processors, alerting functions), silent failure can mask security events entirely.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://docs.aws.amazon.com/lambda/latest/dg/invocation-async.html#invocation-dlq", + "https://docs.aws.amazon.com/securityhub/latest/userguide/lambda-controls.html#lambda-4", + "https://repost.aws/knowledge-center/lambda-dead-letter-queue" + ], + "Remediation": { + "Code": { + "CLI": "aws lambda update-function-configuration --function-name --dead-letter-config TargetArn=", + "NativeIaC": "```yaml\nAWSTemplateFormatVersion: '2010-09-09'\nResources:\n LambdaFunction:\n Type: AWS::Lambda::Function\n Properties:\n FunctionName: \n Role: \n Handler: index.handler\n Runtime: python3.12\n Code:\n S3Bucket: \n S3Key: \n DeadLetterConfig:\n TargetArn: \n```", + "Other": "1. Open the AWS Lambda console and select your function\n2. Go to Configuration > Asynchronous invocation\n3. Under Dead-letter queue service, select SQS or SNS\n4. Choose or create the target queue/topic\n5. Save changes", + "Terraform": "```hcl\nresource \"aws_lambda_function\" \"example\" {\n function_name = \"\"\n role = \"\"\n handler = \"index.handler\"\n runtime = \"python3.12\"\n filename = \"\"\n\n dead_letter_config {\n target_arn = \"\"\n }\n}\n```" + }, + "Recommendation": { + "Text": "Configure a Dead Letter Queue for every Lambda function that handles asynchronous invocations.\n- Prefer an **SQS queue** as the DLQ target for retry and replay capability\n- Ensure the Lambda execution role has `sqs:SendMessage` permission on the DLQ\n- Monitor DLQ depth with a CloudWatch alarm to alert on processing failures", + "Url": "https://hub.prowler.com/check/awslambda_function_no_dead_letter_queue" + } + }, + "Categories": [ + "resilience" + ], + "DependsOn": [], + "RelatedTo": [], + "Notes": "" +} diff --git a/prowler/providers/aws/services/awslambda/awslambda_function_no_dead_letter_queue/awslambda_function_no_dead_letter_queue.py b/prowler/providers/aws/services/awslambda/awslambda_function_no_dead_letter_queue/awslambda_function_no_dead_letter_queue.py new file mode 100644 index 0000000000..49f709676a --- /dev/null +++ b/prowler/providers/aws/services/awslambda/awslambda_function_no_dead_letter_queue/awslambda_function_no_dead_letter_queue.py @@ -0,0 +1,17 @@ +from prowler.lib.check.models import Check, Check_Report_AWS +from prowler.providers.aws.services.awslambda.awslambda_client import awslambda_client + + +class awslambda_function_no_dead_letter_queue(Check): + def execute(self): + findings = [] + for function in awslambda_client.functions.values(): + report = Check_Report_AWS(metadata=self.metadata(), resource=function) + if function.dead_letter_config: + report.status = "PASS" + report.status_extended = f"Lambda function {function.name} has a Dead Letter Queue configured at {function.dead_letter_config.target_arn}." + else: + report.status = "FAIL" + report.status_extended = f"Lambda function {function.name} does not have a Dead Letter Queue configured." + findings.append(report) + return findings diff --git a/prowler/providers/aws/services/awslambda/awslambda_function_using_cross_account_layers/__init__.py b/prowler/providers/aws/services/awslambda/awslambda_function_using_cross_account_layers/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/aws/services/awslambda/awslambda_function_using_cross_account_layers/awslambda_function_using_cross_account_layers.metadata.json b/prowler/providers/aws/services/awslambda/awslambda_function_using_cross_account_layers/awslambda_function_using_cross_account_layers.metadata.json new file mode 100644 index 0000000000..c1b9fae2fc --- /dev/null +++ b/prowler/providers/aws/services/awslambda/awslambda_function_using_cross_account_layers/awslambda_function_using_cross_account_layers.metadata.json @@ -0,0 +1,41 @@ +{ + "Provider": "aws", + "CheckID": "awslambda_function_using_cross_account_layers", + "CheckTitle": "Lambda function does not use cross-account layers", + "CheckType": [ + "Software and Configuration Checks/AWS Security Best Practices", + "TTPs/Initial Access" + ], + "ServiceName": "awslambda", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "high", + "ResourceType": "AwsLambdaFunction", + "ResourceGroup": "serverless", + "Description": "**AWS Lambda functions** use only **layers published within the same AWS account**, rather than layers owned by external accounts.\n\nA Lambda layer bundles shared code or dependencies that are injected into the function execution environment at runtime.", + "Risk": "A layer from an external account is a **supply chain dependency outside your control**. If that account is compromised or the layer is updated maliciously, every consumer function executes attacker code with its IAM role — a direct **privilege escalation** and **lateral movement** path across all functions using that layer.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://docs.aws.amazon.com/lambda/latest/dg/configuration-layers.html", + "https://docs.aws.amazon.com/lambda/latest/dg/best-practices.html", + "https://unit42.paloaltonetworks.com/lambda-layers-supply-chain/" + ], + "Remediation": { + "Code": { + "CLI": "# Copy the cross-account layer version into your own account first:\naws lambda publish-layer-version --layer-name --zip-file fileb://\n# Then update the function to use your own layer ARN:\naws lambda update-function-configuration --function-name --layers ", + "NativeIaC": "```yaml\nAWSTemplateFormatVersion: '2010-09-09'\nResources:\n OwnedLayer:\n Type: AWS::Lambda::LayerVersion\n Properties:\n LayerName: \n Content:\n S3Bucket: \n S3Key: \n LambdaFunction:\n Type: AWS::Lambda::Function\n Properties:\n FunctionName: \n Role: \n Handler: index.handler\n Runtime: python3.12\n Code:\n S3Bucket: \n S3Key: \n Layers:\n - !Ref OwnedLayer\n```", + "Other": "1. Download the cross-account layer ZIP\n2. Publish the layer in your own account: Lambda > Layers > Create layer\n3. Update the function configuration to reference your layer ARN\n4. Remove the cross-account layer ARN from the function", + "Terraform": "```hcl\nresource \"aws_lambda_layer_version\" \"example\" {\n layer_name = \"\"\n filename = \"\"\n}\n\nresource \"aws_lambda_function\" \"example\" {\n function_name = \"\"\n role = \"\"\n handler = \"index.handler\"\n runtime = \"python3.12\"\n filename = \"\"\n\n layers = [aws_lambda_layer_version.example.arn]\n}\n```" + }, + "Recommendation": { + "Text": "Eliminate cross-account layer dependencies by hosting all layers in your own AWS account.\n- Audit all layers with `aws lambda get-function-configuration` and inspect `Layers[].Arn`\n- Extract the account ID from the ARN (field 5 in colon-split) and compare against your account\n- For approved vendor layers, pin to a specific immutable version ARN and review on each update\n- Enforce this via SCP: deny `lambda:UpdateFunctionConfiguration` when `lambda:Layer` ARN does not match your account ID", + "Url": "https://hub.prowler.com/check/awslambda_function_using_cross_account_layers" + } + }, + "Categories": [ + "software-supply-chain" + ], + "DependsOn": [], + "RelatedTo": [], + "Notes": "" +} diff --git a/prowler/providers/aws/services/awslambda/awslambda_function_using_cross_account_layers/awslambda_function_using_cross_account_layers.py b/prowler/providers/aws/services/awslambda/awslambda_function_using_cross_account_layers/awslambda_function_using_cross_account_layers.py new file mode 100644 index 0000000000..1030321fed --- /dev/null +++ b/prowler/providers/aws/services/awslambda/awslambda_function_using_cross_account_layers/awslambda_function_using_cross_account_layers.py @@ -0,0 +1,34 @@ +from prowler.lib.check.models import Check, Check_Report_AWS +from prowler.providers.aws.services.awslambda.awslambda_client import awslambda_client + + +class awslambda_function_using_cross_account_layers(Check): + def execute(self): + findings = [] + for function in awslambda_client.functions.values(): + report = Check_Report_AWS(metadata=self.metadata(), resource=function) + cross_account_layers = [ + layer + for layer in function.layers + if layer.account_id != awslambda_client.audited_account + ] + if not function.layers: + report.status = "PASS" + report.status_extended = ( + f"Lambda function {function.name} does not use any layers." + ) + elif cross_account_layers: + report.status = "FAIL" + layer_arns = ", ".join(layer.arn for layer in cross_account_layers) + report.status_extended = ( + f"Lambda function {function.name} uses cross-account " + f"layer(s): {layer_arns}." + ) + else: + report.status = "PASS" + report.status_extended = ( + f"Lambda function {function.name} only uses layers " + f"from the same account ({awslambda_client.audited_account})." + ) + findings.append(report) + return findings diff --git a/prowler/providers/aws/services/awslambda/awslambda_service.py b/prowler/providers/aws/services/awslambda/awslambda_service.py index aea2bec272..433a5ab588 100644 --- a/prowler/providers/aws/services/awslambda/awslambda_service.py +++ b/prowler/providers/aws/services/awslambda/awslambda_service.py @@ -23,6 +23,7 @@ class Lambda(AWSService): self._list_tags_for_resource() self.__threading_call__(self._get_policy) self.__threading_call__(self._get_function_url_config) + self.__threading_call__(self._list_event_source_mappings) def _list_functions(self, regional_client): logger.info("Lambda - Listing Functions...") @@ -54,6 +55,19 @@ class Lambda(AWSService): "Variables" ) self.functions[lambda_arn].environment = lambda_environment + if "KMSKeyArn" in function: + self.functions[lambda_arn].kms_key_arn = function[ + "KMSKeyArn" + ] + if "Layers" in function: + self.functions[lambda_arn].layers = [ + Layer(arn=layer["Arn"]) for layer in function["Layers"] + ] + dlq_arn = function.get("DeadLetterConfig", {}).get("TargetArn") + if dlq_arn: + self.functions[lambda_arn].dead_letter_config = ( + DeadLetterConfig(target_arn=dlq_arn) + ) except Exception as error: logger.error( @@ -62,6 +76,33 @@ class Lambda(AWSService): f" {error}" ) + def _list_event_source_mappings(self, regional_client): + logger.info("Lambda - Listing Event Source Mappings...") + try: + paginator = regional_client.get_paginator("list_event_source_mappings") + for page in paginator.paginate(): + for mapping in page.get("EventSourceMappings", []): + function_arn = mapping.get("FunctionArn", "") + # Normalise to unqualified ARN (strip :qualifier suffix if present) + base_arn = ":".join(function_arn.split(":")[:7]) + if base_arn not in self.functions: + continue + self.functions[base_arn].event_source_mappings.append( + EventSourceMapping( + uuid=mapping["UUID"], + event_source_arn=mapping.get("EventSourceArn", ""), + state=mapping.get("State", ""), + batch_size=mapping.get("BatchSize"), + starting_position=mapping.get("StartingPosition"), + ) + ) + except Exception as error: + logger.error( + f"{regional_client.region} --" + f" {error.__class__.__name__}[{error.__traceback__.tb_lineno}]:" + f" {error}" + ) + def _get_function_code(self): logger.info("Lambda - Getting Function Code...") # Use a thread pool handle the queueing and execution of the _fetch_function_code tasks, up to max_workers tasks concurrently. @@ -192,16 +233,42 @@ class URLConfig(BaseModel): cors_config: URLConfigCORS +class Layer(BaseModel): + arn: str + + @property + def account_id(self) -> str: + """Extract the account ID from the layer ARN.""" + parts = self.arn.split(":") + return parts[4] if len(parts) >= 5 else "" + + +class DeadLetterConfig(BaseModel): + target_arn: str + + +class EventSourceMapping(BaseModel): + uuid: str + event_source_arn: str + state: str + batch_size: Optional[int] = None + starting_position: Optional[str] = None + + class Function(BaseModel): name: str arn: str security_groups: list runtime: Optional[str] = None - environment: dict = None + environment: Optional[dict] = None region: str policy: dict = {} code: LambdaCode = None url_config: URLConfig = None vpc_id: Optional[str] = None subnet_ids: Optional[set] = None + kms_key_arn: Optional[str] = None + layers: list[Layer] = [] + dead_letter_config: Optional[DeadLetterConfig] = None + event_source_mappings: list[EventSourceMapping] = [] tags: Optional[list] = [] diff --git a/tests/providers/aws/services/awslambda/awslambda_function_env_vars_not_encrypted_with_cmk/awslambda_function_env_vars_not_encrypted_with_cmk_test.py b/tests/providers/aws/services/awslambda/awslambda_function_env_vars_not_encrypted_with_cmk/awslambda_function_env_vars_not_encrypted_with_cmk_test.py new file mode 100644 index 0000000000..ac648d2e93 --- /dev/null +++ b/tests/providers/aws/services/awslambda/awslambda_function_env_vars_not_encrypted_with_cmk/awslambda_function_env_vars_not_encrypted_with_cmk_test.py @@ -0,0 +1,197 @@ +from json import dumps +from unittest import mock + +from boto3 import client +from moto import mock_aws + +from tests.providers.aws.utils import ( + AWS_ACCOUNT_NUMBER, + AWS_REGION_EU_WEST_1, + set_mocked_aws_provider, +) + +ROLE_POLICY = dumps( + { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": {"Service": "lambda.amazonaws.com"}, + "Action": "sts:AssumeRole", + } + ], + } +) + + +class Test_awslambda_function_env_vars_not_encrypted_with_cmk: + def test_no_functions(self): + from prowler.providers.aws.services.awslambda.awslambda_service import Lambda + + aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), + mock.patch( + "prowler.providers.aws.services.awslambda.awslambda_function_env_vars_not_encrypted_with_cmk.awslambda_function_env_vars_not_encrypted_with_cmk.awslambda_client", + new=Lambda(aws_provider), + ), + ): + from prowler.providers.aws.services.awslambda.awslambda_function_env_vars_not_encrypted_with_cmk.awslambda_function_env_vars_not_encrypted_with_cmk import ( + awslambda_function_env_vars_not_encrypted_with_cmk, + ) + + check = awslambda_function_env_vars_not_encrypted_with_cmk() + result = check.execute() + + assert len(result) == 0 + + @mock_aws + def test_function_no_env_vars(self): + iam_client = client("iam", region_name=AWS_REGION_EU_WEST_1) + role_arn = iam_client.create_role( + RoleName="test-role", + AssumeRolePolicyDocument=ROLE_POLICY, + )["Role"]["Arn"] + + lambda_client = client("lambda", region_name=AWS_REGION_EU_WEST_1) + function_name = "test-fn-no-env" + function_arn = lambda_client.create_function( + FunctionName=function_name, + Runtime="python3.11", + Role=role_arn, + Handler="index.handler", + Code={"ZipFile": b"file not used"}, + )["FunctionArn"] + + from prowler.providers.aws.services.awslambda.awslambda_service import Lambda + + aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), + mock.patch( + "prowler.providers.aws.services.awslambda.awslambda_function_env_vars_not_encrypted_with_cmk.awslambda_function_env_vars_not_encrypted_with_cmk.awslambda_client", + new=Lambda(aws_provider), + ), + ): + from prowler.providers.aws.services.awslambda.awslambda_function_env_vars_not_encrypted_with_cmk.awslambda_function_env_vars_not_encrypted_with_cmk import ( + awslambda_function_env_vars_not_encrypted_with_cmk, + ) + + check = awslambda_function_env_vars_not_encrypted_with_cmk() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert "no environment variables" in result[0].status_extended + assert result[0].resource_id == function_name + assert result[0].resource_arn == function_arn + assert result[0].region == AWS_REGION_EU_WEST_1 + + @mock_aws + def test_function_env_vars_no_kms(self): + iam_client = client("iam", region_name=AWS_REGION_EU_WEST_1) + role_arn = iam_client.create_role( + RoleName="test-role", + AssumeRolePolicyDocument=ROLE_POLICY, + )["Role"]["Arn"] + + lambda_client = client("lambda", region_name=AWS_REGION_EU_WEST_1) + function_name = "test-fn-env-no-kms" + function_arn = lambda_client.create_function( + FunctionName=function_name, + Runtime="python3.11", + Role=role_arn, + Handler="index.handler", + Code={"ZipFile": b"file not used"}, + Environment={"Variables": {"DB_HOST": "localhost"}}, + )["FunctionArn"] + + from prowler.providers.aws.services.awslambda.awslambda_service import Lambda + + aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), + mock.patch( + "prowler.providers.aws.services.awslambda.awslambda_function_env_vars_not_encrypted_with_cmk.awslambda_function_env_vars_not_encrypted_with_cmk.awslambda_client", + new=Lambda(aws_provider), + ), + ): + from prowler.providers.aws.services.awslambda.awslambda_function_env_vars_not_encrypted_with_cmk.awslambda_function_env_vars_not_encrypted_with_cmk import ( + awslambda_function_env_vars_not_encrypted_with_cmk, + ) + + check = awslambda_function_env_vars_not_encrypted_with_cmk() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert "customer-managed KMS key" in result[0].status_extended + assert result[0].resource_id == function_name + assert result[0].resource_arn == function_arn + assert result[0].region == AWS_REGION_EU_WEST_1 + + @mock_aws + def test_function_env_vars_with_cmk(self): + iam_client = client("iam", region_name=AWS_REGION_EU_WEST_1) + role_arn = iam_client.create_role( + RoleName="test-role", + AssumeRolePolicyDocument=ROLE_POLICY, + )["Role"]["Arn"] + + lambda_client = client("lambda", region_name=AWS_REGION_EU_WEST_1) + function_name = "test-fn-env-with-kms" + key_arn = ( + f"arn:aws:kms:{AWS_REGION_EU_WEST_1}:{AWS_ACCOUNT_NUMBER}:key/test-key-id" + ) + function_arn = lambda_client.create_function( + FunctionName=function_name, + Runtime="python3.11", + Role=role_arn, + Handler="index.handler", + Code={"ZipFile": b"file not used"}, + Environment={"Variables": {"DB_HOST": "localhost"}}, + )["FunctionArn"] + + from prowler.providers.aws.services.awslambda.awslambda_service import Lambda + + aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) + lambda_service = Lambda(aws_provider) + + # moto does not return KMSKeyArn in list_functions; inject it to test PASS branch. + lambda_service.functions[function_arn].kms_key_arn = key_arn + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), + mock.patch( + "prowler.providers.aws.services.awslambda.awslambda_function_env_vars_not_encrypted_with_cmk.awslambda_function_env_vars_not_encrypted_with_cmk.awslambda_client", + new=lambda_service, + ), + ): + from prowler.providers.aws.services.awslambda.awslambda_function_env_vars_not_encrypted_with_cmk.awslambda_function_env_vars_not_encrypted_with_cmk import ( + awslambda_function_env_vars_not_encrypted_with_cmk, + ) + + check = awslambda_function_env_vars_not_encrypted_with_cmk() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert key_arn in result[0].status_extended + assert result[0].resource_id == function_name + assert result[0].resource_arn == function_arn + assert result[0].region == AWS_REGION_EU_WEST_1 diff --git a/tests/providers/aws/services/awslambda/awslambda_function_no_dead_letter_queue/awslambda_function_no_dead_letter_queue_test.py b/tests/providers/aws/services/awslambda/awslambda_function_no_dead_letter_queue/awslambda_function_no_dead_letter_queue_test.py new file mode 100644 index 0000000000..0daeb5261b --- /dev/null +++ b/tests/providers/aws/services/awslambda/awslambda_function_no_dead_letter_queue/awslambda_function_no_dead_letter_queue_test.py @@ -0,0 +1,149 @@ +from json import dumps +from unittest import mock + +from boto3 import client +from moto import mock_aws + +from prowler.providers.aws.services.awslambda.awslambda_service import DeadLetterConfig +from tests.providers.aws.utils import AWS_REGION_EU_WEST_1, set_mocked_aws_provider + +ROLE_POLICY = dumps( + { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": {"Service": "lambda.amazonaws.com"}, + "Action": "sts:AssumeRole", + } + ], + } +) + + +class Test_awslambda_function_no_dead_letter_queue: + def test_no_functions(self): + from prowler.providers.aws.services.awslambda.awslambda_service import Lambda + + aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), + mock.patch( + "prowler.providers.aws.services.awslambda.awslambda_function_no_dead_letter_queue.awslambda_function_no_dead_letter_queue.awslambda_client", + new=Lambda(aws_provider), + ), + ): + from prowler.providers.aws.services.awslambda.awslambda_function_no_dead_letter_queue.awslambda_function_no_dead_letter_queue import ( + awslambda_function_no_dead_letter_queue, + ) + + check = awslambda_function_no_dead_letter_queue() + result = check.execute() + + assert len(result) == 0 + + @mock_aws + def test_function_without_dlq(self): + iam_client = client("iam", region_name=AWS_REGION_EU_WEST_1) + role_arn = iam_client.create_role( + RoleName="test-role", + AssumeRolePolicyDocument=ROLE_POLICY, + )["Role"]["Arn"] + + lambda_client = client("lambda", region_name=AWS_REGION_EU_WEST_1) + function_name = "test-function-no-dlq" + function_arn = lambda_client.create_function( + FunctionName=function_name, + Runtime="python3.11", + Role=role_arn, + Handler="index.handler", + Code={"ZipFile": b"file not used"}, + )["FunctionArn"] + + from prowler.providers.aws.services.awslambda.awslambda_service import Lambda + + aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), + mock.patch( + "prowler.providers.aws.services.awslambda.awslambda_function_no_dead_letter_queue.awslambda_function_no_dead_letter_queue.awslambda_client", + new=Lambda(aws_provider), + ), + ): + from prowler.providers.aws.services.awslambda.awslambda_function_no_dead_letter_queue.awslambda_function_no_dead_letter_queue import ( + awslambda_function_no_dead_letter_queue, + ) + + check = awslambda_function_no_dead_letter_queue() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert function_name in result[0].status_extended + assert "Dead Letter Queue" in result[0].status_extended + assert result[0].resource_id == function_name + assert result[0].resource_arn == function_arn + assert result[0].region == AWS_REGION_EU_WEST_1 + + @mock_aws + def test_function_with_sqs_dlq(self): + iam_client = client("iam", region_name=AWS_REGION_EU_WEST_1) + role_arn = iam_client.create_role( + RoleName="test-role", + AssumeRolePolicyDocument=ROLE_POLICY, + )["Role"]["Arn"] + + lambda_client = client("lambda", region_name=AWS_REGION_EU_WEST_1) + function_name = "test-function-with-dlq" + queue_arn = f"arn:aws:sqs:{AWS_REGION_EU_WEST_1}:123456789012:test-dlq" + function_arn = lambda_client.create_function( + FunctionName=function_name, + Runtime="python3.11", + Role=role_arn, + Handler="index.handler", + Code={"ZipFile": b"file not used"}, + )["FunctionArn"] + + from prowler.providers.aws.services.awslambda.awslambda_service import Lambda + + aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) + lambda_service = Lambda(aws_provider) + + # moto does not return DeadLetterConfig in list_functions; + # set it directly to test the PASS branch of the check logic. + lambda_service.functions[function_arn].dead_letter_config = DeadLetterConfig( + target_arn=queue_arn + ) + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), + mock.patch( + "prowler.providers.aws.services.awslambda.awslambda_function_no_dead_letter_queue.awslambda_function_no_dead_letter_queue.awslambda_client", + new=lambda_service, + ), + ): + from prowler.providers.aws.services.awslambda.awslambda_function_no_dead_letter_queue.awslambda_function_no_dead_letter_queue import ( + awslambda_function_no_dead_letter_queue, + ) + + check = awslambda_function_no_dead_letter_queue() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert function_name in result[0].status_extended + assert queue_arn in result[0].status_extended + assert result[0].resource_id == function_name + assert result[0].resource_arn == function_arn + assert result[0].region == AWS_REGION_EU_WEST_1 diff --git a/tests/providers/aws/services/awslambda/awslambda_function_using_cross_account_layers/awslambda_function_using_cross_account_layers_test.py b/tests/providers/aws/services/awslambda/awslambda_function_using_cross_account_layers/awslambda_function_using_cross_account_layers_test.py new file mode 100644 index 0000000000..3f0090437f --- /dev/null +++ b/tests/providers/aws/services/awslambda/awslambda_function_using_cross_account_layers/awslambda_function_using_cross_account_layers_test.py @@ -0,0 +1,200 @@ +from json import dumps +from unittest import mock + +from boto3 import client +from moto import mock_aws + +from tests.providers.aws.utils import ( + AWS_ACCOUNT_NUMBER, + AWS_REGION_EU_WEST_1, + set_mocked_aws_provider, +) + +ROLE_POLICY = dumps( + { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": {"Service": "lambda.amazonaws.com"}, + "Action": "sts:AssumeRole", + } + ], + } +) +EXTERNAL_ACCOUNT = "999999999999" + + +def _create_role(iam_client): + return iam_client.create_role( + RoleName="test-role", + AssumeRolePolicyDocument=ROLE_POLICY, + )["Role"]["Arn"] + + +class Test_awslambda_function_using_cross_account_layers: + def test_no_functions(self): + from prowler.providers.aws.services.awslambda.awslambda_service import Lambda + + aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), + mock.patch( + "prowler.providers.aws.services.awslambda.awslambda_function_using_cross_account_layers.awslambda_function_using_cross_account_layers.awslambda_client", + new=Lambda(aws_provider), + ), + ): + from prowler.providers.aws.services.awslambda.awslambda_function_using_cross_account_layers.awslambda_function_using_cross_account_layers import ( + awslambda_function_using_cross_account_layers, + ) + + check = awslambda_function_using_cross_account_layers() + result = check.execute() + + assert len(result) == 0 + + @mock_aws + def test_function_no_layers(self): + iam_client = client("iam", region_name=AWS_REGION_EU_WEST_1) + role_arn = _create_role(iam_client) + + lambda_client = client("lambda", region_name=AWS_REGION_EU_WEST_1) + function_name = "test-fn-no-layers" + function_arn = lambda_client.create_function( + FunctionName=function_name, + Runtime="python3.11", + Role=role_arn, + Handler="index.handler", + Code={"ZipFile": b"file not used"}, + )["FunctionArn"] + + from prowler.providers.aws.services.awslambda.awslambda_service import Lambda + + aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), + mock.patch( + "prowler.providers.aws.services.awslambda.awslambda_function_using_cross_account_layers.awslambda_function_using_cross_account_layers.awslambda_client", + new=Lambda(aws_provider), + ), + ): + from prowler.providers.aws.services.awslambda.awslambda_function_using_cross_account_layers.awslambda_function_using_cross_account_layers import ( + awslambda_function_using_cross_account_layers, + ) + + check = awslambda_function_using_cross_account_layers() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert "does not use any layers" in result[0].status_extended + assert result[0].resource_id == function_name + assert result[0].resource_arn == function_arn + + @mock_aws + def test_function_own_account_layer(self): + iam_client = client("iam", region_name=AWS_REGION_EU_WEST_1) + role_arn = _create_role(iam_client) + + lambda_client = client("lambda", region_name=AWS_REGION_EU_WEST_1) + function_name = "test-fn-own-layer" + function_arn = lambda_client.create_function( + FunctionName=function_name, + Runtime="python3.11", + Role=role_arn, + Handler="index.handler", + Code={"ZipFile": b"file not used"}, + )["FunctionArn"] + + from prowler.providers.aws.services.awslambda.awslambda_service import ( + Lambda, + Layer, + ) + + aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) + lambda_service = Lambda(aws_provider) + + # moto does not return Layers in list_functions; inject an own-account layer. + own_layer_arn = f"arn:aws:lambda:{AWS_REGION_EU_WEST_1}:{AWS_ACCOUNT_NUMBER}:layer:my-layer:1" + lambda_service.functions[function_arn].layers = [Layer(arn=own_layer_arn)] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), + mock.patch( + "prowler.providers.aws.services.awslambda.awslambda_function_using_cross_account_layers.awslambda_function_using_cross_account_layers.awslambda_client", + new=lambda_service, + ), + ): + from prowler.providers.aws.services.awslambda.awslambda_function_using_cross_account_layers.awslambda_function_using_cross_account_layers import ( + awslambda_function_using_cross_account_layers, + ) + + check = awslambda_function_using_cross_account_layers() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert AWS_ACCOUNT_NUMBER in result[0].status_extended + + @mock_aws + def test_function_cross_account_layer(self): + """Function uses a layer from an external account — FAIL.""" + iam_client = client("iam", region_name=AWS_REGION_EU_WEST_1) + role_arn = _create_role(iam_client) + + lambda_client = client("lambda", region_name=AWS_REGION_EU_WEST_1) + function_name = "test-fn-cross-layer" + function_arn = lambda_client.create_function( + FunctionName=function_name, + Runtime="python3.11", + Role=role_arn, + Handler="index.handler", + Code={"ZipFile": b"file not used"}, + )["FunctionArn"] + + from prowler.providers.aws.services.awslambda.awslambda_service import ( + Lambda, + Layer, + ) + + cross_layer_arn = f"arn:aws:lambda:{AWS_REGION_EU_WEST_1}:{EXTERNAL_ACCOUNT}:layer:ext-layer:1" + aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) + lambda_service = Lambda(aws_provider) + + # moto does not return Layers; inject a cross-account layer to test FAIL branch. + lambda_service.functions[function_arn].layers = [Layer(arn=cross_layer_arn)] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), + mock.patch( + "prowler.providers.aws.services.awslambda.awslambda_function_using_cross_account_layers.awslambda_function_using_cross_account_layers.awslambda_client", + new=lambda_service, + ), + ): + from prowler.providers.aws.services.awslambda.awslambda_function_using_cross_account_layers.awslambda_function_using_cross_account_layers import ( + awslambda_function_using_cross_account_layers, + ) + + check = awslambda_function_using_cross_account_layers() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert cross_layer_arn in result[0].status_extended + assert result[0].resource_id == function_name + assert result[0].resource_arn == function_arn + assert result[0].region == AWS_REGION_EU_WEST_1