#!/usr/bin/env python3 """AWS IAM permission boundary management agent using boto3.""" import argparse import json import logging import os import sys from datetime import datetime from typing import List try: import boto3 from botocore.exceptions import ClientError except ImportError: sys.exit("boto3 required: pip install boto3") logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s") logger = logging.getLogger(__name__) def get_iam_client(profile: str = "", region: str = "us-east-1"): """Create IAM client with optional profile.""" session = boto3.Session(profile_name=profile) if profile else boto3.Session() return session.client("iam", region_name=region) def create_permission_boundary(client, policy_name: str, allowed_services: List[str], allowed_regions: List[str] = None) -> dict: """Create a permission boundary policy restricting services and regions.""" statements = [{ "Sid": "AllowedServices", "Effect": "Allow", "Action": [f"{svc}:*" for svc in allowed_services], "Resource": "*", }] if allowed_regions: statements[0]["Condition"] = { "StringEquals": {"aws:RequestedRegion": allowed_regions} } statements.append({ "Sid": "DenyBoundaryChanges", "Effect": "Deny", "Action": ["iam:DeleteRolePermissionsBoundary", "iam:PutRolePermissionsBoundary"], "Resource": "*", }) policy_doc = {"Version": "2012-10-17", "Statement": statements} try: resp = client.create_policy( PolicyName=policy_name, PolicyDocument=json.dumps(policy_doc), Description=f"Permission boundary: {', '.join(allowed_services)}", ) arn = resp["Policy"]["Arn"] logger.info("Created boundary policy: %s", arn) return {"policy_arn": arn, "policy_document": policy_doc} except ClientError as exc: return {"error": str(exc)} def attach_boundary_to_role(client, role_name: str, boundary_arn: str) -> dict: """Attach permission boundary to an IAM role.""" try: client.put_role_permissions_boundary( RoleName=role_name, PermissionsBoundary=boundary_arn) logger.info("Attached boundary %s to role %s", boundary_arn, role_name) return {"role": role_name, "boundary_arn": boundary_arn, "attached": True} except ClientError as exc: return {"role": role_name, "error": str(exc)} def audit_roles_without_boundary(client) -> List[dict]: """Find IAM roles that lack a permission boundary.""" paginator = client.get_paginator("list_roles") unbounded = [] for page in paginator.paginate(): for role in page["Roles"]: if "PermissionsBoundary" not in role: unbounded.append({ "role_name": role["RoleName"], "arn": role["Arn"], "created": role["CreateDate"].isoformat(), }) logger.info("Found %d roles without permission boundary", len(unbounded)) return unbounded def audit_boundary_effectiveness(client, role_name: str) -> dict: """Audit effective permissions for a role with boundary.""" try: role = client.get_role(RoleName=role_name)["Role"] boundary = role.get("PermissionsBoundary", {}) policies_resp = client.list_attached_role_policies(RoleName=role_name) inline_resp = client.list_role_policies(RoleName=role_name) return { "role": role_name, "boundary_arn": boundary.get("PermissionsBoundaryArn", "NONE"), "attached_policies": [p["PolicyName"] for p in policies_resp["AttachedPolicies"]], "inline_policies": inline_resp["PolicyNames"], } except ClientError as exc: return {"role": role_name, "error": str(exc)} def generate_report(client) -> dict: """Generate permission boundary compliance report.""" unbounded = audit_roles_without_boundary(client) report = { "analysis_date": datetime.utcnow().isoformat(), "roles_without_boundary": unbounded, "roles_without_boundary_count": len(unbounded), "recommendations": [], } if unbounded: report["recommendations"].append( f"Attach permission boundaries to {len(unbounded)} roles lacking boundaries") return report def main(): parser = argparse.ArgumentParser(description="AWS IAM Permission Boundary Agent") parser.add_argument("--profile", default="", help="AWS CLI profile name") parser.add_argument("--region", default="us-east-1") parser.add_argument("--audit", action="store_true", help="Audit roles without boundaries") parser.add_argument("--output-dir", default=".") parser.add_argument("--output", default="iam_boundary_report.json") args = parser.parse_args() os.makedirs(args.output_dir, exist_ok=True) client = get_iam_client(args.profile, args.region) report = generate_report(client) out_path = os.path.join(args.output_dir, args.output) with open(out_path, "w") as f: json.dump(report, f, indent=2, default=str) logger.info("Report saved to %s", out_path) print(json.dumps(report, indent=2, default=str)) if __name__ == "__main__": main()