runbooks 1.1.3__py3-none-any.whl → 1.1.5__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (247) hide show
  1. runbooks/__init__.py +31 -2
  2. runbooks/__init___optimized.py +18 -4
  3. runbooks/_platform/__init__.py +1 -5
  4. runbooks/_platform/core/runbooks_wrapper.py +141 -138
  5. runbooks/aws2/accuracy_validator.py +812 -0
  6. runbooks/base.py +7 -0
  7. runbooks/cfat/WEIGHT_CONFIG_README.md +1 -1
  8. runbooks/cfat/assessment/compliance.py +8 -8
  9. runbooks/cfat/assessment/runner.py +1 -0
  10. runbooks/cfat/cloud_foundations_assessment.py +227 -239
  11. runbooks/cfat/models.py +6 -2
  12. runbooks/cfat/tests/__init__.py +6 -1
  13. runbooks/cli/__init__.py +13 -0
  14. runbooks/cli/commands/cfat.py +274 -0
  15. runbooks/cli/commands/finops.py +1164 -0
  16. runbooks/cli/commands/inventory.py +379 -0
  17. runbooks/cli/commands/operate.py +239 -0
  18. runbooks/cli/commands/security.py +248 -0
  19. runbooks/cli/commands/validation.py +825 -0
  20. runbooks/cli/commands/vpc.py +310 -0
  21. runbooks/cli/registry.py +107 -0
  22. runbooks/cloudops/__init__.py +23 -30
  23. runbooks/cloudops/base.py +96 -107
  24. runbooks/cloudops/cost_optimizer.py +549 -547
  25. runbooks/cloudops/infrastructure_optimizer.py +5 -4
  26. runbooks/cloudops/interfaces.py +226 -227
  27. runbooks/cloudops/lifecycle_manager.py +5 -4
  28. runbooks/cloudops/mcp_cost_validation.py +252 -235
  29. runbooks/cloudops/models.py +78 -53
  30. runbooks/cloudops/monitoring_automation.py +5 -4
  31. runbooks/cloudops/notebook_framework.py +179 -215
  32. runbooks/cloudops/security_enforcer.py +125 -159
  33. runbooks/common/accuracy_validator.py +11 -0
  34. runbooks/common/aws_pricing.py +349 -326
  35. runbooks/common/aws_pricing_api.py +211 -212
  36. runbooks/common/aws_profile_manager.py +341 -0
  37. runbooks/common/aws_utils.py +75 -80
  38. runbooks/common/business_logic.py +127 -105
  39. runbooks/common/cli_decorators.py +36 -60
  40. runbooks/common/comprehensive_cost_explorer_integration.py +456 -464
  41. runbooks/common/cross_account_manager.py +198 -205
  42. runbooks/common/date_utils.py +27 -39
  43. runbooks/common/decorators.py +235 -0
  44. runbooks/common/dry_run_examples.py +173 -208
  45. runbooks/common/dry_run_framework.py +157 -155
  46. runbooks/common/enhanced_exception_handler.py +15 -4
  47. runbooks/common/enhanced_logging_example.py +50 -64
  48. runbooks/common/enhanced_logging_integration_example.py +65 -37
  49. runbooks/common/env_utils.py +16 -16
  50. runbooks/common/error_handling.py +40 -38
  51. runbooks/common/lazy_loader.py +41 -23
  52. runbooks/common/logging_integration_helper.py +79 -86
  53. runbooks/common/mcp_cost_explorer_integration.py +478 -495
  54. runbooks/common/mcp_integration.py +63 -74
  55. runbooks/common/memory_optimization.py +140 -118
  56. runbooks/common/module_cli_base.py +37 -58
  57. runbooks/common/organizations_client.py +176 -194
  58. runbooks/common/patterns.py +204 -0
  59. runbooks/common/performance_monitoring.py +67 -71
  60. runbooks/common/performance_optimization_engine.py +283 -274
  61. runbooks/common/profile_utils.py +248 -39
  62. runbooks/common/rich_utils.py +643 -92
  63. runbooks/common/sre_performance_suite.py +177 -186
  64. runbooks/enterprise/__init__.py +1 -1
  65. runbooks/enterprise/logging.py +144 -106
  66. runbooks/enterprise/security.py +187 -204
  67. runbooks/enterprise/validation.py +43 -56
  68. runbooks/finops/__init__.py +29 -33
  69. runbooks/finops/account_resolver.py +1 -1
  70. runbooks/finops/advanced_optimization_engine.py +980 -0
  71. runbooks/finops/automation_core.py +268 -231
  72. runbooks/finops/business_case_config.py +184 -179
  73. runbooks/finops/cli.py +660 -139
  74. runbooks/finops/commvault_ec2_analysis.py +157 -164
  75. runbooks/finops/compute_cost_optimizer.py +336 -320
  76. runbooks/finops/config.py +20 -20
  77. runbooks/finops/cost_optimizer.py +488 -622
  78. runbooks/finops/cost_processor.py +332 -214
  79. runbooks/finops/dashboard_runner.py +1006 -172
  80. runbooks/finops/ebs_cost_optimizer.py +991 -657
  81. runbooks/finops/elastic_ip_optimizer.py +317 -257
  82. runbooks/finops/enhanced_mcp_integration.py +340 -0
  83. runbooks/finops/enhanced_progress.py +40 -37
  84. runbooks/finops/enhanced_trend_visualization.py +3 -2
  85. runbooks/finops/enterprise_wrappers.py +230 -292
  86. runbooks/finops/executive_export.py +203 -160
  87. runbooks/finops/helpers.py +130 -288
  88. runbooks/finops/iam_guidance.py +1 -1
  89. runbooks/finops/infrastructure/__init__.py +80 -0
  90. runbooks/finops/infrastructure/commands.py +506 -0
  91. runbooks/finops/infrastructure/load_balancer_optimizer.py +866 -0
  92. runbooks/finops/infrastructure/vpc_endpoint_optimizer.py +832 -0
  93. runbooks/finops/markdown_exporter.py +338 -175
  94. runbooks/finops/mcp_validator.py +1952 -0
  95. runbooks/finops/nat_gateway_optimizer.py +1513 -482
  96. runbooks/finops/network_cost_optimizer.py +657 -587
  97. runbooks/finops/notebook_utils.py +226 -188
  98. runbooks/finops/optimization_engine.py +1136 -0
  99. runbooks/finops/optimizer.py +25 -29
  100. runbooks/finops/rds_snapshot_optimizer.py +367 -411
  101. runbooks/finops/reservation_optimizer.py +427 -363
  102. runbooks/finops/scenario_cli_integration.py +77 -78
  103. runbooks/finops/scenarios.py +1278 -439
  104. runbooks/finops/schemas.py +218 -182
  105. runbooks/finops/snapshot_manager.py +2289 -0
  106. runbooks/finops/tests/test_finops_dashboard.py +3 -3
  107. runbooks/finops/tests/test_reference_images_validation.py +2 -2
  108. runbooks/finops/tests/test_single_account_features.py +17 -17
  109. runbooks/finops/tests/validate_test_suite.py +1 -1
  110. runbooks/finops/types.py +3 -3
  111. runbooks/finops/validation_framework.py +263 -269
  112. runbooks/finops/vpc_cleanup_exporter.py +191 -146
  113. runbooks/finops/vpc_cleanup_optimizer.py +593 -575
  114. runbooks/finops/workspaces_analyzer.py +171 -182
  115. runbooks/hitl/enhanced_workflow_engine.py +1 -1
  116. runbooks/integration/__init__.py +89 -0
  117. runbooks/integration/mcp_integration.py +1920 -0
  118. runbooks/inventory/CLAUDE.md +816 -0
  119. runbooks/inventory/README.md +3 -3
  120. runbooks/inventory/Tests/common_test_data.py +30 -30
  121. runbooks/inventory/__init__.py +2 -2
  122. runbooks/inventory/cloud_foundations_integration.py +144 -149
  123. runbooks/inventory/collectors/aws_comprehensive.py +28 -11
  124. runbooks/inventory/collectors/aws_networking.py +111 -101
  125. runbooks/inventory/collectors/base.py +4 -0
  126. runbooks/inventory/core/collector.py +495 -313
  127. runbooks/inventory/discovery.md +2 -2
  128. runbooks/inventory/drift_detection_cli.py +69 -96
  129. runbooks/inventory/find_ec2_security_groups.py +1 -1
  130. runbooks/inventory/inventory_mcp_cli.py +48 -46
  131. runbooks/inventory/list_rds_snapshots_aggregator.py +192 -208
  132. runbooks/inventory/mcp_inventory_validator.py +549 -465
  133. runbooks/inventory/mcp_vpc_validator.py +359 -442
  134. runbooks/inventory/organizations_discovery.py +56 -52
  135. runbooks/inventory/rich_inventory_display.py +33 -32
  136. runbooks/inventory/unified_validation_engine.py +278 -251
  137. runbooks/inventory/vpc_analyzer.py +733 -696
  138. runbooks/inventory/vpc_architecture_validator.py +293 -348
  139. runbooks/inventory/vpc_dependency_analyzer.py +382 -378
  140. runbooks/inventory/vpc_flow_analyzer.py +3 -3
  141. runbooks/main.py +152 -9147
  142. runbooks/main_final.py +91 -60
  143. runbooks/main_minimal.py +22 -10
  144. runbooks/main_optimized.py +131 -100
  145. runbooks/main_ultra_minimal.py +7 -2
  146. runbooks/mcp/__init__.py +36 -0
  147. runbooks/mcp/integration.py +679 -0
  148. runbooks/metrics/dora_metrics_engine.py +2 -2
  149. runbooks/monitoring/performance_monitor.py +9 -4
  150. runbooks/operate/dynamodb_operations.py +3 -1
  151. runbooks/operate/ec2_operations.py +145 -137
  152. runbooks/operate/iam_operations.py +146 -152
  153. runbooks/operate/mcp_integration.py +1 -1
  154. runbooks/operate/networking_cost_heatmap.py +33 -10
  155. runbooks/operate/privatelink_operations.py +1 -1
  156. runbooks/operate/rds_operations.py +223 -254
  157. runbooks/operate/s3_operations.py +107 -118
  158. runbooks/operate/vpc_endpoints.py +1 -1
  159. runbooks/operate/vpc_operations.py +648 -618
  160. runbooks/remediation/base.py +1 -1
  161. runbooks/remediation/commons.py +10 -7
  162. runbooks/remediation/commvault_ec2_analysis.py +71 -67
  163. runbooks/remediation/ec2_unattached_ebs_volumes.py +1 -0
  164. runbooks/remediation/multi_account.py +24 -21
  165. runbooks/remediation/rds_snapshot_list.py +91 -65
  166. runbooks/remediation/remediation_cli.py +92 -146
  167. runbooks/remediation/universal_account_discovery.py +83 -79
  168. runbooks/remediation/workspaces_list.py +49 -44
  169. runbooks/security/__init__.py +19 -0
  170. runbooks/security/assessment_runner.py +1150 -0
  171. runbooks/security/baseline_checker.py +812 -0
  172. runbooks/security/cloudops_automation_security_validator.py +509 -535
  173. runbooks/security/compliance_automation_engine.py +17 -17
  174. runbooks/security/config/__init__.py +2 -2
  175. runbooks/security/config/compliance_config.py +50 -50
  176. runbooks/security/config_template_generator.py +63 -76
  177. runbooks/security/enterprise_security_framework.py +1 -1
  178. runbooks/security/executive_security_dashboard.py +519 -508
  179. runbooks/security/integration_test_enterprise_security.py +5 -3
  180. runbooks/security/multi_account_security_controls.py +959 -1210
  181. runbooks/security/real_time_security_monitor.py +422 -444
  182. runbooks/security/run_script.py +1 -1
  183. runbooks/security/security_baseline_tester.py +1 -1
  184. runbooks/security/security_cli.py +143 -112
  185. runbooks/security/test_2way_validation.py +439 -0
  186. runbooks/security/two_way_validation_framework.py +852 -0
  187. runbooks/sre/mcp_reliability_engine.py +6 -6
  188. runbooks/sre/production_monitoring_framework.py +167 -177
  189. runbooks/tdd/__init__.py +15 -0
  190. runbooks/tdd/cli.py +1071 -0
  191. runbooks/utils/__init__.py +14 -17
  192. runbooks/utils/logger.py +7 -2
  193. runbooks/utils/version_validator.py +51 -48
  194. runbooks/validation/__init__.py +6 -6
  195. runbooks/validation/cli.py +9 -3
  196. runbooks/validation/comprehensive_2way_validator.py +754 -708
  197. runbooks/validation/mcp_validator.py +906 -228
  198. runbooks/validation/terraform_citations_validator.py +104 -115
  199. runbooks/validation/terraform_drift_detector.py +447 -451
  200. runbooks/vpc/README.md +617 -0
  201. runbooks/vpc/__init__.py +8 -1
  202. runbooks/vpc/analyzer.py +577 -0
  203. runbooks/vpc/cleanup_wrapper.py +476 -413
  204. runbooks/vpc/cli_cloudtrail_commands.py +339 -0
  205. runbooks/vpc/cli_mcp_validation_commands.py +480 -0
  206. runbooks/vpc/cloudtrail_audit_integration.py +717 -0
  207. runbooks/vpc/config.py +92 -97
  208. runbooks/vpc/cost_engine.py +411 -148
  209. runbooks/vpc/cost_explorer_integration.py +553 -0
  210. runbooks/vpc/cross_account_session.py +101 -106
  211. runbooks/vpc/enhanced_mcp_validation.py +917 -0
  212. runbooks/vpc/eni_gate_validator.py +961 -0
  213. runbooks/vpc/heatmap_engine.py +190 -162
  214. runbooks/vpc/mcp_no_eni_validator.py +681 -640
  215. runbooks/vpc/nat_gateway_optimizer.py +358 -0
  216. runbooks/vpc/networking_wrapper.py +15 -8
  217. runbooks/vpc/pdca_remediation_planner.py +528 -0
  218. runbooks/vpc/performance_optimized_analyzer.py +219 -231
  219. runbooks/vpc/runbooks_adapter.py +1167 -241
  220. runbooks/vpc/tdd_red_phase_stubs.py +601 -0
  221. runbooks/vpc/test_data_loader.py +358 -0
  222. runbooks/vpc/tests/conftest.py +314 -4
  223. runbooks/vpc/tests/test_cleanup_framework.py +1022 -0
  224. runbooks/vpc/tests/test_cost_engine.py +0 -2
  225. runbooks/vpc/topology_generator.py +326 -0
  226. runbooks/vpc/unified_scenarios.py +1302 -1129
  227. runbooks/vpc/vpc_cleanup_integration.py +1943 -1115
  228. runbooks-1.1.5.dist-info/METADATA +328 -0
  229. {runbooks-1.1.3.dist-info → runbooks-1.1.5.dist-info}/RECORD +233 -200
  230. runbooks/finops/README.md +0 -414
  231. runbooks/finops/accuracy_cross_validator.py +0 -647
  232. runbooks/finops/business_cases.py +0 -950
  233. runbooks/finops/dashboard_router.py +0 -922
  234. runbooks/finops/ebs_optimizer.py +0 -956
  235. runbooks/finops/embedded_mcp_validator.py +0 -1629
  236. runbooks/finops/enhanced_dashboard_runner.py +0 -527
  237. runbooks/finops/finops_dashboard.py +0 -584
  238. runbooks/finops/finops_scenarios.py +0 -1218
  239. runbooks/finops/legacy_migration.py +0 -730
  240. runbooks/finops/multi_dashboard.py +0 -1519
  241. runbooks/finops/single_dashboard.py +0 -1113
  242. runbooks/finops/unlimited_scenarios.py +0 -393
  243. runbooks-1.1.3.dist-info/METADATA +0 -799
  244. {runbooks-1.1.3.dist-info → runbooks-1.1.5.dist-info}/WHEEL +0 -0
  245. {runbooks-1.1.3.dist-info → runbooks-1.1.5.dist-info}/entry_points.txt +0 -0
  246. {runbooks-1.1.3.dist-info → runbooks-1.1.5.dist-info}/licenses/LICENSE +0 -0
  247. {runbooks-1.1.3.dist-info → runbooks-1.1.5.dist-info}/top_level.txt +0 -0
@@ -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(profile=profile)
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 v0.9.9", "Multi-Organization Landing Zone Enhanced")
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 ['success', 'cached'] else "INACTIVE",
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(f"✅ Landing Zone Discovery: {len(successful_sessions)}/{len(all_accounts)} accounts accessible")
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
- '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
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(f" 🎯 Target met: {'✅ Yes' if landing_zone_metrics['performance_target_met'] else '❌ No'} (<60s)")
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
- 'total_accounts': len(successful_sessions),
286
- 'excluded_accounts_list': self.excluded_accounts,
287
- 'discovery_timestamp': datetime.now().isoformat(),
288
- 'landing_zone_structure': landing_zone_structure
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['source_account'] = session.account_id
359
- vpc['source_account_name'] = session.account_name
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['source_account'] = session.account_id
364
- nat_gw['source_account_name'] = session.account_name
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['source_account'] = session.account_id
380
- resource['source_account_name'] = session.account_name
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(f"🎯 Parallel VPC discovery complete: {total_resources} total resources across {len(sessions)} accounts")
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 ['success', 'cached']:
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('ec2', region_name=self.region)
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['Vpcs']:
439
- vpcs.append({
440
- 'VpcId': vpc['VpcId'],
441
- 'State': vpc['State'],
442
- 'CidrBlock': vpc['CidrBlock'],
443
- 'IsDefault': vpc.get('IsDefault', False),
444
- 'Tags': vpc.get('Tags', [])
445
- })
446
-
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['NatGateways']:
450
- nat_gateways.append({
451
- 'NatGatewayId': nat_gw['NatGatewayId'],
452
- 'VpcId': nat_gw.get('VpcId'),
453
- 'State': nat_gw['State'],
454
- 'SubnetId': nat_gw.get('SubnetId'),
455
- 'Tags': nat_gw.get('Tags', [])
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['NetworkInterfaces']:
461
- network_interfaces.append({
462
- 'NetworkInterfaceId': eni['NetworkInterfaceId'],
463
- 'VpcId': eni.get('VpcId'),
464
- 'SubnetId': eni.get('SubnetId'),
465
- 'Status': eni['Status'],
466
- 'InterfaceType': eni.get('InterfaceType', 'interface'),
467
- 'Attachment': eni.get('Attachment', {}),
468
- 'Tags': eni.get('TagSet', [])
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 = (len(vpcs) + len(nat_gateways) + len(vpc_endpoints) +
474
- len(internet_gateways) + len(route_tables) + len(subnets) +
475
- len(network_interfaces) + len(transit_gateway_attachments) +
476
- len(vpc_peering_connections) + len(security_groups))
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) + len(nat_gateways) + len(vpc_endpoints) +
590
- len(internet_gateways) + len(route_tables) + len(subnets) +
591
- len(network_interfaces) + len(tgw_attachments) +
592
- len(vpc_peering) + len(security_groups),
593
- discovery_timestamp=datetime.now().isoformat()
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 == 'ACTIVE']
690
-
705
+ active_accounts = [acc for acc in accounts if acc.status == "ACTIVE"]
706
+
691
707
  # Filter out decommissioned accounts
692
- filtered_accounts = [
693
- acc for acc in active_accounts
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
- 'account_id': session.account_id,
738
- 'account_name': session.account_name,
739
- 'role_used': session.role_used,
740
- 'discovery_timestamp': discovery_result.discovery_timestamp
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
- 'account_id': session.account_id,
753
- 'account_name': session.account_name,
754
- 'error': str(e)
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
- ], return_exceptions=True)
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['AccountId'] = account_id
825
+ vpc["AccountId"] = account_id
816
826
  all_vpcs.append(vpc)
817
-
827
+
818
828
  for nat in discovery_result.nat_gateways:
819
- nat['AccountId'] = account_id
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['AccountId'] = account_id
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['AccountId'] = account_id
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['AccountId'] = account_id
841
+ rt["AccountId"] = account_id
832
842
  all_route_tables.append(rt)
833
-
843
+
834
844
  for subnet in discovery_result.subnets:
835
- subnet['AccountId'] = account_id
845
+ subnet["AccountId"] = account_id
836
846
  all_subnets.append(subnet)
837
-
847
+
838
848
  for eni in discovery_result.network_interfaces:
839
- eni['AccountId'] = account_id
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['AccountId'] = account_id
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['AccountId'] = account_id
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['AccountId'] = account_id
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) + len(all_nat_gateways) + len(all_vpc_endpoints) +
869
- len(all_internet_gateways) + len(all_route_tables) + len(all_subnets) +
870
- len(all_network_interfaces) + len(all_tgw_attachments) +
871
- len(all_vpc_peering) + len(all_security_groups)
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={'accounts_discovered': account_summaries},
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 ['success', 'cached']])
905
-
916
+
917
+ successful_accounts = len([s for s in sessions if s.status in ["success", "cached"]])
918
+
906
919
  return {
907
- 'total_accounts_targeted': len(sessions),
908
- 'successful_discoveries': successful_accounts,
909
- 'failed_discoveries': len(sessions) - successful_accounts,
910
- 'discovery_success_rate': (successful_accounts / len(sessions) * 100) if sessions else 0,
911
- 'total_vpc_resources': sum(resources_per_account.values()),
912
- 'average_resources_per_account': (
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
- 'accounts_with_resources': len([count for count in resources_per_account.values() if count > 0]),
917
- 'empty_accounts': len([count for count in resources_per_account.values() if count == 0]),
918
- 'decommissioned_accounts_excluded': len(self.excluded_accounts),
919
- 'excluded_account_list': self.excluded_accounts,
920
- 'session_manager_metrics': (
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
- 'generated_at': datetime.now().isoformat()
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('session_manager_metrics'):
959
- session_metrics = metrics['session_manager_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(discovery_result, {
1054
- 'eni_warnings': eni_warnings,
1055
- 'network_deps': network_deps,
1056
- 'gateway_deps': gateway_deps,
1057
- 'security_deps': security_deps,
1058
- 'cross_account_deps': cross_account_deps,
1059
- 'default_vpcs': default_vpcs
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
- 'network_resources': network_deps,
1065
- 'gateway_resources': gateway_deps,
1066
- 'security_resources': security_deps,
1067
- 'cross_account_resources': cross_account_deps
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['discovery'] = str(discovery_file)
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['awso_analysis'] = str(awso_file)
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['executive_summary'] = str(summary_file)
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['manifest'] = str(manifest_file)
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('file_checksums', {}).get(evidence_type, 'N/A')
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({'Name': 'vpc-id', 'Values': vpc_ids})
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('Vpcs', []):
1187
+
1188
+ for vpc in response.get("Vpcs", []):
1177
1189
  vpc_info = {
1178
- 'VpcId': vpc['VpcId'],
1179
- 'CidrBlock': vpc['CidrBlock'],
1180
- 'State': vpc['State'],
1181
- 'IsDefault': vpc['IsDefault'],
1182
- 'InstanceTenancy': vpc['InstanceTenancy'],
1183
- 'DhcpOptionsId': vpc['DhcpOptionsId'],
1184
- 'Tags': {tag['Key']: tag['Value'] for tag in vpc.get('Tags', [])},
1185
- 'Name': self._get_name_tag(vpc.get('Tags', [])),
1186
- 'DiscoveredAt': datetime.now().isoformat()
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('NatGateways', []):
1213
+
1214
+ for nat in response.get("NatGateways", []):
1203
1215
  # Filter by VPC if specified
1204
- if vpc_ids and nat.get('VpcId') not in vpc_ids:
1216
+ if vpc_ids and nat.get("VpcId") not in vpc_ids:
1205
1217
  continue
1206
-
1218
+
1207
1219
  nat_info = {
1208
- 'NatGatewayId': nat['NatGatewayId'],
1209
- 'VpcId': nat.get('VpcId'),
1210
- 'SubnetId': nat.get('SubnetId'),
1211
- 'State': nat['State'],
1212
- 'CreateTime': nat.get('CreateTime', '').isoformat() if nat.get('CreateTime') else None,
1213
- 'ConnectivityType': nat.get('ConnectivityType', 'public'),
1214
- 'Tags': {tag['Key']: tag['Value'] for tag in nat.get('Tags', [])},
1215
- 'Name': self._get_name_tag(nat.get('Tags', [])),
1216
- 'EstimatedMonthlyCost': 45.0, # Base NAT Gateway cost
1217
- 'DiscoveredAt': datetime.now().isoformat()
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('VpcEndpoints', []):
1244
+
1245
+ for endpoint in response.get("VpcEndpoints", []):
1234
1246
  # Filter by VPC if specified
1235
- if vpc_ids and endpoint.get('VpcId') not in vpc_ids:
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('VpcEndpointType') == 'Interface':
1241
- az_count = len(endpoint.get('SubnetIds', []))
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
- 'VpcEndpointId': endpoint['VpcEndpointId'],
1246
- 'VpcId': endpoint.get('VpcId'),
1247
- 'ServiceName': endpoint.get('ServiceName'),
1248
- 'VpcEndpointType': endpoint.get('VpcEndpointType', 'Gateway'),
1249
- 'State': endpoint.get('State'),
1250
- 'SubnetIds': endpoint.get('SubnetIds', []),
1251
- 'RouteTableIds': endpoint.get('RouteTableIds', []),
1252
- 'PolicyDocument': endpoint.get('PolicyDocument'),
1253
- 'Tags': {tag['Key']: tag['Value'] for tag in endpoint.get('Tags', [])},
1254
- 'Name': self._get_name_tag(endpoint.get('Tags', [])),
1255
- 'EstimatedMonthlyCost': monthly_cost,
1256
- 'DiscoveredAt': datetime.now().isoformat()
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('InternetGateways', []):
1283
+
1284
+ for igw in response.get("InternetGateways", []):
1273
1285
  # Filter by attached VPC if specified
1274
- attached_vpc_ids = [attachment['VpcId'] for attachment in igw.get('Attachments', [])]
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
- 'InternetGatewayId': igw['InternetGatewayId'],
1280
- 'Attachments': igw.get('Attachments', []),
1281
- 'AttachedVpcIds': attached_vpc_ids,
1282
- 'Tags': {tag['Key']: tag['Value'] for tag in igw.get('Tags', [])},
1283
- 'Name': self._get_name_tag(igw.get('Tags', [])),
1284
- 'DiscoveredAt': datetime.now().isoformat()
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({'Name': 'vpc-id', 'Values': vpc_ids})
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('RouteTables', []):
1315
+
1316
+ for rt in response.get("RouteTables", []):
1305
1317
  rt_info = {
1306
- 'RouteTableId': rt['RouteTableId'],
1307
- 'VpcId': rt['VpcId'],
1308
- 'Routes': rt.get('Routes', []),
1309
- 'Associations': rt.get('Associations', []),
1310
- 'Tags': {tag['Key']: tag['Value'] for tag in rt.get('Tags', [])},
1311
- 'Name': self._get_name_tag(rt.get('Tags', [])),
1312
- 'IsMainRouteTable': any(assoc.get('Main', False) for assoc in rt.get('Associations', [])),
1313
- 'AssociatedSubnets': [assoc.get('SubnetId') for assoc in rt.get('Associations', []) if assoc.get('SubnetId')],
1314
- 'DiscoveredAt': datetime.now().isoformat()
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({'Name': 'vpc-id', 'Values': vpc_ids})
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('Subnets', []):
1347
+
1348
+ for subnet in response.get("Subnets", []):
1335
1349
  subnet_info = {
1336
- 'SubnetId': subnet['SubnetId'],
1337
- 'VpcId': subnet['VpcId'],
1338
- 'CidrBlock': subnet['CidrBlock'],
1339
- 'AvailabilityZone': subnet['AvailabilityZone'],
1340
- 'State': subnet['State'],
1341
- 'MapPublicIpOnLaunch': subnet.get('MapPublicIpOnLaunch', False),
1342
- 'AvailableIpAddressCount': subnet.get('AvailableIpAddressCount', 0),
1343
- 'Tags': {tag['Key']: tag['Value'] for tag in subnet.get('Tags', [])},
1344
- 'Name': self._get_name_tag(subnet.get('Tags', [])),
1345
- 'DiscoveredAt': datetime.now().isoformat()
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({'Name': 'vpc-id', 'Values': vpc_ids})
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('NetworkInterfaces', []):
1378
+
1379
+ for eni in response.get("NetworkInterfaces", []):
1366
1380
  eni_info = {
1367
- 'NetworkInterfaceId': eni['NetworkInterfaceId'],
1368
- 'VpcId': eni.get('VpcId'),
1369
- 'SubnetId': eni.get('SubnetId'),
1370
- 'Status': eni.get('Status'),
1371
- 'InterfaceType': eni.get('InterfaceType', 'interface'),
1372
- 'Attachment': eni.get('Attachment'),
1373
- 'Groups': eni.get('Groups', []),
1374
- 'PrivateIpAddress': eni.get('PrivateIpAddress'),
1375
- 'PrivateIpAddresses': eni.get('PrivateIpAddresses', []),
1376
- 'Tags': {tag['Key']: tag['Value'] for tag in eni.get('Tags', [])},
1377
- 'Name': self._get_name_tag(eni.get('Tags', [])),
1378
- 'RequesterManaged': eni.get('RequesterManaged', False),
1379
- 'IsAttached': bool(eni.get('Attachment')),
1380
- 'AttachedInstanceId': eni.get('Attachment', {}).get('InstanceId'),
1381
- 'DiscoveredAt': datetime.now().isoformat()
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('TransitGatewayAttachments', []):
1410
+
1411
+ for attachment in response.get("TransitGatewayAttachments", []):
1398
1412
  # Filter by VPC if specified
1399
- if vpc_ids and attachment.get('ResourceType') == 'vpc' and attachment.get('ResourceId') not in vpc_ids:
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
- 'TransitGatewayAttachmentId': attachment['TransitGatewayAttachmentId'],
1404
- 'TransitGatewayId': attachment.get('TransitGatewayId'),
1405
- 'ResourceType': attachment.get('ResourceType'),
1406
- 'ResourceId': attachment.get('ResourceId'),
1407
- 'State': attachment.get('State'),
1408
- 'Tags': {tag['Key']: tag['Value'] for tag in attachment.get('Tags', [])},
1409
- 'Name': self._get_name_tag(attachment.get('Tags', [])),
1410
- 'ResourceOwnerId': attachment.get('ResourceOwnerId'),
1411
- 'DiscoveredAt': datetime.now().isoformat()
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('VpcPeeringConnections', []):
1428
- accepter_vpc_id = connection.get('AccepterVpcInfo', {}).get('VpcId')
1429
- requester_vpc_id = connection.get('RequesterVpcInfo', {}).get('VpcId')
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
- 'VpcPeeringConnectionId': connection['VpcPeeringConnectionId'],
1437
- 'AccepterVpcInfo': connection.get('AccepterVpcInfo', {}),
1438
- 'RequesterVpcInfo': connection.get('RequesterVpcInfo', {}),
1439
- 'Status': connection.get('Status', {}),
1440
- 'Tags': {tag['Key']: tag['Value'] for tag in connection.get('Tags', [])},
1441
- 'Name': self._get_name_tag(connection.get('Tags', [])),
1442
- 'ExpirationTime': connection.get('ExpirationTime'),
1443
- 'DiscoveredAt': datetime.now().isoformat()
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({'Name': 'vpc-id', 'Values': vpc_ids})
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('SecurityGroups', []):
1476
+
1477
+ for sg in response.get("SecurityGroups", []):
1464
1478
  sg_info = {
1465
- 'GroupId': sg['GroupId'],
1466
- 'GroupName': sg['GroupName'],
1467
- 'VpcId': sg.get('VpcId'),
1468
- 'Description': sg.get('Description', ''),
1469
- 'IpPermissions': sg.get('IpPermissions', []),
1470
- 'IpPermissionsEgress': sg.get('IpPermissionsEgress', []),
1471
- 'Tags': {tag['Key']: tag['Value'] for tag in sg.get('Tags', [])},
1472
- 'Name': self._get_name_tag(sg.get('Tags', [])),
1473
- 'IsDefault': sg.get('GroupName') == 'default',
1474
- 'DiscoveredAt': datetime.now().isoformat()
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['IsAttached'] and not eni['RequesterManaged']:
1492
- warnings.append({
1493
- 'NetworkInterfaceId': eni['NetworkInterfaceId'],
1494
- 'VpcId': eni['VpcId'],
1495
- 'AttachedInstanceId': eni.get('AttachedInstanceId'),
1496
- 'WarningType': 'ATTACHED_ENI',
1497
- 'RiskLevel': 'HIGH',
1498
- 'Message': f"ENI {eni['NetworkInterfaceId']} is attached to instance {eni.get('AttachedInstanceId')} - VPC cleanup may disrupt workload",
1499
- 'Recommendation': 'Verify workload migration before VPC cleanup'
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['VpcId']
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['VpcId']
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['AttachedVpcIds']:
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['ResourceType'] == 'vpc':
1538
- vpc_id = attachment['ResourceId']
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['VpcId']
1567
+ vpc_id = rt["VpcId"]
1552
1568
  if vpc_id not in dependencies:
1553
1569
  dependencies[vpc_id] = []
1554
- if not rt['IsMainRouteTable']: # Don't list main route tables as dependencies
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['IsDefault']:
1560
- vpc_id = sg['VpcId']
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['AccepterVpcInfo']
1574
- requester_vpc = connection['RequesterVpcInfo']
1575
-
1589
+ accepter_vpc = connection["AccepterVpcInfo"]
1590
+ requester_vpc = connection["RequesterVpcInfo"]
1591
+
1576
1592
  # Check for cross-account peering
1577
- if accepter_vpc.get('OwnerId') != requester_vpc.get('OwnerId'):
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('VpcId')
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(f"Cross-Account VPC Peering: {connection['VpcPeeringConnectionId']}")
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['IsDefault']:
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([eni for eni in discovery.network_interfaces
1598
- if eni['VpcId'] == vpc['VpcId'] and not eni['RequesterManaged']])
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['VpcId'] == vpc['VpcId']])
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['VpcId'] == vpc['VpcId']])
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
- 'VpcId': vpc['VpcId'],
1614
- 'CidrBlock': vpc['CidrBlock'],
1615
- 'Region': self.region,
1616
- 'ResourcesPresent': resources_in_vpc,
1617
- 'ResourceCount': len(resources_in_vpc),
1618
- 'CleanupRecommendation': 'DELETE' if len(resources_in_vpc) == 0 else 'MIGRATE_RESOURCES_FIRST',
1619
- 'CISBenchmarkCompliance': 'NON_COMPLIANT',
1620
- 'SecurityRisk': 'HIGH' if len(resources_in_vpc) > 0 else 'MEDIUM'
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['Routes']:
1634
- if route.get('NatGatewayId'):
1635
- used_nat_gateways.add(route['NatGatewayId'])
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['NatGatewayId'] not in used_nat_gateways and nat['State'] == 'available':
1639
- orphaned.append({
1640
- 'ResourceType': 'NAT Gateway',
1641
- 'ResourceId': nat['NatGatewayId'],
1642
- 'VpcId': nat['VpcId'],
1643
- 'Reason': 'No route table references',
1644
- 'EstimatedMonthlySavings': nat['EstimatedMonthlyCost']
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(self, discovery: VPCDiscoveryResult,
1650
- eni_warnings: List[Dict],
1651
- default_vpcs: List[Dict]) -> List[Dict[str, Any]]:
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['CleanupRecommendation'] == 'DELETE':
1658
- recommendations.append({
1659
- 'Priority': 'HIGH',
1660
- 'Action': 'DELETE_DEFAULT_VPC',
1661
- 'ResourceType': 'VPC',
1662
- 'ResourceId': default_vpc['VpcId'],
1663
- 'Reason': 'Empty default VPC - CIS Benchmark compliance',
1664
- 'EstimatedMonthlySavings': 0,
1665
- 'SecurityBenefit': 'Reduces attack surface',
1666
- 'RiskLevel': 'LOW'
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
- 'Priority': 'MEDIUM',
1671
- 'Action': 'MIGRATE_FROM_DEFAULT_VPC',
1672
- 'ResourceType': 'VPC',
1673
- 'ResourceId': default_vpc['VpcId'],
1674
- 'Reason': 'Default VPC with resources - requires migration',
1675
- 'EstimatedMonthlySavings': 0,
1676
- 'SecurityBenefit': 'Improves security posture',
1677
- 'RiskLevel': 'HIGH'
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
- 'Priority': 'CRITICAL',
1684
- 'Action': 'REVIEW_WORKLOAD_MIGRATION',
1685
- 'ResourceType': 'Multiple',
1686
- 'ResourceId': 'Multiple ENIs',
1687
- 'Reason': f'{len(eni_warnings)} attached ENIs detected - workload migration required',
1688
- 'EstimatedMonthlySavings': 0,
1689
- 'SecurityBenefit': 'Prevents workload disruption',
1690
- 'RiskLevel': 'CRITICAL'
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
- 'BundleVersion': '1.0',
1699
- 'GeneratedAt': datetime.now().isoformat(),
1700
- 'Profile': self.profile,
1701
- 'Region': self.region,
1702
- 'DiscoverySummary': {
1703
- 'TotalVPCs': len(discovery.vpcs),
1704
- 'DefaultVPCs': len(analysis_data['default_vpcs']),
1705
- 'TotalResources': discovery.total_resources,
1706
- 'ENIWarnings': len(analysis_data['eni_warnings'])
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
- 'ComplianceStatus': {
1709
- 'CISBenchmark': 'NON_COMPLIANT' if analysis_data['default_vpcs'] else 'COMPLIANT',
1710
- 'ENIGateValidation': 'PASSED' if not analysis_data['eni_warnings'] else 'WARNINGS_PRESENT'
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
- 'CleanupReadiness': 'READY' if not analysis_data['eni_warnings'] else 'REQUIRES_WORKLOAD_MIGRATION'
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
- self.landing_zone_sessions
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=[], nat_gateways=[], vpc_endpoints=[], internet_gateways=[],
1767
- route_tables=[], subnets=[], network_interfaces=[],
1768
- transit_gateway_attachments=[], vpc_peering_connections=[],
1769
- security_groups=[], total_resources=0,
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=[], orphaned_resources=[], dependency_chain={},
1779
- eni_gate_warnings=[], cleanup_recommendations=[],
1780
- evidence_bundle={}
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['Key'] == 'Name':
1787
- return tag['Value']
1788
- return 'Unnamed'
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['SecurityRisk'] == 'HIGH' else "🟡 Requires Review"
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['Priority'] == 'CRITICAL' else "🟡" if rec['Priority'] == 'HIGH' else "🟢"
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, 'w') as f:
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('%Y-%m-%d %H:%M:%S')}
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('CleanupReadiness', 'UNKNOWN')}
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['Priority'] == 'CRITICAL' else "🟡" if rec['Priority'] == 'HIGH' else "🟢"
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, 'w') as f:
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
- 'ManifestVersion': '1.0',
1909
- 'GeneratedAt': datetime.now().isoformat(),
1910
- 'EvidenceFiles': list(evidence_files.keys()),
1911
- 'FileCount': len(evidence_files),
1912
- 'FileChecksums': {}
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, 'rb') as f:
1955
+ with open(file_path, "rb") as f:
1919
1956
  file_hash = hashlib.sha256(f.read()).hexdigest()
1920
- manifest['FileChecksums'][evidence_type] = file_hash
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['FileChecksums'][evidence_type] = 'ERROR'
1924
-
1925
- return manifest
1960
+ manifest["FileChecksums"][evidence_type] = "ERROR"
1961
+
1962
+ return manifest