runbooks 0.2.5__py3-none-any.whl → 0.6.1__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 (221) hide show
  1. conftest.py +26 -0
  2. jupyter-agent/.env.template +2 -0
  3. jupyter-agent/.gitattributes +35 -0
  4. jupyter-agent/README.md +16 -0
  5. jupyter-agent/app.py +256 -0
  6. jupyter-agent/cloudops-agent.png +0 -0
  7. jupyter-agent/ds-system-prompt.txt +154 -0
  8. jupyter-agent/jupyter-agent.png +0 -0
  9. jupyter-agent/llama3_template.jinja +123 -0
  10. jupyter-agent/requirements.txt +9 -0
  11. jupyter-agent/utils.py +409 -0
  12. runbooks/__init__.py +71 -3
  13. runbooks/__main__.py +13 -0
  14. runbooks/aws/ec2_describe_instances.py +1 -1
  15. runbooks/aws/ec2_run_instances.py +8 -2
  16. runbooks/aws/ec2_start_stop_instances.py +17 -4
  17. runbooks/aws/ec2_unused_volumes.py +5 -1
  18. runbooks/aws/s3_create_bucket.py +4 -2
  19. runbooks/aws/s3_list_objects.py +6 -1
  20. runbooks/aws/tagging_lambda_handler.py +13 -2
  21. runbooks/aws/tags.json +12 -0
  22. runbooks/base.py +353 -0
  23. runbooks/cfat/README.md +49 -0
  24. runbooks/cfat/__init__.py +74 -0
  25. runbooks/cfat/app.ts +644 -0
  26. runbooks/cfat/assessment/__init__.py +40 -0
  27. runbooks/cfat/assessment/asana-import.csv +39 -0
  28. runbooks/cfat/assessment/cfat-checks.csv +31 -0
  29. runbooks/cfat/assessment/cfat.txt +520 -0
  30. runbooks/cfat/assessment/collectors.py +200 -0
  31. runbooks/cfat/assessment/jira-import.csv +39 -0
  32. runbooks/cfat/assessment/runner.py +387 -0
  33. runbooks/cfat/assessment/validators.py +290 -0
  34. runbooks/cfat/cli.py +103 -0
  35. runbooks/cfat/docs/asana-import.csv +24 -0
  36. runbooks/cfat/docs/cfat-checks.csv +31 -0
  37. runbooks/cfat/docs/cfat.txt +335 -0
  38. runbooks/cfat/docs/checks-output.png +0 -0
  39. runbooks/cfat/docs/cloudshell-console-run.png +0 -0
  40. runbooks/cfat/docs/cloudshell-download.png +0 -0
  41. runbooks/cfat/docs/cloudshell-output.png +0 -0
  42. runbooks/cfat/docs/downloadfile.png +0 -0
  43. runbooks/cfat/docs/jira-import.csv +24 -0
  44. runbooks/cfat/docs/open-cloudshell.png +0 -0
  45. runbooks/cfat/docs/report-header.png +0 -0
  46. runbooks/cfat/models.py +1026 -0
  47. runbooks/cfat/package-lock.json +5116 -0
  48. runbooks/cfat/package.json +38 -0
  49. runbooks/cfat/report.py +496 -0
  50. runbooks/cfat/reporting/__init__.py +46 -0
  51. runbooks/cfat/reporting/exporters.py +337 -0
  52. runbooks/cfat/reporting/formatters.py +496 -0
  53. runbooks/cfat/reporting/templates.py +135 -0
  54. runbooks/cfat/run-assessment.sh +23 -0
  55. runbooks/cfat/runner.py +69 -0
  56. runbooks/cfat/src/actions/check-cloudtrail-existence.ts +43 -0
  57. runbooks/cfat/src/actions/check-config-existence.ts +37 -0
  58. runbooks/cfat/src/actions/check-control-tower.ts +37 -0
  59. runbooks/cfat/src/actions/check-ec2-existence.ts +46 -0
  60. runbooks/cfat/src/actions/check-iam-users.ts +50 -0
  61. runbooks/cfat/src/actions/check-legacy-cur.ts +30 -0
  62. runbooks/cfat/src/actions/check-org-cloudformation.ts +30 -0
  63. runbooks/cfat/src/actions/check-vpc-existence.ts +43 -0
  64. runbooks/cfat/src/actions/create-asanaimport.ts +14 -0
  65. runbooks/cfat/src/actions/create-backlog.ts +372 -0
  66. runbooks/cfat/src/actions/create-jiraimport.ts +15 -0
  67. runbooks/cfat/src/actions/create-report.ts +616 -0
  68. runbooks/cfat/src/actions/define-account-type.ts +51 -0
  69. runbooks/cfat/src/actions/get-enabled-org-policy-types.ts +40 -0
  70. runbooks/cfat/src/actions/get-enabled-org-services.ts +26 -0
  71. runbooks/cfat/src/actions/get-idc-info.ts +34 -0
  72. runbooks/cfat/src/actions/get-org-da-accounts.ts +34 -0
  73. runbooks/cfat/src/actions/get-org-details.ts +35 -0
  74. runbooks/cfat/src/actions/get-org-member-accounts.ts +44 -0
  75. runbooks/cfat/src/actions/get-org-ous.ts +35 -0
  76. runbooks/cfat/src/actions/get-regions.ts +22 -0
  77. runbooks/cfat/src/actions/zip-assessment.ts +27 -0
  78. runbooks/cfat/src/types/index.d.ts +147 -0
  79. runbooks/cfat/tests/__init__.py +141 -0
  80. runbooks/cfat/tests/test_cli.py +340 -0
  81. runbooks/cfat/tests/test_integration.py +290 -0
  82. runbooks/cfat/tests/test_models.py +505 -0
  83. runbooks/cfat/tests/test_reporting.py +354 -0
  84. runbooks/cfat/tsconfig.json +16 -0
  85. runbooks/cfat/webpack.config.cjs +27 -0
  86. runbooks/config.py +260 -0
  87. runbooks/finops/__init__.py +88 -0
  88. runbooks/finops/aws_client.py +245 -0
  89. runbooks/finops/cli.py +151 -0
  90. runbooks/finops/cost_processor.py +410 -0
  91. runbooks/finops/dashboard_runner.py +448 -0
  92. runbooks/finops/helpers.py +355 -0
  93. runbooks/finops/main.py +14 -0
  94. runbooks/finops/profile_processor.py +174 -0
  95. runbooks/finops/types.py +66 -0
  96. runbooks/finops/visualisations.py +80 -0
  97. runbooks/inventory/.gitignore +354 -0
  98. runbooks/inventory/ArgumentsClass.py +261 -0
  99. runbooks/inventory/Inventory_Modules.py +6130 -0
  100. runbooks/inventory/LandingZone/delete_lz.py +1075 -0
  101. runbooks/inventory/README.md +1320 -0
  102. runbooks/inventory/__init__.py +62 -0
  103. runbooks/inventory/account_class.py +532 -0
  104. runbooks/inventory/all_my_instances_wrapper.py +123 -0
  105. runbooks/inventory/aws_decorators.py +201 -0
  106. runbooks/inventory/cfn_move_stack_instances.py +1526 -0
  107. runbooks/inventory/check_cloudtrail_compliance.py +614 -0
  108. runbooks/inventory/check_controltower_readiness.py +1107 -0
  109. runbooks/inventory/check_landingzone_readiness.py +711 -0
  110. runbooks/inventory/cloudtrail.md +727 -0
  111. runbooks/inventory/collectors/__init__.py +20 -0
  112. runbooks/inventory/collectors/aws_compute.py +518 -0
  113. runbooks/inventory/collectors/aws_networking.py +275 -0
  114. runbooks/inventory/collectors/base.py +222 -0
  115. runbooks/inventory/core/__init__.py +19 -0
  116. runbooks/inventory/core/collector.py +303 -0
  117. runbooks/inventory/core/formatter.py +296 -0
  118. runbooks/inventory/delete_s3_buckets_objects.py +169 -0
  119. runbooks/inventory/discovery.md +81 -0
  120. runbooks/inventory/draw_org_structure.py +748 -0
  121. runbooks/inventory/ec2_vpc_utils.py +341 -0
  122. runbooks/inventory/find_cfn_drift_detection.py +272 -0
  123. runbooks/inventory/find_cfn_orphaned_stacks.py +719 -0
  124. runbooks/inventory/find_cfn_stackset_drift.py +733 -0
  125. runbooks/inventory/find_ec2_security_groups.py +669 -0
  126. runbooks/inventory/find_landingzone_versions.py +201 -0
  127. runbooks/inventory/find_vpc_flow_logs.py +1221 -0
  128. runbooks/inventory/inventory.sh +659 -0
  129. runbooks/inventory/list_cfn_stacks.py +558 -0
  130. runbooks/inventory/list_cfn_stackset_operation_results.py +252 -0
  131. runbooks/inventory/list_cfn_stackset_operations.py +734 -0
  132. runbooks/inventory/list_cfn_stacksets.py +453 -0
  133. runbooks/inventory/list_config_recorders_delivery_channels.py +681 -0
  134. runbooks/inventory/list_ds_directories.py +354 -0
  135. runbooks/inventory/list_ec2_availability_zones.py +286 -0
  136. runbooks/inventory/list_ec2_ebs_volumes.py +244 -0
  137. runbooks/inventory/list_ec2_instances.py +425 -0
  138. runbooks/inventory/list_ecs_clusters_and_tasks.py +562 -0
  139. runbooks/inventory/list_elbs_load_balancers.py +411 -0
  140. runbooks/inventory/list_enis_network_interfaces.py +526 -0
  141. runbooks/inventory/list_guardduty_detectors.py +568 -0
  142. runbooks/inventory/list_iam_policies.py +404 -0
  143. runbooks/inventory/list_iam_roles.py +518 -0
  144. runbooks/inventory/list_iam_saml_providers.py +359 -0
  145. runbooks/inventory/list_lambda_functions.py +882 -0
  146. runbooks/inventory/list_org_accounts.py +446 -0
  147. runbooks/inventory/list_org_accounts_users.py +354 -0
  148. runbooks/inventory/list_rds_db_instances.py +406 -0
  149. runbooks/inventory/list_route53_hosted_zones.py +318 -0
  150. runbooks/inventory/list_servicecatalog_provisioned_products.py +575 -0
  151. runbooks/inventory/list_sns_topics.py +360 -0
  152. runbooks/inventory/list_ssm_parameters.py +402 -0
  153. runbooks/inventory/list_vpc_subnets.py +433 -0
  154. runbooks/inventory/list_vpcs.py +422 -0
  155. runbooks/inventory/lockdown_cfn_stackset_role.py +224 -0
  156. runbooks/inventory/models/__init__.py +24 -0
  157. runbooks/inventory/models/account.py +192 -0
  158. runbooks/inventory/models/inventory.py +309 -0
  159. runbooks/inventory/models/resource.py +247 -0
  160. runbooks/inventory/recover_cfn_stack_ids.py +205 -0
  161. runbooks/inventory/requirements.txt +12 -0
  162. runbooks/inventory/run_on_multi_accounts.py +211 -0
  163. runbooks/inventory/tests/common_test_data.py +3661 -0
  164. runbooks/inventory/tests/common_test_functions.py +204 -0
  165. runbooks/inventory/tests/script_test_data.py +0 -0
  166. runbooks/inventory/tests/setup.py +24 -0
  167. runbooks/inventory/tests/src.py +18 -0
  168. runbooks/inventory/tests/test_cfn_describe_stacks.py +208 -0
  169. runbooks/inventory/tests/test_ec2_describe_instances.py +162 -0
  170. runbooks/inventory/tests/test_inventory_modules.py +55 -0
  171. runbooks/inventory/tests/test_lambda_list_functions.py +86 -0
  172. runbooks/inventory/tests/test_moto_integration_example.py +273 -0
  173. runbooks/inventory/tests/test_org_list_accounts.py +49 -0
  174. runbooks/inventory/update_aws_actions.py +173 -0
  175. runbooks/inventory/update_cfn_stacksets.py +1215 -0
  176. runbooks/inventory/update_cloudwatch_logs_retention_policy.py +294 -0
  177. runbooks/inventory/update_iam_roles_cross_accounts.py +478 -0
  178. runbooks/inventory/update_s3_public_access_block.py +539 -0
  179. runbooks/inventory/utils/__init__.py +23 -0
  180. runbooks/inventory/utils/aws_helpers.py +510 -0
  181. runbooks/inventory/utils/threading_utils.py +493 -0
  182. runbooks/inventory/utils/validation.py +682 -0
  183. runbooks/inventory/verify_ec2_security_groups.py +1430 -0
  184. runbooks/main.py +785 -0
  185. runbooks/organizations/__init__.py +12 -0
  186. runbooks/organizations/manager.py +374 -0
  187. runbooks/security_baseline/README.md +324 -0
  188. runbooks/security_baseline/checklist/alternate_contacts.py +8 -1
  189. runbooks/security_baseline/checklist/bucket_public_access.py +4 -1
  190. runbooks/security_baseline/checklist/cloudwatch_alarm_configuration.py +9 -2
  191. runbooks/security_baseline/checklist/guardduty_enabled.py +9 -2
  192. runbooks/security_baseline/checklist/multi_region_instance_usage.py +5 -1
  193. runbooks/security_baseline/checklist/root_access_key.py +6 -1
  194. runbooks/security_baseline/config-origin.json +1 -1
  195. runbooks/security_baseline/config.json +1 -1
  196. runbooks/security_baseline/permission.json +1 -1
  197. runbooks/security_baseline/report_generator.py +10 -2
  198. runbooks/security_baseline/report_template_en.html +7 -7
  199. runbooks/security_baseline/report_template_jp.html +7 -7
  200. runbooks/security_baseline/report_template_kr.html +12 -12
  201. runbooks/security_baseline/report_template_vn.html +7 -7
  202. runbooks/security_baseline/requirements.txt +7 -0
  203. runbooks/security_baseline/run_script.py +8 -2
  204. runbooks/security_baseline/security_baseline_tester.py +10 -2
  205. runbooks/security_baseline/utils/common.py +5 -1
  206. runbooks/utils/__init__.py +204 -0
  207. runbooks-0.6.1.dist-info/METADATA +373 -0
  208. runbooks-0.6.1.dist-info/RECORD +237 -0
  209. {runbooks-0.2.5.dist-info → runbooks-0.6.1.dist-info}/WHEEL +1 -1
  210. runbooks-0.6.1.dist-info/entry_points.txt +7 -0
  211. runbooks-0.6.1.dist-info/licenses/LICENSE +201 -0
  212. runbooks-0.6.1.dist-info/top_level.txt +3 -0
  213. runbooks/python101/calculator.py +0 -34
  214. runbooks/python101/config.py +0 -1
  215. runbooks/python101/exceptions.py +0 -16
  216. runbooks/python101/file_manager.py +0 -218
  217. runbooks/python101/toolkit.py +0 -153
  218. runbooks-0.2.5.dist-info/METADATA +0 -439
  219. runbooks-0.2.5.dist-info/RECORD +0 -61
  220. runbooks-0.2.5.dist-info/entry_points.txt +0 -3
  221. runbooks-0.2.5.dist-info/top_level.txt +0 -1
@@ -0,0 +1,505 @@
1
+ """
2
+ Unit tests for CFAT data models.
3
+
4
+ Tests comprehensive validation, serialization, and business logic
5
+ of enhanced Pydantic models including field validation, type checking,
6
+ and model consistency.
7
+ """
8
+
9
+ import json
10
+ from datetime import datetime
11
+ from typing import Any, Dict
12
+
13
+ import pytest
14
+
15
+ from runbooks.cfat.models import (
16
+ AssessmentConfig,
17
+ AssessmentReport,
18
+ AssessmentResult,
19
+ AssessmentSummary,
20
+ CheckConfig,
21
+ CheckStatus,
22
+ Severity,
23
+ )
24
+ from runbooks.cfat.tests import (
25
+ TEST_ACCOUNT_ID,
26
+ TEST_PROFILE,
27
+ TEST_REGION,
28
+ create_sample_assessment_report,
29
+ create_sample_assessment_result,
30
+ create_sample_assessment_summary,
31
+ )
32
+
33
+
34
+ class TestSeverityEnum:
35
+ """Test Severity enumeration."""
36
+
37
+ def test_severity_values(self):
38
+ """Test severity enum values."""
39
+ assert Severity.INFO == "INFO"
40
+ assert Severity.WARNING == "WARNING"
41
+ assert Severity.CRITICAL == "CRITICAL"
42
+
43
+ def test_severity_ordering(self):
44
+ """Test severity enum values."""
45
+ # String enums can't be compared directly, but we can check the values
46
+ severity_order = {Severity.INFO: 0, Severity.WARNING: 1, Severity.CRITICAL: 2}
47
+ assert severity_order[Severity.INFO] < severity_order[Severity.WARNING] < severity_order[Severity.CRITICAL]
48
+
49
+
50
+ class TestCheckStatusEnum:
51
+ """Test CheckStatus enumeration."""
52
+
53
+ def test_status_values(self):
54
+ """Test status enum values."""
55
+ assert CheckStatus.PASS == "PASS"
56
+ assert CheckStatus.FAIL == "FAIL"
57
+ assert CheckStatus.SKIP == "SKIP"
58
+ assert CheckStatus.ERROR == "ERROR"
59
+
60
+
61
+ @pytest.mark.models
62
+ class TestAssessmentResult:
63
+ """Test AssessmentResult model."""
64
+
65
+ def test_create_valid_result(self):
66
+ """Test creating valid assessment result."""
67
+ result = create_sample_assessment_result()
68
+
69
+ assert result.finding_id == "TEST-001"
70
+ assert result.check_name == "test_check"
71
+ assert result.check_category == "test"
72
+ assert result.status == CheckStatus.PASS
73
+ assert result.severity == Severity.INFO
74
+ assert result.message == "Test check passed"
75
+ assert result.execution_time == 0.1
76
+ assert isinstance(result.timestamp, datetime)
77
+
78
+ def test_result_properties(self):
79
+ """Test assessment result properties."""
80
+ passed_result = create_sample_assessment_result(status=CheckStatus.PASS)
81
+ assert passed_result.passed is True
82
+ assert passed_result.failed is False
83
+
84
+ failed_result = create_sample_assessment_result(status=CheckStatus.FAIL)
85
+ assert failed_result.passed is False
86
+ assert failed_result.failed is True
87
+
88
+ def test_severity_properties(self):
89
+ """Test severity-based properties."""
90
+ critical_result = create_sample_assessment_result(severity=Severity.CRITICAL)
91
+ assert critical_result.is_critical is True
92
+ assert critical_result.is_warning is False
93
+
94
+ warning_result = create_sample_assessment_result(severity=Severity.WARNING)
95
+ assert warning_result.is_critical is False
96
+ assert warning_result.is_warning is True
97
+
98
+ def test_finding_id_validation(self):
99
+ """Test finding ID validation and formatting."""
100
+ result = AssessmentResult(
101
+ finding_id="iam-001", # lowercase
102
+ check_name="test",
103
+ check_category="iam",
104
+ status=CheckStatus.PASS,
105
+ severity=Severity.INFO,
106
+ message="test",
107
+ execution_time=0.1,
108
+ )
109
+ # Should be converted to uppercase
110
+ assert result.finding_id == "IAM-001"
111
+
112
+ def test_finding_id_empty_validation(self):
113
+ """Test finding ID cannot be empty."""
114
+ from pydantic import ValidationError
115
+
116
+ with pytest.raises(ValidationError, match="String should have at least 1 character"):
117
+ AssessmentResult(
118
+ finding_id="",
119
+ check_name="test",
120
+ check_category="iam",
121
+ status=CheckStatus.PASS,
122
+ severity=Severity.INFO,
123
+ message="test",
124
+ execution_time=0.1,
125
+ )
126
+
127
+ def test_arn_validation(self):
128
+ """Test AWS ARN validation."""
129
+ # Valid ARN should pass
130
+ result = AssessmentResult(
131
+ finding_id="TEST-001",
132
+ check_name="test",
133
+ check_category="iam",
134
+ status=CheckStatus.PASS,
135
+ severity=Severity.INFO,
136
+ message="test",
137
+ resource_arn="arn:aws:iam::123456789012:user/test",
138
+ execution_time=0.1,
139
+ )
140
+ assert result.resource_arn == "arn:aws:iam::123456789012:user/test"
141
+
142
+ # Invalid ARN should log warning but not fail
143
+ result = AssessmentResult(
144
+ finding_id="TEST-002",
145
+ check_name="test",
146
+ check_category="iam",
147
+ status=CheckStatus.PASS,
148
+ severity=Severity.INFO,
149
+ message="test",
150
+ resource_arn="invalid-arn",
151
+ execution_time=0.1,
152
+ )
153
+ assert result.resource_arn == "invalid-arn"
154
+
155
+ def test_add_recommendation(self):
156
+ """Test adding recommendations."""
157
+ result = create_sample_assessment_result()
158
+
159
+ result.add_recommendation("New recommendation")
160
+ assert "New recommendation" in result.recommendations
161
+
162
+ # Adding duplicate should not create duplicate
163
+ result.add_recommendation("New recommendation")
164
+ assert result.recommendations.count("New recommendation") == 1
165
+
166
+ def test_category_prefix(self):
167
+ """Test category prefix extraction."""
168
+ result = create_sample_assessment_result(finding_id="IAM-001")
169
+ assert result.category_prefix == "IAM"
170
+
171
+ result_no_dash = create_sample_assessment_result(finding_id="NOHYPHEN")
172
+ assert result_no_dash.category_prefix == "TEST" # Falls back to category
173
+
174
+ def test_to_dict(self):
175
+ """Test dictionary conversion."""
176
+ result = create_sample_assessment_result()
177
+ result_dict = result.to_dict()
178
+
179
+ assert isinstance(result_dict, dict)
180
+ assert result_dict["finding_id"] == "TEST-001"
181
+ assert result_dict["check_name"] == "test_check"
182
+ assert result_dict["status"] == "PASS"
183
+
184
+ def test_serialization(self):
185
+ """Test JSON serialization."""
186
+ result = create_sample_assessment_result()
187
+
188
+ # Should be able to serialize to JSON
189
+ json_str = json.dumps(result.model_dump(), default=str)
190
+ assert isinstance(json_str, str)
191
+
192
+ # Should be able to deserialize
193
+ data = json.loads(json_str)
194
+ assert data["finding_id"] == "TEST-001"
195
+
196
+
197
+ @pytest.mark.models
198
+ class TestAssessmentSummary:
199
+ """Test AssessmentSummary model."""
200
+
201
+ def test_create_valid_summary(self):
202
+ """Test creating valid assessment summary."""
203
+ summary = create_sample_assessment_summary()
204
+
205
+ assert summary.total_checks == 10
206
+ assert summary.passed_checks == 8
207
+ assert summary.failed_checks == 2
208
+ assert summary.pass_rate == 80.0
209
+
210
+ def test_pass_rate_calculation(self):
211
+ """Test pass rate calculation."""
212
+ summary = create_sample_assessment_summary(total_checks=4, passed_checks=3, failed_checks=1)
213
+ assert summary.pass_rate == 75.0
214
+
215
+ # Edge case: no checks
216
+ empty_summary = create_sample_assessment_summary(total_checks=0, passed_checks=0, failed_checks=0)
217
+ assert empty_summary.pass_rate == 0.0
218
+
219
+ def test_failure_rate_calculation(self):
220
+ """Test failure rate calculation."""
221
+ summary = create_sample_assessment_summary(total_checks=10, passed_checks=7, failed_checks=3)
222
+ assert summary.failure_rate == 30.0
223
+
224
+ def test_compliance_score_calculation(self):
225
+ """Test compliance score calculation with weighted severity."""
226
+ # Perfect score scenario
227
+ perfect_summary = create_sample_assessment_summary(
228
+ total_checks=5, passed_checks=5, failed_checks=0, critical_issues=0
229
+ )
230
+ assert perfect_summary.compliance_score == 100
231
+
232
+ # Critical issues should heavily penalize score
233
+ critical_summary = create_sample_assessment_summary(
234
+ total_checks=5, passed_checks=3, failed_checks=2, critical_issues=2
235
+ )
236
+ assert critical_summary.compliance_score < 50 # Should be significantly penalized
237
+
238
+ def test_risk_level_assessment(self):
239
+ """Test risk level assessment."""
240
+ # High risk: has critical issues
241
+ high_risk = create_sample_assessment_summary(critical_issues=1)
242
+ assert high_risk.risk_level == "HIGH"
243
+
244
+ # Medium risk: low compliance score
245
+ medium_risk = create_sample_assessment_summary(
246
+ total_checks=10, passed_checks=3, failed_checks=7, critical_issues=0
247
+ )
248
+ assert medium_risk.risk_level == "MEDIUM"
249
+
250
+ # Low risk: decent compliance score
251
+ low_risk = create_sample_assessment_summary(
252
+ total_checks=10, passed_checks=9, failed_checks=1, critical_issues=0, warnings=0
253
+ )
254
+ assert low_risk.risk_level in ["LOW", "MINIMAL"]
255
+
256
+ def test_execution_summary(self):
257
+ """Test execution summary generation."""
258
+ summary = create_sample_assessment_summary(total_checks=10, total_execution_time=20.0)
259
+
260
+ exec_summary = summary.execution_summary
261
+ assert "10 checks" in exec_summary
262
+ assert "20.0s" in exec_summary
263
+ assert "2.00s per check" in exec_summary
264
+
265
+ def test_avg_execution_time(self):
266
+ """Test average execution time calculation."""
267
+ summary = create_sample_assessment_summary(total_checks=5, total_execution_time=10.0)
268
+ assert summary.avg_execution_time == 2.0
269
+
270
+ # Edge case: no checks
271
+ empty_summary = create_sample_assessment_summary(total_checks=0, total_execution_time=0.0)
272
+ assert empty_summary.avg_execution_time == 0.0
273
+
274
+
275
+ @pytest.mark.models
276
+ class TestAssessmentReport:
277
+ """Test AssessmentReport model."""
278
+
279
+ def test_create_valid_report(self):
280
+ """Test creating valid assessment report."""
281
+ report = create_sample_assessment_report()
282
+
283
+ assert report.account_id == TEST_ACCOUNT_ID
284
+ assert report.region == TEST_REGION
285
+ assert report.profile == "test"
286
+ assert len(report.results) == 5
287
+ assert isinstance(report.summary, AssessmentSummary)
288
+ assert isinstance(report.timestamp, datetime)
289
+
290
+ def test_account_id_validation(self):
291
+ """Test AWS account ID validation."""
292
+ # Valid 12-digit account ID
293
+ report = AssessmentReport(
294
+ account_id="123456789012",
295
+ region="us-east-1",
296
+ profile="test",
297
+ version="0.5.0",
298
+ included_checks=["test"],
299
+ severity_threshold=Severity.INFO,
300
+ results=[],
301
+ summary=create_sample_assessment_summary(),
302
+ )
303
+ assert report.account_id == "123456789012"
304
+
305
+ # Invalid account ID should raise validation error
306
+ from pydantic import ValidationError
307
+
308
+ with pytest.raises(ValidationError):
309
+ AssessmentReport(
310
+ account_id="invalid",
311
+ region="us-east-1",
312
+ profile="test",
313
+ version="0.5.0",
314
+ included_checks=["test"],
315
+ severity_threshold=Severity.INFO,
316
+ results=[],
317
+ summary=create_sample_assessment_summary(),
318
+ )
319
+
320
+ def test_query_methods(self):
321
+ """Test report query methods."""
322
+ report = create_sample_assessment_report(num_results=10)
323
+
324
+ # Test category filtering
325
+ test_results = report.get_results_by_category("test")
326
+ assert len(test_results) == 10 # All results are in "test" category
327
+
328
+ empty_results = report.get_results_by_category("nonexistent")
329
+ assert len(empty_results) == 0
330
+
331
+ # Test severity filtering
332
+ critical_results = report.get_results_by_severity(Severity.CRITICAL)
333
+ assert len(critical_results) > 0
334
+
335
+ # Test failed results
336
+ failed_results = report.get_failed_results()
337
+ assert len(failed_results) > 0
338
+
339
+ # Test passed results
340
+ passed_results = report.get_passed_results()
341
+ assert len(passed_results) > 0
342
+
343
+ # Test critical results
344
+ critical_results = report.get_critical_results()
345
+ assert len(critical_results) > 0
346
+
347
+ def test_categories_extraction(self):
348
+ """Test category extraction from results."""
349
+ report = create_sample_assessment_report()
350
+ categories = report.get_categories()
351
+
352
+ assert isinstance(categories, list)
353
+ assert "test" in categories
354
+ assert categories == sorted(categories) # Should be sorted
355
+
356
+ def test_category_summary(self):
357
+ """Test category summary generation."""
358
+ report = create_sample_assessment_report(num_results=6)
359
+ category_summary = report.get_category_summary()
360
+
361
+ assert isinstance(category_summary, dict)
362
+ assert "test" in category_summary
363
+
364
+ test_stats = category_summary["test"]
365
+ assert "total" in test_stats
366
+ assert "passed" in test_stats
367
+ assert "failed" in test_stats
368
+ assert "critical" in test_stats
369
+ assert test_stats["total"] == 6
370
+
371
+ def test_report_export_methods(self):
372
+ """Test report export method signatures."""
373
+ report = create_sample_assessment_report()
374
+
375
+ # These should not raise exceptions (actual file operations tested separately)
376
+ assert hasattr(report, "to_html")
377
+ assert hasattr(report, "to_json")
378
+ assert hasattr(report, "to_csv")
379
+ assert hasattr(report, "to_markdown")
380
+
381
+
382
+ @pytest.mark.models
383
+ class TestCheckConfig:
384
+ """Test CheckConfig model."""
385
+
386
+ def test_create_valid_config(self):
387
+ """Test creating valid check configuration."""
388
+ config = CheckConfig(
389
+ name="iam_root_mfa",
390
+ enabled=True,
391
+ severity=Severity.CRITICAL,
392
+ timeout=30,
393
+ description="Check root MFA",
394
+ category="iam",
395
+ )
396
+
397
+ assert config.name == "iam_root_mfa"
398
+ assert config.enabled is True
399
+ assert config.severity == Severity.CRITICAL
400
+ assert config.timeout == 30
401
+
402
+ def test_name_validation_and_normalization(self):
403
+ """Test check name validation and normalization."""
404
+ config = CheckConfig(name="IAM Root MFA")
405
+ assert config.name == "iam_root_mfa" # Should be normalized
406
+
407
+ config = CheckConfig(name="vpc-flow-logs")
408
+ assert config.name == "vpc_flow_logs" # Hyphens to underscores
409
+
410
+ def test_parameter_methods(self):
411
+ """Test parameter get/set methods."""
412
+ config = CheckConfig(name="test_check")
413
+
414
+ # Test setting parameter
415
+ config.set_parameter("timeout", 60)
416
+ assert config.get_parameter("timeout") == 60
417
+
418
+ # Test default value
419
+ assert config.get_parameter("nonexistent", "default") == "default"
420
+ assert config.get_parameter("nonexistent") is None
421
+
422
+
423
+ @pytest.mark.models
424
+ class TestAssessmentConfig:
425
+ """Test AssessmentConfig model."""
426
+
427
+ def test_create_default_config(self):
428
+ """Test creating default assessment configuration."""
429
+ config = AssessmentConfig()
430
+
431
+ assert "iam" in config.included_categories
432
+ assert "vpc" in config.included_categories
433
+ assert config.parallel_execution is True
434
+ assert config.max_workers == 10
435
+ assert config.severity_threshold == Severity.WARNING
436
+
437
+ def test_max_workers_validation(self):
438
+ """Test max workers validation."""
439
+ # Valid worker count
440
+ config = AssessmentConfig(max_workers=5)
441
+ assert config.max_workers == 5
442
+
443
+ # Invalid worker count should raise error
444
+ from pydantic import ValidationError
445
+
446
+ with pytest.raises(ValidationError):
447
+ AssessmentConfig(max_workers=0)
448
+
449
+ def test_category_validation(self):
450
+ """Test category name validation and normalization."""
451
+ config = AssessmentConfig(included_categories=["IAM", "VPC", "CloudTrail"], excluded_categories=["EC2"])
452
+
453
+ # Should be normalized to lowercase
454
+ assert "iam" in config.included_categories
455
+ assert "vpc" in config.included_categories
456
+ assert "cloudtrail" in config.included_categories
457
+ assert "ec2" in config.excluded_categories
458
+
459
+ def test_check_config_methods(self):
460
+ """Test check configuration management."""
461
+ config = AssessmentConfig()
462
+
463
+ # Add check config
464
+ check_config = CheckConfig(name="test_check", severity=Severity.CRITICAL)
465
+ config.add_check_config("test_check", check_config)
466
+
467
+ retrieved = config.get_check_config("test_check")
468
+ assert retrieved.severity == Severity.CRITICAL
469
+
470
+ # Add via dictionary
471
+ config.add_check_config("another_check", {"severity": Severity.WARNING})
472
+ retrieved = config.get_check_config("another_check")
473
+ assert retrieved.severity == Severity.WARNING
474
+
475
+ # Remove config
476
+ assert config.remove_check_config("test_check") is True
477
+ assert config.remove_check_config("nonexistent") is False
478
+
479
+ def test_effective_checks_calculation(self):
480
+ """Test effective checks calculation."""
481
+ config = AssessmentConfig(included_categories=["iam"], excluded_checks=["iam_unused_credentials"])
482
+
483
+ available_checks = [
484
+ "iam_root_mfa",
485
+ "iam_unused_credentials",
486
+ "iam_password_policy",
487
+ "vpc_flow_logs",
488
+ "ec2_security_groups",
489
+ ]
490
+
491
+ effective = config.get_effective_checks(available_checks)
492
+
493
+ # Should include IAM checks but exclude the specific one
494
+ assert "iam_root_mfa" in effective
495
+ assert "iam_password_policy" in effective
496
+ assert "iam_unused_credentials" not in effective
497
+ assert "vpc_flow_logs" not in effective # Not in included categories
498
+
499
+ def test_to_dict(self):
500
+ """Test configuration serialization."""
501
+ config = AssessmentConfig(compliance_framework="SOC2")
502
+ config_dict = config.to_dict()
503
+
504
+ assert isinstance(config_dict, dict)
505
+ assert config_dict["compliance_framework"] == "SOC2"