From 6100932c60c3fbaba831cd3ae6a83aeda884e7bc Mon Sep 17 00:00:00 2001 From: Raajhesh Kannaa Chidambaram Date: Wed, 25 Mar 2026 07:25:36 -0400 Subject: [PATCH] feat(glue): add check for plaintext secrets in ETL job arguments (#10368) Co-authored-by: Raajhesh Kannaa Chidambaram <495042+raajheshkannaa@users.noreply.github.com> Co-authored-by: Daniel Barranquero --- prowler/CHANGELOG.md | 1 + .../__init__.py | 0 ...jobs_no_secrets_in_arguments.metadata.json | 43 ++++ .../glue_etl_jobs_no_secrets_in_arguments.py | 52 +++++ ...e_etl_jobs_no_secrets_in_arguments_test.py | 190 ++++++++++++++++++ 5 files changed, 286 insertions(+) create mode 100644 prowler/providers/aws/services/glue/glue_etl_jobs_no_secrets_in_arguments/__init__.py create mode 100644 prowler/providers/aws/services/glue/glue_etl_jobs_no_secrets_in_arguments/glue_etl_jobs_no_secrets_in_arguments.metadata.json create mode 100644 prowler/providers/aws/services/glue/glue_etl_jobs_no_secrets_in_arguments/glue_etl_jobs_no_secrets_in_arguments.py create mode 100644 tests/providers/aws/services/glue/glue_etl_jobs_no_secrets_in_arguments/glue_etl_jobs_no_secrets_in_arguments_test.py diff --git a/prowler/CHANGELOG.md b/prowler/CHANGELOG.md index 36b380bbb3..79cd5d1169 100644 --- a/prowler/CHANGELOG.md +++ b/prowler/CHANGELOG.md @@ -9,6 +9,7 @@ All notable changes to the **Prowler SDK** are documented in this file. - `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) - `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) --- diff --git a/prowler/providers/aws/services/glue/glue_etl_jobs_no_secrets_in_arguments/__init__.py b/prowler/providers/aws/services/glue/glue_etl_jobs_no_secrets_in_arguments/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/aws/services/glue/glue_etl_jobs_no_secrets_in_arguments/glue_etl_jobs_no_secrets_in_arguments.metadata.json b/prowler/providers/aws/services/glue/glue_etl_jobs_no_secrets_in_arguments/glue_etl_jobs_no_secrets_in_arguments.metadata.json new file mode 100644 index 0000000000..530599053d --- /dev/null +++ b/prowler/providers/aws/services/glue/glue_etl_jobs_no_secrets_in_arguments/glue_etl_jobs_no_secrets_in_arguments.metadata.json @@ -0,0 +1,43 @@ +{ + "Provider": "aws", + "CheckID": "glue_etl_jobs_no_secrets_in_arguments", + "CheckTitle": "Glue ETL job has no secrets in default arguments", + "CheckType": [ + "Software and Configuration Checks/AWS Security Best Practices", + "TTPs/Credential Access", + "Effects/Data Exposure", + "Sensitive Data Identifications/Security" + ], + "ServiceName": "glue", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "critical", + "ResourceType": "Other", + "ResourceGroup": "analytics", + "Description": "**AWS Glue ETL jobs** are inspected for **default arguments** (`DefaultArguments`) that resemble **secrets** (keys, tokens, passwords).\n\nSuch values indicate sensitive data is stored directly in job arguments instead of being sourced securely from AWS Secrets Manager or Systems Manager Parameter Store.", + "Risk": "Plaintext secrets in default arguments reduce confidentiality: values can be viewed in consoles, CLI output, and CloudTrail logs. Compromised credentials enable unauthorized AWS actions, data exfiltration, and lateral movement across the environment.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://docs.aws.amazon.com/glue/latest/dg/aws-glue-programming-etl-glue-arguments.html", + "https://docs.aws.amazon.com/glue/latest/webapi/API_Job.html", + "https://docs.aws.amazon.com/secretsmanager/latest/userguide/intro.html" + ], + "Remediation": { + "Code": { + "CLI": "aws glue update-job --job-name --job-update '{\"DefaultArguments\":{\"--secret_name\":\"{{resolve:secretsmanager:my-secret}}\"}}'", + "NativeIaC": "```yaml\nResources:\n GlueJob:\n Type: AWS::Glue::Job\n Properties:\n Name: \n Role: \n Command:\n Name: glueetl\n ScriptLocation: \"s3:///script.py\"\n DefaultArguments:\n \"--secret_name\": !Sub \"{{resolve:secretsmanager:${MySecret}}}\" # Reference secret from Secrets Manager instead of plaintext\n```", + "Other": "1. Open the AWS Glue console and go to Jobs\n2. Select the job and click Edit\n3. Under Job parameters, identify any arguments containing sensitive values\n4. Store those values in AWS Secrets Manager or Systems Manager Parameter Store\n5. Update the job arguments to reference the secret by name or ARN instead of the plaintext value\n6. Save the job", + "Terraform": "```hcl\nresource \"aws_glue_job\" \"example\" {\n name = \"\"\n role_arn = \"\"\n\n command {\n script_location = \"s3:///script.py\"\n }\n\n default_arguments = {\n \"--secret_name\" = aws_secretsmanager_secret_version.example.secret_string # Reference secret from Secrets Manager instead of plaintext\n }\n}\n```" + }, + "Recommendation": { + "Text": "Store secrets in **AWS Secrets Manager** or **AWS Systems Manager Parameter Store** and reference them by name or ARN in job arguments instead of embedding plaintext values. Enforce **least privilege** on the Glue job IAM role, rotate secrets regularly, and avoid logging or exporting argument values.", + "Url": "https://hub.prowler.com/check/glue_etl_jobs_no_secrets_in_arguments" + } + }, + "Categories": [ + "secrets" + ], + "DependsOn": [], + "RelatedTo": [], + "Notes": "" +} diff --git a/prowler/providers/aws/services/glue/glue_etl_jobs_no_secrets_in_arguments/glue_etl_jobs_no_secrets_in_arguments.py b/prowler/providers/aws/services/glue/glue_etl_jobs_no_secrets_in_arguments/glue_etl_jobs_no_secrets_in_arguments.py new file mode 100644 index 0000000000..50c92f8619 --- /dev/null +++ b/prowler/providers/aws/services/glue/glue_etl_jobs_no_secrets_in_arguments/glue_etl_jobs_no_secrets_in_arguments.py @@ -0,0 +1,52 @@ +import json + +from prowler.lib.check.models import Check, Check_Report_AWS +from prowler.lib.utils.utils import detect_secrets_scan +from prowler.providers.aws.services.glue.glue_client import glue_client + + +class glue_etl_jobs_no_secrets_in_arguments(Check): + """Check if Glue ETL jobs have secrets in their default arguments. + + Scans the DefaultArguments of each Glue job for hardcoded credentials, + tokens, passwords, and other sensitive values that should be stored in + Secrets Manager or Parameter Store instead. + """ + + def execute(self): + findings = [] + secrets_ignore_patterns = glue_client.audit_config.get( + "secrets_ignore_patterns", [] + ) + for job in glue_client.jobs: + report = Check_Report_AWS(metadata=self.metadata(), resource=job) + report.status = "PASS" + report.status_extended = ( + f"No secrets found in Glue job {job.name} default arguments." + ) + + if job.arguments: + secrets_found = [] + for arg_name, arg_value in job.arguments.items(): + detect_secrets_output = detect_secrets_scan( + data=json.dumps({arg_name: arg_value}), + excluded_secrets=secrets_ignore_patterns, + detect_secrets_plugins=glue_client.audit_config.get( + "detect_secrets_plugins", + ), + ) + if detect_secrets_output: + secrets_found.extend( + [ + f"{secret['type']} in argument {arg_name}" + for secret in detect_secrets_output + ] + ) + + if secrets_found: + report.status = "FAIL" + report.status_extended = f"Potential secrets found in Glue job {job.name} default arguments: {', '.join(secrets_found)}." + + findings.append(report) + + return findings diff --git a/tests/providers/aws/services/glue/glue_etl_jobs_no_secrets_in_arguments/glue_etl_jobs_no_secrets_in_arguments_test.py b/tests/providers/aws/services/glue/glue_etl_jobs_no_secrets_in_arguments/glue_etl_jobs_no_secrets_in_arguments_test.py new file mode 100644 index 0000000000..def9b16a13 --- /dev/null +++ b/tests/providers/aws/services/glue/glue_etl_jobs_no_secrets_in_arguments/glue_etl_jobs_no_secrets_in_arguments_test.py @@ -0,0 +1,190 @@ +from unittest import mock + +from boto3 import client +from moto import mock_aws + +from tests.providers.aws.utils import ( + AWS_ACCOUNT_NUMBER, + AWS_REGION_US_EAST_1, + set_mocked_aws_provider, +) + + +class Test_glue_etl_jobs_no_secrets_in_arguments: + @mock_aws + def test_glue_no_jobs(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, + ): + with mock.patch( + "prowler.providers.aws.services.glue.glue_etl_jobs_no_secrets_in_arguments.glue_etl_jobs_no_secrets_in_arguments.glue_client", + new=Glue(aws_provider), + ): + from prowler.providers.aws.services.glue.glue_etl_jobs_no_secrets_in_arguments.glue_etl_jobs_no_secrets_in_arguments import ( + glue_etl_jobs_no_secrets_in_arguments, + ) + + check = glue_etl_jobs_no_secrets_in_arguments() + result = check.execute() + + assert len(result) == 0 + + @mock_aws + def test_glue_job_no_secrets(self): + glue_client = client("glue", region_name=AWS_REGION_US_EAST_1) + job_name = "test-job" + job_arn = ( + f"arn:aws:glue:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:job/{job_name}" + ) + glue_client.create_job( + Name=job_name, + Role="role_test", + Command={"Name": "name_test", "ScriptLocation": "script_test"}, + DefaultArguments={ + "--enable-continuous-cloudwatch-log": "true", + "--TempDir": "s3://my-bucket/temp/", + }, + Tags={"key_test": "value_test"}, + GlueVersion="1.0", + MaxCapacity=0.0625, + MaxRetries=0, + Timeout=10, + NumberOfWorkers=2, + WorkerType="G.1X", + ) + + 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, + ): + with mock.patch( + "prowler.providers.aws.services.glue.glue_etl_jobs_no_secrets_in_arguments.glue_etl_jobs_no_secrets_in_arguments.glue_client", + new=Glue(aws_provider), + ): + from prowler.providers.aws.services.glue.glue_etl_jobs_no_secrets_in_arguments.glue_etl_jobs_no_secrets_in_arguments import ( + glue_etl_jobs_no_secrets_in_arguments, + ) + + check = glue_etl_jobs_no_secrets_in_arguments() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == f"No secrets found in Glue job {job_name} default arguments." + ) + assert result[0].resource_id == job_name + assert result[0].resource_arn == job_arn + assert result[0].resource_tags == [{"key_test": "value_test"}] + + @mock_aws + def test_glue_job_with_secrets(self): + glue_client = client("glue", region_name=AWS_REGION_US_EAST_1) + job_name = "test-job" + job_arn = ( + f"arn:aws:glue:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:job/{job_name}" + ) + glue_client.create_job( + Name=job_name, + Role="role_test", + Command={"Name": "name_test", "ScriptLocation": "script_test"}, + DefaultArguments={ + "--db-password": "AKIAsupersecretkey1234", + "--TempDir": "s3://my-bucket/temp/", + }, + Tags={"key_test": "value_test"}, + GlueVersion="1.0", + MaxCapacity=0.0625, + MaxRetries=0, + Timeout=10, + NumberOfWorkers=2, + WorkerType="G.1X", + ) + + 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, + ): + with mock.patch( + "prowler.providers.aws.services.glue.glue_etl_jobs_no_secrets_in_arguments.glue_etl_jobs_no_secrets_in_arguments.glue_client", + new=Glue(aws_provider), + ): + from prowler.providers.aws.services.glue.glue_etl_jobs_no_secrets_in_arguments.glue_etl_jobs_no_secrets_in_arguments import ( + glue_etl_jobs_no_secrets_in_arguments, + ) + + check = glue_etl_jobs_no_secrets_in_arguments() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert "Potential secrets found" in result[0].status_extended + assert job_name in result[0].status_extended + assert "--db-password" in result[0].status_extended + assert result[0].resource_id == job_name + assert result[0].resource_arn == job_arn + assert result[0].resource_tags == [{"key_test": "value_test"}] + + @mock_aws + def test_glue_job_empty_arguments(self): + glue_client = client("glue", region_name=AWS_REGION_US_EAST_1) + job_name = "test-job" + job_arn = ( + f"arn:aws:glue:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:job/{job_name}" + ) + glue_client.create_job( + Name=job_name, + Role="role_test", + Command={"Name": "name_test", "ScriptLocation": "script_test"}, + DefaultArguments={}, + Tags={"key_test": "value_test"}, + GlueVersion="1.0", + MaxCapacity=0.0625, + MaxRetries=0, + Timeout=10, + NumberOfWorkers=2, + WorkerType="G.1X", + ) + + 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, + ): + with mock.patch( + "prowler.providers.aws.services.glue.glue_etl_jobs_no_secrets_in_arguments.glue_etl_jobs_no_secrets_in_arguments.glue_client", + new=Glue(aws_provider), + ): + from prowler.providers.aws.services.glue.glue_etl_jobs_no_secrets_in_arguments.glue_etl_jobs_no_secrets_in_arguments import ( + glue_etl_jobs_no_secrets_in_arguments, + ) + + check = glue_etl_jobs_no_secrets_in_arguments() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == f"No secrets found in Glue job {job_name} default arguments." + ) + assert result[0].resource_id == job_name + assert result[0].resource_arn == job_arn + assert result[0].resource_tags == [{"key_test": "value_test"}]