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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (132) hide show
  1. runbooks/__init__.py +87 -37
  2. runbooks/cfat/README.md +300 -49
  3. runbooks/cfat/__init__.py +2 -2
  4. runbooks/finops/__init__.py +1 -1
  5. runbooks/finops/cli.py +1 -1
  6. runbooks/inventory/collectors/__init__.py +8 -0
  7. runbooks/inventory/collectors/aws_management.py +791 -0
  8. runbooks/inventory/collectors/aws_networking.py +3 -3
  9. runbooks/main.py +3389 -782
  10. runbooks/operate/__init__.py +207 -0
  11. runbooks/operate/base.py +311 -0
  12. runbooks/operate/cloudformation_operations.py +619 -0
  13. runbooks/operate/cloudwatch_operations.py +496 -0
  14. runbooks/operate/dynamodb_operations.py +812 -0
  15. runbooks/operate/ec2_operations.py +926 -0
  16. runbooks/operate/iam_operations.py +569 -0
  17. runbooks/operate/s3_operations.py +1211 -0
  18. runbooks/operate/tagging_operations.py +655 -0
  19. runbooks/remediation/CLAUDE.md +100 -0
  20. runbooks/remediation/DOME9.md +218 -0
  21. runbooks/remediation/README.md +26 -0
  22. runbooks/remediation/Tests/__init__.py +0 -0
  23. runbooks/remediation/Tests/update_policy.py +74 -0
  24. runbooks/remediation/__init__.py +95 -0
  25. runbooks/remediation/acm_cert_expired_unused.py +98 -0
  26. runbooks/remediation/acm_remediation.py +875 -0
  27. runbooks/remediation/api_gateway_list.py +167 -0
  28. runbooks/remediation/base.py +643 -0
  29. runbooks/remediation/cloudtrail_remediation.py +908 -0
  30. runbooks/remediation/cloudtrail_s3_modifications.py +296 -0
  31. runbooks/remediation/cognito_active_users.py +78 -0
  32. runbooks/remediation/cognito_remediation.py +856 -0
  33. runbooks/remediation/cognito_user_password_reset.py +163 -0
  34. runbooks/remediation/commons.py +455 -0
  35. runbooks/remediation/dynamodb_optimize.py +155 -0
  36. runbooks/remediation/dynamodb_remediation.py +744 -0
  37. runbooks/remediation/dynamodb_server_side_encryption.py +108 -0
  38. runbooks/remediation/ec2_public_ips.py +134 -0
  39. runbooks/remediation/ec2_remediation.py +892 -0
  40. runbooks/remediation/ec2_subnet_disable_auto_ip_assignment.py +72 -0
  41. runbooks/remediation/ec2_unattached_ebs_volumes.py +448 -0
  42. runbooks/remediation/ec2_unused_security_groups.py +202 -0
  43. runbooks/remediation/kms_enable_key_rotation.py +651 -0
  44. runbooks/remediation/kms_remediation.py +717 -0
  45. runbooks/remediation/lambda_list.py +243 -0
  46. runbooks/remediation/lambda_remediation.py +971 -0
  47. runbooks/remediation/multi_account.py +569 -0
  48. runbooks/remediation/rds_instance_list.py +199 -0
  49. runbooks/remediation/rds_remediation.py +873 -0
  50. runbooks/remediation/rds_snapshot_list.py +192 -0
  51. runbooks/remediation/requirements.txt +118 -0
  52. runbooks/remediation/s3_block_public_access.py +159 -0
  53. runbooks/remediation/s3_bucket_public_access.py +143 -0
  54. runbooks/remediation/s3_disable_static_website_hosting.py +74 -0
  55. runbooks/remediation/s3_downloader.py +215 -0
  56. runbooks/remediation/s3_enable_access_logging.py +562 -0
  57. runbooks/remediation/s3_encryption.py +526 -0
  58. runbooks/remediation/s3_force_ssl_secure_policy.py +143 -0
  59. runbooks/remediation/s3_list.py +141 -0
  60. runbooks/remediation/s3_object_search.py +201 -0
  61. runbooks/remediation/s3_remediation.py +816 -0
  62. runbooks/remediation/scan_for_phrase.py +425 -0
  63. runbooks/remediation/workspaces_list.py +220 -0
  64. runbooks/security/__init__.py +9 -10
  65. runbooks/security/security_baseline_tester.py +4 -2
  66. runbooks-0.7.6.dist-info/METADATA +608 -0
  67. {runbooks-0.7.0.dist-info → runbooks-0.7.6.dist-info}/RECORD +84 -76
  68. {runbooks-0.7.0.dist-info → runbooks-0.7.6.dist-info}/entry_points.txt +0 -1
  69. {runbooks-0.7.0.dist-info → runbooks-0.7.6.dist-info}/top_level.txt +0 -1
  70. jupyter-agent/.env +0 -2
  71. jupyter-agent/.env.template +0 -2
  72. jupyter-agent/.gitattributes +0 -35
  73. jupyter-agent/.gradio/certificate.pem +0 -31
  74. jupyter-agent/README.md +0 -16
  75. jupyter-agent/__main__.log +0 -8
  76. jupyter-agent/app.py +0 -256
  77. jupyter-agent/cloudops-agent.png +0 -0
  78. jupyter-agent/ds-system-prompt.txt +0 -154
  79. jupyter-agent/jupyter-agent.png +0 -0
  80. jupyter-agent/llama3_template.jinja +0 -123
  81. jupyter-agent/requirements.txt +0 -9
  82. jupyter-agent/tmp/4ojbs8a02ir/jupyter-agent.ipynb +0 -68
  83. jupyter-agent/tmp/cm5iasgpm3p/jupyter-agent.ipynb +0 -91
  84. jupyter-agent/tmp/crqbsseag5/jupyter-agent.ipynb +0 -91
  85. jupyter-agent/tmp/hohanq1u097/jupyter-agent.ipynb +0 -57
  86. jupyter-agent/tmp/jns1sam29wm/jupyter-agent.ipynb +0 -53
  87. jupyter-agent/tmp/jupyter-agent.ipynb +0 -27
  88. jupyter-agent/utils.py +0 -409
  89. runbooks/aws/__init__.py +0 -58
  90. runbooks/aws/dynamodb_operations.py +0 -231
  91. runbooks/aws/ec2_copy_image_cross-region.py +0 -195
  92. runbooks/aws/ec2_describe_instances.py +0 -202
  93. runbooks/aws/ec2_ebs_snapshots_delete.py +0 -186
  94. runbooks/aws/ec2_run_instances.py +0 -213
  95. runbooks/aws/ec2_start_stop_instances.py +0 -212
  96. runbooks/aws/ec2_terminate_instances.py +0 -143
  97. runbooks/aws/ec2_unused_eips.py +0 -196
  98. runbooks/aws/ec2_unused_volumes.py +0 -188
  99. runbooks/aws/s3_create_bucket.py +0 -142
  100. runbooks/aws/s3_list_buckets.py +0 -152
  101. runbooks/aws/s3_list_objects.py +0 -156
  102. runbooks/aws/s3_object_operations.py +0 -183
  103. runbooks/aws/tagging_lambda_handler.py +0 -183
  104. runbooks/inventory/FAILED_SCRIPTS_TROUBLESHOOTING.md +0 -619
  105. runbooks/inventory/PASSED_SCRIPTS_GUIDE.md +0 -738
  106. runbooks/inventory/aws_organization.png +0 -0
  107. runbooks/inventory/cfn_move_stack_instances.py +0 -1526
  108. runbooks/inventory/delete_s3_buckets_objects.py +0 -169
  109. runbooks/inventory/lockdown_cfn_stackset_role.py +0 -224
  110. runbooks/inventory/update_aws_actions.py +0 -173
  111. runbooks/inventory/update_cfn_stacksets.py +0 -1215
  112. runbooks/inventory/update_cloudwatch_logs_retention_policy.py +0 -294
  113. runbooks/inventory/update_iam_roles_cross_accounts.py +0 -478
  114. runbooks/inventory/update_s3_public_access_block.py +0 -539
  115. runbooks/organizations/__init__.py +0 -12
  116. runbooks/organizations/manager.py +0 -374
  117. runbooks-0.7.0.dist-info/METADATA +0 -375
  118. /runbooks/inventory/{tests → Tests}/common_test_data.py +0 -0
  119. /runbooks/inventory/{tests → Tests}/common_test_functions.py +0 -0
  120. /runbooks/inventory/{tests → Tests}/script_test_data.py +0 -0
  121. /runbooks/inventory/{tests → Tests}/setup.py +0 -0
  122. /runbooks/inventory/{tests → Tests}/src.py +0 -0
  123. /runbooks/inventory/{tests/test_inventory_modules.py → Tests/test_Inventory_Modules.py} +0 -0
  124. /runbooks/inventory/{tests → Tests}/test_cfn_describe_stacks.py +0 -0
  125. /runbooks/inventory/{tests → Tests}/test_ec2_describe_instances.py +0 -0
  126. /runbooks/inventory/{tests → Tests}/test_lambda_list_functions.py +0 -0
  127. /runbooks/inventory/{tests → Tests}/test_moto_integration_example.py +0 -0
  128. /runbooks/inventory/{tests → Tests}/test_org_list_accounts.py +0 -0
  129. /runbooks/inventory/{Inventory_Modules.py → inventory_modules.py} +0 -0
  130. /runbooks/{aws → operate}/tags.json +0 -0
  131. {runbooks-0.7.0.dist-info → runbooks-0.7.6.dist-info}/WHEEL +0 -0
  132. {runbooks-0.7.0.dist-info → runbooks-0.7.6.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,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