runbooks 0.9.9__py3-none-any.whl → 1.0.1__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 (111) hide show
  1. runbooks/__init__.py +1 -1
  2. runbooks/cfat/WEIGHT_CONFIG_README.md +368 -0
  3. runbooks/cfat/app.ts +27 -19
  4. runbooks/cfat/assessment/runner.py +6 -5
  5. runbooks/cfat/cloud_foundations_assessment.py +626 -0
  6. runbooks/cfat/tests/test_weight_configuration.ts +449 -0
  7. runbooks/cfat/weight_config.ts +574 -0
  8. runbooks/cloudops/cost_optimizer.py +95 -33
  9. runbooks/common/__init__.py +26 -9
  10. runbooks/common/aws_pricing.py +1353 -0
  11. runbooks/common/aws_pricing_api.py +205 -0
  12. runbooks/common/aws_utils.py +2 -2
  13. runbooks/common/comprehensive_cost_explorer_integration.py +979 -0
  14. runbooks/common/cross_account_manager.py +606 -0
  15. runbooks/common/date_utils.py +115 -0
  16. runbooks/common/enhanced_exception_handler.py +14 -7
  17. runbooks/common/env_utils.py +96 -0
  18. runbooks/common/mcp_cost_explorer_integration.py +5 -4
  19. runbooks/common/mcp_integration.py +49 -2
  20. runbooks/common/organizations_client.py +579 -0
  21. runbooks/common/profile_utils.py +127 -72
  22. runbooks/common/rich_utils.py +3 -3
  23. runbooks/finops/cost_optimizer.py +2 -1
  24. runbooks/finops/dashboard_runner.py +47 -28
  25. runbooks/finops/ebs_optimizer.py +56 -9
  26. runbooks/finops/elastic_ip_optimizer.py +13 -9
  27. runbooks/finops/embedded_mcp_validator.py +31 -0
  28. runbooks/finops/enhanced_trend_visualization.py +10 -4
  29. runbooks/finops/finops_dashboard.py +6 -5
  30. runbooks/finops/iam_guidance.py +6 -1
  31. runbooks/finops/markdown_exporter.py +217 -2
  32. runbooks/finops/nat_gateway_optimizer.py +76 -20
  33. runbooks/finops/tests/test_integration.py +3 -1
  34. runbooks/finops/vpc_cleanup_exporter.py +28 -26
  35. runbooks/finops/vpc_cleanup_optimizer.py +363 -16
  36. runbooks/inventory/__init__.py +10 -1
  37. runbooks/inventory/cloud_foundations_integration.py +409 -0
  38. runbooks/inventory/core/collector.py +1177 -94
  39. runbooks/inventory/discovery.md +339 -0
  40. runbooks/inventory/drift_detection_cli.py +327 -0
  41. runbooks/inventory/inventory_mcp_cli.py +171 -0
  42. runbooks/inventory/inventory_modules.py +6 -9
  43. runbooks/inventory/list_ec2_instances.py +3 -3
  44. runbooks/inventory/mcp_inventory_validator.py +2149 -0
  45. runbooks/inventory/mcp_vpc_validator.py +23 -6
  46. runbooks/inventory/organizations_discovery.py +104 -9
  47. runbooks/inventory/rich_inventory_display.py +129 -1
  48. runbooks/inventory/unified_validation_engine.py +1279 -0
  49. runbooks/inventory/verify_ec2_security_groups.py +3 -1
  50. runbooks/inventory/vpc_analyzer.py +825 -7
  51. runbooks/inventory/vpc_flow_analyzer.py +36 -42
  52. runbooks/main.py +708 -47
  53. runbooks/monitoring/performance_monitor.py +11 -7
  54. runbooks/operate/base.py +9 -6
  55. runbooks/operate/deployment_framework.py +5 -4
  56. runbooks/operate/deployment_validator.py +6 -5
  57. runbooks/operate/dynamodb_operations.py +6 -5
  58. runbooks/operate/ec2_operations.py +3 -2
  59. runbooks/operate/mcp_integration.py +6 -5
  60. runbooks/operate/networking_cost_heatmap.py +21 -16
  61. runbooks/operate/s3_operations.py +13 -12
  62. runbooks/operate/vpc_operations.py +100 -12
  63. runbooks/remediation/base.py +4 -2
  64. runbooks/remediation/commons.py +5 -5
  65. runbooks/remediation/commvault_ec2_analysis.py +68 -15
  66. runbooks/remediation/config/accounts_example.json +31 -0
  67. runbooks/remediation/ec2_unattached_ebs_volumes.py +6 -3
  68. runbooks/remediation/multi_account.py +120 -7
  69. runbooks/remediation/rds_snapshot_list.py +5 -3
  70. runbooks/remediation/remediation_cli.py +710 -0
  71. runbooks/remediation/universal_account_discovery.py +377 -0
  72. runbooks/security/compliance_automation_engine.py +99 -20
  73. runbooks/security/config/__init__.py +24 -0
  74. runbooks/security/config/compliance_config.py +255 -0
  75. runbooks/security/config/compliance_weights_example.json +22 -0
  76. runbooks/security/config_template_generator.py +500 -0
  77. runbooks/security/security_cli.py +377 -0
  78. runbooks/validation/__init__.py +21 -1
  79. runbooks/validation/cli.py +8 -7
  80. runbooks/validation/comprehensive_2way_validator.py +2007 -0
  81. runbooks/validation/mcp_validator.py +965 -101
  82. runbooks/validation/terraform_citations_validator.py +363 -0
  83. runbooks/validation/terraform_drift_detector.py +1098 -0
  84. runbooks/vpc/cleanup_wrapper.py +231 -10
  85. runbooks/vpc/config.py +346 -73
  86. runbooks/vpc/cross_account_session.py +312 -0
  87. runbooks/vpc/heatmap_engine.py +115 -41
  88. runbooks/vpc/manager_interface.py +9 -9
  89. runbooks/vpc/mcp_no_eni_validator.py +1630 -0
  90. runbooks/vpc/networking_wrapper.py +14 -8
  91. runbooks/vpc/runbooks_adapter.py +33 -12
  92. runbooks/vpc/tests/conftest.py +4 -2
  93. runbooks/vpc/tests/test_cost_engine.py +4 -2
  94. runbooks/vpc/unified_scenarios.py +73 -3
  95. runbooks/vpc/vpc_cleanup_integration.py +512 -78
  96. {runbooks-0.9.9.dist-info → runbooks-1.0.1.dist-info}/METADATA +94 -52
  97. {runbooks-0.9.9.dist-info → runbooks-1.0.1.dist-info}/RECORD +101 -81
  98. runbooks/finops/runbooks.inventory.organizations_discovery.log +0 -0
  99. runbooks/finops/runbooks.security.report_generator.log +0 -0
  100. runbooks/finops/runbooks.security.run_script.log +0 -0
  101. runbooks/finops/runbooks.security.security_export.log +0 -0
  102. runbooks/finops/tests/results_test_finops_dashboard.xml +0 -1
  103. runbooks/inventory/artifacts/scale-optimize-status.txt +0 -12
  104. runbooks/inventory/runbooks.inventory.organizations_discovery.log +0 -0
  105. runbooks/inventory/runbooks.security.report_generator.log +0 -0
  106. runbooks/inventory/runbooks.security.run_script.log +0 -0
  107. runbooks/inventory/runbooks.security.security_export.log +0 -0
  108. {runbooks-0.9.9.dist-info → runbooks-1.0.1.dist-info}/WHEEL +0 -0
  109. {runbooks-0.9.9.dist-info → runbooks-1.0.1.dist-info}/entry_points.txt +0 -0
  110. {runbooks-0.9.9.dist-info → runbooks-1.0.1.dist-info}/licenses/LICENSE +0 -0
  111. {runbooks-0.9.9.dist-info → runbooks-1.0.1.dist-info}/top_level.txt +0 -0
@@ -22,7 +22,8 @@ AWS_AVAILABLE = True
22
22
 
23
23
  def get_aws_profiles() -> List[str]:
24
24
  """Stub implementation - use dashboard_runner.py instead."""
25
- return ["default", "ams-admin-Billing-ReadOnlyAccess-909135376185"]
25
+ import os
26
+ return ["default", os.getenv("BILLING_PROFILE", "default-billing-profile")]
26
27
 
27
28
 
28
29
  def get_account_id(profile: str = "default") -> str:
@@ -46,10 +47,10 @@ class FinOpsConfig:
46
47
  include_budget_data: bool = True
47
48
  include_resource_analysis: bool = True
48
49
 
49
- # Legacy compatibility properties with environment variable support
50
- billing_profile: str = "ams-admin-Billing-ReadOnlyAccess-909135376185"
51
- management_profile: str = "ams-admin-ReadOnlyAccess-909135376185"
52
- operational_profile: str = "ams-centralised-ops-ReadOnlyAccess-335083429030"
50
+ # Legacy compatibility properties with universal environment support
51
+ billing_profile: str = field(default_factory=lambda: os.getenv("BILLING_PROFILE", "default-billing-profile"))
52
+ management_profile: str = field(default_factory=lambda: os.getenv("MANAGEMENT_PROFILE", "default-management-profile"))
53
+ operational_profile: str = field(default_factory=lambda: os.getenv("CENTRALISED_OPS_PROFILE", "default-ops-profile"))
53
54
 
54
55
  # Additional expected attributes from tests
55
56
  time_range_days: int = 30
@@ -11,6 +11,8 @@ from rich.console import Console
11
11
  from rich.panel import Panel
12
12
  from rich.table import Table
13
13
 
14
+ from runbooks.common import get_aws_cli_example_period
15
+
14
16
  console = Console()
15
17
 
16
18
 
@@ -311,6 +313,9 @@ def handle_cost_explorer_error(error: Exception, profile_name: Optional[str] = N
311
313
 
312
314
  def _display_single_account_cost_explorer_guidance(error: Exception, profile_name: Optional[str] = None):
313
315
  """Display context-aware guidance for single account Cost Explorer limitations."""
316
+
317
+ # Get dynamic date period for CLI examples
318
+ start_date, end_date = get_aws_cli_example_period()
314
319
 
315
320
  # Main explanation panel
316
321
  explanation_panel = Panel(
@@ -354,7 +359,7 @@ def _display_single_account_cost_explorer_guidance(error: Exception, profile_nam
354
359
  f"[green]✅ Recommended Solutions:[/green]\n\n"
355
360
  f"{solution_commands}\n\n"
356
361
  f"[bold]🎯 Quick Test Commands:[/bold]\n"
357
- f"• Test billing access: `aws ce get-cost-and-usage --time-period Start=2024-01-01,End=2024-01-02 --granularity MONTHLY --metrics UnblendedCost --profile your-billing-profile`\n"
362
+ f"• Test billing access: `aws ce get-cost-and-usage --time-period Start={start_date},End={end_date} --granularity MONTHLY --metrics UnblendedCost --profile your-billing-profile`\n"
358
363
  f"• List available profiles: `aws configure list-profiles`\n"
359
364
  f"• Check current identity: `aws sts get-caller-identity --profile {profile_name or 'your-profile'}`\n\n"
360
365
  f"[bold]💡 Alternative Approach:[/bold]\n"
@@ -458,7 +458,7 @@ class MarkdownExporter:
458
458
  "| " + " | ".join(["---" for _ in headers]) + " |"
459
459
  ]
460
460
 
461
- # Process each VPC candidate
461
+ # Process each VPC candidate with enhanced data extraction
462
462
  for candidate in vpc_candidates:
463
463
  # Extract data with safe attribute access and formatting
464
464
  account_id = getattr(candidate, 'account_id', 'Unknown')
@@ -466,7 +466,44 @@ class MarkdownExporter:
466
466
  vpc_name = getattr(candidate, 'vpc_name', '') or 'Unnamed'
467
467
  cidr_block = getattr(candidate, 'cidr_block', 'Unknown')
468
468
 
469
- # Handle overlapping logic - may need to calculate from CIDR analysis
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)
470
507
  overlapping = "Yes" if getattr(candidate, 'overlapping', False) else "No"
471
508
 
472
509
  # Format boolean indicators with emoji
@@ -649,6 +686,184 @@ class MarkdownExporter:
649
686
  print_warning(f"❌ Failed to export VPC analysis: {e}")
650
687
  return ""
651
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
+
652
867
 
653
868
  def export_finops_to_markdown(
654
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 = get_service_monthly_cost("data_transfer", "us-east-1") # Dynamic 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 using dynamic pricing
159
+ from ..common.aws_pricing import calculate_regional_cost
160
+ return calculate_regional_cost(self._base_monthly_cost_us_east_1, region, "nat_gateway", self.profile_name)
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,62 @@ 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 with universal compatibility."""
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, self.profile),
788
+ "nat_gateway_data_processing": get_service_monthly_cost("data_transfer", base_region, self.profile),
789
+ "transit_gateway_monthly": get_service_monthly_cost("transit_gateway", base_region, self.profile),
790
+ "vpc_endpoint_monthly": get_service_monthly_cost("vpc_endpoint", base_region, self.profile),
791
+ "vpc_endpoint_interface_hourly": get_service_monthly_cost("vpc_endpoint_interface", base_region, self.profile) / (24 * 30),
792
+ "transit_gateway_attachment_hourly": get_service_monthly_cost("transit_gateway_attachment", base_region, self.profile) / (24 * 30),
793
+ "data_transfer_regional": get_service_monthly_cost("data_transfer", base_region, self.profile),
794
+ "data_transfer_internet": get_service_monthly_cost("data_transfer", base_region, self.profile) * 4.5, # Internet is ~4.5x higher
795
+ }
796
+ except Exception as e:
797
+ print_warning(f"Dynamic pricing initialization failed: {e}")
798
+ print_warning("Attempting AWS Pricing API fallback with universal profile support")
799
+ # Enhanced fallback with AWS Pricing API integration
800
+ from ..common.aws_pricing import get_aws_pricing_engine, AWSOfficialPricingEngine
801
+
802
+ try:
803
+ # Use AWS Pricing API with profile support for universal compatibility
804
+ pricing_engine = get_aws_pricing_engine(profile=self.profile, enable_fallback=True)
805
+
806
+ # Get actual AWS pricing instead of hardcoded values
807
+ nat_gateway_pricing = pricing_engine.get_nat_gateway_pricing("us-east-1")
808
+ transit_gateway_pricing = pricing_engine.get_transit_gateway_pricing("us-east-1")
809
+ vpc_endpoint_pricing = pricing_engine.get_vpc_endpoint_pricing("us-east-1")
810
+ data_transfer_pricing = pricing_engine.get_data_transfer_pricing("us-east-1", "internet")
811
+
812
+ return {
813
+ "nat_gateway_monthly": nat_gateway_pricing.monthly_cost,
814
+ "nat_gateway_data_processing": data_transfer_pricing.cost_per_gb,
815
+ "transit_gateway_monthly": transit_gateway_pricing.monthly_cost,
816
+ "transit_gateway_attachment_hourly": transit_gateway_pricing.attachment_hourly_cost,
817
+ "vpc_endpoint_interface_hourly": vpc_endpoint_pricing.interface_hourly_cost,
818
+ "data_transfer_regional": data_transfer_pricing.cost_per_gb * 0.1, # Regional is ~10% of internet cost
819
+ "data_transfer_cross_region": data_transfer_pricing.cost_per_gb * 0.2, # Cross-region is ~20% of internet cost
820
+ "data_transfer_internet": data_transfer_pricing.cost_per_gb
821
+ }
822
+
823
+ except Exception as pricing_error:
824
+ print_error(f"ENTERPRISE COMPLIANCE VIOLATION: Cannot determine pricing without AWS API access: {pricing_error}")
825
+ print_warning("Universal compatibility requires dynamic pricing - hardcoded values not permitted")
826
+
827
+ # Return error state instead of hardcoded values to maintain enterprise compliance
828
+ raise RuntimeError(
829
+ "Universal compatibility mode requires dynamic AWS pricing API access. "
830
+ "Please ensure your AWS profile has pricing:GetProducts permissions or configure "
831
+ "appropriate billing/management profile access."
832
+ )
777
833
 
778
834
  async def analyze_comprehensive_vpc_costs(self, profile: Optional[str] = None,
779
835
  regions: Optional[List[str]] = None) -> Dict[str, Any]:
@@ -34,6 +34,8 @@ try:
34
34
  except ImportError:
35
35
  # Define mock_costexplorer as a no-op decorator for compatibility
36
36
  def mock_costexplorer(func):
37
+ # Dynamic test period for consistent test data
38
+ test_period = get_test_date_period(30)
37
39
  def wrapper(*args, **kwargs):
38
40
  return func(*args, **kwargs)
39
41
 
@@ -128,7 +130,7 @@ class TestAWSIntegrationWithMoto:
128
130
  mock_cost_data = {
129
131
  "ResultsByTime": [
130
132
  {
131
- "TimePeriod": {"Start": "2024-01-01", "End": "2024-01-31"},
133
+ "TimePeriod": {"Start": test_period["Start"], "End": test_period["End"]},
132
134
  "Total": {"UnblendedCost": {"Amount": "50000.00", "Unit": "USD"}},
133
135
  "Groups": [
134
136
  {"Keys": ["EC2-Instance"], "Metrics": {"UnblendedCost": {"Amount": "20000.00", "Unit": "USD"}}},
@@ -19,20 +19,28 @@ from .markdown_exporter import MarkdownExporter
19
19
 
20
20
 
21
21
  def _format_tags_for_display(tags_dict: Dict[str, str]) -> str:
22
- """Format tags for display with priority order."""
22
+ """Format tags for display with priority order, emphasizing ownership tags."""
23
23
  if not tags_dict:
24
24
  return "No tags"
25
25
 
26
- priority_keys = ['Name', 'Environment', 'Project', 'Owner', 'BusinessOwner', 'Team', 'CostCenter']
26
+ # Enhanced priority keys with focus on ownership and approvals
27
+ priority_keys = ['Name', 'Owner', 'BusinessOwner', 'TechnicalOwner', 'Team', 'Contact',
28
+ 'Environment', 'Project', 'CostCenter', 'CreatedBy', 'ManagedBy']
27
29
  relevant_tags = []
28
30
 
29
31
  for key in priority_keys:
30
32
  if key in tags_dict and tags_dict[key]:
31
33
  relevant_tags.append(f"{key}:{tags_dict[key]}")
32
34
 
35
+ # Add CloudFormation/Terraform tags for IaC detection
36
+ iac_keys = ['aws:cloudformation:stack-name', 'terraform:module', 'cdktf:stack', 'pulumi:project']
37
+ for key in iac_keys:
38
+ if key in tags_dict and tags_dict[key] and len(relevant_tags) < 6:
39
+ relevant_tags.append(f"IaC:{tags_dict[key]}")
40
+
33
41
  # Add other important tags
34
42
  for key, value in tags_dict.items():
35
- if key not in priority_keys and value and len(relevant_tags) < 5:
43
+ if key not in priority_keys + iac_keys and value and len(relevant_tags) < 5:
36
44
  relevant_tags.append(f"{key}:{value}")
37
45
 
38
46
  return "; ".join(relevant_tags) if relevant_tags else f"({len(tags_dict)} tags)"
@@ -118,40 +126,34 @@ def _export_vpc_candidates_csv(vpc_candidates: List[Any], output_dir: str) -> st
118
126
  # Extract data with enhanced tag and owner handling
119
127
  tags_dict = getattr(candidate, 'tags', {}) or {}
120
128
 
121
- # Enhanced tag display - prioritize important tags
122
- if tags_dict:
123
- priority_keys = ['Name', 'Environment', 'Project', 'Owner', 'BusinessOwner', 'Team']
124
- relevant_tags = []
125
- for key in priority_keys:
126
- if key in tags_dict and tags_dict[key]:
127
- relevant_tags.append(f"{key}:{tags_dict[key]}")
128
-
129
- # Add other important tags
130
- for key, value in tags_dict.items():
131
- if key not in priority_keys and value and len(relevant_tags) < 5:
132
- relevant_tags.append(f"{key}:{value}")
133
-
134
- tags_str = "; ".join(relevant_tags)
135
- else:
136
- tags_str = "No tags"
129
+ # Use enhanced tag formatting function
130
+ tags_str = _format_tags_for_display(tags_dict)
137
131
 
138
132
  load_balancers = getattr(candidate, 'load_balancers', []) or []
139
133
  lbs_present = "Yes" if load_balancers else "No"
140
134
 
141
- # Enhanced owner extraction
135
+ # Enhanced owner extraction from multiple sources
142
136
  owners = getattr(candidate, 'owners_approvals', []) or []
143
137
 
144
- # If no owners found via attributes, extract from tags directly
138
+ # Extract owners from tags with enhanced logic
145
139
  if not owners and tags_dict:
146
140
  owner_keys = ['Owner', 'BusinessOwner', 'TechnicalOwner', 'Team', 'Contact', 'CreatedBy', 'ManagedBy']
147
141
  for key in owner_keys:
148
142
  if key in tags_dict and tags_dict[key]:
149
- if 'business' in key.lower() or 'manager' in tags_dict[key].lower():
150
- owners.append(f"{tags_dict[key]} (Business)")
151
- elif 'technical' in key.lower() or any(tech in tags_dict[key].lower() for tech in ['ops', 'devops', 'engineering']):
152
- owners.append(f"{tags_dict[key]} (Technical)")
143
+ value = tags_dict[key]
144
+ if 'business' in key.lower() or 'manager' in value.lower():
145
+ owners.append(f"{value} (Business)")
146
+ elif 'technical' in key.lower() or 'engineer' in value.lower():
147
+ owners.append(f"{value} (Technical)")
148
+ elif 'team' in key.lower():
149
+ owners.append(f"{value} (Team)")
153
150
  else:
154
- owners.append(tags_dict[key])
151
+ owners.append(f"{value} ({key})")
152
+
153
+ # For default VPCs, add system indicator
154
+ is_default = getattr(candidate, 'is_default', False)
155
+ if is_default and not owners:
156
+ owners.append("System Default")
155
157
 
156
158
  if owners:
157
159
  owners_str = "; ".join(owners)