runbooks 1.1.9__py3-none-any.whl → 1.1.10__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 (107) hide show
  1. runbooks/__init__.py +1 -1
  2. runbooks/__init___optimized.py +2 -1
  3. runbooks/_platform/__init__.py +1 -1
  4. runbooks/cfat/cli.py +4 -3
  5. runbooks/cfat/cloud_foundations_assessment.py +1 -2
  6. runbooks/cfat/tests/test_cli.py +4 -1
  7. runbooks/cli/commands/finops.py +68 -19
  8. runbooks/cli/commands/inventory.py +796 -7
  9. runbooks/cli/commands/operate.py +65 -4
  10. runbooks/cloudops/cost_optimizer.py +1 -3
  11. runbooks/common/cli_decorators.py +6 -4
  12. runbooks/common/config_loader.py +787 -0
  13. runbooks/common/config_schema.py +280 -0
  14. runbooks/common/dry_run_framework.py +14 -2
  15. runbooks/common/mcp_integration.py +238 -0
  16. runbooks/finops/ebs_cost_optimizer.py +7 -4
  17. runbooks/finops/elastic_ip_optimizer.py +7 -4
  18. runbooks/finops/infrastructure/__init__.py +3 -2
  19. runbooks/finops/infrastructure/commands.py +7 -4
  20. runbooks/finops/infrastructure/load_balancer_optimizer.py +7 -4
  21. runbooks/finops/infrastructure/vpc_endpoint_optimizer.py +7 -4
  22. runbooks/finops/nat_gateway_optimizer.py +7 -4
  23. runbooks/finops/tests/run_tests.py +1 -1
  24. runbooks/inventory/ArgumentsClass.py +2 -1
  25. runbooks/inventory/README.md +111 -12
  26. runbooks/inventory/Tests/test_Inventory_Modules.py +27 -10
  27. runbooks/inventory/Tests/test_cfn_describe_stacks.py +18 -7
  28. runbooks/inventory/Tests/test_ec2_describe_instances.py +30 -15
  29. runbooks/inventory/Tests/test_lambda_list_functions.py +17 -3
  30. runbooks/inventory/Tests/test_org_list_accounts.py +17 -4
  31. runbooks/inventory/account_class.py +0 -1
  32. runbooks/inventory/all_my_instances_wrapper.py +4 -8
  33. runbooks/inventory/aws_organization.png +0 -0
  34. runbooks/inventory/check_cloudtrail_compliance.py +4 -4
  35. runbooks/inventory/check_controltower_readiness.py +50 -47
  36. runbooks/inventory/check_landingzone_readiness.py +35 -31
  37. runbooks/inventory/cloud_foundations_integration.py +8 -3
  38. runbooks/inventory/core/collector.py +201 -1
  39. runbooks/inventory/discovery.md +2 -1
  40. runbooks/inventory/{draw_org_structure.py → draw_org.py} +55 -9
  41. runbooks/inventory/drift_detection_cli.py +8 -68
  42. runbooks/inventory/find_cfn_drift_detection.py +14 -4
  43. runbooks/inventory/find_cfn_orphaned_stacks.py +7 -5
  44. runbooks/inventory/find_cfn_stackset_drift.py +5 -5
  45. runbooks/inventory/find_ec2_security_groups.py +6 -3
  46. runbooks/inventory/find_landingzone_versions.py +5 -5
  47. runbooks/inventory/find_vpc_flow_logs.py +5 -5
  48. runbooks/inventory/inventory.sh +20 -7
  49. runbooks/inventory/inventory_mcp_cli.py +4 -0
  50. runbooks/inventory/inventory_modules.py +9 -7
  51. runbooks/inventory/list_cfn_stacks.py +18 -8
  52. runbooks/inventory/list_cfn_stackset_operation_results.py +2 -2
  53. runbooks/inventory/list_cfn_stackset_operations.py +32 -20
  54. runbooks/inventory/list_cfn_stacksets.py +7 -4
  55. runbooks/inventory/list_config_recorders_delivery_channels.py +4 -4
  56. runbooks/inventory/list_ds_directories.py +3 -3
  57. runbooks/inventory/list_ec2_availability_zones.py +7 -3
  58. runbooks/inventory/list_ec2_ebs_volumes.py +3 -3
  59. runbooks/inventory/list_ec2_instances.py +1 -1
  60. runbooks/inventory/list_ecs_clusters_and_tasks.py +8 -4
  61. runbooks/inventory/list_elbs_load_balancers.py +7 -3
  62. runbooks/inventory/list_enis_network_interfaces.py +3 -3
  63. runbooks/inventory/list_guardduty_detectors.py +9 -5
  64. runbooks/inventory/list_iam_policies.py +7 -3
  65. runbooks/inventory/list_iam_roles.py +3 -3
  66. runbooks/inventory/list_iam_saml_providers.py +8 -4
  67. runbooks/inventory/list_lambda_functions.py +8 -4
  68. runbooks/inventory/list_org_accounts.py +306 -276
  69. runbooks/inventory/list_org_accounts_users.py +45 -9
  70. runbooks/inventory/list_rds_db_instances.py +4 -4
  71. runbooks/inventory/list_route53_hosted_zones.py +3 -3
  72. runbooks/inventory/list_servicecatalog_provisioned_products.py +5 -5
  73. runbooks/inventory/list_sns_topics.py +4 -4
  74. runbooks/inventory/list_ssm_parameters.py +6 -3
  75. runbooks/inventory/list_vpc_subnets.py +8 -4
  76. runbooks/inventory/list_vpcs.py +15 -4
  77. runbooks/inventory/mcp_vpc_validator.py +6 -0
  78. runbooks/inventory/organizations_discovery.py +17 -3
  79. runbooks/inventory/organizations_utils.py +553 -0
  80. runbooks/inventory/output_formatters.py +422 -0
  81. runbooks/inventory/recover_cfn_stack_ids.py +5 -5
  82. runbooks/inventory/run_on_multi_accounts.py +3 -3
  83. runbooks/inventory/tag_coverage.py +481 -0
  84. runbooks/inventory/validation_utils.py +358 -0
  85. runbooks/inventory/verify_ec2_security_groups.py +18 -5
  86. runbooks/inventory/vpc_architecture_validator.py +7 -1
  87. runbooks/inventory/vpc_dependency_analyzer.py +6 -0
  88. runbooks/main_final.py +2 -2
  89. runbooks/main_ultra_minimal.py +2 -2
  90. runbooks/mcp/integration.py +6 -4
  91. runbooks/remediation/acm_remediation.py +2 -2
  92. runbooks/remediation/cloudtrail_remediation.py +2 -2
  93. runbooks/remediation/cognito_remediation.py +2 -2
  94. runbooks/remediation/dynamodb_remediation.py +2 -2
  95. runbooks/remediation/ec2_remediation.py +2 -2
  96. runbooks/remediation/kms_remediation.py +2 -2
  97. runbooks/remediation/lambda_remediation.py +2 -2
  98. runbooks/remediation/rds_remediation.py +2 -2
  99. runbooks/remediation/s3_remediation.py +1 -1
  100. runbooks/vpc/cloudtrail_audit_integration.py +1 -1
  101. {runbooks-1.1.9.dist-info → runbooks-1.1.10.dist-info}/METADATA +74 -4
  102. {runbooks-1.1.9.dist-info → runbooks-1.1.10.dist-info}/RECORD +106 -100
  103. runbooks/__init__.py.backup +0 -134
  104. {runbooks-1.1.9.dist-info → runbooks-1.1.10.dist-info}/WHEEL +0 -0
  105. {runbooks-1.1.9.dist-info → runbooks-1.1.10.dist-info}/entry_points.txt +0 -0
  106. {runbooks-1.1.9.dist-info → runbooks-1.1.10.dist-info}/licenses/LICENSE +0 -0
  107. {runbooks-1.1.9.dist-info → runbooks-1.1.10.dist-info}/top_level.txt +0 -0
@@ -71,7 +71,7 @@ def create_inventory_group():
71
71
  @common_output_options
72
72
  @common_multi_account_options
73
73
  @common_filter_options
74
- def inventory(ctx, profile, region, dry_run, output_format, output_dir, export,
74
+ def inventory(ctx, profile, region, dry_run, format, output_dir, export,
75
75
  all_profiles, profiles, regions, all_regions, tags, accounts):
76
76
  """
77
77
  Universal AWS resource discovery and inventory - works with ANY AWS environment.
@@ -89,7 +89,7 @@ def create_inventory_group():
89
89
  runbooks inventory collect # Use default profile
90
90
  runbooks inventory collect --profile my-profile # Use specific profile
91
91
  runbooks inventory collect --resources ec2,rds # Specific resources
92
- runbooks inventory collect --all-profiles # Multi-account (if Organizations access)
92
+ runbooks inventory collect --all-profile MANAGEMENT_PROFILE # Multi-account Organizations auto-discovery
93
93
  runbooks inventory collect --tags Environment=prod # Filtered discovery
94
94
  """
95
95
  # Ensure context object exists
@@ -102,7 +102,7 @@ def create_inventory_group():
102
102
  "profile": profile,
103
103
  "region": region,
104
104
  "dry_run": dry_run,
105
- "output_format": output_format,
105
+ "format": format,
106
106
  "output_dir": output_dir,
107
107
  "export": export,
108
108
  "all_profiles": all_profiles,
@@ -121,7 +121,7 @@ def create_inventory_group():
121
121
  @click.option("--profile", type=str, default=None, help="AWS profile to use (overrides parent group)")
122
122
  @click.option("--resources", "-r", multiple=True, help="Resource types (ec2, rds, lambda, s3, etc.)")
123
123
  @click.option("--all-resources", is_flag=True, help="Collect all resource types")
124
- @click.option("--all-profiles", is_flag=True, help="Collect from all organization accounts")
124
+ @click.option("--all-profile", type=str, default=None, help="Management profile for Organizations API auto-discovery (MANAGEMENT_PROFILE, BILLING_PROFILE, or CENTRALISED_OPS_PROFILE)")
125
125
  @click.option("--all-regions", is_flag=True, help="Execute inventory collection across all AWS regions")
126
126
  @click.option("--include-costs", is_flag=True, help="Include cost information")
127
127
  @click.option(
@@ -177,7 +177,7 @@ def create_inventory_group():
177
177
  profile,
178
178
  resources,
179
179
  all_resources,
180
- all_profiles,
180
+ all_profile,
181
181
  all_regions,
182
182
  include_costs,
183
183
  include_security,
@@ -226,7 +226,7 @@ def create_inventory_group():
226
226
  # Universal compatibility - works with any AWS setup
227
227
  runbooks inventory collect # Default profile
228
228
  runbooks inventory collect --profile my-aws-profile # Any profile
229
- runbooks inventory collect --all-profiles # Auto-detects Organizations
229
+ runbooks inventory collect --all-profile MANAGEMENT_PROFILE # Organizations auto-discovery
230
230
 
231
231
  # Resource-specific discovery
232
232
  runbooks inventory collect --resources ec2,rds,s3 # Specific services
@@ -255,7 +255,7 @@ def create_inventory_group():
255
255
  "dry_run": dry_run,
256
256
  "resources": resources,
257
257
  "all_resources": all_resources,
258
- "all_profiles": all_profiles,
258
+ "all_profile": all_profile,
259
259
  "all_regions": all_regions,
260
260
  "include_costs": include_costs,
261
261
  "include_security": include_security,
@@ -364,4 +364,793 @@ def create_inventory_group():
364
364
  # Future work: Implement proper RDS snapshots discovery in v1.2.0
365
365
  # See: artifacts/future-work/rds-snapshots-discovery-v1.2.0.md
366
366
 
367
+ @inventory.command(name="draw-org")
368
+ @click.option("--profile", type=str, default=None, help="AWS profile to use (overrides group-level --profile)")
369
+ @click.option("--policy/--no-policy", is_flag=True, default=False,
370
+ help="Include policies in organization diagram")
371
+ @click.option("--show-aws-managed/--hide-aws-managed", is_flag=True, default=False,
372
+ help="Show AWS managed SCPs (hidden by default)")
373
+ @click.option("--ou", "--starting-ou", type=str, default=None,
374
+ help="Starting organizational unit ID (defaults to root)")
375
+ @click.option("-f", "--format", "--output-format",
376
+ type=click.Choice(["graphviz", "mermaid", "diagrams"]),
377
+ default="graphviz",
378
+ help="Diagram format: graphviz (PNG), mermaid (text), diagrams (Python library). (-f/--format preferred, --output-format legacy)")
379
+ @click.option("-v", "--verbose", count=True, help="Increase verbosity: -v (WARNING), -vv (INFO), -vvv (DEBUG). Default: ERROR level")
380
+ @click.option("-d", "--debug", is_flag=True, help="Enable DEBUG level logging (equivalent to -vvv)")
381
+ @click.option("--timing", is_flag=True, help="Show performance metrics")
382
+ @click.option("--skip-accounts", multiple=True, help="Exclude AWS account IDs from diagram (space-separated)")
383
+ @click.option("--skip-ous", multiple=True, help="Exclude organizational unit IDs from diagram (space-separated)")
384
+ @click.option("--output", "-o", default=None, help="Custom output filename (without extension). Default: aws_organization")
385
+ @click.pass_context
386
+ def draw_org(ctx, profile, policy, show_aws_managed, ou, format, verbose, debug, timing, skip_accounts, skip_ous, output):
387
+ """
388
+ Visualize AWS Organizations structure with multiple output formats.
389
+
390
+ Generates organization diagrams showing accounts, OUs, and policies
391
+ with support for Graphviz (PNG), Mermaid, and Diagrams library formats.
392
+
393
+ Examples:
394
+ # Basic diagram with default profile
395
+ runbooks inventory draw-org
396
+
397
+ # With specific management profile
398
+ runbooks inventory draw-org --profile $MANAGEMENT_PROFILE
399
+
400
+ # Include policies and AWS managed SCPs
401
+ runbooks inventory draw-org --policy --show-aws-managed
402
+
403
+ # Start from specific OU in Mermaid format
404
+ runbooks inventory draw-org --ou ou-1234567890 --output-format mermaid
405
+
406
+ # Diagrams library format with timing
407
+ runbooks inventory draw-org --output-format diagrams --timing
408
+
409
+ # Multi-level verbosity
410
+ runbooks inventory draw-org -vv # WARNING level
411
+ runbooks inventory draw-org -vvv # INFO level
412
+
413
+ # Skip accounts/OUs (large organizations)
414
+ runbooks inventory draw-org --skip-accounts 123456789012 987654321098
415
+
416
+ # Custom output filename
417
+ runbooks inventory draw-org --output prod-org
418
+ """
419
+ try:
420
+ from runbooks.inventory.draw_org import (
421
+ draw_org as draw_org_diagram,
422
+ generate_mermaid,
423
+ generate_diagrams,
424
+ find_accounts_in_org,
425
+ get_enabled_policy_types
426
+ )
427
+ import boto3
428
+ import logging
429
+ from time import time as get_time
430
+
431
+ # Profile priority: command-level > group-level > environment > boto3 default
432
+ # This allows both patterns to work:
433
+ # runbooks inventory draw-org --profile X (command-level)
434
+ # runbooks inventory --profile X draw-org (group-level)
435
+ if not profile:
436
+ profile = ctx.obj.get('profile')
437
+ if not profile:
438
+ import os
439
+ profile = os.getenv('AWS_PROFILE')
440
+
441
+ # Note: boto3.Session() handles 'default' profile fallback internally.
442
+ # Explicit fallback to 'default' here causes SSO profile users to fail when
443
+ # no profile is specified (SSO configs don't have 'default' entry).
444
+
445
+ # Configure logging based on verbosity level
446
+ # v1.1.10 enhancement: Error-visible default (no silent mode)
447
+ log_levels = {
448
+ 0: logging.ERROR, # Default (errors visible)
449
+ 1: logging.WARNING, # -v (warnings)
450
+ 2: logging.INFO, # -vv (info)
451
+ 3: logging.DEBUG # -vvv (debug)
452
+ }
453
+
454
+ # Handle -d/--debug flag (overrides verbose count)
455
+ if debug:
456
+ log_level = logging.DEBUG
457
+ else:
458
+ log_level = log_levels.get(verbose, logging.ERROR)
459
+
460
+ logging.basicConfig(
461
+ level=log_level,
462
+ format='[%(filename)s:%(lineno)s - %(funcName)20s() ] %(message)s'
463
+ )
464
+
465
+ # Suppress boto3 noise unless in DEBUG mode
466
+ if log_level > logging.DEBUG:
467
+ logging.getLogger("boto3").setLevel(logging.CRITICAL)
468
+ logging.getLogger("botocore").setLevel(logging.CRITICAL)
469
+ logging.getLogger("s3transfer").setLevel(logging.CRITICAL)
470
+ logging.getLogger("urllib3").setLevel(logging.CRITICAL)
471
+
472
+ # Rich CLI output with enterprise UX
473
+ console.print(f"[blue]🌳 AWS Organizations Structure Visualization[/blue]")
474
+ verbosity_label = {0: "error", 1: "warning", 2: "info", 3: "debug"}.get(verbose, "error")
475
+ if debug:
476
+ verbosity_label = "debug"
477
+ console.print(f"[dim]Profile: {profile or 'environment fallback'} | Format: {format} | Verbosity: {verbosity_label}[/dim]")
478
+
479
+ begin_time = get_time()
480
+
481
+ # AWS Organizations client initialization
482
+ org_session = boto3.Session(profile_name=profile)
483
+ org_client = org_session.client('organizations')
484
+
485
+ # Get enabled policy types (required even for non-policy diagrams)
486
+ # Note: This is a module-level function that uses the global org_client
487
+ # We need to set the global org_client before calling get_enabled_policy_types
488
+ import runbooks.inventory.draw_org as draw_org_module
489
+ draw_org_module.org_client = org_client
490
+ enabled_policy_types = get_enabled_policy_types()
491
+
492
+ # Determine starting point and output filename
493
+ if ou:
494
+ root = ou
495
+ # Use custom output filename if provided, otherwise default to subset
496
+ filename = output if output else "aws_organization_subset"
497
+ console.print(f"[dim]Starting from OU: {ou}[/dim]")
498
+ else:
499
+ root = org_client.list_roots()["Roots"][0]["Id"]
500
+ # Use custom output filename if provided, otherwise default
501
+ filename = output if output else "aws_organization"
502
+ console.print(f"[dim]Starting from organization root[/dim]")
503
+
504
+ # Display custom filename if provided
505
+ if output:
506
+ console.print(f"[dim]Custom output: {filename}.{{png|dot|mmd}}[/dim]")
507
+
508
+ # Account discovery for progress estimation
509
+ all_accounts = find_accounts_in_org()
510
+
511
+ # Apply skip filters if provided
512
+ excluded_accounts = set(skip_accounts) if skip_accounts else set()
513
+ excluded_ous = set(skip_ous) if skip_ous else set()
514
+
515
+ if excluded_accounts:
516
+ console.print(f"[yellow]⚠️ Excluding {len(excluded_accounts)} accounts[/yellow]")
517
+ logging.info(f"Excluded accounts: {excluded_accounts}")
518
+ # Filter accounts
519
+ all_accounts = [acc for acc in all_accounts if acc['Id'] not in excluded_accounts]
520
+
521
+ # Validation: Ensure at least 1 account remains
522
+ if not all_accounts:
523
+ console.print(f"[red]❌ All accounts excluded by filters. Diagram would be empty.[/red]")
524
+ raise click.ClickException(
525
+ "Skip filters excluded all accounts. Remove some exclusions or check account IDs."
526
+ )
527
+
528
+ if excluded_ous:
529
+ console.print(f"[yellow]⚠️ Excluding {len(excluded_ous)} organizational units[/yellow]")
530
+ logging.info(f"Excluded OUs: {excluded_ous}")
531
+
532
+ console.print(f"[dim]Discovered {len(all_accounts)} accounts in organization{' (after filtering)' if excluded_accounts else ''}[/dim]")
533
+
534
+ # Set module-level variables for policy handling and filters
535
+ draw_org_module.pPolicy = policy
536
+ draw_org_module.pManaged = show_aws_managed
537
+
538
+ # Set module-level skip filters (for diagram generation)
539
+ draw_org_module.excluded_accounts = excluded_accounts
540
+ draw_org_module.excluded_ous = excluded_ous
541
+
542
+ # Generate diagram based on format
543
+ if format == "graphviz":
544
+ draw_org_diagram(root, filename)
545
+ console.print(f"[green]✅ Graphviz diagram: {filename}.png[/green]")
546
+ elif format == "mermaid":
547
+ mermaid_file = f"{filename}.mmd"
548
+ generate_mermaid(root, mermaid_file)
549
+ console.print(f"[green]✅ Mermaid diagram: {mermaid_file}[/green]")
550
+ elif format == "diagrams":
551
+ generate_diagrams(root, filename)
552
+ console.print(f"[green]✅ Diagrams visualization: {filename}[/green]")
553
+
554
+ if timing:
555
+ elapsed = get_time() - begin_time
556
+ console.print(f"[dim]⏱️ Execution time: {elapsed:.2f}s[/dim]")
557
+
558
+ except Exception as e:
559
+ console.print(f"[red]❌ Organization diagram generation failed: {e}[/red]")
560
+ if verbose:
561
+ import traceback
562
+ console.print(f"[dim]{traceback.format_exc()}[/dim]")
563
+ raise click.ClickException(str(e))
564
+
565
+ @inventory.command(name='list-org-accounts')
566
+ @click.option('--profile', type=str, default=None, help='AWS profile to use (overrides group-level --profile)')
567
+ @click.option('--short', '-s', '-q', is_flag=True, help='Brief listing without child accounts')
568
+ @click.option('--acct', '-A', multiple=True, help='Find which org these accounts belong to')
569
+ @click.option('--root-only', is_flag=True, help='Show only management accounts')
570
+ @click.option('-f', '--format', '--export-format',
571
+ type=click.Choice(['json', 'csv', 'markdown', 'table']),
572
+ default='table', help='Export format (-f/--format preferred, --export-format legacy)')
573
+ @click.option('--output', '-o', help='Output filename (for export formats)')
574
+ @click.option('--timing', is_flag=True, help='Show performance metrics')
575
+ @click.option('-v', '--verbose', count=True, help='Increase verbosity')
576
+ @click.option('--skip-profiles', multiple=True, help='Profiles to exclude from discovery')
577
+ @click.pass_context
578
+ def list_org_accounts(ctx, profile, short, acct, root_only, format, output, timing, verbose, skip_profiles):
579
+ """
580
+ List all accounts in AWS Organizations.
581
+
582
+ Supports multi-account discovery via --all-profiles flag at group level:
583
+ runbooks inventory --all-profiles mgmt list-org-accounts
584
+
585
+ Single account mode:
586
+ runbooks inventory --profile mgmt list-org-accounts
587
+
588
+ Examples:
589
+ # Multi-account Organizations discovery
590
+ runbooks inventory --all-profiles $MANAGEMENT_PROFILE list-org-accounts
591
+
592
+ # Brief listing with timing
593
+ runbooks inventory --profile mgmt list-org-accounts --short --timing
594
+
595
+ # Find specific accounts across organizations
596
+ runbooks inventory --all-profiles mgmt list-org-accounts --acct 123456789012 987654321098
597
+
598
+ # Export to CSV
599
+ runbooks inventory --profile mgmt list-org-accounts --export-format csv --output orgs
600
+ """
601
+ try:
602
+ from runbooks.inventory.list_org_accounts import list_organization_accounts
603
+ import logging
604
+ from time import time as get_time
605
+ import os
606
+
607
+ # Configure logging based on verbosity
608
+ log_levels = {0: logging.ERROR, 1: logging.WARNING, 2: logging.INFO, 3: logging.DEBUG}
609
+ log_level = log_levels.get(verbose, logging.ERROR)
610
+ logging.basicConfig(level=log_level, format='[%(filename)s:%(lineno)s] %(message)s')
611
+
612
+ # Suppress AWS SDK noise
613
+ if log_level > logging.DEBUG:
614
+ for logger_name in ['boto3', 'botocore', 's3transfer', 'urllib3']:
615
+ logging.getLogger(logger_name).setLevel(logging.CRITICAL)
616
+
617
+ begin_time = get_time()
618
+
619
+ # Profile priority: command-level > group-level > environment > default
620
+ # This allows both patterns to work:
621
+ # runbooks inventory list-org-accounts --profile X (command-level)
622
+ # runbooks inventory --profile X list-org-accounts (group-level)
623
+ if not profile:
624
+ profile = ctx.obj.get('profile')
625
+
626
+ # Get other context parameters
627
+ all_profiles = ctx.obj.get('all_profiles')
628
+ profiles = ctx.obj.get('profiles', [])
629
+
630
+ # Determine discovery mode
631
+ if all_profiles:
632
+ # --all-profiles mode: Organizations API discovery
633
+ discovery_profiles = [all_profiles]
634
+ discovery_mode = "Organizations API (--all-profiles)"
635
+ elif profiles:
636
+ # --profiles mode: Multiple profiles specified
637
+ discovery_profiles = profiles
638
+ discovery_mode = f"Multi-profile ({len(profiles)} profiles)"
639
+ elif profile:
640
+ # --profile mode: Single profile
641
+ discovery_profiles = [profile]
642
+ discovery_mode = "Single profile"
643
+ else:
644
+ # Default: AWS_PROFILE environment variable or boto3 default
645
+ # Note: boto3.Session() handles 'default' profile fallback internally.
646
+ # Explicit fallback to 'default' here causes SSO profile users to fail when
647
+ # no profile is specified (SSO configs don't have 'default' entry).
648
+ env_profile = os.getenv('AWS_PROFILE')
649
+ discovery_profiles = [env_profile] if env_profile else [None]
650
+ discovery_mode = "Environment/Default profile"
651
+
652
+ console.print(f"[blue]📋 AWS Organizations Account Inventory[/blue]")
653
+ console.print(f"[dim]Mode: {discovery_mode} | Profiles: {len(discovery_profiles)} | Format: {format}[/dim]")
654
+
655
+ # Execute discovery
656
+ results = list_organization_accounts(
657
+ profiles=discovery_profiles,
658
+ short_form=short,
659
+ root_only=root_only,
660
+ account_lookup=list(acct) if acct else None,
661
+ export_format=format,
662
+ output_file=output,
663
+ skip_profiles=list(skip_profiles) if skip_profiles else None,
664
+ verbose=log_level
665
+ )
666
+
667
+ if timing:
668
+ elapsed = get_time() - begin_time
669
+ console.print(f"[dim]⏱️ Execution time: {elapsed:.2f}s[/dim]")
670
+
671
+ console.print("[green]✅ Account discovery complete[/green]")
672
+
673
+ except Exception as e:
674
+ console.print(f"[red]❌ Organizations account discovery failed: {e}[/red]")
675
+ if verbose >= 2:
676
+ import traceback
677
+ console.print(f"[dim]{traceback.format_exc()}[/dim]")
678
+ raise click.ClickException(str(e))
679
+
680
+ @inventory.command(name='list-org-users')
681
+ @click.option('--profile', type=str, default=None, help='AWS profile (overrides group-level)')
682
+ @click.option('--iam', is_flag=True, help='Discover IAM users only')
683
+ @click.option('--idc', is_flag=True, help='Discover Identity Center users only')
684
+ @click.option('--short', '-s', '-q', is_flag=True, help='Brief summary without detailed enumeration')
685
+ @click.option('-f', '--format', '--export-format',
686
+ type=click.Choice(['json', 'csv', 'markdown', 'table']),
687
+ default='table', help='Export format (-f/--format preferred, --export-format legacy)')
688
+ @click.option('--output', '-o', help='Output filename')
689
+ @click.option('--timing', is_flag=True, help='Show performance metrics')
690
+ @click.option('-v', '--verbose', count=True, help='Increase verbosity')
691
+ @click.pass_context
692
+ def list_org_users_cmd(ctx, profile, iam, idc, short, format, output, timing, verbose):
693
+ """
694
+ Discover IAM users and AWS Identity Center users across AWS Organizations.
695
+
696
+ Comprehensive user discovery supporting both traditional IAM and modern
697
+ AWS Identity Center identity sources for enterprise identity governance.
698
+
699
+ Identity Sources:
700
+ Default: Both IAM and Identity Center users
701
+ --iam: Traditional IAM users only
702
+ --idc: AWS Identity Center users only
703
+
704
+ Examples:
705
+ # Discover all users (IAM + Identity Center)
706
+ runbooks inventory --profile $MANAGEMENT_PROFILE list-org-users
707
+
708
+ # IAM users only
709
+ runbooks inventory --profile mgmt list-org-users --iam --short
710
+
711
+ # Identity Center only with CSV export
712
+ runbooks inventory --profile mgmt list-org-users --idc --export-format csv
713
+ """
714
+ try:
715
+ from runbooks.inventory.list_org_accounts_users import find_all_org_users
716
+ from runbooks.inventory.inventory_modules import get_all_credentials, display_results
717
+ import logging
718
+ from time import time as get_time
719
+
720
+ # Configure logging
721
+ log_levels = {0: logging.ERROR, 1: logging.WARNING, 2: logging.INFO, 3: logging.DEBUG}
722
+ log_level = log_levels.get(verbose, logging.ERROR)
723
+ logging.basicConfig(level=log_level, format='[%(filename)s:%(lineno)s] %(message)s')
724
+
725
+ if log_level > logging.DEBUG:
726
+ for logger_name in ['boto3', 'botocore', 's3transfer', 'urllib3']:
727
+ logging.getLogger(logger_name).setLevel(logging.CRITICAL)
728
+
729
+ begin_time = get_time()
730
+
731
+ # Profile resolution (SSO compatible - NO 'default' hardcoding)
732
+ if not profile:
733
+ profile = ctx.obj.get('profile')
734
+ if not profile:
735
+ import os
736
+ profile = os.getenv('AWS_PROFILE')
737
+
738
+ # Identity source selection (default: both IAM and IDC)
739
+ if not iam and not idc:
740
+ iam = True
741
+ idc = True
742
+
743
+ console.print(f"[blue]👥 AWS Organizations User Inventory[/blue]")
744
+ console.print(f"[dim]Profile: {profile or 'environment fallback'} | Sources: {'IAM' if iam else ''}{' + ' if iam and idc else ''}{'Identity Center' if idc else ''}[/dim]")
745
+
746
+ # Get credentials for cross-account access
747
+ credential_list = get_all_credentials(
748
+ [profile] if profile else [None],
749
+ pTiming=timing,
750
+ pSkipProfiles=[],
751
+ pSkipAccounts=[],
752
+ pRootOnly=False,
753
+ pAccounts=None,
754
+ pRegionList=['us-east-1'],
755
+ pAccessRoles=None
756
+ )
757
+
758
+ # Discover users across organization
759
+ user_listing = find_all_org_users(credential_list, f_IDC=idc, f_IAM=iam)
760
+ sorted_user_listing = sorted(
761
+ user_listing, key=lambda k: (k["MgmtAccount"], k["AccountId"], k["Region"], k["UserName"])
762
+ )
763
+
764
+ # Display results
765
+ display_dict = {
766
+ "MgmtAccount": {"DisplayOrder": 1, "Heading": "Mgmt Acct"},
767
+ "AccountId": {"DisplayOrder": 2, "Heading": "Acct Number"},
768
+ "Region": {"DisplayOrder": 3, "Heading": "Region"},
769
+ "UserName": {"DisplayOrder": 4, "Heading": "User Name"},
770
+ "PasswordLastUsed": {"DisplayOrder": 5, "Heading": "Last Used"},
771
+ "Type": {"DisplayOrder": 6, "Heading": "Source"},
772
+ }
773
+
774
+ # Handle output file naming
775
+ output_file = output if export_format != 'table' else None
776
+
777
+ display_results(sorted_user_listing, display_dict, "N/A", output_file)
778
+
779
+ successful_accounts = [x for x in credential_list if x["Success"]]
780
+ console.print(f"\n[green]✅ Found {len(user_listing)} users across {len(successful_accounts)} accounts[/green]")
781
+
782
+ if timing:
783
+ elapsed = get_time() - begin_time
784
+ console.print(f"[dim]⏱️ Execution time: {elapsed:.2f}s[/dim]")
785
+
786
+ except Exception as e:
787
+ console.print(f"[red]❌ User discovery failed: {e}[/red]")
788
+ if verbose >= 2:
789
+ import traceback
790
+ console.print(f"[dim]{traceback.format_exc()}[/dim]")
791
+ raise click.ClickException(str(e))
792
+
793
+ @inventory.command(name='find-lz-versions')
794
+ @click.option('--profile', type=str, default=None, help='AWS profile (overrides group-level)')
795
+ @click.option('--timing', is_flag=True, help='Show performance metrics')
796
+ @click.option('-f', '--format', '--export-format',
797
+ type=click.Choice(['json', 'csv', 'markdown', 'table']),
798
+ default='table', help='Export format (-f/--format preferred, --export-format legacy)')
799
+ @click.option('--output', '-o', help='Output filename')
800
+ @click.option('--latest', is_flag=True, help='Show only accounts not on latest version')
801
+ @click.option('-v', '--verbose', count=True, help='Increase verbosity')
802
+ @click.pass_context
803
+ def find_lz_versions_cmd(ctx, profile, timing, format, output, latest, verbose):
804
+ """
805
+ Discover AWS Landing Zone versions across organization.
806
+
807
+ Identifies Landing Zone deployments by analyzing CloudFormation stacks
808
+ for SO0044 solution and extracting version information from stack outputs.
809
+
810
+ Version Analysis:
811
+ - CloudFormation stack detection (SO0044 Landing Zone solution)
812
+ - Version extraction from stack outputs
813
+ - Account Factory product versions (Service Catalog)
814
+ - Version drift calculation
815
+
816
+ Examples:
817
+ # Basic version discovery
818
+ runbooks inventory --profile $MANAGEMENT_PROFILE find-lz-versions
819
+
820
+ # Show only version drift
821
+ runbooks inventory --profile mgmt find-lz-versions --latest
822
+
823
+ # CSV export with timing
824
+ runbooks inventory --profile mgmt find-lz-versions --export-format csv --timing
825
+ """
826
+ try:
827
+ import boto3
828
+ import logging
829
+ from time import time as get_time
830
+ from runbooks.inventory import inventory_modules as Inventory_Modules
831
+ from runbooks.common.rich_utils import create_table
832
+
833
+ # Configure logging
834
+ log_levels = {0: logging.ERROR, 1: logging.WARNING, 2: logging.INFO, 3: logging.DEBUG}
835
+ log_level = log_levels.get(verbose, logging.ERROR)
836
+ logging.basicConfig(level=log_level, format='[%(filename)s:%(lineno)s] %(message)s')
837
+
838
+ if log_level > logging.DEBUG:
839
+ for logger_name in ['boto3', 'botocore', 's3transfer', 'urllib3']:
840
+ logging.getLogger(logger_name).setLevel(logging.CRITICAL)
841
+
842
+ begin_time = get_time()
843
+
844
+ # Profile resolution (SSO compatible)
845
+ if not profile:
846
+ profile = ctx.obj.get('profile')
847
+ if not profile:
848
+ import os
849
+ profile = os.getenv('AWS_PROFILE')
850
+
851
+ console.print(f"[blue]🔍 AWS Landing Zone Version Discovery[/blue]")
852
+ console.print(f"[dim]Profile: {profile or 'environment fallback'} | Format: {format} | Drift only: {latest}[/dim]")
853
+
854
+ # Discover Landing Zone Management Accounts
855
+ all_profiles = [profile] if profile else [None]
856
+ skip_profiles = ["default"]
857
+
858
+ alz_profiles = []
859
+ for prof in all_profiles:
860
+ try:
861
+ alz_mgmt_acct = Inventory_Modules.find_if_alz(prof)
862
+ if alz_mgmt_acct["ALZ"]:
863
+ account_num = Inventory_Modules.find_account_number(prof)
864
+ alz_profiles.append({
865
+ "Profile": prof,
866
+ "Acctnum": account_num,
867
+ "Region": alz_mgmt_acct["Region"]
868
+ })
869
+ except Exception as e:
870
+ logging.debug(f"Profile {prof} is not a Landing Zone Management Account: {e}")
871
+ continue
872
+
873
+ if not alz_profiles:
874
+ console.print("[yellow]⚠️ No Landing Zone Management Accounts found[/yellow]")
875
+ return
876
+
877
+ # Create results table
878
+ table = create_table(
879
+ title="AWS Landing Zone Versions",
880
+ columns=[
881
+ {"header": "Profile", "justify": "left"},
882
+ {"header": "Account", "justify": "left"},
883
+ {"header": "Region", "justify": "left"},
884
+ {"header": "Stack Name", "justify": "left"},
885
+ {"header": "Version", "justify": "left"},
886
+ ]
887
+ )
888
+
889
+ # Analyze Landing Zone versions
890
+ for item in alz_profiles:
891
+ aws_session = boto3.Session(profile_name=item["Profile"], region_name=item["Region"])
892
+ cfn_client = aws_session.client("cloudformation")
893
+
894
+ stack_list = cfn_client.describe_stacks()["Stacks"]
895
+
896
+ for stack in stack_list:
897
+ if "Description" in stack and "SO0044" in stack["Description"]:
898
+ for output in stack.get("Outputs", []):
899
+ if output["OutputKey"] == "LandingZoneSolutionVersion":
900
+ alz_version = output["OutputValue"]
901
+ table.add_row(
902
+ item["Profile"],
903
+ item["Acctnum"],
904
+ item["Region"],
905
+ stack["StackName"],
906
+ alz_version
907
+ )
908
+
909
+ console.print()
910
+ console.print(table)
911
+ console.print(f"\n[green]✅ Discovered {len(alz_profiles)} Landing Zone deployments[/green]")
912
+
913
+ if timing:
914
+ elapsed = get_time() - begin_time
915
+ console.print(f"[dim]⏱️ Execution time: {elapsed:.2f}s[/dim]")
916
+
917
+ except Exception as e:
918
+ console.print(f"[red]❌ Landing Zone version discovery failed: {e}[/red]")
919
+ if verbose >= 2:
920
+ import traceback
921
+ console.print(f"[dim]{traceback.format_exc()}[/dim]")
922
+ raise click.ClickException(str(e))
923
+
924
+ @inventory.command(name='check-landingzone')
925
+ @click.option('--profile', type=str, default=None, help='AWS profile (overrides group-level)')
926
+ @click.option('--timing', is_flag=True, help='Show performance metrics')
927
+ @click.option('-f', '--format', '--export-format',
928
+ type=click.Choice(['json', 'markdown', 'table']),
929
+ default='table', help='Export format (-f/--format preferred, --export-format legacy)')
930
+ @click.option('--output', '-o', help='Output filename')
931
+ @click.option('--ou', type=str, default=None, help='Specific OU to validate')
932
+ @click.option('-v', '--verbose', count=True, help='Increase verbosity')
933
+ @click.pass_context
934
+ def check_landingzone_cmd(ctx, profile, timing, format, output, ou, verbose):
935
+ """
936
+ Validate AWS Landing Zone readiness and prerequisites.
937
+
938
+ Comprehensive validation of Landing Zone deployment prerequisites including
939
+ default VPCs, Config recorders, CloudTrail trails, and organizational membership.
940
+
941
+ Validation Checks:
942
+ - Default VPCs across all regions
943
+ - Config Recorder and Delivery Channel conflicts
944
+ - CloudTrail trail naming conflicts
945
+ - AWS Organizations membership
946
+ - Organizational Unit placement
947
+
948
+ Examples:
949
+ # Full readiness check
950
+ runbooks inventory --profile $MANAGEMENT_PROFILE check-landingzone
951
+
952
+ # Specific OU validation
953
+ runbooks inventory --profile mgmt check-landingzone --ou ou-xxxx-xxxxxxxx
954
+
955
+ # JSON export with timing
956
+ runbooks inventory --profile mgmt check-landingzone --export-format json --timing
957
+ """
958
+ try:
959
+ from runbooks.inventory.validation_utils import (
960
+ validate_organizations_enabled,
961
+ validate_iam_role_exists,
962
+ validate_config_enabled,
963
+ validate_cloudtrail_enabled,
964
+ calculate_readiness_score,
965
+ generate_remediation_recommendations
966
+ )
967
+ import logging
968
+ from time import time as get_time
969
+ from runbooks.common.rich_utils import create_table
970
+
971
+ # Configure logging
972
+ log_levels = {0: logging.ERROR, 1: logging.WARNING, 2: logging.INFO, 3: logging.DEBUG}
973
+ log_level = log_levels.get(verbose, logging.ERROR)
974
+ logging.basicConfig(level=log_level, format='[%(filename)s:%(lineno)s] %(message)s')
975
+
976
+ if log_level > logging.DEBUG:
977
+ for logger_name in ['boto3', 'botocore', 's3transfer', 'urllib3']:
978
+ logging.getLogger(logger_name).setLevel(logging.CRITICAL)
979
+
980
+ begin_time = get_time()
981
+
982
+ # Profile resolution (SSO compatible)
983
+ if not profile:
984
+ profile = ctx.obj.get('profile')
985
+ if not profile:
986
+ import os
987
+ profile = os.getenv('AWS_PROFILE')
988
+
989
+ console.print(f"[blue]🔍 AWS Landing Zone Readiness Validation[/blue]")
990
+ console.print(f"[dim]Profile: {profile or 'environment fallback'} | OU: {ou or 'all'} | Format: {format}[/dim]")
991
+
992
+ # Execute validation checks
993
+ checks = []
994
+ checks.append(validate_organizations_enabled(profile))
995
+ checks.append(validate_iam_role_exists(profile, 'AWSCloudFormationStackSetExecutionRole'))
996
+ checks.append(validate_config_enabled(profile))
997
+ checks.append(validate_cloudtrail_enabled(profile))
998
+
999
+ # Calculate readiness score
1000
+ score = calculate_readiness_score(checks)
1001
+ status = "READY" if score >= 90 else "PARTIAL" if score >= 50 else "NOT READY"
1002
+
1003
+ # Generate remediation recommendations
1004
+ remediations = generate_remediation_recommendations(checks)
1005
+
1006
+ # Create results table
1007
+ table = create_table(
1008
+ title="Landing Zone Readiness Assessment",
1009
+ columns=[
1010
+ {"header": "Check", "justify": "left"},
1011
+ {"header": "Status", "justify": "center"},
1012
+ {"header": "Details", "justify": "left"},
1013
+ ]
1014
+ )
1015
+
1016
+ for check in checks:
1017
+ status_indicator = "[green]✅ PASS[/green]" if check["passed"] else "[red]❌ FAIL[/red]"
1018
+ table.add_row(check["check_name"], status_indicator, check.get("message", ""))
1019
+
1020
+ console.print()
1021
+ console.print(table)
1022
+ console.print(f"\n[{'green' if score >= 90 else 'yellow' if score >= 50 else 'red'}]Readiness Score: {score}/100 - {status}[/{'green' if score >= 90 else 'yellow' if score >= 50 else 'red'}]")
1023
+
1024
+ if remediations:
1025
+ console.print("\n[yellow]📋 Remediation Recommendations:[/yellow]")
1026
+ for remediation in remediations:
1027
+ console.print(f" • {remediation}")
1028
+
1029
+ if timing:
1030
+ elapsed = get_time() - begin_time
1031
+ console.print(f"\n[dim]⏱️ Execution time: {elapsed:.2f}s[/dim]")
1032
+
1033
+ except Exception as e:
1034
+ console.print(f"[red]❌ Landing Zone readiness check failed: {e}[/red]")
1035
+ if verbose >= 2:
1036
+ import traceback
1037
+ console.print(f"[dim]{traceback.format_exc()}[/dim]")
1038
+ raise click.ClickException(str(e))
1039
+
1040
+ @inventory.command(name='check-controltower')
1041
+ @click.option('--profile', type=str, default=None, help='AWS profile (overrides group-level)')
1042
+ @click.option('--timing', is_flag=True, help='Show performance metrics')
1043
+ @click.option('-f', '--format', '--export-format',
1044
+ type=click.Choice(['json', 'markdown', 'table']),
1045
+ default='table', help='Export format (-f/--format preferred, --export-format legacy)')
1046
+ @click.option('--output', '-o', help='Output filename')
1047
+ @click.option('-v', '--verbose', count=True, help='Increase verbosity')
1048
+ @click.pass_context
1049
+ def check_controltower_cmd(ctx, profile, timing, format, output, verbose):
1050
+ """
1051
+ Validate AWS Control Tower readiness and prerequisites.
1052
+
1053
+ Comprehensive validation of Control Tower deployment prerequisites including
1054
+ AWS Config, CloudTrail, IAM roles, and organizational compliance requirements.
1055
+
1056
+ Validation Checks:
1057
+ - AWS Organizations enabled
1058
+ - CloudTrail organizational trail configured
1059
+ - AWS Config Recorder and Delivery Channel
1060
+ - Required IAM roles (AWSControlTowerExecution, AWSControlTowerStackSetRole)
1061
+ - Service-linked roles and permissions
1062
+
1063
+ Examples:
1064
+ # Full Control Tower readiness assessment
1065
+ runbooks inventory --profile $MANAGEMENT_PROFILE check-controltower
1066
+
1067
+ # JSON export for automation
1068
+ runbooks inventory --profile mgmt check-controltower --export-format json --output ct-readiness
1069
+
1070
+ # With timing and verbose output
1071
+ runbooks inventory --profile mgmt check-controltower --timing -vv
1072
+ """
1073
+ try:
1074
+ from runbooks.inventory.validation_utils import (
1075
+ validate_organizations_enabled,
1076
+ validate_cloudtrail_enabled,
1077
+ validate_config_enabled,
1078
+ validate_iam_role_exists,
1079
+ calculate_readiness_score,
1080
+ generate_remediation_recommendations
1081
+ )
1082
+ import logging
1083
+ from time import time as get_time
1084
+ from runbooks.common.rich_utils import create_table
1085
+
1086
+ # Configure logging
1087
+ log_levels = {0: logging.ERROR, 1: logging.WARNING, 2: logging.INFO, 3: logging.DEBUG}
1088
+ log_level = log_levels.get(verbose, logging.ERROR)
1089
+ logging.basicConfig(level=log_level, format='[%(filename)s:%(lineno)s] %(message)s')
1090
+
1091
+ if log_level > logging.DEBUG:
1092
+ for logger_name in ['boto3', 'botocore', 's3transfer', 'urllib3']:
1093
+ logging.getLogger(logger_name).setLevel(logging.CRITICAL)
1094
+
1095
+ begin_time = get_time()
1096
+
1097
+ # Profile resolution (SSO compatible)
1098
+ if not profile:
1099
+ profile = ctx.obj.get('profile')
1100
+ if not profile:
1101
+ import os
1102
+ profile = os.getenv('AWS_PROFILE')
1103
+
1104
+ console.print(f"[blue]🔍 AWS Control Tower Readiness Validation[/blue]")
1105
+ console.print(f"[dim]Profile: {profile or 'environment fallback'} | Format: {format}[/dim]")
1106
+
1107
+ # Execute validation checks
1108
+ checks = []
1109
+ checks.append(validate_organizations_enabled(profile))
1110
+ checks.append(validate_cloudtrail_enabled(profile))
1111
+ checks.append(validate_config_enabled(profile))
1112
+ checks.append(validate_iam_role_exists(profile, 'AWSControlTowerExecution'))
1113
+ checks.append(validate_iam_role_exists(profile, 'AWSControlTowerStackSetRole'))
1114
+
1115
+ # Calculate readiness score
1116
+ score = calculate_readiness_score(checks)
1117
+ status = "READY" if score >= 90 else "PARTIAL" if score >= 50 else "NOT_READY"
1118
+
1119
+ # Generate remediation recommendations
1120
+ remediations = generate_remediation_recommendations(checks)
1121
+
1122
+ # Create results table
1123
+ table = create_table(
1124
+ title="Control Tower Readiness Assessment",
1125
+ columns=[
1126
+ {"header": "Check", "justify": "left"},
1127
+ {"header": "Status", "justify": "center"},
1128
+ {"header": "Details", "justify": "left"},
1129
+ ]
1130
+ )
1131
+
1132
+ for check in checks:
1133
+ status_indicator = "[green]✅ PASS[/green]" if check["passed"] else "[red]❌ FAIL[/red]"
1134
+ table.add_row(check["check_name"], status_indicator, check.get("message", ""))
1135
+
1136
+ console.print()
1137
+ console.print(table)
1138
+ console.print(f"\n[{'green' if score >= 90 else 'yellow' if score >= 50 else 'red'}]Readiness Score: {score}/100 - {status}[/{'green' if score >= 90 else 'yellow' if score >= 50 else 'red'}]")
1139
+
1140
+ if remediations:
1141
+ console.print("\n[yellow]📋 Remediation Recommendations:[/yellow]")
1142
+ for remediation in remediations:
1143
+ console.print(f" • {remediation}")
1144
+
1145
+ if timing:
1146
+ elapsed = get_time() - begin_time
1147
+ console.print(f"\n[dim]⏱️ Execution time: {elapsed:.2f}s[/dim]")
1148
+
1149
+ except Exception as e:
1150
+ console.print(f"[red]❌ Control Tower readiness check failed: {e}[/red]")
1151
+ if verbose >= 2:
1152
+ import traceback
1153
+ console.print(f"[dim]{traceback.format_exc()}[/dim]")
1154
+ raise click.ClickException(str(e))
1155
+
367
1156
  return inventory