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.

Files changed (112) hide show
  1. regscale/_version.py +1 -1
  2. regscale/core/app/utils/app_utils.py +11 -2
  3. regscale/dev/cli.py +26 -0
  4. regscale/dev/version.py +72 -0
  5. regscale/integrations/commercial/__init__.py +15 -1
  6. regscale/integrations/commercial/amazon/amazon/__init__.py +0 -0
  7. regscale/integrations/commercial/amazon/amazon/common.py +204 -0
  8. regscale/integrations/commercial/amazon/common.py +48 -58
  9. regscale/integrations/commercial/aws/audit_manager_compliance.py +2671 -0
  10. regscale/integrations/commercial/aws/cli.py +3093 -55
  11. regscale/integrations/commercial/aws/cloudtrail_control_mappings.py +333 -0
  12. regscale/integrations/commercial/aws/cloudtrail_evidence.py +501 -0
  13. regscale/integrations/commercial/aws/cloudwatch_control_mappings.py +357 -0
  14. regscale/integrations/commercial/aws/cloudwatch_evidence.py +490 -0
  15. regscale/integrations/commercial/aws/config_compliance.py +914 -0
  16. regscale/integrations/commercial/aws/conformance_pack_mappings.py +198 -0
  17. regscale/integrations/commercial/aws/evidence_generator.py +283 -0
  18. regscale/integrations/commercial/aws/guardduty_control_mappings.py +340 -0
  19. regscale/integrations/commercial/aws/guardduty_evidence.py +1053 -0
  20. regscale/integrations/commercial/aws/iam_control_mappings.py +368 -0
  21. regscale/integrations/commercial/aws/iam_evidence.py +574 -0
  22. regscale/integrations/commercial/aws/inventory/__init__.py +223 -22
  23. regscale/integrations/commercial/aws/inventory/base.py +107 -5
  24. regscale/integrations/commercial/aws/inventory/resources/audit_manager.py +513 -0
  25. regscale/integrations/commercial/aws/inventory/resources/cloudtrail.py +315 -0
  26. regscale/integrations/commercial/aws/inventory/resources/cloudtrail_logs_metadata.py +476 -0
  27. regscale/integrations/commercial/aws/inventory/resources/cloudwatch.py +191 -0
  28. regscale/integrations/commercial/aws/inventory/resources/compute.py +66 -9
  29. regscale/integrations/commercial/aws/inventory/resources/config.py +464 -0
  30. regscale/integrations/commercial/aws/inventory/resources/containers.py +74 -9
  31. regscale/integrations/commercial/aws/inventory/resources/database.py +106 -31
  32. regscale/integrations/commercial/aws/inventory/resources/guardduty.py +286 -0
  33. regscale/integrations/commercial/aws/inventory/resources/iam.py +470 -0
  34. regscale/integrations/commercial/aws/inventory/resources/inspector.py +476 -0
  35. regscale/integrations/commercial/aws/inventory/resources/integration.py +175 -61
  36. regscale/integrations/commercial/aws/inventory/resources/kms.py +447 -0
  37. regscale/integrations/commercial/aws/inventory/resources/networking.py +103 -67
  38. regscale/integrations/commercial/aws/inventory/resources/s3.py +394 -0
  39. regscale/integrations/commercial/aws/inventory/resources/security.py +268 -72
  40. regscale/integrations/commercial/aws/inventory/resources/securityhub.py +473 -0
  41. regscale/integrations/commercial/aws/inventory/resources/storage.py +53 -29
  42. regscale/integrations/commercial/aws/inventory/resources/systems_manager.py +657 -0
  43. regscale/integrations/commercial/aws/inventory/resources/vpc.py +655 -0
  44. regscale/integrations/commercial/aws/kms_control_mappings.py +288 -0
  45. regscale/integrations/commercial/aws/kms_evidence.py +879 -0
  46. regscale/integrations/commercial/aws/ocsf/__init__.py +7 -0
  47. regscale/integrations/commercial/aws/ocsf/constants.py +115 -0
  48. regscale/integrations/commercial/aws/ocsf/mapper.py +435 -0
  49. regscale/integrations/commercial/aws/org_control_mappings.py +286 -0
  50. regscale/integrations/commercial/aws/org_evidence.py +666 -0
  51. regscale/integrations/commercial/aws/s3_control_mappings.py +356 -0
  52. regscale/integrations/commercial/aws/s3_evidence.py +632 -0
  53. regscale/integrations/commercial/aws/scanner.py +851 -206
  54. regscale/integrations/commercial/aws/security_hub.py +319 -0
  55. regscale/integrations/commercial/aws/session_manager.py +282 -0
  56. regscale/integrations/commercial/aws/ssm_control_mappings.py +291 -0
  57. regscale/integrations/commercial/aws/ssm_evidence.py +492 -0
  58. regscale/integrations/compliance_integration.py +308 -38
  59. regscale/integrations/due_date_handler.py +3 -0
  60. regscale/integrations/scanner_integration.py +399 -84
  61. regscale/models/integration_models/cisa_kev_data.json +34 -4
  62. regscale/models/integration_models/synqly_models/capabilities.json +1 -1
  63. regscale/models/integration_models/synqly_models/connectors/vulnerabilities.py +17 -9
  64. regscale/models/regscale_models/assessment.py +2 -1
  65. regscale/models/regscale_models/control_objective.py +74 -5
  66. regscale/models/regscale_models/file.py +2 -0
  67. regscale/models/regscale_models/issue.py +2 -5
  68. {regscale_cli-6.27.3.0.dist-info → regscale_cli-6.28.0.0.dist-info}/METADATA +1 -1
  69. {regscale_cli-6.27.3.0.dist-info → regscale_cli-6.28.0.0.dist-info}/RECORD +112 -33
  70. tests/regscale/integrations/commercial/aws/__init__.py +0 -0
  71. tests/regscale/integrations/commercial/aws/test_audit_manager_compliance.py +1304 -0
  72. tests/regscale/integrations/commercial/aws/test_audit_manager_evidence_aggregation.py +341 -0
  73. tests/regscale/integrations/commercial/aws/test_aws_audit_manager_collector.py +1155 -0
  74. tests/regscale/integrations/commercial/aws/test_aws_cloudtrail_collector.py +534 -0
  75. tests/regscale/integrations/commercial/aws/test_aws_config_collector.py +400 -0
  76. tests/regscale/integrations/commercial/aws/test_aws_guardduty_collector.py +315 -0
  77. tests/regscale/integrations/commercial/aws/test_aws_iam_collector.py +458 -0
  78. tests/regscale/integrations/commercial/aws/test_aws_inspector_collector.py +353 -0
  79. tests/regscale/integrations/commercial/aws/test_aws_inventory_integration.py +530 -0
  80. tests/regscale/integrations/commercial/aws/test_aws_kms_collector.py +919 -0
  81. tests/regscale/integrations/commercial/aws/test_aws_s3_collector.py +722 -0
  82. tests/regscale/integrations/commercial/aws/test_aws_scanner_integration.py +722 -0
  83. tests/regscale/integrations/commercial/aws/test_aws_securityhub_collector.py +792 -0
  84. tests/regscale/integrations/commercial/aws/test_aws_systems_manager_collector.py +918 -0
  85. tests/regscale/integrations/commercial/aws/test_aws_vpc_collector.py +996 -0
  86. tests/regscale/integrations/commercial/aws/test_cli_evidence.py +431 -0
  87. tests/regscale/integrations/commercial/aws/test_cloudtrail_control_mappings.py +452 -0
  88. tests/regscale/integrations/commercial/aws/test_cloudtrail_evidence.py +788 -0
  89. tests/regscale/integrations/commercial/aws/test_config_compliance.py +298 -0
  90. tests/regscale/integrations/commercial/aws/test_conformance_pack_mappings.py +200 -0
  91. tests/regscale/integrations/commercial/aws/test_evidence_generator.py +386 -0
  92. tests/regscale/integrations/commercial/aws/test_guardduty_control_mappings.py +564 -0
  93. tests/regscale/integrations/commercial/aws/test_guardduty_evidence.py +1041 -0
  94. tests/regscale/integrations/commercial/aws/test_iam_control_mappings.py +718 -0
  95. tests/regscale/integrations/commercial/aws/test_iam_evidence.py +1375 -0
  96. tests/regscale/integrations/commercial/aws/test_kms_control_mappings.py +656 -0
  97. tests/regscale/integrations/commercial/aws/test_kms_evidence.py +1163 -0
  98. tests/regscale/integrations/commercial/aws/test_ocsf_mapper.py +370 -0
  99. tests/regscale/integrations/commercial/aws/test_org_control_mappings.py +546 -0
  100. tests/regscale/integrations/commercial/aws/test_org_evidence.py +1240 -0
  101. tests/regscale/integrations/commercial/aws/test_s3_control_mappings.py +672 -0
  102. tests/regscale/integrations/commercial/aws/test_s3_evidence.py +987 -0
  103. tests/regscale/integrations/commercial/aws/test_scanner_evidence.py +373 -0
  104. tests/regscale/integrations/commercial/aws/test_security_hub_config_filtering.py +539 -0
  105. tests/regscale/integrations/commercial/aws/test_session_manager.py +516 -0
  106. tests/regscale/integrations/commercial/aws/test_ssm_control_mappings.py +588 -0
  107. tests/regscale/integrations/commercial/aws/test_ssm_evidence.py +735 -0
  108. tests/regscale/integrations/commercial/test_aws.py +55 -56
  109. {regscale_cli-6.27.3.0.dist-info → regscale_cli-6.28.0.0.dist-info}/LICENSE +0 -0
  110. {regscale_cli-6.27.3.0.dist-info → regscale_cli-6.28.0.0.dist-info}/WHEEL +0 -0
  111. {regscale_cli-6.27.3.0.dist-info → regscale_cli-6.28.0.0.dist-info}/entry_points.txt +0 -0
  112. {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"])