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,619 @@
|
|
1
|
+
"""
|
2
|
+
CloudFormation Operations Module.
|
3
|
+
|
4
|
+
Provides comprehensive CloudFormation resource management capabilities including
|
5
|
+
stack operations, StackSet management, and infrastructure automation.
|
6
|
+
|
7
|
+
Migrated and enhanced from:
|
8
|
+
- inventory/cfn_move_stack_instances.py
|
9
|
+
- inventory/update_cfn_stacksets.py
|
10
|
+
- inventory/lockdown_cfn_stackset_role.py
|
11
|
+
"""
|
12
|
+
|
13
|
+
import json
|
14
|
+
from datetime import datetime
|
15
|
+
from typing import Any, Dict, List, Optional, Union
|
16
|
+
|
17
|
+
import boto3
|
18
|
+
from botocore.exceptions import ClientError
|
19
|
+
from loguru import logger
|
20
|
+
|
21
|
+
from runbooks.operate.base import BaseOperation, OperationContext, OperationResult, OperationStatus
|
22
|
+
|
23
|
+
|
24
|
+
class CloudFormationOperations(BaseOperation):
|
25
|
+
"""
|
26
|
+
CloudFormation resource operations and lifecycle management.
|
27
|
+
|
28
|
+
Handles all CloudFormation-related operational tasks including stack management,
|
29
|
+
StackSet operations, and infrastructure automation workflows.
|
30
|
+
"""
|
31
|
+
|
32
|
+
service_name = "cloudformation"
|
33
|
+
supported_operations = {
|
34
|
+
"create_stack",
|
35
|
+
"update_stack",
|
36
|
+
"delete_stack",
|
37
|
+
"create_stack_set",
|
38
|
+
"update_stack_set",
|
39
|
+
"delete_stack_set",
|
40
|
+
"create_stack_instances",
|
41
|
+
"update_stack_instances",
|
42
|
+
"delete_stack_instances",
|
43
|
+
"move_stack_instances",
|
44
|
+
"lockdown_stackset_role",
|
45
|
+
"enable_drift_detection",
|
46
|
+
"detect_stack_drift",
|
47
|
+
"cancel_update_stack",
|
48
|
+
}
|
49
|
+
requires_confirmation = True
|
50
|
+
|
51
|
+
def __init__(self, profile: Optional[str] = None, region: Optional[str] = None, dry_run: bool = False):
|
52
|
+
"""Initialize CloudFormation operations."""
|
53
|
+
super().__init__(profile, region, dry_run)
|
54
|
+
|
55
|
+
def execute_operation(self, context: OperationContext, operation_type: str, **kwargs) -> List[OperationResult]:
|
56
|
+
"""
|
57
|
+
Execute CloudFormation operation.
|
58
|
+
|
59
|
+
Args:
|
60
|
+
context: Operation context
|
61
|
+
operation_type: Type of operation to execute
|
62
|
+
**kwargs: Operation-specific arguments
|
63
|
+
|
64
|
+
Returns:
|
65
|
+
List of operation results
|
66
|
+
"""
|
67
|
+
self.validate_context(context)
|
68
|
+
|
69
|
+
if operation_type == "create_stack":
|
70
|
+
return self.create_stack(context, **kwargs)
|
71
|
+
elif operation_type == "update_stack":
|
72
|
+
return self.update_stack(context, **kwargs)
|
73
|
+
elif operation_type == "delete_stack":
|
74
|
+
return self.delete_stack(context, kwargs.get("stack_name"))
|
75
|
+
elif operation_type == "create_stack_set":
|
76
|
+
return self.create_stack_set(context, **kwargs)
|
77
|
+
elif operation_type == "update_stack_set":
|
78
|
+
return self.update_stack_set(context, **kwargs)
|
79
|
+
elif operation_type == "delete_stack_set":
|
80
|
+
return self.delete_stack_set(context, kwargs.get("stack_set_name"))
|
81
|
+
elif operation_type == "create_stack_instances":
|
82
|
+
return self.create_stack_instances(context, **kwargs)
|
83
|
+
elif operation_type == "update_stack_instances":
|
84
|
+
return self.update_stack_instances(context, **kwargs)
|
85
|
+
elif operation_type == "delete_stack_instances":
|
86
|
+
return self.delete_stack_instances(context, **kwargs)
|
87
|
+
elif operation_type == "move_stack_instances":
|
88
|
+
return self.move_stack_instances(context, **kwargs)
|
89
|
+
elif operation_type == "lockdown_stackset_role":
|
90
|
+
return self.lockdown_stackset_role(context, **kwargs)
|
91
|
+
elif operation_type == "enable_drift_detection":
|
92
|
+
return self.enable_drift_detection(context, kwargs.get("stack_name"))
|
93
|
+
elif operation_type == "detect_stack_drift":
|
94
|
+
return self.detect_stack_drift(context, kwargs.get("stack_name"))
|
95
|
+
elif operation_type == "cancel_update_stack":
|
96
|
+
return self.cancel_update_stack(context, kwargs.get("stack_name"))
|
97
|
+
else:
|
98
|
+
raise ValueError(f"Unsupported operation: {operation_type}")
|
99
|
+
|
100
|
+
def create_stack(
|
101
|
+
self,
|
102
|
+
context: OperationContext,
|
103
|
+
stack_name: str,
|
104
|
+
template_body: Optional[str] = None,
|
105
|
+
template_url: Optional[str] = None,
|
106
|
+
parameters: Optional[List[Dict[str, str]]] = None,
|
107
|
+
capabilities: Optional[List[str]] = None,
|
108
|
+
tags: Optional[List[Dict[str, str]]] = None,
|
109
|
+
role_arn: Optional[str] = None,
|
110
|
+
enable_termination_protection: bool = False,
|
111
|
+
) -> List[OperationResult]:
|
112
|
+
"""
|
113
|
+
Create CloudFormation stack.
|
114
|
+
|
115
|
+
Args:
|
116
|
+
context: Operation context
|
117
|
+
stack_name: Name of stack to create
|
118
|
+
template_body: Template body as string
|
119
|
+
template_url: Template URL
|
120
|
+
parameters: Stack parameters
|
121
|
+
capabilities: Required capabilities
|
122
|
+
tags: Stack tags
|
123
|
+
role_arn: Service role ARN
|
124
|
+
enable_termination_protection: Enable termination protection
|
125
|
+
|
126
|
+
Returns:
|
127
|
+
List of operation results
|
128
|
+
"""
|
129
|
+
cfn_client = self.get_client("cloudformation", context.region)
|
130
|
+
|
131
|
+
result = self.create_operation_result(context, "create_stack", "cloudformation:stack", stack_name)
|
132
|
+
|
133
|
+
try:
|
134
|
+
if context.dry_run:
|
135
|
+
logger.info(f"[DRY-RUN] Would create CloudFormation stack {stack_name}")
|
136
|
+
result.mark_completed(OperationStatus.DRY_RUN)
|
137
|
+
return [result]
|
138
|
+
|
139
|
+
create_params = {
|
140
|
+
"StackName": stack_name,
|
141
|
+
"EnableTerminationProtection": enable_termination_protection,
|
142
|
+
}
|
143
|
+
|
144
|
+
if template_body:
|
145
|
+
create_params["TemplateBody"] = template_body
|
146
|
+
elif template_url:
|
147
|
+
create_params["TemplateURL"] = template_url
|
148
|
+
else:
|
149
|
+
raise ValueError("Either template_body or template_url must be provided")
|
150
|
+
|
151
|
+
if parameters:
|
152
|
+
create_params["Parameters"] = parameters
|
153
|
+
if capabilities:
|
154
|
+
create_params["Capabilities"] = capabilities
|
155
|
+
if tags:
|
156
|
+
create_params["Tags"] = tags
|
157
|
+
if role_arn:
|
158
|
+
create_params["RoleARN"] = role_arn
|
159
|
+
|
160
|
+
response = self.execute_aws_call(cfn_client, "create_stack", **create_params)
|
161
|
+
|
162
|
+
result.response_data = response
|
163
|
+
result.mark_completed(OperationStatus.SUCCESS)
|
164
|
+
logger.info(f"Successfully created CloudFormation stack {stack_name}")
|
165
|
+
|
166
|
+
except ClientError as e:
|
167
|
+
error_msg = f"Failed to create CloudFormation stack {stack_name}: {e}"
|
168
|
+
logger.error(error_msg)
|
169
|
+
result.mark_completed(OperationStatus.FAILED, error_msg)
|
170
|
+
|
171
|
+
return [result]
|
172
|
+
|
173
|
+
def delete_stack(
|
174
|
+
self, context: OperationContext, stack_name: str, role_arn: Optional[str] = None
|
175
|
+
) -> List[OperationResult]:
|
176
|
+
"""
|
177
|
+
Delete CloudFormation stack.
|
178
|
+
|
179
|
+
Args:
|
180
|
+
context: Operation context
|
181
|
+
stack_name: Name of stack to delete
|
182
|
+
role_arn: Service role ARN
|
183
|
+
|
184
|
+
Returns:
|
185
|
+
List of operation results
|
186
|
+
"""
|
187
|
+
cfn_client = self.get_client("cloudformation", context.region)
|
188
|
+
|
189
|
+
result = self.create_operation_result(context, "delete_stack", "cloudformation:stack", stack_name)
|
190
|
+
|
191
|
+
try:
|
192
|
+
if not self.confirm_operation(context, stack_name, "delete CloudFormation stack"):
|
193
|
+
result.mark_completed(OperationStatus.CANCELLED, "Operation cancelled by user")
|
194
|
+
return [result]
|
195
|
+
|
196
|
+
if context.dry_run:
|
197
|
+
logger.info(f"[DRY-RUN] Would delete CloudFormation stack {stack_name}")
|
198
|
+
result.mark_completed(OperationStatus.DRY_RUN)
|
199
|
+
else:
|
200
|
+
delete_params = {"StackName": stack_name}
|
201
|
+
if role_arn:
|
202
|
+
delete_params["RoleARN"] = role_arn
|
203
|
+
|
204
|
+
response = self.execute_aws_call(cfn_client, "delete_stack", **delete_params)
|
205
|
+
|
206
|
+
result.response_data = response
|
207
|
+
result.mark_completed(OperationStatus.SUCCESS)
|
208
|
+
logger.info(f"Successfully initiated deletion of CloudFormation stack {stack_name}")
|
209
|
+
|
210
|
+
except ClientError as e:
|
211
|
+
error_msg = f"Failed to delete CloudFormation stack {stack_name}: {e}"
|
212
|
+
logger.error(error_msg)
|
213
|
+
result.mark_completed(OperationStatus.FAILED, error_msg)
|
214
|
+
|
215
|
+
return [result]
|
216
|
+
|
217
|
+
def move_stack_instances(
|
218
|
+
self,
|
219
|
+
context: OperationContext,
|
220
|
+
source_stack_set_name: str,
|
221
|
+
target_stack_set_name: str,
|
222
|
+
account_ids: List[str],
|
223
|
+
regions: List[str],
|
224
|
+
operation_preferences: Optional[Dict[str, Any]] = None,
|
225
|
+
) -> List[OperationResult]:
|
226
|
+
"""
|
227
|
+
Move stack instances between StackSets.
|
228
|
+
|
229
|
+
Migrated from inventory/cfn_move_stack_instances.py
|
230
|
+
|
231
|
+
Args:
|
232
|
+
context: Operation context
|
233
|
+
source_stack_set_name: Source StackSet name
|
234
|
+
target_stack_set_name: Target StackSet name
|
235
|
+
account_ids: Account IDs to move
|
236
|
+
regions: Regions to move
|
237
|
+
operation_preferences: Operation preferences
|
238
|
+
|
239
|
+
Returns:
|
240
|
+
List of operation results
|
241
|
+
"""
|
242
|
+
cfn_client = self.get_client("cloudformation", context.region)
|
243
|
+
|
244
|
+
result = self.create_operation_result(
|
245
|
+
context,
|
246
|
+
"move_stack_instances",
|
247
|
+
"cloudformation:stackset",
|
248
|
+
f"{source_stack_set_name} -> {target_stack_set_name}",
|
249
|
+
)
|
250
|
+
|
251
|
+
try:
|
252
|
+
if not self.confirm_operation(context, f"{len(account_ids)} instances", "move StackSet instances"):
|
253
|
+
result.mark_completed(OperationStatus.CANCELLED, "Operation cancelled by user")
|
254
|
+
return [result]
|
255
|
+
|
256
|
+
if context.dry_run:
|
257
|
+
logger.info(
|
258
|
+
f"[DRY-RUN] Would move {len(account_ids)} instances from {source_stack_set_name} to {target_stack_set_name}"
|
259
|
+
)
|
260
|
+
result.mark_completed(OperationStatus.DRY_RUN)
|
261
|
+
return [result]
|
262
|
+
|
263
|
+
# Step 1: Delete instances from source StackSet
|
264
|
+
delete_params = {"StackSetName": source_stack_set_name, "Accounts": account_ids, "Regions": regions}
|
265
|
+
|
266
|
+
if operation_preferences:
|
267
|
+
delete_params["OperationPreferences"] = operation_preferences
|
268
|
+
|
269
|
+
delete_response = self.execute_aws_call(cfn_client, "delete_stack_instances", **delete_params)
|
270
|
+
|
271
|
+
delete_operation_id = delete_response["OperationId"]
|
272
|
+
logger.info(f"Initiated deletion from source StackSet: {delete_operation_id}")
|
273
|
+
|
274
|
+
# Wait for deletion to complete (simplified - in production, implement proper polling)
|
275
|
+
# For now, we'll return the operation ID for monitoring
|
276
|
+
|
277
|
+
# Step 2: Create instances in target StackSet
|
278
|
+
create_params = {"StackSetName": target_stack_set_name, "Accounts": account_ids, "Regions": regions}
|
279
|
+
|
280
|
+
if operation_preferences:
|
281
|
+
create_params["OperationPreferences"] = operation_preferences
|
282
|
+
|
283
|
+
create_response = self.execute_aws_call(cfn_client, "create_stack_instances", **create_params)
|
284
|
+
|
285
|
+
create_operation_id = create_response["OperationId"]
|
286
|
+
logger.info(f"Initiated creation in target StackSet: {create_operation_id}")
|
287
|
+
|
288
|
+
result.response_data = {
|
289
|
+
"delete_operation_id": delete_operation_id,
|
290
|
+
"create_operation_id": create_operation_id,
|
291
|
+
"moved_accounts": account_ids,
|
292
|
+
"moved_regions": regions,
|
293
|
+
}
|
294
|
+
result.mark_completed(OperationStatus.SUCCESS)
|
295
|
+
logger.info(f"Successfully initiated move of {len(account_ids)} instances")
|
296
|
+
|
297
|
+
except ClientError as e:
|
298
|
+
error_msg = f"Failed to move stack instances: {e}"
|
299
|
+
logger.error(error_msg)
|
300
|
+
result.mark_completed(OperationStatus.FAILED, error_msg)
|
301
|
+
|
302
|
+
return [result]
|
303
|
+
|
304
|
+
def lockdown_stackset_role(
|
305
|
+
self,
|
306
|
+
context: OperationContext,
|
307
|
+
target_role_name: str = "AWSCloudFormationStackSetExecutionRole",
|
308
|
+
management_account_id: Optional[str] = None,
|
309
|
+
lock_policy: bool = True,
|
310
|
+
) -> List[OperationResult]:
|
311
|
+
"""
|
312
|
+
Lockdown CloudFormation StackSet execution role.
|
313
|
+
|
314
|
+
Migrated from inventory/lockdown_cfn_stackset_role.py
|
315
|
+
|
316
|
+
Args:
|
317
|
+
context: Operation context
|
318
|
+
target_role_name: Role name to lockdown
|
319
|
+
management_account_id: Management account ID to restrict access to
|
320
|
+
lock_policy: Whether to apply restrictive policy
|
321
|
+
|
322
|
+
Returns:
|
323
|
+
List of operation results
|
324
|
+
"""
|
325
|
+
iam_client = self.get_client("iam", context.region)
|
326
|
+
|
327
|
+
result = self.create_operation_result(context, "lockdown_stackset_role", "iam:role", target_role_name)
|
328
|
+
|
329
|
+
try:
|
330
|
+
if not self.confirm_operation(context, target_role_name, "lockdown StackSet role"):
|
331
|
+
result.mark_completed(OperationStatus.CANCELLED, "Operation cancelled by user")
|
332
|
+
return [result]
|
333
|
+
|
334
|
+
if context.dry_run:
|
335
|
+
logger.info(f"[DRY-RUN] Would lockdown StackSet role {target_role_name}")
|
336
|
+
result.mark_completed(OperationStatus.DRY_RUN)
|
337
|
+
return [result]
|
338
|
+
|
339
|
+
# Get current role
|
340
|
+
role_response = self.execute_aws_call(iam_client, "get_role", RoleName=target_role_name)
|
341
|
+
|
342
|
+
current_trust_policy = role_response["Role"]["AssumeRolePolicyDocument"]
|
343
|
+
|
344
|
+
if lock_policy and management_account_id:
|
345
|
+
# Create restrictive trust policy
|
346
|
+
locked_trust_policy = {
|
347
|
+
"Version": "2012-10-17",
|
348
|
+
"Statement": [
|
349
|
+
{
|
350
|
+
"Effect": "Allow",
|
351
|
+
"Principal": {"AWS": f"arn:aws:iam::{management_account_id}:root"},
|
352
|
+
"Action": "sts:AssumeRole",
|
353
|
+
"Condition": {"StringEquals": {"aws:PrincipalServiceName": "cloudformation.amazonaws.com"}},
|
354
|
+
}
|
355
|
+
],
|
356
|
+
}
|
357
|
+
|
358
|
+
# Update trust policy
|
359
|
+
response = self.execute_aws_call(
|
360
|
+
iam_client,
|
361
|
+
"update_assume_role_policy",
|
362
|
+
RoleName=target_role_name,
|
363
|
+
PolicyDocument=json.dumps(locked_trust_policy),
|
364
|
+
)
|
365
|
+
|
366
|
+
result.response_data = {
|
367
|
+
"role_name": target_role_name,
|
368
|
+
"previous_policy": current_trust_policy,
|
369
|
+
"new_policy": locked_trust_policy,
|
370
|
+
"management_account_id": management_account_id,
|
371
|
+
}
|
372
|
+
result.mark_completed(OperationStatus.SUCCESS)
|
373
|
+
logger.info(f"Successfully locked down StackSet role {target_role_name}")
|
374
|
+
else:
|
375
|
+
result.response_data = {
|
376
|
+
"role_name": target_role_name,
|
377
|
+
"current_policy": current_trust_policy,
|
378
|
+
"action": "Policy retrieved but not modified",
|
379
|
+
}
|
380
|
+
result.mark_completed(OperationStatus.SUCCESS)
|
381
|
+
logger.info(f"Retrieved current policy for role {target_role_name}")
|
382
|
+
|
383
|
+
except ClientError as e:
|
384
|
+
error_msg = f"Failed to lockdown StackSet role {target_role_name}: {e}"
|
385
|
+
logger.error(error_msg)
|
386
|
+
result.mark_completed(OperationStatus.FAILED, error_msg)
|
387
|
+
|
388
|
+
return [result]
|
389
|
+
|
390
|
+
def create_stack_set(
|
391
|
+
self,
|
392
|
+
context: OperationContext,
|
393
|
+
stack_set_name: str,
|
394
|
+
template_body: Optional[str] = None,
|
395
|
+
template_url: Optional[str] = None,
|
396
|
+
parameters: Optional[List[Dict[str, str]]] = None,
|
397
|
+
capabilities: Optional[List[str]] = None,
|
398
|
+
tags: Optional[List[Dict[str, str]]] = None,
|
399
|
+
administration_role_arn: Optional[str] = None,
|
400
|
+
execution_role_name: Optional[str] = None,
|
401
|
+
permission_model: str = "SERVICE_MANAGED",
|
402
|
+
) -> List[OperationResult]:
|
403
|
+
"""
|
404
|
+
Create CloudFormation StackSet.
|
405
|
+
|
406
|
+
Args:
|
407
|
+
context: Operation context
|
408
|
+
stack_set_name: Name of StackSet to create
|
409
|
+
template_body: Template body as string
|
410
|
+
template_url: Template URL
|
411
|
+
parameters: StackSet parameters
|
412
|
+
capabilities: Required capabilities
|
413
|
+
tags: StackSet tags
|
414
|
+
administration_role_arn: Administration role ARN
|
415
|
+
execution_role_name: Execution role name
|
416
|
+
permission_model: Permission model (SERVICE_MANAGED or SELF_MANAGED)
|
417
|
+
|
418
|
+
Returns:
|
419
|
+
List of operation results
|
420
|
+
"""
|
421
|
+
cfn_client = self.get_client("cloudformation", context.region)
|
422
|
+
|
423
|
+
result = self.create_operation_result(context, "create_stack_set", "cloudformation:stackset", stack_set_name)
|
424
|
+
|
425
|
+
try:
|
426
|
+
if context.dry_run:
|
427
|
+
logger.info(f"[DRY-RUN] Would create CloudFormation StackSet {stack_set_name}")
|
428
|
+
result.mark_completed(OperationStatus.DRY_RUN)
|
429
|
+
return [result]
|
430
|
+
|
431
|
+
create_params = {
|
432
|
+
"StackSetName": stack_set_name,
|
433
|
+
"PermissionModel": permission_model,
|
434
|
+
}
|
435
|
+
|
436
|
+
if template_body:
|
437
|
+
create_params["TemplateBody"] = template_body
|
438
|
+
elif template_url:
|
439
|
+
create_params["TemplateURL"] = template_url
|
440
|
+
else:
|
441
|
+
raise ValueError("Either template_body or template_url must be provided")
|
442
|
+
|
443
|
+
if parameters:
|
444
|
+
create_params["Parameters"] = parameters
|
445
|
+
if capabilities:
|
446
|
+
create_params["Capabilities"] = capabilities
|
447
|
+
if tags:
|
448
|
+
create_params["Tags"] = tags
|
449
|
+
if administration_role_arn:
|
450
|
+
create_params["AdministrationRoleARN"] = administration_role_arn
|
451
|
+
if execution_role_name:
|
452
|
+
create_params["ExecutionRoleName"] = execution_role_name
|
453
|
+
|
454
|
+
response = self.execute_aws_call(cfn_client, "create_stack_set", **create_params)
|
455
|
+
|
456
|
+
result.response_data = response
|
457
|
+
result.mark_completed(OperationStatus.SUCCESS)
|
458
|
+
logger.info(f"Successfully created CloudFormation StackSet {stack_set_name}")
|
459
|
+
|
460
|
+
except ClientError as e:
|
461
|
+
error_msg = f"Failed to create CloudFormation StackSet {stack_set_name}: {e}"
|
462
|
+
logger.error(error_msg)
|
463
|
+
result.mark_completed(OperationStatus.FAILED, error_msg)
|
464
|
+
|
465
|
+
return [result]
|
466
|
+
|
467
|
+
def delete_stack_set(self, context: OperationContext, stack_set_name: str) -> List[OperationResult]:
|
468
|
+
"""
|
469
|
+
Delete CloudFormation StackSet.
|
470
|
+
|
471
|
+
Args:
|
472
|
+
context: Operation context
|
473
|
+
stack_set_name: Name of StackSet to delete
|
474
|
+
|
475
|
+
Returns:
|
476
|
+
List of operation results
|
477
|
+
"""
|
478
|
+
cfn_client = self.get_client("cloudformation", context.region)
|
479
|
+
|
480
|
+
result = self.create_operation_result(context, "delete_stack_set", "cloudformation:stackset", stack_set_name)
|
481
|
+
|
482
|
+
try:
|
483
|
+
if not self.confirm_operation(context, stack_set_name, "delete CloudFormation StackSet"):
|
484
|
+
result.mark_completed(OperationStatus.CANCELLED, "Operation cancelled by user")
|
485
|
+
return [result]
|
486
|
+
|
487
|
+
if context.dry_run:
|
488
|
+
logger.info(f"[DRY-RUN] Would delete CloudFormation StackSet {stack_set_name}")
|
489
|
+
result.mark_completed(OperationStatus.DRY_RUN)
|
490
|
+
else:
|
491
|
+
response = self.execute_aws_call(cfn_client, "delete_stack_set", StackSetName=stack_set_name)
|
492
|
+
|
493
|
+
result.response_data = response
|
494
|
+
result.mark_completed(OperationStatus.SUCCESS)
|
495
|
+
logger.info(f"Successfully deleted CloudFormation StackSet {stack_set_name}")
|
496
|
+
|
497
|
+
except ClientError as e:
|
498
|
+
error_msg = f"Failed to delete CloudFormation StackSet {stack_set_name}: {e}"
|
499
|
+
logger.error(error_msg)
|
500
|
+
result.mark_completed(OperationStatus.FAILED, error_msg)
|
501
|
+
|
502
|
+
return [result]
|
503
|
+
|
504
|
+
def create_stack_instances(
|
505
|
+
self,
|
506
|
+
context: OperationContext,
|
507
|
+
stack_set_name: str,
|
508
|
+
account_ids: List[str],
|
509
|
+
regions: List[str],
|
510
|
+
parameter_overrides: Optional[List[Dict[str, str]]] = None,
|
511
|
+
operation_preferences: Optional[Dict[str, Any]] = None,
|
512
|
+
) -> List[OperationResult]:
|
513
|
+
"""
|
514
|
+
Create CloudFormation StackSet instances.
|
515
|
+
|
516
|
+
Args:
|
517
|
+
context: Operation context
|
518
|
+
stack_set_name: StackSet name
|
519
|
+
account_ids: Target account IDs
|
520
|
+
regions: Target regions
|
521
|
+
parameter_overrides: Parameter overrides
|
522
|
+
operation_preferences: Operation preferences
|
523
|
+
|
524
|
+
Returns:
|
525
|
+
List of operation results
|
526
|
+
"""
|
527
|
+
cfn_client = self.get_client("cloudformation", context.region)
|
528
|
+
|
529
|
+
result = self.create_operation_result(
|
530
|
+
context, "create_stack_instances", "cloudformation:stackset", stack_set_name
|
531
|
+
)
|
532
|
+
|
533
|
+
try:
|
534
|
+
if context.dry_run:
|
535
|
+
logger.info(f"[DRY-RUN] Would create {len(account_ids)} stack instances in StackSet {stack_set_name}")
|
536
|
+
result.mark_completed(OperationStatus.DRY_RUN)
|
537
|
+
return [result]
|
538
|
+
|
539
|
+
create_params = {"StackSetName": stack_set_name, "Accounts": account_ids, "Regions": regions}
|
540
|
+
|
541
|
+
if parameter_overrides:
|
542
|
+
create_params["ParameterOverrides"] = parameter_overrides
|
543
|
+
if operation_preferences:
|
544
|
+
create_params["OperationPreferences"] = operation_preferences
|
545
|
+
|
546
|
+
response = self.execute_aws_call(cfn_client, "create_stack_instances", **create_params)
|
547
|
+
|
548
|
+
result.response_data = response
|
549
|
+
result.mark_completed(OperationStatus.SUCCESS)
|
550
|
+
logger.info(f"Successfully initiated creation of {len(account_ids)} stack instances")
|
551
|
+
|
552
|
+
except ClientError as e:
|
553
|
+
error_msg = f"Failed to create stack instances: {e}"
|
554
|
+
logger.error(error_msg)
|
555
|
+
result.mark_completed(OperationStatus.FAILED, error_msg)
|
556
|
+
|
557
|
+
return [result]
|
558
|
+
|
559
|
+
def delete_stack_instances(
|
560
|
+
self,
|
561
|
+
context: OperationContext,
|
562
|
+
stack_set_name: str,
|
563
|
+
account_ids: List[str],
|
564
|
+
regions: List[str],
|
565
|
+
retain_stacks: bool = False,
|
566
|
+
operation_preferences: Optional[Dict[str, Any]] = None,
|
567
|
+
) -> List[OperationResult]:
|
568
|
+
"""
|
569
|
+
Delete CloudFormation StackSet instances.
|
570
|
+
|
571
|
+
Args:
|
572
|
+
context: Operation context
|
573
|
+
stack_set_name: StackSet name
|
574
|
+
account_ids: Target account IDs
|
575
|
+
regions: Target regions
|
576
|
+
retain_stacks: Whether to retain stacks
|
577
|
+
operation_preferences: Operation preferences
|
578
|
+
|
579
|
+
Returns:
|
580
|
+
List of operation results
|
581
|
+
"""
|
582
|
+
cfn_client = self.get_client("cloudformation", context.region)
|
583
|
+
|
584
|
+
result = self.create_operation_result(
|
585
|
+
context, "delete_stack_instances", "cloudformation:stackset", stack_set_name
|
586
|
+
)
|
587
|
+
|
588
|
+
try:
|
589
|
+
if not self.confirm_operation(context, f"{len(account_ids)} instances", "delete StackSet instances"):
|
590
|
+
result.mark_completed(OperationStatus.CANCELLED, "Operation cancelled by user")
|
591
|
+
return [result]
|
592
|
+
|
593
|
+
if context.dry_run:
|
594
|
+
logger.info(f"[DRY-RUN] Would delete {len(account_ids)} stack instances from StackSet {stack_set_name}")
|
595
|
+
result.mark_completed(OperationStatus.DRY_RUN)
|
596
|
+
return [result]
|
597
|
+
|
598
|
+
delete_params = {
|
599
|
+
"StackSetName": stack_set_name,
|
600
|
+
"Accounts": account_ids,
|
601
|
+
"Regions": regions,
|
602
|
+
"RetainStacks": retain_stacks,
|
603
|
+
}
|
604
|
+
|
605
|
+
if operation_preferences:
|
606
|
+
delete_params["OperationPreferences"] = operation_preferences
|
607
|
+
|
608
|
+
response = self.execute_aws_call(cfn_client, "delete_stack_instances", **delete_params)
|
609
|
+
|
610
|
+
result.response_data = response
|
611
|
+
result.mark_completed(OperationStatus.SUCCESS)
|
612
|
+
logger.info(f"Successfully initiated deletion of {len(account_ids)} stack instances")
|
613
|
+
|
614
|
+
except ClientError as e:
|
615
|
+
error_msg = f"Failed to delete stack instances: {e}"
|
616
|
+
logger.error(error_msg)
|
617
|
+
result.mark_completed(OperationStatus.FAILED, error_msg)
|
618
|
+
|
619
|
+
return [result]
|