runbooks 0.7.9__py3-none-any.whl → 0.9.1__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 (122) hide show
  1. runbooks/__init__.py +1 -1
  2. runbooks/cfat/README.md +12 -1
  3. runbooks/cfat/__init__.py +1 -1
  4. runbooks/cfat/assessment/compliance.py +4 -1
  5. runbooks/cfat/assessment/runner.py +42 -34
  6. runbooks/cfat/models.py +1 -1
  7. runbooks/cloudops/__init__.py +123 -0
  8. runbooks/cloudops/base.py +385 -0
  9. runbooks/cloudops/cost_optimizer.py +811 -0
  10. runbooks/cloudops/infrastructure_optimizer.py +29 -0
  11. runbooks/cloudops/interfaces.py +828 -0
  12. runbooks/cloudops/lifecycle_manager.py +29 -0
  13. runbooks/cloudops/mcp_cost_validation.py +678 -0
  14. runbooks/cloudops/models.py +251 -0
  15. runbooks/cloudops/monitoring_automation.py +29 -0
  16. runbooks/cloudops/notebook_framework.py +676 -0
  17. runbooks/cloudops/security_enforcer.py +449 -0
  18. runbooks/common/__init__.py +152 -0
  19. runbooks/common/accuracy_validator.py +1039 -0
  20. runbooks/common/context_logger.py +440 -0
  21. runbooks/common/cross_module_integration.py +594 -0
  22. runbooks/common/enhanced_exception_handler.py +1108 -0
  23. runbooks/common/enterprise_audit_integration.py +634 -0
  24. runbooks/common/mcp_cost_explorer_integration.py +900 -0
  25. runbooks/common/mcp_integration.py +548 -0
  26. runbooks/common/performance_monitor.py +387 -0
  27. runbooks/common/profile_utils.py +216 -0
  28. runbooks/common/rich_utils.py +172 -1
  29. runbooks/feedback/user_feedback_collector.py +440 -0
  30. runbooks/finops/README.md +377 -458
  31. runbooks/finops/__init__.py +4 -21
  32. runbooks/finops/account_resolver.py +279 -0
  33. runbooks/finops/accuracy_cross_validator.py +638 -0
  34. runbooks/finops/aws_client.py +721 -36
  35. runbooks/finops/budget_integration.py +313 -0
  36. runbooks/finops/cli.py +59 -5
  37. runbooks/finops/cost_optimizer.py +1340 -0
  38. runbooks/finops/cost_processor.py +211 -37
  39. runbooks/finops/dashboard_router.py +900 -0
  40. runbooks/finops/dashboard_runner.py +990 -232
  41. runbooks/finops/embedded_mcp_validator.py +288 -0
  42. runbooks/finops/enhanced_dashboard_runner.py +8 -7
  43. runbooks/finops/enhanced_progress.py +327 -0
  44. runbooks/finops/enhanced_trend_visualization.py +423 -0
  45. runbooks/finops/finops_dashboard.py +184 -1829
  46. runbooks/finops/helpers.py +509 -196
  47. runbooks/finops/iam_guidance.py +400 -0
  48. runbooks/finops/markdown_exporter.py +466 -0
  49. runbooks/finops/multi_dashboard.py +1502 -0
  50. runbooks/finops/optimizer.py +15 -15
  51. runbooks/finops/profile_processor.py +2 -2
  52. runbooks/finops/runbooks.inventory.organizations_discovery.log +0 -0
  53. runbooks/finops/runbooks.security.report_generator.log +0 -0
  54. runbooks/finops/runbooks.security.run_script.log +0 -0
  55. runbooks/finops/runbooks.security.security_export.log +0 -0
  56. runbooks/finops/schemas.py +589 -0
  57. runbooks/finops/service_mapping.py +195 -0
  58. runbooks/finops/single_dashboard.py +710 -0
  59. runbooks/finops/tests/test_reference_images_validation.py +1 -1
  60. runbooks/inventory/README.md +12 -1
  61. runbooks/inventory/core/collector.py +157 -29
  62. runbooks/inventory/list_ec2_instances.py +9 -6
  63. runbooks/inventory/list_ssm_parameters.py +10 -10
  64. runbooks/inventory/organizations_discovery.py +210 -164
  65. runbooks/inventory/rich_inventory_display.py +74 -107
  66. runbooks/inventory/run_on_multi_accounts.py +13 -13
  67. runbooks/inventory/runbooks.inventory.organizations_discovery.log +0 -0
  68. runbooks/inventory/runbooks.security.security_export.log +0 -0
  69. runbooks/main.py +1371 -240
  70. runbooks/metrics/dora_metrics_engine.py +711 -17
  71. runbooks/monitoring/performance_monitor.py +433 -0
  72. runbooks/operate/README.md +394 -0
  73. runbooks/operate/base.py +215 -47
  74. runbooks/operate/ec2_operations.py +435 -5
  75. runbooks/operate/iam_operations.py +598 -3
  76. runbooks/operate/privatelink_operations.py +1 -1
  77. runbooks/operate/rds_operations.py +508 -0
  78. runbooks/operate/s3_operations.py +508 -0
  79. runbooks/operate/vpc_endpoints.py +1 -1
  80. runbooks/remediation/README.md +489 -13
  81. runbooks/remediation/base.py +5 -3
  82. runbooks/remediation/commons.py +8 -4
  83. runbooks/security/ENTERPRISE_SECURITY_FRAMEWORK.md +506 -0
  84. runbooks/security/README.md +12 -1
  85. runbooks/security/__init__.py +265 -33
  86. runbooks/security/cloudops_automation_security_validator.py +1164 -0
  87. runbooks/security/compliance_automation.py +12 -10
  88. runbooks/security/compliance_automation_engine.py +1021 -0
  89. runbooks/security/enterprise_security_framework.py +930 -0
  90. runbooks/security/enterprise_security_policies.json +293 -0
  91. runbooks/security/executive_security_dashboard.py +1247 -0
  92. runbooks/security/integration_test_enterprise_security.py +879 -0
  93. runbooks/security/module_security_integrator.py +641 -0
  94. runbooks/security/multi_account_security_controls.py +2254 -0
  95. runbooks/security/real_time_security_monitor.py +1196 -0
  96. runbooks/security/report_generator.py +1 -1
  97. runbooks/security/run_script.py +4 -8
  98. runbooks/security/security_baseline_tester.py +39 -52
  99. runbooks/security/security_export.py +99 -120
  100. runbooks/sre/README.md +472 -0
  101. runbooks/sre/__init__.py +33 -0
  102. runbooks/sre/mcp_reliability_engine.py +1049 -0
  103. runbooks/sre/performance_optimization_engine.py +1032 -0
  104. runbooks/sre/production_monitoring_framework.py +584 -0
  105. runbooks/sre/reliability_monitoring_framework.py +1011 -0
  106. runbooks/validation/__init__.py +2 -2
  107. runbooks/validation/benchmark.py +154 -149
  108. runbooks/validation/cli.py +159 -147
  109. runbooks/validation/mcp_validator.py +291 -248
  110. runbooks/vpc/README.md +478 -0
  111. runbooks/vpc/__init__.py +2 -2
  112. runbooks/vpc/manager_interface.py +366 -351
  113. runbooks/vpc/networking_wrapper.py +68 -36
  114. runbooks/vpc/rich_formatters.py +22 -8
  115. runbooks-0.9.1.dist-info/METADATA +308 -0
  116. {runbooks-0.7.9.dist-info → runbooks-0.9.1.dist-info}/RECORD +120 -59
  117. {runbooks-0.7.9.dist-info → runbooks-0.9.1.dist-info}/entry_points.txt +1 -1
  118. runbooks/finops/cross_validation.py +0 -375
  119. runbooks-0.7.9.dist-info/METADATA +0 -636
  120. {runbooks-0.7.9.dist-info → runbooks-0.9.1.dist-info}/WHEEL +0 -0
  121. {runbooks-0.7.9.dist-info → runbooks-0.9.1.dist-info}/licenses/LICENSE +0 -0
  122. {runbooks-0.7.9.dist-info → runbooks-0.9.1.dist-info}/top_level.txt +0 -0
@@ -8,7 +8,7 @@ Scope: Enhanced multi-account discovery for 200+ accounts with Organizations API
8
8
 
9
9
  ENHANCED: 4-Profile AWS SSO Architecture & Performance Benchmarking (v0.8.0)
10
10
  - Proven FinOps success patterns: 61 accounts, $474,406 validated
11
- - Performance targets: <45s for multi-account discovery operations
11
+ - Performance targets: <45s for multi-account discovery operations
12
12
  - Comprehensive error handling with profile fallbacks
13
13
  - Enterprise-grade reliability and monitoring
14
14
  """
@@ -26,7 +26,7 @@ import boto3
26
26
  from botocore.exceptions import ClientError, NoCredentialsError
27
27
  from rich.console import Console
28
28
  from rich.panel import Panel
29
- from rich.progress import Progress, SpinnerColumn, TextColumn, BarColumn, TimeElapsedColumn
29
+ from rich.progress import BarColumn, Progress, SpinnerColumn, TextColumn, TimeElapsedColumn
30
30
  from rich.status import Status
31
31
  from rich.table import Table
32
32
 
@@ -39,16 +39,17 @@ logger = configure_logger(__name__)
39
39
 
40
40
  # Enterprise 4-Profile AWS SSO Architecture (Proven FinOps Success Pattern)
41
41
  ENTERPRISE_PROFILES = {
42
- "BILLING_PROFILE": "ams-admin-Billing-ReadOnlyAccess-909135376185", # Cost Explorer access
43
- "MANAGEMENT_PROFILE": "ams-admin-ReadOnlyAccess-909135376185", # Organizations access
44
- "CENTRALISED_OPS_PROFILE": "ams-centralised-ops-ReadOnlyAccess-335083429030", # Operations access
45
- "SINGLE_ACCOUNT_PROFILE": "ams-shared-services-non-prod-ReadOnlyAccess-499201730520" # Single account ops
42
+ "BILLING_PROFILE": "ams-admin-Billing-ReadOnlyAccess-909135376185", # Cost Explorer access
43
+ "MANAGEMENT_PROFILE": "ams-admin-ReadOnlyAccess-909135376185", # Organizations access
44
+ "CENTRALISED_OPS_PROFILE": "ams-centralised-ops-ReadOnlyAccess-335083429030", # Operations access
45
+ "SINGLE_ACCOUNT_PROFILE": "ams-shared-services-non-prod-ReadOnlyAccess-499201730520", # Single account ops
46
46
  }
47
47
 
48
- @dataclass
48
+
49
+ @dataclass
49
50
  class PerformanceBenchmark:
50
51
  """Performance benchmarking for enterprise scale operations"""
51
-
52
+
52
53
  operation_name: str
53
54
  start_time: datetime
54
55
  end_time: Optional[datetime] = None
@@ -58,18 +59,18 @@ class PerformanceBenchmark:
58
59
  error_message: Optional[str] = None
59
60
  accounts_processed: int = 0
60
61
  api_calls_made: int = 0
61
-
62
+
62
63
  def finish(self, success: bool = True, error_message: Optional[str] = None):
63
64
  """Mark benchmark as complete"""
64
65
  self.end_time = datetime.now(timezone.utc)
65
66
  self.duration_seconds = (self.end_time - self.start_time).total_seconds()
66
67
  self.success = success
67
68
  self.error_message = error_message
68
-
69
+
69
70
  def is_within_target(self) -> bool:
70
71
  """Check if operation completed within target time"""
71
72
  return self.duration_seconds <= self.target_seconds
72
-
73
+
73
74
  def get_performance_grade(self) -> str:
74
75
  """Get performance grade based on target achievement"""
75
76
  if not self.success:
@@ -77,13 +78,13 @@ class PerformanceBenchmark:
77
78
  elif self.duration_seconds <= self.target_seconds * 0.5:
78
79
  return "A+" # Exceptional performance (under 50% of target)
79
80
  elif self.duration_seconds <= self.target_seconds * 0.75:
80
- return "A" # Excellent performance (under 75% of target)
81
+ return "A" # Excellent performance (under 75% of target)
81
82
  elif self.duration_seconds <= self.target_seconds:
82
- return "B" # Good performance (within target)
83
+ return "B" # Good performance (within target)
83
84
  elif self.duration_seconds <= self.target_seconds * 1.5:
84
- return "C" # Acceptable performance (within 150% of target)
85
+ return "C" # Acceptable performance (within 150% of target)
85
86
  else:
86
- return "D" # Poor performance (over 150% of target)
87
+ return "D" # Poor performance (over 150% of target)
87
88
 
88
89
 
89
90
  @dataclass
@@ -143,7 +144,7 @@ class CrossAccountRole:
143
144
  class EnhancedOrganizationsDiscovery:
144
145
  """
145
146
  Enhanced multi-account discovery with 4-Profile AWS SSO Architecture
146
-
147
+
147
148
  Implements proven FinOps success patterns with enterprise-grade reliability:
148
149
  - 4-profile AWS SSO architecture with failover
149
150
  - Performance benchmarking targeting <45s operations
@@ -166,7 +167,7 @@ class EnhancedOrganizationsDiscovery:
166
167
 
167
168
  Args:
168
169
  management_profile: AWS profile with Organizations read access
169
- billing_profile: AWS profile with Cost Explorer access
170
+ billing_profile: AWS profile with Cost Explorer access
170
171
  operational_profile: AWS profile with operational access
171
172
  single_account_profile: AWS profile for single account operations
172
173
  max_workers: Maximum concurrent workers for parallel operations
@@ -177,14 +178,14 @@ class EnhancedOrganizationsDiscovery:
177
178
  self.billing_profile = billing_profile or ENTERPRISE_PROFILES["BILLING_PROFILE"]
178
179
  self.operational_profile = operational_profile or ENTERPRISE_PROFILES["CENTRALISED_OPS_PROFILE"]
179
180
  self.single_account_profile = single_account_profile or ENTERPRISE_PROFILES["SINGLE_ACCOUNT_PROFILE"]
180
-
181
+
181
182
  self.max_workers = max_workers
182
183
  self.performance_target_seconds = performance_target_seconds
183
184
 
184
185
  # Initialize session storage for all 4 profiles
185
186
  self.sessions = {}
186
187
  self.clients = {}
187
-
188
+
188
189
  # Cache for discovered data
189
190
  self.accounts_cache: Dict[str, AWSAccount] = {}
190
191
  self.ous_cache: Dict[str, OrganizationalUnit] = {}
@@ -212,7 +213,7 @@ class EnhancedOrganizationsDiscovery:
212
213
  def initialize_sessions(self) -> Dict[str, str]:
213
214
  """
214
215
  Initialize AWS sessions with 4-profile architecture and comprehensive validation
215
-
216
+
216
217
  Implements enterprise-grade session management with:
217
218
  - Profile validation and credential verification
218
219
  - Comprehensive error handling and fallback
@@ -221,11 +222,11 @@ class EnhancedOrganizationsDiscovery:
221
222
  """
222
223
  profiles_to_test = [
223
224
  ("management", self.management_profile),
224
- ("billing", self.billing_profile),
225
+ ("billing", self.billing_profile),
225
226
  ("operational", self.operational_profile),
226
227
  ("single_account", self.single_account_profile),
227
228
  ]
228
-
229
+
229
230
  session_results = {
230
231
  "status": "initializing",
231
232
  "profiles_tested": 0,
@@ -243,25 +244,24 @@ class EnhancedOrganizationsDiscovery:
243
244
  TimeElapsedColumn(),
244
245
  console=console,
245
246
  ) as progress:
246
-
247
247
  task = progress.add_task("Initializing AWS profiles...", total=len(profiles_to_test))
248
-
248
+
249
249
  for profile_type, profile_name in profiles_to_test:
250
250
  progress.update(task, description=f"Testing profile: {profile_type}")
251
251
  session_results["profiles_tested"] += 1
252
252
  self.discovery_metrics["profiles_tested"] += 1
253
-
253
+
254
254
  try:
255
255
  # Create session and verify credentials
256
256
  session = boto3.Session(profile_name=profile_name)
257
257
  sts_client = session.client("sts")
258
258
  identity = sts_client.get_caller_identity()
259
-
259
+
260
260
  # Store successful session
261
261
  self.sessions[profile_type] = session
262
262
  session_results["profiles_successful"] += 1
263
263
  self.discovery_metrics["profiles_successful"] += 1
264
-
264
+
265
265
  # Store session details
266
266
  session_results["session_details"][profile_type] = {
267
267
  "profile_name": profile_name,
@@ -270,7 +270,7 @@ class EnhancedOrganizationsDiscovery:
270
270
  "user_id": identity["UserId"],
271
271
  "status": "active",
272
272
  }
273
-
273
+
274
274
  # Initialize specific clients based on profile type
275
275
  if profile_type == "management":
276
276
  self.clients["organizations"] = session.client("organizations")
@@ -283,54 +283,62 @@ class EnhancedOrganizationsDiscovery:
283
283
  self.clients["sts_operational"] = sts_client
284
284
  elif profile_type == "single_account":
285
285
  self.clients["sts_single"] = sts_client
286
-
286
+
287
287
  console.print(f"✅ [green]{profile_type}[/green]: {identity['Account']} ({profile_name})")
288
-
288
+
289
289
  except (NoCredentialsError, ClientError) as e:
290
290
  error_msg = f"Profile '{profile_type}' ({profile_name}) failed: {str(e)}"
291
291
  session_results["errors"].append(error_msg)
292
292
  self.discovery_metrics["errors_encountered"] += 1
293
293
  console.print(f"❌ [red]{profile_type}[/red]: {str(e)}")
294
-
294
+
295
295
  # Add warning about missing profile
296
- session_results["warnings"].append(f"Profile {profile_type} unavailable - some features may be limited")
297
-
296
+ session_results["warnings"].append(
297
+ f"Profile {profile_type} unavailable - some features may be limited"
298
+ )
299
+
298
300
  progress.advance(task)
299
-
301
+
300
302
  # Determine overall status
301
303
  if session_results["profiles_successful"] == 0:
302
304
  session_results["status"] = "failed"
303
305
  session_results["message"] = "No AWS profiles could be initialized - check credentials"
304
306
  elif session_results["profiles_successful"] < len(profiles_to_test):
305
307
  session_results["status"] = "partial"
306
- session_results["message"] = f"Initialized {session_results['profiles_successful']}/{len(profiles_to_test)} profiles"
308
+ session_results["message"] = (
309
+ f"Initialized {session_results['profiles_successful']}/{len(profiles_to_test)} profiles"
310
+ )
307
311
  else:
308
312
  session_results["status"] = "success"
309
- session_results["message"] = f"All {session_results['profiles_successful']} profiles initialized successfully"
310
-
313
+ session_results["message"] = (
314
+ f"All {session_results['profiles_successful']} profiles initialized successfully"
315
+ )
316
+
311
317
  # Display summary panel
312
318
  summary_text = f"""
313
- [green]✅ Successful:[/green] {session_results['profiles_successful']}/{len(profiles_to_test)} profiles
314
- [yellow]⚠️ Warnings:[/yellow] {len(session_results['warnings'])} profile issues
315
- [red]❌ Errors:[/red] {len(session_results['errors'])} initialization failures
319
+ [green]✅ Successful:[/green] {session_results["profiles_successful"]}/{len(profiles_to_test)} profiles
320
+ [yellow]⚠️ Warnings:[/yellow] {len(session_results["warnings"])} profile issues
321
+ [red]❌ Errors:[/red] {len(session_results["errors"])} initialization failures
316
322
  """
317
-
318
- console.print(Panel(
319
- summary_text.strip(),
320
- title=f"[bold cyan]4-Profile AWS SSO Initialization[/bold cyan]",
321
- title_align="left",
322
- border_style="cyan"
323
- ))
324
-
323
+
324
+ console.print(
325
+ Panel(
326
+ summary_text.strip(),
327
+ title=f"[bold cyan]4-Profile AWS SSO Initialization[/bold cyan]",
328
+ title_align="left",
329
+ border_style="cyan",
330
+ )
331
+ )
332
+
325
333
  return session_results
326
334
 
327
335
  async def discover_organization_structure(self) -> Dict:
328
336
  """
329
337
  Discover complete organization structure with performance benchmarking
330
-
338
+
331
339
  Enhanced with:
332
340
  - Performance benchmark tracking (<45s target)
333
- - Rich console progress monitoring
341
+ - Rich console progress monitoring
334
342
  - Comprehensive error recovery
335
343
  - Multi-profile fallback support
336
344
  """
@@ -338,23 +346,33 @@ class EnhancedOrganizationsDiscovery:
338
346
  self.current_benchmark = PerformanceBenchmark(
339
347
  operation_name="organization_structure_discovery",
340
348
  start_time=datetime.now(timezone.utc),
341
- target_seconds=self.performance_target_seconds
349
+ target_seconds=self.performance_target_seconds,
342
350
  )
343
-
351
+
344
352
  logger.info("🏢 Starting enhanced organization structure discovery with performance tracking")
345
353
  self.discovery_metrics["start_time"] = self.current_benchmark.start_time
346
-
354
+
347
355
  with Status("Initializing enterprise discovery...", console=console, spinner="dots"):
348
356
  try:
349
357
  # Initialize sessions with 4-profile architecture
350
358
  session_result = self.initialize_sessions()
351
359
  if session_result["status"] == "failed":
352
360
  self.current_benchmark.finish(success=False, error_message="Profile initialization failed")
361
+ # CRITICAL FIX: Ensure performance_grade and metrics are set during early failures
362
+ self.discovery_metrics["performance_grade"] = self.current_benchmark.get_performance_grade()
363
+ self.discovery_metrics["end_time"] = self.current_benchmark.end_time
364
+ self.discovery_metrics["duration_seconds"] = self.current_benchmark.duration_seconds
365
+ self.discovery_metrics["errors_encountered"] += 1
366
+ # Create performance benchmark dict with computed performance_grade for early failures
367
+ performance_benchmark_dict = asdict(self.current_benchmark)
368
+ performance_benchmark_dict["performance_grade"] = self.current_benchmark.get_performance_grade()
369
+
353
370
  return {
354
- "status": "error",
371
+ "status": "error",
355
372
  "error": "Profile initialization failed",
356
373
  "session_result": session_result,
357
- "benchmark": asdict(self.current_benchmark)
374
+ "metrics": self.discovery_metrics,
375
+ "performance_benchmark": performance_benchmark_dict,
358
376
  }
359
377
 
360
378
  # Continue with partial profile set if needed
@@ -370,16 +388,15 @@ class EnhancedOrganizationsDiscovery:
370
388
  TimeElapsedColumn(),
371
389
  console=console,
372
390
  ) as progress:
373
-
374
391
  discovery_task = progress.add_task("Discovering organization structure...", total=5)
375
-
392
+
376
393
  # Discover accounts
377
394
  progress.update(discovery_task, description="Discovering accounts...")
378
395
  accounts_result = await self._discover_accounts()
379
396
  self.current_benchmark.accounts_processed = accounts_result.get("total_accounts", 0)
380
397
  progress.advance(discovery_task)
381
398
 
382
- # Discover organizational units
399
+ # Discover organizational units
383
400
  progress.update(discovery_task, description="Discovering organizational units...")
384
401
  ous_result = await self._discover_organizational_units()
385
402
  progress.advance(discovery_task)
@@ -403,7 +420,7 @@ class EnhancedOrganizationsDiscovery:
403
420
  self.current_benchmark.finish(success=True)
404
421
  self.current_benchmark.api_calls_made = self.discovery_metrics["api_calls_made"]
405
422
  self.benchmarks.append(self.current_benchmark)
406
-
423
+
407
424
  # Calculate final metrics
408
425
  self.discovery_metrics["end_time"] = self.current_benchmark.end_time
409
426
  self.discovery_metrics["duration_seconds"] = self.current_benchmark.duration_seconds
@@ -416,18 +433,24 @@ class EnhancedOrganizationsDiscovery:
416
433
 
417
434
  ⏱️ Duration: [bold {performance_color}]{self.current_benchmark.duration_seconds:.1f}s[/bold {performance_color}] (Target: {self.performance_target_seconds}s)
418
435
  📈 Grade: [bold {performance_color}]{self.current_benchmark.get_performance_grade()}[/bold {performance_color}]
419
- 🏢 Accounts: [yellow]{self.discovery_metrics['accounts_discovered']}[/yellow]
420
- 🏗️ OUs: [yellow]{self.discovery_metrics['ous_discovered']}[/yellow]
421
- 🔐 Roles: [yellow]{self.discovery_metrics['roles_discovered']}[/yellow]
422
- 📡 API Calls: [blue]{self.discovery_metrics['api_calls_made']}[/blue]
436
+ 🏢 Accounts: [yellow]{self.discovery_metrics["accounts_discovered"]}[/yellow]
437
+ 🏗️ OUs: [yellow]{self.discovery_metrics["ous_discovered"]}[/yellow]
438
+ 🔐 Roles: [yellow]{self.discovery_metrics["roles_discovered"]}[/yellow]
439
+ 📡 API Calls: [blue]{self.discovery_metrics["api_calls_made"]}[/blue]
423
440
  """
424
-
425
- console.print(Panel(
426
- performance_text.strip(),
427
- title="[bold green]✅ Discovery Complete[/bold green]",
428
- title_align="left",
429
- border_style="green" if self.current_benchmark.is_within_target() else "red"
430
- ))
441
+
442
+ console.print(
443
+ Panel(
444
+ performance_text.strip(),
445
+ title="[bold green]✅ Discovery Complete[/bold green]",
446
+ title_align="left",
447
+ border_style="green" if self.current_benchmark.is_within_target() else "red",
448
+ )
449
+ )
450
+
451
+ # Create performance benchmark dict with computed performance_grade
452
+ performance_benchmark_dict = asdict(self.current_benchmark)
453
+ performance_benchmark_dict["performance_grade"] = self.current_benchmark.get_performance_grade()
431
454
 
432
455
  return {
433
456
  "status": "completed",
@@ -438,7 +461,7 @@ class EnhancedOrganizationsDiscovery:
438
461
  "cross_account_roles": roles_result,
439
462
  "session_info": session_result,
440
463
  "metrics": self.discovery_metrics,
441
- "performance_benchmark": asdict(self.current_benchmark),
464
+ "performance_benchmark": performance_benchmark_dict,
442
465
  "timestamp": datetime.now().isoformat(),
443
466
  }
444
467
 
@@ -446,27 +469,45 @@ class EnhancedOrganizationsDiscovery:
446
469
  # Handle discovery failure
447
470
  error_message = f"Organization discovery failed: {str(e)}"
448
471
  logger.error(error_message)
449
-
472
+
450
473
  if self.current_benchmark:
451
474
  self.current_benchmark.finish(success=False, error_message=error_message)
452
475
  self.benchmarks.append(self.current_benchmark)
453
-
476
+ # CRITICAL FIX: Ensure performance_grade is always set, even during errors
477
+ self.discovery_metrics["performance_grade"] = self.current_benchmark.get_performance_grade()
478
+ else:
479
+ # No benchmark available - set failed performance grade
480
+ self.discovery_metrics["performance_grade"] = "F"
481
+
454
482
  self.discovery_metrics["errors_encountered"] += 1
455
-
483
+ # Ensure end_time and duration are set for error cases
484
+ self.discovery_metrics["end_time"] = datetime.now(timezone.utc)
485
+ if self.discovery_metrics["start_time"]:
486
+ duration = (
487
+ self.discovery_metrics["end_time"] - self.discovery_metrics["start_time"]
488
+ ).total_seconds()
489
+ self.discovery_metrics["duration_seconds"] = duration
490
+
491
+ # Create performance benchmark dict with computed performance_grade for errors
492
+ performance_benchmark_dict = None
493
+ if self.current_benchmark:
494
+ performance_benchmark_dict = asdict(self.current_benchmark)
495
+ performance_benchmark_dict["performance_grade"] = self.current_benchmark.get_performance_grade()
496
+
456
497
  return {
457
- "status": "error",
458
- "error": error_message,
498
+ "status": "error",
499
+ "error": error_message,
459
500
  "metrics": self.discovery_metrics,
460
- "performance_benchmark": asdict(self.current_benchmark) if self.current_benchmark else None
501
+ "performance_benchmark": performance_benchmark_dict,
461
502
  }
462
503
 
463
504
  async def _discover_accounts(self) -> Dict:
464
505
  """
465
506
  Discover all accounts in the organization using 4-profile architecture
466
-
507
+
467
508
  Enhanced with:
468
509
  - Multi-profile fallback support
469
- - Comprehensive error handling
510
+ - Comprehensive error handling
470
511
  - Performance optimizations
471
512
  - Rich progress tracking
472
513
  """
@@ -514,13 +555,13 @@ class EnhancedOrganizationsDiscovery:
514
555
  active_accounts = [a for a in accounts if a.status == "ACTIVE"]
515
556
  suspended_accounts = [a for a in accounts if a.status == "SUSPENDED"]
516
557
  closed_accounts = [a for a in accounts if a.status == "CLOSED"]
517
-
558
+
518
559
  logger.info(f"✅ Discovered {len(accounts)} total accounts ({len(active_accounts)} active)")
519
560
 
520
561
  return {
521
562
  "total_accounts": len(accounts),
522
563
  "active_accounts": len(active_accounts),
523
- "suspended_accounts": len(suspended_accounts),
564
+ "suspended_accounts": len(suspended_accounts),
524
565
  "closed_accounts": len(closed_accounts),
525
566
  "accounts": [asdict(account) for account in accounts],
526
567
  "discovery_method": "organizations_api",
@@ -530,7 +571,7 @@ class EnhancedOrganizationsDiscovery:
530
571
  except ClientError as e:
531
572
  logger.error(f"Failed to discover accounts via Organizations API: {e}")
532
573
  self.discovery_metrics["errors_encountered"] += 1
533
-
574
+
534
575
  # Attempt fallback discovery
535
576
  logger.info("Attempting fallback account discovery...")
536
577
  return await self._discover_accounts_fallback()
@@ -538,19 +579,19 @@ class EnhancedOrganizationsDiscovery:
538
579
  async def _discover_accounts_fallback(self) -> Dict:
539
580
  """
540
581
  Fallback account discovery when Organizations API is not available
541
-
582
+
542
583
  Uses individual profile sessions to identify accessible accounts
543
584
  """
544
585
  logger.info("🔄 Using fallback account discovery via individual profiles")
545
-
586
+
546
587
  discovered_accounts = {}
547
-
588
+
548
589
  for profile_type, session in self.sessions.items():
549
590
  try:
550
591
  sts_client = session.client("sts")
551
592
  identity = sts_client.get_caller_identity()
552
593
  account_id = identity["Account"]
553
-
594
+
554
595
  if account_id not in discovered_accounts:
555
596
  # Create account info from STS identity
556
597
  account = AWSAccount(
@@ -560,28 +601,28 @@ class EnhancedOrganizationsDiscovery:
560
601
  status="ACTIVE", # Assume active if accessible
561
602
  joined_method="UNKNOWN",
562
603
  joined_timestamp=None,
563
- tags={"DiscoveryMethod": "fallback", "ProfileType": profile_type}
604
+ tags={"DiscoveryMethod": "fallback", "ProfileType": profile_type},
564
605
  )
565
-
606
+
566
607
  discovered_accounts[account_id] = account
567
608
  self.accounts_cache[account_id] = account
568
-
609
+
569
610
  self.discovery_metrics["api_calls_made"] += 1
570
-
611
+
571
612
  except Exception as e:
572
613
  logger.debug(f"Could not get identity for profile {profile_type}: {e}")
573
614
  continue
574
-
615
+
575
616
  accounts = list(discovered_accounts.values())
576
617
  self.discovery_metrics["accounts_discovered"] = len(accounts)
577
-
618
+
578
619
  logger.info(f"✅ Fallback discovery found {len(accounts)} accessible accounts")
579
-
620
+
580
621
  return {
581
622
  "total_accounts": len(accounts),
582
623
  "active_accounts": len(accounts), # All fallback accounts assumed active
583
624
  "suspended_accounts": 0,
584
- "closed_accounts": 0,
625
+ "closed_accounts": 0,
585
626
  "accounts": [asdict(account) for account in accounts],
586
627
  "discovery_method": "fallback_sts",
587
628
  "profile_used": "multiple",
@@ -590,7 +631,7 @@ class EnhancedOrganizationsDiscovery:
590
631
  async def _discover_organizational_units(self) -> Dict:
591
632
  """
592
633
  Discover all organizational units with enhanced error handling
593
-
634
+
594
635
  Enhanced with:
595
636
  - Multi-profile fallback support
596
637
  - Comprehensive error recovery
@@ -606,12 +647,12 @@ class EnhancedOrganizationsDiscovery:
606
647
  "total_ous": 0,
607
648
  "organizational_units": [],
608
649
  "discovery_method": "unavailable",
609
- "message": "Organizations API not accessible - OU discovery skipped"
650
+ "message": "Organizations API not accessible - OU discovery skipped",
610
651
  }
611
652
 
612
653
  try:
613
654
  organizations_client = self.clients["organizations"]
614
-
655
+
615
656
  # Get root OU
616
657
  roots_response = organizations_client.list_roots()
617
658
  if not roots_response.get("Roots"):
@@ -621,9 +662,9 @@ class EnhancedOrganizationsDiscovery:
621
662
  "total_ous": 0,
622
663
  "organizational_units": [],
623
664
  "discovery_method": "organizations_api",
624
- "message": "No root OUs found in organization"
665
+ "message": "No root OUs found in organization",
625
666
  }
626
-
667
+
627
668
  root_id = roots_response["Roots"][0]["Id"]
628
669
  self.discovery_metrics["api_calls_made"] += 1
629
670
 
@@ -634,7 +675,7 @@ class EnhancedOrganizationsDiscovery:
634
675
  except ClientError as ou_error:
635
676
  logger.warning(f"Partial OU discovery failed: {ou_error}")
636
677
  # Continue with what we have discovered so far
637
-
678
+
638
679
  self.discovery_metrics["ous_discovered"] = len(all_ous)
639
680
 
640
681
  logger.info(f"✅ Discovered {len(all_ous)} organizational units")
@@ -650,7 +691,7 @@ class EnhancedOrganizationsDiscovery:
650
691
  except ClientError as e:
651
692
  logger.error(f"Failed to discover OUs: {e}")
652
693
  self.discovery_metrics["errors_encountered"] += 1
653
-
694
+
654
695
  # Return graceful failure result instead of raising
655
696
  return {
656
697
  "root_id": None,
@@ -658,14 +699,14 @@ class EnhancedOrganizationsDiscovery:
658
699
  "organizational_units": [],
659
700
  "discovery_method": "failed",
660
701
  "error": str(e),
661
- "message": "OU discovery failed - continuing without organizational structure"
702
+ "message": "OU discovery failed - continuing without organizational structure",
662
703
  }
663
704
 
664
705
  async def _discover_ou_recursive(self, parent_id: str, ou_list: List[OrganizationalUnit]):
665
706
  """Recursively discover organizational units with enhanced error handling"""
666
707
  try:
667
708
  organizations_client = self.clients["organizations"]
668
-
709
+
669
710
  # Get child OUs
670
711
  paginator = organizations_client.get_paginator("list_organizational_units_for_parent")
671
712
 
@@ -702,7 +743,7 @@ class EnhancedOrganizationsDiscovery:
702
743
 
703
744
  try:
704
745
  organizations_client = self.clients["organizations"]
705
-
746
+
706
747
  for account_id, account in self.accounts_cache.items():
707
748
  # Find which OU this account belongs to
708
749
  try:
@@ -821,14 +862,14 @@ class EnhancedOrganizationsDiscovery:
821
862
  logger.warning("Organizations client not available - using fallback organization info")
822
863
  return {
823
864
  "organization_id": "unavailable",
824
- "master_account_id": "unavailable",
865
+ "master_account_id": "unavailable",
825
866
  "master_account_email": "unavailable",
826
867
  "feature_set": "unavailable",
827
868
  "available_policy_types": [],
828
869
  "discovery_method": "unavailable",
829
- "message": "Organizations API not accessible"
870
+ "message": "Organizations API not accessible",
830
871
  }
831
-
872
+
832
873
  try:
833
874
  organizations_client = self.clients["organizations"]
834
875
  org_response = organizations_client.describe_organization()
@@ -849,18 +890,18 @@ class EnhancedOrganizationsDiscovery:
849
890
  return {
850
891
  "organization_id": "error",
851
892
  "master_account_id": "error",
852
- "master_account_email": "error",
893
+ "master_account_email": "error",
853
894
  "feature_set": "error",
854
895
  "available_policy_types": [],
855
896
  "discovery_method": "failed",
856
897
  "error": str(e),
857
- "message": "Organization info retrieval failed"
898
+ "message": "Organization info retrieval failed",
858
899
  }
859
900
 
860
901
  async def get_cost_validation_data(self, time_range_days: int = 30) -> Dict:
861
902
  """
862
903
  Get cost data for validation and analysis using 4-profile architecture
863
-
904
+
864
905
  Enhanced with:
865
906
  - Billing profile validation and fallback
866
907
  - Comprehensive error handling
@@ -880,7 +921,7 @@ class EnhancedOrganizationsDiscovery:
880
921
  "cost_by_account": {},
881
922
  "high_spend_accounts": {},
882
923
  "discovery_method": "unavailable",
883
- "message": "Billing profile not accessible - cost data unavailable"
924
+ "message": "Billing profile not accessible - cost data unavailable",
884
925
  }
885
926
 
886
927
  try:
@@ -918,11 +959,15 @@ class EnhancedOrganizationsDiscovery:
918
959
 
919
960
  # Enhanced cost analysis
920
961
  high_spend_accounts = {
921
- k: round(v, 2) for k, v in cost_by_account.items() if v > 1000 # >$1000/month
962
+ k: round(v, 2)
963
+ for k, v in cost_by_account.items()
964
+ if v > 1000 # >$1000/month
922
965
  }
923
-
966
+
924
967
  medium_spend_accounts = {
925
- k: round(v, 2) for k, v in cost_by_account.items() if 100 <= v <= 1000 # $100-$1000/month
968
+ k: round(v, 2)
969
+ for k, v in cost_by_account.items()
970
+ if 100 <= v <= 1000 # $100-$1000/month
926
971
  }
927
972
 
928
973
  logger.info(f"✅ Cost validation complete: ${total_cost:.2f} across {len(cost_by_account)} accounts")
@@ -939,16 +984,16 @@ class EnhancedOrganizationsDiscovery:
939
984
  "profile_used": "billing",
940
985
  "cost_breakdown": {
941
986
  "high_spend_count": len(high_spend_accounts),
942
- "medium_spend_count": len(medium_spend_accounts),
987
+ "medium_spend_count": len(medium_spend_accounts),
943
988
  "low_spend_count": len(cost_by_account) - len(high_spend_accounts) - len(medium_spend_accounts),
944
989
  "average_cost_per_account": round(total_cost / len(cost_by_account), 2) if cost_by_account else 0,
945
- }
990
+ },
946
991
  }
947
992
 
948
993
  except ClientError as e:
949
994
  logger.error(f"Failed to get cost data: {e}")
950
995
  self.discovery_metrics["errors_encountered"] += 1
951
-
996
+
952
997
  return {
953
998
  "status": "error",
954
999
  "time_range_days": time_range_days,
@@ -1089,16 +1134,16 @@ async def run_enhanced_organizations_discovery(
1089
1134
  ) -> Dict:
1090
1135
  """
1091
1136
  Run complete enhanced organizations discovery workflow with 4-profile architecture
1092
-
1137
+
1093
1138
  Implements proven FinOps success patterns with enterprise-grade reliability:
1094
1139
  - 4-profile AWS SSO architecture with failover
1095
- - Performance benchmarking targeting <45s operations
1140
+ - Performance benchmarking targeting <45s operations
1096
1141
  - Comprehensive error handling and profile fallbacks
1097
1142
  - Rich console progress tracking and monitoring
1098
-
1143
+
1099
1144
  Args:
1100
1145
  management_profile: AWS profile with Organizations access (defaults to proven enterprise profile)
1101
- billing_profile: AWS profile with Cost Explorer access (defaults to proven enterprise profile)
1146
+ billing_profile: AWS profile with Cost Explorer access (defaults to proven enterprise profile)
1102
1147
  operational_profile: AWS profile with operational access (defaults to proven enterprise profile)
1103
1148
  single_account_profile: AWS profile for single account operations (defaults to proven enterprise profile)
1104
1149
  performance_target_seconds: Performance target for discovery operations (default: 45s)
@@ -1107,17 +1152,19 @@ async def run_enhanced_organizations_discovery(
1107
1152
  Complete discovery results with organization structure, costs, analysis, and performance metrics
1108
1153
  """
1109
1154
 
1110
- console.print(Panel.fit(
1111
- "[bold bright_cyan]🚀 Enhanced Organizations Discovery[/bold bright_cyan]\n\n"
1112
- "[green] Features:[/green]\n"
1113
- " 4-Profile AWS SSO Architecture\n"
1114
- "• Performance Benchmarking (<45s target)\n"
1115
- "• Comprehensive Error Handling\n"
1116
- "• Multi-Account Enterprise Scale\n\n"
1117
- "[yellow]⚡ Initializing enhanced discovery engine...[/yellow]",
1118
- title="Enterprise Discovery Engine v0.8.0",
1119
- style="bright_cyan"
1120
- ))
1155
+ console.print(
1156
+ Panel.fit(
1157
+ "[bold bright_cyan]🚀 Enhanced Organizations Discovery[/bold bright_cyan]\n\n"
1158
+ "[green]✨ Features:[/green]\n"
1159
+ "• 4-Profile AWS SSO Architecture\n"
1160
+ "• Performance Benchmarking (<45s target)\n"
1161
+ "• Comprehensive Error Handling\n"
1162
+ " Multi-Account Enterprise Scale\n\n"
1163
+ "[yellow]⚡ Initializing enhanced discovery engine...[/yellow]",
1164
+ title="Enterprise Discovery Engine v0.8.0",
1165
+ style="bright_cyan",
1166
+ )
1167
+ )
1121
1168
 
1122
1169
  discovery = EnhancedOrganizationsDiscovery(
1123
1170
  management_profile=management_profile,
@@ -1125,14 +1172,14 @@ async def run_enhanced_organizations_discovery(
1125
1172
  operational_profile=operational_profile,
1126
1173
  single_account_profile=single_account_profile,
1127
1174
  max_workers=50,
1128
- performance_target_seconds=performance_target_seconds
1175
+ performance_target_seconds=performance_target_seconds,
1129
1176
  )
1130
1177
 
1131
1178
  # Run main discovery with performance benchmarking
1132
1179
  org_results = await discovery.discover_organization_structure()
1133
1180
 
1134
1181
  if org_results["status"] == "completed":
1135
- # Add cost validation using billing profile
1182
+ # Add cost validation using billing profile
1136
1183
  cost_data = await discovery.get_cost_validation_data()
1137
1184
  org_results["cost_validation"] = cost_data
1138
1185
 
@@ -1143,19 +1190,20 @@ async def run_enhanced_organizations_discovery(
1143
1190
  # Add hierarchy visualization
1144
1191
  hierarchy_viz = discovery.generate_account_hierarchy_visualization()
1145
1192
  org_results["hierarchy_visualization"] = hierarchy_viz
1146
-
1193
+
1147
1194
  # Add performance summary
1148
1195
  org_results["performance_summary"] = {
1149
1196
  "benchmarks_completed": len(discovery.benchmarks),
1150
1197
  "total_duration": org_results["performance_benchmark"]["duration_seconds"],
1151
- "performance_grade": org_results["performance_benchmark"]["performance_grade"],
1198
+ "performance_grade": org_results["performance_benchmark"].get("performance_grade", "N/A"),
1152
1199
  "target_achieved": discovery.current_benchmark.is_within_target() if discovery.current_benchmark else False,
1153
1200
  "profiles_successful": org_results["session_info"]["profiles_successful"],
1154
- "api_calls_total": org_results["metrics"]["api_calls_made"]
1201
+ "api_calls_total": org_results["metrics"]["api_calls_made"],
1155
1202
  }
1156
1203
 
1157
1204
  return org_results
1158
1205
 
1206
+
1159
1207
  # Legacy compatibility function
1160
1208
  async def run_organizations_discovery(
1161
1209
  management_profile: str = "ams-admin-ReadOnlyAccess-909135376185",
@@ -1163,12 +1211,12 @@ async def run_organizations_discovery(
1163
1211
  ) -> Dict:
1164
1212
  """
1165
1213
  Legacy compatibility function - redirects to enhanced discovery
1166
-
1214
+
1167
1215
  Returns:
1168
1216
  Complete discovery results using enhanced 4-profile architecture
1169
1217
  """
1170
1218
  console.print("[yellow]ℹ️ Using enhanced discovery engine for improved reliability and performance[/yellow]")
1171
-
1219
+
1172
1220
  return await run_enhanced_organizations_discovery(
1173
1221
  management_profile=management_profile,
1174
1222
  billing_profile=billing_profile,
@@ -1191,7 +1239,7 @@ if __name__ == "__main__":
1191
1239
  help=f"AWS profile with Cost Explorer access (default: {ENTERPRISE_PROFILES['BILLING_PROFILE']})",
1192
1240
  )
1193
1241
  parser.add_argument(
1194
- "--operational-profile",
1242
+ "--operational-profile",
1195
1243
  help=f"AWS profile with operational access (default: {ENTERPRISE_PROFILES['CENTRALISED_OPS_PROFILE']})",
1196
1244
  )
1197
1245
  parser.add_argument(
@@ -1205,11 +1253,7 @@ if __name__ == "__main__":
1205
1253
  help="Performance target in seconds (default: 45s)",
1206
1254
  )
1207
1255
  parser.add_argument("--output", "-o", default="enhanced_organizations_discovery.json", help="Output file path")
1208
- parser.add_argument(
1209
- "--legacy",
1210
- action="store_true",
1211
- help="Use legacy discovery method (compatibility mode)"
1212
- )
1256
+ parser.add_argument("--legacy", action="store_true", help="Use legacy discovery method (compatibility mode)")
1213
1257
 
1214
1258
  args = parser.parse_args()
1215
1259
 
@@ -1218,7 +1262,7 @@ if __name__ == "__main__":
1218
1262
  console.print("[yellow]⚠️ Using legacy compatibility mode[/yellow]")
1219
1263
  results = await run_organizations_discovery(
1220
1264
  management_profile=args.management_profile or ENTERPRISE_PROFILES["MANAGEMENT_PROFILE"],
1221
- billing_profile=args.billing_profile or ENTERPRISE_PROFILES["BILLING_PROFILE"]
1265
+ billing_profile=args.billing_profile or ENTERPRISE_PROFILES["BILLING_PROFILE"],
1222
1266
  )
1223
1267
  else:
1224
1268
  console.print("[cyan]🚀 Using enhanced 4-profile discovery engine[/cyan]")
@@ -1235,35 +1279,37 @@ if __name__ == "__main__":
1235
1279
  json.dump(results, f, indent=2, default=str)
1236
1280
 
1237
1281
  # Create enhanced Rich formatted summary
1238
- accounts_count = results.get('accounts', {}).get('total_accounts', 0)
1239
- ous_count = results.get('organizational_units', {}).get('total_ous', 0)
1240
- monthly_cost = results.get('cost_validation', {}).get('total_monthly_cost', 0)
1241
-
1282
+ accounts_count = results.get("accounts", {}).get("total_accounts", 0)
1283
+ ous_count = results.get("organizational_units", {}).get("total_ous", 0)
1284
+ monthly_cost = results.get("cost_validation", {}).get("total_monthly_cost", 0)
1285
+
1242
1286
  # Performance metrics if available
1243
- performance_grade = results.get('performance_benchmark', {}).get('performance_grade', 'N/A')
1244
- duration = results.get('performance_benchmark', {}).get('duration_seconds', 0)
1245
- profiles_successful = results.get('session_info', {}).get('profiles_successful', 0)
1246
-
1287
+ performance_grade = results.get("performance_benchmark", {}).get("performance_grade", "N/A")
1288
+ duration = results.get("performance_benchmark", {}).get("duration_seconds", 0)
1289
+ profiles_successful = results.get("session_info", {}).get("profiles_successful", 0)
1290
+
1247
1291
  summary_table = Table(show_header=False, box=None)
1248
1292
  summary_table.add_column("Metric", style="cyan", no_wrap=True)
1249
1293
  summary_table.add_column("Value", style="green")
1250
-
1294
+
1251
1295
  summary_table.add_row("📊 Accounts discovered:", f"{accounts_count}")
1252
1296
  summary_table.add_row("🏢 OUs discovered:", f"{ous_count}")
1253
1297
  summary_table.add_row("💰 Monthly cost:", f"${monthly_cost:,.2f}" if monthly_cost else "N/A")
1254
-
1298
+
1255
1299
  if not args.legacy:
1256
1300
  summary_table.add_row("⚡ Performance grade:", f"{performance_grade}")
1257
1301
  summary_table.add_row("⏱️ Duration:", f"{duration:.1f}s")
1258
1302
  summary_table.add_row("🔧 Profiles active:", f"{profiles_successful}/4")
1259
-
1303
+
1260
1304
  title_color = "green" if performance_grade in ["A+", "A", "B"] else "yellow"
1261
-
1262
- console.print(Panel(
1263
- summary_table,
1264
- title=f"[{title_color}]✅ Enhanced Discovery Complete - Results saved to {args.output}[/{title_color}]",
1265
- title_align="left",
1266
- border_style=title_color
1267
- ))
1305
+
1306
+ console.print(
1307
+ Panel(
1308
+ summary_table,
1309
+ title=f"[{title_color}]✅ Enhanced Discovery Complete - Results saved to {args.output}[/{title_color}]",
1310
+ title_align="left",
1311
+ border_style=title_color,
1312
+ )
1313
+ )
1268
1314
 
1269
1315
  asyncio.run(main())