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.
- runbooks/__init__.py +87 -37
- runbooks/cfat/README.md +300 -49
- runbooks/cfat/__init__.py +2 -2
- runbooks/finops/__init__.py +1 -1
- runbooks/finops/cli.py +1 -1
- runbooks/inventory/collectors/__init__.py +8 -0
- runbooks/inventory/collectors/aws_management.py +791 -0
- runbooks/inventory/collectors/aws_networking.py +3 -3
- runbooks/main.py +3389 -782
- runbooks/operate/__init__.py +207 -0
- runbooks/operate/base.py +311 -0
- runbooks/operate/cloudformation_operations.py +619 -0
- runbooks/operate/cloudwatch_operations.py +496 -0
- runbooks/operate/dynamodb_operations.py +812 -0
- runbooks/operate/ec2_operations.py +926 -0
- runbooks/operate/iam_operations.py +569 -0
- runbooks/operate/s3_operations.py +1211 -0
- runbooks/operate/tagging_operations.py +655 -0
- runbooks/remediation/CLAUDE.md +100 -0
- runbooks/remediation/DOME9.md +218 -0
- runbooks/remediation/README.md +26 -0
- runbooks/remediation/Tests/__init__.py +0 -0
- runbooks/remediation/Tests/update_policy.py +74 -0
- runbooks/remediation/__init__.py +95 -0
- runbooks/remediation/acm_cert_expired_unused.py +98 -0
- runbooks/remediation/acm_remediation.py +875 -0
- runbooks/remediation/api_gateway_list.py +167 -0
- runbooks/remediation/base.py +643 -0
- runbooks/remediation/cloudtrail_remediation.py +908 -0
- runbooks/remediation/cloudtrail_s3_modifications.py +296 -0
- runbooks/remediation/cognito_active_users.py +78 -0
- runbooks/remediation/cognito_remediation.py +856 -0
- runbooks/remediation/cognito_user_password_reset.py +163 -0
- runbooks/remediation/commons.py +455 -0
- runbooks/remediation/dynamodb_optimize.py +155 -0
- runbooks/remediation/dynamodb_remediation.py +744 -0
- runbooks/remediation/dynamodb_server_side_encryption.py +108 -0
- runbooks/remediation/ec2_public_ips.py +134 -0
- runbooks/remediation/ec2_remediation.py +892 -0
- runbooks/remediation/ec2_subnet_disable_auto_ip_assignment.py +72 -0
- runbooks/remediation/ec2_unattached_ebs_volumes.py +448 -0
- runbooks/remediation/ec2_unused_security_groups.py +202 -0
- runbooks/remediation/kms_enable_key_rotation.py +651 -0
- runbooks/remediation/kms_remediation.py +717 -0
- runbooks/remediation/lambda_list.py +243 -0
- runbooks/remediation/lambda_remediation.py +971 -0
- runbooks/remediation/multi_account.py +569 -0
- runbooks/remediation/rds_instance_list.py +199 -0
- runbooks/remediation/rds_remediation.py +873 -0
- runbooks/remediation/rds_snapshot_list.py +192 -0
- runbooks/remediation/requirements.txt +118 -0
- runbooks/remediation/s3_block_public_access.py +159 -0
- runbooks/remediation/s3_bucket_public_access.py +143 -0
- runbooks/remediation/s3_disable_static_website_hosting.py +74 -0
- runbooks/remediation/s3_downloader.py +215 -0
- runbooks/remediation/s3_enable_access_logging.py +562 -0
- runbooks/remediation/s3_encryption.py +526 -0
- runbooks/remediation/s3_force_ssl_secure_policy.py +143 -0
- runbooks/remediation/s3_list.py +141 -0
- runbooks/remediation/s3_object_search.py +201 -0
- runbooks/remediation/s3_remediation.py +816 -0
- runbooks/remediation/scan_for_phrase.py +425 -0
- runbooks/remediation/workspaces_list.py +220 -0
- runbooks/security/__init__.py +9 -10
- runbooks/security/security_baseline_tester.py +4 -2
- runbooks-0.7.6.dist-info/METADATA +608 -0
- {runbooks-0.7.0.dist-info → runbooks-0.7.6.dist-info}/RECORD +84 -76
- {runbooks-0.7.0.dist-info → runbooks-0.7.6.dist-info}/entry_points.txt +0 -1
- {runbooks-0.7.0.dist-info → runbooks-0.7.6.dist-info}/top_level.txt +0 -1
- jupyter-agent/.env +0 -2
- jupyter-agent/.env.template +0 -2
- jupyter-agent/.gitattributes +0 -35
- jupyter-agent/.gradio/certificate.pem +0 -31
- jupyter-agent/README.md +0 -16
- jupyter-agent/__main__.log +0 -8
- jupyter-agent/app.py +0 -256
- jupyter-agent/cloudops-agent.png +0 -0
- jupyter-agent/ds-system-prompt.txt +0 -154
- jupyter-agent/jupyter-agent.png +0 -0
- jupyter-agent/llama3_template.jinja +0 -123
- jupyter-agent/requirements.txt +0 -9
- jupyter-agent/tmp/4ojbs8a02ir/jupyter-agent.ipynb +0 -68
- jupyter-agent/tmp/cm5iasgpm3p/jupyter-agent.ipynb +0 -91
- jupyter-agent/tmp/crqbsseag5/jupyter-agent.ipynb +0 -91
- jupyter-agent/tmp/hohanq1u097/jupyter-agent.ipynb +0 -57
- jupyter-agent/tmp/jns1sam29wm/jupyter-agent.ipynb +0 -53
- jupyter-agent/tmp/jupyter-agent.ipynb +0 -27
- jupyter-agent/utils.py +0 -409
- runbooks/aws/__init__.py +0 -58
- runbooks/aws/dynamodb_operations.py +0 -231
- runbooks/aws/ec2_copy_image_cross-region.py +0 -195
- runbooks/aws/ec2_describe_instances.py +0 -202
- runbooks/aws/ec2_ebs_snapshots_delete.py +0 -186
- runbooks/aws/ec2_run_instances.py +0 -213
- runbooks/aws/ec2_start_stop_instances.py +0 -212
- runbooks/aws/ec2_terminate_instances.py +0 -143
- runbooks/aws/ec2_unused_eips.py +0 -196
- runbooks/aws/ec2_unused_volumes.py +0 -188
- runbooks/aws/s3_create_bucket.py +0 -142
- runbooks/aws/s3_list_buckets.py +0 -152
- runbooks/aws/s3_list_objects.py +0 -156
- runbooks/aws/s3_object_operations.py +0 -183
- runbooks/aws/tagging_lambda_handler.py +0 -183
- runbooks/inventory/FAILED_SCRIPTS_TROUBLESHOOTING.md +0 -619
- runbooks/inventory/PASSED_SCRIPTS_GUIDE.md +0 -738
- runbooks/inventory/aws_organization.png +0 -0
- runbooks/inventory/cfn_move_stack_instances.py +0 -1526
- runbooks/inventory/delete_s3_buckets_objects.py +0 -169
- runbooks/inventory/lockdown_cfn_stackset_role.py +0 -224
- runbooks/inventory/update_aws_actions.py +0 -173
- runbooks/inventory/update_cfn_stacksets.py +0 -1215
- runbooks/inventory/update_cloudwatch_logs_retention_policy.py +0 -294
- runbooks/inventory/update_iam_roles_cross_accounts.py +0 -478
- runbooks/inventory/update_s3_public_access_block.py +0 -539
- runbooks/organizations/__init__.py +0 -12
- runbooks/organizations/manager.py +0 -374
- runbooks-0.7.0.dist-info/METADATA +0 -375
- /runbooks/inventory/{tests → Tests}/common_test_data.py +0 -0
- /runbooks/inventory/{tests → Tests}/common_test_functions.py +0 -0
- /runbooks/inventory/{tests → Tests}/script_test_data.py +0 -0
- /runbooks/inventory/{tests → Tests}/setup.py +0 -0
- /runbooks/inventory/{tests → Tests}/src.py +0 -0
- /runbooks/inventory/{tests/test_inventory_modules.py → Tests/test_Inventory_Modules.py} +0 -0
- /runbooks/inventory/{tests → Tests}/test_cfn_describe_stacks.py +0 -0
- /runbooks/inventory/{tests → Tests}/test_ec2_describe_instances.py +0 -0
- /runbooks/inventory/{tests → Tests}/test_lambda_list_functions.py +0 -0
- /runbooks/inventory/{tests → Tests}/test_moto_integration_example.py +0 -0
- /runbooks/inventory/{tests → Tests}/test_org_list_accounts.py +0 -0
- /runbooks/inventory/{Inventory_Modules.py → inventory_modules.py} +0 -0
- /runbooks/{aws → operate}/tags.json +0 -0
- {runbooks-0.7.0.dist-info → runbooks-0.7.6.dist-info}/WHEEL +0 -0
- {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]
|