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
@@ -9,10 +9,12 @@ from typing import Any, Dict, List, Optional
9
9
 
10
10
  import yaml
11
11
  from reportlab.lib import colors
12
- from reportlab.lib.pagesizes import landscape, letter
12
+ from reportlab.lib.pagesizes import A4, landscape, letter
13
13
  from reportlab.lib.styles import ParagraphStyle, getSampleStyleSheet
14
+ from reportlab.lib.units import inch
14
15
  from reportlab.platypus import (
15
16
  Flowable,
17
+ PageBreak,
16
18
  Paragraph,
17
19
  SimpleDocTemplate,
18
20
  Spacer,
@@ -73,10 +75,22 @@ def export_audit_report_to_pdf(
73
75
  "Unused Volumes",
74
76
  "Unused EIPs",
75
77
  "Budget Alerts",
78
+ "Risk Score",
76
79
  ]
77
80
  table_data = [headers]
78
81
 
79
82
  for row in audit_data_list:
83
+ # Format risk score for PDF display
84
+ risk_score = row.get("risk_score", 0)
85
+ if risk_score == 0:
86
+ risk_display = "LOW (0)"
87
+ elif risk_score <= 10:
88
+ risk_display = f"MEDIUM ({risk_score})"
89
+ elif risk_score <= 25:
90
+ risk_display = f"HIGH ({risk_score})"
91
+ else:
92
+ risk_display = f"CRITICAL ({risk_score})"
93
+
80
94
  table_data.append(
81
95
  [
82
96
  row.get("profile", ""),
@@ -86,6 +100,7 @@ def export_audit_report_to_pdf(
86
100
  row.get("unused_volumes", ""),
87
101
  row.get("unused_eips", ""),
88
102
  row.get("budget_alerts", ""),
103
+ risk_display,
89
104
  ]
90
105
  )
91
106
 
@@ -105,20 +120,24 @@ def export_audit_report_to_pdf(
105
120
  )
106
121
  )
107
122
 
108
- elements.append(Paragraph("AWS FinOps Dashboard (Audit Report)", styles["Title"]))
123
+ elements.append(Paragraph("🎯 CloudOps Runbooks FinOps - Enterprise Audit Report", styles["Title"]))
109
124
  elements.append(Spacer(1, 12))
110
125
  elements.append(table)
111
126
  elements.append(Spacer(1, 4))
112
- elements.append(
113
- Paragraph(
114
- "Note: This table lists untagged EC2, RDS, Lambda, ELBv2 only.",
115
- audit_footer_style,
116
- )
127
+
128
+ # Enhanced notes with PDCA information
129
+ pdca_info = Paragraph(
130
+ "📊 PDCA Framework: This report follows Plan-Do-Check-Act continuous improvement methodology.<br/>"
131
+ "📝 Coverage: Scans EC2, RDS, Lambda, ELBv2 resources across all accessible regions.<br/>"
132
+ "🎯 Risk Scoring: LOW (0-10), MEDIUM (11-25), HIGH (26-50), CRITICAL (>50)",
133
+ audit_footer_style,
117
134
  )
135
+ elements.append(pdca_info)
136
+
118
137
  elements.append(Spacer(1, 2))
119
138
  current_time_str = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
120
139
  footer_text = (
121
- f"This audit report is generated using AWS FinOps Dashboard (CLI) \u00a9 2025 on {current_time_str}"
140
+ f"🚀 Generated using CloudOps-Runbooks FinOps Dashboard (PDCA Enhanced) \u00a9 2025 on {current_time_str}"
122
141
  )
123
142
  elements.append(Paragraph(footer_text, audit_footer_style))
124
143
 
@@ -129,6 +148,93 @@ def export_audit_report_to_pdf(
129
148
  return None
130
149
 
131
150
 
151
+ def _truncate_service_costs(services_data: str, max_length: int = 500) -> str:
152
+ """
153
+ Truncate service costs data for PDF display if too long.
154
+
155
+ :param services_data: Service costs formatted as string
156
+ :param max_length: Maximum character length before truncation
157
+ :return: Truncated service costs string
158
+ """
159
+ if len(services_data) <= max_length:
160
+ return services_data
161
+
162
+ lines = services_data.split("\n")
163
+ truncated_lines = []
164
+ current_length = 0
165
+
166
+ for line in lines:
167
+ if current_length + len(line) + 1 <= max_length - 50: # Reserve space for truncation message
168
+ truncated_lines.append(line)
169
+ current_length += len(line) + 1
170
+ else:
171
+ break
172
+
173
+ # Add truncation indicator with service count
174
+ remaining_services = len(lines) - len(truncated_lines)
175
+ if remaining_services > 0:
176
+ truncated_lines.append(f"... and {remaining_services} more services")
177
+
178
+ return "\n".join(truncated_lines)
179
+
180
+
181
+ def _optimize_table_for_pdf(table_data: List[List[str]], max_col_width: int = 120) -> List[List[str]]:
182
+ """
183
+ Optimize table data for PDF rendering by managing column widths.
184
+
185
+ :param table_data: Raw table data with headers and rows
186
+ :param max_col_width: Maximum character width for any column
187
+ :return: Optimized table data
188
+ """
189
+ optimized_data = []
190
+
191
+ for row_idx, row in enumerate(table_data):
192
+ optimized_row = []
193
+
194
+ for col_idx, cell in enumerate(row):
195
+ if col_idx == 4: # "Cost By Service" column (index 4)
196
+ # Apply special handling to service costs column
197
+ optimized_cell = _truncate_service_costs(str(cell), max_col_width)
198
+ else:
199
+ # General cell optimization
200
+ cell_str = str(cell)
201
+ if len(cell_str) > max_col_width:
202
+ # Truncate long content with ellipsis
203
+ optimized_cell = cell_str[: max_col_width - 3] + "..."
204
+ else:
205
+ optimized_cell = cell_str
206
+
207
+ optimized_row.append(optimized_cell)
208
+
209
+ optimized_data.append(optimized_row)
210
+
211
+ return optimized_data
212
+
213
+
214
+ def _create_paginated_tables(table_data: List[List[str]], max_rows_per_page: int = 15) -> List[List[List[str]]]:
215
+ """
216
+ Split large table data into multiple pages for PDF generation.
217
+
218
+ :param table_data: Complete table data including headers
219
+ :param max_rows_per_page: Maximum data rows per page (excluding header)
220
+ :return: List of table data chunks, each with headers
221
+ """
222
+ if len(table_data) <= max_rows_per_page + 1: # +1 for header
223
+ return [table_data]
224
+
225
+ headers = table_data[0]
226
+ data_rows = table_data[1:]
227
+
228
+ paginated_tables = []
229
+
230
+ for i in range(0, len(data_rows), max_rows_per_page):
231
+ chunk = data_rows[i : i + max_rows_per_page]
232
+ table_chunk = [headers] + chunk
233
+ paginated_tables.append(table_chunk)
234
+
235
+ return paginated_tables
236
+
237
+
132
238
  def clean_rich_tags(text: str) -> str:
133
239
  """
134
240
  Clean the rich text before writing the data to a pdf.
@@ -161,6 +267,7 @@ def export_audit_report_to_csv(
161
267
  "Unused Volumes",
162
268
  "Unused EIPs",
163
269
  "Budget Alerts",
270
+ "Risk Score",
164
271
  ]
165
272
  # Corresponding keys in the audit_data_list dictionaries
166
273
  data_keys = [
@@ -171,6 +278,7 @@ def export_audit_report_to_csv(
171
278
  "unused_volumes",
172
279
  "unused_eips",
173
280
  "budget_alerts",
281
+ "risk_score",
174
282
  ]
175
283
 
176
284
  with open(output_filename, "w", newline="") as csvfile:
@@ -231,7 +339,7 @@ def export_cost_dashboard_to_pdf(
231
339
  previous_period_dates: str = "N/A",
232
340
  current_period_dates: str = "N/A",
233
341
  ) -> Optional[str]:
234
- """Export dashboard data to a PDF file."""
342
+ """Export dashboard data to a PDF file with enterprise-grade layout handling."""
235
343
  try:
236
344
  timestamp = datetime.now().strftime("%Y%m%d_%H%M")
237
345
  base_filename = f"{filename}_{timestamp}.pdf"
@@ -242,71 +350,213 @@ def export_cost_dashboard_to_pdf(
242
350
  else:
243
351
  output_filename = base_filename
244
352
 
245
- doc = SimpleDocTemplate(output_filename, pagesize=landscape(letter))
353
+ # Use A4 landscape for better space utilization
354
+ doc = SimpleDocTemplate(
355
+ output_filename,
356
+ pagesize=landscape(A4),
357
+ rightMargin=0.5 * inch,
358
+ leftMargin=0.5 * inch,
359
+ topMargin=0.5 * inch,
360
+ bottomMargin=0.5 * inch,
361
+ )
246
362
  styles = getSampleStyleSheet()
247
363
  elements: List[Flowable] = []
248
364
 
249
- previous_period_header = f"Cost for period\n({previous_period_dates})"
250
- current_period_header = f"Cost for period\n({current_period_dates})"
365
+ # Enhanced title with executive summary
366
+ title_style = ParagraphStyle(
367
+ name="EnhancedTitle",
368
+ parent=styles["Title"],
369
+ fontSize=16,
370
+ spaceAfter=12,
371
+ textColor=colors.darkblue,
372
+ alignment=1, # Center alignment
373
+ )
374
+
375
+ # Calculate summary metrics
376
+ total_accounts = len(data)
377
+ total_current_cost = sum(row["current_month"] for row in data if row.get("current_month", 0))
378
+ total_previous_cost = sum(row["last_month"] for row in data if row.get("last_month", 0))
379
+ cost_change = (
380
+ ((total_current_cost - total_previous_cost) / total_previous_cost * 100) if total_previous_cost > 0 else 0
381
+ )
382
+
383
+ elements.append(Paragraph("🏢 CloudOps Runbooks FinOps - Enterprise Cost Report", title_style))
384
+
385
+ # Executive summary
386
+ summary_style = ParagraphStyle(
387
+ name="Summary", parent=styles["Normal"], fontSize=10, spaceAfter=8, textColor=colors.darkgreen
388
+ )
389
+
390
+ summary_text = (
391
+ f"📊 Executive Summary: {total_accounts} accounts analyzed | "
392
+ f"Total Current Cost: ${total_current_cost:,.2f} | "
393
+ f"Cost Change: {cost_change:+.1f}% | "
394
+ f"Report Period: {current_period_dates}"
395
+ )
396
+ elements.append(Paragraph(summary_text, summary_style))
397
+ elements.append(Spacer(1, 12))
398
+
399
+ # Prepare table data with optimization
400
+ previous_period_header = f"Cost Period\n({previous_period_dates})"
401
+ current_period_header = f"Cost Period\n({current_period_dates})"
251
402
 
252
403
  headers = [
253
- "CLI Profile",
254
- "AWS Account ID",
404
+ "Profile",
405
+ "Account ID",
255
406
  previous_period_header,
256
407
  current_period_header,
257
- "Cost By Service",
408
+ "Top Services",
258
409
  "Budget Status",
259
- "EC2 Instances",
410
+ "EC2 Summary",
260
411
  ]
261
- table_data = [headers]
262
412
 
263
- for row in data:
264
- services_data = "\n".join([f"{service}: ${cost:.2f}" for service, cost in row["service_costs"]])
265
- budgets_data = "\n".join(row["budget_info"]) if row["budget_info"] else "No budgets"
266
- ec2_data_summary = "\n".join(
267
- [f"{state}: {count}" for state, count in row["ec2_summary"].items() if count > 0]
268
- )
413
+ raw_table_data = [headers]
269
414
 
270
- table_data.append(
415
+ for row in data:
416
+ # Optimize service costs for PDF display
417
+ if row["service_costs"]:
418
+ # Show only top 10 services to prevent width issues
419
+ top_services = row["service_costs"][:10]
420
+ services_data = "\n".join([f"{service}: ${cost:.2f}" for service, cost in top_services])
421
+ if len(row["service_costs"]) > 10:
422
+ remaining_count = len(row["service_costs"]) - 10
423
+ services_data += f"\n... and {remaining_count} more services"
424
+ else:
425
+ services_data = "No costs"
426
+
427
+ # Optimize budget display
428
+ budget_lines = row["budget_info"][:3] if row["budget_info"] else ["No budgets"]
429
+ budgets_data = "\n".join(budget_lines)
430
+ if len(row["budget_info"]) > 3:
431
+ budgets_data += f"\n... +{len(row['budget_info']) - 3} more"
432
+
433
+ # Optimize EC2 summary
434
+ ec2_items = [(state, count) for state, count in row["ec2_summary"].items() if count > 0]
435
+ if ec2_items:
436
+ ec2_data_summary = "\n".join([f"{state}: {count}" for state, count in ec2_items[:5]])
437
+ if len(ec2_items) > 5:
438
+ ec2_data_summary += f"\n... +{len(ec2_items) - 5} more"
439
+ else:
440
+ ec2_data_summary = "No instances"
441
+
442
+ # Format cost change indicator
443
+ cost_change_text = ""
444
+ if row.get("percent_change_in_total_cost") is not None:
445
+ change = row["percent_change_in_total_cost"]
446
+ if change > 0:
447
+ cost_change_text = f"\n↑ +{change:.1f}%"
448
+ elif change < 0:
449
+ cost_change_text = f"\n↓ {change:.1f}%"
450
+ else:
451
+ cost_change_text = "\n→ 0%"
452
+
453
+ raw_table_data.append(
271
454
  [
272
455
  row["profile"],
273
456
  row["account_id"],
274
- f"${row['last_month']:.2f}",
275
- f"${row['current_month']:.2f}",
276
- services_data or "No costs",
277
- budgets_data or "No budgets",
278
- ec2_data_summary or "No instances",
457
+ f"${row['last_month']:,.2f}",
458
+ f"${row['current_month']:,.2f}{cost_change_text}",
459
+ services_data,
460
+ budgets_data,
461
+ ec2_data_summary,
279
462
  ]
280
463
  )
281
464
 
282
- table = Table(table_data, repeatRows=1)
283
- table.setStyle(
284
- TableStyle(
285
- [
286
- ("BACKGROUND", (0, 0), (-1, 0), colors.black),
287
- ("TEXTCOLOR", (0, 0), (-1, 0), colors.whitesmoke),
288
- ("FONTNAME", (0, 0), (-1, -1), "Helvetica"),
289
- ("FONTSIZE", (0, 0), (-1, -1), 8),
290
- ("ALIGN", (0, 0), (-1, -1), "LEFT"),
291
- ("VALIGN", (0, 0), (-1, -1), "TOP"),
292
- ("GRID", (0, 0), (-1, -1), 0.25, colors.grey),
293
- ("BACKGROUND", (0, 1), (-1, -1), colors.whitesmoke),
294
- ]
295
- )
465
+ # Optimize table data for PDF rendering
466
+ optimized_table_data = _optimize_table_for_pdf(raw_table_data, max_col_width=80)
467
+
468
+ # Create paginated tables for large datasets
469
+ paginated_tables = _create_paginated_tables(optimized_table_data, max_rows_per_page=12)
470
+
471
+ # Style configuration for optimal PDF rendering
472
+ table_style = TableStyle(
473
+ [
474
+ # Header styling
475
+ ("BACKGROUND", (0, 0), (-1, 0), colors.navy),
476
+ ("TEXTCOLOR", (0, 0), (-1, 0), colors.whitesmoke),
477
+ ("FONTNAME", (0, 0), (-1, 0), "Helvetica-Bold"),
478
+ ("FONTSIZE", (0, 0), (-1, 0), 7),
479
+ # Data styling
480
+ ("FONTNAME", (0, 1), (-1, -1), "Helvetica"),
481
+ ("FONTSIZE", (0, 1), (-1, -1), 6),
482
+ ("ALIGN", (0, 0), (-1, -1), "LEFT"),
483
+ ("VALIGN", (0, 0), (-1, -1), "TOP"),
484
+ # Grid and background
485
+ ("GRID", (0, 0), (-1, -1), 0.5, colors.darkgrey),
486
+ ("BACKGROUND", (0, 1), (-1, -1), colors.beige),
487
+ ("ROWBACKGROUNDS", (0, 1), (-1, -1), [colors.white, colors.lightgrey]),
488
+ # Column-specific styling
489
+ ("ALIGN", (2, 0), (3, -1), "RIGHT"), # Cost columns right-aligned
490
+ ("FONTNAME", (2, 1), (3, -1), "Helvetica-Bold"), # Bold cost values
491
+ ]
296
492
  )
297
493
 
298
- elements.append(Paragraph("AWS FinOps Dashboard (Cost Report)", styles["Title"]))
494
+ # Generate tables with pagination
495
+ for page_idx, table_data_chunk in enumerate(paginated_tables):
496
+ if page_idx > 0:
497
+ elements.append(PageBreak())
498
+ # Add page header for continuation pages
499
+ page_header = Paragraph(
500
+ f"CloudOps Runbooks FinOps - Page {page_idx + 1} of {len(paginated_tables)}",
501
+ ParagraphStyle(
502
+ name="PageHeader", parent=styles["Heading2"], fontSize=12, textColor=colors.darkblue
503
+ ),
504
+ )
505
+ elements.append(page_header)
506
+ elements.append(Spacer(1, 8))
507
+
508
+ # Create table with dynamic column widths
509
+ available_width = landscape(A4)[0] - 1 * inch # Account for margins
510
+ col_widths = [
511
+ available_width * 0.12, # Profile (12%)
512
+ available_width * 0.15, # Account ID (15%)
513
+ available_width * 0.12, # Previous Cost (12%)
514
+ available_width * 0.12, # Current Cost (12%)
515
+ available_width * 0.25, # Services (25%)
516
+ available_width * 0.12, # Budget (12%)
517
+ available_width * 0.12, # EC2 (12%)
518
+ ]
519
+
520
+ table = Table(table_data_chunk, repeatRows=1, colWidths=col_widths)
521
+ table.setStyle(table_style)
522
+ elements.append(table)
523
+
524
+ # Enhanced footer with metadata
299
525
  elements.append(Spacer(1, 12))
300
- elements.append(table)
301
- elements.append(Spacer(1, 4))
302
- current_time_str = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
303
- footer_text = f"This report is generated using AWS FinOps Dashboard (CLI) \u00a9 2025 on {current_time_str}"
304
- elements.append(Paragraph(footer_text, audit_footer_style))
305
526
 
527
+ footer_style = ParagraphStyle(
528
+ name="EnhancedFooter", parent=styles["Normal"], fontSize=8, textColor=colors.grey, alignment=1
529
+ )
530
+
531
+ current_time_str = datetime.now().strftime("%Y-%m-%d %H:%M:%S UTC")
532
+ footer_text = (
533
+ f"🚀 Generated by CloudOps-Runbooks FinOps Dashboard v0.7.8 | "
534
+ f"Report Generated: {current_time_str} | "
535
+ f"Accounts Analyzed: {total_accounts} | "
536
+ f"© 2025 CloudOps Enterprise"
537
+ )
538
+ elements.append(Paragraph(footer_text, footer_style))
539
+
540
+ # Build PDF with error handling
306
541
  doc.build(elements)
307
- return os.path.abspath(output_filename)
542
+
543
+ # Verify file creation
544
+ if os.path.exists(output_filename):
545
+ file_size = os.path.getsize(output_filename)
546
+ console.print(
547
+ f"[bright_green]✅ PDF generated successfully: {os.path.abspath(output_filename)} ({file_size:,} bytes)[/]"
548
+ )
549
+ return os.path.abspath(output_filename)
550
+ else:
551
+ console.print("[bold red]❌ PDF file was not created[/]")
552
+ return None
553
+
308
554
  except Exception as e:
309
- console.print(f"[bold red]Error exporting to PDF: {str(e)}[/]")
555
+ console.print(f"[bold red]Error exporting to PDF: {str(e)}[/]")
556
+ # Print more detailed error information for debugging
557
+ import traceback
558
+
559
+ console.print(f"[red]Detailed error trace: {traceback.format_exc()}[/]")
310
560
  return None
311
561
 
312
562
 
@@ -353,3 +603,187 @@ def load_config_file(file_path: str) -> Optional[Dict[str, Any]]:
353
603
  except Exception as e:
354
604
  console.print(f"[bold red]Error loading configuration file {file_path}: {e}[/]")
355
605
  return None
606
+
607
+
608
+ def generate_pdca_improvement_report(
609
+ pdca_metrics: List[Dict[str, Any]], file_name: str = "pdca_improvement", path: Optional[str] = None
610
+ ) -> Optional[str]:
611
+ """
612
+ Generate PDCA (Plan-Do-Check-Act) continuous improvement report.
613
+
614
+ :param pdca_metrics: List of PDCA metrics for each profile
615
+ :param file_name: The base name of the output file
616
+ :param path: Optional directory where the file will be saved
617
+ :return: Full path of the generated report or None on error
618
+ """
619
+ try:
620
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M")
621
+ base_filename = f"{file_name}_pdca_report_{timestamp}.json"
622
+
623
+ if path:
624
+ os.makedirs(path, exist_ok=True)
625
+ output_filename = os.path.join(path, base_filename)
626
+ else:
627
+ output_filename = base_filename
628
+
629
+ # Calculate aggregate metrics
630
+ total_risk = sum(m["risk_score"] for m in pdca_metrics)
631
+ avg_risk = total_risk / len(pdca_metrics) if pdca_metrics else 0
632
+
633
+ high_risk_accounts = [m for m in pdca_metrics if m["risk_score"] > 25]
634
+ medium_risk_accounts = [m for m in pdca_metrics if 10 < m["risk_score"] <= 25]
635
+ low_risk_accounts = [m for m in pdca_metrics if m["risk_score"] <= 10]
636
+
637
+ total_untagged = sum(m["untagged_count"] for m in pdca_metrics)
638
+ total_stopped = sum(m["stopped_count"] for m in pdca_metrics)
639
+ total_unused_volumes = sum(m["unused_volumes_count"] for m in pdca_metrics)
640
+ total_unused_eips = sum(m["unused_eips_count"] for m in pdca_metrics)
641
+ total_budget_overruns = sum(m["budget_overruns"] for m in pdca_metrics)
642
+
643
+ # Generate improvement recommendations
644
+ recommendations = []
645
+
646
+ # PLAN phase recommendations
647
+ if total_untagged > 50:
648
+ recommendations.append(
649
+ {
650
+ "phase": "PLAN",
651
+ "priority": "HIGH",
652
+ "category": "Compliance",
653
+ "issue": f"Found {total_untagged} untagged resources across all accounts",
654
+ "action": "Implement mandatory tagging strategy using AWS Config rules",
655
+ "expected_outcome": "100% resource compliance within 30 days",
656
+ "owner": "Cloud Governance Team",
657
+ }
658
+ )
659
+
660
+ if total_unused_eips > 5:
661
+ recommendations.append(
662
+ {
663
+ "phase": "PLAN",
664
+ "priority": "MEDIUM",
665
+ "category": "Cost Optimization",
666
+ "issue": f"Found {total_unused_eips} unused Elastic IPs",
667
+ "action": "Schedule monthly EIP cleanup automation",
668
+ "expected_outcome": f"Save ~${total_unused_eips * 3.65:.2f}/month",
669
+ "owner": "FinOps Team",
670
+ }
671
+ )
672
+
673
+ # DO phase recommendations
674
+ if high_risk_accounts:
675
+ recommendations.append(
676
+ {
677
+ "phase": "DO",
678
+ "priority": "CRITICAL",
679
+ "category": "Risk Management",
680
+ "issue": f"{len(high_risk_accounts)} accounts have critical risk scores",
681
+ "action": "Execute immediate remediation on high-risk accounts",
682
+ "expected_outcome": "Reduce risk scores by 70% within 2 weeks",
683
+ "owner": "Security Team",
684
+ }
685
+ )
686
+
687
+ # CHECK phase recommendations
688
+ if avg_risk > 15:
689
+ recommendations.append(
690
+ {
691
+ "phase": "CHECK",
692
+ "priority": "HIGH",
693
+ "category": "Monitoring",
694
+ "issue": f"Average risk score ({avg_risk:.1f}) exceeds threshold",
695
+ "action": "Implement automated risk scoring dashboard",
696
+ "expected_outcome": "Real-time risk visibility and alerting",
697
+ "owner": "DevOps Team",
698
+ }
699
+ )
700
+
701
+ # ACT phase recommendations
702
+ recommendations.append(
703
+ {
704
+ "phase": "ACT",
705
+ "priority": "MEDIUM",
706
+ "category": "Process Improvement",
707
+ "issue": "Need continuous improvement framework",
708
+ "action": "Establish monthly PDCA review cycles",
709
+ "expected_outcome": "25% reduction in average risk score per quarter",
710
+ "owner": "Cloud Center of Excellence",
711
+ }
712
+ )
713
+
714
+ # Create comprehensive PDCA report
715
+ pdca_report = {
716
+ "report_metadata": {
717
+ "generated_at": datetime.now().isoformat(),
718
+ "report_type": "PDCA Continuous Improvement Analysis",
719
+ "accounts_analyzed": len(pdca_metrics),
720
+ "framework_version": "v1.0",
721
+ },
722
+ "executive_summary": {
723
+ "overall_risk_score": total_risk,
724
+ "average_risk_score": round(avg_risk, 2),
725
+ "risk_distribution": {
726
+ "critical_accounts": len(high_risk_accounts),
727
+ "high_risk_accounts": len(medium_risk_accounts),
728
+ "low_risk_accounts": len(low_risk_accounts),
729
+ },
730
+ "key_findings": {
731
+ "untagged_resources": total_untagged,
732
+ "stopped_instances": total_stopped,
733
+ "unused_volumes": total_unused_volumes,
734
+ "unused_elastic_ips": total_unused_eips,
735
+ "budget_overruns": total_budget_overruns,
736
+ },
737
+ },
738
+ "pdca_analysis": {
739
+ "plan_phase": {
740
+ "description": "Strategic planning based on current state analysis",
741
+ "metrics_collected": len(pdca_metrics),
742
+ "baseline_established": True,
743
+ },
744
+ "do_phase": {
745
+ "description": "Implementation of audit data collection",
746
+ "data_sources": ["EC2", "RDS", "Lambda", "ELBv2", "Budgets"],
747
+ "regions_scanned": "All accessible regions",
748
+ },
749
+ "check_phase": {
750
+ "description": "Analysis of collected audit data",
751
+ "risk_assessment_completed": True,
752
+ "trends_identified": True,
753
+ },
754
+ "act_phase": {
755
+ "description": "Actionable recommendations for improvement",
756
+ "recommendations_generated": len(recommendations),
757
+ "prioritization_completed": True,
758
+ },
759
+ },
760
+ "detailed_metrics": pdca_metrics,
761
+ "improvement_recommendations": recommendations,
762
+ "next_steps": {
763
+ "immediate_actions": [
764
+ "Review high-risk accounts within 48 hours",
765
+ "Implement automated tagging for untagged resources",
766
+ "Schedule EIP cleanup automation",
767
+ ],
768
+ "medium_term_goals": [
769
+ "Establish monthly PDCA review cycle",
770
+ "Implement risk scoring dashboard",
771
+ "Create automated remediation workflows",
772
+ ],
773
+ "long_term_objectives": [
774
+ "Achieve average risk score below 5",
775
+ "Maintain 100% resource compliance",
776
+ "Reduce cloud waste by 25%",
777
+ ],
778
+ },
779
+ }
780
+
781
+ # Export to JSON
782
+ with open(output_filename, "w", encoding="utf-8") as jsonfile:
783
+ json.dump(pdca_report, jsonfile, indent=4, default=str)
784
+
785
+ return os.path.abspath(output_filename)
786
+
787
+ except Exception as e:
788
+ console.print(f"[bold red]Error generating PDCA improvement report: {str(e)}[/]")
789
+ return None