runbooks 0.2.5__py3-none-any.whl → 0.7.0__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 (249) hide show
  1. conftest.py +26 -0
  2. jupyter-agent/.env +2 -0
  3. jupyter-agent/.env.template +2 -0
  4. jupyter-agent/.gitattributes +35 -0
  5. jupyter-agent/.gradio/certificate.pem +31 -0
  6. jupyter-agent/README.md +16 -0
  7. jupyter-agent/__main__.log +8 -0
  8. jupyter-agent/app.py +256 -0
  9. jupyter-agent/cloudops-agent.png +0 -0
  10. jupyter-agent/ds-system-prompt.txt +154 -0
  11. jupyter-agent/jupyter-agent.png +0 -0
  12. jupyter-agent/llama3_template.jinja +123 -0
  13. jupyter-agent/requirements.txt +9 -0
  14. jupyter-agent/tmp/4ojbs8a02ir/jupyter-agent.ipynb +68 -0
  15. jupyter-agent/tmp/cm5iasgpm3p/jupyter-agent.ipynb +91 -0
  16. jupyter-agent/tmp/crqbsseag5/jupyter-agent.ipynb +91 -0
  17. jupyter-agent/tmp/hohanq1u097/jupyter-agent.ipynb +57 -0
  18. jupyter-agent/tmp/jns1sam29wm/jupyter-agent.ipynb +53 -0
  19. jupyter-agent/tmp/jupyter-agent.ipynb +27 -0
  20. jupyter-agent/utils.py +409 -0
  21. runbooks/__init__.py +71 -3
  22. runbooks/__main__.py +13 -0
  23. runbooks/aws/ec2_describe_instances.py +1 -1
  24. runbooks/aws/ec2_run_instances.py +8 -2
  25. runbooks/aws/ec2_start_stop_instances.py +17 -4
  26. runbooks/aws/ec2_unused_volumes.py +5 -1
  27. runbooks/aws/s3_create_bucket.py +4 -2
  28. runbooks/aws/s3_list_objects.py +6 -1
  29. runbooks/aws/tagging_lambda_handler.py +13 -2
  30. runbooks/aws/tags.json +12 -0
  31. runbooks/base.py +353 -0
  32. runbooks/cfat/README.md +49 -0
  33. runbooks/cfat/__init__.py +74 -0
  34. runbooks/cfat/app.ts +644 -0
  35. runbooks/cfat/assessment/__init__.py +40 -0
  36. runbooks/cfat/assessment/asana-import.csv +39 -0
  37. runbooks/cfat/assessment/cfat-checks.csv +31 -0
  38. runbooks/cfat/assessment/cfat.txt +520 -0
  39. runbooks/cfat/assessment/collectors.py +200 -0
  40. runbooks/cfat/assessment/jira-import.csv +39 -0
  41. runbooks/cfat/assessment/runner.py +387 -0
  42. runbooks/cfat/assessment/validators.py +290 -0
  43. runbooks/cfat/cli.py +103 -0
  44. runbooks/cfat/docs/asana-import.csv +24 -0
  45. runbooks/cfat/docs/cfat-checks.csv +31 -0
  46. runbooks/cfat/docs/cfat.txt +335 -0
  47. runbooks/cfat/docs/checks-output.png +0 -0
  48. runbooks/cfat/docs/cloudshell-console-run.png +0 -0
  49. runbooks/cfat/docs/cloudshell-download.png +0 -0
  50. runbooks/cfat/docs/cloudshell-output.png +0 -0
  51. runbooks/cfat/docs/downloadfile.png +0 -0
  52. runbooks/cfat/docs/jira-import.csv +24 -0
  53. runbooks/cfat/docs/open-cloudshell.png +0 -0
  54. runbooks/cfat/docs/report-header.png +0 -0
  55. runbooks/cfat/models.py +1026 -0
  56. runbooks/cfat/package-lock.json +5116 -0
  57. runbooks/cfat/package.json +38 -0
  58. runbooks/cfat/report.py +496 -0
  59. runbooks/cfat/reporting/__init__.py +46 -0
  60. runbooks/cfat/reporting/exporters.py +337 -0
  61. runbooks/cfat/reporting/formatters.py +496 -0
  62. runbooks/cfat/reporting/templates.py +135 -0
  63. runbooks/cfat/run-assessment.sh +23 -0
  64. runbooks/cfat/runner.py +69 -0
  65. runbooks/cfat/src/actions/check-cloudtrail-existence.ts +43 -0
  66. runbooks/cfat/src/actions/check-config-existence.ts +37 -0
  67. runbooks/cfat/src/actions/check-control-tower.ts +37 -0
  68. runbooks/cfat/src/actions/check-ec2-existence.ts +46 -0
  69. runbooks/cfat/src/actions/check-iam-users.ts +50 -0
  70. runbooks/cfat/src/actions/check-legacy-cur.ts +30 -0
  71. runbooks/cfat/src/actions/check-org-cloudformation.ts +30 -0
  72. runbooks/cfat/src/actions/check-vpc-existence.ts +43 -0
  73. runbooks/cfat/src/actions/create-asanaimport.ts +14 -0
  74. runbooks/cfat/src/actions/create-backlog.ts +372 -0
  75. runbooks/cfat/src/actions/create-jiraimport.ts +15 -0
  76. runbooks/cfat/src/actions/create-report.ts +616 -0
  77. runbooks/cfat/src/actions/define-account-type.ts +51 -0
  78. runbooks/cfat/src/actions/get-enabled-org-policy-types.ts +40 -0
  79. runbooks/cfat/src/actions/get-enabled-org-services.ts +26 -0
  80. runbooks/cfat/src/actions/get-idc-info.ts +34 -0
  81. runbooks/cfat/src/actions/get-org-da-accounts.ts +34 -0
  82. runbooks/cfat/src/actions/get-org-details.ts +35 -0
  83. runbooks/cfat/src/actions/get-org-member-accounts.ts +44 -0
  84. runbooks/cfat/src/actions/get-org-ous.ts +35 -0
  85. runbooks/cfat/src/actions/get-regions.ts +22 -0
  86. runbooks/cfat/src/actions/zip-assessment.ts +27 -0
  87. runbooks/cfat/src/types/index.d.ts +147 -0
  88. runbooks/cfat/tests/__init__.py +141 -0
  89. runbooks/cfat/tests/test_cli.py +340 -0
  90. runbooks/cfat/tests/test_integration.py +290 -0
  91. runbooks/cfat/tests/test_models.py +505 -0
  92. runbooks/cfat/tests/test_reporting.py +354 -0
  93. runbooks/cfat/tsconfig.json +16 -0
  94. runbooks/cfat/webpack.config.cjs +27 -0
  95. runbooks/config.py +260 -0
  96. runbooks/finops/README.md +337 -0
  97. runbooks/finops/__init__.py +86 -0
  98. runbooks/finops/aws_client.py +245 -0
  99. runbooks/finops/cli.py +151 -0
  100. runbooks/finops/cost_processor.py +410 -0
  101. runbooks/finops/dashboard_runner.py +448 -0
  102. runbooks/finops/helpers.py +355 -0
  103. runbooks/finops/main.py +14 -0
  104. runbooks/finops/profile_processor.py +174 -0
  105. runbooks/finops/types.py +66 -0
  106. runbooks/finops/visualisations.py +80 -0
  107. runbooks/inventory/.gitignore +354 -0
  108. runbooks/inventory/ArgumentsClass.py +261 -0
  109. runbooks/inventory/FAILED_SCRIPTS_TROUBLESHOOTING.md +619 -0
  110. runbooks/inventory/Inventory_Modules.py +6130 -0
  111. runbooks/inventory/LandingZone/delete_lz.py +1075 -0
  112. runbooks/inventory/PASSED_SCRIPTS_GUIDE.md +738 -0
  113. runbooks/inventory/README.md +1320 -0
  114. runbooks/inventory/__init__.py +62 -0
  115. runbooks/inventory/account_class.py +532 -0
  116. runbooks/inventory/all_my_instances_wrapper.py +123 -0
  117. runbooks/inventory/aws_decorators.py +201 -0
  118. runbooks/inventory/aws_organization.png +0 -0
  119. runbooks/inventory/cfn_move_stack_instances.py +1526 -0
  120. runbooks/inventory/check_cloudtrail_compliance.py +614 -0
  121. runbooks/inventory/check_controltower_readiness.py +1107 -0
  122. runbooks/inventory/check_landingzone_readiness.py +711 -0
  123. runbooks/inventory/cloudtrail.md +727 -0
  124. runbooks/inventory/collectors/__init__.py +20 -0
  125. runbooks/inventory/collectors/aws_compute.py +518 -0
  126. runbooks/inventory/collectors/aws_networking.py +275 -0
  127. runbooks/inventory/collectors/base.py +222 -0
  128. runbooks/inventory/core/__init__.py +19 -0
  129. runbooks/inventory/core/collector.py +303 -0
  130. runbooks/inventory/core/formatter.py +296 -0
  131. runbooks/inventory/delete_s3_buckets_objects.py +169 -0
  132. runbooks/inventory/discovery.md +81 -0
  133. runbooks/inventory/draw_org_structure.py +748 -0
  134. runbooks/inventory/ec2_vpc_utils.py +341 -0
  135. runbooks/inventory/find_cfn_drift_detection.py +272 -0
  136. runbooks/inventory/find_cfn_orphaned_stacks.py +719 -0
  137. runbooks/inventory/find_cfn_stackset_drift.py +733 -0
  138. runbooks/inventory/find_ec2_security_groups.py +669 -0
  139. runbooks/inventory/find_landingzone_versions.py +201 -0
  140. runbooks/inventory/find_vpc_flow_logs.py +1221 -0
  141. runbooks/inventory/inventory.sh +659 -0
  142. runbooks/inventory/list_cfn_stacks.py +558 -0
  143. runbooks/inventory/list_cfn_stackset_operation_results.py +252 -0
  144. runbooks/inventory/list_cfn_stackset_operations.py +734 -0
  145. runbooks/inventory/list_cfn_stacksets.py +453 -0
  146. runbooks/inventory/list_config_recorders_delivery_channels.py +681 -0
  147. runbooks/inventory/list_ds_directories.py +354 -0
  148. runbooks/inventory/list_ec2_availability_zones.py +286 -0
  149. runbooks/inventory/list_ec2_ebs_volumes.py +244 -0
  150. runbooks/inventory/list_ec2_instances.py +425 -0
  151. runbooks/inventory/list_ecs_clusters_and_tasks.py +562 -0
  152. runbooks/inventory/list_elbs_load_balancers.py +411 -0
  153. runbooks/inventory/list_enis_network_interfaces.py +526 -0
  154. runbooks/inventory/list_guardduty_detectors.py +568 -0
  155. runbooks/inventory/list_iam_policies.py +404 -0
  156. runbooks/inventory/list_iam_roles.py +518 -0
  157. runbooks/inventory/list_iam_saml_providers.py +359 -0
  158. runbooks/inventory/list_lambda_functions.py +882 -0
  159. runbooks/inventory/list_org_accounts.py +446 -0
  160. runbooks/inventory/list_org_accounts_users.py +354 -0
  161. runbooks/inventory/list_rds_db_instances.py +406 -0
  162. runbooks/inventory/list_route53_hosted_zones.py +318 -0
  163. runbooks/inventory/list_servicecatalog_provisioned_products.py +575 -0
  164. runbooks/inventory/list_sns_topics.py +360 -0
  165. runbooks/inventory/list_ssm_parameters.py +402 -0
  166. runbooks/inventory/list_vpc_subnets.py +433 -0
  167. runbooks/inventory/list_vpcs.py +422 -0
  168. runbooks/inventory/lockdown_cfn_stackset_role.py +224 -0
  169. runbooks/inventory/models/__init__.py +24 -0
  170. runbooks/inventory/models/account.py +192 -0
  171. runbooks/inventory/models/inventory.py +309 -0
  172. runbooks/inventory/models/resource.py +247 -0
  173. runbooks/inventory/recover_cfn_stack_ids.py +205 -0
  174. runbooks/inventory/requirements.txt +12 -0
  175. runbooks/inventory/run_on_multi_accounts.py +211 -0
  176. runbooks/inventory/tests/common_test_data.py +3661 -0
  177. runbooks/inventory/tests/common_test_functions.py +204 -0
  178. runbooks/inventory/tests/setup.py +24 -0
  179. runbooks/inventory/tests/src.py +18 -0
  180. runbooks/inventory/tests/test_cfn_describe_stacks.py +208 -0
  181. runbooks/inventory/tests/test_ec2_describe_instances.py +162 -0
  182. runbooks/inventory/tests/test_inventory_modules.py +55 -0
  183. runbooks/inventory/tests/test_lambda_list_functions.py +86 -0
  184. runbooks/inventory/tests/test_moto_integration_example.py +273 -0
  185. runbooks/inventory/tests/test_org_list_accounts.py +49 -0
  186. runbooks/inventory/update_aws_actions.py +173 -0
  187. runbooks/inventory/update_cfn_stacksets.py +1215 -0
  188. runbooks/inventory/update_cloudwatch_logs_retention_policy.py +294 -0
  189. runbooks/inventory/update_iam_roles_cross_accounts.py +478 -0
  190. runbooks/inventory/update_s3_public_access_block.py +539 -0
  191. runbooks/inventory/utils/__init__.py +23 -0
  192. runbooks/inventory/utils/aws_helpers.py +510 -0
  193. runbooks/inventory/utils/threading_utils.py +493 -0
  194. runbooks/inventory/utils/validation.py +682 -0
  195. runbooks/inventory/verify_ec2_security_groups.py +1430 -0
  196. runbooks/main.py +1004 -0
  197. runbooks/organizations/__init__.py +12 -0
  198. runbooks/organizations/manager.py +374 -0
  199. runbooks/security/README.md +447 -0
  200. runbooks/security/__init__.py +71 -0
  201. runbooks/{security_baseline → security}/checklist/alternate_contacts.py +8 -1
  202. runbooks/{security_baseline → security}/checklist/bucket_public_access.py +4 -1
  203. runbooks/{security_baseline → security}/checklist/cloudwatch_alarm_configuration.py +9 -2
  204. runbooks/{security_baseline → security}/checklist/guardduty_enabled.py +9 -2
  205. runbooks/{security_baseline → security}/checklist/multi_region_instance_usage.py +5 -1
  206. runbooks/{security_baseline → security}/checklist/root_access_key.py +6 -1
  207. runbooks/{security_baseline → security}/config-origin.json +1 -1
  208. runbooks/{security_baseline → security}/config.json +1 -1
  209. runbooks/{security_baseline → security}/permission.json +1 -1
  210. runbooks/{security_baseline → security}/report_generator.py +10 -2
  211. runbooks/{security_baseline → security}/report_template_en.html +7 -7
  212. runbooks/{security_baseline → security}/report_template_jp.html +7 -7
  213. runbooks/{security_baseline → security}/report_template_kr.html +12 -12
  214. runbooks/{security_baseline → security}/report_template_vn.html +7 -7
  215. runbooks/{security_baseline → security}/run_script.py +8 -2
  216. runbooks/{security_baseline → security}/security_baseline_tester.py +12 -4
  217. runbooks/{security_baseline → security}/utils/common.py +5 -1
  218. runbooks/utils/__init__.py +204 -0
  219. runbooks-0.7.0.dist-info/METADATA +375 -0
  220. runbooks-0.7.0.dist-info/RECORD +249 -0
  221. {runbooks-0.2.5.dist-info → runbooks-0.7.0.dist-info}/WHEEL +1 -1
  222. runbooks-0.7.0.dist-info/entry_points.txt +7 -0
  223. runbooks-0.7.0.dist-info/licenses/LICENSE +201 -0
  224. runbooks-0.7.0.dist-info/top_level.txt +3 -0
  225. runbooks/python101/calculator.py +0 -34
  226. runbooks/python101/config.py +0 -1
  227. runbooks/python101/exceptions.py +0 -16
  228. runbooks/python101/file_manager.py +0 -218
  229. runbooks/python101/toolkit.py +0 -153
  230. runbooks-0.2.5.dist-info/METADATA +0 -439
  231. runbooks-0.2.5.dist-info/RECORD +0 -61
  232. runbooks-0.2.5.dist-info/entry_points.txt +0 -3
  233. runbooks-0.2.5.dist-info/top_level.txt +0 -1
  234. /runbooks/{security_baseline/__init__.py → inventory/tests/script_test_data.py} +0 -0
  235. /runbooks/{security_baseline → security}/checklist/__init__.py +0 -0
  236. /runbooks/{security_baseline → security}/checklist/account_level_bucket_public_access.py +0 -0
  237. /runbooks/{security_baseline → security}/checklist/direct_attached_policy.py +0 -0
  238. /runbooks/{security_baseline → security}/checklist/iam_password_policy.py +0 -0
  239. /runbooks/{security_baseline → security}/checklist/iam_user_mfa.py +0 -0
  240. /runbooks/{security_baseline → security}/checklist/multi_region_trail.py +0 -0
  241. /runbooks/{security_baseline → security}/checklist/root_mfa.py +0 -0
  242. /runbooks/{security_baseline → security}/checklist/root_usage.py +0 -0
  243. /runbooks/{security_baseline → security}/checklist/trail_enabled.py +0 -0
  244. /runbooks/{security_baseline → security}/checklist/trusted_advisor.py +0 -0
  245. /runbooks/{security_baseline → security}/utils/__init__.py +0 -0
  246. /runbooks/{security_baseline → security}/utils/enums.py +0 -0
  247. /runbooks/{security_baseline → security}/utils/language.py +0 -0
  248. /runbooks/{security_baseline → security}/utils/level_const.py +0 -0
  249. /runbooks/{security_baseline → security}/utils/permission_list.py +0 -0
@@ -0,0 +1,355 @@
1
+ import csv # Added csv
2
+ import json
3
+ import os
4
+ import re
5
+ import sys
6
+ import tomllib # Built-in since Python 3.11
7
+ from datetime import datetime
8
+ from typing import Any, Dict, List, Optional
9
+
10
+ import yaml
11
+ from reportlab.lib import colors
12
+ from reportlab.lib.pagesizes import landscape, letter
13
+ from reportlab.lib.styles import ParagraphStyle, getSampleStyleSheet
14
+ from reportlab.platypus import (
15
+ Flowable,
16
+ Paragraph,
17
+ SimpleDocTemplate,
18
+ Spacer,
19
+ Table,
20
+ TableStyle,
21
+ )
22
+ from rich.console import Console
23
+
24
+ from runbooks.finops.types import ProfileData
25
+
26
+ console = Console()
27
+
28
+ styles = getSampleStyleSheet()
29
+
30
+ # Custom style for the footer
31
+ audit_footer_style = ParagraphStyle(
32
+ name="AuditFooter",
33
+ parent=styles["Normal"],
34
+ fontSize=8,
35
+ textColor=colors.grey,
36
+ alignment=1,
37
+ leading=10,
38
+ )
39
+
40
+
41
+ def export_audit_report_to_pdf(
42
+ audit_data_list: List[Dict[str, str]],
43
+ file_name: str = "audit_report",
44
+ path: Optional[str] = None,
45
+ ) -> Optional[str]:
46
+ """
47
+ Export the audit report to a PDF file.
48
+
49
+ :param audit_data_list: List of dictionaries, each representing a profile/account's audit data.
50
+ :param file_name: The base name of the output PDF file.
51
+ :param path: Optional directory where the PDF file will be saved.
52
+ :return: Full path of the generated PDF file or None on error.
53
+ """
54
+ try:
55
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M")
56
+ base_filename = f"{file_name}_{timestamp}.pdf"
57
+
58
+ if path:
59
+ os.makedirs(path, exist_ok=True)
60
+ output_filename = os.path.join(path, base_filename)
61
+ else:
62
+ output_filename = base_filename
63
+
64
+ doc = SimpleDocTemplate(output_filename, pagesize=landscape(letter))
65
+ styles = getSampleStyleSheet()
66
+ elements: List[Flowable] = []
67
+
68
+ headers = [
69
+ "Profile",
70
+ "Account ID",
71
+ "Untagged Resources",
72
+ "Stopped EC2 Instances",
73
+ "Unused Volumes",
74
+ "Unused EIPs",
75
+ "Budget Alerts",
76
+ ]
77
+ table_data = [headers]
78
+
79
+ for row in audit_data_list:
80
+ table_data.append(
81
+ [
82
+ row.get("profile", ""),
83
+ row.get("account_id", ""),
84
+ row.get("untagged_resources", ""),
85
+ row.get("stopped_instances", ""),
86
+ row.get("unused_volumes", ""),
87
+ row.get("unused_eips", ""),
88
+ row.get("budget_alerts", ""),
89
+ ]
90
+ )
91
+
92
+ table = Table(table_data, repeatRows=1)
93
+ table.setStyle(
94
+ TableStyle(
95
+ [
96
+ ("BACKGROUND", (0, 0), (-1, 0), colors.black),
97
+ ("TEXTCOLOR", (0, 0), (-1, 0), colors.whitesmoke),
98
+ ("FONTNAME", (0, 0), (-1, -1), "Helvetica"),
99
+ ("FONTSIZE", (0, 0), (-1, -1), 8),
100
+ ("ALIGN", (0, 0), (-1, -1), "LEFT"),
101
+ ("VALIGN", (0, 0), (-1, -1), "TOP"),
102
+ ("GRID", (0, 0), (-1, -1), 0.25, colors.black),
103
+ ("BACKGROUND", (0, 1), (-1, -1), colors.whitesmoke),
104
+ ]
105
+ )
106
+ )
107
+
108
+ elements.append(Paragraph("AWS FinOps Dashboard (Audit Report)", styles["Title"]))
109
+ elements.append(Spacer(1, 12))
110
+ elements.append(table)
111
+ 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
+ )
117
+ )
118
+ elements.append(Spacer(1, 2))
119
+ current_time_str = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
120
+ footer_text = (
121
+ f"This audit report is generated using AWS FinOps Dashboard (CLI) \u00a9 2025 on {current_time_str}"
122
+ )
123
+ elements.append(Paragraph(footer_text, audit_footer_style))
124
+
125
+ doc.build(elements)
126
+ return output_filename
127
+ except Exception as e:
128
+ console.print(f"[bold red]Error exporting audit report to PDF: {str(e)}[/]")
129
+ return None
130
+
131
+
132
+ def clean_rich_tags(text: str) -> str:
133
+ """
134
+ Clean the rich text before writing the data to a pdf.
135
+
136
+ :param text: The rich text to clean.
137
+ :return: Cleaned text.
138
+ """
139
+ return re.sub(r"\[/?[a-zA-Z0-9#_]*\]", "", text)
140
+
141
+
142
+ def export_audit_report_to_csv(
143
+ audit_data_list: List[Dict[str, str]],
144
+ file_name: str = "audit_report",
145
+ path: Optional[str] = None,
146
+ ) -> Optional[str]:
147
+ """Export the audit report to a CSV file."""
148
+ try:
149
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M")
150
+ base_filename = f"{file_name}_{timestamp}.csv"
151
+ output_filename = base_filename
152
+ if path:
153
+ os.makedirs(path, exist_ok=True)
154
+ output_filename = os.path.join(path, base_filename)
155
+
156
+ headers = [
157
+ "Profile",
158
+ "Account ID",
159
+ "Untagged Resources",
160
+ "Stopped EC2 Instances",
161
+ "Unused Volumes",
162
+ "Unused EIPs",
163
+ "Budget Alerts",
164
+ ]
165
+ # Corresponding keys in the audit_data_list dictionaries
166
+ data_keys = [
167
+ "profile",
168
+ "account_id",
169
+ "untagged_resources",
170
+ "stopped_instances",
171
+ "unused_volumes",
172
+ "unused_eips",
173
+ "budget_alerts",
174
+ ]
175
+
176
+ with open(output_filename, "w", newline="") as csvfile:
177
+ writer = csv.writer(csvfile)
178
+ writer.writerow(headers)
179
+ for item in audit_data_list:
180
+ writer.writerow([item.get(key, "") for key in data_keys])
181
+ return output_filename
182
+ except Exception as e:
183
+ console.print(f"[bold red]Error exporting audit report to CSV: {str(e)}[/]")
184
+ return None
185
+
186
+
187
+ def export_audit_report_to_json(
188
+ raw_audit_data: List[Dict[str, Any]], file_name: str = "audit_report", path: Optional[str] = None
189
+ ) -> Optional[str]:
190
+ """Export the audit report to a JSON file."""
191
+ try:
192
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M")
193
+ base_filename = f"{file_name}_{timestamp}.json"
194
+ output_filename = base_filename
195
+ if path:
196
+ os.makedirs(path, exist_ok=True)
197
+ output_filename = os.path.join(path, base_filename)
198
+
199
+ with open(output_filename, "w", encoding="utf-8") as jsonfile:
200
+ json.dump(raw_audit_data, jsonfile, indent=4) # Use the structured list
201
+ return output_filename
202
+ except Exception as e:
203
+ console.print(f"[bold red]Error exporting audit report to JSON: {str(e)}[/]")
204
+ return None
205
+
206
+
207
+ def export_trend_data_to_json(
208
+ trend_data: List[Dict[str, Any]], file_name: str = "trend_data", path: Optional[str] = None
209
+ ) -> Optional[str]:
210
+ """Export trend data to a JSON file."""
211
+ try:
212
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M")
213
+ base_filename = f"{file_name}_{timestamp}.json"
214
+ output_filename = base_filename
215
+ if path:
216
+ os.makedirs(path, exist_ok=True)
217
+ output_filename = os.path.join(path, base_filename)
218
+
219
+ with open(output_filename, "w", encoding="utf-8") as jsonfile:
220
+ json.dump(trend_data, jsonfile, indent=4)
221
+ return output_filename
222
+ except Exception as e:
223
+ console.print(f"[bold red]Error exporting trend data to JSON: {str(e)}[/]")
224
+ return None
225
+
226
+
227
+ def export_cost_dashboard_to_pdf(
228
+ data: List[ProfileData],
229
+ filename: str,
230
+ output_dir: Optional[str] = None,
231
+ previous_period_dates: str = "N/A",
232
+ current_period_dates: str = "N/A",
233
+ ) -> Optional[str]:
234
+ """Export dashboard data to a PDF file."""
235
+ try:
236
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M")
237
+ base_filename = f"{filename}_{timestamp}.pdf"
238
+
239
+ if output_dir:
240
+ os.makedirs(output_dir, exist_ok=True)
241
+ output_filename = os.path.join(output_dir, base_filename)
242
+ else:
243
+ output_filename = base_filename
244
+
245
+ doc = SimpleDocTemplate(output_filename, pagesize=landscape(letter))
246
+ styles = getSampleStyleSheet()
247
+ elements: List[Flowable] = []
248
+
249
+ previous_period_header = f"Cost for period\n({previous_period_dates})"
250
+ current_period_header = f"Cost for period\n({current_period_dates})"
251
+
252
+ headers = [
253
+ "CLI Profile",
254
+ "AWS Account ID",
255
+ previous_period_header,
256
+ current_period_header,
257
+ "Cost By Service",
258
+ "Budget Status",
259
+ "EC2 Instances",
260
+ ]
261
+ table_data = [headers]
262
+
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
+ )
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"]))
299
+ 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
+
306
+ doc.build(elements)
307
+ return os.path.abspath(output_filename)
308
+ except Exception as e:
309
+ console.print(f"[bold red]Error exporting to PDF: {str(e)}[/]")
310
+ return None
311
+
312
+
313
+ def load_config_file(file_path: str) -> Optional[Dict[str, Any]]:
314
+ """Load configuration from TOML, YAML, or JSON file."""
315
+ _, file_extension = os.path.splitext(file_path)
316
+ file_extension = file_extension.lower()
317
+
318
+ try:
319
+ with open(file_path, "rb" if file_extension == ".toml" else "r") as f:
320
+ if file_extension == ".toml":
321
+ loaded_data = tomllib.load(f)
322
+ if isinstance(loaded_data, dict):
323
+ return loaded_data
324
+ console.print(f"[bold red]Error: TOML file {file_path} did not load as a dictionary.[/]")
325
+ return None
326
+ elif file_extension in [".yaml", ".yml"]:
327
+ loaded_data = yaml.safe_load(f)
328
+ if isinstance(loaded_data, dict):
329
+ return loaded_data
330
+ console.print(f"[bold red]Error: YAML file {file_path} did not load as a dictionary.[/]")
331
+ return None
332
+ elif file_extension == ".json":
333
+ loaded_data = json.load(f)
334
+ if isinstance(loaded_data, dict):
335
+ return loaded_data
336
+ console.print(f"[bold red]Error: JSON file {file_path} did not load as a dictionary.[/]")
337
+ return None
338
+ else:
339
+ console.print(f"[bold red]Error: Unsupported configuration file format: {file_extension}[/]")
340
+ return None
341
+ except FileNotFoundError:
342
+ console.print(f"[bold red]Error: Configuration file not found: {file_path}[/]")
343
+ return None
344
+ except tomllib.TOMLDecodeError as e:
345
+ console.print(f"[bold red]Error decoding TOML file {file_path}: {e}[/]")
346
+ return None
347
+ except yaml.YAMLError as e:
348
+ console.print(f"[bold red]Error decoding YAML file {file_path}: {e}[/]")
349
+ return None
350
+ except json.JSONDecodeError as e:
351
+ console.print(f"[bold red]Error decoding JSON file {file_path}: {e}[/]")
352
+ return None
353
+ except Exception as e:
354
+ console.print(f"[bold red]Error loading configuration file {file_path}: {e}[/]")
355
+ return None
@@ -0,0 +1,14 @@
1
+ import argparse
2
+ import sys
3
+ from typing import Dict, List, Optional
4
+
5
+ from runbooks.finops.cli import main as cli_main_entry
6
+
7
+
8
+ def main() -> int:
9
+ """Entry point for the finops submodule when run directly."""
10
+ return cli_main_entry()
11
+
12
+
13
+ if __name__ == "__main__":
14
+ sys.exit(main())
@@ -0,0 +1,174 @@
1
+ from collections import defaultdict
2
+ from typing import Dict, List, Optional
3
+
4
+ import boto3
5
+ from rich.console import Console
6
+
7
+ from runbooks.finops.aws_client import (
8
+ ec2_summary,
9
+ get_accessible_regions,
10
+ )
11
+ from runbooks.finops.cost_processor import (
12
+ change_in_total_cost,
13
+ format_budget_info,
14
+ format_ec2_summary,
15
+ get_cost_data,
16
+ process_service_costs,
17
+ )
18
+ from runbooks.finops.types import (
19
+ BudgetInfo,
20
+ CostData,
21
+ ProfileData,
22
+ )
23
+
24
+ console = Console()
25
+
26
+
27
+ def process_single_profile(
28
+ profile: str,
29
+ user_regions: Optional[List[str]] = None,
30
+ time_range: Optional[int] = None,
31
+ tag: Optional[List[str]] = None,
32
+ ) -> ProfileData:
33
+ """Process a single AWS profile and return its data."""
34
+ try:
35
+ session = boto3.Session(profile_name=profile)
36
+ cost_data = get_cost_data(session, time_range, tag)
37
+
38
+ if user_regions:
39
+ profile_regions = user_regions
40
+ else:
41
+ profile_regions = get_accessible_regions(session)
42
+
43
+ ec2_data = ec2_summary(session, profile_regions)
44
+ service_costs, service_cost_data = process_service_costs(cost_data)
45
+ budget_info = format_budget_info(cost_data["budgets"])
46
+ account_id = cost_data.get("account_id", "Unknown") or "Unknown"
47
+ ec2_summary_text = format_ec2_summary(ec2_data)
48
+ percent_change_in_total_cost = change_in_total_cost(cost_data["current_month"], cost_data["last_month"])
49
+
50
+ return {
51
+ "profile": profile,
52
+ "account_id": account_id,
53
+ "last_month": cost_data["last_month"],
54
+ "current_month": cost_data["current_month"],
55
+ "service_costs": service_cost_data,
56
+ "service_costs_formatted": service_costs,
57
+ "budget_info": budget_info,
58
+ "ec2_summary": ec2_data,
59
+ "ec2_summary_formatted": ec2_summary_text,
60
+ "success": True,
61
+ "error": None,
62
+ "current_period_name": cost_data["current_period_name"],
63
+ "previous_period_name": cost_data["previous_period_name"],
64
+ "percent_change_in_total_cost": percent_change_in_total_cost,
65
+ }
66
+
67
+ except Exception as e:
68
+ return {
69
+ "profile": profile,
70
+ "account_id": "Error",
71
+ "last_month": 0,
72
+ "current_month": 0,
73
+ "service_costs": [],
74
+ "service_costs_formatted": [f"Failed to process profile: {str(e)}"],
75
+ "budget_info": ["N/A"],
76
+ "ec2_summary": {"N/A": 0},
77
+ "ec2_summary_formatted": ["Error"],
78
+ "success": False,
79
+ "error": str(e),
80
+ "current_period_name": "Current month",
81
+ "previous_period_name": "Last month",
82
+ "percent_change_in_total_cost": None,
83
+ }
84
+
85
+
86
+ def process_combined_profiles(
87
+ account_id: str,
88
+ profiles: List[str],
89
+ user_regions: Optional[List[str]] = None,
90
+ time_range: Optional[int] = None,
91
+ tag: Optional[List[str]] = None,
92
+ ) -> ProfileData:
93
+ """Process multiple profiles from the same AWS account."""
94
+
95
+ primary_profile = profiles[0]
96
+ primary_session = boto3.Session(profile_name=primary_profile)
97
+
98
+ account_cost_data: CostData = {
99
+ "account_id": account_id,
100
+ "current_month": 0.0,
101
+ "last_month": 0.0,
102
+ "current_month_cost_by_service": [],
103
+ "budgets": [],
104
+ "current_period_name": "Current month",
105
+ "previous_period_name": "Last month",
106
+ "time_range": time_range,
107
+ "current_period_start": "N/A",
108
+ "current_period_end": "N/A",
109
+ "previous_period_start": "N/A",
110
+ "previous_period_end": "N/A",
111
+ "monthly_costs": None,
112
+ }
113
+
114
+ try:
115
+ # Attempt to overwrite with actual data from Cost Explorer
116
+ account_cost_data = get_cost_data(primary_session, time_range, tag)
117
+ except Exception as e:
118
+ console.log(f"[bold red]Error getting cost data for account {account_id}: {str(e)}[/]")
119
+ # account_cost_data retains its default values if an error occurs
120
+
121
+ combined_current_month = account_cost_data["current_month"]
122
+ combined_last_month = account_cost_data["last_month"]
123
+ combined_service_costs_dict: Dict[str, float] = defaultdict(float)
124
+
125
+ for group in account_cost_data["current_month_cost_by_service"]:
126
+ if "Keys" in group and "Metrics" in group:
127
+ service_name = group["Keys"][0]
128
+ cost_amount = float(group["Metrics"]["UnblendedCost"]["Amount"])
129
+ if cost_amount > 0.001:
130
+ combined_service_costs_dict[service_name] += cost_amount
131
+
132
+ combined_budgets = account_cost_data["budgets"]
133
+
134
+ if user_regions:
135
+ primary_regions = user_regions
136
+ else:
137
+ primary_regions = get_accessible_regions(primary_session)
138
+
139
+ combined_ec2 = ec2_summary(primary_session, primary_regions)
140
+
141
+ service_costs = []
142
+ service_cost_data = [(service, cost) for service, cost in combined_service_costs_dict.items() if cost > 0.001]
143
+ service_cost_data.sort(key=lambda x: x[1], reverse=True)
144
+
145
+ if not service_cost_data:
146
+ service_costs.append("No costs associated with this account")
147
+ else:
148
+ for service_name, cost_amount in service_cost_data:
149
+ service_costs.append(f"{service_name}: ${cost_amount:.2f}")
150
+
151
+ budget_info = format_budget_info(combined_budgets)
152
+
153
+ ec2_summary_text = format_ec2_summary(combined_ec2)
154
+
155
+ profile_list = ", ".join(profiles)
156
+
157
+ percent_change_in_total_cost = change_in_total_cost(combined_current_month, combined_last_month)
158
+
159
+ return {
160
+ "profile": profile_list,
161
+ "account_id": account_id,
162
+ "last_month": combined_last_month,
163
+ "current_month": combined_current_month,
164
+ "service_costs": service_cost_data,
165
+ "service_costs_formatted": service_costs,
166
+ "budget_info": budget_info,
167
+ "ec2_summary": combined_ec2,
168
+ "ec2_summary_formatted": ec2_summary_text,
169
+ "success": True,
170
+ "error": None,
171
+ "current_period_name": account_cost_data["current_period_name"],
172
+ "previous_period_name": account_cost_data["previous_period_name"],
173
+ "percent_change_in_total_cost": percent_change_in_total_cost,
174
+ }
@@ -0,0 +1,66 @@
1
+ """Type definitions for AWS FinOps Dashboard."""
2
+
3
+ from typing import Dict, List, Optional, Tuple, TypedDict
4
+
5
+
6
+ class BudgetInfo(TypedDict):
7
+ """Type for a budget entry."""
8
+
9
+ name: str
10
+ limit: float
11
+ actual: float
12
+ forecast: Optional[float]
13
+
14
+
15
+ class CostData(TypedDict):
16
+ """Type for cost data returned from AWS Cost Explorer."""
17
+
18
+ account_id: Optional[str]
19
+ current_month: float
20
+ last_month: float
21
+ current_month_cost_by_service: List[Dict]
22
+ budgets: List[BudgetInfo]
23
+ current_period_name: str
24
+ previous_period_name: str
25
+ time_range: Optional[int]
26
+ current_period_start: str
27
+ current_period_end: str
28
+ previous_period_start: str
29
+ previous_period_end: str
30
+ monthly_costs: Optional[List[Tuple[str, float]]]
31
+
32
+
33
+ class ProfileData(TypedDict):
34
+ """Type for processed profile data."""
35
+
36
+ profile: str
37
+ account_id: str
38
+ last_month: float
39
+ current_month: float
40
+ service_costs: List[Tuple[str, float]]
41
+ service_costs_formatted: List[str]
42
+ budget_info: List[str]
43
+ ec2_summary: Dict[str, int]
44
+ ec2_summary_formatted: List[str]
45
+ success: bool
46
+ error: Optional[str]
47
+ current_period_name: str
48
+ previous_period_name: str
49
+ percent_change_in_total_cost: Optional[float]
50
+
51
+
52
+ class CLIArgs(TypedDict, total=False):
53
+ """Type for CLI arguments."""
54
+
55
+ profiles: Optional[List[str]]
56
+ regions: Optional[List[str]]
57
+ all: bool
58
+ combine: bool
59
+ report_name: Optional[str]
60
+ report_type: Optional[List[str]]
61
+ dir: Optional[str]
62
+ time_range: Optional[int]
63
+
64
+
65
+ RegionName = str
66
+ EC2Summary = Dict[str, int]
@@ -0,0 +1,80 @@
1
+ from decimal import ROUND_HALF_UP, Decimal, getcontext
2
+ from typing import List, Tuple
3
+
4
+ from rich.console import Console
5
+ from rich.panel import Panel
6
+ from rich.table import Table
7
+
8
+ # Set precision context for Decimal operations
9
+ getcontext().prec = 6
10
+
11
+ console = Console()
12
+
13
+
14
+ def create_trend_bars(monthly_costs: List[Tuple[str, float]]) -> None:
15
+ """Create colorful trend bars using Rich's styling and precise Decimal math."""
16
+ if not monthly_costs:
17
+ return
18
+
19
+ table = Table(box=None, padding=(1, 1), collapse_padding=True)
20
+
21
+ table.add_column("Month", style="bright_magenta", width=10)
22
+ table.add_column("Cost", style="bright_cyan", justify="right", width=15)
23
+ table.add_column("", width=50)
24
+ table.add_column("MoM Change", style="bright_yellow", width=12)
25
+
26
+ max_cost = max(cost for _, cost in monthly_costs)
27
+ if max_cost == 0:
28
+ console.print("[yellow]All costs are $0.00 for this period[/]")
29
+ return
30
+
31
+ prev_cost = None
32
+
33
+ for month, cost in monthly_costs:
34
+ cost_d = Decimal(str(cost))
35
+ bar_length = int((cost / max_cost) * 40) if max_cost > 0 else 0
36
+ bar = "█" * bar_length
37
+
38
+ # Default values
39
+ bar_color = "blue"
40
+ change = ""
41
+
42
+ if prev_cost is not None:
43
+ prev_d = Decimal(str(prev_cost))
44
+
45
+ if prev_d < Decimal("0.01"):
46
+ if cost_d < Decimal("0.01"):
47
+ change = "[bright_yellow]0%[/]"
48
+ bar_color = "yellow"
49
+ else:
50
+ change = "[bright_red]N/A[/]"
51
+ bar_color = "bright_red"
52
+ else:
53
+ change_pct = ((cost_d - prev_d) / prev_d * Decimal("100")).quantize(
54
+ Decimal("0.01"), rounding=ROUND_HALF_UP
55
+ )
56
+
57
+ if abs(change_pct) < Decimal("0.01"):
58
+ change = "[bright_yellow]0%[/]"
59
+ bar_color = "yellow"
60
+ elif abs(change_pct) > Decimal("999"):
61
+ color = "bright_red" if change_pct > 0 else "bright_green"
62
+ change = f"[{color}]{'>+' if change_pct > 0 else '-'}999%[/]"
63
+ bar_color = color
64
+ else:
65
+ color = "bright_red" if change_pct > 0 else "bright_green"
66
+ sign = "+" if change_pct > 0 else ""
67
+ change = f"[{color}]{sign}{change_pct}%[/]"
68
+ bar_color = color
69
+
70
+ table.add_row(month, f"${cost:,.2f}", f"[{bar_color}]{bar}[/]", change)
71
+ prev_cost = cost
72
+
73
+ console.print(
74
+ Panel(
75
+ table,
76
+ title="[cyan]AWS Cost Trend Analysis[/]",
77
+ border_style="bright_blue",
78
+ padding=(1, 1),
79
+ )
80
+ )