runbooks 0.2.5__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 +7 -7
  199. runbooks/security_baseline/report_template_jp.html +7 -7
  200. runbooks/security_baseline/report_template_kr.html +12 -12
  201. runbooks/security_baseline/report_template_vn.html +7 -7
  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.5.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.5.dist-info/METADATA +0 -439
  219. runbooks-0.2.5.dist-info/RECORD +0 -61
  220. runbooks-0.2.5.dist-info/entry_points.txt +0 -3
  221. runbooks-0.2.5.dist-info/top_level.txt +0 -1
@@ -0,0 +1,1107 @@
1
+ #!/usr/bin/env python3
2
+
3
+ import logging
4
+ import os
5
+ import sys
6
+ from pprint import pprint
7
+ from queue import Queue
8
+ from threading import Thread
9
+ from time import time
10
+
11
+ import Inventory_Modules
12
+ from account_class import aws_acct_access
13
+ from ArgumentsClass import CommonArguments
14
+ from botocore.exceptions import ClientError
15
+ from colorama import Fore, init
16
+ from prettytable import PrettyTable
17
+
18
+ init()
19
+ __version__ = "2024.05.18"
20
+
21
+ script_path, script_name = os.path.split(sys.argv[0])
22
+ parser = CommonArguments()
23
+ parser.singleprofile()
24
+ parser.multiregion()
25
+ # The following includes "force", "skipaccount", "skipprofile", "account"
26
+ parser.extendedargs()
27
+ parser.deletion()
28
+ parser.roletouse()
29
+ parser.verbosity()
30
+ parser.timing()
31
+ parser.version(__version__)
32
+ local = parser.my_parser.add_argument_group(script_name, "Parameters specific to this script")
33
+ local.add_argument(
34
+ "--explain",
35
+ dest="pExplain",
36
+ const=True,
37
+ default=False,
38
+ action="store_const",
39
+ help="This flag prints out the explanation of what this script would do.",
40
+ )
41
+ local.add_argument(
42
+ "-q",
43
+ "--quick",
44
+ dest="Quick",
45
+ metavar="Shortcut the checking to only a single region",
46
+ const=True,
47
+ default=False,
48
+ action="store_const",
49
+ help="This flag only checks 'us-east-1', so makes the whole script run really fast.",
50
+ )
51
+ local.add_argument(
52
+ "+fix",
53
+ "+delete",
54
+ dest="FixRun",
55
+ const=True,
56
+ default=False,
57
+ action="store_const",
58
+ help="This will fix the issues found. If default VPCs must be deleted, you'll be asked to confirm.",
59
+ )
60
+ args = parser.my_parser.parse_args()
61
+
62
+ Quick = args.Quick
63
+ pProfile = args.Profile
64
+ pRegions = args.Regions
65
+ pSkipAccounts = args.SkipAccounts
66
+ pTiming = args.Time
67
+ verbose = args.loglevel
68
+ pAccessRole = args.AccessRole
69
+ pChildAccountList = args.Accounts
70
+ FixRun = args.FixRun
71
+ pExplain = args.pExplain
72
+ pVPCConfirm = args.Force
73
+ # Setup logging levels
74
+ logging.basicConfig(level=verbose, format="[%(filename)s:%(lineno)s - %(funcName)20s() ] %(message)s")
75
+ logging.getLogger("boto3").setLevel(logging.CRITICAL)
76
+ logging.getLogger("botocore").setLevel(logging.CRITICAL)
77
+ logging.getLogger("s3transfer").setLevel(logging.CRITICAL)
78
+ logging.getLogger("urllib3").setLevel(logging.CRITICAL)
79
+
80
+
81
+ def intersection(lst1, lst2):
82
+ lst3 = [value for value in lst1 if value in lst2]
83
+ return lst3
84
+
85
+
86
+ def explain_script():
87
+ print("This script does the following... ")
88
+ print(
89
+ f"{Fore.BLUE} 0.{Fore.RESET} Checks to ensure you have the necessary cross-account role access to the child account."
90
+ )
91
+ print(f"{Fore.BLUE} 1.{Fore.RESET} This check previously checked for default VPCs, but has since been removed.")
92
+ print(f"{Fore.BLUE} 2.{Fore.RESET} Checks the child account in each of the regions")
93
+ print(f" to see if there's already a {Fore.RED}Config Recorder and Delivery Channel {Fore.RESET}enabled...")
94
+ print(
95
+ f"{Fore.BLUE} 3.{Fore.RESET} Checks that there isn't a duplicate {Fore.RED}CloudTrail{Fore.RESET} trail in the account."
96
+ )
97
+ print(
98
+ f"{Fore.BLUE} 4.{Fore.RESET} This check previously checked for the presence of GuardDuty within this account, but has since been removed."
99
+ )
100
+ print(
101
+ f"{Fore.BLUE} 5.{Fore.RESET} This child account {Fore.RED}must exist{Fore.RESET} within the Parent Organization."
102
+ )
103
+ print(" If it doesn't - then you must move it into this Org - this script can't do that for you.")
104
+ print(
105
+ f"{Fore.BLUE} 6.{Fore.RESET} The target account {Fore.RED}can't exist{Fore.RESET} within an already managed OU."
106
+ )
107
+ print(" If it does - then you're already managing this account with Control Tower and just don't know it.")
108
+ print(f"{Fore.BLUE} 7.{Fore.RESET} Looking for {Fore.RED}SNS Topics{Fore.RESET} with duplicate names.")
109
+ print(" If found, we can delete them, but you probably want to do that manually - to be sure.")
110
+ print(f"{Fore.BLUE} 8.{Fore.RESET} Looking for {Fore.RED}Lambda Functions{Fore.RESET} with duplicate names.")
111
+ print(" If found, we can delete them, but you probably want to do that manually - to be sure.")
112
+ print(f"{Fore.BLUE} 9.{Fore.RESET} Looking for {Fore.RED}IAM Roles{Fore.RESET} with duplicate names.")
113
+ print(" If found, we can delete them, but you probably want to do that manually - to be sure.")
114
+ print(f"{Fore.BLUE} 10.{Fore.RESET} Looking for duplicate {Fore.RED}CloudWatch Log Groups.{Fore.RESET}")
115
+ print(" If found, we can delete them, but you probably want to do that manually - to be sure.")
116
+ print()
117
+ print("Since this script is fairly new - All comments or suggestions are enthusiastically encouraged")
118
+ print()
119
+
120
+
121
+ def summarizeOrgResults(fOrgResults):
122
+ summary = {}
123
+ for record in fOrgResults:
124
+ account = record["AccountId"]
125
+ region = record["Region"]
126
+ if account not in summary:
127
+ summary[account] = {"AccountId": account, "Regions": [], "IssuesFound": 0, "IssuesFixed": 0, "Ready": True}
128
+ if region not in summary[account]["Regions"]:
129
+ summary[account]["Regions"].append(region)
130
+ summary[account]["IssuesFound"] += record["IssuesFound"]
131
+ summary[account]["IssuesFixed"] += record["IssuesFixed"]
132
+ if not record["Ready"]:
133
+ summary[account]["Ready"] = False
134
+ return dict(sorted(summary.items()))
135
+
136
+
137
+ def DoAccountSteps(fChildAccountId, aws_account, fFixRun, fRegion):
138
+ def InitDict(StepCount):
139
+ fProcessStatus = {}
140
+ # fProcessStatus['ChildAccountIsReady']=True
141
+ # fProcessStatus['IssuesFound']=0
142
+ # fProcessStatus['IssuesFixed']=0
143
+ for item in range(StepCount):
144
+ Step = f"Step{str(item)}"
145
+ fProcessStatus[Step] = {}
146
+ fProcessStatus[Step]["Success"] = True
147
+ fProcessStatus[Step]["IssuesFound"] = 0
148
+ fProcessStatus[Step]["IssuesFixed"] = 0
149
+ fProcessStatus[Step]["ProblemsFound"] = []
150
+ return fProcessStatus
151
+
152
+ NumOfSteps = 11
153
+
154
+ # Step 0
155
+ ProcessStatus = InitDict(NumOfSteps)
156
+ OrgAccountList = [d["AccountId"] for d in aws_account.ChildAccounts]
157
+ account_credentials = {"Success": False, "AccessError": True, "ErrorMessage": "Initialization Parameters"}
158
+ Step = "Step0"
159
+ # This next list is the list of attempted roles. If you use a different named role for broad access, make sure it appears in this list.
160
+ CTRoles = [
161
+ pAccessRole,
162
+ "AWSControlTowerExecution",
163
+ "AWSCloudFormationStackSetExecutionRole",
164
+ "Owner",
165
+ "OrganizationAccountAccessRole",
166
+ ]
167
+ # TODO: I don't use this next variable, but eventually I intend to supply the JSON code needed to update a role with.
168
+ json_formatted_str_TP = ""
169
+ print(f"{Fore.BLUE}{Step}:{Fore.RESET}") if verbose < 50 else None
170
+ print(
171
+ f"Confirming we have the necessary cross-account access to account {fChildAccountId} in region {fRegion}"
172
+ ) if verbose < 50 else None
173
+ try:
174
+ account_credentials = Inventory_Modules.get_child_access3(aws_account, fChildAccountId, fRegion, CTRoles)
175
+ except ClientError as my_Error:
176
+ if "AuthFailure" in str(my_Error):
177
+ # TODO: This whole section is waiting on an enhancement. Until then, we have to assume that ProServe or someone familiar with Control Tower is running this script
178
+ print(f"{aws_account.acct_number}: Authorization Failure for account {fChildAccountId}")
179
+ print(
180
+ "The child account MUST allow access into the proper IAM role from the Organization's Management Account for the rest of this script (and the overall migration) to run."
181
+ )
182
+ print("You must add the following lines to the Trust Policy of that role in the child account")
183
+ print(json_formatted_str_TP)
184
+ print(my_Error)
185
+ ProcessStatus[Step]["Success"] = False
186
+ sys.exit("Exiting due to Authorization Failure...")
187
+ elif str(my_Error).find("AccessDenied") > 0:
188
+ # TODO: This whole section is waiting on an enhancement. Until then, we have to assume that ProServe or someone familiar with Control Tower is running this script
189
+ print(f"{aws_account.acct_number}: Access Denied Failure for account {fChildAccountId}")
190
+ print(
191
+ "The child account MUST allow access into the proper IAM role from the Organization's Management Account for the rest of this script (and the overall migration) to run."
192
+ )
193
+ print("You must add the following lines to the Trust Policy of that role in the child account")
194
+ print(json_formatted_str_TP)
195
+ print(my_Error)
196
+ ProcessStatus[Step]["Success"] = False
197
+ sys.exit("Exiting due to Access Denied Failure...")
198
+ else:
199
+ print(f"{aws_account.acct_number}: Other kind of failure for account {fChildAccountId}")
200
+ print(my_Error)
201
+ ProcessStatus[Step]["Success"] = False
202
+ sys.exit("Exiting for other failure...")
203
+ except Exception as my_Error:
204
+ error_message = f"This shouldn't happen - failing to access {fChildAccountId} from this Management Account {aws_account.acct_number} for region {fRegion}"
205
+ logging.error(f"Error Message: {error_message}\nError: {my_Error}")
206
+ finally:
207
+ if account_credentials["AccessError"]:
208
+ print(
209
+ f"{Fore.RED}We weren't able to connect to the Child Account {fChildAccountId} from this Management Account {aws_account.acct_number}. Please check the role Trust Policy and re-run this script.{Fore.RESET}"
210
+ )
211
+ print(
212
+ f"The following list of roles were tried, but none were allowed access to account {fChildAccountId} using the {aws_account.acct_number} account in region {fRegion}"
213
+ )
214
+ print(Fore.RED, CTRoles, Fore.RESET)
215
+ logging.debug(account_credentials)
216
+ ProcessStatus[Step]["Success"] = False
217
+ sys.exit("Exiting due to cross-account access failure")
218
+
219
+ logging.info("Was able to successfully connect using the credentials... ")
220
+ print() if verbose < 50 else None
221
+ print(
222
+ f"Confirmed the role {Fore.GREEN}{account_credentials['Role']}{Fore.RESET}"
223
+ f" exists in account {Fore.GREEN}{fChildAccountId}{Fore.RESET} and trusts the Management Account"
224
+ ) if verbose < 50 else None
225
+ print(f"{Fore.GREEN}** Step 0 completed without issues{Fore.RESET}") if verbose < 50 else None
226
+ print() if verbose < 50 else None
227
+
228
+ """
229
+ Step 1 - We should check whether Config is enabled as an Organizationally Trusted Service here.
230
+
231
+ """
232
+ Step = "Step1"
233
+ serviceName = "config.amazonaws.com"
234
+ print(f"{Fore.BLUE}{Step}:{Fore.RESET}") if verbose < 50 else None
235
+ print(
236
+ f"Checking Org Account {fChildAccountId} to see if Config is enabled at the Org Level\n"
237
+ f"Which means we only run this step if this is the Root Account\n"
238
+ f"{'which this account is not, so we are continuing...' if not account_credentials['AccountId'] == account_credentials['ParentAcctId'] else None}"
239
+ ) if verbose < 50 else None
240
+ # Checks to see if 'config.amazonaws.com' is a trusted org service in the Management Account. If so - we'll FAIL, since Control Tower wants to turn it on.
241
+ result = (
242
+ Inventory_Modules.find_org_services2(account_credentials, [serviceName])
243
+ if account_credentials["AccountId"] == account_credentials["ParentAcctId"]
244
+ else None
245
+ )
246
+ if result is not None and len(result) != 0:
247
+ print() if verbose < 50 else None
248
+ print(
249
+ f"{serviceName} is enabled within your Organization. Control Tower needs it to be disabled before continuing."
250
+ ) if verbose < 50 else None
251
+ print(
252
+ "This is easiest done manually right now, or you could re-run this script with the '+fix' parameter and we'll fix EVERYTHING we find - without asking first."
253
+ ) if verbose < 50 else None
254
+ ProcessStatus[Step]["Success"] = False
255
+ ProcessStatus[Step]["IssuesFound"] += 1
256
+
257
+ if fFixRun:
258
+ logging.warning(
259
+ f"{Fore.RED}Found {serviceName} is enabled as an Organizational Service and you've requested that we remedy that for you... {Fore.RESET}"
260
+ )
261
+ ProcessStatus[Step]["Success"] = False
262
+ ProcessStatus[Step]["IssuesFound"] += 1
263
+ ProcessStatus[Step]["ProblemsFound"].extend(
264
+ {
265
+ "Name": result["ServicePrincipal"],
266
+ "AccountId": account_credentials["AccountId"],
267
+ "Region": account_credentials["Region"],
268
+ }
269
+ )
270
+ logging.warning(
271
+ f"Deleting in account {account_credentials['AccountId']} in region {account_credentials['Region']}"
272
+ )
273
+ DelConfigService = Inventory_Modules.disable_org_service2(account_credentials, serviceName)
274
+ if DelConfigService["Success"]:
275
+ ProcessStatus[Step]["IssuesFixed"] += 1
276
+ else:
277
+ ProcessStatus[Step]["Success"] = False
278
+ logging.error(DelConfigService["ErrorMessage"])
279
+
280
+ if ProcessStatus[Step]["Success"]:
281
+ print(f"{ERASE_LINE + Fore.GREEN}** {Step} completed with no issues{Fore.RESET}") if verbose < 50 else None
282
+ elif ProcessStatus[Step]["IssuesFound"] - ProcessStatus[Step]["IssuesFixed"] == 0:
283
+ print(
284
+ f"{ERASE_LINE + Fore.GREEN}** {Step} found {ProcessStatus[Step]['IssuesFound']} issue, but we were able to fix it{Fore.RESET}"
285
+ ) if verbose < 50 else None
286
+ ProcessStatus[Step]["Success"] = True
287
+ elif ProcessStatus[Step]["IssuesFound"] > ProcessStatus[Step]["IssuesFixed"]:
288
+ print(
289
+ f"{ERASE_LINE + Fore.RED}** {Step} completed, but there was {ProcessStatus[Step]['IssuesFound'] - ProcessStatus[Step]['IssuesFixed']} blocker found that wasn't fixed{Fore.RESET}"
290
+ ) if verbose < 50 else None
291
+ else:
292
+ print(f"{ERASE_LINE + Fore.RED}** {Step} completed with blockers found{Fore.RESET}") if verbose < 50 else None
293
+ print() if verbose < 50 else None
294
+
295
+ # Step 2
296
+ # This part will check the Config Recorder and Delivery Channel. If they have one, we need to delete it, so we can create another. We'll ask whether this is ok before we delete.
297
+ Step = "Step2"
298
+ try:
299
+ print(f"{Fore.BLUE}{Step}:{Fore.RESET}") if verbose < 50 else None
300
+ print(
301
+ f" Checking account {fChildAccountId} for a Config Recorders and Delivery Channels in any region"
302
+ ) if verbose < 50 else None
303
+ ConfigList = []
304
+ DeliveryChanList = []
305
+ print(
306
+ ERASE_LINE, f"Checking account {fChildAccountId} in region {fRegion} for Config Recorder", end="\r"
307
+ ) if verbose < 50 else None
308
+ logging.info("Looking for Config Recorders in account %s from Region %s", fChildAccountId, fRegion)
309
+ ConfigRecorder = Inventory_Modules.find_config_recorders2(account_credentials, fRegion)
310
+ logging.debug("Tried to capture Config Recorder")
311
+ if len(ConfigRecorder["ConfigurationRecorders"]) > 0:
312
+ ConfigList.append(
313
+ {
314
+ "Name": ConfigRecorder["ConfigurationRecorders"][0]["name"],
315
+ "roleARN": ConfigRecorder["ConfigurationRecorders"][0]["roleARN"],
316
+ "AccountID": fChildAccountId,
317
+ "Region": fRegion,
318
+ }
319
+ )
320
+ print(
321
+ f"{ERASE_LINE}Checking account {fChildAccountId} in region {fRegion} for Delivery Channel", end="\r"
322
+ ) if verbose < 50 else None
323
+ DeliveryChannel = Inventory_Modules.find_delivery_channels2(account_credentials, fRegion)
324
+ logging.debug("Tried to capture Delivery Channel")
325
+ if len(DeliveryChannel["DeliveryChannels"]) > 0:
326
+ DeliveryChanList.append(
327
+ {
328
+ "Name": DeliveryChannel["DeliveryChannels"][0]["name"],
329
+ "AccountID": fChildAccountId,
330
+ "Region": fRegion,
331
+ }
332
+ )
333
+ logging.warning(
334
+ f"Checked account {fChildAccountId} in {len(RegionList)} regions. Found {len(ConfigList) + len(DeliveryChanList)} issues with Config Recorders and Delivery Channels"
335
+ )
336
+ except ClientError as my_Error:
337
+ logging.warning("Failed to capture Config Recorder and Delivery Channels")
338
+ ProcessStatus[Step]["Success"] = False
339
+ print(my_Error)
340
+
341
+ for _ in range(len(ConfigList)):
342
+ logging.warning(
343
+ f"{Fore.RED}Found a config recorder for account %s in region %s",
344
+ ConfigList[_]["AccountID"],
345
+ ConfigList[_]["Region"] + Fore.RESET,
346
+ )
347
+ ProcessStatus[Step]["Success"] = False
348
+ ProcessStatus[Step]["IssuesFound"] += 1
349
+ ProcessStatus[Step]["ProblemsFound"].extend(ConfigList)
350
+ if fFixRun:
351
+ logging.warning(
352
+ "Deleting %s in account %s in region %s",
353
+ ConfigList[_]["Name"],
354
+ ConfigList[_]["AccountID"],
355
+ ConfigList[_]["Region"],
356
+ )
357
+ DelConfigRecorder = Inventory_Modules.del_config_recorder2(
358
+ account_credentials, ConfigList[_]["Region"], ConfigList[_]["Name"]
359
+ )
360
+ # We assume the process worked. We should probably NOT assume this.
361
+ ProcessStatus[Step]["IssuesFixed"] += 1
362
+ for _ in range(len(DeliveryChanList)):
363
+ logging.warning(
364
+ f"{Fore.RED}Found a delivery channel for account {DeliveryChanList[_]['AccountID']} in region {DeliveryChanList[_]['Region']}{Fore.RESET}"
365
+ )
366
+ ProcessStatus[Step]["Success"] = False
367
+ ProcessStatus[Step]["IssuesFound"] += 1
368
+ ProcessStatus[Step]["ProblemsFound"].extend(DeliveryChanList)
369
+ if fFixRun:
370
+ logging.warning(
371
+ "Deleting %s in account %s in region %s",
372
+ DeliveryChanList[_]["Name"],
373
+ DeliveryChanList[_]["AccountID"],
374
+ DeliveryChanList[_]["Region"],
375
+ )
376
+ DelDeliveryChannel = Inventory_Modules.del_delivery_channel2(
377
+ account_credentials, DeliveryChanList[_]["Region"], DeliveryChanList[_]["Name"]
378
+ )
379
+ # We assume the process worked. We should probably NOT assume this.
380
+ ProcessStatus[Step]["IssuesFixed"] += 1
381
+
382
+ if ProcessStatus[Step]["Success"]:
383
+ print(f"{ERASE_LINE + Fore.GREEN}** Step 2 completed with no issues{Fore.RESET}") if verbose < 50 else None
384
+ elif ProcessStatus[Step]["IssuesFound"] - ProcessStatus[Step]["IssuesFixed"] == 0:
385
+ print(
386
+ f"{ERASE_LINE + Fore.GREEN}** Step 2 found {ProcessStatus[Step]['IssuesFound']} issues, but they were fixed by deleting the existing Config Recorders and Delivery Channels{Fore.RESET}"
387
+ ) if verbose < 50 else None
388
+ ProcessStatus[Step]["Success"] = True
389
+ elif ProcessStatus[Step]["IssuesFound"] > ProcessStatus[Step]["IssuesFixed"]:
390
+ print(
391
+ f"{ERASE_LINE + Fore.RED}** Step 2 completed, but there were {ProcessStatus[Step]['IssuesFound'] - ProcessStatus[Step]['IssuesFixed']} items found that weren't deleted{Fore.RESET}"
392
+ ) if verbose < 50 else None
393
+ else:
394
+ print(f"{ERASE_LINE + Fore.RED}** Step 2 completed with blockers found{Fore.RESET}") if verbose < 50 else None
395
+ print() if verbose < 50 else None
396
+
397
+ # Step 3
398
+ # 3. The account must not have a Cloudtrail Trail name the same name as the CT Trail ("AWS-Landing-Zone-BaselineCloudTrail")
399
+ Step = "Step3"
400
+ CTtrails2 = []
401
+ try:
402
+ print(f"{Fore.BLUE}{Step}:{Fore.RESET}") if verbose < 50 else None
403
+ print(
404
+ f" Checking account {fChildAccountId} for a specially named CloudTrail in all regions"
405
+ ) if verbose < 50 else None
406
+ print(
407
+ ERASE_LINE, f"Checking account {fChildAccountId} in region {fRegion} for CloudTrail trails", end="\r"
408
+ ) if verbose < 50 else None
409
+ CTtrails = Inventory_Modules.find_cloudtrails2(
410
+ account_credentials, fRegion, ["aws-controltower-BaselineCloudTrail"]
411
+ )
412
+ if len(CTtrails) > 0:
413
+ logging.warning(
414
+ f"Unfortunately, we've found a CloudTrail log named {CTtrails[0]['Name']} in account {fChildAccountId} "
415
+ f"in the {fRegion} region, which means we'll have to delete it before this account can be adopted."
416
+ )
417
+ CTtrails2.append(CTtrails[0])
418
+ ProcessStatus[Step]["Success"] = False
419
+ except ClientError as my_Error:
420
+ print(my_Error)
421
+ ProcessStatus[Step]["Success"] = False
422
+
423
+ for _ in range(len(CTtrails2)):
424
+ logging.warning(
425
+ f"{Fore.RED}Found a CloudTrail trail for account {fChildAccountId} in region {CTtrails2[_]['HomeRegion']} named {CTtrails2[_]['Name']}{Fore.RESET}"
426
+ )
427
+ ProcessStatus[Step]["IssuesFound"] += 1
428
+ ProcessStatus[Step]["ProblemsFound"].extend(CTtrails2)
429
+ if fFixRun:
430
+ try:
431
+ logging.warning("CloudTrail trail deletion commencing...")
432
+ delresponse = Inventory_Modules.del_cloudtrails2(account_credentials, fRegion, CTtrails2[_]["TrailARN"])
433
+ ProcessStatus[Step]["IssuesFixed"] += 1
434
+ except ClientError as my_Error:
435
+ print(my_Error)
436
+
437
+ if ProcessStatus[Step]["Success"]:
438
+ print(f"{ERASE_LINE + Fore.GREEN}** {Step} completed with no issues{Fore.RESET}") if verbose < 50 else None
439
+ elif ProcessStatus[Step]["IssuesFound"] - ProcessStatus[Step]["IssuesFixed"] == 0:
440
+ print(
441
+ f"{ERASE_LINE + Fore.GREEN}** {Step} found {ProcessStatus[Step]['IssuesFound']} issues, but they were fixed by deleting the existing CloudTrail trail names{Fore.RESET}"
442
+ ) if verbose < 50 else None
443
+ ProcessStatus[Step]["Success"] = True
444
+ elif ProcessStatus[Step]["IssuesFound"] > ProcessStatus[Step]["IssuesFixed"]:
445
+ print(
446
+ f"{ERASE_LINE + Fore.RED}** {Step} completed, but there were {ProcessStatus[Step]['IssuesFound'] - ProcessStatus[Step]['IssuesFixed']} trail names found that wasn't deleted{Fore.RESET}"
447
+ ) if verbose < 50 else None
448
+ else:
449
+ print(f"{ERASE_LINE + Fore.RED}** {Step} completed with blockers found{Fore.RESET}") if verbose < 50 else None
450
+ print() if verbose < 50 else None
451
+
452
+ """ Step 4
453
+ # Step 4 -- The lack of or use of GuardDuty isn't a pre-requisite for Control Tower --
454
+ # 4. This section checks for a pending guard duty invite. You can also check from the Guard Duty Console
455
+ Step='Step4'
456
+ try:
457
+ print(Fore.BLUE + "{}:".format(Step) + Fore.RESET)
458
+ print(" Checking account {} for any GuardDuty invites".format(fChildAccountId))
459
+ GDinvites2=[]
460
+ for region in fRegionList:
461
+ logging.warning("Checking account %s in region %s for", fChildAccountId, region+Fore.RED+" GuardDuty"+Fore.RESET+" invitations")
462
+ logging.warning("Checking account %s in region %s for GuardDuty invites", fChildAccountId, region)
463
+ GDinvites=Inventory_Modules.find_gd_invites(account_credentials, region)
464
+ if len(GDinvites) > 0:
465
+ for x in range(len(GDinvites['Invitations'])):
466
+ logging.warning("GD Invite: %s", str(GDinvites['Invitations'][x]))
467
+ logging.warning("Unfortunately, we've found a GuardDuty invitation for account %s in the %s region from account %s, which means we'll have to delete it before this account can be adopted.", fChildAccountId, region, GDinvites['Invitations'][x]['AccountId'])
468
+ ProcessStatus[Step]['IssuesFound']+=1
469
+ GDinvites2.append({
470
+ 'AccountId': GDinvites['Invitations'][x]['AccountId'],
471
+ 'InvitationId': GDinvites['Invitations'][x]['InvitationId'],
472
+ 'Region': region
473
+ })
474
+ except ClientError as my_Error:
475
+ print(my_Error)
476
+ ProcessStatus[Step]['Success']=False
477
+
478
+ for i in range(len(GDinvites2)):
479
+ logging.warning(Fore.RED+"I found a GuardDuty invitation for account %s in region %s from account %s ", fChildAccountId, GDinvites2[i]['Region'], GDinvites2[i]['AccountId']+Fore.RESET)
480
+ ProcessStatus[Step]['IssuesFound']+=1
481
+ ProcessStatus[Step]['Success']=False
482
+ if fFixRun:
483
+ for x in range(len(GDinvites2)):
484
+ try:
485
+ logging.warning("GuardDuty invite deletion commencing...")
486
+ delresponse=Inventory_Modules.delete_gd_invites(account_credentials, region, GDinvites2[x]['AccountId'])
487
+ ProcessStatus[Step]['IssuesFixed']+=1
488
+ # We assume the process worked. We should probably NOT assume this.
489
+ except ClientError as my_Error:
490
+ print(my_Error)
491
+
492
+ if ProcessStatus[Step]['Success']:
493
+ print(ERASE_LINE+Fore.GREEN+"** {} completed with no issues".format(Step)+Fore.RESET)
494
+ elif ProcessStatus[Step]['IssuesFound']-ProcessStatus[Step]['IssuesFixed']==0:
495
+ print(ERASE_LINE+Fore.GREEN+"** {} found {} guardduty invites, but they were deleted".format(Step,ProcessStatus[Step]['IssuesFound'])+Fore.RESET)
496
+ ProcessStatus[Step]['Success']=True
497
+ elif (ProcessStatus[Step]['IssuesFound']>ProcessStatus[Step]['IssuesFixed']):
498
+ print(ERASE_LINE+Fore.RED+"** {} completed, but there were {} guardduty invites found that couldn't be deleted".format(Step,ProcessStatus[Step]['IssuesFound']-ProcessStatus[Step]['IssuesFixed'])+Fore.RESET)
499
+ else:
500
+ print(ERASE_LINE+Fore.RED+"** {} completed with blockers found".format(Step)+Fore.RESET)
501
+ print()
502
+ """
503
+
504
+ # Step 4a
505
+ # 4a. STS must be active in all regions. You can check from the Account Settings page in IAM.
506
+ # TODO: We would have already verified this - since we've used STS to connect to each region already for the previous steps.
507
+ # Except for the "quick" shortcut - which means we probably need to point that out in this section.
508
+
509
+ # Step 5
510
+ """
511
+ 5. The account must be part of the Organization and the email address being entered into the CT parameters must match the account.
512
+ If you try to add an email from an account which is not part of the Org, you will get an error that you are not using a unique email address. If it’s part of the Org, CT just finds the account and uses the CFN roles.
513
+ - If the existing account is to be imported as a Core Account, modify the manifest.yaml file to use it.
514
+ - If the existing account will be a child account in the Organization, use the AVM launch template through Service Catalog and enter the appropriate configuration parameters.
515
+ """
516
+ # try:
517
+ Step = "Step5"
518
+ print(f"{Fore.BLUE}{Step}:{Fore.RESET}") if verbose < 50 else None
519
+ print(" Checking that the account is part of the AWS Organization.") if verbose < 50 else None
520
+ if not (fChildAccountId in OrgAccountList):
521
+ print() if verbose < 50 else None
522
+ print(
523
+ f"Account # {fChildAccountId} is not a part of the Organization. This account needs to be moved into the Organization to be adopted into Control Tower"
524
+ ) if verbose < 50 else None
525
+ print("This is easiest done manually right now.") if verbose < 50 else None
526
+ ProcessStatus[Step]["Success"] = False
527
+ ProcessStatus[Step]["IssuesFound"] += 1
528
+ else:
529
+ print("Which it is - so ...") if verbose < 50 else None
530
+
531
+ if ProcessStatus[Step]["Success"]:
532
+ print(f"{ERASE_LINE + Fore.GREEN}** {Step} completed with no issues{Fore.RESET}") if verbose < 50 else None
533
+ elif ProcessStatus[Step]["IssuesFound"] - ProcessStatus[Step]["IssuesFixed"] == 0:
534
+ print(
535
+ f"{ERASE_LINE + Fore.GREEN}** {Step} found {ProcessStatus[Step]['IssuesFound']} issue, but we were able to fix it{Fore.RESET}"
536
+ ) if verbose < 50 else None
537
+ ProcessStatus[Step]["Success"] = True
538
+ elif ProcessStatus[Step]["IssuesFound"] > ProcessStatus[Step]["IssuesFixed"]:
539
+ print(
540
+ f"{ERASE_LINE + Fore.RED}** {Step} completed, but there was {ProcessStatus[Step]['IssuesFound'] - ProcessStatus[Step]['IssuesFixed']} blocker found that wasn't fixed{Fore.RESET}"
541
+ ) if verbose < 50 else None
542
+ else:
543
+ print(f"{ERASE_LINE + Fore.RED}** {Step} completed with blockers found{Fore.RESET}") if verbose < 50 else None
544
+ print() if verbose < 50 else None
545
+
546
+ # Step 6
547
+ # 6. The existing account can not be in any of the CT-managed Organizations OUs.
548
+ # By default, these OUs are Core and Applications, but the customer may have chosen different or additional OUs to manage by CT.
549
+ # TODO: This step should only be done once per Org, instead of per region...
550
+
551
+ Step = "Step6"
552
+ try:
553
+ print(f"{Fore.BLUE}{Step}:{Fore.RESET}") if verbose < 50 else None
554
+ print(
555
+ f" Checking account {fChildAccountId} to make sure it's not already in a Control-Tower managed OU"
556
+ ) if verbose < 50 else None
557
+ # TODO: So we'll need to verify that the parent OU of the account is the root of the organization.
558
+ print(
559
+ " -- Not yet implemented because Control Tower doesn't list the OUs they manage vs. those they don't via an API -- "
560
+ ) if verbose < 50 else None
561
+ except ClientError as my_Error:
562
+ print(my_Error)
563
+ ProcessStatus[Step]["Success"] = False
564
+ print() if verbose < 50 else None
565
+
566
+ # Step 7 - Check for other resources which have 'controltower' in the name
567
+ # Checking for SNS Topics
568
+ Step = "Step7"
569
+ try:
570
+ print(f"{Fore.BLUE}{Step}:{Fore.RESET}") if verbose < 50 else None
571
+ print(
572
+ f" Checking account {fChildAccountId} for any SNS topics containing the 'controltower' string"
573
+ ) if verbose < 50 else None
574
+ SNSTopics2 = []
575
+ logging.warning(
576
+ "Checking account %s in region %s for", fChildAccountId, f"{fRegion + Fore.RED} SNS Topics{Fore.RESET}"
577
+ )
578
+ print(
579
+ ERASE_LINE, f"Checking account {fChildAccountId} in region {fRegion} for SNS Topics", end="\r"
580
+ ) if verbose < 50 else None
581
+ SNSTopics = Inventory_Modules.find_sns_topics2(account_credentials, fRegion, ["controltower", "ControlTower"])
582
+ if len(SNSTopics) > 0:
583
+ for x in range(len(SNSTopics)):
584
+ logging.warning("SNS Topic: %s", str(SNSTopics[x]))
585
+ logging.info(
586
+ "Unfortunately, we've found an SNS Topic for account %s in the %s region, which means we'll have to delete it before this account can be adopted.",
587
+ fChildAccountId,
588
+ fRegion,
589
+ )
590
+ ProcessStatus[Step]["Success"] = False
591
+ ProcessStatus[Step]["IssuesFound"] += 1
592
+ SNSTopics2.append({"AccountId": fChildAccountId, "TopicArn": SNSTopics[x], "Region": fRegion})
593
+ ProcessStatus[Step]["ProblemsFound"].extend(SNSTopics2)
594
+ except ClientError as my_Error:
595
+ print(my_Error)
596
+ ProcessStatus[Step]["Success"] = False
597
+
598
+ if ProcessStatus[Step]["Success"]:
599
+ print(f"{ERASE_LINE + Fore.GREEN}** {Step} completed with no issues{Fore.RESET}") if verbose < 50 else None
600
+ elif ProcessStatus[Step]["IssuesFound"] - ProcessStatus[Step]["IssuesFixed"] == 0:
601
+ print(
602
+ f"{ERASE_LINE + Fore.GREEN}** {Step} found {ProcessStatus[Step]['IssuesFound']} issues, but we were able to remove the offending SNS Topics{Fore.RESET}"
603
+ ) if verbose < 50 else None
604
+ ProcessStatus[Step]["Success"] = True
605
+ elif ProcessStatus[Step]["IssuesFound"] > ProcessStatus[Step]["IssuesFixed"]:
606
+ print(
607
+ f"{ERASE_LINE + Fore.RED}** {Step} completed, but there were {ProcessStatus[Step]['IssuesFound'] - ProcessStatus[Step]['IssuesFixed']} blockers found that wasn't fixed{Fore.RESET}"
608
+ ) if verbose < 50 else None
609
+ else:
610
+ print(f"{ERASE_LINE + Fore.RED}** {Step} completed with blockers found{Fore.RESET}") if verbose < 50 else None
611
+ print() if verbose < 50 else None
612
+
613
+ # Step 8
614
+ # Checking for Lambda functions
615
+ Step = "Step8"
616
+ try:
617
+ print(f"{Fore.BLUE}{Step}:{Fore.RESET}") if verbose < 50 else None
618
+ print(
619
+ f" Checking account {fChildAccountId} for any Lambda functions containing the 'controltower' string"
620
+ ) if verbose < 50 else None
621
+ LambdaFunctions2 = []
622
+ logging.warning(
623
+ f"Checking account %s in region %s for {Fore.RED}Lambda functions{Fore.RESET}", fChildAccountId, fRegion
624
+ )
625
+ print(
626
+ ERASE_LINE, f"Checking account {fChildAccountId} in region {fRegion} for Lambda Functions", end="\r"
627
+ ) if verbose < 50 else None
628
+ LambdaFunctions = Inventory_Modules.find_lambda_functions2(
629
+ account_credentials, fRegion, ["controltower", "ControlTower"]
630
+ )
631
+ if len(LambdaFunctions) > 0:
632
+ logging.info(
633
+ f"Unfortunately, account {fChildAccountId} contains {len(LambdaFunctions)} functions with reserved names, which means we'll have to delete them before this account can be adopted."
634
+ )
635
+ for x in range(len(LambdaFunctions)):
636
+ logging.warning(f"Found Lambda function {LambdaFunctions[x]['FunctionName']} in region {fRegion}")
637
+ ProcessStatus[Step]["Success"] = False
638
+ ProcessStatus[Step]["IssuesFound"] += 1
639
+ LambdaFunctions2.append(
640
+ {
641
+ "AccountId": fChildAccountId,
642
+ "FunctionName": LambdaFunctions[x]["FunctionName"],
643
+ "FunctionArn": LambdaFunctions[x]["FunctionArn"],
644
+ "Role": LambdaFunctions[x]["Role"],
645
+ "Region": fRegion,
646
+ }
647
+ )
648
+ ProcessStatus[Step]["ProblemsFound"].extend(LambdaFunctions2)
649
+ except ClientError as my_Error:
650
+ print(my_Error)
651
+ ProcessStatus[Step]["Success"] = False
652
+
653
+ if ProcessStatus[Step]["Success"]:
654
+ print(f"{ERASE_LINE + Fore.GREEN}** {Step} completed with no issues{Fore.RESET}") if verbose < 50 else None
655
+ elif ProcessStatus[Step]["IssuesFound"] - ProcessStatus[Step]["IssuesFixed"] == 0:
656
+ print(
657
+ f"{ERASE_LINE + Fore.GREEN}** {Step} found {ProcessStatus[Step]['IssuesFound']} issues, but we were able to remove the offending Lambda Functions{Fore.RESET}"
658
+ ) if verbose < 50 else None
659
+ ProcessStatus[Step]["Success"] = True
660
+ elif ProcessStatus[Step]["IssuesFound"] > ProcessStatus[Step]["IssuesFixed"]:
661
+ print(
662
+ f"{ERASE_LINE + Fore.RED}** {Step} completed, but there were {ProcessStatus[Step]['IssuesFound'] - ProcessStatus[Step]['IssuesFixed']} blockers found that wasn't fixed{Fore.RESET}"
663
+ ) if verbose < 50 else None
664
+ else:
665
+ print(f"{ERASE_LINE + Fore.RED}** {Step} completed with blockers found{Fore.RESET}") if verbose < 50 else None
666
+ print() if verbose < 50 else None
667
+
668
+ # Step 9
669
+ # Checking for Role names - unfortunately, this gets called for every region, even though the results will be the same in every region.
670
+ # TODO: Need to find a way to only run this once, instead of for every region.
671
+ Step = "Step9"
672
+ try:
673
+ print(f"{Fore.BLUE}{Step}:{Fore.RESET}") if verbose < 50 else None
674
+ print(
675
+ f" Checking account {fChildAccountId} for any Role names containing the 'controltower' string"
676
+ ) if verbose < 50 else None
677
+ RoleNames2 = []
678
+ logging.warning(f"Checking account {Fore.RED}{fChildAccountId}{Fore.RESET} for Role names")
679
+ RoleNames = Inventory_Modules.find_role_names2(
680
+ account_credentials, "us-east-1", ["controltower", "ControlTower"]
681
+ )
682
+ if len(RoleNames) > 0:
683
+ logging.info(
684
+ f"Unfortunately, account {fChildAccountId} contains {len(RoleNames)} roles with reserved names,"
685
+ f" which means we'll have to delete them before this account can be adopted."
686
+ )
687
+ for x in range(len(RoleNames)):
688
+ logging.warning(f"Role Name: {str(RoleNames[x])}")
689
+ ProcessStatus[Step]["Success"] = False
690
+ ProcessStatus[Step]["IssuesFound"] += 1
691
+ RoleNames2.append({"AccountId": fChildAccountId, "RoleName": RoleNames[x]})
692
+ ProcessStatus[Step]["ProblemsFound"].extend(RoleNames2)
693
+ except ClientError as my_Error:
694
+ print(my_Error)
695
+ ProcessStatus[Step]["Success"] = False
696
+
697
+ if ProcessStatus[Step]["Success"]:
698
+ print(f"{ERASE_LINE + Fore.GREEN}** {Step} completed with no issues{Fore.RESET}") if verbose < 50 else None
699
+ elif ProcessStatus[Step]["IssuesFound"] - ProcessStatus[Step]["IssuesFixed"] == 0:
700
+ print(
701
+ f"{ERASE_LINE + Fore.GREEN}** {Step} found {ProcessStatus[Step]['IssuesFound']} issues, but we were able to remove the offending IAM roles{Fore.RESET}"
702
+ ) if verbose < 50 else None
703
+ ProcessStatus[Step]["Success"] = True
704
+ elif ProcessStatus[Step]["IssuesFound"] > ProcessStatus[Step]["IssuesFixed"]:
705
+ print(
706
+ f"{ERASE_LINE + Fore.RED}** {Step} completed, but there were {ProcessStatus[Step]['IssuesFound'] - ProcessStatus[Step]['IssuesFixed']} blockers found that remain to be fixed{Fore.RESET}"
707
+ ) if verbose < 50 else None
708
+ else:
709
+ print(f"{ERASE_LINE + Fore.RED}** {Step} completed with blockers found{Fore.RESET}") if verbose < 50 else None
710
+ print() if verbose < 50 else None
711
+
712
+ # Step 10
713
+ # 10. The existing account can not have any CloudWatch Log Groups named "controltower"
714
+ # TODO: So we'll need to find and remove the CloudWatch Log Groups - if there are any.
715
+
716
+ Step = "Step10"
717
+ try:
718
+ print(f"{Fore.BLUE}{Step}:{Fore.RESET}") if verbose < 50 else None
719
+ print(
720
+ f"Checking account {fChildAccountId} to make sure there are no duplicate CloudWatch Log Groups"
721
+ ) if verbose < 50 else None
722
+ LogGroupNames2 = []
723
+ logging.warning(
724
+ f"Checking account {fChildAccountId} for {Fore.RED}duplicate CloudWatch Log Group names{Fore.RESET}"
725
+ )
726
+ LogGroupNames = Inventory_Modules.find_cw_log_group_names2(
727
+ account_credentials, fRegion, ["controltower", "ControlTower"]
728
+ )
729
+ if len(LogGroupNames) > 0:
730
+ logging.info(
731
+ f"Unfortunately, account {fChildAccountId} contains {len(LogGroupNames)} log groups with reserved names,"
732
+ f" which means we'll have to delete them before this account can be adopted."
733
+ )
734
+ for _ in range(len(LogGroupNames)):
735
+ logging.warning(f"Log Group Name: {str(LogGroupNames[_])}")
736
+ ProcessStatus[Step]["Success"] = False
737
+ ProcessStatus[Step]["IssuesFound"] += 1
738
+ LogGroupNames2.append({"AccountId": fChildAccountId, "LogGroupName": LogGroupNames[_]})
739
+ ProcessStatus[Step]["ProblemsFound"].extend(LogGroupNames2)
740
+ except ClientError as my_Error:
741
+ print(my_Error)
742
+ ProcessStatus[Step]["Success"] = False
743
+
744
+ if ProcessStatus[Step]["Success"]:
745
+ print(f"{ERASE_LINE + Fore.GREEN}** {Step} completed with no issues{Fore.RESET}") if verbose < 50 else None
746
+ elif ProcessStatus[Step]["IssuesFound"] - ProcessStatus[Step]["IssuesFixed"] == 0:
747
+ print(
748
+ f"{ERASE_LINE}{Fore.GREEN}** {Step} found {ProcessStatus[Step]['IssuesFound']} issues, but we were able to remove the offending CW Log groups{Fore.RESET}"
749
+ ) if verbose < 50 else None
750
+ ProcessStatus[Step]["Success"] = True
751
+ elif ProcessStatus[Step]["IssuesFound"] > ProcessStatus[Step]["IssuesFixed"]:
752
+ print(
753
+ f"{ERASE_LINE}{Fore.RED}** {Step} completed, but there were {ProcessStatus[Step]['IssuesFound'] - ProcessStatus[Step]['IssuesFixed']} blockers found that remain to be fixed {Fore.RESET}"
754
+ ) if verbose < 50 else None
755
+ else:
756
+ print(f"{ERASE_LINE + Fore.RED}** {Step} completed with blockers found{Fore.RESET}") if verbose < 50 else None
757
+ print() if verbose < 50 else None
758
+
759
+ """ Function Summary """
760
+ TotalIssuesFound = 0
761
+ TotalIssuesFixed = 0
762
+ MemberReady = True
763
+ for item in ProcessStatus:
764
+ TotalIssuesFound = TotalIssuesFound + ProcessStatus[item]["IssuesFound"]
765
+ TotalIssuesFixed = TotalIssuesFixed + ProcessStatus[item]["IssuesFixed"]
766
+ MemberReady = MemberReady and ProcessStatus[item]["Success"]
767
+
768
+ ProcessStatus["AccountId"] = fChildAccountId
769
+ ProcessStatus["Region"] = fRegion
770
+ ProcessStatus["Ready"] = MemberReady
771
+ ProcessStatus["IssuesFound"] = TotalIssuesFound
772
+ ProcessStatus["IssuesFixed"] = TotalIssuesFixed
773
+ return ProcessStatus
774
+
775
+
776
+ # The parameters passed to this function should be the dictionary of attributes that will be examined within the thread.
777
+ def DoThreadedAccountSteps(fChildAccountList, aws_account, fFixRun, fRegionList=None):
778
+ """
779
+ Note that this function takes a list of account numbers and a list of regions and runs the CT_Checks within them
780
+ """
781
+
782
+ class ThreadedAccountSteps(Thread):
783
+ def __init__(self, queue):
784
+ Thread.__init__(self)
785
+ self.queue = queue
786
+
787
+ def run(self):
788
+ while True:
789
+ # Get the work from the queue and expand the tuple
790
+ c_member_account, c_fixrun, c_region, c_PlaceCount = self.queue.get()
791
+ logging.info(f"De-queued info for account {c_member_account} and region {c_region}")
792
+ try:
793
+ logging.info(
794
+ f"Looking through {c_PlaceCount} of {len(fChildAccountList) * len(RegionList)} accounts in {RegionList} regions"
795
+ )
796
+ SingleAccountandRegionResults = DoAccountSteps(c_member_account, aws_account, c_fixrun, c_region)
797
+ logging.warning(f"Found {len(SingleAccountandRegionResults)} rows in Steps")
798
+ AllOrgSteps.append(SingleAccountandRegionResults)
799
+ except KeyError as my_Error:
800
+ logging.error(f"Account Access failed - trying to access {c_member_account}")
801
+ logging.info(f"Actual Error: {my_Error}")
802
+ pass
803
+ except AttributeError as my_Error:
804
+ logging.error(f"Error: Likely that one of the supplied profiles was wrong")
805
+ logging.warning(my_Error)
806
+ continue
807
+ except ClientError as my_Error:
808
+ logging.error(f"Error: Likely throttling errors from too much activity")
809
+ logging.warning(my_Error)
810
+ continue
811
+ finally:
812
+ print(
813
+ f"{ERASE_LINE}Finished looking through {c_member_account} in region {c_region} - {c_PlaceCount} / {len(fChildAccountList) * len(fRegionList)}",
814
+ end="\r",
815
+ )
816
+ self.queue.task_done()
817
+
818
+ ###########
819
+
820
+ if fRegionList is None:
821
+ fRegionList = ["us-east-1"]
822
+ checkqueue = Queue()
823
+
824
+ AllOrgSteps = []
825
+ PlaceCount = 1
826
+ WorkerThreads = min(len(fChildAccountList) * len(fRegionList), 150)
827
+ WorkerThreads = min(len(fChildAccountList) * len(fRegionList), 50)
828
+ # WorkerThreads = 1
829
+
830
+ for x in range(WorkerThreads):
831
+ worker = ThreadedAccountSteps(checkqueue)
832
+ # Setting daemon to True will let the main thread exit even though the workers are blocking
833
+ worker.daemon = True
834
+ worker.start()
835
+
836
+ for member_account in fChildAccountList:
837
+ print(f"Queuing data for {member_account} in each of the {len(fRegionList)} regions you specified...")
838
+ for region in fRegionList:
839
+ logging.debug(f"Beginning to queue data - starting with {member_account} and region {region}")
840
+ try:
841
+ # I don't know why - but double parens are necessary below. If you remove them, only the first parameter is queued.
842
+ checkqueue.put((member_account, fFixRun, region, PlaceCount))
843
+ PlaceCount += 1
844
+ except ClientError as my_Error:
845
+ if "AuthFailure" in str(my_Error):
846
+ logging.error(f"Authorization Failure accessing account {member_account} in {region} region")
847
+ logging.warning(f"It's possible that the region {region} hasn't been opted-into")
848
+ pass
849
+ print(f"Threads are starting... Results coming in shortly... It takes around 1 second per account per region... ")
850
+ checkqueue.join()
851
+ return AllOrgSteps
852
+
853
+
854
+ def display_results():
855
+ print()
856
+ x = PrettyTable()
857
+ y = PrettyTable()
858
+
859
+ x.field_names = ["Account", "# of Regions", "Issues Found", "Issues Fixed", "Ready?"]
860
+ y.field_names = [
861
+ "Account",
862
+ "Region",
863
+ "Account Access",
864
+ "Org Config",
865
+ "Config",
866
+ "CloudTrail",
867
+ "GuardDuty",
868
+ "Org Member",
869
+ "CT OU",
870
+ "SNS Topics",
871
+ "Lambda",
872
+ "Roles",
873
+ "CW Log Groups",
874
+ "Ready?",
875
+ ]
876
+ for i in SummarizedOrgResults:
877
+ x.add_row(
878
+ [
879
+ SummarizedOrgResults[i]["AccountId"],
880
+ len(SummarizedOrgResults[i]["Regions"]),
881
+ SummarizedOrgResults[i]["IssuesFound"],
882
+ SummarizedOrgResults[i]["IssuesFixed"],
883
+ SummarizedOrgResults[i]["Ready"],
884
+ ]
885
+ )
886
+
887
+ sorted_OrgResults = sorted(OrgResults, key=lambda k: (k["AccountId"], k["Region"]))
888
+
889
+ for i in sorted_OrgResults:
890
+ if pSkipAccounts is not None and i["AccountId"] in pSkipAccounts:
891
+ y.add_row(
892
+ [
893
+ i["AccountId"],
894
+ i["Region"],
895
+ "N/A",
896
+ "N/A",
897
+ "N/A",
898
+ "N/A",
899
+ "N/A",
900
+ "N/A",
901
+ "N/A",
902
+ "N/A",
903
+ "N/A",
904
+ "N/A",
905
+ "Skipped",
906
+ ]
907
+ )
908
+ else:
909
+ # x.add_row([i['AccountId'], i['Region'], i['IssuesFound'], i['IssuesFixed'], i['Ready']])
910
+ y.add_row(
911
+ [
912
+ i["AccountId"],
913
+ i["Region"],
914
+ i["Step0"]["IssuesFound"] - i["Step0"]["IssuesFixed"],
915
+ i["Step1"]["IssuesFound"] - i["Step1"]["IssuesFixed"],
916
+ i["Step2"]["IssuesFound"] - i["Step2"]["IssuesFixed"],
917
+ i["Step3"]["IssuesFound"] - i["Step3"]["IssuesFixed"],
918
+ i["Step4"]["IssuesFound"] - i["Step4"]["IssuesFixed"],
919
+ i["Step5"]["IssuesFound"] - i["Step5"]["IssuesFixed"],
920
+ i["Step6"]["IssuesFound"] - i["Step6"]["IssuesFixed"],
921
+ i["Step7"]["IssuesFound"] - i["Step7"]["IssuesFixed"],
922
+ i["Step8"]["IssuesFound"] - i["Step8"]["IssuesFixed"],
923
+ i["Step9"]["IssuesFound"] - i["Step9"]["IssuesFixed"],
924
+ i["Step10"]["IssuesFound"] - i["Step10"]["IssuesFixed"],
925
+ i["Step0"]["Success"]
926
+ and i["Step1"]["Success"]
927
+ and i["Step2"]["Success"]
928
+ and i["Step3"]["Success"]
929
+ and i["Step4"]["Success"]
930
+ and i["Step5"]["Success"]
931
+ and i["Step6"]["Success"]
932
+ and i["Step7"]["Success"]
933
+ and i["Step8"]["Success"]
934
+ and i["Step9"]["Success"]
935
+ and i["Step10"]["Success"],
936
+ ]
937
+ )
938
+ print(
939
+ "The following table represents the accounts looked at, and whether they are ready to be incorporated into a Control Tower environment."
940
+ )
941
+ print()
942
+ if aws_acct.AccountType.lower() == "root" and (
943
+ pChildAccountList is None or aws_acct.acct_number in pChildAccountList
944
+ ):
945
+ print(
946
+ f"Please note that for the Org Root account {aws_acct.acct_number}, the number of issues found for 'Org Config' will mistakenly show as 1 per region, since these issues are checked on a per-region basis."
947
+ )
948
+ print(
949
+ f"Additionally, issues found with 'Roles', though global, will show as regional as well. This will be remedied in future versions of this script."
950
+ )
951
+ print(x)
952
+ print()
953
+ print(
954
+ "The following table represents the accounts looked at, and gives details under each type of issue as to what might prevent a successful migration of this account into a Control Tower environment."
955
+ )
956
+ print(y)
957
+
958
+ if verbose < 50:
959
+ for account in OrgResults:
960
+ print()
961
+ FixesWorked = account["IssuesFound"] - account["IssuesFixed"] == 0
962
+ if account["Ready"] and account["IssuesFound"] == 0:
963
+ print(
964
+ f"{Fore.GREEN}**** We've found NO issues that would hinder the adoption of account {account['AccountId']} ****{Fore.RESET}"
965
+ )
966
+ elif account["Ready"] and FixesWorked:
967
+ print(
968
+ f"{Fore.GREEN}We've found and fixed{Fore.RED}",
969
+ f"{account['IssuesFixed']}{Fore.RESET}",
970
+ f"{Fore.GREEN}issues that would have otherwise blocked the adoption of account {account['AccountId']}{Fore.RESET}",
971
+ )
972
+ else:
973
+ print(
974
+ f"{Fore.RED}Account # {account['AccountId']} has {account['IssuesFound'] - account['IssuesFixed']} issues that would hinder the adoption of this account{Fore.RESET}"
975
+ )
976
+ for step in account:
977
+ if step[:4] == "Step" and len(account[step]["ProblemsFound"]) > 0:
978
+ print(f"{Fore.LIGHTRED_EX}Issues Found for {step} in account {account['AccountId']}:{Fore.RESET}")
979
+ pprint(account[step]["ProblemsFound"])
980
+
981
+
982
+ def setup(fProfile, fRegions):
983
+ f_aws_acct = aws_acct_access(fProfile)
984
+ if not f_aws_acct.Success:
985
+ logging.error(f"Error: {f_aws_acct.ErrorType}")
986
+ print(f"{Fore.RED}\nThere was an error with profile {fProfile}. Unable to continue.\n{Fore.RESET}")
987
+ sys.exit(9)
988
+
989
+ if Quick:
990
+ f_RegionList = ["us-east-1"]
991
+ else:
992
+ # Control Tower now published its regions.
993
+ GlobalRegionList = Inventory_Modules.get_service_regions("controltower", faws_acct=f_aws_acct)
994
+ AllowedRegionList = Inventory_Modules.get_regions3(f_aws_acct, fRegions)
995
+ f_RegionList = intersection(GlobalRegionList, AllowedRegionList)
996
+
997
+ if pExplain:
998
+ explain_script()
999
+ sys.exit("Exiting after Script Explanation...")
1000
+ return f_aws_acct, f_RegionList
1001
+
1002
+
1003
+ def CT_CheckAccount(faws_acct):
1004
+ logging.info(f"Confirming that this profile {pProfile} represents a Management Account")
1005
+
1006
+ if faws_acct.AccountType.lower() == "root" and pChildAccountList is None:
1007
+ # Creates a list of the account numbers in the Org.
1008
+ ChildAccountList = [d["AccountId"] for d in faws_acct.ChildAccounts]
1009
+ print(
1010
+ f"Since you didn't specify a specific account, we'll check all {len(faws_acct.ChildAccounts)} accounts in the Org."
1011
+ )
1012
+ elif aws_acct.AccountType.lower() == "root" and pChildAccountList is not None:
1013
+ print(
1014
+ f"Account {faws_acct.acct_number} is a {faws_acct.AccountType} account.\n"
1015
+ f"We're specifically checking to validate that account{'' if len(pChildAccountList) == 1 else 's'} {pChildAccountList} can be adopted into the Landing Zone"
1016
+ )
1017
+ ChildAccountList = pChildAccountList
1018
+ else:
1019
+ sys.exit(
1020
+ f"Account {faws_acct.acct_number} is a {faws_acct.AccountType} account.\n"
1021
+ f" This script should be run with Management Account credentials."
1022
+ )
1023
+
1024
+ print()
1025
+
1026
+ if pSkipAccounts is not None:
1027
+ for account_to_skip in pSkipAccounts:
1028
+ ChildAccountList.remove(account_to_skip)
1029
+
1030
+ print(f"Beginning to evaluate the Org and Accounts to see if they're ready to deploy Control Tower")
1031
+ f_OrgResults = DoThreadedAccountSteps(ChildAccountList, faws_acct, FixRun, RegionList)
1032
+
1033
+ if pSkipAccounts is not None:
1034
+ for MemberAccount in pSkipAccounts:
1035
+ f_OrgResults.append(
1036
+ {
1037
+ "AccountId": MemberAccount,
1038
+ "Region": "None",
1039
+ "IssuesFound": "N/A",
1040
+ "IssuesFixed": "N/A",
1041
+ "Ready": "Skipped",
1042
+ }
1043
+ )
1044
+
1045
+ ####
1046
+ # Summary at the end
1047
+ ####
1048
+
1049
+ f_SummarizedOrgResults = summarizeOrgResults(f_OrgResults)
1050
+
1051
+ return f_OrgResults, f_SummarizedOrgResults
1052
+
1053
+
1054
+ ###################
1055
+ ExplainMessage = """
1056
+
1057
+ Objective: This script aims to identify issues and make it easier to "adopt" an existing account into a Control Tower environment.
1058
+
1059
+ 0. The targeted account MUST allow the Management account access into the Child IAM role called "AWSControlTowerExecution" or another coded role, so that we have access to do read-only operations (by default).
1060
+ 0a. There must be an "AWSControlTowerExecution" role present in the account so that StackSets can assume it and deploy stack instances. This role must trust the Organizations Management account or at least the necessary Lambda functions.
1061
+ ** TODO ** - update the JSON to be able to update the role to ensure it trusts the least privileged roles from management account, instead of the whole account.
1062
+ 0b. STS must be active in all regions checked. You can check from the Account Settings page in IAM. Since we're using STS to connect to the account from the Management, this requirement is checked by successfully completing step 0.
1063
+
1064
+ 1. We're using this step to check to see if your Org has Config Service enabled at the Org level.
1065
+
1066
+ 2. There must be no active config channel and recorder in the account as “there can be only one” of each.
1067
+ This must also be deleted via CLI, not console, switching config off in the console is NOT good enough and just disables it. To Delete the delivery channel and the configuration recorder (can be done via CLI and Python script only):
1068
+ aws configservice describe-delivery-channels
1069
+ aws configservice describe-delivery-channel-status
1070
+ aws configservice describe-configuration-recorders
1071
+ aws configservice stop-configuration-recorder --configuration-recorder-name <NAME-FROM-DESCRIBE-OUTPUT>
1072
+ aws configservice delete-delivery-channel --delivery-channel-name <NAME-FROM-DESCRIBE-OUTPUT>
1073
+ aws configservice delete-configuration-recorder --configuration-recorder-name <NAME-FROM-DESCRIBE-OUTPUT
1074
+
1075
+ 3. The account must not have a Cloudtrail Trail name with 'ControlTower' in the name ("aws-controltower-BaselineCloudTrail")
1076
+
1077
+ 4. The account must not have a pending guard duty invite. You can check from the Guard Duty Console
1078
+
1079
+ 5. The account must be part of the Organization and the email address being entered into the CT parameters must match the account.
1080
+ If you try to add an email from an account which is not part of the Org, you will get an error that you are not using a unique email address. If it’s part of the Org, CT just finds the account and uses the CFN roles.
1081
+ ** TODO ** - If the existing account will be a child account in the Organization, use the Account Factory and enter the appropriate email address.
1082
+
1083
+ 6. The existing account can not be in any of the CT-managed Organizations OUs. By default, these OUs are Core and Applications, but the customer may have chosen different or additional OUs to manage by CT.
1084
+ -- not yet implemented --
1085
+
1086
+ 7. SNS topics name containing "ControlTower"
1087
+ 8. Lambda Functions name containing "ControlTower"
1088
+ 9. Role name containing "ControlTower"
1089
+ Bucket created for AWS Config -- not yet implemented
1090
+ SNS topic created for AWS Config -- not yet implemented
1091
+ 10. CloudWatch Log group containing "aws-controltower/CloudTrailLogs" -- not yet implemented --
1092
+
1093
+ """
1094
+
1095
+ ERASE_LINE = "\x1b[2K"
1096
+ begin_time = time()
1097
+
1098
+ if __name__ == "__main__":
1099
+ aws_acct, RegionList = setup(pProfile, pRegions)
1100
+ OrgResults, SummarizedOrgResults = CT_CheckAccount(aws_acct)
1101
+ display_results()
1102
+
1103
+ if pTiming:
1104
+ print(ERASE_LINE)
1105
+ print(f"{Fore.GREEN}This script took {time() - begin_time:.2f} seconds{Fore.RESET}")
1106
+ print("Thanks for using this script...")
1107
+ print()