runbooks 1.1.4__py3-none-any.whl → 1.1.6__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 (273) hide show
  1. runbooks/__init__.py +31 -2
  2. runbooks/__init___optimized.py +18 -4
  3. runbooks/_platform/__init__.py +1 -5
  4. runbooks/_platform/core/runbooks_wrapper.py +141 -138
  5. runbooks/aws2/accuracy_validator.py +812 -0
  6. runbooks/base.py +7 -0
  7. runbooks/cfat/assessment/compliance.py +1 -1
  8. runbooks/cfat/assessment/runner.py +1 -0
  9. runbooks/cfat/cloud_foundations_assessment.py +227 -239
  10. runbooks/cli/__init__.py +1 -1
  11. runbooks/cli/commands/cfat.py +64 -23
  12. runbooks/cli/commands/finops.py +1005 -54
  13. runbooks/cli/commands/inventory.py +135 -91
  14. runbooks/cli/commands/operate.py +9 -36
  15. runbooks/cli/commands/security.py +42 -18
  16. runbooks/cli/commands/validation.py +432 -18
  17. runbooks/cli/commands/vpc.py +81 -17
  18. runbooks/cli/registry.py +22 -10
  19. runbooks/cloudops/__init__.py +20 -27
  20. runbooks/cloudops/base.py +96 -107
  21. runbooks/cloudops/cost_optimizer.py +544 -542
  22. runbooks/cloudops/infrastructure_optimizer.py +5 -4
  23. runbooks/cloudops/interfaces.py +224 -225
  24. runbooks/cloudops/lifecycle_manager.py +5 -4
  25. runbooks/cloudops/mcp_cost_validation.py +252 -235
  26. runbooks/cloudops/models.py +78 -53
  27. runbooks/cloudops/monitoring_automation.py +5 -4
  28. runbooks/cloudops/notebook_framework.py +177 -213
  29. runbooks/cloudops/security_enforcer.py +125 -159
  30. runbooks/common/accuracy_validator.py +17 -12
  31. runbooks/common/aws_pricing.py +349 -326
  32. runbooks/common/aws_pricing_api.py +211 -212
  33. runbooks/common/aws_profile_manager.py +40 -36
  34. runbooks/common/aws_utils.py +74 -79
  35. runbooks/common/business_logic.py +126 -104
  36. runbooks/common/cli_decorators.py +36 -60
  37. runbooks/common/comprehensive_cost_explorer_integration.py +455 -463
  38. runbooks/common/cross_account_manager.py +197 -204
  39. runbooks/common/date_utils.py +27 -39
  40. runbooks/common/decorators.py +29 -19
  41. runbooks/common/dry_run_examples.py +173 -208
  42. runbooks/common/dry_run_framework.py +157 -155
  43. runbooks/common/enhanced_exception_handler.py +15 -4
  44. runbooks/common/enhanced_logging_example.py +50 -64
  45. runbooks/common/enhanced_logging_integration_example.py +65 -37
  46. runbooks/common/env_utils.py +16 -16
  47. runbooks/common/error_handling.py +40 -38
  48. runbooks/common/lazy_loader.py +41 -23
  49. runbooks/common/logging_integration_helper.py +79 -86
  50. runbooks/common/mcp_cost_explorer_integration.py +476 -493
  51. runbooks/common/mcp_integration.py +99 -79
  52. runbooks/common/memory_optimization.py +140 -118
  53. runbooks/common/module_cli_base.py +37 -58
  54. runbooks/common/organizations_client.py +175 -193
  55. runbooks/common/patterns.py +23 -25
  56. runbooks/common/performance_monitoring.py +67 -71
  57. runbooks/common/performance_optimization_engine.py +283 -274
  58. runbooks/common/profile_utils.py +111 -37
  59. runbooks/common/rich_utils.py +315 -141
  60. runbooks/common/sre_performance_suite.py +177 -186
  61. runbooks/enterprise/__init__.py +1 -1
  62. runbooks/enterprise/logging.py +144 -106
  63. runbooks/enterprise/security.py +187 -204
  64. runbooks/enterprise/validation.py +43 -56
  65. runbooks/finops/__init__.py +26 -30
  66. runbooks/finops/account_resolver.py +1 -1
  67. runbooks/finops/advanced_optimization_engine.py +980 -0
  68. runbooks/finops/automation_core.py +268 -231
  69. runbooks/finops/business_case_config.py +184 -179
  70. runbooks/finops/cli.py +660 -139
  71. runbooks/finops/commvault_ec2_analysis.py +157 -164
  72. runbooks/finops/compute_cost_optimizer.py +336 -320
  73. runbooks/finops/config.py +20 -20
  74. runbooks/finops/cost_optimizer.py +484 -618
  75. runbooks/finops/cost_processor.py +332 -214
  76. runbooks/finops/dashboard_runner.py +1006 -172
  77. runbooks/finops/ebs_cost_optimizer.py +991 -657
  78. runbooks/finops/elastic_ip_optimizer.py +317 -257
  79. runbooks/finops/enhanced_mcp_integration.py +340 -0
  80. runbooks/finops/enhanced_progress.py +32 -29
  81. runbooks/finops/enhanced_trend_visualization.py +3 -2
  82. runbooks/finops/enterprise_wrappers.py +223 -285
  83. runbooks/finops/executive_export.py +203 -160
  84. runbooks/finops/helpers.py +130 -288
  85. runbooks/finops/iam_guidance.py +1 -1
  86. runbooks/finops/infrastructure/__init__.py +80 -0
  87. runbooks/finops/infrastructure/commands.py +506 -0
  88. runbooks/finops/infrastructure/load_balancer_optimizer.py +866 -0
  89. runbooks/finops/infrastructure/vpc_endpoint_optimizer.py +832 -0
  90. runbooks/finops/markdown_exporter.py +337 -174
  91. runbooks/finops/mcp_validator.py +1952 -0
  92. runbooks/finops/nat_gateway_optimizer.py +1512 -481
  93. runbooks/finops/network_cost_optimizer.py +657 -587
  94. runbooks/finops/notebook_utils.py +226 -188
  95. runbooks/finops/optimization_engine.py +1136 -0
  96. runbooks/finops/optimizer.py +19 -23
  97. runbooks/finops/rds_snapshot_optimizer.py +367 -411
  98. runbooks/finops/reservation_optimizer.py +427 -363
  99. runbooks/finops/scenario_cli_integration.py +64 -65
  100. runbooks/finops/scenarios.py +1277 -438
  101. runbooks/finops/schemas.py +218 -182
  102. runbooks/finops/snapshot_manager.py +2289 -0
  103. runbooks/finops/types.py +3 -3
  104. runbooks/finops/validation_framework.py +259 -265
  105. runbooks/finops/vpc_cleanup_exporter.py +189 -144
  106. runbooks/finops/vpc_cleanup_optimizer.py +591 -573
  107. runbooks/finops/workspaces_analyzer.py +171 -182
  108. runbooks/integration/__init__.py +89 -0
  109. runbooks/integration/mcp_integration.py +1920 -0
  110. runbooks/inventory/CLAUDE.md +816 -0
  111. runbooks/inventory/__init__.py +2 -2
  112. runbooks/inventory/aws_decorators.py +2 -3
  113. runbooks/inventory/check_cloudtrail_compliance.py +2 -4
  114. runbooks/inventory/check_controltower_readiness.py +152 -151
  115. runbooks/inventory/check_landingzone_readiness.py +85 -84
  116. runbooks/inventory/cloud_foundations_integration.py +144 -149
  117. runbooks/inventory/collectors/aws_comprehensive.py +1 -1
  118. runbooks/inventory/collectors/aws_networking.py +109 -99
  119. runbooks/inventory/collectors/base.py +4 -0
  120. runbooks/inventory/core/collector.py +495 -313
  121. runbooks/inventory/core/formatter.py +11 -0
  122. runbooks/inventory/draw_org_structure.py +8 -9
  123. runbooks/inventory/drift_detection_cli.py +69 -96
  124. runbooks/inventory/ec2_vpc_utils.py +2 -2
  125. runbooks/inventory/find_cfn_drift_detection.py +5 -7
  126. runbooks/inventory/find_cfn_orphaned_stacks.py +7 -9
  127. runbooks/inventory/find_cfn_stackset_drift.py +5 -6
  128. runbooks/inventory/find_ec2_security_groups.py +48 -42
  129. runbooks/inventory/find_landingzone_versions.py +4 -6
  130. runbooks/inventory/find_vpc_flow_logs.py +7 -9
  131. runbooks/inventory/inventory_mcp_cli.py +48 -46
  132. runbooks/inventory/inventory_modules.py +103 -91
  133. runbooks/inventory/list_cfn_stacks.py +9 -10
  134. runbooks/inventory/list_cfn_stackset_operation_results.py +1 -3
  135. runbooks/inventory/list_cfn_stackset_operations.py +79 -57
  136. runbooks/inventory/list_cfn_stacksets.py +8 -10
  137. runbooks/inventory/list_config_recorders_delivery_channels.py +49 -39
  138. runbooks/inventory/list_ds_directories.py +65 -53
  139. runbooks/inventory/list_ec2_availability_zones.py +2 -4
  140. runbooks/inventory/list_ec2_ebs_volumes.py +32 -35
  141. runbooks/inventory/list_ec2_instances.py +23 -28
  142. runbooks/inventory/list_ecs_clusters_and_tasks.py +26 -34
  143. runbooks/inventory/list_elbs_load_balancers.py +22 -20
  144. runbooks/inventory/list_enis_network_interfaces.py +26 -33
  145. runbooks/inventory/list_guardduty_detectors.py +2 -4
  146. runbooks/inventory/list_iam_policies.py +2 -4
  147. runbooks/inventory/list_iam_roles.py +5 -7
  148. runbooks/inventory/list_iam_saml_providers.py +4 -6
  149. runbooks/inventory/list_lambda_functions.py +38 -38
  150. runbooks/inventory/list_org_accounts.py +6 -8
  151. runbooks/inventory/list_org_accounts_users.py +55 -44
  152. runbooks/inventory/list_rds_db_instances.py +31 -33
  153. runbooks/inventory/list_rds_snapshots_aggregator.py +192 -208
  154. runbooks/inventory/list_route53_hosted_zones.py +3 -5
  155. runbooks/inventory/list_servicecatalog_provisioned_products.py +37 -41
  156. runbooks/inventory/list_sns_topics.py +2 -4
  157. runbooks/inventory/list_ssm_parameters.py +4 -7
  158. runbooks/inventory/list_vpc_subnets.py +2 -4
  159. runbooks/inventory/list_vpcs.py +7 -10
  160. runbooks/inventory/mcp_inventory_validator.py +554 -468
  161. runbooks/inventory/mcp_vpc_validator.py +359 -442
  162. runbooks/inventory/organizations_discovery.py +63 -55
  163. runbooks/inventory/recover_cfn_stack_ids.py +7 -8
  164. runbooks/inventory/requirements.txt +0 -1
  165. runbooks/inventory/rich_inventory_display.py +35 -34
  166. runbooks/inventory/run_on_multi_accounts.py +3 -5
  167. runbooks/inventory/unified_validation_engine.py +281 -253
  168. runbooks/inventory/verify_ec2_security_groups.py +1 -1
  169. runbooks/inventory/vpc_analyzer.py +735 -697
  170. runbooks/inventory/vpc_architecture_validator.py +293 -348
  171. runbooks/inventory/vpc_dependency_analyzer.py +384 -380
  172. runbooks/inventory/vpc_flow_analyzer.py +1 -1
  173. runbooks/main.py +49 -34
  174. runbooks/main_final.py +91 -60
  175. runbooks/main_minimal.py +22 -10
  176. runbooks/main_optimized.py +131 -100
  177. runbooks/main_ultra_minimal.py +7 -2
  178. runbooks/mcp/__init__.py +36 -0
  179. runbooks/mcp/integration.py +679 -0
  180. runbooks/monitoring/performance_monitor.py +9 -4
  181. runbooks/operate/dynamodb_operations.py +3 -1
  182. runbooks/operate/ec2_operations.py +145 -137
  183. runbooks/operate/iam_operations.py +146 -152
  184. runbooks/operate/networking_cost_heatmap.py +29 -8
  185. runbooks/operate/rds_operations.py +223 -254
  186. runbooks/operate/s3_operations.py +107 -118
  187. runbooks/operate/vpc_operations.py +646 -616
  188. runbooks/remediation/base.py +1 -1
  189. runbooks/remediation/commons.py +10 -7
  190. runbooks/remediation/commvault_ec2_analysis.py +70 -66
  191. runbooks/remediation/ec2_unattached_ebs_volumes.py +1 -0
  192. runbooks/remediation/multi_account.py +24 -21
  193. runbooks/remediation/rds_snapshot_list.py +86 -60
  194. runbooks/remediation/remediation_cli.py +92 -146
  195. runbooks/remediation/universal_account_discovery.py +83 -79
  196. runbooks/remediation/workspaces_list.py +46 -41
  197. runbooks/security/__init__.py +19 -0
  198. runbooks/security/assessment_runner.py +1150 -0
  199. runbooks/security/baseline_checker.py +812 -0
  200. runbooks/security/cloudops_automation_security_validator.py +509 -535
  201. runbooks/security/compliance_automation_engine.py +17 -17
  202. runbooks/security/config/__init__.py +2 -2
  203. runbooks/security/config/compliance_config.py +50 -50
  204. runbooks/security/config_template_generator.py +63 -76
  205. runbooks/security/enterprise_security_framework.py +1 -1
  206. runbooks/security/executive_security_dashboard.py +519 -508
  207. runbooks/security/multi_account_security_controls.py +959 -1210
  208. runbooks/security/real_time_security_monitor.py +422 -444
  209. runbooks/security/security_baseline_tester.py +1 -1
  210. runbooks/security/security_cli.py +143 -112
  211. runbooks/security/test_2way_validation.py +439 -0
  212. runbooks/security/two_way_validation_framework.py +852 -0
  213. runbooks/sre/production_monitoring_framework.py +167 -177
  214. runbooks/tdd/__init__.py +15 -0
  215. runbooks/tdd/cli.py +1071 -0
  216. runbooks/utils/__init__.py +14 -17
  217. runbooks/utils/logger.py +7 -2
  218. runbooks/utils/version_validator.py +50 -47
  219. runbooks/validation/__init__.py +6 -6
  220. runbooks/validation/cli.py +9 -3
  221. runbooks/validation/comprehensive_2way_validator.py +745 -704
  222. runbooks/validation/mcp_validator.py +906 -228
  223. runbooks/validation/terraform_citations_validator.py +104 -115
  224. runbooks/validation/terraform_drift_detector.py +461 -454
  225. runbooks/vpc/README.md +617 -0
  226. runbooks/vpc/__init__.py +8 -1
  227. runbooks/vpc/analyzer.py +577 -0
  228. runbooks/vpc/cleanup_wrapper.py +476 -413
  229. runbooks/vpc/cli_cloudtrail_commands.py +339 -0
  230. runbooks/vpc/cli_mcp_validation_commands.py +480 -0
  231. runbooks/vpc/cloudtrail_audit_integration.py +717 -0
  232. runbooks/vpc/config.py +92 -97
  233. runbooks/vpc/cost_engine.py +411 -148
  234. runbooks/vpc/cost_explorer_integration.py +553 -0
  235. runbooks/vpc/cross_account_session.py +101 -106
  236. runbooks/vpc/enhanced_mcp_validation.py +917 -0
  237. runbooks/vpc/eni_gate_validator.py +961 -0
  238. runbooks/vpc/heatmap_engine.py +185 -160
  239. runbooks/vpc/mcp_no_eni_validator.py +680 -639
  240. runbooks/vpc/nat_gateway_optimizer.py +358 -0
  241. runbooks/vpc/networking_wrapper.py +15 -8
  242. runbooks/vpc/pdca_remediation_planner.py +528 -0
  243. runbooks/vpc/performance_optimized_analyzer.py +219 -231
  244. runbooks/vpc/runbooks_adapter.py +1167 -241
  245. runbooks/vpc/tdd_red_phase_stubs.py +601 -0
  246. runbooks/vpc/test_data_loader.py +358 -0
  247. runbooks/vpc/tests/conftest.py +314 -4
  248. runbooks/vpc/tests/test_cleanup_framework.py +1022 -0
  249. runbooks/vpc/tests/test_cost_engine.py +0 -2
  250. runbooks/vpc/topology_generator.py +326 -0
  251. runbooks/vpc/unified_scenarios.py +1297 -1124
  252. runbooks/vpc/vpc_cleanup_integration.py +1943 -1115
  253. runbooks-1.1.6.dist-info/METADATA +327 -0
  254. runbooks-1.1.6.dist-info/RECORD +489 -0
  255. runbooks/finops/README.md +0 -414
  256. runbooks/finops/accuracy_cross_validator.py +0 -647
  257. runbooks/finops/business_cases.py +0 -950
  258. runbooks/finops/dashboard_router.py +0 -922
  259. runbooks/finops/ebs_optimizer.py +0 -973
  260. runbooks/finops/embedded_mcp_validator.py +0 -1629
  261. runbooks/finops/enhanced_dashboard_runner.py +0 -527
  262. runbooks/finops/finops_dashboard.py +0 -584
  263. runbooks/finops/finops_scenarios.py +0 -1218
  264. runbooks/finops/legacy_migration.py +0 -730
  265. runbooks/finops/multi_dashboard.py +0 -1519
  266. runbooks/finops/single_dashboard.py +0 -1113
  267. runbooks/finops/unlimited_scenarios.py +0 -393
  268. runbooks-1.1.4.dist-info/METADATA +0 -800
  269. runbooks-1.1.4.dist-info/RECORD +0 -468
  270. {runbooks-1.1.4.dist-info → runbooks-1.1.6.dist-info}/WHEEL +0 -0
  271. {runbooks-1.1.4.dist-info → runbooks-1.1.6.dist-info}/entry_points.txt +0 -0
  272. {runbooks-1.1.4.dist-info → runbooks-1.1.6.dist-info}/licenses/LICENSE +0 -0
  273. {runbooks-1.1.4.dist-info → runbooks-1.1.6.dist-info}/top_level.txt +0 -0
@@ -88,13 +88,11 @@ from time import time
88
88
 
89
89
  from ArgumentsClass import CommonArguments
90
90
  from botocore.exceptions import ClientError
91
- from colorama import Fore, init
91
+ from runbooks.common.rich_utils import console
92
92
  from Inventory_Modules import display_results, find_load_balancers2, get_all_credentials
93
- from tqdm.auto import tqdm
93
+ from runbooks.common.rich_utils import create_progress_bar
94
94
 
95
- init()
96
95
  __version__ = "2024.05.06"
97
- ERASE_LINE = "\x1b[2K"
98
96
  begin_time = time()
99
97
 
100
98
 
@@ -306,20 +304,24 @@ def find_all_elbs(fAllCredentials: list, ffragment: list, fstatus: str):
306
304
  worker.start()
307
305
 
308
306
  # Queue credential sets with progress tracking for operational visibility
309
- for credential in tqdm(fAllCredentials):
310
- logging.info(f"Beginning to queue data - starting with {credential['AccountId']}")
311
- try:
312
- # Queue credential set with fragment and status filters for targeted discovery
313
- # Tuple format: (credentials, fragment_filter, status_filter)
314
- checkqueue.put((credential, ffragment, fstatus))
315
- except ClientError as my_Error:
316
- # Handle authorization failures during credential queuing
317
- if "AuthFailure" in str(my_Error):
318
- logging.error(
319
- f"Authorization Failure accessing account {credential['AccountId']} in {credential['Region']} region"
320
- )
321
- logging.warning(f"It's possible that the region {credential['Region']} hasn't been opted-into")
322
- pass
307
+ with create_progress_bar() as progress:
308
+ task = progress.add_task("Queueing ELB discovery tasks", total=len(fAllCredentials))
309
+ for credential in fAllCredentials:
310
+ logging.info(f"Beginning to queue data - starting with {credential['AccountId']}")
311
+ try:
312
+ # Queue credential set with fragment and status filters for targeted discovery
313
+ # Tuple format: (credentials, fragment_filter, status_filter)
314
+ checkqueue.put((credential, ffragment, fstatus))
315
+ except ClientError as my_Error:
316
+ # Handle authorization failures during credential queuing
317
+ if "AuthFailure" in str(my_Error):
318
+ logging.error(
319
+ f"Authorization Failure accessing account {credential['AccountId']} in {credential['Region']} region"
320
+ )
321
+ logging.warning(f"It's possible that the region {credential['Region']} hasn't been opted-into")
322
+ pass
323
+ finally:
324
+ progress.update(task, advance=1)
323
325
 
324
326
  # Wait for all queued work to complete before proceeding
325
327
  checkqueue.join()
@@ -396,13 +398,13 @@ if __name__ == "__main__":
396
398
  # Display performance timing metrics for operational optimization and SLA compliance
397
399
  if pTiming:
398
400
  print(ERASE_LINE)
399
- print(f"{Fore.GREEN}This script took {time() - begin_time:.2f} seconds{Fore.RESET}")
401
+ print(f"[green]This script took {time() - begin_time:.2f} seconds")
400
402
 
401
403
  print(ERASE_LINE)
402
404
 
403
405
  # Display comprehensive operational summary for executive traffic management reporting
404
406
  print(
405
- f"{Fore.RED}Found {len(All_Load_Balancers)} Load Balancers across {AccountNum} profiles across {RegionNum} regions{Fore.RESET}"
407
+ f"[red]Found {len(All_Load_Balancers)} Load Balancers across {AccountNum} profiles across {RegionNum} regions"
406
408
  )
407
409
  print()
408
410
 
@@ -96,11 +96,10 @@ from ArgumentsClass import CommonArguments
96
96
  from botocore.exceptions import ClientError
97
97
 
98
98
  # from datetime import datetime
99
- from colorama import Fore, init
99
+ from runbooks.common.rich_utils import console
100
100
  from Inventory_Modules import display_results, find_account_enis2, get_all_credentials
101
- from tqdm.auto import tqdm
101
+ from runbooks.common.rich_utils import create_progress_bar
102
102
 
103
- init()
104
103
 
105
104
  __version__ = "2024.10.24"
106
105
 
@@ -269,8 +268,8 @@ def check_accounts_for_enis(fCredentialList, fip=None, fPublicOnly: bool = False
269
268
  """
270
269
  while True:
271
270
  # Retrieve ENI discovery work item from thread-safe queue
272
- c_account_credentials, c_region, c_fip, c_PlacesToLook, c_PlaceCount = self.queue.get()
273
- pbar.update() # Update progress tracking for operational visibility
271
+ c_account_credentials, c_region, c_fip, c_PlacesToLook, c_PlaceCount, c_progress, c_task = self.queue.get()
272
+ c_progress.update(c_task, advance=1) # Update progress tracking for operational visibility
274
273
  logging.info(f"De-queued info for account {c_account_credentials['AccountId']}")
275
274
 
276
275
  try:
@@ -323,13 +322,6 @@ def check_accounts_for_enis(fCredentialList, fip=None, fPublicOnly: bool = False
323
322
  # Initialize queue-based threading architecture for scalable ENI discovery
324
323
  checkqueue = Queue()
325
324
 
326
- # Initialize progress tracking for operational visibility during large-scale operations
327
- pbar = tqdm(
328
- desc=f"Finding enis from {len(CredentialList)} accounts / regions",
329
- total=len(fCredentialList),
330
- unit=" locations",
331
- )
332
-
333
325
  # Initialize results list for aggregating discovered ENIs
334
326
  Results = []
335
327
  PlaceCount = 0
@@ -347,24 +339,26 @@ def check_accounts_for_enis(fCredentialList, fip=None, fPublicOnly: bool = False
347
339
  worker.start()
348
340
 
349
341
  # Queue credential sets for processing by worker threads
350
- for credential in fCredentialList:
351
- logging.info(f"Connecting to account {credential['AccountId']} in region {credential['Region']}")
352
- try:
353
- # Queue credential set with IP filter and progress tracking parameters
354
- # Tuple format: (credentials, region, ip_filter, total_places, current_count)
355
- checkqueue.put((credential, credential["Region"], fip, PlacesToLook, PlaceCount))
356
- PlaceCount += 1
357
- except ClientError as my_Error:
358
- # Handle authorization failures during credential queuing
359
- if "AuthFailure" in str(my_Error):
360
- logging.error(
361
- f"Authorization Failure accessing account {credential['AccountId']} in {credential['Region']} region"
362
- )
363
- logging.warning(f"It's possible that the region {credential['Region']} hasn't been opted-into")
364
- pass
365
-
366
- # Wait for all queued work to complete before proceeding
367
- checkqueue.join()
342
+ with create_progress_bar() as progress:
343
+ task = progress.add_task("Discovering ENIs", total=len(fCredentialList))
344
+ for credential in fCredentialList:
345
+ logging.info(f"Connecting to account {credential['AccountId']} in region {credential['Region']}")
346
+ try:
347
+ # Queue credential set with IP filter and progress tracking parameters
348
+ # Tuple format: (credentials, region, ip_filter, total_places, current_count, progress, task)
349
+ checkqueue.put((credential, credential["Region"], fip, PlacesToLook, PlaceCount, progress, task))
350
+ PlaceCount += 1
351
+ except ClientError as my_Error:
352
+ # Handle authorization failures during credential queuing
353
+ if "AuthFailure" in str(my_Error):
354
+ logging.error(
355
+ f"Authorization Failure accessing account {credential['AccountId']} in {credential['Region']} region"
356
+ )
357
+ logging.warning(f"It's possible that the region {credential['Region']} hasn't been opted-into")
358
+ pass
359
+
360
+ # Wait for all queued work to complete before proceeding
361
+ checkqueue.join()
368
362
  return Results
369
363
 
370
364
 
@@ -450,7 +444,7 @@ def present_results(f_ENIsFound: list):
450
444
 
451
445
  # Highlight cost optimization opportunities with unused ENI identification
452
446
  print(
453
- f"{Fore.RED}Found {len(DetachedENIs)} ENIs that are not listed as 'in-use' and may therefore be costing you additional money while they're unused.{Fore.RESET}"
447
+ f"[red]Found {len(DetachedENIs)} ENIs that are not listed as 'in-use' and may therefore be costing you additional money while they're unused."
454
448
  ) if DetachedENIs else ""
455
449
  print()
456
450
 
@@ -464,7 +458,6 @@ def present_results(f_ENIsFound: list):
464
458
  # Main execution entry point for enterprise ENI discovery and network security analysis
465
459
  ##################
466
460
 
467
- ERASE_LINE = "\x1b[2K"
468
461
 
469
462
  if __name__ == "__main__":
470
463
  """
@@ -518,7 +511,7 @@ if __name__ == "__main__":
518
511
 
519
512
  # Display performance timing metrics for operational optimization and SLA compliance
520
513
  if pTiming:
521
- print(f"{Fore.GREEN}This script took {time() - begin_time:.2f} seconds{Fore.RESET}")
514
+ print(f"[green]This script took {time() - begin_time:.2f} seconds")
522
515
 
523
516
  # Display completion message for user confirmation and operational closure
524
517
  print()
@@ -95,10 +95,9 @@ import Inventory_Modules
95
95
  from account_class import aws_acct_access
96
96
  from ArgumentsClass import CommonArguments
97
97
  from botocore.exceptions import ClientError
98
- from colorama import Fore, init
98
+ from runbooks.common.rich_utils import console
99
99
  from Inventory_Modules import get_all_credentials
100
100
 
101
- init()
102
101
  __version__ = "2023.07.18"
103
102
 
104
103
  # Parse enterprise command-line arguments with GuardDuty-specific security management options
@@ -150,7 +149,6 @@ logging.getLogger("urllib3").setLevel(logging.CRITICAL)
150
149
  # Initialize enterprise GuardDuty discovery and security management operations
151
150
  ##########################
152
151
 
153
- ERASE_LINE = "\x1b[2K"
154
152
 
155
153
  # Initialize AWS account access for GuardDuty administrative operations
156
154
  aws_acct = aws_acct_access(pProfile)
@@ -337,7 +335,7 @@ for credential in AllCredentials:
337
335
  # Display progress for accounts without GuardDuty detectors for operational visibility
338
336
  print(
339
337
  ERASE_LINE,
340
- f"{Fore.RED}No luck in account: {credential['AccountId']} in region {credential['Region']}{Fore.RESET} -- {places_to_try} of {len(AllCredentials)}",
338
+ f"[red]No luck in account: {credential['AccountId']} in region {credential['Region']} -- {places_to_try} of {len(AllCredentials)}",
341
339
  end="\r",
342
340
  )
343
341
  except ClientError as my_Error:
@@ -94,12 +94,10 @@ from time import time
94
94
 
95
95
  from ArgumentsClass import CommonArguments
96
96
  from botocore.exceptions import ClientError
97
- from colorama import Fore, init
97
+ from runbooks.common.rich_utils import console
98
98
  from Inventory_Modules import display_results, find_account_policies2, find_policy_action2, get_all_credentials
99
99
 
100
- init()
101
100
  __version__ = "2023.12.12"
102
- ERASE_LINE = "\x1b[2K"
103
101
  begin_time = time()
104
102
 
105
103
 
@@ -392,7 +390,7 @@ if __name__ == "__main__":
392
390
 
393
391
  if pTiming:
394
392
  print(ERASE_LINE)
395
- print(f"{Fore.GREEN}This script took {time() - begin_time:.2f} seconds{Fore.RESET}")
393
+ print(f"[green]This script took {time() - begin_time:.2f} seconds")
396
394
  print(f"These accounts were skipped - as requested: {pSkipAccounts}") if pSkipAccounts is not None else print()
397
395
  print()
398
396
  print(
@@ -43,10 +43,9 @@ from time import sleep, time
43
43
  import boto3
44
44
  from ArgumentsClass import CommonArguments
45
45
  from botocore.exceptions import ClientError
46
- from colorama import Fore, init
47
46
  from Inventory_Modules import display_results, find_in, get_all_credentials
47
+ from runbooks.common.rich_utils import console
48
48
 
49
- init()
50
49
  __version__ = "2023.11.06"
51
50
 
52
51
 
@@ -249,8 +248,8 @@ def find_and_collect_roles_across_accounts(fAllCredentials: list, frole_fragment
249
248
  print(f"Listing out all roles across {len(fAllCredentials)} accounts")
250
249
  print()
251
250
  elif pExact:
252
- print(
253
- f"Looking for a role {Fore.RED}exactly{Fore.RESET} named one of these strings {frole_fragments} across {len(fAllCredentials)} accounts"
251
+ console.print(
252
+ f"Looking for a role [red]exactly[/red] named one of these strings {frole_fragments} across {len(fAllCredentials)} accounts"
254
253
  )
255
254
  print()
256
255
  else:
@@ -475,7 +474,6 @@ if __name__ == "__main__":
475
474
  logging.getLogger("s3transfer").setLevel(logging.CRITICAL)
476
475
  logging.getLogger("urllib3").setLevel(logging.CRITICAL)
477
476
 
478
- ERASE_LINE = "\x1b[2K"
479
477
  time_to_sleep = 5
480
478
  begin_time = time()
481
479
 
@@ -511,8 +509,8 @@ if __name__ == "__main__":
511
509
  )
512
510
 
513
511
  if pTiming:
514
- print(ERASE_LINE)
515
- print(f"{Fore.GREEN}This script took {time() - begin_time:.2f} seconds{Fore.RESET}")
512
+ console.print()
513
+ console.print(f"[green]This script took {time() - begin_time:.2f} seconds[/green]")
516
514
  print()
517
515
  print("Thanks for using this script...")
518
516
  print()
@@ -101,14 +101,12 @@ import boto3
101
101
  from account_class import aws_acct_access
102
102
  from ArgumentsClass import CommonArguments
103
103
  from botocore.exceptions import ClientError
104
- from colorama import Fore, init
104
+ from runbooks.common.rich_utils import console
105
105
  from Inventory_Modules import display_results, find_saml_components_in_acct2, get_child_access3
106
106
 
107
- init()
108
107
  __version__ = "2024.03.27"
109
108
 
110
109
  begin_time = time()
111
- ERASE_LINE = "\x1b[2K"
112
110
 
113
111
 
114
112
  ##################
@@ -254,7 +252,7 @@ def all_my_saml_providers(faws_acct: aws_acct_access, fChildAccounts: list, f_ac
254
252
  idpNum = len(Idps)
255
253
  logging.info(f"Account: {account['AccountId']} | Region: {pRegion} | Found {idpNum} Idps")
256
254
  logging.info(
257
- f"{ERASE_LINE}{Fore.RED}Account: {account['AccountId']} pRegion: {pRegion} Found {idpNum} Idps.{Fore.RESET}"
255
+ f"{ERASE_LINE}[red]Account: {account['AccountId']} pRegion: {pRegion} Found {idpNum} Idps."
258
256
  )
259
257
 
260
258
  # Process discovered identity providers and extract metadata
@@ -342,7 +340,7 @@ if __name__ == "__main__":
342
340
  RegionsFound = list(set([x["Region"] for x in IdpsFound]))
343
341
  print()
344
342
  print(
345
- f"{Fore.RED}Found {len(IdpsFound)} Idps across {len(AccountsFound)} accounts in {len(RegionsFound)} regions{Fore.RESET}"
343
+ f"[red]Found {len(IdpsFound)} Idps across {len(AccountsFound)} accounts in {len(RegionsFound)} regions"
346
344
  )
347
345
  print()
348
346
 
@@ -353,7 +351,7 @@ if __name__ == "__main__":
353
351
 
354
352
  print()
355
353
  if pTiming:
356
- print(f"{Fore.GREEN}This script took {time() - begin_time:.2f} seconds{Fore.RESET}")
354
+ print(f"[green]This script took {time() - begin_time:.2f} seconds")
357
355
  print()
358
356
  print("Thanks for using this script...")
359
357
  print()
@@ -82,14 +82,12 @@ import boto3
82
82
  import Inventory_Modules
83
83
  from ArgumentsClass import CommonArguments
84
84
  from botocore.exceptions import ClientError
85
- from colorama import Fore, init
85
+ from runbooks.common.rich_utils import console
86
86
  from Inventory_Modules import display_results, get_all_credentials
87
- from tqdm.auto import tqdm
87
+ from runbooks.common.rich_utils import create_progress_bar
88
88
 
89
- init()
90
89
  __version__ = "2024.06.05"
91
90
 
92
- ERASE_LINE = "\x1b[2K"
93
91
  begin_time = time()
94
92
 
95
93
 
@@ -497,7 +495,6 @@ def check_accounts_for_functions(CredentialList, fFragments=None):
497
495
  while True:
498
496
  # Retrieve account credentials and fragment filters from thread-safe work queue
499
497
  c_account_credentials, c_fragment_list = self.queue.get()
500
- pbar.update() # Update progress bar for operational visibility
501
498
  Functions = []
502
499
  logging.info(f"De-queued info for account {c_account_credentials['AccountId']}")
503
500
  try:
@@ -543,6 +540,9 @@ def check_accounts_for_functions(CredentialList, fFragments=None):
543
540
  # Thread-safe aggregation of discovered Lambda functions
544
541
  AllFuncs.extend(Functions)
545
542
 
543
+ # Update progress bar for operational visibility
544
+ progress.update(task, advance=1)
545
+
546
546
  # Signal task completion for thread-safe work queue management
547
547
  self.queue.task_done()
548
548
 
@@ -556,40 +556,40 @@ def check_accounts_for_functions(CredentialList, fFragments=None):
556
556
  checkqueue = Queue()
557
557
 
558
558
  # Initialize progress tracking for operational visibility during discovery
559
- pbar = tqdm(
560
- desc=f"Finding instances from {len(CredentialList)} accounts / regions",
561
- total=len(CredentialList),
562
- unit=" locations",
563
- )
564
-
565
- # Initialize multi-threaded Lambda function discovery worker pool
566
- for x in range(WorkerThreads):
567
- worker = FindFunctions(checkqueue)
568
- # Enable graceful shutdown with main thread termination for enterprise operational safety
569
- worker.daemon = True
570
- worker.start() # Begin concurrent Lambda function discovery processing
559
+ with create_progress_bar() as progress:
560
+ task = progress.add_task(
561
+ f"Finding instances from {len(CredentialList)} accounts / regions",
562
+ total=len(CredentialList)
563
+ )
571
564
 
572
- # Populate work queue with account credentials for distributed serverless discovery
573
- for credential in CredentialList:
574
- logging.info(f"Connecting to account {credential['AccountId']}")
575
- try:
576
- # Log work queue population for operational audit trail
577
- logging.info(
578
- f"{ERASE_LINE}Queuing account {credential['AccountId']} in region {credential['Region']}", end="\r"
579
- )
580
- # Add credential and fragment filter to processing queue
581
- checkqueue.put((credential, fFragments))
582
- except ClientError as my_Error:
583
- # Handle AWS API authorization failures during queue population
584
- if "AuthFailure" in str(my_Error):
585
- logging.error(
586
- f"Authorization Failure accessing account {credential['AccountId']} in {credential['Region']} region"
565
+ # Initialize multi-threaded Lambda function discovery worker pool
566
+ for x in range(WorkerThreads):
567
+ worker = FindFunctions(checkqueue)
568
+ # Enable graceful shutdown with main thread termination for enterprise operational safety
569
+ worker.daemon = True
570
+ worker.start() # Begin concurrent Lambda function discovery processing
571
+
572
+ # Populate work queue with account credentials for distributed serverless discovery
573
+ for credential in CredentialList:
574
+ logging.info(f"Connecting to account {credential['AccountId']}")
575
+ try:
576
+ # Log work queue population for operational audit trail
577
+ logging.info(
578
+ f"{ERASE_LINE}Queuing account {credential['AccountId']} in region {credential['Region']}", end="\r"
587
579
  )
588
- logging.error(f"It's possible that the region {credential['Region']} hasn't been opted-into")
589
- pass # Continue processing remaining accounts despite individual failures
580
+ # Add credential and fragment filter to processing queue
581
+ checkqueue.put((credential, fFragments))
582
+ except ClientError as my_Error:
583
+ # Handle AWS API authorization failures during queue population
584
+ if "AuthFailure" in str(my_Error):
585
+ logging.error(
586
+ f"Authorization Failure accessing account {credential['AccountId']} in {credential['Region']} region"
587
+ )
588
+ logging.error(f"It's possible that the region {credential['Region']} hasn't been opted-into")
589
+ pass # Continue processing remaining accounts despite individual failures
590
590
 
591
- # Wait for all Lambda function discovery tasks to complete before result aggregation
592
- checkqueue.join()
591
+ # Wait for all Lambda function discovery tasks to complete before result aggregation
592
+ checkqueue.join()
593
593
 
594
594
  # Return comprehensive Lambda function inventory with enterprise serverless metadata
595
595
  return AllFuncs
@@ -744,7 +744,7 @@ def fix_my_functions(fAllFunctions, fRuntime, fNewRuntime, fForceDelete, fTiming
744
744
  if fTiming:
745
745
  print(ERASE_LINE)
746
746
  print(
747
- f"{Fore.GREEN}Fixing {len(return_response)} functions took {time() - begin_fix_time:.3f} seconds{Fore.RESET}"
747
+ f"[green]Fixing {len(return_response)} functions took {time() - begin_fix_time:.3f} seconds"
748
748
  )
749
749
 
750
750
  # Return operation results for enterprise reporting and audit trail
@@ -869,7 +869,7 @@ if __name__ == "__main__":
869
869
  # Display performance timing metrics for operational optimization and SLA compliance
870
870
  if pTiming:
871
871
  print(ERASE_LINE)
872
- print(f"{Fore.GREEN}This script took {time() - begin_time:.3f} seconds{Fore.RESET}")
872
+ print(f"[green]This script took {time() - begin_time:.3f} seconds")
873
873
 
874
874
  print(ERASE_LINE)
875
875
 
@@ -71,12 +71,10 @@ from time import time
71
71
  from ArgumentsClass import CommonArguments
72
72
 
73
73
  # from botocore.exceptions import ClientError, NoCredentialsError, InvalidConfigError
74
- from colorama import Fore, Style, init
74
+ from runbooks.common.rich_utils import console
75
75
  from Inventory_Modules import display_results, get_org_accounts_from_profiles, get_profiles
76
76
 
77
- init()
78
77
  __version__ = "2024.05.08"
79
- ERASE_LINE = "\x1b[2K"
80
78
  begin_time = time()
81
79
 
82
80
 
@@ -251,7 +249,7 @@ def all_my_orgs(
251
249
  # Print out the results
252
250
  if f_Timing:
253
251
  print()
254
- print(f"It's taken {Fore.GREEN}{time() - begin_time:.2f}{Fore.RESET} seconds to find profile accounts...")
252
+ print(f"It's taken [green]{time() - begin_time:.2f} seconds to find profile accounts...")
255
253
  print()
256
254
  fmt = "%-23s %-15s %-15s %-12s %-10s"
257
255
  print("<------------------------------------>")
@@ -278,7 +276,7 @@ def all_my_orgs(
278
276
  else:
279
277
  logging.info(f"{item['profile']} was successful.")
280
278
  print(
281
- f"{Fore.RED if item['RootAcct'] else ''}{item['profile']:23s} {item['aws_acct'].acct_number:15s} {item['MgmtAccount']:15s} {str(item['OrgId']):12s} {item['RootAcct']}{Fore.RESET}"
279
+ f"{Fore.RED if item['RootAcct'] else ''}{item['profile']:23s} {item['aws_acct'].acct_number:15s} {item['MgmtAccount']:15s} {str(item['OrgId']):12s} {item['RootAcct']}"
282
280
  )
283
281
  except TypeError as my_Error:
284
282
  logging.error(f"Error - {my_Error} on {item}")
@@ -339,14 +337,14 @@ def all_my_orgs(
339
337
  for item in AllProfileAccounts:
340
338
  if item["Success"] and item["RootAcct"]:
341
339
  print(
342
- f"{item['profile']:{ProfileNameLength}s} {Style.BRIGHT}{item['MgmtAccount']:15s}{Style.RESET_ALL}"
340
+ f"{item['profile']:{ProfileNameLength}s} [bold]{item['MgmtAccount']:15s}"
343
341
  )
344
342
  print(
345
343
  f"\t{'Child Account Number':{len('Child Account Number')}s} {'Child Account Status':{len('Child Account Status')}s} {'Child Email Address'}"
346
344
  )
347
345
  for child_acct in item["aws_acct"].ChildAccounts:
348
346
  print(
349
- f"\t{Fore.RED if not child_acct['AccountStatus'] == 'ACTIVE' else ''}{child_acct['AccountId']:{len('Child Account Number')}s} {child_acct['AccountStatus']:{len('Child Account Status')}s} {child_acct['AccountEmail']}{Fore.RESET}"
347
+ f"\t{Fore.RED if not child_acct['AccountStatus'] == 'ACTIVE' else ''}{child_acct['AccountId']:{len('Child Account Number')}s} {child_acct['AccountStatus']:{len('Child Account Status')}s} {child_acct['AccountEmail']}"
350
348
  )
351
349
 
352
350
  elif f_SaveFilename is not None:
@@ -440,7 +438,7 @@ if __name__ == "__main__":
440
438
 
441
439
  print()
442
440
  if pTiming:
443
- print(f"{Fore.GREEN}This script took {time() - begin_time:.2f} seconds{Fore.RESET}")
441
+ print(f"[green]This script took {time() - begin_time:.2f} seconds")
444
442
  print()
445
443
  print("Thanks for using this script")
446
444
  print()
@@ -97,7 +97,7 @@ from time import time
97
97
 
98
98
  from ArgumentsClass import CommonArguments
99
99
  from botocore.exceptions import ClientError
100
- from colorama import Fore, init
100
+ from runbooks.common.rich_utils import console
101
101
  from Inventory_Modules import (
102
102
  display_results,
103
103
  find_iam_users2,
@@ -105,11 +105,10 @@ from Inventory_Modules import (
105
105
  find_idc_users2,
106
106
  get_all_credentials,
107
107
  )
108
- from tqdm.auto import tqdm
108
+ # Migrated to Rich.Progress - see rich_utils.py for enterprise UX standards
109
+ # from tqdm.auto import tqdm
109
110
 
110
- init()
111
111
  __version__ = "2024.05.09"
112
- ERASE_LINE = "\x1b[2K"
113
112
  begin_time = time()
114
113
 
115
114
 
@@ -253,46 +252,58 @@ def find_all_org_users(f_credentials, f_IDC: bool, f_IAM: bool) -> list:
253
252
  User_List = []
254
253
  directories_seen = set()
255
254
 
255
+ # Import Rich display utilities for professional progress tracking
256
+ from runbooks.common.rich_utils import create_progress_bar
257
+
256
258
  # TODO: Enhance with multi-threading for improved performance across large organizations
257
- for credential in tqdm(
258
- f_credentials, desc=f"Looking for users across {len(f_credentials)} Accounts", unit=" accounts"
259
- ):
260
- # Skip credentials that failed validation
261
- if not credential["Success"]:
262
- logging.info(f"{credential['ErrorMessage']} with roles: {credential['RolesTried']}")
263
- continue
264
-
265
- # Discover traditional IAM users if requested
266
- if f_IAM:
267
- try:
268
- # Call inventory module to discover IAM users in this account
269
- User_List.extend(find_iam_users2(credential))
270
- # Optional verbose logging for user discovery progress (currently commented)
271
- # logging.info(f"{ERASE_LINE}Account: {credential['AccountId']} Found {len(User_List)} users")
272
- except ClientError as my_Error:
273
- # Handle IAM API authorization failures gracefully
274
- if "AuthFailure" in str(my_Error):
275
- logging.error(f"{ERASE_LINE}{credential}: Authorization Failure")
276
-
277
- # Discover AWS Identity Center users if requested
278
- if f_IDC:
279
- try:
280
- # Find out if this account hosts an Identity Center with a user directory
281
- directory_ids = find_idc_directory_id2(credential)
282
- for directory_instance_id in directory_ids:
283
- # Directory deduplication: if we've already interrogated this directory, skip it
284
- if directory_instance_id in directories_seen:
285
- continue
286
- else:
287
- # Mark this directory as processed and discover users
288
- directories_seen.update(directory_ids)
289
- User_List.extend(find_idc_users2(credential, directory_instance_id))
290
- # Optional verbose logging for user discovery progress (currently commented)
291
- # logging.info(f"{ERASE_LINE}Account: {credential['AccountId']} Found {len(User_List)} users")
292
- except ClientError as my_Error:
293
- # Handle Identity Center API authorization failures gracefully
294
- if "AuthFailure" in str(my_Error):
295
- logging.error(f"{ERASE_LINE}{credential}: Authorization Failure")
259
+ with create_progress_bar() as progress:
260
+ task = progress.add_task(
261
+ f"[cyan]Looking for users across {len(f_credentials)} Accounts...",
262
+ total=len(f_credentials)
263
+ )
264
+
265
+ for credential in f_credentials:
266
+ # Skip credentials that failed validation
267
+ if not credential["Success"]:
268
+ logging.info(f"{credential['ErrorMessage']} with roles: {credential['RolesTried']}")
269
+ progress.update(task, advance=1)
270
+ continue
271
+
272
+ # Discover traditional IAM users if requested
273
+ if f_IAM:
274
+ try:
275
+ # Call inventory module to discover IAM users in this account
276
+ User_List.extend(find_iam_users2(credential))
277
+ # Optional verbose logging for user discovery progress (currently commented)
278
+ # logging.info(f"{ERASE_LINE}Account: {credential['AccountId']} Found {len(User_List)} users")
279
+ except ClientError as my_Error:
280
+ # Handle IAM API authorization failures gracefully
281
+ if "AuthFailure" in str(my_Error):
282
+ logging.error(f"{ERASE_LINE}{credential}: Authorization Failure")
283
+
284
+ # Discover AWS Identity Center users if requested
285
+ if f_IDC:
286
+ try:
287
+ # Find out if this account hosts an Identity Center with a user directory
288
+ directory_ids = find_idc_directory_id2(credential)
289
+ for directory_instance_id in directory_ids:
290
+ # Directory deduplication: if we've already interrogated this directory, skip it
291
+ if directory_instance_id in directories_seen:
292
+ continue
293
+ else:
294
+ # Mark this directory as processed and discover users
295
+ directories_seen.update(directory_ids)
296
+ User_List.extend(find_idc_users2(credential, directory_instance_id))
297
+ # Optional verbose logging for user discovery progress (currently commented)
298
+ # logging.info(f"{ERASE_LINE}Account: {credential['AccountId']} Found {len(User_List)} users")
299
+ except ClientError as my_Error:
300
+ # Handle Identity Center API authorization failures gracefully
301
+ if "AuthFailure" in str(my_Error):
302
+ logging.error(f"{ERASE_LINE}{credential}: Authorization Failure")
303
+
304
+ # Update progress after processing each credential
305
+ progress.update(task, advance=1)
306
+
296
307
  return User_List
297
308
 
298
309
 
@@ -344,7 +355,7 @@ if __name__ == "__main__":
344
355
  display_results(sorted_UserListing, display_dict, "N/A", pFilename)
345
356
  if pTiming:
346
357
  print(ERASE_LINE)
347
- print(f"{Fore.GREEN}This script took {time() - begin_time:.2f} seconds{Fore.RESET}")
358
+ print(f"[green]This script took {time() - begin_time:.2f} seconds")
348
359
  print(ERASE_LINE)
349
360
  print(
350
361
  f"Found {len(UserListing)} users across {len(SuccessfulAccountAccesses)} account{'' if len(SuccessfulAccountAccesses) == 1 else 's'}"