runbooks 1.1.3__py3-none-any.whl → 1.1.5__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 (247) hide show
  1. runbooks/__init__.py +31 -2
  2. runbooks/__init___optimized.py +18 -4
  3. runbooks/_platform/__init__.py +1 -5
  4. runbooks/_platform/core/runbooks_wrapper.py +141 -138
  5. runbooks/aws2/accuracy_validator.py +812 -0
  6. runbooks/base.py +7 -0
  7. runbooks/cfat/WEIGHT_CONFIG_README.md +1 -1
  8. runbooks/cfat/assessment/compliance.py +8 -8
  9. runbooks/cfat/assessment/runner.py +1 -0
  10. runbooks/cfat/cloud_foundations_assessment.py +227 -239
  11. runbooks/cfat/models.py +6 -2
  12. runbooks/cfat/tests/__init__.py +6 -1
  13. runbooks/cli/__init__.py +13 -0
  14. runbooks/cli/commands/cfat.py +274 -0
  15. runbooks/cli/commands/finops.py +1164 -0
  16. runbooks/cli/commands/inventory.py +379 -0
  17. runbooks/cli/commands/operate.py +239 -0
  18. runbooks/cli/commands/security.py +248 -0
  19. runbooks/cli/commands/validation.py +825 -0
  20. runbooks/cli/commands/vpc.py +310 -0
  21. runbooks/cli/registry.py +107 -0
  22. runbooks/cloudops/__init__.py +23 -30
  23. runbooks/cloudops/base.py +96 -107
  24. runbooks/cloudops/cost_optimizer.py +549 -547
  25. runbooks/cloudops/infrastructure_optimizer.py +5 -4
  26. runbooks/cloudops/interfaces.py +226 -227
  27. runbooks/cloudops/lifecycle_manager.py +5 -4
  28. runbooks/cloudops/mcp_cost_validation.py +252 -235
  29. runbooks/cloudops/models.py +78 -53
  30. runbooks/cloudops/monitoring_automation.py +5 -4
  31. runbooks/cloudops/notebook_framework.py +179 -215
  32. runbooks/cloudops/security_enforcer.py +125 -159
  33. runbooks/common/accuracy_validator.py +11 -0
  34. runbooks/common/aws_pricing.py +349 -326
  35. runbooks/common/aws_pricing_api.py +211 -212
  36. runbooks/common/aws_profile_manager.py +341 -0
  37. runbooks/common/aws_utils.py +75 -80
  38. runbooks/common/business_logic.py +127 -105
  39. runbooks/common/cli_decorators.py +36 -60
  40. runbooks/common/comprehensive_cost_explorer_integration.py +456 -464
  41. runbooks/common/cross_account_manager.py +198 -205
  42. runbooks/common/date_utils.py +27 -39
  43. runbooks/common/decorators.py +235 -0
  44. runbooks/common/dry_run_examples.py +173 -208
  45. runbooks/common/dry_run_framework.py +157 -155
  46. runbooks/common/enhanced_exception_handler.py +15 -4
  47. runbooks/common/enhanced_logging_example.py +50 -64
  48. runbooks/common/enhanced_logging_integration_example.py +65 -37
  49. runbooks/common/env_utils.py +16 -16
  50. runbooks/common/error_handling.py +40 -38
  51. runbooks/common/lazy_loader.py +41 -23
  52. runbooks/common/logging_integration_helper.py +79 -86
  53. runbooks/common/mcp_cost_explorer_integration.py +478 -495
  54. runbooks/common/mcp_integration.py +63 -74
  55. runbooks/common/memory_optimization.py +140 -118
  56. runbooks/common/module_cli_base.py +37 -58
  57. runbooks/common/organizations_client.py +176 -194
  58. runbooks/common/patterns.py +204 -0
  59. runbooks/common/performance_monitoring.py +67 -71
  60. runbooks/common/performance_optimization_engine.py +283 -274
  61. runbooks/common/profile_utils.py +248 -39
  62. runbooks/common/rich_utils.py +643 -92
  63. runbooks/common/sre_performance_suite.py +177 -186
  64. runbooks/enterprise/__init__.py +1 -1
  65. runbooks/enterprise/logging.py +144 -106
  66. runbooks/enterprise/security.py +187 -204
  67. runbooks/enterprise/validation.py +43 -56
  68. runbooks/finops/__init__.py +29 -33
  69. runbooks/finops/account_resolver.py +1 -1
  70. runbooks/finops/advanced_optimization_engine.py +980 -0
  71. runbooks/finops/automation_core.py +268 -231
  72. runbooks/finops/business_case_config.py +184 -179
  73. runbooks/finops/cli.py +660 -139
  74. runbooks/finops/commvault_ec2_analysis.py +157 -164
  75. runbooks/finops/compute_cost_optimizer.py +336 -320
  76. runbooks/finops/config.py +20 -20
  77. runbooks/finops/cost_optimizer.py +488 -622
  78. runbooks/finops/cost_processor.py +332 -214
  79. runbooks/finops/dashboard_runner.py +1006 -172
  80. runbooks/finops/ebs_cost_optimizer.py +991 -657
  81. runbooks/finops/elastic_ip_optimizer.py +317 -257
  82. runbooks/finops/enhanced_mcp_integration.py +340 -0
  83. runbooks/finops/enhanced_progress.py +40 -37
  84. runbooks/finops/enhanced_trend_visualization.py +3 -2
  85. runbooks/finops/enterprise_wrappers.py +230 -292
  86. runbooks/finops/executive_export.py +203 -160
  87. runbooks/finops/helpers.py +130 -288
  88. runbooks/finops/iam_guidance.py +1 -1
  89. runbooks/finops/infrastructure/__init__.py +80 -0
  90. runbooks/finops/infrastructure/commands.py +506 -0
  91. runbooks/finops/infrastructure/load_balancer_optimizer.py +866 -0
  92. runbooks/finops/infrastructure/vpc_endpoint_optimizer.py +832 -0
  93. runbooks/finops/markdown_exporter.py +338 -175
  94. runbooks/finops/mcp_validator.py +1952 -0
  95. runbooks/finops/nat_gateway_optimizer.py +1513 -482
  96. runbooks/finops/network_cost_optimizer.py +657 -587
  97. runbooks/finops/notebook_utils.py +226 -188
  98. runbooks/finops/optimization_engine.py +1136 -0
  99. runbooks/finops/optimizer.py +25 -29
  100. runbooks/finops/rds_snapshot_optimizer.py +367 -411
  101. runbooks/finops/reservation_optimizer.py +427 -363
  102. runbooks/finops/scenario_cli_integration.py +77 -78
  103. runbooks/finops/scenarios.py +1278 -439
  104. runbooks/finops/schemas.py +218 -182
  105. runbooks/finops/snapshot_manager.py +2289 -0
  106. runbooks/finops/tests/test_finops_dashboard.py +3 -3
  107. runbooks/finops/tests/test_reference_images_validation.py +2 -2
  108. runbooks/finops/tests/test_single_account_features.py +17 -17
  109. runbooks/finops/tests/validate_test_suite.py +1 -1
  110. runbooks/finops/types.py +3 -3
  111. runbooks/finops/validation_framework.py +263 -269
  112. runbooks/finops/vpc_cleanup_exporter.py +191 -146
  113. runbooks/finops/vpc_cleanup_optimizer.py +593 -575
  114. runbooks/finops/workspaces_analyzer.py +171 -182
  115. runbooks/hitl/enhanced_workflow_engine.py +1 -1
  116. runbooks/integration/__init__.py +89 -0
  117. runbooks/integration/mcp_integration.py +1920 -0
  118. runbooks/inventory/CLAUDE.md +816 -0
  119. runbooks/inventory/README.md +3 -3
  120. runbooks/inventory/Tests/common_test_data.py +30 -30
  121. runbooks/inventory/__init__.py +2 -2
  122. runbooks/inventory/cloud_foundations_integration.py +144 -149
  123. runbooks/inventory/collectors/aws_comprehensive.py +28 -11
  124. runbooks/inventory/collectors/aws_networking.py +111 -101
  125. runbooks/inventory/collectors/base.py +4 -0
  126. runbooks/inventory/core/collector.py +495 -313
  127. runbooks/inventory/discovery.md +2 -2
  128. runbooks/inventory/drift_detection_cli.py +69 -96
  129. runbooks/inventory/find_ec2_security_groups.py +1 -1
  130. runbooks/inventory/inventory_mcp_cli.py +48 -46
  131. runbooks/inventory/list_rds_snapshots_aggregator.py +192 -208
  132. runbooks/inventory/mcp_inventory_validator.py +549 -465
  133. runbooks/inventory/mcp_vpc_validator.py +359 -442
  134. runbooks/inventory/organizations_discovery.py +56 -52
  135. runbooks/inventory/rich_inventory_display.py +33 -32
  136. runbooks/inventory/unified_validation_engine.py +278 -251
  137. runbooks/inventory/vpc_analyzer.py +733 -696
  138. runbooks/inventory/vpc_architecture_validator.py +293 -348
  139. runbooks/inventory/vpc_dependency_analyzer.py +382 -378
  140. runbooks/inventory/vpc_flow_analyzer.py +3 -3
  141. runbooks/main.py +152 -9147
  142. runbooks/main_final.py +91 -60
  143. runbooks/main_minimal.py +22 -10
  144. runbooks/main_optimized.py +131 -100
  145. runbooks/main_ultra_minimal.py +7 -2
  146. runbooks/mcp/__init__.py +36 -0
  147. runbooks/mcp/integration.py +679 -0
  148. runbooks/metrics/dora_metrics_engine.py +2 -2
  149. runbooks/monitoring/performance_monitor.py +9 -4
  150. runbooks/operate/dynamodb_operations.py +3 -1
  151. runbooks/operate/ec2_operations.py +145 -137
  152. runbooks/operate/iam_operations.py +146 -152
  153. runbooks/operate/mcp_integration.py +1 -1
  154. runbooks/operate/networking_cost_heatmap.py +33 -10
  155. runbooks/operate/privatelink_operations.py +1 -1
  156. runbooks/operate/rds_operations.py +223 -254
  157. runbooks/operate/s3_operations.py +107 -118
  158. runbooks/operate/vpc_endpoints.py +1 -1
  159. runbooks/operate/vpc_operations.py +648 -618
  160. runbooks/remediation/base.py +1 -1
  161. runbooks/remediation/commons.py +10 -7
  162. runbooks/remediation/commvault_ec2_analysis.py +71 -67
  163. runbooks/remediation/ec2_unattached_ebs_volumes.py +1 -0
  164. runbooks/remediation/multi_account.py +24 -21
  165. runbooks/remediation/rds_snapshot_list.py +91 -65
  166. runbooks/remediation/remediation_cli.py +92 -146
  167. runbooks/remediation/universal_account_discovery.py +83 -79
  168. runbooks/remediation/workspaces_list.py +49 -44
  169. runbooks/security/__init__.py +19 -0
  170. runbooks/security/assessment_runner.py +1150 -0
  171. runbooks/security/baseline_checker.py +812 -0
  172. runbooks/security/cloudops_automation_security_validator.py +509 -535
  173. runbooks/security/compliance_automation_engine.py +17 -17
  174. runbooks/security/config/__init__.py +2 -2
  175. runbooks/security/config/compliance_config.py +50 -50
  176. runbooks/security/config_template_generator.py +63 -76
  177. runbooks/security/enterprise_security_framework.py +1 -1
  178. runbooks/security/executive_security_dashboard.py +519 -508
  179. runbooks/security/integration_test_enterprise_security.py +5 -3
  180. runbooks/security/multi_account_security_controls.py +959 -1210
  181. runbooks/security/real_time_security_monitor.py +422 -444
  182. runbooks/security/run_script.py +1 -1
  183. runbooks/security/security_baseline_tester.py +1 -1
  184. runbooks/security/security_cli.py +143 -112
  185. runbooks/security/test_2way_validation.py +439 -0
  186. runbooks/security/two_way_validation_framework.py +852 -0
  187. runbooks/sre/mcp_reliability_engine.py +6 -6
  188. runbooks/sre/production_monitoring_framework.py +167 -177
  189. runbooks/tdd/__init__.py +15 -0
  190. runbooks/tdd/cli.py +1071 -0
  191. runbooks/utils/__init__.py +14 -17
  192. runbooks/utils/logger.py +7 -2
  193. runbooks/utils/version_validator.py +51 -48
  194. runbooks/validation/__init__.py +6 -6
  195. runbooks/validation/cli.py +9 -3
  196. runbooks/validation/comprehensive_2way_validator.py +754 -708
  197. runbooks/validation/mcp_validator.py +906 -228
  198. runbooks/validation/terraform_citations_validator.py +104 -115
  199. runbooks/validation/terraform_drift_detector.py +447 -451
  200. runbooks/vpc/README.md +617 -0
  201. runbooks/vpc/__init__.py +8 -1
  202. runbooks/vpc/analyzer.py +577 -0
  203. runbooks/vpc/cleanup_wrapper.py +476 -413
  204. runbooks/vpc/cli_cloudtrail_commands.py +339 -0
  205. runbooks/vpc/cli_mcp_validation_commands.py +480 -0
  206. runbooks/vpc/cloudtrail_audit_integration.py +717 -0
  207. runbooks/vpc/config.py +92 -97
  208. runbooks/vpc/cost_engine.py +411 -148
  209. runbooks/vpc/cost_explorer_integration.py +553 -0
  210. runbooks/vpc/cross_account_session.py +101 -106
  211. runbooks/vpc/enhanced_mcp_validation.py +917 -0
  212. runbooks/vpc/eni_gate_validator.py +961 -0
  213. runbooks/vpc/heatmap_engine.py +190 -162
  214. runbooks/vpc/mcp_no_eni_validator.py +681 -640
  215. runbooks/vpc/nat_gateway_optimizer.py +358 -0
  216. runbooks/vpc/networking_wrapper.py +15 -8
  217. runbooks/vpc/pdca_remediation_planner.py +528 -0
  218. runbooks/vpc/performance_optimized_analyzer.py +219 -231
  219. runbooks/vpc/runbooks_adapter.py +1167 -241
  220. runbooks/vpc/tdd_red_phase_stubs.py +601 -0
  221. runbooks/vpc/test_data_loader.py +358 -0
  222. runbooks/vpc/tests/conftest.py +314 -4
  223. runbooks/vpc/tests/test_cleanup_framework.py +1022 -0
  224. runbooks/vpc/tests/test_cost_engine.py +0 -2
  225. runbooks/vpc/topology_generator.py +326 -0
  226. runbooks/vpc/unified_scenarios.py +1302 -1129
  227. runbooks/vpc/vpc_cleanup_integration.py +1943 -1115
  228. runbooks-1.1.5.dist-info/METADATA +328 -0
  229. {runbooks-1.1.3.dist-info → runbooks-1.1.5.dist-info}/RECORD +233 -200
  230. runbooks/finops/README.md +0 -414
  231. runbooks/finops/accuracy_cross_validator.py +0 -647
  232. runbooks/finops/business_cases.py +0 -950
  233. runbooks/finops/dashboard_router.py +0 -922
  234. runbooks/finops/ebs_optimizer.py +0 -956
  235. runbooks/finops/embedded_mcp_validator.py +0 -1629
  236. runbooks/finops/enhanced_dashboard_runner.py +0 -527
  237. runbooks/finops/finops_dashboard.py +0 -584
  238. runbooks/finops/finops_scenarios.py +0 -1218
  239. runbooks/finops/legacy_migration.py +0 -730
  240. runbooks/finops/multi_dashboard.py +0 -1519
  241. runbooks/finops/single_dashboard.py +0 -1113
  242. runbooks/finops/unlimited_scenarios.py +0 -393
  243. runbooks-1.1.3.dist-info/METADATA +0 -799
  244. {runbooks-1.1.3.dist-info → runbooks-1.1.5.dist-info}/WHEEL +0 -0
  245. {runbooks-1.1.3.dist-info → runbooks-1.1.5.dist-info}/entry_points.txt +0 -0
  246. {runbooks-1.1.3.dist-info → runbooks-1.1.5.dist-info}/licenses/LICENSE +0 -0
  247. {runbooks-1.1.3.dist-info → runbooks-1.1.5.dist-info}/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env python3
2
2
  """
3
- Rich-styled Markdown Export Module for CloudOps Runbooks FinOps
3
+ Rich-styled Markdown Export Module for CloudOps & FinOps Runbooks
4
4
 
5
5
  This module provides Rich table to markdown conversion functionality with
6
6
  MkDocs compatibility for copy-pasteable documentation tables.
@@ -27,7 +27,6 @@ from rich.table import Table
27
27
  from rich.text import Text
28
28
 
29
29
  from runbooks import __version__
30
-
31
30
  from runbooks.common.rich_utils import (
32
31
  STATUS_INDICATORS,
33
32
  console,
@@ -241,7 +240,7 @@ class MarkdownExporter:
241
240
  | Untagged Resources | {profile_data.get("untagged_resources", 0)} | N/A | Implement tagging strategy |
242
241
 
243
242
  ---
244
- *Generated by CloudOps Runbooks FinOps Module v{__version__}*
243
+ *Generated by CloudOps & FinOps Runbooks Module v{__version__}*
245
244
  """
246
245
 
247
246
  return markdown_content
@@ -329,7 +328,7 @@ class MarkdownExporter:
329
328
  4. **Governance**: Tag {sum(p.get("untagged_resources", 0) for p in multi_profile_data)} untagged resources
330
329
 
331
330
  ---
332
- *Generated by CloudOps Runbooks FinOps Module v{__version__}*
331
+ *Generated by CloudOps & FinOps Runbooks Module v{__version__}*
333
332
  """
334
333
 
335
334
  return markdown_content
@@ -400,14 +399,131 @@ class MarkdownExporter:
400
399
  return "❓ Unknown"
401
400
 
402
401
  def _calculate_mom_change(self, profiles: List[Dict[str, Any]]) -> float:
403
- """Calculate month-over-month change percentage."""
404
- total_current = sum(p.get("total_cost", 0) for p in profiles)
405
- total_last = sum(p.get("last_month_cost", 0) for p in profiles)
402
+ """
403
+ Calculate month-over-month change percentage with smart partial month normalization.
404
+
405
+ Features:
406
+ - Detects partial months and applies normalization
407
+ - Handles equal-day vs full-month comparisons intelligently
408
+ - Integrates with existing period metadata infrastructure
409
+ - Rich CLI formatting for transparency
410
+ """
411
+ try:
412
+ from datetime import date, timedelta
413
+
414
+ from ..common.rich_utils import console
415
+
416
+ total_current = sum(p.get("total_cost", 0) for p in profiles)
417
+ total_last = sum(p.get("last_month_cost", 0) for p in profiles)
418
+
419
+ if total_last == 0:
420
+ return 0.0
421
+
422
+ # Check for period metadata in profiles to detect partial month scenarios
423
+ period_metadata = None
424
+ for profile in profiles:
425
+ if "period_metadata" in profile and profile["period_metadata"]:
426
+ period_metadata = profile["period_metadata"]
427
+ break
428
+
429
+ # Calculate raw percentage change
430
+ raw_change = ((total_current - total_last) / total_last) * 100
431
+
432
+ # If no period metadata available, return raw calculation
433
+ if not period_metadata:
434
+ return raw_change
435
+
436
+ # Extract period information
437
+ current_days = period_metadata.get("current_days", 0)
438
+ previous_days = period_metadata.get("previous_days", 0)
439
+ alignment_strategy = period_metadata.get("period_alignment_strategy", "standard")
440
+ is_partial_comparison = period_metadata.get("is_partial_comparison", False)
441
+ comparison_type = period_metadata.get("comparison_type", "standard_month_comparison")
442
+
443
+ # Apply smart normalization based on period alignment strategy
444
+ if alignment_strategy == "equal_days" and current_days > 0 and previous_days > 0:
445
+ # Equal-day comparison - use raw calculation (already normalized)
446
+ normalized_change = raw_change
447
+ console.log(
448
+ f"[dim cyan]📊 MoM calculation: equal-day comparison ({current_days} vs {previous_days} days) - no adjustment needed[/]"
449
+ )
450
+
451
+ elif is_partial_comparison and current_days > 0 and previous_days > 0:
452
+ # Partial month vs full month - apply normalization factor
453
+ if current_days < previous_days:
454
+ # Current month is partial, previous is full - normalize previous month
455
+ normalization_factor = current_days / previous_days
456
+ adjusted_last_cost = total_last * normalization_factor
457
+ normalized_change = (
458
+ ((total_current - adjusted_last_cost) / adjusted_last_cost) * 100
459
+ if adjusted_last_cost > 0
460
+ else 0.0
461
+ )
462
+
463
+ console.log(
464
+ f"[yellow]⚡ MoM normalization: partial current month ({current_days} days) vs full previous ({previous_days} days)[/]"
465
+ )
466
+ console.log(
467
+ f"[dim yellow] Normalized factor: {normalization_factor:.2f} (adjusted previous: ${adjusted_last_cost:,.2f})[/]"
468
+ )
469
+
470
+ elif current_days > previous_days:
471
+ # Previous month is partial, current is full - normalize current month
472
+ normalization_factor = previous_days / current_days
473
+ adjusted_current_cost = total_current * normalization_factor
474
+ normalized_change = (
475
+ ((adjusted_current_cost - total_last) / total_last) * 100 if total_last > 0 else 0.0
476
+ )
477
+
478
+ console.log(
479
+ f"[yellow]⚡ MoM normalization: full current month ({current_days} days) vs partial previous ({previous_days} days)[/]"
480
+ )
481
+ console.log(
482
+ f"[dim yellow] Normalized factor: {normalization_factor:.2f} (adjusted current: ${adjusted_current_cost:,.2f})[/]"
483
+ )
484
+ else:
485
+ # Equal days but marked as partial comparison - use raw calculation
486
+ normalized_change = raw_change
487
+ console.log(
488
+ f"[cyan]📊 MoM calculation: equal periods ({current_days} days) - using standard calculation[/]"
489
+ )
490
+ else:
491
+ # Standard monthly comparison - use raw calculation
492
+ normalized_change = raw_change
493
+ console.log(f"[dim cyan]📊 MoM calculation: standard monthly comparison - no normalization applied[/]")
494
+
495
+ # Detect and warn about extreme changes that might indicate data issues
496
+ if abs(normalized_change) > 200: # >200% change
497
+ today = date.today()
498
+ days_into_month = today.day
499
+
500
+ if days_into_month <= 5: # Early in month
501
+ console.log(
502
+ f"[yellow]⚠️ Large MoM change ({normalized_change:.1f}%) detected in early month (day {days_into_month})[/]"
503
+ )
504
+ console.log(
505
+ f"[dim yellow] This may be due to limited current month data - consider weekly analysis for accuracy[/]"
506
+ )
507
+ elif comparison_type == "standard_month_comparison" and is_partial_comparison:
508
+ console.log(
509
+ f"[yellow]⚠️ Large MoM change ({normalized_change:.1f}%) with unequal periods detected[/]"
510
+ )
511
+ console.log(
512
+ f"[dim yellow] Consider using equal-day comparison for more accurate trend analysis[/]"
513
+ )
514
+
515
+ return normalized_change
516
+
517
+ except Exception as e:
518
+ # Fallback to simple calculation if enhancement fails
519
+ console.log(f"[red]⚠️ MoM calculation enhancement failed: {str(e)}, using fallback calculation[/]")
520
+ total_current = sum(p.get("total_cost", 0) for p in profiles)
521
+ total_last = sum(p.get("last_month_cost", 0) for p in profiles)
406
522
 
407
- if total_last == 0:
408
- return 0.0
523
+ if total_last == 0:
524
+ return 0.0
409
525
 
410
- return ((total_current - total_last) / total_last) * 100
526
+ return ((total_current - total_last) / total_last) * 100
411
527
 
412
528
  def _get_highest_cost_account(self, profiles: List[Dict[str, Any]]) -> str:
413
529
  """Get the account with highest cost."""
@@ -428,111 +544,133 @@ class MarkdownExporter:
428
544
  def format_vpc_cleanup_table(self, vpc_candidates: List[Any]) -> str:
429
545
  """
430
546
  Format VPC cleanup candidates into 15-column markdown table.
431
-
547
+
432
548
  Args:
433
549
  vpc_candidates: List of VPCCandidate objects from vpc.unified_scenarios
434
-
550
+
435
551
  Returns:
436
552
  Markdown formatted table string with VPC cleanup analysis
437
553
  """
438
554
  if not vpc_candidates:
439
555
  return "⚠️ No VPC candidates to format"
440
-
556
+
441
557
  timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S UTC")
442
-
558
+
443
559
  # Build table header and separator
444
560
  headers = [
445
- "Account_ID", "VPC_ID", "VPC_Name", "CIDR_Block", "Overlapping",
446
- "Is_Default", "ENI_Count", "Tags", "Flow_Logs", "TGW/Peering",
447
- "LBs_Present", "IaC", "Timeline", "Decision", "Owners/Approvals", "Notes"
561
+ "Account_ID",
562
+ "VPC_ID",
563
+ "VPC_Name",
564
+ "CIDR_Block",
565
+ "Overlapping",
566
+ "Is_Default",
567
+ "ENI_Count",
568
+ "Tags",
569
+ "Flow_Logs",
570
+ "TGW/Peering",
571
+ "LBs_Present",
572
+ "IaC",
573
+ "Timeline",
574
+ "Decision",
575
+ "Owners/Approvals",
576
+ "Notes",
448
577
  ]
449
-
578
+
450
579
  markdown_lines = [
451
580
  "# VPC Cleanup Analysis Report",
452
581
  "",
453
582
  f"**Generated**: {timestamp}",
454
583
  f"**Total VPC Candidates**: {len(vpc_candidates)}",
455
- f"**Analysis Source**: CloudOps Runbooks VPC Module v0.9.9",
584
+ f"**Analysis Source**: CloudOps Runbooks VPC Module latest version",
456
585
  "",
457
586
  "## VPC Cleanup Decision Table",
458
587
  "",
459
588
  "| " + " | ".join(headers) + " |",
460
- "| " + " | ".join(["---" for _ in headers]) + " |"
589
+ "| " + " | ".join(["---" for _ in headers]) + " |",
461
590
  ]
462
-
591
+
463
592
  # Process each VPC candidate with enhanced data extraction
464
593
  for candidate in vpc_candidates:
465
594
  # Extract data with safe attribute access and formatting
466
- account_id = getattr(candidate, 'account_id', 'Unknown')
467
- vpc_id = getattr(candidate, 'vpc_id', 'Unknown')
468
- vpc_name = getattr(candidate, 'vpc_name', '') or 'Unnamed'
469
- cidr_block = getattr(candidate, 'cidr_block', 'Unknown')
470
-
595
+ account_id = getattr(candidate, "account_id", "Unknown")
596
+ vpc_id = getattr(candidate, "vpc_id", "Unknown")
597
+ vpc_name = getattr(candidate, "vpc_name", "") or "Unnamed"
598
+ cidr_block = getattr(candidate, "cidr_block", "Unknown")
599
+
471
600
  # Handle overlapping logic - check CIDR conflicts
472
601
  overlapping = self._check_cidr_overlapping(cidr_block, vpc_candidates)
473
-
602
+
474
603
  # Enhanced is_default handling
475
- is_default = getattr(candidate, 'is_default', False)
604
+ is_default = getattr(candidate, "is_default", False)
476
605
  is_default_display = "⚠️ Yes" if is_default else "No"
477
-
606
+
478
607
  # Enhanced ENI count
479
- dependency_analysis = getattr(candidate, 'dependency_analysis', None)
608
+ dependency_analysis = getattr(candidate, "dependency_analysis", None)
480
609
  eni_count = dependency_analysis.eni_count if dependency_analysis else 0
481
-
610
+
482
611
  # Enhanced tags with owner focus
483
- tags_dict = getattr(candidate, 'tags', {}) or {}
612
+ tags_dict = getattr(candidate, "tags", {}) or {}
484
613
  tags_display = self._format_tags_for_owners_display(tags_dict)
485
-
614
+
486
615
  # Flow logs detection
487
616
  flow_logs = self._detect_flow_logs(candidate)
488
-
617
+
489
618
  # TGW/Peering detection
490
619
  tgw_peering = self._detect_tgw_peering(candidate)
491
-
620
+
492
621
  # Load balancers detection
493
622
  lbs_present = self._detect_load_balancers(candidate)
494
-
623
+
495
624
  # IaC detection from tags
496
625
  iac_detected = self._detect_iac_from_tags(tags_dict)
497
-
626
+
498
627
  # Timeline estimation based on VPC state
499
628
  timeline = self._estimate_cleanup_timeline(candidate)
500
-
629
+
501
630
  # Decision based on bucket classification
502
631
  decision = self._determine_cleanup_decision(candidate)
503
-
632
+
504
633
  # Enhanced owners/approvals extraction
505
634
  owners_approvals = self._extract_owners_approvals(tags_dict, is_default)
506
-
635
+
507
636
  # Notes based on VPC characteristics
508
637
  notes = self._generate_vpc_notes(candidate)
509
- overlapping = "Yes" if getattr(candidate, 'overlapping', False) else "No"
510
-
638
+ overlapping = "Yes" if getattr(candidate, "overlapping", False) else "No"
639
+
511
640
  # Format boolean indicators with emoji
512
- is_default = "⚠️ Yes" if getattr(candidate, 'is_default', False) else "✅ No"
513
- flow_logs = "✅ Yes" if getattr(candidate, 'flow_logs_enabled', False) else "❌ No"
514
- tgw_peering = "✅ Yes" if getattr(candidate, 'tgw_peering_attached', False) else "❌ No"
515
- load_balancers = "✅ Yes" if getattr(candidate, 'load_balancers_present', False) else "❌ No"
516
- iac_managed = "✅ Yes" if getattr(candidate, 'iac_managed', False) else "❌ No"
517
-
641
+ is_default = "⚠️ Yes" if getattr(candidate, "is_default", False) else "✅ No"
642
+ flow_logs = "✅ Yes" if getattr(candidate, "flow_logs_enabled", False) else "❌ No"
643
+ tgw_peering = "✅ Yes" if getattr(candidate, "tgw_peering_attached", False) else "❌ No"
644
+ load_balancers = "✅ Yes" if getattr(candidate, "load_balancers_present", False) else "❌ No"
645
+ iac_managed = "✅ Yes" if getattr(candidate, "iac_managed", False) else "❌ No"
646
+
518
647
  # ENI Count handling
519
- eni_count = getattr(candidate, 'eni_count', 0)
520
-
648
+ eni_count = getattr(candidate, "eni_count", 0)
649
+
521
650
  # Tags formatting - prioritize important tags with enhanced display
522
- tags = getattr(candidate, 'tags', {}) or {}
651
+ tags = getattr(candidate, "tags", {}) or {}
523
652
  relevant_tags = []
524
653
  if tags:
525
654
  # Priority order for business-relevant tags
526
- priority_keys = ['Name', 'Environment', 'Project', 'Owner', 'BusinessOwner', 'Team', 'CostCenter', 'Application']
655
+ priority_keys = [
656
+ "Name",
657
+ "Environment",
658
+ "Project",
659
+ "Owner",
660
+ "BusinessOwner",
661
+ "Team",
662
+ "CostCenter",
663
+ "Application",
664
+ ]
527
665
  for key in priority_keys:
528
666
  if key in tags and tags[key] and len(relevant_tags) < 3: # Increased limit for better visibility
529
667
  relevant_tags.append(f"{key}:{tags[key]}")
530
-
668
+
531
669
  # Add other important tags if space available
532
670
  for key, value in tags.items():
533
671
  if key not in priority_keys and value and len(relevant_tags) < 3:
534
672
  relevant_tags.append(f"{key}:{value}")
535
-
673
+
536
674
  # Enhanced display logic for tags
537
675
  if relevant_tags:
538
676
  tags_display = "; ".join(relevant_tags)
@@ -544,146 +682,167 @@ class MarkdownExporter:
544
682
  else:
545
683
  # No tags at all
546
684
  tags_display = "No tags"
547
-
685
+
548
686
  # Timeline and Decision
549
- timeline = getattr(candidate, 'cleanup_timeline', '') or getattr(candidate, 'implementation_timeline', 'Unknown')
550
-
687
+ timeline = getattr(candidate, "cleanup_timeline", "") or getattr(
688
+ candidate, "implementation_timeline", "Unknown"
689
+ )
690
+
551
691
  # Decision handling - check for different decision attribute names
552
- decision_attr = getattr(candidate, 'decision', None)
692
+ decision_attr = getattr(candidate, "decision", None)
553
693
  if decision_attr:
554
- if hasattr(decision_attr, 'value'):
694
+ if hasattr(decision_attr, "value"):
555
695
  decision = decision_attr.value
556
696
  else:
557
697
  decision = str(decision_attr)
558
698
  else:
559
699
  # Fallback decision logic based on risk/dependencies
560
- decision = getattr(candidate, 'cleanup_bucket', 'Unknown')
561
-
700
+ decision = getattr(candidate, "cleanup_bucket", "Unknown")
701
+
562
702
  # Owners/Approvals - Enhanced extraction from tags if not populated
563
- owners = getattr(candidate, 'owners_approvals', []) or getattr(candidate, 'stakeholders', [])
564
-
703
+ owners = getattr(candidate, "owners_approvals", []) or getattr(candidate, "stakeholders", [])
704
+
565
705
  # If no owners found via attributes, try to extract from tags directly
566
706
  if not owners and tags:
567
- owner_keys = ['Owner', 'BusinessOwner', 'TechnicalOwner', 'Team', 'Contact', 'CreatedBy', 'ManagedBy']
707
+ owner_keys = ["Owner", "BusinessOwner", "TechnicalOwner", "Team", "Contact", "CreatedBy", "ManagedBy"]
568
708
  for key in owner_keys:
569
709
  if key in tags and tags[key]:
570
- if 'business' in key.lower() or 'manager' in tags[key].lower():
710
+ if "business" in key.lower() or "manager" in tags[key].lower():
571
711
  owners.append(f"{tags[key]} (Business)")
572
- elif 'technical' in key.lower() or any(tech in tags[key].lower() for tech in ['ops', 'devops', 'engineering']):
712
+ elif "technical" in key.lower() or any(
713
+ tech in tags[key].lower() for tech in ["ops", "devops", "engineering"]
714
+ ):
573
715
  owners.append(f"{tags[key]} (Technical)")
574
716
  else:
575
717
  owners.append(tags[key])
576
718
  break # Take first found owner to avoid clutter
577
-
719
+
578
720
  if owners:
579
721
  owners_display = "; ".join(owners)
580
722
  if len(owners_display) > 30: # Increased width for better display
581
723
  owners_display = owners_display[:27] + "..."
582
724
  else:
583
725
  # Enhanced "unknown" display based on VPC characteristics
584
- if getattr(candidate, 'is_default', False):
726
+ if getattr(candidate, "is_default", False):
585
727
  owners_display = "System Default"
586
- elif getattr(candidate, 'iac_detected', False):
728
+ elif getattr(candidate, "iac_detected", False):
587
729
  owners_display = "IaC Managed"
588
730
  else:
589
731
  owners_display = "No owner tags"
590
-
732
+
591
733
  # Notes - combination of risk assessment and business impact
592
734
  notes_parts = []
593
- risk_level = getattr(candidate, 'risk_level', None)
735
+ risk_level = getattr(candidate, "risk_level", None)
594
736
  if risk_level:
595
- risk_val = risk_level.value if hasattr(risk_level, 'value') else str(risk_level)
737
+ risk_val = risk_level.value if hasattr(risk_level, "value") else str(risk_level)
596
738
  notes_parts.append(f"Risk:{risk_val}")
597
-
598
- business_impact = getattr(candidate, 'business_impact', '')
739
+
740
+ business_impact = getattr(candidate, "business_impact", "")
599
741
  if business_impact:
600
742
  notes_parts.append(business_impact[:15]) # Truncate
601
-
602
- notes = "; ".join(notes_parts) if notes_parts else getattr(candidate, 'notes', 'No notes')
743
+
744
+ notes = "; ".join(notes_parts) if notes_parts else getattr(candidate, "notes", "No notes")
603
745
  if len(notes) > 30: # Truncate for table formatting
604
746
  notes = notes[:27] + "..."
605
-
747
+
606
748
  # Create table row - escape pipes for markdown compatibility
607
749
  row_data = [
608
- account_id, vpc_id, vpc_name, cidr_block, overlapping,
609
- is_default, str(eni_count), tags_display, flow_logs, tgw_peering,
610
- load_balancers, iac_managed, timeline, decision, owners_display, notes
750
+ account_id,
751
+ vpc_id,
752
+ vpc_name,
753
+ cidr_block,
754
+ overlapping,
755
+ is_default,
756
+ str(eni_count),
757
+ tags_display,
758
+ flow_logs,
759
+ tgw_peering,
760
+ load_balancers,
761
+ iac_managed,
762
+ timeline,
763
+ decision,
764
+ owners_display,
765
+ notes,
611
766
  ]
612
-
767
+
613
768
  # Escape pipes and format row
614
769
  escaped_data = [str(cell).replace("|", "\\|") for cell in row_data]
615
770
  markdown_lines.append("| " + " | ".join(escaped_data) + " |")
616
-
771
+
617
772
  # Add summary statistics
618
773
  total_vpcs = len(vpc_candidates)
619
- default_vpcs = sum(1 for c in vpc_candidates if getattr(c, 'is_default', False))
620
- flow_logs_enabled = sum(1 for c in vpc_candidates if getattr(c, 'flow_logs_enabled', False))
621
- iac_managed_count = sum(1 for c in vpc_candidates if getattr(c, 'iac_managed', False))
622
- zero_eni_vpcs = sum(1 for c in vpc_candidates if getattr(c, 'eni_count', 1) == 0)
623
-
624
- markdown_lines.extend([
625
- "",
626
- "## Analysis Summary",
627
- "",
628
- f"- **Total VPCs Analyzed**: {total_vpcs}",
629
- f"- **Default VPCs**: {default_vpcs} ({(default_vpcs/total_vpcs*100):.1f}%)",
630
- f"- **Flow Logs Enabled**: {flow_logs_enabled} ({(flow_logs_enabled/total_vpcs*100):.1f}%)",
631
- f"- **IaC Managed**: {iac_managed_count} ({(iac_managed_count/total_vpcs*100):.1f}%)",
632
- f"- **Zero ENI Attachments**: {zero_eni_vpcs} ({(zero_eni_vpcs/total_vpcs*100):.1f}%)",
633
- "",
634
- "## Cleanup Recommendations",
635
- "",
636
- "1. **Priority 1**: VPCs with zero ENI attachments and no dependencies",
637
- "2. **Priority 2**: Default VPCs with no active resources",
638
- "3. **Priority 3**: Non-IaC managed VPCs requiring manual cleanup",
639
- "4. **Review Required**: VPCs with unclear ownership or business impact",
640
- "",
641
- "---",
642
- f"*Generated by CloudOps Runbooks VPC Module v0.9.9 at {timestamp}*"
643
- ])
644
-
774
+ default_vpcs = sum(1 for c in vpc_candidates if getattr(c, "is_default", False))
775
+ flow_logs_enabled = sum(1 for c in vpc_candidates if getattr(c, "flow_logs_enabled", False))
776
+ iac_managed_count = sum(1 for c in vpc_candidates if getattr(c, "iac_managed", False))
777
+ zero_eni_vpcs = sum(1 for c in vpc_candidates if getattr(c, "eni_count", 1) == 0)
778
+
779
+ markdown_lines.extend(
780
+ [
781
+ "",
782
+ "## Analysis Summary",
783
+ "",
784
+ f"- **Total VPCs Analyzed**: {total_vpcs}",
785
+ f"- **Default VPCs**: {default_vpcs} ({(default_vpcs / total_vpcs * 100):.1f}%)",
786
+ f"- **Flow Logs Enabled**: {flow_logs_enabled} ({(flow_logs_enabled / total_vpcs * 100):.1f}%)",
787
+ f"- **IaC Managed**: {iac_managed_count} ({(iac_managed_count / total_vpcs * 100):.1f}%)",
788
+ f"- **Zero ENI Attachments**: {zero_eni_vpcs} ({(zero_eni_vpcs / total_vpcs * 100):.1f}%)",
789
+ "",
790
+ "## Cleanup Recommendations",
791
+ "",
792
+ "1. **Priority 1**: VPCs with zero ENI attachments and no dependencies",
793
+ "2. **Priority 2**: Default VPCs with no active resources",
794
+ "3. **Priority 3**: Non-IaC managed VPCs requiring manual cleanup",
795
+ "4. **Review Required**: VPCs with unclear ownership or business impact",
796
+ "",
797
+ "---",
798
+ f"*Generated by CloudOps Runbooks VPC Module latest version at {timestamp}*",
799
+ ]
800
+ )
801
+
645
802
  return "\n".join(markdown_lines)
646
803
 
647
- def export_vpc_analysis_to_file(self, vpc_candidates: List[Any], filename: str = None, output_dir: str = "./exports") -> str:
804
+ def export_vpc_analysis_to_file(
805
+ self, vpc_candidates: List[Any], filename: str = None, output_dir: str = "./exports"
806
+ ) -> str:
648
807
  """
649
808
  Export VPC analysis to markdown file with intelligent naming.
650
-
809
+
651
810
  Args:
652
811
  vpc_candidates: List of VPC candidates from analysis
653
812
  filename: Base filename (optional, auto-generated if not provided)
654
813
  output_dir: Output directory path
655
-
814
+
656
815
  Returns:
657
816
  Path to exported file
658
817
  """
659
818
  if not filename:
660
819
  timestamp = datetime.now().strftime("%Y-%m-%d")
661
820
  filename = f"vpc-cleanup-analysis-{timestamp}.md"
662
-
821
+
663
822
  # Ensure .md extension
664
- if not filename.endswith('.md'):
823
+ if not filename.endswith(".md"):
665
824
  filename = f"{filename}.md"
666
-
825
+
667
826
  # Create output directory
668
827
  output_path = Path(output_dir)
669
828
  output_path.mkdir(parents=True, exist_ok=True)
670
-
829
+
671
830
  # Generate markdown content
672
831
  markdown_content = self.format_vpc_cleanup_table(vpc_candidates)
673
-
832
+
674
833
  # Write to file
675
834
  filepath = output_path / filename
676
-
835
+
677
836
  print_info(f"📝 Exporting VPC analysis to: {filename}")
678
-
837
+
679
838
  try:
680
839
  with open(filepath, "w", encoding="utf-8") as f:
681
840
  f.write(markdown_content)
682
-
841
+
683
842
  print_success(f"✅ VPC analysis exported: {filepath}")
684
843
  print_info(f"🔗 Ready for executive review or documentation systems")
685
844
  return str(filepath)
686
-
845
+
687
846
  except Exception as e:
688
847
  print_warning(f"❌ Failed to export VPC analysis: {e}")
689
848
  return ""
@@ -692,26 +851,30 @@ class MarkdownExporter:
692
851
  """Check for CIDR block overlapping across VPCs."""
693
852
  if not cidr_block or not vpc_candidates:
694
853
  return "No"
695
-
854
+
696
855
  # Simple overlapping check - in enterprise scenario, this would use more sophisticated logic
697
856
  current_cidr = cidr_block
698
857
  for candidate in vpc_candidates:
699
- other_cidr = getattr(candidate, 'cidr_block', None)
700
- if other_cidr and other_cidr != current_cidr and current_cidr.startswith(other_cidr.split('/')[0].rsplit('.', 1)[0]):
858
+ other_cidr = getattr(candidate, "cidr_block", None)
859
+ if (
860
+ other_cidr
861
+ and other_cidr != current_cidr
862
+ and current_cidr.startswith(other_cidr.split("/")[0].rsplit(".", 1)[0])
863
+ ):
701
864
  return "Yes"
702
-
865
+
703
866
  return "No"
704
867
 
705
868
  def _detect_flow_logs(self, candidate: Any) -> str:
706
869
  """Detect if VPC has flow logs enabled."""
707
- return "Yes" if getattr(candidate, 'flow_logs_enabled', False) else "No"
870
+ return "Yes" if getattr(candidate, "flow_logs_enabled", False) else "No"
708
871
 
709
872
  def _detect_tgw_peering(self, candidate: Any) -> str:
710
873
  """Analyze Transit Gateway and VPC peering connections."""
711
874
  # Check for TGW attachments and peering connections
712
- tgw_attachments = getattr(candidate, 'tgw_attachments', []) or []
713
- peering_connections = getattr(candidate, 'peering_connections', []) or []
714
-
875
+ tgw_attachments = getattr(candidate, "tgw_attachments", []) or []
876
+ peering_connections = getattr(candidate, "peering_connections", []) or []
877
+
715
878
  if tgw_attachments or peering_connections:
716
879
  connection_count = len(tgw_attachments) + len(peering_connections)
717
880
  return f"Yes ({connection_count})"
@@ -719,12 +882,12 @@ class MarkdownExporter:
719
882
 
720
883
  def _detect_load_balancers(self, candidate: Any) -> str:
721
884
  """Detect load balancers in the VPC."""
722
- load_balancers = getattr(candidate, 'load_balancers', []) or []
885
+ load_balancers = getattr(candidate, "load_balancers", []) or []
723
886
  return "Yes" if load_balancers else "No"
724
887
 
725
888
  def _detect_iac_from_tags(self, tags_dict: dict) -> str:
726
889
  """Detect Infrastructure as Code management from tags."""
727
- iac_keys = ['aws:cloudformation:stack-name', 'terraform:module', 'cdktf:stack', 'pulumi:project']
890
+ iac_keys = ["aws:cloudformation:stack-name", "terraform:module", "cdktf:stack", "pulumi:project"]
728
891
  for key in iac_keys:
729
892
  if key in tags_dict and tags_dict[key]:
730
893
  return "Yes"
@@ -733,26 +896,26 @@ class MarkdownExporter:
733
896
  def _estimate_cleanup_timeline(self, candidate: Any) -> str:
734
897
  """Estimate cleanup timeline based on complexity."""
735
898
  # Simple heuristic based on dependencies
736
- if hasattr(candidate, 'dependency_analysis') and candidate.dependency_analysis:
737
- eni_count = getattr(candidate.dependency_analysis, 'eni_count', 0)
899
+ if hasattr(candidate, "dependency_analysis") and candidate.dependency_analysis:
900
+ eni_count = getattr(candidate.dependency_analysis, "eni_count", 0)
738
901
  else:
739
902
  eni_count = 0
740
-
903
+
741
904
  if eni_count == 0:
742
905
  return "1-2 days"
743
906
  elif eni_count < 5:
744
- return "3-5 days"
907
+ return "3-5 days"
745
908
  else:
746
909
  return "1-2 weeks"
747
910
 
748
911
  def _format_cleanup_decision(self, candidate: Any) -> str:
749
912
  """Format cleanup decision recommendation."""
750
- recommendation = getattr(candidate, 'cleanup_recommendation', 'unknown')
751
- if recommendation == 'delete':
913
+ recommendation = getattr(candidate, "cleanup_recommendation", "unknown")
914
+ if recommendation == "delete":
752
915
  return "Delete"
753
- elif recommendation == 'keep':
916
+ elif recommendation == "keep":
754
917
  return "Keep"
755
- elif recommendation == 'review':
918
+ elif recommendation == "review":
756
919
  return "Review"
757
920
  else:
758
921
  return "TBD"
@@ -761,35 +924,35 @@ class MarkdownExporter:
761
924
  """Format tags for display with priority on ownership information."""
762
925
  if not tags_dict:
763
926
  return "No tags"
764
-
927
+
765
928
  # Priority keys focusing on ownership and approvals
766
- priority_keys = ['Name', 'Owner', 'BusinessOwner', 'TechnicalOwner', 'Team', 'Contact']
929
+ priority_keys = ["Name", "Owner", "BusinessOwner", "TechnicalOwner", "Team", "Contact"]
767
930
  relevant_tags = []
768
-
931
+
769
932
  for key in priority_keys:
770
933
  if key in tags_dict and tags_dict[key]:
771
934
  relevant_tags.append(f"{key}:{tags_dict[key]}")
772
935
  if len(relevant_tags) >= 3: # Limit for table readability
773
936
  break
774
-
937
+
775
938
  return "; ".join(relevant_tags) if relevant_tags else f"({len(tags_dict)} tags)"
776
939
 
777
940
  def _determine_cleanup_decision(self, candidate: Any) -> str:
778
941
  """Determine cleanup decision based on VPC analysis."""
779
942
  # Check the cleanup bucket from three-bucket strategy
780
- cleanup_bucket = getattr(candidate, 'cleanup_bucket', 'unknown')
781
-
782
- if cleanup_bucket == 'bucket_1':
943
+ cleanup_bucket = getattr(candidate, "cleanup_bucket", "unknown")
944
+
945
+ if cleanup_bucket == "bucket_1":
783
946
  return "Delete"
784
- elif cleanup_bucket == 'bucket_2':
947
+ elif cleanup_bucket == "bucket_2":
785
948
  return "Review"
786
- elif cleanup_bucket == 'bucket_3':
949
+ elif cleanup_bucket == "bucket_3":
787
950
  return "Keep"
788
951
  else:
789
952
  # Fallback logic based on other attributes
790
- is_default = getattr(candidate, 'is_default', False)
791
- has_eni = getattr(candidate, 'eni_count', 0) > 0
792
-
953
+ is_default = getattr(candidate, "is_default", False)
954
+ has_eni = getattr(candidate, "eni_count", 0) > 0
955
+
793
956
  if is_default and not has_eni:
794
957
  return "Delete"
795
958
  elif has_eni:
@@ -800,33 +963,33 @@ class MarkdownExporter:
800
963
  def _extract_owners_approvals(self, tags_dict: dict, is_default: bool) -> str:
801
964
  """Extract owners and approval information from tags and VPC status."""
802
965
  # Extract from tags with enhanced owner detection
803
- owner_keys = ['Owner', 'BusinessOwner', 'TechnicalOwner', 'Team', 'Contact', 'CreatedBy', 'ManagedBy']
804
-
966
+ owner_keys = ["Owner", "BusinessOwner", "TechnicalOwner", "Team", "Contact", "CreatedBy", "ManagedBy"]
967
+
805
968
  extracted_owners = []
806
969
  for key in owner_keys:
807
970
  if key in tags_dict and tags_dict[key]:
808
971
  value = tags_dict[key]
809
- if 'business' in key.lower():
972
+ if "business" in key.lower():
810
973
  extracted_owners.append(f"{value} (Business)")
811
- elif 'technical' in key.lower():
974
+ elif "technical" in key.lower():
812
975
  extracted_owners.append(f"{value} (Technical)")
813
- elif 'team' in key.lower():
976
+ elif "team" in key.lower():
814
977
  extracted_owners.append(f"{value} (Team)")
815
978
  else:
816
979
  extracted_owners.append(f"{value} ({key})")
817
-
980
+
818
981
  if len(extracted_owners) >= 2: # Limit for table readability
819
982
  break
820
-
983
+
821
984
  if extracted_owners:
822
985
  return "; ".join(extracted_owners)
823
-
986
+
824
987
  # Fallback based on VPC type
825
988
  if is_default:
826
989
  return "System Default VPC"
827
990
  else:
828
991
  # Check for IaC tags
829
- iac_keys = ['aws:cloudformation:stack-name', 'terraform:module', 'cdktf:stack', 'pulumi:project']
992
+ iac_keys = ["aws:cloudformation:stack-name", "terraform:module", "cdktf:stack", "pulumi:project"]
830
993
  for key in iac_keys:
831
994
  if key in tags_dict and tags_dict[key]:
832
995
  return "IaC Managed"
@@ -835,35 +998,35 @@ class MarkdownExporter:
835
998
  def _generate_vpc_notes(self, candidate: Any) -> str:
836
999
  """Generate comprehensive notes for VPC candidate."""
837
1000
  notes = []
838
-
1001
+
839
1002
  # Add bucket classification note
840
- cleanup_bucket = getattr(candidate, 'cleanup_bucket', 'unknown')
841
- if cleanup_bucket == 'bucket_1':
1003
+ cleanup_bucket = getattr(candidate, "cleanup_bucket", "unknown")
1004
+ if cleanup_bucket == "bucket_1":
842
1005
  notes.append("Internal data plane - safe for cleanup")
843
- elif cleanup_bucket == 'bucket_2':
1006
+ elif cleanup_bucket == "bucket_2":
844
1007
  notes.append("External interconnects - requires analysis")
845
- elif cleanup_bucket == 'bucket_3':
1008
+ elif cleanup_bucket == "bucket_3":
846
1009
  notes.append("Control plane - manual review required")
847
-
1010
+
848
1011
  # Add ENI count if significant
849
- if hasattr(candidate, 'dependency_analysis') and candidate.dependency_analysis:
850
- eni_count = getattr(candidate.dependency_analysis, 'eni_count', 0)
1012
+ if hasattr(candidate, "dependency_analysis") and candidate.dependency_analysis:
1013
+ eni_count = getattr(candidate.dependency_analysis, "eni_count", 0)
851
1014
  if eni_count > 0:
852
1015
  notes.append(f"{eni_count} ENI attachments")
853
-
1016
+
854
1017
  # Add default VPC note
855
- if getattr(candidate, 'is_default', False):
1018
+ if getattr(candidate, "is_default", False):
856
1019
  notes.append("Default VPC (CIS compliance issue)")
857
-
1020
+
858
1021
  # Add IaC detection
859
- if getattr(candidate, 'iac_detected', False):
1022
+ if getattr(candidate, "iac_detected", False):
860
1023
  notes.append("IaC managed")
861
-
1024
+
862
1025
  # Add security concerns
863
- risk_level = getattr(candidate, 'risk_level', 'unknown')
864
- if risk_level == 'high':
1026
+ risk_level = getattr(candidate, "risk_level", "unknown")
1027
+ if risk_level == "high":
865
1028
  notes.append("High security risk")
866
-
1029
+
867
1030
  return "; ".join(notes) if notes else "Standard VPC cleanup candidate"
868
1031
 
869
1032