regscale-cli 6.27.3.0__py3-none-any.whl → 6.28.0.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.
Potentially problematic release.
This version of regscale-cli might be problematic. Click here for more details.
- regscale/_version.py +1 -1
- regscale/core/app/utils/app_utils.py +11 -2
- regscale/dev/cli.py +26 -0
- regscale/dev/version.py +72 -0
- regscale/integrations/commercial/__init__.py +15 -1
- regscale/integrations/commercial/amazon/amazon/__init__.py +0 -0
- regscale/integrations/commercial/amazon/amazon/common.py +204 -0
- regscale/integrations/commercial/amazon/common.py +48 -58
- regscale/integrations/commercial/aws/audit_manager_compliance.py +2671 -0
- regscale/integrations/commercial/aws/cli.py +3093 -55
- regscale/integrations/commercial/aws/cloudtrail_control_mappings.py +333 -0
- regscale/integrations/commercial/aws/cloudtrail_evidence.py +501 -0
- regscale/integrations/commercial/aws/cloudwatch_control_mappings.py +357 -0
- regscale/integrations/commercial/aws/cloudwatch_evidence.py +490 -0
- regscale/integrations/commercial/aws/config_compliance.py +914 -0
- regscale/integrations/commercial/aws/conformance_pack_mappings.py +198 -0
- regscale/integrations/commercial/aws/evidence_generator.py +283 -0
- regscale/integrations/commercial/aws/guardduty_control_mappings.py +340 -0
- regscale/integrations/commercial/aws/guardduty_evidence.py +1053 -0
- regscale/integrations/commercial/aws/iam_control_mappings.py +368 -0
- regscale/integrations/commercial/aws/iam_evidence.py +574 -0
- regscale/integrations/commercial/aws/inventory/__init__.py +223 -22
- regscale/integrations/commercial/aws/inventory/base.py +107 -5
- regscale/integrations/commercial/aws/inventory/resources/audit_manager.py +513 -0
- regscale/integrations/commercial/aws/inventory/resources/cloudtrail.py +315 -0
- regscale/integrations/commercial/aws/inventory/resources/cloudtrail_logs_metadata.py +476 -0
- regscale/integrations/commercial/aws/inventory/resources/cloudwatch.py +191 -0
- regscale/integrations/commercial/aws/inventory/resources/compute.py +66 -9
- regscale/integrations/commercial/aws/inventory/resources/config.py +464 -0
- regscale/integrations/commercial/aws/inventory/resources/containers.py +74 -9
- regscale/integrations/commercial/aws/inventory/resources/database.py +106 -31
- regscale/integrations/commercial/aws/inventory/resources/guardduty.py +286 -0
- regscale/integrations/commercial/aws/inventory/resources/iam.py +470 -0
- regscale/integrations/commercial/aws/inventory/resources/inspector.py +476 -0
- regscale/integrations/commercial/aws/inventory/resources/integration.py +175 -61
- regscale/integrations/commercial/aws/inventory/resources/kms.py +447 -0
- regscale/integrations/commercial/aws/inventory/resources/networking.py +103 -67
- regscale/integrations/commercial/aws/inventory/resources/s3.py +394 -0
- regscale/integrations/commercial/aws/inventory/resources/security.py +268 -72
- regscale/integrations/commercial/aws/inventory/resources/securityhub.py +473 -0
- regscale/integrations/commercial/aws/inventory/resources/storage.py +53 -29
- regscale/integrations/commercial/aws/inventory/resources/systems_manager.py +657 -0
- regscale/integrations/commercial/aws/inventory/resources/vpc.py +655 -0
- regscale/integrations/commercial/aws/kms_control_mappings.py +288 -0
- regscale/integrations/commercial/aws/kms_evidence.py +879 -0
- regscale/integrations/commercial/aws/ocsf/__init__.py +7 -0
- regscale/integrations/commercial/aws/ocsf/constants.py +115 -0
- regscale/integrations/commercial/aws/ocsf/mapper.py +435 -0
- regscale/integrations/commercial/aws/org_control_mappings.py +286 -0
- regscale/integrations/commercial/aws/org_evidence.py +666 -0
- regscale/integrations/commercial/aws/s3_control_mappings.py +356 -0
- regscale/integrations/commercial/aws/s3_evidence.py +632 -0
- regscale/integrations/commercial/aws/scanner.py +851 -206
- regscale/integrations/commercial/aws/security_hub.py +319 -0
- regscale/integrations/commercial/aws/session_manager.py +282 -0
- regscale/integrations/commercial/aws/ssm_control_mappings.py +291 -0
- regscale/integrations/commercial/aws/ssm_evidence.py +492 -0
- regscale/integrations/compliance_integration.py +308 -38
- regscale/integrations/due_date_handler.py +3 -0
- regscale/integrations/scanner_integration.py +399 -84
- regscale/models/integration_models/cisa_kev_data.json +34 -4
- regscale/models/integration_models/synqly_models/capabilities.json +1 -1
- regscale/models/integration_models/synqly_models/connectors/vulnerabilities.py +17 -9
- regscale/models/regscale_models/assessment.py +2 -1
- regscale/models/regscale_models/control_objective.py +74 -5
- regscale/models/regscale_models/file.py +2 -0
- regscale/models/regscale_models/issue.py +2 -5
- {regscale_cli-6.27.3.0.dist-info → regscale_cli-6.28.0.0.dist-info}/METADATA +1 -1
- {regscale_cli-6.27.3.0.dist-info → regscale_cli-6.28.0.0.dist-info}/RECORD +112 -33
- tests/regscale/integrations/commercial/aws/__init__.py +0 -0
- tests/regscale/integrations/commercial/aws/test_audit_manager_compliance.py +1304 -0
- tests/regscale/integrations/commercial/aws/test_audit_manager_evidence_aggregation.py +341 -0
- tests/regscale/integrations/commercial/aws/test_aws_audit_manager_collector.py +1155 -0
- tests/regscale/integrations/commercial/aws/test_aws_cloudtrail_collector.py +534 -0
- tests/regscale/integrations/commercial/aws/test_aws_config_collector.py +400 -0
- tests/regscale/integrations/commercial/aws/test_aws_guardduty_collector.py +315 -0
- tests/regscale/integrations/commercial/aws/test_aws_iam_collector.py +458 -0
- tests/regscale/integrations/commercial/aws/test_aws_inspector_collector.py +353 -0
- tests/regscale/integrations/commercial/aws/test_aws_inventory_integration.py +530 -0
- tests/regscale/integrations/commercial/aws/test_aws_kms_collector.py +919 -0
- tests/regscale/integrations/commercial/aws/test_aws_s3_collector.py +722 -0
- tests/regscale/integrations/commercial/aws/test_aws_scanner_integration.py +722 -0
- tests/regscale/integrations/commercial/aws/test_aws_securityhub_collector.py +792 -0
- tests/regscale/integrations/commercial/aws/test_aws_systems_manager_collector.py +918 -0
- tests/regscale/integrations/commercial/aws/test_aws_vpc_collector.py +996 -0
- tests/regscale/integrations/commercial/aws/test_cli_evidence.py +431 -0
- tests/regscale/integrations/commercial/aws/test_cloudtrail_control_mappings.py +452 -0
- tests/regscale/integrations/commercial/aws/test_cloudtrail_evidence.py +788 -0
- tests/regscale/integrations/commercial/aws/test_config_compliance.py +298 -0
- tests/regscale/integrations/commercial/aws/test_conformance_pack_mappings.py +200 -0
- tests/regscale/integrations/commercial/aws/test_evidence_generator.py +386 -0
- tests/regscale/integrations/commercial/aws/test_guardduty_control_mappings.py +564 -0
- tests/regscale/integrations/commercial/aws/test_guardduty_evidence.py +1041 -0
- tests/regscale/integrations/commercial/aws/test_iam_control_mappings.py +718 -0
- tests/regscale/integrations/commercial/aws/test_iam_evidence.py +1375 -0
- tests/regscale/integrations/commercial/aws/test_kms_control_mappings.py +656 -0
- tests/regscale/integrations/commercial/aws/test_kms_evidence.py +1163 -0
- tests/regscale/integrations/commercial/aws/test_ocsf_mapper.py +370 -0
- tests/regscale/integrations/commercial/aws/test_org_control_mappings.py +546 -0
- tests/regscale/integrations/commercial/aws/test_org_evidence.py +1240 -0
- tests/regscale/integrations/commercial/aws/test_s3_control_mappings.py +672 -0
- tests/regscale/integrations/commercial/aws/test_s3_evidence.py +987 -0
- tests/regscale/integrations/commercial/aws/test_scanner_evidence.py +373 -0
- tests/regscale/integrations/commercial/aws/test_security_hub_config_filtering.py +539 -0
- tests/regscale/integrations/commercial/aws/test_session_manager.py +516 -0
- tests/regscale/integrations/commercial/aws/test_ssm_control_mappings.py +588 -0
- tests/regscale/integrations/commercial/aws/test_ssm_evidence.py +735 -0
- tests/regscale/integrations/commercial/test_aws.py +55 -56
- {regscale_cli-6.27.3.0.dist-info → regscale_cli-6.28.0.0.dist-info}/LICENSE +0 -0
- {regscale_cli-6.27.3.0.dist-info → regscale_cli-6.28.0.0.dist-info}/WHEEL +0 -0
- {regscale_cli-6.27.3.0.dist-info → regscale_cli-6.28.0.0.dist-info}/entry_points.txt +0 -0
- {regscale_cli-6.27.3.0.dist-info → regscale_cli-6.28.0.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,1375 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""Unit tests for AWS IAM Evidence Integration."""
|
|
4
|
+
|
|
5
|
+
import gzip
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
import time
|
|
9
|
+
from datetime import datetime, timedelta
|
|
10
|
+
from io import BytesIO
|
|
11
|
+
from unittest.mock import MagicMock, Mock, patch, mock_open
|
|
12
|
+
|
|
13
|
+
import pytest
|
|
14
|
+
from botocore.exceptions import ClientError
|
|
15
|
+
|
|
16
|
+
from regscale.integrations.commercial.aws.iam_evidence import (
|
|
17
|
+
IAMComplianceItem,
|
|
18
|
+
AWSIAMEvidenceIntegration,
|
|
19
|
+
IAM_CACHE_FILE,
|
|
20
|
+
CACHE_TTL_SECONDS,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
PATH = "regscale.integrations.commercial.aws.iam_evidence"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class TestIAMComplianceItem:
|
|
27
|
+
"""Test cases for IAMComplianceItem class."""
|
|
28
|
+
|
|
29
|
+
def setup_method(self):
|
|
30
|
+
"""Set up test fixtures."""
|
|
31
|
+
self.mock_mapper = MagicMock()
|
|
32
|
+
self.mock_mapper.framework = "NIST800-53R5"
|
|
33
|
+
|
|
34
|
+
def test_init_with_complete_data(self):
|
|
35
|
+
"""Test initialization with complete IAM data."""
|
|
36
|
+
iam_data = {
|
|
37
|
+
"users": [
|
|
38
|
+
{"UserName": "user1", "MfaEnabled": True},
|
|
39
|
+
{"UserName": "user2", "MfaEnabled": True},
|
|
40
|
+
],
|
|
41
|
+
"groups": [{"GroupName": "group1"}],
|
|
42
|
+
"roles": [{"RoleName": "role1"}],
|
|
43
|
+
"policies": [{"PolicyName": "policy1"}],
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
self.mock_mapper.assess_iam_compliance.return_value = {
|
|
47
|
+
"AC-2": "PASS",
|
|
48
|
+
"AC-6": "PASS",
|
|
49
|
+
"IA-2": "PASS",
|
|
50
|
+
"IA-5": "PASS",
|
|
51
|
+
"AC-3": "PASS",
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
item = IAMComplianceItem(iam_data, self.mock_mapper)
|
|
55
|
+
|
|
56
|
+
assert item.iam_data == iam_data
|
|
57
|
+
assert item.control_mapper == self.mock_mapper
|
|
58
|
+
assert len(item._users) == 2
|
|
59
|
+
assert len(item._groups) == 1
|
|
60
|
+
assert len(item._roles) == 1
|
|
61
|
+
assert len(item._policies) == 1
|
|
62
|
+
assert item._compliance_results == self.mock_mapper.assess_iam_compliance.return_value
|
|
63
|
+
|
|
64
|
+
def test_init_with_minimal_data(self):
|
|
65
|
+
"""Test initialization with minimal IAM data."""
|
|
66
|
+
iam_data = {}
|
|
67
|
+
|
|
68
|
+
self.mock_mapper.assess_iam_compliance.return_value = {}
|
|
69
|
+
|
|
70
|
+
item = IAMComplianceItem(iam_data, self.mock_mapper)
|
|
71
|
+
|
|
72
|
+
assert len(item._users) == 0
|
|
73
|
+
assert len(item._groups) == 0
|
|
74
|
+
assert len(item._roles) == 0
|
|
75
|
+
assert len(item._policies) == 0
|
|
76
|
+
|
|
77
|
+
def test_resource_id_property(self):
|
|
78
|
+
"""Test resource_id property."""
|
|
79
|
+
iam_data = {}
|
|
80
|
+
self.mock_mapper.assess_iam_compliance.return_value = {}
|
|
81
|
+
|
|
82
|
+
item = IAMComplianceItem(iam_data, self.mock_mapper)
|
|
83
|
+
|
|
84
|
+
assert item.resource_id == "iam-account"
|
|
85
|
+
|
|
86
|
+
def test_resource_name_property(self):
|
|
87
|
+
"""Test resource_name property."""
|
|
88
|
+
iam_data = {
|
|
89
|
+
"users": [{"UserName": "user1"}, {"UserName": "user2"}, {"UserName": "user3"}],
|
|
90
|
+
"groups": [],
|
|
91
|
+
"roles": [{"RoleName": "role1"}, {"RoleName": "role2"}],
|
|
92
|
+
"policies": [],
|
|
93
|
+
}
|
|
94
|
+
self.mock_mapper.assess_iam_compliance.return_value = {}
|
|
95
|
+
|
|
96
|
+
item = IAMComplianceItem(iam_data, self.mock_mapper)
|
|
97
|
+
|
|
98
|
+
assert item.resource_name == "AWS IAM Account (3 users, 2 roles)"
|
|
99
|
+
|
|
100
|
+
def test_control_id_property_with_failure(self):
|
|
101
|
+
"""Test control_id property returns first failed control."""
|
|
102
|
+
iam_data = {}
|
|
103
|
+
self.mock_mapper.assess_iam_compliance.return_value = {
|
|
104
|
+
"AC-2": "PASS",
|
|
105
|
+
"AC-6": "FAIL",
|
|
106
|
+
"IA-2": "FAIL",
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
item = IAMComplianceItem(iam_data, self.mock_mapper)
|
|
110
|
+
|
|
111
|
+
assert item.control_id == "AC-6"
|
|
112
|
+
|
|
113
|
+
def test_control_id_property_all_pass(self):
|
|
114
|
+
"""Test control_id property when all controls pass."""
|
|
115
|
+
iam_data = {}
|
|
116
|
+
self.mock_mapper.assess_iam_compliance.return_value = {
|
|
117
|
+
"AC-2": "PASS",
|
|
118
|
+
"AC-6": "PASS",
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
item = IAMComplianceItem(iam_data, self.mock_mapper)
|
|
122
|
+
|
|
123
|
+
assert item.control_id == "AC-2"
|
|
124
|
+
|
|
125
|
+
def test_control_id_property_empty_results(self):
|
|
126
|
+
"""Test control_id property with no compliance results."""
|
|
127
|
+
iam_data = {}
|
|
128
|
+
self.mock_mapper.assess_iam_compliance.return_value = {}
|
|
129
|
+
|
|
130
|
+
item = IAMComplianceItem(iam_data, self.mock_mapper)
|
|
131
|
+
|
|
132
|
+
assert item.control_id == "AC-2"
|
|
133
|
+
|
|
134
|
+
def test_compliance_result_property_pass(self):
|
|
135
|
+
"""Test compliance_result property when all checks pass."""
|
|
136
|
+
iam_data = {}
|
|
137
|
+
self.mock_mapper.assess_iam_compliance.return_value = {
|
|
138
|
+
"AC-2": "PASS",
|
|
139
|
+
"AC-6": "PASS",
|
|
140
|
+
"IA-2": "PASS",
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
item = IAMComplianceItem(iam_data, self.mock_mapper)
|
|
144
|
+
|
|
145
|
+
assert item.compliance_result == "PASS"
|
|
146
|
+
|
|
147
|
+
def test_compliance_result_property_fail(self):
|
|
148
|
+
"""Test compliance_result property when any check fails."""
|
|
149
|
+
iam_data = {}
|
|
150
|
+
self.mock_mapper.assess_iam_compliance.return_value = {
|
|
151
|
+
"AC-2": "PASS",
|
|
152
|
+
"AC-6": "FAIL",
|
|
153
|
+
"IA-2": "PASS",
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
item = IAMComplianceItem(iam_data, self.mock_mapper)
|
|
157
|
+
|
|
158
|
+
assert item.compliance_result == "FAIL"
|
|
159
|
+
|
|
160
|
+
def test_compliance_result_property_empty(self):
|
|
161
|
+
"""Test compliance_result property with no results."""
|
|
162
|
+
iam_data = {}
|
|
163
|
+
self.mock_mapper.assess_iam_compliance.return_value = {}
|
|
164
|
+
|
|
165
|
+
item = IAMComplianceItem(iam_data, self.mock_mapper)
|
|
166
|
+
|
|
167
|
+
assert item.compliance_result == "PASS"
|
|
168
|
+
|
|
169
|
+
def test_severity_property_pass(self):
|
|
170
|
+
"""Test severity property when compliance passes."""
|
|
171
|
+
iam_data = {}
|
|
172
|
+
self.mock_mapper.assess_iam_compliance.return_value = {
|
|
173
|
+
"AC-2": "PASS",
|
|
174
|
+
"AC-6": "PASS",
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
item = IAMComplianceItem(iam_data, self.mock_mapper)
|
|
178
|
+
|
|
179
|
+
assert item.severity is None
|
|
180
|
+
|
|
181
|
+
def test_severity_property_high(self):
|
|
182
|
+
"""Test severity property for high severity failures."""
|
|
183
|
+
iam_data = {}
|
|
184
|
+
self.mock_mapper.assess_iam_compliance.return_value = {
|
|
185
|
+
"AC-2": "FAIL",
|
|
186
|
+
"AC-6": "PASS",
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
item = IAMComplianceItem(iam_data, self.mock_mapper)
|
|
190
|
+
|
|
191
|
+
assert item.severity == "HIGH"
|
|
192
|
+
|
|
193
|
+
def test_severity_property_high_ia2(self):
|
|
194
|
+
"""Test severity property for IA-2 failures."""
|
|
195
|
+
iam_data = {}
|
|
196
|
+
self.mock_mapper.assess_iam_compliance.return_value = {
|
|
197
|
+
"AC-2": "PASS",
|
|
198
|
+
"IA-2": "FAIL",
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
item = IAMComplianceItem(iam_data, self.mock_mapper)
|
|
202
|
+
|
|
203
|
+
assert item.severity == "HIGH"
|
|
204
|
+
|
|
205
|
+
def test_severity_property_medium(self):
|
|
206
|
+
"""Test severity property for medium severity failures."""
|
|
207
|
+
iam_data = {}
|
|
208
|
+
self.mock_mapper.assess_iam_compliance.return_value = {
|
|
209
|
+
"AC-6": "FAIL",
|
|
210
|
+
"AC-3": "PASS",
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
item = IAMComplianceItem(iam_data, self.mock_mapper)
|
|
214
|
+
|
|
215
|
+
assert item.severity == "MEDIUM"
|
|
216
|
+
|
|
217
|
+
def test_description_property_pass(self):
|
|
218
|
+
"""Test description property with passing compliance."""
|
|
219
|
+
iam_data = {
|
|
220
|
+
"users": [{"UserName": "user1"}],
|
|
221
|
+
"groups": [{"GroupName": "group1"}],
|
|
222
|
+
"roles": [{"RoleName": "role1"}],
|
|
223
|
+
"policies": [{"PolicyName": "policy1"}],
|
|
224
|
+
}
|
|
225
|
+
self.mock_mapper.assess_iam_compliance.return_value = {
|
|
226
|
+
"AC-2": "PASS",
|
|
227
|
+
"AC-6": "PASS",
|
|
228
|
+
}
|
|
229
|
+
self.mock_mapper.get_control_description.side_effect = lambda x: f"{x} Description"
|
|
230
|
+
|
|
231
|
+
item = IAMComplianceItem(iam_data, self.mock_mapper)
|
|
232
|
+
description = item.description
|
|
233
|
+
|
|
234
|
+
assert "AWS IAM Access Control Assessment" in description
|
|
235
|
+
assert "Users:</strong> 1" in description
|
|
236
|
+
assert "Groups:</strong> 1" in description
|
|
237
|
+
assert "Roles:</strong> 1" in description
|
|
238
|
+
assert "Managed Policies:</strong> 1" in description
|
|
239
|
+
assert "AC-2" in description
|
|
240
|
+
assert "AC-6" in description
|
|
241
|
+
assert "PASS" in description
|
|
242
|
+
assert "Remediation Guidance" not in description
|
|
243
|
+
|
|
244
|
+
def test_description_property_fail(self):
|
|
245
|
+
"""Test description property with failing compliance."""
|
|
246
|
+
iam_data = {
|
|
247
|
+
"users": [{"UserName": "user1"}],
|
|
248
|
+
"groups": [],
|
|
249
|
+
"roles": [],
|
|
250
|
+
"policies": [],
|
|
251
|
+
}
|
|
252
|
+
self.mock_mapper.assess_iam_compliance.return_value = {
|
|
253
|
+
"AC-2": "FAIL",
|
|
254
|
+
"AC-6": "FAIL",
|
|
255
|
+
"IA-2": "FAIL",
|
|
256
|
+
"IA-5": "FAIL",
|
|
257
|
+
"AC-3": "FAIL",
|
|
258
|
+
}
|
|
259
|
+
self.mock_mapper.get_control_description.side_effect = lambda x: f"{x} Description"
|
|
260
|
+
|
|
261
|
+
item = IAMComplianceItem(iam_data, self.mock_mapper)
|
|
262
|
+
description = item.description
|
|
263
|
+
|
|
264
|
+
assert "FAIL" in description
|
|
265
|
+
assert "Remediation Guidance" in description
|
|
266
|
+
assert "Enable MFA for all IAM users" in description
|
|
267
|
+
assert "Remove AdministratorAccess from users" in description
|
|
268
|
+
assert "Strengthen password policy requirements" in description
|
|
269
|
+
assert "Rotate access keys older than 90 days" in description
|
|
270
|
+
assert "Review and restrict role trust policies" in description
|
|
271
|
+
|
|
272
|
+
def test_framework_property(self):
|
|
273
|
+
"""Test framework property."""
|
|
274
|
+
iam_data = {}
|
|
275
|
+
self.mock_mapper.assess_iam_compliance.return_value = {}
|
|
276
|
+
self.mock_mapper.framework = "NIST800-53R5"
|
|
277
|
+
|
|
278
|
+
item = IAMComplianceItem(iam_data, self.mock_mapper)
|
|
279
|
+
|
|
280
|
+
assert item.framework == "NIST800-53R5"
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
class TestAWSIAMEvidenceIntegrationInit:
|
|
284
|
+
"""Test cases for AWSIAMEvidenceIntegration initialization."""
|
|
285
|
+
|
|
286
|
+
@patch(f"{PATH}.IAMControlMapper")
|
|
287
|
+
@patch(f"{PATH}.boto3.Session")
|
|
288
|
+
def test_init_with_defaults(self, mock_session_class, mock_mapper_class):
|
|
289
|
+
"""Test initialization with default parameters."""
|
|
290
|
+
mock_session = MagicMock()
|
|
291
|
+
mock_client = MagicMock()
|
|
292
|
+
mock_session.client.return_value = mock_client
|
|
293
|
+
mock_session_class.return_value = mock_session
|
|
294
|
+
|
|
295
|
+
integration = AWSIAMEvidenceIntegration(plan_id=123)
|
|
296
|
+
|
|
297
|
+
assert integration.plan_id == 123
|
|
298
|
+
assert integration.region == "us-east-1"
|
|
299
|
+
assert integration.title == "AWS IAM"
|
|
300
|
+
assert integration.collect_evidence is False
|
|
301
|
+
assert integration.evidence_as_attachments is True
|
|
302
|
+
assert integration.evidence_control_ids is None
|
|
303
|
+
assert integration.evidence_frequency == 30
|
|
304
|
+
assert integration.force_refresh is False
|
|
305
|
+
assert integration.create_issues is True
|
|
306
|
+
assert integration.update_control_status is True
|
|
307
|
+
assert integration.create_poams is False
|
|
308
|
+
assert integration.parent_module == "securityplans"
|
|
309
|
+
|
|
310
|
+
mock_session_class.assert_called_once_with(profile_name=None, region_name="us-east-1")
|
|
311
|
+
mock_mapper_class.assert_called_once_with(framework="NIST800-53R5")
|
|
312
|
+
|
|
313
|
+
@patch(f"{PATH}.IAMControlMapper")
|
|
314
|
+
@patch(f"{PATH}.boto3.Session")
|
|
315
|
+
def test_init_with_explicit_credentials(self, mock_session_class, mock_mapper_class):
|
|
316
|
+
"""Test initialization with explicit AWS credentials."""
|
|
317
|
+
mock_session = MagicMock()
|
|
318
|
+
mock_client = MagicMock()
|
|
319
|
+
mock_session.client.return_value = mock_client
|
|
320
|
+
mock_session_class.return_value = mock_session
|
|
321
|
+
|
|
322
|
+
integration = AWSIAMEvidenceIntegration(
|
|
323
|
+
plan_id=456,
|
|
324
|
+
region="us-west-2",
|
|
325
|
+
aws_access_key_id="AKIATEST",
|
|
326
|
+
aws_secret_access_key="secret",
|
|
327
|
+
aws_session_token="token",
|
|
328
|
+
)
|
|
329
|
+
|
|
330
|
+
assert integration.region == "us-west-2"
|
|
331
|
+
mock_session_class.assert_called_once_with(
|
|
332
|
+
region_name="us-west-2",
|
|
333
|
+
aws_access_key_id="AKIATEST",
|
|
334
|
+
aws_secret_access_key="secret",
|
|
335
|
+
aws_session_token="token",
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
@patch(f"{PATH}.IAMControlMapper")
|
|
339
|
+
@patch(f"{PATH}.boto3.Session")
|
|
340
|
+
def test_init_with_profile(self, mock_session_class, mock_mapper_class):
|
|
341
|
+
"""Test initialization with AWS profile."""
|
|
342
|
+
mock_session = MagicMock()
|
|
343
|
+
mock_client = MagicMock()
|
|
344
|
+
mock_session.client.return_value = mock_client
|
|
345
|
+
mock_session_class.return_value = mock_session
|
|
346
|
+
|
|
347
|
+
AWSIAMEvidenceIntegration(plan_id=789, region="eu-west-1", profile="test-profile") # noqa: F841
|
|
348
|
+
|
|
349
|
+
mock_session_class.assert_called_once_with(profile_name="test-profile", region_name="eu-west-1")
|
|
350
|
+
|
|
351
|
+
@patch(f"{PATH}.IAMControlMapper")
|
|
352
|
+
@patch(f"{PATH}.boto3.Session")
|
|
353
|
+
def test_init_with_all_options(self, mock_session_class, mock_mapper_class):
|
|
354
|
+
"""Test initialization with all optional parameters."""
|
|
355
|
+
mock_session = MagicMock()
|
|
356
|
+
mock_client = MagicMock()
|
|
357
|
+
mock_session.client.return_value = mock_client
|
|
358
|
+
mock_session_class.return_value = mock_session
|
|
359
|
+
|
|
360
|
+
integration = AWSIAMEvidenceIntegration(
|
|
361
|
+
plan_id=999,
|
|
362
|
+
region="ap-southeast-1",
|
|
363
|
+
framework="ISO27001",
|
|
364
|
+
create_issues=False,
|
|
365
|
+
update_control_status=False,
|
|
366
|
+
create_poams=True,
|
|
367
|
+
parent_module="assessments",
|
|
368
|
+
collect_evidence=True,
|
|
369
|
+
evidence_as_attachments=False,
|
|
370
|
+
evidence_control_ids=["AC-2", "IA-2"],
|
|
371
|
+
evidence_frequency=60,
|
|
372
|
+
force_refresh=True,
|
|
373
|
+
)
|
|
374
|
+
|
|
375
|
+
assert integration.plan_id == 999
|
|
376
|
+
assert integration.region == "ap-southeast-1"
|
|
377
|
+
assert integration.framework == "ISO27001"
|
|
378
|
+
assert integration.create_issues is False
|
|
379
|
+
assert integration.update_control_status is False
|
|
380
|
+
assert integration.create_poams is True
|
|
381
|
+
# Note: parent_module defaults to "securityplans" in ComplianceIntegration
|
|
382
|
+
assert integration.parent_module in ["assessments", "securityplans"]
|
|
383
|
+
assert integration.collect_evidence is True
|
|
384
|
+
assert integration.evidence_as_attachments is False
|
|
385
|
+
assert integration.evidence_control_ids == ["AC-2", "IA-2"]
|
|
386
|
+
assert integration.evidence_frequency == 60
|
|
387
|
+
assert integration.force_refresh is True
|
|
388
|
+
|
|
389
|
+
@patch(f"{PATH}.IAMControlMapper")
|
|
390
|
+
@patch(f"{PATH}.boto3.Session")
|
|
391
|
+
def test_init_client_creation_failure(self, mock_session_class, mock_mapper_class):
|
|
392
|
+
"""Test initialization when IAM client creation fails."""
|
|
393
|
+
mock_session = MagicMock()
|
|
394
|
+
mock_session.client.side_effect = Exception("Failed to create IAM client")
|
|
395
|
+
mock_session_class.return_value = mock_session
|
|
396
|
+
|
|
397
|
+
with pytest.raises(Exception) as exc_info:
|
|
398
|
+
AWSIAMEvidenceIntegration(plan_id=123, region="us-east-1")
|
|
399
|
+
|
|
400
|
+
assert "Failed to create IAM client" in str(exc_info.value)
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
class TestCacheManagement:
|
|
404
|
+
"""Test cases for cache management methods."""
|
|
405
|
+
|
|
406
|
+
@patch(f"{PATH}.IAMControlMapper")
|
|
407
|
+
@patch(f"{PATH}.boto3.Session")
|
|
408
|
+
@patch(f"{PATH}.os.path.exists")
|
|
409
|
+
def test_is_cache_valid_no_file(self, mock_exists, mock_session_class, mock_mapper_class):
|
|
410
|
+
"""Test cache validation when file does not exist."""
|
|
411
|
+
mock_exists.return_value = False
|
|
412
|
+
mock_session = MagicMock()
|
|
413
|
+
mock_session.client.return_value = MagicMock()
|
|
414
|
+
mock_session_class.return_value = mock_session
|
|
415
|
+
|
|
416
|
+
integration = AWSIAMEvidenceIntegration(plan_id=123, region="us-east-1")
|
|
417
|
+
assert integration._is_cache_valid() is False
|
|
418
|
+
|
|
419
|
+
@patch(f"{PATH}.IAMControlMapper")
|
|
420
|
+
@patch(f"{PATH}.boto3.Session")
|
|
421
|
+
@patch(f"{PATH}.os.path.exists")
|
|
422
|
+
@patch(f"{PATH}.os.path.getmtime")
|
|
423
|
+
@patch(f"{PATH}.time.time")
|
|
424
|
+
def test_is_cache_valid_expired(self, mock_time, mock_getmtime, mock_exists, mock_session_class, mock_mapper_class):
|
|
425
|
+
"""Test cache validation when cache is expired."""
|
|
426
|
+
mock_exists.return_value = True
|
|
427
|
+
mock_time.return_value = 1000000
|
|
428
|
+
mock_getmtime.return_value = 1000000 - CACHE_TTL_SECONDS - 100
|
|
429
|
+
mock_session = MagicMock()
|
|
430
|
+
mock_session.client.return_value = MagicMock()
|
|
431
|
+
mock_session_class.return_value = mock_session
|
|
432
|
+
|
|
433
|
+
integration = AWSIAMEvidenceIntegration(plan_id=123, region="us-east-1")
|
|
434
|
+
assert integration._is_cache_valid() is False
|
|
435
|
+
|
|
436
|
+
@patch(f"{PATH}.IAMControlMapper")
|
|
437
|
+
@patch(f"{PATH}.boto3.Session")
|
|
438
|
+
@patch(f"{PATH}.os.path.exists")
|
|
439
|
+
@patch(f"{PATH}.os.path.getmtime")
|
|
440
|
+
@patch(f"{PATH}.time.time")
|
|
441
|
+
def test_is_cache_valid_fresh(self, mock_time, mock_getmtime, mock_exists, mock_session_class, mock_mapper_class):
|
|
442
|
+
"""Test cache validation when cache is fresh."""
|
|
443
|
+
mock_exists.return_value = True
|
|
444
|
+
mock_time.return_value = 1000000
|
|
445
|
+
mock_getmtime.return_value = 1000000 - 1000
|
|
446
|
+
mock_session = MagicMock()
|
|
447
|
+
mock_session.client.return_value = MagicMock()
|
|
448
|
+
mock_session_class.return_value = mock_session
|
|
449
|
+
|
|
450
|
+
integration = AWSIAMEvidenceIntegration(plan_id=123, region="us-east-1")
|
|
451
|
+
assert integration._is_cache_valid() is True
|
|
452
|
+
|
|
453
|
+
@patch(f"{PATH}.IAMControlMapper")
|
|
454
|
+
@patch(f"{PATH}.boto3.Session")
|
|
455
|
+
def test_load_cached_data_success(self, mock_session_class, mock_mapper_class):
|
|
456
|
+
"""Test loading cached data successfully."""
|
|
457
|
+
mock_session = MagicMock()
|
|
458
|
+
mock_session.client.return_value = MagicMock()
|
|
459
|
+
mock_session_class.return_value = mock_session
|
|
460
|
+
|
|
461
|
+
test_data = {"users": [{"UserName": "user1"}], "groups": [], "roles": [], "policies": []}
|
|
462
|
+
mock_file = mock_open(read_data=json.dumps(test_data))
|
|
463
|
+
|
|
464
|
+
integration = AWSIAMEvidenceIntegration(plan_id=123, region="us-east-1")
|
|
465
|
+
|
|
466
|
+
with patch("builtins.open", mock_file):
|
|
467
|
+
result = integration._load_cached_data()
|
|
468
|
+
|
|
469
|
+
assert result == test_data
|
|
470
|
+
|
|
471
|
+
@patch(f"{PATH}.IAMControlMapper")
|
|
472
|
+
@patch(f"{PATH}.boto3.Session")
|
|
473
|
+
def test_load_cached_data_json_error(self, mock_session_class, mock_mapper_class):
|
|
474
|
+
"""Test loading cached data with JSON decode error."""
|
|
475
|
+
mock_session = MagicMock()
|
|
476
|
+
mock_session.client.return_value = MagicMock()
|
|
477
|
+
mock_session_class.return_value = mock_session
|
|
478
|
+
|
|
479
|
+
mock_file = mock_open(read_data="invalid json")
|
|
480
|
+
integration = AWSIAMEvidenceIntegration(plan_id=123, region="us-east-1")
|
|
481
|
+
|
|
482
|
+
with patch("builtins.open", mock_file):
|
|
483
|
+
result = integration._load_cached_data()
|
|
484
|
+
|
|
485
|
+
assert result == {}
|
|
486
|
+
|
|
487
|
+
@patch(f"{PATH}.IAMControlMapper")
|
|
488
|
+
@patch(f"{PATH}.boto3.Session")
|
|
489
|
+
def test_load_cached_data_io_error(self, mock_session_class, mock_mapper_class):
|
|
490
|
+
"""Test loading cached data with IO error."""
|
|
491
|
+
mock_session = MagicMock()
|
|
492
|
+
mock_session.client.return_value = MagicMock()
|
|
493
|
+
mock_session_class.return_value = mock_session
|
|
494
|
+
|
|
495
|
+
integration = AWSIAMEvidenceIntegration(plan_id=123, region="us-east-1")
|
|
496
|
+
|
|
497
|
+
with patch("builtins.open", side_effect=IOError("File not found")):
|
|
498
|
+
result = integration._load_cached_data()
|
|
499
|
+
|
|
500
|
+
assert result == {}
|
|
501
|
+
|
|
502
|
+
@patch(f"{PATH}.IAMControlMapper")
|
|
503
|
+
@patch(f"{PATH}.boto3.Session")
|
|
504
|
+
@patch(f"{PATH}.os.makedirs")
|
|
505
|
+
def test_save_to_cache_success(self, mock_makedirs, mock_session_class, mock_mapper_class):
|
|
506
|
+
"""Test saving data to cache successfully."""
|
|
507
|
+
mock_session = MagicMock()
|
|
508
|
+
mock_session.client.return_value = MagicMock()
|
|
509
|
+
mock_session_class.return_value = mock_session
|
|
510
|
+
|
|
511
|
+
test_data = {"users": [], "groups": [], "roles": [], "policies": []}
|
|
512
|
+
mock_file = mock_open()
|
|
513
|
+
|
|
514
|
+
integration = AWSIAMEvidenceIntegration(plan_id=123, region="us-east-1")
|
|
515
|
+
|
|
516
|
+
with patch("builtins.open", mock_file):
|
|
517
|
+
integration._save_to_cache(test_data)
|
|
518
|
+
|
|
519
|
+
mock_makedirs.assert_called_once()
|
|
520
|
+
mock_file.assert_called_once()
|
|
521
|
+
|
|
522
|
+
@patch(f"{PATH}.IAMControlMapper")
|
|
523
|
+
@patch(f"{PATH}.boto3.Session")
|
|
524
|
+
@patch(f"{PATH}.os.makedirs")
|
|
525
|
+
def test_save_to_cache_io_error(self, mock_makedirs, mock_session_class, mock_mapper_class):
|
|
526
|
+
"""Test saving data to cache with IO error."""
|
|
527
|
+
mock_session = MagicMock()
|
|
528
|
+
mock_session.client.return_value = MagicMock()
|
|
529
|
+
mock_session_class.return_value = mock_session
|
|
530
|
+
|
|
531
|
+
test_data = {"users": [], "groups": [], "roles": [], "policies": []}
|
|
532
|
+
|
|
533
|
+
integration = AWSIAMEvidenceIntegration(plan_id=123, region="us-east-1")
|
|
534
|
+
|
|
535
|
+
with patch("builtins.open", side_effect=IOError("Permission denied")):
|
|
536
|
+
integration._save_to_cache(test_data)
|
|
537
|
+
|
|
538
|
+
|
|
539
|
+
class TestFetchIAMData:
|
|
540
|
+
"""Test cases for fetching IAM data."""
|
|
541
|
+
|
|
542
|
+
@patch(f"{PATH}.IAMControlMapper")
|
|
543
|
+
@patch(f"{PATH}.boto3.Session")
|
|
544
|
+
def test_get_password_policy_success(self, mock_session_class, mock_mapper_class):
|
|
545
|
+
"""Test getting password policy successfully."""
|
|
546
|
+
mock_client = MagicMock()
|
|
547
|
+
mock_client.get_account_password_policy.return_value = {
|
|
548
|
+
"PasswordPolicy": {
|
|
549
|
+
"MinimumPasswordLength": 14,
|
|
550
|
+
"RequireSymbols": True,
|
|
551
|
+
"RequireNumbers": True,
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
mock_session = MagicMock()
|
|
556
|
+
mock_session.client.return_value = mock_client
|
|
557
|
+
mock_session_class.return_value = mock_session
|
|
558
|
+
|
|
559
|
+
integration = AWSIAMEvidenceIntegration(plan_id=123, region="us-east-1")
|
|
560
|
+
result = integration._get_password_policy()
|
|
561
|
+
|
|
562
|
+
assert result["MinimumPasswordLength"] == 14
|
|
563
|
+
assert result["RequireSymbols"] is True
|
|
564
|
+
|
|
565
|
+
@patch(f"{PATH}.IAMControlMapper")
|
|
566
|
+
@patch(f"{PATH}.boto3.Session")
|
|
567
|
+
def test_get_password_policy_not_configured(self, mock_session_class, mock_mapper_class):
|
|
568
|
+
"""Test getting password policy when not configured."""
|
|
569
|
+
mock_client = MagicMock()
|
|
570
|
+
error_response = {"Error": {"Code": "NoSuchEntity", "Message": "Policy not found"}}
|
|
571
|
+
mock_client.get_account_password_policy.side_effect = ClientError(error_response, "GetAccountPasswordPolicy")
|
|
572
|
+
|
|
573
|
+
mock_session = MagicMock()
|
|
574
|
+
mock_session.client.return_value = mock_client
|
|
575
|
+
mock_session_class.return_value = mock_session
|
|
576
|
+
|
|
577
|
+
integration = AWSIAMEvidenceIntegration(plan_id=123, region="us-east-1")
|
|
578
|
+
result = integration._get_password_policy()
|
|
579
|
+
|
|
580
|
+
assert result == {}
|
|
581
|
+
|
|
582
|
+
@patch(f"{PATH}.IAMControlMapper")
|
|
583
|
+
@patch(f"{PATH}.boto3.Session")
|
|
584
|
+
def test_get_password_policy_other_error(self, mock_session_class, mock_mapper_class):
|
|
585
|
+
"""Test getting password policy with other error."""
|
|
586
|
+
mock_client = MagicMock()
|
|
587
|
+
error_response = {"Error": {"Code": "AccessDenied", "Message": "Access denied"}}
|
|
588
|
+
mock_client.get_account_password_policy.side_effect = ClientError(error_response, "GetAccountPasswordPolicy")
|
|
589
|
+
|
|
590
|
+
mock_session = MagicMock()
|
|
591
|
+
mock_session.client.return_value = mock_client
|
|
592
|
+
mock_session_class.return_value = mock_session
|
|
593
|
+
|
|
594
|
+
integration = AWSIAMEvidenceIntegration(plan_id=123, region="us-east-1")
|
|
595
|
+
|
|
596
|
+
with pytest.raises(ClientError):
|
|
597
|
+
integration._get_password_policy()
|
|
598
|
+
|
|
599
|
+
@patch(f"{PATH}.IAMControlMapper")
|
|
600
|
+
@patch(f"{PATH}.boto3.Session")
|
|
601
|
+
def test_user_has_mfa_true(self, mock_session_class, mock_mapper_class):
|
|
602
|
+
"""Test checking if user has MFA enabled."""
|
|
603
|
+
mock_client = MagicMock()
|
|
604
|
+
mock_client.list_mfa_devices.return_value = {"MFADevices": [{"SerialNumber": "arn:aws:iam::123:mfa/user"}]}
|
|
605
|
+
|
|
606
|
+
mock_session = MagicMock()
|
|
607
|
+
mock_session.client.return_value = mock_client
|
|
608
|
+
mock_session_class.return_value = mock_session
|
|
609
|
+
|
|
610
|
+
integration = AWSIAMEvidenceIntegration(plan_id=123, region="us-east-1")
|
|
611
|
+
result = integration._user_has_mfa("testuser")
|
|
612
|
+
|
|
613
|
+
assert result is True
|
|
614
|
+
|
|
615
|
+
@patch(f"{PATH}.IAMControlMapper")
|
|
616
|
+
@patch(f"{PATH}.boto3.Session")
|
|
617
|
+
def test_user_has_mfa_false(self, mock_session_class, mock_mapper_class):
|
|
618
|
+
"""Test checking if user has no MFA."""
|
|
619
|
+
mock_client = MagicMock()
|
|
620
|
+
mock_client.list_mfa_devices.return_value = {"MFADevices": []}
|
|
621
|
+
|
|
622
|
+
mock_session = MagicMock()
|
|
623
|
+
mock_session.client.return_value = mock_client
|
|
624
|
+
mock_session_class.return_value = mock_session
|
|
625
|
+
|
|
626
|
+
integration = AWSIAMEvidenceIntegration(plan_id=123, region="us-east-1")
|
|
627
|
+
result = integration._user_has_mfa("testuser")
|
|
628
|
+
|
|
629
|
+
assert result is False
|
|
630
|
+
|
|
631
|
+
@patch(f"{PATH}.IAMControlMapper")
|
|
632
|
+
@patch(f"{PATH}.boto3.Session")
|
|
633
|
+
def test_user_has_mfa_error(self, mock_session_class, mock_mapper_class):
|
|
634
|
+
"""Test checking MFA with client error."""
|
|
635
|
+
mock_client = MagicMock()
|
|
636
|
+
error_response = {"Error": {"Code": "NoSuchEntity", "Message": "User not found"}}
|
|
637
|
+
mock_client.list_mfa_devices.side_effect = ClientError(error_response, "ListMFADevices")
|
|
638
|
+
|
|
639
|
+
mock_session = MagicMock()
|
|
640
|
+
mock_session.client.return_value = mock_client
|
|
641
|
+
mock_session_class.return_value = mock_session
|
|
642
|
+
|
|
643
|
+
integration = AWSIAMEvidenceIntegration(plan_id=123, region="us-east-1")
|
|
644
|
+
result = integration._user_has_mfa("testuser")
|
|
645
|
+
|
|
646
|
+
assert result is False
|
|
647
|
+
|
|
648
|
+
@patch(f"{PATH}.IAMControlMapper")
|
|
649
|
+
@patch(f"{PATH}.boto3.Session")
|
|
650
|
+
def test_list_user_access_keys(self, mock_session_class, mock_mapper_class):
|
|
651
|
+
"""Test listing user access keys."""
|
|
652
|
+
mock_client = MagicMock()
|
|
653
|
+
created_date = datetime.now() - timedelta(days=100)
|
|
654
|
+
mock_client.list_access_keys.return_value = {
|
|
655
|
+
"AccessKeyMetadata": [
|
|
656
|
+
{"AccessKeyId": "AKIATEST", "Status": "Active", "CreateDate": created_date},
|
|
657
|
+
]
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
mock_session = MagicMock()
|
|
661
|
+
mock_session.client.return_value = mock_client
|
|
662
|
+
mock_session_class.return_value = mock_session
|
|
663
|
+
|
|
664
|
+
integration = AWSIAMEvidenceIntegration(plan_id=123, region="us-east-1")
|
|
665
|
+
result = integration._list_user_access_keys("testuser")
|
|
666
|
+
|
|
667
|
+
assert len(result) == 1
|
|
668
|
+
assert result[0]["AccessKeyId"] == "AKIATEST"
|
|
669
|
+
assert "AgeDays" in result[0]
|
|
670
|
+
assert result[0]["AgeDays"] == 100
|
|
671
|
+
|
|
672
|
+
@patch(f"{PATH}.IAMControlMapper")
|
|
673
|
+
@patch(f"{PATH}.boto3.Session")
|
|
674
|
+
def test_list_user_access_keys_error(self, mock_session_class, mock_mapper_class):
|
|
675
|
+
"""Test listing user access keys with error."""
|
|
676
|
+
mock_client = MagicMock()
|
|
677
|
+
error_response = {"Error": {"Code": "NoSuchEntity", "Message": "User not found"}}
|
|
678
|
+
mock_client.list_access_keys.side_effect = ClientError(error_response, "ListAccessKeys")
|
|
679
|
+
|
|
680
|
+
mock_session = MagicMock()
|
|
681
|
+
mock_session.client.return_value = mock_client
|
|
682
|
+
mock_session_class.return_value = mock_session
|
|
683
|
+
|
|
684
|
+
integration = AWSIAMEvidenceIntegration(plan_id=123, region="us-east-1")
|
|
685
|
+
result = integration._list_user_access_keys("testuser")
|
|
686
|
+
|
|
687
|
+
assert result == []
|
|
688
|
+
|
|
689
|
+
@patch(f"{PATH}.IAMControlMapper")
|
|
690
|
+
@patch(f"{PATH}.boto3.Session")
|
|
691
|
+
def test_list_user_attached_policies(self, mock_session_class, mock_mapper_class):
|
|
692
|
+
"""Test listing user attached policies."""
|
|
693
|
+
mock_client = MagicMock()
|
|
694
|
+
mock_client.list_attached_user_policies.return_value = {
|
|
695
|
+
"AttachedPolicies": [
|
|
696
|
+
{"PolicyName": "ReadOnlyAccess", "PolicyArn": "arn:aws:iam::aws:policy/ReadOnlyAccess"},
|
|
697
|
+
]
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
mock_session = MagicMock()
|
|
701
|
+
mock_session.client.return_value = mock_client
|
|
702
|
+
mock_session_class.return_value = mock_session
|
|
703
|
+
|
|
704
|
+
integration = AWSIAMEvidenceIntegration(plan_id=123, region="us-east-1")
|
|
705
|
+
result = integration._list_user_attached_policies("testuser")
|
|
706
|
+
|
|
707
|
+
assert len(result) == 1
|
|
708
|
+
assert result[0]["PolicyName"] == "ReadOnlyAccess"
|
|
709
|
+
|
|
710
|
+
@patch(f"{PATH}.IAMControlMapper")
|
|
711
|
+
@patch(f"{PATH}.boto3.Session")
|
|
712
|
+
def test_list_user_inline_policies(self, mock_session_class, mock_mapper_class):
|
|
713
|
+
"""Test listing user inline policies."""
|
|
714
|
+
mock_client = MagicMock()
|
|
715
|
+
mock_client.list_user_policies.return_value = {"PolicyNames": ["custom-policy"]}
|
|
716
|
+
mock_client.get_user_policy.return_value = {
|
|
717
|
+
"PolicyName": "custom-policy",
|
|
718
|
+
"PolicyDocument": {"Version": "2012-10-17", "Statement": []},
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
mock_session = MagicMock()
|
|
722
|
+
mock_session.client.return_value = mock_client
|
|
723
|
+
mock_session_class.return_value = mock_session
|
|
724
|
+
|
|
725
|
+
integration = AWSIAMEvidenceIntegration(plan_id=123, region="us-east-1")
|
|
726
|
+
result = integration._list_user_inline_policies("testuser")
|
|
727
|
+
|
|
728
|
+
assert len(result) == 1
|
|
729
|
+
assert result[0]["PolicyName"] == "custom-policy"
|
|
730
|
+
|
|
731
|
+
@patch(f"{PATH}.IAMControlMapper")
|
|
732
|
+
@patch(f"{PATH}.boto3.Session")
|
|
733
|
+
def test_get_password_last_used(self, mock_session_class, mock_mapper_class):
|
|
734
|
+
"""Test getting password last used information."""
|
|
735
|
+
mock_session = MagicMock()
|
|
736
|
+
mock_session.client.return_value = MagicMock()
|
|
737
|
+
mock_session_class.return_value = mock_session
|
|
738
|
+
|
|
739
|
+
integration = AWSIAMEvidenceIntegration(plan_id=123, region="us-east-1")
|
|
740
|
+
|
|
741
|
+
last_used_date = datetime.now() - timedelta(days=30)
|
|
742
|
+
user_data = {"UserName": "testuser", "PasswordLastUsed": last_used_date}
|
|
743
|
+
|
|
744
|
+
result = integration._get_password_last_used(user_data)
|
|
745
|
+
|
|
746
|
+
assert result is not None
|
|
747
|
+
assert "LastUsedDate" in result
|
|
748
|
+
assert "DaysSinceUsed" in result
|
|
749
|
+
assert result["DaysSinceUsed"] == 30
|
|
750
|
+
|
|
751
|
+
@patch(f"{PATH}.IAMControlMapper")
|
|
752
|
+
@patch(f"{PATH}.boto3.Session")
|
|
753
|
+
def test_get_password_last_used_none(self, mock_session_class, mock_mapper_class):
|
|
754
|
+
"""Test getting password last used when never used."""
|
|
755
|
+
mock_session = MagicMock()
|
|
756
|
+
mock_session.client.return_value = MagicMock()
|
|
757
|
+
mock_session_class.return_value = mock_session
|
|
758
|
+
|
|
759
|
+
integration = AWSIAMEvidenceIntegration(plan_id=123, region="us-east-1")
|
|
760
|
+
|
|
761
|
+
user_data = {"UserName": "testuser"}
|
|
762
|
+
|
|
763
|
+
result = integration._get_password_last_used(user_data)
|
|
764
|
+
|
|
765
|
+
assert result is None
|
|
766
|
+
|
|
767
|
+
@patch(f"{PATH}.IAMControlMapper")
|
|
768
|
+
@patch(f"{PATH}.boto3.Session")
|
|
769
|
+
def test_list_users(self, mock_session_class, mock_mapper_class):
|
|
770
|
+
"""Test listing IAM users."""
|
|
771
|
+
mock_client = MagicMock()
|
|
772
|
+
mock_paginator = MagicMock()
|
|
773
|
+
mock_paginator.paginate.return_value = [
|
|
774
|
+
{
|
|
775
|
+
"Users": [
|
|
776
|
+
{"UserName": "user1", "CreateDate": datetime.now()},
|
|
777
|
+
{"UserName": "user2", "CreateDate": datetime.now()},
|
|
778
|
+
]
|
|
779
|
+
}
|
|
780
|
+
]
|
|
781
|
+
mock_client.get_paginator.return_value = mock_paginator
|
|
782
|
+
|
|
783
|
+
mock_session = MagicMock()
|
|
784
|
+
mock_session.client.return_value = mock_client
|
|
785
|
+
mock_session_class.return_value = mock_session
|
|
786
|
+
|
|
787
|
+
integration = AWSIAMEvidenceIntegration(plan_id=123, region="us-east-1")
|
|
788
|
+
integration._user_has_mfa = Mock(return_value=True)
|
|
789
|
+
integration._list_user_access_keys = Mock(return_value=[])
|
|
790
|
+
integration._list_user_attached_policies = Mock(return_value=[])
|
|
791
|
+
integration._list_user_inline_policies = Mock(return_value=[])
|
|
792
|
+
integration._get_password_last_used = Mock(return_value=None)
|
|
793
|
+
|
|
794
|
+
result = integration._list_users()
|
|
795
|
+
|
|
796
|
+
assert len(result) == 2
|
|
797
|
+
assert result[0]["UserName"] == "user1"
|
|
798
|
+
assert result[1]["UserName"] == "user2"
|
|
799
|
+
|
|
800
|
+
@patch(f"{PATH}.IAMControlMapper")
|
|
801
|
+
@patch(f"{PATH}.boto3.Session")
|
|
802
|
+
def test_list_groups(self, mock_session_class, mock_mapper_class):
|
|
803
|
+
"""Test listing IAM groups."""
|
|
804
|
+
mock_client = MagicMock()
|
|
805
|
+
mock_paginator = MagicMock()
|
|
806
|
+
mock_paginator.paginate.return_value = [{"Groups": [{"GroupName": "group1"}, {"GroupName": "group2"}]}]
|
|
807
|
+
mock_client.get_paginator.return_value = mock_paginator
|
|
808
|
+
|
|
809
|
+
mock_session = MagicMock()
|
|
810
|
+
mock_session.client.return_value = mock_client
|
|
811
|
+
mock_session_class.return_value = mock_session
|
|
812
|
+
|
|
813
|
+
integration = AWSIAMEvidenceIntegration(plan_id=123, region="us-east-1")
|
|
814
|
+
result = integration._list_groups()
|
|
815
|
+
|
|
816
|
+
assert len(result) == 2
|
|
817
|
+
assert result[0]["GroupName"] == "group1"
|
|
818
|
+
|
|
819
|
+
@patch(f"{PATH}.IAMControlMapper")
|
|
820
|
+
@patch(f"{PATH}.boto3.Session")
|
|
821
|
+
def test_list_roles(self, mock_session_class, mock_mapper_class):
|
|
822
|
+
"""Test listing IAM roles."""
|
|
823
|
+
mock_client = MagicMock()
|
|
824
|
+
mock_paginator = MagicMock()
|
|
825
|
+
mock_paginator.paginate.return_value = [{"Roles": [{"RoleName": "role1"}, {"RoleName": "role2"}]}]
|
|
826
|
+
mock_client.get_paginator.return_value = mock_paginator
|
|
827
|
+
|
|
828
|
+
mock_session = MagicMock()
|
|
829
|
+
mock_session.client.return_value = mock_client
|
|
830
|
+
mock_session_class.return_value = mock_session
|
|
831
|
+
|
|
832
|
+
integration = AWSIAMEvidenceIntegration(plan_id=123, region="us-east-1")
|
|
833
|
+
integration._list_role_attached_policies = Mock(return_value=[])
|
|
834
|
+
|
|
835
|
+
result = integration._list_roles()
|
|
836
|
+
|
|
837
|
+
assert len(result) == 2
|
|
838
|
+
assert result[0]["RoleName"] == "role1"
|
|
839
|
+
assert "AttachedPolicies" in result[0]
|
|
840
|
+
|
|
841
|
+
@patch(f"{PATH}.IAMControlMapper")
|
|
842
|
+
@patch(f"{PATH}.boto3.Session")
|
|
843
|
+
def test_list_policies(self, mock_session_class, mock_mapper_class):
|
|
844
|
+
"""Test listing customer managed policies."""
|
|
845
|
+
mock_client = MagicMock()
|
|
846
|
+
mock_paginator = MagicMock()
|
|
847
|
+
mock_paginator.paginate.return_value = [{"Policies": [{"PolicyName": "policy1"}, {"PolicyName": "policy2"}]}]
|
|
848
|
+
mock_client.get_paginator.return_value = mock_paginator
|
|
849
|
+
|
|
850
|
+
mock_session = MagicMock()
|
|
851
|
+
mock_session.client.return_value = mock_client
|
|
852
|
+
mock_session_class.return_value = mock_session
|
|
853
|
+
|
|
854
|
+
integration = AWSIAMEvidenceIntegration(plan_id=123, region="us-east-1")
|
|
855
|
+
result = integration._list_policies()
|
|
856
|
+
|
|
857
|
+
assert len(result) == 2
|
|
858
|
+
assert result[0]["PolicyName"] == "policy1"
|
|
859
|
+
|
|
860
|
+
@patch(f"{PATH}.IAMControlMapper")
|
|
861
|
+
@patch(f"{PATH}.boto3.Session")
|
|
862
|
+
def test_fetch_fresh_iam_data(self, mock_session_class, mock_mapper_class):
|
|
863
|
+
"""Test fetching fresh IAM data."""
|
|
864
|
+
mock_client = MagicMock()
|
|
865
|
+
mock_client.get_account_summary.return_value = {"SummaryMap": {"Users": 5}}
|
|
866
|
+
|
|
867
|
+
mock_session = MagicMock()
|
|
868
|
+
mock_session.client.return_value = mock_client
|
|
869
|
+
mock_session_class.return_value = mock_session
|
|
870
|
+
|
|
871
|
+
integration = AWSIAMEvidenceIntegration(plan_id=123, region="us-east-1")
|
|
872
|
+
integration._get_password_policy = Mock(return_value={"MinimumPasswordLength": 14})
|
|
873
|
+
integration._list_users = Mock(return_value=[{"UserName": "user1"}])
|
|
874
|
+
integration._list_groups = Mock(return_value=[{"GroupName": "group1"}])
|
|
875
|
+
integration._list_roles = Mock(return_value=[{"RoleName": "role1"}])
|
|
876
|
+
integration._list_policies = Mock(return_value=[{"PolicyName": "policy1"}])
|
|
877
|
+
|
|
878
|
+
result = integration._fetch_fresh_iam_data()
|
|
879
|
+
|
|
880
|
+
assert "account_summary" in result
|
|
881
|
+
assert "password_policy" in result
|
|
882
|
+
assert "users" in result
|
|
883
|
+
assert "groups" in result
|
|
884
|
+
assert "roles" in result
|
|
885
|
+
assert "policies" in result
|
|
886
|
+
assert len(result["users"]) == 1
|
|
887
|
+
|
|
888
|
+
@patch(f"{PATH}.IAMControlMapper")
|
|
889
|
+
@patch(f"{PATH}.boto3.Session")
|
|
890
|
+
def test_fetch_fresh_iam_data_error(self, mock_session_class, mock_mapper_class):
|
|
891
|
+
"""Test fetching fresh IAM data with error."""
|
|
892
|
+
mock_client = MagicMock()
|
|
893
|
+
error_response = {"Error": {"Code": "AccessDenied", "Message": "Access denied"}}
|
|
894
|
+
mock_client.get_account_summary.side_effect = ClientError(error_response, "GetAccountSummary")
|
|
895
|
+
|
|
896
|
+
mock_session = MagicMock()
|
|
897
|
+
mock_session.client.return_value = mock_client
|
|
898
|
+
mock_session_class.return_value = mock_session
|
|
899
|
+
|
|
900
|
+
integration = AWSIAMEvidenceIntegration(plan_id=123, region="us-east-1")
|
|
901
|
+
|
|
902
|
+
result = integration._fetch_fresh_iam_data()
|
|
903
|
+
|
|
904
|
+
assert result == {}
|
|
905
|
+
|
|
906
|
+
@patch(f"{PATH}.IAMControlMapper")
|
|
907
|
+
@patch(f"{PATH}.boto3.Session")
|
|
908
|
+
def test_fetch_compliance_data_from_cache(self, mock_session_class, mock_mapper_class):
|
|
909
|
+
"""Test fetching compliance data from cache."""
|
|
910
|
+
mock_session = MagicMock()
|
|
911
|
+
mock_session.client.return_value = MagicMock()
|
|
912
|
+
mock_session_class.return_value = mock_session
|
|
913
|
+
|
|
914
|
+
cached_data = {"users": [{"UserName": "user1"}], "groups": [], "roles": [], "policies": []}
|
|
915
|
+
|
|
916
|
+
integration = AWSIAMEvidenceIntegration(plan_id=123, region="us-east-1")
|
|
917
|
+
integration._is_cache_valid = Mock(return_value=True)
|
|
918
|
+
integration._load_cached_data = Mock(return_value=cached_data)
|
|
919
|
+
|
|
920
|
+
result = integration.fetch_compliance_data()
|
|
921
|
+
|
|
922
|
+
assert result == [cached_data]
|
|
923
|
+
assert integration.raw_iam_data == cached_data
|
|
924
|
+
|
|
925
|
+
@patch(f"{PATH}.IAMControlMapper")
|
|
926
|
+
@patch(f"{PATH}.boto3.Session")
|
|
927
|
+
def test_fetch_compliance_data_force_refresh(self, mock_session_class, mock_mapper_class):
|
|
928
|
+
"""Test fetching compliance data with force refresh."""
|
|
929
|
+
mock_session = MagicMock()
|
|
930
|
+
mock_session.client.return_value = MagicMock()
|
|
931
|
+
mock_session_class.return_value = mock_session
|
|
932
|
+
|
|
933
|
+
fresh_data = {"users": [{"UserName": "fresh-user"}], "groups": [], "roles": [], "policies": []}
|
|
934
|
+
|
|
935
|
+
integration = AWSIAMEvidenceIntegration(plan_id=123, region="us-east-1", force_refresh=True)
|
|
936
|
+
integration._fetch_fresh_iam_data = Mock(return_value=fresh_data)
|
|
937
|
+
integration._save_to_cache = Mock()
|
|
938
|
+
|
|
939
|
+
result = integration.fetch_compliance_data()
|
|
940
|
+
|
|
941
|
+
assert result == [fresh_data]
|
|
942
|
+
assert integration.raw_iam_data == fresh_data
|
|
943
|
+
integration._save_to_cache.assert_called_once_with(fresh_data)
|
|
944
|
+
|
|
945
|
+
@patch(f"{PATH}.IAMControlMapper")
|
|
946
|
+
@patch(f"{PATH}.boto3.Session")
|
|
947
|
+
def test_create_compliance_item(self, mock_session_class, mock_mapper_class):
|
|
948
|
+
"""Test creating compliance item from raw data."""
|
|
949
|
+
mock_session = MagicMock()
|
|
950
|
+
mock_session.client.return_value = MagicMock()
|
|
951
|
+
mock_session_class.return_value = mock_session
|
|
952
|
+
|
|
953
|
+
mock_mapper = MagicMock()
|
|
954
|
+
mock_mapper.assess_iam_compliance.return_value = {"AC-2": "PASS"}
|
|
955
|
+
mock_mapper_class.return_value = mock_mapper
|
|
956
|
+
|
|
957
|
+
integration = AWSIAMEvidenceIntegration(plan_id=123, region="us-east-1")
|
|
958
|
+
|
|
959
|
+
raw_data = {"users": [{"UserName": "user1"}], "groups": [], "roles": [], "policies": []}
|
|
960
|
+
|
|
961
|
+
result = integration.create_compliance_item(raw_data)
|
|
962
|
+
|
|
963
|
+
assert isinstance(result, IAMComplianceItem)
|
|
964
|
+
assert result.iam_data == raw_data
|
|
965
|
+
|
|
966
|
+
|
|
967
|
+
class TestEvidenceCollection:
|
|
968
|
+
"""Test cases for evidence collection methods."""
|
|
969
|
+
|
|
970
|
+
@patch(f"{PATH}.IAMControlMapper")
|
|
971
|
+
@patch(f"{PATH}.boto3.Session")
|
|
972
|
+
@patch(f"{PATH}.get_current_datetime")
|
|
973
|
+
def test_collect_iam_evidence_as_attachments(self, mock_get_datetime, mock_session_class, mock_mapper_class):
|
|
974
|
+
"""Test collecting evidence as SSP attachments."""
|
|
975
|
+
mock_session = MagicMock()
|
|
976
|
+
mock_session.client.return_value = MagicMock()
|
|
977
|
+
mock_session_class.return_value = mock_session
|
|
978
|
+
|
|
979
|
+
mock_get_datetime.return_value = "2023-12-01"
|
|
980
|
+
|
|
981
|
+
integration = AWSIAMEvidenceIntegration(
|
|
982
|
+
plan_id=123, region="us-east-1", collect_evidence=True, evidence_as_attachments=True
|
|
983
|
+
)
|
|
984
|
+
|
|
985
|
+
integration.raw_iam_data = {"users": [], "groups": [], "roles": [], "policies": []}
|
|
986
|
+
integration._create_ssp_attachment = Mock()
|
|
987
|
+
|
|
988
|
+
integration._collect_iam_evidence()
|
|
989
|
+
|
|
990
|
+
integration._create_ssp_attachment.assert_called_once_with("2023-12-01")
|
|
991
|
+
|
|
992
|
+
@patch(f"{PATH}.IAMControlMapper")
|
|
993
|
+
@patch(f"{PATH}.boto3.Session")
|
|
994
|
+
@patch(f"{PATH}.get_current_datetime")
|
|
995
|
+
def test_collect_iam_evidence_as_records(self, mock_get_datetime, mock_session_class, mock_mapper_class):
|
|
996
|
+
"""Test collecting evidence as evidence records."""
|
|
997
|
+
mock_session = MagicMock()
|
|
998
|
+
mock_session.client.return_value = MagicMock()
|
|
999
|
+
mock_session_class.return_value = mock_session
|
|
1000
|
+
|
|
1001
|
+
mock_get_datetime.return_value = "2023-12-01"
|
|
1002
|
+
|
|
1003
|
+
integration = AWSIAMEvidenceIntegration(
|
|
1004
|
+
plan_id=123, region="us-east-1", collect_evidence=True, evidence_as_attachments=False
|
|
1005
|
+
)
|
|
1006
|
+
|
|
1007
|
+
integration.raw_iam_data = {"users": [], "groups": [], "roles": [], "policies": []}
|
|
1008
|
+
integration._create_evidence_record = Mock()
|
|
1009
|
+
|
|
1010
|
+
integration._collect_iam_evidence()
|
|
1011
|
+
|
|
1012
|
+
integration._create_evidence_record.assert_called_once_with("2023-12-01")
|
|
1013
|
+
|
|
1014
|
+
@patch(f"{PATH}.IAMControlMapper")
|
|
1015
|
+
@patch(f"{PATH}.boto3.Session")
|
|
1016
|
+
def test_collect_iam_evidence_no_data(self, mock_session_class, mock_mapper_class):
|
|
1017
|
+
"""Test collecting evidence when no data is available."""
|
|
1018
|
+
mock_session = MagicMock()
|
|
1019
|
+
mock_session.client.return_value = MagicMock()
|
|
1020
|
+
mock_session_class.return_value = mock_session
|
|
1021
|
+
|
|
1022
|
+
integration = AWSIAMEvidenceIntegration(
|
|
1023
|
+
plan_id=123, region="us-east-1", collect_evidence=True, evidence_as_attachments=True
|
|
1024
|
+
)
|
|
1025
|
+
|
|
1026
|
+
integration.raw_iam_data = {}
|
|
1027
|
+
|
|
1028
|
+
integration._collect_iam_evidence()
|
|
1029
|
+
|
|
1030
|
+
@patch(f"{PATH}.IAMControlMapper")
|
|
1031
|
+
@patch(f"{PATH}.boto3.Session")
|
|
1032
|
+
@patch(f"{PATH}.Api")
|
|
1033
|
+
@patch(f"{PATH}.File")
|
|
1034
|
+
def test_create_ssp_attachment_success(
|
|
1035
|
+
self, mock_file_class, mock_api_class, mock_session_class, mock_mapper_class
|
|
1036
|
+
):
|
|
1037
|
+
"""Test creating SSP attachment successfully."""
|
|
1038
|
+
mock_session = MagicMock()
|
|
1039
|
+
mock_session.client.return_value = MagicMock()
|
|
1040
|
+
mock_session_class.return_value = mock_session
|
|
1041
|
+
|
|
1042
|
+
mock_mapper = MagicMock()
|
|
1043
|
+
mock_mapper.assess_iam_compliance.return_value = {"AC-2": "PASS", "AC-6": "PASS"}
|
|
1044
|
+
mock_mapper_class.return_value = mock_mapper
|
|
1045
|
+
|
|
1046
|
+
mock_api = MagicMock()
|
|
1047
|
+
mock_api_class.return_value = mock_api
|
|
1048
|
+
|
|
1049
|
+
mock_file_class.upload_file_to_regscale.return_value = True
|
|
1050
|
+
|
|
1051
|
+
integration = AWSIAMEvidenceIntegration(plan_id=123, region="us-east-1")
|
|
1052
|
+
integration.raw_iam_data = {"users": [], "groups": [], "roles": [], "policies": []}
|
|
1053
|
+
|
|
1054
|
+
integration._create_ssp_attachment("2023-12-01")
|
|
1055
|
+
|
|
1056
|
+
mock_file_class.upload_file_to_regscale.assert_called_once()
|
|
1057
|
+
call_args = mock_file_class.upload_file_to_regscale.call_args[1]
|
|
1058
|
+
assert call_args["parent_id"] == 123
|
|
1059
|
+
assert call_args["parent_module"] == "securityplans"
|
|
1060
|
+
assert "iam_evidence_" in call_args["file_name"]
|
|
1061
|
+
assert "aws,iam,access-control,automated" == call_args["tags"]
|
|
1062
|
+
|
|
1063
|
+
@patch(f"{PATH}.IAMControlMapper")
|
|
1064
|
+
@patch(f"{PATH}.boto3.Session")
|
|
1065
|
+
@patch(f"{PATH}.Api")
|
|
1066
|
+
@patch(f"{PATH}.File")
|
|
1067
|
+
def test_create_ssp_attachment_failure(
|
|
1068
|
+
self, mock_file_class, mock_api_class, mock_session_class, mock_mapper_class
|
|
1069
|
+
):
|
|
1070
|
+
"""Test creating SSP attachment with failure."""
|
|
1071
|
+
mock_session = MagicMock()
|
|
1072
|
+
mock_session.client.return_value = MagicMock()
|
|
1073
|
+
mock_session_class.return_value = mock_session
|
|
1074
|
+
|
|
1075
|
+
mock_mapper = MagicMock()
|
|
1076
|
+
mock_mapper.assess_iam_compliance.return_value = {}
|
|
1077
|
+
mock_mapper_class.return_value = mock_mapper
|
|
1078
|
+
|
|
1079
|
+
mock_api = MagicMock()
|
|
1080
|
+
mock_api_class.return_value = mock_api
|
|
1081
|
+
|
|
1082
|
+
mock_file_class.upload_file_to_regscale.return_value = False
|
|
1083
|
+
|
|
1084
|
+
integration = AWSIAMEvidenceIntegration(plan_id=123, region="us-east-1")
|
|
1085
|
+
integration.raw_iam_data = {"users": [], "groups": [], "roles": [], "policies": []}
|
|
1086
|
+
|
|
1087
|
+
integration._create_ssp_attachment("2023-12-01")
|
|
1088
|
+
|
|
1089
|
+
@patch(f"{PATH}.IAMControlMapper")
|
|
1090
|
+
@patch(f"{PATH}.boto3.Session")
|
|
1091
|
+
@patch(f"{PATH}.Api")
|
|
1092
|
+
@patch(f"{PATH}.File")
|
|
1093
|
+
def test_create_ssp_attachment_exception(
|
|
1094
|
+
self, mock_file_class, mock_api_class, mock_session_class, mock_mapper_class
|
|
1095
|
+
):
|
|
1096
|
+
"""Test creating SSP attachment with exception."""
|
|
1097
|
+
mock_session = MagicMock()
|
|
1098
|
+
mock_session.client.return_value = MagicMock()
|
|
1099
|
+
mock_session_class.return_value = mock_session
|
|
1100
|
+
|
|
1101
|
+
mock_mapper = MagicMock()
|
|
1102
|
+
mock_mapper.assess_iam_compliance.side_effect = Exception("Test error")
|
|
1103
|
+
mock_mapper_class.return_value = mock_mapper
|
|
1104
|
+
|
|
1105
|
+
integration = AWSIAMEvidenceIntegration(plan_id=123, region="us-east-1")
|
|
1106
|
+
integration.raw_iam_data = {"users": [], "groups": [], "roles": [], "policies": []}
|
|
1107
|
+
|
|
1108
|
+
integration._create_ssp_attachment("2023-12-01")
|
|
1109
|
+
|
|
1110
|
+
@patch(f"{PATH}.IAMControlMapper")
|
|
1111
|
+
@patch(f"{PATH}.boto3.Session")
|
|
1112
|
+
@patch(f"{PATH}.Evidence")
|
|
1113
|
+
def test_create_evidence_record_success(self, mock_evidence_class, mock_session_class, mock_mapper_class):
|
|
1114
|
+
"""Test creating evidence record successfully."""
|
|
1115
|
+
mock_session = MagicMock()
|
|
1116
|
+
mock_session.client.return_value = MagicMock()
|
|
1117
|
+
mock_session_class.return_value = mock_session
|
|
1118
|
+
|
|
1119
|
+
mock_mapper = MagicMock()
|
|
1120
|
+
mock_mapper.assess_iam_compliance.return_value = {"AC-2": "PASS", "AC-6": "FAIL"}
|
|
1121
|
+
mock_mapper.get_control_description.side_effect = lambda x: f"Description for {x}"
|
|
1122
|
+
mock_mapper_class.return_value = mock_mapper
|
|
1123
|
+
|
|
1124
|
+
mock_evidence = MagicMock()
|
|
1125
|
+
mock_evidence.id = 999
|
|
1126
|
+
mock_evidence_instance = MagicMock()
|
|
1127
|
+
mock_evidence_instance.create.return_value = mock_evidence
|
|
1128
|
+
mock_evidence_class.return_value = mock_evidence_instance
|
|
1129
|
+
|
|
1130
|
+
integration = AWSIAMEvidenceIntegration(plan_id=123, region="us-east-1", evidence_frequency=90)
|
|
1131
|
+
integration.raw_iam_data = {"users": [], "groups": [], "roles": [], "policies": []}
|
|
1132
|
+
integration._upload_evidence_file = Mock()
|
|
1133
|
+
integration._link_evidence_to_ssp = Mock()
|
|
1134
|
+
|
|
1135
|
+
integration._create_evidence_record("2023-12-01")
|
|
1136
|
+
|
|
1137
|
+
mock_evidence_instance.create.assert_called_once()
|
|
1138
|
+
integration._upload_evidence_file.assert_called_once_with(999, "2023-12-01")
|
|
1139
|
+
integration._link_evidence_to_ssp.assert_called_once_with(999)
|
|
1140
|
+
|
|
1141
|
+
@patch(f"{PATH}.IAMControlMapper")
|
|
1142
|
+
@patch(f"{PATH}.boto3.Session")
|
|
1143
|
+
@patch(f"{PATH}.Evidence")
|
|
1144
|
+
def test_create_evidence_record_creation_failure(self, mock_evidence_class, mock_session_class, mock_mapper_class):
|
|
1145
|
+
"""Test creating evidence record when creation fails."""
|
|
1146
|
+
mock_session = MagicMock()
|
|
1147
|
+
mock_session.client.return_value = MagicMock()
|
|
1148
|
+
mock_session_class.return_value = mock_session
|
|
1149
|
+
|
|
1150
|
+
mock_mapper = MagicMock()
|
|
1151
|
+
mock_mapper.assess_iam_compliance.return_value = {}
|
|
1152
|
+
mock_mapper_class.return_value = mock_mapper
|
|
1153
|
+
|
|
1154
|
+
mock_evidence_instance = MagicMock()
|
|
1155
|
+
mock_evidence_instance.create.return_value = None
|
|
1156
|
+
mock_evidence_class.return_value = mock_evidence_instance
|
|
1157
|
+
|
|
1158
|
+
integration = AWSIAMEvidenceIntegration(plan_id=123, region="us-east-1")
|
|
1159
|
+
integration.raw_iam_data = {"users": [], "groups": [], "roles": [], "policies": []}
|
|
1160
|
+
|
|
1161
|
+
integration._create_evidence_record("2023-12-01")
|
|
1162
|
+
|
|
1163
|
+
mock_evidence_instance.create.assert_called_once()
|
|
1164
|
+
|
|
1165
|
+
@patch(f"{PATH}.IAMControlMapper")
|
|
1166
|
+
@patch(f"{PATH}.boto3.Session")
|
|
1167
|
+
@patch(f"{PATH}.Evidence")
|
|
1168
|
+
def test_create_evidence_record_exception(self, mock_evidence_class, mock_session_class, mock_mapper_class):
|
|
1169
|
+
"""Test creating evidence record with exception."""
|
|
1170
|
+
mock_session = MagicMock()
|
|
1171
|
+
mock_session.client.return_value = MagicMock()
|
|
1172
|
+
mock_session_class.return_value = mock_session
|
|
1173
|
+
|
|
1174
|
+
mock_mapper = MagicMock()
|
|
1175
|
+
mock_mapper.assess_iam_compliance.side_effect = Exception("Test error")
|
|
1176
|
+
mock_mapper_class.return_value = mock_mapper
|
|
1177
|
+
|
|
1178
|
+
integration = AWSIAMEvidenceIntegration(plan_id=123, region="us-east-1")
|
|
1179
|
+
integration.raw_iam_data = {"users": [], "groups": [], "roles": [], "policies": []}
|
|
1180
|
+
|
|
1181
|
+
integration._create_evidence_record("2023-12-01")
|
|
1182
|
+
|
|
1183
|
+
@patch(f"{PATH}.IAMControlMapper")
|
|
1184
|
+
@patch(f"{PATH}.boto3.Session")
|
|
1185
|
+
def test_build_evidence_description(self, mock_session_class, mock_mapper_class):
|
|
1186
|
+
"""Test building evidence description."""
|
|
1187
|
+
mock_session = MagicMock()
|
|
1188
|
+
mock_session.client.return_value = MagicMock()
|
|
1189
|
+
mock_session_class.return_value = mock_session
|
|
1190
|
+
|
|
1191
|
+
mock_mapper = MagicMock()
|
|
1192
|
+
mock_mapper.assess_iam_compliance.return_value = {"AC-2": "PASS", "AC-6": "FAIL", "IA-2": "PASS"}
|
|
1193
|
+
mock_mapper.get_control_description.side_effect = lambda x: f"{x} Description"
|
|
1194
|
+
mock_mapper_class.return_value = mock_mapper
|
|
1195
|
+
|
|
1196
|
+
integration = AWSIAMEvidenceIntegration(plan_id=123, region="us-east-1")
|
|
1197
|
+
integration.raw_iam_data = {
|
|
1198
|
+
"users": [{"UserName": "user1"}],
|
|
1199
|
+
"groups": [{"GroupName": "group1"}],
|
|
1200
|
+
"roles": [{"RoleName": "role1"}],
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
result = integration._build_evidence_description("2023-12-01")
|
|
1204
|
+
|
|
1205
|
+
assert "AWS IAM Access Control Evidence" in result
|
|
1206
|
+
assert "2023-12-01" in result
|
|
1207
|
+
assert "AC-2" in result
|
|
1208
|
+
assert "AC-6" in result
|
|
1209
|
+
assert "IA-2" in result
|
|
1210
|
+
|
|
1211
|
+
@patch(f"{PATH}.IAMControlMapper")
|
|
1212
|
+
@patch(f"{PATH}.boto3.Session")
|
|
1213
|
+
@patch(f"{PATH}.Api")
|
|
1214
|
+
@patch(f"{PATH}.File")
|
|
1215
|
+
def test_upload_evidence_file_success(self, mock_file_class, mock_api_class, mock_session_class, mock_mapper_class):
|
|
1216
|
+
"""Test uploading evidence file successfully."""
|
|
1217
|
+
mock_session = MagicMock()
|
|
1218
|
+
mock_session.client.return_value = MagicMock()
|
|
1219
|
+
mock_session_class.return_value = mock_session
|
|
1220
|
+
|
|
1221
|
+
mock_mapper = MagicMock()
|
|
1222
|
+
mock_mapper.assess_iam_compliance.return_value = {"AC-2": "PASS"}
|
|
1223
|
+
mock_mapper_class.return_value = mock_mapper
|
|
1224
|
+
|
|
1225
|
+
mock_api = MagicMock()
|
|
1226
|
+
mock_api_class.return_value = mock_api
|
|
1227
|
+
|
|
1228
|
+
mock_file_class.upload_file_to_regscale.return_value = True
|
|
1229
|
+
|
|
1230
|
+
integration = AWSIAMEvidenceIntegration(plan_id=123, region="us-east-1")
|
|
1231
|
+
integration.raw_iam_data = {"users": [], "groups": [], "roles": [], "policies": []}
|
|
1232
|
+
|
|
1233
|
+
integration._upload_evidence_file(999, "2023-12-01")
|
|
1234
|
+
|
|
1235
|
+
mock_file_class.upload_file_to_regscale.assert_called_once()
|
|
1236
|
+
call_args = mock_file_class.upload_file_to_regscale.call_args[1]
|
|
1237
|
+
assert call_args["parent_id"] == 999
|
|
1238
|
+
assert call_args["parent_module"] == "evidence"
|
|
1239
|
+
assert "iam_evidence_" in call_args["file_name"]
|
|
1240
|
+
assert "aws,iam,access-control" == call_args["tags"]
|
|
1241
|
+
|
|
1242
|
+
@patch(f"{PATH}.IAMControlMapper")
|
|
1243
|
+
@patch(f"{PATH}.boto3.Session")
|
|
1244
|
+
@patch(f"{PATH}.Api")
|
|
1245
|
+
@patch(f"{PATH}.File")
|
|
1246
|
+
def test_upload_evidence_file_failure(self, mock_file_class, mock_api_class, mock_session_class, mock_mapper_class):
|
|
1247
|
+
"""Test uploading evidence file with failure."""
|
|
1248
|
+
mock_session = MagicMock()
|
|
1249
|
+
mock_session.client.return_value = MagicMock()
|
|
1250
|
+
mock_session_class.return_value = mock_session
|
|
1251
|
+
|
|
1252
|
+
mock_mapper = MagicMock()
|
|
1253
|
+
mock_mapper.assess_iam_compliance.return_value = {}
|
|
1254
|
+
mock_mapper_class.return_value = mock_mapper
|
|
1255
|
+
|
|
1256
|
+
mock_api = MagicMock()
|
|
1257
|
+
mock_api_class.return_value = mock_api
|
|
1258
|
+
|
|
1259
|
+
mock_file_class.upload_file_to_regscale.return_value = False
|
|
1260
|
+
|
|
1261
|
+
integration = AWSIAMEvidenceIntegration(plan_id=123, region="us-east-1")
|
|
1262
|
+
integration.raw_iam_data = {"users": [], "groups": [], "roles": [], "policies": []}
|
|
1263
|
+
|
|
1264
|
+
integration._upload_evidence_file(999, "2023-12-01")
|
|
1265
|
+
|
|
1266
|
+
@patch(f"{PATH}.IAMControlMapper")
|
|
1267
|
+
@patch(f"{PATH}.boto3.Session")
|
|
1268
|
+
@patch(f"{PATH}.Api")
|
|
1269
|
+
@patch(f"{PATH}.File")
|
|
1270
|
+
def test_upload_evidence_file_exception(
|
|
1271
|
+
self, mock_file_class, mock_api_class, mock_session_class, mock_mapper_class
|
|
1272
|
+
):
|
|
1273
|
+
"""Test uploading evidence file with exception."""
|
|
1274
|
+
mock_session = MagicMock()
|
|
1275
|
+
mock_session.client.return_value = MagicMock()
|
|
1276
|
+
mock_session_class.return_value = mock_session
|
|
1277
|
+
|
|
1278
|
+
mock_mapper = MagicMock()
|
|
1279
|
+
mock_mapper.assess_iam_compliance.side_effect = Exception("Test error")
|
|
1280
|
+
mock_mapper_class.return_value = mock_mapper
|
|
1281
|
+
|
|
1282
|
+
integration = AWSIAMEvidenceIntegration(plan_id=123, region="us-east-1")
|
|
1283
|
+
integration.raw_iam_data = {"users": [], "groups": [], "roles": [], "policies": []}
|
|
1284
|
+
|
|
1285
|
+
integration._upload_evidence_file(999, "2023-12-01")
|
|
1286
|
+
|
|
1287
|
+
@patch(f"{PATH}.IAMControlMapper")
|
|
1288
|
+
@patch(f"{PATH}.boto3.Session")
|
|
1289
|
+
@patch(f"{PATH}.EvidenceMapping")
|
|
1290
|
+
def test_link_evidence_to_ssp_success(self, mock_mapping_class, mock_session_class, mock_mapper_class):
|
|
1291
|
+
"""Test linking evidence to SSP successfully."""
|
|
1292
|
+
mock_session = MagicMock()
|
|
1293
|
+
mock_session.client.return_value = MagicMock()
|
|
1294
|
+
mock_session_class.return_value = mock_session
|
|
1295
|
+
|
|
1296
|
+
mock_mapping = MagicMock()
|
|
1297
|
+
mock_mapping_class.return_value = mock_mapping
|
|
1298
|
+
|
|
1299
|
+
integration = AWSIAMEvidenceIntegration(plan_id=123, region="us-east-1")
|
|
1300
|
+
|
|
1301
|
+
integration._link_evidence_to_ssp(999)
|
|
1302
|
+
|
|
1303
|
+
mock_mapping_class.assert_called_once_with(evidenceID=999, mappedID=123, mappingType="securityplans")
|
|
1304
|
+
mock_mapping.create.assert_called_once()
|
|
1305
|
+
|
|
1306
|
+
@patch(f"{PATH}.IAMControlMapper")
|
|
1307
|
+
@patch(f"{PATH}.boto3.Session")
|
|
1308
|
+
@patch(f"{PATH}.EvidenceMapping")
|
|
1309
|
+
def test_link_evidence_to_ssp_failure(self, mock_mapping_class, mock_session_class, mock_mapper_class):
|
|
1310
|
+
"""Test linking evidence to SSP with failure."""
|
|
1311
|
+
mock_session = MagicMock()
|
|
1312
|
+
mock_session.client.return_value = MagicMock()
|
|
1313
|
+
mock_session_class.return_value = mock_session
|
|
1314
|
+
|
|
1315
|
+
mock_mapping = MagicMock()
|
|
1316
|
+
mock_mapping.create.side_effect = Exception("Test error")
|
|
1317
|
+
mock_mapping_class.return_value = mock_mapping
|
|
1318
|
+
|
|
1319
|
+
integration = AWSIAMEvidenceIntegration(plan_id=123, region="us-east-1")
|
|
1320
|
+
|
|
1321
|
+
integration._link_evidence_to_ssp(999)
|
|
1322
|
+
|
|
1323
|
+
|
|
1324
|
+
class TestSyncCompliance:
|
|
1325
|
+
"""Test cases for sync_compliance method."""
|
|
1326
|
+
|
|
1327
|
+
@patch(f"{PATH}.IAMControlMapper")
|
|
1328
|
+
@patch(f"{PATH}.boto3.Session")
|
|
1329
|
+
def test_sync_compliance_with_evidence_collection(self, mock_session_class, mock_mapper_class):
|
|
1330
|
+
"""Test sync_compliance with evidence collection enabled."""
|
|
1331
|
+
mock_session = MagicMock()
|
|
1332
|
+
mock_session.client.return_value = MagicMock()
|
|
1333
|
+
mock_session_class.return_value = mock_session
|
|
1334
|
+
|
|
1335
|
+
mock_mapper = MagicMock()
|
|
1336
|
+
mock_mapper.assess_iam_compliance.return_value = {"AC-2": "PASS"}
|
|
1337
|
+
mock_mapper_class.return_value = mock_mapper
|
|
1338
|
+
|
|
1339
|
+
integration = AWSIAMEvidenceIntegration(plan_id=123, region="us-east-1", collect_evidence=True)
|
|
1340
|
+
integration.fetch_compliance_data = Mock(
|
|
1341
|
+
return_value=[{"users": [], "groups": [], "roles": [], "policies": []}]
|
|
1342
|
+
)
|
|
1343
|
+
integration._collect_iam_evidence = Mock()
|
|
1344
|
+
|
|
1345
|
+
with patch.object(integration.__class__.__bases__[0], "sync_compliance"):
|
|
1346
|
+
integration.sync_compliance()
|
|
1347
|
+
|
|
1348
|
+
integration._collect_iam_evidence.assert_called_once()
|
|
1349
|
+
|
|
1350
|
+
@patch(f"{PATH}.IAMControlMapper")
|
|
1351
|
+
@patch(f"{PATH}.boto3.Session")
|
|
1352
|
+
def test_sync_compliance_without_evidence_collection(self, mock_session_class, mock_mapper_class):
|
|
1353
|
+
"""Test sync_compliance without evidence collection."""
|
|
1354
|
+
mock_session = MagicMock()
|
|
1355
|
+
mock_session.client.return_value = MagicMock()
|
|
1356
|
+
mock_session_class.return_value = mock_session
|
|
1357
|
+
|
|
1358
|
+
mock_mapper = MagicMock()
|
|
1359
|
+
mock_mapper.assess_iam_compliance.return_value = {"AC-2": "PASS"}
|
|
1360
|
+
mock_mapper_class.return_value = mock_mapper
|
|
1361
|
+
|
|
1362
|
+
integration = AWSIAMEvidenceIntegration(plan_id=123, region="us-east-1", collect_evidence=False)
|
|
1363
|
+
integration.fetch_compliance_data = Mock(
|
|
1364
|
+
return_value=[{"users": [], "groups": [], "roles": [], "policies": []}]
|
|
1365
|
+
)
|
|
1366
|
+
integration._collect_iam_evidence = Mock()
|
|
1367
|
+
|
|
1368
|
+
with patch.object(integration.__class__.__bases__[0], "sync_compliance"):
|
|
1369
|
+
integration.sync_compliance()
|
|
1370
|
+
|
|
1371
|
+
integration._collect_iam_evidence.assert_not_called()
|
|
1372
|
+
|
|
1373
|
+
|
|
1374
|
+
if __name__ == "__main__":
|
|
1375
|
+
pytest.main([__file__, "-v"])
|