runbooks 0.9.9__py3-none-any.whl → 1.0.0__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 (71) hide show
  1. runbooks/cfat/cloud_foundations_assessment.py +626 -0
  2. runbooks/cloudops/cost_optimizer.py +95 -33
  3. runbooks/common/aws_pricing.py +388 -0
  4. runbooks/common/aws_pricing_api.py +205 -0
  5. runbooks/common/aws_utils.py +2 -2
  6. runbooks/common/comprehensive_cost_explorer_integration.py +979 -0
  7. runbooks/common/cross_account_manager.py +606 -0
  8. runbooks/common/enhanced_exception_handler.py +4 -0
  9. runbooks/common/env_utils.py +96 -0
  10. runbooks/common/mcp_integration.py +49 -2
  11. runbooks/common/organizations_client.py +579 -0
  12. runbooks/common/profile_utils.py +96 -2
  13. runbooks/finops/cost_optimizer.py +2 -1
  14. runbooks/finops/elastic_ip_optimizer.py +13 -9
  15. runbooks/finops/embedded_mcp_validator.py +31 -0
  16. runbooks/finops/enhanced_trend_visualization.py +3 -2
  17. runbooks/finops/markdown_exporter.py +217 -2
  18. runbooks/finops/nat_gateway_optimizer.py +57 -20
  19. runbooks/finops/vpc_cleanup_exporter.py +28 -26
  20. runbooks/finops/vpc_cleanup_optimizer.py +370 -16
  21. runbooks/inventory/__init__.py +10 -1
  22. runbooks/inventory/cloud_foundations_integration.py +409 -0
  23. runbooks/inventory/core/collector.py +1148 -88
  24. runbooks/inventory/discovery.md +389 -0
  25. runbooks/inventory/drift_detection_cli.py +327 -0
  26. runbooks/inventory/inventory_mcp_cli.py +171 -0
  27. runbooks/inventory/inventory_modules.py +4 -7
  28. runbooks/inventory/mcp_inventory_validator.py +2149 -0
  29. runbooks/inventory/mcp_vpc_validator.py +23 -6
  30. runbooks/inventory/organizations_discovery.py +91 -1
  31. runbooks/inventory/rich_inventory_display.py +129 -1
  32. runbooks/inventory/unified_validation_engine.py +1292 -0
  33. runbooks/inventory/verify_ec2_security_groups.py +3 -1
  34. runbooks/inventory/vpc_analyzer.py +825 -7
  35. runbooks/inventory/vpc_flow_analyzer.py +36 -42
  36. runbooks/main.py +654 -35
  37. runbooks/monitoring/performance_monitor.py +11 -7
  38. runbooks/operate/dynamodb_operations.py +6 -5
  39. runbooks/operate/ec2_operations.py +3 -2
  40. runbooks/operate/networking_cost_heatmap.py +4 -3
  41. runbooks/operate/s3_operations.py +13 -12
  42. runbooks/operate/vpc_operations.py +49 -1
  43. runbooks/remediation/base.py +1 -1
  44. runbooks/remediation/commvault_ec2_analysis.py +6 -1
  45. runbooks/remediation/ec2_unattached_ebs_volumes.py +6 -3
  46. runbooks/remediation/rds_snapshot_list.py +5 -3
  47. runbooks/validation/__init__.py +21 -1
  48. runbooks/validation/comprehensive_2way_validator.py +1996 -0
  49. runbooks/validation/mcp_validator.py +904 -94
  50. runbooks/validation/terraform_citations_validator.py +363 -0
  51. runbooks/validation/terraform_drift_detector.py +1098 -0
  52. runbooks/vpc/cleanup_wrapper.py +231 -10
  53. runbooks/vpc/config.py +310 -62
  54. runbooks/vpc/cross_account_session.py +308 -0
  55. runbooks/vpc/heatmap_engine.py +96 -29
  56. runbooks/vpc/manager_interface.py +9 -9
  57. runbooks/vpc/mcp_no_eni_validator.py +1551 -0
  58. runbooks/vpc/networking_wrapper.py +14 -8
  59. runbooks/vpc/runbooks.inventory.organizations_discovery.log +0 -0
  60. runbooks/vpc/runbooks.security.report_generator.log +0 -0
  61. runbooks/vpc/runbooks.security.run_script.log +0 -0
  62. runbooks/vpc/runbooks.security.security_export.log +0 -0
  63. runbooks/vpc/tests/test_cost_engine.py +1 -1
  64. runbooks/vpc/unified_scenarios.py +73 -3
  65. runbooks/vpc/vpc_cleanup_integration.py +512 -78
  66. {runbooks-0.9.9.dist-info → runbooks-1.0.0.dist-info}/METADATA +94 -52
  67. {runbooks-0.9.9.dist-info → runbooks-1.0.0.dist-info}/RECORD +71 -49
  68. {runbooks-0.9.9.dist-info → runbooks-1.0.0.dist-info}/WHEEL +0 -0
  69. {runbooks-0.9.9.dist-info → runbooks-1.0.0.dist-info}/entry_points.txt +0 -0
  70. {runbooks-0.9.9.dist-info → runbooks-1.0.0.dist-info}/licenses/LICENSE +0 -0
  71. {runbooks-0.9.9.dist-info → runbooks-1.0.0.dist-info}/top_level.txt +0 -0
@@ -194,7 +194,7 @@ class VPCCleanupCLI:
194
194
  'analysis_summary': {
195
195
  'total_vpcs': len(candidates),
196
196
  'immediate_cleanup': len([c for c in candidates if c.cleanup_phase == VPCCleanupPhase.IMMEDIATE]),
197
- 'total_annual_savings': sum(c.annual_savings for c in candidates),
197
+ 'total_annual_savings': sum((c.annual_savings or 0.0) for c in candidates),
198
198
  'safety_mode_enabled': self.safety_mode
199
199
  }
200
200
  }
@@ -289,9 +289,9 @@ class VPCCleanupCLI:
289
289
  execution_results['execution_plan'].append(vpc_plan)
290
290
 
291
291
  # Safety warnings
292
- if candidate.blocking_dependencies > 0:
292
+ if (candidate.blocking_dependencies or 0) > 0:
293
293
  execution_results['warnings'].append(
294
- f"VPC {candidate.vpc_id} has {candidate.blocking_dependencies} blocking dependencies"
294
+ f"VPC {candidate.vpc_id} has {candidate.blocking_dependencies or 0} blocking dependencies"
295
295
  )
296
296
 
297
297
  if candidate.is_default:
@@ -417,7 +417,7 @@ class VPCCleanupCLI:
417
417
  safety_results = {
418
418
  'vpc_id': vpc_id,
419
419
  'safety_score': 'SAFE',
420
- 'blocking_dependencies': vpc_candidate.blocking_dependencies,
420
+ 'blocking_dependencies': vpc_candidate.blocking_dependencies or 0,
421
421
  'risk_level': vpc_candidate.risk_level.value,
422
422
  'safety_checks': [],
423
423
  'warnings': [],
@@ -504,7 +504,7 @@ class VPCCleanupCLI:
504
504
  f"Investigation Required: [yellow]{exec_summary.get('investigation_required', 0)}[/yellow]\n"
505
505
  f"Governance Approval Needed: [blue]{exec_summary.get('governance_approval_needed', 0)}[/blue]\n"
506
506
  f"Complex Migration Required: [red]{exec_summary.get('complex_migration_required', 0)}[/red]\n\n"
507
- f"Total Annual Savings: [bold green]${cleanup_plan['metadata']['total_annual_savings']:,.2f}[/bold green]\n"
507
+ f"Total Annual Savings: [bold green]${(cleanup_plan['metadata']['total_annual_savings'] or 0.0):,.2f}[/bold green]\n"
508
508
  f"Business Case Strength: [cyan]{exec_summary.get('business_case_strength', 'Unknown')}[/cyan]"
509
509
  )
510
510
 
@@ -518,7 +518,7 @@ class VPCCleanupCLI:
518
518
  f"[bold blue]🚀 EXECUTION PLAN[/bold blue]\n\n"
519
519
  f"Mode: {mode_text}\n"
520
520
  f"VPCs to Process: [yellow]{len(candidates)}[/yellow]\n"
521
- f"Total Dependencies to Remove: [red]{sum(c.blocking_dependencies for c in candidates)}[/red]\n"
521
+ f"Total Dependencies to Remove: [red]{sum((c.blocking_dependencies or 0) for c in candidates)}[/red]\n"
522
522
  f"High Risk VPCs: [red]{len([c for c in candidates if c.risk_level == VPCCleanupRisk.HIGH])}[/red]\n"
523
523
  f"Default VPCs: [magenta]{len([c for c in candidates if c.is_default])}[/magenta]"
524
524
  )
@@ -655,7 +655,9 @@ def analyze_cleanup_candidates(
655
655
  vpc_ids: Optional[List[str]] = None,
656
656
  all_accounts: bool = False,
657
657
  region: str = "us-east-1",
658
- export_results: bool = True
658
+ export_results: bool = True,
659
+ account_limit: Optional[int] = None,
660
+ region_limit: Optional[int] = None
659
661
  ) -> Dict[str, Any]:
660
662
  """
661
663
  CLI function to analyze VPC cleanup candidates
@@ -666,6 +668,8 @@ def analyze_cleanup_candidates(
666
668
  all_accounts: Analyze across all accessible accounts
667
669
  region: AWS region
668
670
  export_results: Export results to files
671
+ account_limit: Limit number of accounts to process for faster testing
672
+ region_limit: Limit number of regions to scan per account
669
673
 
670
674
  Returns:
671
675
  Dictionary with analysis results
@@ -683,9 +687,92 @@ def analyze_cleanup_candidates(
683
687
  # Handle multi-account analysis
684
688
  account_profiles = None
685
689
  if all_accounts:
686
- # In a real implementation, this would discover all accessible accounts/profiles
687
- # For now, we'll use the single profile
688
- account_profiles = [operational_profile] if operational_profile else None
690
+ # Use Organizations API to discover all accounts
691
+ console.print("[blue]🔍 Discovering organization accounts for multi-account VPC analysis...[/blue]")
692
+
693
+ try:
694
+ # Import Organizations discovery functionality from FinOps module
695
+ from runbooks.finops.aws_client import get_organization_accounts
696
+ from runbooks.common.profile_utils import create_operational_session, create_management_session
697
+ from runbooks.vpc.cross_account_session import convert_accounts_to_sessions
698
+
699
+ # Check for cached Organizations data first (performance optimization)
700
+
701
+ # Use CENTRALISED_OPS_PROFILE if available for operational accounts
702
+ import os
703
+ centralised_ops_profile = os.getenv('CENTRALISED_OPS_PROFILE')
704
+ if centralised_ops_profile:
705
+ console.print(f"[green]✅ Using CENTRALISED_OPS_PROFILE: {centralised_ops_profile}[/green]")
706
+ from .mcp_no_eni_validator import _get_cached_organizations_data, _cache_organizations_data
707
+
708
+ org_accounts = _get_cached_organizations_data()
709
+
710
+ if not org_accounts:
711
+ # Create management session for Organizations discovery (needs Organizations permissions)
712
+ session = create_management_session(profile=operational_profile)
713
+
714
+ # Discover all organization accounts
715
+ org_accounts = get_organization_accounts(session, operational_profile)
716
+
717
+ # Cache the results for future use (prevents duplicate calls)
718
+ if org_accounts:
719
+ _cache_organizations_data(org_accounts)
720
+
721
+ if org_accounts:
722
+ # Apply account limit for performance optimization before session creation
723
+ if account_limit and account_limit < len(org_accounts):
724
+ console.print(f"[yellow]🎯 Performance mode: limiting to first {account_limit} accounts[/yellow]")
725
+ org_accounts = org_accounts[:account_limit]
726
+
727
+ # Convert accounts to cross-account sessions using STS AssumeRole
728
+ account_sessions, account_metadata = convert_accounts_to_sessions(org_accounts, operational_profile)
729
+
730
+ console.print(f"[green]✅ Discovered {len(org_accounts)} organization accounts[/green]")
731
+ console.print(f"[cyan]📋 Created {len(account_sessions)} cross-account sessions for VPC analysis[/cyan]")
732
+
733
+ # Log account discovery for transparency
734
+ active_count = len([acc for acc in org_accounts if acc.get("status") == "ACTIVE"])
735
+ inactive_count = len(org_accounts) - active_count
736
+ console.print(f"[dim]Organization scope: {active_count} active, {inactive_count} inactive accounts[/dim]")
737
+
738
+ # Detect STS AssumeRole failures and switch to multi-profile discovery
739
+ if len(account_sessions) == 0 and len(org_accounts) > 0:
740
+ console.print(f"[red]❌ STS AssumeRole failed for all {len(org_accounts)} accounts - cross-account access denied[/red]")
741
+ console.print("[yellow]💡 Enhancing to multi-profile discovery for comprehensive VPC scanning[/yellow]")
742
+
743
+ # Enhanced multi-profile discovery pattern (KISS & DRY)
744
+ console.print("[blue]🔍 Discovering VPC profiles from available AWS configurations...[/blue]")
745
+ account_profiles = _discover_vpc_profiles_from_available_aws_profiles(operational_profile)
746
+
747
+ if account_profiles and len(account_profiles) > 1:
748
+ console.print(f"[green]✅ Enhanced discovery found {len(account_profiles)} profiles with VPC access[/green]")
749
+ else:
750
+ console.print("[yellow]⚠️ Enhanced discovery fallback to single profile[/yellow]")
751
+ else:
752
+ # Store sessions for VPC discovery instead of profiles
753
+ account_profiles = account_sessions # Pass sessions instead of profile strings
754
+
755
+ else:
756
+ console.print("[yellow]⚠️ No organization accounts found, falling back to single profile[/yellow]")
757
+ account_profiles = [operational_profile] if operational_profile else None
758
+
759
+ except ImportError as e:
760
+ console.print(f"[red]❌ Organizations discovery unavailable: {e}[/red]")
761
+ console.print("[yellow]💡 Falling back to single profile analysis[/yellow]")
762
+ account_profiles = [operational_profile] if operational_profile else None
763
+
764
+ except Exception as e:
765
+ console.print(f"[red]❌ Organizations discovery failed: {e}[/red]")
766
+ console.print("[yellow]💡 Enhancing to multi-profile discovery for comprehensive VPC scanning[/yellow]")
767
+
768
+ # Enhanced multi-profile discovery pattern (KISS & DRY)
769
+ console.print("[blue]🔍 Discovering VPC profiles from available AWS configurations...[/blue]")
770
+ account_profiles = _discover_vpc_profiles_from_available_aws_profiles(operational_profile)
771
+
772
+ if account_profiles and len(account_profiles) > 1:
773
+ console.print(f"[green]✅ Enhanced discovery found {len(account_profiles)} profiles with VPC access[/green]")
774
+ else:
775
+ console.print("[yellow]⚠️ Enhanced discovery fallback to single profile[/yellow]")
689
776
 
690
777
  return cleanup_cli.analyze_vpc_cleanup_candidates(
691
778
  vpc_ids=vpc_ids,
@@ -724,6 +811,140 @@ def validate_cleanup_safety(
724
811
  )
725
812
 
726
813
 
814
+ def _discover_vpc_profiles_from_available_aws_profiles(primary_profile: str) -> List[str]:
815
+ """
816
+ Enhanced multi-profile discovery for comprehensive VPC scanning across all available AWS profiles.
817
+
818
+ KISS & DRY approach: Use boto3's available_profiles to discover VPCs across Landing Zone
819
+ when Organizations API cross-account role assumption fails.
820
+
821
+ Args:
822
+ primary_profile: Primary operational profile to include
823
+
824
+ Returns:
825
+ List of validated AWS profile names for VPC discovery
826
+ """
827
+ import boto3
828
+ from rich.progress import Progress, TaskID
829
+
830
+ console.print("[blue]🔍 Discovering VPC profiles from available AWS configurations...[/blue]")
831
+
832
+ # Get all available AWS profiles
833
+ try:
834
+ session = boto3.Session()
835
+ available_profiles = session.available_profiles
836
+
837
+ if not available_profiles:
838
+ console.print("[yellow]⚠️ No AWS profiles found in configuration[/yellow]")
839
+ return [primary_profile] if primary_profile else []
840
+
841
+ console.print(f"[cyan]📋 Found {len(available_profiles)} AWS profiles in configuration[/cyan]")
842
+
843
+ # Enhanced multi-region discovery for comprehensive Landing Zone coverage
844
+ # Based on user's confirmed NO-ENI VPCs in: us-east-1, us-west-2, ap-southeast-2
845
+ regions_to_check = [
846
+ 'us-east-1', # Primary US region - user confirmed VPCs here
847
+ 'us-west-2', # Secondary US region - user confirmed VPCs here
848
+ 'ap-southeast-2', # APAC region - user confirmed VPCs here
849
+ 'eu-west-1', # Europe primary
850
+ 'ca-central-1', # Canada
851
+ 'ap-northeast-1', # Tokyo (common enterprise region)
852
+ ]
853
+
854
+ # Validate profiles by attempting to create sessions and check VPC access
855
+ validated_profiles = []
856
+ profile_vpc_details = {}
857
+
858
+ with Progress() as progress:
859
+ profile_task = progress.add_task("🔍 Validating profiles for VPC access...", total=len(available_profiles))
860
+
861
+ for profile_name in available_profiles:
862
+ try:
863
+ # Skip obvious non-VPC profiles but be less restrictive
864
+ if 'billing' in profile_name.lower() and 'readonly' in profile_name.lower():
865
+ console.print(f"[dim]⏭️ Skipping {profile_name} (billing-only profile)[/dim]")
866
+ progress.advance(profile_task)
867
+ continue
868
+
869
+ # Create test session
870
+ test_session = boto3.Session(profile_name=profile_name)
871
+ total_vpcs = 0
872
+ regions_with_vpcs = []
873
+
874
+ # Check multiple regions for VPCs (Landing Zone accounts may have VPCs in different regions)
875
+ for region in regions_to_check:
876
+ try:
877
+ ec2_client = test_session.client('ec2', region_name=region)
878
+ vpc_response = ec2_client.describe_vpcs(MaxResults=10) # Check more VPCs per region
879
+ region_vpc_count = len(vpc_response.get('Vpcs', []))
880
+
881
+ if region_vpc_count > 0:
882
+ total_vpcs += region_vpc_count
883
+ regions_with_vpcs.append(f"{region}:{region_vpc_count}")
884
+
885
+ except Exception as region_error:
886
+ # Log region-specific errors but don't fail the whole profile
887
+ if "UnauthorizedOperation" not in str(region_error):
888
+ console.print(f"[dim]⚠️ {profile_name} in {region}: {str(region_error)[:30]}...[/dim]")
889
+ continue
890
+
891
+ # Add profile if it has VPCs in any region OR if it's the primary profile
892
+ if total_vpcs > 0:
893
+ validated_profiles.append(profile_name)
894
+ profile_vpc_details[profile_name] = {
895
+ 'total_vpcs': total_vpcs,
896
+ 'regions': regions_with_vpcs
897
+ }
898
+ console.print(f"[green]✅ {profile_name}: {total_vpcs} VPCs across {len(regions_with_vpcs)} regions[/green]")
899
+ elif profile_name == primary_profile:
900
+ # Always include primary profile even if no VPCs found
901
+ validated_profiles.append(profile_name)
902
+ console.print(f"[yellow]🔑 {profile_name}: Primary profile (included despite no VPCs found)[/yellow]")
903
+ else:
904
+ console.print(f"[dim]⚪ {profile_name}: No VPCs found in {len(regions_to_check)} regions[/dim]")
905
+
906
+ except Exception as e:
907
+ console.print(f"[dim]❌ {profile_name}: Access failed ({str(e)[:50]}...)[/dim]")
908
+
909
+ progress.advance(profile_task)
910
+
911
+ # Ensure primary profile is included if it was validated
912
+ if primary_profile and primary_profile not in validated_profiles:
913
+ try:
914
+ # Test primary profile separately
915
+ test_session = boto3.Session(profile_name=primary_profile)
916
+ ec2_client = test_session.client('ec2', region_name='us-east-1')
917
+ ec2_client.describe_vpcs(MaxResults=1)
918
+ validated_profiles.insert(0, primary_profile) # Add at front
919
+ console.print(f"[green]✅ Primary profile {primary_profile} added[/green]")
920
+ except Exception:
921
+ console.print(f"[yellow]⚠️ Primary profile {primary_profile} validation failed[/yellow]")
922
+
923
+ # Enhanced VPC discovery summary
924
+ total_vpcs_found = sum(details.get('total_vpcs', 0) for details in profile_vpc_details.values())
925
+ console.print(f"[bold green]🎯 VPC Discovery Ready: {len(validated_profiles)} validated profiles[/bold green]")
926
+
927
+ if total_vpcs_found > 0:
928
+ console.print(f"[bold cyan]📊 Total VPCs discovered: {total_vpcs_found} across {len(profile_vpc_details)} accounts[/bold cyan]")
929
+
930
+ # Show detailed breakdown for profiles with VPCs
931
+ for profile, details in profile_vpc_details.items():
932
+ if details['total_vpcs'] > 0:
933
+ regions_str = ', '.join(details['regions'])
934
+ console.print(f"[dim] • {profile}: {regions_str}[/dim]")
935
+ else:
936
+ console.print(f"[yellow]⚠️ No VPCs found across {len(validated_profiles)} profiles - Landing Zone accounts may be empty[/yellow]")
937
+
938
+ console.print(f"[dim]Profiles: {', '.join(validated_profiles[:3])}{'...' if len(validated_profiles) > 3 else ''}[/dim]")
939
+
940
+ return validated_profiles
941
+
942
+ except Exception as e:
943
+ console.print(f"[red]❌ Profile discovery failed: {e}[/red]")
944
+ console.print(f"[yellow]💡 Falling back to primary profile: {primary_profile}[/yellow]")
945
+ return [primary_profile] if primary_profile else []
946
+
947
+
727
948
  def generate_business_report(
728
949
  profile: Optional[str] = None,
729
950
  region: str = "us-east-1",