runbooks 0.7.5__py3-none-any.whl → 0.7.7__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 (70) hide show
  1. runbooks/__init__.py +1 -1
  2. runbooks/base.py +5 -1
  3. runbooks/cfat/__init__.py +2 -2
  4. runbooks/cfat/assessment/compliance.py +847 -0
  5. runbooks/finops/__init__.py +1 -1
  6. runbooks/finops/cli.py +63 -1
  7. runbooks/finops/dashboard_runner.py +632 -161
  8. runbooks/finops/helpers.py +492 -61
  9. runbooks/finops/optimizer.py +822 -0
  10. runbooks/inventory/collectors/aws_comprehensive.py +435 -0
  11. runbooks/inventory/discovery.md +1 -1
  12. runbooks/main.py +158 -12
  13. runbooks/operate/__init__.py +2 -2
  14. runbooks/remediation/__init__.py +2 -2
  15. runbooks/remediation/acm_remediation.py +1 -1
  16. runbooks/remediation/base.py +1 -1
  17. runbooks/remediation/cloudtrail_remediation.py +1 -1
  18. runbooks/remediation/cognito_remediation.py +1 -1
  19. runbooks/remediation/dynamodb_remediation.py +1 -1
  20. runbooks/remediation/ec2_remediation.py +1 -1
  21. runbooks/remediation/ec2_unattached_ebs_volumes.py +1 -1
  22. runbooks/remediation/kms_enable_key_rotation.py +1 -1
  23. runbooks/remediation/kms_remediation.py +1 -1
  24. runbooks/remediation/lambda_remediation.py +1 -1
  25. runbooks/remediation/multi_account.py +1 -1
  26. runbooks/remediation/rds_remediation.py +1 -1
  27. runbooks/remediation/requirements.txt +2 -2
  28. runbooks/remediation/s3_block_public_access.py +1 -1
  29. runbooks/remediation/s3_enable_access_logging.py +1 -1
  30. runbooks/remediation/s3_encryption.py +1 -1
  31. runbooks/remediation/s3_remediation.py +1 -1
  32. runbooks/security/__init__.py +1 -1
  33. {runbooks-0.7.5.dist-info → runbooks-0.7.7.dist-info}/METADATA +4 -2
  34. {runbooks-0.7.5.dist-info → runbooks-0.7.7.dist-info}/RECORD +50 -67
  35. {runbooks-0.7.5.dist-info → runbooks-0.7.7.dist-info}/top_level.txt +0 -1
  36. jupyter-agent/.env +0 -2
  37. jupyter-agent/.env.template +0 -2
  38. jupyter-agent/.gitattributes +0 -35
  39. jupyter-agent/.gradio/certificate.pem +0 -31
  40. jupyter-agent/README.md +0 -16
  41. jupyter-agent/__main__.log +0 -8
  42. jupyter-agent/app.py +0 -256
  43. jupyter-agent/cloudops-agent.png +0 -0
  44. jupyter-agent/ds-system-prompt.txt +0 -154
  45. jupyter-agent/jupyter-agent.png +0 -0
  46. jupyter-agent/llama3_template.jinja +0 -123
  47. jupyter-agent/requirements.txt +0 -9
  48. jupyter-agent/tmp/4ojbs8a02ir/jupyter-agent.ipynb +0 -68
  49. jupyter-agent/tmp/cm5iasgpm3p/jupyter-agent.ipynb +0 -91
  50. jupyter-agent/tmp/crqbsseag5/jupyter-agent.ipynb +0 -91
  51. jupyter-agent/tmp/hohanq1u097/jupyter-agent.ipynb +0 -57
  52. jupyter-agent/tmp/jns1sam29wm/jupyter-agent.ipynb +0 -53
  53. jupyter-agent/tmp/jupyter-agent.ipynb +0 -27
  54. jupyter-agent/utils.py +0 -409
  55. runbooks/inventory/aws_organization.png +0 -0
  56. /runbooks/inventory/{tests → Tests}/common_test_data.py +0 -0
  57. /runbooks/inventory/{tests → Tests}/common_test_functions.py +0 -0
  58. /runbooks/inventory/{tests → Tests}/script_test_data.py +0 -0
  59. /runbooks/inventory/{tests → Tests}/setup.py +0 -0
  60. /runbooks/inventory/{tests → Tests}/src.py +0 -0
  61. /runbooks/inventory/{tests/test_inventory_modules.py → Tests/test_Inventory_Modules.py} +0 -0
  62. /runbooks/inventory/{tests → Tests}/test_cfn_describe_stacks.py +0 -0
  63. /runbooks/inventory/{tests → Tests}/test_ec2_describe_instances.py +0 -0
  64. /runbooks/inventory/{tests → Tests}/test_lambda_list_functions.py +0 -0
  65. /runbooks/inventory/{tests → Tests}/test_moto_integration_example.py +0 -0
  66. /runbooks/inventory/{tests → Tests}/test_org_list_accounts.py +0 -0
  67. /runbooks/inventory/{Inventory_Modules.py → inventory_modules.py} +0 -0
  68. {runbooks-0.7.5.dist-info → runbooks-0.7.7.dist-info}/WHEEL +0 -0
  69. {runbooks-0.7.5.dist-info → runbooks-0.7.7.dist-info}/entry_points.txt +0 -0
  70. {runbooks-0.7.5.dist-info → runbooks-0.7.7.dist-info}/licenses/LICENSE +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 landscape, letter, A4
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,
@@ -69,14 +71,26 @@ def export_audit_report_to_pdf(
69
71
  "Profile",
70
72
  "Account ID",
71
73
  "Untagged Resources",
72
- "Stopped EC2 Instances",
74
+ "Stopped EC2 Instances",
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("🎯 AWS FinOps Dashboard - PDCA Enhanced 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,16 +267,18 @@ 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 = [
167
274
  "profile",
168
275
  "account_id",
169
- "untagged_resources",
276
+ "untagged_resources",
170
277
  "stopped_instances",
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,218 @@ 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 = ((total_current_cost - total_previous_cost) / total_previous_cost * 100) if total_previous_cost > 0 else 0
380
+
381
+ elements.append(Paragraph("🏢 AWS FinOps Dashboard - Enterprise Cost Report", title_style))
382
+
383
+ # Executive summary
384
+ summary_style = ParagraphStyle(
385
+ name="Summary",
386
+ parent=styles["Normal"],
387
+ fontSize=10,
388
+ spaceAfter=8,
389
+ textColor=colors.darkgreen
390
+ )
391
+
392
+ summary_text = (
393
+ f"📊 Executive Summary: {total_accounts} accounts analyzed | "
394
+ f"Total Current Cost: ${total_current_cost:,.2f} | "
395
+ f"Cost Change: {cost_change:+.1f}% | "
396
+ f"Report Period: {current_period_dates}"
397
+ )
398
+ elements.append(Paragraph(summary_text, summary_style))
399
+ elements.append(Spacer(1, 12))
400
+
401
+ # Prepare table data with optimization
402
+ previous_period_header = f"Cost Period\n({previous_period_dates})"
403
+ current_period_header = f"Cost Period\n({current_period_dates})"
251
404
 
252
405
  headers = [
253
- "CLI Profile",
254
- "AWS Account ID",
406
+ "Profile",
407
+ "Account ID",
255
408
  previous_period_header,
256
409
  current_period_header,
257
- "Cost By Service",
410
+ "Top Services",
258
411
  "Budget Status",
259
- "EC2 Instances",
412
+ "EC2 Summary",
260
413
  ]
261
- table_data = [headers]
414
+
415
+ raw_table_data = [headers]
262
416
 
263
417
  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
- )
269
-
270
- table_data.append(
271
- [
272
- row["profile"],
273
- 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",
279
- ]
280
- )
281
-
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
- )
296
- )
297
-
298
- elements.append(Paragraph("AWS FinOps Dashboard (Cost Report)", styles["Title"]))
418
+ # Optimize service costs for PDF display
419
+ if row["service_costs"]:
420
+ # Show only top 10 services to prevent width issues
421
+ top_services = row["service_costs"][:10]
422
+ services_data = "\n".join([f"{service}: ${cost:.2f}" for service, cost in top_services])
423
+ if len(row["service_costs"]) > 10:
424
+ remaining_count = len(row["service_costs"]) - 10
425
+ services_data += f"\n... and {remaining_count} more services"
426
+ else:
427
+ services_data = "No costs"
428
+
429
+ # Optimize budget display
430
+ budget_lines = row["budget_info"][:3] if row["budget_info"] else ["No budgets"]
431
+ budgets_data = "\n".join(budget_lines)
432
+ if len(row["budget_info"]) > 3:
433
+ budgets_data += f"\n... +{len(row['budget_info']) - 3} more"
434
+
435
+ # Optimize EC2 summary
436
+ ec2_items = [(state, count) for state, count in row["ec2_summary"].items() if count > 0]
437
+ if ec2_items:
438
+ ec2_data_summary = "\n".join([f"{state}: {count}" for state, count in ec2_items[:5]])
439
+ if len(ec2_items) > 5:
440
+ ec2_data_summary += f"\n... +{len(ec2_items) - 5} more"
441
+ else:
442
+ ec2_data_summary = "No instances"
443
+
444
+ # Format cost change indicator
445
+ cost_change_text = ""
446
+ if row.get("percent_change_in_total_cost") is not None:
447
+ change = row["percent_change_in_total_cost"]
448
+ if change > 0:
449
+ cost_change_text = f"\n↑ +{change:.1f}%"
450
+ elif change < 0:
451
+ cost_change_text = f"\n↓ {change:.1f}%"
452
+ else:
453
+ cost_change_text = "\n→ 0%"
454
+
455
+ raw_table_data.append([
456
+ row["profile"],
457
+ row["account_id"],
458
+ f"${row['last_month']:,.2f}",
459
+ f"${row['current_month']:,.2f}{cost_change_text}",
460
+ services_data,
461
+ budgets_data,
462
+ ec2_data_summary,
463
+ ])
464
+
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
+ # Header styling
474
+ ("BACKGROUND", (0, 0), (-1, 0), colors.navy),
475
+ ("TEXTCOLOR", (0, 0), (-1, 0), colors.whitesmoke),
476
+ ("FONTNAME", (0, 0), (-1, 0), "Helvetica-Bold"),
477
+ ("FONTSIZE", (0, 0), (-1, 0), 7),
478
+
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
+
485
+ # Grid and background
486
+ ("GRID", (0, 0), (-1, -1), 0.5, colors.darkgrey),
487
+ ("BACKGROUND", (0, 1), (-1, -1), colors.beige),
488
+ ("ROWBACKGROUNDS", (0, 1), (-1, -1), [colors.white, colors.lightgrey]),
489
+
490
+ # Column-specific styling
491
+ ("ALIGN", (2, 0), (3, -1), "RIGHT"), # Cost columns right-aligned
492
+ ("FONTNAME", (2, 1), (3, -1), "Helvetica-Bold"), # Bold cost values
493
+ ])
494
+
495
+ # Generate tables with pagination
496
+ for page_idx, table_data_chunk in enumerate(paginated_tables):
497
+ if page_idx > 0:
498
+ elements.append(PageBreak())
499
+ # Add page header for continuation pages
500
+ page_header = Paragraph(
501
+ f"AWS FinOps Dashboard - Page {page_idx + 1} of {len(paginated_tables)}",
502
+ ParagraphStyle(
503
+ name="PageHeader",
504
+ parent=styles["Heading2"],
505
+ fontSize=12,
506
+ textColor=colors.darkblue
507
+ )
508
+ )
509
+ elements.append(page_header)
510
+ elements.append(Spacer(1, 8))
511
+
512
+ # Create table with dynamic column widths
513
+ available_width = landscape(A4)[0] - 1*inch # Account for margins
514
+ col_widths = [
515
+ available_width * 0.12, # Profile (12%)
516
+ available_width * 0.15, # Account ID (15%)
517
+ available_width * 0.12, # Previous Cost (12%)
518
+ available_width * 0.12, # Current Cost (12%)
519
+ available_width * 0.25, # Services (25%)
520
+ available_width * 0.12, # Budget (12%)
521
+ available_width * 0.12, # EC2 (12%)
522
+ ]
523
+
524
+ table = Table(table_data_chunk, repeatRows=1, colWidths=col_widths)
525
+ table.setStyle(table_style)
526
+ elements.append(table)
527
+
528
+ # Enhanced footer with metadata
299
529
  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))
530
+
531
+ footer_style = ParagraphStyle(
532
+ name="EnhancedFooter",
533
+ parent=styles["Normal"],
534
+ fontSize=8,
535
+ textColor=colors.grey,
536
+ alignment=1
537
+ )
538
+
539
+ current_time_str = datetime.now().strftime("%Y-%m-%d %H:%M:%S UTC")
540
+ footer_text = (
541
+ f"🚀 Generated by CloudOps-Runbooks FinOps Dashboard v0.7.3 | "
542
+ f"Report Generated: {current_time_str} | "
543
+ f"Accounts Analyzed: {total_accounts} | "
544
+ f"© 2025 CloudOps Enterprise"
545
+ )
546
+ elements.append(Paragraph(footer_text, footer_style))
305
547
 
548
+ # Build PDF with error handling
306
549
  doc.build(elements)
307
- return os.path.abspath(output_filename)
550
+
551
+ # Verify file creation
552
+ if os.path.exists(output_filename):
553
+ file_size = os.path.getsize(output_filename)
554
+ console.print(f"[bright_green]✅ PDF generated successfully: {os.path.abspath(output_filename)} ({file_size:,} bytes)[/]")
555
+ return os.path.abspath(output_filename)
556
+ else:
557
+ console.print("[bold red]❌ PDF file was not created[/]")
558
+ return None
559
+
308
560
  except Exception as e:
309
- console.print(f"[bold red]Error exporting to PDF: {str(e)}[/]")
561
+ console.print(f"[bold red]Error exporting to PDF: {str(e)}[/]")
562
+ # Print more detailed error information for debugging
563
+ import traceback
564
+ console.print(f"[red]Detailed error trace: {traceback.format_exc()}[/]")
310
565
  return None
311
566
 
312
567
 
@@ -353,3 +608,179 @@ def load_config_file(file_path: str) -> Optional[Dict[str, Any]]:
353
608
  except Exception as e:
354
609
  console.print(f"[bold red]Error loading configuration file {file_path}: {e}[/]")
355
610
  return None
611
+
612
+
613
+ def generate_pdca_improvement_report(
614
+ pdca_metrics: List[Dict[str, Any]],
615
+ file_name: str = "pdca_improvement",
616
+ path: Optional[str] = None
617
+ ) -> Optional[str]:
618
+ """
619
+ Generate PDCA (Plan-Do-Check-Act) continuous improvement report.
620
+
621
+ :param pdca_metrics: List of PDCA metrics for each profile
622
+ :param file_name: The base name of the output file
623
+ :param path: Optional directory where the file will be saved
624
+ :return: Full path of the generated report or None on error
625
+ """
626
+ try:
627
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M")
628
+ base_filename = f"{file_name}_pdca_report_{timestamp}.json"
629
+
630
+ if path:
631
+ os.makedirs(path, exist_ok=True)
632
+ output_filename = os.path.join(path, base_filename)
633
+ else:
634
+ output_filename = base_filename
635
+
636
+ # Calculate aggregate metrics
637
+ total_risk = sum(m["risk_score"] for m in pdca_metrics)
638
+ avg_risk = total_risk / len(pdca_metrics) if pdca_metrics else 0
639
+
640
+ high_risk_accounts = [m for m in pdca_metrics if m["risk_score"] > 25]
641
+ medium_risk_accounts = [m for m in pdca_metrics if 10 < m["risk_score"] <= 25]
642
+ low_risk_accounts = [m for m in pdca_metrics if m["risk_score"] <= 10]
643
+
644
+ total_untagged = sum(m["untagged_count"] for m in pdca_metrics)
645
+ total_stopped = sum(m["stopped_count"] for m in pdca_metrics)
646
+ total_unused_volumes = sum(m["unused_volumes_count"] for m in pdca_metrics)
647
+ total_unused_eips = sum(m["unused_eips_count"] for m in pdca_metrics)
648
+ total_budget_overruns = sum(m["budget_overruns"] for m in pdca_metrics)
649
+
650
+ # Generate improvement recommendations
651
+ recommendations = []
652
+
653
+ # PLAN phase recommendations
654
+ if total_untagged > 50:
655
+ recommendations.append({
656
+ "phase": "PLAN",
657
+ "priority": "HIGH",
658
+ "category": "Compliance",
659
+ "issue": f"Found {total_untagged} untagged resources across all accounts",
660
+ "action": "Implement mandatory tagging strategy using AWS Config rules",
661
+ "expected_outcome": "100% resource compliance within 30 days",
662
+ "owner": "Cloud Governance Team"
663
+ })
664
+
665
+ if total_unused_eips > 5:
666
+ recommendations.append({
667
+ "phase": "PLAN",
668
+ "priority": "MEDIUM",
669
+ "category": "Cost Optimization",
670
+ "issue": f"Found {total_unused_eips} unused Elastic IPs",
671
+ "action": "Schedule monthly EIP cleanup automation",
672
+ "expected_outcome": f"Save ~${total_unused_eips * 3.65:.2f}/month",
673
+ "owner": "FinOps Team"
674
+ })
675
+
676
+ # DO phase recommendations
677
+ if high_risk_accounts:
678
+ recommendations.append({
679
+ "phase": "DO",
680
+ "priority": "CRITICAL",
681
+ "category": "Risk Management",
682
+ "issue": f"{len(high_risk_accounts)} accounts have critical risk scores",
683
+ "action": "Execute immediate remediation on high-risk accounts",
684
+ "expected_outcome": "Reduce risk scores by 70% within 2 weeks",
685
+ "owner": "Security Team"
686
+ })
687
+
688
+ # CHECK phase recommendations
689
+ if avg_risk > 15:
690
+ recommendations.append({
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
+ # ACT phase recommendations
701
+ recommendations.append({
702
+ "phase": "ACT",
703
+ "priority": "MEDIUM",
704
+ "category": "Process Improvement",
705
+ "issue": "Need continuous improvement framework",
706
+ "action": "Establish monthly PDCA review cycles",
707
+ "expected_outcome": "25% reduction in average risk score per quarter",
708
+ "owner": "Cloud Center of Excellence"
709
+ })
710
+
711
+ # Create comprehensive PDCA report
712
+ pdca_report = {
713
+ "report_metadata": {
714
+ "generated_at": datetime.now().isoformat(),
715
+ "report_type": "PDCA Continuous Improvement Analysis",
716
+ "accounts_analyzed": len(pdca_metrics),
717
+ "framework_version": "v1.0"
718
+ },
719
+ "executive_summary": {
720
+ "overall_risk_score": total_risk,
721
+ "average_risk_score": round(avg_risk, 2),
722
+ "risk_distribution": {
723
+ "critical_accounts": len(high_risk_accounts),
724
+ "high_risk_accounts": len(medium_risk_accounts),
725
+ "low_risk_accounts": len(low_risk_accounts)
726
+ },
727
+ "key_findings": {
728
+ "untagged_resources": total_untagged,
729
+ "stopped_instances": total_stopped,
730
+ "unused_volumes": total_unused_volumes,
731
+ "unused_elastic_ips": total_unused_eips,
732
+ "budget_overruns": total_budget_overruns
733
+ }
734
+ },
735
+ "pdca_analysis": {
736
+ "plan_phase": {
737
+ "description": "Strategic planning based on current state analysis",
738
+ "metrics_collected": len(pdca_metrics),
739
+ "baseline_established": True
740
+ },
741
+ "do_phase": {
742
+ "description": "Implementation of audit data collection",
743
+ "data_sources": ["EC2", "RDS", "Lambda", "ELBv2", "Budgets"],
744
+ "regions_scanned": "All accessible regions"
745
+ },
746
+ "check_phase": {
747
+ "description": "Analysis of collected audit data",
748
+ "risk_assessment_completed": True,
749
+ "trends_identified": True
750
+ },
751
+ "act_phase": {
752
+ "description": "Actionable recommendations for improvement",
753
+ "recommendations_generated": len(recommendations),
754
+ "prioritization_completed": True
755
+ }
756
+ },
757
+ "detailed_metrics": pdca_metrics,
758
+ "improvement_recommendations": recommendations,
759
+ "next_steps": {
760
+ "immediate_actions": [
761
+ "Review high-risk accounts within 48 hours",
762
+ "Implement automated tagging for untagged resources",
763
+ "Schedule EIP cleanup automation"
764
+ ],
765
+ "medium_term_goals": [
766
+ "Establish monthly PDCA review cycle",
767
+ "Implement risk scoring dashboard",
768
+ "Create automated remediation workflows"
769
+ ],
770
+ "long_term_objectives": [
771
+ "Achieve average risk score below 5",
772
+ "Maintain 100% resource compliance",
773
+ "Reduce cloud waste by 25%"
774
+ ]
775
+ }
776
+ }
777
+
778
+ # Export to JSON
779
+ with open(output_filename, "w", encoding="utf-8") as jsonfile:
780
+ json.dump(pdca_report, jsonfile, indent=4, default=str)
781
+
782
+ return os.path.abspath(output_filename)
783
+
784
+ except Exception as e:
785
+ console.print(f"[bold red]Error generating PDCA improvement report: {str(e)}[/]")
786
+ return None