feat(aws): add elasticbeanstalk_environment_no_secrets_in_configuration check (#12378)

Signed-off-by: unknown <sej1306kook@gmail.com>
Co-authored-by: Daniel Barranquero <danielbo2001@gmail.com>
This commit is contained in:
Sejal
2026-08-28 13:54:23 +02:00
committed by GitHub
co-authored by Daniel Barranquero
parent c88f745038
commit 2cae2058e9
6 changed files with 472 additions and 0 deletions
@@ -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
@@ -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 <environment-name> --option-settings Namespace=<namespace>,OptionName=<option-name>,Value=<secure-reference>",
"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."
}
@@ -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
@@ -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
@@ -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