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,534 @@
|
|
|
1
|
+
"""Unit tests for AWS CloudTrail collector."""
|
|
2
|
+
|
|
3
|
+
import unittest
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from unittest.mock import MagicMock, Mock, patch
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
from botocore.exceptions import ClientError
|
|
9
|
+
|
|
10
|
+
from regscale.integrations.commercial.aws.inventory.resources.cloudtrail import (
|
|
11
|
+
CloudTrailCollector,
|
|
12
|
+
CloudTrailEventsCollector,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class TestCloudTrailCollector(unittest.TestCase):
|
|
17
|
+
"""Test cases for CloudTrailCollector."""
|
|
18
|
+
|
|
19
|
+
def setUp(self):
|
|
20
|
+
"""Set up test fixtures."""
|
|
21
|
+
self.mock_session = MagicMock()
|
|
22
|
+
self.region = "us-east-1"
|
|
23
|
+
self.account_id = "123456789012"
|
|
24
|
+
self.collector = CloudTrailCollector(self.mock_session, self.region, self.account_id)
|
|
25
|
+
|
|
26
|
+
def test_init(self):
|
|
27
|
+
"""Test CloudTrailCollector initialization."""
|
|
28
|
+
assert self.collector.session == self.mock_session
|
|
29
|
+
assert self.collector.region == self.region
|
|
30
|
+
assert self.collector.account_id == self.account_id
|
|
31
|
+
|
|
32
|
+
def test_init_without_account_id(self):
|
|
33
|
+
"""Test CloudTrailCollector initialization without account ID."""
|
|
34
|
+
collector = CloudTrailCollector(self.mock_session, self.region)
|
|
35
|
+
assert collector.account_id is None
|
|
36
|
+
|
|
37
|
+
@patch("regscale.integrations.commercial.aws.inventory.resources.cloudtrail.logger")
|
|
38
|
+
def test_collect_success(self, mock_logger):
|
|
39
|
+
"""Test successful collection of CloudTrail trails."""
|
|
40
|
+
# Setup mock client
|
|
41
|
+
mock_client = MagicMock()
|
|
42
|
+
self.mock_session.client.return_value = mock_client
|
|
43
|
+
|
|
44
|
+
# Mock trail data
|
|
45
|
+
trail_arn = f"arn:aws:cloudtrail:{self.region}:{self.account_id}:trail/test-trail"
|
|
46
|
+
mock_client.list_trails.return_value = {"Trails": [{"TrailARN": trail_arn, "Name": "test-trail"}]}
|
|
47
|
+
|
|
48
|
+
mock_client.describe_trails.return_value = {
|
|
49
|
+
"trailList": [
|
|
50
|
+
{
|
|
51
|
+
"Name": "test-trail",
|
|
52
|
+
"TrailARN": trail_arn,
|
|
53
|
+
"S3BucketName": "test-bucket",
|
|
54
|
+
"IsMultiRegionTrail": True,
|
|
55
|
+
"IsOrganizationTrail": False,
|
|
56
|
+
}
|
|
57
|
+
]
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
mock_client.get_trail_status.return_value = {
|
|
61
|
+
"IsLogging": True,
|
|
62
|
+
"LatestDeliveryTime": datetime(2024, 1, 1),
|
|
63
|
+
"ResponseMetadata": {"RequestId": "test"},
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
mock_client.get_event_selectors.return_value = {
|
|
67
|
+
"EventSelectors": [{"ReadWriteType": "All", "IncludeManagementEvents": True}]
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
# Execute
|
|
71
|
+
result = self.collector.collect()
|
|
72
|
+
|
|
73
|
+
# Verify
|
|
74
|
+
assert "Trails" in result
|
|
75
|
+
assert "TrailStatuses" in result
|
|
76
|
+
assert len(result["Trails"]) == 1
|
|
77
|
+
assert result["Trails"][0]["Name"] == "test-trail"
|
|
78
|
+
assert result["Trails"][0]["Region"] == self.region
|
|
79
|
+
assert "Status" in result["Trails"][0]
|
|
80
|
+
assert "EventSelectors" in result["Trails"][0]
|
|
81
|
+
assert result["Trails"][0]["Status"]["IsLogging"] is True
|
|
82
|
+
|
|
83
|
+
@patch("regscale.integrations.commercial.aws.inventory.resources.cloudtrail.logger")
|
|
84
|
+
def test_collect_filters_by_account_id(self, mock_logger):
|
|
85
|
+
"""Test that collection filters trails by account ID."""
|
|
86
|
+
# Setup mock client
|
|
87
|
+
mock_client = MagicMock()
|
|
88
|
+
self.mock_session.client.return_value = mock_client
|
|
89
|
+
|
|
90
|
+
# Mock trails from different accounts
|
|
91
|
+
trail_arn_match = f"arn:aws:cloudtrail:{self.region}:{self.account_id}:trail/test-trail-1"
|
|
92
|
+
trail_arn_no_match = f"arn:aws:cloudtrail:{self.region}:999999999999:trail/test-trail-2"
|
|
93
|
+
|
|
94
|
+
mock_client.list_trails.return_value = {
|
|
95
|
+
"Trails": [
|
|
96
|
+
{"TrailARN": trail_arn_match, "Name": "test-trail-1"},
|
|
97
|
+
{"TrailARN": trail_arn_no_match, "Name": "test-trail-2"},
|
|
98
|
+
]
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
mock_client.describe_trails.return_value = {
|
|
102
|
+
"trailList": [{"Name": "test-trail-1", "TrailARN": trail_arn_match}]
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
mock_client.get_trail_status.return_value = {"IsLogging": True}
|
|
106
|
+
mock_client.get_event_selectors.return_value = {"EventSelectors": []}
|
|
107
|
+
|
|
108
|
+
# Execute
|
|
109
|
+
result = self.collector.collect()
|
|
110
|
+
|
|
111
|
+
# Verify - should only have one trail (the matching account)
|
|
112
|
+
assert len(result["Trails"]) == 1
|
|
113
|
+
assert result["Trails"][0]["Name"] == "test-trail-1"
|
|
114
|
+
mock_logger.debug.assert_called()
|
|
115
|
+
|
|
116
|
+
@patch("regscale.integrations.commercial.aws.inventory.resources.cloudtrail.logger")
|
|
117
|
+
def test_collect_no_account_filter(self, mock_logger):
|
|
118
|
+
"""Test collection without account ID filter."""
|
|
119
|
+
# Create collector without account ID
|
|
120
|
+
collector = CloudTrailCollector(self.mock_session, self.region)
|
|
121
|
+
|
|
122
|
+
# Setup mock client
|
|
123
|
+
mock_client = MagicMock()
|
|
124
|
+
self.mock_session.client.return_value = mock_client
|
|
125
|
+
|
|
126
|
+
trail_arn_1 = f"arn:aws:cloudtrail:{self.region}:111111111111:trail/trail-1"
|
|
127
|
+
trail_arn_2 = f"arn:aws:cloudtrail:{self.region}:222222222222:trail/trail-2"
|
|
128
|
+
|
|
129
|
+
mock_client.list_trails.return_value = {
|
|
130
|
+
"Trails": [{"TrailARN": trail_arn_1, "Name": "trail-1"}, {"TrailARN": trail_arn_2, "Name": "trail-2"}]
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
mock_client.describe_trails.side_effect = [
|
|
134
|
+
{"trailList": [{"Name": "trail-1", "TrailARN": trail_arn_1}]},
|
|
135
|
+
{"trailList": [{"Name": "trail-2", "TrailARN": trail_arn_2}]},
|
|
136
|
+
]
|
|
137
|
+
|
|
138
|
+
mock_client.get_trail_status.return_value = {"IsLogging": True}
|
|
139
|
+
mock_client.get_event_selectors.return_value = {"EventSelectors": []}
|
|
140
|
+
|
|
141
|
+
# Execute
|
|
142
|
+
result = collector.collect()
|
|
143
|
+
|
|
144
|
+
# Verify - should have both trails
|
|
145
|
+
assert len(result["Trails"]) == 2
|
|
146
|
+
|
|
147
|
+
@patch("regscale.integrations.commercial.aws.inventory.resources.cloudtrail.logger")
|
|
148
|
+
def test_collect_handles_access_denied(self, mock_logger):
|
|
149
|
+
"""Test collection handles AccessDeniedException."""
|
|
150
|
+
# Setup mock client
|
|
151
|
+
mock_client = MagicMock()
|
|
152
|
+
self.mock_session.client.return_value = mock_client
|
|
153
|
+
|
|
154
|
+
# Simulate AccessDeniedException
|
|
155
|
+
error_response = {"Error": {"Code": "AccessDeniedException", "Message": "Access denied"}}
|
|
156
|
+
mock_client.list_trails.side_effect = ClientError(error_response, "list_trails")
|
|
157
|
+
|
|
158
|
+
# Execute
|
|
159
|
+
result = self.collector.collect()
|
|
160
|
+
|
|
161
|
+
# Verify
|
|
162
|
+
assert result["Trails"] == []
|
|
163
|
+
mock_logger.warning.assert_called()
|
|
164
|
+
|
|
165
|
+
@patch("regscale.integrations.commercial.aws.inventory.resources.cloudtrail.logger")
|
|
166
|
+
def test_collect_handles_unexpected_error(self, mock_logger):
|
|
167
|
+
"""Test collection handles unexpected errors."""
|
|
168
|
+
# Setup mock client
|
|
169
|
+
mock_client = MagicMock()
|
|
170
|
+
self.mock_session.client.return_value = mock_client
|
|
171
|
+
|
|
172
|
+
# Simulate unexpected error
|
|
173
|
+
mock_client.list_trails.side_effect = Exception("Unexpected error")
|
|
174
|
+
|
|
175
|
+
# Execute
|
|
176
|
+
result = self.collector.collect()
|
|
177
|
+
|
|
178
|
+
# Verify
|
|
179
|
+
assert result["Trails"] == []
|
|
180
|
+
mock_logger.error.assert_called()
|
|
181
|
+
|
|
182
|
+
def test_list_trails_success(self):
|
|
183
|
+
"""Test successful listing of trails."""
|
|
184
|
+
mock_client = MagicMock()
|
|
185
|
+
mock_client.list_trails.return_value = {"Trails": [{"TrailARN": "arn:test", "Name": "test"}]}
|
|
186
|
+
|
|
187
|
+
result = self.collector._list_trails(mock_client)
|
|
188
|
+
|
|
189
|
+
assert len(result) == 1
|
|
190
|
+
assert result[0]["Name"] == "test"
|
|
191
|
+
|
|
192
|
+
def test_list_trails_access_denied(self):
|
|
193
|
+
"""Test listing trails with access denied."""
|
|
194
|
+
mock_client = MagicMock()
|
|
195
|
+
error_response = {"Error": {"Code": "AccessDeniedException", "Message": "Access denied"}}
|
|
196
|
+
mock_client.list_trails.side_effect = ClientError(error_response, "list_trails")
|
|
197
|
+
|
|
198
|
+
result = self.collector._list_trails(mock_client)
|
|
199
|
+
|
|
200
|
+
assert result == []
|
|
201
|
+
|
|
202
|
+
def test_describe_trail_success(self):
|
|
203
|
+
"""Test successful trail description."""
|
|
204
|
+
mock_client = MagicMock()
|
|
205
|
+
trail_arn = "arn:aws:cloudtrail:us-east-1:123456789012:trail/test"
|
|
206
|
+
mock_client.describe_trails.return_value = {"trailList": [{"Name": "test", "TrailARN": trail_arn}]}
|
|
207
|
+
|
|
208
|
+
result = self.collector._describe_trail(mock_client, trail_arn)
|
|
209
|
+
|
|
210
|
+
assert result is not None
|
|
211
|
+
assert result["Name"] == "test"
|
|
212
|
+
|
|
213
|
+
def test_describe_trail_not_found(self):
|
|
214
|
+
"""Test trail description when trail not found."""
|
|
215
|
+
mock_client = MagicMock()
|
|
216
|
+
trail_arn = "arn:aws:cloudtrail:us-east-1:123456789012:trail/nonexistent"
|
|
217
|
+
error_response = {"Error": {"Code": "TrailNotFoundException", "Message": "Trail not found"}}
|
|
218
|
+
mock_client.describe_trails.side_effect = ClientError(error_response, "describe_trails")
|
|
219
|
+
|
|
220
|
+
result = self.collector._describe_trail(mock_client, trail_arn)
|
|
221
|
+
|
|
222
|
+
assert result is None
|
|
223
|
+
|
|
224
|
+
def test_describe_trail_empty_response(self):
|
|
225
|
+
"""Test trail description with empty response."""
|
|
226
|
+
mock_client = MagicMock()
|
|
227
|
+
trail_arn = "arn:aws:cloudtrail:us-east-1:123456789012:trail/test"
|
|
228
|
+
mock_client.describe_trails.return_value = {"trailList": []}
|
|
229
|
+
|
|
230
|
+
result = self.collector._describe_trail(mock_client, trail_arn)
|
|
231
|
+
|
|
232
|
+
assert result is None
|
|
233
|
+
|
|
234
|
+
def test_get_trail_status_success(self):
|
|
235
|
+
"""Test successful retrieval of trail status."""
|
|
236
|
+
mock_client = MagicMock()
|
|
237
|
+
trail_arn = "arn:aws:cloudtrail:us-east-1:123456789012:trail/test"
|
|
238
|
+
mock_client.get_trail_status.return_value = {
|
|
239
|
+
"IsLogging": True,
|
|
240
|
+
"LatestDeliveryTime": datetime(2024, 1, 1),
|
|
241
|
+
"ResponseMetadata": {"RequestId": "test"},
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
result = self.collector._get_trail_status(mock_client, trail_arn)
|
|
245
|
+
|
|
246
|
+
assert "IsLogging" in result
|
|
247
|
+
assert result["IsLogging"] is True
|
|
248
|
+
assert "ResponseMetadata" not in result # Should be removed
|
|
249
|
+
|
|
250
|
+
def test_get_trail_status_error(self):
|
|
251
|
+
"""Test trail status retrieval with error."""
|
|
252
|
+
mock_client = MagicMock()
|
|
253
|
+
trail_arn = "arn:aws:cloudtrail:us-east-1:123456789012:trail/test"
|
|
254
|
+
mock_client.get_trail_status.side_effect = ClientError(
|
|
255
|
+
{"Error": {"Code": "TrailNotFoundException", "Message": "Trail not found"}}, "get_trail_status"
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
result = self.collector._get_trail_status(mock_client, trail_arn)
|
|
259
|
+
|
|
260
|
+
assert result == {}
|
|
261
|
+
|
|
262
|
+
def test_get_event_selectors_success(self):
|
|
263
|
+
"""Test successful retrieval of event selectors."""
|
|
264
|
+
mock_client = MagicMock()
|
|
265
|
+
trail_arn = "arn:aws:cloudtrail:us-east-1:123456789012:trail/test"
|
|
266
|
+
mock_client.get_event_selectors.return_value = {
|
|
267
|
+
"EventSelectors": [{"ReadWriteType": "All", "IncludeManagementEvents": True}]
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
result = self.collector._get_event_selectors(mock_client, trail_arn)
|
|
271
|
+
|
|
272
|
+
assert len(result) == 1
|
|
273
|
+
assert result[0]["ReadWriteType"] == "All"
|
|
274
|
+
|
|
275
|
+
def test_get_event_selectors_error(self):
|
|
276
|
+
"""Test event selectors retrieval with error."""
|
|
277
|
+
mock_client = MagicMock()
|
|
278
|
+
trail_arn = "arn:aws:cloudtrail:us-east-1:123456789012:trail/test"
|
|
279
|
+
mock_client.get_event_selectors.side_effect = ClientError(
|
|
280
|
+
{"Error": {"Code": "TrailNotFoundException", "Message": "Trail not found"}}, "get_event_selectors"
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
result = self.collector._get_event_selectors(mock_client, trail_arn)
|
|
284
|
+
|
|
285
|
+
assert result == []
|
|
286
|
+
|
|
287
|
+
def test_matches_account_id_with_matching_arn(self):
|
|
288
|
+
"""Test account ID matching with matching ARN."""
|
|
289
|
+
trail_arn = f"arn:aws:cloudtrail:{self.region}:{self.account_id}:trail/test"
|
|
290
|
+
assert self.collector._matches_account_id(trail_arn) is True
|
|
291
|
+
|
|
292
|
+
def test_matches_account_id_with_non_matching_arn(self):
|
|
293
|
+
"""Test account ID matching with non-matching ARN."""
|
|
294
|
+
trail_arn = f"arn:aws:cloudtrail:{self.region}:999999999999:trail/test"
|
|
295
|
+
assert self.collector._matches_account_id(trail_arn) is False
|
|
296
|
+
|
|
297
|
+
def test_matches_account_id_with_invalid_arn(self):
|
|
298
|
+
"""Test account ID matching with invalid ARN."""
|
|
299
|
+
trail_arn = "invalid-arn"
|
|
300
|
+
assert self.collector._matches_account_id(trail_arn) is False
|
|
301
|
+
|
|
302
|
+
def test_matches_account_id_without_filter(self):
|
|
303
|
+
"""Test account ID matching without account filter."""
|
|
304
|
+
collector = CloudTrailCollector(self.mock_session, self.region)
|
|
305
|
+
trail_arn = f"arn:aws:cloudtrail:{self.region}:999999999999:trail/test"
|
|
306
|
+
assert collector._matches_account_id(trail_arn) is True
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
class TestCloudTrailEventsCollector(unittest.TestCase):
|
|
310
|
+
"""Test cases for CloudTrailEventsCollector."""
|
|
311
|
+
|
|
312
|
+
def setUp(self):
|
|
313
|
+
"""Set up test fixtures."""
|
|
314
|
+
self.mock_session = MagicMock()
|
|
315
|
+
self.region = "us-east-1"
|
|
316
|
+
self.collector = CloudTrailEventsCollector(self.mock_session, self.region)
|
|
317
|
+
|
|
318
|
+
def test_init_default_params(self):
|
|
319
|
+
"""Test CloudTrailEventsCollector initialization with defaults."""
|
|
320
|
+
assert self.collector.session == self.mock_session
|
|
321
|
+
assert self.collector.region == self.region
|
|
322
|
+
assert self.collector.max_results == 50
|
|
323
|
+
assert self.collector.lookup_attributes == []
|
|
324
|
+
assert self.collector.start_time is None
|
|
325
|
+
assert self.collector.end_time is None
|
|
326
|
+
|
|
327
|
+
def test_init_custom_params(self):
|
|
328
|
+
"""Test CloudTrailEventsCollector initialization with custom params."""
|
|
329
|
+
start_time = datetime(2024, 1, 1)
|
|
330
|
+
end_time = datetime(2024, 1, 31)
|
|
331
|
+
lookup_attributes = [{"AttributeKey": "EventName", "AttributeValue": "CreateBucket"}]
|
|
332
|
+
|
|
333
|
+
collector = CloudTrailEventsCollector(
|
|
334
|
+
self.mock_session,
|
|
335
|
+
self.region,
|
|
336
|
+
max_results=25,
|
|
337
|
+
lookup_attributes=lookup_attributes,
|
|
338
|
+
start_time=start_time,
|
|
339
|
+
end_time=end_time,
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
assert collector.max_results == 25
|
|
343
|
+
assert collector.lookup_attributes == lookup_attributes
|
|
344
|
+
assert collector.start_time == start_time
|
|
345
|
+
assert collector.end_time == end_time
|
|
346
|
+
|
|
347
|
+
def test_init_enforces_max_results_limit(self):
|
|
348
|
+
"""Test that max_results is capped at 50."""
|
|
349
|
+
collector = CloudTrailEventsCollector(self.mock_session, self.region, max_results=100)
|
|
350
|
+
assert collector.max_results == 50
|
|
351
|
+
|
|
352
|
+
@patch("regscale.integrations.commercial.aws.inventory.resources.cloudtrail.logger")
|
|
353
|
+
def test_collect_success(self, mock_logger):
|
|
354
|
+
"""Test successful collection of CloudTrail events."""
|
|
355
|
+
# Setup mock client
|
|
356
|
+
mock_client = MagicMock()
|
|
357
|
+
self.mock_session.client.return_value = mock_client
|
|
358
|
+
|
|
359
|
+
# Mock event data
|
|
360
|
+
mock_events = [
|
|
361
|
+
{
|
|
362
|
+
"EventId": "event-1",
|
|
363
|
+
"EventName": "CreateBucket",
|
|
364
|
+
"EventTime": datetime(2024, 1, 1),
|
|
365
|
+
"Username": "test-user",
|
|
366
|
+
},
|
|
367
|
+
{
|
|
368
|
+
"EventId": "event-2",
|
|
369
|
+
"EventName": "PutObject",
|
|
370
|
+
"EventTime": datetime(2024, 1, 2),
|
|
371
|
+
"Username": "test-user",
|
|
372
|
+
},
|
|
373
|
+
]
|
|
374
|
+
|
|
375
|
+
mock_client.lookup_events.return_value = {"Events": mock_events}
|
|
376
|
+
|
|
377
|
+
# Execute
|
|
378
|
+
result = self.collector.collect()
|
|
379
|
+
|
|
380
|
+
# Verify
|
|
381
|
+
assert "Events" in result
|
|
382
|
+
assert "EventCount" in result
|
|
383
|
+
assert result["EventCount"] == 2
|
|
384
|
+
assert len(result["Events"]) == 2
|
|
385
|
+
assert result["Events"][0]["EventName"] == "CreateBucket"
|
|
386
|
+
|
|
387
|
+
@patch("regscale.integrations.commercial.aws.inventory.resources.cloudtrail.logger")
|
|
388
|
+
def test_collect_with_pagination(self, mock_logger):
|
|
389
|
+
"""Test collection with pagination."""
|
|
390
|
+
# Setup mock client
|
|
391
|
+
mock_client = MagicMock()
|
|
392
|
+
self.mock_session.client.return_value = mock_client
|
|
393
|
+
|
|
394
|
+
# Mock paginated responses
|
|
395
|
+
mock_client.lookup_events.side_effect = [
|
|
396
|
+
{"Events": [{"EventId": "event-1"}], "NextToken": "token-1"},
|
|
397
|
+
{"Events": [{"EventId": "event-2"}], "NextToken": "token-2"},
|
|
398
|
+
{"Events": [{"EventId": "event-3"}]}, # No NextToken
|
|
399
|
+
]
|
|
400
|
+
|
|
401
|
+
# Execute
|
|
402
|
+
result = self.collector.collect()
|
|
403
|
+
|
|
404
|
+
# Verify
|
|
405
|
+
assert result["EventCount"] == 3
|
|
406
|
+
assert len(result["Events"]) == 3
|
|
407
|
+
assert mock_client.lookup_events.call_count == 3
|
|
408
|
+
|
|
409
|
+
@patch("regscale.integrations.commercial.aws.inventory.resources.cloudtrail.logger")
|
|
410
|
+
def test_collect_with_lookup_attributes(self, mock_logger):
|
|
411
|
+
"""Test collection with lookup attributes."""
|
|
412
|
+
lookup_attributes = [{"AttributeKey": "EventName", "AttributeValue": "CreateBucket"}]
|
|
413
|
+
collector = CloudTrailEventsCollector(self.mock_session, self.region, lookup_attributes=lookup_attributes)
|
|
414
|
+
|
|
415
|
+
# Setup mock client
|
|
416
|
+
mock_client = MagicMock()
|
|
417
|
+
self.mock_session.client.return_value = mock_client
|
|
418
|
+
mock_client.lookup_events.return_value = {"Events": [{"EventId": "event-1"}]}
|
|
419
|
+
|
|
420
|
+
# Execute
|
|
421
|
+
result = collector.collect()
|
|
422
|
+
|
|
423
|
+
# Verify lookup_events was called with correct parameters
|
|
424
|
+
call_args = mock_client.lookup_events.call_args[1]
|
|
425
|
+
assert "LookupAttributes" in call_args
|
|
426
|
+
assert call_args["LookupAttributes"] == lookup_attributes
|
|
427
|
+
assert result["EventCount"] == 1
|
|
428
|
+
|
|
429
|
+
@patch("regscale.integrations.commercial.aws.inventory.resources.cloudtrail.logger")
|
|
430
|
+
def test_collect_with_time_range(self, mock_logger):
|
|
431
|
+
"""Test collection with time range."""
|
|
432
|
+
start_time = datetime(2024, 1, 1)
|
|
433
|
+
end_time = datetime(2024, 1, 31)
|
|
434
|
+
collector = CloudTrailEventsCollector(self.mock_session, self.region, start_time=start_time, end_time=end_time)
|
|
435
|
+
|
|
436
|
+
# Setup mock client
|
|
437
|
+
mock_client = MagicMock()
|
|
438
|
+
self.mock_session.client.return_value = mock_client
|
|
439
|
+
mock_client.lookup_events.return_value = {"Events": [{"EventId": "event-1"}]}
|
|
440
|
+
|
|
441
|
+
# Execute
|
|
442
|
+
result = collector.collect()
|
|
443
|
+
|
|
444
|
+
# Verify lookup_events was called with correct parameters
|
|
445
|
+
call_args = mock_client.lookup_events.call_args[1]
|
|
446
|
+
assert "StartTime" in call_args
|
|
447
|
+
assert "EndTime" in call_args
|
|
448
|
+
assert call_args["StartTime"] == start_time
|
|
449
|
+
assert call_args["EndTime"] == end_time
|
|
450
|
+
assert result["EventCount"] == 1
|
|
451
|
+
|
|
452
|
+
@patch("regscale.integrations.commercial.aws.inventory.resources.cloudtrail.logger")
|
|
453
|
+
def test_collect_handles_access_denied(self, mock_logger):
|
|
454
|
+
"""Test collection handles AccessDeniedException."""
|
|
455
|
+
# Setup mock client
|
|
456
|
+
mock_client = MagicMock()
|
|
457
|
+
self.mock_session.client.return_value = mock_client
|
|
458
|
+
|
|
459
|
+
# Simulate AccessDeniedException
|
|
460
|
+
error_response = {"Error": {"Code": "AccessDeniedException", "Message": "Access denied"}}
|
|
461
|
+
mock_client.lookup_events.side_effect = ClientError(error_response, "lookup_events")
|
|
462
|
+
|
|
463
|
+
# Execute
|
|
464
|
+
result = self.collector.collect()
|
|
465
|
+
|
|
466
|
+
# Verify
|
|
467
|
+
assert result["Events"] == []
|
|
468
|
+
assert result["EventCount"] == 0
|
|
469
|
+
mock_logger.warning.assert_called()
|
|
470
|
+
|
|
471
|
+
@patch("regscale.integrations.commercial.aws.inventory.resources.cloudtrail.logger")
|
|
472
|
+
def test_collect_handles_unexpected_error(self, mock_logger):
|
|
473
|
+
"""Test collection handles unexpected errors."""
|
|
474
|
+
# Setup mock client
|
|
475
|
+
mock_client = MagicMock()
|
|
476
|
+
self.mock_session.client.return_value = mock_client
|
|
477
|
+
|
|
478
|
+
# Simulate unexpected error
|
|
479
|
+
mock_client.lookup_events.side_effect = Exception("Unexpected error")
|
|
480
|
+
|
|
481
|
+
# Execute
|
|
482
|
+
result = self.collector.collect()
|
|
483
|
+
|
|
484
|
+
# Verify
|
|
485
|
+
assert result["Events"] == []
|
|
486
|
+
mock_logger.error.assert_called()
|
|
487
|
+
|
|
488
|
+
def test_lookup_events_success(self):
|
|
489
|
+
"""Test successful event lookup."""
|
|
490
|
+
mock_client = MagicMock()
|
|
491
|
+
mock_client.lookup_events.return_value = {"Events": [{"EventId": "event-1"}, {"EventId": "event-2"}]}
|
|
492
|
+
|
|
493
|
+
result = self.collector._lookup_events(mock_client)
|
|
494
|
+
|
|
495
|
+
assert len(result) == 2
|
|
496
|
+
assert result[0]["EventId"] == "event-1"
|
|
497
|
+
|
|
498
|
+
def test_lookup_events_with_pagination(self):
|
|
499
|
+
"""Test event lookup with pagination."""
|
|
500
|
+
mock_client = MagicMock()
|
|
501
|
+
mock_client.lookup_events.side_effect = [
|
|
502
|
+
{"Events": [{"EventId": "event-1"}], "NextToken": "token-1"},
|
|
503
|
+
{"Events": [{"EventId": "event-2"}], "NextToken": "token-2"},
|
|
504
|
+
{"Events": [{"EventId": "event-3"}]},
|
|
505
|
+
]
|
|
506
|
+
|
|
507
|
+
result = self.collector._lookup_events(mock_client)
|
|
508
|
+
|
|
509
|
+
assert len(result) == 3
|
|
510
|
+
assert mock_client.lookup_events.call_count == 3
|
|
511
|
+
|
|
512
|
+
def test_lookup_events_access_denied(self):
|
|
513
|
+
"""Test event lookup with access denied."""
|
|
514
|
+
mock_client = MagicMock()
|
|
515
|
+
error_response = {"Error": {"Code": "AccessDeniedException", "Message": "Access denied"}}
|
|
516
|
+
mock_client.lookup_events.side_effect = ClientError(error_response, "lookup_events")
|
|
517
|
+
|
|
518
|
+
result = self.collector._lookup_events(mock_client)
|
|
519
|
+
|
|
520
|
+
assert result == []
|
|
521
|
+
|
|
522
|
+
def test_lookup_events_other_error(self):
|
|
523
|
+
"""Test event lookup with other error."""
|
|
524
|
+
mock_client = MagicMock()
|
|
525
|
+
error_response = {"Error": {"Code": "InternalError", "Message": "Internal error"}}
|
|
526
|
+
mock_client.lookup_events.side_effect = ClientError(error_response, "lookup_events")
|
|
527
|
+
|
|
528
|
+
result = self.collector._lookup_events(mock_client)
|
|
529
|
+
|
|
530
|
+
assert result == []
|
|
531
|
+
|
|
532
|
+
|
|
533
|
+
if __name__ == "__main__":
|
|
534
|
+
pytest.main([__file__, "-v"])
|