runbooks 0.7.0__py3-none-any.whl → 0.7.6__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (132) hide show
  1. runbooks/__init__.py +87 -37
  2. runbooks/cfat/README.md +300 -49
  3. runbooks/cfat/__init__.py +2 -2
  4. runbooks/finops/__init__.py +1 -1
  5. runbooks/finops/cli.py +1 -1
  6. runbooks/inventory/collectors/__init__.py +8 -0
  7. runbooks/inventory/collectors/aws_management.py +791 -0
  8. runbooks/inventory/collectors/aws_networking.py +3 -3
  9. runbooks/main.py +3389 -782
  10. runbooks/operate/__init__.py +207 -0
  11. runbooks/operate/base.py +311 -0
  12. runbooks/operate/cloudformation_operations.py +619 -0
  13. runbooks/operate/cloudwatch_operations.py +496 -0
  14. runbooks/operate/dynamodb_operations.py +812 -0
  15. runbooks/operate/ec2_operations.py +926 -0
  16. runbooks/operate/iam_operations.py +569 -0
  17. runbooks/operate/s3_operations.py +1211 -0
  18. runbooks/operate/tagging_operations.py +655 -0
  19. runbooks/remediation/CLAUDE.md +100 -0
  20. runbooks/remediation/DOME9.md +218 -0
  21. runbooks/remediation/README.md +26 -0
  22. runbooks/remediation/Tests/__init__.py +0 -0
  23. runbooks/remediation/Tests/update_policy.py +74 -0
  24. runbooks/remediation/__init__.py +95 -0
  25. runbooks/remediation/acm_cert_expired_unused.py +98 -0
  26. runbooks/remediation/acm_remediation.py +875 -0
  27. runbooks/remediation/api_gateway_list.py +167 -0
  28. runbooks/remediation/base.py +643 -0
  29. runbooks/remediation/cloudtrail_remediation.py +908 -0
  30. runbooks/remediation/cloudtrail_s3_modifications.py +296 -0
  31. runbooks/remediation/cognito_active_users.py +78 -0
  32. runbooks/remediation/cognito_remediation.py +856 -0
  33. runbooks/remediation/cognito_user_password_reset.py +163 -0
  34. runbooks/remediation/commons.py +455 -0
  35. runbooks/remediation/dynamodb_optimize.py +155 -0
  36. runbooks/remediation/dynamodb_remediation.py +744 -0
  37. runbooks/remediation/dynamodb_server_side_encryption.py +108 -0
  38. runbooks/remediation/ec2_public_ips.py +134 -0
  39. runbooks/remediation/ec2_remediation.py +892 -0
  40. runbooks/remediation/ec2_subnet_disable_auto_ip_assignment.py +72 -0
  41. runbooks/remediation/ec2_unattached_ebs_volumes.py +448 -0
  42. runbooks/remediation/ec2_unused_security_groups.py +202 -0
  43. runbooks/remediation/kms_enable_key_rotation.py +651 -0
  44. runbooks/remediation/kms_remediation.py +717 -0
  45. runbooks/remediation/lambda_list.py +243 -0
  46. runbooks/remediation/lambda_remediation.py +971 -0
  47. runbooks/remediation/multi_account.py +569 -0
  48. runbooks/remediation/rds_instance_list.py +199 -0
  49. runbooks/remediation/rds_remediation.py +873 -0
  50. runbooks/remediation/rds_snapshot_list.py +192 -0
  51. runbooks/remediation/requirements.txt +118 -0
  52. runbooks/remediation/s3_block_public_access.py +159 -0
  53. runbooks/remediation/s3_bucket_public_access.py +143 -0
  54. runbooks/remediation/s3_disable_static_website_hosting.py +74 -0
  55. runbooks/remediation/s3_downloader.py +215 -0
  56. runbooks/remediation/s3_enable_access_logging.py +562 -0
  57. runbooks/remediation/s3_encryption.py +526 -0
  58. runbooks/remediation/s3_force_ssl_secure_policy.py +143 -0
  59. runbooks/remediation/s3_list.py +141 -0
  60. runbooks/remediation/s3_object_search.py +201 -0
  61. runbooks/remediation/s3_remediation.py +816 -0
  62. runbooks/remediation/scan_for_phrase.py +425 -0
  63. runbooks/remediation/workspaces_list.py +220 -0
  64. runbooks/security/__init__.py +9 -10
  65. runbooks/security/security_baseline_tester.py +4 -2
  66. runbooks-0.7.6.dist-info/METADATA +608 -0
  67. {runbooks-0.7.0.dist-info → runbooks-0.7.6.dist-info}/RECORD +84 -76
  68. {runbooks-0.7.0.dist-info → runbooks-0.7.6.dist-info}/entry_points.txt +0 -1
  69. {runbooks-0.7.0.dist-info → runbooks-0.7.6.dist-info}/top_level.txt +0 -1
  70. jupyter-agent/.env +0 -2
  71. jupyter-agent/.env.template +0 -2
  72. jupyter-agent/.gitattributes +0 -35
  73. jupyter-agent/.gradio/certificate.pem +0 -31
  74. jupyter-agent/README.md +0 -16
  75. jupyter-agent/__main__.log +0 -8
  76. jupyter-agent/app.py +0 -256
  77. jupyter-agent/cloudops-agent.png +0 -0
  78. jupyter-agent/ds-system-prompt.txt +0 -154
  79. jupyter-agent/jupyter-agent.png +0 -0
  80. jupyter-agent/llama3_template.jinja +0 -123
  81. jupyter-agent/requirements.txt +0 -9
  82. jupyter-agent/tmp/4ojbs8a02ir/jupyter-agent.ipynb +0 -68
  83. jupyter-agent/tmp/cm5iasgpm3p/jupyter-agent.ipynb +0 -91
  84. jupyter-agent/tmp/crqbsseag5/jupyter-agent.ipynb +0 -91
  85. jupyter-agent/tmp/hohanq1u097/jupyter-agent.ipynb +0 -57
  86. jupyter-agent/tmp/jns1sam29wm/jupyter-agent.ipynb +0 -53
  87. jupyter-agent/tmp/jupyter-agent.ipynb +0 -27
  88. jupyter-agent/utils.py +0 -409
  89. runbooks/aws/__init__.py +0 -58
  90. runbooks/aws/dynamodb_operations.py +0 -231
  91. runbooks/aws/ec2_copy_image_cross-region.py +0 -195
  92. runbooks/aws/ec2_describe_instances.py +0 -202
  93. runbooks/aws/ec2_ebs_snapshots_delete.py +0 -186
  94. runbooks/aws/ec2_run_instances.py +0 -213
  95. runbooks/aws/ec2_start_stop_instances.py +0 -212
  96. runbooks/aws/ec2_terminate_instances.py +0 -143
  97. runbooks/aws/ec2_unused_eips.py +0 -196
  98. runbooks/aws/ec2_unused_volumes.py +0 -188
  99. runbooks/aws/s3_create_bucket.py +0 -142
  100. runbooks/aws/s3_list_buckets.py +0 -152
  101. runbooks/aws/s3_list_objects.py +0 -156
  102. runbooks/aws/s3_object_operations.py +0 -183
  103. runbooks/aws/tagging_lambda_handler.py +0 -183
  104. runbooks/inventory/FAILED_SCRIPTS_TROUBLESHOOTING.md +0 -619
  105. runbooks/inventory/PASSED_SCRIPTS_GUIDE.md +0 -738
  106. runbooks/inventory/aws_organization.png +0 -0
  107. runbooks/inventory/cfn_move_stack_instances.py +0 -1526
  108. runbooks/inventory/delete_s3_buckets_objects.py +0 -169
  109. runbooks/inventory/lockdown_cfn_stackset_role.py +0 -224
  110. runbooks/inventory/update_aws_actions.py +0 -173
  111. runbooks/inventory/update_cfn_stacksets.py +0 -1215
  112. runbooks/inventory/update_cloudwatch_logs_retention_policy.py +0 -294
  113. runbooks/inventory/update_iam_roles_cross_accounts.py +0 -478
  114. runbooks/inventory/update_s3_public_access_block.py +0 -539
  115. runbooks/organizations/__init__.py +0 -12
  116. runbooks/organizations/manager.py +0 -374
  117. runbooks-0.7.0.dist-info/METADATA +0 -375
  118. /runbooks/inventory/{tests → Tests}/common_test_data.py +0 -0
  119. /runbooks/inventory/{tests → Tests}/common_test_functions.py +0 -0
  120. /runbooks/inventory/{tests → Tests}/script_test_data.py +0 -0
  121. /runbooks/inventory/{tests → Tests}/setup.py +0 -0
  122. /runbooks/inventory/{tests → Tests}/src.py +0 -0
  123. /runbooks/inventory/{tests/test_inventory_modules.py → Tests/test_Inventory_Modules.py} +0 -0
  124. /runbooks/inventory/{tests → Tests}/test_cfn_describe_stacks.py +0 -0
  125. /runbooks/inventory/{tests → Tests}/test_ec2_describe_instances.py +0 -0
  126. /runbooks/inventory/{tests → Tests}/test_lambda_list_functions.py +0 -0
  127. /runbooks/inventory/{tests → Tests}/test_moto_integration_example.py +0 -0
  128. /runbooks/inventory/{tests → Tests}/test_org_list_accounts.py +0 -0
  129. /runbooks/inventory/{Inventory_Modules.py → inventory_modules.py} +0 -0
  130. /runbooks/{aws → operate}/tags.json +0 -0
  131. {runbooks-0.7.0.dist-info → runbooks-0.7.6.dist-info}/WHEEL +0 -0
  132. {runbooks-0.7.0.dist-info → runbooks-0.7.6.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,569 @@
1
+ """
2
+ IAM Operations Module.
3
+
4
+ Provides comprehensive IAM resource management capabilities including role management,
5
+ policy operations, and cross-account access management.
6
+
7
+ Migrated and enhanced from:
8
+ - inventory/update_iam_roles_cross_accounts.py
9
+ """
10
+
11
+ import json
12
+ from datetime import datetime
13
+ from typing import Any, Dict, List, Optional, Union
14
+
15
+ import boto3
16
+ from botocore.exceptions import ClientError
17
+ from loguru import logger
18
+
19
+ from runbooks.operate.base import BaseOperation, OperationContext, OperationResult, OperationStatus
20
+
21
+
22
+ class IAMOperations(BaseOperation):
23
+ """
24
+ IAM resource operations and lifecycle management.
25
+
26
+ Handles all IAM-related operational tasks including role management,
27
+ policy operations, and cross-account access configuration.
28
+ """
29
+
30
+ service_name = "iam"
31
+ supported_operations = {
32
+ "create_role",
33
+ "update_role",
34
+ "delete_role",
35
+ "create_policy",
36
+ "update_policy",
37
+ "delete_policy",
38
+ "attach_role_policy",
39
+ "detach_role_policy",
40
+ "update_assume_role_policy",
41
+ "update_roles_cross_accounts",
42
+ "create_service_linked_role",
43
+ "tag_role",
44
+ "untag_role",
45
+ }
46
+ requires_confirmation = True
47
+
48
+ def __init__(self, profile: Optional[str] = None, region: Optional[str] = None, dry_run: bool = False):
49
+ """Initialize IAM operations."""
50
+ super().__init__(profile, region, dry_run)
51
+
52
+ def execute_operation(self, context: OperationContext, operation_type: str, **kwargs) -> List[OperationResult]:
53
+ """
54
+ Execute IAM operation.
55
+
56
+ Args:
57
+ context: Operation context
58
+ operation_type: Type of operation to execute
59
+ **kwargs: Operation-specific arguments
60
+
61
+ Returns:
62
+ List of operation results
63
+ """
64
+ self.validate_context(context)
65
+
66
+ if operation_type == "create_role":
67
+ return self.create_role(context, **kwargs)
68
+ elif operation_type == "update_role":
69
+ return self.update_role(context, **kwargs)
70
+ elif operation_type == "delete_role":
71
+ return self.delete_role(context, kwargs.get("role_name"))
72
+ elif operation_type == "create_policy":
73
+ return self.create_policy(context, **kwargs)
74
+ elif operation_type == "update_policy":
75
+ return self.update_policy(context, **kwargs)
76
+ elif operation_type == "delete_policy":
77
+ return self.delete_policy(context, kwargs.get("policy_arn"))
78
+ elif operation_type == "attach_role_policy":
79
+ return self.attach_role_policy(context, **kwargs)
80
+ elif operation_type == "detach_role_policy":
81
+ return self.detach_role_policy(context, **kwargs)
82
+ elif operation_type == "update_assume_role_policy":
83
+ return self.update_assume_role_policy(context, **kwargs)
84
+ elif operation_type == "update_roles_cross_accounts":
85
+ return self.update_roles_cross_accounts(context, **kwargs)
86
+ elif operation_type == "create_service_linked_role":
87
+ return self.create_service_linked_role(context, **kwargs)
88
+ elif operation_type == "tag_role":
89
+ return self.tag_role(context, **kwargs)
90
+ elif operation_type == "untag_role":
91
+ return self.untag_role(context, **kwargs)
92
+ else:
93
+ raise ValueError(f"Unsupported operation: {operation_type}")
94
+
95
+ def create_role(
96
+ self,
97
+ context: OperationContext,
98
+ role_name: str,
99
+ assume_role_policy_document: str,
100
+ path: str = "/",
101
+ description: Optional[str] = None,
102
+ max_session_duration: int = 3600,
103
+ permissions_boundary: Optional[str] = None,
104
+ tags: Optional[List[Dict[str, str]]] = None,
105
+ ) -> List[OperationResult]:
106
+ """
107
+ Create IAM role.
108
+
109
+ Args:
110
+ context: Operation context
111
+ role_name: Name of role to create
112
+ assume_role_policy_document: Trust policy document
113
+ path: Role path
114
+ description: Role description
115
+ max_session_duration: Maximum session duration
116
+ permissions_boundary: Permissions boundary ARN
117
+ tags: Role tags
118
+
119
+ Returns:
120
+ List of operation results
121
+ """
122
+ iam_client = self.get_client("iam")
123
+
124
+ result = self.create_operation_result(context, "create_role", "iam:role", role_name)
125
+
126
+ try:
127
+ if context.dry_run:
128
+ logger.info(f"[DRY-RUN] Would create IAM role {role_name}")
129
+ result.mark_completed(OperationStatus.DRY_RUN)
130
+ return [result]
131
+
132
+ create_params = {
133
+ "RoleName": role_name,
134
+ "AssumeRolePolicyDocument": assume_role_policy_document,
135
+ "Path": path,
136
+ "MaxSessionDuration": max_session_duration,
137
+ }
138
+
139
+ if description:
140
+ create_params["Description"] = description
141
+ if permissions_boundary:
142
+ create_params["PermissionsBoundary"] = permissions_boundary
143
+ if tags:
144
+ create_params["Tags"] = tags
145
+
146
+ response = self.execute_aws_call(iam_client, "create_role", **create_params)
147
+
148
+ result.response_data = response
149
+ result.mark_completed(OperationStatus.SUCCESS)
150
+ logger.info(f"Successfully created IAM role {role_name}")
151
+
152
+ except ClientError as e:
153
+ error_msg = f"Failed to create IAM role {role_name}: {e}"
154
+ logger.error(error_msg)
155
+ result.mark_completed(OperationStatus.FAILED, error_msg)
156
+
157
+ return [result]
158
+
159
+ def delete_role(self, context: OperationContext, role_name: str) -> List[OperationResult]:
160
+ """
161
+ Delete IAM role.
162
+
163
+ Args:
164
+ context: Operation context
165
+ role_name: Name of role to delete
166
+
167
+ Returns:
168
+ List of operation results
169
+ """
170
+ iam_client = self.get_client("iam")
171
+
172
+ result = self.create_operation_result(context, "delete_role", "iam:role", role_name)
173
+
174
+ try:
175
+ if not self.confirm_operation(context, role_name, "delete IAM role"):
176
+ result.mark_completed(OperationStatus.CANCELLED, "Operation cancelled by user")
177
+ return [result]
178
+
179
+ if context.dry_run:
180
+ logger.info(f"[DRY-RUN] Would delete IAM role {role_name}")
181
+ result.mark_completed(OperationStatus.DRY_RUN)
182
+ else:
183
+ # First detach all policies
184
+ attached_policies = self.execute_aws_call(iam_client, "list_attached_role_policies", RoleName=role_name)
185
+
186
+ for policy in attached_policies.get("AttachedPolicies", []):
187
+ self.execute_aws_call(
188
+ iam_client, "detach_role_policy", RoleName=role_name, PolicyArn=policy["PolicyArn"]
189
+ )
190
+
191
+ # Delete inline policies
192
+ inline_policies = self.execute_aws_call(iam_client, "list_role_policies", RoleName=role_name)
193
+
194
+ for policy_name in inline_policies.get("PolicyNames", []):
195
+ self.execute_aws_call(iam_client, "delete_role_policy", RoleName=role_name, PolicyName=policy_name)
196
+
197
+ # Finally delete the role
198
+ response = self.execute_aws_call(iam_client, "delete_role", RoleName=role_name)
199
+
200
+ result.response_data = response
201
+ result.mark_completed(OperationStatus.SUCCESS)
202
+ logger.info(f"Successfully deleted IAM role {role_name}")
203
+
204
+ except ClientError as e:
205
+ error_msg = f"Failed to delete IAM role {role_name}: {e}"
206
+ logger.error(error_msg)
207
+ result.mark_completed(OperationStatus.FAILED, error_msg)
208
+
209
+ return [result]
210
+
211
+ def update_assume_role_policy(
212
+ self, context: OperationContext, role_name: str, policy_document: str
213
+ ) -> List[OperationResult]:
214
+ """
215
+ Update IAM role trust policy.
216
+
217
+ Args:
218
+ context: Operation context
219
+ role_name: Name of role to update
220
+ policy_document: New trust policy document
221
+
222
+ Returns:
223
+ List of operation results
224
+ """
225
+ iam_client = self.get_client("iam")
226
+
227
+ result = self.create_operation_result(context, "update_assume_role_policy", "iam:role", role_name)
228
+
229
+ try:
230
+ if context.dry_run:
231
+ logger.info(f"[DRY-RUN] Would update trust policy for IAM role {role_name}")
232
+ result.mark_completed(OperationStatus.DRY_RUN)
233
+ else:
234
+ response = self.execute_aws_call(
235
+ iam_client, "update_assume_role_policy", RoleName=role_name, PolicyDocument=policy_document
236
+ )
237
+
238
+ result.response_data = response
239
+ result.mark_completed(OperationStatus.SUCCESS)
240
+ logger.info(f"Successfully updated trust policy for IAM role {role_name}")
241
+
242
+ except ClientError as e:
243
+ error_msg = f"Failed to update trust policy for IAM role {role_name}: {e}"
244
+ logger.error(error_msg)
245
+ result.mark_completed(OperationStatus.FAILED, error_msg)
246
+
247
+ return [result]
248
+
249
+ def update_roles_cross_accounts(
250
+ self,
251
+ context: OperationContext,
252
+ role_name: str,
253
+ trusted_account_ids: List[str],
254
+ external_id: Optional[str] = None,
255
+ require_mfa: bool = False,
256
+ session_duration: int = 3600,
257
+ ) -> List[OperationResult]:
258
+ """
259
+ Update IAM roles for cross-account access.
260
+
261
+ Migrated from inventory/update_iam_roles_cross_accounts.py
262
+
263
+ Args:
264
+ context: Operation context
265
+ role_name: Name of role to update
266
+ trusted_account_ids: List of trusted account IDs
267
+ external_id: External ID for additional security
268
+ require_mfa: Whether to require MFA
269
+ session_duration: Session duration in seconds
270
+
271
+ Returns:
272
+ List of operation results
273
+ """
274
+ iam_client = self.get_client("iam")
275
+
276
+ result = self.create_operation_result(context, "update_roles_cross_accounts", "iam:role", role_name)
277
+
278
+ try:
279
+ if context.dry_run:
280
+ logger.info(f"[DRY-RUN] Would update cross-account access for role {role_name}")
281
+ result.mark_completed(OperationStatus.DRY_RUN)
282
+ return [result]
283
+
284
+ # Build trust policy for cross-account access
285
+ trust_policy = {"Version": "2012-10-17", "Statement": []}
286
+
287
+ for account_id in trusted_account_ids:
288
+ statement = {
289
+ "Effect": "Allow",
290
+ "Principal": {"AWS": f"arn:aws:iam::{account_id}:root"},
291
+ "Action": "sts:AssumeRole",
292
+ }
293
+
294
+ # Add conditions if specified
295
+ conditions = {}
296
+
297
+ if external_id:
298
+ conditions["StringEquals"] = {"sts:ExternalId": external_id}
299
+
300
+ if require_mfa:
301
+ conditions["Bool"] = {"aws:MultiFactorAuthPresent": "true"}
302
+
303
+ if session_duration != 3600:
304
+ conditions["NumericLessThan"] = {"aws:TokenIssueTime": str(session_duration)}
305
+
306
+ if conditions:
307
+ statement["Condition"] = conditions
308
+
309
+ trust_policy["Statement"].append(statement)
310
+
311
+ # Update the role's trust policy
312
+ response = self.execute_aws_call(
313
+ iam_client, "update_assume_role_policy", RoleName=role_name, PolicyDocument=json.dumps(trust_policy)
314
+ )
315
+
316
+ # Update max session duration if different from default
317
+ if session_duration != 3600:
318
+ self.execute_aws_call(
319
+ iam_client, "update_role", RoleName=role_name, MaxSessionDuration=session_duration
320
+ )
321
+
322
+ result.response_data = {
323
+ "role_name": role_name,
324
+ "trusted_accounts": trusted_account_ids,
325
+ "external_id": external_id,
326
+ "require_mfa": require_mfa,
327
+ "session_duration": session_duration,
328
+ "trust_policy": trust_policy,
329
+ }
330
+ result.mark_completed(OperationStatus.SUCCESS)
331
+ logger.info(f"Successfully updated cross-account access for role {role_name}")
332
+
333
+ except ClientError as e:
334
+ error_msg = f"Failed to update cross-account access for role {role_name}: {e}"
335
+ logger.error(error_msg)
336
+ result.mark_completed(OperationStatus.FAILED, error_msg)
337
+
338
+ return [result]
339
+
340
+ def attach_role_policy(self, context: OperationContext, role_name: str, policy_arn: str) -> List[OperationResult]:
341
+ """
342
+ Attach policy to IAM role.
343
+
344
+ Args:
345
+ context: Operation context
346
+ role_name: Name of role
347
+ policy_arn: Policy ARN to attach
348
+
349
+ Returns:
350
+ List of operation results
351
+ """
352
+ iam_client = self.get_client("iam")
353
+
354
+ result = self.create_operation_result(context, "attach_role_policy", "iam:role", f"{role_name}:{policy_arn}")
355
+
356
+ try:
357
+ if context.dry_run:
358
+ logger.info(f"[DRY-RUN] Would attach policy {policy_arn} to role {role_name}")
359
+ result.mark_completed(OperationStatus.DRY_RUN)
360
+ else:
361
+ response = self.execute_aws_call(
362
+ iam_client, "attach_role_policy", RoleName=role_name, PolicyArn=policy_arn
363
+ )
364
+
365
+ result.response_data = response
366
+ result.mark_completed(OperationStatus.SUCCESS)
367
+ logger.info(f"Successfully attached policy {policy_arn} to role {role_name}")
368
+
369
+ except ClientError as e:
370
+ error_msg = f"Failed to attach policy to role: {e}"
371
+ logger.error(error_msg)
372
+ result.mark_completed(OperationStatus.FAILED, error_msg)
373
+
374
+ return [result]
375
+
376
+ def detach_role_policy(self, context: OperationContext, role_name: str, policy_arn: str) -> List[OperationResult]:
377
+ """
378
+ Detach policy from IAM role.
379
+
380
+ Args:
381
+ context: Operation context
382
+ role_name: Name of role
383
+ policy_arn: Policy ARN to detach
384
+
385
+ Returns:
386
+ List of operation results
387
+ """
388
+ iam_client = self.get_client("iam")
389
+
390
+ result = self.create_operation_result(context, "detach_role_policy", "iam:role", f"{role_name}:{policy_arn}")
391
+
392
+ try:
393
+ if context.dry_run:
394
+ logger.info(f"[DRY-RUN] Would detach policy {policy_arn} from role {role_name}")
395
+ result.mark_completed(OperationStatus.DRY_RUN)
396
+ else:
397
+ response = self.execute_aws_call(
398
+ iam_client, "detach_role_policy", RoleName=role_name, PolicyArn=policy_arn
399
+ )
400
+
401
+ result.response_data = response
402
+ result.mark_completed(OperationStatus.SUCCESS)
403
+ logger.info(f"Successfully detached policy {policy_arn} from role {role_name}")
404
+
405
+ except ClientError as e:
406
+ error_msg = f"Failed to detach policy from role: {e}"
407
+ logger.error(error_msg)
408
+ result.mark_completed(OperationStatus.FAILED, error_msg)
409
+
410
+ return [result]
411
+
412
+ def create_policy(
413
+ self,
414
+ context: OperationContext,
415
+ policy_name: str,
416
+ policy_document: str,
417
+ path: str = "/",
418
+ description: Optional[str] = None,
419
+ tags: Optional[List[Dict[str, str]]] = None,
420
+ ) -> List[OperationResult]:
421
+ """
422
+ Create IAM policy.
423
+
424
+ Args:
425
+ context: Operation context
426
+ policy_name: Name of policy to create
427
+ policy_document: Policy document JSON
428
+ path: Policy path
429
+ description: Policy description
430
+ tags: Policy tags
431
+
432
+ Returns:
433
+ List of operation results
434
+ """
435
+ iam_client = self.get_client("iam")
436
+
437
+ result = self.create_operation_result(context, "create_policy", "iam:policy", policy_name)
438
+
439
+ try:
440
+ if context.dry_run:
441
+ logger.info(f"[DRY-RUN] Would create IAM policy {policy_name}")
442
+ result.mark_completed(OperationStatus.DRY_RUN)
443
+ return [result]
444
+
445
+ create_params = {
446
+ "PolicyName": policy_name,
447
+ "PolicyDocument": policy_document,
448
+ "Path": path,
449
+ }
450
+
451
+ if description:
452
+ create_params["Description"] = description
453
+ if tags:
454
+ create_params["Tags"] = tags
455
+
456
+ response = self.execute_aws_call(iam_client, "create_policy", **create_params)
457
+
458
+ result.response_data = response
459
+ result.mark_completed(OperationStatus.SUCCESS)
460
+ logger.info(f"Successfully created IAM policy {policy_name}")
461
+
462
+ except ClientError as e:
463
+ error_msg = f"Failed to create IAM policy {policy_name}: {e}"
464
+ logger.error(error_msg)
465
+ result.mark_completed(OperationStatus.FAILED, error_msg)
466
+
467
+ return [result]
468
+
469
+ def delete_policy(self, context: OperationContext, policy_arn: str) -> List[OperationResult]:
470
+ """
471
+ Delete IAM policy.
472
+
473
+ Args:
474
+ context: Operation context
475
+ policy_arn: ARN of policy to delete
476
+
477
+ Returns:
478
+ List of operation results
479
+ """
480
+ iam_client = self.get_client("iam")
481
+
482
+ result = self.create_operation_result(context, "delete_policy", "iam:policy", policy_arn)
483
+
484
+ try:
485
+ if not self.confirm_operation(context, policy_arn, "delete IAM policy"):
486
+ result.mark_completed(OperationStatus.CANCELLED, "Operation cancelled by user")
487
+ return [result]
488
+
489
+ if context.dry_run:
490
+ logger.info(f"[DRY-RUN] Would delete IAM policy {policy_arn}")
491
+ result.mark_completed(OperationStatus.DRY_RUN)
492
+ else:
493
+ # Detach policy from all entities first
494
+ entities = self.execute_aws_call(iam_client, "list_entities_for_policy", PolicyArn=policy_arn)
495
+
496
+ # Detach from roles
497
+ for role in entities.get("PolicyRoles", []):
498
+ self.execute_aws_call(
499
+ iam_client, "detach_role_policy", RoleName=role["RoleName"], PolicyArn=policy_arn
500
+ )
501
+
502
+ # Detach from users
503
+ for user in entities.get("PolicyUsers", []):
504
+ self.execute_aws_call(
505
+ iam_client, "detach_user_policy", UserName=user["UserName"], PolicyArn=policy_arn
506
+ )
507
+
508
+ # Detach from groups
509
+ for group in entities.get("PolicyGroups", []):
510
+ self.execute_aws_call(
511
+ iam_client, "detach_group_policy", GroupName=group["GroupName"], PolicyArn=policy_arn
512
+ )
513
+
514
+ # Delete all non-default versions
515
+ versions = self.execute_aws_call(iam_client, "list_policy_versions", PolicyArn=policy_arn)
516
+
517
+ for version in versions.get("Versions", []):
518
+ if not version["IsDefaultVersion"]:
519
+ self.execute_aws_call(
520
+ iam_client, "delete_policy_version", PolicyArn=policy_arn, VersionId=version["VersionId"]
521
+ )
522
+
523
+ # Finally delete the policy
524
+ response = self.execute_aws_call(iam_client, "delete_policy", PolicyArn=policy_arn)
525
+
526
+ result.response_data = response
527
+ result.mark_completed(OperationStatus.SUCCESS)
528
+ logger.info(f"Successfully deleted IAM policy {policy_arn}")
529
+
530
+ except ClientError as e:
531
+ error_msg = f"Failed to delete IAM policy {policy_arn}: {e}"
532
+ logger.error(error_msg)
533
+ result.mark_completed(OperationStatus.FAILED, error_msg)
534
+
535
+ return [result]
536
+
537
+ def tag_role(self, context: OperationContext, role_name: str, tags: List[Dict[str, str]]) -> List[OperationResult]:
538
+ """
539
+ Add tags to IAM role.
540
+
541
+ Args:
542
+ context: Operation context
543
+ role_name: Name of role to tag
544
+ tags: Tags to add
545
+
546
+ Returns:
547
+ List of operation results
548
+ """
549
+ iam_client = self.get_client("iam")
550
+
551
+ result = self.create_operation_result(context, "tag_role", "iam:role", role_name)
552
+
553
+ try:
554
+ if context.dry_run:
555
+ logger.info(f"[DRY-RUN] Would add {len(tags)} tags to role {role_name}")
556
+ result.mark_completed(OperationStatus.DRY_RUN)
557
+ else:
558
+ response = self.execute_aws_call(iam_client, "tag_role", RoleName=role_name, Tags=tags)
559
+
560
+ result.response_data = response
561
+ result.mark_completed(OperationStatus.SUCCESS)
562
+ logger.info(f"Successfully added {len(tags)} tags to role {role_name}")
563
+
564
+ except ClientError as e:
565
+ error_msg = f"Failed to tag role {role_name}: {e}"
566
+ logger.error(error_msg)
567
+ result.mark_completed(OperationStatus.FAILED, error_msg)
568
+
569
+ return [result]