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.
- conftest.py +26 -0
- jupyter-agent/.env.template +2 -0
- jupyter-agent/.gitattributes +35 -0
- jupyter-agent/README.md +16 -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/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/__init__.py +88 -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/Inventory_Modules.py +6130 -0
- runbooks/inventory/LandingZone/delete_lz.py +1075 -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/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/script_test_data.py +0 -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 +785 -0
- runbooks/organizations/__init__.py +12 -0
- runbooks/organizations/manager.py +374 -0
- runbooks/security_baseline/README.md +324 -0
- runbooks/security_baseline/checklist/alternate_contacts.py +8 -1
- runbooks/security_baseline/checklist/bucket_public_access.py +4 -1
- runbooks/security_baseline/checklist/cloudwatch_alarm_configuration.py +9 -2
- runbooks/security_baseline/checklist/guardduty_enabled.py +9 -2
- runbooks/security_baseline/checklist/multi_region_instance_usage.py +5 -1
- runbooks/security_baseline/checklist/root_access_key.py +6 -1
- runbooks/security_baseline/config-origin.json +1 -1
- runbooks/security_baseline/config.json +1 -1
- runbooks/security_baseline/permission.json +1 -1
- runbooks/security_baseline/report_generator.py +10 -2
- runbooks/security_baseline/report_template_en.html +7 -7
- runbooks/security_baseline/report_template_jp.html +7 -7
- runbooks/security_baseline/report_template_kr.html +12 -12
- runbooks/security_baseline/report_template_vn.html +7 -7
- runbooks/security_baseline/requirements.txt +7 -0
- runbooks/security_baseline/run_script.py +8 -2
- runbooks/security_baseline/security_baseline_tester.py +10 -2
- runbooks/security_baseline/utils/common.py +5 -1
- runbooks/utils/__init__.py +204 -0
- runbooks-0.6.1.dist-info/METADATA +373 -0
- runbooks-0.6.1.dist-info/RECORD +237 -0
- {runbooks-0.2.5.dist-info → runbooks-0.6.1.dist-info}/WHEEL +1 -1
- runbooks-0.6.1.dist-info/entry_points.txt +7 -0
- runbooks-0.6.1.dist-info/licenses/LICENSE +201 -0
- runbooks-0.6.1.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
@@ -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()
|