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,575 @@
1
+ #!/usr/bin/env python3
2
+
3
+ """
4
+ AWS Service Catalog Provisioned Products Discovery and Management Script
5
+
6
+ This script provides comprehensive discovery, analysis, and optional cleanup capabilities for
7
+ AWS Service Catalog provisioned products within single AWS accounts. It's designed for enterprise
8
+ cloud governance teams who need visibility into Service Catalog product lifecycle management,
9
+ CloudFormation stack reconciliation, and automated cleanup capabilities for failed or orphaned products.
10
+
11
+ Key Features:
12
+ - Service Catalog provisioned product discovery with comprehensive metadata extraction
13
+ - CloudFormation stack reconciliation and correlation analysis
14
+ - Automated detection of errored, tainted, or orphaned products
15
+ - Optional automated cleanup capabilities with safety controls
16
+ - Account status correlation and suspended account detection
17
+ - Fragment-based search for targeted product discovery and analysis
18
+ - Enterprise reporting with CSV export and structured output
19
+
20
+ Enterprise Use Cases:
21
+ - Service Catalog governance and product lifecycle management
22
+ - CloudFormation stack reconciliation and orphaned resource detection
23
+ - Automated cleanup of failed or errored provisioned products
24
+ - Account provisioning audit and compliance reporting
25
+ - Cost optimization through orphaned resource identification
26
+ - Service Catalog product portfolio analysis and governance
27
+ - Multi-account provisioning oversight and standardization
28
+
29
+ Service Catalog Product Analysis Features:
30
+ - Comprehensive provisioned product enumeration with status tracking
31
+ - CloudFormation stack correlation and dependency analysis
32
+ - Account creation tracking through Service Catalog account vending
33
+ - Product version and artifact analysis for governance oversight
34
+ - Error detection and automated remediation recommendations
35
+ - Orphaned product identification for cleanup and cost optimization
36
+
37
+ Security Considerations:
38
+ - Uses single profile authentication for focused Service Catalog operations
39
+ - Implements proper error handling for authorization failures
40
+ - Supports optional deletion operations with explicit safety controls
41
+ - Respects Service Catalog permissions and regional access constraints
42
+ - Provides comprehensive audit trail through detailed logging
43
+ - Sensitive provisioning information handling with appropriate access controls
44
+
45
+ Governance and Compliance Features:
46
+ - Service Catalog product lifecycle tracking for organizational oversight
47
+ - CloudFormation stack correlation for governance and compliance
48
+ - Account provisioning tracking through Service Catalog automation
49
+ - Product portfolio analysis for standardization and governance
50
+ - Error detection and remediation for operational excellence
51
+
52
+ Performance Considerations:
53
+ - Multi-threaded processing for concurrent CloudFormation stack reconciliation
54
+ - Progress tracking for operational visibility during large-scale operations
55
+ - Efficient credential management for Service Catalog operations
56
+ - Memory-optimized data structures for large product inventories
57
+ - Queue-based worker architecture for scalable stack reconciliation
58
+
59
+ Deletion Safety Features:
60
+ - Explicit deletion flags (+delete) for Service Catalog product cleanup
61
+ - Error and taint status detection for safe automated cleanup
62
+ - CloudFormation stack validation before product termination
63
+ - Comprehensive logging of deletion operations for audit trails
64
+ - Safety controls to prevent accidental product termination
65
+
66
+ Threading Architecture:
67
+ - Worker thread pool for concurrent CloudFormation stack reconciliation
68
+ - Queue-based task distribution for efficient stack discovery
69
+ - Thread-safe error handling and progress tracking
70
+ - Graceful degradation for stack access failures
71
+
72
+ Dependencies:
73
+ - boto3/botocore for AWS Service Catalog and CloudFormation API interactions
74
+ - account_class for AWS account access management
75
+ - ArgumentsClass for standardized CLI argument parsing
76
+ - Inventory_Modules for common utility functions and stack discovery
77
+ - colorama for enhanced output formatting
78
+ - tqdm for progress tracking
79
+
80
+ Future Enhancements:
81
+ - Multi-account Service Catalog product discovery across organizations
82
+ - Integration with AWS Config for product configuration monitoring
83
+ - Product optimization recommendations for cost and governance
84
+ - Automated product lifecycle management and policy enforcement
85
+
86
+ Author: AWS CloudOps Team
87
+ Version: 2023.08.09
88
+ """
89
+
90
+ import logging
91
+ import sys
92
+ from queue import Queue
93
+ from threading import Thread
94
+ from time import time
95
+
96
+ import Inventory_Modules
97
+ from account_class import aws_acct_access
98
+ from ArgumentsClass import CommonArguments
99
+ from botocore.exceptions import ClientError, ProfileNotFound, UnknownCredentialError, UnknownRegionError
100
+ from colorama import Fore, init
101
+ from Inventory_Modules import display_results
102
+ from tqdm.auto import tqdm
103
+
104
+ init()
105
+ __version__ = "2023.08.09"
106
+
107
+ parser = CommonArguments()
108
+ parser.singleprofile()
109
+ parser.singleregion()
110
+ parser.extendedargs()
111
+ parser.save_to_file()
112
+ parser.verbosity()
113
+ parser.timing()
114
+ parser.version(__version__)
115
+ parser.my_parser.add_argument(
116
+ "+d",
117
+ "+delete",
118
+ dest="DeletionRun",
119
+ action="store_true",
120
+ help="This will delete the SC Provisioned Products found to be in error, or without active CloudFormation stacks - without any opportunity to confirm. Be careful!!",
121
+ )
122
+ parser.my_parser.add_argument(
123
+ "-f",
124
+ "--fragment",
125
+ dest="Fragment",
126
+ metavar="Fragment of the Product name or the product id",
127
+ default=None,
128
+ help="Unique fragment of the product name or the product id",
129
+ )
130
+ parser.my_parser.add_argument(
131
+ "-e",
132
+ "--exact",
133
+ dest="Exact",
134
+ action="store_true",
135
+ help="Use this flag to make sure that ONLY the string you specified will be identified",
136
+ )
137
+ args = parser.my_parser.parse_args()
138
+
139
+ pProfile = args.Profile
140
+ pRegion = args.Region
141
+ pTiming = args.Time
142
+ pFileName = args.Filename
143
+ pFragment = args.Fragment
144
+ pExact = args.Exact
145
+ verbose = args.loglevel
146
+ DeletionRun = args.DeletionRun
147
+ # Setup logging levels
148
+ logging.basicConfig(level=verbose, format="[%(filename)s:%(lineno)s - %(funcName)20s() ] %(message)s")
149
+ logging.getLogger("boto3").setLevel(logging.CRITICAL)
150
+ logging.getLogger("botocore").setLevel(logging.CRITICAL)
151
+ logging.getLogger("s3transfer").setLevel(logging.CRITICAL)
152
+ logging.getLogger("urllib3").setLevel(logging.CRITICAL)
153
+ logging.getLogger("botocore").setLevel(logging.CRITICAL)
154
+
155
+
156
+ ##########################
157
+
158
+
159
+ def sort_by_email(elem):
160
+ return elem("AccountEmail")
161
+
162
+
163
+ def find_account_stacksets(faws_acct, f_SCProducts, fRegion=None, fstacksetname=None):
164
+ """
165
+ Note that this function takes a list of Credentials and checks for stacks in every account it has creds for
166
+ """
167
+
168
+ class CheckProducts(Thread):
169
+ def __init__(self, queue):
170
+ Thread.__init__(self)
171
+ self.queue = queue
172
+
173
+ def run(self):
174
+ while True:
175
+ # Get the work from the queue and expand the tuple
176
+ c_sc_product, c_region, c_fstacksetname, c_PlacesToLook, c_PlaceCount = self.queue.get()
177
+ logging.info(f"De-queued info for SC Product: {c_sc_product['SCPName']}")
178
+ logging.info(f"{Fore.RED}Checking {PlaceCount} of {len(f_SCProducts)} products{Fore.RESET}")
179
+ CFNresponse = Inventory_Modules.find_stacks3(faws_acct, pRegion, c_sc_product["SCPId"])
180
+ logging.info(
181
+ f"There are {len(CFNresponse)} matches for SC Provisioned Product Name {c_sc_product['SCPName']}"
182
+ )
183
+ try:
184
+ if len(CFNresponse) > 0:
185
+ stack_info = client_cfn.describe_stacks(StackName=CFNresponse[0]["StackName"])
186
+ # The above command fails if the stack found (by the find_stacks3 function) has been deleted
187
+ # The following section determines the NEW Account's AccountEmail and AccountID
188
+ AccountEmail = AccountID = AccountStatus = None
189
+ if (
190
+ "Parameters" in stack_info["Stacks"][0].keys()
191
+ and len(stack_info["Stacks"][0]["Parameters"]) > 0
192
+ ):
193
+ for y in range(len(stack_info["Stacks"][0]["Parameters"])):
194
+ if stack_info["Stacks"][0]["Parameters"][y]["ParameterKey"] == "AccountEmail":
195
+ AccountEmail = stack_info["Stacks"][0]["Parameters"][y]["ParameterValue"]
196
+ logging.info(f"Account Email is {AccountEmail}")
197
+ if "Outputs" in stack_info["Stacks"][0].keys():
198
+ for y in range(len(stack_info["Stacks"][0]["Outputs"])):
199
+ logging.info(
200
+ f"Output Key {stack_info['Stacks'][0]['Outputs'][y]['OutputKey']} "
201
+ f"for stack {CFNresponse[0]['StackName']} is {stack_info['Stacks'][0]['Outputs'][y]['OutputValue']}"
202
+ )
203
+ if stack_info["Stacks"][0]["Outputs"][y]["OutputKey"] == "AccountID":
204
+ AccountID = stack_info["Stacks"][0]["Outputs"][y]["OutputValue"]
205
+ if AccountID in AccountHistogram.keys():
206
+ AccountStatus = AccountHistogram[AccountID]
207
+ else:
208
+ AccountStatus = "Closed"
209
+ logging.info(f"{Fore.RED}Found the Account ID: {AccountID}{Fore.RESET}")
210
+ if AccountID in SuspendedAccounts:
211
+ logging.error(
212
+ f"{Fore.RED}Account ID {AccountID} has been suspended{Fore.RESET}"
213
+ )
214
+ break
215
+ else:
216
+ logging.error("Outputs key present, but no account ID")
217
+ AccountID = None
218
+ AccountStatus = None
219
+ else:
220
+ logging.error("No Outputs key present")
221
+ AccountID = "None"
222
+ AccountStatus = "None"
223
+ CFNStackName = CFNresponse[0]["StackName"]
224
+ CFNStackStatus = CFNresponse[0]["StackStatus"]
225
+ # AccountEmail should have been assigned in the 'Parameters' if-then above
226
+ # AccountID should have been assigned in the 'Outputs' if-then above
227
+ # AccountStatus should have been assigned in the 'Outputs' if-then above
228
+ else: # This takes effect when CFNResponse can't find any stacks with the Service Catalog Product ID
229
+ CFNStackName = CFNStackStatus = AccountID = AccountEmail = AccountStatus = None
230
+ logging.error(
231
+ f"AccountID: {AccountID} | AccountEmail: {AccountEmail} | CFNStackName: {CFNStackName} | CFNStackStatus: {CFNStackStatus} | SC Product: {c_sc_product['SCPName']}"
232
+ )
233
+ SCP2Stacks.append(
234
+ {
235
+ "SCProductName": c_sc_product["SCPName"],
236
+ "SCProductId": c_sc_product["SCPId"],
237
+ "SCStatus": c_sc_product["SCPStatus"],
238
+ "ProvisioningArtifactName": c_sc_product["ProvisioningArtifactName"],
239
+ "CFNStackName": CFNStackName,
240
+ "CFNStackStatus": CFNStackStatus,
241
+ "AccountEmail": AccountEmail,
242
+ "AccountID": AccountID,
243
+ "AccountStatus": AccountStatus,
244
+ }
245
+ )
246
+ except ClientError as my_Error:
247
+ if str(my_Error).find("ValidationError") > 0:
248
+ print("Validation Failure ")
249
+ print(
250
+ f"Validation Failure in profile {pProfile} looking for stack {CFNresponse[0]['StackName']} with status of {CFNresponse[0]['StackStatus']}"
251
+ )
252
+ elif str(my_Error).find("AccessDenied") > 0:
253
+ print(f"{pProfile}: Access Denied Failure ")
254
+ else:
255
+ print(f"{pProfile}: Other kind of failure ")
256
+ print(my_Error)
257
+ finally:
258
+ logging.info(
259
+ f"Finished finding product {c_sc_product['SCPName']} - {c_PlaceCount} / {c_PlacesToLook}"
260
+ )
261
+ pbar.update()
262
+ self.queue.task_done()
263
+
264
+ if fRegion is None:
265
+ fRegion = ["us-east-1"]
266
+ checkqueue = Queue()
267
+
268
+ # AllSubnets = []
269
+ PlaceCount = 0
270
+ PlacesToLook = WorkerThreads = min(len(f_SCProducts), 10)
271
+
272
+ pbar = tqdm(
273
+ desc=f"Reconciling SC Products with CloudFormation Stacks in accounts",
274
+ leave=True,
275
+ total=len(f_SCProducts),
276
+ unit=" products",
277
+ )
278
+
279
+ # Create and start the worker threads
280
+ for x in range(WorkerThreads):
281
+ worker = CheckProducts(checkqueue)
282
+ # Setting daemon to True will let the main thread exit even though the workers are blocking
283
+ worker.daemon = True
284
+ worker.start()
285
+
286
+ for SCProduct in SCProducts:
287
+ logging.info(f"Checking service catalog product: {SCProduct['SCPName']}")
288
+ try:
289
+ # print(f"{ERASE_LINE}Queuing account {credential['AccountId']} in region {region}", end='\r')
290
+ checkqueue.put((SCProduct, fRegion, fstacksetname, PlacesToLook, PlaceCount))
291
+ PlaceCount += 1
292
+ except ClientError as my_Error:
293
+ if "AuthFailure" in str(my_Error):
294
+ logging.error(f"Authorization Failure accessing account {faws_acct.acct_number} in {fRegion} region")
295
+ logging.warning(f"It's possible that the region {fRegion} hasn't been opted-into")
296
+ pass
297
+ checkqueue.join()
298
+ pbar.close()
299
+ return SCP2Stacks
300
+
301
+
302
+ ##########################
303
+
304
+
305
+ """
306
+ TODO:
307
+ - Need to add a parameter that allows the user to close / terminate the SC products for only the closed / suspended accounts (very safe)
308
+ - Add a parameter that refuses to close/ terminate accounts with active VPCs?
309
+ """
310
+
311
+ """
312
+ Significant Variable Explanation:
313
+ 'AcctList' holds a list of accounts within this Org.
314
+ 'SCresponse' holds a native list of Service Catalog Provisioned Products supplied by the native API.
315
+ 'SCProducts' holds a refined list of the Service Catalog Provisioned Products from the 'SCresponse' list, but only the fields we're interested in.
316
+ ** TODO: I duplicated this listing, in case later I decided to add some additional useful fields to the dict.
317
+ 'SCP2Stacks' holds a list of the CloudFormation Stacks in this account that *match* the Provisioned Products.
318
+ ** TODO: This list should hold *all* stacks and then we could find stacks for accounts that no longer exist.
319
+ 'AccountHistogram' holds the list of accounts (the account numbers are the keys in this dict) and the number of SC products that are created for this account is the value of that key.
320
+ """
321
+ begin_time = time()
322
+
323
+ ERASE_LINE = "\x1b[2K"
324
+
325
+ print()
326
+
327
+ SCP2Stacks = []
328
+ SCProducts = []
329
+ try:
330
+ if pProfile is None:
331
+ aws_acct = aws_acct_access()
332
+ else:
333
+ aws_acct = aws_acct_access(fProfile=pProfile, fRegion=pRegion)
334
+ if aws_acct.ErrorType is not None and aws_acct.ErrorType.lower() == "invalid region":
335
+ raise UnknownRegionError(region_name=pRegion, error_msg=aws_acct.ErrorType)
336
+ session_aws = aws_acct.session
337
+ except UnknownCredentialError as my_Error:
338
+ print(
339
+ f"Single profile '{pProfile}' doesn't seem to exist, or work. Please check your credentials-1.\n"
340
+ f"Error Message: {my_Error}"
341
+ )
342
+ print()
343
+ sys.exit(1)
344
+ except ProfileNotFound as my_Error:
345
+ print(
346
+ f"Single profile '{pProfile}' doesn't seem to exist, or work. Please check your credentials-1.\n"
347
+ f"Error Message: {my_Error}"
348
+ )
349
+ print()
350
+ sys.exit(1)
351
+ except AttributeError as my_Error:
352
+ print(
353
+ f"Single profile '{pProfile}' doesn't seem to exist, or work. Please check your credentials-2.\n"
354
+ f"Error Message: {my_Error}"
355
+ )
356
+ print()
357
+ sys.exit(1)
358
+ except (ConnectionError, UnknownRegionError) as my_Error:
359
+ print(
360
+ f"There is no single region '{pRegion}'. Please re-run and specify a valid AWS region for this Org account.\n"
361
+ f"Error Message: {my_Error}"
362
+ )
363
+ print()
364
+ sys.exit(1)
365
+ client_org = aws_acct.session.client("organizations")
366
+ client_cfn = aws_acct.session.client("cloudformation")
367
+
368
+ AccountHistogram = {}
369
+ SuspendedAccounts = []
370
+ ClosedAccts = []
371
+
372
+
373
+ def main():
374
+ client_sc = aws_acct.session.client("servicecatalog")
375
+ ErroredSCProductExists = False
376
+ # Creates AccountHistogram which tracks which accounts have SC Products built for them, and which do not.
377
+ # The Histogram is initialized with ALL accounts and their status.
378
+ # It's then later overwritten with the count of how many SC Products relate to that account.
379
+ # So the accounts which only have a status as their value - are missing the SC Product
380
+ for account in aws_acct.ChildAccounts:
381
+ AccountHistogram[account["AccountId"]] = account["AccountStatus"]
382
+ if account["AccountStatus"] == "SUSPENDED":
383
+ SuspendedAccounts.append(account["AccountId"])
384
+
385
+ # Find the provisioned product ids
386
+ AVM_prod_id = None
387
+ if pFragment is None:
388
+ result = client_sc.search_products_as_admin()
389
+ prod_ids = result["ProductViewDetails"]
390
+ # while 'NextPageToken' in result.keys() and 'NextPageToken' is not None:
391
+ # result = client_sc.search_products_as_admin()
392
+ # prod_ids.extend(result['ProductViewDetails'])
393
+ # if verbose < 50:
394
+ # print(f"{ERASE_LINE}Found {len(result['ProductViewDetails'])} products | Total found: {len(prod_ids)}", end='\r')
395
+ # print()
396
+ for product in prod_ids:
397
+ if product["ProductViewSummary"]["Name"].find("Account-Vending-Machine") > 0:
398
+ AVM_prod_id = product["ProductViewSummary"]["ProductId"]
399
+ elif pFragment is not None and not pExact:
400
+ result = client_sc.search_products_as_admin()
401
+ prod_ids = result["ProductViewDetails"]
402
+ # while 'NextPageToken' in result.keys() and 'NextPageToken' is not None:
403
+ # result = client_sc.search_products_as_admin()
404
+ # prod_ids.extend(result['ProductViewDetails'])
405
+ # if verbose < 50:
406
+ # print(f"{ERASE_LINE}Found {len(result['ProductViewDetails'])} products | Total found: {len(prod_ids)}", end='\r')
407
+ for product in prod_ids:
408
+ if (
409
+ product["ProductViewSummary"]["Name"].find(pFragment) > -1
410
+ or product["ProductViewSummary"]["ProductId"].find(pFragment) > -1
411
+ ):
412
+ AVM_prod_id = product["ProductViewSummary"]["ProductId"]
413
+ elif pFragment is not None and pExact:
414
+ AVM_prod_id = pFragment
415
+
416
+ # Finds Service Catalog Products and reconciles them to the account they belong to
417
+ try:
418
+ # The function below takes the parent account (object),
419
+ # the stack statuses we're trying to find, the number of products to find at once, and possibly the product id (if provided)
420
+ SCresponse = Inventory_Modules.find_sc_products3(aws_acct, "All", 10, AVM_prod_id)
421
+ logging.warning("A list of the SC Products found:")
422
+ for i in range(len(SCresponse)):
423
+ logging.warning(f"SC Product Name {SCresponse[i]['Name']} | SC Product Status {SCresponse[i]['Status']}")
424
+ SCProducts.append(
425
+ {
426
+ "SCPName": SCresponse[i]["Name"],
427
+ "SCPId": SCresponse[i]["Id"],
428
+ "SCPStatus": SCresponse[i]["Status"],
429
+ "SCPRecordId": SCresponse[i]["LastRecordId"],
430
+ "ProvisioningArtifactName": SCresponse[i]["ProvisioningArtifactName"],
431
+ }
432
+ )
433
+ if SCresponse[i]["Status"] == "ERROR" or SCresponse[i]["Status"] == "TAINTED":
434
+ ErroredSCProductExists = True
435
+
436
+ CFNStacks = Inventory_Modules.find_stacks3(aws_acct, pRegion, f"SC-{aws_acct.acct_number}")
437
+
438
+ if pTiming:
439
+ print(
440
+ f"{Fore.GREEN}Finding stacks in your account has taken {time() - begin_time:.2f} seconds now...{Fore.RESET}"
441
+ )
442
+ milestone1 = time()
443
+
444
+ SCresponse = None
445
+
446
+ # TODO: Create a queue - place the SCProducts on that queue, one by one, and let this code run in a multi-thread worker
447
+ # def find_account_stacksets(faws_acct, f_SCProducts, fRegion=None, fstacksetname=None):
448
+ CFNresponse = find_account_stacksets(aws_acct, SCProducts, pRegion, pFragment)
449
+
450
+ # TODO: We should list out Suspended accounts in the SCP2Stacks readout at the end - in case any accounts have
451
+ # both a provisioned product, but are also suspended.
452
+ # Do any of the account numbers show up more than once in this list?
453
+ # We initialize the listing from the full list of accounts in the Org.
454
+
455
+ if pTiming:
456
+ print(
457
+ f"{Fore.GREEN}Reconciling products to the CloudFormation stacks took {time() - milestone1:.2f} seconds{Fore.RESET}"
458
+ )
459
+
460
+ # TODO: This might not be a good idea, if it misses the stacks which are associated with accounts no longer within the Org.
461
+ # We add a one to each account which is represented within the Stacks listing. This allows us to catch duplicates
462
+ # and also accounts which do not have a stack associated.
463
+ # Note it does *not* help us catch stacks associated with an account that's been removed.
464
+ for i in range(len(CFNresponse)):
465
+ if CFNresponse[i]["AccountID"] == "None":
466
+ continue
467
+ elif not CFNresponse[i]["AccountID"] in AccountHistogram.keys():
468
+ CFNresponse[i]["AccountStatus"] = "CLOSED"
469
+ ClosedAccts.append(CFNresponse[i]["AccountID"])
470
+ else:
471
+ # This means that the value is still either "ACTIVE" or "SUSPENDED"
472
+ if isinstance(AccountHistogram[CFNresponse[i]["AccountID"]], str):
473
+ AccountHistogram[CFNresponse[i]["AccountID"]] = 1
474
+ else:
475
+ AccountHistogram[CFNresponse[i]["AccountID"]] += 1
476
+
477
+ display_dict = {
478
+ "AccountID": {"DisplayOrder": 1, "Heading": "Account Id", "Condition": ["None"]},
479
+ "SCProductName": {"DisplayOrder": 2, "Heading": "SC Product Name"},
480
+ "ProvisioningArtifactName": {"DisplayOrder": 3, "Heading": "Version"},
481
+ "CFNStackName": {"DisplayOrder": 4, "Heading": "CFN Stack Name"},
482
+ "SCStatus": {"DisplayOrder": 5, "Heading": "SC Status", "Condition": ["ERROR", "TAINTED"]},
483
+ "CFNStackStatus": {"DisplayOrder": 6, "Heading": "CFN Stack Status"},
484
+ "AccountStatus": {"DisplayOrder": 7, "Heading": "Account Status", "Condition": ["CLOSED"]},
485
+ "AccountEmail": {"DisplayOrder": 8, "Heading": "Account Email"},
486
+ }
487
+ display_results(CFNresponse, display_dict, "None", pFileName)
488
+
489
+ except ClientError as my_Error:
490
+ if "AuthFailure" in str(my_Error):
491
+ print(f"{pProfile}: Authorization Failure ")
492
+ elif str(my_Error).find("AccessDenied") > 0:
493
+ print(f"{pProfile}: Access Denied Failure ")
494
+ else:
495
+ print(f"{pProfile}: Other kind of failure ")
496
+ print(my_Error)
497
+
498
+ print()
499
+ for acctnum in AccountHistogram.keys():
500
+ if AccountHistogram[acctnum] == 1:
501
+ pass # This is the desired state, so no user output is needed.
502
+ elif AccountHistogram[acctnum] == "SUSPENDED":
503
+ print(
504
+ f"{Fore.RED}While there is no SC Product associated, account number {acctnum} appears to be a suspended account.{Fore.RESET}"
505
+ )
506
+ elif (
507
+ AccountHistogram[acctnum] == "ACTIVE"
508
+ ): # This compare needs to be separate from below, since we can't compare a string with a "<" operator
509
+ print(
510
+ f"Account Number {Fore.RED}{acctnum}{Fore.RESET} appears to have no SC Product associated with it. This can be a problem"
511
+ )
512
+ elif AccountHistogram[acctnum] < 1:
513
+ print(
514
+ f"Account Number {Fore.RED}{acctnum}{Fore.RESET} appears to have no SC Product associated with it. This can be a problem"
515
+ )
516
+ elif AccountHistogram[acctnum] > 1:
517
+ print(
518
+ f"Account Number {Fore.RED}{acctnum}{Fore.RESET} appears to have multiple SC Products associated with it. This can be a problem"
519
+ )
520
+
521
+ if ErroredSCProductExists:
522
+ print()
523
+ print("You probably want to remove the following SC Products:")
524
+ session_sc = aws_acct.session
525
+ client_sc = session_sc.client("servicecatalog")
526
+ for i in range(len(CFNresponse)):
527
+ if (
528
+ (CFNresponse[i]["SCStatus"] == "ERROR") or (CFNresponse[i]["CFNStackName"] == "None")
529
+ ) and not DeletionRun:
530
+ print(
531
+ f"aws servicecatalog terminate-provisioned-product --provisioned-product-id {CFNresponse[i]['SCProductId']} --ignore-errors",
532
+ end="",
533
+ )
534
+ # Finishes the line for display, based on whether they used a profile or not to run this command
535
+ if pProfile is None:
536
+ print()
537
+ elif pProfile is not None:
538
+ print(f" --profile {pProfile}")
539
+ elif (
540
+ (CFNresponse[i]["SCStatus"] == "ERROR") or (CFNresponse[i]["CFNStackName"] == "None")
541
+ ) and DeletionRun:
542
+ print(
543
+ f"Deleting Service Catalog Provisioned Product {CFNresponse[i]['SCProductName']} from account {aws_acct.acct_number}"
544
+ )
545
+ StackDelete = client_sc.terminate_provisioned_product(
546
+ ProvisionedProductId=CFNresponse[i]["SCProductId"],
547
+ IgnoreErrors=True,
548
+ )
549
+ logging.error("Result of Deletion: %s", StackDelete["RecordDetail"]["Status"])
550
+ if len(StackDelete["RecordDetail"]["RecordErrors"]) > 0:
551
+ logging.error("Error code: %s", StackDelete["RecordDetail"]["RecordErrors"][0]["Code"])
552
+ logging.error(
553
+ "Error description: %s", StackDelete["RecordDetail"]["RecordErrors"][0]["Description"]
554
+ )
555
+
556
+ print()
557
+ for i in AccountHistogram:
558
+ logging.info(
559
+ f"There are {AccountHistogram[i] if isinstance(AccountHistogram[i], int) else '0'} active SC products for Account ID: {i}"
560
+ )
561
+ end_time = time()
562
+ duration = end_time - begin_time
563
+ if pTiming:
564
+ print(f"{Fore.GREEN}This script took {duration:.2f} seconds{Fore.RESET}")
565
+ print(f"We found {len(aws_acct.ChildAccounts)} accounts within the Org")
566
+ print(f"We found {len(SCProducts)} Service Catalog Products")
567
+ print(f"We found {len(SuspendedAccounts)} Suspended accounts")
568
+ print(f"We found {len(ClosedAccts)} Closed Accounts that still have an SC product")
569
+ # print("We found {} Service Catalog Products with no account attached".format('Some Number'))
570
+ print("Thanks for using this script...")
571
+ print()
572
+
573
+
574
+ if __name__ == "__main__":
575
+ main()