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,791 @@
|
|
1
|
+
"""
|
2
|
+
AWS management and governance resource collector.
|
3
|
+
|
4
|
+
This module provides specialized collection of management and governance resources including
|
5
|
+
AWS Organizations, CloudFormation stacks/stacksets, Service Catalog, and related components.
|
6
|
+
Integrates the functionality from the organizations module.
|
7
|
+
"""
|
8
|
+
|
9
|
+
from datetime import datetime
|
10
|
+
from pathlib import Path
|
11
|
+
from typing import Any, Dict, List, Optional, Set, Union
|
12
|
+
|
13
|
+
import boto3
|
14
|
+
import yaml
|
15
|
+
from botocore.exceptions import ClientError
|
16
|
+
from loguru import logger
|
17
|
+
|
18
|
+
from runbooks.inventory.collectors.base import BaseResourceCollector, CollectionContext
|
19
|
+
from runbooks.inventory.models.resource import AWSResource, ResourceCost, ResourceState
|
20
|
+
from runbooks.inventory.utils.aws_helpers import aws_api_retry
|
21
|
+
|
22
|
+
|
23
|
+
class ManagementResourceCollector(BaseResourceCollector):
|
24
|
+
"""
|
25
|
+
Collector for AWS management and governance resources.
|
26
|
+
|
27
|
+
Handles discovery and inventory of:
|
28
|
+
- AWS Organizations (OUs, accounts, policies)
|
29
|
+
- CloudFormation stacks and stacksets
|
30
|
+
- Service Catalog portfolios and products
|
31
|
+
- Config rules and conformance packs
|
32
|
+
- Systems Manager documents and parameters
|
33
|
+
- CloudWatch dashboards and alarms
|
34
|
+
"""
|
35
|
+
|
36
|
+
service_category = "management"
|
37
|
+
supported_resources = {
|
38
|
+
"organizations:account",
|
39
|
+
"organizations:organizational_unit",
|
40
|
+
"organizations:policy",
|
41
|
+
"cloudformation:stack",
|
42
|
+
"cloudformation:stackset",
|
43
|
+
"servicecatalog:portfolio",
|
44
|
+
"servicecatalog:product",
|
45
|
+
"config:rule",
|
46
|
+
"config:conformance_pack",
|
47
|
+
"ssm:document",
|
48
|
+
"ssm:parameter",
|
49
|
+
"cloudwatch:dashboard",
|
50
|
+
"cloudwatch:alarm",
|
51
|
+
}
|
52
|
+
requires_org_access = True # Organizations requires management account access
|
53
|
+
|
54
|
+
def collect_resources(
|
55
|
+
self, context: CollectionContext, resource_filters: Optional[Dict[str, Any]] = None
|
56
|
+
) -> List[AWSResource]:
|
57
|
+
"""
|
58
|
+
Collect management and governance resources.
|
59
|
+
|
60
|
+
Args:
|
61
|
+
context: Collection context with account and region info
|
62
|
+
resource_filters: Optional filters for resource selection
|
63
|
+
|
64
|
+
Returns:
|
65
|
+
List of discovered AWS management resources
|
66
|
+
"""
|
67
|
+
resources = []
|
68
|
+
resource_types = context.resource_types or self.supported_resources
|
69
|
+
|
70
|
+
# Create AWS clients
|
71
|
+
clients = self._create_clients(context)
|
72
|
+
|
73
|
+
for resource_type in resource_types:
|
74
|
+
if resource_type not in self.supported_resources:
|
75
|
+
continue
|
76
|
+
|
77
|
+
try:
|
78
|
+
if resource_type.startswith("organizations:"):
|
79
|
+
resources.extend(self._collect_organizations_resources(clients, context, resource_type))
|
80
|
+
elif resource_type.startswith("cloudformation:"):
|
81
|
+
resources.extend(self._collect_cloudformation_resources(clients, context, resource_type))
|
82
|
+
elif resource_type.startswith("servicecatalog:"):
|
83
|
+
resources.extend(self._collect_servicecatalog_resources(clients, context, resource_type))
|
84
|
+
elif resource_type.startswith("config:"):
|
85
|
+
resources.extend(self._collect_config_resources(clients, context, resource_type))
|
86
|
+
elif resource_type.startswith("ssm:"):
|
87
|
+
resources.extend(self._collect_ssm_resources(clients, context, resource_type))
|
88
|
+
elif resource_type.startswith("cloudwatch:"):
|
89
|
+
resources.extend(self._collect_cloudwatch_resources(clients, context, resource_type))
|
90
|
+
|
91
|
+
except Exception as e:
|
92
|
+
logger.error(f"Error collecting {resource_type} resources: {e}")
|
93
|
+
|
94
|
+
logger.info(f"Collected {len(resources)} management resources for account {context.account.account_id}")
|
95
|
+
return resources
|
96
|
+
|
97
|
+
def _create_clients(self, context: CollectionContext) -> Dict[str, Any]:
|
98
|
+
"""Create AWS service clients for management services."""
|
99
|
+
session = self._get_session(context.account)
|
100
|
+
return {
|
101
|
+
"organizations": session.client("organizations", region_name="us-east-1"), # Global service
|
102
|
+
"cloudformation": session.client("cloudformation", region_name=context.region),
|
103
|
+
"servicecatalog": session.client("servicecatalog", region_name=context.region),
|
104
|
+
"config": session.client("config", region_name=context.region),
|
105
|
+
"ssm": session.client("ssm", region_name=context.region),
|
106
|
+
"cloudwatch": session.client("cloudwatch", region_name=context.region),
|
107
|
+
}
|
108
|
+
|
109
|
+
def _collect_organizations_resources(
|
110
|
+
self, clients: Dict[str, Any], context: CollectionContext, resource_type: str
|
111
|
+
) -> List[AWSResource]:
|
112
|
+
"""Collect AWS Organizations resources."""
|
113
|
+
resources = []
|
114
|
+
org_client = clients["organizations"]
|
115
|
+
|
116
|
+
try:
|
117
|
+
if resource_type == "organizations:account":
|
118
|
+
resources.extend(self._collect_organization_accounts(org_client, context))
|
119
|
+
elif resource_type == "organizations:organizational_unit":
|
120
|
+
resources.extend(self._collect_organizational_units(org_client, context))
|
121
|
+
elif resource_type == "organizations:policy":
|
122
|
+
resources.extend(self._collect_organization_policies(org_client, context))
|
123
|
+
|
124
|
+
except ClientError as e:
|
125
|
+
if e.response["Error"]["Code"] == "AWSOrganizationsNotInUseException":
|
126
|
+
logger.warning("AWS Organizations is not enabled in this account")
|
127
|
+
else:
|
128
|
+
logger.error(f"Error collecting organizations resources: {e}")
|
129
|
+
|
130
|
+
return resources
|
131
|
+
|
132
|
+
@aws_api_retry
|
133
|
+
def _collect_organization_accounts(self, org_client, context: CollectionContext) -> List[AWSResource]:
|
134
|
+
"""Collect organization accounts."""
|
135
|
+
resources = []
|
136
|
+
|
137
|
+
try:
|
138
|
+
paginator = org_client.get_paginator("list_accounts")
|
139
|
+
for page in paginator.paginate():
|
140
|
+
for account in page.get("Accounts", []):
|
141
|
+
resource = AWSResource(
|
142
|
+
resource_id=account["Id"],
|
143
|
+
resource_type="organizations:account",
|
144
|
+
resource_name=account.get("Name", account["Id"]),
|
145
|
+
region="global", # Organizations is global
|
146
|
+
account_id=context.account.account_id,
|
147
|
+
state=ResourceState.AVAILABLE if account["Status"] == "ACTIVE" else ResourceState.UNKNOWN,
|
148
|
+
properties={
|
149
|
+
"email": account.get("Email"),
|
150
|
+
"status": account.get("Status"),
|
151
|
+
"joined_method": account.get("JoinedMethod"),
|
152
|
+
"joined_timestamp": account.get("JoinedTimestamp"),
|
153
|
+
},
|
154
|
+
tags=self._get_account_tags(org_client, account["Id"]),
|
155
|
+
created_date=account.get("JoinedTimestamp"),
|
156
|
+
last_modified=datetime.utcnow(),
|
157
|
+
)
|
158
|
+
resources.append(resource)
|
159
|
+
|
160
|
+
except Exception as e:
|
161
|
+
logger.error(f"Error collecting organization accounts: {e}")
|
162
|
+
|
163
|
+
return resources
|
164
|
+
|
165
|
+
@aws_api_retry
|
166
|
+
def _collect_organizational_units(self, org_client, context: CollectionContext) -> List[AWSResource]:
|
167
|
+
"""Collect organizational units."""
|
168
|
+
resources = []
|
169
|
+
|
170
|
+
try:
|
171
|
+
# Get organization root
|
172
|
+
roots = org_client.list_roots()["Roots"]
|
173
|
+
if not roots:
|
174
|
+
return resources
|
175
|
+
|
176
|
+
root_id = roots[0]["Id"]
|
177
|
+
|
178
|
+
# Recursively collect OUs
|
179
|
+
def collect_ous(parent_id: str, level: int = 0):
|
180
|
+
response = org_client.list_organizational_units_for_parent(ParentId=parent_id)
|
181
|
+
|
182
|
+
for ou in response.get("OrganizationalUnits", []):
|
183
|
+
resource = AWSResource(
|
184
|
+
resource_id=ou["Id"],
|
185
|
+
resource_type="organizations:organizational_unit",
|
186
|
+
resource_name=ou["Name"],
|
187
|
+
region="global",
|
188
|
+
account_id=context.account.account_id,
|
189
|
+
state=ResourceState.AVAILABLE,
|
190
|
+
properties={
|
191
|
+
"arn": ou.get("Arn"),
|
192
|
+
"parent_id": parent_id,
|
193
|
+
"level": level,
|
194
|
+
},
|
195
|
+
tags=self._get_ou_tags(org_client, ou["Id"]),
|
196
|
+
created_date=datetime.utcnow(),
|
197
|
+
last_modified=datetime.utcnow(),
|
198
|
+
)
|
199
|
+
resources.append(resource)
|
200
|
+
|
201
|
+
# Recursively collect child OUs
|
202
|
+
collect_ous(ou["Id"], level + 1)
|
203
|
+
|
204
|
+
collect_ous(root_id)
|
205
|
+
|
206
|
+
except Exception as e:
|
207
|
+
logger.error(f"Error collecting organizational units: {e}")
|
208
|
+
|
209
|
+
return resources
|
210
|
+
|
211
|
+
@aws_api_retry
|
212
|
+
def _collect_organization_policies(self, org_client, context: CollectionContext) -> List[AWSResource]:
|
213
|
+
"""Collect organization policies."""
|
214
|
+
resources = []
|
215
|
+
|
216
|
+
try:
|
217
|
+
# Get all policy types
|
218
|
+
policy_types = ["SERVICE_CONTROL_POLICY", "TAG_POLICY", "BACKUP_POLICY", "AISERVICES_OPT_OUT_POLICY"]
|
219
|
+
|
220
|
+
for policy_type in policy_types:
|
221
|
+
try:
|
222
|
+
paginator = org_client.get_paginator("list_policies")
|
223
|
+
for page in paginator.paginate(Filter=policy_type):
|
224
|
+
for policy in page.get("Policies", []):
|
225
|
+
resource = AWSResource(
|
226
|
+
resource_id=policy["Id"],
|
227
|
+
resource_type="organizations:policy",
|
228
|
+
resource_name=policy["Name"],
|
229
|
+
region="global",
|
230
|
+
account_id=context.account.account_id,
|
231
|
+
state=ResourceState.AVAILABLE,
|
232
|
+
properties={
|
233
|
+
"arn": policy.get("Arn"),
|
234
|
+
"type": policy.get("Type"),
|
235
|
+
"description": policy.get("Description"),
|
236
|
+
"aws_managed": policy.get("AwsManaged", False),
|
237
|
+
},
|
238
|
+
tags=self._get_policy_tags(org_client, policy["Id"]),
|
239
|
+
created_date=datetime.utcnow(),
|
240
|
+
last_modified=datetime.utcnow(),
|
241
|
+
)
|
242
|
+
resources.append(resource)
|
243
|
+
|
244
|
+
except Exception as e:
|
245
|
+
logger.warning(f"Error collecting {policy_type} policies: {e}")
|
246
|
+
|
247
|
+
except Exception as e:
|
248
|
+
logger.error(f"Error collecting organization policies: {e}")
|
249
|
+
|
250
|
+
return resources
|
251
|
+
|
252
|
+
def _get_account_tags(self, org_client, account_id: str) -> Dict[str, str]:
|
253
|
+
"""Get tags for an organization account."""
|
254
|
+
try:
|
255
|
+
response = org_client.list_tags_for_resource(ResourceId=account_id)
|
256
|
+
return {tag["Key"]: tag["Value"] for tag in response.get("Tags", [])}
|
257
|
+
except Exception:
|
258
|
+
return {}
|
259
|
+
|
260
|
+
def _get_ou_tags(self, org_client, ou_id: str) -> Dict[str, str]:
|
261
|
+
"""Get tags for an organizational unit."""
|
262
|
+
try:
|
263
|
+
response = org_client.list_tags_for_resource(ResourceId=ou_id)
|
264
|
+
return {tag["Key"]: tag["Value"] for tag in response.get("Tags", [])}
|
265
|
+
except Exception:
|
266
|
+
return {}
|
267
|
+
|
268
|
+
def _get_policy_tags(self, org_client, policy_id: str) -> Dict[str, str]:
|
269
|
+
"""Get tags for an organization policy."""
|
270
|
+
try:
|
271
|
+
response = org_client.list_tags_for_resource(ResourceId=policy_id)
|
272
|
+
return {tag["Key"]: tag["Value"] for tag in response.get("Tags", [])}
|
273
|
+
except Exception:
|
274
|
+
return {}
|
275
|
+
|
276
|
+
def _collect_cloudformation_resources(
|
277
|
+
self, clients: Dict[str, Any], context: CollectionContext, resource_type: str
|
278
|
+
) -> List[AWSResource]:
|
279
|
+
"""Collect CloudFormation resources."""
|
280
|
+
resources = []
|
281
|
+
cfn_client = clients["cloudformation"]
|
282
|
+
|
283
|
+
try:
|
284
|
+
if resource_type == "cloudformation:stack":
|
285
|
+
resources.extend(self._collect_cfn_stacks(cfn_client, context))
|
286
|
+
elif resource_type == "cloudformation:stackset":
|
287
|
+
resources.extend(self._collect_cfn_stacksets(cfn_client, context))
|
288
|
+
|
289
|
+
except Exception as e:
|
290
|
+
logger.error(f"Error collecting CloudFormation resources: {e}")
|
291
|
+
|
292
|
+
return resources
|
293
|
+
|
294
|
+
@aws_api_retry
|
295
|
+
def _collect_cfn_stacks(self, cfn_client, context: CollectionContext) -> List[AWSResource]:
|
296
|
+
"""Collect CloudFormation stacks."""
|
297
|
+
resources = []
|
298
|
+
|
299
|
+
try:
|
300
|
+
paginator = cfn_client.get_paginator("list_stacks")
|
301
|
+
for page in paginator.paginate():
|
302
|
+
for stack in page.get("StackSummaries", []):
|
303
|
+
if stack["StackStatus"] != "DELETE_COMPLETE":
|
304
|
+
resource = AWSResource(
|
305
|
+
resource_id=stack["StackId"],
|
306
|
+
resource_type="cloudformation:stack",
|
307
|
+
resource_name=stack["StackName"],
|
308
|
+
region=context.region,
|
309
|
+
account_id=context.account.account_id,
|
310
|
+
state=self._get_cfn_stack_state(stack["StackStatus"]),
|
311
|
+
properties={
|
312
|
+
"stack_status": stack["StackStatus"],
|
313
|
+
"creation_time": stack.get("CreationTime"),
|
314
|
+
"last_updated_time": stack.get("LastUpdatedTime"),
|
315
|
+
"deletion_time": stack.get("DeletionTime"),
|
316
|
+
"stack_status_reason": stack.get("StackStatusReason"),
|
317
|
+
"template_description": stack.get("TemplateDescription"),
|
318
|
+
"drift_status": stack.get("DriftInformation", {}).get("StackDriftStatus"),
|
319
|
+
},
|
320
|
+
tags=self._get_cfn_stack_tags(cfn_client, stack["StackName"]),
|
321
|
+
created_date=stack.get("CreationTime"),
|
322
|
+
last_modified=stack.get("LastUpdatedTime"),
|
323
|
+
)
|
324
|
+
resources.append(resource)
|
325
|
+
|
326
|
+
except Exception as e:
|
327
|
+
logger.error(f"Error collecting CloudFormation stacks: {e}")
|
328
|
+
|
329
|
+
return resources
|
330
|
+
|
331
|
+
@aws_api_retry
|
332
|
+
def _collect_cfn_stacksets(self, cfn_client, context: CollectionContext) -> List[AWSResource]:
|
333
|
+
"""Collect CloudFormation stacksets."""
|
334
|
+
resources = []
|
335
|
+
|
336
|
+
try:
|
337
|
+
paginator = cfn_client.get_paginator("list_stack_sets")
|
338
|
+
for page in paginator.paginate():
|
339
|
+
for stackset in page.get("Summaries", []):
|
340
|
+
resource = AWSResource(
|
341
|
+
resource_id=stackset["StackSetId"],
|
342
|
+
resource_type="cloudformation:stackset",
|
343
|
+
resource_name=stackset["StackSetName"],
|
344
|
+
region=context.region,
|
345
|
+
account_id=context.account.account_id,
|
346
|
+
state=self._get_cfn_stackset_state(stackset["Status"]),
|
347
|
+
properties={
|
348
|
+
"status": stackset["Status"],
|
349
|
+
"description": stackset.get("Description"),
|
350
|
+
"drift_status": stackset.get("DriftStatus"),
|
351
|
+
"last_drift_check_timestamp": stackset.get("LastDriftCheckTimestamp"),
|
352
|
+
"auto_deployment": stackset.get("AutoDeployment"),
|
353
|
+
"permission_model": stackset.get("PermissionModel"),
|
354
|
+
},
|
355
|
+
tags=self._get_cfn_stackset_tags(cfn_client, stackset["StackSetName"]),
|
356
|
+
created_date=datetime.utcnow(),
|
357
|
+
last_modified=datetime.utcnow(),
|
358
|
+
)
|
359
|
+
resources.append(resource)
|
360
|
+
|
361
|
+
except Exception as e:
|
362
|
+
logger.error(f"Error collecting CloudFormation stacksets: {e}")
|
363
|
+
|
364
|
+
return resources
|
365
|
+
|
366
|
+
def _get_cfn_stack_state(self, status: str) -> ResourceState:
|
367
|
+
"""Map CloudFormation stack status to ResourceState."""
|
368
|
+
if status.endswith("_COMPLETE"):
|
369
|
+
return ResourceState.AVAILABLE
|
370
|
+
elif status.endswith("_IN_PROGRESS"):
|
371
|
+
return ResourceState.PENDING
|
372
|
+
elif status.endswith("_FAILED"):
|
373
|
+
return ResourceState.ERROR
|
374
|
+
else:
|
375
|
+
return ResourceState.UNKNOWN
|
376
|
+
|
377
|
+
def _get_cfn_stackset_state(self, status: str) -> ResourceState:
|
378
|
+
"""Map CloudFormation stackset status to ResourceState."""
|
379
|
+
if status == "ACTIVE":
|
380
|
+
return ResourceState.AVAILABLE
|
381
|
+
else:
|
382
|
+
return ResourceState.UNKNOWN
|
383
|
+
|
384
|
+
def _get_cfn_stack_tags(self, cfn_client, stack_name: str) -> Dict[str, str]:
|
385
|
+
"""Get tags for a CloudFormation stack."""
|
386
|
+
try:
|
387
|
+
response = cfn_client.describe_stacks(StackName=stack_name)
|
388
|
+
stacks = response.get("Stacks", [])
|
389
|
+
if stacks:
|
390
|
+
return {tag["Key"]: tag["Value"] for tag in stacks[0].get("Tags", [])}
|
391
|
+
except Exception:
|
392
|
+
pass
|
393
|
+
return {}
|
394
|
+
|
395
|
+
def _get_cfn_stackset_tags(self, cfn_client, stackset_name: str) -> Dict[str, str]:
|
396
|
+
"""Get tags for a CloudFormation stackset."""
|
397
|
+
try:
|
398
|
+
response = cfn_client.describe_stack_set(StackSetName=stackset_name)
|
399
|
+
stackset = response.get("StackSet", {})
|
400
|
+
return {tag["Key"]: tag["Value"] for tag in stackset.get("Tags", [])}
|
401
|
+
except Exception:
|
402
|
+
pass
|
403
|
+
return {}
|
404
|
+
|
405
|
+
def _collect_servicecatalog_resources(
|
406
|
+
self, clients: Dict[str, Any], context: CollectionContext, resource_type: str
|
407
|
+
) -> List[AWSResource]:
|
408
|
+
"""Collect Service Catalog resources."""
|
409
|
+
# Placeholder for Service Catalog collection
|
410
|
+
return []
|
411
|
+
|
412
|
+
def _collect_config_resources(
|
413
|
+
self, clients: Dict[str, Any], context: CollectionContext, resource_type: str
|
414
|
+
) -> List[AWSResource]:
|
415
|
+
"""Collect AWS Config resources."""
|
416
|
+
# Placeholder for Config collection
|
417
|
+
return []
|
418
|
+
|
419
|
+
def _collect_ssm_resources(
|
420
|
+
self, clients: Dict[str, Any], context: CollectionContext, resource_type: str
|
421
|
+
) -> List[AWSResource]:
|
422
|
+
"""Collect Systems Manager resources."""
|
423
|
+
# Placeholder for SSM collection
|
424
|
+
return []
|
425
|
+
|
426
|
+
def _collect_cloudwatch_resources(
|
427
|
+
self, clients: Dict[str, Any], context: CollectionContext, resource_type: str
|
428
|
+
) -> List[AWSResource]:
|
429
|
+
"""Collect CloudWatch resources."""
|
430
|
+
# Placeholder for CloudWatch collection
|
431
|
+
return []
|
432
|
+
|
433
|
+
|
434
|
+
class OrganizationsManager:
|
435
|
+
"""
|
436
|
+
Organizational Unit (OU) management for AWS Organizations.
|
437
|
+
|
438
|
+
This class provides capabilities for setting up and managing
|
439
|
+
AWS Organizations structure following Cloud Foundations best practices.
|
440
|
+
Integrated from the previous organizations module.
|
441
|
+
"""
|
442
|
+
|
443
|
+
def __init__(self, profile: Optional[str] = None, region: Optional[str] = None):
|
444
|
+
"""Initialize OU manager."""
|
445
|
+
self.profile = profile
|
446
|
+
self.region = region or "us-east-1" # Organizations is global but requires a region
|
447
|
+
self._org_client = None
|
448
|
+
self._session = None
|
449
|
+
|
450
|
+
@property
|
451
|
+
def session(self):
|
452
|
+
"""Get AWS session."""
|
453
|
+
if self._session is None:
|
454
|
+
if self.profile:
|
455
|
+
self._session = boto3.Session(profile_name=self.profile)
|
456
|
+
else:
|
457
|
+
self._session = boto3.Session()
|
458
|
+
return self._session
|
459
|
+
|
460
|
+
@property
|
461
|
+
def org_client(self):
|
462
|
+
"""Get AWS Organizations client."""
|
463
|
+
if self._org_client is None:
|
464
|
+
self._org_client = self.session.client("organizations", region_name=self.region)
|
465
|
+
return self._org_client
|
466
|
+
|
467
|
+
def get_template_structure(self, template: str) -> Dict[str, Any]:
|
468
|
+
"""
|
469
|
+
Get predefined OU structure template.
|
470
|
+
|
471
|
+
Args:
|
472
|
+
template: Template name ('standard', 'security', 'custom')
|
473
|
+
|
474
|
+
Returns:
|
475
|
+
OU structure definition
|
476
|
+
"""
|
477
|
+
templates = {
|
478
|
+
"standard": {
|
479
|
+
"name": "Standard OU Structure",
|
480
|
+
"description": "Standard Cloud Foundations OU structure",
|
481
|
+
"organizational_units": [
|
482
|
+
{
|
483
|
+
"name": "Core",
|
484
|
+
"description": "Core organizational units for foundational services",
|
485
|
+
"children": [
|
486
|
+
{
|
487
|
+
"name": "Log Archive",
|
488
|
+
"description": "Centralized logging account",
|
489
|
+
"policies": ["LogArchivePolicy"],
|
490
|
+
},
|
491
|
+
{
|
492
|
+
"name": "Audit",
|
493
|
+
"description": "Security and compliance auditing",
|
494
|
+
"policies": ["AuditPolicy"],
|
495
|
+
},
|
496
|
+
{
|
497
|
+
"name": "Shared Services",
|
498
|
+
"description": "Shared infrastructure services",
|
499
|
+
"policies": ["SharedServicesPolicy"],
|
500
|
+
},
|
501
|
+
],
|
502
|
+
},
|
503
|
+
{
|
504
|
+
"name": "Production",
|
505
|
+
"description": "Production workload accounts",
|
506
|
+
"children": [
|
507
|
+
{
|
508
|
+
"name": "Prod-WebApps",
|
509
|
+
"description": "Production web applications",
|
510
|
+
"policies": ["ProductionPolicy"],
|
511
|
+
},
|
512
|
+
{
|
513
|
+
"name": "Prod-Data",
|
514
|
+
"description": "Production data services",
|
515
|
+
"policies": ["ProductionPolicy", "DataPolicy"],
|
516
|
+
},
|
517
|
+
],
|
518
|
+
},
|
519
|
+
{
|
520
|
+
"name": "Non-Production",
|
521
|
+
"description": "Development and testing accounts",
|
522
|
+
"children": [
|
523
|
+
{
|
524
|
+
"name": "Development",
|
525
|
+
"description": "Development environments",
|
526
|
+
"policies": ["DevelopmentPolicy"],
|
527
|
+
},
|
528
|
+
{
|
529
|
+
"name": "Testing",
|
530
|
+
"description": "Testing and staging environments",
|
531
|
+
"policies": ["TestingPolicy"],
|
532
|
+
},
|
533
|
+
],
|
534
|
+
},
|
535
|
+
],
|
536
|
+
},
|
537
|
+
"security": {
|
538
|
+
"name": "Security-Focused OU Structure",
|
539
|
+
"description": "Enhanced security OU structure with additional controls",
|
540
|
+
"organizational_units": [
|
541
|
+
{
|
542
|
+
"name": "Security",
|
543
|
+
"description": "Security and compliance organizational unit",
|
544
|
+
"children": [
|
545
|
+
{
|
546
|
+
"name": "Security-Prod",
|
547
|
+
"description": "Production security tools",
|
548
|
+
"policies": ["SecurityProdPolicy"],
|
549
|
+
},
|
550
|
+
{
|
551
|
+
"name": "Security-NonProd",
|
552
|
+
"description": "Non-production security tools",
|
553
|
+
"policies": ["SecurityNonProdPolicy"],
|
554
|
+
},
|
555
|
+
{
|
556
|
+
"name": "Log Archive",
|
557
|
+
"description": "Centralized security logging",
|
558
|
+
"policies": ["LogArchivePolicy", "SecurityLogPolicy"],
|
559
|
+
},
|
560
|
+
{
|
561
|
+
"name": "Audit",
|
562
|
+
"description": "Security auditing and compliance",
|
563
|
+
"policies": ["AuditPolicy", "CompliancePolicy"],
|
564
|
+
},
|
565
|
+
],
|
566
|
+
},
|
567
|
+
{
|
568
|
+
"name": "Workloads",
|
569
|
+
"description": "Application workload accounts",
|
570
|
+
"children": [
|
571
|
+
{
|
572
|
+
"name": "Prod-HighSecurity",
|
573
|
+
"description": "High security production workloads",
|
574
|
+
"policies": ["HighSecurityPolicy", "ProductionPolicy"],
|
575
|
+
},
|
576
|
+
{
|
577
|
+
"name": "Prod-Standard",
|
578
|
+
"description": "Standard production workloads",
|
579
|
+
"policies": ["StandardSecurityPolicy", "ProductionPolicy"],
|
580
|
+
},
|
581
|
+
{
|
582
|
+
"name": "NonProd",
|
583
|
+
"description": "Non-production workloads",
|
584
|
+
"policies": ["NonProdPolicy"],
|
585
|
+
},
|
586
|
+
],
|
587
|
+
},
|
588
|
+
],
|
589
|
+
},
|
590
|
+
}
|
591
|
+
|
592
|
+
if template not in templates:
|
593
|
+
raise ValueError(f"Unknown template: {template}. Available: {list(templates.keys())}")
|
594
|
+
|
595
|
+
logger.info(f"Using OU structure template: {template}")
|
596
|
+
return templates[template]
|
597
|
+
|
598
|
+
def load_structure_from_file(self, file_path: Union[str, Path]) -> Dict[str, Any]:
|
599
|
+
"""
|
600
|
+
Load OU structure from YAML file.
|
601
|
+
|
602
|
+
Args:
|
603
|
+
file_path: Path to YAML structure file
|
604
|
+
|
605
|
+
Returns:
|
606
|
+
OU structure definition
|
607
|
+
"""
|
608
|
+
config_path = Path(file_path)
|
609
|
+
|
610
|
+
if not config_path.exists():
|
611
|
+
raise FileNotFoundError(f"Structure file not found: {config_path}")
|
612
|
+
|
613
|
+
try:
|
614
|
+
with open(config_path, "r", encoding="utf-8") as f:
|
615
|
+
structure = yaml.safe_load(f)
|
616
|
+
|
617
|
+
logger.info(f"Loaded OU structure from: {config_path}")
|
618
|
+
return structure
|
619
|
+
|
620
|
+
except Exception as e:
|
621
|
+
logger.error(f"Failed to load structure file: {e}")
|
622
|
+
raise
|
623
|
+
|
624
|
+
def create_ou_structure(self, structure: Dict[str, Any]) -> Dict[str, Any]:
|
625
|
+
"""
|
626
|
+
Create OU structure in AWS Organizations.
|
627
|
+
|
628
|
+
Args:
|
629
|
+
structure: OU structure definition
|
630
|
+
|
631
|
+
Returns:
|
632
|
+
Creation results with OU IDs and status
|
633
|
+
"""
|
634
|
+
logger.info(f"Creating OU structure: {structure.get('name', 'Unnamed')}")
|
635
|
+
|
636
|
+
try:
|
637
|
+
# Get organization root
|
638
|
+
root_id = self._get_organization_root()
|
639
|
+
|
640
|
+
# Create OUs
|
641
|
+
results = {"structure_name": structure.get("name"), "root_id": root_id, "created_ous": [], "errors": []}
|
642
|
+
|
643
|
+
organizational_units = structure.get("organizational_units", [])
|
644
|
+
|
645
|
+
for ou_def in organizational_units:
|
646
|
+
try:
|
647
|
+
ou_result = self._create_ou_recursive(ou_def, root_id)
|
648
|
+
results["created_ous"].append(ou_result)
|
649
|
+
logger.info(f"Created OU: {ou_def['name']}")
|
650
|
+
|
651
|
+
except Exception as e:
|
652
|
+
error_msg = f"Failed to create OU {ou_def['name']}: {e}"
|
653
|
+
logger.error(error_msg)
|
654
|
+
results["errors"].append(error_msg)
|
655
|
+
|
656
|
+
logger.info(f"OU structure creation completed. Created {len(results['created_ous'])} OUs")
|
657
|
+
return results
|
658
|
+
|
659
|
+
except Exception as e:
|
660
|
+
logger.error(f"OU structure creation failed: {e}")
|
661
|
+
raise
|
662
|
+
|
663
|
+
def _get_organization_root(self) -> str:
|
664
|
+
"""Get the organization root ID."""
|
665
|
+
try:
|
666
|
+
response = self.org_client.list_roots()
|
667
|
+
|
668
|
+
if not response.get("Roots"):
|
669
|
+
raise Exception("No organization roots found")
|
670
|
+
|
671
|
+
root_id = response["Roots"][0]["Id"]
|
672
|
+
logger.debug(f"Found organization root: {root_id}")
|
673
|
+
return root_id
|
674
|
+
|
675
|
+
except Exception as e:
|
676
|
+
logger.error(f"Failed to get organization root: {e}")
|
677
|
+
raise
|
678
|
+
|
679
|
+
def _create_ou_recursive(self, ou_def: Dict[str, Any], parent_id: str) -> Dict[str, Any]:
|
680
|
+
"""
|
681
|
+
Recursively create OU and its children.
|
682
|
+
|
683
|
+
Args:
|
684
|
+
ou_def: OU definition
|
685
|
+
parent_id: Parent OU ID
|
686
|
+
|
687
|
+
Returns:
|
688
|
+
Creation result with OU details
|
689
|
+
"""
|
690
|
+
ou_name = ou_def["name"]
|
691
|
+
ou_description = ou_def.get("description", "")
|
692
|
+
|
693
|
+
logger.info(f"Creating OU: {ou_name} under parent: {parent_id}")
|
694
|
+
|
695
|
+
# Check if OU already exists
|
696
|
+
existing_ou = self._find_existing_ou(ou_name, parent_id)
|
697
|
+
if existing_ou:
|
698
|
+
logger.info(f"OU {ou_name} already exists: {existing_ou['Id']}")
|
699
|
+
ou_id = existing_ou["Id"]
|
700
|
+
else:
|
701
|
+
# Create the OU
|
702
|
+
response = self.org_client.create_organizational_unit(ParentId=parent_id, Name=ou_name)
|
703
|
+
ou_id = response["OrganizationalUnit"]["Id"]
|
704
|
+
logger.info(f"Created OU {ou_name}: {ou_id}")
|
705
|
+
|
706
|
+
result = {"name": ou_name, "id": ou_id, "parent_id": parent_id, "description": ou_description, "children": []}
|
707
|
+
|
708
|
+
# Create child OUs
|
709
|
+
children = ou_def.get("children", [])
|
710
|
+
for child_def in children:
|
711
|
+
try:
|
712
|
+
child_result = self._create_ou_recursive(child_def, ou_id)
|
713
|
+
result["children"].append(child_result)
|
714
|
+
except Exception as e:
|
715
|
+
logger.error(f"Failed to create child OU {child_def.get('name', 'Unknown')}: {e}")
|
716
|
+
|
717
|
+
return result
|
718
|
+
|
719
|
+
def _find_existing_ou(self, ou_name: str, parent_id: str) -> Optional[Dict[str, Any]]:
|
720
|
+
"""Find existing OU by name under a parent."""
|
721
|
+
try:
|
722
|
+
response = self.org_client.list_organizational_units_for_parent(ParentId=parent_id)
|
723
|
+
|
724
|
+
for ou in response.get("OrganizationalUnits", []):
|
725
|
+
if ou["Name"] == ou_name:
|
726
|
+
return ou
|
727
|
+
|
728
|
+
return None
|
729
|
+
|
730
|
+
except Exception as e:
|
731
|
+
logger.warning(f"Error checking for existing OU {ou_name}: {e}")
|
732
|
+
return None
|
733
|
+
|
734
|
+
def list_organizational_units(self) -> List[Dict[str, Any]]:
|
735
|
+
"""List all organizational units in the organization."""
|
736
|
+
try:
|
737
|
+
root_id = self._get_organization_root()
|
738
|
+
all_ous = []
|
739
|
+
|
740
|
+
def collect_ous(parent_id: str, level: int = 0):
|
741
|
+
response = self.org_client.list_organizational_units_for_parent(ParentId=parent_id)
|
742
|
+
|
743
|
+
for ou in response.get("OrganizationalUnits", []):
|
744
|
+
ou["Level"] = level
|
745
|
+
ou["ParentId"] = parent_id
|
746
|
+
all_ous.append(ou)
|
747
|
+
|
748
|
+
# Recursively collect child OUs
|
749
|
+
collect_ous(ou["Id"], level + 1)
|
750
|
+
|
751
|
+
collect_ous(root_id)
|
752
|
+
|
753
|
+
logger.info(f"Found {len(all_ous)} organizational units")
|
754
|
+
return all_ous
|
755
|
+
|
756
|
+
except Exception as e:
|
757
|
+
logger.error(f"Failed to list organizational units: {e}")
|
758
|
+
raise
|
759
|
+
|
760
|
+
def delete_ou(self, ou_id: str) -> bool:
|
761
|
+
"""
|
762
|
+
Delete an organizational unit.
|
763
|
+
|
764
|
+
Args:
|
765
|
+
ou_id: OU ID to delete
|
766
|
+
|
767
|
+
Returns:
|
768
|
+
True if successful
|
769
|
+
"""
|
770
|
+
try:
|
771
|
+
# Check if OU has any accounts
|
772
|
+
accounts_response = self.org_client.list_accounts_for_parent(ParentId=ou_id)
|
773
|
+
|
774
|
+
if accounts_response.get("Accounts"):
|
775
|
+
raise Exception(f"Cannot delete OU {ou_id}: it contains accounts")
|
776
|
+
|
777
|
+
# Check if OU has child OUs
|
778
|
+
ous_response = self.org_client.list_organizational_units_for_parent(ParentId=ou_id)
|
779
|
+
|
780
|
+
if ous_response.get("OrganizationalUnits"):
|
781
|
+
raise Exception(f"Cannot delete OU {ou_id}: it contains child OUs")
|
782
|
+
|
783
|
+
# Delete the OU
|
784
|
+
self.org_client.delete_organizational_unit(OrganizationalUnitId=ou_id)
|
785
|
+
|
786
|
+
logger.info(f"Deleted OU: {ou_id}")
|
787
|
+
return True
|
788
|
+
|
789
|
+
except Exception as e:
|
790
|
+
logger.error(f"Failed to delete OU {ou_id}: {e}")
|
791
|
+
raise
|