runbooks 0.7.0__py3-none-any.whl → 0.7.5__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 (100) 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.5.dist-info/METADATA +606 -0
  67. {runbooks-0.7.0.dist-info → runbooks-0.7.5.dist-info}/RECORD +72 -44
  68. {runbooks-0.7.0.dist-info → runbooks-0.7.5.dist-info}/entry_points.txt +0 -1
  69. runbooks/aws/__init__.py +0 -58
  70. runbooks/aws/dynamodb_operations.py +0 -231
  71. runbooks/aws/ec2_copy_image_cross-region.py +0 -195
  72. runbooks/aws/ec2_describe_instances.py +0 -202
  73. runbooks/aws/ec2_ebs_snapshots_delete.py +0 -186
  74. runbooks/aws/ec2_run_instances.py +0 -213
  75. runbooks/aws/ec2_start_stop_instances.py +0 -212
  76. runbooks/aws/ec2_terminate_instances.py +0 -143
  77. runbooks/aws/ec2_unused_eips.py +0 -196
  78. runbooks/aws/ec2_unused_volumes.py +0 -188
  79. runbooks/aws/s3_create_bucket.py +0 -142
  80. runbooks/aws/s3_list_buckets.py +0 -152
  81. runbooks/aws/s3_list_objects.py +0 -156
  82. runbooks/aws/s3_object_operations.py +0 -183
  83. runbooks/aws/tagging_lambda_handler.py +0 -183
  84. runbooks/inventory/FAILED_SCRIPTS_TROUBLESHOOTING.md +0 -619
  85. runbooks/inventory/PASSED_SCRIPTS_GUIDE.md +0 -738
  86. runbooks/inventory/cfn_move_stack_instances.py +0 -1526
  87. runbooks/inventory/delete_s3_buckets_objects.py +0 -169
  88. runbooks/inventory/lockdown_cfn_stackset_role.py +0 -224
  89. runbooks/inventory/update_aws_actions.py +0 -173
  90. runbooks/inventory/update_cfn_stacksets.py +0 -1215
  91. runbooks/inventory/update_cloudwatch_logs_retention_policy.py +0 -294
  92. runbooks/inventory/update_iam_roles_cross_accounts.py +0 -478
  93. runbooks/inventory/update_s3_public_access_block.py +0 -539
  94. runbooks/organizations/__init__.py +0 -12
  95. runbooks/organizations/manager.py +0 -374
  96. runbooks-0.7.0.dist-info/METADATA +0 -375
  97. /runbooks/{aws → operate}/tags.json +0 -0
  98. {runbooks-0.7.0.dist-info → runbooks-0.7.5.dist-info}/WHEEL +0 -0
  99. {runbooks-0.7.0.dist-info → runbooks-0.7.5.dist-info}/licenses/LICENSE +0 -0
  100. {runbooks-0.7.0.dist-info → runbooks-0.7.5.dist-info}/top_level.txt +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]