runbooks 0.7.6__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 (111) hide show
  1. runbooks/__init__.py +1 -1
  2. runbooks/base.py +5 -1
  3. runbooks/cfat/__init__.py +8 -4
  4. runbooks/cfat/assessment/collectors.py +171 -14
  5. runbooks/cfat/assessment/compliance.py +871 -0
  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 +83 -18
  17. runbooks/finops/cross_validation.py +375 -0
  18. runbooks/finops/dashboard_runner.py +812 -164
  19. runbooks/finops/enhanced_dashboard_runner.py +525 -0
  20. runbooks/finops/finops_dashboard.py +1892 -0
  21. runbooks/finops/helpers.py +485 -51
  22. runbooks/finops/optimizer.py +823 -0
  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 +442 -0
  38. runbooks/inventory/collectors/enterprise_scale.py +281 -0
  39. runbooks/inventory/core/collector.py +172 -13
  40. runbooks/inventory/discovery.md +1 -1
  41. runbooks/inventory/list_ec2_instances.py +18 -20
  42. runbooks/inventory/list_ssm_parameters.py +31 -3
  43. runbooks/inventory/organizations_discovery.py +1269 -0
  44. runbooks/inventory/rich_inventory_display.py +393 -0
  45. runbooks/inventory/run_on_multi_accounts.py +35 -19
  46. runbooks/inventory/runbooks.security.report_generator.log +0 -0
  47. runbooks/inventory/runbooks.security.run_script.log +0 -0
  48. runbooks/inventory/vpc_flow_analyzer.py +1030 -0
  49. runbooks/main.py +2215 -119
  50. runbooks/metrics/dora_metrics_engine.py +599 -0
  51. runbooks/operate/__init__.py +2 -2
  52. runbooks/operate/base.py +122 -10
  53. runbooks/operate/deployment_framework.py +1032 -0
  54. runbooks/operate/deployment_validator.py +853 -0
  55. runbooks/operate/dynamodb_operations.py +10 -6
  56. runbooks/operate/ec2_operations.py +319 -11
  57. runbooks/operate/executive_dashboard.py +779 -0
  58. runbooks/operate/mcp_integration.py +750 -0
  59. runbooks/operate/nat_gateway_operations.py +1120 -0
  60. runbooks/operate/networking_cost_heatmap.py +685 -0
  61. runbooks/operate/privatelink_operations.py +940 -0
  62. runbooks/operate/s3_operations.py +10 -6
  63. runbooks/operate/vpc_endpoints.py +644 -0
  64. runbooks/operate/vpc_operations.py +1038 -0
  65. runbooks/remediation/__init__.py +2 -2
  66. runbooks/remediation/acm_remediation.py +1 -1
  67. runbooks/remediation/base.py +1 -1
  68. runbooks/remediation/cloudtrail_remediation.py +1 -1
  69. runbooks/remediation/cognito_remediation.py +1 -1
  70. runbooks/remediation/dynamodb_remediation.py +1 -1
  71. runbooks/remediation/ec2_remediation.py +1 -1
  72. runbooks/remediation/ec2_unattached_ebs_volumes.py +1 -1
  73. runbooks/remediation/kms_enable_key_rotation.py +1 -1
  74. runbooks/remediation/kms_remediation.py +1 -1
  75. runbooks/remediation/lambda_remediation.py +1 -1
  76. runbooks/remediation/multi_account.py +1 -1
  77. runbooks/remediation/rds_remediation.py +1 -1
  78. runbooks/remediation/s3_block_public_access.py +1 -1
  79. runbooks/remediation/s3_enable_access_logging.py +1 -1
  80. runbooks/remediation/s3_encryption.py +1 -1
  81. runbooks/remediation/s3_remediation.py +1 -1
  82. runbooks/remediation/vpc_remediation.py +475 -0
  83. runbooks/security/__init__.py +3 -1
  84. runbooks/security/compliance_automation.py +632 -0
  85. runbooks/security/report_generator.py +10 -0
  86. runbooks/security/run_script.py +31 -5
  87. runbooks/security/security_baseline_tester.py +169 -30
  88. runbooks/security/security_export.py +477 -0
  89. runbooks/validation/__init__.py +10 -0
  90. runbooks/validation/benchmark.py +484 -0
  91. runbooks/validation/cli.py +356 -0
  92. runbooks/validation/mcp_validator.py +768 -0
  93. runbooks/vpc/__init__.py +38 -0
  94. runbooks/vpc/config.py +212 -0
  95. runbooks/vpc/cost_engine.py +347 -0
  96. runbooks/vpc/heatmap_engine.py +605 -0
  97. runbooks/vpc/manager_interface.py +634 -0
  98. runbooks/vpc/networking_wrapper.py +1260 -0
  99. runbooks/vpc/rich_formatters.py +679 -0
  100. runbooks/vpc/tests/__init__.py +5 -0
  101. runbooks/vpc/tests/conftest.py +356 -0
  102. runbooks/vpc/tests/test_cli_integration.py +530 -0
  103. runbooks/vpc/tests/test_config.py +458 -0
  104. runbooks/vpc/tests/test_cost_engine.py +479 -0
  105. runbooks/vpc/tests/test_networking_wrapper.py +512 -0
  106. {runbooks-0.7.6.dist-info → runbooks-0.7.9.dist-info}/METADATA +40 -12
  107. {runbooks-0.7.6.dist-info → runbooks-0.7.9.dist-info}/RECORD +111 -50
  108. {runbooks-0.7.6.dist-info → runbooks-0.7.9.dist-info}/WHEEL +0 -0
  109. {runbooks-0.7.6.dist-info → runbooks-0.7.9.dist-info}/entry_points.txt +0 -0
  110. {runbooks-0.7.6.dist-info → runbooks-0.7.9.dist-info}/licenses/LICENSE +0 -0
  111. {runbooks-0.7.6.dist-info → runbooks-0.7.9.dist-info}/top_level.txt +0 -0
@@ -1,17 +1,20 @@
1
1
  import argparse
2
+ import os
2
3
  from collections import defaultdict
3
4
  from typing import Any, Dict, List, Optional, Tuple
4
5
 
5
6
  import boto3
6
7
  from rich import box
7
8
  from rich.console import Console
8
- from rich.progress import track
9
+ from rich.progress import BarColumn, Progress, SpinnerColumn, TaskProgressColumn, TextColumn, TimeElapsedColumn, track
9
10
  from rich.status import Status
10
11
  from rich.table import Column, Table
11
12
 
12
13
  from runbooks.finops.aws_client import (
14
+ ec2_summary,
13
15
  get_accessible_regions,
14
16
  get_account_id,
17
+ get_aws_profiles,
15
18
  get_budgets,
16
19
  get_stopped_instances,
17
20
  get_untagged_resources,
@@ -19,10 +22,14 @@ from runbooks.finops.aws_client import (
19
22
  get_unused_volumes,
20
23
  )
21
24
  from runbooks.finops.cost_processor import (
25
+ change_in_total_cost,
22
26
  export_to_csv,
23
27
  export_to_json,
28
+ format_budget_info,
29
+ format_ec2_summary,
24
30
  get_cost_data,
25
31
  get_trend,
32
+ process_service_costs,
26
33
  )
27
34
  from runbooks.finops.helpers import (
28
35
  clean_rich_tags,
@@ -31,6 +38,7 @@ from runbooks.finops.helpers import (
31
38
  export_audit_report_to_pdf,
32
39
  export_cost_dashboard_to_pdf,
33
40
  export_trend_data_to_json,
41
+ generate_pdca_improvement_report,
34
42
  )
35
43
  from runbooks.finops.profile_processor import (
36
44
  process_combined_profiles,
@@ -42,6 +50,153 @@ from runbooks.finops.visualisations import create_trend_bars
42
50
  console = Console()
43
51
 
44
52
 
53
+ def _get_profile_for_operation(operation_type: str, default_profile: str) -> str:
54
+ """
55
+ Get the appropriate AWS profile based on operation type.
56
+
57
+ Args:
58
+ operation_type: Type of operation ('billing', 'management', 'operational')
59
+ default_profile: Default profile to fall back to
60
+
61
+ Returns:
62
+ str: Profile name to use for the operation
63
+ """
64
+ profile_map = {
65
+ "billing": os.getenv("BILLING_PROFILE"),
66
+ "management": os.getenv("MANAGEMENT_PROFILE"),
67
+ "operational": os.getenv("CENTRALISED_OPS_PROFILE"),
68
+ }
69
+
70
+ profile = profile_map.get(operation_type)
71
+ if profile:
72
+ # Verify profile exists
73
+ available_profiles = boto3.Session().available_profiles
74
+ if profile in available_profiles:
75
+ console.log(f"[dim cyan]Using {operation_type} profile: {profile}[/]")
76
+ return profile
77
+ else:
78
+ console.log(
79
+ f"[yellow]Warning: {operation_type.title()} profile '{profile}' not found in AWS config. Using default: {default_profile}[/]"
80
+ )
81
+
82
+ return default_profile
83
+
84
+
85
+ def _create_cost_session(profile: str) -> boto3.Session:
86
+ """
87
+ Create a boto3 session specifically for cost operations.
88
+ Uses BILLING_PROFILE if available, falls back to provided profile.
89
+
90
+ Args:
91
+ profile: Default profile to use
92
+
93
+ Returns:
94
+ boto3.Session: Session configured for cost operations
95
+ """
96
+ cost_profile = _get_profile_for_operation("billing", profile)
97
+ return boto3.Session(profile_name=cost_profile)
98
+
99
+
100
+ def _create_management_session(profile: str) -> boto3.Session:
101
+ """
102
+ Create a boto3 session specifically for management operations.
103
+ Uses MANAGEMENT_PROFILE if available, falls back to provided profile.
104
+
105
+ Args:
106
+ profile: Default profile to use
107
+
108
+ Returns:
109
+ boto3.Session: Session configured for management operations
110
+ """
111
+ mgmt_profile = _get_profile_for_operation("management", profile)
112
+ return boto3.Session(profile_name=mgmt_profile)
113
+
114
+
115
+ def _create_operational_session(profile: str) -> boto3.Session:
116
+ """
117
+ Create a boto3 session specifically for operational tasks.
118
+ Uses CENTRALISED_OPS_PROFILE if available, falls back to provided profile.
119
+
120
+ Args:
121
+ profile: Default profile to use
122
+
123
+ Returns:
124
+ boto3.Session: Session configured for operational tasks
125
+ """
126
+ ops_profile = _get_profile_for_operation("operational", profile)
127
+ return boto3.Session(profile_name=ops_profile)
128
+
129
+
130
+ def _calculate_risk_score(untagged, stopped, unused_vols, unused_eips, budget_data):
131
+ """Calculate risk score based on audit findings for PDCA tracking."""
132
+ score = 0
133
+
134
+ # Untagged resources (high risk for compliance)
135
+ untagged_count = sum(len(ids) for region_map in untagged.values() for ids in region_map.values())
136
+ score += untagged_count * 2 # High weight for untagged
137
+
138
+ # Stopped instances (medium risk for cost)
139
+ stopped_count = sum(len(ids) for ids in stopped.values())
140
+ score += stopped_count * 1
141
+
142
+ # Unused volumes (medium risk for cost)
143
+ volume_count = sum(len(ids) for ids in unused_vols.values())
144
+ score += volume_count * 1
145
+
146
+ # Unused EIPs (high risk for cost)
147
+ eip_count = sum(len(ids) for ids in unused_eips.values())
148
+ score += eip_count * 3 # High cost impact
149
+
150
+ # Budget overruns (critical risk)
151
+ overruns = len([b for b in budget_data if b["actual"] > b["limit"]])
152
+ score += overruns * 5 # Critical weight
153
+
154
+ return score
155
+
156
+
157
+ def _format_risk_score(score):
158
+ """Format risk score with visual indicators."""
159
+ if score == 0:
160
+ return "[bright_green]🟢 LOW\n(0)[/]"
161
+ elif score <= 10:
162
+ return f"[yellow]🟡 MEDIUM\n({score})[/]"
163
+ elif score <= 25:
164
+ return f"[orange1]🟠 HIGH\n({score})[/]"
165
+ else:
166
+ return f"[bright_red]🔴 CRITICAL\n({score})[/]"
167
+
168
+
169
+ def _display_pdca_summary(pdca_metrics):
170
+ """Display PDCA improvement summary with actionable insights."""
171
+ if not pdca_metrics:
172
+ return
173
+
174
+ total_risk = sum(m["risk_score"] for m in pdca_metrics)
175
+ avg_risk = total_risk / len(pdca_metrics)
176
+
177
+ high_risk_accounts = [m for m in pdca_metrics if m["risk_score"] > 25]
178
+ total_untagged = sum(m["untagged_count"] for m in pdca_metrics)
179
+ total_unused_eips = sum(m["unused_eips_count"] for m in pdca_metrics)
180
+
181
+ summary_table = Table(title="🎯 PDCA Continuous Improvement Metrics", box=box.SIMPLE, style="cyan")
182
+ summary_table.add_column("Metric", style="bold")
183
+ summary_table.add_column("Value", justify="right")
184
+ summary_table.add_column("Action Required", style="yellow")
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
+
197
+ console.print(summary_table)
198
+
199
+
45
200
  def _initialize_profiles(
46
201
  args: argparse.Namespace,
47
202
  ) -> Tuple[List[str], Optional[List[str]], Optional[int]]:
@@ -74,102 +229,177 @@ def _initialize_profiles(
74
229
 
75
230
 
76
231
  def _run_audit_report(profiles_to_use: List[str], args: argparse.Namespace) -> None:
77
- """Generate and export an audit report."""
78
- console.print("[bold bright_cyan]Preparing your audit report...[/]")
232
+ """Generate and export an audit report with PDCA continuous improvement."""
233
+ console.print("[bold bright_cyan]🔍 PLAN: Preparing comprehensive audit report...[/]")
234
+
235
+ # Display multi-profile configuration
236
+ billing_profile = os.getenv("BILLING_PROFILE")
237
+ mgmt_profile = os.getenv("MANAGEMENT_PROFILE")
238
+ ops_profile = os.getenv("CENTRALISED_OPS_PROFILE")
239
+
240
+ if any([billing_profile, mgmt_profile, ops_profile]):
241
+ console.print("[dim cyan]Multi-profile configuration detected:[/]")
242
+ if billing_profile:
243
+ console.print(f"[dim cyan] • Billing operations: {billing_profile}[/]")
244
+ if mgmt_profile:
245
+ console.print(f"[dim cyan] • Management operations: {mgmt_profile}[/]")
246
+ if ops_profile:
247
+ console.print(f"[dim cyan] • Operational tasks: {ops_profile}[/]")
248
+ console.print()
249
+
250
+ # Enhanced table with better visual hierarchy
79
251
  table = Table(
80
- Column("Profile", justify="center"),
81
- Column("Account ID", justify="center"),
82
- Column("Untagged Resources"),
83
- Column("Stopped EC2 Instances"),
84
- Column("Unused Volumes"),
85
- Column("Unused EIPs"),
86
- Column("Budget Alerts"),
87
- title="AWS FinOps Audit Report",
252
+ Column("Profile", justify="center", style="bold magenta"),
253
+ Column("Account ID", justify="center", style="dim"),
254
+ Column("Untagged Resources", style="yellow"),
255
+ Column("Stopped EC2 Instances", style="red"),
256
+ Column("Unused Volumes", style="orange1"),
257
+ Column("Unused EIPs", style="cyan"),
258
+ Column("Budget Alerts", style="bright_red"),
259
+ Column("Risk Score", justify="center", style="bold"),
260
+ title="🎯 AWS FinOps Audit Report - PDCA Enhanced",
88
261
  show_lines=True,
89
- box=box.ASCII_DOUBLE_HEAD,
262
+ box=box.ROUNDED,
90
263
  style="bright_cyan",
264
+ caption="🚀 PDCA Cycle: Plan → Do → Check → Act",
91
265
  )
92
266
 
93
267
  audit_data = []
94
268
  raw_audit_data = []
269
+ pdca_metrics = [] # New: Track PDCA improvement metrics
95
270
  nl = "\n"
96
271
  comma_nl = ",\n"
97
272
 
98
- for profile in profiles_to_use:
99
- session = boto3.Session(profile_name=profile)
100
- account_id = get_account_id(session) or "Unknown"
101
- regions = args.regions or get_accessible_regions(session)
273
+ console.print("[bold green]⚙️ DO: Collecting audit data across profiles...[/]")
102
274
 
103
- try:
104
- untagged = get_untagged_resources(session, regions)
105
- anomalies = []
106
- for service, region_map in untagged.items():
107
- if region_map:
108
- service_block = f"[bright_yellow]{service}[/]:\n"
109
- for region, ids in region_map.items():
110
- if ids:
111
- ids_block = "\n".join(f"[orange1]{res_id}[/]" for res_id in ids)
112
- service_block += f"\n{region}:\n{ids_block}\n"
113
- anomalies.append(service_block)
114
- if not any(region_map for region_map in untagged.values()):
115
- anomalies = ["None"]
116
- except Exception as e:
117
- anomalies = [f"Error: {str(e)}"]
118
-
119
- stopped = get_stopped_instances(session, regions)
120
- stopped_list = [f"{r}:\n[gold1]{nl.join(ids)}[/]" for r, ids in stopped.items()] or ["None"]
121
-
122
- unused_vols = get_unused_volumes(session, regions)
123
- vols_list = [f"{r}:\n[dark_orange]{nl.join(ids)}[/]" for r, ids in unused_vols.items()] or ["None"]
124
-
125
- unused_eips = get_unused_eips(session, regions)
126
- eips_list = [f"{r}:\n{comma_nl.join(ids)}" for r, ids in unused_eips.items()] or ["None"]
127
-
128
- budget_data = get_budgets(session)
129
- alerts = []
130
- for b in budget_data:
131
- if b["actual"] > b["limit"]:
132
- alerts.append(f"[red1]{b['name']}[/]: ${b['actual']:.2f} > ${b['limit']:.2f}")
133
- if not alerts:
134
- alerts = ["No budgets exceeded"]
135
-
136
- audit_data.append(
137
- {
138
- "profile": profile,
139
- "account_id": account_id,
140
- "untagged_resources": clean_rich_tags("\n".join(anomalies)),
141
- "stopped_instances": clean_rich_tags("\n".join(stopped_list)),
142
- "unused_volumes": clean_rich_tags("\n".join(vols_list)),
143
- "unused_eips": clean_rich_tags("\n".join(eips_list)),
144
- "budget_alerts": clean_rich_tags("\n".join(alerts)),
145
- }
146
- )
275
+ # Create progress tracker for enhanced user experience
276
+ with Progress(
277
+ SpinnerColumn(),
278
+ TextColumn("[progress.description]{task.description}"),
279
+ BarColumn(),
280
+ TaskProgressColumn(),
281
+ TimeElapsedColumn(),
282
+ console=console,
283
+ transient=True,
284
+ ) as progress:
285
+ task = progress.add_task("Collecting audit data", total=len(profiles_to_use))
147
286
 
148
- # Data for JSON which includes raw audit data
149
- raw_audit_data.append(
150
- {
151
- "profile": profile,
152
- "account_id": account_id,
153
- "untagged_resources": untagged,
154
- "stopped_instances": stopped,
155
- "unused_volumes": unused_vols,
156
- "unused_eips": unused_eips,
157
- "budget_alerts": budget_data,
158
- }
159
- )
287
+ for profile in profiles_to_use:
288
+ progress.update(task, description=f"Processing profile: {profile}")
160
289
 
161
- table.add_row(
162
- f"[dark_magenta]{profile}[/]",
163
- account_id,
164
- "\n".join(anomalies),
165
- "\n".join(stopped_list),
166
- "\n".join(vols_list),
167
- "\n".join(eips_list),
168
- "\n".join(alerts),
169
- )
290
+ # Use operational session for resource discovery
291
+ ops_session = _create_operational_session(profile)
292
+ # Use management session for account and governance operations
293
+ mgmt_session = _create_management_session(profile)
294
+ # Use billing session for cost and budget operations
295
+ billing_session = _create_cost_session(profile)
296
+
297
+ account_id = get_account_id(mgmt_session) or "Unknown"
298
+ regions = args.regions or get_accessible_regions(ops_session)
299
+
300
+ try:
301
+ # Use operational session for resource discovery
302
+ untagged = get_untagged_resources(ops_session, regions)
303
+ anomalies = []
304
+ for service, region_map in untagged.items():
305
+ if region_map:
306
+ service_block = f"[bright_yellow]{service}[/]:\n"
307
+ for region, ids in region_map.items():
308
+ if ids:
309
+ ids_block = "\n".join(f"[orange1]{res_id}[/]" for res_id in ids)
310
+ service_block += f"\n{region}:\n{ids_block}\n"
311
+ anomalies.append(service_block)
312
+ if not any(region_map for region_map in untagged.values()):
313
+ anomalies = ["None"]
314
+ except Exception as e:
315
+ anomalies = [f"Error: {str(e)}"]
316
+
317
+ # Use operational session for EC2 and resource operations
318
+ stopped = get_stopped_instances(ops_session, regions)
319
+ stopped_list = [f"{r}:\n[gold1]{nl.join(ids)}[/]" for r, ids in stopped.items()] or ["None"]
320
+
321
+ unused_vols = get_unused_volumes(ops_session, regions)
322
+ vols_list = [f"{r}:\n[dark_orange]{nl.join(ids)}[/]" for r, ids in unused_vols.items()] or ["None"]
323
+
324
+ unused_eips = get_unused_eips(ops_session, regions)
325
+ eips_list = [f"{r}:\n{comma_nl.join(ids)}" for r, ids in unused_eips.items()] or ["None"]
326
+
327
+ # Use billing session for budget data
328
+ budget_data = get_budgets(billing_session)
329
+ alerts = []
330
+ for b in budget_data:
331
+ if b["actual"] > b["limit"]:
332
+ alerts.append(f"[red1]{b['name']}[/]: ${b['actual']:.2f} > ${b['limit']:.2f}")
333
+ if not alerts:
334
+ alerts = ["✅ No budgets exceeded"]
335
+
336
+ # Calculate risk score for PDCA improvement tracking
337
+ risk_score = _calculate_risk_score(untagged, stopped, unused_vols, unused_eips, budget_data)
338
+ risk_display = _format_risk_score(risk_score)
339
+
340
+ # Track PDCA metrics
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
+ )
353
+
354
+ audit_data.append(
355
+ {
356
+ "profile": profile,
357
+ "account_id": account_id,
358
+ "untagged_resources": clean_rich_tags("\n".join(anomalies)),
359
+ "stopped_instances": clean_rich_tags("\n".join(stopped_list)),
360
+ "unused_volumes": clean_rich_tags("\n".join(vols_list)),
361
+ "unused_eips": clean_rich_tags("\n".join(eips_list)),
362
+ "budget_alerts": clean_rich_tags("\n".join(alerts)),
363
+ "risk_score": risk_score,
364
+ }
365
+ )
366
+
367
+ # Data for JSON which includes raw audit data
368
+ raw_audit_data.append(
369
+ {
370
+ "profile": profile,
371
+ "account_id": account_id,
372
+ "untagged_resources": untagged,
373
+ "stopped_instances": stopped,
374
+ "unused_volumes": unused_vols,
375
+ "unused_eips": unused_eips,
376
+ "budget_alerts": budget_data,
377
+ }
378
+ )
379
+
380
+ table.add_row(
381
+ f"[dark_magenta]{profile}[/]",
382
+ account_id,
383
+ "\n".join(anomalies),
384
+ "\n".join(stopped_list),
385
+ "\n".join(vols_list),
386
+ "\n".join(eips_list),
387
+ "\n".join(alerts),
388
+ risk_display,
389
+ )
390
+
391
+ progress.advance(task)
170
392
  console.print(table)
171
- console.print("[bold bright_cyan]Note: The dashboard only lists untagged EC2, RDS, Lambda, ELBv2.\n[/]")
172
393
 
394
+ # CHECK phase: Display PDCA improvement metrics
395
+ console.print("\n[bold yellow]📊 CHECK: PDCA Improvement Analysis[/]")
396
+ _display_pdca_summary(pdca_metrics)
397
+
398
+ console.print(
399
+ "[bold bright_cyan]📝 Note: Dashboard scans EC2, RDS, Lambda, ELBv2 resources across all accessible regions.\n[/]"
400
+ )
401
+
402
+ # ACT phase: Export reports with PDCA enhancements
173
403
  if args.report_name: # Ensure report_name is provided for any export
174
404
  if args.report_type:
175
405
  for report_type in args.report_type:
@@ -184,59 +414,97 @@ def _run_audit_report(profiles_to_use: List[str], args: argparse.Namespace) -> N
184
414
  elif report_type == "pdf":
185
415
  pdf_path = export_audit_report_to_pdf(audit_data, args.report_name, args.dir)
186
416
  if pdf_path:
187
- console.print(f"[bright_green]Successfully exported to PDF format: {pdf_path}[/]")
417
+ console.print(f"[bright_green]Successfully exported to PDF format: {pdf_path}[/]")
418
+
419
+ # Generate PDCA improvement report
420
+ console.print("\n[bold cyan]🎯 ACT: Generating PDCA improvement recommendations...[/]")
421
+ pdca_path = generate_pdca_improvement_report(pdca_metrics, args.report_name, args.dir)
422
+ if pdca_path:
423
+ console.print(f"[bright_green]🚀 PDCA improvement report saved: {pdca_path}[/]")
188
424
 
189
425
 
190
426
  def _run_trend_analysis(profiles_to_use: List[str], args: argparse.Namespace) -> None:
191
- """Analyze and display cost trends."""
427
+ """Analyze and display cost trends with multi-profile support."""
192
428
  console.print("[bold bright_cyan]Analysing cost trends...[/]")
429
+
430
+ # Display billing profile information
431
+ billing_profile = os.getenv("BILLING_PROFILE")
432
+ if billing_profile:
433
+ console.print(f"[dim cyan]Using billing profile for cost data: {billing_profile}[/]")
434
+
193
435
  raw_trend_data = []
194
- if args.combine:
195
- account_profiles = defaultdict(list)
196
- for profile in profiles_to_use:
197
- try:
198
- session = boto3.Session(profile_name=profile)
199
- account_id = get_account_id(session)
200
- if account_id:
201
- account_profiles[account_id].append(profile)
202
- except Exception as e:
203
- console.print(f"[red]Error checking account ID for profile {profile}: {str(e)}[/]")
204
436
 
205
- for account_id, profiles in account_profiles.items():
206
- try:
207
- primary_profile = profiles[0]
208
- session = boto3.Session(profile_name=primary_profile)
209
- cost_data = get_trend(session, args.tag)
210
- trend_data = cost_data.get("monthly_costs")
211
-
212
- if not trend_data:
213
- console.print(f"[yellow]No trend data available for account {account_id}[/]")
214
- continue
215
-
216
- profile_list = ", ".join(profiles)
217
- console.print(f"\n[bright_yellow]Account: {account_id} (Profiles: {profile_list})[/]")
218
- raw_trend_data.append(cost_data)
219
- create_trend_bars(trend_data)
220
- except Exception as e:
221
- console.print(f"[red]Error getting trend for account {account_id}: {str(e)}[/]")
437
+ # Enhanced progress tracking for trend analysis
438
+ with Progress(
439
+ SpinnerColumn(),
440
+ TextColumn("[progress.description]{task.description}"),
441
+ BarColumn(),
442
+ TaskProgressColumn(),
443
+ TimeElapsedColumn(),
444
+ console=console,
445
+ transient=True,
446
+ ) as progress:
447
+ if args.combine:
448
+ account_profiles = defaultdict(list)
449
+ task1 = progress.add_task("Grouping profiles by account", total=len(profiles_to_use))
222
450
 
223
- else:
224
- for profile in profiles_to_use:
225
- try:
226
- session = boto3.Session(profile_name=profile)
227
- cost_data = get_trend(session, args.tag)
228
- trend_data = cost_data.get("monthly_costs")
229
- account_id = cost_data.get("account_id", "Unknown")
230
-
231
- if not trend_data:
232
- console.print(f"[yellow]No trend data available for profile {profile}[/]")
233
- continue
234
-
235
- console.print(f"\n[bright_yellow]Account: {account_id} (Profile: {profile})[/]")
236
- raw_trend_data.append(cost_data)
237
- create_trend_bars(trend_data)
238
- except Exception as e:
239
- console.print(f"[red]Error getting trend for profile {profile}: {str(e)}[/]")
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)
483
+
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)
240
508
 
241
509
  if raw_trend_data and args.report_name and args.report_type:
242
510
  if "json" in args.report_type:
@@ -246,10 +514,11 @@ def _run_trend_analysis(profiles_to_use: List[str], args: argparse.Namespace) ->
246
514
 
247
515
 
248
516
  def _get_display_table_period_info(profiles_to_use: List[str], time_range: Optional[int]) -> Tuple[str, str, str, str]:
249
- """Get period information for the display table."""
517
+ """Get period information for the display table using appropriate billing profile."""
250
518
  if profiles_to_use:
251
519
  try:
252
- sample_session = boto3.Session(profile_name=profiles_to_use[0])
520
+ # Use billing session for cost data period information
521
+ sample_session = _create_cost_session(profiles_to_use[0])
253
522
  sample_cost_data = get_cost_data(sample_session, time_range)
254
523
  previous_period_name = sample_cost_data.get("previous_period_name", "Last Month Due")
255
524
  current_period_name = sample_cost_data.get("current_period_name", "Current Month Cost")
@@ -292,8 +561,8 @@ def create_display_table(
292
561
  Column("Cost By Service", vertical="middle"),
293
562
  Column("Budget Status", vertical="middle"),
294
563
  Column("EC2 Instance Summary", justify="center", vertical="middle"),
295
- title="AWS FinOps Dashboard",
296
- caption="AWS FinOps Dashboard CLI",
564
+ title="CloudOps Runbooks FinOps Platform",
565
+ caption="Enterprise Multi-Account Cost Optimization",
297
566
  box=box.ASCII_DOUBLE_HEAD,
298
567
  show_lines=True,
299
568
  style="bright_cyan",
@@ -342,42 +611,220 @@ def _generate_dashboard_data(
342
611
  args: argparse.Namespace,
343
612
  table: Table,
344
613
  ) -> List[ProfileData]:
345
- """Fetch, process, and prepare the main dashboard data."""
614
+ """Fetch, process, and prepare the main dashboard data with multi-profile support."""
346
615
  export_data: List[ProfileData] = []
347
- if args.combine:
348
- account_profiles = defaultdict(list)
349
- for profile in profiles_to_use:
350
- try:
351
- session = boto3.Session(profile_name=profile)
352
- current_account_id = get_account_id(session) # Renamed to avoid conflict
353
- if current_account_id:
354
- account_profiles[current_account_id].append(profile)
616
+
617
+ # Enhanced progress tracking with enterprise-grade progress indicators
618
+ with Progress(
619
+ SpinnerColumn(),
620
+ TextColumn("[progress.description]{task.description}"),
621
+ BarColumn(complete_style="bright_green", finished_style="bright_green"),
622
+ TaskProgressColumn(),
623
+ TimeElapsedColumn(),
624
+ console=console,
625
+ transient=False, # Keep progress visible
626
+ ) as progress:
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)
639
+ else:
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
+ )
355
654
  else:
356
- console.log(f"[yellow]Could not determine account ID for profile {profile}[/]")
357
- except Exception as e:
358
- console.log(f"[bold red]Error checking account ID for profile {profile}: {str(e)}[/]")
359
-
360
- for account_id_key, profiles_list in track( # Renamed loop variables
361
- account_profiles.items(), description="[bright_cyan]Fetching cost data..."
362
- ):
363
- # account_id_key here is known to be a string because it's a key from account_profiles
364
- # where None keys were filtered out when populating it.
365
- if len(profiles_list) > 1:
366
- profile_data = process_combined_profiles(
367
- account_id_key, profiles_list, user_regions, time_range, args.tag
368
- )
369
- else:
370
- profile_data = process_single_profile(profiles_list[0], user_regions, time_range, args.tag)
371
- export_data.append(profile_data)
372
- add_profile_to_table(table, profile_data)
373
- else:
374
- for profile in track(profiles_to_use, description="[bright_cyan]Fetching cost data..."):
375
- profile_data = process_single_profile(profile, user_regions, time_range, args.tag)
376
- export_data.append(profile_data)
377
- add_profile_to_table(table, profile_data)
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
+
378
672
  return export_data
379
673
 
380
674
 
675
+ def _process_single_profile_enhanced(
676
+ profile: str,
677
+ user_regions: Optional[List[str]] = None,
678
+ time_range: Optional[int] = None,
679
+ tag: Optional[List[str]] = None,
680
+ ) -> ProfileData:
681
+ """
682
+ Enhanced single profile processing with multi-profile session support.
683
+ Uses appropriate sessions for different operations: billing, management, operational.
684
+ """
685
+ try:
686
+ # Use billing session for cost data
687
+ cost_session = _create_cost_session(profile)
688
+ cost_data = get_cost_data(cost_session, time_range, tag)
689
+
690
+ # Use operational session for EC2 and resource operations
691
+ ops_session = _create_operational_session(profile)
692
+
693
+ if user_regions:
694
+ profile_regions = user_regions
695
+ else:
696
+ profile_regions = get_accessible_regions(ops_session)
697
+
698
+ ec2_data = ec2_summary(ops_session, profile_regions)
699
+ service_costs, service_cost_data = process_service_costs(cost_data)
700
+ budget_info = format_budget_info(cost_data["budgets"])
701
+ account_id = cost_data.get("account_id", "Unknown") or "Unknown"
702
+ ec2_summary_text = format_ec2_summary(ec2_data)
703
+ percent_change_in_total_cost = change_in_total_cost(cost_data["current_month"], cost_data["last_month"])
704
+
705
+ return {
706
+ "profile": profile,
707
+ "account_id": account_id,
708
+ "last_month": cost_data["last_month"],
709
+ "current_month": cost_data["current_month"],
710
+ "service_costs": service_cost_data,
711
+ "service_costs_formatted": service_costs,
712
+ "budget_info": budget_info,
713
+ "ec2_summary": ec2_data,
714
+ "ec2_summary_formatted": ec2_summary_text,
715
+ "success": True,
716
+ "error": None,
717
+ "current_period_name": cost_data["current_period_name"],
718
+ "previous_period_name": cost_data["previous_period_name"],
719
+ "percent_change_in_total_cost": percent_change_in_total_cost,
720
+ }
721
+
722
+ except Exception as e:
723
+ console.log(f"[red]Error processing profile {profile}: {str(e)}[/]")
724
+ return {
725
+ "profile": profile,
726
+ "account_id": "Error",
727
+ "last_month": 0,
728
+ "current_month": 0,
729
+ "service_costs": [],
730
+ "service_costs_formatted": [f"Failed to process profile: {str(e)}"],
731
+ "budget_info": ["N/A"],
732
+ "ec2_summary": {"N/A": 0},
733
+ "ec2_summary_formatted": ["Error"],
734
+ "success": False,
735
+ "error": str(e),
736
+ "current_period_name": "Current month",
737
+ "previous_period_name": "Last month",
738
+ "percent_change_in_total_cost": None,
739
+ }
740
+
741
+
742
+ def _process_combined_profiles_enhanced(
743
+ account_id: str,
744
+ profiles: List[str],
745
+ user_regions: Optional[List[str]] = None,
746
+ time_range: Optional[int] = None,
747
+ tag: Optional[List[str]] = None,
748
+ ) -> ProfileData:
749
+ """
750
+ Enhanced combined profile processing with multi-profile session support.
751
+ Aggregates data from multiple profiles in the same AWS account.
752
+ """
753
+ try:
754
+ primary_profile = profiles[0]
755
+
756
+ # Use billing session for cost data aggregation
757
+ primary_cost_session = _create_cost_session(primary_profile)
758
+ # Use operational session for resource data
759
+ primary_ops_session = _create_operational_session(primary_profile)
760
+
761
+ # Get cost data using billing session
762
+ account_cost_data = get_cost_data(primary_cost_session, time_range, tag)
763
+
764
+ if user_regions:
765
+ profile_regions = user_regions
766
+ else:
767
+ profile_regions = get_accessible_regions(primary_ops_session)
768
+
769
+ # Aggregate EC2 data from all profiles using operational sessions
770
+ combined_ec2_data = defaultdict(int)
771
+ for profile in profiles:
772
+ try:
773
+ profile_ops_session = _create_operational_session(profile)
774
+ profile_ec2_data = ec2_summary(profile_ops_session, profile_regions)
775
+ for instance_type, count in profile_ec2_data.items():
776
+ combined_ec2_data[instance_type] += count
777
+ except Exception as e:
778
+ console.log(f"[yellow]Warning: Could not get EC2 data for profile {profile}: {str(e)}[/]")
779
+
780
+ service_costs, service_cost_data = process_service_costs(account_cost_data)
781
+ budget_info = format_budget_info(account_cost_data["budgets"])
782
+ ec2_summary_text = format_ec2_summary(dict(combined_ec2_data))
783
+ percent_change_in_total_cost = change_in_total_cost(
784
+ account_cost_data["current_month"], account_cost_data["last_month"]
785
+ )
786
+
787
+ profile_list = ", ".join(profiles)
788
+ console.log(f"[dim cyan]Combined {len(profiles)} profiles for account {account_id}: {profile_list}[/]")
789
+
790
+ return {
791
+ "profile": f"Combined ({profile_list})",
792
+ "account_id": account_id,
793
+ "last_month": account_cost_data["last_month"],
794
+ "current_month": account_cost_data["current_month"],
795
+ "service_costs": service_cost_data,
796
+ "service_costs_formatted": service_costs,
797
+ "budget_info": budget_info,
798
+ "ec2_summary": dict(combined_ec2_data),
799
+ "ec2_summary_formatted": ec2_summary_text,
800
+ "success": True,
801
+ "error": None,
802
+ "current_period_name": account_cost_data["current_period_name"],
803
+ "previous_period_name": account_cost_data["previous_period_name"],
804
+ "percent_change_in_total_cost": percent_change_in_total_cost,
805
+ }
806
+
807
+ except Exception as e:
808
+ console.log(f"[red]Error processing combined profiles for account {account_id}: {str(e)}[/]")
809
+ profile_list = ", ".join(profiles)
810
+ return {
811
+ "profile": f"Combined ({profile_list})",
812
+ "account_id": account_id,
813
+ "last_month": 0,
814
+ "current_month": 0,
815
+ "service_costs": [],
816
+ "service_costs_formatted": [f"Failed to process combined profiles: {str(e)}"],
817
+ "budget_info": ["N/A"],
818
+ "ec2_summary": {"N/A": 0},
819
+ "ec2_summary_formatted": ["Error"],
820
+ "success": False,
821
+ "error": str(e),
822
+ "current_period_name": "Current month",
823
+ "previous_period_name": "Last month",
824
+ "percent_change_in_total_cost": None,
825
+ }
826
+
827
+
381
828
  def _export_dashboard_reports(
382
829
  export_data: List[ProfileData],
383
830
  args: argparse.Namespace,
@@ -414,10 +861,34 @@ def _export_dashboard_reports(
414
861
 
415
862
 
416
863
  def run_dashboard(args: argparse.Namespace) -> int:
417
- """Main function to run the AWS FinOps dashboard."""
864
+ """Main function to run the CloudOps Runbooks FinOps Platform with multi-profile support."""
418
865
  with Status("[bright_cyan]Initialising...", spinner="aesthetic", speed=0.4):
419
866
  profiles_to_use, user_regions, time_range = _initialize_profiles(args)
420
867
 
868
+ # Display multi-profile configuration at startup
869
+ billing_profile = os.getenv("BILLING_PROFILE")
870
+ mgmt_profile = os.getenv("MANAGEMENT_PROFILE")
871
+ ops_profile = os.getenv("CENTRALISED_OPS_PROFILE")
872
+
873
+ if any([billing_profile, mgmt_profile, ops_profile]):
874
+ console.print("\n[bold bright_cyan]🔧 Multi-Profile Configuration Detected[/]")
875
+ config_table = Table(
876
+ title="Profile Configuration", show_header=True, header_style="bold cyan", box=box.SIMPLE, style="dim"
877
+ )
878
+ config_table.add_column("Operation Type", style="bold")
879
+ config_table.add_column("Profile", style="bright_cyan")
880
+ config_table.add_column("Purpose", style="dim")
881
+
882
+ if billing_profile:
883
+ config_table.add_row("💰 Billing", billing_profile, "Cost Explorer & Budget API access")
884
+ if mgmt_profile:
885
+ config_table.add_row("🏛️ Management", mgmt_profile, "Account ID & Organizations operations")
886
+ if ops_profile:
887
+ config_table.add_row("⚙️ Operational", ops_profile, "EC2, S3, and resource discovery")
888
+
889
+ console.print(config_table)
890
+ console.print("[dim]Fallback: Using profile-specific sessions when env vars not set[/]\n")
891
+
421
892
  if args.audit:
422
893
  _run_audit_report(profiles_to_use, args)
423
894
  return 0
@@ -446,3 +917,180 @@ def run_dashboard(args: argparse.Namespace) -> int:
446
917
  _export_dashboard_reports(export_data, args, previous_period_dates, current_period_dates)
447
918
 
448
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)}