runbooks 1.1.4__py3-none-any.whl → 1.1.6__py3-none-any.whl

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