From 2cae2058e9e25e7b9d08c9b4efb6437a61dc91ee Mon Sep 17 00:00:00 2001 From: Sejal Date: Fri, 28 Aug 2026 17:24:23 +0530 Subject: [PATCH] feat(aws): add elasticbeanstalk_environment_no_secrets_in_configuration check (#12378) Signed-off-by: unknown Co-authored-by: Daniel Barranquero --- ...cbeanstalk-environment-no-secrets.added.md | 1 + .../__init__.py | 0 ..._no_secrets_in_configuration.metadata.json | 43 +++ ...environment_no_secrets_in_configuration.py | 110 ++++++ .../elasticbeanstalk_service.py | 3 + ...onment_no_secrets_in_configuration_test.py | 315 ++++++++++++++++++ 6 files changed, 472 insertions(+) create mode 100644 prowler/changelog.d/elasticbeanstalk-environment-no-secrets.added.md create mode 100644 prowler/providers/aws/services/elasticbeanstalk/elasticbeanstalk_environment_no_secrets_in_configuration/__init__.py create mode 100644 prowler/providers/aws/services/elasticbeanstalk/elasticbeanstalk_environment_no_secrets_in_configuration/elasticbeanstalk_environment_no_secrets_in_configuration.metadata.json create mode 100644 prowler/providers/aws/services/elasticbeanstalk/elasticbeanstalk_environment_no_secrets_in_configuration/elasticbeanstalk_environment_no_secrets_in_configuration.py create mode 100644 tests/providers/aws/services/elasticbeanstalk/elasticbeanstalk_environment_no_secrets_in_configuration/elasticbeanstalk_environment_no_secrets_in_configuration_test.py diff --git a/prowler/changelog.d/elasticbeanstalk-environment-no-secrets.added.md b/prowler/changelog.d/elasticbeanstalk-environment-no-secrets.added.md new file mode 100644 index 0000000000..9f85bf8550 --- /dev/null +++ b/prowler/changelog.d/elasticbeanstalk-environment-no-secrets.added.md @@ -0,0 +1 @@ +`elasticbeanstalk_environment_no_secrets_in_configuration` check for AWS provider, scanning the option settings of every Elastic Beanstalk environment for hardcoded secrets diff --git a/prowler/providers/aws/services/elasticbeanstalk/elasticbeanstalk_environment_no_secrets_in_configuration/__init__.py b/prowler/providers/aws/services/elasticbeanstalk/elasticbeanstalk_environment_no_secrets_in_configuration/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/aws/services/elasticbeanstalk/elasticbeanstalk_environment_no_secrets_in_configuration/elasticbeanstalk_environment_no_secrets_in_configuration.metadata.json b/prowler/providers/aws/services/elasticbeanstalk/elasticbeanstalk_environment_no_secrets_in_configuration/elasticbeanstalk_environment_no_secrets_in_configuration.metadata.json new file mode 100644 index 0000000000..04a449298d --- /dev/null +++ b/prowler/providers/aws/services/elasticbeanstalk/elasticbeanstalk_environment_no_secrets_in_configuration/elasticbeanstalk_environment_no_secrets_in_configuration.metadata.json @@ -0,0 +1,43 @@ +{ + "Provider": "aws", + "CheckID": "elasticbeanstalk_environment_no_secrets_in_configuration", + "CheckTitle": "Elastic Beanstalk environment configuration has no hardcoded secrets", + "CheckType": [ + "Software and Configuration Checks/AWS Security Best Practices", + "TTPs/Credential Access", + "Effects/Data Exposure", + "Sensitive Data Identifications/Security" + ], + "ServiceName": "elasticbeanstalk", + "SubServiceName": "", + "ResourceIdTemplate": "arn:partition:elasticbeanstalk:region:account-id:environment/environment-name", + "Severity": "high", + "ResourceType": "AwsElasticBeanstalkEnvironment", + "ResourceGroup": "compute", + "Description": "AWS Elastic Beanstalk environments are inspected for hardcoded secrets in configuration option settings. Secrets such as API keys, passwords, access tokens, or credentials should not be stored in environment configuration.", + "Risk": "Plaintext secrets stored in Elastic Beanstalk environment configuration can be viewed by users with read access to the environment configuration, increasing the risk of credential exposure and unauthorized access to downstream resources.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://docs.aws.amazon.com/elasticbeanstalk/latest/api/API_DescribeConfigurationSettings.html", + "https://docs.aws.amazon.com/boto3/latest/reference/services/elasticbeanstalk/client/describe_configuration_settings.html", + "https://docs.prowler.com/developer-guide/secret-scanning-checks" + ], + "Remediation": { + "Code": { + "CLI": "aws elasticbeanstalk update-environment --environment-name --option-settings Namespace=,OptionName=,Value=", + "NativeIaC": "", + "Other": "1. Review the Elastic Beanstalk environment configuration.\n2. Remove hardcoded secrets from OptionSettings.\n3. Store secrets in AWS Secrets Manager or AWS Systems Manager Parameter Store.\n4. Configure the application to retrieve secrets securely at runtime instead of storing them in environment configuration.", + "Terraform": "" + }, + "Recommendation": { + "Text": "Avoid storing secrets in Elastic Beanstalk environment configuration. Store sensitive values in AWS Secrets Manager or AWS Systems Manager Parameter Store and retrieve them securely at runtime.", + "Url": "https://hub.prowler.com/check/elasticbeanstalk_environment_no_secrets_in_configuration" + } + }, + "Categories": [ + "secrets" + ], + "DependsOn": [], + "RelatedTo": [], + "Notes": "False positives can be suppressed with the `secrets_ignore_patterns` configuration option. When `secrets_validate` is enabled and a detected secret is confirmed to be live, the finding is escalated to critical." +} diff --git a/prowler/providers/aws/services/elasticbeanstalk/elasticbeanstalk_environment_no_secrets_in_configuration/elasticbeanstalk_environment_no_secrets_in_configuration.py b/prowler/providers/aws/services/elasticbeanstalk/elasticbeanstalk_environment_no_secrets_in_configuration/elasticbeanstalk_environment_no_secrets_in_configuration.py new file mode 100644 index 0000000000..6b0ed01942 --- /dev/null +++ b/prowler/providers/aws/services/elasticbeanstalk/elasticbeanstalk_environment_no_secrets_in_configuration/elasticbeanstalk_environment_no_secrets_in_configuration.py @@ -0,0 +1,110 @@ +import json + +from prowler.lib.check.models import Check, Check_Report_AWS +from prowler.lib.utils.utils import ( + SecretsScanError, + annotate_verified_secrets, + detect_secrets_scan_batch, +) +from prowler.providers.aws.services.elasticbeanstalk.elasticbeanstalk_client import ( + elasticbeanstalk_client, +) + + +class elasticbeanstalk_environment_no_secrets_in_configuration(Check): + """Check that Elastic Beanstalk environment configurations contain no hardcoded secrets.""" + + def execute(self) -> list[Check_Report_AWS]: + """Scan the option settings of each Elastic Beanstalk environment for secrets. + + Every option setting is scanned as ``{OptionName: Value}`` so the scanner + gets the same name context the other secrets checks provide. Findings are + keyed by ``(environment index, option setting index)`` because the same + namespace and option name can appear more than once per environment (one + entry per ``ResourceName``). + + Returns: + list[Check_Report_AWS]: A report for each Elastic Beanstalk environment. + """ + findings = [] + secrets_ignore_patterns = elasticbeanstalk_client.audit_config.get( + "secrets_ignore_patterns", [] + ) + validate = elasticbeanstalk_client.audit_config.get("secrets_validate", False) + environments = list(elasticbeanstalk_client.environments.values()) + + # Phase 1: collect — build the payload strings only, no scan yet. + def payloads(): + for environment_index, environment in enumerate(environments): + for option_index, option_setting in enumerate( + environment.option_settings or [] + ): + value = option_setting.get("Value") + if not value: + continue + yield (environment_index, option_index), json.dumps( + {option_setting.get("OptionName", ""): value} + ) + + # Phase 2: batch — one scan for every environment. + scan_error = None + try: + batch_results = detect_secrets_scan_batch( + payloads(), excluded_secrets=secrets_ignore_patterns, validate=validate + ) + except SecretsScanError as error: + batch_results = {} + scan_error = error + + # Phase 3: report — one finding per environment. + for environment_index, environment in enumerate(environments): + report = Check_Report_AWS(metadata=self.metadata(), resource=environment) + + if environment.option_settings is None: + report.status = "MANUAL" + report.status_extended = ( + f"Could not retrieve the configuration of Elastic Beanstalk " + f"environment {environment.name}; manual review is required." + ) + findings.append(report) + continue + + if scan_error and any( + option_setting.get("Value") + for option_setting in environment.option_settings + ): + report.status = "MANUAL" + report.status_extended = ( + f"Could not scan the configuration of Elastic Beanstalk " + f"environment {environment.name} for secrets; manual review is required." + ) + findings.append(report) + continue + + report.status = "PASS" + report.status_extended = f"No secrets found in the configuration of Elastic Beanstalk environment {environment.name}." + + secret_settings = [] + all_secrets = [] + for option_index, option_setting in enumerate(environment.option_settings): + detect_secrets_output = batch_results.get( + (environment_index, option_index) + ) + if detect_secrets_output: + all_secrets.extend(detect_secrets_output) + secret_settings.append( + f"{option_setting.get('Namespace', '')}/{option_setting.get('OptionName', '')}" + ) + + if secret_settings: + report.status = "FAIL" + report.status_extended = ( + f"Potential {'secrets' if len(secret_settings) > 1 else 'secret'} " + f"found in the configuration of Elastic Beanstalk environment " + f"{environment.name} -> {', '.join(secret_settings)}." + ) + annotate_verified_secrets(report, all_secrets) + + findings.append(report) + + return findings diff --git a/prowler/providers/aws/services/elasticbeanstalk/elasticbeanstalk_service.py b/prowler/providers/aws/services/elasticbeanstalk/elasticbeanstalk_service.py index c8d821687d..04db5d52dd 100644 --- a/prowler/providers/aws/services/elasticbeanstalk/elasticbeanstalk_service.py +++ b/prowler/providers/aws/services/elasticbeanstalk/elasticbeanstalk_service.py @@ -56,6 +56,8 @@ class ElasticBeanstalk(AWSService): option_settings = configuration_settings["ConfigurationSettings"][0].get( "OptionSettings", {} ) + environment.option_settings = option_settings + for option in option_settings: if ( option["Namespace"] == "aws:elasticbeanstalk:healthreporting:system" @@ -123,3 +125,4 @@ class Environment(BaseModel): managed_platform_updates: Optional[str] cloudwatch_stream_logs: Optional[str] tags: Optional[list] = [] + option_settings: Optional[list] = None diff --git a/tests/providers/aws/services/elasticbeanstalk/elasticbeanstalk_environment_no_secrets_in_configuration/elasticbeanstalk_environment_no_secrets_in_configuration_test.py b/tests/providers/aws/services/elasticbeanstalk/elasticbeanstalk_environment_no_secrets_in_configuration/elasticbeanstalk_environment_no_secrets_in_configuration_test.py new file mode 100644 index 0000000000..966f129739 --- /dev/null +++ b/tests/providers/aws/services/elasticbeanstalk/elasticbeanstalk_environment_no_secrets_in_configuration/elasticbeanstalk_environment_no_secrets_in_configuration_test.py @@ -0,0 +1,315 @@ +from unittest import mock + +from prowler.lib.utils.utils import SecretsScanError +from prowler.providers.aws.services.elasticbeanstalk.elasticbeanstalk_service import ( + Environment, +) +from tests.providers.aws.utils import ( + AWS_ACCOUNT_NUMBER, + AWS_REGION_US_EAST_1, + set_mocked_aws_provider, +) + + +class Test_elasticbeanstalk_environment_no_secrets_in_configuration: + def test_environment_configuration_with_secrets(self): + elasticbeanstalk_client = mock.MagicMock() + elasticbeanstalk_client.audit_config = {"secrets_ignore_patterns": []} + eb_env_arn = f"arn:partition:elasticbeanstalk:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:environment/production" + + environment = Environment( + id="e-vbxmknpy2z", + name="production", + arn=eb_env_arn, + region=AWS_REGION_US_EAST_1, + application_name="test-app", + option_settings=[ + { + "Namespace": "aws:elasticbeanstalk:application:environment", + "OptionName": "JSON_WEB_TOKEN", + "Value": "test-token", + }, + { + "Namespace": "aws:elasticbeanstalk:application:environment", + "OptionName": "MONGODB_URI", + "Value": "test-mongodb-uri", + }, + ], + ) + elasticbeanstalk_client.environments = {eb_env_arn: environment} + + mocked_scan_results = { + (0, 0): [ + { + "filename": "payload", + "line_number": 1, + "type": "SecretKeyword", + "hashed_secret": "mocked-hash", + "is_verified": False, + } + ], + (0, 1): [ + { + "filename": "payload", + "line_number": 1, + "type": "SecretKeyword", + "hashed_secret": "mocked-hash-2", + "is_verified": False, + } + ], + } + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_aws_provider(), + ), + mock.patch( + "prowler.providers.aws.services.elasticbeanstalk.elasticbeanstalk_environment_no_secrets_in_configuration.elasticbeanstalk_environment_no_secrets_in_configuration.elasticbeanstalk_client", + new=elasticbeanstalk_client, + ), + mock.patch( + "prowler.providers.aws.services.elasticbeanstalk.elasticbeanstalk_environment_no_secrets_in_configuration.elasticbeanstalk_environment_no_secrets_in_configuration.detect_secrets_scan_batch", + return_value=mocked_scan_results, + ), + ): + from prowler.providers.aws.services.elasticbeanstalk.elasticbeanstalk_environment_no_secrets_in_configuration.elasticbeanstalk_environment_no_secrets_in_configuration import ( + elasticbeanstalk_environment_no_secrets_in_configuration, + ) + + check = elasticbeanstalk_environment_no_secrets_in_configuration() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert environment.name in result[0].status_extended + assert "JSON_WEB_TOKEN" in result[0].status_extended + assert "MONGODB_URI" in result[0].status_extended + + def test_environment_configuration_without_secrets(self): + elasticbeanstalk_client = mock.MagicMock() + elasticbeanstalk_client.audit_config = {"secrets_ignore_patterns": []} + eb_env_arn = f"arn:partition:elasticbeanstalk:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:environment/staging" + + environment = Environment( + id="e-icsgecu3wf", + name="staging", + arn=eb_env_arn, + region=AWS_REGION_US_EAST_1, + application_name="test-app", + option_settings=[ + { + "Namespace": "aws:elasticbeanstalk:application:environment", + "OptionName": "LOG_LEVEL", + "Value": "INFO", + }, + { + "Namespace": "aws:elasticbeanstalk:application:environment", + "OptionName": "SystemType", + "Value": "enhanced", + }, + ], + ) + elasticbeanstalk_client.environments = {eb_env_arn: environment} + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_aws_provider(), + ), + mock.patch( + "prowler.providers.aws.services.elasticbeanstalk.elasticbeanstalk_environment_no_secrets_in_configuration.elasticbeanstalk_environment_no_secrets_in_configuration.elasticbeanstalk_client", + new=elasticbeanstalk_client, + ), + ): + from prowler.providers.aws.services.elasticbeanstalk.elasticbeanstalk_environment_no_secrets_in_configuration.elasticbeanstalk_environment_no_secrets_in_configuration import ( + elasticbeanstalk_environment_no_secrets_in_configuration, + ) + + check = elasticbeanstalk_environment_no_secrets_in_configuration() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == f"No secrets found in the configuration of Elastic Beanstalk environment {environment.name}." + ) + + def test_environment_configuration_scan_error(self): + elasticbeanstalk_client = mock.MagicMock() + elasticbeanstalk_client.audit_config = {"secrets_ignore_patterns": []} + eb_env_arn = f"arn:partition:elasticbeanstalk:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:environment/testing" + + environment = Environment( + id="e-mz7paq4pqp", + name="testing", + arn=eb_env_arn, + region=AWS_REGION_US_EAST_1, + application_name="test-app", + option_settings=[ + { + "Namespace": "aws:elasticbeanstalk:application:environment", + "OptionName": "DB_SSL_ENABLED", + "Value": "true", + }, + { + "Namespace": "aws:elasticbeanstalk:application:environment", + "OptionName": "CACHE_TTL", + "Value": "300", + }, + ], + ) + elasticbeanstalk_client.environments = {eb_env_arn: environment} + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_aws_provider(), + ), + mock.patch( + "prowler.providers.aws.services.elasticbeanstalk.elasticbeanstalk_environment_no_secrets_in_configuration.elasticbeanstalk_environment_no_secrets_in_configuration.elasticbeanstalk_client", + new=elasticbeanstalk_client, + ), + mock.patch( + "prowler.providers.aws.services.elasticbeanstalk.elasticbeanstalk_environment_no_secrets_in_configuration.elasticbeanstalk_environment_no_secrets_in_configuration.detect_secrets_scan_batch", + side_effect=SecretsScanError("Kingfisher exited with code 1"), + ), + ): + from prowler.providers.aws.services.elasticbeanstalk.elasticbeanstalk_environment_no_secrets_in_configuration.elasticbeanstalk_environment_no_secrets_in_configuration import ( + elasticbeanstalk_environment_no_secrets_in_configuration, + ) + + check = elasticbeanstalk_environment_no_secrets_in_configuration() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "MANUAL" + assert ( + f"Could not scan the configuration of Elastic Beanstalk environment {environment.name} for secrets; manual review is required." + in result[0].status_extended + ) + + def test_no_environment(self): + elasticbeanstalk_client = mock.MagicMock() + elasticbeanstalk_client.audit_config = {"secrets_ignore_patterns": []} + + elasticbeanstalk_client.environments = {} + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_aws_provider(), + ), + mock.patch( + "prowler.providers.aws.services.elasticbeanstalk.elasticbeanstalk_environment_no_secrets_in_configuration.elasticbeanstalk_environment_no_secrets_in_configuration.elasticbeanstalk_client", + new=elasticbeanstalk_client, + ), + ): + from prowler.providers.aws.services.elasticbeanstalk.elasticbeanstalk_environment_no_secrets_in_configuration.elasticbeanstalk_environment_no_secrets_in_configuration import ( + elasticbeanstalk_environment_no_secrets_in_configuration, + ) + + check = elasticbeanstalk_environment_no_secrets_in_configuration() + result = check.execute() + + assert len(result) == 0 + + def test_environment_option_settings_is_none(self): + elasticbeanstalk_client = mock.MagicMock() + elasticbeanstalk_client.audit_config = {"secrets_ignore_patterns": []} + eb_env_arn = f"arn:partition:elasticbeanstalk:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:environment/dev" + + environment = Environment( + id="e-c4f7hda2nb", + name="dev", + arn=eb_env_arn, + region=AWS_REGION_US_EAST_1, + application_name="test-app", + option_settings=None, + ) + elasticbeanstalk_client.environments = {eb_env_arn: environment} + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_aws_provider(), + ), + mock.patch( + "prowler.providers.aws.services.elasticbeanstalk.elasticbeanstalk_environment_no_secrets_in_configuration.elasticbeanstalk_environment_no_secrets_in_configuration.elasticbeanstalk_client", + new=elasticbeanstalk_client, + ), + ): + from prowler.providers.aws.services.elasticbeanstalk.elasticbeanstalk_environment_no_secrets_in_configuration.elasticbeanstalk_environment_no_secrets_in_configuration import ( + elasticbeanstalk_environment_no_secrets_in_configuration, + ) + + check = elasticbeanstalk_environment_no_secrets_in_configuration() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "MANUAL" + assert ( + result[0].status_extended + == f"Could not retrieve the configuration of Elastic Beanstalk environment {environment.name}; manual review is required." + ) + + def test_environment_configuration_with_password_real_scanner(self): + """Run the real scanner: a plaintext password in an environment variable must be reported. + + The option name is part of the scanned payload; without it generic + credentials such as passwords are not detected. + """ + elasticbeanstalk_client = mock.MagicMock() + elasticbeanstalk_client.audit_config = {"secrets_ignore_patterns": []} + eb_env_arn = f"arn:partition:elasticbeanstalk:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:environment/production" + + environment = Environment( + id="e-vbxmknpy2z", + name="production", + arn=eb_env_arn, + region=AWS_REGION_US_EAST_1, + application_name="test-app", + option_settings=[ + { + "Namespace": "aws:elasticbeanstalk:application:environment", + "OptionName": "APP_ENV", + "Value": "production", + }, + { + "Namespace": "aws:elasticbeanstalk:application:environment", + "OptionName": "DB_PASSWORD", + "Value": "Tr0ub4dor3xKq9vLmZ", + }, + { + "Namespace": "aws:autoscaling:launchconfiguration", + "OptionName": "InstanceType", + "Value": "t3.micro", + }, + ], + ) + elasticbeanstalk_client.environments = {eb_env_arn: environment} + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_aws_provider(), + ), + mock.patch( + "prowler.providers.aws.services.elasticbeanstalk.elasticbeanstalk_environment_no_secrets_in_configuration.elasticbeanstalk_environment_no_secrets_in_configuration.elasticbeanstalk_client", + new=elasticbeanstalk_client, + ), + ): + from prowler.providers.aws.services.elasticbeanstalk.elasticbeanstalk_environment_no_secrets_in_configuration.elasticbeanstalk_environment_no_secrets_in_configuration import ( + elasticbeanstalk_environment_no_secrets_in_configuration, + ) + + check = elasticbeanstalk_environment_no_secrets_in_configuration() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "Potential secret found in the configuration of Elastic Beanstalk environment production -> aws:elasticbeanstalk:application:environment/DB_PASSWORD." + ) + assert result[0].resource_id == "e-vbxmknpy2z" + assert result[0].resource_arn == eb_env_arn + assert result[0].region == AWS_REGION_US_EAST_1