runbooks 0.7.7__py3-none-any.whl → 0.7.9__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 (110) hide show
  1. runbooks/__init__.py +1 -1
  2. runbooks/base.py +2 -2
  3. runbooks/cfat/__init__.py +8 -4
  4. runbooks/cfat/assessment/collectors.py +171 -14
  5. runbooks/cfat/assessment/compliance.py +546 -522
  6. runbooks/cfat/assessment/runner.py +122 -11
  7. runbooks/cfat/models.py +6 -2
  8. runbooks/common/logger.py +14 -0
  9. runbooks/common/rich_utils.py +451 -0
  10. runbooks/enterprise/__init__.py +68 -0
  11. runbooks/enterprise/error_handling.py +411 -0
  12. runbooks/enterprise/logging.py +439 -0
  13. runbooks/enterprise/multi_tenant.py +583 -0
  14. runbooks/finops/README.md +468 -241
  15. runbooks/finops/__init__.py +39 -3
  16. runbooks/finops/cli.py +31 -28
  17. runbooks/finops/cross_validation.py +375 -0
  18. runbooks/finops/dashboard_runner.py +384 -207
  19. runbooks/finops/enhanced_dashboard_runner.py +525 -0
  20. runbooks/finops/finops_dashboard.py +1892 -0
  21. runbooks/finops/helpers.py +176 -173
  22. runbooks/finops/optimizer.py +384 -383
  23. runbooks/finops/tests/__init__.py +19 -0
  24. runbooks/finops/tests/results_test_finops_dashboard.xml +1 -0
  25. runbooks/finops/tests/run_comprehensive_tests.py +421 -0
  26. runbooks/finops/tests/run_tests.py +305 -0
  27. runbooks/finops/tests/test_finops_dashboard.py +705 -0
  28. runbooks/finops/tests/test_integration.py +477 -0
  29. runbooks/finops/tests/test_performance.py +380 -0
  30. runbooks/finops/tests/test_performance_benchmarks.py +500 -0
  31. runbooks/finops/tests/test_reference_images_validation.py +867 -0
  32. runbooks/finops/tests/test_single_account_features.py +715 -0
  33. runbooks/finops/tests/validate_test_suite.py +220 -0
  34. runbooks/finops/types.py +1 -1
  35. runbooks/hitl/enhanced_workflow_engine.py +725 -0
  36. runbooks/inventory/artifacts/scale-optimize-status.txt +12 -0
  37. runbooks/inventory/collectors/aws_comprehensive.py +192 -185
  38. runbooks/inventory/collectors/enterprise_scale.py +281 -0
  39. runbooks/inventory/core/collector.py +172 -13
  40. runbooks/inventory/list_ec2_instances.py +18 -20
  41. runbooks/inventory/list_ssm_parameters.py +31 -3
  42. runbooks/inventory/organizations_discovery.py +1269 -0
  43. runbooks/inventory/rich_inventory_display.py +393 -0
  44. runbooks/inventory/run_on_multi_accounts.py +35 -19
  45. runbooks/inventory/runbooks.security.report_generator.log +0 -0
  46. runbooks/inventory/runbooks.security.run_script.log +0 -0
  47. runbooks/inventory/vpc_flow_analyzer.py +1030 -0
  48. runbooks/main.py +2124 -174
  49. runbooks/metrics/dora_metrics_engine.py +599 -0
  50. runbooks/operate/__init__.py +2 -2
  51. runbooks/operate/base.py +122 -10
  52. runbooks/operate/deployment_framework.py +1032 -0
  53. runbooks/operate/deployment_validator.py +853 -0
  54. runbooks/operate/dynamodb_operations.py +10 -6
  55. runbooks/operate/ec2_operations.py +319 -11
  56. runbooks/operate/executive_dashboard.py +779 -0
  57. runbooks/operate/mcp_integration.py +750 -0
  58. runbooks/operate/nat_gateway_operations.py +1120 -0
  59. runbooks/operate/networking_cost_heatmap.py +685 -0
  60. runbooks/operate/privatelink_operations.py +940 -0
  61. runbooks/operate/s3_operations.py +10 -6
  62. runbooks/operate/vpc_endpoints.py +644 -0
  63. runbooks/operate/vpc_operations.py +1038 -0
  64. runbooks/remediation/__init__.py +2 -2
  65. runbooks/remediation/acm_remediation.py +1 -1
  66. runbooks/remediation/base.py +1 -1
  67. runbooks/remediation/cloudtrail_remediation.py +1 -1
  68. runbooks/remediation/cognito_remediation.py +1 -1
  69. runbooks/remediation/dynamodb_remediation.py +1 -1
  70. runbooks/remediation/ec2_remediation.py +1 -1
  71. runbooks/remediation/ec2_unattached_ebs_volumes.py +1 -1
  72. runbooks/remediation/kms_enable_key_rotation.py +1 -1
  73. runbooks/remediation/kms_remediation.py +1 -1
  74. runbooks/remediation/lambda_remediation.py +1 -1
  75. runbooks/remediation/multi_account.py +1 -1
  76. runbooks/remediation/rds_remediation.py +1 -1
  77. runbooks/remediation/s3_block_public_access.py +1 -1
  78. runbooks/remediation/s3_enable_access_logging.py +1 -1
  79. runbooks/remediation/s3_encryption.py +1 -1
  80. runbooks/remediation/s3_remediation.py +1 -1
  81. runbooks/remediation/vpc_remediation.py +475 -0
  82. runbooks/security/__init__.py +3 -1
  83. runbooks/security/compliance_automation.py +632 -0
  84. runbooks/security/report_generator.py +10 -0
  85. runbooks/security/run_script.py +31 -5
  86. runbooks/security/security_baseline_tester.py +169 -30
  87. runbooks/security/security_export.py +477 -0
  88. runbooks/validation/__init__.py +10 -0
  89. runbooks/validation/benchmark.py +484 -0
  90. runbooks/validation/cli.py +356 -0
  91. runbooks/validation/mcp_validator.py +768 -0
  92. runbooks/vpc/__init__.py +38 -0
  93. runbooks/vpc/config.py +212 -0
  94. runbooks/vpc/cost_engine.py +347 -0
  95. runbooks/vpc/heatmap_engine.py +605 -0
  96. runbooks/vpc/manager_interface.py +634 -0
  97. runbooks/vpc/networking_wrapper.py +1260 -0
  98. runbooks/vpc/rich_formatters.py +679 -0
  99. runbooks/vpc/tests/__init__.py +5 -0
  100. runbooks/vpc/tests/conftest.py +356 -0
  101. runbooks/vpc/tests/test_cli_integration.py +530 -0
  102. runbooks/vpc/tests/test_config.py +458 -0
  103. runbooks/vpc/tests/test_cost_engine.py +479 -0
  104. runbooks/vpc/tests/test_networking_wrapper.py +512 -0
  105. {runbooks-0.7.7.dist-info → runbooks-0.7.9.dist-info}/METADATA +40 -12
  106. {runbooks-0.7.7.dist-info → runbooks-0.7.9.dist-info}/RECORD +110 -52
  107. {runbooks-0.7.7.dist-info → runbooks-0.7.9.dist-info}/WHEEL +0 -0
  108. {runbooks-0.7.7.dist-info → runbooks-0.7.9.dist-info}/entry_points.txt +0 -0
  109. {runbooks-0.7.7.dist-info → runbooks-0.7.9.dist-info}/licenses/LICENSE +0 -0
  110. {runbooks-0.7.7.dist-info → runbooks-0.7.9.dist-info}/top_level.txt +0 -0
@@ -6,8 +6,7 @@ from typing import Any, Dict, List, Optional, Tuple
6
6
  import boto3
7
7
  from rich import box
8
8
  from rich.console import Console
9
- from rich.progress import Progress, SpinnerColumn, TextColumn, BarColumn, TaskProgressColumn, TimeElapsedColumn
10
- from rich.progress import track
9
+ from rich.progress import BarColumn, Progress, SpinnerColumn, TaskProgressColumn, TextColumn, TimeElapsedColumn, track
11
10
  from rich.status import Status
12
11
  from rich.table import Column, Table
13
12
 
@@ -23,13 +22,13 @@ from runbooks.finops.aws_client import (
23
22
  get_unused_volumes,
24
23
  )
25
24
  from runbooks.finops.cost_processor import (
25
+ change_in_total_cost,
26
26
  export_to_csv,
27
27
  export_to_json,
28
- get_cost_data,
29
- get_trend,
30
- change_in_total_cost,
31
28
  format_budget_info,
32
29
  format_ec2_summary,
30
+ get_cost_data,
31
+ get_trend,
33
32
  process_service_costs,
34
33
  )
35
34
  from runbooks.finops.helpers import (
@@ -54,20 +53,20 @@ console = Console()
54
53
  def _get_profile_for_operation(operation_type: str, default_profile: str) -> str:
55
54
  """
56
55
  Get the appropriate AWS profile based on operation type.
57
-
56
+
58
57
  Args:
59
58
  operation_type: Type of operation ('billing', 'management', 'operational')
60
59
  default_profile: Default profile to fall back to
61
-
60
+
62
61
  Returns:
63
62
  str: Profile name to use for the operation
64
63
  """
65
64
  profile_map = {
66
- 'billing': os.getenv('BILLING_PROFILE'),
67
- 'management': os.getenv('MANAGEMENT_PROFILE'),
68
- 'operational': os.getenv('CENTRALISED_OPS_PROFILE')
65
+ "billing": os.getenv("BILLING_PROFILE"),
66
+ "management": os.getenv("MANAGEMENT_PROFILE"),
67
+ "operational": os.getenv("CENTRALISED_OPS_PROFILE"),
69
68
  }
70
-
69
+
71
70
  profile = profile_map.get(operation_type)
72
71
  if profile:
73
72
  # Verify profile exists
@@ -76,8 +75,10 @@ def _get_profile_for_operation(operation_type: str, default_profile: str) -> str
76
75
  console.log(f"[dim cyan]Using {operation_type} profile: {profile}[/]")
77
76
  return profile
78
77
  else:
79
- console.log(f"[yellow]Warning: {operation_type.title()} profile '{profile}' not found in AWS config. Using default: {default_profile}[/]")
80
-
78
+ console.log(
79
+ f"[yellow]Warning: {operation_type.title()} profile '{profile}' not found in AWS config. Using default: {default_profile}[/]"
80
+ )
81
+
81
82
  return default_profile
82
83
 
83
84
 
@@ -85,14 +86,14 @@ def _create_cost_session(profile: str) -> boto3.Session:
85
86
  """
86
87
  Create a boto3 session specifically for cost operations.
87
88
  Uses BILLING_PROFILE if available, falls back to provided profile.
88
-
89
+
89
90
  Args:
90
91
  profile: Default profile to use
91
-
92
+
92
93
  Returns:
93
94
  boto3.Session: Session configured for cost operations
94
95
  """
95
- cost_profile = _get_profile_for_operation('billing', profile)
96
+ cost_profile = _get_profile_for_operation("billing", profile)
96
97
  return boto3.Session(profile_name=cost_profile)
97
98
 
98
99
 
@@ -100,14 +101,14 @@ def _create_management_session(profile: str) -> boto3.Session:
100
101
  """
101
102
  Create a boto3 session specifically for management operations.
102
103
  Uses MANAGEMENT_PROFILE if available, falls back to provided profile.
103
-
104
+
104
105
  Args:
105
106
  profile: Default profile to use
106
-
107
+
107
108
  Returns:
108
- boto3.Session: Session configured for management operations
109
+ boto3.Session: Session configured for management operations
109
110
  """
110
- mgmt_profile = _get_profile_for_operation('management', profile)
111
+ mgmt_profile = _get_profile_for_operation("management", profile)
111
112
  return boto3.Session(profile_name=mgmt_profile)
112
113
 
113
114
 
@@ -115,41 +116,41 @@ def _create_operational_session(profile: str) -> boto3.Session:
115
116
  """
116
117
  Create a boto3 session specifically for operational tasks.
117
118
  Uses CENTRALISED_OPS_PROFILE if available, falls back to provided profile.
118
-
119
+
119
120
  Args:
120
121
  profile: Default profile to use
121
-
122
+
122
123
  Returns:
123
124
  boto3.Session: Session configured for operational tasks
124
125
  """
125
- ops_profile = _get_profile_for_operation('operational', profile)
126
+ ops_profile = _get_profile_for_operation("operational", profile)
126
127
  return boto3.Session(profile_name=ops_profile)
127
128
 
128
129
 
129
130
  def _calculate_risk_score(untagged, stopped, unused_vols, unused_eips, budget_data):
130
131
  """Calculate risk score based on audit findings for PDCA tracking."""
131
132
  score = 0
132
-
133
+
133
134
  # Untagged resources (high risk for compliance)
134
135
  untagged_count = sum(len(ids) for region_map in untagged.values() for ids in region_map.values())
135
136
  score += untagged_count * 2 # High weight for untagged
136
-
137
+
137
138
  # Stopped instances (medium risk for cost)
138
139
  stopped_count = sum(len(ids) for ids in stopped.values())
139
140
  score += stopped_count * 1
140
-
141
+
141
142
  # Unused volumes (medium risk for cost)
142
143
  volume_count = sum(len(ids) for ids in unused_vols.values())
143
144
  score += volume_count * 1
144
-
145
+
145
146
  # Unused EIPs (high risk for cost)
146
147
  eip_count = sum(len(ids) for ids in unused_eips.values())
147
148
  score += eip_count * 3 # High cost impact
148
-
149
+
149
150
  # Budget overruns (critical risk)
150
151
  overruns = len([b for b in budget_data if b["actual"] > b["limit"]])
151
152
  score += overruns * 5 # Critical weight
152
-
153
+
153
154
  return score
154
155
 
155
156
 
@@ -169,32 +170,30 @@ def _display_pdca_summary(pdca_metrics):
169
170
  """Display PDCA improvement summary with actionable insights."""
170
171
  if not pdca_metrics:
171
172
  return
172
-
173
+
173
174
  total_risk = sum(m["risk_score"] for m in pdca_metrics)
174
175
  avg_risk = total_risk / len(pdca_metrics)
175
-
176
+
176
177
  high_risk_accounts = [m for m in pdca_metrics if m["risk_score"] > 25]
177
178
  total_untagged = sum(m["untagged_count"] for m in pdca_metrics)
178
179
  total_unused_eips = sum(m["unused_eips_count"] for m in pdca_metrics)
179
-
180
- summary_table = Table(
181
- title="🎯 PDCA Continuous Improvement Metrics",
182
- box=box.SIMPLE,
183
- style="cyan"
184
- )
180
+
181
+ summary_table = Table(title="🎯 PDCA Continuous Improvement Metrics", box=box.SIMPLE, style="cyan")
185
182
  summary_table.add_column("Metric", style="bold")
186
183
  summary_table.add_column("Value", justify="right")
187
184
  summary_table.add_column("Action Required", style="yellow")
188
-
189
- summary_table.add_row("Average Risk Score", f"{avg_risk:.1f}",
190
- "✅ Good" if avg_risk < 10 else "⚠️ Review Required")
191
- summary_table.add_row("High-Risk Accounts", str(len(high_risk_accounts)),
192
- "🔴 Immediate Action" if high_risk_accounts else "✅ Good")
193
- summary_table.add_row("Total Untagged Resources", str(total_untagged),
194
- "📋 Tag Management" if total_untagged > 50 else "✅ Good")
195
- summary_table.add_row("Total Unused EIPs", str(total_unused_eips),
196
- "💰 Cost Optimization" if total_unused_eips > 5 else "✅ Good")
197
-
185
+
186
+ summary_table.add_row("Average Risk Score", f"{avg_risk:.1f}", "✅ Good" if avg_risk < 10 else "⚠️ Review Required")
187
+ summary_table.add_row(
188
+ "High-Risk Accounts", str(len(high_risk_accounts)), "🔴 Immediate Action" if high_risk_accounts else "✅ Good"
189
+ )
190
+ summary_table.add_row(
191
+ "Total Untagged Resources", str(total_untagged), "📋 Tag Management" if total_untagged > 50 else "✅ Good"
192
+ )
193
+ summary_table.add_row(
194
+ "Total Unused EIPs", str(total_unused_eips), "💰 Cost Optimization" if total_unused_eips > 5 else "✅ Good"
195
+ )
196
+
198
197
  console.print(summary_table)
199
198
 
200
199
 
@@ -232,12 +231,12 @@ def _initialize_profiles(
232
231
  def _run_audit_report(profiles_to_use: List[str], args: argparse.Namespace) -> None:
233
232
  """Generate and export an audit report with PDCA continuous improvement."""
234
233
  console.print("[bold bright_cyan]🔍 PLAN: Preparing comprehensive audit report...[/]")
235
-
234
+
236
235
  # Display multi-profile configuration
237
- billing_profile = os.getenv('BILLING_PROFILE')
238
- mgmt_profile = os.getenv('MANAGEMENT_PROFILE')
239
- ops_profile = os.getenv('CENTRALISED_OPS_PROFILE')
240
-
236
+ billing_profile = os.getenv("BILLING_PROFILE")
237
+ mgmt_profile = os.getenv("MANAGEMENT_PROFILE")
238
+ ops_profile = os.getenv("CENTRALISED_OPS_PROFILE")
239
+
241
240
  if any([billing_profile, mgmt_profile, ops_profile]):
242
241
  console.print("[dim cyan]Multi-profile configuration detected:[/]")
243
242
  if billing_profile:
@@ -247,7 +246,7 @@ def _run_audit_report(profiles_to_use: List[str], args: argparse.Namespace) -> N
247
246
  if ops_profile:
248
247
  console.print(f"[dim cyan] • Operational tasks: {ops_profile}[/]")
249
248
  console.print()
250
-
249
+
251
250
  # Enhanced table with better visual hierarchy
252
251
  table = Table(
253
252
  Column("Profile", justify="center", style="bold magenta"),
@@ -272,8 +271,8 @@ def _run_audit_report(profiles_to_use: List[str], args: argparse.Namespace) -> N
272
271
  comma_nl = ",\n"
273
272
 
274
273
  console.print("[bold green]⚙️ DO: Collecting audit data across profiles...[/]")
275
-
276
- # Create progress tracker for enhanced user experience (v2.2.3 reference)
274
+
275
+ # Create progress tracker for enhanced user experience
277
276
  with Progress(
278
277
  SpinnerColumn(),
279
278
  TextColumn("[progress.description]{task.description}"),
@@ -281,20 +280,20 @@ def _run_audit_report(profiles_to_use: List[str], args: argparse.Namespace) -> N
281
280
  TaskProgressColumn(),
282
281
  TimeElapsedColumn(),
283
282
  console=console,
284
- transient=True
283
+ transient=True,
285
284
  ) as progress:
286
285
  task = progress.add_task("Collecting audit data", total=len(profiles_to_use))
287
-
286
+
288
287
  for profile in profiles_to_use:
289
288
  progress.update(task, description=f"Processing profile: {profile}")
290
-
289
+
291
290
  # Use operational session for resource discovery
292
291
  ops_session = _create_operational_session(profile)
293
292
  # Use management session for account and governance operations
294
293
  mgmt_session = _create_management_session(profile)
295
294
  # Use billing session for cost and budget operations
296
295
  billing_session = _create_cost_session(profile)
297
-
296
+
298
297
  account_id = get_account_id(mgmt_session) or "Unknown"
299
298
  regions = args.regions or get_accessible_regions(ops_session)
300
299
 
@@ -337,18 +336,20 @@ def _run_audit_report(profiles_to_use: List[str], args: argparse.Namespace) -> N
337
336
  # Calculate risk score for PDCA improvement tracking
338
337
  risk_score = _calculate_risk_score(untagged, stopped, unused_vols, unused_eips, budget_data)
339
338
  risk_display = _format_risk_score(risk_score)
340
-
339
+
341
340
  # Track PDCA metrics
342
- pdca_metrics.append({
343
- "profile": profile,
344
- "account_id": account_id,
345
- "risk_score": risk_score,
346
- "untagged_count": sum(len(ids) for region_map in untagged.values() for ids in region_map.values()),
347
- "stopped_count": sum(len(ids) for ids in stopped.values()),
348
- "unused_volumes_count": sum(len(ids) for ids in unused_vols.values()),
349
- "unused_eips_count": sum(len(ids) for ids in unused_eips.values()),
350
- "budget_overruns": len([b for b in budget_data if b["actual"] > b["limit"]])
351
- })
341
+ pdca_metrics.append(
342
+ {
343
+ "profile": profile,
344
+ "account_id": account_id,
345
+ "risk_score": risk_score,
346
+ "untagged_count": sum(len(ids) for region_map in untagged.values() for ids in region_map.values()),
347
+ "stopped_count": sum(len(ids) for ids in stopped.values()),
348
+ "unused_volumes_count": sum(len(ids) for ids in unused_vols.values()),
349
+ "unused_eips_count": sum(len(ids) for ids in unused_eips.values()),
350
+ "budget_overruns": len([b for b in budget_data if b["actual"] > b["limit"]]),
351
+ }
352
+ )
352
353
 
353
354
  audit_data.append(
354
355
  {
@@ -386,15 +387,17 @@ def _run_audit_report(profiles_to_use: List[str], args: argparse.Namespace) -> N
386
387
  "\n".join(alerts),
387
388
  risk_display,
388
389
  )
389
-
390
+
390
391
  progress.advance(task)
391
392
  console.print(table)
392
-
393
+
393
394
  # CHECK phase: Display PDCA improvement metrics
394
395
  console.print("\n[bold yellow]📊 CHECK: PDCA Improvement Analysis[/]")
395
396
  _display_pdca_summary(pdca_metrics)
396
-
397
- console.print("[bold bright_cyan]📝 Note: Dashboard scans EC2, RDS, Lambda, ELBv2 resources across all accessible regions.\n[/]")
397
+
398
+ console.print(
399
+ "[bold bright_cyan]📝 Note: Dashboard scans EC2, RDS, Lambda, ELBv2 resources across all accessible regions.\n[/]"
400
+ )
398
401
 
399
402
  # ACT phase: Export reports with PDCA enhancements
400
403
  if args.report_name: # Ensure report_name is provided for any export
@@ -412,7 +415,7 @@ def _run_audit_report(profiles_to_use: List[str], args: argparse.Namespace) -> N
412
415
  pdf_path = export_audit_report_to_pdf(audit_data, args.report_name, args.dir)
413
416
  if pdf_path:
414
417
  console.print(f"[bright_green]✅ Successfully exported to PDF format: {pdf_path}[/]")
415
-
418
+
416
419
  # Generate PDCA improvement report
417
420
  console.print("\n[bold cyan]🎯 ACT: Generating PDCA improvement recommendations...[/]")
418
421
  pdca_path = generate_pdca_improvement_report(pdca_metrics, args.report_name, args.dir)
@@ -423,14 +426,14 @@ def _run_audit_report(profiles_to_use: List[str], args: argparse.Namespace) -> N
423
426
  def _run_trend_analysis(profiles_to_use: List[str], args: argparse.Namespace) -> None:
424
427
  """Analyze and display cost trends with multi-profile support."""
425
428
  console.print("[bold bright_cyan]Analysing cost trends...[/]")
426
-
429
+
427
430
  # Display billing profile information
428
- billing_profile = os.getenv('BILLING_PROFILE')
431
+ billing_profile = os.getenv("BILLING_PROFILE")
429
432
  if billing_profile:
430
433
  console.print(f"[dim cyan]Using billing profile for cost data: {billing_profile}[/]")
431
-
434
+
432
435
  raw_trend_data = []
433
-
436
+
434
437
  # Enhanced progress tracking for trend analysis
435
438
  with Progress(
436
439
  SpinnerColumn(),
@@ -439,69 +442,69 @@ def _run_trend_analysis(profiles_to_use: List[str], args: argparse.Namespace) ->
439
442
  TaskProgressColumn(),
440
443
  TimeElapsedColumn(),
441
444
  console=console,
442
- transient=True
445
+ transient=True,
443
446
  ) as progress:
444
- if args.combine:
445
- account_profiles = defaultdict(list)
446
- task1 = progress.add_task("Grouping profiles by account", total=len(profiles_to_use))
447
-
448
- for profile in profiles_to_use:
449
- try:
450
- # Use management session to get account ID
451
- session = _create_management_session(profile)
452
- account_id = get_account_id(session)
453
- if account_id:
454
- account_profiles[account_id].append(profile)
455
- except Exception as e:
456
- console.print(f"[red]Error checking account ID for profile {profile}: {str(e)}[/]")
457
- progress.advance(task1)
458
-
459
- task2 = progress.add_task("Fetching cost trends", total=len(account_profiles))
460
- for account_id, profiles in account_profiles.items():
461
- progress.update(task2, description=f"Fetching trends for account: {account_id}")
462
- try:
463
- primary_profile = profiles[0]
464
- # Use billing session for cost trend data
465
- cost_session = _create_cost_session(primary_profile)
466
- cost_data = get_trend(cost_session, args.tag)
467
- trend_data = cost_data.get("monthly_costs")
468
-
469
- if not trend_data:
470
- console.print(f"[yellow]No trend data available for account {account_id}[/]")
471
- continue
472
-
473
- profile_list = ", ".join(profiles)
474
- console.print(f"\n[bright_yellow]Account: {account_id} (Profiles: {profile_list})[/]")
475
- raw_trend_data.append(cost_data)
476
- create_trend_bars(trend_data)
477
- except Exception as e:
478
- console.print(f"[red]Error getting trend for account {account_id}: {str(e)}[/]")
479
- progress.advance(task2)
447
+ if args.combine:
448
+ account_profiles = defaultdict(list)
449
+ task1 = progress.add_task("Grouping profiles by account", total=len(profiles_to_use))
450
+
451
+ for profile in profiles_to_use:
452
+ try:
453
+ # Use management session to get account ID
454
+ session = _create_management_session(profile)
455
+ account_id = get_account_id(session)
456
+ if account_id:
457
+ account_profiles[account_id].append(profile)
458
+ except Exception as e:
459
+ console.print(f"[red]Error checking account ID for profile {profile}: {str(e)}[/]")
460
+ progress.advance(task1)
461
+
462
+ task2 = progress.add_task("Fetching cost trends", total=len(account_profiles))
463
+ for account_id, profiles in account_profiles.items():
464
+ progress.update(task2, description=f"Fetching trends for account: {account_id}")
465
+ try:
466
+ primary_profile = profiles[0]
467
+ # Use billing session for cost trend data
468
+ cost_session = _create_cost_session(primary_profile)
469
+ cost_data = get_trend(cost_session, args.tag)
470
+ trend_data = cost_data.get("monthly_costs")
471
+
472
+ if not trend_data:
473
+ console.print(f"[yellow]No trend data available for account {account_id}[/]")
474
+ continue
475
+
476
+ profile_list = ", ".join(profiles)
477
+ console.print(f"\n[bright_yellow]Account: {account_id} (Profiles: {profile_list})[/]")
478
+ raw_trend_data.append(cost_data)
479
+ create_trend_bars(trend_data)
480
+ except Exception as e:
481
+ console.print(f"[red]Error getting trend for account {account_id}: {str(e)}[/]")
482
+ progress.advance(task2)
480
483
 
481
- else:
482
- task3 = progress.add_task("Fetching individual trends", total=len(profiles_to_use))
483
- for profile in profiles_to_use:
484
- progress.update(task3, description=f"Processing profile: {profile}")
485
- try:
486
- # Use billing session for cost data
487
- cost_session = _create_cost_session(profile)
488
- # Use management session for account ID
489
- mgmt_session = _create_management_session(profile)
490
-
491
- cost_data = get_trend(cost_session, args.tag)
492
- trend_data = cost_data.get("monthly_costs")
493
- account_id = get_account_id(mgmt_session) or cost_data.get("account_id", "Unknown")
494
-
495
- if not trend_data:
496
- console.print(f"[yellow]No trend data available for profile {profile}[/]")
497
- continue
498
-
499
- console.print(f"\n[bright_yellow]Account: {account_id} (Profile: {profile})[/]")
500
- raw_trend_data.append(cost_data)
501
- create_trend_bars(trend_data)
502
- except Exception as e:
503
- console.print(f"[red]Error getting trend for profile {profile}: {str(e)}[/]")
504
- progress.advance(task3)
484
+ else:
485
+ task3 = progress.add_task("Fetching individual trends", total=len(profiles_to_use))
486
+ for profile in profiles_to_use:
487
+ progress.update(task3, description=f"Processing profile: {profile}")
488
+ try:
489
+ # Use billing session for cost data
490
+ cost_session = _create_cost_session(profile)
491
+ # Use management session for account ID
492
+ mgmt_session = _create_management_session(profile)
493
+
494
+ cost_data = get_trend(cost_session, args.tag)
495
+ trend_data = cost_data.get("monthly_costs")
496
+ account_id = get_account_id(mgmt_session) or cost_data.get("account_id", "Unknown")
497
+
498
+ if not trend_data:
499
+ console.print(f"[yellow]No trend data available for profile {profile}[/]")
500
+ continue
501
+
502
+ console.print(f"\n[bright_yellow]Account: {account_id} (Profile: {profile})[/]")
503
+ raw_trend_data.append(cost_data)
504
+ create_trend_bars(trend_data)
505
+ except Exception as e:
506
+ console.print(f"[red]Error getting trend for profile {profile}: {str(e)}[/]")
507
+ progress.advance(task3)
505
508
 
506
509
  if raw_trend_data and args.report_name and args.report_type:
507
510
  if "json" in args.report_type:
@@ -558,8 +561,8 @@ def create_display_table(
558
561
  Column("Cost By Service", vertical="middle"),
559
562
  Column("Budget Status", vertical="middle"),
560
563
  Column("EC2 Instance Summary", justify="center", vertical="middle"),
561
- title="AWS FinOps Dashboard",
562
- caption="AWS FinOps Dashboard CLI",
564
+ title="CloudOps Runbooks FinOps Platform",
565
+ caption="Enterprise Multi-Account Cost Optimization",
563
566
  box=box.ASCII_DOUBLE_HEAD,
564
567
  show_lines=True,
565
568
  style="bright_cyan",
@@ -610,8 +613,8 @@ def _generate_dashboard_data(
610
613
  ) -> List[ProfileData]:
611
614
  """Fetch, process, and prepare the main dashboard data with multi-profile support."""
612
615
  export_data: List[ProfileData] = []
613
-
614
- # Enhanced progress tracking with v2.2.3 style progress bar
616
+
617
+ # Enhanced progress tracking with enterprise-grade progress indicators
615
618
  with Progress(
616
619
  SpinnerColumn(),
617
620
  TextColumn("[progress.description]{task.description}"),
@@ -619,52 +622,53 @@ def _generate_dashboard_data(
619
622
  TaskProgressColumn(),
620
623
  TimeElapsedColumn(),
621
624
  console=console,
622
- transient=False # Keep progress visible
625
+ transient=False, # Keep progress visible
623
626
  ) as progress:
624
-
625
- if args.combine:
626
- account_profiles = defaultdict(list)
627
- grouping_task = progress.add_task("Grouping profiles by account", total=len(profiles_to_use))
628
-
629
- for profile in profiles_to_use:
630
- progress.update(grouping_task, description=f"Checking account for profile: {profile}")
631
- try:
632
- # Use management session for account identification
633
- mgmt_session = _create_management_session(profile)
634
- current_account_id = get_account_id(mgmt_session)
635
- if current_account_id:
636
- account_profiles[current_account_id].append(profile)
637
- else:
638
- console.log(f"[yellow]Could not determine account ID for profile {profile}[/]")
639
- except Exception as e:
640
- console.log(f"[bold red]Error checking account ID for profile {profile}: {str(e)}[/]")
641
- progress.advance(grouping_task)
642
-
643
- # Process combined profiles with enhanced progress tracking
644
- processing_task = progress.add_task("Processing account data", total=len(account_profiles))
645
- for account_id_key, profiles_list in account_profiles.items():
646
- progress.update(processing_task, description=f"Processing account: {account_id_key}")
647
-
648
- if len(profiles_list) > 1:
649
- profile_data = _process_combined_profiles_enhanced(
650
- account_id_key, profiles_list, user_regions, time_range, args.tag
651
- )
627
+ if args.combine:
628
+ account_profiles = defaultdict(list)
629
+ grouping_task = progress.add_task("Grouping profiles by account", total=len(profiles_to_use))
630
+
631
+ for profile in profiles_to_use:
632
+ progress.update(grouping_task, description=f"Checking account for profile: {profile}")
633
+ try:
634
+ # Use management session for account identification
635
+ mgmt_session = _create_management_session(profile)
636
+ current_account_id = get_account_id(mgmt_session)
637
+ if current_account_id:
638
+ account_profiles[current_account_id].append(profile)
652
639
  else:
653
- profile_data = _process_single_profile_enhanced(profiles_list[0], user_regions, time_range, args.tag)
654
- export_data.append(profile_data)
655
- add_profile_to_table(table, profile_data)
656
- progress.advance(processing_task)
657
-
658
- else:
659
- # Process individual profiles with enhanced progress tracking
660
- individual_task = progress.add_task("Processing individual profiles", total=len(profiles_to_use))
661
- for profile in profiles_to_use:
662
- progress.update(individual_task, description=f"Processing profile: {profile}")
663
- profile_data = _process_single_profile_enhanced(profile, user_regions, time_range, args.tag)
664
- export_data.append(profile_data)
665
- add_profile_to_table(table, profile_data)
666
- progress.advance(individual_task)
667
-
640
+ console.log(f"[yellow]Could not determine account ID for profile {profile}[/]")
641
+ except Exception as e:
642
+ console.log(f"[bold red]Error checking account ID for profile {profile}: {str(e)}[/]")
643
+ progress.advance(grouping_task)
644
+
645
+ # Process combined profiles with enhanced progress tracking
646
+ processing_task = progress.add_task("Processing account data", total=len(account_profiles))
647
+ for account_id_key, profiles_list in account_profiles.items():
648
+ progress.update(processing_task, description=f"Processing account: {account_id_key}")
649
+
650
+ if len(profiles_list) > 1:
651
+ profile_data = _process_combined_profiles_enhanced(
652
+ account_id_key, profiles_list, user_regions, time_range, args.tag
653
+ )
654
+ else:
655
+ profile_data = _process_single_profile_enhanced(
656
+ profiles_list[0], user_regions, time_range, args.tag
657
+ )
658
+ export_data.append(profile_data)
659
+ add_profile_to_table(table, profile_data)
660
+ progress.advance(processing_task)
661
+
662
+ else:
663
+ # Process individual profiles with enhanced progress tracking
664
+ individual_task = progress.add_task("Processing individual profiles", total=len(profiles_to_use))
665
+ for profile in profiles_to_use:
666
+ progress.update(individual_task, description=f"Processing profile: {profile}")
667
+ profile_data = _process_single_profile_enhanced(profile, user_regions, time_range, args.tag)
668
+ export_data.append(profile_data)
669
+ add_profile_to_table(table, profile_data)
670
+ progress.advance(individual_task)
671
+
668
672
  return export_data
669
673
 
670
674
 
@@ -682,10 +686,10 @@ def _process_single_profile_enhanced(
682
686
  # Use billing session for cost data
683
687
  cost_session = _create_cost_session(profile)
684
688
  cost_data = get_cost_data(cost_session, time_range, tag)
685
-
689
+
686
690
  # Use operational session for EC2 and resource operations
687
691
  ops_session = _create_operational_session(profile)
688
-
692
+
689
693
  if user_regions:
690
694
  profile_regions = user_regions
691
695
  else:
@@ -748,15 +752,15 @@ def _process_combined_profiles_enhanced(
748
752
  """
749
753
  try:
750
754
  primary_profile = profiles[0]
751
-
755
+
752
756
  # Use billing session for cost data aggregation
753
757
  primary_cost_session = _create_cost_session(primary_profile)
754
758
  # Use operational session for resource data
755
759
  primary_ops_session = _create_operational_session(primary_profile)
756
-
760
+
757
761
  # Get cost data using billing session
758
762
  account_cost_data = get_cost_data(primary_cost_session, time_range, tag)
759
-
763
+
760
764
  if user_regions:
761
765
  profile_regions = user_regions
762
766
  else:
@@ -779,7 +783,7 @@ def _process_combined_profiles_enhanced(
779
783
  percent_change_in_total_cost = change_in_total_cost(
780
784
  account_cost_data["current_month"], account_cost_data["last_month"]
781
785
  )
782
-
786
+
783
787
  profile_list = ", ".join(profiles)
784
788
  console.log(f"[dim cyan]Combined {len(profiles)} profiles for account {account_id}: {profile_list}[/]")
785
789
 
@@ -857,35 +861,31 @@ def _export_dashboard_reports(
857
861
 
858
862
 
859
863
  def run_dashboard(args: argparse.Namespace) -> int:
860
- """Main function to run the AWS FinOps dashboard with multi-profile support."""
864
+ """Main function to run the CloudOps Runbooks FinOps Platform with multi-profile support."""
861
865
  with Status("[bright_cyan]Initialising...", spinner="aesthetic", speed=0.4):
862
866
  profiles_to_use, user_regions, time_range = _initialize_profiles(args)
863
-
867
+
864
868
  # Display multi-profile configuration at startup
865
- billing_profile = os.getenv('BILLING_PROFILE')
866
- mgmt_profile = os.getenv('MANAGEMENT_PROFILE')
867
- ops_profile = os.getenv('CENTRALISED_OPS_PROFILE')
868
-
869
+ billing_profile = os.getenv("BILLING_PROFILE")
870
+ mgmt_profile = os.getenv("MANAGEMENT_PROFILE")
871
+ ops_profile = os.getenv("CENTRALISED_OPS_PROFILE")
872
+
869
873
  if any([billing_profile, mgmt_profile, ops_profile]):
870
874
  console.print("\n[bold bright_cyan]🔧 Multi-Profile Configuration Detected[/]")
871
875
  config_table = Table(
872
- title="Profile Configuration",
873
- show_header=True,
874
- header_style="bold cyan",
875
- box=box.SIMPLE,
876
- style="dim"
876
+ title="Profile Configuration", show_header=True, header_style="bold cyan", box=box.SIMPLE, style="dim"
877
877
  )
878
878
  config_table.add_column("Operation Type", style="bold")
879
879
  config_table.add_column("Profile", style="bright_cyan")
880
880
  config_table.add_column("Purpose", style="dim")
881
-
881
+
882
882
  if billing_profile:
883
883
  config_table.add_row("💰 Billing", billing_profile, "Cost Explorer & Budget API access")
884
884
  if mgmt_profile:
885
885
  config_table.add_row("🏛️ Management", mgmt_profile, "Account ID & Organizations operations")
886
886
  if ops_profile:
887
887
  config_table.add_row("⚙️ Operational", ops_profile, "EC2, S3, and resource discovery")
888
-
888
+
889
889
  console.print(config_table)
890
890
  console.print("[dim]Fallback: Using profile-specific sessions when env vars not set[/]\n")
891
891
 
@@ -917,3 +917,180 @@ def run_dashboard(args: argparse.Namespace) -> int:
917
917
  _export_dashboard_reports(export_data, args, previous_period_dates, current_period_dates)
918
918
 
919
919
  return 0
920
+
921
+
922
+ def _run_cost_trend_analysis(profiles: List[str], args: argparse.Namespace) -> Dict[str, Any]:
923
+ """
924
+ Run cost trend analysis across multiple accounts.
925
+
926
+ Args:
927
+ profiles: List of AWS profiles to analyze
928
+ args: Command line arguments
929
+
930
+ Returns:
931
+ Dict containing cost trend analysis results
932
+ """
933
+ try:
934
+ # Import the new dashboard module
935
+ from runbooks.finops.finops_dashboard import FinOpsConfig, MultiAccountCostTrendAnalyzer
936
+
937
+ # Create configuration
938
+ config = FinOpsConfig()
939
+ config.dry_run = not args.live_mode if hasattr(args, "live_mode") else True
940
+
941
+ # Run cost trend analysis
942
+ analyzer = MultiAccountCostTrendAnalyzer(config)
943
+ results = analyzer.analyze_cost_trends()
944
+
945
+ console.log(f"[green]✅ Cost trend analysis completed for {len(profiles)} profiles[/]")
946
+
947
+ if results.get("status") == "completed":
948
+ cost_data = results["cost_trends"]
949
+ optimization = results["optimization_opportunities"]
950
+
951
+ console.log(f"[cyan]📊 Analyzed {cost_data['total_accounts']} accounts[/]")
952
+ console.log(f"[cyan]💰 Total monthly spend: ${cost_data['total_monthly_spend']:,.2f}[/]")
953
+ console.log(f"[cyan]🎯 Potential savings: {optimization['savings_percentage']:.1f}%[/]")
954
+
955
+ return results
956
+
957
+ except Exception as e:
958
+ console.log(f"[red]❌ Cost trend analysis failed: {e}[/]")
959
+ return {"status": "error", "error": str(e)}
960
+
961
+
962
+ def _run_resource_heatmap_analysis(
963
+ profiles: List[str], cost_data: Dict[str, Any], args: argparse.Namespace
964
+ ) -> Dict[str, Any]:
965
+ """
966
+ Run resource utilization heatmap analysis.
967
+
968
+ Args:
969
+ profiles: List of AWS profiles to analyze
970
+ cost_data: Cost analysis data from previous step
971
+ args: Command line arguments
972
+
973
+ Returns:
974
+ Dict containing resource heatmap analysis results
975
+ """
976
+ try:
977
+ # Import the new dashboard module
978
+ from runbooks.finops.finops_dashboard import FinOpsConfig, ResourceUtilizationHeatmapAnalyzer
979
+
980
+ # Create configuration
981
+ config = FinOpsConfig()
982
+ config.dry_run = not args.live_mode if hasattr(args, "live_mode") else True
983
+
984
+ # Run heatmap analysis
985
+ analyzer = ResourceUtilizationHeatmapAnalyzer(config, cost_data)
986
+ results = analyzer.analyze_resource_utilization()
987
+
988
+ console.log(f"[green]✅ Resource heatmap analysis completed[/]")
989
+
990
+ if results.get("status") == "completed":
991
+ heatmap_data = results["heatmap_data"]
992
+ efficiency = results["efficiency_scoring"]
993
+
994
+ console.log(f"[cyan]🔥 Analyzed {heatmap_data['total_resources']:,} resources[/]")
995
+ console.log(f"[cyan]⚡ Average efficiency: {efficiency['average_efficiency_score']:.1f}%[/]")
996
+
997
+ return results
998
+
999
+ except Exception as e:
1000
+ console.log(f"[red]❌ Resource heatmap analysis failed: {e}[/]")
1001
+ return {"status": "error", "error": str(e)}
1002
+
1003
+
1004
+ def _run_executive_dashboard(
1005
+ discovery_results: Dict[str, Any],
1006
+ cost_analysis: Dict[str, Any],
1007
+ audit_results: Dict[str, Any],
1008
+ args: argparse.Namespace,
1009
+ ) -> Dict[str, Any]:
1010
+ """
1011
+ Generate executive dashboard summary.
1012
+
1013
+ Args:
1014
+ discovery_results: Account discovery results
1015
+ cost_analysis: Cost analysis results
1016
+ audit_results: Audit results
1017
+ args: Command line arguments
1018
+
1019
+ Returns:
1020
+ Dict containing executive dashboard results
1021
+ """
1022
+ try:
1023
+ # Import the new dashboard module
1024
+ from runbooks.finops.finops_dashboard import EnterpriseExecutiveDashboard, FinOpsConfig
1025
+
1026
+ # Create configuration
1027
+ config = FinOpsConfig()
1028
+ config.dry_run = not args.live_mode if hasattr(args, "live_mode") else True
1029
+
1030
+ # Generate executive dashboard
1031
+ dashboard = EnterpriseExecutiveDashboard(config, discovery_results, cost_analysis, audit_results)
1032
+ results = dashboard.generate_executive_summary()
1033
+
1034
+ console.log(f"[green]✅ Executive dashboard generated[/]")
1035
+
1036
+ # Display key metrics
1037
+ if "financial_overview" in results:
1038
+ fin = results["financial_overview"]
1039
+ status_icon = "✅" if fin["target_achieved"] else "⚠️"
1040
+ console.log(f"[cyan]💰 Monthly spend: ${fin['current_monthly_spend']:,.2f}[/]")
1041
+ console.log(f"[cyan]🎯 Target status: {status_icon}[/]")
1042
+
1043
+ return results
1044
+
1045
+ except Exception as e:
1046
+ console.log(f"[red]❌ Executive dashboard generation failed: {e}[/]")
1047
+ return {"status": "error", "error": str(e)}
1048
+
1049
+
1050
+ def run_complete_finops_workflow(profiles: List[str], args: argparse.Namespace) -> Dict[str, Any]:
1051
+ """
1052
+ Run the complete FinOps analysis workflow.
1053
+
1054
+ Args:
1055
+ profiles: List of AWS profiles to analyze
1056
+ args: Command line arguments
1057
+
1058
+ Returns:
1059
+ Dict containing complete analysis results
1060
+ """
1061
+ try:
1062
+ # Import the new dashboard module
1063
+ from runbooks.finops.finops_dashboard import FinOpsConfig, run_complete_finops_analysis
1064
+
1065
+ console.log("[blue]🚀 Starting complete FinOps analysis workflow...[/]")
1066
+
1067
+ # Create configuration from args
1068
+ config = FinOpsConfig()
1069
+ config.dry_run = not args.live_mode if hasattr(args, "live_mode") else True
1070
+
1071
+ # Run complete analysis
1072
+ results = run_complete_finops_analysis(config)
1073
+
1074
+ console.log("[green]✅ Complete FinOps workflow completed successfully[/]")
1075
+
1076
+ # Display summary
1077
+ if results.get("workflow_status") == "completed":
1078
+ if "cost_analysis" in results and results["cost_analysis"].get("status") == "completed":
1079
+ cost_data = results["cost_analysis"]["cost_trends"]
1080
+ optimization = results["cost_analysis"]["optimization_opportunities"]
1081
+
1082
+ console.log(f"[cyan]📊 Analyzed {cost_data['total_accounts']} accounts[/]")
1083
+ console.log(f"[cyan]💰 Monthly spend: ${cost_data['total_monthly_spend']:,.2f}[/]")
1084
+ console.log(f"[cyan]🎯 Potential savings: {optimization['savings_percentage']:.1f}%[/]")
1085
+ console.log(f"[cyan]💵 Annual impact: ${optimization['annual_savings_potential']:,.2f}[/]")
1086
+
1087
+ if "export_status" in results:
1088
+ successful = len(results["export_status"]["successful_exports"])
1089
+ failed = len(results["export_status"]["failed_exports"])
1090
+ console.log(f"[cyan]📄 Exports: {successful} successful, {failed} failed[/]")
1091
+
1092
+ return results
1093
+
1094
+ except Exception as e:
1095
+ console.log(f"[red]❌ Complete FinOps workflow failed: {e}[/]")
1096
+ return {"status": "error", "error": str(e)}