59 / 75 · 08 Infrastructure as Code Testing · IAM Policy and Network Rule Verification← prev⊞ allnext →☰ Read as one page
11.2Programmatic IAM Policy Testing
Python Test Suite for IAM Policies
# tests/test_iam_policies.py
import json
import pytest
import os
import glob
def load_policy(path):
"""Load an IAM policy document from a JSON file."""
with open(path) as f:
return json.load(f)
def get_all_policy_files():
"""Find all IAM policy JSON files in the Terraform directory."""
return glob.glob("terraform/policies/*.json")
class TestLambdaExecutionRole:
"""Tests for the Lambda execution role policy."""
policy = load_policy("terraform/policies/lambda-execution-role.json")
def test_no_wildcard_resources(self):
"""IAM policies must never use Resource: '*' with mutating actions."""
for statement in self.policy["Statement"]:
if statement["Effect"] == "Allow":
actions = statement.get("Action", [])
if isinstance(actions, str):
actions = [actions]
mutating = [a for a in actions if not a.endswith(":Get*")
and not a.endswith(":List*")
and not a.endswith(":Describe*")]
if mutating:
resources = statement.get("Resource", [])
if isinstance(resources, str):
resources = [resources]
assert "*" not in resources, \
f"Wildcard resource with mutating actions: {mutating}"
def test_no_admin_access(self):
"""No policy should grant full admin access."""
for statement in self.policy["Statement"]:
if statement["Effect"] == "Allow":
actions = statement.get("Action", [])
if isinstance(actions, str):
actions = [actions]
assert "*" not in actions, "Policy grants full admin access"
assert "iam:*" not in actions, "Policy grants full IAM access"
def test_has_condition_keys(self):
"""Sensitive actions should have condition constraints."""
for statement in self.policy["Statement"]:
actions = statement.get("Action", [])
if isinstance(actions, str):
actions = [actions]
sensitive = [a for a in actions if "s3:Delete" in a or "dynamodb:Delete" in a]
if sensitive:
assert "Condition" in statement, \
f"Sensitive actions {sensitive} lack Condition constraints"
def test_no_pass_role_without_conditions(self):
"""iam:PassRole must have a condition limiting which roles can be passed."""
for statement in self.policy["Statement"]:
if statement["Effect"] == "Allow":
actions = statement.get("Action", [])
if isinstance(actions, str):
actions = [actions]
if "iam:PassRole" in actions:
assert "Condition" in statement, \
"iam:PassRole must have conditions (e.g., iam:PassedToService)"
Generic Policy Scanner
# tests/test_all_iam_policies.py
import json
import glob
import pytest
POLICY_FILES = glob.glob("terraform/policies/*.json")
@pytest.mark.parametrize("policy_path", POLICY_FILES)
def test_no_wildcard_actions(policy_path):
"""No policy should grant Action: '*'."""
with open(policy_path) as f:
policy = json.load(f)
for statement in policy.get("Statement", []):
if statement.get("Effect") == "Allow":
actions = statement.get("Action", [])
if isinstance(actions, str):
actions = [actions]
assert "*" not in actions, \
f"{policy_path}: Statement grants wildcard action"
@pytest.mark.parametrize("policy_path", POLICY_FILES)
def test_no_wildcard_resource_with_write(policy_path):
"""No write action should use Resource: '*'."""
with open(policy_path) as f:
policy = json.load(f)
readonly_suffixes = [":Get*", ":List*", ":Describe*", ":Head*"]
for statement in policy.get("Statement", []):
if statement.get("Effect") == "Allow":
actions = statement.get("Action", [])
if isinstance(actions, str):
actions = [actions]
has_write = any(
not any(a.endswith(suffix) for suffix in readonly_suffixes)
for a in actions
)
if has_write:
resources = statement.get("Resource", [])
if isinstance(resources, str):
resources = [resources]
assert "*" not in resources, \
f"{policy_path}: Wildcard resource with write actions"
@pytest.mark.parametrize("policy_path", POLICY_FILES)
def test_deny_statements_exist(policy_path):
"""Policies should include explicit Deny statements for dangerous actions."""
with open(policy_path) as f:
policy = json.load(f)
dangerous_actions = ["iam:CreateUser", "iam:CreateAccessKey",
"organizations:LeaveOrganization", "ec2:RunInstances"]
for statement in policy.get("Statement", []):
if statement.get("Effect") == "Allow":
actions = statement.get("Action", [])
if isinstance(actions, str):
actions = [actions]
for dangerous in dangerous_actions:
assert dangerous not in actions, \
f"{policy_path}: Allows dangerous action {dangerous}"