mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-08-19 09:30:21 +00:00
feat(batch): add batch_job_definition_no_secrets check (#12117)
Co-authored-by: Daniel Barranquero <danielbo2001@gmail.com>
This commit is contained in:
co-authored by
Daniel Barranquero
parent
6e71dee85d
commit
94594d6766
@@ -0,0 +1 @@
|
||||
`batch_job_definition_no_secrets` check for AWS provider, scanning Batch job definition environment variables and command parameters for hardcoded secrets
|
||||
@@ -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.
|
||||
|
||||
@@ -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())
|
||||
+41
@@ -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."
|
||||
}
|
||||
+120
@@ -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
|
||||
)
|
||||
}
|
||||
+535
@@ -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"
|
||||
]
|
||||
Reference in New Issue
Block a user