runbooks 0.9.8__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 (75) hide show
  1. runbooks/__init__.py +1 -1
  2. runbooks/cfat/cloud_foundations_assessment.py +626 -0
  3. runbooks/cloudops/cost_optimizer.py +95 -33
  4. runbooks/common/aws_pricing.py +388 -0
  5. runbooks/common/aws_pricing_api.py +205 -0
  6. runbooks/common/aws_utils.py +2 -2
  7. runbooks/common/comprehensive_cost_explorer_integration.py +979 -0
  8. runbooks/common/cross_account_manager.py +606 -0
  9. runbooks/common/enhanced_exception_handler.py +4 -0
  10. runbooks/common/env_utils.py +96 -0
  11. runbooks/common/mcp_integration.py +49 -2
  12. runbooks/common/organizations_client.py +579 -0
  13. runbooks/common/profile_utils.py +96 -2
  14. runbooks/common/rich_utils.py +3 -0
  15. runbooks/finops/cost_optimizer.py +2 -1
  16. runbooks/finops/elastic_ip_optimizer.py +13 -9
  17. runbooks/finops/embedded_mcp_validator.py +31 -0
  18. runbooks/finops/enhanced_trend_visualization.py +3 -2
  19. runbooks/finops/markdown_exporter.py +441 -0
  20. runbooks/finops/nat_gateway_optimizer.py +57 -20
  21. runbooks/finops/optimizer.py +2 -0
  22. runbooks/finops/single_dashboard.py +2 -2
  23. runbooks/finops/vpc_cleanup_exporter.py +330 -0
  24. runbooks/finops/vpc_cleanup_optimizer.py +895 -40
  25. runbooks/inventory/__init__.py +10 -1
  26. runbooks/inventory/cloud_foundations_integration.py +409 -0
  27. runbooks/inventory/core/collector.py +1148 -88
  28. runbooks/inventory/discovery.md +389 -0
  29. runbooks/inventory/drift_detection_cli.py +327 -0
  30. runbooks/inventory/inventory_mcp_cli.py +171 -0
  31. runbooks/inventory/inventory_modules.py +4 -7
  32. runbooks/inventory/mcp_inventory_validator.py +2149 -0
  33. runbooks/inventory/mcp_vpc_validator.py +23 -6
  34. runbooks/inventory/organizations_discovery.py +91 -1
  35. runbooks/inventory/rich_inventory_display.py +129 -1
  36. runbooks/inventory/unified_validation_engine.py +1292 -0
  37. runbooks/inventory/verify_ec2_security_groups.py +3 -1
  38. runbooks/inventory/vpc_analyzer.py +825 -7
  39. runbooks/inventory/vpc_flow_analyzer.py +36 -42
  40. runbooks/main.py +969 -42
  41. runbooks/monitoring/performance_monitor.py +11 -7
  42. runbooks/operate/dynamodb_operations.py +6 -5
  43. runbooks/operate/ec2_operations.py +3 -2
  44. runbooks/operate/networking_cost_heatmap.py +4 -3
  45. runbooks/operate/s3_operations.py +13 -12
  46. runbooks/operate/vpc_operations.py +50 -2
  47. runbooks/remediation/base.py +1 -1
  48. runbooks/remediation/commvault_ec2_analysis.py +6 -1
  49. runbooks/remediation/ec2_unattached_ebs_volumes.py +6 -3
  50. runbooks/remediation/rds_snapshot_list.py +5 -3
  51. runbooks/validation/__init__.py +21 -1
  52. runbooks/validation/comprehensive_2way_validator.py +1996 -0
  53. runbooks/validation/mcp_validator.py +904 -94
  54. runbooks/validation/terraform_citations_validator.py +363 -0
  55. runbooks/validation/terraform_drift_detector.py +1098 -0
  56. runbooks/vpc/cleanup_wrapper.py +231 -10
  57. runbooks/vpc/config.py +310 -62
  58. runbooks/vpc/cross_account_session.py +308 -0
  59. runbooks/vpc/heatmap_engine.py +96 -29
  60. runbooks/vpc/manager_interface.py +9 -9
  61. runbooks/vpc/mcp_no_eni_validator.py +1551 -0
  62. runbooks/vpc/networking_wrapper.py +14 -8
  63. runbooks/vpc/runbooks.inventory.organizations_discovery.log +0 -0
  64. runbooks/vpc/runbooks.security.report_generator.log +0 -0
  65. runbooks/vpc/runbooks.security.run_script.log +0 -0
  66. runbooks/vpc/runbooks.security.security_export.log +0 -0
  67. runbooks/vpc/tests/test_cost_engine.py +1 -1
  68. runbooks/vpc/unified_scenarios.py +3269 -0
  69. runbooks/vpc/vpc_cleanup_integration.py +516 -82
  70. {runbooks-0.9.8.dist-info → runbooks-1.0.0.dist-info}/METADATA +94 -52
  71. {runbooks-0.9.8.dist-info → runbooks-1.0.0.dist-info}/RECORD +75 -51
  72. {runbooks-0.9.8.dist-info → runbooks-1.0.0.dist-info}/WHEEL +0 -0
  73. {runbooks-0.9.8.dist-info → runbooks-1.0.0.dist-info}/entry_points.txt +0 -0
  74. {runbooks-0.9.8.dist-info → runbooks-1.0.0.dist-info}/licenses/LICENSE +0 -0
  75. {runbooks-0.9.8.dist-info → runbooks-1.0.0.dist-info}/top_level.txt +0 -0
@@ -423,6 +423,447 @@ class MarkdownExporter:
423
423
  highest = max(profiles, key=lambda p: p.get("potential_savings", 0))
424
424
  return highest.get("profile_name", "Unknown")[:20]
425
425
 
426
+ def format_vpc_cleanup_table(self, vpc_candidates: List[Any]) -> str:
427
+ """
428
+ Format VPC cleanup candidates into 15-column markdown table.
429
+
430
+ Args:
431
+ vpc_candidates: List of VPCCandidate objects from vpc.unified_scenarios
432
+
433
+ Returns:
434
+ Markdown formatted table string with VPC cleanup analysis
435
+ """
436
+ if not vpc_candidates:
437
+ return "⚠️ No VPC candidates to format"
438
+
439
+ timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S UTC")
440
+
441
+ # Build table header and separator
442
+ headers = [
443
+ "Account_ID", "VPC_ID", "VPC_Name", "CIDR_Block", "Overlapping",
444
+ "Is_Default", "ENI_Count", "Tags", "Flow_Logs", "TGW/Peering",
445
+ "LBs_Present", "IaC", "Timeline", "Decision", "Owners/Approvals", "Notes"
446
+ ]
447
+
448
+ markdown_lines = [
449
+ "# VPC Cleanup Analysis Report",
450
+ "",
451
+ f"**Generated**: {timestamp}",
452
+ f"**Total VPC Candidates**: {len(vpc_candidates)}",
453
+ f"**Analysis Source**: CloudOps Runbooks VPC Module v0.9.9",
454
+ "",
455
+ "## VPC Cleanup Decision Table",
456
+ "",
457
+ "| " + " | ".join(headers) + " |",
458
+ "| " + " | ".join(["---" for _ in headers]) + " |"
459
+ ]
460
+
461
+ # Process each VPC candidate with enhanced data extraction
462
+ for candidate in vpc_candidates:
463
+ # Extract data with safe attribute access and formatting
464
+ account_id = getattr(candidate, 'account_id', 'Unknown')
465
+ vpc_id = getattr(candidate, 'vpc_id', 'Unknown')
466
+ vpc_name = getattr(candidate, 'vpc_name', '') or 'Unnamed'
467
+ cidr_block = getattr(candidate, 'cidr_block', 'Unknown')
468
+
469
+ # Handle overlapping logic - check CIDR conflicts
470
+ overlapping = self._check_cidr_overlapping(cidr_block, vpc_candidates)
471
+
472
+ # Enhanced is_default handling
473
+ is_default = getattr(candidate, 'is_default', False)
474
+ is_default_display = "⚠️ Yes" if is_default else "No"
475
+
476
+ # Enhanced ENI count
477
+ dependency_analysis = getattr(candidate, 'dependency_analysis', None)
478
+ eni_count = dependency_analysis.eni_count if dependency_analysis else 0
479
+
480
+ # Enhanced tags with owner focus
481
+ tags_dict = getattr(candidate, 'tags', {}) or {}
482
+ tags_display = self._format_tags_for_owners_display(tags_dict)
483
+
484
+ # Flow logs detection
485
+ flow_logs = self._detect_flow_logs(candidate)
486
+
487
+ # TGW/Peering detection
488
+ tgw_peering = self._detect_tgw_peering(candidate)
489
+
490
+ # Load balancers detection
491
+ lbs_present = self._detect_load_balancers(candidate)
492
+
493
+ # IaC detection from tags
494
+ iac_detected = self._detect_iac_from_tags(tags_dict)
495
+
496
+ # Timeline estimation based on VPC state
497
+ timeline = self._estimate_cleanup_timeline(candidate)
498
+
499
+ # Decision based on bucket classification
500
+ decision = self._determine_cleanup_decision(candidate)
501
+
502
+ # Enhanced owners/approvals extraction
503
+ owners_approvals = self._extract_owners_approvals(tags_dict, is_default)
504
+
505
+ # Notes based on VPC characteristics
506
+ notes = self._generate_vpc_notes(candidate)
507
+ overlapping = "Yes" if getattr(candidate, 'overlapping', False) else "No"
508
+
509
+ # Format boolean indicators with emoji
510
+ is_default = "⚠️ Yes" if getattr(candidate, 'is_default', False) else "✅ No"
511
+ flow_logs = "✅ Yes" if getattr(candidate, 'flow_logs_enabled', False) else "❌ No"
512
+ tgw_peering = "✅ Yes" if getattr(candidate, 'tgw_peering_attached', False) else "❌ No"
513
+ load_balancers = "✅ Yes" if getattr(candidate, 'load_balancers_present', False) else "❌ No"
514
+ iac_managed = "✅ Yes" if getattr(candidate, 'iac_managed', False) else "❌ No"
515
+
516
+ # ENI Count handling
517
+ eni_count = getattr(candidate, 'eni_count', 0)
518
+
519
+ # Tags formatting - prioritize important tags with enhanced display
520
+ tags = getattr(candidate, 'tags', {}) or {}
521
+ relevant_tags = []
522
+ if tags:
523
+ # Priority order for business-relevant tags
524
+ priority_keys = ['Name', 'Environment', 'Project', 'Owner', 'BusinessOwner', 'Team', 'CostCenter', 'Application']
525
+ for key in priority_keys:
526
+ if key in tags and tags[key] and len(relevant_tags) < 3: # Increased limit for better visibility
527
+ relevant_tags.append(f"{key}:{tags[key]}")
528
+
529
+ # Add other important tags if space available
530
+ for key, value in tags.items():
531
+ if key not in priority_keys and value and len(relevant_tags) < 3:
532
+ relevant_tags.append(f"{key}:{value}")
533
+
534
+ # Enhanced display logic for tags
535
+ if relevant_tags:
536
+ tags_display = "; ".join(relevant_tags)
537
+ if len(tags_display) > 35: # Slightly increased width for better readability
538
+ tags_display = tags_display[:32] + "..."
539
+ elif tags:
540
+ # If tags exist but none were priority, show count
541
+ tags_display = f"({len(tags)} tags)"
542
+ else:
543
+ # No tags at all
544
+ tags_display = "No tags"
545
+
546
+ # Timeline and Decision
547
+ timeline = getattr(candidate, 'cleanup_timeline', '') or getattr(candidate, 'implementation_timeline', 'Unknown')
548
+
549
+ # Decision handling - check for different decision attribute names
550
+ decision_attr = getattr(candidate, 'decision', None)
551
+ if decision_attr:
552
+ if hasattr(decision_attr, 'value'):
553
+ decision = decision_attr.value
554
+ else:
555
+ decision = str(decision_attr)
556
+ else:
557
+ # Fallback decision logic based on risk/dependencies
558
+ decision = getattr(candidate, 'cleanup_bucket', 'Unknown')
559
+
560
+ # Owners/Approvals - Enhanced extraction from tags if not populated
561
+ owners = getattr(candidate, 'owners_approvals', []) or getattr(candidate, 'stakeholders', [])
562
+
563
+ # If no owners found via attributes, try to extract from tags directly
564
+ if not owners and tags:
565
+ owner_keys = ['Owner', 'BusinessOwner', 'TechnicalOwner', 'Team', 'Contact', 'CreatedBy', 'ManagedBy']
566
+ for key in owner_keys:
567
+ if key in tags and tags[key]:
568
+ if 'business' in key.lower() or 'manager' in tags[key].lower():
569
+ owners.append(f"{tags[key]} (Business)")
570
+ elif 'technical' in key.lower() or any(tech in tags[key].lower() for tech in ['ops', 'devops', 'engineering']):
571
+ owners.append(f"{tags[key]} (Technical)")
572
+ else:
573
+ owners.append(tags[key])
574
+ break # Take first found owner to avoid clutter
575
+
576
+ if owners:
577
+ owners_display = "; ".join(owners)
578
+ if len(owners_display) > 30: # Increased width for better display
579
+ owners_display = owners_display[:27] + "..."
580
+ else:
581
+ # Enhanced "unknown" display based on VPC characteristics
582
+ if getattr(candidate, 'is_default', False):
583
+ owners_display = "System Default"
584
+ elif getattr(candidate, 'iac_detected', False):
585
+ owners_display = "IaC Managed"
586
+ else:
587
+ owners_display = "No owner tags"
588
+
589
+ # Notes - combination of risk assessment and business impact
590
+ notes_parts = []
591
+ risk_level = getattr(candidate, 'risk_level', None)
592
+ if risk_level:
593
+ risk_val = risk_level.value if hasattr(risk_level, 'value') else str(risk_level)
594
+ notes_parts.append(f"Risk:{risk_val}")
595
+
596
+ business_impact = getattr(candidate, 'business_impact', '')
597
+ if business_impact:
598
+ notes_parts.append(business_impact[:15]) # Truncate
599
+
600
+ notes = "; ".join(notes_parts) if notes_parts else getattr(candidate, 'notes', 'No notes')
601
+ if len(notes) > 30: # Truncate for table formatting
602
+ notes = notes[:27] + "..."
603
+
604
+ # Create table row - escape pipes for markdown compatibility
605
+ row_data = [
606
+ account_id, vpc_id, vpc_name, cidr_block, overlapping,
607
+ is_default, str(eni_count), tags_display, flow_logs, tgw_peering,
608
+ load_balancers, iac_managed, timeline, decision, owners_display, notes
609
+ ]
610
+
611
+ # Escape pipes and format row
612
+ escaped_data = [str(cell).replace("|", "\\|") for cell in row_data]
613
+ markdown_lines.append("| " + " | ".join(escaped_data) + " |")
614
+
615
+ # Add summary statistics
616
+ total_vpcs = len(vpc_candidates)
617
+ default_vpcs = sum(1 for c in vpc_candidates if getattr(c, 'is_default', False))
618
+ flow_logs_enabled = sum(1 for c in vpc_candidates if getattr(c, 'flow_logs_enabled', False))
619
+ iac_managed_count = sum(1 for c in vpc_candidates if getattr(c, 'iac_managed', False))
620
+ zero_eni_vpcs = sum(1 for c in vpc_candidates if getattr(c, 'eni_count', 1) == 0)
621
+
622
+ markdown_lines.extend([
623
+ "",
624
+ "## Analysis Summary",
625
+ "",
626
+ f"- **Total VPCs Analyzed**: {total_vpcs}",
627
+ f"- **Default VPCs**: {default_vpcs} ({(default_vpcs/total_vpcs*100):.1f}%)",
628
+ f"- **Flow Logs Enabled**: {flow_logs_enabled} ({(flow_logs_enabled/total_vpcs*100):.1f}%)",
629
+ f"- **IaC Managed**: {iac_managed_count} ({(iac_managed_count/total_vpcs*100):.1f}%)",
630
+ f"- **Zero ENI Attachments**: {zero_eni_vpcs} ({(zero_eni_vpcs/total_vpcs*100):.1f}%)",
631
+ "",
632
+ "## Cleanup Recommendations",
633
+ "",
634
+ "1. **Priority 1**: VPCs with zero ENI attachments and no dependencies",
635
+ "2. **Priority 2**: Default VPCs with no active resources",
636
+ "3. **Priority 3**: Non-IaC managed VPCs requiring manual cleanup",
637
+ "4. **Review Required**: VPCs with unclear ownership or business impact",
638
+ "",
639
+ "---",
640
+ f"*Generated by CloudOps Runbooks VPC Module v0.9.9 at {timestamp}*"
641
+ ])
642
+
643
+ return "\n".join(markdown_lines)
644
+
645
+ def export_vpc_analysis_to_file(self, vpc_candidates: List[Any], filename: str = None, output_dir: str = "./exports") -> str:
646
+ """
647
+ Export VPC analysis to markdown file with intelligent naming.
648
+
649
+ Args:
650
+ vpc_candidates: List of VPC candidates from analysis
651
+ filename: Base filename (optional, auto-generated if not provided)
652
+ output_dir: Output directory path
653
+
654
+ Returns:
655
+ Path to exported file
656
+ """
657
+ if not filename:
658
+ timestamp = datetime.now().strftime("%Y-%m-%d")
659
+ filename = f"vpc-cleanup-analysis-{timestamp}.md"
660
+
661
+ # Ensure .md extension
662
+ if not filename.endswith('.md'):
663
+ filename = f"{filename}.md"
664
+
665
+ # Create output directory
666
+ output_path = Path(output_dir)
667
+ output_path.mkdir(parents=True, exist_ok=True)
668
+
669
+ # Generate markdown content
670
+ markdown_content = self.format_vpc_cleanup_table(vpc_candidates)
671
+
672
+ # Write to file
673
+ filepath = output_path / filename
674
+
675
+ print_info(f"📝 Exporting VPC analysis to: {filename}")
676
+
677
+ try:
678
+ with open(filepath, "w", encoding="utf-8") as f:
679
+ f.write(markdown_content)
680
+
681
+ print_success(f"✅ VPC analysis exported: {filepath}")
682
+ print_info(f"🔗 Ready for executive review or documentation systems")
683
+ return str(filepath)
684
+
685
+ except Exception as e:
686
+ print_warning(f"❌ Failed to export VPC analysis: {e}")
687
+ return ""
688
+
689
+ def _check_cidr_overlapping(self, cidr_block: str, vpc_candidates: List[Any]) -> str:
690
+ """Check for CIDR block overlapping across VPCs."""
691
+ if not cidr_block or not vpc_candidates:
692
+ return "No"
693
+
694
+ # Simple overlapping check - in enterprise scenario, this would use more sophisticated logic
695
+ current_cidr = cidr_block
696
+ for candidate in vpc_candidates:
697
+ other_cidr = getattr(candidate, 'cidr_block', None)
698
+ if other_cidr and other_cidr != current_cidr and current_cidr.startswith(other_cidr.split('/')[0].rsplit('.', 1)[0]):
699
+ return "Yes"
700
+
701
+ return "No"
702
+
703
+ def _detect_flow_logs(self, candidate: Any) -> str:
704
+ """Detect if VPC has flow logs enabled."""
705
+ return "Yes" if getattr(candidate, 'flow_logs_enabled', False) else "No"
706
+
707
+ def _detect_tgw_peering(self, candidate: Any) -> str:
708
+ """Analyze Transit Gateway and VPC peering connections."""
709
+ # Check for TGW attachments and peering connections
710
+ tgw_attachments = getattr(candidate, 'tgw_attachments', []) or []
711
+ peering_connections = getattr(candidate, 'peering_connections', []) or []
712
+
713
+ if tgw_attachments or peering_connections:
714
+ connection_count = len(tgw_attachments) + len(peering_connections)
715
+ return f"Yes ({connection_count})"
716
+ return "No"
717
+
718
+ def _detect_load_balancers(self, candidate: Any) -> str:
719
+ """Detect load balancers in the VPC."""
720
+ load_balancers = getattr(candidate, 'load_balancers', []) or []
721
+ return "Yes" if load_balancers else "No"
722
+
723
+ def _detect_iac_from_tags(self, tags_dict: dict) -> str:
724
+ """Detect Infrastructure as Code management from tags."""
725
+ iac_keys = ['aws:cloudformation:stack-name', 'terraform:module', 'cdktf:stack', 'pulumi:project']
726
+ for key in iac_keys:
727
+ if key in tags_dict and tags_dict[key]:
728
+ return "Yes"
729
+ return "No"
730
+
731
+ def _estimate_cleanup_timeline(self, candidate: Any) -> str:
732
+ """Estimate cleanup timeline based on complexity."""
733
+ # Simple heuristic based on dependencies
734
+ if hasattr(candidate, 'dependency_analysis') and candidate.dependency_analysis:
735
+ eni_count = getattr(candidate.dependency_analysis, 'eni_count', 0)
736
+ else:
737
+ eni_count = 0
738
+
739
+ if eni_count == 0:
740
+ return "1-2 days"
741
+ elif eni_count < 5:
742
+ return "3-5 days"
743
+ else:
744
+ return "1-2 weeks"
745
+
746
+ def _format_cleanup_decision(self, candidate: Any) -> str:
747
+ """Format cleanup decision recommendation."""
748
+ recommendation = getattr(candidate, 'cleanup_recommendation', 'unknown')
749
+ if recommendation == 'delete':
750
+ return "Delete"
751
+ elif recommendation == 'keep':
752
+ return "Keep"
753
+ elif recommendation == 'review':
754
+ return "Review"
755
+ else:
756
+ return "TBD"
757
+
758
+ def _format_tags_for_owners_display(self, tags_dict: dict) -> str:
759
+ """Format tags for display with priority on ownership information."""
760
+ if not tags_dict:
761
+ return "No tags"
762
+
763
+ # Priority keys focusing on ownership and approvals
764
+ priority_keys = ['Name', 'Owner', 'BusinessOwner', 'TechnicalOwner', 'Team', 'Contact']
765
+ relevant_tags = []
766
+
767
+ for key in priority_keys:
768
+ if key in tags_dict and tags_dict[key]:
769
+ relevant_tags.append(f"{key}:{tags_dict[key]}")
770
+ if len(relevant_tags) >= 3: # Limit for table readability
771
+ break
772
+
773
+ return "; ".join(relevant_tags) if relevant_tags else f"({len(tags_dict)} tags)"
774
+
775
+ def _determine_cleanup_decision(self, candidate: Any) -> str:
776
+ """Determine cleanup decision based on VPC analysis."""
777
+ # Check the cleanup bucket from three-bucket strategy
778
+ cleanup_bucket = getattr(candidate, 'cleanup_bucket', 'unknown')
779
+
780
+ if cleanup_bucket == 'bucket_1':
781
+ return "Delete"
782
+ elif cleanup_bucket == 'bucket_2':
783
+ return "Review"
784
+ elif cleanup_bucket == 'bucket_3':
785
+ return "Keep"
786
+ else:
787
+ # Fallback logic based on other attributes
788
+ is_default = getattr(candidate, 'is_default', False)
789
+ has_eni = getattr(candidate, 'eni_count', 0) > 0
790
+
791
+ if is_default and not has_eni:
792
+ return "Delete"
793
+ elif has_eni:
794
+ return "Review"
795
+ else:
796
+ return "TBD"
797
+
798
+ def _extract_owners_approvals(self, tags_dict: dict, is_default: bool) -> str:
799
+ """Extract owners and approval information from tags and VPC status."""
800
+ # Extract from tags with enhanced owner detection
801
+ owner_keys = ['Owner', 'BusinessOwner', 'TechnicalOwner', 'Team', 'Contact', 'CreatedBy', 'ManagedBy']
802
+
803
+ extracted_owners = []
804
+ for key in owner_keys:
805
+ if key in tags_dict and tags_dict[key]:
806
+ value = tags_dict[key]
807
+ if 'business' in key.lower():
808
+ extracted_owners.append(f"{value} (Business)")
809
+ elif 'technical' in key.lower():
810
+ extracted_owners.append(f"{value} (Technical)")
811
+ elif 'team' in key.lower():
812
+ extracted_owners.append(f"{value} (Team)")
813
+ else:
814
+ extracted_owners.append(f"{value} ({key})")
815
+
816
+ if len(extracted_owners) >= 2: # Limit for table readability
817
+ break
818
+
819
+ if extracted_owners:
820
+ return "; ".join(extracted_owners)
821
+
822
+ # Fallback based on VPC type
823
+ if is_default:
824
+ return "System Default VPC"
825
+ else:
826
+ # Check for IaC tags
827
+ iac_keys = ['aws:cloudformation:stack-name', 'terraform:module', 'cdktf:stack', 'pulumi:project']
828
+ for key in iac_keys:
829
+ if key in tags_dict and tags_dict[key]:
830
+ return "IaC Managed"
831
+ return "No owner tags found"
832
+
833
+ def _generate_vpc_notes(self, candidate: Any) -> str:
834
+ """Generate comprehensive notes for VPC candidate."""
835
+ notes = []
836
+
837
+ # Add bucket classification note
838
+ cleanup_bucket = getattr(candidate, 'cleanup_bucket', 'unknown')
839
+ if cleanup_bucket == 'bucket_1':
840
+ notes.append("Internal data plane - safe for cleanup")
841
+ elif cleanup_bucket == 'bucket_2':
842
+ notes.append("External interconnects - requires analysis")
843
+ elif cleanup_bucket == 'bucket_3':
844
+ notes.append("Control plane - manual review required")
845
+
846
+ # Add ENI count if significant
847
+ if hasattr(candidate, 'dependency_analysis') and candidate.dependency_analysis:
848
+ eni_count = getattr(candidate.dependency_analysis, 'eni_count', 0)
849
+ if eni_count > 0:
850
+ notes.append(f"{eni_count} ENI attachments")
851
+
852
+ # Add default VPC note
853
+ if getattr(candidate, 'is_default', False):
854
+ notes.append("Default VPC (CIS compliance issue)")
855
+
856
+ # Add IaC detection
857
+ if getattr(candidate, 'iac_detected', False):
858
+ notes.append("IaC managed")
859
+
860
+ # Add security concerns
861
+ risk_level = getattr(candidate, 'risk_level', 'unknown')
862
+ if risk_level == 'high':
863
+ notes.append("High security risk")
864
+
865
+ return "; ".join(notes) if notes else "Standard VPC cleanup candidate"
866
+
426
867
 
427
868
  def export_finops_to_markdown(
428
869
  profile_data: Union[Dict[str, Any], List[Dict[str, Any]]],
@@ -49,6 +49,7 @@ from ..common.rich_utils import (
49
49
  console, print_header, print_success, print_error, print_warning, print_info,
50
50
  create_table, create_progress_bar, format_cost, create_panel, STATUS_INDICATORS
51
51
  )
52
+ from ..common.aws_pricing import get_service_monthly_cost, calculate_annual_cost
52
53
  from .embedded_mcp_validator import EmbeddedMCPValidator
53
54
  from ..common.profile_utils import get_profile_for_operation
54
55
 
@@ -139,14 +140,24 @@ class NATGatewayOptimizer:
139
140
  profile_name=get_profile_for_operation("operational", profile_name)
140
141
  )
141
142
 
142
- # NAT Gateway pricing (per hour, as of 2024)
143
- self.nat_gateway_hourly_cost = 0.045 # $0.045/hour
144
- self.nat_gateway_data_processing_cost = 0.045 # $0.045/GB
143
+ # NAT Gateway pricing - using dynamic pricing engine
144
+ # Base monthly cost calculation (will be applied per region)
145
+ self._base_monthly_cost_us_east_1 = get_service_monthly_cost("nat_gateway", "us-east-1")
146
+ self.nat_gateway_data_processing_cost = 0.045 # $0.045/GB (data transfer pricing)
145
147
 
146
148
  # Enterprise thresholds for optimization recommendations
147
149
  self.low_usage_threshold_connections = 10 # Active connections per day
148
150
  self.low_usage_threshold_bytes = 1_000_000 # 1MB per day
149
151
  self.analysis_period_days = 7 # CloudWatch analysis period
152
+
153
+ def _get_regional_monthly_cost(self, region: str) -> float:
154
+ """Get dynamic monthly NAT Gateway cost for specified region."""
155
+ try:
156
+ return get_service_monthly_cost("nat_gateway", region)
157
+ except Exception:
158
+ # Fallback to regional cost calculation
159
+ from ..common.aws_pricing import calculate_regional_cost
160
+ return calculate_regional_cost(self._base_monthly_cost_us_east_1, region)
150
161
 
151
162
  async def analyze_nat_gateways(self, dry_run: bool = True) -> NATGatewayOptimizerResults:
152
163
  """
@@ -413,9 +424,9 @@ class NATGatewayOptimizer:
413
424
  metrics = usage_metrics.get(nat_gateway.nat_gateway_id)
414
425
  route_tables = dependencies.get(nat_gateway.nat_gateway_id, [])
415
426
 
416
- # Calculate current costs
417
- monthly_cost = self.nat_gateway_hourly_cost * 24 * 30 # Base hourly cost
418
- annual_cost = monthly_cost * 12
427
+ # Calculate current costs using dynamic pricing
428
+ monthly_cost = self._get_regional_monthly_cost(nat_gateway.region)
429
+ annual_cost = calculate_annual_cost(monthly_cost)
419
430
 
420
431
  # Determine optimization recommendation
421
432
  recommendation = "retain" # Default: keep the NAT Gateway
@@ -724,9 +735,9 @@ class TransitGatewayCostAnalysis(BaseModel):
724
735
  """Transit Gateway cost analysis results"""
725
736
  transit_gateway_id: str
726
737
  region: str
727
- monthly_base_cost: float = 36.50 # $36.50/month base cost
738
+ monthly_base_cost: float = 0.0 # Will be calculated dynamically based on region
728
739
  attachment_count: int = 0
729
- attachment_hourly_cost: float = 0.05 # $0.05/hour per attachment
740
+ attachment_hourly_cost: float = 0.05 # $0.05/hour per attachment (attachment pricing)
730
741
  data_processing_cost: float = 0.0
731
742
  total_monthly_cost: float = 0.0
732
743
  annual_cost: float = 0.0
@@ -737,7 +748,7 @@ class NetworkDataTransferCostAnalysis(BaseModel):
737
748
  """Network data transfer cost analysis"""
738
749
  region_pair: str # e.g., "us-east-1 -> us-west-2"
739
750
  monthly_gb_transferred: float = 0.0
740
- cost_per_gb: float = 0.02 # Varies by region pair
751
+ cost_per_gb: float = 0.0 # Will be calculated dynamically based on region pair
741
752
  monthly_transfer_cost: float = 0.0
742
753
  annual_transfer_cost: float = 0.0
743
754
  optimization_recommendations: List[str] = Field(default_factory=list)
@@ -763,17 +774,43 @@ class EnhancedVPCCostOptimizer:
763
774
  self.profile = profile
764
775
  self.nat_optimizer = NATGatewayOptimizer(profile=profile)
765
776
 
766
- # Cost model from vpc cost_engine.py
767
- self.cost_model = {
768
- "nat_gateway_hourly": 0.045,
769
- "nat_gateway_data_processing": 0.045, # per GB
770
- "transit_gateway_monthly": 36.50,
771
- "transit_gateway_attachment_hourly": 0.05,
772
- "vpc_endpoint_interface_hourly": 0.01,
773
- "data_transfer_regional": 0.01, # per GB within region
774
- "data_transfer_cross_region": 0.02, # per GB cross-region
775
- "data_transfer_internet": 0.09 # per GB to internet
776
- }
777
+ # Dynamic cost model using AWS pricing engine
778
+ self.cost_model = self._initialize_dynamic_cost_model()
779
+
780
+ def _initialize_dynamic_cost_model(self) -> Dict[str, float]:
781
+ """Initialize dynamic cost model using AWS pricing engine."""
782
+ try:
783
+ # Get base pricing for us-east-1, then apply regional multipliers as needed
784
+ base_region = "us-east-1"
785
+
786
+ return {
787
+ "nat_gateway_monthly": get_service_monthly_cost("nat_gateway", base_region),
788
+ "nat_gateway_data_processing": get_service_monthly_cost("data_transfer", base_region),
789
+ "transit_gateway_monthly": get_service_monthly_cost("transit_gateway", base_region),
790
+ "vpc_endpoint_monthly": get_service_monthly_cost("vpc_endpoint", base_region),
791
+ "data_transfer_regional": get_service_monthly_cost("data_transfer", base_region),
792
+ "data_transfer_internet": get_service_monthly_cost("data_transfer", base_region) * 4.5, # Internet is ~4.5x higher
793
+ }
794
+ except Exception as e:
795
+ print_warning(f"Dynamic pricing initialization failed: {e}")
796
+ # Fallback to regional cost calculation
797
+ from ..common.aws_pricing import calculate_regional_cost
798
+ base_costs = {
799
+ "nat_gateway_hourly": 0.045,
800
+ "nat_gateway_data_processing": 0.045, # per GB
801
+ "transit_gateway_monthly": 36.50,
802
+ "transit_gateway_attachment_hourly": 0.05,
803
+ "vpc_endpoint_interface_hourly": 0.01,
804
+ "data_transfer_regional": 0.01, # per GB within region
805
+ "data_transfer_cross_region": 0.02, # per GB cross-region
806
+ "data_transfer_internet": 0.09 # per GB to internet
807
+ }
808
+
809
+ # Apply regional multipliers to fallback costs
810
+ return {
811
+ key: calculate_regional_cost(value, "us-east-1")
812
+ for key, value in base_costs.items()
813
+ }
777
814
 
778
815
  async def analyze_comprehensive_vpc_costs(self, profile: Optional[str] = None,
779
816
  regions: Optional[List[str]] = None) -> Dict[str, Any]:
@@ -11,6 +11,8 @@ from typing import Any, Dict, List, Optional
11
11
 
12
12
  import boto3
13
13
 
14
+ from ..common.rich_utils import console
15
+
14
16
 
15
17
  @dataclass
16
18
  class CostSavingsOpportunity:
@@ -638,7 +638,7 @@ class SingleAccountDashboard:
638
638
  style="dim",
639
639
  )
640
640
 
641
- console.print(table)
641
+ rich_console.print(table)
642
642
 
643
643
  # Summary panel (using filtered services for consistent analysis)
644
644
  total_current = sum(filtered_current_services.values())
@@ -681,7 +681,7 @@ class SingleAccountDashboard:
681
681
  • Services Analyzed: {len(all_services)}{period_info}
682
682
  """
683
683
 
684
- console.print(Panel(summary_text.strip(), title="📊 Analysis Summary", style="info"))
684
+ rich_console.print(Panel(summary_text.strip(), title="📊 Analysis Summary", style="info"))
685
685
 
686
686
  def _export_service_analysis(
687
687
  self, args: argparse.Namespace, cost_data: Dict[str, Any], service_costs: List[str], account_id: str