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
@@ -0,0 +1,525 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Enhanced FinOps Dashboard Runner
4
+ Migrated and optimized from README/aws-finops-dashboard
5
+
6
+ This module provides enterprise-grade FinOps dashboard capabilities including:
7
+ - Multi-profile AWS cost analysis with Rich console formatting
8
+ - Advanced audit reporting with PDF/CSV/JSON export
9
+ - Resource utilization tracking and optimization recommendations
10
+ - Budget monitoring and alerting integration
11
+ - Trend analysis and forecasting capabilities
12
+ """
13
+
14
+ import argparse
15
+ import csv
16
+ import json
17
+ from collections import defaultdict
18
+ from datetime import datetime, timedelta
19
+ from pathlib import Path
20
+ from typing import Any, Dict, List, Optional, Tuple, Union
21
+
22
+ import boto3
23
+ from rich import box
24
+ from rich.console import Console
25
+ from rich.panel import Panel
26
+ from rich.progress import Progress, SpinnerColumn, TextColumn, track
27
+ from rich.status import Status
28
+ from rich.table import Column, Table
29
+ from rich.tree import Tree
30
+
31
+ from ..common.rich_utils import get_console
32
+ from .finops_dashboard import FinOpsConfig
33
+
34
+ console = Console()
35
+
36
+
37
+ class EnhancedFinOpsDashboard:
38
+ """Enhanced FinOps Dashboard with production-tested capabilities from aws-finops-dashboard"""
39
+
40
+ def __init__(self, config: Optional[FinOpsConfig] = None):
41
+ self.config = config or FinOpsConfig()
42
+ self.console = Console()
43
+ self.rich_console = self.console # Use the console instance directly
44
+
45
+ # Export directory setup
46
+ self.export_dir = Path("artifacts/finops-exports")
47
+ self.export_dir.mkdir(parents=True, exist_ok=True)
48
+
49
+ def get_aws_profiles(self) -> List[str]:
50
+ """Get available AWS profiles from AWS CLI configuration"""
51
+ try:
52
+ import configparser
53
+ import os
54
+
55
+ aws_config_path = os.path.expanduser("~/.aws/config")
56
+ aws_credentials_path = os.path.expanduser("~/.aws/credentials")
57
+
58
+ profiles = set()
59
+
60
+ # Parse AWS config file
61
+ if os.path.exists(aws_config_path):
62
+ config = configparser.ConfigParser()
63
+ config.read(aws_config_path)
64
+ for section in config.sections():
65
+ if section.startswith("profile "):
66
+ profiles.add(section.replace("profile ", ""))
67
+ elif section == "default":
68
+ profiles.add("default")
69
+
70
+ # Parse AWS credentials file
71
+ if os.path.exists(aws_credentials_path):
72
+ credentials = configparser.ConfigParser()
73
+ credentials.read(aws_credentials_path)
74
+ profiles.update(credentials.sections())
75
+
76
+ return sorted(list(profiles))
77
+
78
+ except Exception as e:
79
+ console.print(f"āš ļø Error reading AWS profiles: {e}", style="yellow")
80
+ return []
81
+
82
+ def get_account_info(self, profile: str) -> Dict[str, Any]:
83
+ """Get AWS account information for a profile"""
84
+ try:
85
+ session = boto3.Session(profile_name=profile)
86
+ sts = session.client("sts")
87
+
88
+ identity = sts.get_caller_identity()
89
+
90
+ return {
91
+ "account_id": identity["Account"],
92
+ "user_arn": identity["Arn"],
93
+ "user_id": identity["UserId"],
94
+ "profile": profile,
95
+ "status": "active",
96
+ }
97
+
98
+ except Exception as e:
99
+ return {"account_id": "N/A", "profile": profile, "status": "error", "error": str(e)}
100
+
101
+ def get_resource_audit_data(self, profile: str, regions: Optional[List[str]] = None) -> Dict[str, Any]:
102
+ """
103
+ Get comprehensive resource audit data for a profile
104
+
105
+ Enhanced with additional resource types and cost impact analysis
106
+ """
107
+ audit_data = {
108
+ "profile": profile,
109
+ "account_info": self.get_account_info(profile),
110
+ "untagged_resources": 0,
111
+ "stopped_instances": 0,
112
+ "unused_volumes": 0,
113
+ "unused_eips": 0,
114
+ "budget_alerts": 0,
115
+ "cost_optimization_opportunities": [],
116
+ "regional_breakdown": {},
117
+ "total_potential_savings": 0.0,
118
+ }
119
+
120
+ if audit_data["account_info"]["status"] == "error":
121
+ return audit_data
122
+
123
+ try:
124
+ session = boto3.Session(profile_name=profile)
125
+
126
+ # Default to common regions if none specified
127
+ if not regions:
128
+ regions = ["us-east-1", "us-west-2", "eu-west-1"]
129
+
130
+ for region in regions:
131
+ region_data = self._audit_region_resources(session, region)
132
+ audit_data["regional_breakdown"][region] = region_data
133
+
134
+ # Aggregate data
135
+ audit_data["untagged_resources"] += region_data["untagged_resources"]
136
+ audit_data["stopped_instances"] += region_data["stopped_instances"]
137
+ audit_data["unused_volumes"] += region_data["unused_volumes"]
138
+ audit_data["unused_eips"] += region_data["unused_eips"]
139
+ audit_data["total_potential_savings"] += region_data["potential_savings"]
140
+ audit_data["cost_optimization_opportunities"].extend(region_data["optimization_opportunities"])
141
+
142
+ # Get budget information
143
+ audit_data["budget_alerts"] = self._get_budget_alerts(session)
144
+
145
+ except Exception as e:
146
+ console.print(f"āš ļø Error auditing resources for {profile}: {e}", style="yellow")
147
+
148
+ return audit_data
149
+
150
+ def _audit_region_resources(self, session: boto3.Session, region: str) -> Dict[str, Any]:
151
+ """Audit resources in a specific region"""
152
+ region_data = {
153
+ "region": region,
154
+ "untagged_resources": 0,
155
+ "stopped_instances": 0,
156
+ "unused_volumes": 0,
157
+ "unused_eips": 0,
158
+ "potential_savings": 0.0,
159
+ "optimization_opportunities": [],
160
+ }
161
+
162
+ try:
163
+ ec2 = session.client("ec2", region_name=region)
164
+
165
+ # Get stopped EC2 instances
166
+ instances_response = ec2.describe_instances(
167
+ Filters=[{"Name": "instance-state-name", "Values": ["stopped"]}]
168
+ )
169
+
170
+ stopped_instances = []
171
+ for reservation in instances_response["Reservations"]:
172
+ for instance in reservation["Instances"]:
173
+ stopped_instances.append(
174
+ {
175
+ "instance_id": instance["InstanceId"],
176
+ "instance_type": instance["InstanceType"],
177
+ "launch_time": instance.get("LaunchTime"),
178
+ "tags": instance.get("Tags", []),
179
+ }
180
+ )
181
+
182
+ region_data["stopped_instances"] = len(stopped_instances)
183
+
184
+ # Calculate potential savings from stopped instances (rough estimate)
185
+ # Assume average $50/month per stopped instance in savings opportunity
186
+ region_data["potential_savings"] += len(stopped_instances) * 50.0
187
+
188
+ if stopped_instances:
189
+ region_data["optimization_opportunities"].append(
190
+ {
191
+ "type": "stopped_instances",
192
+ "count": len(stopped_instances),
193
+ "description": f"{len(stopped_instances)} stopped EC2 instances - consider termination",
194
+ "potential_savings": len(stopped_instances) * 50.0,
195
+ "priority": "high",
196
+ }
197
+ )
198
+
199
+ # Get unused EBS volumes
200
+ volumes_response = ec2.describe_volumes(Filters=[{"Name": "status", "Values": ["available"]}])
201
+
202
+ unused_volumes = volumes_response["Volumes"]
203
+ region_data["unused_volumes"] = len(unused_volumes)
204
+
205
+ # Calculate EBS volume costs (rough estimate: $0.10/GB/month)
206
+ volume_savings = sum(vol["Size"] * 0.10 for vol in unused_volumes)
207
+ region_data["potential_savings"] += volume_savings
208
+
209
+ if unused_volumes:
210
+ region_data["optimization_opportunities"].append(
211
+ {
212
+ "type": "unused_volumes",
213
+ "count": len(unused_volumes),
214
+ "description": f"{len(unused_volumes)} unused EBS volumes",
215
+ "potential_savings": volume_savings,
216
+ "priority": "medium",
217
+ }
218
+ )
219
+
220
+ # Get unused Elastic IPs
221
+ eips_response = ec2.describe_addresses()
222
+ unused_eips = [eip for eip in eips_response["Addresses"] if "InstanceId" not in eip]
223
+ region_data["unused_eips"] = len(unused_eips)
224
+
225
+ # Unused EIP cost: $3.65/month each
226
+ eip_savings = len(unused_eips) * 3.65
227
+ region_data["potential_savings"] += eip_savings
228
+
229
+ if unused_eips:
230
+ region_data["optimization_opportunities"].append(
231
+ {
232
+ "type": "unused_eips",
233
+ "count": len(unused_eips),
234
+ "description": f"{len(unused_eips)} unused Elastic IPs",
235
+ "potential_savings": eip_savings,
236
+ "priority": "high",
237
+ }
238
+ )
239
+
240
+ # Count untagged resources (simplified check)
241
+ untagged_count = 0
242
+ for instance in stopped_instances:
243
+ if not instance["tags"]:
244
+ untagged_count += 1
245
+ for volume in unused_volumes:
246
+ if not volume.get("Tags"):
247
+ untagged_count += 1
248
+
249
+ region_data["untagged_resources"] = untagged_count
250
+
251
+ except Exception as e:
252
+ console.print(f"āš ļø Error auditing {region}: {e}", style="yellow")
253
+
254
+ return region_data
255
+
256
+ def _get_budget_alerts(self, session: boto3.Session) -> int:
257
+ """Get budget alert count"""
258
+ try:
259
+ budgets = session.client("budgets")
260
+
261
+ # Get account ID for budgets API
262
+ sts = session.client("sts")
263
+ account_id = sts.get_caller_identity()["Account"]
264
+
265
+ response = budgets.describe_budgets(AccountId=account_id)
266
+ return len(response.get("Budgets", []))
267
+
268
+ except Exception:
269
+ return 0 # Budgets API might not be accessible
270
+
271
+ def generate_audit_report(
272
+ self, profiles: Optional[List[str]] = None, regions: Optional[List[str]] = None
273
+ ) -> Dict[str, Any]:
274
+ """Generate comprehensive audit report for specified profiles"""
275
+
276
+ if not profiles:
277
+ profiles = self.get_aws_profiles()
278
+ if not profiles:
279
+ console.print("āŒ No AWS profiles found", style="red")
280
+ return {}
281
+
282
+ audit_results = {
283
+ "report_metadata": {
284
+ "generated_at": datetime.now().isoformat(),
285
+ "profiles_analyzed": len(profiles),
286
+ "regions_analyzed": len(regions) if regions else 3,
287
+ "report_type": "comprehensive_audit",
288
+ },
289
+ "profile_data": {},
290
+ "summary": {
291
+ "total_untagged_resources": 0,
292
+ "total_stopped_instances": 0,
293
+ "total_unused_volumes": 0,
294
+ "total_unused_eips": 0,
295
+ "total_budget_alerts": 0,
296
+ "total_potential_savings": 0.0,
297
+ "optimization_opportunities": [],
298
+ },
299
+ }
300
+
301
+ with Progress(
302
+ SpinnerColumn(), TextColumn("[progress.description]{task.description}"), console=console
303
+ ) as progress:
304
+ for profile in profiles:
305
+ task = progress.add_task(f"Auditing profile {profile}...", total=None)
306
+
307
+ profile_data = self.get_resource_audit_data(profile, regions)
308
+ audit_results["profile_data"][profile] = profile_data
309
+
310
+ # Aggregate summary data
311
+ summary = audit_results["summary"]
312
+ summary["total_untagged_resources"] += profile_data["untagged_resources"]
313
+ summary["total_stopped_instances"] += profile_data["stopped_instances"]
314
+ summary["total_unused_volumes"] += profile_data["unused_volumes"]
315
+ summary["total_unused_eips"] += profile_data["unused_eips"]
316
+ summary["total_budget_alerts"] += profile_data["budget_alerts"]
317
+ summary["total_potential_savings"] += profile_data["total_potential_savings"]
318
+ summary["optimization_opportunities"].extend(profile_data["cost_optimization_opportunities"])
319
+
320
+ progress.remove_task(task)
321
+
322
+ return audit_results
323
+
324
+ def display_audit_report(self, audit_results: Dict[str, Any]):
325
+ """Display audit report with enhanced Rich formatting"""
326
+
327
+ summary = audit_results["summary"]
328
+ profile_data = audit_results["profile_data"]
329
+
330
+ # Report header
331
+ header_panel = Panel.fit(
332
+ f"[bold bright_cyan]šŸ¢ AWS FinOps Comprehensive Audit Report[/bold bright_cyan]\n\n"
333
+ f"šŸ“Š Profiles Analyzed: [yellow]{len(profile_data)}[/yellow]\n"
334
+ f"šŸ•’ Generated: [green]{audit_results['report_metadata']['generated_at'][:19]}[/green]\n"
335
+ f"šŸ’° Total Savings Potential: [bold green]${summary['total_potential_savings']:.2f}/month[/bold green]",
336
+ title="Audit Report",
337
+ style="bright_cyan",
338
+ )
339
+ console.print(header_panel)
340
+
341
+ # Summary table
342
+ summary_table = Table(title="šŸ“ˆ Executive Summary", box=box.ASCII_DOUBLE_HEAD, style="bright_cyan")
343
+ summary_table.add_column("Metric", style="cyan", width=25)
344
+ summary_table.add_column("Count", style="yellow", width=10)
345
+ summary_table.add_column("Impact", style="green", width=20)
346
+
347
+ summary_table.add_row("Untagged Resources", str(summary["total_untagged_resources"]), "Compliance Risk")
348
+ summary_table.add_row(
349
+ "Stopped Instances",
350
+ str(summary["total_stopped_instances"]),
351
+ f"${summary['total_stopped_instances'] * 50:.0f}/month potential",
352
+ )
353
+ summary_table.add_row("Unused Volumes", str(summary["total_unused_volumes"]), "Storage waste")
354
+ summary_table.add_row(
355
+ "Unused EIPs", str(summary["total_unused_eips"]), f"${summary['total_unused_eips'] * 3.65:.0f}/month waste"
356
+ )
357
+ summary_table.add_row("Budget Alerts", str(summary["total_budget_alerts"]), "Monitoring coverage")
358
+
359
+ console.print(summary_table)
360
+
361
+ # Profile-specific table
362
+ profile_table = Table(
363
+ title="šŸ‘„ Profile-Specific Analysis", show_lines=True, box=box.ASCII_DOUBLE_HEAD, style="bright_cyan"
364
+ )
365
+
366
+ profile_table.add_column("Profile", justify="center", width=20)
367
+ profile_table.add_column("Account ID", justify="center", width=15)
368
+ profile_table.add_column("Untagged", width=10)
369
+ profile_table.add_column("Stopped EC2", width=12)
370
+ profile_table.add_column("Unused Vol", width=12)
371
+ profile_table.add_column("Unused EIP", width=12)
372
+ profile_table.add_column("Savings", width=12)
373
+
374
+ for profile, data in profile_data.items():
375
+ account_info = data["account_info"]
376
+ profile_table.add_row(
377
+ profile,
378
+ account_info["account_id"] if account_info["status"] == "active" else "ERROR",
379
+ str(data["untagged_resources"]),
380
+ str(data["stopped_instances"]),
381
+ str(data["unused_volumes"]),
382
+ str(data["unused_eips"]),
383
+ f"${data['total_potential_savings']:.0f}",
384
+ )
385
+
386
+ console.print(profile_table)
387
+
388
+ # Top optimization opportunities
389
+ if summary["optimization_opportunities"]:
390
+ console.print("\n[bold blue]šŸŽÆ Top Optimization Opportunities[/bold blue]")
391
+
392
+ # Sort by potential savings
393
+ sorted_opportunities = sorted(
394
+ summary["optimization_opportunities"], key=lambda x: x.get("potential_savings", 0), reverse=True
395
+ )
396
+
397
+ for i, opp in enumerate(sorted_opportunities[:10], 1): # Top 10
398
+ priority_color = {"high": "red", "medium": "yellow", "low": "green"}
399
+ color = priority_color.get(opp.get("priority", "low"), "white")
400
+
401
+ console.print(
402
+ f"{i:2d}. [bold {color}]{opp['description']}[/bold {color}] "
403
+ f"([green]${opp.get('potential_savings', 0):.0f}/month[/green])"
404
+ )
405
+
406
+ def export_audit_report(
407
+ self, audit_results: Dict[str, Any], formats: List[str] = ["json", "csv"]
408
+ ) -> Dict[str, str]:
409
+ """Export audit report in multiple formats"""
410
+
411
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
412
+ export_files = {}
413
+
414
+ # JSON export
415
+ if "json" in formats:
416
+ json_file = self.export_dir / f"finops_audit_report_{timestamp}.json"
417
+ with open(json_file, "w") as f:
418
+ json.dump(audit_results, f, indent=2, default=str)
419
+ export_files["json"] = str(json_file)
420
+
421
+ # CSV export
422
+ if "csv" in formats:
423
+ csv_file = self.export_dir / f"finops_audit_summary_{timestamp}.csv"
424
+ with open(csv_file, "w", newline="") as f:
425
+ writer = csv.writer(f)
426
+
427
+ # Header
428
+ writer.writerow(
429
+ [
430
+ "Profile",
431
+ "Account_ID",
432
+ "Untagged_Resources",
433
+ "Stopped_Instances",
434
+ "Unused_Volumes",
435
+ "Unused_EIPs",
436
+ "Budget_Alerts",
437
+ "Potential_Savings_Monthly",
438
+ ]
439
+ )
440
+
441
+ # Data rows
442
+ for profile, data in audit_results["profile_data"].items():
443
+ writer.writerow(
444
+ [
445
+ profile,
446
+ data["account_info"]["account_id"],
447
+ data["untagged_resources"],
448
+ data["stopped_instances"],
449
+ data["unused_volumes"],
450
+ data["unused_eips"],
451
+ data["budget_alerts"],
452
+ f"${data['total_potential_savings']:.2f}",
453
+ ]
454
+ )
455
+
456
+ export_files["csv"] = str(csv_file)
457
+
458
+ return export_files
459
+
460
+ def run_comprehensive_audit(
461
+ self,
462
+ profiles: Optional[List[str]] = None,
463
+ regions: Optional[List[str]] = None,
464
+ export_formats: List[str] = ["json", "csv"],
465
+ display_report: bool = True,
466
+ ) -> Dict[str, Any]:
467
+ """Run comprehensive FinOps audit with reporting and export"""
468
+
469
+ console.print("[bold bright_cyan]šŸš€ Starting Enhanced FinOps Audit...[/bold bright_cyan]")
470
+
471
+ # Generate audit data
472
+ audit_results = self.generate_audit_report(profiles, regions)
473
+
474
+ if not audit_results:
475
+ console.print("āŒ No audit data generated", style="red")
476
+ return {}
477
+
478
+ # Display report
479
+ if display_report:
480
+ self.display_audit_report(audit_results)
481
+
482
+ # Export results
483
+ if export_formats:
484
+ console.print(f"\nšŸ“„ Exporting report in formats: {', '.join(export_formats)}")
485
+ export_files = self.export_audit_report(audit_results, export_formats)
486
+
487
+ console.print("āœ… Export completed:")
488
+ for format_type, file_path in export_files.items():
489
+ console.print(f" šŸ“ {format_type.upper()}: {file_path}")
490
+
491
+ # Summary of potential savings
492
+ total_savings = audit_results["summary"]["total_potential_savings"]
493
+ if total_savings > 0:
494
+ annual_savings = total_savings * 12
495
+ console.print(f"\nšŸ’° [bold green]Total Optimization Potential:[/bold green]")
496
+ console.print(f" Monthly: [yellow]${total_savings:.2f}[/yellow]")
497
+ console.print(f" Annual: [green]${annual_savings:.2f}[/green]")
498
+
499
+ return audit_results
500
+
501
+
502
+ # CLI integration functions
503
+ def enhanced_audit_cli(
504
+ profiles: Optional[str] = None,
505
+ regions: Optional[str] = None,
506
+ export_formats: str = "json,csv",
507
+ output_dir: Optional[str] = None,
508
+ ) -> None:
509
+ """CLI command for enhanced FinOps audit"""
510
+
511
+ profile_list = profiles.split(",") if profiles else None
512
+ region_list = regions.split(",") if regions else None
513
+ format_list = export_formats.split(",") if export_formats else ["json"]
514
+
515
+ dashboard = EnhancedFinOpsDashboard()
516
+
517
+ if output_dir:
518
+ dashboard.export_dir = Path(output_dir)
519
+ dashboard.export_dir.mkdir(parents=True, exist_ok=True)
520
+
521
+ audit_results = dashboard.run_comprehensive_audit(
522
+ profiles=profile_list, regions=region_list, export_formats=format_list, display_report=True
523
+ )
524
+
525
+ return audit_results