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 <danielbo2001@gmail.com>
This commit is contained in:
Raajhesh Kannaa Chidambaram
2026-03-25 07:25:36 -04:00
committed by GitHub
parent 1c2b146e6e
commit 6100932c60
5 changed files with 286 additions and 0 deletions
+1
View File
@@ -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)
---
@@ -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_name> --job-update '{\"DefaultArguments\":{\"--secret_name\":\"{{resolve:secretsmanager:my-secret}}\"}}'",
"NativeIaC": "```yaml\nResources:\n GlueJob:\n Type: AWS::Glue::Job\n Properties:\n Name: <job_name>\n Role: <role_arn>\n Command:\n Name: glueetl\n ScriptLocation: \"s3://<bucket>/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 = \"<job_name>\"\n role_arn = \"<role_arn>\"\n\n command {\n script_location = \"s3://<bucket>/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": ""
}
@@ -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
@@ -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"}]