runbooks 0.9.2__py3-none-any.whl → 0.9.5__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. runbooks/__init__.py +15 -6
  2. runbooks/cfat/__init__.py +3 -1
  3. runbooks/cloudops/__init__.py +3 -1
  4. runbooks/common/aws_utils.py +367 -0
  5. runbooks/common/enhanced_logging_example.py +239 -0
  6. runbooks/common/enhanced_logging_integration_example.py +257 -0
  7. runbooks/common/logging_integration_helper.py +344 -0
  8. runbooks/common/profile_utils.py +8 -6
  9. runbooks/common/rich_utils.py +347 -3
  10. runbooks/enterprise/logging.py +400 -38
  11. runbooks/finops/README.md +262 -406
  12. runbooks/finops/__init__.py +44 -1
  13. runbooks/finops/accuracy_cross_validator.py +12 -3
  14. runbooks/finops/business_cases.py +552 -0
  15. runbooks/finops/commvault_ec2_analysis.py +415 -0
  16. runbooks/finops/cost_processor.py +718 -42
  17. runbooks/finops/dashboard_router.py +44 -22
  18. runbooks/finops/dashboard_runner.py +302 -39
  19. runbooks/finops/embedded_mcp_validator.py +358 -48
  20. runbooks/finops/finops_scenarios.py +1122 -0
  21. runbooks/finops/helpers.py +182 -0
  22. runbooks/finops/multi_dashboard.py +30 -15
  23. runbooks/finops/scenarios.py +789 -0
  24. runbooks/finops/single_dashboard.py +386 -58
  25. runbooks/finops/types.py +29 -4
  26. runbooks/inventory/__init__.py +2 -1
  27. runbooks/main.py +522 -29
  28. runbooks/operate/__init__.py +3 -1
  29. runbooks/remediation/__init__.py +3 -1
  30. runbooks/remediation/commons.py +55 -16
  31. runbooks/remediation/commvault_ec2_analysis.py +259 -0
  32. runbooks/remediation/rds_snapshot_list.py +267 -102
  33. runbooks/remediation/workspaces_list.py +182 -31
  34. runbooks/security/__init__.py +3 -1
  35. runbooks/sre/__init__.py +2 -1
  36. runbooks/utils/__init__.py +81 -6
  37. runbooks/utils/version_validator.py +241 -0
  38. runbooks/vpc/__init__.py +2 -1
  39. {runbooks-0.9.2.dist-info → runbooks-0.9.5.dist-info}/METADATA +98 -60
  40. {runbooks-0.9.2.dist-info → runbooks-0.9.5.dist-info}/RECORD +44 -39
  41. {runbooks-0.9.2.dist-info → runbooks-0.9.5.dist-info}/entry_points.txt +1 -0
  42. runbooks/inventory/cloudtrail.md +0 -727
  43. runbooks/inventory/discovery.md +0 -81
  44. runbooks/remediation/CLAUDE.md +0 -100
  45. runbooks/remediation/DOME9.md +0 -218
  46. runbooks/security/ENTERPRISE_SECURITY_FRAMEWORK.md +0 -506
  47. {runbooks-0.9.2.dist-info → runbooks-0.9.5.dist-info}/WHEEL +0 -0
  48. {runbooks-0.9.2.dist-info → runbooks-0.9.5.dist-info}/licenses/LICENSE +0 -0
  49. {runbooks-0.9.2.dist-info → runbooks-0.9.5.dist-info}/top_level.txt +0 -0
@@ -55,13 +55,22 @@ from .cost_processor import (
55
55
  get_cost_data,
56
56
  process_service_costs,
57
57
  )
58
- from .dashboard_runner import (
59
- _create_cost_session,
60
- _create_management_session,
61
- _create_operational_session,
58
+ from runbooks.common.profile_utils import (
59
+ create_cost_session,
60
+ create_management_session,
61
+ create_operational_session,
62
62
  )
63
63
  from .enhanced_progress import EnhancedProgressTracker
64
64
  from .helpers import export_cost_dashboard_to_pdf
65
+
66
+ # Embedded MCP Integration for Cross-Validation (Enterprise Accuracy Standards)
67
+ try:
68
+ from .embedded_mcp_validator import EmbeddedMCPValidator, validate_finops_results_with_embedded_mcp
69
+ EMBEDDED_MCP_AVAILABLE = True
70
+ print_info("Enterprise accuracy validation enabled - Embedded MCP validator loaded successfully")
71
+ except ImportError:
72
+ EMBEDDED_MCP_AVAILABLE = False
73
+ print_warning("Cross-validation unavailable - Embedded MCP validation module not found")
65
74
  from .service_mapping import get_service_display_name
66
75
 
67
76
 
@@ -155,9 +164,9 @@ class SingleAccountDashboard:
155
164
  """Execute the service-focused cost analysis."""
156
165
  try:
157
166
  # Initialize sessions
158
- cost_session = _create_cost_session(profile)
159
- mgmt_session = _create_management_session(profile)
160
- ops_session = _create_operational_session(profile)
167
+ cost_session = create_cost_session(profile)
168
+ mgmt_session = create_management_session(profile)
169
+ ops_session = create_operational_session(profile)
161
170
 
162
171
  # Initialize account resolver for readable account names
163
172
  management_profile = os.getenv("MANAGEMENT_PROFILE") or profile
@@ -262,31 +271,53 @@ class SingleAccountDashboard:
262
271
  # Prepare service data for markdown export with Tax filtering
263
272
  current_services = cost_data.get("costs_by_service", {})
264
273
  previous_services = last_month_data.get("costs_by_service", {}) # Use already collected data
274
+ quarterly_services = last_month_data.get("quarterly_costs_by_service", {}) # Add quarterly data
265
275
 
266
276
  # Apply same Tax filtering for consistent markdown export
267
277
  filtered_current_services = filter_analytical_services(current_services)
268
278
  filtered_previous_services = filter_analytical_services(previous_services)
279
+ filtered_quarterly_services = filter_analytical_services(quarterly_services)
269
280
 
270
281
  all_services_sorted = sorted(filtered_current_services.items(), key=lambda x: x[1], reverse=True)
271
282
 
272
- # Calculate totals for markdown export
283
+ # Calculate totals for markdown export with quarterly context
273
284
  total_current = cost_data.get("current_month", 0)
274
285
  total_previous = cost_data.get("last_month", 0)
286
+ total_quarterly = sum(filtered_quarterly_services.values())
275
287
  total_trend_pct = ((total_current - total_previous) / total_previous * 100) if total_previous > 0 else 0
276
288
 
277
289
  self._export_service_table_to_markdown(
278
290
  all_services_sorted,
279
291
  filtered_current_services,
280
292
  filtered_previous_services,
293
+ filtered_quarterly_services,
281
294
  profile,
282
295
  account_id,
283
296
  total_current,
284
297
  total_previous,
298
+ total_quarterly,
285
299
  total_trend_pct,
286
300
  args,
287
301
  )
288
302
 
289
303
  print_success(f"Service analysis completed for account {account_id}")
304
+
305
+ # Export functionality - Add PDF/CSV/JSON support to enhanced router
306
+ # Get service data for export (recreate since it's scoped to display function)
307
+ current_services = cost_data.get("costs_by_service", {})
308
+ filtered_services = filter_analytical_services(current_services)
309
+ service_list = sorted(filtered_services.items(), key=lambda x: x[1], reverse=True)
310
+ self._handle_exports(args, profile, account_id, service_list, cost_data, last_month_data)
311
+
312
+ # MCP Cross-Validation for Enterprise Accuracy Standards (>=99.5%)
313
+ # Note: User explicitly requested real MCP validation after discovering fabricated accuracy claims
314
+ validate_flag = getattr(args, 'validate', False)
315
+ if validate_flag or EMBEDDED_MCP_AVAILABLE:
316
+ if EMBEDDED_MCP_AVAILABLE:
317
+ self._run_embedded_mcp_validation([profile], cost_data, service_list, args)
318
+ else:
319
+ self.console.print(f"[yellow]⚠️ MCP validation requested but not available - check MCP server configuration[/]")
320
+
290
321
  return 0
291
322
 
292
323
  except Exception as e:
@@ -294,14 +325,57 @@ class SingleAccountDashboard:
294
325
  return 1
295
326
 
296
327
  def _get_last_month_trends(self, cost_session: boto3.Session, profile: str) -> Dict[str, Any]:
297
- """Get last month cost data for trend analysis."""
328
+ """
329
+ Get accurate trend data using equal-period comparisons with quarterly context.
330
+
331
+ MATHEMATICAL FIX: Replaces the previous implementation that used 60-day time ranges
332
+ which created unequal period comparisons (e.g., 2 days vs 31 days).
333
+
334
+ Now uses month-to-date vs same period from previous month for accurate trends,
335
+ enhanced with quarterly data for strategic financial intelligence.
336
+ """
298
337
  try:
299
- # Get cost data for previous month
300
- previous_month_data = get_cost_data(cost_session, 60, None, profile_name=profile) # 60 days for comparison
301
- return previous_month_data
338
+ # Use the corrected get_cost_data function without time_range parameter
339
+ # This will use the enhanced logic for equal-period comparisons
340
+ corrected_trend_data = get_cost_data(cost_session, None, None, profile_name=profile)
341
+
342
+ # ENHANCEMENT: Add quarterly cost data for strategic context
343
+ from .cost_processor import get_quarterly_cost_data
344
+ quarterly_costs = get_quarterly_cost_data(cost_session, profile_name=profile)
345
+
346
+ # Integrate quarterly data into trend data structure
347
+ corrected_trend_data["quarterly_costs_by_service"] = quarterly_costs
348
+
349
+ # Log the trend analysis context for transparency
350
+ if "period_metadata" in corrected_trend_data:
351
+ metadata = corrected_trend_data["period_metadata"]
352
+ current_days = metadata.get("current_days", 0)
353
+ previous_days = metadata.get("previous_days", 0)
354
+ reliability = metadata.get("trend_reliability", "unknown")
355
+
356
+ if metadata.get("is_partial_comparison", False):
357
+ print_warning(f"Partial period comparison detected: {current_days} vs {previous_days} days")
358
+ print_info(f"Trend reliability: {reliability}")
359
+ else:
360
+ print_success(f"Equal period comparison: {current_days} vs {previous_days} days")
361
+
362
+ return corrected_trend_data
363
+
302
364
  except Exception as e:
303
- print_warning(f"Trend data collection failed: {str(e)[:30]}")
304
- return {}
365
+ print_warning(f"Enhanced trend data collection failed: {str(e)[:50]}")
366
+ # Return basic structure to prevent downstream errors
367
+ return {
368
+ "current_month": 0,
369
+ "last_month": 0,
370
+ "costs_by_service": {},
371
+ "quarterly_costs_by_service": {}, # Added for quarterly intelligence
372
+ "period_metadata": {
373
+ "current_days": 0,
374
+ "previous_days": 0,
375
+ "is_partial_comparison": True,
376
+ "trend_reliability": "unavailable"
377
+ }
378
+ }
305
379
 
306
380
  def _analyze_service_utilization(self, ops_session: boto3.Session, cost_data: Dict[str, Any]) -> Dict[str, Any]:
307
381
  """Analyze service utilization patterns for optimization opportunities."""
@@ -437,45 +511,80 @@ class SingleAccountDashboard:
437
511
  table = Table(
438
512
  Column("Service", style="resource", width=20),
439
513
  Column("Current Cost", justify="right", style="cost", width=15),
440
- Column("Last Month", justify="right", width=15),
514
+ Column("Last Month", justify="right", width=12),
515
+ Column("Last Quarter", justify="right", width=12),
441
516
  Column("Trend", justify="center", width=10),
442
517
  Column("Optimization Opportunities", width=35),
443
518
  title=f"🎯 TOP {top_services} Services Analysis - {account_display}",
444
519
  box=box.ROUNDED,
445
520
  show_lines=True,
446
521
  style="bright_cyan",
447
- caption=f"[dim]Service-focused analysis • {account_caption} • Each row represents one service[/]",
522
+ caption=f"[dim]Service-focused analysis with quarterly intelligence • {account_caption} • Each row represents one service[/]",
448
523
  )
449
524
 
450
- # Get current and previous service costs
525
+ # Get current, previous, and quarterly service costs
451
526
  current_services = cost_data.get("costs_by_service", {})
452
527
  previous_services = last_month_data.get("costs_by_service", {})
528
+ quarterly_services = last_month_data.get("quarterly_costs_by_service", {})
453
529
 
454
530
  # WIP.md requirement: Exclude "Tax" service as it provides no analytical insights
455
531
  # Use centralized filtering function for consistency across all dashboards
456
532
  filtered_current_services = filter_analytical_services(current_services)
457
533
  filtered_previous_services = filter_analytical_services(previous_services)
458
-
459
- # Sort services by current cost and take top N, plus "Other Services" summary
460
- all_services = sorted(filtered_current_services.items(), key=lambda x: x[1], reverse=True)
534
+ filtered_quarterly_services = filter_analytical_services(quarterly_services)
535
+
536
+ # Create comprehensive service list from current, previous, and quarterly periods
537
+ # This ensures services appear even when current costs are $0 but historical costs existed
538
+ all_service_names = set(filtered_current_services.keys()) | set(filtered_previous_services.keys()) | set(filtered_quarterly_services.keys())
539
+
540
+ # Build service data with current, previous, and quarterly costs for intelligent sorting
541
+ service_data = []
542
+ for service_name in all_service_names:
543
+ current_cost = filtered_current_services.get(service_name, 0.0)
544
+ previous_cost = filtered_previous_services.get(service_name, 0.0)
545
+ quarterly_cost = filtered_quarterly_services.get(service_name, 0.0)
546
+
547
+ # Sort by max(current_cost, previous_cost, quarterly_cost) to show most relevant services first
548
+ # This ensures services with historical significance appear prominently
549
+ max_cost = max(current_cost, previous_cost, quarterly_cost)
550
+ service_data.append((service_name, current_cost, previous_cost, quarterly_cost, max_cost))
551
+
552
+ # Sort by maximum cost across current, previous, and quarterly periods
553
+ all_services = sorted(service_data, key=lambda x: x[4], reverse=True)
461
554
  top_services_list = all_services[:top_services]
462
555
  remaining_services = all_services[top_services:]
463
556
 
464
557
  # Add individual service rows
465
- for service, current_cost in top_services_list:
466
- previous_cost = filtered_previous_services.get(service, 0)
467
-
468
- # Calculate trend
469
- if previous_cost > 0:
470
- trend_percent = ((current_cost - previous_cost) / previous_cost) * 100
471
- if trend_percent > 5:
472
- trend_display = f"[red]⬆ {trend_percent:.1f}%[/]"
473
- elif trend_percent < -5:
474
- trend_display = f"[green]⬇ {abs(trend_percent):.1f}%[/]"
475
- else:
476
- trend_display = f"[yellow]➡ {trend_percent:.1f}%[/]"
558
+ for service, current_cost, previous_cost, quarterly_cost, _ in top_services_list:
559
+
560
+ # Calculate trend using quarterly-enhanced intelligence
561
+ from .cost_processor import calculate_quarterly_enhanced_trend
562
+
563
+ # Get period metadata for intelligent trend analysis
564
+ period_metadata = last_month_data.get("period_metadata", {})
565
+ current_days = period_metadata.get("current_days")
566
+ previous_days = period_metadata.get("previous_days")
567
+
568
+ # Use quarterly-enhanced trend calculation with strategic context
569
+ trend_display = calculate_quarterly_enhanced_trend(
570
+ current_cost,
571
+ previous_cost,
572
+ quarterly_cost,
573
+ current_days,
574
+ previous_days
575
+ )
576
+
577
+ # Apply Rich formatting to the trend display
578
+ if "⚠️" in trend_display:
579
+ trend_display = f"[yellow]{trend_display}[/]"
580
+ elif "↑" in trend_display:
581
+ trend_display = f"[red]{trend_display}[/]"
582
+ elif "↓" in trend_display:
583
+ trend_display = f"[green]{trend_display}[/]"
584
+ elif "→" in trend_display:
585
+ trend_display = f"[yellow]{trend_display}[/]"
477
586
  else:
478
- trend_display = "[dim]New[/]"
587
+ trend_display = f"[dim]{trend_display}[/]"
479
588
 
480
589
  # Enhanced service-specific optimization recommendations
481
590
  optimization_rec = self._get_enhanced_service_recommendation(service, current_cost, previous_cost)
@@ -484,24 +593,35 @@ class SingleAccountDashboard:
484
593
  display_name = get_service_display_name(service)
485
594
 
486
595
  table.add_row(
487
- display_name, format_cost(current_cost), format_cost(previous_cost), trend_display, optimization_rec
596
+ display_name, format_cost(current_cost), format_cost(previous_cost), format_cost(quarterly_cost), trend_display, optimization_rec
488
597
  )
489
598
 
490
599
  # Add "Other Services" summary row if there are remaining services
491
600
  if remaining_services:
492
- other_current = sum(cost for _, cost in remaining_services)
493
- other_previous = sum(filtered_previous_services.get(service, 0) for service, _ in remaining_services)
494
-
495
- if other_previous > 0:
496
- other_trend_percent = ((other_current - other_previous) / other_previous) * 100
497
- if other_trend_percent > 5:
498
- other_trend = f"[red]⬆ {other_trend_percent:.1f}%[/]"
499
- elif other_trend_percent < -5:
500
- other_trend = f"[green]⬇ {abs(other_trend_percent):.1f}%[/]"
501
- else:
502
- other_trend = f"[yellow]➡ {other_trend_percent:.1f}%[/]"
601
+ other_current = sum(current_cost for _, current_cost, _, _, _ in remaining_services)
602
+ other_previous = sum(previous_cost for _, _, previous_cost, _, _ in remaining_services)
603
+ other_quarterly = sum(quarterly_cost for _, _, _, quarterly_cost, _ in remaining_services)
604
+
605
+ # Use quarterly-enhanced trend calculation for "Other Services" as well
606
+ other_trend = calculate_quarterly_enhanced_trend(
607
+ other_current,
608
+ other_previous,
609
+ other_quarterly,
610
+ current_days,
611
+ previous_days
612
+ )
613
+
614
+ # Apply Rich formatting
615
+ if "⚠️" in other_trend:
616
+ other_trend = f"[yellow]{other_trend}[/]"
617
+ elif "↑" in other_trend:
618
+ other_trend = f"[red]{other_trend}[/]"
619
+ elif "↓" in other_trend:
620
+ other_trend = f"[green]{other_trend}[/]"
621
+ elif "→" in other_trend:
622
+ other_trend = f"[yellow]{other_trend}[/]"
503
623
  else:
504
- other_trend = "[dim]Various[/]"
624
+ other_trend = f"[dim]{other_trend}[/]"
505
625
 
506
626
  other_optimization = (
507
627
  f"[dim]{len(remaining_services)} services: review individually for optimization opportunities[/]"
@@ -512,6 +632,7 @@ class SingleAccountDashboard:
512
632
  "[dim]Other Services[/]",
513
633
  format_cost(other_current),
514
634
  format_cost(other_previous),
635
+ format_cost(other_quarterly),
515
636
  other_trend,
516
637
  other_optimization,
517
638
  style="dim",
@@ -522,7 +643,16 @@ class SingleAccountDashboard:
522
643
  # Summary panel (using filtered services for consistent analysis)
523
644
  total_current = sum(filtered_current_services.values())
524
645
  total_previous = sum(filtered_previous_services.values())
525
- total_trend = ((total_current - total_previous) / total_previous * 100) if total_previous > 0 else 0
646
+ total_quarterly = sum(filtered_quarterly_services.values())
647
+
648
+ # Use quarterly-enhanced trend calculation for total trend as well
649
+ total_trend_display = calculate_quarterly_enhanced_trend(
650
+ total_current,
651
+ total_previous,
652
+ total_quarterly,
653
+ current_days,
654
+ previous_days
655
+ )
526
656
 
527
657
  # Use readable account name in summary
528
658
  if self.account_resolver and account_id != "Unknown":
@@ -534,13 +664,21 @@ class SingleAccountDashboard:
534
664
  else:
535
665
  account_summary_line = f"• Profile: {profile}"
536
666
 
667
+ # Add period information to summary for transparency
668
+ period_info = ""
669
+ if period_metadata.get("is_partial_comparison", False):
670
+ period_info = f"\n• Period Comparison: {current_days} vs {previous_days} days (partial month)"
671
+ else:
672
+ period_info = f"\n• Period Comparison: {current_days} vs {previous_days} days (equal periods)"
673
+
537
674
  summary_text = f"""
538
675
  [highlight]Account Summary[/]
539
676
  {account_summary_line}
540
677
  • Total Current: {format_cost(total_current)}
541
678
  • Total Previous: {format_cost(total_previous)}
542
- Overall Trend: {"⬆" if total_trend > 0 else "⬇"} {abs(total_trend):.1f}%
543
- Services Analyzed: {len(all_services)}
679
+ Total Quarterly: {format_cost(total_quarterly)}
680
+ Overall Trend: {total_trend_display}
681
+ • Services Analyzed: {len(all_services)}{period_info}
544
682
  """
545
683
 
546
684
  self.console.print(Panel(summary_text.strip(), title="📊 Analysis Summary", style="info"))
@@ -579,10 +717,12 @@ class SingleAccountDashboard:
579
717
  sorted_services,
580
718
  current_services,
581
719
  previous_services,
720
+ quarterly_services,
582
721
  profile,
583
722
  account_id,
584
723
  total_current,
585
724
  total_previous,
725
+ total_quarterly,
586
726
  total_trend_pct,
587
727
  args,
588
728
  ):
@@ -618,32 +758,34 @@ class SingleAccountDashboard:
618
758
  lines.append("## Service Cost Breakdown")
619
759
  lines.append("")
620
760
 
621
- # Create GitHub-compatible markdown table with proper alignment syntax
622
- lines.append("| Service | Last Month | Current Month | Trend | Optimization Opportunities |")
623
- lines.append("| --- | ---: | ---: | :---: | --- |") # GitHub-compliant alignment
761
+ # Create GitHub-compatible markdown table with quarterly intelligence
762
+ lines.append("| Service | Current Cost | Last Month | Last Quarter | Trend | Optimization Opportunities |")
763
+ lines.append("| --- | ---: | ---: | ---: | :---: | --- |") # GitHub-compliant alignment with quarterly column
624
764
 
625
- # Add TOP 10 services with proper formatting
765
+ # Add TOP 10 services with quarterly context
626
766
  for i, (service_name, current_cost) in enumerate(sorted_services[:10]):
627
767
  previous_cost = previous_services.get(service_name, 0)
768
+ quarterly_cost = quarterly_services.get(service_name, 0)
628
769
  trend_pct = ((current_cost - previous_cost) / previous_cost * 100) if previous_cost > 0 else 0
629
770
  trend_icon = "⬆️" if trend_pct > 0 else "⬇️" if trend_pct < 0 else "➡️"
630
771
 
631
- # Generate optimization recommendation
772
+ # Generate optimization recommendation with quarterly context
632
773
  optimization = self._get_service_optimization(service_name, current_cost, previous_cost)
633
774
 
634
- # Format row for GitHub-compatible table
775
+ # Format row for GitHub-compatible table with quarterly data
635
776
  service_name_clean = service_name.replace("|", "\\|") # Escape pipes in service names
636
777
  optimization_clean = optimization.replace("|", "\\|") # Escape pipes in text
637
778
 
638
779
  lines.append(
639
- f"| {service_name_clean} | ${previous_cost:.2f} | ${current_cost:.2f} | {trend_icon} {abs(trend_pct):.1f}% | {optimization_clean} |"
780
+ f"| {service_name_clean} | ${current_cost:.2f} | ${previous_cost:.2f} | ${quarterly_cost:.2f} | {trend_icon} {abs(trend_pct):.1f}% | {optimization_clean} |"
640
781
  )
641
782
 
642
- # Add Others row if there are remaining services
783
+ # Add Others row with quarterly context if there are remaining services
643
784
  remaining_services = sorted_services[10:]
644
785
  if remaining_services:
645
786
  others_current = sum(current_cost for _, current_cost in remaining_services)
646
787
  others_previous = sum(previous_services.get(service_name, 0) for service_name, _ in remaining_services)
788
+ others_quarterly = sum(quarterly_services.get(service_name, 0) for service_name, _ in remaining_services)
647
789
  others_trend_pct = (
648
790
  ((others_current - others_previous) / others_previous * 100) if others_previous > 0 else 0
649
791
  )
@@ -651,7 +793,7 @@ class SingleAccountDashboard:
651
793
 
652
794
  others_row = f"Others ({len(remaining_services)} services)"
653
795
  lines.append(
654
- f"| {others_row} | ${others_previous:.2f} | ${others_current:.2f} | {trend_icon} {abs(others_trend_pct):.1f}% | Review individually for optimization |"
796
+ f"| {others_row} | ${others_current:.2f} | ${others_previous:.2f} | ${others_quarterly:.2f} | {trend_icon} {abs(others_trend_pct):.1f}% | Review individually for optimization |"
655
797
  )
656
798
 
657
799
  lines.append("")
@@ -659,6 +801,7 @@ class SingleAccountDashboard:
659
801
  lines.append("")
660
802
  lines.append(f"- **Total Current Cost:** ${total_current:,.2f}")
661
803
  lines.append(f"- **Total Previous Cost:** ${total_previous:,.2f}")
804
+ lines.append(f"- **Total Quarterly Cost:** ${total_quarterly:,.2f}")
662
805
  trend_icon = "⬆️" if total_trend_pct > 0 else "⬇️" if total_trend_pct < 0 else "➡️"
663
806
  lines.append(f"- **Overall Trend:** {trend_icon} {abs(total_trend_pct):.1f}%")
664
807
  lines.append(f"- **Services Analyzed:** {len(sorted_services)}")
@@ -703,6 +846,191 @@ class SingleAccountDashboard:
703
846
  return "Monitor usage patterns & optimization opportunities"
704
847
  else: # Lower cost services
705
848
  return "Continue monitoring for optimization opportunities"
849
+
850
+ def _handle_exports(self, args: argparse.Namespace, profile: str, account_id: str,
851
+ services_data, cost_data, last_month_data) -> None:
852
+ """Handle all export formats for enhanced router."""
853
+ if not (hasattr(args, 'report_name') and args.report_name and
854
+ hasattr(args, 'report_type') and args.report_type):
855
+ return
856
+
857
+ self.console.print(f"[cyan]📊 Processing export requests...[/]")
858
+
859
+ # Convert service data to ProfileData format compatible with existing export functions
860
+ from .types import ProfileData
861
+
862
+ try:
863
+ # Create ProfileData compatible structure with dual-metric foundation
864
+ export_data = [ProfileData(
865
+ profile_name=profile,
866
+ account_id=account_id,
867
+ current_month=cost_data.get("current_month", 0), # Primary: UnblendedCost
868
+ current_month_formatted=f"${cost_data.get('current_month', 0):,.2f}",
869
+ previous_month=cost_data.get("last_month", 0), # Primary: UnblendedCost
870
+ previous_month_formatted=f"${cost_data.get('last_month', 0):,.2f}",
871
+ # Dual-metric architecture foundation (to be implemented)
872
+ current_month_amortized=None, # Secondary: AmortizedCost
873
+ previous_month_amortized=None, # Secondary: AmortizedCost
874
+ current_month_amortized_formatted=None,
875
+ previous_month_amortized_formatted=None,
876
+ metric_context="technical", # Default to technical context (UnblendedCost)
877
+ service_costs=[], # Service costs in simplified format
878
+ service_costs_formatted=[f"${cost:.2f}" for _, cost in services_data[:10]],
879
+ budget_info=[],
880
+ ec2_summary={},
881
+ ec2_summary_formatted=[],
882
+ success=True,
883
+ error=None,
884
+ current_period_name="Current Month",
885
+ previous_period_name="Previous Month",
886
+ percent_change_in_total_cost=None
887
+ )]
888
+
889
+ # Process each requested export type
890
+ export_count = 0
891
+ for report_type in args.report_type:
892
+ if report_type == "pdf":
893
+ self.console.print(f"[cyan]Generating PDF export...[/]")
894
+ pdf_path = export_cost_dashboard_to_pdf(
895
+ export_data,
896
+ args.report_name,
897
+ getattr(args, 'dir', None),
898
+ previous_period_dates="Previous Month",
899
+ current_period_dates="Current Month"
900
+ )
901
+ if pdf_path:
902
+ print_success(f"PDF export completed: {pdf_path}")
903
+ export_count += 1
904
+ else:
905
+ self.console.print(f"[red]PDF export failed[/]")
906
+
907
+ elif report_type == "csv":
908
+ self.console.print(f"[cyan]Generating CSV export...[/]")
909
+ from .cost_processor import export_to_csv
910
+ csv_path = export_to_csv(
911
+ export_data,
912
+ args.report_name,
913
+ getattr(args, 'dir', None),
914
+ previous_period_dates="Previous Month",
915
+ current_period_dates="Current Month"
916
+ )
917
+ if csv_path:
918
+ print_success(f"CSV export completed: {csv_path}")
919
+ export_count += 1
920
+
921
+ elif report_type == "json":
922
+ self.console.print(f"[cyan]Generating JSON export...[/]")
923
+ from .cost_processor import export_to_json
924
+ json_path = export_to_json(export_data, args.report_name, getattr(args, 'dir', None))
925
+ if json_path:
926
+ print_success(f"JSON export completed: {json_path}")
927
+ export_count += 1
928
+
929
+ elif report_type == "markdown":
930
+ self.console.print(f"[cyan]Generating Markdown export...[/]")
931
+ # Use existing markdown export functionality
932
+ self._export_service_table_to_markdown(
933
+ services_data[:10], {}, {}, # Simplified data structure
934
+ profile, account_id,
935
+ cost_data.get("current_month", 0),
936
+ cost_data.get("last_month", 0),
937
+ 0, args # Simplified trend calculation
938
+ )
939
+ export_count += 1
940
+
941
+ if export_count > 0:
942
+ self.console.print(f"[bright_green]✅ {export_count} exports completed successfully[/]")
943
+ else:
944
+ self.console.print(f"[yellow]⚠️ No exports were generated[/]")
945
+
946
+ except Exception as e:
947
+ self.console.print(f"[red]❌ Export failed: {str(e)}[/]")
948
+ import traceback
949
+ self.console.print(f"[red]Details: {traceback.format_exc()}[/]")
950
+
951
+ def _run_embedded_mcp_validation(self, profiles: List[str], cost_data: Dict[str, Any],
952
+ service_list: List[Tuple[str, float]], args: argparse.Namespace) -> None:
953
+ """
954
+ Run embedded MCP cross-validation for single account dashboard with real-time AWS API comparison.
955
+
956
+ This addresses the user's critical feedback about fabricated accuracy claims by providing
957
+ genuine MCP validation with actual AWS Cost Explorer API cross-validation.
958
+ """
959
+ try:
960
+ self.console.print(f"\n[bright_cyan]🔍 Embedded MCP Cross-Validation: Enterprise Accuracy Check[/]")
961
+ self.console.print(f"[dim]Validating single account with direct AWS API integration[/]")
962
+
963
+ # Prepare runbooks data in format expected by MCP validator
964
+ runbooks_data = {
965
+ profiles[0]: {
966
+ "total_cost": cost_data.get("current_month", 0),
967
+ "services": dict(service_list) if service_list else {},
968
+ "profile": profiles[0],
969
+ }
970
+ }
971
+
972
+ # Run embedded validation
973
+ validator = EmbeddedMCPValidator(profiles=profiles, console=self.console)
974
+ validation_results = validator.validate_cost_data(runbooks_data)
975
+
976
+ # Enhanced results display with detailed variance information (same as dashboard_runner.py)
977
+ overall_accuracy = validation_results.get("total_accuracy", 0)
978
+ profiles_validated = validation_results.get("profiles_validated", 0)
979
+ passed = validation_results.get("passed_validation", False)
980
+ profile_results = validation_results.get("profile_results", [])
981
+
982
+ self.console.print(f"\n[bright_cyan]🔍 MCP Cross-Validation Results:[/]")
983
+
984
+ # Display detailed per-profile results
985
+ for profile_result in profile_results:
986
+ profile_name = profile_result.get("profile", "Unknown")[:30]
987
+ runbooks_cost = profile_result.get("runbooks_cost", 0)
988
+ aws_cost = profile_result.get("aws_api_cost", 0)
989
+ accuracy = profile_result.get("accuracy_percent", 0)
990
+ cost_diff = profile_result.get("cost_difference", 0)
991
+
992
+ if profile_result.get("error"):
993
+ self.console.print(f"├── {profile_name}: [red]❌ Error: {profile_result['error']}[/]")
994
+ else:
995
+ variance_pct = 100 - accuracy if accuracy > 0 else 100
996
+ self.console.print(f"├── {profile_name}:")
997
+ self.console.print(f"│ ├── Runbooks Cost: ${runbooks_cost:,.2f}")
998
+ self.console.print(f"│ ├── MCP API Cost: ${aws_cost:,.2f}")
999
+ self.console.print(f"│ ├── Variance: ${cost_diff:,.2f} ({variance_pct:.2f}%)")
1000
+
1001
+ if accuracy >= 99.5:
1002
+ self.console.print(f"│ └── Status: [green]✅ {accuracy:.2f}% accuracy[/]")
1003
+ elif accuracy >= 95.0:
1004
+ self.console.print(f"│ └── Status: [yellow]⚠️ {accuracy:.2f}% accuracy[/]")
1005
+ else:
1006
+ self.console.print(f"│ └── Status: [red]❌ {accuracy:.2f}% accuracy[/]")
1007
+
1008
+ # Overall summary
1009
+ if passed:
1010
+ self.console.print(f"└── [bright_green]✅ MCP Validation PASSED: {overall_accuracy:.2f}% overall accuracy[/]")
1011
+ self.console.print(f" [green]🏢 Enterprise compliance: {profiles_validated}/{len(profiles)} profiles validated[/]")
1012
+ else:
1013
+ self.console.print(f"└── [bright_yellow]⚠️ MCP Validation: {overall_accuracy:.2f}% overall accuracy[/]")
1014
+ self.console.print(f" [yellow]📊 Enterprise target: ≥99.5% accuracy required for compliance[/]")
1015
+
1016
+ # Save validation report
1017
+ import json
1018
+ import os
1019
+ from datetime import datetime
1020
+
1021
+ validation_file = (
1022
+ f"artifacts/validation/embedded_mcp_validation_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
1023
+ )
1024
+ os.makedirs(os.path.dirname(validation_file), exist_ok=True)
1025
+
1026
+ with open(validation_file, "w") as f:
1027
+ json.dump(validation_results, f, indent=2, default=str)
1028
+
1029
+ self.console.print(f"[cyan]📋 Validation report saved: {validation_file}[/]")
1030
+
1031
+ except Exception as e:
1032
+ self.console.print(f"[red]❌ Embedded MCP validation failed: {str(e)[:100]}[/]")
1033
+ self.console.print(f"[dim]Continuing with standard FinOps analysis[/]")
706
1034
 
707
1035
 
708
1036
  def create_single_dashboard(console: Optional[Console] = None) -> SingleAccountDashboard: