runbooks 0.2.3__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.
- conftest.py +26 -0
- jupyter-agent/.env.template +2 -0
- jupyter-agent/.gitattributes +35 -0
- jupyter-agent/README.md +16 -0
- jupyter-agent/app.py +256 -0
- jupyter-agent/cloudops-agent.png +0 -0
- jupyter-agent/ds-system-prompt.txt +154 -0
- jupyter-agent/jupyter-agent.png +0 -0
- jupyter-agent/llama3_template.jinja +123 -0
- jupyter-agent/requirements.txt +9 -0
- jupyter-agent/utils.py +409 -0
- runbooks/__init__.py +71 -3
- runbooks/__main__.py +13 -0
- runbooks/aws/ec2_describe_instances.py +1 -1
- runbooks/aws/ec2_run_instances.py +8 -2
- runbooks/aws/ec2_start_stop_instances.py +17 -4
- runbooks/aws/ec2_unused_volumes.py +5 -1
- runbooks/aws/s3_create_bucket.py +4 -2
- runbooks/aws/s3_list_objects.py +6 -1
- runbooks/aws/tagging_lambda_handler.py +13 -2
- runbooks/aws/tags.json +12 -0
- runbooks/base.py +353 -0
- runbooks/cfat/README.md +49 -0
- runbooks/cfat/__init__.py +74 -0
- runbooks/cfat/app.ts +644 -0
- runbooks/cfat/assessment/__init__.py +40 -0
- runbooks/cfat/assessment/asana-import.csv +39 -0
- runbooks/cfat/assessment/cfat-checks.csv +31 -0
- runbooks/cfat/assessment/cfat.txt +520 -0
- runbooks/cfat/assessment/collectors.py +200 -0
- runbooks/cfat/assessment/jira-import.csv +39 -0
- runbooks/cfat/assessment/runner.py +387 -0
- runbooks/cfat/assessment/validators.py +290 -0
- runbooks/cfat/cli.py +103 -0
- runbooks/cfat/docs/asana-import.csv +24 -0
- runbooks/cfat/docs/cfat-checks.csv +31 -0
- runbooks/cfat/docs/cfat.txt +335 -0
- runbooks/cfat/docs/checks-output.png +0 -0
- runbooks/cfat/docs/cloudshell-console-run.png +0 -0
- runbooks/cfat/docs/cloudshell-download.png +0 -0
- runbooks/cfat/docs/cloudshell-output.png +0 -0
- runbooks/cfat/docs/downloadfile.png +0 -0
- runbooks/cfat/docs/jira-import.csv +24 -0
- runbooks/cfat/docs/open-cloudshell.png +0 -0
- runbooks/cfat/docs/report-header.png +0 -0
- runbooks/cfat/models.py +1026 -0
- runbooks/cfat/package-lock.json +5116 -0
- runbooks/cfat/package.json +38 -0
- runbooks/cfat/report.py +496 -0
- runbooks/cfat/reporting/__init__.py +46 -0
- runbooks/cfat/reporting/exporters.py +337 -0
- runbooks/cfat/reporting/formatters.py +496 -0
- runbooks/cfat/reporting/templates.py +135 -0
- runbooks/cfat/run-assessment.sh +23 -0
- runbooks/cfat/runner.py +69 -0
- runbooks/cfat/src/actions/check-cloudtrail-existence.ts +43 -0
- runbooks/cfat/src/actions/check-config-existence.ts +37 -0
- runbooks/cfat/src/actions/check-control-tower.ts +37 -0
- runbooks/cfat/src/actions/check-ec2-existence.ts +46 -0
- runbooks/cfat/src/actions/check-iam-users.ts +50 -0
- runbooks/cfat/src/actions/check-legacy-cur.ts +30 -0
- runbooks/cfat/src/actions/check-org-cloudformation.ts +30 -0
- runbooks/cfat/src/actions/check-vpc-existence.ts +43 -0
- runbooks/cfat/src/actions/create-asanaimport.ts +14 -0
- runbooks/cfat/src/actions/create-backlog.ts +372 -0
- runbooks/cfat/src/actions/create-jiraimport.ts +15 -0
- runbooks/cfat/src/actions/create-report.ts +616 -0
- runbooks/cfat/src/actions/define-account-type.ts +51 -0
- runbooks/cfat/src/actions/get-enabled-org-policy-types.ts +40 -0
- runbooks/cfat/src/actions/get-enabled-org-services.ts +26 -0
- runbooks/cfat/src/actions/get-idc-info.ts +34 -0
- runbooks/cfat/src/actions/get-org-da-accounts.ts +34 -0
- runbooks/cfat/src/actions/get-org-details.ts +35 -0
- runbooks/cfat/src/actions/get-org-member-accounts.ts +44 -0
- runbooks/cfat/src/actions/get-org-ous.ts +35 -0
- runbooks/cfat/src/actions/get-regions.ts +22 -0
- runbooks/cfat/src/actions/zip-assessment.ts +27 -0
- runbooks/cfat/src/types/index.d.ts +147 -0
- runbooks/cfat/tests/__init__.py +141 -0
- runbooks/cfat/tests/test_cli.py +340 -0
- runbooks/cfat/tests/test_integration.py +290 -0
- runbooks/cfat/tests/test_models.py +505 -0
- runbooks/cfat/tests/test_reporting.py +354 -0
- runbooks/cfat/tsconfig.json +16 -0
- runbooks/cfat/webpack.config.cjs +27 -0
- runbooks/config.py +260 -0
- runbooks/finops/__init__.py +88 -0
- runbooks/finops/aws_client.py +245 -0
- runbooks/finops/cli.py +151 -0
- runbooks/finops/cost_processor.py +410 -0
- runbooks/finops/dashboard_runner.py +448 -0
- runbooks/finops/helpers.py +355 -0
- runbooks/finops/main.py +14 -0
- runbooks/finops/profile_processor.py +174 -0
- runbooks/finops/types.py +66 -0
- runbooks/finops/visualisations.py +80 -0
- runbooks/inventory/.gitignore +354 -0
- runbooks/inventory/ArgumentsClass.py +261 -0
- runbooks/inventory/Inventory_Modules.py +6130 -0
- runbooks/inventory/LandingZone/delete_lz.py +1075 -0
- runbooks/inventory/README.md +1320 -0
- runbooks/inventory/__init__.py +62 -0
- runbooks/inventory/account_class.py +532 -0
- runbooks/inventory/all_my_instances_wrapper.py +123 -0
- runbooks/inventory/aws_decorators.py +201 -0
- runbooks/inventory/cfn_move_stack_instances.py +1526 -0
- runbooks/inventory/check_cloudtrail_compliance.py +614 -0
- runbooks/inventory/check_controltower_readiness.py +1107 -0
- runbooks/inventory/check_landingzone_readiness.py +711 -0
- runbooks/inventory/cloudtrail.md +727 -0
- runbooks/inventory/collectors/__init__.py +20 -0
- runbooks/inventory/collectors/aws_compute.py +518 -0
- runbooks/inventory/collectors/aws_networking.py +275 -0
- runbooks/inventory/collectors/base.py +222 -0
- runbooks/inventory/core/__init__.py +19 -0
- runbooks/inventory/core/collector.py +303 -0
- runbooks/inventory/core/formatter.py +296 -0
- runbooks/inventory/delete_s3_buckets_objects.py +169 -0
- runbooks/inventory/discovery.md +81 -0
- runbooks/inventory/draw_org_structure.py +748 -0
- runbooks/inventory/ec2_vpc_utils.py +341 -0
- runbooks/inventory/find_cfn_drift_detection.py +272 -0
- runbooks/inventory/find_cfn_orphaned_stacks.py +719 -0
- runbooks/inventory/find_cfn_stackset_drift.py +733 -0
- runbooks/inventory/find_ec2_security_groups.py +669 -0
- runbooks/inventory/find_landingzone_versions.py +201 -0
- runbooks/inventory/find_vpc_flow_logs.py +1221 -0
- runbooks/inventory/inventory.sh +659 -0
- runbooks/inventory/list_cfn_stacks.py +558 -0
- runbooks/inventory/list_cfn_stackset_operation_results.py +252 -0
- runbooks/inventory/list_cfn_stackset_operations.py +734 -0
- runbooks/inventory/list_cfn_stacksets.py +453 -0
- runbooks/inventory/list_config_recorders_delivery_channels.py +681 -0
- runbooks/inventory/list_ds_directories.py +354 -0
- runbooks/inventory/list_ec2_availability_zones.py +286 -0
- runbooks/inventory/list_ec2_ebs_volumes.py +244 -0
- runbooks/inventory/list_ec2_instances.py +425 -0
- runbooks/inventory/list_ecs_clusters_and_tasks.py +562 -0
- runbooks/inventory/list_elbs_load_balancers.py +411 -0
- runbooks/inventory/list_enis_network_interfaces.py +526 -0
- runbooks/inventory/list_guardduty_detectors.py +568 -0
- runbooks/inventory/list_iam_policies.py +404 -0
- runbooks/inventory/list_iam_roles.py +518 -0
- runbooks/inventory/list_iam_saml_providers.py +359 -0
- runbooks/inventory/list_lambda_functions.py +882 -0
- runbooks/inventory/list_org_accounts.py +446 -0
- runbooks/inventory/list_org_accounts_users.py +354 -0
- runbooks/inventory/list_rds_db_instances.py +406 -0
- runbooks/inventory/list_route53_hosted_zones.py +318 -0
- runbooks/inventory/list_servicecatalog_provisioned_products.py +575 -0
- runbooks/inventory/list_sns_topics.py +360 -0
- runbooks/inventory/list_ssm_parameters.py +402 -0
- runbooks/inventory/list_vpc_subnets.py +433 -0
- runbooks/inventory/list_vpcs.py +422 -0
- runbooks/inventory/lockdown_cfn_stackset_role.py +224 -0
- runbooks/inventory/models/__init__.py +24 -0
- runbooks/inventory/models/account.py +192 -0
- runbooks/inventory/models/inventory.py +309 -0
- runbooks/inventory/models/resource.py +247 -0
- runbooks/inventory/recover_cfn_stack_ids.py +205 -0
- runbooks/inventory/requirements.txt +12 -0
- runbooks/inventory/run_on_multi_accounts.py +211 -0
- runbooks/inventory/tests/common_test_data.py +3661 -0
- runbooks/inventory/tests/common_test_functions.py +204 -0
- runbooks/inventory/tests/script_test_data.py +0 -0
- runbooks/inventory/tests/setup.py +24 -0
- runbooks/inventory/tests/src.py +18 -0
- runbooks/inventory/tests/test_cfn_describe_stacks.py +208 -0
- runbooks/inventory/tests/test_ec2_describe_instances.py +162 -0
- runbooks/inventory/tests/test_inventory_modules.py +55 -0
- runbooks/inventory/tests/test_lambda_list_functions.py +86 -0
- runbooks/inventory/tests/test_moto_integration_example.py +273 -0
- runbooks/inventory/tests/test_org_list_accounts.py +49 -0
- runbooks/inventory/update_aws_actions.py +173 -0
- runbooks/inventory/update_cfn_stacksets.py +1215 -0
- runbooks/inventory/update_cloudwatch_logs_retention_policy.py +294 -0
- runbooks/inventory/update_iam_roles_cross_accounts.py +478 -0
- runbooks/inventory/update_s3_public_access_block.py +539 -0
- runbooks/inventory/utils/__init__.py +23 -0
- runbooks/inventory/utils/aws_helpers.py +510 -0
- runbooks/inventory/utils/threading_utils.py +493 -0
- runbooks/inventory/utils/validation.py +682 -0
- runbooks/inventory/verify_ec2_security_groups.py +1430 -0
- runbooks/main.py +785 -0
- runbooks/organizations/__init__.py +12 -0
- runbooks/organizations/manager.py +374 -0
- runbooks/security_baseline/README.md +324 -0
- runbooks/security_baseline/checklist/alternate_contacts.py +8 -1
- runbooks/security_baseline/checklist/bucket_public_access.py +4 -1
- runbooks/security_baseline/checklist/cloudwatch_alarm_configuration.py +9 -2
- runbooks/security_baseline/checklist/guardduty_enabled.py +9 -2
- runbooks/security_baseline/checklist/multi_region_instance_usage.py +5 -1
- runbooks/security_baseline/checklist/root_access_key.py +6 -1
- runbooks/security_baseline/config-origin.json +1 -1
- runbooks/security_baseline/config.json +1 -1
- runbooks/security_baseline/permission.json +1 -1
- runbooks/security_baseline/report_generator.py +10 -2
- runbooks/security_baseline/report_template_en.html +8 -8
- runbooks/security_baseline/report_template_jp.html +8 -8
- runbooks/security_baseline/report_template_kr.html +13 -13
- runbooks/security_baseline/report_template_vn.html +8 -8
- runbooks/security_baseline/requirements.txt +7 -0
- runbooks/security_baseline/run_script.py +8 -2
- runbooks/security_baseline/security_baseline_tester.py +10 -2
- runbooks/security_baseline/utils/common.py +5 -1
- runbooks/utils/__init__.py +204 -0
- runbooks-0.6.1.dist-info/METADATA +373 -0
- runbooks-0.6.1.dist-info/RECORD +237 -0
- {runbooks-0.2.3.dist-info → runbooks-0.6.1.dist-info}/WHEEL +1 -1
- runbooks-0.6.1.dist-info/entry_points.txt +7 -0
- runbooks-0.6.1.dist-info/licenses/LICENSE +201 -0
- runbooks-0.6.1.dist-info/top_level.txt +3 -0
- runbooks/python101/calculator.py +0 -34
- runbooks/python101/config.py +0 -1
- runbooks/python101/exceptions.py +0 -16
- runbooks/python101/file_manager.py +0 -218
- runbooks/python101/toolkit.py +0 -153
- runbooks-0.2.3.dist-info/METADATA +0 -435
- runbooks-0.2.3.dist-info/RECORD +0 -61
- runbooks-0.2.3.dist-info/entry_points.txt +0 -3
- runbooks-0.2.3.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"
|