Modern QA2026Programmatic IAM Policy Testing — tiles
Log inJoin
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}"