mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-10-09 21:14:22 +00:00
feat(aws): add glue_catalog_connection_no_secrets check (#11963)
Signed-off-by: Alex Chen <l46983284@gmail.com> Co-authored-by: Daniel Barranquero <danielbo2001@gmail.com> Co-authored-by: Rishi943 <84287593+Rishi943@users.noreply.github.com> Co-authored-by: Utkarsh <udaydeepak1928@gmail.com>
This commit is contained in:
co-authored by
Daniel Barranquero
Rishi943
Utkarsh
parent
fc0204a40d
commit
0b98a34687
@@ -0,0 +1 @@
|
||||
Add glue_catalog_connection_no_secrets check to detect secrets in Glue Data Catalog connection properties
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"Provider": "aws",
|
||||
"CheckID": "glue_catalog_connection_no_secrets",
|
||||
"CheckTitle": "Glue Data Catalog connection has no secrets in connection properties",
|
||||
"CheckType": [
|
||||
"Software and Configuration Checks/AWS Security Best Practices",
|
||||
"TTPs/Credential Access",
|
||||
"Effects/Data Exposure",
|
||||
"Sensitive Data Identifications/Security"
|
||||
],
|
||||
"ServiceName": "glue",
|
||||
"SubServiceName": "",
|
||||
"ResourceIdTemplate": "",
|
||||
"Severity": "high",
|
||||
"ResourceType": "Other",
|
||||
"ResourceGroup": "analytics",
|
||||
"Description": "**AWS Glue Data Catalog connections** are inspected for **ConnectionProperties** values that resemble **secrets** (keys, tokens, passwords).\n\nSuch values indicate sensitive data is stored directly in connection configuration instead of being sourced securely from AWS Secrets Manager or Systems Manager Parameter Store.",
|
||||
"Risk": "Plaintext secrets in Glue connection properties reduce confidentiality: values can be viewed in consoles, CLI output, and CloudTrail logs. Compromised credentials enable unauthorized data access and lateral movement.",
|
||||
"RelatedUrl": "",
|
||||
"AdditionalURLs": [
|
||||
"https://docs.aws.amazon.com/glue/latest/dg/console-connections.html",
|
||||
"https://docs.aws.amazon.com/glue/latest/webapi/API_Connection.html",
|
||||
"https://docs.aws.amazon.com/secretsmanager/latest/userguide/intro.html"
|
||||
],
|
||||
"Remediation": {
|
||||
"Code": {
|
||||
"CLI": "aws glue update-connection --name <connection_name> --connection-input '{\"ConnectionProperties\":{\"SECRET_ID\":\"arn:aws:secretsmanager:REGION:ACCOUNT:secret:NAME\"}}'",
|
||||
"NativeIaC": "```yaml\nResources:\n GlueConnection:\n Type: AWS::Glue::Connection\n Properties:\n CatalogId: !Ref AWS::AccountId\n ConnectionInput:\n Name: <connection_name>\n ConnectionType: JDBC\n ConnectionProperties:\n SECRET_ID: !Ref MySecretArn # Reference secret instead of plaintext password\n```",
|
||||
"Other": "1. Open the AWS Glue console and go to Data Catalog > Connections\n2. Select the connection and click Edit\n3. Identify any ConnectionProperties containing sensitive values (passwords, keys, tokens)\n4. Store those values in AWS Secrets Manager or Systems Manager Parameter Store\n5. Update the connection to reference the secret by name or ARN instead of plaintext\n6. Save the connection",
|
||||
"Terraform": "```hcl\nresource \"aws_glue_connection\" \"example\" {\n name = \"<connection_name>\"\n\n connection_properties = {\n SECRET_ID = aws_secretsmanager_secret.example.arn # Reference secret instead of plaintext\n }\n}\n```"
|
||||
},
|
||||
"Recommendation": {
|
||||
"Text": "Store secrets in **AWS Secrets Manager** or **AWS Systems Manager Parameter Store** and reference them from Glue connection properties instead of embedding plaintext values. Enforce **least privilege** on Glue IAM roles and rotate secrets regularly.",
|
||||
"Url": "https://hub.prowler.com/check/glue_catalog_connection_no_secrets"
|
||||
}
|
||||
},
|
||||
"Categories": [
|
||||
"secrets"
|
||||
],
|
||||
"DependsOn": [],
|
||||
"RelatedTo": [],
|
||||
"Notes": ""
|
||||
}
|
||||
+90
@@ -0,0 +1,90 @@
|
||||
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.glue.glue_client import glue_client
|
||||
|
||||
|
||||
class glue_catalog_connection_no_secrets(Check):
|
||||
"""Check if Glue Data Catalog connections store secrets in ConnectionProperties.
|
||||
|
||||
Scans the ConnectionProperties of each Data Catalog connection for
|
||||
hardcoded credentials, tokens, passwords, and other sensitive values that
|
||||
should be stored in Secrets Manager or Parameter Store instead.
|
||||
"""
|
||||
|
||||
def execute(self) -> list[Check_Report_AWS]:
|
||||
findings = []
|
||||
secrets_ignore_patterns = glue_client.audit_config.get(
|
||||
"secrets_ignore_patterns", []
|
||||
)
|
||||
validate = glue_client.audit_config.get("secrets_validate", False)
|
||||
connections = list(glue_client.connections)
|
||||
|
||||
# Collect every connection property across all connections and scan them
|
||||
# in batched Kingfisher invocations instead of one subprocess per
|
||||
# property. Findings are keyed by (connection index, property name) so a
|
||||
# detected secret is reported against the exact property it came from.
|
||||
def payloads():
|
||||
for conn_index, connection in enumerate(connections):
|
||||
if connection.properties:
|
||||
for prop_name, prop_value in connection.properties.items():
|
||||
yield (conn_index, prop_name), json.dumps(
|
||||
{prop_name: prop_value}
|
||||
)
|
||||
|
||||
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
|
||||
|
||||
for conn_index, connection in enumerate(connections):
|
||||
report = Check_Report_AWS(metadata=self.metadata(), resource=connection)
|
||||
report.status = "PASS"
|
||||
report.status_extended = (
|
||||
f"No secrets found in Glue Data Catalog connection "
|
||||
f"{connection.name} properties."
|
||||
)
|
||||
|
||||
if connection.properties and scan_error:
|
||||
report.status = "MANUAL"
|
||||
report.status_extended = (
|
||||
f"Could not scan Glue Data Catalog connection {connection.name} "
|
||||
f"properties for secrets: {scan_error}; manual review is required."
|
||||
)
|
||||
findings.append(report)
|
||||
continue
|
||||
|
||||
if connection.properties:
|
||||
secrets_found = []
|
||||
all_secrets = []
|
||||
for prop_name in connection.properties:
|
||||
detect_secrets_output = batch_results.get((conn_index, prop_name))
|
||||
if detect_secrets_output:
|
||||
all_secrets.extend(detect_secrets_output)
|
||||
secrets_found.extend(
|
||||
[
|
||||
f"{secret['type']} in property {prop_name}"
|
||||
for secret in detect_secrets_output
|
||||
]
|
||||
)
|
||||
|
||||
if secrets_found:
|
||||
report.status = "FAIL"
|
||||
report.status_extended = (
|
||||
f"Potential secrets found in Glue Data Catalog connection "
|
||||
f"{connection.name} properties: {', '.join(secrets_found)}."
|
||||
)
|
||||
annotate_verified_secrets(report, all_secrets)
|
||||
|
||||
findings.append(report)
|
||||
|
||||
return findings
|
||||
+270
@@ -0,0 +1,270 @@
|
||||
from unittest import mock
|
||||
|
||||
import botocore
|
||||
from moto import mock_aws
|
||||
|
||||
from tests.providers.aws.utils import (
|
||||
AWS_ACCOUNT_NUMBER,
|
||||
AWS_REGION_US_EAST_1,
|
||||
set_mocked_aws_provider,
|
||||
)
|
||||
|
||||
make_api_call = botocore.client.BaseClient._make_api_call
|
||||
|
||||
|
||||
def mock_make_api_call_no_connections(self, operation_name, kwarg):
|
||||
if operation_name == "GetConnections":
|
||||
return {"ConnectionList": []}
|
||||
return make_api_call(self, operation_name, kwarg)
|
||||
|
||||
|
||||
def mock_make_api_call_no_secrets(self, operation_name, kwarg):
|
||||
if operation_name == "GetConnections":
|
||||
return {
|
||||
"ConnectionList": [
|
||||
{
|
||||
"Name": "jdbc-clean",
|
||||
"ConnectionType": "JDBC",
|
||||
"ConnectionProperties": {
|
||||
"JDBC_CONNECTION_URL": "jdbc:postgresql://db.example:5432/app",
|
||||
"USERNAME": "app_user",
|
||||
},
|
||||
}
|
||||
]
|
||||
}
|
||||
if operation_name == "GetTags":
|
||||
return {"Tags": {"env": "test"}}
|
||||
return make_api_call(self, operation_name, kwarg)
|
||||
|
||||
|
||||
def mock_make_api_call_with_secrets(self, operation_name, kwarg):
|
||||
if operation_name == "GetConnections":
|
||||
return {
|
||||
"ConnectionList": [
|
||||
{
|
||||
"Name": "jdbc-secret",
|
||||
"ConnectionType": "JDBC",
|
||||
"ConnectionProperties": {
|
||||
"JDBC_CONNECTION_URL": "jdbc:postgresql://db.example:5432/app",
|
||||
"PASSWORD": "AKIAsupersecretkey1234",
|
||||
},
|
||||
}
|
||||
]
|
||||
}
|
||||
if operation_name == "GetTags":
|
||||
return {"Tags": {"env": "test"}}
|
||||
return make_api_call(self, operation_name, kwarg)
|
||||
|
||||
|
||||
def mock_make_api_call_empty_properties(self, operation_name, kwarg):
|
||||
if operation_name == "GetConnections":
|
||||
return {
|
||||
"ConnectionList": [
|
||||
{
|
||||
"Name": "empty-props",
|
||||
"ConnectionType": "JDBC",
|
||||
"ConnectionProperties": {},
|
||||
}
|
||||
]
|
||||
}
|
||||
if operation_name == "GetTags":
|
||||
return {"Tags": {}}
|
||||
return make_api_call(self, operation_name, kwarg)
|
||||
|
||||
|
||||
class Test_glue_catalog_connection_no_secrets:
|
||||
@mock_aws
|
||||
@mock.patch(
|
||||
"botocore.client.BaseClient._make_api_call",
|
||||
new=mock_make_api_call_no_connections,
|
||||
)
|
||||
def test_glue_no_connections(self):
|
||||
from prowler.providers.aws.services.glue.glue_service import Glue
|
||||
|
||||
aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1])
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=aws_provider,
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.aws.services.glue.glue_catalog_connection_no_secrets.glue_catalog_connection_no_secrets.glue_client",
|
||||
new=Glue(aws_provider),
|
||||
),
|
||||
):
|
||||
from prowler.providers.aws.services.glue.glue_catalog_connection_no_secrets.glue_catalog_connection_no_secrets import (
|
||||
glue_catalog_connection_no_secrets,
|
||||
)
|
||||
|
||||
check = glue_catalog_connection_no_secrets()
|
||||
result = check.execute()
|
||||
|
||||
assert len(result) == 0
|
||||
|
||||
@mock_aws
|
||||
@mock.patch(
|
||||
"botocore.client.BaseClient._make_api_call", new=mock_make_api_call_no_secrets
|
||||
)
|
||||
def test_glue_connection_no_secrets(self):
|
||||
from prowler.providers.aws.services.glue.glue_service import Glue
|
||||
|
||||
aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1])
|
||||
connection_arn = (
|
||||
f"arn:aws:glue:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:"
|
||||
f"connection/jdbc-clean"
|
||||
)
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=aws_provider,
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.aws.services.glue.glue_catalog_connection_no_secrets.glue_catalog_connection_no_secrets.glue_client",
|
||||
new=Glue(aws_provider),
|
||||
),
|
||||
):
|
||||
from prowler.providers.aws.services.glue.glue_catalog_connection_no_secrets.glue_catalog_connection_no_secrets import (
|
||||
glue_catalog_connection_no_secrets,
|
||||
)
|
||||
|
||||
check = glue_catalog_connection_no_secrets()
|
||||
result = check.execute()
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "PASS"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== "No secrets found in Glue Data Catalog connection jdbc-clean properties."
|
||||
)
|
||||
assert result[0].resource_id == "jdbc-clean"
|
||||
assert result[0].resource_arn == connection_arn
|
||||
assert result[0].resource_tags == [{"env": "test"}]
|
||||
assert result[0].region == AWS_REGION_US_EAST_1
|
||||
|
||||
@mock_aws
|
||||
@mock.patch(
|
||||
"botocore.client.BaseClient._make_api_call", new=mock_make_api_call_with_secrets
|
||||
)
|
||||
def test_glue_connection_with_secrets(self):
|
||||
from prowler.providers.aws.services.glue.glue_service import Glue
|
||||
|
||||
aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1])
|
||||
connection_arn = (
|
||||
f"arn:aws:glue:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:"
|
||||
f"connection/jdbc-secret"
|
||||
)
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=aws_provider,
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.aws.services.glue.glue_catalog_connection_no_secrets.glue_catalog_connection_no_secrets.glue_client",
|
||||
new=Glue(aws_provider),
|
||||
),
|
||||
):
|
||||
from prowler.providers.aws.services.glue.glue_catalog_connection_no_secrets.glue_catalog_connection_no_secrets import (
|
||||
glue_catalog_connection_no_secrets,
|
||||
)
|
||||
|
||||
check = glue_catalog_connection_no_secrets()
|
||||
result = check.execute()
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "FAIL"
|
||||
assert "Potential secrets found" in result[0].status_extended
|
||||
assert "jdbc-secret" in result[0].status_extended
|
||||
assert "in property PASSWORD" in result[0].status_extended
|
||||
assert "AKIAsupersecretkey1234" not in result[0].status_extended
|
||||
assert result[0].resource_id == "jdbc-secret"
|
||||
assert result[0].resource_arn == connection_arn
|
||||
assert result[0].resource_tags == [{"env": "test"}]
|
||||
assert result[0].region == AWS_REGION_US_EAST_1
|
||||
|
||||
@mock_aws
|
||||
@mock.patch(
|
||||
"botocore.client.BaseClient._make_api_call",
|
||||
new=mock_make_api_call_empty_properties,
|
||||
)
|
||||
def test_glue_connection_empty_properties(self):
|
||||
from prowler.providers.aws.services.glue.glue_service import Glue
|
||||
|
||||
aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1])
|
||||
connection_arn = (
|
||||
f"arn:aws:glue:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:"
|
||||
f"connection/empty-props"
|
||||
)
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=aws_provider,
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.aws.services.glue.glue_catalog_connection_no_secrets.glue_catalog_connection_no_secrets.glue_client",
|
||||
new=Glue(aws_provider),
|
||||
),
|
||||
):
|
||||
from prowler.providers.aws.services.glue.glue_catalog_connection_no_secrets.glue_catalog_connection_no_secrets import (
|
||||
glue_catalog_connection_no_secrets,
|
||||
)
|
||||
|
||||
check = glue_catalog_connection_no_secrets()
|
||||
result = check.execute()
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "PASS"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== "No secrets found in Glue Data Catalog connection empty-props properties."
|
||||
)
|
||||
assert result[0].resource_id == "empty-props"
|
||||
assert result[0].resource_arn == connection_arn
|
||||
assert result[0].resource_tags == [{}]
|
||||
assert result[0].region == AWS_REGION_US_EAST_1
|
||||
|
||||
@mock_aws
|
||||
@mock.patch(
|
||||
"botocore.client.BaseClient._make_api_call", new=mock_make_api_call_with_secrets
|
||||
)
|
||||
def test_glue_connection_scan_error(self):
|
||||
from prowler.lib.utils.utils import SecretsScanError
|
||||
from prowler.providers.aws.services.glue.glue_service import Glue
|
||||
|
||||
aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1])
|
||||
connection_arn = (
|
||||
f"arn:aws:glue:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:"
|
||||
f"connection/jdbc-secret"
|
||||
)
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=aws_provider,
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.aws.services.glue.glue_catalog_connection_no_secrets.glue_catalog_connection_no_secrets.glue_client",
|
||||
new=Glue(aws_provider),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.aws.services.glue.glue_catalog_connection_no_secrets.glue_catalog_connection_no_secrets.detect_secrets_scan_batch",
|
||||
side_effect=SecretsScanError("secret scan failed"),
|
||||
),
|
||||
):
|
||||
from prowler.providers.aws.services.glue.glue_catalog_connection_no_secrets.glue_catalog_connection_no_secrets import (
|
||||
glue_catalog_connection_no_secrets,
|
||||
)
|
||||
|
||||
check = glue_catalog_connection_no_secrets()
|
||||
result = check.execute()
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "MANUAL"
|
||||
assert "manual review is required" in result[0].status_extended
|
||||
assert "jdbc-secret" in result[0].status_extended
|
||||
assert result[0].resource_id == "jdbc-secret"
|
||||
assert result[0].resource_arn == connection_arn
|
||||
assert result[0].region == AWS_REGION_US_EAST_1
|
||||
Reference in New Issue
Block a user