From b898f257f1b25a482523b733eeb2ae6370d7d242 Mon Sep 17 00:00:00 2001 From: Avula Jeevan Yadav Date: Thu, 9 Apr 2026 19:26:29 +0530 Subject: [PATCH] feat(stepfunctions): add check for secrets in state machine definition (#10570) Co-authored-by: Andoni A. <14891798+andoniaf@users.noreply.github.com> --- prowler/CHANGELOG.md | 1 + .../__init__.py | 0 ...ine_no_secrets_in_definition.metadata.json | 44 +++++ ...s_statemachine_no_secrets_in_definition.py | 45 +++++ .../__init__.py | 0 ...temachine_no_secrets_in_definition_test.py | 180 ++++++++++++++++++ 6 files changed, 270 insertions(+) create mode 100644 prowler/providers/aws/services/stepfunctions/stepfunctions_statemachine_no_secrets_in_definition/__init__.py create mode 100644 prowler/providers/aws/services/stepfunctions/stepfunctions_statemachine_no_secrets_in_definition/stepfunctions_statemachine_no_secrets_in_definition.metadata.json create mode 100644 prowler/providers/aws/services/stepfunctions/stepfunctions_statemachine_no_secrets_in_definition/stepfunctions_statemachine_no_secrets_in_definition.py create mode 100644 tests/providers/aws/services/stepfunctions/stepfunctions_statemachine_no_secrets_in_definition/__init__.py create mode 100644 tests/providers/aws/services/stepfunctions/stepfunctions_statemachine_no_secrets_in_definition/stepfunctions_statemachine_no_secrets_in_definition_test.py diff --git a/prowler/CHANGELOG.md b/prowler/CHANGELOG.md index 4835fe7f75..eb7944ff09 100644 --- a/prowler/CHANGELOG.md +++ b/prowler/CHANGELOG.md @@ -21,6 +21,7 @@ All notable changes to the **Prowler SDK** are documented in this file. - `entra_conditional_access_policy_device_registration_mfa_required` check and `entra_intune_enrollment_sign_in_frequency_every_time` enhancement for M365 provider [(#10222)](https://github.com/prowler-cloud/prowler/pull/10222) - `entra_conditional_access_policy_block_elevated_insider_risk` check for M365 provider [(#10234)](https://github.com/prowler-cloud/prowler/pull/10234) - `Vercel` provider support with 30 checks [(#10189)](https://github.com/prowler-cloud/prowler/pull/10189) +- `stepfunctions_statemachine_no_secrets_in_definition` check for hardcoded secrets in AWS Step Functions state machine definitions [(#10570)](https://github.com/prowler-cloud/prowler/pull/10570) - CCC improvements with the latest checks and new mappings [(#10625)](https://github.com/prowler-cloud/prowler/pull/10625) ### 🔄 Changed diff --git a/prowler/providers/aws/services/stepfunctions/stepfunctions_statemachine_no_secrets_in_definition/__init__.py b/prowler/providers/aws/services/stepfunctions/stepfunctions_statemachine_no_secrets_in_definition/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/aws/services/stepfunctions/stepfunctions_statemachine_no_secrets_in_definition/stepfunctions_statemachine_no_secrets_in_definition.metadata.json b/prowler/providers/aws/services/stepfunctions/stepfunctions_statemachine_no_secrets_in_definition/stepfunctions_statemachine_no_secrets_in_definition.metadata.json new file mode 100644 index 0000000000..746b53d8fd --- /dev/null +++ b/prowler/providers/aws/services/stepfunctions/stepfunctions_statemachine_no_secrets_in_definition/stepfunctions_statemachine_no_secrets_in_definition.metadata.json @@ -0,0 +1,44 @@ +{ + "Provider": "aws", + "CheckID": "stepfunctions_statemachine_no_secrets_in_definition", + "CheckTitle": "Step Functions state machine has no sensitive credentials in its definition", + "CheckType": [ + "Software and Configuration Checks/AWS Security Best Practices", + "TTPs/Credential Access", + "Effects/Data Exposure", + "Sensitive Data Identifications/Security" + ], + "ServiceName": "stepfunctions", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "critical", + "ResourceType": "AwsStepFunctionStateMachine", + "ResourceGroup": "serverless", + "Description": "**AWS Step Functions state machines** are inspected for **hardcoded secrets** (keys, tokens, passwords) embedded directly in the state machine **definition** (Amazon States Language JSON).\n\nSuch values indicate sensitive data is stored directly in task parameters instead of being sourced securely.", + "Risk": "Plaintext secrets in state machine definitions reduce confidentiality: values can be viewed in the AWS Console, CLI, and may leak into execution logs or public outputs. Compromised credentials enable unauthorized AWS actions, lateral movement, and data exfiltration.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://docs.aws.amazon.com/step-functions/latest/dg/concepts-amazon-states-language.html", + "https://docs.aws.amazon.com/step-functions/latest/dg/security-best-practices.html", + "https://docs.aws.amazon.com/secretsmanager/latest/userguide/integrating_how-services-use-secrets_step-functions.html", + "https://docs.aws.amazon.com/systems-manager/latest/userguide/integration-ps-secretsmanager.html" + ], + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "```yaml\nResources:\n :\n Type: AWS::StepFunctions::StateMachine\n Properties:\n StateMachineName: \n RoleArn: \n DefinitionString: |\n {\n \"Comment\": \"Example state machine\",\n \"StartAt\": \"MyTask\",\n \"States\": {\n \"MyTask\": {\n \"Type\": \"Task\",\n \"Resource\": \"arn:aws:states:::aws-sdk:secretsmanager:getSecretValue\",\n \"Parameters\": {\n \"SecretId\": \"\"\n },\n \"End\": true\n }\n }\n }\n```", + "Other": "1. In AWS Console, go to Step Functions and open your state machine\n2. Click Edit\n3. Remove any hardcoded secrets from the definition\n4. Use AWS Secrets Manager or Parameter Store to retrieve secrets at runtime\n5. Grant the state machine IAM role permission to access the secret\n6. Save the updated definition", + "Terraform": "```hcl\nresource \"aws_sfn_state_machine\" \"\" {\n name = \"\"\n role_arn = \"\"\n\n definition = jsonencode({\n Comment = \"Example state machine\"\n StartAt = \"MyTask\"\n States = {\n MyTask = {\n Type = \"Task\"\n Resource = \"arn:aws:states:::aws-sdk:secretsmanager:getSecretValue\"\n Parameters = {\n SecretId = \"\" # Reference secret by name, never hardcode value\n }\n End = true\n }\n }\n })\n}\n```" + }, + "Recommendation": { + "Text": "Store secrets outside the state machine definition and retrieve them securely at runtime using **AWS Secrets Manager** or **AWS Systems Manager Parameter Store**.\n- Use the `aws-sdk:secretsmanager:getSecretValue` integration to fetch secrets dynamically\n- Enforce **least privilege** on the state machine IAM role\n- Rotate secrets regularly and never embed them in the definition", + "Url": "https://hub.prowler.com/check/stepfunctions_statemachine_no_secrets_in_definition" + } + }, + "Categories": [ + "secrets" + ], + "DependsOn": [], + "RelatedTo": [], + "Notes": "" +} diff --git a/prowler/providers/aws/services/stepfunctions/stepfunctions_statemachine_no_secrets_in_definition/stepfunctions_statemachine_no_secrets_in_definition.py b/prowler/providers/aws/services/stepfunctions/stepfunctions_statemachine_no_secrets_in_definition/stepfunctions_statemachine_no_secrets_in_definition.py new file mode 100644 index 0000000000..db04710029 --- /dev/null +++ b/prowler/providers/aws/services/stepfunctions/stepfunctions_statemachine_no_secrets_in_definition/stepfunctions_statemachine_no_secrets_in_definition.py @@ -0,0 +1,45 @@ +from prowler.lib.check.models import Check, Check_Report_AWS +from prowler.lib.utils.utils import detect_secrets_scan +from prowler.providers.aws.services.stepfunctions.stepfunctions_client import ( + stepfunctions_client, +) + + +class stepfunctions_statemachine_no_secrets_in_definition(Check): + """Check that AWS Step Functions state machine definitions contain no hardcoded secrets.""" + + def execute(self) -> list[Check_Report_AWS]: + findings = [] + secrets_ignore_patterns = stepfunctions_client.audit_config.get( + "secrets_ignore_patterns", [] + ) + for state_machine in stepfunctions_client.state_machines.values(): + report = Check_Report_AWS(metadata=self.metadata(), resource=state_machine) + report.status = "PASS" + report.status_extended = f"No secrets found in Step Functions state machine {state_machine.name} definition." + + if state_machine.definition: + detect_secrets_output = detect_secrets_scan( + data=state_machine.definition, + excluded_secrets=secrets_ignore_patterns, + detect_secrets_plugins=stepfunctions_client.audit_config.get( + "detect_secrets_plugins", + ), + ) + + if detect_secrets_output: + secrets_string = ", ".join( + [ + f"{secret['type']} on line {secret['line_number']}" + for secret in detect_secrets_output + ] + ) + report.status = "FAIL" + report.status_extended = ( + f"Potential {'secrets' if len(detect_secrets_output) > 1 else 'secret'} " + f"found in Step Functions state machine {state_machine.name} definition " + f"-> {secrets_string}." + ) + + findings.append(report) + return findings diff --git a/tests/providers/aws/services/stepfunctions/stepfunctions_statemachine_no_secrets_in_definition/__init__.py b/tests/providers/aws/services/stepfunctions/stepfunctions_statemachine_no_secrets_in_definition/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/providers/aws/services/stepfunctions/stepfunctions_statemachine_no_secrets_in_definition/stepfunctions_statemachine_no_secrets_in_definition_test.py b/tests/providers/aws/services/stepfunctions/stepfunctions_statemachine_no_secrets_in_definition/stepfunctions_statemachine_no_secrets_in_definition_test.py new file mode 100644 index 0000000000..628525e542 --- /dev/null +++ b/tests/providers/aws/services/stepfunctions/stepfunctions_statemachine_no_secrets_in_definition/stepfunctions_statemachine_no_secrets_in_definition_test.py @@ -0,0 +1,180 @@ +from datetime import datetime +from unittest import mock + +from tests.providers.aws.utils import AWS_ACCOUNT_NUMBER, AWS_REGION_US_EAST_1 + + +class Test_stepfunctions_statemachine_no_secrets_in_definition: + def test_no_statemachines(self): + stepfunctions_client = mock.MagicMock() + stepfunctions_client.state_machines = {} + stepfunctions_client.audit_config = {"secrets_ignore_patterns": []} + + with ( + mock.patch( + "prowler.providers.aws.services.stepfunctions.stepfunctions_service.StepFunctions", + stepfunctions_client, + ), + mock.patch( + "prowler.providers.aws.services.stepfunctions.stepfunctions_statemachine_no_secrets_in_definition.stepfunctions_statemachine_no_secrets_in_definition.stepfunctions_client", + stepfunctions_client, + ), + ): + from prowler.providers.aws.services.stepfunctions.stepfunctions_statemachine_no_secrets_in_definition.stepfunctions_statemachine_no_secrets_in_definition import ( + stepfunctions_statemachine_no_secrets_in_definition, + ) + + check = stepfunctions_statemachine_no_secrets_in_definition() + result = check.execute() + + assert len(result) == 0 + + def test_statemachine_with_no_definition(self): + stepfunctions_client = mock.MagicMock() + + from prowler.providers.aws.services.stepfunctions.stepfunctions_service import ( + StateMachine, + StateMachineStatus, + StateMachineType, + ) + + statemachine_arn = f"arn:aws:states:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:stateMachine:TestStateMachine" + stepfunctions_client.state_machines = { + statemachine_arn: StateMachine( + id="TestStateMachine", + arn=statemachine_arn, + name="TestStateMachine", + status=StateMachineStatus.ACTIVE, + definition=None, + region=AWS_REGION_US_EAST_1, + type=StateMachineType.STANDARD, + creation_date=datetime.now(), + ) + } + stepfunctions_client.audit_config = {"secrets_ignore_patterns": []} + + with ( + mock.patch( + "prowler.providers.aws.services.stepfunctions.stepfunctions_service.StepFunctions", + stepfunctions_client, + ), + mock.patch( + "prowler.providers.aws.services.stepfunctions.stepfunctions_statemachine_no_secrets_in_definition.stepfunctions_statemachine_no_secrets_in_definition.stepfunctions_client", + stepfunctions_client, + ), + ): + from prowler.providers.aws.services.stepfunctions.stepfunctions_statemachine_no_secrets_in_definition.stepfunctions_statemachine_no_secrets_in_definition import ( + stepfunctions_statemachine_no_secrets_in_definition, + ) + + check = stepfunctions_statemachine_no_secrets_in_definition() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == "No secrets found in Step Functions state machine TestStateMachine definition." + ) + assert result[0].region == AWS_REGION_US_EAST_1 + assert result[0].resource_id == "TestStateMachine" + assert result[0].resource_arn == statemachine_arn + + def test_statemachine_with_no_secrets_in_definition(self): + stepfunctions_client = mock.MagicMock() + + from prowler.providers.aws.services.stepfunctions.stepfunctions_service import ( + StateMachine, + StateMachineStatus, + StateMachineType, + ) + + statemachine_arn = f"arn:aws:states:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:stateMachine:TestStateMachine" + stepfunctions_client.state_machines = { + statemachine_arn: StateMachine( + id="TestStateMachine", + arn=statemachine_arn, + name="TestStateMachine", + status=StateMachineStatus.ACTIVE, + definition='{"Comment": "A simple example", "StartAt": "HelloWorld", "States": {"HelloWorld": {"Type": "Pass", "End": true}}}', + region=AWS_REGION_US_EAST_1, + type=StateMachineType.STANDARD, + creation_date=datetime.now(), + ) + } + stepfunctions_client.audit_config = {"secrets_ignore_patterns": []} + + with ( + mock.patch( + "prowler.providers.aws.services.stepfunctions.stepfunctions_service.StepFunctions", + stepfunctions_client, + ), + mock.patch( + "prowler.providers.aws.services.stepfunctions.stepfunctions_statemachine_no_secrets_in_definition.stepfunctions_statemachine_no_secrets_in_definition.stepfunctions_client", + stepfunctions_client, + ), + ): + from prowler.providers.aws.services.stepfunctions.stepfunctions_statemachine_no_secrets_in_definition.stepfunctions_statemachine_no_secrets_in_definition import ( + stepfunctions_statemachine_no_secrets_in_definition, + ) + + check = stepfunctions_statemachine_no_secrets_in_definition() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == "No secrets found in Step Functions state machine TestStateMachine definition." + ) + assert result[0].region == AWS_REGION_US_EAST_1 + assert result[0].resource_id == "TestStateMachine" + assert result[0].resource_arn == statemachine_arn + + def test_statemachine_with_secrets_in_definition(self): + stepfunctions_client = mock.MagicMock() + + from prowler.providers.aws.services.stepfunctions.stepfunctions_service import ( + StateMachine, + StateMachineStatus, + StateMachineType, + ) + + statemachine_arn = f"arn:aws:states:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:stateMachine:TestStateMachine" + stepfunctions_client.state_machines = { + statemachine_arn: StateMachine( + id="TestStateMachine", + arn=statemachine_arn, + name="TestStateMachine", + status=StateMachineStatus.ACTIVE, + definition='{"Comment": "Example with secret", "StartAt": "MyTask", "States": {"MyTask": {"Type": "Task", "Parameters": {"api_key": "AKIAIOSFODNN7EXAMPLE"}, "End": true}}}', + region=AWS_REGION_US_EAST_1, + type=StateMachineType.STANDARD, + creation_date=datetime.now(), + ) + } + stepfunctions_client.audit_config = {"secrets_ignore_patterns": []} + + with ( + mock.patch( + "prowler.providers.aws.services.stepfunctions.stepfunctions_service.StepFunctions", + stepfunctions_client, + ), + mock.patch( + "prowler.providers.aws.services.stepfunctions.stepfunctions_statemachine_no_secrets_in_definition.stepfunctions_statemachine_no_secrets_in_definition.stepfunctions_client", + stepfunctions_client, + ), + ): + from prowler.providers.aws.services.stepfunctions.stepfunctions_statemachine_no_secrets_in_definition.stepfunctions_statemachine_no_secrets_in_definition import ( + stepfunctions_statemachine_no_secrets_in_definition, + ) + + check = stepfunctions_statemachine_no_secrets_in_definition() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert "TestStateMachine" in result[0].status_extended + assert result[0].region == AWS_REGION_US_EAST_1 + assert result[0].resource_id == "TestStateMachine" + assert result[0].resource_arn == statemachine_arn