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.
- runbooks/cfat/cloud_foundations_assessment.py +626 -0
- runbooks/cloudops/cost_optimizer.py +95 -33
- runbooks/common/aws_pricing.py +388 -0
- runbooks/common/aws_pricing_api.py +205 -0
- runbooks/common/aws_utils.py +2 -2
- runbooks/common/comprehensive_cost_explorer_integration.py +979 -0
- runbooks/common/cross_account_manager.py +606 -0
- runbooks/common/enhanced_exception_handler.py +4 -0
- runbooks/common/env_utils.py +96 -0
- runbooks/common/mcp_integration.py +49 -2
- runbooks/common/organizations_client.py +579 -0
- runbooks/common/profile_utils.py +96 -2
- runbooks/finops/cost_optimizer.py +2 -1
- runbooks/finops/elastic_ip_optimizer.py +13 -9
- runbooks/finops/embedded_mcp_validator.py +31 -0
- runbooks/finops/enhanced_trend_visualization.py +3 -2
- runbooks/finops/markdown_exporter.py +217 -2
- runbooks/finops/nat_gateway_optimizer.py +57 -20
- runbooks/finops/vpc_cleanup_exporter.py +28 -26
- runbooks/finops/vpc_cleanup_optimizer.py +370 -16
- runbooks/inventory/__init__.py +10 -1
- runbooks/inventory/cloud_foundations_integration.py +409 -0
- runbooks/inventory/core/collector.py +1148 -88
- runbooks/inventory/discovery.md +389 -0
- runbooks/inventory/drift_detection_cli.py +327 -0
- runbooks/inventory/inventory_mcp_cli.py +171 -0
- runbooks/inventory/inventory_modules.py +4 -7
- runbooks/inventory/mcp_inventory_validator.py +2149 -0
- runbooks/inventory/mcp_vpc_validator.py +23 -6
- runbooks/inventory/organizations_discovery.py +91 -1
- runbooks/inventory/rich_inventory_display.py +129 -1
- runbooks/inventory/unified_validation_engine.py +1292 -0
- runbooks/inventory/verify_ec2_security_groups.py +3 -1
- runbooks/inventory/vpc_analyzer.py +825 -7
- runbooks/inventory/vpc_flow_analyzer.py +36 -42
- runbooks/main.py +654 -35
- runbooks/monitoring/performance_monitor.py +11 -7
- runbooks/operate/dynamodb_operations.py +6 -5
- runbooks/operate/ec2_operations.py +3 -2
- runbooks/operate/networking_cost_heatmap.py +4 -3
- runbooks/operate/s3_operations.py +13 -12
- runbooks/operate/vpc_operations.py +49 -1
- runbooks/remediation/base.py +1 -1
- runbooks/remediation/commvault_ec2_analysis.py +6 -1
- runbooks/remediation/ec2_unattached_ebs_volumes.py +6 -3
- runbooks/remediation/rds_snapshot_list.py +5 -3
- runbooks/validation/__init__.py +21 -1
- runbooks/validation/comprehensive_2way_validator.py +1996 -0
- runbooks/validation/mcp_validator.py +904 -94
- runbooks/validation/terraform_citations_validator.py +363 -0
- runbooks/validation/terraform_drift_detector.py +1098 -0
- runbooks/vpc/cleanup_wrapper.py +231 -10
- runbooks/vpc/config.py +310 -62
- runbooks/vpc/cross_account_session.py +308 -0
- runbooks/vpc/heatmap_engine.py +96 -29
- runbooks/vpc/manager_interface.py +9 -9
- runbooks/vpc/mcp_no_eni_validator.py +1551 -0
- runbooks/vpc/networking_wrapper.py +14 -8
- runbooks/vpc/runbooks.inventory.organizations_discovery.log +0 -0
- runbooks/vpc/runbooks.security.report_generator.log +0 -0
- runbooks/vpc/runbooks.security.run_script.log +0 -0
- runbooks/vpc/runbooks.security.security_export.log +0 -0
- runbooks/vpc/tests/test_cost_engine.py +1 -1
- runbooks/vpc/unified_scenarios.py +73 -3
- runbooks/vpc/vpc_cleanup_integration.py +512 -78
- {runbooks-0.9.9.dist-info → runbooks-1.0.0.dist-info}/METADATA +94 -52
- {runbooks-0.9.9.dist-info → runbooks-1.0.0.dist-info}/RECORD +71 -49
- {runbooks-0.9.9.dist-info → runbooks-1.0.0.dist-info}/WHEEL +0 -0
- {runbooks-0.9.9.dist-info → runbooks-1.0.0.dist-info}/entry_points.txt +0 -0
- {runbooks-0.9.9.dist-info → runbooks-1.0.0.dist-info}/licenses/LICENSE +0 -0
- {runbooks-0.9.9.dist-info → runbooks-1.0.0.dist-info}/top_level.txt +0 -0
@@ -19,20 +19,28 @@ from .markdown_exporter import MarkdownExporter
|
|
19
19
|
|
20
20
|
|
21
21
|
def _format_tags_for_display(tags_dict: Dict[str, str]) -> str:
|
22
|
-
"""Format tags for display with priority order."""
|
22
|
+
"""Format tags for display with priority order, emphasizing ownership tags."""
|
23
23
|
if not tags_dict:
|
24
24
|
return "No tags"
|
25
25
|
|
26
|
-
|
26
|
+
# Enhanced priority keys with focus on ownership and approvals
|
27
|
+
priority_keys = ['Name', 'Owner', 'BusinessOwner', 'TechnicalOwner', 'Team', 'Contact',
|
28
|
+
'Environment', 'Project', 'CostCenter', 'CreatedBy', 'ManagedBy']
|
27
29
|
relevant_tags = []
|
28
30
|
|
29
31
|
for key in priority_keys:
|
30
32
|
if key in tags_dict and tags_dict[key]:
|
31
33
|
relevant_tags.append(f"{key}:{tags_dict[key]}")
|
32
34
|
|
35
|
+
# Add CloudFormation/Terraform tags for IaC detection
|
36
|
+
iac_keys = ['aws:cloudformation:stack-name', 'terraform:module', 'cdktf:stack', 'pulumi:project']
|
37
|
+
for key in iac_keys:
|
38
|
+
if key in tags_dict and tags_dict[key] and len(relevant_tags) < 6:
|
39
|
+
relevant_tags.append(f"IaC:{tags_dict[key]}")
|
40
|
+
|
33
41
|
# Add other important tags
|
34
42
|
for key, value in tags_dict.items():
|
35
|
-
if key not in priority_keys and value and len(relevant_tags) < 5:
|
43
|
+
if key not in priority_keys + iac_keys and value and len(relevant_tags) < 5:
|
36
44
|
relevant_tags.append(f"{key}:{value}")
|
37
45
|
|
38
46
|
return "; ".join(relevant_tags) if relevant_tags else f"({len(tags_dict)} tags)"
|
@@ -118,40 +126,34 @@ def _export_vpc_candidates_csv(vpc_candidates: List[Any], output_dir: str) -> st
|
|
118
126
|
# Extract data with enhanced tag and owner handling
|
119
127
|
tags_dict = getattr(candidate, 'tags', {}) or {}
|
120
128
|
|
121
|
-
#
|
122
|
-
|
123
|
-
priority_keys = ['Name', 'Environment', 'Project', 'Owner', 'BusinessOwner', 'Team']
|
124
|
-
relevant_tags = []
|
125
|
-
for key in priority_keys:
|
126
|
-
if key in tags_dict and tags_dict[key]:
|
127
|
-
relevant_tags.append(f"{key}:{tags_dict[key]}")
|
128
|
-
|
129
|
-
# Add other important tags
|
130
|
-
for key, value in tags_dict.items():
|
131
|
-
if key not in priority_keys and value and len(relevant_tags) < 5:
|
132
|
-
relevant_tags.append(f"{key}:{value}")
|
133
|
-
|
134
|
-
tags_str = "; ".join(relevant_tags)
|
135
|
-
else:
|
136
|
-
tags_str = "No tags"
|
129
|
+
# Use enhanced tag formatting function
|
130
|
+
tags_str = _format_tags_for_display(tags_dict)
|
137
131
|
|
138
132
|
load_balancers = getattr(candidate, 'load_balancers', []) or []
|
139
133
|
lbs_present = "Yes" if load_balancers else "No"
|
140
134
|
|
141
|
-
# Enhanced owner extraction
|
135
|
+
# Enhanced owner extraction from multiple sources
|
142
136
|
owners = getattr(candidate, 'owners_approvals', []) or []
|
143
137
|
|
144
|
-
#
|
138
|
+
# Extract owners from tags with enhanced logic
|
145
139
|
if not owners and tags_dict:
|
146
140
|
owner_keys = ['Owner', 'BusinessOwner', 'TechnicalOwner', 'Team', 'Contact', 'CreatedBy', 'ManagedBy']
|
147
141
|
for key in owner_keys:
|
148
142
|
if key in tags_dict and tags_dict[key]:
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
143
|
+
value = tags_dict[key]
|
144
|
+
if 'business' in key.lower() or 'manager' in value.lower():
|
145
|
+
owners.append(f"{value} (Business)")
|
146
|
+
elif 'technical' in key.lower() or 'engineer' in value.lower():
|
147
|
+
owners.append(f"{value} (Technical)")
|
148
|
+
elif 'team' in key.lower():
|
149
|
+
owners.append(f"{value} (Team)")
|
153
150
|
else:
|
154
|
-
owners.append(
|
151
|
+
owners.append(f"{value} ({key})")
|
152
|
+
|
153
|
+
# For default VPCs, add system indicator
|
154
|
+
is_default = getattr(candidate, 'is_default', False)
|
155
|
+
if is_default and not owners:
|
156
|
+
owners.append("System Default")
|
155
157
|
|
156
158
|
if owners:
|
157
159
|
owners_str = "; ".join(owners)
|
@@ -47,9 +47,11 @@ from ..common.rich_utils import (
|
|
47
47
|
console, print_header, print_success, print_error, print_warning, print_info,
|
48
48
|
create_table, create_progress_bar, format_cost, create_panel, STATUS_INDICATORS
|
49
49
|
)
|
50
|
+
from ..common.aws_pricing import DynamicAWSPricing
|
50
51
|
from .embedded_mcp_validator import EmbeddedMCPValidator
|
51
52
|
from ..common.profile_utils import get_profile_for_operation
|
52
53
|
from ..security.enterprise_security_framework import EnterpriseSecurityFramework
|
54
|
+
from ..vpc.mcp_no_eni_validator import NOENIVPCMCPValidator, DynamicDiscoveryResults
|
53
55
|
|
54
56
|
logger = logging.getLogger(__name__)
|
55
57
|
|
@@ -364,22 +366,76 @@ class VPCCleanupOptimizer:
|
|
364
366
|
"""
|
365
367
|
Enhanced VPC discovery with organization account context.
|
366
368
|
|
367
|
-
CRITICAL
|
368
|
-
|
369
|
-
|
369
|
+
CRITICAL FIX: This method now attempts to discover VPCs across multiple accounts
|
370
|
+
by trying different access patterns:
|
371
|
+
1. Direct access with current profile
|
372
|
+
2. Cross-account role assumption (if available)
|
373
|
+
3. Aggregation from multiple AWS SSO profiles
|
370
374
|
"""
|
371
375
|
vpc_candidates = []
|
376
|
+
total_accounts_checked = 0
|
377
|
+
accounts_with_vpcs = set()
|
372
378
|
|
373
379
|
if progress_callback:
|
374
|
-
progress_callback("Discovering VPCs across
|
380
|
+
progress_callback(f"Discovering VPCs across {len(account_ids)} organization accounts...")
|
375
381
|
|
376
382
|
# Get list of all regions
|
377
383
|
ec2_client = self.session.client('ec2', region_name='us-east-1')
|
378
384
|
regions = [region['RegionName'] for region in ec2_client.describe_regions()['Regions']]
|
379
385
|
|
380
|
-
print_info(f"🌍 Scanning {len(regions)} AWS regions
|
386
|
+
print_info(f"🌍 Scanning {len(regions)} AWS regions across {len(account_ids)} accounts...")
|
381
387
|
|
388
|
+
# First, discover VPCs in current profile's accessible accounts
|
389
|
+
current_account_vpcs = self._discover_vpcs_current_profile(regions, progress_callback)
|
390
|
+
vpc_candidates.extend(current_account_vpcs)
|
391
|
+
|
392
|
+
# Extract unique account IDs from discovered VPCs
|
393
|
+
for vpc in current_account_vpcs:
|
394
|
+
if vpc.account_id:
|
395
|
+
accounts_with_vpcs.add(vpc.account_id)
|
396
|
+
|
397
|
+
# Attempt cross-account discovery for remaining accounts
|
398
|
+
remaining_accounts = [acc for acc in account_ids if acc not in accounts_with_vpcs]
|
399
|
+
|
400
|
+
if remaining_accounts:
|
401
|
+
print_info(f"🔄 Attempting cross-account discovery for {len(remaining_accounts)} additional accounts...")
|
402
|
+
|
403
|
+
# Try different access patterns for remaining accounts
|
404
|
+
for account_id in remaining_accounts[:10]: # Limit to first 10 for performance
|
405
|
+
total_accounts_checked += 1
|
406
|
+
account_name = accounts_info.get(account_id, {}).get('name', 'Unknown')
|
407
|
+
|
408
|
+
if progress_callback:
|
409
|
+
progress_callback(f"Checking account {account_name} ({account_id[:12]}...)")
|
410
|
+
|
411
|
+
# Attempt cross-account access
|
412
|
+
cross_account_vpcs = self._attempt_cross_account_discovery(
|
413
|
+
account_id, account_name, regions
|
414
|
+
)
|
415
|
+
|
416
|
+
if cross_account_vpcs:
|
417
|
+
vpc_candidates.extend(cross_account_vpcs)
|
418
|
+
accounts_with_vpcs.add(account_id)
|
419
|
+
print_success(f" ✅ Found {len(cross_account_vpcs)} VPCs in {account_name}")
|
420
|
+
|
421
|
+
# Summary
|
422
|
+
print_success(f"✅ Discovered {len(vpc_candidates)} total VPCs across {len(accounts_with_vpcs)} accounts")
|
423
|
+
print_info(f"📊 Organization scope: {len(account_ids)} accounts, {total_accounts_checked} checked, {len(accounts_with_vpcs)} with VPCs")
|
424
|
+
|
425
|
+
# If we still have < 13 VPCs, provide guidance
|
426
|
+
if len(vpc_candidates) < 13:
|
427
|
+
print_warning(f"⚠️ Only {len(vpc_candidates)} VPCs found (target: ≥13). Consider:")
|
428
|
+
print_info(" 1. Using MANAGEMENT_PROFILE with broader cross-account access")
|
429
|
+
print_info(" 2. Configuring cross-account roles for VPC discovery")
|
430
|
+
print_info(" 3. Running discovery from each account individually")
|
431
|
+
|
432
|
+
return vpc_candidates
|
433
|
+
|
434
|
+
def _discover_vpcs_current_profile(self, regions: List[str], progress_callback=None) -> List[VPCCleanupCandidate]:
|
435
|
+
"""Discover VPCs accessible with current profile."""
|
436
|
+
vpc_candidates = []
|
382
437
|
regions_with_vpcs = 0
|
438
|
+
|
383
439
|
for region in regions:
|
384
440
|
try:
|
385
441
|
regional_ec2 = self.session.client('ec2', region_name=region)
|
@@ -409,13 +465,64 @@ class VPCCleanupOptimizer:
|
|
409
465
|
regions_with_vpcs += 1
|
410
466
|
|
411
467
|
except ClientError as e:
|
412
|
-
|
413
|
-
|
414
|
-
else:
|
415
|
-
print_warning(f"Could not access region {region}: {e}")
|
468
|
+
# Silently skip regions with no access
|
469
|
+
pass
|
416
470
|
|
417
|
-
|
418
|
-
|
471
|
+
return vpc_candidates
|
472
|
+
|
473
|
+
def _attempt_cross_account_discovery(self, account_id: str, account_name: str,
|
474
|
+
regions: List[str]) -> List[VPCCleanupCandidate]:
|
475
|
+
"""Attempt to discover VPCs in a specific account using cross-account access."""
|
476
|
+
vpc_candidates = []
|
477
|
+
|
478
|
+
# Try to assume a cross-account role (if configured)
|
479
|
+
role_name = "OrganizationAccountAccessRole" # Standard AWS Organizations role
|
480
|
+
|
481
|
+
try:
|
482
|
+
# Attempt to assume role in target account
|
483
|
+
sts_client = self.session.client('sts')
|
484
|
+
assumed_role = sts_client.assume_role(
|
485
|
+
RoleArn=f"arn:aws:iam::{account_id}:role/{role_name}",
|
486
|
+
RoleSessionName=f"VPCDiscovery-{account_id[:12]}"
|
487
|
+
)
|
488
|
+
|
489
|
+
# Create session with assumed role credentials
|
490
|
+
assumed_session = boto3.Session(
|
491
|
+
aws_access_key_id=assumed_role['Credentials']['AccessKeyId'],
|
492
|
+
aws_secret_access_key=assumed_role['Credentials']['SecretAccessKey'],
|
493
|
+
aws_session_token=assumed_role['Credentials']['SessionToken']
|
494
|
+
)
|
495
|
+
|
496
|
+
# Discover VPCs in target account
|
497
|
+
for region in regions[:3]: # Check first 3 regions for performance
|
498
|
+
try:
|
499
|
+
ec2_client = assumed_session.client('ec2', region_name=region)
|
500
|
+
response = ec2_client.describe_vpcs()
|
501
|
+
|
502
|
+
for vpc in response['Vpcs']:
|
503
|
+
candidate = VPCCleanupCandidate(
|
504
|
+
vpc_id=vpc['VpcId'],
|
505
|
+
region=region,
|
506
|
+
state=vpc['State'],
|
507
|
+
cidr_block=vpc['CidrBlock'],
|
508
|
+
is_default=vpc.get('IsDefault', False),
|
509
|
+
account_id=account_id, # Set explicit account ID
|
510
|
+
dependency_analysis=VPCDependencyAnalysis(
|
511
|
+
vpc_id=vpc['VpcId'],
|
512
|
+
region=region,
|
513
|
+
is_default_vpc=vpc.get('IsDefault', False)
|
514
|
+
),
|
515
|
+
tags={tag['Key']: tag['Value'] for tag in vpc.get('Tags', [])}
|
516
|
+
)
|
517
|
+
vpc_candidates.append(candidate)
|
518
|
+
|
519
|
+
except Exception:
|
520
|
+
# Skip regions with access issues
|
521
|
+
pass
|
522
|
+
|
523
|
+
except Exception as e:
|
524
|
+
# Cross-account access not available - this is expected for most profiles
|
525
|
+
pass
|
419
526
|
|
420
527
|
return vpc_candidates
|
421
528
|
|
@@ -482,6 +589,244 @@ class VPCCleanupOptimizer:
|
|
482
589
|
print_success(f"✅ Discovered {len(vpc_candidates)} VPC candidates across {len(regions)} regions")
|
483
590
|
return vpc_candidates
|
484
591
|
|
592
|
+
async def discover_no_eni_vpcs_with_mcp_validation(self,
|
593
|
+
target_regions: List[str] = None,
|
594
|
+
max_concurrent_accounts: int = 10) -> Tuple[List[VPCCleanupCandidate], DynamicDiscoveryResults]:
|
595
|
+
"""
|
596
|
+
Discover NO-ENI VPCs across all AWS accounts using real-time MCP validation.
|
597
|
+
|
598
|
+
This method integrates with the dynamic MCP validator to discover the actual
|
599
|
+
count of NO-ENI VPCs across all accessible accounts, not hardcoded numbers.
|
600
|
+
|
601
|
+
Args:
|
602
|
+
target_regions: List of regions to scan (default: ['ap-southeast-2'])
|
603
|
+
max_concurrent_accounts: Maximum concurrent account scans
|
604
|
+
|
605
|
+
Returns:
|
606
|
+
Tuple of (VPC cleanup candidates, Dynamic discovery results)
|
607
|
+
"""
|
608
|
+
if target_regions is None:
|
609
|
+
target_regions = ['ap-southeast-2']
|
610
|
+
|
611
|
+
print_header("🌐 Real-Time NO-ENI VPC Discovery", "MCP-Validated VPC Cleanup Analysis")
|
612
|
+
|
613
|
+
# Configure enterprise profiles for MCP validation - Universal compatibility
|
614
|
+
from runbooks.common.profile_utils import get_enterprise_profile_mapping
|
615
|
+
enterprise_profiles = get_enterprise_profile_mapping()
|
616
|
+
|
617
|
+
# Override with current profile if available
|
618
|
+
current_profile_type = self._determine_profile_type(self.profile)
|
619
|
+
if current_profile_type:
|
620
|
+
enterprise_profiles[current_profile_type] = self.profile
|
621
|
+
|
622
|
+
# Initialize MCP validator for dynamic discovery
|
623
|
+
print_info("🔧 Initializing dynamic MCP validator...")
|
624
|
+
mcp_validator = NOENIVPCMCPValidator(enterprise_profiles)
|
625
|
+
|
626
|
+
# Perform dynamic discovery across all accounts
|
627
|
+
print_info("🚀 Starting real-time discovery across all AWS accounts...")
|
628
|
+
discovery_results = await mcp_validator.discover_all_no_eni_vpcs_dynamically(
|
629
|
+
target_regions=target_regions,
|
630
|
+
max_concurrent_accounts=max_concurrent_accounts
|
631
|
+
)
|
632
|
+
|
633
|
+
# Convert MCP discovery results to VPC cleanup candidates
|
634
|
+
print_info("🔄 Converting MCP results to VPC cleanup candidates...")
|
635
|
+
cleanup_candidates = []
|
636
|
+
|
637
|
+
for target in discovery_results.account_region_results:
|
638
|
+
if not target.has_access or not target.no_eni_vpcs:
|
639
|
+
continue
|
640
|
+
|
641
|
+
# Get detailed VPC information for each NO-ENI VPC
|
642
|
+
try:
|
643
|
+
# Use appropriate session for this account
|
644
|
+
session = self._get_session_for_account(target.account_id, enterprise_profiles)
|
645
|
+
ec2_client = session.client('ec2', region_name=target.region)
|
646
|
+
|
647
|
+
# Get VPC details
|
648
|
+
vpc_response = ec2_client.describe_vpcs(VpcIds=target.no_eni_vpcs)
|
649
|
+
|
650
|
+
for vpc in vpc_response.get('Vpcs', []):
|
651
|
+
# Create comprehensive dependency analysis
|
652
|
+
dependency_analysis = await self._create_dependency_analysis_for_vpc(
|
653
|
+
vpc['VpcId'], target.region, ec2_client
|
654
|
+
)
|
655
|
+
|
656
|
+
# Create VPC cleanup candidate
|
657
|
+
candidate = VPCCleanupCandidate(
|
658
|
+
vpc_id=vpc['VpcId'],
|
659
|
+
region=target.region,
|
660
|
+
state=vpc.get('State', 'unknown'),
|
661
|
+
cidr_block=vpc.get('CidrBlock', ''),
|
662
|
+
is_default=vpc.get('IsDefault', False),
|
663
|
+
dependency_analysis=dependency_analysis,
|
664
|
+
cleanup_bucket='internal', # NO-ENI VPCs go to internal bucket
|
665
|
+
monthly_cost=self._estimate_vpc_monthly_cost(vpc),
|
666
|
+
annual_savings=self._estimate_vpc_monthly_cost(vpc) * 12,
|
667
|
+
cleanup_recommendation='ready' if dependency_analysis.eni_count == 0 else 'investigate',
|
668
|
+
risk_assessment='low' if dependency_analysis.eni_count == 0 else 'medium',
|
669
|
+
business_impact='minimal',
|
670
|
+
tags=self._extract_vpc_tags(vpc),
|
671
|
+
account_id=target.account_id,
|
672
|
+
flow_logs_enabled=await self._check_flow_logs_enabled(vpc['VpcId'], ec2_client)
|
673
|
+
)
|
674
|
+
|
675
|
+
cleanup_candidates.append(candidate)
|
676
|
+
|
677
|
+
except Exception as e:
|
678
|
+
print_warning(f"Failed to analyze VPC details for account {target.account_id}: {e}")
|
679
|
+
continue
|
680
|
+
|
681
|
+
# Display integration results
|
682
|
+
print_header("🎯 MCP-Validated VPC Cleanup Summary", "Real-Time Integration Results")
|
683
|
+
console.print(f"[bold green]✅ NO-ENI VPCs discovered: {len(cleanup_candidates)}[/bold green]")
|
684
|
+
console.print(f"[bold blue]📊 Accounts scanned: {discovery_results.total_accounts_scanned}[/bold blue]")
|
685
|
+
console.print(f"[bold yellow]🌍 Regions scanned: {discovery_results.total_regions_scanned}[/bold yellow]")
|
686
|
+
console.print(f"[bold magenta]🧪 MCP validation accuracy: {discovery_results.mcp_validation_accuracy:.2f}%[/bold magenta]")
|
687
|
+
|
688
|
+
# Calculate potential savings
|
689
|
+
total_annual_savings = sum(candidate.annual_savings for candidate in cleanup_candidates)
|
690
|
+
console.print(f"[bold cyan]💰 Potential annual savings: {format_cost(total_annual_savings)}[/bold cyan]")
|
691
|
+
|
692
|
+
# Validation status
|
693
|
+
if discovery_results.mcp_validation_accuracy >= 99.5:
|
694
|
+
print_success(f"✅ ENTERPRISE VALIDATION PASSED: {discovery_results.mcp_validation_accuracy:.2f}% accuracy")
|
695
|
+
else:
|
696
|
+
print_warning(f"⚠️ VALIDATION REVIEW REQUIRED: {discovery_results.mcp_validation_accuracy:.2f}% accuracy")
|
697
|
+
|
698
|
+
return cleanup_candidates, discovery_results
|
699
|
+
|
700
|
+
def _determine_profile_type(self, profile_name: str) -> Optional[str]:
|
701
|
+
"""Determine profile type from profile name for universal compatibility."""
|
702
|
+
# Universal pattern matching for any AWS profile naming convention
|
703
|
+
if 'billing' in profile_name.lower() or 'Billing' in profile_name:
|
704
|
+
return 'BILLING'
|
705
|
+
elif 'management' in profile_name.lower() or 'admin' in profile_name.lower():
|
706
|
+
return 'MANAGEMENT'
|
707
|
+
elif 'ops' in profile_name.lower() or 'operational' in profile_name.lower():
|
708
|
+
return 'CENTRALISED_OPS'
|
709
|
+
return None
|
710
|
+
|
711
|
+
def _get_session_for_account(self, account_id: str, enterprise_profiles: Dict[str, str]) -> boto3.Session:
|
712
|
+
"""Get appropriate session for accessing a specific account."""
|
713
|
+
# In enterprise setup, would assume role here
|
714
|
+
# For now, return session with best available profile
|
715
|
+
|
716
|
+
# Priority order for account access
|
717
|
+
profile_priority = ['MANAGEMENT', 'CENTRALISED_OPS', 'BILLING']
|
718
|
+
|
719
|
+
for profile_type in profile_priority:
|
720
|
+
if profile_type in enterprise_profiles:
|
721
|
+
try:
|
722
|
+
session = boto3.Session(profile_name=enterprise_profiles[profile_type])
|
723
|
+
# Verify access
|
724
|
+
sts_client = session.client('sts')
|
725
|
+
identity = sts_client.get_caller_identity()
|
726
|
+
|
727
|
+
if identity['Account'] == account_id:
|
728
|
+
return session
|
729
|
+
except Exception:
|
730
|
+
continue
|
731
|
+
|
732
|
+
# Fallback to current session
|
733
|
+
return self.session
|
734
|
+
|
735
|
+
async def _create_dependency_analysis_for_vpc(self,
|
736
|
+
vpc_id: str,
|
737
|
+
region: str,
|
738
|
+
ec2_client) -> VPCDependencyAnalysis:
|
739
|
+
"""Create comprehensive dependency analysis for a VPC."""
|
740
|
+
try:
|
741
|
+
# Get ENI count
|
742
|
+
eni_response = ec2_client.describe_network_interfaces(
|
743
|
+
Filters=[{'Name': 'vpc-id', 'Values': [vpc_id]}]
|
744
|
+
)
|
745
|
+
eni_count = len(eni_response.get('NetworkInterfaces', []))
|
746
|
+
|
747
|
+
# Get route tables
|
748
|
+
rt_response = ec2_client.describe_route_tables(
|
749
|
+
Filters=[{'Name': 'vpc-id', 'Values': [vpc_id]}]
|
750
|
+
)
|
751
|
+
route_tables = [rt['RouteTableId'] for rt in rt_response.get('RouteTables', [])]
|
752
|
+
|
753
|
+
# Get security groups
|
754
|
+
sg_response = ec2_client.describe_security_groups(
|
755
|
+
Filters=[{'Name': 'vpc-id', 'Values': [vpc_id]}]
|
756
|
+
)
|
757
|
+
security_groups = [sg['GroupId'] for sg in sg_response.get('SecurityGroups', [])]
|
758
|
+
|
759
|
+
# Get internet gateways
|
760
|
+
igw_response = ec2_client.describe_internet_gateways(
|
761
|
+
Filters=[{'Name': 'attachment.vpc-id', 'Values': [vpc_id]}]
|
762
|
+
)
|
763
|
+
internet_gateways = [igw['InternetGatewayId'] for igw in igw_response.get('InternetGateways', [])]
|
764
|
+
|
765
|
+
# Get NAT gateways
|
766
|
+
nat_response = ec2_client.describe_nat_gateways(
|
767
|
+
Filters=[{'Name': 'vpc-id', 'Values': [vpc_id]}]
|
768
|
+
)
|
769
|
+
nat_gateways = [nat['NatGatewayId'] for nat in nat_response.get('NatGateways', [])]
|
770
|
+
|
771
|
+
# Determine risk level
|
772
|
+
if eni_count == 0 and len(nat_gateways) == 0 and len(internet_gateways) <= 1:
|
773
|
+
risk_level = 'low'
|
774
|
+
elif eni_count == 0:
|
775
|
+
risk_level = 'medium'
|
776
|
+
else:
|
777
|
+
risk_level = 'high'
|
778
|
+
|
779
|
+
return VPCDependencyAnalysis(
|
780
|
+
vpc_id=vpc_id,
|
781
|
+
region=region,
|
782
|
+
eni_count=eni_count,
|
783
|
+
route_tables=route_tables,
|
784
|
+
security_groups=security_groups,
|
785
|
+
internet_gateways=internet_gateways,
|
786
|
+
nat_gateways=nat_gateways,
|
787
|
+
dependency_risk_level=risk_level
|
788
|
+
)
|
789
|
+
|
790
|
+
except Exception as e:
|
791
|
+
print_warning(f"Failed to analyze dependencies for {vpc_id}: {e}")
|
792
|
+
return VPCDependencyAnalysis(
|
793
|
+
vpc_id=vpc_id,
|
794
|
+
region=region,
|
795
|
+
dependency_risk_level='unknown'
|
796
|
+
)
|
797
|
+
|
798
|
+
def _estimate_vpc_monthly_cost(self, vpc: Dict[str, Any]) -> float:
|
799
|
+
"""Estimate monthly cost for VPC resources."""
|
800
|
+
# Base VPC cost estimation (simplified)
|
801
|
+
# In enterprise setup, would integrate with Cost Explorer
|
802
|
+
base_cost = 0.0
|
803
|
+
|
804
|
+
# Default VPCs might have default resources
|
805
|
+
if vpc.get('IsDefault', False):
|
806
|
+
base_cost += 5.0 # Estimated monthly cost for default VPC resources
|
807
|
+
|
808
|
+
return base_cost
|
809
|
+
|
810
|
+
def _extract_vpc_tags(self, vpc: Dict[str, Any]) -> Dict[str, str]:
|
811
|
+
"""Extract tags from VPC data."""
|
812
|
+
tags = {}
|
813
|
+
for tag in vpc.get('Tags', []):
|
814
|
+
tags[tag['Key']] = tag['Value']
|
815
|
+
return tags
|
816
|
+
|
817
|
+
async def _check_flow_logs_enabled(self, vpc_id: str, ec2_client) -> bool:
|
818
|
+
"""Check if VPC Flow Logs are enabled."""
|
819
|
+
try:
|
820
|
+
response = ec2_client.describe_flow_logs(
|
821
|
+
Filters=[
|
822
|
+
{'Name': 'resource-id', 'Values': [vpc_id]},
|
823
|
+
{'Name': 'resource-type', 'Values': ['VPC']}
|
824
|
+
]
|
825
|
+
)
|
826
|
+
return len(response.get('FlowLogs', [])) > 0
|
827
|
+
except Exception:
|
828
|
+
return False
|
829
|
+
|
485
830
|
def _analyze_vpc_dependencies(self, candidates: List[VPCCleanupCandidate]) -> List[VPCCleanupCandidate]:
|
486
831
|
"""Analyze VPC dependencies for cleanup safety assessment."""
|
487
832
|
print_info("🔍 Analyzing VPC dependencies for safety assessment...")
|
@@ -972,12 +1317,21 @@ class VPCCleanupOptimizer:
|
|
972
1317
|
"""Calculate VPC cleanup costs and savings estimation."""
|
973
1318
|
print_info("💰 Calculating VPC cleanup costs and savings...")
|
974
1319
|
|
975
|
-
#
|
976
|
-
#
|
1320
|
+
# Dynamic VPC cost calculation (enterprise compliance)
|
1321
|
+
# Using dynamic pricing engine for accurate regional costs
|
1322
|
+
pricing_engine = DynamicAWSPricing()
|
1323
|
+
default_region = 'us-east-1' # Default region for cost estimation
|
1324
|
+
|
977
1325
|
monthly_vpc_base_cost = 0.0 # VPCs themselves are free
|
978
|
-
|
979
|
-
|
980
|
-
|
1326
|
+
nat_result = pricing_engine.get_service_pricing('nat_gateway', default_region)
|
1327
|
+
monthly_nat_gateway_cost = nat_result.monthly_cost
|
1328
|
+
|
1329
|
+
endpoint_result = pricing_engine.get_service_pricing('vpc_endpoint', default_region)
|
1330
|
+
monthly_vpc_endpoint_cost = endpoint_result.monthly_cost
|
1331
|
+
|
1332
|
+
# Data processing costs vary by usage - using conservative estimate per region
|
1333
|
+
regional_multiplier = pricing_engine.regional_multipliers.get(default_region, 1.0)
|
1334
|
+
monthly_data_processing_cost = 50.0 * regional_multiplier # Base estimate adjusted for region
|
981
1335
|
|
982
1336
|
total_annual_savings = 0.0
|
983
1337
|
cost_details = {}
|
runbooks/inventory/__init__.py
CHANGED
@@ -26,6 +26,11 @@ from runbooks.inventory.collectors.base import BaseResourceCollector
|
|
26
26
|
from runbooks.inventory.core.collector import InventoryCollector
|
27
27
|
from runbooks.inventory.core.formatter import InventoryFormatter
|
28
28
|
|
29
|
+
# Enhanced collector integrated into core collector module
|
30
|
+
from runbooks.inventory.core.collector import (
|
31
|
+
EnhancedInventoryCollector,
|
32
|
+
)
|
33
|
+
|
29
34
|
# Data models
|
30
35
|
from runbooks.inventory.models.account import AWSAccount, OrganizationAccount
|
31
36
|
from runbooks.inventory.models.inventory import InventoryMetadata, InventoryResult
|
@@ -38,13 +43,17 @@ from runbooks.inventory.utils.validation import validate_aws_account_id, validat
|
|
38
43
|
# VPC Module Migration Integration
|
39
44
|
from runbooks.inventory.vpc_analyzer import VPCAnalyzer, VPCDiscoveryResult, AWSOAnalysis
|
40
45
|
|
46
|
+
# Note: EnhancedInventoryCollector now imported above from core.collector
|
47
|
+
|
41
48
|
# Import centralized version from main runbooks package
|
42
49
|
from runbooks import __version__
|
43
50
|
|
44
51
|
__all__ = [
|
45
52
|
# Core functionality
|
46
53
|
"InventoryCollector",
|
47
|
-
"InventoryFormatter",
|
54
|
+
"InventoryFormatter",
|
55
|
+
# Enhanced functionality with proven finops patterns
|
56
|
+
"EnhancedInventoryCollector",
|
48
57
|
# Base classes for extension
|
49
58
|
"BaseResourceCollector",
|
50
59
|
# Data models
|