feat(batch): add batch_job_definition_no_secrets check (#12117)

Co-authored-by: Daniel Barranquero <danielbo2001@gmail.com>
This commit is contained in:
praneetrajv
2026-08-07 11:57:58 +02:00
committed by GitHub
co-authored by Daniel Barranquero
parent 6e71dee85d
commit 94594d6766
11 changed files with 1039 additions and 0 deletions
@@ -0,0 +1 @@
`batch_job_definition_no_secrets` check for AWS provider, scanning Batch job definition environment variables and command parameters for hardcoded secrets
+2
View File
@@ -27,6 +27,8 @@ aws:
max_lambda_functions: null
# aws.max_ecs_task_definitions --> ecs_task_definitions_* checks
max_ecs_task_definitions: null
# aws.max_batch_job_definitions --> batch_job_definition_* checks
max_batch_job_definitions: null
# aws.max_codeartifact_packages --> codeartifact_packages_* checks
max_codeartifact_packages: null
# aws.disallowed_regions --> List of AWS regions to exclude from the scan.
+6
View File
@@ -153,6 +153,12 @@ class AWSProviderConfig(ProviderConfigBase):
le=1_000_000,
description="Resource scan limit for ECS task definitions. Use 0 or -1 to disable.",
)
max_batch_job_definitions: ResourceScanLimit = Field(
default=None,
ge=-1,
le=1_000_000,
description="Resource scan limit for Batch job definitions. Use 0 or -1 to disable.",
)
max_codeartifact_packages: ResourceScanLimit = Field(
default=None,
ge=-1,
@@ -0,0 +1,6 @@
"""AWS Batch service client singleton."""
from prowler.providers.aws.services.batch.batch_service import Batch
from prowler.providers.common.provider import Provider
batch_client = Batch(Provider.get_global_provider())
@@ -0,0 +1,41 @@
{
"Provider": "aws",
"CheckID": "batch_job_definition_no_secrets",
"CheckTitle": "AWS Batch job definitions have no secrets in environment variables or command parameters",
"CheckType": [
"Software and Configuration Checks/AWS Security Best Practices",
"Software and Configuration Checks/Industry and Regulatory Standards/AWS Foundational Security Best Practices",
"Sensitive Data Identifications/Passwords",
"TTPs/Credential Access"
],
"ServiceName": "batch",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "high",
"ResourceType": "AwsBatchJobDefinition",
"ResourceGroup": "container",
"Description": "**AWS Batch job definitions** are analyzed for **plaintext secrets** placed in container `environment` variables and `command` parameters. It identifies values that resemble credentials (keys, tokens, passwords) within job definitions.",
"Risk": "Exposed secrets in env vars or command parameters undermine confidentiality via logs, job metadata, and introspection.\n\nWith container or read-only API access, attackers can reuse credentials to read databases, modify records (integrity), pivot to other services, and trigger outages or unauthorized costs (availability).",
"RelatedUrl": "",
"AdditionalURLs": [
"https://docs.aws.amazon.com/batch/latest/userguide/job_definition_parameters.html"
],
"Remediation": {
"Code": {
"CLI": "aws batch register-job-definition --job-definition-name <JOB_DEFINITION_NAME> --type container --container-properties '{\"image\":\"<IMAGE>\",\"secrets\":[{\"name\":\"<SECRET_NAME>\",\"valueFrom\":\"arn:aws:secretsmanager:<REGION>:<ACCOUNT_ID>:secret:<SECRET_NAME>-<RANDOM>\"}]}' # Register a new revision without plaintext secrets; reference Secrets Manager or SSM Parameter Store via valueFrom",
"NativeIaC": "```yaml\nResources:\n <example_resource_name>:\n Type: AWS::Batch::JobDefinition\n Properties:\n Type: container\n ContainerProperties:\n Image: <image>\n Environment:\n - Name: DB_PASSWORD\n Value: !Ref <secret_parameter> # Reference SSM Parameter or Secrets Manager\n```",
"Other": "1. In the AWS Console, go to Batch > Job Definitions and open your job definition\n2. Create a new revision\n3. Remove any sensitive values from Environment variables and command parameters\n4. Reference secrets from AWS Secrets Manager or SSM Parameter Store instead\n5. Save to create the new revision\n6. Update any Batch job queues to use the new job definition revision",
"Terraform": "```hcl\nresource \"aws_batch_job_definition\" \"<example_resource_name>\" {\n name = \"<example_resource_name>\"\n type = \"container\"\n\n container_properties = jsonencode({\n image = \"<image>\"\n environment = [\n {\n name = \"DB_PASSWORD\"\n value = var.db_password # Use variable from Secrets Manager or SSM\n }\n ]\n })\n}\n```"
},
"Recommendation": {
"Text": "Store secrets in **AWS Secrets Manager** or **SSM Parameter Store** and inject them at runtime instead of plaintext env vars.\n\nApply **least privilege** via job role, enable regular **rotation**, avoid logging secret values, and prefer **ephemeral credentials** for downstream services.",
"Url": "https://hub.prowler.com/check/batch_job_definition_no_secrets"
}
},
"Categories": [
"secrets"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": "Only container job definitions are evaluated (containerProperties.environment and command). Multi-node parallel (nodeProperties) and EKS (eksProperties) job definitions are not analyzed."
}
@@ -0,0 +1,120 @@
from json import dumps
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.batch.batch_client import batch_client
class batch_job_definition_no_secrets(Check):
"""Detect secrets in AWS Batch job definition environment variables and commands."""
def execute(self) -> list[Check_Report_AWS]:
"""Scan job definitions for hardcoded secrets in env vars and commands."""
findings = []
secrets_ignore_patterns = batch_client.audit_config.get(
"secrets_ignore_patterns", []
)
validate = batch_client.audit_config.get("secrets_validate", False)
job_definitions = list(batch_client.job_definitions.values())
def scan_payloads():
"""Yield index-keyed payloads for each env var and the command."""
for jd_index, job_definition in enumerate(job_definitions):
container = job_definition.container_properties
for env_index, env_var in enumerate(container.environment):
yield (jd_index, env_index), dumps(
{env_var.name: env_var.value}, indent=2
)
if container.command:
yield (
(jd_index, "command"),
" ".join(container.command),
)
scan_error = None
try:
batch_results = detect_secrets_scan_batch(
scan_payloads(),
excluded_secrets=secrets_ignore_patterns,
validate=validate,
)
except SecretsScanError as error:
batch_results = {}
scan_error = error
for jd_index, job_definition in enumerate(job_definitions):
report = Check_Report_AWS(
metadata=self.metadata(),
resource=job_definition,
)
report.resource_id = f"{job_definition.name}:{job_definition.revision}"
report.status = "PASS"
extended_status_parts = []
all_secrets = []
container = job_definition.container_properties
if scan_error and (container.environment or container.command):
report.status = "MANUAL"
report.status_extended = (
f"Could not scan Batch job definition "
f"{job_definition.name} with revision "
f"{job_definition.revision} for secrets: "
f"{scan_error}; manual review is required."
)
findings.append(report)
continue
for env_index, env_var in enumerate(container.environment):
env_secrets = batch_results.get((jd_index, env_index))
if env_secrets:
all_secrets.extend(env_secrets)
secrets_string = ", ".join(
f"{secret['type']} on the environment variable {env_var.name}"
for secret in env_secrets
)
extended_status_parts.append(
f"Secrets in environment variables -> {secrets_string}"
)
if container.command:
command_secrets = batch_results.get((jd_index, "command"))
if command_secrets:
all_secrets.extend(command_secrets)
secrets_string = ", ".join(
secret["type"] for secret in command_secrets
)
extended_status_parts.append(
f"Secrets in command -> {secrets_string}"
)
if extended_status_parts:
report.status = "FAIL"
report.status_extended = (
f"Potential secrets found in Batch job definition "
f"{job_definition.name} with revision "
f"{job_definition.revision}: "
+ "; ".join(extended_status_parts)
+ "."
)
annotate_verified_secrets(report, all_secrets)
else:
report.status_extended = (
f"No secrets found in Batch job definition "
f"{job_definition.name} with revision "
f"{job_definition.revision}."
)
findings.append(report)
return findings
@@ -0,0 +1,105 @@
from itertools import zip_longest
from typing import Optional
from pydantic.v1 import BaseModel
from prowler.lib.logger import logger
from prowler.lib.resource_limit import get_resource_scan_limit, limit_resources
from prowler.lib.scan_filters.scan_filters import is_resource_filtered
from prowler.providers.aws.lib.service.service import AWSService
class ContainerEnvVariable(BaseModel):
"""An environment variable name-value pair."""
name: str
value: str
class BatchContainerProperties(BaseModel):
"""Container properties for an AWS Batch job definition."""
image: Optional[str]
command: list[str] = []
environment: list[ContainerEnvVariable] = []
class BatchJobDefinition(BaseModel):
"""An AWS Batch job definition with its container properties."""
name: str
arn: str
revision: int
region: str
container_properties: BatchContainerProperties
class Batch(AWSService):
"""AWS Batch service client for listing job definitions."""
def __init__(self, provider):
super().__init__(__class__.__name__, provider)
self.job_definitions = {}
self._job_definitions_by_region = {}
self.job_definition_limit = get_resource_scan_limit(
self.audit_config, "max_batch_job_definitions"
)
self.__threading_call__(self._list_job_definitions)
self._select_job_definitions_for_analysis()
def _list_job_definitions(self, regional_client):
"""List ACTIVE job definitions for a regional client."""
logger.info("Batch - Listing Job Definitions...")
try:
paginator = regional_client.get_paginator("describe_job_definitions")
regional_job_definitions = []
# Deregistered (INACTIVE) revisions are excluded: they cannot run
# new jobs, and reporting them would only produce noise.
for page in paginator.paginate(status="ACTIVE"):
for job in page.get("jobDefinitions", []):
if self.audit_resources and not is_resource_filtered(
job["jobDefinitionArn"], self.audit_resources
):
continue
container = job.get("containerProperties", {})
environment = [
ContainerEnvVariable(
name=env["name"], value=env.get("value", "")
)
for env in container.get("environment", [])
]
regional_job_definitions.append(
BatchJobDefinition(
name=job["jobDefinitionName"],
arn=job["jobDefinitionArn"],
revision=job["revision"],
region=regional_client.region,
container_properties=BatchContainerProperties(
image=container.get("image"),
command=container.get("command", []),
environment=environment,
),
)
)
self._job_definitions_by_region[regional_client.region] = (
regional_job_definitions
)
except Exception as error:
logger.error(
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
def _select_job_definitions_for_analysis(self):
"""Apply the global resource limit, interleaving regions fairly."""
interleaved = [
job_definition
for region_batch in zip_longest(*self._job_definitions_by_region.values())
for job_definition in region_batch
if job_definition
]
self.job_definitions = {
job_definition.arn: job_definition
for job_definition in limit_resources(
interleaved, self.job_definition_limit
)
}
@@ -0,0 +1,535 @@
from unittest import mock
from unittest.mock import patch
from boto3 import client
from moto import mock_aws
from prowler.providers.aws.services.batch.batch_service import (
BatchContainerProperties,
BatchJobDefinition,
ContainerEnvVariable,
)
from tests.providers.aws.utils import AWS_REGION_US_EAST_1, set_mocked_aws_provider
JOB_NAME = "test-batch-job"
JOB_REVISION = 1
ENV_VAR_NAME_NO_SECRETS = "host"
ENV_VAR_VALUE_NO_SECRETS = "localhost:1234"
ENV_VAR_NAME_WITH_KEYWORD = "DB_PASSWORD"
# Realistic fake secrets that Kingfisher actually detects.
ENV_VAR_VALUE_WITH_SECRETS = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.dozjgNryP4J3jVmNHl0w5N_XgL0n3I9PlFUP0THsR8U"
ENV_VAR_NAME_WITH_KEYWORD2 = "DATABASE_PASSWORD"
ENV_VAR_VALUE_WITH_SECRETS2 = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI5ODc2NTQzMjEwIiwibmFtZSI6IkphbmUifQ.s5LqY8mC2pX1vN0bQwReTyUiOpAsDfGhJkLzXcVbNm0"
ENV_VAR_VALUE_GENERIC_SECRET = "Tr0ub4dor3xKq9vLmZ"
class Test_batch_job_definition_no_secrets:
def test_no_job_definitions(self):
from prowler.providers.aws.services.batch.batch_service import Batch
mocked_aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1])
with (
patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=mocked_aws_provider,
),
patch(
"prowler.providers.aws.services.batch.batch_job_definition_no_secrets.batch_job_definition_no_secrets.batch_client",
new=Batch(mocked_aws_provider),
),
):
from prowler.providers.aws.services.batch.batch_job_definition_no_secrets.batch_job_definition_no_secrets import (
batch_job_definition_no_secrets,
)
check = batch_job_definition_no_secrets()
result = check.execute()
assert len(result) == 0
@mock_aws
def test_job_definition_env_var_no_secrets(self):
batch_client = client("batch", region_name=AWS_REGION_US_EAST_1)
response = batch_client.register_job_definition(
jobDefinitionName=JOB_NAME,
type="container",
containerProperties={
"image": "test-image:latest",
"memory": 128,
"vcpus": 1,
"environment": [
{
"name": ENV_VAR_NAME_NO_SECRETS,
"value": ENV_VAR_VALUE_NO_SECRETS,
}
],
},
)
job_arn = response["jobDefinitionArn"]
from prowler.providers.aws.services.batch.batch_service import Batch
mocked_aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1])
with (
patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=mocked_aws_provider,
),
patch(
"prowler.providers.aws.services.batch.batch_job_definition_no_secrets.batch_job_definition_no_secrets.batch_client",
new=Batch(mocked_aws_provider),
),
):
from prowler.providers.aws.services.batch.batch_job_definition_no_secrets.batch_job_definition_no_secrets import (
batch_job_definition_no_secrets,
)
check = batch_job_definition_no_secrets()
result = check.execute()
assert len(result) == 1
assert result[0].status == "PASS"
assert (
result[0].status_extended
== f"No secrets found in Batch job definition {JOB_NAME} with revision {JOB_REVISION}."
)
assert result[0].resource_id == f"{JOB_NAME}:{JOB_REVISION}"
assert result[0].resource_arn == job_arn
assert result[0].region == AWS_REGION_US_EAST_1
@mock_aws
def test_job_definition_env_var_with_secret(self):
batch_client = client("batch", region_name=AWS_REGION_US_EAST_1)
response = batch_client.register_job_definition(
jobDefinitionName=JOB_NAME,
type="container",
containerProperties={
"image": "test-image:latest",
"memory": 128,
"vcpus": 1,
"environment": [
{
"name": ENV_VAR_NAME_NO_SECRETS,
"value": ENV_VAR_VALUE_WITH_SECRETS,
}
],
},
)
job_arn = response["jobDefinitionArn"]
from prowler.providers.aws.services.batch.batch_service import Batch
mocked_aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1])
with (
patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=mocked_aws_provider,
),
patch(
"prowler.providers.aws.services.batch.batch_job_definition_no_secrets.batch_job_definition_no_secrets.batch_client",
new=Batch(mocked_aws_provider),
),
):
from prowler.providers.aws.services.batch.batch_job_definition_no_secrets.batch_job_definition_no_secrets import (
batch_job_definition_no_secrets,
)
check = batch_job_definition_no_secrets()
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert (
f"Potential secrets found in Batch job definition {JOB_NAME} with revision {JOB_REVISION}:"
in result[0].status_extended
)
assert (
"JSON Web Token (base64url-encoded) on the environment variable host"
in result[0].status_extended
)
assert result[0].resource_id == f"{JOB_NAME}:{JOB_REVISION}"
assert result[0].resource_arn == job_arn
assert result[0].region == AWS_REGION_US_EAST_1
@mock_aws
def test_job_definition_env_var_with_keyword(self):
batch_client = client("batch", region_name=AWS_REGION_US_EAST_1)
response = batch_client.register_job_definition(
jobDefinitionName=JOB_NAME,
type="container",
containerProperties={
"image": "test-image:latest",
"memory": 128,
"vcpus": 1,
"environment": [
{
"name": ENV_VAR_NAME_WITH_KEYWORD,
"value": ENV_VAR_VALUE_GENERIC_SECRET,
}
],
},
)
job_arn = response["jobDefinitionArn"]
from prowler.providers.aws.services.batch.batch_service import Batch
mocked_aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1])
with (
patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=mocked_aws_provider,
),
patch(
"prowler.providers.aws.services.batch.batch_job_definition_no_secrets.batch_job_definition_no_secrets.batch_client",
new=Batch(mocked_aws_provider),
),
):
from prowler.providers.aws.services.batch.batch_job_definition_no_secrets.batch_job_definition_no_secrets import (
batch_job_definition_no_secrets,
)
check = batch_job_definition_no_secrets()
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert (
f"Potential secrets found in Batch job definition {JOB_NAME} with revision {JOB_REVISION}:"
in result[0].status_extended
)
assert (
"Generic Password on the environment variable DB_PASSWORD"
in result[0].status_extended
)
assert result[0].resource_id == f"{JOB_NAME}:{JOB_REVISION}"
assert result[0].resource_arn == job_arn
assert result[0].region == AWS_REGION_US_EAST_1
@mock_aws
def test_job_definition_no_env_vars(self):
batch_client = client("batch", region_name=AWS_REGION_US_EAST_1)
response = batch_client.register_job_definition(
jobDefinitionName=JOB_NAME,
type="container",
containerProperties={
"image": "test-image:latest",
"memory": 128,
"vcpus": 1,
},
)
job_arn = response["jobDefinitionArn"]
from prowler.providers.aws.services.batch.batch_service import Batch
mocked_aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1])
with (
patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=mocked_aws_provider,
),
patch(
"prowler.providers.aws.services.batch.batch_job_definition_no_secrets.batch_job_definition_no_secrets.batch_client",
new=Batch(mocked_aws_provider),
),
):
from prowler.providers.aws.services.batch.batch_job_definition_no_secrets.batch_job_definition_no_secrets import (
batch_job_definition_no_secrets,
)
check = batch_job_definition_no_secrets()
result = check.execute()
assert len(result) == 1
assert result[0].status == "PASS"
assert (
result[0].status_extended
== f"No secrets found in Batch job definition {JOB_NAME} with revision {JOB_REVISION}."
)
assert result[0].resource_id == f"{JOB_NAME}:{JOB_REVISION}"
assert result[0].resource_arn == job_arn
assert result[0].region == AWS_REGION_US_EAST_1
@mock_aws
def test_job_definition_command_with_secret(self):
batch_client = client("batch", region_name=AWS_REGION_US_EAST_1)
response = batch_client.register_job_definition(
jobDefinitionName=JOB_NAME,
type="container",
containerProperties={
"image": "test-image:latest",
"memory": 128,
"vcpus": 1,
"command": [
"python",
"app.py",
"--token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.dozjgNryP4J3jVmNHl0w5N_XgL0n3I9PlFUP0THsR8U",
],
},
)
job_arn = response["jobDefinitionArn"]
from prowler.providers.aws.services.batch.batch_service import Batch
mocked_aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1])
with (
patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=mocked_aws_provider,
),
patch(
"prowler.providers.aws.services.batch.batch_job_definition_no_secrets.batch_job_definition_no_secrets.batch_client",
new=Batch(mocked_aws_provider),
),
):
from prowler.providers.aws.services.batch.batch_job_definition_no_secrets.batch_job_definition_no_secrets import (
batch_job_definition_no_secrets,
)
check = batch_job_definition_no_secrets()
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert (
f"Potential secrets found in Batch job definition {JOB_NAME} with revision {JOB_REVISION}:"
in result[0].status_extended
)
assert "Secrets in command" in result[0].status_extended
assert result[0].resource_id == f"{JOB_NAME}:{JOB_REVISION}"
assert result[0].resource_arn == job_arn
assert result[0].region == AWS_REGION_US_EAST_1
@mock_aws
def test_job_definition_command_no_secrets(self):
batch_client = client("batch", region_name=AWS_REGION_US_EAST_1)
response = batch_client.register_job_definition(
jobDefinitionName=JOB_NAME,
type="container",
containerProperties={
"image": "test-image:latest",
"memory": 128,
"vcpus": 1,
"command": ["python", "app.py"],
},
)
job_arn = response["jobDefinitionArn"]
from prowler.providers.aws.services.batch.batch_service import Batch
mocked_aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1])
with (
patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=mocked_aws_provider,
),
patch(
"prowler.providers.aws.services.batch.batch_job_definition_no_secrets.batch_job_definition_no_secrets.batch_client",
new=Batch(mocked_aws_provider),
),
):
from prowler.providers.aws.services.batch.batch_job_definition_no_secrets.batch_job_definition_no_secrets import (
batch_job_definition_no_secrets,
)
check = batch_job_definition_no_secrets()
result = check.execute()
assert len(result) == 1
assert result[0].status == "PASS"
assert (
result[0].status_extended
== f"No secrets found in Batch job definition {JOB_NAME} with revision {JOB_REVISION}."
)
assert result[0].resource_id == f"{JOB_NAME}:{JOB_REVISION}"
assert result[0].resource_arn == job_arn
assert result[0].region == AWS_REGION_US_EAST_1
@mock_aws
def test_job_definition_multiple_env_vars_with_secrets(self):
batch_client = client("batch", region_name=AWS_REGION_US_EAST_1)
response = batch_client.register_job_definition(
jobDefinitionName=JOB_NAME,
type="container",
containerProperties={
"image": "test-image:latest",
"memory": 128,
"vcpus": 1,
"environment": [
{
"name": ENV_VAR_NAME_WITH_KEYWORD,
"value": ENV_VAR_VALUE_WITH_SECRETS,
},
{
"name": ENV_VAR_NAME_NO_SECRETS,
"value": ENV_VAR_VALUE_WITH_SECRETS2,
},
],
},
)
job_arn = response["jobDefinitionArn"]
from prowler.providers.aws.services.batch.batch_service import Batch
mocked_aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1])
with (
patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=mocked_aws_provider,
),
patch(
"prowler.providers.aws.services.batch.batch_job_definition_no_secrets.batch_job_definition_no_secrets.batch_client",
new=Batch(mocked_aws_provider),
),
):
from prowler.providers.aws.services.batch.batch_job_definition_no_secrets.batch_job_definition_no_secrets import (
batch_job_definition_no_secrets,
)
check = batch_job_definition_no_secrets()
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert (
f"Potential secrets found in Batch job definition {JOB_NAME} with revision {JOB_REVISION}:"
in result[0].status_extended
)
assert (
"JSON Web Token (base64url-encoded) on the environment variable DB_PASSWORD"
in result[0].status_extended
)
assert (
"Generic Password on the environment variable DB_PASSWORD"
in result[0].status_extended
)
assert (
"JSON Web Token (base64url-encoded) on the environment variable host"
in result[0].status_extended
)
assert result[0].resource_id == f"{JOB_NAME}:{JOB_REVISION}"
assert result[0].resource_arn == job_arn
assert result[0].region == AWS_REGION_US_EAST_1
@mock_aws
def test_job_definition_all_env_vars_with_keyword_and_secret(self):
batch_client = client("batch", region_name=AWS_REGION_US_EAST_1)
response = batch_client.register_job_definition(
jobDefinitionName=JOB_NAME,
type="container",
containerProperties={
"image": "test-image:latest",
"memory": 128,
"vcpus": 1,
"environment": [
{
"name": ENV_VAR_NAME_WITH_KEYWORD,
"value": ENV_VAR_VALUE_WITH_SECRETS,
},
{
"name": ENV_VAR_NAME_WITH_KEYWORD2,
"value": ENV_VAR_VALUE_GENERIC_SECRET,
},
],
},
)
job_arn = response["jobDefinitionArn"]
from prowler.providers.aws.services.batch.batch_service import Batch
mocked_aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1])
with (
patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=mocked_aws_provider,
),
patch(
"prowler.providers.aws.services.batch.batch_job_definition_no_secrets.batch_job_definition_no_secrets.batch_client",
new=Batch(mocked_aws_provider),
),
):
from prowler.providers.aws.services.batch.batch_job_definition_no_secrets.batch_job_definition_no_secrets import (
batch_job_definition_no_secrets,
)
check = batch_job_definition_no_secrets()
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert (
f"Potential secrets found in Batch job definition {JOB_NAME} with revision {JOB_REVISION}:"
in result[0].status_extended
)
assert (
"JSON Web Token (base64url-encoded) on the environment variable DB_PASSWORD"
in result[0].status_extended
)
assert (
"Generic Password on the environment variable DB_PASSWORD"
in result[0].status_extended
)
assert (
"Generic Password on the environment variable DATABASE_PASSWORD"
in result[0].status_extended
)
assert result[0].resource_id == f"{JOB_NAME}:{JOB_REVISION}"
assert result[0].resource_arn == job_arn
assert result[0].region == AWS_REGION_US_EAST_1
def test_scan_failure_reports_manual(self):
from prowler.lib.utils.utils import SecretsScanError
batch_client = mock.MagicMock()
job_definition_arn = f"arn:aws:batch:{AWS_REGION_US_EAST_1}:123456789012:job-definition/{JOB_NAME}:1"
batch_client.job_definitions = {
job_definition_arn: BatchJobDefinition(
name=JOB_NAME,
arn=job_definition_arn,
revision=JOB_REVISION,
region=AWS_REGION_US_EAST_1,
container_properties=BatchContainerProperties(
image="test-image:latest",
command=[],
environment=[
ContainerEnvVariable(name="DB_PASSWORD", value="pass-12343")
],
),
)
}
batch_client.audit_config = {
"secrets_ignore_patterns": [],
"secrets_validate": False,
}
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_aws_provider([AWS_REGION_US_EAST_1]),
),
mock.patch(
"prowler.providers.aws.services.batch.batch_job_definition_no_secrets.batch_job_definition_no_secrets.batch_client",
batch_client,
),
mock.patch(
"prowler.providers.aws.services.batch.batch_job_definition_no_secrets.batch_job_definition_no_secrets.detect_secrets_scan_batch",
side_effect=SecretsScanError("Kingfisher exited with code 1"),
),
):
from prowler.providers.aws.services.batch.batch_job_definition_no_secrets.batch_job_definition_no_secrets import (
batch_job_definition_no_secrets,
)
check = batch_job_definition_no_secrets()
result = check.execute()
assert len(result) == 1
assert result[0].status == "MANUAL"
assert "Could not scan" in result[0].status_extended
@@ -0,0 +1,223 @@
from unittest.mock import patch
import botocore
from prowler.providers.aws.services.batch.batch_service import Batch
from tests.providers.aws.utils import (
AWS_REGION_EU_WEST_1,
AWS_REGION_US_EAST_1,
set_mocked_aws_provider,
)
make_api_call = botocore.client.BaseClient._make_api_call
def mock_make_api_call(self, operation_name, kwarg):
if operation_name == "DescribeJobDefinitions":
return {
"jobDefinitions": [
{
"jobDefinitionName": "test-batch-job",
"jobDefinitionArn": "arn:aws:batch:eu-west-1:123456789012:job-definition/test-batch-job:1",
"revision": 1,
"containerProperties": {
"image": "test-image:latest",
"command": ["python", "app.py"],
"environment": [
{"name": "DB_PASSWORD", "value": "pass-12343"},
{"name": "APP_NAME", "value": "myapp"},
],
},
}
]
}
return make_api_call(self, operation_name, kwarg)
def mock_generate_regional_clients(provider, service):
regional_client = provider._session.current_session.client(
service, region_name=AWS_REGION_EU_WEST_1
)
regional_client.region = AWS_REGION_EU_WEST_1
return {AWS_REGION_EU_WEST_1: regional_client}
def mock_generate_multi_region_clients(provider, service):
eu_west_1_client = provider._session.current_session.client(
service, region_name=AWS_REGION_EU_WEST_1
)
eu_west_1_client.region = AWS_REGION_EU_WEST_1
us_east_1_client = provider._session.current_session.client(
service, region_name=AWS_REGION_US_EAST_1
)
us_east_1_client.region = AWS_REGION_US_EAST_1
return {
AWS_REGION_EU_WEST_1: eu_west_1_client,
AWS_REGION_US_EAST_1: us_east_1_client,
}
@patch(
"prowler.providers.aws.aws_provider.AwsProvider.generate_regional_clients",
new=mock_generate_regional_clients,
)
class Test_Batch_Service:
def test_service(self):
aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1])
batch = Batch(aws_provider)
assert batch.service == "batch"
def test_client(self):
aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1])
batch = Batch(aws_provider)
for reg_client in batch.regional_clients.values():
assert reg_client.__class__.__name__ == "Batch"
def test__get_session__(self):
aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1])
batch = Batch(aws_provider)
assert batch.session.__class__.__name__ == "Session"
@patch("botocore.client.BaseClient._make_api_call", new=mock_make_api_call)
def test_list_job_definitions(self):
aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1])
batch = Batch(aws_provider)
assert len(batch.job_definitions) == 1
jd_arn = "arn:aws:batch:eu-west-1:123456789012:job-definition/test-batch-job:1"
jd = batch.job_definitions[jd_arn]
assert jd.name == "test-batch-job"
assert jd.arn == jd_arn
assert jd.revision == 1
assert jd.region == AWS_REGION_EU_WEST_1
@patch("botocore.client.BaseClient._make_api_call", new=mock_make_api_call)
def test_describe_job_definitions(self):
aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1])
batch = Batch(aws_provider)
assert len(batch.job_definitions) == 1
jd = list(batch.job_definitions.values())[0]
assert jd.name == "test-batch-job"
assert jd.container_properties.image == "test-image:latest"
assert jd.container_properties.command == ["python", "app.py"]
assert len(jd.container_properties.environment) == 2
assert jd.container_properties.environment[0].name == "DB_PASSWORD"
assert jd.container_properties.environment[0].value == "pass-12343"
assert jd.container_properties.environment[1].name == "APP_NAME"
assert jd.container_properties.environment[1].value == "myapp"
@patch("botocore.client.BaseClient._make_api_call", new=mock_make_api_call)
def test_no_job_definitions(self):
def mock_make_api_call_empty(self, operation_name, kwarg):
if operation_name == "DescribeJobDefinitions":
return {"jobDefinitions": []}
return make_api_call(self, operation_name, kwarg)
with patch(
"botocore.client.BaseClient._make_api_call",
new=mock_make_api_call_empty,
):
aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1])
batch = Batch(aws_provider)
assert len(batch.job_definitions) == 0
def test_job_definitions_are_loaded_for_analysis(self):
describe_calls = []
def counting_make_api_call(self, operation_name, kwarg):
if operation_name == "DescribeJobDefinitions":
describe_calls.append(kwarg)
return {
"jobDefinitions": [
{
"jobDefinitionName": f"job-{i}",
"jobDefinitionArn": f"arn:aws:batch:eu-west-1:123456789012:job-definition/job-{i}:{i}",
"revision": i,
"containerProperties": {
"image": "test-image:latest",
"environment": [],
},
}
for i in (3, 2, 1)
]
}
return make_api_call(self, operation_name, kwarg)
with patch(
"botocore.client.BaseClient._make_api_call", new=counting_make_api_call
):
aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1])
batch = Batch(aws_provider)
assert [jd.revision for jd in batch.job_definitions.values()] == [3, 2, 1]
assert len(describe_calls) == 1
assert describe_calls[0].get("status") == "ACTIVE"
def test_job_definition_limit_exposes_only_selected_resources(self):
describe_calls = []
def counting_make_api_call(self, operation_name, kwarg):
if operation_name == "DescribeJobDefinitions":
describe_calls.append(kwarg)
return {
"jobDefinitions": [
{
"jobDefinitionName": f"job-{i}",
"jobDefinitionArn": f"arn:aws:batch:eu-west-1:123456789012:job-definition/job-{i}:{i}",
"revision": i,
"containerProperties": {
"image": "test-image:latest",
"environment": [],
},
}
for i in (3, 2, 1)
]
}
return make_api_call(self, operation_name, kwarg)
with patch(
"botocore.client.BaseClient._make_api_call", new=counting_make_api_call
):
aws_provider = set_mocked_aws_provider(
[AWS_REGION_EU_WEST_1],
audit_config={"max_batch_job_definitions": 2},
)
batch = Batch(aws_provider)
assert [jd.revision for jd in batch.job_definitions.values()] == [3, 2]
assert len(describe_calls) == 1
def test_audit_resources_filters_job_definitions(self):
def counting_make_api_call(self, operation_name, kwarg):
if operation_name == "DescribeJobDefinitions":
return {
"jobDefinitions": [
{
"jobDefinitionName": f"job-{i}",
"jobDefinitionArn": f"arn:aws:batch:eu-west-1:123456789012:job-definition/job-{i}:{i}",
"revision": i,
"containerProperties": {
"image": "test-image:latest",
"environment": [],
},
}
for i in (1, 2)
]
}
return make_api_call(self, operation_name, kwarg)
with patch(
"botocore.client.BaseClient._make_api_call", new=counting_make_api_call
):
aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1])
aws_provider._audit_resources = [
"arn:aws:batch:eu-west-1:123456789012:job-definition/job-2:2"
]
batch = Batch(aws_provider)
assert list(batch.job_definitions.keys()) == [
"arn:aws:batch:eu-west-1:123456789012:job-definition/job-2:2"
]