runbooks 0.2.5__py3-none-any.whl → 0.7.0__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.
- conftest.py +26 -0
- jupyter-agent/.env +2 -0
- jupyter-agent/.env.template +2 -0
- jupyter-agent/.gitattributes +35 -0
- jupyter-agent/.gradio/certificate.pem +31 -0
- jupyter-agent/README.md +16 -0
- jupyter-agent/__main__.log +8 -0
- jupyter-agent/app.py +256 -0
- jupyter-agent/cloudops-agent.png +0 -0
- jupyter-agent/ds-system-prompt.txt +154 -0
- jupyter-agent/jupyter-agent.png +0 -0
- jupyter-agent/llama3_template.jinja +123 -0
- jupyter-agent/requirements.txt +9 -0
- jupyter-agent/tmp/4ojbs8a02ir/jupyter-agent.ipynb +68 -0
- jupyter-agent/tmp/cm5iasgpm3p/jupyter-agent.ipynb +91 -0
- jupyter-agent/tmp/crqbsseag5/jupyter-agent.ipynb +91 -0
- jupyter-agent/tmp/hohanq1u097/jupyter-agent.ipynb +57 -0
- jupyter-agent/tmp/jns1sam29wm/jupyter-agent.ipynb +53 -0
- jupyter-agent/tmp/jupyter-agent.ipynb +27 -0
- jupyter-agent/utils.py +409 -0
- runbooks/__init__.py +71 -3
- runbooks/__main__.py +13 -0
- runbooks/aws/ec2_describe_instances.py +1 -1
- runbooks/aws/ec2_run_instances.py +8 -2
- runbooks/aws/ec2_start_stop_instances.py +17 -4
- runbooks/aws/ec2_unused_volumes.py +5 -1
- runbooks/aws/s3_create_bucket.py +4 -2
- runbooks/aws/s3_list_objects.py +6 -1
- runbooks/aws/tagging_lambda_handler.py +13 -2
- runbooks/aws/tags.json +12 -0
- runbooks/base.py +353 -0
- runbooks/cfat/README.md +49 -0
- runbooks/cfat/__init__.py +74 -0
- runbooks/cfat/app.ts +644 -0
- runbooks/cfat/assessment/__init__.py +40 -0
- runbooks/cfat/assessment/asana-import.csv +39 -0
- runbooks/cfat/assessment/cfat-checks.csv +31 -0
- runbooks/cfat/assessment/cfat.txt +520 -0
- runbooks/cfat/assessment/collectors.py +200 -0
- runbooks/cfat/assessment/jira-import.csv +39 -0
- runbooks/cfat/assessment/runner.py +387 -0
- runbooks/cfat/assessment/validators.py +290 -0
- runbooks/cfat/cli.py +103 -0
- runbooks/cfat/docs/asana-import.csv +24 -0
- runbooks/cfat/docs/cfat-checks.csv +31 -0
- runbooks/cfat/docs/cfat.txt +335 -0
- runbooks/cfat/docs/checks-output.png +0 -0
- runbooks/cfat/docs/cloudshell-console-run.png +0 -0
- runbooks/cfat/docs/cloudshell-download.png +0 -0
- runbooks/cfat/docs/cloudshell-output.png +0 -0
- runbooks/cfat/docs/downloadfile.png +0 -0
- runbooks/cfat/docs/jira-import.csv +24 -0
- runbooks/cfat/docs/open-cloudshell.png +0 -0
- runbooks/cfat/docs/report-header.png +0 -0
- runbooks/cfat/models.py +1026 -0
- runbooks/cfat/package-lock.json +5116 -0
- runbooks/cfat/package.json +38 -0
- runbooks/cfat/report.py +496 -0
- runbooks/cfat/reporting/__init__.py +46 -0
- runbooks/cfat/reporting/exporters.py +337 -0
- runbooks/cfat/reporting/formatters.py +496 -0
- runbooks/cfat/reporting/templates.py +135 -0
- runbooks/cfat/run-assessment.sh +23 -0
- runbooks/cfat/runner.py +69 -0
- runbooks/cfat/src/actions/check-cloudtrail-existence.ts +43 -0
- runbooks/cfat/src/actions/check-config-existence.ts +37 -0
- runbooks/cfat/src/actions/check-control-tower.ts +37 -0
- runbooks/cfat/src/actions/check-ec2-existence.ts +46 -0
- runbooks/cfat/src/actions/check-iam-users.ts +50 -0
- runbooks/cfat/src/actions/check-legacy-cur.ts +30 -0
- runbooks/cfat/src/actions/check-org-cloudformation.ts +30 -0
- runbooks/cfat/src/actions/check-vpc-existence.ts +43 -0
- runbooks/cfat/src/actions/create-asanaimport.ts +14 -0
- runbooks/cfat/src/actions/create-backlog.ts +372 -0
- runbooks/cfat/src/actions/create-jiraimport.ts +15 -0
- runbooks/cfat/src/actions/create-report.ts +616 -0
- runbooks/cfat/src/actions/define-account-type.ts +51 -0
- runbooks/cfat/src/actions/get-enabled-org-policy-types.ts +40 -0
- runbooks/cfat/src/actions/get-enabled-org-services.ts +26 -0
- runbooks/cfat/src/actions/get-idc-info.ts +34 -0
- runbooks/cfat/src/actions/get-org-da-accounts.ts +34 -0
- runbooks/cfat/src/actions/get-org-details.ts +35 -0
- runbooks/cfat/src/actions/get-org-member-accounts.ts +44 -0
- runbooks/cfat/src/actions/get-org-ous.ts +35 -0
- runbooks/cfat/src/actions/get-regions.ts +22 -0
- runbooks/cfat/src/actions/zip-assessment.ts +27 -0
- runbooks/cfat/src/types/index.d.ts +147 -0
- runbooks/cfat/tests/__init__.py +141 -0
- runbooks/cfat/tests/test_cli.py +340 -0
- runbooks/cfat/tests/test_integration.py +290 -0
- runbooks/cfat/tests/test_models.py +505 -0
- runbooks/cfat/tests/test_reporting.py +354 -0
- runbooks/cfat/tsconfig.json +16 -0
- runbooks/cfat/webpack.config.cjs +27 -0
- runbooks/config.py +260 -0
- runbooks/finops/README.md +337 -0
- runbooks/finops/__init__.py +86 -0
- runbooks/finops/aws_client.py +245 -0
- runbooks/finops/cli.py +151 -0
- runbooks/finops/cost_processor.py +410 -0
- runbooks/finops/dashboard_runner.py +448 -0
- runbooks/finops/helpers.py +355 -0
- runbooks/finops/main.py +14 -0
- runbooks/finops/profile_processor.py +174 -0
- runbooks/finops/types.py +66 -0
- runbooks/finops/visualisations.py +80 -0
- runbooks/inventory/.gitignore +354 -0
- runbooks/inventory/ArgumentsClass.py +261 -0
- runbooks/inventory/FAILED_SCRIPTS_TROUBLESHOOTING.md +619 -0
- runbooks/inventory/Inventory_Modules.py +6130 -0
- runbooks/inventory/LandingZone/delete_lz.py +1075 -0
- runbooks/inventory/PASSED_SCRIPTS_GUIDE.md +738 -0
- runbooks/inventory/README.md +1320 -0
- runbooks/inventory/__init__.py +62 -0
- runbooks/inventory/account_class.py +532 -0
- runbooks/inventory/all_my_instances_wrapper.py +123 -0
- runbooks/inventory/aws_decorators.py +201 -0
- runbooks/inventory/aws_organization.png +0 -0
- runbooks/inventory/cfn_move_stack_instances.py +1526 -0
- runbooks/inventory/check_cloudtrail_compliance.py +614 -0
- runbooks/inventory/check_controltower_readiness.py +1107 -0
- runbooks/inventory/check_landingzone_readiness.py +711 -0
- runbooks/inventory/cloudtrail.md +727 -0
- runbooks/inventory/collectors/__init__.py +20 -0
- runbooks/inventory/collectors/aws_compute.py +518 -0
- runbooks/inventory/collectors/aws_networking.py +275 -0
- runbooks/inventory/collectors/base.py +222 -0
- runbooks/inventory/core/__init__.py +19 -0
- runbooks/inventory/core/collector.py +303 -0
- runbooks/inventory/core/formatter.py +296 -0
- runbooks/inventory/delete_s3_buckets_objects.py +169 -0
- runbooks/inventory/discovery.md +81 -0
- runbooks/inventory/draw_org_structure.py +748 -0
- runbooks/inventory/ec2_vpc_utils.py +341 -0
- runbooks/inventory/find_cfn_drift_detection.py +272 -0
- runbooks/inventory/find_cfn_orphaned_stacks.py +719 -0
- runbooks/inventory/find_cfn_stackset_drift.py +733 -0
- runbooks/inventory/find_ec2_security_groups.py +669 -0
- runbooks/inventory/find_landingzone_versions.py +201 -0
- runbooks/inventory/find_vpc_flow_logs.py +1221 -0
- runbooks/inventory/inventory.sh +659 -0
- runbooks/inventory/list_cfn_stacks.py +558 -0
- runbooks/inventory/list_cfn_stackset_operation_results.py +252 -0
- runbooks/inventory/list_cfn_stackset_operations.py +734 -0
- runbooks/inventory/list_cfn_stacksets.py +453 -0
- runbooks/inventory/list_config_recorders_delivery_channels.py +681 -0
- runbooks/inventory/list_ds_directories.py +354 -0
- runbooks/inventory/list_ec2_availability_zones.py +286 -0
- runbooks/inventory/list_ec2_ebs_volumes.py +244 -0
- runbooks/inventory/list_ec2_instances.py +425 -0
- runbooks/inventory/list_ecs_clusters_and_tasks.py +562 -0
- runbooks/inventory/list_elbs_load_balancers.py +411 -0
- runbooks/inventory/list_enis_network_interfaces.py +526 -0
- runbooks/inventory/list_guardduty_detectors.py +568 -0
- runbooks/inventory/list_iam_policies.py +404 -0
- runbooks/inventory/list_iam_roles.py +518 -0
- runbooks/inventory/list_iam_saml_providers.py +359 -0
- runbooks/inventory/list_lambda_functions.py +882 -0
- runbooks/inventory/list_org_accounts.py +446 -0
- runbooks/inventory/list_org_accounts_users.py +354 -0
- runbooks/inventory/list_rds_db_instances.py +406 -0
- runbooks/inventory/list_route53_hosted_zones.py +318 -0
- runbooks/inventory/list_servicecatalog_provisioned_products.py +575 -0
- runbooks/inventory/list_sns_topics.py +360 -0
- runbooks/inventory/list_ssm_parameters.py +402 -0
- runbooks/inventory/list_vpc_subnets.py +433 -0
- runbooks/inventory/list_vpcs.py +422 -0
- runbooks/inventory/lockdown_cfn_stackset_role.py +224 -0
- runbooks/inventory/models/__init__.py +24 -0
- runbooks/inventory/models/account.py +192 -0
- runbooks/inventory/models/inventory.py +309 -0
- runbooks/inventory/models/resource.py +247 -0
- runbooks/inventory/recover_cfn_stack_ids.py +205 -0
- runbooks/inventory/requirements.txt +12 -0
- runbooks/inventory/run_on_multi_accounts.py +211 -0
- runbooks/inventory/tests/common_test_data.py +3661 -0
- runbooks/inventory/tests/common_test_functions.py +204 -0
- runbooks/inventory/tests/setup.py +24 -0
- runbooks/inventory/tests/src.py +18 -0
- runbooks/inventory/tests/test_cfn_describe_stacks.py +208 -0
- runbooks/inventory/tests/test_ec2_describe_instances.py +162 -0
- runbooks/inventory/tests/test_inventory_modules.py +55 -0
- runbooks/inventory/tests/test_lambda_list_functions.py +86 -0
- runbooks/inventory/tests/test_moto_integration_example.py +273 -0
- runbooks/inventory/tests/test_org_list_accounts.py +49 -0
- runbooks/inventory/update_aws_actions.py +173 -0
- runbooks/inventory/update_cfn_stacksets.py +1215 -0
- runbooks/inventory/update_cloudwatch_logs_retention_policy.py +294 -0
- runbooks/inventory/update_iam_roles_cross_accounts.py +478 -0
- runbooks/inventory/update_s3_public_access_block.py +539 -0
- runbooks/inventory/utils/__init__.py +23 -0
- runbooks/inventory/utils/aws_helpers.py +510 -0
- runbooks/inventory/utils/threading_utils.py +493 -0
- runbooks/inventory/utils/validation.py +682 -0
- runbooks/inventory/verify_ec2_security_groups.py +1430 -0
- runbooks/main.py +1004 -0
- runbooks/organizations/__init__.py +12 -0
- runbooks/organizations/manager.py +374 -0
- runbooks/security/README.md +447 -0
- runbooks/security/__init__.py +71 -0
- runbooks/{security_baseline → security}/checklist/alternate_contacts.py +8 -1
- runbooks/{security_baseline → security}/checklist/bucket_public_access.py +4 -1
- runbooks/{security_baseline → security}/checklist/cloudwatch_alarm_configuration.py +9 -2
- runbooks/{security_baseline → security}/checklist/guardduty_enabled.py +9 -2
- runbooks/{security_baseline → security}/checklist/multi_region_instance_usage.py +5 -1
- runbooks/{security_baseline → security}/checklist/root_access_key.py +6 -1
- runbooks/{security_baseline → security}/config-origin.json +1 -1
- runbooks/{security_baseline → security}/config.json +1 -1
- runbooks/{security_baseline → security}/permission.json +1 -1
- runbooks/{security_baseline → security}/report_generator.py +10 -2
- runbooks/{security_baseline → security}/report_template_en.html +7 -7
- runbooks/{security_baseline → security}/report_template_jp.html +7 -7
- runbooks/{security_baseline → security}/report_template_kr.html +12 -12
- runbooks/{security_baseline → security}/report_template_vn.html +7 -7
- runbooks/{security_baseline → security}/run_script.py +8 -2
- runbooks/{security_baseline → security}/security_baseline_tester.py +12 -4
- runbooks/{security_baseline → security}/utils/common.py +5 -1
- runbooks/utils/__init__.py +204 -0
- runbooks-0.7.0.dist-info/METADATA +375 -0
- runbooks-0.7.0.dist-info/RECORD +249 -0
- {runbooks-0.2.5.dist-info → runbooks-0.7.0.dist-info}/WHEEL +1 -1
- runbooks-0.7.0.dist-info/entry_points.txt +7 -0
- runbooks-0.7.0.dist-info/licenses/LICENSE +201 -0
- runbooks-0.7.0.dist-info/top_level.txt +3 -0
- runbooks/python101/calculator.py +0 -34
- runbooks/python101/config.py +0 -1
- runbooks/python101/exceptions.py +0 -16
- runbooks/python101/file_manager.py +0 -218
- runbooks/python101/toolkit.py +0 -153
- runbooks-0.2.5.dist-info/METADATA +0 -439
- runbooks-0.2.5.dist-info/RECORD +0 -61
- runbooks-0.2.5.dist-info/entry_points.txt +0 -3
- runbooks-0.2.5.dist-info/top_level.txt +0 -1
- /runbooks/{security_baseline/__init__.py → inventory/tests/script_test_data.py} +0 -0
- /runbooks/{security_baseline → security}/checklist/__init__.py +0 -0
- /runbooks/{security_baseline → security}/checklist/account_level_bucket_public_access.py +0 -0
- /runbooks/{security_baseline → security}/checklist/direct_attached_policy.py +0 -0
- /runbooks/{security_baseline → security}/checklist/iam_password_policy.py +0 -0
- /runbooks/{security_baseline → security}/checklist/iam_user_mfa.py +0 -0
- /runbooks/{security_baseline → security}/checklist/multi_region_trail.py +0 -0
- /runbooks/{security_baseline → security}/checklist/root_mfa.py +0 -0
- /runbooks/{security_baseline → security}/checklist/root_usage.py +0 -0
- /runbooks/{security_baseline → security}/checklist/trail_enabled.py +0 -0
- /runbooks/{security_baseline → security}/checklist/trusted_advisor.py +0 -0
- /runbooks/{security_baseline → security}/utils/__init__.py +0 -0
- /runbooks/{security_baseline → security}/utils/enums.py +0 -0
- /runbooks/{security_baseline → security}/utils/language.py +0 -0
- /runbooks/{security_baseline → security}/utils/level_const.py +0 -0
- /runbooks/{security_baseline → security}/utils/permission_list.py +0 -0
@@ -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()
|