runbooks 1.1.4__py3-none-any.whl → 1.1.6__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 +135 -91
- 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 +17 -12
- 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 +99 -79
- 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 +315 -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/aws_decorators.py +2 -3
- runbooks/inventory/check_cloudtrail_compliance.py +2 -4
- runbooks/inventory/check_controltower_readiness.py +152 -151
- runbooks/inventory/check_landingzone_readiness.py +85 -84
- 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/core/formatter.py +11 -0
- runbooks/inventory/draw_org_structure.py +8 -9
- runbooks/inventory/drift_detection_cli.py +69 -96
- runbooks/inventory/ec2_vpc_utils.py +2 -2
- runbooks/inventory/find_cfn_drift_detection.py +5 -7
- runbooks/inventory/find_cfn_orphaned_stacks.py +7 -9
- runbooks/inventory/find_cfn_stackset_drift.py +5 -6
- runbooks/inventory/find_ec2_security_groups.py +48 -42
- runbooks/inventory/find_landingzone_versions.py +4 -6
- runbooks/inventory/find_vpc_flow_logs.py +7 -9
- runbooks/inventory/inventory_mcp_cli.py +48 -46
- runbooks/inventory/inventory_modules.py +103 -91
- runbooks/inventory/list_cfn_stacks.py +9 -10
- runbooks/inventory/list_cfn_stackset_operation_results.py +1 -3
- runbooks/inventory/list_cfn_stackset_operations.py +79 -57
- runbooks/inventory/list_cfn_stacksets.py +8 -10
- runbooks/inventory/list_config_recorders_delivery_channels.py +49 -39
- runbooks/inventory/list_ds_directories.py +65 -53
- runbooks/inventory/list_ec2_availability_zones.py +2 -4
- runbooks/inventory/list_ec2_ebs_volumes.py +32 -35
- runbooks/inventory/list_ec2_instances.py +23 -28
- runbooks/inventory/list_ecs_clusters_and_tasks.py +26 -34
- runbooks/inventory/list_elbs_load_balancers.py +22 -20
- runbooks/inventory/list_enis_network_interfaces.py +26 -33
- runbooks/inventory/list_guardduty_detectors.py +2 -4
- runbooks/inventory/list_iam_policies.py +2 -4
- runbooks/inventory/list_iam_roles.py +5 -7
- runbooks/inventory/list_iam_saml_providers.py +4 -6
- runbooks/inventory/list_lambda_functions.py +38 -38
- runbooks/inventory/list_org_accounts.py +6 -8
- runbooks/inventory/list_org_accounts_users.py +55 -44
- runbooks/inventory/list_rds_db_instances.py +31 -33
- runbooks/inventory/list_rds_snapshots_aggregator.py +192 -208
- runbooks/inventory/list_route53_hosted_zones.py +3 -5
- runbooks/inventory/list_servicecatalog_provisioned_products.py +37 -41
- runbooks/inventory/list_sns_topics.py +2 -4
- runbooks/inventory/list_ssm_parameters.py +4 -7
- runbooks/inventory/list_vpc_subnets.py +2 -4
- runbooks/inventory/list_vpcs.py +7 -10
- runbooks/inventory/mcp_inventory_validator.py +554 -468
- runbooks/inventory/mcp_vpc_validator.py +359 -442
- runbooks/inventory/organizations_discovery.py +63 -55
- runbooks/inventory/recover_cfn_stack_ids.py +7 -8
- runbooks/inventory/requirements.txt +0 -1
- runbooks/inventory/rich_inventory_display.py +35 -34
- runbooks/inventory/run_on_multi_accounts.py +3 -5
- runbooks/inventory/unified_validation_engine.py +281 -253
- runbooks/inventory/verify_ec2_security_groups.py +1 -1
- runbooks/inventory/vpc_analyzer.py +735 -697
- runbooks/inventory/vpc_architecture_validator.py +293 -348
- runbooks/inventory/vpc_dependency_analyzer.py +384 -380
- 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 +461 -454
- 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.6.dist-info/METADATA +327 -0
- runbooks-1.1.6.dist-info/RECORD +489 -0
- 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/RECORD +0 -468
- {runbooks-1.1.4.dist-info → runbooks-1.1.6.dist-info}/WHEEL +0 -0
- {runbooks-1.1.4.dist-info → runbooks-1.1.6.dist-info}/entry_points.txt +0 -0
- {runbooks-1.1.4.dist-info → runbooks-1.1.6.dist-info}/licenses/LICENSE +0 -0
- {runbooks-1.1.4.dist-info → runbooks-1.1.6.dist-info}/top_level.txt +0 -0
@@ -34,9 +34,9 @@ import asyncio
|
|
34
34
|
|
35
35
|
import boto3
|
36
36
|
from botocore.exceptions import ClientError
|
37
|
-
from rich.console import Console
|
38
37
|
from rich.panel import Panel
|
39
|
-
from rich.progress import
|
38
|
+
from rich.progress import SpinnerColumn, TextColumn
|
39
|
+
from runbooks.common.rich_utils import Progress
|
40
40
|
from rich.table import Table
|
41
41
|
from rich.tree import Tree
|
42
42
|
|
@@ -44,6 +44,7 @@ from runbooks.common.profile_utils import create_operational_session
|
|
44
44
|
from runbooks.common.cross_account_manager import EnhancedCrossAccountManager, CrossAccountSession
|
45
45
|
from runbooks.common.organizations_client import OrganizationAccount, get_unified_organizations_client
|
46
46
|
from runbooks.common.rich_utils import (
|
47
|
+
Console,
|
47
48
|
console,
|
48
49
|
print_header,
|
49
50
|
print_success,
|
@@ -52,7 +53,7 @@ from runbooks.common.rich_utils import (
|
|
52
53
|
print_info,
|
53
54
|
create_table,
|
54
55
|
create_progress_bar,
|
55
|
-
format_cost
|
56
|
+
format_cost,
|
56
57
|
)
|
57
58
|
|
58
59
|
logger = logging.getLogger(__name__)
|
@@ -61,6 +62,7 @@ logger = logging.getLogger(__name__)
|
|
61
62
|
@dataclass
|
62
63
|
class VPCDiscoveryResult:
|
63
64
|
"""Results from VPC discovery operations"""
|
65
|
+
|
64
66
|
vpcs: List[Dict[str, Any]]
|
65
67
|
nat_gateways: List[Dict[str, Any]]
|
66
68
|
vpc_endpoints: List[Dict[str, Any]]
|
@@ -80,6 +82,7 @@ class VPCDiscoveryResult:
|
|
80
82
|
@dataclass
|
81
83
|
class AWSOAnalysis:
|
82
84
|
"""AWSO-05 specific analysis results"""
|
85
|
+
|
83
86
|
default_vpcs: List[Dict[str, Any]]
|
84
87
|
orphaned_resources: List[Dict[str, Any]]
|
85
88
|
dependency_chain: Dict[str, List[str]]
|
@@ -91,7 +94,7 @@ class AWSOAnalysis:
|
|
91
94
|
class VPCAnalyzer:
|
92
95
|
"""
|
93
96
|
Enterprise VPC Discovery and Analysis Engine
|
94
|
-
|
97
|
+
|
95
98
|
Migrated from VPC module with enhanced capabilities:
|
96
99
|
- Complete VPC topology discovery
|
97
100
|
- AWSO-05 cleanup support with 12-step dependency analysis
|
@@ -106,13 +109,13 @@ class VPCAnalyzer:
|
|
106
109
|
region: Optional[str] = "us-east-1",
|
107
110
|
console: Optional[Console] = None,
|
108
111
|
dry_run: bool = True,
|
109
|
-
excluded_accounts: Optional[List[str]] = None, # Enhanced: Decommissioned accounts filtering
|
112
|
+
excluded_accounts: Optional[List[str]] = None, # Enhanced: Decommissioned accounts filtering
|
110
113
|
enable_multi_account: bool = False, # Enhanced: Multi-Organization Landing Zone mode
|
111
|
-
max_workers: int = 10 # Enhanced: Parallel processing for 60-account operations
|
114
|
+
max_workers: int = 10, # Enhanced: Parallel processing for 60-account operations
|
112
115
|
):
|
113
116
|
"""
|
114
117
|
Initialize VPC Analyzer with enterprise profile management and 60-account Landing Zone support
|
115
|
-
|
118
|
+
|
116
119
|
Args:
|
117
120
|
profile: AWS profile name (3-tier priority: User > Environment > Default)
|
118
121
|
region: AWS region for analysis (defaults to us-east-1)
|
@@ -127,189 +130,186 @@ class VPCAnalyzer:
|
|
127
130
|
self.console = console or Console()
|
128
131
|
self.dry_run = dry_run
|
129
132
|
self.max_workers = max_workers
|
130
|
-
|
133
|
+
|
131
134
|
# Decommissioned account filtering (default: account 294618320542)
|
132
135
|
self.excluded_accounts = excluded_accounts or ["294618320542"]
|
133
136
|
self.enable_multi_account = enable_multi_account
|
134
|
-
|
137
|
+
|
135
138
|
# Initialize AWS session using enterprise profile management
|
136
139
|
self.session = None
|
137
140
|
if profile:
|
138
141
|
try:
|
139
|
-
self.session = create_operational_session(
|
142
|
+
self.session = create_operational_session(profile_name=profile)
|
140
143
|
print_success(f"Connected to AWS profile: {profile}")
|
141
144
|
except Exception as e:
|
142
145
|
print_error(f"Failed to connect to AWS: {e}")
|
143
|
-
|
146
|
+
|
144
147
|
# NEW: Initialize Enhanced Cross-Account Manager for 60-account operations
|
145
148
|
self.cross_account_manager = None
|
146
149
|
if enable_multi_account:
|
147
150
|
self.cross_account_manager = EnhancedCrossAccountManager(
|
148
151
|
base_profile=profile,
|
149
152
|
max_workers=max_workers,
|
150
|
-
session_ttl_minutes=240 # 4-hour TTL for enterprise operations
|
153
|
+
session_ttl_minutes=240, # 4-hour TTL for enterprise operations
|
151
154
|
)
|
152
155
|
print_info(f"🌐 Multi-Organization Landing Zone mode enabled for {max_workers} parallel accounts")
|
153
156
|
print_info(f"🚫 Excluded decommissioned accounts: {self.excluded_accounts}")
|
154
|
-
|
157
|
+
|
155
158
|
# Results storage
|
156
159
|
self.last_discovery = None
|
157
160
|
self.last_awso_analysis = None
|
158
161
|
self.landing_zone_sessions = [] # Enhanced: Store cross-account sessions
|
159
|
-
|
162
|
+
|
160
163
|
print_header(f"VPC Analyzer latest version", "Multi-Organization Landing Zone Enhanced")
|
161
|
-
|
164
|
+
|
162
165
|
if self.enable_multi_account:
|
163
166
|
print_info(f"🎯 Target: 60-account Multi-Organization Landing Zone discovery")
|
164
167
|
print_info(f"⚡ Performance: <60s complete analysis with {max_workers} parallel workers")
|
165
168
|
print_info(f"🔒 Session TTL: 4-hour enterprise standard with auto-refresh")
|
166
169
|
|
167
170
|
def _filter_landing_zone_accounts(
|
168
|
-
self,
|
169
|
-
accounts: List[OrganizationAccount],
|
170
|
-
excluded_accounts: Optional[List[str]] = None
|
171
|
+
self, accounts: List[OrganizationAccount], excluded_accounts: Optional[List[str]] = None
|
171
172
|
) -> List[OrganizationAccount]:
|
172
173
|
"""
|
173
174
|
Enhanced: Filter out decommissioned accounts from Landing Zone discovery
|
174
|
-
|
175
|
+
|
175
176
|
Args:
|
176
177
|
accounts: List of organization accounts
|
177
178
|
excluded_accounts: Additional accounts to exclude (merged with instance defaults)
|
178
|
-
|
179
|
+
|
179
180
|
Returns:
|
180
181
|
Filtered list of active accounts for discovery
|
181
182
|
"""
|
182
183
|
exclusion_list = (excluded_accounts or []) + (self.excluded_accounts or [])
|
183
|
-
|
184
|
+
|
184
185
|
# Remove duplicates while preserving order
|
185
186
|
exclusion_list = list(dict.fromkeys(exclusion_list))
|
186
|
-
|
187
|
+
|
187
188
|
if not exclusion_list:
|
188
189
|
return accounts
|
189
|
-
|
190
|
+
|
190
191
|
filtered_accounts = []
|
191
192
|
excluded_count = 0
|
192
|
-
|
193
|
+
|
193
194
|
for account in accounts:
|
194
195
|
if account.account_id in exclusion_list:
|
195
196
|
excluded_count += 1
|
196
197
|
print_info(f"🚫 Excluded decommissioned account: {account.account_id} ({account.name or 'Unknown'})")
|
197
198
|
else:
|
198
199
|
filtered_accounts.append(account)
|
199
|
-
|
200
|
+
|
200
201
|
if excluded_count > 0:
|
201
202
|
print_warning(f"⚠️ Excluded {excluded_count} decommissioned accounts from discovery")
|
202
203
|
print_info(f"✅ Active accounts for discovery: {len(filtered_accounts)}")
|
203
|
-
|
204
|
+
|
204
205
|
return filtered_accounts
|
205
|
-
|
206
|
+
|
206
207
|
async def discover_multi_org_vpc_topology(
|
207
|
-
self,
|
208
|
-
target_accounts: int = 60,
|
209
|
-
landing_zone_structure: Optional[Dict] = None
|
208
|
+
self, target_accounts: int = 60, landing_zone_structure: Optional[Dict] = None
|
210
209
|
) -> VPCDiscoveryResult:
|
211
210
|
"""
|
212
211
|
Enhanced: Discover VPC topology across Multi-Organization Landing Zone
|
213
|
-
|
212
|
+
|
214
213
|
This is the primary method for 60-account enterprise discovery operations.
|
215
|
-
|
214
|
+
|
216
215
|
Args:
|
217
216
|
target_accounts: Expected number of accounts (default: 60)
|
218
217
|
landing_zone_structure: Optional Landing Zone structure metadata
|
219
|
-
|
218
|
+
|
220
219
|
Returns:
|
221
220
|
VPCDiscoveryResult with comprehensive multi-account topology
|
222
221
|
"""
|
223
222
|
if not self.enable_multi_account or not self.cross_account_manager:
|
224
223
|
raise ValueError("Multi-account mode not enabled. Initialize with enable_multi_account=True")
|
225
|
-
|
224
|
+
|
226
225
|
print_header("Multi-Organization Landing Zone VPC Discovery", f"Target: {target_accounts} accounts")
|
227
226
|
start_time = time.time()
|
228
|
-
|
227
|
+
|
229
228
|
# Step 1: Discover and filter Landing Zone accounts
|
230
229
|
print_info("🏢 Step 1: Discovering Landing Zone accounts...")
|
231
230
|
try:
|
232
231
|
sessions = await self.cross_account_manager.create_cross_account_sessions_from_organization()
|
233
|
-
|
232
|
+
|
234
233
|
# Extract accounts from sessions and filter decommissioned
|
235
234
|
all_accounts = [
|
236
235
|
OrganizationAccount(
|
237
236
|
account_id=session.account_id,
|
238
237
|
name=session.account_name or session.account_id,
|
239
238
|
email="discovered@system",
|
240
|
-
status="ACTIVE" if session.status in [
|
241
|
-
joined_method="DISCOVERED"
|
239
|
+
status="ACTIVE" if session.status in ["success", "cached"] else "INACTIVE",
|
240
|
+
joined_method="DISCOVERED",
|
242
241
|
)
|
243
242
|
for session in sessions
|
244
243
|
]
|
245
|
-
|
244
|
+
|
246
245
|
active_accounts = self._filter_landing_zone_accounts(all_accounts)
|
247
246
|
successful_sessions = self.cross_account_manager.get_successful_sessions(sessions)
|
248
|
-
|
249
|
-
print_success(
|
250
|
-
|
247
|
+
|
248
|
+
print_success(
|
249
|
+
f"✅ Landing Zone Discovery: {len(successful_sessions)}/{len(all_accounts)} accounts accessible"
|
250
|
+
)
|
251
|
+
|
251
252
|
except Exception as e:
|
252
253
|
print_error(f"❌ Failed to discover Landing Zone accounts: {e}")
|
253
254
|
raise
|
254
|
-
|
255
|
+
|
255
256
|
# Step 2: Parallel VPC topology discovery
|
256
257
|
print_info(f"🔍 Step 2: Parallel VPC discovery across {len(successful_sessions)} accounts...")
|
257
|
-
|
258
|
+
|
258
259
|
aggregated_results = await self._discover_vpc_topology_parallel(successful_sessions)
|
259
|
-
|
260
|
+
|
260
261
|
# Step 3: Generate comprehensive analytics
|
261
262
|
discovery_time = time.time() - start_time
|
262
|
-
|
263
|
+
|
263
264
|
landing_zone_metrics = {
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
265
|
+
"total_accounts_discovered": len(all_accounts),
|
266
|
+
"successful_sessions": len(successful_sessions),
|
267
|
+
"excluded_accounts": len(all_accounts) - len(active_accounts),
|
268
|
+
"discovery_time_seconds": discovery_time,
|
269
|
+
"performance_target_met": discovery_time < 60.0, # <60s target
|
270
|
+
"accounts_per_second": len(successful_sessions) / discovery_time if discovery_time > 0 else 0,
|
271
|
+
"session_ttl_hours": 4, # Enhanced: 4-hour TTL
|
272
|
+
"parallel_workers": self.max_workers,
|
272
273
|
}
|
273
|
-
|
274
|
+
|
274
275
|
print_success(f"🎯 Multi-Organization Landing Zone Discovery Complete!")
|
275
276
|
print_info(f" 📊 Performance: {discovery_time:.1f}s for {len(successful_sessions)} accounts")
|
276
277
|
print_info(f" ⚡ Rate: {landing_zone_metrics['accounts_per_second']:.1f} accounts/second")
|
277
|
-
print_info(
|
278
|
-
|
278
|
+
print_info(
|
279
|
+
f" 🎯 Target met: {'✅ Yes' if landing_zone_metrics['performance_target_met'] else '❌ No'} (<60s)"
|
280
|
+
)
|
281
|
+
|
279
282
|
# Store sessions for future operations
|
280
283
|
self.landing_zone_sessions = successful_sessions
|
281
|
-
|
284
|
+
|
282
285
|
# Enhanced result with Landing Zone metadata
|
283
286
|
aggregated_results.landing_zone_metrics = landing_zone_metrics
|
284
287
|
aggregated_results.account_summary = {
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
288
|
+
"total_accounts": len(successful_sessions),
|
289
|
+
"excluded_accounts_list": self.excluded_accounts,
|
290
|
+
"discovery_timestamp": datetime.now().isoformat(),
|
291
|
+
"landing_zone_structure": landing_zone_structure,
|
289
292
|
}
|
290
|
-
|
293
|
+
|
291
294
|
self.last_discovery = aggregated_results
|
292
295
|
return aggregated_results
|
293
|
-
|
294
|
-
async def _discover_vpc_topology_parallel(
|
295
|
-
self,
|
296
|
-
sessions: List[CrossAccountSession]
|
297
|
-
) -> VPCDiscoveryResult:
|
296
|
+
|
297
|
+
async def _discover_vpc_topology_parallel(self, sessions: List[CrossAccountSession]) -> VPCDiscoveryResult:
|
298
298
|
"""
|
299
299
|
Enhanced: Discover VPC topology across multiple accounts in parallel
|
300
|
-
|
300
|
+
|
301
301
|
Optimized for 60-account operations with <60s performance target.
|
302
|
-
|
302
|
+
|
303
303
|
Args:
|
304
304
|
sessions: List of successful cross-account sessions
|
305
|
-
|
305
|
+
|
306
306
|
Returns:
|
307
307
|
Aggregated VPCDiscoveryResult from all accounts
|
308
308
|
"""
|
309
309
|
if not sessions:
|
310
310
|
print_warning("⚠️ No successful sessions available for VPC discovery")
|
311
311
|
return self._create_empty_discovery_result()
|
312
|
-
|
312
|
+
|
313
313
|
# Initialize aggregated results
|
314
314
|
aggregated_vpcs = []
|
315
315
|
aggregated_nat_gateways = []
|
@@ -321,49 +321,49 @@ class VPCAnalyzer:
|
|
321
321
|
aggregated_transit_gateway_attachments = []
|
322
322
|
aggregated_vpc_peering_connections = []
|
323
323
|
aggregated_security_groups = []
|
324
|
-
|
324
|
+
|
325
325
|
total_resources = 0
|
326
|
-
|
326
|
+
|
327
327
|
# Create progress tracking
|
328
328
|
with create_progress_bar() as progress:
|
329
329
|
task = progress.add_task(f"VPC discovery across {len(sessions)} accounts...", total=len(sessions))
|
330
|
-
|
330
|
+
|
331
331
|
# Process accounts in parallel batches
|
332
332
|
batch_size = min(self.max_workers, len(sessions))
|
333
|
-
|
333
|
+
|
334
334
|
for i in range(0, len(sessions), batch_size):
|
335
|
-
batch_sessions = sessions[i:i + batch_size]
|
335
|
+
batch_sessions = sessions[i : i + batch_size]
|
336
336
|
batch_tasks = []
|
337
|
-
|
337
|
+
|
338
338
|
# Create async tasks for parallel processing
|
339
339
|
for session in batch_sessions:
|
340
340
|
task_coro = self._discover_single_account_vpc_topology(session)
|
341
341
|
batch_tasks.append(asyncio.create_task(task_coro))
|
342
|
-
|
342
|
+
|
343
343
|
# Wait for batch completion
|
344
344
|
batch_results = await asyncio.gather(*batch_tasks, return_exceptions=True)
|
345
|
-
|
345
|
+
|
346
346
|
# Process batch results
|
347
347
|
for idx, result in enumerate(batch_results):
|
348
348
|
session = batch_sessions[idx]
|
349
|
-
|
349
|
+
|
350
350
|
if isinstance(result, Exception):
|
351
351
|
print_warning(f"⚠️ Account {session.account_id} discovery failed: {result}")
|
352
352
|
progress.advance(task)
|
353
353
|
continue
|
354
|
-
|
354
|
+
|
355
355
|
if result:
|
356
356
|
# Aggregate resources with account context
|
357
357
|
for vpc in result.vpcs:
|
358
|
-
vpc[
|
359
|
-
vpc[
|
358
|
+
vpc["source_account"] = session.account_id
|
359
|
+
vpc["source_account_name"] = session.account_name
|
360
360
|
aggregated_vpcs.extend(result.vpcs)
|
361
|
-
|
361
|
+
|
362
362
|
for nat_gw in result.nat_gateways:
|
363
|
-
nat_gw[
|
364
|
-
nat_gw[
|
363
|
+
nat_gw["source_account"] = session.account_id
|
364
|
+
nat_gw["source_account_name"] = session.account_name
|
365
365
|
aggregated_nat_gateways.extend(result.nat_gateways)
|
366
|
-
|
366
|
+
|
367
367
|
# Add account context to all resource types
|
368
368
|
for resource_list, aggregated_list in [
|
369
369
|
(result.vpc_endpoints, aggregated_vpc_endpoints),
|
@@ -373,19 +373,21 @@ class VPCAnalyzer:
|
|
373
373
|
(result.network_interfaces, aggregated_network_interfaces),
|
374
374
|
(result.transit_gateway_attachments, aggregated_transit_gateway_attachments),
|
375
375
|
(result.vpc_peering_connections, aggregated_vpc_peering_connections),
|
376
|
-
(result.security_groups, aggregated_security_groups)
|
376
|
+
(result.security_groups, aggregated_security_groups),
|
377
377
|
]:
|
378
378
|
for resource in resource_list:
|
379
|
-
resource[
|
380
|
-
resource[
|
379
|
+
resource["source_account"] = session.account_id
|
380
|
+
resource["source_account_name"] = session.account_name
|
381
381
|
aggregated_list.extend(resource_list)
|
382
|
-
|
382
|
+
|
383
383
|
total_resources += result.total_resources
|
384
|
-
|
384
|
+
|
385
385
|
progress.advance(task)
|
386
|
-
|
387
|
-
print_success(
|
388
|
-
|
386
|
+
|
387
|
+
print_success(
|
388
|
+
f"🎯 Parallel VPC discovery complete: {total_resources} total resources across {len(sessions)} accounts"
|
389
|
+
)
|
390
|
+
|
389
391
|
return VPCDiscoveryResult(
|
390
392
|
vpcs=aggregated_vpcs,
|
391
393
|
nat_gateways=aggregated_nat_gateways,
|
@@ -398,29 +400,26 @@ class VPCAnalyzer:
|
|
398
400
|
vpc_peering_connections=aggregated_vpc_peering_connections,
|
399
401
|
security_groups=aggregated_security_groups,
|
400
402
|
total_resources=total_resources,
|
401
|
-
discovery_timestamp=datetime.now().isoformat()
|
403
|
+
discovery_timestamp=datetime.now().isoformat(),
|
402
404
|
)
|
403
405
|
|
404
|
-
async def _discover_single_account_vpc_topology(
|
405
|
-
self,
|
406
|
-
session: CrossAccountSession
|
407
|
-
) -> Optional[VPCDiscoveryResult]:
|
406
|
+
async def _discover_single_account_vpc_topology(self, session: CrossAccountSession) -> Optional[VPCDiscoveryResult]:
|
408
407
|
"""
|
409
408
|
Discover VPC topology for a single account using cross-account session
|
410
|
-
|
409
|
+
|
411
410
|
Args:
|
412
411
|
session: CrossAccountSession with valid AWS credentials
|
413
|
-
|
412
|
+
|
414
413
|
Returns:
|
415
414
|
VPCDiscoveryResult for the account, or None if discovery fails
|
416
415
|
"""
|
417
|
-
if not session.session or session.status not in [
|
416
|
+
if not session.session or session.status not in ["success", "cached"]:
|
418
417
|
return None
|
419
|
-
|
418
|
+
|
420
419
|
try:
|
421
420
|
# Create EC2 client with assumed role session
|
422
|
-
ec2_client = session.session.client(
|
423
|
-
|
421
|
+
ec2_client = session.session.client("ec2", region_name=self.region)
|
422
|
+
|
424
423
|
# Discover VPC resources
|
425
424
|
vpcs = []
|
426
425
|
nat_gateways = []
|
@@ -432,49 +431,63 @@ class VPCAnalyzer:
|
|
432
431
|
transit_gateway_attachments = []
|
433
432
|
vpc_peering_connections = []
|
434
433
|
security_groups = []
|
435
|
-
|
434
|
+
|
436
435
|
# VPC Discovery
|
437
436
|
vpc_response = ec2_client.describe_vpcs()
|
438
|
-
for vpc in vpc_response[
|
439
|
-
vpcs.append(
|
440
|
-
|
441
|
-
|
442
|
-
|
443
|
-
|
444
|
-
|
445
|
-
|
446
|
-
|
437
|
+
for vpc in vpc_response["Vpcs"]:
|
438
|
+
vpcs.append(
|
439
|
+
{
|
440
|
+
"VpcId": vpc["VpcId"],
|
441
|
+
"State": vpc["State"],
|
442
|
+
"CidrBlock": vpc["CidrBlock"],
|
443
|
+
"IsDefault": vpc.get("IsDefault", False),
|
444
|
+
"Tags": vpc.get("Tags", []),
|
445
|
+
}
|
446
|
+
)
|
447
|
+
|
447
448
|
# NAT Gateway Discovery
|
448
449
|
nat_response = ec2_client.describe_nat_gateways()
|
449
|
-
for nat_gw in nat_response[
|
450
|
-
nat_gateways.append(
|
451
|
-
|
452
|
-
|
453
|
-
|
454
|
-
|
455
|
-
|
456
|
-
|
457
|
-
|
450
|
+
for nat_gw in nat_response["NatGateways"]:
|
451
|
+
nat_gateways.append(
|
452
|
+
{
|
453
|
+
"NatGatewayId": nat_gw["NatGatewayId"],
|
454
|
+
"VpcId": nat_gw.get("VpcId"),
|
455
|
+
"State": nat_gw["State"],
|
456
|
+
"SubnetId": nat_gw.get("SubnetId"),
|
457
|
+
"Tags": nat_gw.get("Tags", []),
|
458
|
+
}
|
459
|
+
)
|
460
|
+
|
458
461
|
# Network Interfaces Discovery (for ENI gate analysis)
|
459
462
|
eni_response = ec2_client.describe_network_interfaces()
|
460
|
-
for eni in eni_response[
|
461
|
-
network_interfaces.append(
|
462
|
-
|
463
|
-
|
464
|
-
|
465
|
-
|
466
|
-
|
467
|
-
|
468
|
-
|
469
|
-
|
470
|
-
|
463
|
+
for eni in eni_response["NetworkInterfaces"]:
|
464
|
+
network_interfaces.append(
|
465
|
+
{
|
466
|
+
"NetworkInterfaceId": eni["NetworkInterfaceId"],
|
467
|
+
"VpcId": eni.get("VpcId"),
|
468
|
+
"SubnetId": eni.get("SubnetId"),
|
469
|
+
"Status": eni["Status"],
|
470
|
+
"InterfaceType": eni.get("InterfaceType", "interface"),
|
471
|
+
"Attachment": eni.get("Attachment", {}),
|
472
|
+
"Tags": eni.get("TagSet", []),
|
473
|
+
}
|
474
|
+
)
|
475
|
+
|
471
476
|
# Continue with other resource types as needed...
|
472
|
-
|
473
|
-
total_resources = (
|
474
|
-
|
475
|
-
|
476
|
-
|
477
|
-
|
477
|
+
|
478
|
+
total_resources = (
|
479
|
+
len(vpcs)
|
480
|
+
+ len(nat_gateways)
|
481
|
+
+ len(vpc_endpoints)
|
482
|
+
+ len(internet_gateways)
|
483
|
+
+ len(route_tables)
|
484
|
+
+ len(subnets)
|
485
|
+
+ len(network_interfaces)
|
486
|
+
+ len(transit_gateway_attachments)
|
487
|
+
+ len(vpc_peering_connections)
|
488
|
+
+ len(security_groups)
|
489
|
+
)
|
490
|
+
|
478
491
|
return VPCDiscoveryResult(
|
479
492
|
vpcs=vpcs,
|
480
493
|
nat_gateways=nat_gateways,
|
@@ -487,9 +500,9 @@ class VPCAnalyzer:
|
|
487
500
|
vpc_peering_connections=vpc_peering_connections,
|
488
501
|
security_groups=security_groups,
|
489
502
|
total_resources=total_resources,
|
490
|
-
discovery_timestamp=datetime.now().isoformat()
|
503
|
+
discovery_timestamp=datetime.now().isoformat(),
|
491
504
|
)
|
492
|
-
|
505
|
+
|
493
506
|
except ClientError as e:
|
494
507
|
print_warning(f"⚠️ AWS API error for account {session.account_id}: {e}")
|
495
508
|
return None
|
@@ -511,69 +524,69 @@ class VPCAnalyzer:
|
|
511
524
|
vpc_peering_connections=[],
|
512
525
|
security_groups=[],
|
513
526
|
total_resources=0,
|
514
|
-
discovery_timestamp=datetime.now().isoformat()
|
527
|
+
discovery_timestamp=datetime.now().isoformat(),
|
515
528
|
)
|
516
529
|
|
517
530
|
def discover_vpc_topology(self, vpc_ids: Optional[List[str]] = None) -> VPCDiscoveryResult:
|
518
531
|
"""
|
519
532
|
Comprehensive VPC topology discovery for AWSO-05 support
|
520
|
-
|
533
|
+
|
521
534
|
Args:
|
522
535
|
vpc_ids: Optional list of specific VPC IDs to analyze
|
523
|
-
|
536
|
+
|
524
537
|
Returns:
|
525
538
|
VPCDiscoveryResult with complete topology information
|
526
539
|
"""
|
527
540
|
print_header("VPC Topology Discovery", "AWSO-05 Enhanced")
|
528
|
-
|
541
|
+
|
529
542
|
if not self.session:
|
530
543
|
print_error("No AWS session available")
|
531
544
|
return self._empty_discovery_result()
|
532
|
-
|
545
|
+
|
533
546
|
with self.console.status("[bold green]Discovering VPC topology...") as status:
|
534
547
|
try:
|
535
548
|
ec2 = self.session.client("ec2", region_name=self.region)
|
536
|
-
|
549
|
+
|
537
550
|
# Discover VPCs
|
538
551
|
status.update("🔍 Discovering VPCs...")
|
539
552
|
vpcs = self._discover_vpcs(ec2, vpc_ids)
|
540
|
-
|
541
|
-
# Discover NAT Gateways
|
553
|
+
|
554
|
+
# Discover NAT Gateways
|
542
555
|
status.update("🌐 Discovering NAT Gateways...")
|
543
556
|
nat_gateways = self._discover_nat_gateways(ec2, vpc_ids)
|
544
|
-
|
557
|
+
|
545
558
|
# Discover VPC Endpoints
|
546
559
|
status.update("🔗 Discovering VPC Endpoints...")
|
547
560
|
vpc_endpoints = self._discover_vpc_endpoints(ec2, vpc_ids)
|
548
|
-
|
561
|
+
|
549
562
|
# Discover Internet Gateways
|
550
563
|
status.update("🌍 Discovering Internet Gateways...")
|
551
564
|
internet_gateways = self._discover_internet_gateways(ec2, vpc_ids)
|
552
|
-
|
565
|
+
|
553
566
|
# Discover Route Tables
|
554
567
|
status.update("📋 Discovering Route Tables...")
|
555
568
|
route_tables = self._discover_route_tables(ec2, vpc_ids)
|
556
|
-
|
569
|
+
|
557
570
|
# Discover Subnets
|
558
571
|
status.update("🏗️ Discovering Subnets...")
|
559
572
|
subnets = self._discover_subnets(ec2, vpc_ids)
|
560
|
-
|
573
|
+
|
561
574
|
# Discover Network Interfaces (ENIs)
|
562
575
|
status.update("🔌 Discovering Network Interfaces...")
|
563
576
|
network_interfaces = self._discover_network_interfaces(ec2, vpc_ids)
|
564
|
-
|
577
|
+
|
565
578
|
# Discover Transit Gateway Attachments
|
566
579
|
status.update("🚇 Discovering Transit Gateway Attachments...")
|
567
580
|
tgw_attachments = self._discover_transit_gateway_attachments(ec2, vpc_ids)
|
568
|
-
|
581
|
+
|
569
582
|
# Discover VPC Peering Connections
|
570
583
|
status.update("🔄 Discovering VPC Peering Connections...")
|
571
584
|
vpc_peering = self._discover_vpc_peering_connections(ec2, vpc_ids)
|
572
|
-
|
585
|
+
|
573
586
|
# Discover Security Groups
|
574
587
|
status.update("🛡️ Discovering Security Groups...")
|
575
588
|
security_groups = self._discover_security_groups(ec2, vpc_ids)
|
576
|
-
|
589
|
+
|
577
590
|
# Create discovery result
|
578
591
|
result = VPCDiscoveryResult(
|
579
592
|
vpcs=vpcs,
|
@@ -586,79 +599,83 @@ class VPCAnalyzer:
|
|
586
599
|
transit_gateway_attachments=tgw_attachments,
|
587
600
|
vpc_peering_connections=vpc_peering,
|
588
601
|
security_groups=security_groups,
|
589
|
-
total_resources=len(vpcs)
|
590
|
-
|
591
|
-
|
592
|
-
|
593
|
-
|
602
|
+
total_resources=len(vpcs)
|
603
|
+
+ len(nat_gateways)
|
604
|
+
+ len(vpc_endpoints)
|
605
|
+
+ len(internet_gateways)
|
606
|
+
+ len(route_tables)
|
607
|
+
+ len(subnets)
|
608
|
+
+ len(network_interfaces)
|
609
|
+
+ len(tgw_attachments)
|
610
|
+
+ len(vpc_peering)
|
611
|
+
+ len(security_groups),
|
612
|
+
discovery_timestamp=datetime.now().isoformat(),
|
594
613
|
)
|
595
|
-
|
614
|
+
|
596
615
|
self.last_discovery = result
|
597
616
|
self._display_discovery_results(result)
|
598
|
-
|
617
|
+
|
599
618
|
return result
|
600
|
-
|
619
|
+
|
601
620
|
except Exception as e:
|
602
621
|
print_error(f"VPC discovery failed: {e}")
|
603
622
|
logger.error(f"VPC discovery error: {e}")
|
604
623
|
return self._empty_discovery_result()
|
605
624
|
|
606
625
|
async def discover_multi_org_vpc_topology(
|
607
|
-
self,
|
608
|
-
target_accounts: int = 60,
|
609
|
-
landing_zone_structure: Optional[Dict] = None
|
626
|
+
self, target_accounts: int = 60, landing_zone_structure: Optional[Dict] = None
|
610
627
|
) -> VPCDiscoveryResult:
|
611
628
|
"""
|
612
629
|
NEW: Multi-Organization Landing Zone VPC discovery for 60-account operations
|
613
|
-
|
630
|
+
|
614
631
|
Optimized discovery across Landing Zone with decommissioned account filtering
|
615
632
|
and enhanced session management.
|
616
|
-
|
633
|
+
|
617
634
|
Args:
|
618
635
|
target_accounts: Target number of accounts to discover (60 for Landing Zone)
|
619
636
|
landing_zone_structure: Optional Landing Zone organizational structure
|
620
|
-
|
637
|
+
|
621
638
|
Returns:
|
622
639
|
VPCDiscoveryResult with comprehensive multi-account topology
|
623
640
|
"""
|
624
641
|
if not self.enable_multi_account or not self.cross_account_manager:
|
625
642
|
print_error("Multi-account mode not enabled. Initialize with enable_multi_account=True")
|
626
643
|
return self._empty_discovery_result()
|
627
|
-
|
644
|
+
|
628
645
|
print_header("Multi-Organization Landing Zone VPC Discovery", f"Target: {target_accounts} accounts")
|
629
|
-
|
646
|
+
|
630
647
|
start_time = time.time()
|
631
|
-
|
648
|
+
|
632
649
|
try:
|
633
650
|
# Step 1: Discover and filter Landing Zone accounts
|
634
651
|
print_info("🏢 Discovering Landing Zone organization accounts...")
|
635
652
|
accounts = await self._discover_landing_zone_accounts()
|
636
|
-
|
653
|
+
|
637
654
|
# Step 2: Filter decommissioned accounts
|
638
655
|
filtered_accounts = self._filter_landing_zone_accounts(accounts)
|
639
|
-
|
656
|
+
|
640
657
|
# Step 3: Create cross-account sessions
|
641
658
|
print_info(f"🔐 Creating cross-account sessions for {len(filtered_accounts)} accounts...")
|
642
659
|
sessions = await self.cross_account_manager.create_cross_account_sessions_from_accounts(filtered_accounts)
|
643
660
|
successful_sessions = self.cross_account_manager.get_successful_sessions(sessions)
|
644
|
-
|
661
|
+
|
645
662
|
self.landing_zone_sessions = successful_sessions
|
646
|
-
|
663
|
+
|
647
664
|
# Step 4: Parallel VPC discovery across all accounts
|
648
665
|
print_info(f"🔍 Discovering VPC topology across {len(successful_sessions)} accounts...")
|
649
666
|
multi_account_results = await self._discover_vpc_topology_parallel(successful_sessions)
|
650
|
-
|
667
|
+
|
651
668
|
# Step 5: Aggregate results and generate Landing Zone metrics
|
652
669
|
aggregated_result = self._aggregate_multi_account_results(multi_account_results, successful_sessions)
|
653
|
-
|
670
|
+
|
654
671
|
# Performance metrics
|
655
672
|
execution_time = time.time() - start_time
|
656
673
|
print_success(f"✅ Multi-Organization Landing Zone discovery complete in {execution_time:.1f}s")
|
657
674
|
print_info(f" 📈 {len(successful_sessions)}/{len(accounts)} accounts discovered")
|
658
675
|
print_info(f" 🏗️ {aggregated_result.total_resources} total VPC resources discovered")
|
659
|
-
|
676
|
+
|
660
677
|
return aggregated_result
|
661
|
-
|
678
|
+
|
662
679
|
except Exception as e:
|
663
680
|
print_error(f"Multi-Organization Landing Zone discovery failed: {e}")
|
664
681
|
logger.error(f"Landing Zone discovery error: {e}")
|
@@ -668,55 +685,51 @@ class VPCAnalyzer:
|
|
668
685
|
"""Discover accounts from Organizations API with Landing Zone context"""
|
669
686
|
orgs_client = get_unified_organizations_client(self.profile)
|
670
687
|
accounts = await orgs_client.get_organization_accounts()
|
671
|
-
|
688
|
+
|
672
689
|
if not accounts:
|
673
690
|
print_warning("No accounts discovered from Organizations API")
|
674
691
|
return []
|
675
|
-
|
692
|
+
|
676
693
|
print_info(f"🏢 Discovered {len(accounts)} total organization accounts")
|
677
694
|
return accounts
|
678
695
|
|
679
696
|
def _filter_landing_zone_accounts(self, accounts: List[OrganizationAccount]) -> List[OrganizationAccount]:
|
680
697
|
"""
|
681
698
|
Filter Landing Zone accounts with decommissioned account exclusion
|
682
|
-
|
699
|
+
|
683
700
|
Applies enterprise-grade filtering:
|
684
701
|
- Excludes decommissioned accounts (294618320542 by default)
|
685
702
|
- Filters to ACTIVE status accounts only
|
686
703
|
- Maintains Landing Zone organizational structure
|
687
704
|
"""
|
688
705
|
# Filter to active accounts only
|
689
|
-
active_accounts = [acc for acc in accounts if acc.status ==
|
690
|
-
|
706
|
+
active_accounts = [acc for acc in accounts if acc.status == "ACTIVE"]
|
707
|
+
|
691
708
|
# Filter out decommissioned accounts
|
692
|
-
filtered_accounts = [
|
693
|
-
|
694
|
-
if acc.account_id not in self.excluded_accounts
|
695
|
-
]
|
696
|
-
|
709
|
+
filtered_accounts = [acc for acc in active_accounts if acc.account_id not in self.excluded_accounts]
|
710
|
+
|
697
711
|
excluded_count = len(active_accounts) - len(filtered_accounts)
|
698
|
-
|
712
|
+
|
699
713
|
print_info(f"🎯 Landing Zone account filtering:")
|
700
714
|
print_info(f" • Total accounts: {len(accounts)}")
|
701
715
|
print_info(f" • Active accounts: {len(active_accounts)}")
|
702
716
|
print_info(f" • Excluded decommissioned: {excluded_count} ({self.excluded_accounts})")
|
703
717
|
print_info(f" • Ready for discovery: {len(filtered_accounts)}")
|
704
|
-
|
718
|
+
|
705
719
|
return filtered_accounts
|
706
720
|
|
707
721
|
async def _discover_vpc_topology_parallel(
|
708
|
-
self,
|
709
|
-
sessions: List[CrossAccountSession]
|
722
|
+
self, sessions: List[CrossAccountSession]
|
710
723
|
) -> List[Tuple[str, VPCDiscoveryResult]]:
|
711
724
|
"""
|
712
725
|
Parallel VPC discovery across multiple accounts optimized for 60-account Landing Zone
|
713
|
-
|
726
|
+
|
714
727
|
Performance optimized for <60s discovery across entire Landing Zone
|
715
728
|
"""
|
716
729
|
results = []
|
717
|
-
|
730
|
+
|
718
731
|
print_info(f"🚀 Starting parallel VPC discovery across {len(sessions)} accounts")
|
719
|
-
|
732
|
+
|
720
733
|
# Use asyncio.gather for concurrent execution
|
721
734
|
async def discover_account_vpc(session: CrossAccountSession) -> Tuple[str, VPCDiscoveryResult]:
|
722
735
|
try:
|
@@ -725,51 +738,51 @@ class VPCAnalyzer:
|
|
725
738
|
profile=None, # Use session directly
|
726
739
|
region=self.region,
|
727
740
|
console=self.console,
|
728
|
-
dry_run=self.dry_run
|
741
|
+
dry_run=self.dry_run,
|
729
742
|
)
|
730
743
|
account_analyzer.session = session.session
|
731
|
-
|
744
|
+
|
732
745
|
# Perform single-account VPC discovery
|
733
746
|
discovery_result = account_analyzer.discover_vpc_topology()
|
734
|
-
|
747
|
+
|
735
748
|
# Add account metadata to results
|
736
749
|
discovery_result.account_summary = {
|
737
|
-
|
738
|
-
|
739
|
-
|
740
|
-
|
750
|
+
"account_id": session.account_id,
|
751
|
+
"account_name": session.account_name,
|
752
|
+
"role_used": session.role_used,
|
753
|
+
"discovery_timestamp": discovery_result.discovery_timestamp,
|
741
754
|
}
|
742
|
-
|
755
|
+
|
743
756
|
return session.account_id, discovery_result
|
744
|
-
|
757
|
+
|
745
758
|
except Exception as e:
|
746
759
|
print_warning(f"⚠️ VPC discovery failed for account {session.account_id}: {e}")
|
747
760
|
logger.warning(f"Account {session.account_id} VPC discovery error: {e}")
|
748
|
-
|
761
|
+
|
749
762
|
# Return empty result for failed account
|
750
763
|
empty_result = self._empty_discovery_result()
|
751
764
|
empty_result.account_summary = {
|
752
|
-
|
753
|
-
|
754
|
-
|
765
|
+
"account_id": session.account_id,
|
766
|
+
"account_name": session.account_name,
|
767
|
+
"error": str(e),
|
755
768
|
}
|
756
769
|
return session.account_id, empty_result
|
757
|
-
|
770
|
+
|
758
771
|
# Execute parallel discovery
|
759
772
|
with create_progress_bar() as progress:
|
760
773
|
task = progress.add_task("Discovering VPC topology across accounts...", total=len(sessions))
|
761
|
-
|
774
|
+
|
762
775
|
# Process accounts in batches to manage resource usage
|
763
776
|
batch_size = min(self.max_workers, len(sessions))
|
764
|
-
|
777
|
+
|
765
778
|
for i in range(0, len(sessions), batch_size):
|
766
|
-
batch_sessions = sessions[i:i + batch_size]
|
767
|
-
|
779
|
+
batch_sessions = sessions[i : i + batch_size]
|
780
|
+
|
768
781
|
# Execute batch concurrently
|
769
|
-
batch_results = await asyncio.gather(
|
770
|
-
discover_account_vpc(session) for session in batch_sessions
|
771
|
-
|
772
|
-
|
782
|
+
batch_results = await asyncio.gather(
|
783
|
+
*[discover_account_vpc(session) for session in batch_sessions], return_exceptions=True
|
784
|
+
)
|
785
|
+
|
773
786
|
# Process batch results
|
774
787
|
for result in batch_results:
|
775
788
|
if isinstance(result, Exception):
|
@@ -777,22 +790,20 @@ class VPCAnalyzer:
|
|
777
790
|
else:
|
778
791
|
results.append(result)
|
779
792
|
progress.advance(task)
|
780
|
-
|
793
|
+
|
781
794
|
print_success(f"✅ Parallel VPC discovery complete: {len(results)} accounts processed")
|
782
795
|
return results
|
783
796
|
|
784
797
|
def _aggregate_multi_account_results(
|
785
|
-
self,
|
786
|
-
multi_account_results: List[Tuple[str, VPCDiscoveryResult]],
|
787
|
-
sessions: List[CrossAccountSession]
|
798
|
+
self, multi_account_results: List[Tuple[str, VPCDiscoveryResult]], sessions: List[CrossAccountSession]
|
788
799
|
) -> VPCDiscoveryResult:
|
789
800
|
"""
|
790
801
|
Aggregate multi-account VPC discovery results into comprehensive Landing Zone view
|
791
|
-
|
802
|
+
|
792
803
|
Creates unified view with Landing Zone metrics and account-level aggregation
|
793
804
|
"""
|
794
805
|
print_info("📊 Aggregating multi-account VPC results...")
|
795
|
-
|
806
|
+
|
796
807
|
# Initialize aggregation containers
|
797
808
|
all_vpcs = []
|
798
809
|
all_nat_gateways = []
|
@@ -804,73 +815,79 @@ class VPCAnalyzer:
|
|
804
815
|
all_tgw_attachments = []
|
805
816
|
all_vpc_peering = []
|
806
817
|
all_security_groups = []
|
807
|
-
|
818
|
+
|
808
819
|
account_summaries = []
|
809
820
|
total_resources_per_account = {}
|
810
|
-
|
821
|
+
|
811
822
|
# Aggregate resources from all accounts
|
812
823
|
for account_id, discovery_result in multi_account_results:
|
813
824
|
# Add account context to all resources
|
814
825
|
for vpc in discovery_result.vpcs:
|
815
|
-
vpc[
|
826
|
+
vpc["AccountId"] = account_id
|
816
827
|
all_vpcs.append(vpc)
|
817
|
-
|
828
|
+
|
818
829
|
for nat in discovery_result.nat_gateways:
|
819
|
-
nat[
|
830
|
+
nat["AccountId"] = account_id
|
820
831
|
all_nat_gateways.append(nat)
|
821
|
-
|
832
|
+
|
822
833
|
for endpoint in discovery_result.vpc_endpoints:
|
823
|
-
endpoint[
|
834
|
+
endpoint["AccountId"] = account_id
|
824
835
|
all_vpc_endpoints.append(endpoint)
|
825
|
-
|
836
|
+
|
826
837
|
for igw in discovery_result.internet_gateways:
|
827
|
-
igw[
|
838
|
+
igw["AccountId"] = account_id
|
828
839
|
all_internet_gateways.append(igw)
|
829
|
-
|
840
|
+
|
830
841
|
for rt in discovery_result.route_tables:
|
831
|
-
rt[
|
842
|
+
rt["AccountId"] = account_id
|
832
843
|
all_route_tables.append(rt)
|
833
|
-
|
844
|
+
|
834
845
|
for subnet in discovery_result.subnets:
|
835
|
-
subnet[
|
846
|
+
subnet["AccountId"] = account_id
|
836
847
|
all_subnets.append(subnet)
|
837
|
-
|
848
|
+
|
838
849
|
for eni in discovery_result.network_interfaces:
|
839
|
-
eni[
|
850
|
+
eni["AccountId"] = account_id
|
840
851
|
all_network_interfaces.append(eni)
|
841
|
-
|
852
|
+
|
842
853
|
for tgw in discovery_result.transit_gateway_attachments:
|
843
|
-
tgw[
|
854
|
+
tgw["AccountId"] = account_id
|
844
855
|
all_tgw_attachments.append(tgw)
|
845
|
-
|
856
|
+
|
846
857
|
for peering in discovery_result.vpc_peering_connections:
|
847
|
-
peering[
|
858
|
+
peering["AccountId"] = account_id
|
848
859
|
all_vpc_peering.append(peering)
|
849
|
-
|
860
|
+
|
850
861
|
for sg in discovery_result.security_groups:
|
851
|
-
sg[
|
862
|
+
sg["AccountId"] = account_id
|
852
863
|
all_security_groups.append(sg)
|
853
|
-
|
864
|
+
|
854
865
|
# Track per-account metrics
|
855
866
|
total_resources_per_account[account_id] = discovery_result.total_resources
|
856
|
-
|
867
|
+
|
857
868
|
# Collect account summary
|
858
869
|
if discovery_result.account_summary:
|
859
870
|
account_summaries.append(discovery_result.account_summary)
|
860
|
-
|
871
|
+
|
861
872
|
# Calculate Landing Zone metrics
|
862
873
|
landing_zone_metrics = self._calculate_landing_zone_metrics(
|
863
874
|
total_resources_per_account, account_summaries, sessions
|
864
875
|
)
|
865
|
-
|
876
|
+
|
866
877
|
# Create aggregated result
|
867
878
|
total_resources = (
|
868
|
-
len(all_vpcs)
|
869
|
-
|
870
|
-
|
871
|
-
|
879
|
+
len(all_vpcs)
|
880
|
+
+ len(all_nat_gateways)
|
881
|
+
+ len(all_vpc_endpoints)
|
882
|
+
+ len(all_internet_gateways)
|
883
|
+
+ len(all_route_tables)
|
884
|
+
+ len(all_subnets)
|
885
|
+
+ len(all_network_interfaces)
|
886
|
+
+ len(all_tgw_attachments)
|
887
|
+
+ len(all_vpc_peering)
|
888
|
+
+ len(all_security_groups)
|
872
889
|
)
|
873
|
-
|
890
|
+
|
874
891
|
aggregated_result = VPCDiscoveryResult(
|
875
892
|
vpcs=all_vpcs,
|
876
893
|
nat_gateways=all_nat_gateways,
|
@@ -884,52 +901,47 @@ class VPCAnalyzer:
|
|
884
901
|
security_groups=all_security_groups,
|
885
902
|
total_resources=total_resources,
|
886
903
|
discovery_timestamp=datetime.now().isoformat(),
|
887
|
-
account_summary={
|
888
|
-
landing_zone_metrics=landing_zone_metrics
|
904
|
+
account_summary={"accounts_discovered": account_summaries},
|
905
|
+
landing_zone_metrics=landing_zone_metrics,
|
889
906
|
)
|
890
|
-
|
907
|
+
|
891
908
|
# Display Landing Zone summary
|
892
909
|
self._display_landing_zone_summary(aggregated_result)
|
893
|
-
|
910
|
+
|
894
911
|
return aggregated_result
|
895
912
|
|
896
913
|
def _calculate_landing_zone_metrics(
|
897
|
-
self,
|
898
|
-
resources_per_account: Dict[str, int],
|
899
|
-
account_summaries: List[Dict],
|
900
|
-
sessions: List[CrossAccountSession]
|
914
|
+
self, resources_per_account: Dict[str, int], account_summaries: List[Dict], sessions: List[CrossAccountSession]
|
901
915
|
) -> Dict[str, Any]:
|
902
916
|
"""Calculate comprehensive Landing Zone analytics"""
|
903
|
-
|
904
|
-
successful_accounts = len([s for s in sessions if s.status in [
|
905
|
-
|
917
|
+
|
918
|
+
successful_accounts = len([s for s in sessions if s.status in ["success", "cached"]])
|
919
|
+
|
906
920
|
return {
|
907
|
-
|
908
|
-
|
909
|
-
|
910
|
-
|
911
|
-
|
912
|
-
|
913
|
-
sum(resources_per_account.values()) / len(resources_per_account)
|
914
|
-
if resources_per_account else 0
|
921
|
+
"total_accounts_targeted": len(sessions),
|
922
|
+
"successful_discoveries": successful_accounts,
|
923
|
+
"failed_discoveries": len(sessions) - successful_accounts,
|
924
|
+
"discovery_success_rate": (successful_accounts / len(sessions) * 100) if sessions else 0,
|
925
|
+
"total_vpc_resources": sum(resources_per_account.values()),
|
926
|
+
"average_resources_per_account": (
|
927
|
+
sum(resources_per_account.values()) / len(resources_per_account) if resources_per_account else 0
|
915
928
|
),
|
916
|
-
|
917
|
-
|
918
|
-
|
919
|
-
|
920
|
-
|
921
|
-
self.cross_account_manager.get_session_summary(sessions)
|
922
|
-
if self.cross_account_manager else {}
|
929
|
+
"accounts_with_resources": len([count for count in resources_per_account.values() if count > 0]),
|
930
|
+
"empty_accounts": len([count for count in resources_per_account.values() if count == 0]),
|
931
|
+
"decommissioned_accounts_excluded": len(self.excluded_accounts),
|
932
|
+
"excluded_account_list": self.excluded_accounts,
|
933
|
+
"session_manager_metrics": (
|
934
|
+
self.cross_account_manager.get_session_summary(sessions) if self.cross_account_manager else {}
|
923
935
|
),
|
924
|
-
|
936
|
+
"generated_at": datetime.now().isoformat(),
|
925
937
|
}
|
926
938
|
|
927
939
|
def _display_landing_zone_summary(self, result: VPCDiscoveryResult):
|
928
940
|
"""Display comprehensive Landing Zone summary with Rich formatting"""
|
929
|
-
|
941
|
+
|
930
942
|
# Landing Zone overview panel
|
931
943
|
metrics = result.landing_zone_metrics
|
932
|
-
|
944
|
+
|
933
945
|
summary_panel = Panel(
|
934
946
|
f"[bold green]Multi-Organization Landing Zone Discovery Complete[/bold green]\n\n"
|
935
947
|
f"🏢 Accounts Discovered: [bold cyan]{metrics['successful_discoveries']}/{metrics['total_accounts_targeted']}[/bold cyan] "
|
@@ -949,46 +961,44 @@ class VPCAnalyzer:
|
|
949
961
|
f"TGW Attachments: [bold orange]{len(result.transit_gateway_attachments)}[/bold orange] | "
|
950
962
|
f"Security Groups: [bold gray]{len(result.security_groups)}[/bold gray]",
|
951
963
|
title="🌐 Landing Zone VPC Discovery Summary",
|
952
|
-
style="bold blue"
|
964
|
+
style="bold blue",
|
953
965
|
)
|
954
|
-
|
966
|
+
|
955
967
|
self.console.print(summary_panel)
|
956
|
-
|
968
|
+
|
957
969
|
# Account distribution table
|
958
|
-
if metrics.get(
|
959
|
-
session_metrics = metrics[
|
960
|
-
|
970
|
+
if metrics.get("session_manager_metrics"):
|
971
|
+
session_metrics = metrics["session_manager_metrics"]
|
972
|
+
|
961
973
|
session_table = create_table(
|
962
974
|
title="🔐 Cross-Account Session Summary",
|
963
975
|
columns=[
|
964
976
|
{"header": "Metric", "style": "cyan"},
|
965
977
|
{"header": "Value", "style": "green"},
|
966
|
-
{"header": "Details", "style": "dim"}
|
967
|
-
]
|
978
|
+
{"header": "Details", "style": "dim"},
|
979
|
+
],
|
968
980
|
)
|
969
|
-
|
981
|
+
|
970
982
|
session_table.add_row(
|
971
983
|
"Session Success Rate",
|
972
984
|
f"{(session_metrics['successful_sessions'] / session_metrics['total_sessions'] * 100):.1f}%",
|
973
|
-
f"{session_metrics['successful_sessions']}/{session_metrics['total_sessions']}"
|
985
|
+
f"{session_metrics['successful_sessions']}/{session_metrics['total_sessions']}",
|
974
986
|
)
|
975
987
|
session_table.add_row(
|
976
988
|
"Cache Performance",
|
977
989
|
f"{(session_metrics['metrics']['cache_hits'] / max(session_metrics['metrics']['cache_hits'] + session_metrics['metrics']['cache_misses'], 1) * 100):.1f}%",
|
978
|
-
f"{session_metrics['metrics']['cache_hits']} hits, {session_metrics['metrics']['cache_misses']} misses"
|
990
|
+
f"{session_metrics['metrics']['cache_hits']} hits, {session_metrics['metrics']['cache_misses']} misses",
|
979
991
|
)
|
980
992
|
session_table.add_row(
|
981
|
-
"Session TTL",
|
982
|
-
f"{session_metrics['session_ttl_minutes']} minutes",
|
983
|
-
"4-hour enterprise standard"
|
993
|
+
"Session TTL", f"{session_metrics['session_ttl_minutes']} minutes", "4-hour enterprise standard"
|
984
994
|
)
|
985
|
-
|
995
|
+
|
986
996
|
self.console.print(session_table)
|
987
997
|
|
988
998
|
def analyze_awso_dependencies(self, discovery_result: Optional[VPCDiscoveryResult] = None) -> AWSOAnalysis:
|
989
999
|
"""
|
990
1000
|
AWSO-05 specific dependency analysis for safe VPC cleanup
|
991
|
-
|
1001
|
+
|
992
1002
|
Implements 12-step dependency analysis:
|
993
1003
|
1. ENI gate validation (critical blocking check)
|
994
1004
|
2. NAT Gateway dependency mapping
|
@@ -1002,71 +1012,74 @@ class VPCAnalyzer:
|
|
1002
1012
|
10. Default VPC identification
|
1003
1013
|
11. Cross-account dependency check
|
1004
1014
|
12. Evidence bundle generation
|
1005
|
-
|
1015
|
+
|
1006
1016
|
Args:
|
1007
1017
|
discovery_result: Previous discovery result (uses last if None)
|
1008
|
-
|
1018
|
+
|
1009
1019
|
Returns:
|
1010
1020
|
AWSOAnalysis with comprehensive dependency mapping
|
1011
1021
|
"""
|
1012
1022
|
print_header("AWSO-05 Dependency Analysis", "12-Step Validation")
|
1013
|
-
|
1023
|
+
|
1014
1024
|
if discovery_result is None:
|
1015
1025
|
discovery_result = self.last_discovery
|
1016
|
-
|
1026
|
+
|
1017
1027
|
if not discovery_result:
|
1018
1028
|
print_warning("No discovery data available. Run discover_vpc_topology() first.")
|
1019
1029
|
return self._empty_awso_analysis()
|
1020
|
-
|
1030
|
+
|
1021
1031
|
with self.console.status("[bold yellow]Analyzing AWSO-05 dependencies...") as status:
|
1022
1032
|
try:
|
1023
1033
|
# Step 1: ENI gate validation (CRITICAL)
|
1024
1034
|
status.update("🚨 Step 1/12: ENI Gate Validation...")
|
1025
1035
|
eni_warnings = self._analyze_eni_gate_validation(discovery_result)
|
1026
|
-
|
1036
|
+
|
1027
1037
|
# Step 2-4: Network resource dependencies
|
1028
1038
|
status.update("🔗 Steps 2-4: Network Dependencies...")
|
1029
1039
|
network_deps = self._analyze_network_dependencies(discovery_result)
|
1030
|
-
|
1040
|
+
|
1031
1041
|
# Step 5-7: Gateway and endpoint dependencies
|
1032
1042
|
status.update("🌐 Steps 5-7: Gateway Dependencies...")
|
1033
1043
|
gateway_deps = self._analyze_gateway_dependencies(discovery_result)
|
1034
|
-
|
1044
|
+
|
1035
1045
|
# Step 8-10: Security and route dependencies
|
1036
1046
|
status.update("🛡️ Steps 8-10: Security Dependencies...")
|
1037
1047
|
security_deps = self._analyze_security_dependencies(discovery_result)
|
1038
|
-
|
1048
|
+
|
1039
1049
|
# Step 11: Cross-account dependency check
|
1040
1050
|
status.update("🔄 Step 11: Cross-Account Dependencies...")
|
1041
1051
|
cross_account_deps = self._analyze_cross_account_dependencies(discovery_result)
|
1042
|
-
|
1052
|
+
|
1043
1053
|
# Step 12: Default VPC identification
|
1044
1054
|
status.update("🎯 Step 12: Default VPC Analysis...")
|
1045
1055
|
default_vpcs = self._identify_default_vpcs(discovery_result)
|
1046
|
-
|
1056
|
+
|
1047
1057
|
# Generate cleanup recommendations
|
1048
1058
|
cleanup_recommendations = self._generate_cleanup_recommendations(
|
1049
1059
|
discovery_result, eni_warnings, default_vpcs
|
1050
1060
|
)
|
1051
|
-
|
1061
|
+
|
1052
1062
|
# Create evidence bundle
|
1053
|
-
evidence_bundle = self._create_evidence_bundle(
|
1054
|
-
|
1055
|
-
|
1056
|
-
|
1057
|
-
|
1058
|
-
|
1059
|
-
|
1060
|
-
|
1061
|
-
|
1063
|
+
evidence_bundle = self._create_evidence_bundle(
|
1064
|
+
discovery_result,
|
1065
|
+
{
|
1066
|
+
"eni_warnings": eni_warnings,
|
1067
|
+
"network_deps": network_deps,
|
1068
|
+
"gateway_deps": gateway_deps,
|
1069
|
+
"security_deps": security_deps,
|
1070
|
+
"cross_account_deps": cross_account_deps,
|
1071
|
+
"default_vpcs": default_vpcs,
|
1072
|
+
},
|
1073
|
+
)
|
1074
|
+
|
1062
1075
|
# Compile dependency chain
|
1063
1076
|
dependency_chain = {
|
1064
|
-
|
1065
|
-
|
1066
|
-
|
1067
|
-
|
1077
|
+
"network_resources": network_deps,
|
1078
|
+
"gateway_resources": gateway_deps,
|
1079
|
+
"security_resources": security_deps,
|
1080
|
+
"cross_account_resources": cross_account_deps,
|
1068
1081
|
}
|
1069
|
-
|
1082
|
+
|
1070
1083
|
# Create AWSO analysis result
|
1071
1084
|
awso_analysis = AWSOAnalysis(
|
1072
1085
|
default_vpcs=default_vpcs,
|
@@ -1074,14 +1087,14 @@ class VPCAnalyzer:
|
|
1074
1087
|
dependency_chain=dependency_chain,
|
1075
1088
|
eni_gate_warnings=eni_warnings,
|
1076
1089
|
cleanup_recommendations=cleanup_recommendations,
|
1077
|
-
evidence_bundle=evidence_bundle
|
1090
|
+
evidence_bundle=evidence_bundle,
|
1078
1091
|
)
|
1079
|
-
|
1092
|
+
|
1080
1093
|
self.last_awso_analysis = awso_analysis
|
1081
1094
|
self._display_awso_analysis(awso_analysis)
|
1082
|
-
|
1095
|
+
|
1083
1096
|
return awso_analysis
|
1084
|
-
|
1097
|
+
|
1085
1098
|
except Exception as e:
|
1086
1099
|
print_error(f"AWSO-05 analysis failed: {e}")
|
1087
1100
|
logger.error(f"AWSO-05 analysis error: {e}")
|
@@ -1090,73 +1103,73 @@ class VPCAnalyzer:
|
|
1090
1103
|
def generate_cleanup_evidence(self, output_dir: str = "./awso_evidence") -> Dict[str, str]:
|
1091
1104
|
"""
|
1092
1105
|
Generate comprehensive evidence bundle for AWSO-05 cleanup
|
1093
|
-
|
1106
|
+
|
1094
1107
|
Creates SHA256-verified evidence bundle with:
|
1095
1108
|
- Complete resource inventory (JSON)
|
1096
|
-
- Dependency analysis (JSON)
|
1109
|
+
- Dependency analysis (JSON)
|
1097
1110
|
- ENI gate validation results (JSON)
|
1098
1111
|
- Cleanup recommendations (JSON)
|
1099
1112
|
- Executive summary (Markdown)
|
1100
1113
|
- Evidence manifest with checksums
|
1101
|
-
|
1114
|
+
|
1102
1115
|
Args:
|
1103
1116
|
output_dir: Directory to store evidence files
|
1104
|
-
|
1117
|
+
|
1105
1118
|
Returns:
|
1106
1119
|
Dict with generated file paths and checksums
|
1107
1120
|
"""
|
1108
1121
|
print_header("Evidence Bundle Generation", "AWSO-05 Compliance")
|
1109
|
-
|
1122
|
+
|
1110
1123
|
output_path = Path(output_dir)
|
1111
1124
|
output_path.mkdir(parents=True, exist_ok=True)
|
1112
|
-
|
1125
|
+
|
1113
1126
|
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
1114
1127
|
evidence_files = {}
|
1115
|
-
|
1128
|
+
|
1116
1129
|
try:
|
1117
1130
|
# Generate discovery evidence
|
1118
1131
|
if self.last_discovery:
|
1119
1132
|
discovery_file = output_path / f"vpc_discovery_{timestamp}.json"
|
1120
1133
|
self._write_json_evidence(self.last_discovery.__dict__, discovery_file)
|
1121
|
-
evidence_files[
|
1122
|
-
|
1123
|
-
# Generate AWSO analysis evidence
|
1134
|
+
evidence_files["discovery"] = str(discovery_file)
|
1135
|
+
|
1136
|
+
# Generate AWSO analysis evidence
|
1124
1137
|
if self.last_awso_analysis:
|
1125
1138
|
awso_file = output_path / f"awso_analysis_{timestamp}.json"
|
1126
1139
|
self._write_json_evidence(self.last_awso_analysis.__dict__, awso_file)
|
1127
|
-
evidence_files[
|
1128
|
-
|
1140
|
+
evidence_files["awso_analysis"] = str(awso_file)
|
1141
|
+
|
1129
1142
|
# Generate executive summary
|
1130
1143
|
summary_file = output_path / f"executive_summary_{timestamp}.md"
|
1131
1144
|
self._write_executive_summary(self.last_awso_analysis, summary_file)
|
1132
|
-
evidence_files[
|
1133
|
-
|
1145
|
+
evidence_files["executive_summary"] = str(summary_file)
|
1146
|
+
|
1134
1147
|
# Generate evidence manifest with checksums
|
1135
1148
|
manifest_file = output_path / f"evidence_manifest_{timestamp}.json"
|
1136
1149
|
manifest = self._create_evidence_manifest(evidence_files)
|
1137
1150
|
self._write_json_evidence(manifest, manifest_file)
|
1138
|
-
evidence_files[
|
1139
|
-
|
1151
|
+
evidence_files["manifest"] = str(manifest_file)
|
1152
|
+
|
1140
1153
|
print_success(f"Evidence bundle generated: {len(evidence_files)} files")
|
1141
|
-
|
1154
|
+
|
1142
1155
|
# Display evidence summary
|
1143
1156
|
table = create_table(
|
1144
1157
|
title="AWSO-05 Evidence Bundle",
|
1145
1158
|
columns=[
|
1146
1159
|
{"header": "Evidence Type", "style": "cyan"},
|
1147
1160
|
{"header": "File Path", "style": "green"},
|
1148
|
-
{"header": "SHA256", "style": "dim"}
|
1149
|
-
]
|
1161
|
+
{"header": "SHA256", "style": "dim"},
|
1162
|
+
],
|
1150
1163
|
)
|
1151
|
-
|
1164
|
+
|
1152
1165
|
for evidence_type, file_path in evidence_files.items():
|
1153
|
-
sha256 = manifest.get(
|
1166
|
+
sha256 = manifest.get("file_checksums", {}).get(evidence_type, "N/A")
|
1154
1167
|
table.add_row(evidence_type, file_path, sha256[:16] + "...")
|
1155
|
-
|
1168
|
+
|
1156
1169
|
self.console.print(table)
|
1157
|
-
|
1170
|
+
|
1158
1171
|
return evidence_files
|
1159
|
-
|
1172
|
+
|
1160
1173
|
except Exception as e:
|
1161
1174
|
print_error(f"Evidence generation failed: {e}")
|
1162
1175
|
logger.error(f"Evidence generation error: {e}")
|
@@ -1168,27 +1181,27 @@ class VPCAnalyzer:
|
|
1168
1181
|
try:
|
1169
1182
|
filters = []
|
1170
1183
|
if vpc_ids:
|
1171
|
-
filters.append({
|
1172
|
-
|
1184
|
+
filters.append({"Name": "vpc-id", "Values": vpc_ids})
|
1185
|
+
|
1173
1186
|
response = ec2_client.describe_vpcs(Filters=filters)
|
1174
1187
|
vpcs = []
|
1175
|
-
|
1176
|
-
for vpc in response.get(
|
1188
|
+
|
1189
|
+
for vpc in response.get("Vpcs", []):
|
1177
1190
|
vpc_info = {
|
1178
|
-
|
1179
|
-
|
1180
|
-
|
1181
|
-
|
1182
|
-
|
1183
|
-
|
1184
|
-
|
1185
|
-
|
1186
|
-
|
1191
|
+
"VpcId": vpc["VpcId"],
|
1192
|
+
"CidrBlock": vpc["CidrBlock"],
|
1193
|
+
"State": vpc["State"],
|
1194
|
+
"IsDefault": vpc["IsDefault"],
|
1195
|
+
"InstanceTenancy": vpc["InstanceTenancy"],
|
1196
|
+
"DhcpOptionsId": vpc["DhcpOptionsId"],
|
1197
|
+
"Tags": {tag["Key"]: tag["Value"] for tag in vpc.get("Tags", [])},
|
1198
|
+
"Name": self._get_name_tag(vpc.get("Tags", [])),
|
1199
|
+
"DiscoveredAt": datetime.now().isoformat(),
|
1187
1200
|
}
|
1188
1201
|
vpcs.append(vpc_info)
|
1189
|
-
|
1202
|
+
|
1190
1203
|
return vpcs
|
1191
|
-
|
1204
|
+
|
1192
1205
|
except Exception as e:
|
1193
1206
|
logger.error(f"Failed to discover VPCs: {e}")
|
1194
1207
|
return []
|
@@ -1198,28 +1211,28 @@ class VPCAnalyzer:
|
|
1198
1211
|
try:
|
1199
1212
|
response = ec2_client.describe_nat_gateways()
|
1200
1213
|
nat_gateways = []
|
1201
|
-
|
1202
|
-
for nat in response.get(
|
1214
|
+
|
1215
|
+
for nat in response.get("NatGateways", []):
|
1203
1216
|
# Filter by VPC if specified
|
1204
|
-
if vpc_ids and nat.get(
|
1217
|
+
if vpc_ids and nat.get("VpcId") not in vpc_ids:
|
1205
1218
|
continue
|
1206
|
-
|
1219
|
+
|
1207
1220
|
nat_info = {
|
1208
|
-
|
1209
|
-
|
1210
|
-
|
1211
|
-
|
1212
|
-
|
1213
|
-
|
1214
|
-
|
1215
|
-
|
1216
|
-
|
1217
|
-
|
1221
|
+
"NatGatewayId": nat["NatGatewayId"],
|
1222
|
+
"VpcId": nat.get("VpcId"),
|
1223
|
+
"SubnetId": nat.get("SubnetId"),
|
1224
|
+
"State": nat["State"],
|
1225
|
+
"CreateTime": nat.get("CreateTime", "").isoformat() if nat.get("CreateTime") else None,
|
1226
|
+
"ConnectivityType": nat.get("ConnectivityType", "public"),
|
1227
|
+
"Tags": {tag["Key"]: tag["Value"] for tag in nat.get("Tags", [])},
|
1228
|
+
"Name": self._get_name_tag(nat.get("Tags", [])),
|
1229
|
+
"EstimatedMonthlyCost": 45.0, # Base NAT Gateway cost
|
1230
|
+
"DiscoveredAt": datetime.now().isoformat(),
|
1218
1231
|
}
|
1219
1232
|
nat_gateways.append(nat_info)
|
1220
|
-
|
1233
|
+
|
1221
1234
|
return nat_gateways
|
1222
|
-
|
1235
|
+
|
1223
1236
|
except Exception as e:
|
1224
1237
|
logger.error(f"Failed to discover NAT Gateways: {e}")
|
1225
1238
|
return []
|
@@ -1229,36 +1242,36 @@ class VPCAnalyzer:
|
|
1229
1242
|
try:
|
1230
1243
|
response = ec2_client.describe_vpc_endpoints()
|
1231
1244
|
endpoints = []
|
1232
|
-
|
1233
|
-
for endpoint in response.get(
|
1245
|
+
|
1246
|
+
for endpoint in response.get("VpcEndpoints", []):
|
1234
1247
|
# Filter by VPC if specified
|
1235
|
-
if vpc_ids and endpoint.get(
|
1248
|
+
if vpc_ids and endpoint.get("VpcId") not in vpc_ids:
|
1236
1249
|
continue
|
1237
|
-
|
1250
|
+
|
1238
1251
|
# Calculate costs
|
1239
1252
|
monthly_cost = 0
|
1240
|
-
if endpoint.get(
|
1241
|
-
az_count = len(endpoint.get(
|
1253
|
+
if endpoint.get("VpcEndpointType") == "Interface":
|
1254
|
+
az_count = len(endpoint.get("SubnetIds", []))
|
1242
1255
|
monthly_cost = 10.0 * az_count # $10/month per AZ
|
1243
|
-
|
1256
|
+
|
1244
1257
|
endpoint_info = {
|
1245
|
-
|
1246
|
-
|
1247
|
-
|
1248
|
-
|
1249
|
-
|
1250
|
-
|
1251
|
-
|
1252
|
-
|
1253
|
-
|
1254
|
-
|
1255
|
-
|
1256
|
-
|
1258
|
+
"VpcEndpointId": endpoint["VpcEndpointId"],
|
1259
|
+
"VpcId": endpoint.get("VpcId"),
|
1260
|
+
"ServiceName": endpoint.get("ServiceName"),
|
1261
|
+
"VpcEndpointType": endpoint.get("VpcEndpointType", "Gateway"),
|
1262
|
+
"State": endpoint.get("State"),
|
1263
|
+
"SubnetIds": endpoint.get("SubnetIds", []),
|
1264
|
+
"RouteTableIds": endpoint.get("RouteTableIds", []),
|
1265
|
+
"PolicyDocument": endpoint.get("PolicyDocument"),
|
1266
|
+
"Tags": {tag["Key"]: tag["Value"] for tag in endpoint.get("Tags", [])},
|
1267
|
+
"Name": self._get_name_tag(endpoint.get("Tags", [])),
|
1268
|
+
"EstimatedMonthlyCost": monthly_cost,
|
1269
|
+
"DiscoveredAt": datetime.now().isoformat(),
|
1257
1270
|
}
|
1258
1271
|
endpoints.append(endpoint_info)
|
1259
|
-
|
1272
|
+
|
1260
1273
|
return endpoints
|
1261
|
-
|
1274
|
+
|
1262
1275
|
except Exception as e:
|
1263
1276
|
logger.error(f"Failed to discover VPC Endpoints: {e}")
|
1264
1277
|
return []
|
@@ -1268,25 +1281,25 @@ class VPCAnalyzer:
|
|
1268
1281
|
try:
|
1269
1282
|
response = ec2_client.describe_internet_gateways()
|
1270
1283
|
igws = []
|
1271
|
-
|
1272
|
-
for igw in response.get(
|
1284
|
+
|
1285
|
+
for igw in response.get("InternetGateways", []):
|
1273
1286
|
# Filter by attached VPC if specified
|
1274
|
-
attached_vpc_ids = [attachment[
|
1287
|
+
attached_vpc_ids = [attachment["VpcId"] for attachment in igw.get("Attachments", [])]
|
1275
1288
|
if vpc_ids and not any(vpc_id in attached_vpc_ids for vpc_id in vpc_ids):
|
1276
1289
|
continue
|
1277
|
-
|
1290
|
+
|
1278
1291
|
igw_info = {
|
1279
|
-
|
1280
|
-
|
1281
|
-
|
1282
|
-
|
1283
|
-
|
1284
|
-
|
1292
|
+
"InternetGatewayId": igw["InternetGatewayId"],
|
1293
|
+
"Attachments": igw.get("Attachments", []),
|
1294
|
+
"AttachedVpcIds": attached_vpc_ids,
|
1295
|
+
"Tags": {tag["Key"]: tag["Value"] for tag in igw.get("Tags", [])},
|
1296
|
+
"Name": self._get_name_tag(igw.get("Tags", [])),
|
1297
|
+
"DiscoveredAt": datetime.now().isoformat(),
|
1285
1298
|
}
|
1286
1299
|
igws.append(igw_info)
|
1287
|
-
|
1300
|
+
|
1288
1301
|
return igws
|
1289
|
-
|
1302
|
+
|
1290
1303
|
except Exception as e:
|
1291
1304
|
logger.error(f"Failed to discover Internet Gateways: {e}")
|
1292
1305
|
return []
|
@@ -1296,27 +1309,29 @@ class VPCAnalyzer:
|
|
1296
1309
|
try:
|
1297
1310
|
filters = []
|
1298
1311
|
if vpc_ids:
|
1299
|
-
filters.append({
|
1300
|
-
|
1312
|
+
filters.append({"Name": "vpc-id", "Values": vpc_ids})
|
1313
|
+
|
1301
1314
|
response = ec2_client.describe_route_tables(Filters=filters)
|
1302
1315
|
route_tables = []
|
1303
|
-
|
1304
|
-
for rt in response.get(
|
1316
|
+
|
1317
|
+
for rt in response.get("RouteTables", []):
|
1305
1318
|
rt_info = {
|
1306
|
-
|
1307
|
-
|
1308
|
-
|
1309
|
-
|
1310
|
-
|
1311
|
-
|
1312
|
-
|
1313
|
-
|
1314
|
-
|
1319
|
+
"RouteTableId": rt["RouteTableId"],
|
1320
|
+
"VpcId": rt["VpcId"],
|
1321
|
+
"Routes": rt.get("Routes", []),
|
1322
|
+
"Associations": rt.get("Associations", []),
|
1323
|
+
"Tags": {tag["Key"]: tag["Value"] for tag in rt.get("Tags", [])},
|
1324
|
+
"Name": self._get_name_tag(rt.get("Tags", [])),
|
1325
|
+
"IsMainRouteTable": any(assoc.get("Main", False) for assoc in rt.get("Associations", [])),
|
1326
|
+
"AssociatedSubnets": [
|
1327
|
+
assoc.get("SubnetId") for assoc in rt.get("Associations", []) if assoc.get("SubnetId")
|
1328
|
+
],
|
1329
|
+
"DiscoveredAt": datetime.now().isoformat(),
|
1315
1330
|
}
|
1316
1331
|
route_tables.append(rt_info)
|
1317
|
-
|
1332
|
+
|
1318
1333
|
return route_tables
|
1319
|
-
|
1334
|
+
|
1320
1335
|
except Exception as e:
|
1321
1336
|
logger.error(f"Failed to discover Route Tables: {e}")
|
1322
1337
|
return []
|
@@ -1326,28 +1341,28 @@ class VPCAnalyzer:
|
|
1326
1341
|
try:
|
1327
1342
|
filters = []
|
1328
1343
|
if vpc_ids:
|
1329
|
-
filters.append({
|
1330
|
-
|
1344
|
+
filters.append({"Name": "vpc-id", "Values": vpc_ids})
|
1345
|
+
|
1331
1346
|
response = ec2_client.describe_subnets(Filters=filters)
|
1332
1347
|
subnets = []
|
1333
|
-
|
1334
|
-
for subnet in response.get(
|
1348
|
+
|
1349
|
+
for subnet in response.get("Subnets", []):
|
1335
1350
|
subnet_info = {
|
1336
|
-
|
1337
|
-
|
1338
|
-
|
1339
|
-
|
1340
|
-
|
1341
|
-
|
1342
|
-
|
1343
|
-
|
1344
|
-
|
1345
|
-
|
1351
|
+
"SubnetId": subnet["SubnetId"],
|
1352
|
+
"VpcId": subnet["VpcId"],
|
1353
|
+
"CidrBlock": subnet["CidrBlock"],
|
1354
|
+
"AvailabilityZone": subnet["AvailabilityZone"],
|
1355
|
+
"State": subnet["State"],
|
1356
|
+
"MapPublicIpOnLaunch": subnet.get("MapPublicIpOnLaunch", False),
|
1357
|
+
"AvailableIpAddressCount": subnet.get("AvailableIpAddressCount", 0),
|
1358
|
+
"Tags": {tag["Key"]: tag["Value"] for tag in subnet.get("Tags", [])},
|
1359
|
+
"Name": self._get_name_tag(subnet.get("Tags", [])),
|
1360
|
+
"DiscoveredAt": datetime.now().isoformat(),
|
1346
1361
|
}
|
1347
1362
|
subnets.append(subnet_info)
|
1348
|
-
|
1363
|
+
|
1349
1364
|
return subnets
|
1350
|
-
|
1365
|
+
|
1351
1366
|
except Exception as e:
|
1352
1367
|
logger.error(f"Failed to discover Subnets: {e}")
|
1353
1368
|
return []
|
@@ -1357,33 +1372,33 @@ class VPCAnalyzer:
|
|
1357
1372
|
try:
|
1358
1373
|
filters = []
|
1359
1374
|
if vpc_ids:
|
1360
|
-
filters.append({
|
1361
|
-
|
1375
|
+
filters.append({"Name": "vpc-id", "Values": vpc_ids})
|
1376
|
+
|
1362
1377
|
response = ec2_client.describe_network_interfaces(Filters=filters)
|
1363
1378
|
network_interfaces = []
|
1364
|
-
|
1365
|
-
for eni in response.get(
|
1379
|
+
|
1380
|
+
for eni in response.get("NetworkInterfaces", []):
|
1366
1381
|
eni_info = {
|
1367
|
-
|
1368
|
-
|
1369
|
-
|
1370
|
-
|
1371
|
-
|
1372
|
-
|
1373
|
-
|
1374
|
-
|
1375
|
-
|
1376
|
-
|
1377
|
-
|
1378
|
-
|
1379
|
-
|
1380
|
-
|
1381
|
-
|
1382
|
+
"NetworkInterfaceId": eni["NetworkInterfaceId"],
|
1383
|
+
"VpcId": eni.get("VpcId"),
|
1384
|
+
"SubnetId": eni.get("SubnetId"),
|
1385
|
+
"Status": eni.get("Status"),
|
1386
|
+
"InterfaceType": eni.get("InterfaceType", "interface"),
|
1387
|
+
"Attachment": eni.get("Attachment"),
|
1388
|
+
"Groups": eni.get("Groups", []),
|
1389
|
+
"PrivateIpAddress": eni.get("PrivateIpAddress"),
|
1390
|
+
"PrivateIpAddresses": eni.get("PrivateIpAddresses", []),
|
1391
|
+
"Tags": {tag["Key"]: tag["Value"] for tag in eni.get("Tags", [])},
|
1392
|
+
"Name": self._get_name_tag(eni.get("Tags", [])),
|
1393
|
+
"RequesterManaged": eni.get("RequesterManaged", False),
|
1394
|
+
"IsAttached": bool(eni.get("Attachment")),
|
1395
|
+
"AttachedInstanceId": eni.get("Attachment", {}).get("InstanceId"),
|
1396
|
+
"DiscoveredAt": datetime.now().isoformat(),
|
1382
1397
|
}
|
1383
1398
|
network_interfaces.append(eni_info)
|
1384
|
-
|
1399
|
+
|
1385
1400
|
return network_interfaces
|
1386
|
-
|
1401
|
+
|
1387
1402
|
except Exception as e:
|
1388
1403
|
logger.error(f"Failed to discover Network Interfaces: {e}")
|
1389
1404
|
return []
|
@@ -1393,27 +1408,27 @@ class VPCAnalyzer:
|
|
1393
1408
|
try:
|
1394
1409
|
response = ec2_client.describe_transit_gateway_attachments()
|
1395
1410
|
attachments = []
|
1396
|
-
|
1397
|
-
for attachment in response.get(
|
1411
|
+
|
1412
|
+
for attachment in response.get("TransitGatewayAttachments", []):
|
1398
1413
|
# Filter by VPC if specified
|
1399
|
-
if vpc_ids and attachment.get(
|
1414
|
+
if vpc_ids and attachment.get("ResourceType") == "vpc" and attachment.get("ResourceId") not in vpc_ids:
|
1400
1415
|
continue
|
1401
|
-
|
1416
|
+
|
1402
1417
|
attachment_info = {
|
1403
|
-
|
1404
|
-
|
1405
|
-
|
1406
|
-
|
1407
|
-
|
1408
|
-
|
1409
|
-
|
1410
|
-
|
1411
|
-
|
1418
|
+
"TransitGatewayAttachmentId": attachment["TransitGatewayAttachmentId"],
|
1419
|
+
"TransitGatewayId": attachment.get("TransitGatewayId"),
|
1420
|
+
"ResourceType": attachment.get("ResourceType"),
|
1421
|
+
"ResourceId": attachment.get("ResourceId"),
|
1422
|
+
"State": attachment.get("State"),
|
1423
|
+
"Tags": {tag["Key"]: tag["Value"] for tag in attachment.get("Tags", [])},
|
1424
|
+
"Name": self._get_name_tag(attachment.get("Tags", [])),
|
1425
|
+
"ResourceOwnerId": attachment.get("ResourceOwnerId"),
|
1426
|
+
"DiscoveredAt": datetime.now().isoformat(),
|
1412
1427
|
}
|
1413
1428
|
attachments.append(attachment_info)
|
1414
|
-
|
1429
|
+
|
1415
1430
|
return attachments
|
1416
|
-
|
1431
|
+
|
1417
1432
|
except Exception as e:
|
1418
1433
|
logger.error(f"Failed to discover Transit Gateway Attachments: {e}")
|
1419
1434
|
return []
|
@@ -1423,29 +1438,29 @@ class VPCAnalyzer:
|
|
1423
1438
|
try:
|
1424
1439
|
response = ec2_client.describe_vpc_peering_connections()
|
1425
1440
|
connections = []
|
1426
|
-
|
1427
|
-
for connection in response.get(
|
1428
|
-
accepter_vpc_id = connection.get(
|
1429
|
-
requester_vpc_id = connection.get(
|
1430
|
-
|
1441
|
+
|
1442
|
+
for connection in response.get("VpcPeeringConnections", []):
|
1443
|
+
accepter_vpc_id = connection.get("AccepterVpcInfo", {}).get("VpcId")
|
1444
|
+
requester_vpc_id = connection.get("RequesterVpcInfo", {}).get("VpcId")
|
1445
|
+
|
1431
1446
|
# Filter by VPC if specified
|
1432
1447
|
if vpc_ids and accepter_vpc_id not in vpc_ids and requester_vpc_id not in vpc_ids:
|
1433
1448
|
continue
|
1434
|
-
|
1449
|
+
|
1435
1450
|
connection_info = {
|
1436
|
-
|
1437
|
-
|
1438
|
-
|
1439
|
-
|
1440
|
-
|
1441
|
-
|
1442
|
-
|
1443
|
-
|
1451
|
+
"VpcPeeringConnectionId": connection["VpcPeeringConnectionId"],
|
1452
|
+
"AccepterVpcInfo": connection.get("AccepterVpcInfo", {}),
|
1453
|
+
"RequesterVpcInfo": connection.get("RequesterVpcInfo", {}),
|
1454
|
+
"Status": connection.get("Status", {}),
|
1455
|
+
"Tags": {tag["Key"]: tag["Value"] for tag in connection.get("Tags", [])},
|
1456
|
+
"Name": self._get_name_tag(connection.get("Tags", [])),
|
1457
|
+
"ExpirationTime": connection.get("ExpirationTime"),
|
1458
|
+
"DiscoveredAt": datetime.now().isoformat(),
|
1444
1459
|
}
|
1445
1460
|
connections.append(connection_info)
|
1446
|
-
|
1461
|
+
|
1447
1462
|
return connections
|
1448
|
-
|
1463
|
+
|
1449
1464
|
except Exception as e:
|
1450
1465
|
logger.error(f"Failed to discover VPC Peering Connections: {e}")
|
1451
1466
|
return []
|
@@ -1455,28 +1470,28 @@ class VPCAnalyzer:
|
|
1455
1470
|
try:
|
1456
1471
|
filters = []
|
1457
1472
|
if vpc_ids:
|
1458
|
-
filters.append({
|
1459
|
-
|
1473
|
+
filters.append({"Name": "vpc-id", "Values": vpc_ids})
|
1474
|
+
|
1460
1475
|
response = ec2_client.describe_security_groups(Filters=filters)
|
1461
1476
|
security_groups = []
|
1462
|
-
|
1463
|
-
for sg in response.get(
|
1477
|
+
|
1478
|
+
for sg in response.get("SecurityGroups", []):
|
1464
1479
|
sg_info = {
|
1465
|
-
|
1466
|
-
|
1467
|
-
|
1468
|
-
|
1469
|
-
|
1470
|
-
|
1471
|
-
|
1472
|
-
|
1473
|
-
|
1474
|
-
|
1480
|
+
"GroupId": sg["GroupId"],
|
1481
|
+
"GroupName": sg["GroupName"],
|
1482
|
+
"VpcId": sg.get("VpcId"),
|
1483
|
+
"Description": sg.get("Description", ""),
|
1484
|
+
"IpPermissions": sg.get("IpPermissions", []),
|
1485
|
+
"IpPermissionsEgress": sg.get("IpPermissionsEgress", []),
|
1486
|
+
"Tags": {tag["Key"]: tag["Value"] for tag in sg.get("Tags", [])},
|
1487
|
+
"Name": self._get_name_tag(sg.get("Tags", [])),
|
1488
|
+
"IsDefault": sg.get("GroupName") == "default",
|
1489
|
+
"DiscoveredAt": datetime.now().isoformat(),
|
1475
1490
|
}
|
1476
1491
|
security_groups.append(sg_info)
|
1477
|
-
|
1492
|
+
|
1478
1493
|
return security_groups
|
1479
|
-
|
1494
|
+
|
1480
1495
|
except Exception as e:
|
1481
1496
|
logger.error(f"Failed to discover Security Groups: {e}")
|
1482
1497
|
return []
|
@@ -1485,241 +1500,258 @@ class VPCAnalyzer:
|
|
1485
1500
|
def _analyze_eni_gate_validation(self, discovery: VPCDiscoveryResult) -> List[Dict[str, Any]]:
|
1486
1501
|
"""AWSO-05 Step 1: Critical ENI gate validation to prevent workload disruption"""
|
1487
1502
|
warnings = []
|
1488
|
-
|
1503
|
+
|
1489
1504
|
for eni in discovery.network_interfaces:
|
1490
1505
|
# Check for attached ENIs that could indicate active workloads
|
1491
|
-
if eni[
|
1492
|
-
warnings.append(
|
1493
|
-
|
1494
|
-
|
1495
|
-
|
1496
|
-
|
1497
|
-
|
1498
|
-
|
1499
|
-
|
1500
|
-
|
1501
|
-
|
1506
|
+
if eni["IsAttached"] and not eni["RequesterManaged"]:
|
1507
|
+
warnings.append(
|
1508
|
+
{
|
1509
|
+
"NetworkInterfaceId": eni["NetworkInterfaceId"],
|
1510
|
+
"VpcId": eni["VpcId"],
|
1511
|
+
"AttachedInstanceId": eni.get("AttachedInstanceId"),
|
1512
|
+
"WarningType": "ATTACHED_ENI",
|
1513
|
+
"RiskLevel": "HIGH",
|
1514
|
+
"Message": f"ENI {eni['NetworkInterfaceId']} is attached to instance {eni.get('AttachedInstanceId')} - VPC cleanup may disrupt workload",
|
1515
|
+
"Recommendation": "Verify workload migration before VPC cleanup",
|
1516
|
+
}
|
1517
|
+
)
|
1518
|
+
|
1502
1519
|
return warnings
|
1503
1520
|
|
1504
1521
|
def _analyze_network_dependencies(self, discovery: VPCDiscoveryResult) -> Dict[str, List[str]]:
|
1505
1522
|
"""AWSO-05 Steps 2-4: Network resource dependency analysis"""
|
1506
1523
|
dependencies = {}
|
1507
|
-
|
1524
|
+
|
1508
1525
|
# NAT Gateway dependencies
|
1509
1526
|
for nat in discovery.nat_gateways:
|
1510
|
-
vpc_id = nat[
|
1527
|
+
vpc_id = nat["VpcId"]
|
1511
1528
|
if vpc_id not in dependencies:
|
1512
1529
|
dependencies[vpc_id] = []
|
1513
1530
|
dependencies[vpc_id].append(f"NAT Gateway: {nat['NatGatewayId']}")
|
1514
|
-
|
1531
|
+
|
1515
1532
|
# VPC Endpoint dependencies
|
1516
1533
|
for endpoint in discovery.vpc_endpoints:
|
1517
|
-
vpc_id = endpoint[
|
1534
|
+
vpc_id = endpoint["VpcId"]
|
1518
1535
|
if vpc_id not in dependencies:
|
1519
1536
|
dependencies[vpc_id] = []
|
1520
1537
|
dependencies[vpc_id].append(f"VPC Endpoint: {endpoint['VpcEndpointId']}")
|
1521
|
-
|
1538
|
+
|
1522
1539
|
return dependencies
|
1523
1540
|
|
1524
1541
|
def _analyze_gateway_dependencies(self, discovery: VPCDiscoveryResult) -> Dict[str, List[str]]:
|
1525
1542
|
"""AWSO-05 Steps 5-7: Gateway dependency analysis"""
|
1526
1543
|
dependencies = {}
|
1527
|
-
|
1544
|
+
|
1528
1545
|
# Internet Gateway dependencies
|
1529
1546
|
for igw in discovery.internet_gateways:
|
1530
|
-
for vpc_id in igw[
|
1547
|
+
for vpc_id in igw["AttachedVpcIds"]:
|
1531
1548
|
if vpc_id not in dependencies:
|
1532
1549
|
dependencies[vpc_id] = []
|
1533
1550
|
dependencies[vpc_id].append(f"Internet Gateway: {igw['InternetGatewayId']}")
|
1534
|
-
|
1551
|
+
|
1535
1552
|
# Transit Gateway Attachment dependencies
|
1536
1553
|
for attachment in discovery.transit_gateway_attachments:
|
1537
|
-
if attachment[
|
1538
|
-
vpc_id = attachment[
|
1554
|
+
if attachment["ResourceType"] == "vpc":
|
1555
|
+
vpc_id = attachment["ResourceId"]
|
1539
1556
|
if vpc_id not in dependencies:
|
1540
1557
|
dependencies[vpc_id] = []
|
1541
1558
|
dependencies[vpc_id].append(f"Transit Gateway Attachment: {attachment['TransitGatewayAttachmentId']}")
|
1542
|
-
|
1559
|
+
|
1543
1560
|
return dependencies
|
1544
1561
|
|
1545
1562
|
def _analyze_security_dependencies(self, discovery: VPCDiscoveryResult) -> Dict[str, List[str]]:
|
1546
1563
|
"""AWSO-05 Steps 8-10: Security and route dependency analysis"""
|
1547
1564
|
dependencies = {}
|
1548
|
-
|
1565
|
+
|
1549
1566
|
# Route Table dependencies
|
1550
1567
|
for rt in discovery.route_tables:
|
1551
|
-
vpc_id = rt[
|
1568
|
+
vpc_id = rt["VpcId"]
|
1552
1569
|
if vpc_id not in dependencies:
|
1553
1570
|
dependencies[vpc_id] = []
|
1554
|
-
if not rt[
|
1571
|
+
if not rt["IsMainRouteTable"]: # Don't list main route tables as dependencies
|
1555
1572
|
dependencies[vpc_id].append(f"Route Table: {rt['RouteTableId']}")
|
1556
|
-
|
1573
|
+
|
1557
1574
|
# Security Group dependencies (non-default)
|
1558
1575
|
for sg in discovery.security_groups:
|
1559
|
-
if not sg[
|
1560
|
-
vpc_id = sg[
|
1576
|
+
if not sg["IsDefault"]:
|
1577
|
+
vpc_id = sg["VpcId"]
|
1561
1578
|
if vpc_id not in dependencies:
|
1562
1579
|
dependencies[vpc_id] = []
|
1563
1580
|
dependencies[vpc_id].append(f"Security Group: {sg['GroupId']}")
|
1564
|
-
|
1581
|
+
|
1565
1582
|
return dependencies
|
1566
1583
|
|
1567
1584
|
def _analyze_cross_account_dependencies(self, discovery: VPCDiscoveryResult) -> Dict[str, List[str]]:
|
1568
1585
|
"""AWSO-05 Step 11: Cross-account dependency analysis"""
|
1569
1586
|
dependencies = {}
|
1570
|
-
|
1587
|
+
|
1571
1588
|
# VPC Peering cross-account connections
|
1572
1589
|
for connection in discovery.vpc_peering_connections:
|
1573
|
-
accepter_vpc = connection[
|
1574
|
-
requester_vpc = connection[
|
1575
|
-
|
1590
|
+
accepter_vpc = connection["AccepterVpcInfo"]
|
1591
|
+
requester_vpc = connection["RequesterVpcInfo"]
|
1592
|
+
|
1576
1593
|
# Check for cross-account peering
|
1577
|
-
if accepter_vpc.get(
|
1594
|
+
if accepter_vpc.get("OwnerId") != requester_vpc.get("OwnerId"):
|
1578
1595
|
for vpc_info in [accepter_vpc, requester_vpc]:
|
1579
|
-
vpc_id = vpc_info.get(
|
1596
|
+
vpc_id = vpc_info.get("VpcId")
|
1580
1597
|
if vpc_id:
|
1581
1598
|
if vpc_id not in dependencies:
|
1582
1599
|
dependencies[vpc_id] = []
|
1583
|
-
dependencies[vpc_id].append(
|
1584
|
-
|
1600
|
+
dependencies[vpc_id].append(
|
1601
|
+
f"Cross-Account VPC Peering: {connection['VpcPeeringConnectionId']}"
|
1602
|
+
)
|
1603
|
+
|
1585
1604
|
return dependencies
|
1586
1605
|
|
1587
1606
|
def _identify_default_vpcs(self, discovery: VPCDiscoveryResult) -> List[Dict[str, Any]]:
|
1588
1607
|
"""AWSO-05 Step 12: Identify default VPCs for CIS Benchmark compliance"""
|
1589
1608
|
default_vpcs = []
|
1590
|
-
|
1609
|
+
|
1591
1610
|
for vpc in discovery.vpcs:
|
1592
|
-
if vpc[
|
1611
|
+
if vpc["IsDefault"]:
|
1593
1612
|
# Check for resources in default VPC
|
1594
1613
|
resources_in_vpc = []
|
1595
|
-
|
1614
|
+
|
1596
1615
|
# Count ENIs (excluding AWS managed)
|
1597
|
-
eni_count = len(
|
1598
|
-
|
1616
|
+
eni_count = len(
|
1617
|
+
[
|
1618
|
+
eni
|
1619
|
+
for eni in discovery.network_interfaces
|
1620
|
+
if eni["VpcId"] == vpc["VpcId"] and not eni["RequesterManaged"]
|
1621
|
+
]
|
1622
|
+
)
|
1599
1623
|
if eni_count > 0:
|
1600
1624
|
resources_in_vpc.append(f"{eni_count} Network Interfaces")
|
1601
|
-
|
1625
|
+
|
1602
1626
|
# Count NAT Gateways
|
1603
|
-
nat_count = len([nat for nat in discovery.nat_gateways if nat[
|
1627
|
+
nat_count = len([nat for nat in discovery.nat_gateways if nat["VpcId"] == vpc["VpcId"]])
|
1604
1628
|
if nat_count > 0:
|
1605
1629
|
resources_in_vpc.append(f"{nat_count} NAT Gateways")
|
1606
|
-
|
1630
|
+
|
1607
1631
|
# Count VPC Endpoints
|
1608
|
-
endpoint_count = len([ep for ep in discovery.vpc_endpoints if ep[
|
1632
|
+
endpoint_count = len([ep for ep in discovery.vpc_endpoints if ep["VpcId"] == vpc["VpcId"]])
|
1609
1633
|
if endpoint_count > 0:
|
1610
1634
|
resources_in_vpc.append(f"{endpoint_count} VPC Endpoints")
|
1611
|
-
|
1635
|
+
|
1612
1636
|
default_vpc_info = {
|
1613
|
-
|
1614
|
-
|
1615
|
-
|
1616
|
-
|
1617
|
-
|
1618
|
-
|
1619
|
-
|
1620
|
-
|
1637
|
+
"VpcId": vpc["VpcId"],
|
1638
|
+
"CidrBlock": vpc["CidrBlock"],
|
1639
|
+
"Region": self.region,
|
1640
|
+
"ResourcesPresent": resources_in_vpc,
|
1641
|
+
"ResourceCount": len(resources_in_vpc),
|
1642
|
+
"CleanupRecommendation": "DELETE" if len(resources_in_vpc) == 0 else "MIGRATE_RESOURCES_FIRST",
|
1643
|
+
"CISBenchmarkCompliance": "NON_COMPLIANT",
|
1644
|
+
"SecurityRisk": "HIGH" if len(resources_in_vpc) > 0 else "MEDIUM",
|
1621
1645
|
}
|
1622
1646
|
default_vpcs.append(default_vpc_info)
|
1623
|
-
|
1647
|
+
|
1624
1648
|
return default_vpcs
|
1625
1649
|
|
1626
1650
|
def _identify_orphaned_resources(self, discovery: VPCDiscoveryResult) -> List[Dict[str, Any]]:
|
1627
1651
|
"""Identify orphaned resources that can be safely cleaned up"""
|
1628
1652
|
orphaned = []
|
1629
|
-
|
1653
|
+
|
1630
1654
|
# Orphaned NAT Gateways (no route table references)
|
1631
1655
|
used_nat_gateways = set()
|
1632
1656
|
for rt in discovery.route_tables:
|
1633
|
-
for route in rt[
|
1634
|
-
if route.get(
|
1635
|
-
used_nat_gateways.add(route[
|
1636
|
-
|
1657
|
+
for route in rt["Routes"]:
|
1658
|
+
if route.get("NatGatewayId"):
|
1659
|
+
used_nat_gateways.add(route["NatGatewayId"])
|
1660
|
+
|
1637
1661
|
for nat in discovery.nat_gateways:
|
1638
|
-
if nat[
|
1639
|
-
orphaned.append(
|
1640
|
-
|
1641
|
-
|
1642
|
-
|
1643
|
-
|
1644
|
-
|
1645
|
-
|
1646
|
-
|
1662
|
+
if nat["NatGatewayId"] not in used_nat_gateways and nat["State"] == "available":
|
1663
|
+
orphaned.append(
|
1664
|
+
{
|
1665
|
+
"ResourceType": "NAT Gateway",
|
1666
|
+
"ResourceId": nat["NatGatewayId"],
|
1667
|
+
"VpcId": nat["VpcId"],
|
1668
|
+
"Reason": "No route table references",
|
1669
|
+
"EstimatedMonthlySavings": nat["EstimatedMonthlyCost"],
|
1670
|
+
}
|
1671
|
+
)
|
1672
|
+
|
1647
1673
|
return orphaned
|
1648
1674
|
|
1649
|
-
def _generate_cleanup_recommendations(
|
1650
|
-
|
1651
|
-
|
1675
|
+
def _generate_cleanup_recommendations(
|
1676
|
+
self, discovery: VPCDiscoveryResult, eni_warnings: List[Dict], default_vpcs: List[Dict]
|
1677
|
+
) -> List[Dict[str, Any]]:
|
1652
1678
|
"""Generate AWSO-05 cleanup recommendations"""
|
1653
1679
|
recommendations = []
|
1654
|
-
|
1680
|
+
|
1655
1681
|
# Default VPC cleanup recommendations
|
1656
1682
|
for default_vpc in default_vpcs:
|
1657
|
-
if default_vpc[
|
1658
|
-
recommendations.append(
|
1659
|
-
|
1660
|
-
|
1661
|
-
|
1662
|
-
|
1663
|
-
|
1664
|
-
|
1665
|
-
|
1666
|
-
|
1667
|
-
|
1683
|
+
if default_vpc["CleanupRecommendation"] == "DELETE":
|
1684
|
+
recommendations.append(
|
1685
|
+
{
|
1686
|
+
"Priority": "HIGH",
|
1687
|
+
"Action": "DELETE_DEFAULT_VPC",
|
1688
|
+
"ResourceType": "VPC",
|
1689
|
+
"ResourceId": default_vpc["VpcId"],
|
1690
|
+
"Reason": "Empty default VPC - CIS Benchmark compliance",
|
1691
|
+
"EstimatedMonthlySavings": 0,
|
1692
|
+
"SecurityBenefit": "Reduces attack surface",
|
1693
|
+
"RiskLevel": "LOW",
|
1694
|
+
}
|
1695
|
+
)
|
1668
1696
|
else:
|
1669
|
-
recommendations.append(
|
1670
|
-
|
1671
|
-
|
1672
|
-
|
1673
|
-
|
1674
|
-
|
1675
|
-
|
1676
|
-
|
1677
|
-
|
1678
|
-
|
1679
|
-
|
1697
|
+
recommendations.append(
|
1698
|
+
{
|
1699
|
+
"Priority": "MEDIUM",
|
1700
|
+
"Action": "MIGRATE_FROM_DEFAULT_VPC",
|
1701
|
+
"ResourceType": "VPC",
|
1702
|
+
"ResourceId": default_vpc["VpcId"],
|
1703
|
+
"Reason": "Default VPC with resources - requires migration",
|
1704
|
+
"EstimatedMonthlySavings": 0,
|
1705
|
+
"SecurityBenefit": "Improves security posture",
|
1706
|
+
"RiskLevel": "HIGH",
|
1707
|
+
}
|
1708
|
+
)
|
1709
|
+
|
1680
1710
|
# ENI-based recommendations
|
1681
1711
|
if eni_warnings:
|
1682
|
-
recommendations.append(
|
1683
|
-
|
1684
|
-
|
1685
|
-
|
1686
|
-
|
1687
|
-
|
1688
|
-
|
1689
|
-
|
1690
|
-
|
1691
|
-
|
1692
|
-
|
1712
|
+
recommendations.append(
|
1713
|
+
{
|
1714
|
+
"Priority": "CRITICAL",
|
1715
|
+
"Action": "REVIEW_WORKLOAD_MIGRATION",
|
1716
|
+
"ResourceType": "Multiple",
|
1717
|
+
"ResourceId": "Multiple ENIs",
|
1718
|
+
"Reason": f"{len(eni_warnings)} attached ENIs detected - workload migration required",
|
1719
|
+
"EstimatedMonthlySavings": 0,
|
1720
|
+
"SecurityBenefit": "Prevents workload disruption",
|
1721
|
+
"RiskLevel": "CRITICAL",
|
1722
|
+
}
|
1723
|
+
)
|
1724
|
+
|
1693
1725
|
return recommendations
|
1694
1726
|
|
1695
1727
|
def _create_evidence_bundle(self, discovery: VPCDiscoveryResult, analysis_data: Dict) -> Dict[str, Any]:
|
1696
1728
|
"""Create comprehensive evidence bundle for AWSO-05 compliance"""
|
1697
1729
|
return {
|
1698
|
-
|
1699
|
-
|
1700
|
-
|
1701
|
-
|
1702
|
-
|
1703
|
-
|
1704
|
-
|
1705
|
-
|
1706
|
-
|
1730
|
+
"BundleVersion": "1.0",
|
1731
|
+
"GeneratedAt": datetime.now().isoformat(),
|
1732
|
+
"Profile": self.profile,
|
1733
|
+
"Region": self.region,
|
1734
|
+
"DiscoverySummary": {
|
1735
|
+
"TotalVPCs": len(discovery.vpcs),
|
1736
|
+
"DefaultVPCs": len(analysis_data["default_vpcs"]),
|
1737
|
+
"TotalResources": discovery.total_resources,
|
1738
|
+
"ENIWarnings": len(analysis_data["eni_warnings"]),
|
1707
1739
|
},
|
1708
|
-
|
1709
|
-
|
1710
|
-
|
1740
|
+
"ComplianceStatus": {
|
1741
|
+
"CISBenchmark": "NON_COMPLIANT" if analysis_data["default_vpcs"] else "COMPLIANT",
|
1742
|
+
"ENIGateValidation": "PASSED" if not analysis_data["eni_warnings"] else "WARNINGS_PRESENT",
|
1711
1743
|
},
|
1712
|
-
|
1744
|
+
"CleanupReadiness": "READY" if not analysis_data["eni_warnings"] else "REQUIRES_WORKLOAD_MIGRATION",
|
1713
1745
|
}
|
1714
1746
|
|
1715
1747
|
# NEW: Convenience methods for CLI integration
|
1716
1748
|
def discover_landing_zone_vpc_topology(self) -> VPCDiscoveryResult:
|
1717
1749
|
"""
|
1718
1750
|
Convenience method for CLI integration - Multi-Organization Landing Zone discovery
|
1719
|
-
|
1751
|
+
|
1720
1752
|
Automatically enables multi-account mode and discovers VPC topology across
|
1721
1753
|
60-account Landing Zone with decommissioned account filtering.
|
1722
|
-
|
1754
|
+
|
1723
1755
|
Returns:
|
1724
1756
|
VPCDiscoveryResult with comprehensive Landing Zone topology
|
1725
1757
|
"""
|
@@ -1727,12 +1759,10 @@ class VPCAnalyzer:
|
|
1727
1759
|
# Auto-enable multi-account mode for Landing Zone discovery
|
1728
1760
|
self.enable_multi_account = True
|
1729
1761
|
self.cross_account_manager = EnhancedCrossAccountManager(
|
1730
|
-
base_profile=self.profile,
|
1731
|
-
max_workers=self.max_workers,
|
1732
|
-
session_ttl_minutes=240
|
1762
|
+
base_profile=self.profile, max_workers=self.max_workers, session_ttl_minutes=240
|
1733
1763
|
)
|
1734
1764
|
print_info("🌐 Auto-enabled Multi-Organization Landing Zone mode")
|
1735
|
-
|
1765
|
+
|
1736
1766
|
# Use asyncio.run for CLI compatibility
|
1737
1767
|
return asyncio.run(self.discover_multi_org_vpc_topology())
|
1738
1768
|
|
@@ -1740,7 +1770,7 @@ class VPCAnalyzer:
|
|
1740
1770
|
"""Get comprehensive Landing Zone session summary for reporting"""
|
1741
1771
|
if not self.landing_zone_sessions or not self.cross_account_manager:
|
1742
1772
|
return None
|
1743
|
-
|
1773
|
+
|
1744
1774
|
return self.cross_account_manager.get_session_summary(self.landing_zone_sessions)
|
1745
1775
|
|
1746
1776
|
def refresh_landing_zone_sessions(self) -> bool:
|
@@ -1748,44 +1778,52 @@ class VPCAnalyzer:
|
|
1748
1778
|
if not self.landing_zone_sessions or not self.cross_account_manager:
|
1749
1779
|
print_warning("No Landing Zone sessions to refresh")
|
1750
1780
|
return False
|
1751
|
-
|
1781
|
+
|
1752
1782
|
print_info("🔄 Refreshing Landing Zone sessions...")
|
1753
|
-
self.landing_zone_sessions = self.cross_account_manager.refresh_expired_sessions(
|
1754
|
-
|
1755
|
-
)
|
1756
|
-
|
1757
|
-
successful_sessions = len([s for s in self.landing_zone_sessions if s.status in ['success', 'cached']])
|
1783
|
+
self.landing_zone_sessions = self.cross_account_manager.refresh_expired_sessions(self.landing_zone_sessions)
|
1784
|
+
|
1785
|
+
successful_sessions = len([s for s in self.landing_zone_sessions if s.status in ["success", "cached"]])
|
1758
1786
|
print_success(f"✅ Session refresh complete: {successful_sessions} sessions ready")
|
1759
|
-
|
1787
|
+
|
1760
1788
|
return successful_sessions > 0
|
1761
1789
|
|
1762
1790
|
# Helper methods
|
1763
1791
|
def _empty_discovery_result(self) -> VPCDiscoveryResult:
|
1764
1792
|
"""Return empty discovery result with Landing Zone structure"""
|
1765
1793
|
return VPCDiscoveryResult(
|
1766
|
-
vpcs=[],
|
1767
|
-
|
1768
|
-
|
1769
|
-
|
1794
|
+
vpcs=[],
|
1795
|
+
nat_gateways=[],
|
1796
|
+
vpc_endpoints=[],
|
1797
|
+
internet_gateways=[],
|
1798
|
+
route_tables=[],
|
1799
|
+
subnets=[],
|
1800
|
+
network_interfaces=[],
|
1801
|
+
transit_gateway_attachments=[],
|
1802
|
+
vpc_peering_connections=[],
|
1803
|
+
security_groups=[],
|
1804
|
+
total_resources=0,
|
1770
1805
|
discovery_timestamp=datetime.now().isoformat(),
|
1771
1806
|
account_summary=None,
|
1772
|
-
landing_zone_metrics=None
|
1807
|
+
landing_zone_metrics=None,
|
1773
1808
|
)
|
1774
1809
|
|
1775
1810
|
def _empty_awso_analysis(self) -> AWSOAnalysis:
|
1776
1811
|
"""Return empty AWSO analysis result"""
|
1777
1812
|
return AWSOAnalysis(
|
1778
|
-
default_vpcs=[],
|
1779
|
-
|
1780
|
-
|
1813
|
+
default_vpcs=[],
|
1814
|
+
orphaned_resources=[],
|
1815
|
+
dependency_chain={},
|
1816
|
+
eni_gate_warnings=[],
|
1817
|
+
cleanup_recommendations=[],
|
1818
|
+
evidence_bundle={},
|
1781
1819
|
)
|
1782
1820
|
|
1783
1821
|
def _get_name_tag(self, tags: List[Dict]) -> str:
|
1784
1822
|
"""Extract Name tag from tag list"""
|
1785
1823
|
for tag in tags:
|
1786
|
-
if tag[
|
1787
|
-
return tag[
|
1788
|
-
return
|
1824
|
+
if tag["Key"] == "Name":
|
1825
|
+
return tag["Value"]
|
1826
|
+
return "Unnamed"
|
1789
1827
|
|
1790
1828
|
def _display_discovery_results(self, result: VPCDiscoveryResult):
|
1791
1829
|
"""Display VPC discovery results with Rich formatting"""
|
@@ -1804,7 +1842,7 @@ class VPCAnalyzer:
|
|
1804
1842
|
f"Security Groups: [bold gray]{len(result.security_groups)}[/bold gray]\n\n"
|
1805
1843
|
f"[dim]Total Resources: {result.total_resources}[/dim]",
|
1806
1844
|
title="🔍 VPC Discovery Summary",
|
1807
|
-
style="bold blue"
|
1845
|
+
style="bold blue",
|
1808
1846
|
)
|
1809
1847
|
self.console.print(summary)
|
1810
1848
|
|
@@ -1812,39 +1850,39 @@ class VPCAnalyzer:
|
|
1812
1850
|
"""Display AWSO-05 analysis results with Rich formatting"""
|
1813
1851
|
# Create summary tree
|
1814
1852
|
tree = Tree("🎯 AWSO-05 Analysis Results")
|
1815
|
-
|
1853
|
+
|
1816
1854
|
# Default VPCs branch
|
1817
1855
|
default_branch = tree.add("🚨 Default VPCs")
|
1818
1856
|
for vpc in analysis.default_vpcs:
|
1819
|
-
status = "🔴 Non-Compliant" if vpc[
|
1857
|
+
status = "🔴 Non-Compliant" if vpc["SecurityRisk"] == "HIGH" else "🟡 Requires Review"
|
1820
1858
|
default_branch.add(f"{vpc['VpcId']} - {status}")
|
1821
|
-
|
1859
|
+
|
1822
1860
|
# ENI Warnings branch
|
1823
1861
|
eni_branch = tree.add("⚠️ ENI Gate Warnings")
|
1824
1862
|
for warning in analysis.eni_gate_warnings:
|
1825
1863
|
eni_branch.add(f"{warning['NetworkInterfaceId']} - {warning['Message']}")
|
1826
|
-
|
1864
|
+
|
1827
1865
|
# Recommendations branch
|
1828
1866
|
rec_branch = tree.add("💡 Cleanup Recommendations")
|
1829
1867
|
for rec in analysis.cleanup_recommendations:
|
1830
|
-
priority_icon = "🔴" if rec[
|
1868
|
+
priority_icon = "🔴" if rec["Priority"] == "CRITICAL" else "🟡" if rec["Priority"] == "HIGH" else "🟢"
|
1831
1869
|
rec_branch.add(f"{priority_icon} {rec['Action']} - {rec['ResourceId']}")
|
1832
|
-
|
1870
|
+
|
1833
1871
|
self.console.print(tree)
|
1834
|
-
|
1872
|
+
|
1835
1873
|
# Evidence bundle summary
|
1836
1874
|
bundle_info = Panel(
|
1837
1875
|
f"Bundle Version: [bold]{analysis.evidence_bundle.get('BundleVersion', 'N/A')}[/bold]\n"
|
1838
1876
|
f"Cleanup Readiness: [bold]{analysis.evidence_bundle.get('CleanupReadiness', 'UNKNOWN')}[/bold]\n"
|
1839
1877
|
f"CIS Benchmark: [bold]{analysis.evidence_bundle.get('ComplianceStatus', {}).get('CISBenchmark', 'UNKNOWN')}[/bold]",
|
1840
1878
|
title="📋 Evidence Bundle",
|
1841
|
-
style="bold green"
|
1879
|
+
style="bold green",
|
1842
1880
|
)
|
1843
1881
|
self.console.print(bundle_info)
|
1844
1882
|
|
1845
1883
|
def _write_json_evidence(self, data: Dict, file_path: Path):
|
1846
1884
|
"""Write JSON evidence file"""
|
1847
|
-
with open(file_path,
|
1885
|
+
with open(file_path, "w") as f:
|
1848
1886
|
json.dump(data, f, indent=2, default=str)
|
1849
1887
|
|
1850
1888
|
def _write_executive_summary(self, analysis: AWSOAnalysis, file_path: Path):
|
@@ -1854,7 +1892,7 @@ class VPCAnalyzer:
|
|
1854
1892
|
## Overview
|
1855
1893
|
This analysis was conducted to support AWSO-05 VPC cleanup operations with comprehensive dependency validation and security compliance assessment.
|
1856
1894
|
|
1857
|
-
**Generated**: {datetime.now().strftime(
|
1895
|
+
**Generated**: {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}
|
1858
1896
|
**Profile**: {self.profile}
|
1859
1897
|
**Region**: {self.region}
|
1860
1898
|
|
@@ -1869,13 +1907,13 @@ This analysis was conducted to support AWSO-05 VPC cleanup operations with compr
|
|
1869
1907
|
- **Workload Impact Risk**: {"🔴 HIGH" if analysis.eni_gate_warnings else "🟢 LOW"}
|
1870
1908
|
|
1871
1909
|
### Cleanup Readiness
|
1872
|
-
**Status**: {analysis.evidence_bundle.get(
|
1910
|
+
**Status**: {analysis.evidence_bundle.get("CleanupReadiness", "UNKNOWN")}
|
1873
1911
|
|
1874
1912
|
## Recommendations
|
1875
1913
|
|
1876
1914
|
"""
|
1877
1915
|
for rec in analysis.cleanup_recommendations:
|
1878
|
-
priority_emoji = "🔴" if rec[
|
1916
|
+
priority_emoji = "🔴" if rec["Priority"] == "CRITICAL" else "🟡" if rec["Priority"] == "HIGH" else "🟢"
|
1879
1917
|
summary += f"### {priority_emoji} {rec['Priority']} Priority\n"
|
1880
1918
|
summary += f"**Action**: {rec['Action']} \n"
|
1881
1919
|
summary += f"**Resource**: {rec['ResourceId']} \n"
|
@@ -1896,30 +1934,30 @@ This analysis was conducted to support AWSO-05 VPC cleanup operations with compr
|
|
1896
1934
|
---
|
1897
1935
|
*Generated by CloudOps-Runbooks AWSO-05 VPC Analyzer*
|
1898
1936
|
"""
|
1899
|
-
|
1900
|
-
with open(file_path,
|
1937
|
+
|
1938
|
+
with open(file_path, "w") as f:
|
1901
1939
|
f.write(summary)
|
1902
1940
|
|
1903
1941
|
def _create_evidence_manifest(self, evidence_files: Dict[str, str]) -> Dict[str, Any]:
|
1904
1942
|
"""Create evidence manifest with SHA256 checksums"""
|
1905
1943
|
import hashlib
|
1906
|
-
|
1944
|
+
|
1907
1945
|
manifest = {
|
1908
|
-
|
1909
|
-
|
1910
|
-
|
1911
|
-
|
1912
|
-
|
1946
|
+
"ManifestVersion": "1.0",
|
1947
|
+
"GeneratedAt": datetime.now().isoformat(),
|
1948
|
+
"EvidenceFiles": list(evidence_files.keys()),
|
1949
|
+
"FileCount": len(evidence_files),
|
1950
|
+
"FileChecksums": {},
|
1913
1951
|
}
|
1914
|
-
|
1952
|
+
|
1915
1953
|
# Generate SHA256 checksums
|
1916
1954
|
for evidence_type, file_path in evidence_files.items():
|
1917
1955
|
try:
|
1918
|
-
with open(file_path,
|
1956
|
+
with open(file_path, "rb") as f:
|
1919
1957
|
file_hash = hashlib.sha256(f.read()).hexdigest()
|
1920
|
-
manifest[
|
1958
|
+
manifest["FileChecksums"][evidence_type] = file_hash
|
1921
1959
|
except Exception as e:
|
1922
1960
|
logger.error(f"Failed to generate checksum for {file_path}: {e}")
|
1923
|
-
manifest[
|
1924
|
-
|
1925
|
-
return manifest
|
1961
|
+
manifest["FileChecksums"][evidence_type] = "ERROR"
|
1962
|
+
|
1963
|
+
return manifest
|