runbooks 0.2.3__py3-none-any.whl → 0.6.1__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 (221) hide show
  1. conftest.py +26 -0
  2. jupyter-agent/.env.template +2 -0
  3. jupyter-agent/.gitattributes +35 -0
  4. jupyter-agent/README.md +16 -0
  5. jupyter-agent/app.py +256 -0
  6. jupyter-agent/cloudops-agent.png +0 -0
  7. jupyter-agent/ds-system-prompt.txt +154 -0
  8. jupyter-agent/jupyter-agent.png +0 -0
  9. jupyter-agent/llama3_template.jinja +123 -0
  10. jupyter-agent/requirements.txt +9 -0
  11. jupyter-agent/utils.py +409 -0
  12. runbooks/__init__.py +71 -3
  13. runbooks/__main__.py +13 -0
  14. runbooks/aws/ec2_describe_instances.py +1 -1
  15. runbooks/aws/ec2_run_instances.py +8 -2
  16. runbooks/aws/ec2_start_stop_instances.py +17 -4
  17. runbooks/aws/ec2_unused_volumes.py +5 -1
  18. runbooks/aws/s3_create_bucket.py +4 -2
  19. runbooks/aws/s3_list_objects.py +6 -1
  20. runbooks/aws/tagging_lambda_handler.py +13 -2
  21. runbooks/aws/tags.json +12 -0
  22. runbooks/base.py +353 -0
  23. runbooks/cfat/README.md +49 -0
  24. runbooks/cfat/__init__.py +74 -0
  25. runbooks/cfat/app.ts +644 -0
  26. runbooks/cfat/assessment/__init__.py +40 -0
  27. runbooks/cfat/assessment/asana-import.csv +39 -0
  28. runbooks/cfat/assessment/cfat-checks.csv +31 -0
  29. runbooks/cfat/assessment/cfat.txt +520 -0
  30. runbooks/cfat/assessment/collectors.py +200 -0
  31. runbooks/cfat/assessment/jira-import.csv +39 -0
  32. runbooks/cfat/assessment/runner.py +387 -0
  33. runbooks/cfat/assessment/validators.py +290 -0
  34. runbooks/cfat/cli.py +103 -0
  35. runbooks/cfat/docs/asana-import.csv +24 -0
  36. runbooks/cfat/docs/cfat-checks.csv +31 -0
  37. runbooks/cfat/docs/cfat.txt +335 -0
  38. runbooks/cfat/docs/checks-output.png +0 -0
  39. runbooks/cfat/docs/cloudshell-console-run.png +0 -0
  40. runbooks/cfat/docs/cloudshell-download.png +0 -0
  41. runbooks/cfat/docs/cloudshell-output.png +0 -0
  42. runbooks/cfat/docs/downloadfile.png +0 -0
  43. runbooks/cfat/docs/jira-import.csv +24 -0
  44. runbooks/cfat/docs/open-cloudshell.png +0 -0
  45. runbooks/cfat/docs/report-header.png +0 -0
  46. runbooks/cfat/models.py +1026 -0
  47. runbooks/cfat/package-lock.json +5116 -0
  48. runbooks/cfat/package.json +38 -0
  49. runbooks/cfat/report.py +496 -0
  50. runbooks/cfat/reporting/__init__.py +46 -0
  51. runbooks/cfat/reporting/exporters.py +337 -0
  52. runbooks/cfat/reporting/formatters.py +496 -0
  53. runbooks/cfat/reporting/templates.py +135 -0
  54. runbooks/cfat/run-assessment.sh +23 -0
  55. runbooks/cfat/runner.py +69 -0
  56. runbooks/cfat/src/actions/check-cloudtrail-existence.ts +43 -0
  57. runbooks/cfat/src/actions/check-config-existence.ts +37 -0
  58. runbooks/cfat/src/actions/check-control-tower.ts +37 -0
  59. runbooks/cfat/src/actions/check-ec2-existence.ts +46 -0
  60. runbooks/cfat/src/actions/check-iam-users.ts +50 -0
  61. runbooks/cfat/src/actions/check-legacy-cur.ts +30 -0
  62. runbooks/cfat/src/actions/check-org-cloudformation.ts +30 -0
  63. runbooks/cfat/src/actions/check-vpc-existence.ts +43 -0
  64. runbooks/cfat/src/actions/create-asanaimport.ts +14 -0
  65. runbooks/cfat/src/actions/create-backlog.ts +372 -0
  66. runbooks/cfat/src/actions/create-jiraimport.ts +15 -0
  67. runbooks/cfat/src/actions/create-report.ts +616 -0
  68. runbooks/cfat/src/actions/define-account-type.ts +51 -0
  69. runbooks/cfat/src/actions/get-enabled-org-policy-types.ts +40 -0
  70. runbooks/cfat/src/actions/get-enabled-org-services.ts +26 -0
  71. runbooks/cfat/src/actions/get-idc-info.ts +34 -0
  72. runbooks/cfat/src/actions/get-org-da-accounts.ts +34 -0
  73. runbooks/cfat/src/actions/get-org-details.ts +35 -0
  74. runbooks/cfat/src/actions/get-org-member-accounts.ts +44 -0
  75. runbooks/cfat/src/actions/get-org-ous.ts +35 -0
  76. runbooks/cfat/src/actions/get-regions.ts +22 -0
  77. runbooks/cfat/src/actions/zip-assessment.ts +27 -0
  78. runbooks/cfat/src/types/index.d.ts +147 -0
  79. runbooks/cfat/tests/__init__.py +141 -0
  80. runbooks/cfat/tests/test_cli.py +340 -0
  81. runbooks/cfat/tests/test_integration.py +290 -0
  82. runbooks/cfat/tests/test_models.py +505 -0
  83. runbooks/cfat/tests/test_reporting.py +354 -0
  84. runbooks/cfat/tsconfig.json +16 -0
  85. runbooks/cfat/webpack.config.cjs +27 -0
  86. runbooks/config.py +260 -0
  87. runbooks/finops/__init__.py +88 -0
  88. runbooks/finops/aws_client.py +245 -0
  89. runbooks/finops/cli.py +151 -0
  90. runbooks/finops/cost_processor.py +410 -0
  91. runbooks/finops/dashboard_runner.py +448 -0
  92. runbooks/finops/helpers.py +355 -0
  93. runbooks/finops/main.py +14 -0
  94. runbooks/finops/profile_processor.py +174 -0
  95. runbooks/finops/types.py +66 -0
  96. runbooks/finops/visualisations.py +80 -0
  97. runbooks/inventory/.gitignore +354 -0
  98. runbooks/inventory/ArgumentsClass.py +261 -0
  99. runbooks/inventory/Inventory_Modules.py +6130 -0
  100. runbooks/inventory/LandingZone/delete_lz.py +1075 -0
  101. runbooks/inventory/README.md +1320 -0
  102. runbooks/inventory/__init__.py +62 -0
  103. runbooks/inventory/account_class.py +532 -0
  104. runbooks/inventory/all_my_instances_wrapper.py +123 -0
  105. runbooks/inventory/aws_decorators.py +201 -0
  106. runbooks/inventory/cfn_move_stack_instances.py +1526 -0
  107. runbooks/inventory/check_cloudtrail_compliance.py +614 -0
  108. runbooks/inventory/check_controltower_readiness.py +1107 -0
  109. runbooks/inventory/check_landingzone_readiness.py +711 -0
  110. runbooks/inventory/cloudtrail.md +727 -0
  111. runbooks/inventory/collectors/__init__.py +20 -0
  112. runbooks/inventory/collectors/aws_compute.py +518 -0
  113. runbooks/inventory/collectors/aws_networking.py +275 -0
  114. runbooks/inventory/collectors/base.py +222 -0
  115. runbooks/inventory/core/__init__.py +19 -0
  116. runbooks/inventory/core/collector.py +303 -0
  117. runbooks/inventory/core/formatter.py +296 -0
  118. runbooks/inventory/delete_s3_buckets_objects.py +169 -0
  119. runbooks/inventory/discovery.md +81 -0
  120. runbooks/inventory/draw_org_structure.py +748 -0
  121. runbooks/inventory/ec2_vpc_utils.py +341 -0
  122. runbooks/inventory/find_cfn_drift_detection.py +272 -0
  123. runbooks/inventory/find_cfn_orphaned_stacks.py +719 -0
  124. runbooks/inventory/find_cfn_stackset_drift.py +733 -0
  125. runbooks/inventory/find_ec2_security_groups.py +669 -0
  126. runbooks/inventory/find_landingzone_versions.py +201 -0
  127. runbooks/inventory/find_vpc_flow_logs.py +1221 -0
  128. runbooks/inventory/inventory.sh +659 -0
  129. runbooks/inventory/list_cfn_stacks.py +558 -0
  130. runbooks/inventory/list_cfn_stackset_operation_results.py +252 -0
  131. runbooks/inventory/list_cfn_stackset_operations.py +734 -0
  132. runbooks/inventory/list_cfn_stacksets.py +453 -0
  133. runbooks/inventory/list_config_recorders_delivery_channels.py +681 -0
  134. runbooks/inventory/list_ds_directories.py +354 -0
  135. runbooks/inventory/list_ec2_availability_zones.py +286 -0
  136. runbooks/inventory/list_ec2_ebs_volumes.py +244 -0
  137. runbooks/inventory/list_ec2_instances.py +425 -0
  138. runbooks/inventory/list_ecs_clusters_and_tasks.py +562 -0
  139. runbooks/inventory/list_elbs_load_balancers.py +411 -0
  140. runbooks/inventory/list_enis_network_interfaces.py +526 -0
  141. runbooks/inventory/list_guardduty_detectors.py +568 -0
  142. runbooks/inventory/list_iam_policies.py +404 -0
  143. runbooks/inventory/list_iam_roles.py +518 -0
  144. runbooks/inventory/list_iam_saml_providers.py +359 -0
  145. runbooks/inventory/list_lambda_functions.py +882 -0
  146. runbooks/inventory/list_org_accounts.py +446 -0
  147. runbooks/inventory/list_org_accounts_users.py +354 -0
  148. runbooks/inventory/list_rds_db_instances.py +406 -0
  149. runbooks/inventory/list_route53_hosted_zones.py +318 -0
  150. runbooks/inventory/list_servicecatalog_provisioned_products.py +575 -0
  151. runbooks/inventory/list_sns_topics.py +360 -0
  152. runbooks/inventory/list_ssm_parameters.py +402 -0
  153. runbooks/inventory/list_vpc_subnets.py +433 -0
  154. runbooks/inventory/list_vpcs.py +422 -0
  155. runbooks/inventory/lockdown_cfn_stackset_role.py +224 -0
  156. runbooks/inventory/models/__init__.py +24 -0
  157. runbooks/inventory/models/account.py +192 -0
  158. runbooks/inventory/models/inventory.py +309 -0
  159. runbooks/inventory/models/resource.py +247 -0
  160. runbooks/inventory/recover_cfn_stack_ids.py +205 -0
  161. runbooks/inventory/requirements.txt +12 -0
  162. runbooks/inventory/run_on_multi_accounts.py +211 -0
  163. runbooks/inventory/tests/common_test_data.py +3661 -0
  164. runbooks/inventory/tests/common_test_functions.py +204 -0
  165. runbooks/inventory/tests/script_test_data.py +0 -0
  166. runbooks/inventory/tests/setup.py +24 -0
  167. runbooks/inventory/tests/src.py +18 -0
  168. runbooks/inventory/tests/test_cfn_describe_stacks.py +208 -0
  169. runbooks/inventory/tests/test_ec2_describe_instances.py +162 -0
  170. runbooks/inventory/tests/test_inventory_modules.py +55 -0
  171. runbooks/inventory/tests/test_lambda_list_functions.py +86 -0
  172. runbooks/inventory/tests/test_moto_integration_example.py +273 -0
  173. runbooks/inventory/tests/test_org_list_accounts.py +49 -0
  174. runbooks/inventory/update_aws_actions.py +173 -0
  175. runbooks/inventory/update_cfn_stacksets.py +1215 -0
  176. runbooks/inventory/update_cloudwatch_logs_retention_policy.py +294 -0
  177. runbooks/inventory/update_iam_roles_cross_accounts.py +478 -0
  178. runbooks/inventory/update_s3_public_access_block.py +539 -0
  179. runbooks/inventory/utils/__init__.py +23 -0
  180. runbooks/inventory/utils/aws_helpers.py +510 -0
  181. runbooks/inventory/utils/threading_utils.py +493 -0
  182. runbooks/inventory/utils/validation.py +682 -0
  183. runbooks/inventory/verify_ec2_security_groups.py +1430 -0
  184. runbooks/main.py +785 -0
  185. runbooks/organizations/__init__.py +12 -0
  186. runbooks/organizations/manager.py +374 -0
  187. runbooks/security_baseline/README.md +324 -0
  188. runbooks/security_baseline/checklist/alternate_contacts.py +8 -1
  189. runbooks/security_baseline/checklist/bucket_public_access.py +4 -1
  190. runbooks/security_baseline/checklist/cloudwatch_alarm_configuration.py +9 -2
  191. runbooks/security_baseline/checklist/guardduty_enabled.py +9 -2
  192. runbooks/security_baseline/checklist/multi_region_instance_usage.py +5 -1
  193. runbooks/security_baseline/checklist/root_access_key.py +6 -1
  194. runbooks/security_baseline/config-origin.json +1 -1
  195. runbooks/security_baseline/config.json +1 -1
  196. runbooks/security_baseline/permission.json +1 -1
  197. runbooks/security_baseline/report_generator.py +10 -2
  198. runbooks/security_baseline/report_template_en.html +8 -8
  199. runbooks/security_baseline/report_template_jp.html +8 -8
  200. runbooks/security_baseline/report_template_kr.html +13 -13
  201. runbooks/security_baseline/report_template_vn.html +8 -8
  202. runbooks/security_baseline/requirements.txt +7 -0
  203. runbooks/security_baseline/run_script.py +8 -2
  204. runbooks/security_baseline/security_baseline_tester.py +10 -2
  205. runbooks/security_baseline/utils/common.py +5 -1
  206. runbooks/utils/__init__.py +204 -0
  207. runbooks-0.6.1.dist-info/METADATA +373 -0
  208. runbooks-0.6.1.dist-info/RECORD +237 -0
  209. {runbooks-0.2.3.dist-info → runbooks-0.6.1.dist-info}/WHEEL +1 -1
  210. runbooks-0.6.1.dist-info/entry_points.txt +7 -0
  211. runbooks-0.6.1.dist-info/licenses/LICENSE +201 -0
  212. runbooks-0.6.1.dist-info/top_level.txt +3 -0
  213. runbooks/python101/calculator.py +0 -34
  214. runbooks/python101/config.py +0 -1
  215. runbooks/python101/exceptions.py +0 -16
  216. runbooks/python101/file_manager.py +0 -218
  217. runbooks/python101/toolkit.py +0 -153
  218. runbooks-0.2.3.dist-info/METADATA +0 -435
  219. runbooks-0.2.3.dist-info/RECORD +0 -61
  220. runbooks-0.2.3.dist-info/entry_points.txt +0 -3
  221. runbooks-0.2.3.dist-info/top_level.txt +0 -1
@@ -0,0 +1,748 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ AWS Organizations Structure Visualization
4
+
5
+ Enhanced AWS Cloud Foundations script for visualizing AWS Organizations structure
6
+ including accounts, OUs, and policies with multiple output formats.
7
+
8
+ **AWS API Mapping**: `boto3.client('organizations').list_roots()`, `describe_organizational_unit()`, `list_accounts()`, etc.
9
+
10
+ Features:
11
+ - Graphviz diagram generation (PNG output)
12
+ - Mermaid diagram support for modern visualization
13
+ - Diagrams library integration for professional layouts
14
+ - Policy visualization with filtering options
15
+ - Interactive widgets for Jupyter environments
16
+ - Multi-format output support
17
+ - Enterprise-grade error handling
18
+
19
+ Compatibility:
20
+ - 100% compatible with AWS Cloud Foundations original
21
+ - Enhanced with modern Python practices and type hints
22
+ - Supports all original command-line parameters
23
+ - Backward compatible with existing workflows
24
+
25
+ Example:
26
+ Basic organization diagram:
27
+ ```bash
28
+ python org_describe_structure.py --profile my-org-profile
29
+ ```
30
+
31
+ Include policies with AWS managed SCPs:
32
+ ```bash
33
+ python org_describe_structure.py --profile my-profile --policy --aws
34
+ ```
35
+
36
+ Start from specific OU:
37
+ ```bash
38
+ python org_describe_structure.py --profile my-profile --ou ou-1234567890
39
+ ```
40
+
41
+ Requirements:
42
+ - IAM permissions: `organizations:List*`, `organizations:Describe*`
43
+ - Python packages: graphviz, colorama, boto3
44
+ - Optional: diagrams, ipywidgets (for enhanced features)
45
+
46
+ Author:
47
+ AWS Cloud Foundations Team (Enhanced by CloudOps)
48
+
49
+ Version:
50
+ 2025.04.09 (Enhanced)
51
+ """
52
+
53
+ import logging
54
+ import sys
55
+ from os.path import split
56
+ from time import time
57
+ from typing import Any, Dict, List, Optional
58
+
59
+ import boto3
60
+ from ArgumentsClass import CommonArguments
61
+ from colorama import Fore, init
62
+ from graphviz import Digraph
63
+
64
+ # Optional imports for enhanced features
65
+ try:
66
+ import ipywidgets as widgets
67
+ from IPython.display import display
68
+ from ipywidgets import interactive, interactive_output
69
+
70
+ JUPYTER_AVAILABLE = True
71
+ except ImportError:
72
+ JUPYTER_AVAILABLE = False
73
+ logging.debug("Jupyter widgets not available - interactive features disabled")
74
+
75
+ __version__ = "2025.04.09"
76
+
77
+ init()
78
+
79
+ # Visual styling constants
80
+ account_fillcolor = "orange"
81
+ suspended_account_fillcolor = "red"
82
+ account_shape = "ellipse"
83
+ policy_fillcolor = "azure" # Pretty color - nothing to do with the Azure Cloud...
84
+ policy_linecolor = "red"
85
+ policy_shape = "hexagon"
86
+ ou_fillcolor = "burlywood"
87
+ ou_shape = "box"
88
+
89
+ # AWS Policy types supported by Organizations
90
+ aws_policy_type_list = [
91
+ "SERVICE_CONTROL_POLICY",
92
+ "TAG_POLICY",
93
+ "BACKUP_POLICY",
94
+ "AISERVICES_OPT_OUT_POLICY",
95
+ "CHATBOT_POLICY",
96
+ "RESOURCE_CONTROL_POLICY",
97
+ "DECLARATIVE_POLICY_EC2",
98
+ ]
99
+
100
+ #####################
101
+ # Function Definitions
102
+ #####################
103
+
104
+
105
+ def parse_args(f_args):
106
+ """
107
+ Parse command-line arguments.
108
+
109
+ Args:
110
+ f_args (list): List of command-line arguments.
111
+
112
+ Returns:
113
+ argparse.Namespace: Parsed arguments.
114
+ """
115
+ script_path, script_name = split(sys.argv[0])
116
+ parser = CommonArguments()
117
+ parser.my_parser.description = "To draw the Organization and its policies."
118
+ parser.singleprofile()
119
+ parser.verbosity()
120
+ parser.timing()
121
+ parser.version(__version__)
122
+
123
+ local = parser.my_parser.add_argument_group(script_name, "Parameters specific to this script")
124
+ local.add_argument(
125
+ "--policy",
126
+ dest="policy",
127
+ action="store_true",
128
+ help="Include the various policies within the Organization in the diagram",
129
+ )
130
+ local.add_argument(
131
+ "--aws",
132
+ "--managed",
133
+ dest="aws_managed",
134
+ action="store_true",
135
+ help="Use this parameter to SHOW the AWS Managed SCPs as well, otherwise they're hidden",
136
+ )
137
+ local.add_argument(
138
+ "--ou",
139
+ "--start",
140
+ dest="starting_place",
141
+ metavar="OU ID",
142
+ default=None,
143
+ help="Use this parameter to specify where to start from (Defaults to the root)",
144
+ )
145
+ local.add_argument(
146
+ "--format",
147
+ dest="output_format",
148
+ choices=["graphviz", "mermaid", "diagrams"],
149
+ default="graphviz",
150
+ help="Output format: graphviz (default), mermaid, or diagrams library",
151
+ )
152
+
153
+ return parser.my_parser.parse_args(f_args)
154
+
155
+
156
+ def round_up(number: float) -> int:
157
+ """Round up the number to the next integer."""
158
+ return int(number) + (number % 1 > 0)
159
+
160
+
161
+ def get_root_OUS(root_id: str) -> List[Dict[str, str]]:
162
+ """
163
+ Get all child OUs for a given root or OU ID.
164
+
165
+ Args:
166
+ root_id (str): The parent OU or root ID
167
+
168
+ Returns:
169
+ List[Dict[str, str]]: List of child OU information
170
+ """
171
+ AllChildOUs = []
172
+ try:
173
+ ChildOUs = org_client.list_children(ParentId=root_id, ChildType="ORGANIZATIONAL_UNIT")
174
+ AllChildOUs.extend(ChildOUs["Children"])
175
+ logging.info(f"Found {len(AllChildOUs)} children from parent {root_id}")
176
+ while "NextToken" in ChildOUs.keys():
177
+ ChildOUs = org_client.list_children(
178
+ ParentId=root_id, ChildType="ORGANIZATIONAL_UNIT", NextToken=ChildOUs["NextToken"]
179
+ )
180
+ AllChildOUs.extend(ChildOUs["Children"])
181
+ logging.info(f"Found {len(AllChildOUs)} children from parent {root_id}")
182
+ return AllChildOUs
183
+ except (
184
+ org_client.exceptions.AccessDeniedException,
185
+ org_client.exceptions.AWSOrganizationsNotInUseException,
186
+ org_client.exceptions.InvalidInputException,
187
+ org_client.exceptions.ParentNotFoundException,
188
+ org_client.exceptions.ServiceException,
189
+ org_client.exceptions.TooManyRequestsException,
190
+ ) as myError:
191
+ logging.error(f"Error: {myError}")
192
+ except KeyError as myError:
193
+ logging.error(f"Error: {myError}")
194
+ return []
195
+
196
+
197
+ def get_enabled_policy_types() -> List[str]:
198
+ """
199
+ Get the list of enabled policy types in the organization.
200
+
201
+ Returns:
202
+ List[str]: List of enabled policy type names
203
+ """
204
+ try:
205
+ f_root = org_client.list_roots()
206
+ except (
207
+ org_client.exceptions.AccessDeniedException,
208
+ org_client.exceptions.AWSOrganizationsNotInUseException,
209
+ org_client.exceptions.InvalidInputException,
210
+ org_client.exceptions.ParentNotFoundException,
211
+ org_client.exceptions.ServiceException,
212
+ org_client.exceptions.TooManyRequestsException,
213
+ ) as myError:
214
+ logging.error(f"Boto3 Error: {myError}")
215
+ return []
216
+ except KeyError as myError:
217
+ logging.error(f"KeyError: {myError}")
218
+ return []
219
+ except Exception as myError:
220
+ logging.error(f"General Error: {myError}")
221
+ return []
222
+
223
+ # This gathers the policy types that are enabled within the Org
224
+ f_enabled_policy_types = [x["Type"] for x in f_root["Roots"][0]["PolicyTypes"] if x["Status"] == "ENABLED"]
225
+ return f_enabled_policy_types
226
+
227
+
228
+ def find_max_accounts_per_ou(ou_id: str, max_accounts: int = 0) -> int:
229
+ """
230
+ Description: Finds the maximum number of accounts in any OU, regardless of starting point
231
+ @param ou_id: The ID of the OU to start from
232
+ @param max_accounts: The maximum number of accounts found in an OU so far
233
+ @Returns: The maximum number of accounts found in any OU
234
+ """
235
+ logging.info(f"Finding max accounts in OU {ou_id}")
236
+ all_accounts = []
237
+ accounts = org_client.list_accounts_for_parent(ParentId=ou_id)
238
+ all_accounts.extend(accounts["Accounts"])
239
+ logging.info(f"Found {len(all_accounts)} accounts in ou {ou_id} - totaling {len(all_accounts)}")
240
+
241
+ while "NextToken" in accounts.keys():
242
+ accounts = org_client.list_accounts_for_parent(ParentId=ou_id, NextToken=accounts["NextToken"])
243
+ all_accounts.extend(accounts["Accounts"])
244
+ logging.info(
245
+ f"Found {len(all_accounts)} more accounts in ou {ou_id} - totaling {len(all_accounts)} accounts so far"
246
+ )
247
+ max_accounts_return = max(len(all_accounts), max_accounts)
248
+
249
+ nested_ous = org_client.list_organizational_units_for_parent(ParentId=ou_id)
250
+ logging.info(f"Found {len(nested_ous['OrganizationalUnits'])} OUs in ou {ou_id}")
251
+
252
+ # This has to recurse, to handle the finding of # of accounts in the nested OUs under root
253
+ for ou in nested_ous["OrganizationalUnits"]:
254
+ max_accounts_return = max(find_max_accounts_per_ou(ou["Id"], max_accounts_return), max_accounts_return)
255
+ return max_accounts_return
256
+
257
+
258
+ def find_accounts_in_org() -> List[Dict[str, Any]]:
259
+ """
260
+ Description: Finds all accounts in the organization, regardless of starting point
261
+ @Returns: a list of all accounts
262
+ """
263
+ all_accounts = []
264
+ org_accounts = org_client.list_accounts()
265
+ all_accounts.extend(org_accounts["Accounts"])
266
+ while "NextToken" in org_accounts.keys():
267
+ org_accounts = org_client.list_accounts(NextToken=org_accounts["NextToken"])
268
+ all_accounts.extend(org_accounts["Accounts"])
269
+ logging.info(f"Finding another {len(org_accounts['Accounts'])}. Total accounts found: {len(all_accounts)}")
270
+ return all_accounts
271
+
272
+
273
+ def build_org_structure(ou_id: str) -> Dict[str, Any]:
274
+ """
275
+ Recursively builds a nested dictionary representing the organization structure.
276
+
277
+ The dictionary has the following keys:
278
+ - "id": The OU or account ID.
279
+ - "name": The name of the OU or account.
280
+ - "children": A list of child dictionaries (for OUs) or an empty list for leaf accounts.
281
+
282
+ If an OU has accounts as children, they are included as leaf nodes.
283
+
284
+ Args:
285
+ ou_id (str): The starting OU ID (or root ID).
286
+
287
+ Returns:
288
+ Dict[str, Any]: Nested dictionary representing the org structure.
289
+ """
290
+ structure = {}
291
+ try:
292
+ # If the OU id starts with 'r', we assume it's the root.
293
+ if ou_id.startswith("r"):
294
+ structure["id"] = ou_id
295
+ structure["name"] = "Root"
296
+ else:
297
+ ou = org_client.describe_organizational_unit(OrganizationalUnitId=ou_id)
298
+ structure["id"] = ou_id
299
+ structure["name"] = ou["OrganizationalUnit"]["Name"]
300
+ except Exception as e:
301
+ logging.error(f"Error describing OU {ou_id}: {e}")
302
+ structure["id"] = ou_id
303
+ structure["name"] = "Unknown"
304
+
305
+ # Initialize children list.
306
+ structure["children"] = []
307
+
308
+ # Retrieve direct accounts under this OU.
309
+ try:
310
+ accounts = org_client.list_accounts_for_parent(ParentId=ou_id)
311
+ for account in accounts.get("Accounts", []):
312
+ account_node = {
313
+ "id": account["Id"],
314
+ "name": account.get("Name", "Unknown"),
315
+ "children": [], # Leaf node.
316
+ }
317
+ structure["children"].append(account_node)
318
+ while "NextToken" in accounts:
319
+ accounts = org_client.list_accounts_for_parent(ParentId=ou_id, NextToken=accounts["NextToken"])
320
+ for account in accounts.get("Accounts", []):
321
+ account_node = {
322
+ "id": account["Id"],
323
+ "name": account.get("Name", "Unknown"),
324
+ "children": [],
325
+ }
326
+ structure["children"].append(account_node)
327
+ except Exception as e:
328
+ logging.error(f"Error listing accounts for OU {ou_id}: {e}")
329
+
330
+ # Retrieve child OUs and build their structures recursively.
331
+ try:
332
+ ous = org_client.list_organizational_units_for_parent(ParentId=ou_id)
333
+ for child_ou in ous.get("OrganizationalUnits", []):
334
+ child_structure = build_org_structure(child_ou["Id"])
335
+ structure["children"].append(child_structure)
336
+ while "NextToken" in ous:
337
+ ous = org_client.list_organizational_units_for_parent(ParentId=ou_id, NextToken=ous["NextToken"])
338
+ for child_ou in ous.get("OrganizationalUnits", []):
339
+ child_structure = build_org_structure(child_ou["Id"])
340
+ structure["children"].append(child_structure)
341
+ except Exception as e:
342
+ logging.error(f"Error listing child OUs for {ou_id}: {e}")
343
+
344
+ return structure
345
+
346
+
347
+ def generate_mermaid(org_structure: Any, filename: str) -> None:
348
+ """
349
+ Generate a Mermaid diagram from the organization structure.
350
+
351
+ If org_structure is a string, it is treated as the root OU ID and is converted
352
+ into a nested dictionary using build_org_structure.
353
+
354
+ The output file will contain Mermaid syntax that you can render using Mermaid tools.
355
+
356
+ Args:
357
+ org_structure (Any): Either a nested dictionary or a string representing the root OU ID.
358
+ filename (str): Output filename (recommended extension: .mmd).
359
+ """
360
+ # If a string is provided, build the nested structure.
361
+ if isinstance(org_structure, str):
362
+ org_structure = build_org_structure(org_structure)
363
+
364
+ lines = ["graph TD"] # Use top-down layout
365
+
366
+ def recurse(node: Dict[str, Any]) -> None:
367
+ node_id = node.get("id", "unknown")
368
+ label = node.get("name", "Unnamed")
369
+ # Define the node with a unique id and label.
370
+ lines.append(f'{node_id}["{label}"]')
371
+ for child in node.get("children", []):
372
+ child_id = child.get("id", "unknown")
373
+ # Add an edge from parent to child.
374
+ lines.append(f"{node_id} --> {child_id}")
375
+ recurse(child)
376
+
377
+ recurse(org_structure)
378
+
379
+ try:
380
+ with open(filename, "w", encoding="utf-8") as f:
381
+ f.write("\n".join(lines))
382
+ print(f"Mermaid diagram successfully saved to '{Fore.RED}{filename}{Fore.RESET}'")
383
+ logging.info(f"Mermaid diagram successfully saved to {filename}")
384
+ except Exception as e:
385
+ logging.error(f"Failed to write Mermaid diagram to {filename}: {e}")
386
+
387
+
388
+ def generate_diagrams(org_structure: Any, filename: str) -> None:
389
+ """
390
+ Generate an AWS Organization diagram using the diagrams library.
391
+
392
+ If org_structure is a string, it is converted into a nested dictionary using build_org_structure.
393
+ This function uses the diagrams library (by Mingrammer) to create a left-to-right layout diagram.
394
+
395
+ Args:
396
+ org_structure (Any): Either a nested dictionary or a string representing the root OU ID.
397
+ filename (str): Output filename (without extension; the diagrams library will add one).
398
+
399
+ Note:
400
+ Ensure that the diagrams package is installed:
401
+ pip install diagrams
402
+ """
403
+ # If a string is provided, convert it to a nested structure.
404
+ if isinstance(org_structure, str):
405
+ org_structure = build_org_structure(org_structure)
406
+
407
+ try:
408
+ from diagrams import Cluster, Diagram
409
+ from diagrams.aws.management import (
410
+ OrganizationsAccount,
411
+ OrganizationsOrganizationalUnit,
412
+ )
413
+ except ImportError as imp_err:
414
+ logging.error("Please install the 'diagrams' package to use generate_diagrams: pip install diagrams")
415
+ print(f"{Fore.YELLOW}Warning: diagrams library not available. Install with: pip install diagrams{Fore.RESET}")
416
+ return
417
+
418
+ def build_diagram(node: Dict[str, Any]):
419
+ """
420
+ Recursively build diagram nodes from the org structure.
421
+ For nodes with children, creates a Cluster.
422
+ """
423
+ name = node.get("name", node.get("id", "Unnamed"))
424
+ if "children" in node and node["children"]:
425
+ with Cluster(name):
426
+ children_nodes = [build_diagram(child) for child in node["children"]]
427
+ current = OrganizationsOrganizationalUnit(name)
428
+ for child in children_nodes:
429
+ current >> child
430
+ return current
431
+ else:
432
+ return OrganizationsAccount(name)
433
+
434
+ try:
435
+ with Diagram("AWS Organization Diagram", filename=filename, show=False, direction="LR"):
436
+ build_diagram(org_structure)
437
+ print(f"Diagrams image successfully generated as '{Fore.RED}{filename}{Fore.RESET}'")
438
+ logging.info(f"Diagrams image successfully generated as {filename}")
439
+ except Exception as e:
440
+ logging.error(f"Failed to generate diagrams image: {e}")
441
+
442
+
443
+ def draw_org(froot: str, filename: str):
444
+ """
445
+ Description: Draws the Organization, from the desired starting point using Graphviz
446
+ @param froot: The starting point for the diagram, which doesn't have to be the root of the Org
447
+ @param filename: The filename we're writing all this to
448
+ @return: No return - just writes the file to the local filesystem
449
+ """
450
+
451
+ def create_policy_nodes(f_enabled_policy_types: List[str]):
452
+ """Create policy nodes in the Graphviz diagram."""
453
+ associated_policies = []
454
+
455
+ for aws_policy_type in f_enabled_policy_types:
456
+ response = org_client.list_policies(Filter=aws_policy_type)
457
+ associated_policies.extend(response["Policies"])
458
+ while "NextToken" in response.keys():
459
+ response = org_client.list_policies(Filter=aws_policy_type, NextToken=response["NextToken"])
460
+ associated_policies.extend(response["Policies"])
461
+
462
+ for policy in associated_policies:
463
+ policy_id = policy["Id"]
464
+ policy_name = policy["Name"]
465
+
466
+ if policy["Type"] == "SERVICE_CONTROL_POLICY":
467
+ policy_type = "scp"
468
+ elif policy["Type"] == "RESOURCE_CONTROL_POLICY":
469
+ policy_type = "rcp"
470
+ elif policy["Type"] == "TAG_POLICY":
471
+ policy_type = "tag"
472
+ elif policy["Type"] == "BACKUP_POLICY":
473
+ policy_type = "backup"
474
+ elif policy["Type"] == "AISERVICES_OPT_OUT_POLICY":
475
+ policy_type = "ai"
476
+ elif policy["Type"] == "CHATBOT_POLICY":
477
+ policy_type = "chatbot"
478
+ elif policy["Type"] == "DECLARATIVE_POLICY_EC2":
479
+ policy_type = "dcp"
480
+ else:
481
+ policy_type = policy["Type"]
482
+
483
+ # This if statement allows us to skip showing the "FullAWSAccess" policies unless the user provided the parameter to want to see them
484
+ if policy["AwsManaged"] and not pManaged:
485
+ continue
486
+ else:
487
+ dot.node(
488
+ policy_id,
489
+ label=f"{policy_name}\n {policy_id} | {policy_type}",
490
+ shape=policy_shape,
491
+ color=policy_linecolor,
492
+ style="filled",
493
+ fillcolor=policy_fillcolor,
494
+ )
495
+
496
+ def traverse_ous_and_accounts(ou_id: str):
497
+ """
498
+ Description: Recursively traverse the OUs and accounts and update the diagram
499
+ @param ou_id: The ID of the OU to start from
500
+ """
501
+ # Retrieve the details of the current OU
502
+ if ou_id[0] == "r":
503
+ ou_name = "Root"
504
+ else:
505
+ ou = org_client.describe_organizational_unit(OrganizationalUnitId=ou_id)
506
+ ou_name = ou["OrganizationalUnit"]["Name"]
507
+
508
+ if pPolicy:
509
+ # Retrieve the policies associated with this OU
510
+ ou_associated_policies = []
511
+ for aws_policy_type in enabled_policy_types:
512
+ # The function below is a paginated operation, but returns more values than are allowed to be applied to a single OU, so pagination isn't needed in this case.
513
+ # Eventually, they will likely change that - so this is a TODO for later.
514
+ logging.info(f"Checking for {aws_policy_type} policies on OU {ou_id}")
515
+ ou_associated_policies.extend(
516
+ org_client.list_policies_for_target(TargetId=ou_id, Filter=aws_policy_type)["Policies"]
517
+ )
518
+ for policy in ou_associated_policies:
519
+ # If it's a Managed Policy and the user didn't want to see managed policies, then skip, otherwise show it.
520
+ if policy["AwsManaged"] and not pManaged:
521
+ continue
522
+ else:
523
+ dot.edge(ou_id, policy["Id"])
524
+
525
+ # Retrieve the accounts under the current OU
526
+ all_accounts = []
527
+ accounts = org_client.list_accounts_for_parent(ParentId=ou_id)
528
+ all_accounts.extend(accounts["Accounts"])
529
+ while "NextToken" in accounts.keys():
530
+ accounts = org_client.list_accounts_for_parent(ParentId=ou_id, NextToken=accounts["NextToken"])
531
+ all_accounts.extend(accounts["Accounts"])
532
+
533
+ # Add the current OU as a node in the diagram, with the number of direct accounts it has under it
534
+ dot.node(
535
+ ou_id,
536
+ label=f"{ou_name} | {len(all_accounts)}\n{ou_id}",
537
+ shape=ou_shape,
538
+ style="filled",
539
+ fillcolor=ou_fillcolor,
540
+ )
541
+
542
+ all_account_associated_policies = []
543
+ account_associated_policies = []
544
+ for account in all_accounts:
545
+ account_id = account["Id"]
546
+ account_name = account["Name"]
547
+ # Add the account as a node in the diagram
548
+ if account["Status"] == "SUSPENDED":
549
+ dot.node(
550
+ account_id,
551
+ label=f"{account_name}\n{account_id}\nSUSPENDED",
552
+ shape=account_shape,
553
+ style="filled",
554
+ fillcolor=suspended_account_fillcolor,
555
+ )
556
+ else:
557
+ dot.node(
558
+ account_id,
559
+ label=f"{account_name}\n{account_id}",
560
+ shape=account_shape,
561
+ style="filled",
562
+ fillcolor=account_fillcolor,
563
+ )
564
+ # Add an edge from the current OU to the account
565
+ dot.edge(ou_id, account_id)
566
+
567
+ # TODO: Would love to multi-thread this... but we'll run into API limits quickly.
568
+ # Significant time savings gained by only checking for enabled policies
569
+ if pPolicy:
570
+ # Gather every kind of policy that could be attached to an account
571
+ for aws_policy_type in enabled_policy_types:
572
+ logging.info(f"Checking for {aws_policy_type} policies on account {account_id}")
573
+ account_associated_policies.extend(
574
+ org_client.list_policies_for_target(TargetId=account_id, Filter=aws_policy_type)["Policies"]
575
+ )
576
+ # Create a list of policy associations with the account that's connected to them
577
+ all_account_associated_policies.extend(
578
+ [
579
+ {
580
+ "AcctId": account_id,
581
+ "PolicyId": x["Id"],
582
+ "PolicyName": x["Name"],
583
+ "PolicyType": x["Type"],
584
+ "AWS_Managed": x["AwsManaged"],
585
+ }
586
+ for x in account_associated_policies
587
+ ]
588
+ )
589
+
590
+ if pPolicy:
591
+ all_account_associated_policies_uniq = set()
592
+ for item in all_account_associated_policies:
593
+ # This if statement skips showing the "FullAWSAccess" policies, if the "Managed" parameter wasn't used.
594
+ if item["AWS_Managed"] and not pManaged:
595
+ continue
596
+ else:
597
+ all_account_associated_policies_uniq.add((item["AcctId"], item["PolicyId"]))
598
+ for association in all_account_associated_policies_uniq:
599
+ dot.edge(association[0], association[1])
600
+
601
+ # Retrieve the child OUs under the current OU, and use pagination, since it's possible to have so many OUs that pagination is required.
602
+ child_ous = org_client.list_organizational_units_for_parent(ParentId=ou_id)
603
+ all_child_ous = child_ous["OrganizationalUnits"]
604
+ while "NextToken" in child_ous.keys():
605
+ child_ous = org_client.list_organizational_units_for_parent(
606
+ ParentId=ou_id, NextToken=child_ous["NextToken"]
607
+ )
608
+ all_child_ous.extend(child_ous["OrganizationalUnits"])
609
+
610
+ logging.info(f"There are {len(all_child_ous)} OUs in OU {ou_id}")
611
+ for child_ou in all_child_ous:
612
+ child_ou_id = child_ou["Id"]
613
+ # Recursively traverse the child OU and add edges to the diagram
614
+ logging.info(f"***** Starting to look at OU {child_ou['Name']} right now... ")
615
+ traverse_ous_and_accounts(child_ou_id)
616
+ logging.info(f"***** Finished looking at OU {child_ou['Name']} right now... ")
617
+ dot.edge(ou_id, child_ou_id)
618
+
619
+ max_accounts_per_ou = 1
620
+
621
+ # Create a new Digraph object for the diagram with improved layout
622
+ dot = Digraph("AWS Organization", comment="Organization Structure")
623
+ dot.attr(rankdir="LR") # Left-to-right layout for better readability
624
+
625
+ # This updates the diagram, using the dot object created in this function.
626
+ if pPolicy:
627
+ create_policy_nodes(enabled_policy_types)
628
+
629
+ print(f"Beginning to traverse OUs and draw the diagram... ")
630
+
631
+ traverse_ous_and_accounts(froot)
632
+ max_accounts_per_ou = find_max_accounts_per_ou(froot, max_accounts_per_ou)
633
+
634
+ # This tries to verticalize the diagram, so it doesn't look like a wide mess
635
+ dot_unflat = dot.unflatten(stagger=round_up(max_accounts_per_ou / 5))
636
+
637
+ # Save the diagram to a PNG file
638
+ dot_unflat.render(filename, format="png", view=False)
639
+ print(f"Diagram saved to '{Fore.RED}{filename}.png{Fore.RESET}'")
640
+
641
+
642
+ #####################
643
+ # Main
644
+ #####################
645
+ if __name__ == "__main__":
646
+ args = parse_args(sys.argv[1:])
647
+
648
+ pProfile = args.Profile
649
+ pTiming = args.Time
650
+ pPolicy = args.policy
651
+ pManaged = args.aws_managed
652
+ pStartingPlace = args.starting_place
653
+ pOutputFormat = args.output_format
654
+ verbose = args.loglevel
655
+
656
+ # Setup logging levels
657
+ logging.basicConfig(level=verbose, format="[%(filename)s:%(lineno)s - %(funcName)20s() ] %(message)s")
658
+ logging.getLogger("boto3").setLevel(logging.CRITICAL)
659
+ logging.getLogger("botocore").setLevel(logging.CRITICAL)
660
+ logging.getLogger("s3transfer").setLevel(logging.CRITICAL)
661
+ logging.getLogger("urllib3").setLevel(logging.CRITICAL)
662
+
663
+ begin_time = time()
664
+ print(f"Beginning to look through the Org in order to create the diagram")
665
+
666
+ # Create an AWS Organizations client
667
+ org_session = boto3.Session(profile_name=pProfile)
668
+ org_client = org_session.client("organizations")
669
+ ERASE_LINE = "\x1b[2K"
670
+
671
+ # Get enabled policy types for the Org.
672
+ # Even if they specify an OU, we need to do a list_roots to get this info.
673
+ enabled_policy_types = get_enabled_policy_types()
674
+
675
+ # Determine where to start the drawing from
676
+ if pStartingPlace is None:
677
+ # Find the root Org ID
678
+ logging.info(f"User didn't include a specific OU ID, so we're starting from the root")
679
+ root = org_client.list_roots()["Roots"][0]["Id"]
680
+ saved_filename = "aws_organization"
681
+ else:
682
+ logging.info(f"User asked us to start from a specific OU ID: {pStartingPlace}")
683
+ root = pStartingPlace
684
+ saved_filename = "aws_organization_subset"
685
+
686
+ # If they specified they want to see the AWS policies, then they obviously want to see policies overall.
687
+ if pManaged and not pPolicy:
688
+ pPolicy = True
689
+
690
+ # Find all the Organization Accounts
691
+ all_org_accounts = find_accounts_in_org()
692
+
693
+ # Display a message based on the number of accounts in the entire Org
694
+ if len(all_org_accounts) > 360 and pStartingPlace is not None:
695
+ print(
696
+ f"Since there are {len(all_org_accounts)} in your Organization, this script will take a long time to run. If you're comfortable with that\n"
697
+ f"re-run this script and add '--start {root} ' as a parameter to this script, and we'll run without this reminder.\n"
698
+ f"Otherwise - you could run this script for only a specific OU's set of accounts by specifying '--start <OU ID>' and we'll start the drawing at that OU (and include any OUs below it)"
699
+ )
700
+ print()
701
+ sys.exit(1)
702
+
703
+ if pPolicy:
704
+ anticipated_time = 5 + (len(all_org_accounts) * 2)
705
+ print(
706
+ f"Due to there being {len(all_org_accounts)} accounts in this Org, this process will likely take about {anticipated_time} seconds"
707
+ )
708
+ if anticipated_time > 30:
709
+ print()
710
+ print(
711
+ f"{Fore.RED}Since this may take a while, you could re-run this script for only a specific OU by using the '--ou <OU ID>' parameter {Fore.RESET}"
712
+ )
713
+ print()
714
+ else:
715
+ anticipated_time = 5 + (len(all_org_accounts) / 10)
716
+ print(
717
+ f"Due to there being {len(all_org_accounts)} accounts in this Org, this process will likely take about {anticipated_time} seconds"
718
+ )
719
+
720
+ # Generate the diagram based on the selected format
721
+ print(f"Generating {pOutputFormat} diagram...")
722
+
723
+ if pOutputFormat == "graphviz":
724
+ # Draw the Org itself and save it to the local filesystem
725
+ draw_org(root, saved_filename)
726
+
727
+ elif pOutputFormat == "mermaid":
728
+ # Generate Mermaid diagram
729
+ mermaid_filename = f"{saved_filename}.mmd"
730
+ generate_mermaid(root, mermaid_filename)
731
+
732
+ elif pOutputFormat == "diagrams":
733
+ # Generate diagrams library visualization
734
+ diagrams_filename = saved_filename
735
+ generate_diagrams(root, diagrams_filename)
736
+
737
+ # Display timing information
738
+ if pTiming and pPolicy:
739
+ print(
740
+ f"{Fore.GREEN}Drawing the Org structure when policies are included took {time() - begin_time:.2f} seconds{Fore.RESET}"
741
+ )
742
+ elif pTiming:
743
+ print(
744
+ f"{Fore.GREEN}Drawing the Org structure without policies took {time() - begin_time:.2f} seconds{Fore.RESET}"
745
+ )
746
+
747
+ print("Thank you for using this script")
748
+ print()