runbooks 1.1.5__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 (62) hide show
  1. runbooks/cli/commands/inventory.py +21 -80
  2. runbooks/common/accuracy_validator.py +6 -12
  3. runbooks/common/mcp_integration.py +38 -7
  4. runbooks/common/rich_utils.py +116 -2
  5. runbooks/inventory/CLAUDE.md +1 -1
  6. runbooks/inventory/aws_decorators.py +2 -3
  7. runbooks/inventory/check_cloudtrail_compliance.py +2 -4
  8. runbooks/inventory/check_controltower_readiness.py +152 -151
  9. runbooks/inventory/check_landingzone_readiness.py +85 -84
  10. runbooks/inventory/core/formatter.py +11 -0
  11. runbooks/inventory/draw_org_structure.py +8 -9
  12. runbooks/inventory/ec2_vpc_utils.py +2 -2
  13. runbooks/inventory/find_cfn_drift_detection.py +5 -7
  14. runbooks/inventory/find_cfn_orphaned_stacks.py +7 -9
  15. runbooks/inventory/find_cfn_stackset_drift.py +5 -6
  16. runbooks/inventory/find_ec2_security_groups.py +48 -42
  17. runbooks/inventory/find_landingzone_versions.py +4 -6
  18. runbooks/inventory/find_vpc_flow_logs.py +7 -9
  19. runbooks/inventory/inventory_modules.py +103 -91
  20. runbooks/inventory/list_cfn_stacks.py +9 -10
  21. runbooks/inventory/list_cfn_stackset_operation_results.py +1 -3
  22. runbooks/inventory/list_cfn_stackset_operations.py +79 -57
  23. runbooks/inventory/list_cfn_stacksets.py +8 -10
  24. runbooks/inventory/list_config_recorders_delivery_channels.py +49 -39
  25. runbooks/inventory/list_ds_directories.py +65 -53
  26. runbooks/inventory/list_ec2_availability_zones.py +2 -4
  27. runbooks/inventory/list_ec2_ebs_volumes.py +32 -35
  28. runbooks/inventory/list_ec2_instances.py +23 -28
  29. runbooks/inventory/list_ecs_clusters_and_tasks.py +26 -34
  30. runbooks/inventory/list_elbs_load_balancers.py +22 -20
  31. runbooks/inventory/list_enis_network_interfaces.py +26 -33
  32. runbooks/inventory/list_guardduty_detectors.py +2 -4
  33. runbooks/inventory/list_iam_policies.py +2 -4
  34. runbooks/inventory/list_iam_roles.py +5 -7
  35. runbooks/inventory/list_iam_saml_providers.py +4 -6
  36. runbooks/inventory/list_lambda_functions.py +38 -38
  37. runbooks/inventory/list_org_accounts.py +6 -8
  38. runbooks/inventory/list_org_accounts_users.py +55 -44
  39. runbooks/inventory/list_rds_db_instances.py +31 -33
  40. runbooks/inventory/list_route53_hosted_zones.py +3 -5
  41. runbooks/inventory/list_servicecatalog_provisioned_products.py +37 -41
  42. runbooks/inventory/list_sns_topics.py +2 -4
  43. runbooks/inventory/list_ssm_parameters.py +4 -7
  44. runbooks/inventory/list_vpc_subnets.py +2 -4
  45. runbooks/inventory/list_vpcs.py +7 -10
  46. runbooks/inventory/mcp_inventory_validator.py +5 -3
  47. runbooks/inventory/organizations_discovery.py +8 -4
  48. runbooks/inventory/recover_cfn_stack_ids.py +7 -8
  49. runbooks/inventory/requirements.txt +0 -1
  50. runbooks/inventory/rich_inventory_display.py +2 -2
  51. runbooks/inventory/run_on_multi_accounts.py +3 -5
  52. runbooks/inventory/unified_validation_engine.py +3 -2
  53. runbooks/inventory/verify_ec2_security_groups.py +1 -1
  54. runbooks/inventory/vpc_analyzer.py +3 -2
  55. runbooks/inventory/vpc_dependency_analyzer.py +2 -2
  56. runbooks/validation/terraform_drift_detector.py +16 -5
  57. {runbooks-1.1.5.dist-info → runbooks-1.1.6.dist-info}/METADATA +3 -4
  58. {runbooks-1.1.5.dist-info → runbooks-1.1.6.dist-info}/RECORD +62 -62
  59. {runbooks-1.1.5.dist-info → runbooks-1.1.6.dist-info}/WHEEL +0 -0
  60. {runbooks-1.1.5.dist-info → runbooks-1.1.6.dist-info}/entry_points.txt +0 -0
  61. {runbooks-1.1.5.dist-info → runbooks-1.1.6.dist-info}/licenses/LICENSE +0 -0
  62. {runbooks-1.1.5.dist-info → runbooks-1.1.6.dist-info}/top_level.txt +0 -0
@@ -8,10 +8,9 @@ import Inventory_Modules
8
8
  from account_class import aws_acct_access
9
9
  from ArgumentsClass import CommonArguments
10
10
  from botocore.exceptions import ClientError
11
- from colorama import Fore, init
12
- from prettytable import PrettyTable
11
+ from runbooks.common.rich_utils import console, print_success, print_error, print_info
12
+ from runbooks.common.rich_utils import console, create_table
13
13
 
14
- init()
15
14
  __version__ = "2023.05.04"
16
15
 
17
16
  parser = CommonArguments()
@@ -109,7 +108,6 @@ else:
109
108
  "us-west-2",
110
109
  ]
111
110
 
112
- ERASE_LINE = "\x1b[2K"
113
111
 
114
112
  ExplainMessage = """
115
113
 
@@ -175,37 +173,37 @@ but that's sometime in the future.
175
173
 
176
174
  print("This script does 6 things... ")
177
175
  print(
178
- f"{Fore.BLUE} 0.{Fore.RESET} Checks to ensure you have the necessary cross-account role access to the child account."
176
+ f"[blue] 0. Checks to ensure you have the necessary cross-account role access to the child account."
179
177
  )
180
- print(f"{Fore.BLUE} 1.{Fore.RESET} Checks to ensure the {Fore.RED}Default VPCs {Fore.RESET}in each region are deleted")
178
+ print(f"[blue] 1. Checks to ensure the [red]Default VPCs in each region are deleted")
181
179
  if FixRun and not pVPCConfirm:
182
180
  print(
183
- f"{Fore.BLUE} You've asked to delete any default VPCs we find - with confirmation on each one.{Fore.RESET}"
181
+ f"[blue] You've asked to delete any default VPCs we find - with confirmation on each one."
184
182
  )
185
183
  elif FixRun and pVPCConfirm:
186
184
  print()
187
185
  print(
188
- f"{Fore.RED} You've asked to delete any default VPCs we find - WITH NO CONFIRMATION on each one.{Fore.RESET}"
186
+ f"[red] You've asked to delete any default VPCs we find - WITH NO CONFIRMATION on each one."
189
187
  )
190
188
  print()
191
189
  elif pVPCConfirm and not FixRun:
192
190
  print()
193
191
  print(
194
- f"{Fore.BLUE} You asked us to delete the default VPCs with no confirmation, but didn't provide the '+fixrun' parameter, so we're proceeding with NOT deleting. You can safely interupt this script and run it again with the necessary parameters.{Fore.RESET}"
192
+ f"[blue] You asked us to delete the default VPCs with no confirmation, but didn't provide the '+fixrun' parameter, so we're proceeding with NOT deleting. You can safely interupt this script and run it again with the necessary parameters."
195
193
  )
196
194
  print()
197
- print(f"{Fore.BLUE} 2.{Fore.RESET} Checks the child account in each of the regions")
198
- print(f" to see if there's already a {Fore.RED}Config Recorder and Delivery Channel {Fore.RESET}enabled...")
195
+ print(f"[blue] 2. Checks the child account in each of the regions")
196
+ print(f" to see if there's already a [red]Config Recorder and Delivery Channel enabled...")
199
197
  print(
200
- f"{Fore.BLUE} 3.{Fore.RESET} Checks that there isn't a duplicate {Fore.RED}CloudTrail{Fore.RESET} trail in the account."
198
+ f"[blue] 3. Checks that there isn't a duplicate [red]CloudTrail trail in the account."
201
199
  )
202
200
  print(
203
- f"{Fore.BLUE} 4.{Fore.RESET} Checks to see if {Fore.RED}GuardDuty{Fore.RESET} has been enabled for this child account."
201
+ f"[blue] 4. Checks to see if [red]GuardDuty has been enabled for this child account."
204
202
  )
205
203
  print(" If it has been, it needs to be deleted before we can adopt this new account")
206
204
  print(" into the Org's Automated Landing Zone.")
207
205
  print(
208
- f"{Fore.BLUE} 5.{Fore.RESET} This child account {Fore.RED}must exist{Fore.RESET} within the Parent Organization."
206
+ f"[blue] 5. This child account [red]must exist within the Parent Organization."
209
207
  )
210
208
  print(" If it doesn't - then you must move it into this Org")
211
209
  print(" (this script can't do that for you).")
@@ -277,10 +275,10 @@ for childaccount in ChildAccountList:
277
275
  finally:
278
276
  if not account_credentials.get("Success", False):
279
277
  logging.error(
280
- f"Was {Fore.RED}not{Fore.RESET} able to successfully connect to account {childaccount} using credentials from account {aws_account.acct_number}... "
278
+ f"Was [red]not able to successfully connect to account {childaccount} using credentials from account {aws_account.acct_number}... "
281
279
  )
282
280
  print()
283
- print(f"{Fore.RED}** Step 0 failed for account {childaccount}{Fore.RESET}")
281
+ print(f"[red]** Step 0 failed for account {childaccount}")
284
282
  print()
285
283
  ProcessStatus[childaccount]["Step0"]["Success"] = False
286
284
  ProcessStatus[childaccount]["Step0"]["IssuesFound"] += 1
@@ -290,7 +288,7 @@ for childaccount in ChildAccountList:
290
288
  f"Was able to successfully connect to account {childaccount} using credentials from account {aws_account.acct_number}... "
291
289
  )
292
290
  print()
293
- print(f"{Fore.GREEN}** Step 0 completed without issues{Fore.RESET}")
291
+ print(f"[green]** Step 0 completed without issues")
294
292
  print()
295
293
  ProcessStatus[childaccount]["Step0"]["Success"] = True
296
294
 
@@ -305,7 +303,7 @@ for childaccount in ChildAccountList:
305
303
  for region in RegionList:
306
304
  print(
307
305
  ERASE_LINE,
308
- f"Checking account {childaccount} in region {region} for {Fore.RED}default VPCs{Fore.RESET}",
306
+ f"Checking account {childaccount} in region {region} for [red]default VPCs",
309
307
  end="\r",
310
308
  )
311
309
  logging.info("Looking for Default VPCs in account {} from Region {}}", childaccount, region)
@@ -364,18 +362,18 @@ for childaccount in ChildAccountList:
364
362
 
365
363
  print()
366
364
  if ProcessStatus[childaccount]["Step1"]["Success"]:
367
- print(f"{ERASE_LINE + Fore.GREEN}** Step 1 completed with no issues{Fore.RESET}")
365
+ print(f"{ERASE_LINE + Fore.GREEN}** Step 1 completed with no issues")
368
366
  elif ProcessStatus[childaccount]["Step1"]["IssuesFound"] - ProcessStatus[childaccount]["Step1"]["IssuesFixed"] == 0:
369
367
  print(
370
- f"{ERASE_LINE + Fore.GREEN}** Step 1 found {ProcessStatus[childaccount]['Step1']['IssuesFound']} issues, but they were fixed by deleting the default vpcs{Fore.RESET}"
368
+ f"{ERASE_LINE + Fore.GREEN}** Step 1 found {ProcessStatus[childaccount]['Step1']['IssuesFound']} issues, but they were fixed by deleting the default vpcs"
371
369
  )
372
370
  ProcessStatus[childaccount]["Step1"]["Success"] = True
373
371
  elif ProcessStatus[childaccount]["Step1"]["IssuesFound"] > ProcessStatus[childaccount]["Step1"]["IssuesFixed"]:
374
372
  print(
375
- f"{ERASE_LINE + Fore.RED}** Step 1 completed, but there were {ProcessStatus[childaccount]['Step1']['IssuesFound'] - ProcessStatus[childaccount]['Step1']['IssuesFixed']} vpcs that couldn't be fixed{Fore.RESET}"
373
+ f"{ERASE_LINE + Fore.RED}** Step 1 completed, but there were {ProcessStatus[childaccount]['Step1']['IssuesFound'] - ProcessStatus[childaccount]['Step1']['IssuesFixed']} vpcs that couldn't be fixed"
376
374
  )
377
375
  else:
378
- print(f"{ERASE_LINE + Fore.RED}** Step 1 completed with blockers found{Fore.RESET}")
376
+ print(f"{ERASE_LINE + Fore.RED}** Step 1 completed with blockers found")
379
377
 
380
378
  # Step 2
381
379
  # 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.
@@ -422,7 +420,7 @@ for childaccount in ChildAccountList:
422
420
 
423
421
  for i in range(len(ConfigList)):
424
422
  logging.error(
425
- f"{Fore.RED}Found a config recorder for account %s in region %s",
423
+ f"[red]Found a config recorder for account %s in region %s",
426
424
  ConfigList[i]["AccountID"],
427
425
  ConfigList[i]["Region"] + Fore.RESET,
428
426
  )
@@ -442,7 +440,7 @@ for childaccount in ChildAccountList:
442
440
  ProcessStatus[childaccount]["Step2"]["IssuesFixed"] += 1
443
441
  for i in range(len(DeliveryChanList)):
444
442
  logging.error(
445
- f"{Fore.RED}I found a delivery channel for account %s in region %s",
443
+ f"[red]I found a delivery channel for account %s in region %s",
446
444
  DeliveryChanList[i]["AccountID"],
447
445
  DeliveryChanList[i]["Region"] + Fore.RESET,
448
446
  )
@@ -466,18 +464,18 @@ for childaccount in ChildAccountList:
466
464
  ProcessStatus[childaccount]["Step2"]["Success"] = True
467
465
 
468
466
  if ProcessStatus[childaccount]["Step2"]["Success"]:
469
- print(f"{ERASE_LINE + Fore.GREEN}** Step 2 completed with no issues{Fore.RESET}")
467
+ print(f"{ERASE_LINE + Fore.GREEN}** Step 2 completed with no issues")
470
468
  elif ProcessStatus[childaccount]["Step2"]["IssuesFound"] - ProcessStatus[childaccount]["Step2"]["IssuesFixed"] == 0:
471
469
  print(
472
- f"{ERASE_LINE + Fore.GREEN}** Step 2 found {ProcessStatus[childaccount]['Step2']['IssuesFound']} issues, but they were fixed by deleting the existing Config Recorders and Delivery Channels{Fore.RESET}"
470
+ f"{ERASE_LINE + Fore.GREEN}** Step 2 found {ProcessStatus[childaccount]['Step2']['IssuesFound']} issues, but they were fixed by deleting the existing Config Recorders and Delivery Channels"
473
471
  )
474
472
  ProcessStatus[childaccount]["Step2"]["Success"] = True
475
473
  elif ProcessStatus[childaccount]["Step2"]["IssuesFound"] > ProcessStatus[childaccount]["Step2"]["IssuesFixed"]:
476
474
  print(
477
- f"{ERASE_LINE + Fore.RED}** Step 2 completed, but there were {ProcessStatus[childaccount]['Step2']['IssuesFound'] - ProcessStatus[childaccount]['Step2']['IssuesFixed']} items found that couldn't be deleted{Fore.RESET}"
475
+ f"{ERASE_LINE + Fore.RED}** Step 2 completed, but there were {ProcessStatus[childaccount]['Step2']['IssuesFound'] - ProcessStatus[childaccount]['Step2']['IssuesFixed']} items found that couldn't be deleted"
478
476
  )
479
477
  else:
480
- print(f"{ERASE_LINE + Fore.RED}** Step 2 completed with blockers found{Fore.RESET}")
478
+ print(f"{ERASE_LINE + Fore.RED}** Step 2 completed with blockers found")
481
479
  print()
482
480
 
483
481
  # Step 3
@@ -505,7 +503,7 @@ for childaccount in ChildAccountList:
505
503
 
506
504
  for i in range(len(CTtrails2)):
507
505
  logging.error(
508
- f"{Fore.RED}Found a CloudTrail trail for account {childaccount} in region {CTtrails2[i]['HomeRegion']} named {CTtrails2[i]['Name']}{Fore.RESET}"
506
+ f"[red]Found a CloudTrail trail for account {childaccount} in region {CTtrails2[i]['HomeRegion']} named {CTtrails2[i]['Name']}"
509
507
  )
510
508
  ProcessStatus[childaccount]["Step3"]["IssuesFound"] += 1
511
509
  if FixRun:
@@ -523,18 +521,18 @@ for childaccount in ChildAccountList:
523
521
  ProcessStatus[childaccount]["Step3"]["Success"] = True
524
522
 
525
523
  if ProcessStatus[childaccount]["Step3"]["Success"]:
526
- print(f"{ERASE_LINE + Fore.GREEN}** Step 3 completed with no issues{Fore.RESET}")
524
+ print(f"{ERASE_LINE + Fore.GREEN}** Step 3 completed with no issues")
527
525
  elif ProcessStatus[childaccount]["Step3"]["IssuesFound"] - ProcessStatus[childaccount]["Step3"]["IssuesFixed"] == 0:
528
526
  print(
529
- f"{ERASE_LINE + Fore.GREEN}** Step 3 found {ProcessStatus[childaccount]['Step3']['IssuesFound']} issues, but they were fixed by deleting the existing CloudTrail trail names{Fore.RESET}"
527
+ f"{ERASE_LINE + Fore.GREEN}** Step 3 found {ProcessStatus[childaccount]['Step3']['IssuesFound']} issues, but they were fixed by deleting the existing CloudTrail trail names"
530
528
  )
531
529
  ProcessStatus[childaccount]["Step3"]["Success"] = True
532
530
  elif ProcessStatus[childaccount]["Step3"]["IssuesFound"] > ProcessStatus[childaccount]["Step3"]["IssuesFixed"]:
533
531
  print(
534
- f"{ERASE_LINE + Fore.RED}** Step 3 completed, but there were {ProcessStatus[childaccount]['Step3']['IssuesFound'] - ProcessStatus[childaccount]['Step3']['IssuesFixed']} trail names found that couldn't be deleted{Fore.RESET}"
532
+ f"{ERASE_LINE + Fore.RED}** Step 3 completed, but there were {ProcessStatus[childaccount]['Step3']['IssuesFound'] - ProcessStatus[childaccount]['Step3']['IssuesFixed']} trail names found that couldn't be deleted"
535
533
  )
536
534
  else:
537
- print(f"{ERASE_LINE + Fore.RED}** Step 3 completed with blockers found{Fore.RESET}")
535
+ print(f"{ERASE_LINE + Fore.RED}** Step 3 completed with blockers found")
538
536
  print()
539
537
 
540
538
  # Step 4
@@ -545,7 +543,7 @@ for childaccount in ChildAccountList:
545
543
  print(f"Checking account {childaccount} for any GuardDuty invites")
546
544
  for region in RegionList:
547
545
  print(
548
- f"{ERASE_LINE}Checking account {childaccount} in region {region} for {Fore.RED}GuardDuty{Fore.RESET}invitations",
546
+ f"{ERASE_LINE}Checking account {childaccount} in region {region} for [red]GuardDutyinvitations",
549
547
  end="\r",
550
548
  )
551
549
  GDinvites = Inventory_Modules.find_gd_invites2(account_credentials, region)
@@ -570,7 +568,7 @@ for childaccount in ChildAccountList:
570
568
 
571
569
  for i in range(len(GDinvites2)):
572
570
  logging.error(
573
- f"{Fore.RED}I found a GuardDuty invitation for account %s in region %s from account %s ",
571
+ f"[red]I found a GuardDuty invitation for account %s in region %s from account %s ",
574
572
  childaccount,
575
573
  GDinvites2[i]["Region"],
576
574
  GDinvites2[i]["AccountId"] + Fore.RESET,
@@ -593,18 +591,18 @@ for childaccount in ChildAccountList:
593
591
  ProcessStatus[childaccount]["Step4"]["Success"] = True
594
592
 
595
593
  if ProcessStatus[childaccount]["Step4"]["Success"]:
596
- print(f"{ERASE_LINE + Fore.GREEN}** Step 4 completed with no issues{Fore.RESET}")
594
+ print(f"{ERASE_LINE + Fore.GREEN}** Step 4 completed with no issues")
597
595
  elif ProcessStatus[childaccount]["Step4"]["IssuesFound"] - ProcessStatus[childaccount]["Step4"]["IssuesFixed"] == 0:
598
596
  print(
599
- f"{ERASE_LINE + Fore.GREEN}** Step 4 found {ProcessStatus[childaccount]['Step4']['IssuesFound']} guardduty invites, but they were deleted{Fore.RESET}"
597
+ f"{ERASE_LINE + Fore.GREEN}** Step 4 found {ProcessStatus[childaccount]['Step4']['IssuesFound']} guardduty invites, but they were deleted"
600
598
  )
601
599
  ProcessStatus[childaccount]["Step4"]["Success"] = True
602
600
  elif ProcessStatus[childaccount]["Step4"]["IssuesFound"] > ProcessStatus[childaccount]["Step4"]["IssuesFixed"]:
603
601
  print(
604
- f"{ERASE_LINE + Fore.RED}** Step 4 completed, but there were {ProcessStatus[childaccount]['Step4']['IssuesFound'] - ProcessStatus[childaccount]['Step4']['IssuesFixed']} guardduty invites found that couldn't be deleted{Fore.RESET}"
602
+ f"{ERASE_LINE + Fore.RED}** Step 4 completed, but there were {ProcessStatus[childaccount]['Step4']['IssuesFound'] - ProcessStatus[childaccount]['Step4']['IssuesFixed']} guardduty invites found that couldn't be deleted"
605
603
  )
606
604
  else:
607
- print(f"{ERASE_LINE + Fore.RED}** Step 4 completed with blockers found{Fore.RESET}")
605
+ print(f"{ERASE_LINE + Fore.RED}** Step 4 completed with blockers found")
608
606
  print()
609
607
 
610
608
  """
@@ -631,21 +629,21 @@ for childaccount in ChildAccountList:
631
629
  ProcessStatus[childaccount]["Step5"]["IssuesFound"] += 1
632
630
 
633
631
  if ProcessStatus[childaccount]["Step5"]["Success"]:
634
- print(f"{ERASE_LINE + Fore.GREEN}** Step 5 completed with no issues{Fore.RESET}")
632
+ print(f"{ERASE_LINE + Fore.GREEN}** Step 5 completed with no issues")
635
633
  elif ProcessStatus[childaccount]["Step5"]["IssuesFound"] - ProcessStatus[childaccount]["Step5"]["IssuesFixed"] == 0:
636
634
  print(
637
- f"{ERASE_LINE + Fore.GREEN}** Step 5 found {ProcessStatus[childaccount]['Step5']['IssuesFound']} issues, but we were able to move the account into the they were able to be fixed{Fore.RESET}"
635
+ f"{ERASE_LINE + Fore.GREEN}** Step 5 found {ProcessStatus[childaccount]['Step5']['IssuesFound']} issues, but we were able to move the account into the they were able to be fixed"
638
636
  )
639
637
  ProcessStatus[childaccount]["Step5"]["Success"] = True
640
638
  elif ProcessStatus[childaccount]["Step5"]["IssuesFound"] > ProcessStatus[childaccount]["Step5"]["IssuesFixed"]:
641
639
  print(
642
- f"{ERASE_LINE + Fore.RED}** Step 5 completed, but there were {ProcessStatus[childaccount]['Step5']['IssuesFound'] - ProcessStatus[childaccount]['Step5']['IssuesFixed']} blockers found that couldn't be fixed{Fore.RESET}"
640
+ f"{ERASE_LINE + Fore.RED}** Step 5 completed, but there were {ProcessStatus[childaccount]['Step5']['IssuesFound'] - ProcessStatus[childaccount]['Step5']['IssuesFixed']} blockers found that couldn't be fixed"
643
641
  )
644
642
  else:
645
- print(f"{ERASE_LINE + Fore.RED}** Step 5 completed with blockers found{Fore.RESET}")
643
+ print(f"{ERASE_LINE + Fore.RED}** Step 5 completed with blockers found")
646
644
  print()
647
645
 
648
- print(f"{Fore.CYAN}Account {childaccount} is complete. {accountsleft} more to go!!{Fore.RESET}")
646
+ print(f"[cyan]Account {childaccount} is complete. {accountsleft} more to go!!")
649
647
 
650
648
  """
651
649
  # Step 6
@@ -653,59 +651,62 @@ for childaccount in ChildAccountList:
653
651
  So we'll need to verify that the parent OU of the account is the root of the organization.
654
652
  """
655
653
 
656
- x = PrettyTable()
657
- y = PrettyTable()
658
-
659
- x.field_names = ["Account", "Issues Found", "Issues Fixed", "Ready?"]
660
- # The following headers represent Step0 through Step5,
661
- y.field_names = [
662
- "Account",
663
- "Account Access",
664
- "Default VPCs",
665
- "Recorders",
666
- "CloudTrail",
667
- "GuardDuty",
668
- "Org Member",
669
- "Ready?",
670
- ]
654
+ x = create_table(
655
+ title="Account Readiness Summary",
656
+ columns=[
657
+ {"header": "Account", "justify": "left"},
658
+ {"header": "Issues Found", "justify": "center"},
659
+ {"header": "Issues Fixed", "justify": "center"},
660
+ {"header": "Ready?", "justify": "center"},
661
+ ]
662
+ )
663
+ y = create_table(
664
+ title="Account Issue Details",
665
+ columns=[
666
+ {"header": "Account", "justify": "left"},
667
+ {"header": "Account Access", "justify": "center"},
668
+ {"header": "Default VPCs", "justify": "center"},
669
+ {"header": "Recorders", "justify": "center"},
670
+ {"header": "CloudTrail", "justify": "center"},
671
+ {"header": "GuardDuty", "justify": "center"},
672
+ {"header": "Org Member", "justify": "center"},
673
+ {"header": "Ready?", "justify": "center"},
674
+ ]
675
+ )
671
676
  for item in ProcessStatus:
672
677
  for _ in range(Steps):
673
678
  Step = f"Step{str(_)}"
674
679
  ProcessStatus[item]["IssuesFound"] += ProcessStatus[item][Step]["IssuesFound"]
675
680
  ProcessStatus[item]["IssuesFound"] += ProcessStatus[item][Step]["IssuesFixed"]
676
681
  x.add_row(
677
- [
678
- item,
679
- ProcessStatus[item]["IssuesFound"],
680
- ProcessStatus[item]["IssuesFixed"],
681
- ProcessStatus[item]["ChildIsReady"],
682
- ]
682
+ item,
683
+ str(ProcessStatus[item]["IssuesFound"]),
684
+ str(ProcessStatus[item]["IssuesFixed"]),
685
+ str(ProcessStatus[item]["ChildIsReady"]),
683
686
  )
684
687
  y.add_row(
685
- [
686
- item,
687
- ProcessStatus[item]["Step0"]["IssuesFound"] - ProcessStatus[item]["Step0"]["IssuesFixed"],
688
- ProcessStatus[item]["Step1"]["IssuesFound"] - ProcessStatus[item]["Step1"]["IssuesFixed"],
689
- ProcessStatus[item]["Step2"]["IssuesFound"] - ProcessStatus[item]["Step2"]["IssuesFixed"],
690
- ProcessStatus[item]["Step3"]["IssuesFound"] - ProcessStatus[item]["Step3"]["IssuesFixed"],
691
- ProcessStatus[item]["Step4"]["IssuesFound"] - ProcessStatus[item]["Step4"]["IssuesFixed"],
692
- ProcessStatus[item]["Step5"]["IssuesFound"] - ProcessStatus[item]["Step5"]["IssuesFixed"],
693
- ProcessStatus[item]["Step0"]["Success"]
688
+ item,
689
+ str(ProcessStatus[item]["Step0"]["IssuesFound"] - ProcessStatus[item]["Step0"]["IssuesFixed"]),
690
+ str(ProcessStatus[item]["Step1"]["IssuesFound"] - ProcessStatus[item]["Step1"]["IssuesFixed"]),
691
+ str(ProcessStatus[item]["Step2"]["IssuesFound"] - ProcessStatus[item]["Step2"]["IssuesFixed"]),
692
+ str(ProcessStatus[item]["Step3"]["IssuesFound"] - ProcessStatus[item]["Step3"]["IssuesFixed"]),
693
+ str(ProcessStatus[item]["Step4"]["IssuesFound"] - ProcessStatus[item]["Step4"]["IssuesFixed"]),
694
+ str(ProcessStatus[item]["Step5"]["IssuesFound"] - ProcessStatus[item]["Step5"]["IssuesFixed"]),
695
+ str(ProcessStatus[item]["Step0"]["Success"]
694
696
  and ProcessStatus[item]["Step1"]["Success"]
695
697
  and ProcessStatus[item]["Step2"]["Success"]
696
698
  and ProcessStatus[item]["Step3"]["Success"]
697
699
  and ProcessStatus[item]["Step4"]["Success"]
698
- and ProcessStatus[item]["Step5"]["Success"],
699
- ]
700
+ and ProcessStatus[item]["Step5"]["Success"]),
700
701
  )
701
- print(
702
- "The following table represents the accounts looked at, and whether they are ready to be incorporated into an ALZ environment."
702
+ console.print(
703
+ "\n[bold cyan]The following table represents the accounts looked at, and whether they are ready to be incorporated into an ALZ environment.[/bold cyan]"
703
704
  )
704
- print(x)
705
- print()
706
- print(
707
- "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 an ALZ environment."
705
+ console.print(x)
706
+ console.print()
707
+ console.print(
708
+ "[bold cyan]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 an ALZ environment.[/bold cyan]"
708
709
  )
709
- print(y)
710
+ console.print(y)
710
711
 
711
- print("Thanks for using this script...")
712
+ console.print("\n[green]Thanks for using this script...[/green]")
@@ -203,6 +203,17 @@ class InventoryFormatter(CloudFoundationsFormatter):
203
203
 
204
204
  def format_console_table(self) -> str:
205
205
  """Format inventory data for console display."""
206
+ import os
207
+ import sys
208
+ import re
209
+
210
+ # Test Mode Support: Disable Rich Console in test environments to prevent I/O conflicts
211
+ USE_RICH = os.getenv("RUNBOOKS_TEST_MODE") != "1"
212
+
213
+ if not USE_RICH:
214
+ # Test mode: Use simple text formatting to prevent Click CliRunner I/O conflicts
215
+ return self._format_simple_text_table()
216
+
206
217
  try:
207
218
  from rich.console import Console
208
219
  from rich.table import Table
@@ -58,7 +58,7 @@ from typing import Any, Dict, List, Optional
58
58
 
59
59
  import boto3
60
60
  from ArgumentsClass import CommonArguments
61
- from colorama import Fore, init
61
+ from runbooks.common.rich_utils import console
62
62
  from graphviz import Digraph
63
63
 
64
64
  # Optional imports for enhanced features
@@ -74,7 +74,6 @@ except ImportError:
74
74
 
75
75
  __version__ = "2025.04.09"
76
76
 
77
- init()
78
77
 
79
78
  # Visual styling constants
80
79
  account_fillcolor = "orange"
@@ -379,7 +378,7 @@ def generate_mermaid(org_structure: Any, filename: str) -> None:
379
378
  try:
380
379
  with open(filename, "w", encoding="utf-8") as f:
381
380
  f.write("\n".join(lines))
382
- print(f"Mermaid diagram successfully saved to '{Fore.RED}{filename}{Fore.RESET}'")
381
+ print(f"Mermaid diagram successfully saved to '[red]{filename}'")
383
382
  logging.info(f"Mermaid diagram successfully saved to {filename}")
384
383
  except Exception as e:
385
384
  logging.error(f"Failed to write Mermaid diagram to {filename}: {e}")
@@ -412,7 +411,7 @@ def generate_diagrams(org_structure: Any, filename: str) -> None:
412
411
  )
413
412
  except ImportError as imp_err:
414
413
  logging.error("Please install the 'diagrams' package to use generate_diagrams: pip install diagrams")
415
- print(f"{Fore.YELLOW}Warning: diagrams library not available. Install with: pip install diagrams{Fore.RESET}")
414
+ print(f"[yellow]Warning: diagrams library not available. Install with: pip install diagrams")
416
415
  return
417
416
 
418
417
  def build_diagram(node: Dict[str, Any]):
@@ -434,7 +433,7 @@ def generate_diagrams(org_structure: Any, filename: str) -> None:
434
433
  try:
435
434
  with Diagram("AWS Organization Diagram", filename=filename, show=False, direction="LR"):
436
435
  build_diagram(org_structure)
437
- print(f"Diagrams image successfully generated as '{Fore.RED}{filename}{Fore.RESET}'")
436
+ print(f"Diagrams image successfully generated as '[red]{filename}'")
438
437
  logging.info(f"Diagrams image successfully generated as {filename}")
439
438
  except Exception as e:
440
439
  logging.error(f"Failed to generate diagrams image: {e}")
@@ -636,7 +635,7 @@ def draw_org(froot: str, filename: str):
636
635
 
637
636
  # Save the diagram to a PNG file
638
637
  dot_unflat.render(filename, format="png", view=False)
639
- print(f"Diagram saved to '{Fore.RED}{filename}.png{Fore.RESET}'")
638
+ print(f"Diagram saved to '[red]{filename}.png'")
640
639
 
641
640
 
642
641
  #####################
@@ -708,7 +707,7 @@ if __name__ == "__main__":
708
707
  if anticipated_time > 30:
709
708
  print()
710
709
  print(
711
- f"{Fore.RED}Since this may take a while, you could re-run this script for only a specific OU by using the '--ou <OU ID>' parameter {Fore.RESET}"
710
+ f"[red]Since this may take a while, you could re-run this script for only a specific OU by using the '--ou <OU ID>' parameter "
712
711
  )
713
712
  print()
714
713
  else:
@@ -737,11 +736,11 @@ if __name__ == "__main__":
737
736
  # Display timing information
738
737
  if pTiming and pPolicy:
739
738
  print(
740
- f"{Fore.GREEN}Drawing the Org structure when policies are included took {time() - begin_time:.2f} seconds{Fore.RESET}"
739
+ f"[green]Drawing the Org structure when policies are included took {time() - begin_time:.2f} seconds"
741
740
  )
742
741
  elif pTiming:
743
742
  print(
744
- f"{Fore.GREEN}Drawing the Org structure without policies took {time() - begin_time:.2f} seconds{Fore.RESET}"
743
+ f"[green]Drawing the Org structure without policies took {time() - begin_time:.2f} seconds"
745
744
  )
746
745
 
747
746
  print("Thank you for using this script")
@@ -14,7 +14,7 @@ def del_vpc(ocredentials, fVPCId, fRegion):
14
14
 
15
15
  import boto3
16
16
  from botocore.exceptions import ClientError
17
- from colorama import Fore, init
17
+ from runbooks.common.rich_utils import console
18
18
 
19
19
  # ERASE_LINE = '\x1b[2K'
20
20
 
@@ -335,7 +335,7 @@ def del_vpc(ocredentials, fVPCId, fRegion):
335
335
  return 1 # out of the try
336
336
  except ClientError as my_Error:
337
337
  print(my_Error)
338
- print(f"{Fore.RED}What to do now?{Fore.RESET}")
338
+ print(f"[red]What to do now?")
339
339
  return 1
340
340
 
341
341
  return 0
@@ -70,9 +70,8 @@ import Inventory_Modules
70
70
  from account_class import aws_acct_access
71
71
  from ArgumentsClass import CommonArguments
72
72
  from botocore.exceptions import ClientError
73
- from colorama import Fore, init
73
+ from runbooks.common.rich_utils import console
74
74
 
75
- init()
76
75
  __version__ = "2023.05.04"
77
76
 
78
77
  # Configure comprehensive argument parsing for CloudFormation drift detection operations
@@ -142,7 +141,6 @@ This would enable:
142
141
  """
143
142
 
144
143
  ##########################
145
- ERASE_LINE = "\x1b[2K" # Terminal control for dynamic output updates
146
144
 
147
145
  # Initialize AWS account access and organizational context for drift detection
148
146
  aws_acct = aws_acct_access(pProfile)
@@ -235,7 +233,7 @@ for account in ChildAccounts:
235
233
  # Log regional stack discovery progress for operational visibility
236
234
  logging.warning(f"Account: {account['AccountId']} | Region: {region} | Found {StackNum} Stacks")
237
235
  logging.info(
238
- f"{ERASE_LINE}{Fore.RED}Account: {account['AccountId']} Region: {region} Found {StackNum} Stacks{Fore.RESET}"
236
+ f"[red]Account: {account['AccountId']} Region: {region} Found {StackNum} Stacks"
239
237
  )
240
238
 
241
239
  except ClientError as my_Error:
@@ -260,10 +258,10 @@ for account in ChildAccounts:
260
258
  NumStacksFound += 1 # Increment total drift detection counter
261
259
 
262
260
  # Provide comprehensive operational summary with drift detection metrics
263
- print(ERASE_LINE)
261
+ console.print()
264
262
  print(
265
- f"{Fore.RED}Looked through {NumStacksFound} Stacks across {len(ChildAccounts)} accounts across "
266
- f"{len(RegionList)} regions{Fore.RESET}"
263
+ f"[red]Looked through {NumStacksFound} Stacks across {len(ChildAccounts)} accounts across "
264
+ f"{len(RegionList)} regions"
267
265
  )
268
266
  print()
269
267
 
@@ -76,7 +76,7 @@ from ArgumentsClass import CommonArguments
76
76
 
77
77
  # import simplejson as json
78
78
  from botocore.exceptions import ClientError
79
- from colorama import Fore, init
79
+ from runbooks.common.rich_utils import console
80
80
  from Inventory_Modules import (
81
81
  display_results,
82
82
  find_stack_instances3,
@@ -87,9 +87,7 @@ from Inventory_Modules import (
87
87
  print_timings,
88
88
  )
89
89
 
90
- init()
91
90
  __version__ = "2024.05.18"
92
- ERASE_LINE = "\x1b[2K"
93
91
  begin_time = time()
94
92
 
95
93
 
@@ -250,7 +248,7 @@ def setup_auth_and_regions(fProfile: str, f_AccountList: list, f_Region: str, f_
250
248
  pass # Valid profile specification (string or None for default)
251
249
  else: # Invalid profile type (list, integer, etc.) - should be caught at argparse level
252
250
  print(
253
- f"{Fore.RED}You specified an invalid profile name. This script only allows for one profile at a time. Please try again.{Fore.RESET}"
251
+ f"[red]You specified an invalid profile name. This script only allows for one profile at a time. Please try again."
254
252
  )
255
253
  sys.exit(7)
256
254
 
@@ -268,8 +266,8 @@ def setup_auth_and_regions(fProfile: str, f_AccountList: list, f_Region: str, f_
268
266
  # Validate home region specification ensuring single region for StackSet control plane
269
267
  if f_Region.lower() not in AllRegions:
270
268
  print(
271
- f"{Fore.RED}You specified '{f_Region}' as the region, but this script only works with a single region.\n"
272
- f"Please run the command again and specify only a single, valid region{Fore.RESET}"
269
+ f"[red]You specified '{f_Region}' as the region, but this script only works with a single region.\n"
270
+ f"Please run the command again and specify only a single, valid region"
273
271
  )
274
272
  sys.exit(9)
275
273
 
@@ -309,12 +307,12 @@ def setup_auth_and_regions(fProfile: str, f_AccountList: list, f_Region: str, f_
309
307
 
310
308
  # Display account exclusion list if specified
311
309
  print(
312
- f"While skipping these accounts:\n{Fore.RED}{f_args.SkipAccounts}{Fore.RESET}"
310
+ f"While skipping these accounts:\n[red]{f_args.SkipAccounts}"
313
311
  ) if f_args.SkipAccounts is not None else ""
314
312
 
315
313
  # Display fragment filtering configuration with exact vs substring matching
316
314
  if f_args.Exact:
317
- print(f"\t\tFor stacksets that {Fore.RED}exactly match{Fore.RESET}: {f_args.Fragments}")
315
+ print(f"\t\tFor stacksets that [red]exactly match: {f_args.Fragments}")
318
316
  else:
319
317
  print(
320
318
  f"\t\tFor stacksets that contain th{'is fragment' if len(f_args.Fragments) == 1 else 'ese fragments'}: {f_args.Fragments}"
@@ -712,7 +710,7 @@ if __name__ == "__main__":
712
710
 
713
711
  if pTiming:
714
712
  print(ERASE_LINE)
715
- print(f"{Fore.GREEN}This script took {time() - begin_time:.2f} seconds{Fore.RESET}")
713
+ print(f"[green]This script took {time() - begin_time:.2f} seconds")
716
714
 
717
715
  print()
718
716
  print("Thanks for using this script...")
@@ -70,11 +70,10 @@ import Inventory_Modules
70
70
  from account_class import aws_acct_access
71
71
  from ArgumentsClass import CommonArguments
72
72
  from botocore.exceptions import ClientError
73
- from colorama import Fore, init
73
+ from runbooks.common.rich_utils import console
74
74
  from Inventory_Modules import display_results
75
75
 
76
76
  # Initialize colorama for cross-platform colored terminal output
77
- init()
78
77
  __version__ = "2024.03.06"
79
78
  begin_time = time() # Script execution timing for performance monitoring
80
79
  sleep_interval = 5 # Default wait interval for drift detection operations
@@ -248,7 +247,7 @@ def setup_auth(fProfile: str) -> aws_acct_access:
248
247
 
249
248
  # Validate successful authentication and account access
250
249
  if not aws_acct.Success:
251
- print(f"{Fore.RED}Profile {pProfile} failed to access an account. Check credentials and try again{Fore.RESET}")
250
+ print(f"[red]Profile {pProfile} failed to access an account. Check credentials and try again")
252
251
  sys.exit(99)
253
252
 
254
253
  # Display comprehensive operational context for user confirmation and audit logging
@@ -259,7 +258,7 @@ def setup_auth(fProfile: str) -> aws_acct_access:
259
258
 
260
259
  # Display fragment filtering criteria with exact vs substring matching context
261
260
  if pExact:
262
- print(f"\t\tFor stacksets that {Fore.RED}exactly match{Fore.RESET}: {pFragments}")
261
+ print(f"\t\tFor stacksets that [red]exactly match: {pFragments}")
263
262
  else:
264
263
  print(
265
264
  f"\t\tFor stacksets that contain th{'is fragment' if len(pFragments) == 1 else 'ese fragments'}: {pFragments}"
@@ -723,11 +722,11 @@ if __name__ == "__main__":
723
722
  display_results(sorted_all_stacksets, display_dict_stacksets, None, pSaveFilename)
724
723
 
725
724
  print(ERASE_LINE)
726
- print(f"{Fore.RED}Looked through {len(sorted_all_stacksets)} StackSets across the {pRegion} region{Fore.RESET}")
725
+ print(f"[red]Looked through {len(sorted_all_stacksets)} StackSets across the {pRegion} region")
727
726
  print()
728
727
 
729
728
  if pTiming:
730
729
  print(ERASE_LINE)
731
- print(f"{Fore.GREEN}This script took {time() - begin_time:.3f} seconds{Fore.RESET}")
730
+ print(f"[green]This script took {time() - begin_time:.3f} seconds")
732
731
  print("Thanks for using this script...")
733
732
  print()