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