runbooks 1.1.4__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.
- runbooks/__init__.py +31 -2
- runbooks/__init___optimized.py +18 -4
- runbooks/_platform/__init__.py +1 -5
- runbooks/_platform/core/runbooks_wrapper.py +141 -138
- runbooks/aws2/accuracy_validator.py +812 -0
- runbooks/base.py +7 -0
- runbooks/cfat/assessment/compliance.py +1 -1
- runbooks/cfat/assessment/runner.py +1 -0
- runbooks/cfat/cloud_foundations_assessment.py +227 -239
- runbooks/cli/__init__.py +1 -1
- runbooks/cli/commands/cfat.py +64 -23
- runbooks/cli/commands/finops.py +1005 -54
- runbooks/cli/commands/inventory.py +138 -35
- runbooks/cli/commands/operate.py +9 -36
- runbooks/cli/commands/security.py +42 -18
- runbooks/cli/commands/validation.py +432 -18
- runbooks/cli/commands/vpc.py +81 -17
- runbooks/cli/registry.py +22 -10
- runbooks/cloudops/__init__.py +20 -27
- runbooks/cloudops/base.py +96 -107
- runbooks/cloudops/cost_optimizer.py +544 -542
- runbooks/cloudops/infrastructure_optimizer.py +5 -4
- runbooks/cloudops/interfaces.py +224 -225
- runbooks/cloudops/lifecycle_manager.py +5 -4
- runbooks/cloudops/mcp_cost_validation.py +252 -235
- runbooks/cloudops/models.py +78 -53
- runbooks/cloudops/monitoring_automation.py +5 -4
- runbooks/cloudops/notebook_framework.py +177 -213
- runbooks/cloudops/security_enforcer.py +125 -159
- runbooks/common/accuracy_validator.py +11 -0
- runbooks/common/aws_pricing.py +349 -326
- runbooks/common/aws_pricing_api.py +211 -212
- runbooks/common/aws_profile_manager.py +40 -36
- runbooks/common/aws_utils.py +74 -79
- runbooks/common/business_logic.py +126 -104
- runbooks/common/cli_decorators.py +36 -60
- runbooks/common/comprehensive_cost_explorer_integration.py +455 -463
- runbooks/common/cross_account_manager.py +197 -204
- runbooks/common/date_utils.py +27 -39
- runbooks/common/decorators.py +29 -19
- runbooks/common/dry_run_examples.py +173 -208
- runbooks/common/dry_run_framework.py +157 -155
- runbooks/common/enhanced_exception_handler.py +15 -4
- runbooks/common/enhanced_logging_example.py +50 -64
- runbooks/common/enhanced_logging_integration_example.py +65 -37
- runbooks/common/env_utils.py +16 -16
- runbooks/common/error_handling.py +40 -38
- runbooks/common/lazy_loader.py +41 -23
- runbooks/common/logging_integration_helper.py +79 -86
- runbooks/common/mcp_cost_explorer_integration.py +476 -493
- runbooks/common/mcp_integration.py +63 -74
- runbooks/common/memory_optimization.py +140 -118
- runbooks/common/module_cli_base.py +37 -58
- runbooks/common/organizations_client.py +175 -193
- runbooks/common/patterns.py +23 -25
- runbooks/common/performance_monitoring.py +67 -71
- runbooks/common/performance_optimization_engine.py +283 -274
- runbooks/common/profile_utils.py +111 -37
- runbooks/common/rich_utils.py +201 -141
- runbooks/common/sre_performance_suite.py +177 -186
- runbooks/enterprise/__init__.py +1 -1
- runbooks/enterprise/logging.py +144 -106
- runbooks/enterprise/security.py +187 -204
- runbooks/enterprise/validation.py +43 -56
- runbooks/finops/__init__.py +26 -30
- runbooks/finops/account_resolver.py +1 -1
- runbooks/finops/advanced_optimization_engine.py +980 -0
- runbooks/finops/automation_core.py +268 -231
- runbooks/finops/business_case_config.py +184 -179
- runbooks/finops/cli.py +660 -139
- runbooks/finops/commvault_ec2_analysis.py +157 -164
- runbooks/finops/compute_cost_optimizer.py +336 -320
- runbooks/finops/config.py +20 -20
- runbooks/finops/cost_optimizer.py +484 -618
- runbooks/finops/cost_processor.py +332 -214
- runbooks/finops/dashboard_runner.py +1006 -172
- runbooks/finops/ebs_cost_optimizer.py +991 -657
- runbooks/finops/elastic_ip_optimizer.py +317 -257
- runbooks/finops/enhanced_mcp_integration.py +340 -0
- runbooks/finops/enhanced_progress.py +32 -29
- runbooks/finops/enhanced_trend_visualization.py +3 -2
- runbooks/finops/enterprise_wrappers.py +223 -285
- runbooks/finops/executive_export.py +203 -160
- runbooks/finops/helpers.py +130 -288
- runbooks/finops/iam_guidance.py +1 -1
- runbooks/finops/infrastructure/__init__.py +80 -0
- runbooks/finops/infrastructure/commands.py +506 -0
- runbooks/finops/infrastructure/load_balancer_optimizer.py +866 -0
- runbooks/finops/infrastructure/vpc_endpoint_optimizer.py +832 -0
- runbooks/finops/markdown_exporter.py +337 -174
- runbooks/finops/mcp_validator.py +1952 -0
- runbooks/finops/nat_gateway_optimizer.py +1512 -481
- runbooks/finops/network_cost_optimizer.py +657 -587
- runbooks/finops/notebook_utils.py +226 -188
- runbooks/finops/optimization_engine.py +1136 -0
- runbooks/finops/optimizer.py +19 -23
- runbooks/finops/rds_snapshot_optimizer.py +367 -411
- runbooks/finops/reservation_optimizer.py +427 -363
- runbooks/finops/scenario_cli_integration.py +64 -65
- runbooks/finops/scenarios.py +1277 -438
- runbooks/finops/schemas.py +218 -182
- runbooks/finops/snapshot_manager.py +2289 -0
- runbooks/finops/types.py +3 -3
- runbooks/finops/validation_framework.py +259 -265
- runbooks/finops/vpc_cleanup_exporter.py +189 -144
- runbooks/finops/vpc_cleanup_optimizer.py +591 -573
- runbooks/finops/workspaces_analyzer.py +171 -182
- runbooks/integration/__init__.py +89 -0
- runbooks/integration/mcp_integration.py +1920 -0
- runbooks/inventory/CLAUDE.md +816 -0
- runbooks/inventory/__init__.py +2 -2
- runbooks/inventory/cloud_foundations_integration.py +144 -149
- runbooks/inventory/collectors/aws_comprehensive.py +1 -1
- runbooks/inventory/collectors/aws_networking.py +109 -99
- runbooks/inventory/collectors/base.py +4 -0
- runbooks/inventory/core/collector.py +495 -313
- runbooks/inventory/drift_detection_cli.py +69 -96
- runbooks/inventory/inventory_mcp_cli.py +48 -46
- runbooks/inventory/list_rds_snapshots_aggregator.py +192 -208
- runbooks/inventory/mcp_inventory_validator.py +549 -465
- runbooks/inventory/mcp_vpc_validator.py +359 -442
- runbooks/inventory/organizations_discovery.py +55 -51
- runbooks/inventory/rich_inventory_display.py +33 -32
- runbooks/inventory/unified_validation_engine.py +278 -251
- runbooks/inventory/vpc_analyzer.py +732 -695
- runbooks/inventory/vpc_architecture_validator.py +293 -348
- runbooks/inventory/vpc_dependency_analyzer.py +382 -378
- runbooks/inventory/vpc_flow_analyzer.py +1 -1
- runbooks/main.py +49 -34
- runbooks/main_final.py +91 -60
- runbooks/main_minimal.py +22 -10
- runbooks/main_optimized.py +131 -100
- runbooks/main_ultra_minimal.py +7 -2
- runbooks/mcp/__init__.py +36 -0
- runbooks/mcp/integration.py +679 -0
- runbooks/monitoring/performance_monitor.py +9 -4
- runbooks/operate/dynamodb_operations.py +3 -1
- runbooks/operate/ec2_operations.py +145 -137
- runbooks/operate/iam_operations.py +146 -152
- runbooks/operate/networking_cost_heatmap.py +29 -8
- runbooks/operate/rds_operations.py +223 -254
- runbooks/operate/s3_operations.py +107 -118
- runbooks/operate/vpc_operations.py +646 -616
- runbooks/remediation/base.py +1 -1
- runbooks/remediation/commons.py +10 -7
- runbooks/remediation/commvault_ec2_analysis.py +70 -66
- runbooks/remediation/ec2_unattached_ebs_volumes.py +1 -0
- runbooks/remediation/multi_account.py +24 -21
- runbooks/remediation/rds_snapshot_list.py +86 -60
- runbooks/remediation/remediation_cli.py +92 -146
- runbooks/remediation/universal_account_discovery.py +83 -79
- runbooks/remediation/workspaces_list.py +46 -41
- runbooks/security/__init__.py +19 -0
- runbooks/security/assessment_runner.py +1150 -0
- runbooks/security/baseline_checker.py +812 -0
- runbooks/security/cloudops_automation_security_validator.py +509 -535
- runbooks/security/compliance_automation_engine.py +17 -17
- runbooks/security/config/__init__.py +2 -2
- runbooks/security/config/compliance_config.py +50 -50
- runbooks/security/config_template_generator.py +63 -76
- runbooks/security/enterprise_security_framework.py +1 -1
- runbooks/security/executive_security_dashboard.py +519 -508
- runbooks/security/multi_account_security_controls.py +959 -1210
- runbooks/security/real_time_security_monitor.py +422 -444
- runbooks/security/security_baseline_tester.py +1 -1
- runbooks/security/security_cli.py +143 -112
- runbooks/security/test_2way_validation.py +439 -0
- runbooks/security/two_way_validation_framework.py +852 -0
- runbooks/sre/production_monitoring_framework.py +167 -177
- runbooks/tdd/__init__.py +15 -0
- runbooks/tdd/cli.py +1071 -0
- runbooks/utils/__init__.py +14 -17
- runbooks/utils/logger.py +7 -2
- runbooks/utils/version_validator.py +50 -47
- runbooks/validation/__init__.py +6 -6
- runbooks/validation/cli.py +9 -3
- runbooks/validation/comprehensive_2way_validator.py +745 -704
- runbooks/validation/mcp_validator.py +906 -228
- runbooks/validation/terraform_citations_validator.py +104 -115
- runbooks/validation/terraform_drift_detector.py +447 -451
- runbooks/vpc/README.md +617 -0
- runbooks/vpc/__init__.py +8 -1
- runbooks/vpc/analyzer.py +577 -0
- runbooks/vpc/cleanup_wrapper.py +476 -413
- runbooks/vpc/cli_cloudtrail_commands.py +339 -0
- runbooks/vpc/cli_mcp_validation_commands.py +480 -0
- runbooks/vpc/cloudtrail_audit_integration.py +717 -0
- runbooks/vpc/config.py +92 -97
- runbooks/vpc/cost_engine.py +411 -148
- runbooks/vpc/cost_explorer_integration.py +553 -0
- runbooks/vpc/cross_account_session.py +101 -106
- runbooks/vpc/enhanced_mcp_validation.py +917 -0
- runbooks/vpc/eni_gate_validator.py +961 -0
- runbooks/vpc/heatmap_engine.py +185 -160
- runbooks/vpc/mcp_no_eni_validator.py +680 -639
- runbooks/vpc/nat_gateway_optimizer.py +358 -0
- runbooks/vpc/networking_wrapper.py +15 -8
- runbooks/vpc/pdca_remediation_planner.py +528 -0
- runbooks/vpc/performance_optimized_analyzer.py +219 -231
- runbooks/vpc/runbooks_adapter.py +1167 -241
- runbooks/vpc/tdd_red_phase_stubs.py +601 -0
- runbooks/vpc/test_data_loader.py +358 -0
- runbooks/vpc/tests/conftest.py +314 -4
- runbooks/vpc/tests/test_cleanup_framework.py +1022 -0
- runbooks/vpc/tests/test_cost_engine.py +0 -2
- runbooks/vpc/topology_generator.py +326 -0
- runbooks/vpc/unified_scenarios.py +1297 -1124
- runbooks/vpc/vpc_cleanup_integration.py +1943 -1115
- runbooks-1.1.5.dist-info/METADATA +328 -0
- {runbooks-1.1.4.dist-info → runbooks-1.1.5.dist-info}/RECORD +214 -193
- runbooks/finops/README.md +0 -414
- runbooks/finops/accuracy_cross_validator.py +0 -647
- runbooks/finops/business_cases.py +0 -950
- runbooks/finops/dashboard_router.py +0 -922
- runbooks/finops/ebs_optimizer.py +0 -973
- runbooks/finops/embedded_mcp_validator.py +0 -1629
- runbooks/finops/enhanced_dashboard_runner.py +0 -527
- runbooks/finops/finops_dashboard.py +0 -584
- runbooks/finops/finops_scenarios.py +0 -1218
- runbooks/finops/legacy_migration.py +0 -730
- runbooks/finops/multi_dashboard.py +0 -1519
- runbooks/finops/single_dashboard.py +0 -1113
- runbooks/finops/unlimited_scenarios.py +0 -393
- runbooks-1.1.4.dist-info/METADATA +0 -800
- {runbooks-1.1.4.dist-info → runbooks-1.1.5.dist-info}/WHEEL +0 -0
- {runbooks-1.1.4.dist-info → runbooks-1.1.5.dist-info}/entry_points.txt +0 -0
- {runbooks-1.1.4.dist-info → runbooks-1.1.5.dist-info}/licenses/LICENSE +0 -0
- {runbooks-1.1.4.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
|
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
|
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
|
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
|
-
"""
|
404
|
-
|
405
|
-
|
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
|
-
|
408
|
-
|
523
|
+
if total_last == 0:
|
524
|
+
return 0.0
|
409
525
|
|
410
|
-
|
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,25 +544,38 @@ 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",
|
446
|
-
"
|
447
|
-
"
|
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
|
"",
|
@@ -457,82 +586,91 @@ class MarkdownExporter:
|
|
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,
|
467
|
-
vpc_id = getattr(candidate,
|
468
|
-
vpc_name = getattr(candidate,
|
469
|
-
cidr_block = getattr(candidate,
|
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,
|
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,
|
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,
|
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,
|
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,
|
513
|
-
flow_logs = "✅ Yes" if getattr(candidate,
|
514
|
-
tgw_peering = "✅ Yes" if getattr(candidate,
|
515
|
-
load_balancers = "✅ Yes" if getattr(candidate,
|
516
|
-
iac_managed = "✅ Yes" if getattr(candidate,
|
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,
|
520
|
-
|
648
|
+
eni_count = getattr(candidate, "eni_count", 0)
|
649
|
+
|
521
650
|
# Tags formatting - prioritize important tags with enhanced display
|
522
|
-
tags = getattr(candidate,
|
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 = [
|
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,
|
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,
|
692
|
+
decision_attr = getattr(candidate, "decision", None)
|
553
693
|
if decision_attr:
|
554
|
-
if hasattr(decision_attr,
|
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,
|
561
|
-
|
700
|
+
decision = getattr(candidate, "cleanup_bucket", "Unknown")
|
701
|
+
|
562
702
|
# Owners/Approvals - Enhanced extraction from tags if not populated
|
563
|
-
owners = getattr(candidate,
|
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 = [
|
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
|
710
|
+
if "business" in key.lower() or "manager" in tags[key].lower():
|
571
711
|
owners.append(f"{tags[key]} (Business)")
|
572
|
-
elif
|
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,
|
726
|
+
if getattr(candidate, "is_default", False):
|
585
727
|
owners_display = "System Default"
|
586
|
-
elif getattr(candidate,
|
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,
|
735
|
+
risk_level = getattr(candidate, "risk_level", None)
|
594
736
|
if risk_level:
|
595
|
-
risk_val = risk_level.value if hasattr(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,
|
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,
|
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,
|
609
|
-
|
610
|
-
|
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,
|
620
|
-
flow_logs_enabled = sum(1 for c in vpc_candidates if getattr(c,
|
621
|
-
iac_managed_count = sum(1 for c in vpc_candidates if getattr(c,
|
622
|
-
zero_eni_vpcs = sum(1 for c in vpc_candidates if getattr(c,
|
623
|
-
|
624
|
-
markdown_lines.extend(
|
625
|
-
|
626
|
-
|
627
|
-
|
628
|
-
|
629
|
-
|
630
|
-
|
631
|
-
|
632
|
-
|
633
|
-
|
634
|
-
|
635
|
-
|
636
|
-
|
637
|
-
|
638
|
-
|
639
|
-
|
640
|
-
|
641
|
-
|
642
|
-
|
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(
|
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(
|
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,
|
700
|
-
if
|
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,
|
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,
|
713
|
-
peering_connections = getattr(candidate,
|
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,
|
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 = [
|
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,
|
737
|
-
eni_count = getattr(candidate.dependency_analysis,
|
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,
|
751
|
-
if recommendation ==
|
913
|
+
recommendation = getattr(candidate, "cleanup_recommendation", "unknown")
|
914
|
+
if recommendation == "delete":
|
752
915
|
return "Delete"
|
753
|
-
elif recommendation ==
|
916
|
+
elif recommendation == "keep":
|
754
917
|
return "Keep"
|
755
|
-
elif recommendation ==
|
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 = [
|
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,
|
781
|
-
|
782
|
-
if cleanup_bucket ==
|
943
|
+
cleanup_bucket = getattr(candidate, "cleanup_bucket", "unknown")
|
944
|
+
|
945
|
+
if cleanup_bucket == "bucket_1":
|
783
946
|
return "Delete"
|
784
|
-
elif cleanup_bucket ==
|
947
|
+
elif cleanup_bucket == "bucket_2":
|
785
948
|
return "Review"
|
786
|
-
elif cleanup_bucket ==
|
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,
|
791
|
-
has_eni = getattr(candidate,
|
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 = [
|
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
|
972
|
+
if "business" in key.lower():
|
810
973
|
extracted_owners.append(f"{value} (Business)")
|
811
|
-
elif
|
974
|
+
elif "technical" in key.lower():
|
812
975
|
extracted_owners.append(f"{value} (Technical)")
|
813
|
-
elif
|
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 = [
|
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,
|
841
|
-
if cleanup_bucket ==
|
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 ==
|
1006
|
+
elif cleanup_bucket == "bucket_2":
|
844
1007
|
notes.append("External interconnects - requires analysis")
|
845
|
-
elif cleanup_bucket ==
|
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,
|
850
|
-
eni_count = getattr(candidate.dependency_analysis,
|
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,
|
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,
|
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,
|
864
|
-
if risk_level ==
|
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
|
|