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,354 @@
1
+ """
2
+ Tests for CFAT reporting functionality.
3
+
4
+ Tests report generation, export formats, and project management
5
+ integrations to ensure reliable multi-format output generation.
6
+ """
7
+
8
+ import csv
9
+ import json
10
+ import os
11
+ import tempfile
12
+ from io import StringIO
13
+ from pathlib import Path
14
+
15
+ import pytest
16
+
17
+ from runbooks.cfat.models import AssessmentReport, CheckStatus, Severity
18
+ from runbooks.cfat.reporting.exporters import (
19
+ AsanaExporter,
20
+ JiraExporter,
21
+ ServiceNowExporter,
22
+ get_exporter,
23
+ list_available_exporters,
24
+ )
25
+ from runbooks.cfat.tests import create_sample_assessment_report
26
+
27
+
28
+ @pytest.mark.reporting
29
+ class TestReportGeneration:
30
+ """Test assessment report generation in various formats."""
31
+
32
+ def setup_method(self):
33
+ """Set up test environment."""
34
+ self.report = create_sample_assessment_report(num_results=10)
35
+ self.temp_dir = tempfile.mkdtemp()
36
+
37
+ def teardown_method(self):
38
+ """Clean up test environment."""
39
+ import shutil
40
+
41
+ shutil.rmtree(self.temp_dir, ignore_errors=True)
42
+
43
+ def test_json_export(self):
44
+ """Test JSON report generation."""
45
+ json_file = os.path.join(self.temp_dir, "test_report.json")
46
+
47
+ # Generate JSON report
48
+ self.report.to_json(json_file)
49
+
50
+ # Verify file was created
51
+ assert os.path.exists(json_file)
52
+
53
+ # Verify JSON content
54
+ with open(json_file, "r") as f:
55
+ data = json.load(f)
56
+
57
+ assert data["account_id"] == self.report.account_id
58
+ assert data["region"] == self.report.region
59
+ assert len(data["results"]) == len(self.report.results)
60
+ assert "summary" in data
61
+ assert data["summary"]["total_checks"] == self.report.summary.total_checks
62
+
63
+ def test_csv_export(self):
64
+ """Test CSV report generation."""
65
+ csv_file = os.path.join(self.temp_dir, "test_report.csv")
66
+
67
+ # Generate CSV report
68
+ self.report.to_csv(csv_file)
69
+
70
+ # Verify file was created
71
+ assert os.path.exists(csv_file)
72
+
73
+ # Verify CSV content
74
+ with open(csv_file, "r") as f:
75
+ reader = csv.DictReader(f)
76
+ rows = list(reader)
77
+
78
+ assert len(rows) == len(self.report.results)
79
+
80
+ # Check required columns
81
+ expected_columns = ["finding_id", "check_name", "category", "status", "severity", "message", "execution_time"]
82
+ for col in expected_columns:
83
+ assert col in reader.fieldnames
84
+
85
+ # Verify data integrity
86
+ first_row = rows[0]
87
+ first_result = self.report.results[0]
88
+ assert first_row["finding_id"] == first_result.finding_id
89
+ assert first_row["check_name"] == first_result.check_name
90
+ assert first_row["status"] == first_result.status.value
91
+
92
+ def test_html_export(self):
93
+ """Test HTML report generation."""
94
+ html_file = os.path.join(self.temp_dir, "test_report.html")
95
+
96
+ # Generate HTML report
97
+ self.report.to_html(html_file)
98
+
99
+ # Verify file was created
100
+ assert os.path.exists(html_file)
101
+
102
+ # Verify HTML content
103
+ with open(html_file, "r") as f:
104
+ html_content = f.read()
105
+
106
+ assert "<!DOCTYPE html>" in html_content
107
+ assert "Cloud Foundations Assessment Report" in html_content
108
+ assert self.report.account_id in html_content
109
+ assert str(self.report.summary.total_checks) in html_content
110
+
111
+ def test_markdown_export(self):
112
+ """Test Markdown report generation."""
113
+ md_file = os.path.join(self.temp_dir, "test_report.md")
114
+
115
+ # Generate Markdown report
116
+ self.report.to_markdown(md_file)
117
+
118
+ # Verify file was created
119
+ assert os.path.exists(md_file)
120
+
121
+ # Verify Markdown content
122
+ with open(md_file, "r") as f:
123
+ md_content = f.read()
124
+
125
+ assert "# Cloud Foundations Assessment Report" in md_content
126
+ assert f"**Account:** {self.report.account_id}" in md_content
127
+ assert f"**Total Checks:** {self.report.summary.total_checks}" in md_content
128
+ assert f"**Compliance Score:** {self.report.summary.compliance_score}" in md_content
129
+
130
+ def test_export_with_failed_results(self):
131
+ """Test export handling when there are failed results."""
132
+ # Create report with failed results
133
+ from runbooks.cfat.tests import create_sample_assessment_result
134
+
135
+ failed_result = create_sample_assessment_result(
136
+ finding_id="FAIL-001",
137
+ status=CheckStatus.FAIL,
138
+ severity=Severity.CRITICAL,
139
+ message="Critical security issue",
140
+ check_name="security_check",
141
+ )
142
+
143
+ # Add to report
144
+ self.report.results.append(failed_result)
145
+
146
+ # Test markdown export shows critical findings
147
+ md_file = os.path.join(self.temp_dir, "failed_report.md")
148
+ self.report.to_markdown(md_file)
149
+
150
+ with open(md_file, "r") as f:
151
+ content = f.read()
152
+
153
+ assert "🚨 Critical Findings" in content
154
+ assert "FAIL-001" in content
155
+
156
+
157
+ @pytest.mark.reporting
158
+ class TestProjectManagementExporters:
159
+ """Test project management tool exporters."""
160
+
161
+ def setup_method(self):
162
+ """Set up test environment."""
163
+ # Create report with some failed results for export
164
+ self.report = create_sample_assessment_report(num_results=5)
165
+
166
+ # Ensure we have some failed results
167
+ from runbooks.cfat.tests import create_sample_assessment_result
168
+
169
+ failed_result = create_sample_assessment_result(
170
+ finding_id="CRITICAL-001",
171
+ status=CheckStatus.FAIL,
172
+ severity=Severity.CRITICAL,
173
+ message="Critical security vulnerability requires immediate attention",
174
+ check_name="security_critical_check",
175
+ )
176
+ failed_result.recommendations = ["Enable MFA for root account", "Review IAM policies for least privilege"]
177
+ self.report.results.append(failed_result)
178
+
179
+ self.temp_dir = tempfile.mkdtemp()
180
+
181
+ def teardown_method(self):
182
+ """Clean up test environment."""
183
+ import shutil
184
+
185
+ shutil.rmtree(self.temp_dir, ignore_errors=True)
186
+
187
+ def test_jira_exporter(self):
188
+ """Test Jira CSV export functionality."""
189
+ exporter = JiraExporter()
190
+ assert exporter.get_exporter_name() == "jira"
191
+
192
+ # Test export to file
193
+ jira_file = os.path.join(self.temp_dir, "jira_export.csv")
194
+ csv_content = exporter.export(self.report, jira_file)
195
+
196
+ # Verify file was created
197
+ assert os.path.exists(jira_file)
198
+
199
+ # Verify CSV content structure
200
+ assert isinstance(csv_content, str)
201
+ assert "Summary,Issue Type,Priority" in csv_content
202
+ assert "[CFAT]" in csv_content
203
+
204
+ # Parse CSV to verify structure
205
+ csv_reader = csv.DictReader(StringIO(csv_content))
206
+ rows = list(csv_reader)
207
+
208
+ # Should have at least one row for failed results
209
+ failed_results = self.report.get_failed_results()
210
+ assert len(rows) >= len(failed_results)
211
+
212
+ # Check required Jira columns
213
+ expected_columns = ["Summary", "Issue Type", "Priority", "Description", "Labels"]
214
+ for col in expected_columns:
215
+ assert col in csv_reader.fieldnames
216
+
217
+ def test_asana_exporter(self):
218
+ """Test Asana CSV export functionality."""
219
+ exporter = AsanaExporter()
220
+ assert exporter.get_exporter_name() == "asana"
221
+
222
+ # Test export
223
+ asana_file = os.path.join(self.temp_dir, "asana_export.csv")
224
+ csv_content = exporter.export(self.report, asana_file)
225
+
226
+ # Verify file and content
227
+ assert os.path.exists(asana_file)
228
+ assert isinstance(csv_content, str)
229
+ assert "Name,Notes,Priority" in csv_content
230
+
231
+ # Parse and verify structure
232
+ csv_reader = csv.DictReader(StringIO(csv_content))
233
+ rows = list(csv_reader)
234
+ assert len(rows) > 0
235
+
236
+ # Check Asana-specific columns
237
+ expected_columns = ["Name", "Notes", "Priority", "Tags", "Due Date"]
238
+ for col in expected_columns:
239
+ assert col in csv_reader.fieldnames
240
+
241
+ def test_servicenow_exporter(self):
242
+ """Test ServiceNow JSON export functionality."""
243
+ exporter = ServiceNowExporter()
244
+ assert exporter.get_exporter_name() == "servicenow"
245
+
246
+ # Test export
247
+ snow_file = os.path.join(self.temp_dir, "servicenow_export.json")
248
+ json_content = exporter.export(self.report, snow_file)
249
+
250
+ # Verify file and content
251
+ assert os.path.exists(snow_file)
252
+ assert isinstance(json_content, str)
253
+
254
+ # Parse JSON to verify structure
255
+ data = json.loads(json_content)
256
+ assert "incidents" in data
257
+ assert "metadata" in data
258
+
259
+ # Verify incident structure
260
+ incidents = data["incidents"]
261
+ assert len(incidents) > 0
262
+
263
+ incident = incidents[0]
264
+ required_fields = ["short_description", "description", "category", "priority", "impact", "urgency"]
265
+ for field in required_fields:
266
+ assert field in incident
267
+
268
+ # Verify metadata
269
+ metadata = data["metadata"]
270
+ assert metadata["account_id"] == self.report.account_id
271
+ assert "assessment_date" in metadata
272
+
273
+ def test_severity_mapping(self):
274
+ """Test severity to priority mapping in exporters."""
275
+ jira_exporter = JiraExporter()
276
+
277
+ # Test severity mapping
278
+ assert jira_exporter._map_severity_to_priority(Severity.CRITICAL) == "Critical"
279
+ assert jira_exporter._map_severity_to_priority(Severity.WARNING) == "High"
280
+ assert jira_exporter._map_severity_to_priority(Severity.INFO) == "Medium"
281
+
282
+ def test_exporter_registry(self):
283
+ """Test exporter registry functionality."""
284
+ # Test getting exporters by name
285
+ jira_exporter = get_exporter("jira")
286
+ assert isinstance(jira_exporter, JiraExporter)
287
+
288
+ asana_exporter = get_exporter("asana")
289
+ assert isinstance(asana_exporter, AsanaExporter)
290
+
291
+ snow_exporter = get_exporter("servicenow")
292
+ assert isinstance(snow_exporter, ServiceNowExporter)
293
+
294
+ # Test invalid exporter
295
+ invalid_exporter = get_exporter("invalid")
296
+ assert invalid_exporter is None
297
+
298
+ # Test listing available exporters
299
+ available = list_available_exporters()
300
+ assert "jira" in available
301
+ assert "asana" in available
302
+ assert "servicenow" in available
303
+
304
+ def test_export_with_no_failed_results(self):
305
+ """Test exporters handle reports with no failed results gracefully."""
306
+ # Create report with only passed results
307
+ from runbooks.cfat.tests import create_sample_assessment_result
308
+
309
+ passed_report = create_sample_assessment_report(num_results=0)
310
+ passed_result = create_sample_assessment_result(status=CheckStatus.PASS, severity=Severity.INFO)
311
+ passed_report.results = [passed_result]
312
+
313
+ # Test Jira exporter
314
+ jira_exporter = JiraExporter()
315
+ jira_file = os.path.join(self.temp_dir, "jira_empty.csv")
316
+ csv_content = jira_exporter.export(passed_report, jira_file)
317
+
318
+ # Should still create file with headers but no data rows
319
+ assert os.path.exists(jira_file)
320
+ csv_reader = csv.DictReader(StringIO(csv_content))
321
+ rows = list(csv_reader)
322
+ assert len(rows) == 0 # No failed results to export
323
+
324
+ def test_large_export(self):
325
+ """Test exporters handle large numbers of findings."""
326
+ # Create report with many failed results
327
+ large_report = create_sample_assessment_report(num_results=0)
328
+
329
+ from runbooks.cfat.tests import create_sample_assessment_result
330
+
331
+ for i in range(100):
332
+ failed_result = create_sample_assessment_result(
333
+ finding_id=f"LARGE-{i:03d}",
334
+ status=CheckStatus.FAIL,
335
+ severity=Severity.WARNING,
336
+ message=f"Large test finding {i}",
337
+ check_name=f"large_check_{i}",
338
+ )
339
+ large_report.results.append(failed_result)
340
+
341
+ # Test Jira export handles large data
342
+ jira_exporter = JiraExporter()
343
+ jira_file = os.path.join(self.temp_dir, "jira_large.csv")
344
+ csv_content = jira_exporter.export(large_report, jira_file)
345
+
346
+ # Verify all results were exported
347
+ csv_reader = csv.DictReader(StringIO(csv_content))
348
+ rows = list(csv_reader)
349
+ assert len(rows) == 100
350
+
351
+ # Verify file size is reasonable
352
+ file_size = os.path.getsize(jira_file)
353
+ assert file_size > 1000 # Should be substantial but not enormous
354
+ assert file_size < 1000000 # Should be less than 1MB
@@ -0,0 +1,16 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "module": "ES2020",
5
+ "strict": true,
6
+ "preserveConstEnums": true,
7
+ "sourceMap": false,
8
+ "outDir": "./build",
9
+ "moduleResolution": "node",
10
+ "esModuleInterop": true,
11
+ "skipLibCheck": true,
12
+ "forceConsistentCasingInFileNames": true
13
+ },
14
+ "include": ["src/**/*", "./**/*.ts"],
15
+ "exclude": ["node_modules", "**/*.test.ts"]
16
+ }
@@ -0,0 +1,27 @@
1
+ //import * as path from 'path'
2
+ const path = require('path');
3
+ const nodeExternals = require('webpack-node-externals');
4
+ module.exports = {
5
+ target: 'node',
6
+ entry: './build/app.js',
7
+ externals: [nodeExternals()],
8
+ mode: 'production',
9
+ devtool: 'inline-source-map',
10
+ watch: false,
11
+ output: {
12
+ filename: 'cfat.js',
13
+ path: path.resolve(__dirname, 'dist'),
14
+ },
15
+ resolve: {
16
+ extensions: ['.ts', '.js'],
17
+ },
18
+ module: {
19
+ rules: [
20
+ {
21
+ test: /\.ts$/,
22
+ use: 'ts-loader',
23
+ exclude: /node_modules/,
24
+ },
25
+ ],
26
+ },
27
+ };
runbooks/config.py ADDED
@@ -0,0 +1,260 @@
1
+ """
2
+ Configuration management for CloudOps Runbooks.
3
+
4
+ This module handles loading, saving, and managing configuration settings
5
+ for the runbooks package, including AWS profiles, regions, and assessment settings.
6
+ """
7
+
8
+ import os
9
+ from pathlib import Path
10
+ from typing import Any, Dict, Optional
11
+
12
+ import yaml
13
+ from loguru import logger
14
+
15
+ try:
16
+ from pydantic import BaseModel, Field
17
+
18
+ _HAS_PYDANTIC = True
19
+ except ImportError:
20
+ _HAS_PYDANTIC = False
21
+
22
+ # Fallback BaseModel
23
+ class BaseModel:
24
+ def __init__(self, **kwargs):
25
+ for key, value in kwargs.items():
26
+ setattr(self, key, value)
27
+
28
+ def model_dump(self, exclude_none=True):
29
+ result = {}
30
+ for key, value in self.__dict__.items():
31
+ if not exclude_none or value is not None:
32
+ result[key] = value
33
+ return result
34
+
35
+ def Field(default=None, default_factory=None, description=""):
36
+ if default_factory:
37
+ return default_factory()
38
+ return default
39
+
40
+
41
+ from runbooks.utils import ensure_directory
42
+
43
+
44
+ class RunbooksConfig(BaseModel):
45
+ """Configuration model for CloudOps Runbooks."""
46
+
47
+ # AWS Configuration
48
+ aws_profile: str = Field(default="default", description="Default AWS profile")
49
+ aws_region: Optional[str] = Field(default=None, description="Default AWS region")
50
+
51
+ # Assessment Configuration
52
+ cfat_checks: Dict[str, bool] = Field(
53
+ default_factory=lambda: {
54
+ "cloudtrail": True,
55
+ "config": True,
56
+ "iam": True,
57
+ "vpc": True,
58
+ "ec2": True,
59
+ "organizations": True,
60
+ "control_tower": True,
61
+ "kms": True,
62
+ },
63
+ description="CFAT checks to run by default",
64
+ )
65
+ cfat_severity_threshold: str = Field(default="WARNING", description="Minimum severity for CFAT reports")
66
+
67
+ # Inventory Configuration
68
+ inventory_parallel: bool = Field(default=True, description="Run inventory collection in parallel")
69
+ inventory_cache_ttl: int = Field(default=3600, description="Inventory cache TTL in seconds")
70
+ inventory_include_costs: bool = Field(default=False, description="Include cost data in inventory")
71
+
72
+ # Output Configuration
73
+ default_output_format: str = Field(default="console", description="Default output format")
74
+ reports_directory: str = Field(default="reports", description="Directory for generated reports")
75
+
76
+ # Logging Configuration
77
+ log_level: str = Field(default="INFO", description="Default log level")
78
+ log_file: Optional[str] = Field(default=None, description="Log file path")
79
+
80
+ class Config:
81
+ """Pydantic config."""
82
+
83
+ extra = "allow"
84
+
85
+
86
+ def get_default_config_path() -> Path:
87
+ """
88
+ Get the default configuration file path.
89
+
90
+ Returns:
91
+ Path to the default config file
92
+ """
93
+ config_dir = Path.home() / ".runbooks"
94
+ ensure_directory(config_dir)
95
+ return config_dir / "config.yaml"
96
+
97
+
98
+ def load_config(config_path: Optional[Path] = None) -> RunbooksConfig:
99
+ """
100
+ Load configuration from file or return default configuration.
101
+
102
+ Args:
103
+ config_path: Path to configuration file
104
+
105
+ Returns:
106
+ Loaded configuration object
107
+ """
108
+ if config_path is None:
109
+ config_path = get_default_config_path()
110
+
111
+ if not config_path.exists():
112
+ logger.info(f"Config file not found at {config_path}, using defaults")
113
+ config = RunbooksConfig()
114
+ save_config(config, config_path)
115
+ return config
116
+
117
+ try:
118
+ with open(config_path, "r", encoding="utf-8") as f:
119
+ config_data = yaml.safe_load(f) or {}
120
+
121
+ # Handle environment variable substitution
122
+ config_data = _substitute_env_vars(config_data)
123
+
124
+ config = RunbooksConfig(**config_data)
125
+ logger.debug(f"Loaded configuration from {config_path}")
126
+ return config
127
+
128
+ except Exception as e:
129
+ logger.error(f"Error loading config from {config_path}: {e}")
130
+ logger.info("Using default configuration")
131
+ return RunbooksConfig()
132
+
133
+
134
+ def save_config(config: RunbooksConfig, config_path: Optional[Path] = None) -> None:
135
+ """
136
+ Save configuration to file.
137
+
138
+ Args:
139
+ config: Configuration object to save
140
+ config_path: Path to save configuration file
141
+ """
142
+ if config_path is None:
143
+ config_path = get_default_config_path()
144
+
145
+ try:
146
+ ensure_directory(config_path.parent)
147
+
148
+ config_dict = config.model_dump(exclude_none=True)
149
+
150
+ with open(config_path, "w", encoding="utf-8") as f:
151
+ yaml.dump(config_dict, f, default_flow_style=False, indent=2, sort_keys=True)
152
+
153
+ logger.debug(f"Saved configuration to {config_path}")
154
+
155
+ except Exception as e:
156
+ logger.error(f"Error saving config to {config_path}: {e}")
157
+ raise
158
+
159
+
160
+ def update_config(updates: Dict[str, Any], config_path: Optional[Path] = None) -> RunbooksConfig:
161
+ """
162
+ Update specific configuration values.
163
+
164
+ Args:
165
+ updates: Dictionary of configuration updates
166
+ config_path: Path to configuration file
167
+
168
+ Returns:
169
+ Updated configuration object
170
+ """
171
+ config = load_config(config_path)
172
+
173
+ # Update configuration
174
+ for key, value in updates.items():
175
+ if hasattr(config, key):
176
+ setattr(config, key, value)
177
+ else:
178
+ logger.warning(f"Unknown configuration key: {key}")
179
+
180
+ save_config(config, config_path)
181
+ return config
182
+
183
+
184
+ def _substitute_env_vars(data: Any) -> Any:
185
+ """
186
+ Recursively substitute environment variables in configuration data.
187
+
188
+ Args:
189
+ data: Configuration data
190
+
191
+ Returns:
192
+ Data with environment variables substituted
193
+ """
194
+ if isinstance(data, dict):
195
+ return {key: _substitute_env_vars(value) for key, value in data.items()}
196
+ elif isinstance(data, list):
197
+ return [_substitute_env_vars(item) for item in data]
198
+ elif isinstance(data, str):
199
+ # Simple environment variable substitution ${VAR_NAME}
200
+ if data.startswith("${") and data.endswith("}"):
201
+ env_var = data[2:-1]
202
+ return os.getenv(env_var, data)
203
+ return data
204
+ else:
205
+ return data
206
+
207
+
208
+ def get_aws_session_config(config: RunbooksConfig) -> Dict[str, str]:
209
+ """
210
+ Get AWS session configuration from runbooks config.
211
+
212
+ Args:
213
+ config: Runbooks configuration
214
+
215
+ Returns:
216
+ Dictionary with AWS session parameters
217
+ """
218
+ session_config = {"profile_name": config.aws_profile}
219
+
220
+ if config.aws_region:
221
+ session_config["region_name"] = config.aws_region
222
+
223
+ return session_config
224
+
225
+
226
+ def validate_config(config: RunbooksConfig) -> bool:
227
+ """
228
+ Validate configuration settings.
229
+
230
+ Args:
231
+ config: Configuration to validate
232
+
233
+ Returns:
234
+ True if configuration is valid
235
+ """
236
+ try:
237
+ # Validate AWS profile if specified
238
+ if config.aws_profile != "default":
239
+ from runbooks.utils import validate_aws_profile
240
+
241
+ if not validate_aws_profile(config.aws_profile):
242
+ logger.warning(f"AWS profile '{config.aws_profile}' not found")
243
+
244
+ # Validate log level
245
+ valid_levels = ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]
246
+ if config.log_level not in valid_levels:
247
+ logger.warning(f"Invalid log level '{config.log_level}', using INFO")
248
+ config.log_level = "INFO"
249
+
250
+ # Validate severity threshold
251
+ valid_severities = ["INFO", "WARNING", "CRITICAL"]
252
+ if config.cfat_severity_threshold not in valid_severities:
253
+ logger.warning(f"Invalid CFAT severity '{config.cfat_severity_threshold}', using WARNING")
254
+ config.cfat_severity_threshold = "WARNING"
255
+
256
+ return True
257
+
258
+ except Exception as e:
259
+ logger.error(f"Configuration validation failed: {e}")
260
+ return False