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.
- runbooks/__init__.py +1 -1
- runbooks/__init___optimized.py +2 -1
- runbooks/_platform/__init__.py +1 -1
- runbooks/cfat/cli.py +4 -3
- runbooks/cfat/cloud_foundations_assessment.py +1 -2
- runbooks/cfat/tests/test_cli.py +4 -1
- runbooks/cli/commands/finops.py +68 -19
- runbooks/cli/commands/inventory.py +796 -7
- runbooks/cli/commands/operate.py +65 -4
- runbooks/cloudops/cost_optimizer.py +1 -3
- runbooks/common/cli_decorators.py +6 -4
- runbooks/common/config_loader.py +787 -0
- runbooks/common/config_schema.py +280 -0
- runbooks/common/dry_run_framework.py +14 -2
- runbooks/common/mcp_integration.py +238 -0
- runbooks/finops/ebs_cost_optimizer.py +7 -4
- runbooks/finops/elastic_ip_optimizer.py +7 -4
- runbooks/finops/infrastructure/__init__.py +3 -2
- runbooks/finops/infrastructure/commands.py +7 -4
- runbooks/finops/infrastructure/load_balancer_optimizer.py +7 -4
- runbooks/finops/infrastructure/vpc_endpoint_optimizer.py +7 -4
- runbooks/finops/nat_gateway_optimizer.py +7 -4
- runbooks/finops/tests/run_tests.py +1 -1
- runbooks/inventory/ArgumentsClass.py +2 -1
- runbooks/inventory/README.md +111 -12
- runbooks/inventory/Tests/test_Inventory_Modules.py +27 -10
- runbooks/inventory/Tests/test_cfn_describe_stacks.py +18 -7
- runbooks/inventory/Tests/test_ec2_describe_instances.py +30 -15
- runbooks/inventory/Tests/test_lambda_list_functions.py +17 -3
- runbooks/inventory/Tests/test_org_list_accounts.py +17 -4
- runbooks/inventory/account_class.py +0 -1
- runbooks/inventory/all_my_instances_wrapper.py +4 -8
- runbooks/inventory/aws_organization.png +0 -0
- runbooks/inventory/check_cloudtrail_compliance.py +4 -4
- runbooks/inventory/check_controltower_readiness.py +50 -47
- runbooks/inventory/check_landingzone_readiness.py +35 -31
- runbooks/inventory/cloud_foundations_integration.py +8 -3
- runbooks/inventory/core/collector.py +201 -1
- runbooks/inventory/discovery.md +2 -1
- runbooks/inventory/{draw_org_structure.py → draw_org.py} +55 -9
- runbooks/inventory/drift_detection_cli.py +8 -68
- runbooks/inventory/find_cfn_drift_detection.py +14 -4
- runbooks/inventory/find_cfn_orphaned_stacks.py +7 -5
- runbooks/inventory/find_cfn_stackset_drift.py +5 -5
- runbooks/inventory/find_ec2_security_groups.py +6 -3
- runbooks/inventory/find_landingzone_versions.py +5 -5
- runbooks/inventory/find_vpc_flow_logs.py +5 -5
- runbooks/inventory/inventory.sh +20 -7
- runbooks/inventory/inventory_mcp_cli.py +4 -0
- runbooks/inventory/inventory_modules.py +9 -7
- runbooks/inventory/list_cfn_stacks.py +18 -8
- runbooks/inventory/list_cfn_stackset_operation_results.py +2 -2
- runbooks/inventory/list_cfn_stackset_operations.py +32 -20
- runbooks/inventory/list_cfn_stacksets.py +7 -4
- runbooks/inventory/list_config_recorders_delivery_channels.py +4 -4
- runbooks/inventory/list_ds_directories.py +3 -3
- runbooks/inventory/list_ec2_availability_zones.py +7 -3
- runbooks/inventory/list_ec2_ebs_volumes.py +3 -3
- runbooks/inventory/list_ec2_instances.py +1 -1
- runbooks/inventory/list_ecs_clusters_and_tasks.py +8 -4
- runbooks/inventory/list_elbs_load_balancers.py +7 -3
- runbooks/inventory/list_enis_network_interfaces.py +3 -3
- runbooks/inventory/list_guardduty_detectors.py +9 -5
- runbooks/inventory/list_iam_policies.py +7 -3
- runbooks/inventory/list_iam_roles.py +3 -3
- runbooks/inventory/list_iam_saml_providers.py +8 -4
- runbooks/inventory/list_lambda_functions.py +8 -4
- runbooks/inventory/list_org_accounts.py +306 -276
- runbooks/inventory/list_org_accounts_users.py +45 -9
- runbooks/inventory/list_rds_db_instances.py +4 -4
- runbooks/inventory/list_route53_hosted_zones.py +3 -3
- runbooks/inventory/list_servicecatalog_provisioned_products.py +5 -5
- runbooks/inventory/list_sns_topics.py +4 -4
- runbooks/inventory/list_ssm_parameters.py +6 -3
- runbooks/inventory/list_vpc_subnets.py +8 -4
- runbooks/inventory/list_vpcs.py +15 -4
- runbooks/inventory/mcp_vpc_validator.py +6 -0
- runbooks/inventory/organizations_discovery.py +17 -3
- runbooks/inventory/organizations_utils.py +553 -0
- runbooks/inventory/output_formatters.py +422 -0
- runbooks/inventory/recover_cfn_stack_ids.py +5 -5
- runbooks/inventory/run_on_multi_accounts.py +3 -3
- runbooks/inventory/tag_coverage.py +481 -0
- runbooks/inventory/validation_utils.py +358 -0
- runbooks/inventory/verify_ec2_security_groups.py +18 -5
- runbooks/inventory/vpc_architecture_validator.py +7 -1
- runbooks/inventory/vpc_dependency_analyzer.py +6 -0
- runbooks/main_final.py +2 -2
- runbooks/main_ultra_minimal.py +2 -2
- runbooks/mcp/integration.py +6 -4
- runbooks/remediation/acm_remediation.py +2 -2
- runbooks/remediation/cloudtrail_remediation.py +2 -2
- runbooks/remediation/cognito_remediation.py +2 -2
- runbooks/remediation/dynamodb_remediation.py +2 -2
- runbooks/remediation/ec2_remediation.py +2 -2
- runbooks/remediation/kms_remediation.py +2 -2
- runbooks/remediation/lambda_remediation.py +2 -2
- runbooks/remediation/rds_remediation.py +2 -2
- runbooks/remediation/s3_remediation.py +1 -1
- runbooks/vpc/cloudtrail_audit_integration.py +1 -1
- {runbooks-1.1.9.dist-info → runbooks-1.1.10.dist-info}/METADATA +74 -4
- {runbooks-1.1.9.dist-info → runbooks-1.1.10.dist-info}/RECORD +106 -100
- runbooks/__init__.py.backup +0 -134
- {runbooks-1.1.9.dist-info → runbooks-1.1.10.dist-info}/WHEEL +0 -0
- {runbooks-1.1.9.dist-info → runbooks-1.1.10.dist-info}/entry_points.txt +0 -0
- {runbooks-1.1.9.dist-info → runbooks-1.1.10.dist-info}/licenses/LICENSE +0 -0
- {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,
|
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-
|
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
|
-
"
|
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-
|
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
|
-
|
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-
|
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
|
-
"
|
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
|