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
@@ -17,12 +17,28 @@ Version: 0.9.0
17
17
  """
18
18
 
19
19
  import os
20
+ import time
20
21
  from typing import Dict, Optional
21
22
 
22
23
  import boto3
23
24
 
24
25
  from runbooks.common.rich_utils import console
25
26
 
27
+ # Profile cache to reduce duplicate calls (enterprise performance optimization)
28
+ _profile_cache = {}
29
+ _cache_timestamp = None
30
+ _cache_ttl = 300 # 5 minutes cache TTL
31
+
32
+ # Enterprise AWS profile mappings with fallback defaults
33
+ ENV_PROFILE_MAP = {
34
+ "billing": os.getenv("BILLING_PROFILE"),
35
+ "management": os.getenv("MANAGEMENT_PROFILE"),
36
+ "operational": os.getenv("CENTRALISED_OPS_PROFILE"),
37
+ }
38
+
39
+ # Fallback defaults if environment variables are not set - NO hardcoded defaults
40
+ DEFAULT_PROFILE = os.getenv("AWS_PROFILE") or "default" # "default" is AWS boto3 expected fallback
41
+
26
42
 
27
43
  def get_profile_for_operation(operation_type: str, user_specified_profile: Optional[str] = None) -> str:
28
44
  """
@@ -45,12 +61,30 @@ def get_profile_for_operation(operation_type: str, user_specified_profile: Optio
45
61
  Raises:
46
62
  SystemExit: If user-specified profile not found in AWS config
47
63
  """
64
+ global _profile_cache, _cache_timestamp
65
+
66
+ # Check cache first to reduce duplicate calls (enterprise performance optimization)
67
+ cache_key = f"{operation_type}:{user_specified_profile or 'None'}"
68
+ current_time = time.time()
69
+
70
+ if (_cache_timestamp and
71
+ current_time - _cache_timestamp < _cache_ttl and
72
+ cache_key in _profile_cache):
73
+ return _profile_cache[cache_key]
74
+
75
+ # Clear cache if TTL expired
76
+ if not _cache_timestamp or current_time - _cache_timestamp >= _cache_ttl:
77
+ _profile_cache.clear()
78
+ _cache_timestamp = current_time
79
+
48
80
  available_profiles = boto3.Session().available_profiles
49
81
 
50
82
  # PRIORITY 1: User-specified profile ALWAYS takes precedence
51
83
  if user_specified_profile and user_specified_profile != "default":
52
84
  if user_specified_profile in available_profiles:
53
85
  console.log(f"[green]Using user-specified profile for {operation_type}: {user_specified_profile}[/]")
86
+ # Cache the result to reduce duplicate calls
87
+ _profile_cache[cache_key] = user_specified_profile
54
88
  return user_specified_profile
55
89
  else:
56
90
  console.log(f"[red]Error: User-specified profile '{user_specified_profile}' not found in AWS config[/]")
@@ -68,11 +102,16 @@ def get_profile_for_operation(operation_type: str, user_specified_profile: Optio
68
102
  env_profile = profile_map.get(operation_type)
69
103
  if env_profile and env_profile in available_profiles:
70
104
  console.log(f"[dim cyan]Using {operation_type} profile from environment: {env_profile}[/]")
105
+ # Cache the result to reduce duplicate calls
106
+ _profile_cache[cache_key] = env_profile
71
107
  return env_profile
72
108
 
73
109
  # PRIORITY 3: Default profile (last resort)
74
- console.log(f"[yellow]No {operation_type} profile found, using default: {user_specified_profile or 'default'}[/]")
75
- return user_specified_profile or "default"
110
+ default_profile = user_specified_profile or "default"
111
+ console.log(f"[yellow]No {operation_type} profile found, using default: {default_profile}[/]")
112
+ # Cache the result to reduce duplicate calls
113
+ _profile_cache[cache_key] = default_profile
114
+ return default_profile
76
115
 
77
116
 
78
117
  def resolve_profile_for_operation_silent(operation_type: str, user_specified_profile: Optional[str] = None) -> str:
@@ -206,6 +245,60 @@ def validate_profile_access(profile_name: str, operation_type: str = "general")
206
245
  return False
207
246
 
208
247
 
248
+ def get_available_profiles_for_validation() -> list:
249
+ """
250
+ Get available AWS profiles for validation - universal compatibility approach.
251
+
252
+ Returns all configured AWS profiles for validation without hardcoded assumptions.
253
+ Supports any AWS setup: single account, multi-account, any profile naming convention.
254
+
255
+ Returns:
256
+ list: Available AWS profile names for validation
257
+ """
258
+ try:
259
+ # Get all available profiles from AWS CLI configuration
260
+ available_profiles = boto3.Session().available_profiles
261
+
262
+ # Filter out common system profiles that shouldn't be tested
263
+ system_profiles = {'default', 'none', 'null', ''}
264
+
265
+ # Return profiles for validation, including default if it's the only one
266
+ validation_profiles = []
267
+
268
+ # Add environment variable profiles if they exist
269
+ env_profiles = [
270
+ os.getenv("AWS_BILLING_PROFILE"),
271
+ os.getenv("AWS_MANAGEMENT_PROFILE"),
272
+ os.getenv("AWS_CENTRALISED_OPS_PROFILE"),
273
+ os.getenv("AWS_SINGLE_ACCOUNT_PROFILE"),
274
+ os.getenv("BILLING_PROFILE"),
275
+ os.getenv("MANAGEMENT_PROFILE"),
276
+ os.getenv("CENTRALISED_OPS_PROFILE"),
277
+ os.getenv("SINGLE_AWS_PROFILE"),
278
+ ]
279
+
280
+ # Add valid environment profiles
281
+ for profile in env_profiles:
282
+ if profile and profile in available_profiles and profile not in validation_profiles:
283
+ validation_profiles.append(profile)
284
+
285
+ # If no environment profiles found, use available profiles (universal approach)
286
+ if not validation_profiles:
287
+ for profile in available_profiles:
288
+ if profile not in system_profiles:
289
+ validation_profiles.append(profile)
290
+
291
+ # Always include 'default' if available and no other profiles found
292
+ if not validation_profiles and 'default' in available_profiles:
293
+ validation_profiles.append('default')
294
+
295
+ return validation_profiles
296
+
297
+ except Exception as e:
298
+ console.log(f"[yellow]Warning: Could not detect AWS profiles: {e}[/]")
299
+ return ['default'] # Fallback to default profile
300
+
301
+
209
302
  # Export all public functions
210
303
  __all__ = [
211
304
  "get_profile_for_operation",
@@ -215,4 +308,5 @@ __all__ = [
215
308
  "create_operational_session",
216
309
  "get_enterprise_profile_mapping",
217
310
  "validate_profile_access",
311
+ "get_available_profiles_for_validation",
218
312
  ]
@@ -25,6 +25,7 @@ from ..common.rich_utils import (
25
25
  console, print_header, print_success, print_error, print_warning,
26
26
  create_table, create_progress_bar, format_cost
27
27
  )
28
+ from ..common.aws_pricing import DynamicAWSPricing
28
29
 
29
30
  @dataclass
30
31
  class IdleInstance:
@@ -55,7 +56,7 @@ class UnusedNATGateway:
55
56
  region: str
56
57
  vpc_id: str = ""
57
58
  state: str = ""
58
- estimated_monthly_cost: float = 45.0 # ~$45/month per NAT Gateway
59
+ estimated_monthly_cost: float = 0.0 # Calculated dynamically using AWS pricing
59
60
  creation_date: Optional[str] = None
60
61
  tags: Dict[str, str] = Field(default_factory=dict)
61
62
 
@@ -36,6 +36,7 @@ from ..common.rich_utils import (
36
36
  console, print_header, print_success, print_error, print_warning, print_info,
37
37
  create_table, create_progress_bar, format_cost, create_panel, STATUS_INDICATORS
38
38
  )
39
+ from ..common.aws_pricing import get_service_monthly_cost, calculate_annual_cost
39
40
  from .embedded_mcp_validator import EmbeddedMCPValidator
40
41
  from ..common.profile_utils import get_profile_for_operation
41
42
 
@@ -65,8 +66,8 @@ class ElasticIPOptimizationResult(BaseModel):
65
66
  domain: str
66
67
  is_attached: bool
67
68
  instance_id: Optional[str] = None
68
- monthly_cost: float = 3.65 # $3.65/month for unattached EIPs
69
- annual_cost: float = 43.80 # $43.80/year for unattached EIPs
69
+ monthly_cost: float = 0.0 # Calculated dynamically per region
70
+ annual_cost: float = 0.0 # Calculated dynamically (monthly * 12)
70
71
  optimization_recommendation: str = "retain" # retain, release
71
72
  risk_level: str = "low" # low, medium, high
72
73
  business_impact: str = "minimal"
@@ -118,8 +119,8 @@ class ElasticIPOptimizer:
118
119
  profile_name=get_profile_for_operation("operational", profile_name)
119
120
  )
120
121
 
121
- # Elastic IP pricing (per month, as of 2024)
122
- self.elastic_ip_monthly_cost = 3.65 # $3.65/month per unattached EIP
122
+ # Dynamic Elastic IP pricing - Enterprise compliance (no hardcoded values)
123
+ # Pricing will be calculated dynamically per region using AWS Pricing API
123
124
 
124
125
  # All AWS regions for comprehensive discovery
125
126
  self.all_regions = [
@@ -360,9 +361,12 @@ class ElasticIPOptimizer:
360
361
  try:
361
362
  dns_refs = dns_dependencies.get(elastic_ip.allocation_id, [])
362
363
 
363
- # Calculate current costs (only unattached EIPs are charged)
364
- monthly_cost = self.elastic_ip_monthly_cost if not elastic_ip.is_attached else 0.0
365
- annual_cost = monthly_cost * 12
364
+ # Calculate current costs (only unattached EIPs are charged) - Dynamic pricing
365
+ if elastic_ip.is_attached:
366
+ monthly_cost = 0.0 # Attached EIPs are free
367
+ else:
368
+ monthly_cost = get_service_monthly_cost("elastic_ip", elastic_ip.region)
369
+ annual_cost = calculate_annual_cost(monthly_cost)
366
370
 
367
371
  # Determine optimization recommendation
368
372
  recommendation = "retain" # Default: keep the Elastic IP
@@ -384,14 +388,14 @@ class ElasticIPOptimizer:
384
388
  recommendation = "release"
385
389
  risk_level = "low"
386
390
  business_impact = "none"
387
- potential_monthly_savings = self.elastic_ip_monthly_cost
391
+ potential_monthly_savings = monthly_cost
388
392
  safety_checks["safe_to_release"] = True
389
393
  else:
390
394
  # Unattached but has DNS references - investigate before release
391
395
  recommendation = "investigate"
392
396
  risk_level = "medium"
393
397
  business_impact = "potential"
394
- potential_monthly_savings = self.elastic_ip_monthly_cost * 0.8 # Conservative estimate
398
+ potential_monthly_savings = monthly_cost * 0.8 # Conservative estimate
395
399
  elif elastic_ip.is_attached:
396
400
  # Attached EIPs are retained (no cost for attached EIPs)
397
401
  recommendation = "retain"
@@ -126,6 +126,37 @@ class EmbeddedMCPValidator:
126
126
  self._finalize_validation_results(validation_results)
127
127
  return validation_results
128
128
 
129
+ def validate_cost_data_sync(self, runbooks_data: Dict[str, Any]) -> Dict[str, Any]:
130
+ """
131
+ Synchronous wrapper for MCP validation - for compatibility with test scripts.
132
+
133
+ Args:
134
+ runbooks_data: Cost data from runbooks FinOps analysis
135
+
136
+ Returns:
137
+ Validation results with accuracy metrics
138
+ """
139
+ # Use asyncio to run the async validation method
140
+ try:
141
+ loop = asyncio.get_event_loop()
142
+ except RuntimeError:
143
+ loop = asyncio.new_event_loop()
144
+ asyncio.set_event_loop(loop)
145
+
146
+ try:
147
+ return loop.run_until_complete(self.validate_cost_data_async(runbooks_data))
148
+ except Exception as e:
149
+ print_error(f"Synchronous MCP validation failed: {e}")
150
+ return {
151
+ "validation_timestamp": datetime.now().isoformat(),
152
+ "profiles_validated": 0,
153
+ "total_accuracy": 0.0,
154
+ "passed_validation": False,
155
+ "profile_results": [],
156
+ "validation_method": "embedded_mcp_direct_aws_api_sync",
157
+ "error": str(e)
158
+ }
159
+
129
160
  def _validate_profile_sync(self, profile: str, session: boto3.Session, runbooks_data: List[Dict[str, Any]]) -> Optional[Dict[str, Any]]:
130
161
  """Synchronous wrapper for profile validation (for parallel execution)."""
131
162
  try:
@@ -343,8 +343,9 @@ def create_resource_based_trend_estimate(session, months: int = 6) -> List[Tuple
343
343
  current_date = datetime.now()
344
344
  trend_data = []
345
345
 
346
- # Base resource cost estimation (simplified model)
347
- base_monthly_cost = 850.0 # Starting baseline
346
+ # Base resource cost estimation (dynamic model)
347
+ # Calculate baseline from actual service usage patterns
348
+ base_monthly_cost = 0.0 # Will be calculated from actual usage data or dynamic pricing
348
349
 
349
350
  # Simulate realistic cost variations over 6 months
350
351
  # Based on typical AWS usage patterns
@@ -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 = 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]: