regscale-cli 6.27.2.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 (140) hide show
  1. regscale/_version.py +1 -1
  2. regscale/core/app/application.py +1 -0
  3. regscale/core/app/internal/control_editor.py +73 -21
  4. regscale/core/app/internal/login.py +4 -1
  5. regscale/core/app/internal/model_editor.py +219 -64
  6. regscale/core/app/utils/app_utils.py +11 -2
  7. regscale/core/login.py +21 -4
  8. regscale/core/utils/date.py +77 -1
  9. regscale/dev/cli.py +26 -0
  10. regscale/dev/version.py +72 -0
  11. regscale/integrations/commercial/__init__.py +15 -1
  12. regscale/integrations/commercial/amazon/amazon/__init__.py +0 -0
  13. regscale/integrations/commercial/amazon/amazon/common.py +204 -0
  14. regscale/integrations/commercial/amazon/common.py +48 -58
  15. regscale/integrations/commercial/aws/audit_manager_compliance.py +2671 -0
  16. regscale/integrations/commercial/aws/cli.py +3093 -55
  17. regscale/integrations/commercial/aws/cloudtrail_control_mappings.py +333 -0
  18. regscale/integrations/commercial/aws/cloudtrail_evidence.py +501 -0
  19. regscale/integrations/commercial/aws/cloudwatch_control_mappings.py +357 -0
  20. regscale/integrations/commercial/aws/cloudwatch_evidence.py +490 -0
  21. regscale/integrations/commercial/aws/config_compliance.py +914 -0
  22. regscale/integrations/commercial/aws/conformance_pack_mappings.py +198 -0
  23. regscale/integrations/commercial/aws/evidence_generator.py +283 -0
  24. regscale/integrations/commercial/aws/guardduty_control_mappings.py +340 -0
  25. regscale/integrations/commercial/aws/guardduty_evidence.py +1053 -0
  26. regscale/integrations/commercial/aws/iam_control_mappings.py +368 -0
  27. regscale/integrations/commercial/aws/iam_evidence.py +574 -0
  28. regscale/integrations/commercial/aws/inventory/__init__.py +223 -22
  29. regscale/integrations/commercial/aws/inventory/base.py +107 -5
  30. regscale/integrations/commercial/aws/inventory/resources/audit_manager.py +513 -0
  31. regscale/integrations/commercial/aws/inventory/resources/cloudtrail.py +315 -0
  32. regscale/integrations/commercial/aws/inventory/resources/cloudtrail_logs_metadata.py +476 -0
  33. regscale/integrations/commercial/aws/inventory/resources/cloudwatch.py +191 -0
  34. regscale/integrations/commercial/aws/inventory/resources/compute.py +66 -9
  35. regscale/integrations/commercial/aws/inventory/resources/config.py +464 -0
  36. regscale/integrations/commercial/aws/inventory/resources/containers.py +74 -9
  37. regscale/integrations/commercial/aws/inventory/resources/database.py +106 -31
  38. regscale/integrations/commercial/aws/inventory/resources/guardduty.py +286 -0
  39. regscale/integrations/commercial/aws/inventory/resources/iam.py +470 -0
  40. regscale/integrations/commercial/aws/inventory/resources/inspector.py +476 -0
  41. regscale/integrations/commercial/aws/inventory/resources/integration.py +175 -61
  42. regscale/integrations/commercial/aws/inventory/resources/kms.py +447 -0
  43. regscale/integrations/commercial/aws/inventory/resources/networking.py +103 -67
  44. regscale/integrations/commercial/aws/inventory/resources/s3.py +394 -0
  45. regscale/integrations/commercial/aws/inventory/resources/security.py +268 -72
  46. regscale/integrations/commercial/aws/inventory/resources/securityhub.py +473 -0
  47. regscale/integrations/commercial/aws/inventory/resources/storage.py +53 -29
  48. regscale/integrations/commercial/aws/inventory/resources/systems_manager.py +657 -0
  49. regscale/integrations/commercial/aws/inventory/resources/vpc.py +655 -0
  50. regscale/integrations/commercial/aws/kms_control_mappings.py +288 -0
  51. regscale/integrations/commercial/aws/kms_evidence.py +879 -0
  52. regscale/integrations/commercial/aws/ocsf/__init__.py +7 -0
  53. regscale/integrations/commercial/aws/ocsf/constants.py +115 -0
  54. regscale/integrations/commercial/aws/ocsf/mapper.py +435 -0
  55. regscale/integrations/commercial/aws/org_control_mappings.py +286 -0
  56. regscale/integrations/commercial/aws/org_evidence.py +666 -0
  57. regscale/integrations/commercial/aws/s3_control_mappings.py +356 -0
  58. regscale/integrations/commercial/aws/s3_evidence.py +632 -0
  59. regscale/integrations/commercial/aws/scanner.py +853 -205
  60. regscale/integrations/commercial/aws/security_hub.py +319 -0
  61. regscale/integrations/commercial/aws/session_manager.py +282 -0
  62. regscale/integrations/commercial/aws/ssm_control_mappings.py +291 -0
  63. regscale/integrations/commercial/aws/ssm_evidence.py +492 -0
  64. regscale/integrations/commercial/synqly/query_builder.py +4 -1
  65. regscale/integrations/compliance_integration.py +308 -38
  66. regscale/integrations/control_matcher.py +78 -23
  67. regscale/integrations/due_date_handler.py +3 -0
  68. regscale/integrations/public/csam/csam.py +572 -763
  69. regscale/integrations/public/csam/csam_agency_defined.py +179 -0
  70. regscale/integrations/public/csam/csam_common.py +154 -0
  71. regscale/integrations/public/csam/csam_controls.py +432 -0
  72. regscale/integrations/public/csam/csam_poam.py +124 -0
  73. regscale/integrations/public/fedramp/click.py +17 -4
  74. regscale/integrations/public/fedramp/fedramp_cis_crm.py +271 -62
  75. regscale/integrations/public/fedramp/poam/scanner.py +74 -7
  76. regscale/integrations/scanner_integration.py +415 -85
  77. regscale/models/integration_models/cisa_kev_data.json +80 -20
  78. regscale/models/integration_models/synqly_models/capabilities.json +1 -1
  79. regscale/models/integration_models/synqly_models/connectors/vulnerabilities.py +44 -3
  80. regscale/models/integration_models/synqly_models/ocsf_mapper.py +41 -12
  81. regscale/models/platform.py +3 -0
  82. regscale/models/regscale_models/__init__.py +5 -0
  83. regscale/models/regscale_models/assessment.py +2 -1
  84. regscale/models/regscale_models/component.py +1 -1
  85. regscale/models/regscale_models/control_implementation.py +55 -24
  86. regscale/models/regscale_models/control_objective.py +74 -5
  87. regscale/models/regscale_models/file.py +2 -0
  88. regscale/models/regscale_models/issue.py +2 -5
  89. regscale/models/regscale_models/organization.py +3 -0
  90. regscale/models/regscale_models/regscale_model.py +17 -5
  91. regscale/models/regscale_models/security_plan.py +1 -0
  92. regscale/regscale.py +11 -1
  93. {regscale_cli-6.27.2.0.dist-info → regscale_cli-6.28.0.0.dist-info}/METADATA +1 -1
  94. {regscale_cli-6.27.2.0.dist-info → regscale_cli-6.28.0.0.dist-info}/RECORD +140 -57
  95. tests/regscale/core/test_login.py +171 -4
  96. tests/regscale/integrations/commercial/aws/__init__.py +0 -0
  97. tests/regscale/integrations/commercial/aws/test_audit_manager_compliance.py +1304 -0
  98. tests/regscale/integrations/commercial/aws/test_audit_manager_evidence_aggregation.py +341 -0
  99. tests/regscale/integrations/commercial/aws/test_aws_audit_manager_collector.py +1155 -0
  100. tests/regscale/integrations/commercial/aws/test_aws_cloudtrail_collector.py +534 -0
  101. tests/regscale/integrations/commercial/aws/test_aws_config_collector.py +400 -0
  102. tests/regscale/integrations/commercial/aws/test_aws_guardduty_collector.py +315 -0
  103. tests/regscale/integrations/commercial/aws/test_aws_iam_collector.py +458 -0
  104. tests/regscale/integrations/commercial/aws/test_aws_inspector_collector.py +353 -0
  105. tests/regscale/integrations/commercial/aws/test_aws_inventory_integration.py +530 -0
  106. tests/regscale/integrations/commercial/aws/test_aws_kms_collector.py +919 -0
  107. tests/regscale/integrations/commercial/aws/test_aws_s3_collector.py +722 -0
  108. tests/regscale/integrations/commercial/aws/test_aws_scanner_integration.py +722 -0
  109. tests/regscale/integrations/commercial/aws/test_aws_securityhub_collector.py +792 -0
  110. tests/regscale/integrations/commercial/aws/test_aws_systems_manager_collector.py +918 -0
  111. tests/regscale/integrations/commercial/aws/test_aws_vpc_collector.py +996 -0
  112. tests/regscale/integrations/commercial/aws/test_cli_evidence.py +431 -0
  113. tests/regscale/integrations/commercial/aws/test_cloudtrail_control_mappings.py +452 -0
  114. tests/regscale/integrations/commercial/aws/test_cloudtrail_evidence.py +788 -0
  115. tests/regscale/integrations/commercial/aws/test_config_compliance.py +298 -0
  116. tests/regscale/integrations/commercial/aws/test_conformance_pack_mappings.py +200 -0
  117. tests/regscale/integrations/commercial/aws/test_evidence_generator.py +386 -0
  118. tests/regscale/integrations/commercial/aws/test_guardduty_control_mappings.py +564 -0
  119. tests/regscale/integrations/commercial/aws/test_guardduty_evidence.py +1041 -0
  120. tests/regscale/integrations/commercial/aws/test_iam_control_mappings.py +718 -0
  121. tests/regscale/integrations/commercial/aws/test_iam_evidence.py +1375 -0
  122. tests/regscale/integrations/commercial/aws/test_kms_control_mappings.py +656 -0
  123. tests/regscale/integrations/commercial/aws/test_kms_evidence.py +1163 -0
  124. tests/regscale/integrations/commercial/aws/test_ocsf_mapper.py +370 -0
  125. tests/regscale/integrations/commercial/aws/test_org_control_mappings.py +546 -0
  126. tests/regscale/integrations/commercial/aws/test_org_evidence.py +1240 -0
  127. tests/regscale/integrations/commercial/aws/test_s3_control_mappings.py +672 -0
  128. tests/regscale/integrations/commercial/aws/test_s3_evidence.py +987 -0
  129. tests/regscale/integrations/commercial/aws/test_scanner_evidence.py +373 -0
  130. tests/regscale/integrations/commercial/aws/test_security_hub_config_filtering.py +539 -0
  131. tests/regscale/integrations/commercial/aws/test_session_manager.py +516 -0
  132. tests/regscale/integrations/commercial/aws/test_ssm_control_mappings.py +588 -0
  133. tests/regscale/integrations/commercial/aws/test_ssm_evidence.py +735 -0
  134. tests/regscale/integrations/commercial/test_aws.py +55 -56
  135. tests/regscale/integrations/test_control_matcher.py +24 -0
  136. tests/regscale/models/test_control_implementation.py +118 -3
  137. {regscale_cli-6.27.2.0.dist-info → regscale_cli-6.28.0.0.dist-info}/LICENSE +0 -0
  138. {regscale_cli-6.27.2.0.dist-info → regscale_cli-6.28.0.0.dist-info}/WHEEL +0 -0
  139. {regscale_cli-6.27.2.0.dist-info → regscale_cli-6.28.0.0.dist-info}/entry_points.txt +0 -0
  140. {regscale_cli-6.27.2.0.dist-info → regscale_cli-6.28.0.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,788 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """Unit tests for AWS CloudTrail Evidence Integration."""
4
+
5
+ import gzip
6
+ import json
7
+ import logging
8
+ import os
9
+ import tempfile
10
+ from datetime import datetime, timedelta
11
+ from pathlib import Path
12
+ from unittest.mock import MagicMock, Mock, patch, mock_open
13
+
14
+ import boto3
15
+ import pytest
16
+
17
+ from regscale.integrations.commercial.aws.cloudtrail_evidence import (
18
+ CloudTrailComplianceItem,
19
+ AWSCloudTrailEvidenceIntegration,
20
+ )
21
+
22
+
23
+ class TestCloudTrailComplianceItem:
24
+ """Test CloudTrailComplianceItem class."""
25
+
26
+ def test_init_with_complete_data(self):
27
+ """Test initialization with complete trail data."""
28
+ trail_data = {
29
+ "Name": "test-trail",
30
+ "TrailARN": "arn:aws:cloudtrail:us-east-1:123456789012:trail/test-trail",
31
+ "S3BucketName": "test-bucket",
32
+ "IsMultiRegionTrail": True,
33
+ "IsOrganizationTrail": False,
34
+ "LogFileValidationEnabled": True,
35
+ "KmsKeyId": "arn:aws:kms:us-east-1:123456789012:key/test-key",
36
+ "CloudWatchLogsLogGroupArn": "arn:aws:logs:us-east-1:123456789012:log-group:cloudtrail",
37
+ "SnsTopicARN": "arn:aws:sns:us-east-1:123456789012:cloudtrail-topic",
38
+ "Status": {"IsLogging": True},
39
+ "EventSelectors": [{"IncludeManagementEvents": True}],
40
+ "Tags": {"Environment": "Production"},
41
+ "Region": "us-east-1",
42
+ }
43
+
44
+ item = CloudTrailComplianceItem(trail_data)
45
+
46
+ assert item.trail_name == "test-trail"
47
+ assert item.trail_arn == "arn:aws:cloudtrail:us-east-1:123456789012:trail/test-trail"
48
+ assert item.s3_bucket_name == "test-bucket"
49
+ assert item.is_multi_region is True
50
+ assert item.is_organization_trail is False
51
+ assert item.log_file_validation_enabled is True
52
+ assert item.kms_key_id == "arn:aws:kms:us-east-1:123456789012:key/test-key"
53
+ assert item.cloud_watch_logs_log_group_arn == "arn:aws:logs:us-east-1:123456789012:log-group:cloudtrail"
54
+ assert item.sns_topic_arn == "arn:aws:sns:us-east-1:123456789012:cloudtrail-topic"
55
+ assert item.status == {"IsLogging": True}
56
+ assert item.event_selectors == [{"IncludeManagementEvents": True}]
57
+ assert item.tags == {"Environment": "Production"}
58
+ assert item.region == "us-east-1"
59
+
60
+ def test_init_with_minimal_data(self):
61
+ """Test initialization with minimal trail data."""
62
+ trail_data = {}
63
+
64
+ item = CloudTrailComplianceItem(trail_data)
65
+
66
+ assert item.trail_name == ""
67
+ assert item.trail_arn == ""
68
+ assert item.s3_bucket_name == ""
69
+ assert item.is_multi_region is False
70
+ assert item.is_organization_trail is False
71
+ assert item.log_file_validation_enabled is False
72
+ assert item.kms_key_id is None
73
+ assert item.cloud_watch_logs_log_group_arn is None
74
+ assert item.sns_topic_arn is None
75
+ assert item.status == {}
76
+ assert item.event_selectors == []
77
+ assert item.tags == {}
78
+ assert item.region == ""
79
+
80
+ def test_to_dict(self):
81
+ """Test conversion to dictionary."""
82
+ trail_data = {
83
+ "Name": "test-trail",
84
+ "TrailARN": "arn:aws:cloudtrail:us-east-1:123456789012:trail/test-trail",
85
+ "S3BucketName": "test-bucket",
86
+ }
87
+
88
+ item = CloudTrailComplianceItem(trail_data)
89
+ result = item.to_dict()
90
+
91
+ assert result == trail_data
92
+ assert result is item.raw_data
93
+
94
+
95
+ class TestAWSCloudTrailEvidenceIntegration:
96
+ """Test AWSCloudTrailEvidenceIntegration class."""
97
+
98
+ @patch("regscale.integrations.commercial.aws.cloudtrail_evidence.CloudTrailControlMapper")
99
+ @patch("regscale.integrations.commercial.aws.cloudtrail_evidence.Api")
100
+ def test_init_with_defaults(self, mock_api, mock_mapper):
101
+ """Test initialization with default parameters."""
102
+ integration = AWSCloudTrailEvidenceIntegration(plan_id=123)
103
+
104
+ assert integration.plan_id == 123
105
+ assert integration.region == "us-east-1"
106
+ assert integration.account_id is None
107
+ assert integration.tags == {}
108
+ assert integration.trail_name_filter is None
109
+ assert integration.create_evidence is False
110
+ assert integration.create_ssp_attachment is True
111
+ assert integration.evidence_control_ids == []
112
+ assert integration.force_refresh is False
113
+ assert integration.aws_profile is None
114
+ assert integration.aws_access_key_id is None
115
+ assert integration.aws_secret_access_key is None
116
+ assert integration.aws_session_token is None
117
+ assert integration.session is None
118
+ assert integration.collector is None
119
+ assert integration.cache_ttl_hours == 4
120
+ assert integration.raw_cloudtrail_data == {}
121
+ assert integration.trails == []
122
+ mock_api.assert_called_once()
123
+ mock_mapper.assert_called_once_with(framework="NIST800-53R5")
124
+
125
+ @patch("regscale.integrations.commercial.aws.cloudtrail_evidence.CloudTrailControlMapper")
126
+ @patch("regscale.integrations.commercial.aws.cloudtrail_evidence.Api")
127
+ def test_init_with_custom_parameters(self, mock_api, mock_mapper):
128
+ """Test initialization with custom parameters."""
129
+ integration = AWSCloudTrailEvidenceIntegration(
130
+ plan_id=456,
131
+ region="us-west-2",
132
+ account_id="123456789012",
133
+ tags={"Environment": "Production"},
134
+ trail_name_filter="prod",
135
+ create_evidence=True,
136
+ create_ssp_attachment=False,
137
+ evidence_control_ids=["AC-2", "AU-3"],
138
+ force_refresh=True,
139
+ aws_profile="test-profile",
140
+ aws_access_key_id="AKIATEST",
141
+ aws_secret_access_key="secret",
142
+ aws_session_token="token",
143
+ )
144
+
145
+ assert integration.plan_id == 456
146
+ assert integration.region == "us-west-2"
147
+ assert integration.account_id == "123456789012"
148
+ assert integration.tags == {"Environment": "Production"}
149
+ assert integration.trail_name_filter == "prod"
150
+ assert integration.create_evidence is True
151
+ assert integration.create_ssp_attachment is False
152
+ assert integration.evidence_control_ids == ["AC-2", "AU-3"]
153
+ assert integration.force_refresh is True
154
+ assert integration.aws_profile == "test-profile"
155
+ assert integration.aws_access_key_id == "AKIATEST"
156
+ assert integration.aws_secret_access_key == "secret"
157
+ assert integration.aws_session_token == "token"
158
+
159
+ @patch("regscale.integrations.commercial.aws.cloudtrail_evidence.CloudTrailControlMapper")
160
+ @patch("regscale.integrations.commercial.aws.cloudtrail_evidence.Api")
161
+ def test_get_cache_file_path(self, mock_api, mock_mapper):
162
+ """Test cache file path generation."""
163
+ integration = AWSCloudTrailEvidenceIntegration(plan_id=123, region="us-east-1", account_id="123456789012")
164
+
165
+ cache_path = integration._get_cache_file_path()
166
+
167
+ assert isinstance(cache_path, Path)
168
+ assert cache_path.name == "cloudtrail_trails_us-east-1_123456789012.json"
169
+ assert "regscale" in str(cache_path)
170
+ assert "aws_cloudtrail_cache" in str(cache_path)
171
+
172
+ @patch("regscale.integrations.commercial.aws.cloudtrail_evidence.CloudTrailControlMapper")
173
+ @patch("regscale.integrations.commercial.aws.cloudtrail_evidence.Api")
174
+ def test_get_cache_file_path_without_account_id(self, mock_api, mock_mapper):
175
+ """Test cache file path generation without account ID."""
176
+ integration = AWSCloudTrailEvidenceIntegration(plan_id=123, region="us-west-2")
177
+
178
+ cache_path = integration._get_cache_file_path()
179
+
180
+ assert cache_path.name == "cloudtrail_trails_us-west-2_default.json"
181
+
182
+ @patch("regscale.integrations.commercial.aws.cloudtrail_evidence.CloudTrailControlMapper")
183
+ @patch("regscale.integrations.commercial.aws.cloudtrail_evidence.Api")
184
+ def test_is_cache_valid_no_cache(self, mock_api, mock_mapper):
185
+ """Test cache validation when cache file doesn't exist."""
186
+ integration = AWSCloudTrailEvidenceIntegration(plan_id=123)
187
+
188
+ # Ensure cache directory exists (it gets created during init)
189
+ cache_file = integration._get_cache_file_path()
190
+ if cache_file.exists():
191
+ cache_file.unlink()
192
+
193
+ is_valid = integration._is_cache_valid()
194
+
195
+ assert is_valid is False
196
+
197
+ @patch("regscale.integrations.commercial.aws.cloudtrail_evidence.CloudTrailControlMapper")
198
+ @patch("regscale.integrations.commercial.aws.cloudtrail_evidence.Api")
199
+ def test_is_cache_valid_expired(self, mock_api, mock_mapper):
200
+ """Test cache validation when cache is expired."""
201
+ integration = AWSCloudTrailEvidenceIntegration(plan_id=123)
202
+ cache_file = integration._get_cache_file_path()
203
+
204
+ # Create cache directory and file
205
+ cache_file.parent.mkdir(parents=True, exist_ok=True)
206
+ cache_file.write_text("[]")
207
+
208
+ # Set modification time to 5 hours ago (expired)
209
+ five_hours_ago = datetime.now() - timedelta(hours=5)
210
+ os.utime(cache_file, (five_hours_ago.timestamp(), five_hours_ago.timestamp()))
211
+
212
+ is_valid = integration._is_cache_valid()
213
+
214
+ assert is_valid is False
215
+
216
+ # Cleanup
217
+ cache_file.unlink()
218
+
219
+ @patch("regscale.integrations.commercial.aws.cloudtrail_evidence.CloudTrailControlMapper")
220
+ @patch("regscale.integrations.commercial.aws.cloudtrail_evidence.Api")
221
+ def test_is_cache_valid_fresh(self, mock_api, mock_mapper):
222
+ """Test cache validation when cache is fresh."""
223
+ integration = AWSCloudTrailEvidenceIntegration(plan_id=123)
224
+ cache_file = integration._get_cache_file_path()
225
+
226
+ # Create cache directory and file
227
+ cache_file.parent.mkdir(parents=True, exist_ok=True)
228
+ cache_file.write_text("[]")
229
+
230
+ is_valid = integration._is_cache_valid()
231
+
232
+ assert is_valid is True
233
+
234
+ # Cleanup
235
+ cache_file.unlink()
236
+
237
+ @patch("regscale.integrations.commercial.aws.cloudtrail_evidence.CloudTrailControlMapper")
238
+ @patch("regscale.integrations.commercial.aws.cloudtrail_evidence.Api")
239
+ def test_save_cache(self, mock_api, mock_mapper):
240
+ """Test saving data to cache."""
241
+ integration = AWSCloudTrailEvidenceIntegration(plan_id=123)
242
+ cache_file = integration._get_cache_file_path()
243
+
244
+ test_data = {"trails": [{"Name": "test-trail"}]}
245
+
246
+ integration._save_cache(test_data)
247
+
248
+ assert cache_file.exists()
249
+ with open(cache_file) as f:
250
+ loaded_data = json.load(f)
251
+ assert loaded_data == test_data
252
+
253
+ # Cleanup
254
+ cache_file.unlink()
255
+
256
+ @patch("regscale.integrations.commercial.aws.cloudtrail_evidence.CloudTrailControlMapper")
257
+ @patch("regscale.integrations.commercial.aws.cloudtrail_evidence.Api")
258
+ def test_save_cache_error(self, mock_api, mock_mapper, caplog):
259
+ """Test saving cache with error."""
260
+ integration = AWSCloudTrailEvidenceIntegration(plan_id=123)
261
+
262
+ with patch("builtins.open", side_effect=OSError("Permission denied")):
263
+ integration._save_cache({"test": "data"})
264
+
265
+ assert "Failed to save cache" in caplog.text
266
+
267
+ @patch("regscale.integrations.commercial.aws.cloudtrail_evidence.CloudTrailControlMapper")
268
+ @patch("regscale.integrations.commercial.aws.cloudtrail_evidence.Api")
269
+ def test_load_cached_data(self, mock_api, mock_mapper):
270
+ """Test loading data from cache."""
271
+ integration = AWSCloudTrailEvidenceIntegration(plan_id=123)
272
+ cache_file = integration._get_cache_file_path()
273
+
274
+ # Test data should be a list of trail dicts (not wrapped in a dict)
275
+ test_data = [{"Name": "test-trail"}]
276
+
277
+ # Create cache
278
+ cache_file.parent.mkdir(parents=True, exist_ok=True)
279
+ with open(cache_file, "w") as f:
280
+ json.dump(test_data, f)
281
+
282
+ loaded_data = integration._load_cached_data()
283
+
284
+ assert loaded_data == test_data
285
+
286
+ # Cleanup
287
+ cache_file.unlink()
288
+
289
+ @patch("regscale.integrations.commercial.aws.cloudtrail_evidence.CloudTrailControlMapper")
290
+ @patch("regscale.integrations.commercial.aws.cloudtrail_evidence.Api")
291
+ def test_load_cached_data_error(self, mock_api, mock_mapper, caplog):
292
+ """Test loading cache with error."""
293
+ integration = AWSCloudTrailEvidenceIntegration(plan_id=123)
294
+
295
+ result = integration._load_cached_data()
296
+
297
+ assert result is None
298
+ # Cache loading can fail either due to file error or invalid format
299
+ assert "Failed to load cache" in caplog.text or "Invalid cache format" in caplog.text
300
+
301
+ @patch("regscale.integrations.commercial.aws.cloudtrail_evidence.CloudTrailControlMapper")
302
+ @patch("regscale.integrations.commercial.aws.cloudtrail_evidence.Api")
303
+ def test_get_cache_age_hours_no_cache(self, mock_api, mock_mapper):
304
+ """Test getting cache age when no cache exists."""
305
+ integration = AWSCloudTrailEvidenceIntegration(plan_id=123)
306
+
307
+ age = integration._get_cache_age_hours()
308
+
309
+ assert age == float("inf")
310
+
311
+ @patch("regscale.integrations.commercial.aws.cloudtrail_evidence.CloudTrailControlMapper")
312
+ @patch("regscale.integrations.commercial.aws.cloudtrail_evidence.Api")
313
+ def test_get_cache_age_hours_with_cache(self, mock_api, mock_mapper):
314
+ """Test getting cache age with existing cache."""
315
+ integration = AWSCloudTrailEvidenceIntegration(plan_id=123)
316
+ cache_file = integration._get_cache_file_path()
317
+
318
+ # Create cache
319
+ cache_file.parent.mkdir(parents=True, exist_ok=True)
320
+ cache_file.write_text("[]")
321
+
322
+ # Set modification time to 2 hours ago
323
+ two_hours_ago = datetime.now() - timedelta(hours=2)
324
+ os.utime(cache_file, (two_hours_ago.timestamp(), two_hours_ago.timestamp()))
325
+
326
+ age = integration._get_cache_age_hours()
327
+
328
+ assert 1.9 < age < 2.1 # Allow small variance
329
+
330
+ # Cleanup
331
+ cache_file.unlink()
332
+
333
+ @patch("regscale.integrations.commercial.aws.cloudtrail_evidence.boto3.Session")
334
+ @patch("regscale.integrations.commercial.aws.cloudtrail_evidence.CloudTrailControlMapper")
335
+ @patch("regscale.integrations.commercial.aws.cloudtrail_evidence.Api")
336
+ def test_initialize_aws_session_with_keys(self, mock_api, mock_mapper, mock_boto_session):
337
+ """Test AWS session initialization with access keys."""
338
+ mock_session = MagicMock()
339
+ mock_boto_session.return_value = mock_session
340
+
341
+ integration = AWSCloudTrailEvidenceIntegration(
342
+ plan_id=123,
343
+ region="us-west-2",
344
+ aws_access_key_id="AKIATEST",
345
+ aws_secret_access_key="secret",
346
+ aws_session_token="token",
347
+ )
348
+
349
+ integration._initialize_aws_session()
350
+
351
+ mock_boto_session.assert_called_once_with(
352
+ aws_access_key_id="AKIATEST",
353
+ aws_secret_access_key="secret",
354
+ aws_session_token="token",
355
+ region_name="us-west-2",
356
+ )
357
+ assert integration.session == mock_session
358
+
359
+ @patch("regscale.integrations.commercial.aws.cloudtrail_evidence.boto3.Session")
360
+ @patch("regscale.integrations.commercial.aws.cloudtrail_evidence.CloudTrailControlMapper")
361
+ @patch("regscale.integrations.commercial.aws.cloudtrail_evidence.Api")
362
+ def test_initialize_aws_session_with_profile(self, mock_api, mock_mapper, mock_boto_session):
363
+ """Test AWS session initialization with profile."""
364
+ mock_session = MagicMock()
365
+ mock_boto_session.return_value = mock_session
366
+
367
+ integration = AWSCloudTrailEvidenceIntegration(plan_id=123, region="us-east-1", aws_profile="test-profile")
368
+
369
+ integration._initialize_aws_session()
370
+
371
+ mock_boto_session.assert_called_once_with(profile_name="test-profile", region_name="us-east-1")
372
+ assert integration.session == mock_session
373
+
374
+ @patch("regscale.integrations.commercial.aws.cloudtrail_evidence.boto3.Session")
375
+ @patch("regscale.integrations.commercial.aws.cloudtrail_evidence.CloudTrailControlMapper")
376
+ @patch("regscale.integrations.commercial.aws.cloudtrail_evidence.Api")
377
+ def test_initialize_aws_session_default(self, mock_api, mock_mapper, mock_boto_session):
378
+ """Test AWS session initialization with default credentials."""
379
+ mock_session = MagicMock()
380
+ mock_boto_session.return_value = mock_session
381
+
382
+ integration = AWSCloudTrailEvidenceIntegration(plan_id=123, region="us-east-1")
383
+
384
+ integration._initialize_aws_session()
385
+
386
+ mock_boto_session.assert_called_once_with(region_name="us-east-1")
387
+ assert integration.session == mock_session
388
+
389
+ @patch("regscale.integrations.commercial.aws.cloudtrail_evidence.CloudTrailControlMapper")
390
+ @patch("regscale.integrations.commercial.aws.cloudtrail_evidence.Api")
391
+ def test_fetch_compliance_data_from_cache(self, mock_api, mock_mapper):
392
+ """Test fetching compliance data from cache."""
393
+ integration = AWSCloudTrailEvidenceIntegration(plan_id=123)
394
+
395
+ cached_data = [{"Name": "test-trail"}]
396
+
397
+ with patch.object(integration, "_is_cache_valid", return_value=True):
398
+ with patch.object(integration, "_load_cached_data", return_value=cached_data):
399
+ result = integration.fetch_compliance_data()
400
+
401
+ assert result == cached_data
402
+
403
+ @patch("regscale.integrations.commercial.aws.cloudtrail_evidence.CloudTrailControlMapper")
404
+ @patch("regscale.integrations.commercial.aws.cloudtrail_evidence.Api")
405
+ def test_fetch_compliance_data_force_refresh(self, mock_api, mock_mapper):
406
+ """Test fetching compliance data with force refresh."""
407
+ integration = AWSCloudTrailEvidenceIntegration(plan_id=123, force_refresh=True)
408
+
409
+ fresh_data = [{"Name": "fresh-trail"}]
410
+
411
+ with patch.object(integration, "_fetch_fresh_cloudtrail_data", return_value=fresh_data):
412
+ result = integration.fetch_compliance_data()
413
+
414
+ assert result == fresh_data
415
+
416
+ @patch("regscale.integrations.commercial.aws.cloudtrail_evidence.CloudTrailCollector")
417
+ @patch("regscale.integrations.commercial.aws.cloudtrail_evidence.CloudTrailControlMapper")
418
+ @patch("regscale.integrations.commercial.aws.cloudtrail_evidence.Api")
419
+ def test_fetch_fresh_cloudtrail_data(self, mock_api, mock_mapper, mock_collector_class):
420
+ """Test fetching fresh CloudTrail data."""
421
+ mock_collector = MagicMock()
422
+ mock_collector_class.return_value = mock_collector
423
+ mock_collector.collect.return_value = {
424
+ "Trails": [{"Name": "trail-1"}, {"Name": "trail-2"}],
425
+ }
426
+
427
+ integration = AWSCloudTrailEvidenceIntegration(plan_id=123, region="us-east-1")
428
+
429
+ mock_session = MagicMock()
430
+ integration.session = mock_session
431
+
432
+ with patch.object(integration, "_save_cache"):
433
+ result = integration._fetch_fresh_cloudtrail_data()
434
+
435
+ assert len(result) == 2
436
+ assert result[0]["Name"] == "trail-1"
437
+ mock_collector_class.assert_called_once_with(session=mock_session, region="us-east-1", account_id=None, tags={})
438
+
439
+ @patch("regscale.integrations.commercial.aws.cloudtrail_evidence.CloudTrailCollector")
440
+ @patch("regscale.integrations.commercial.aws.cloudtrail_evidence.CloudTrailControlMapper")
441
+ @patch("regscale.integrations.commercial.aws.cloudtrail_evidence.Api")
442
+ def test_fetch_fresh_cloudtrail_data_with_filter(self, mock_api, mock_mapper, mock_collector_class):
443
+ """Test fetching fresh CloudTrail data with trail name filter."""
444
+ mock_collector = MagicMock()
445
+ mock_collector_class.return_value = mock_collector
446
+ mock_collector.collect.return_value = {
447
+ "Trails": [
448
+ {"Name": "prod-trail"},
449
+ {"Name": "dev-trail"},
450
+ {"Name": "prod-trail-2"},
451
+ ],
452
+ }
453
+
454
+ integration = AWSCloudTrailEvidenceIntegration(plan_id=123, trail_name_filter="prod")
455
+
456
+ mock_session = MagicMock()
457
+ integration.session = mock_session
458
+
459
+ with patch.object(integration, "_save_cache"):
460
+ result = integration._fetch_fresh_cloudtrail_data()
461
+
462
+ assert len(result) == 2
463
+ assert all("prod" in trail["Name"] for trail in result)
464
+
465
+ @patch("regscale.integrations.commercial.aws.cloudtrail_evidence.CloudTrailControlMapper")
466
+ @patch("regscale.integrations.commercial.aws.cloudtrail_evidence.Api")
467
+ def test_fetch_fresh_cloudtrail_data_initializes_session(self, mock_api, mock_mapper):
468
+ """Test that fetch_fresh_cloudtrail_data initializes session if needed."""
469
+ integration = AWSCloudTrailEvidenceIntegration(plan_id=123)
470
+
471
+ with patch.object(integration, "_initialize_aws_session") as mock_init:
472
+ with patch("regscale.integrations.commercial.aws.cloudtrail_evidence.CloudTrailCollector"):
473
+ integration._fetch_fresh_cloudtrail_data()
474
+
475
+ mock_init.assert_called_once()
476
+
477
+ @patch("regscale.integrations.commercial.aws.cloudtrail_evidence.CloudTrailControlMapper")
478
+ @patch("regscale.integrations.commercial.aws.cloudtrail_evidence.Api")
479
+ def test_create_compliance_item(self, mock_api, mock_mapper):
480
+ """Test creating a compliance item from raw data."""
481
+ integration = AWSCloudTrailEvidenceIntegration(plan_id=123)
482
+
483
+ raw_data = {
484
+ "Name": "test-trail",
485
+ "TrailARN": "arn:aws:cloudtrail:us-east-1:123456789012:trail/test-trail",
486
+ }
487
+
488
+ result = integration.create_compliance_item(raw_data)
489
+
490
+ assert isinstance(result, CloudTrailComplianceItem)
491
+ assert result.trail_name == "test-trail"
492
+
493
+ @patch("regscale.integrations.commercial.aws.cloudtrail_evidence.CloudTrailControlMapper")
494
+ @patch("regscale.integrations.commercial.aws.cloudtrail_evidence.Api")
495
+ def test_assess_compliance(self, mock_api, mock_mapper):
496
+ """Test assessing compliance."""
497
+ mock_mapper_instance = MagicMock()
498
+ mock_mapper.return_value = mock_mapper_instance
499
+
500
+ mock_mapper_instance.assess_trail_compliance.return_value = {
501
+ "AU-2": "PASS",
502
+ "AU-3": "FAIL",
503
+ }
504
+ mock_mapper_instance.assess_all_trails_compliance.return_value = {
505
+ "AU-2": "PASS",
506
+ "AU-3": "FAIL",
507
+ "AU-6": "FAIL",
508
+ }
509
+
510
+ integration = AWSCloudTrailEvidenceIntegration(plan_id=123)
511
+ integration.trails = [
512
+ CloudTrailComplianceItem({"Name": "trail-1"}),
513
+ CloudTrailComplianceItem({"Name": "trail-2"}),
514
+ ]
515
+
516
+ result = integration._assess_compliance()
517
+
518
+ assert "overall" in result
519
+ assert "trails" in result
520
+ assert len(result["trails"]) == 2
521
+ assert result["overall"] == {"AU-2": "PASS", "AU-3": "FAIL", "AU-6": "FAIL"}
522
+
523
+ @patch("regscale.integrations.commercial.aws.cloudtrail_evidence.CloudTrailControlMapper")
524
+ @patch("regscale.integrations.commercial.aws.cloudtrail_evidence.Api")
525
+ def test_sync_compliance_data_no_trails(self, mock_api, mock_mapper, caplog):
526
+ """Test sync_compliance_data with no trails."""
527
+ integration = AWSCloudTrailEvidenceIntegration(plan_id=123)
528
+
529
+ with patch.object(integration, "fetch_compliance_data", return_value=[]):
530
+ integration.sync_compliance_data()
531
+
532
+ assert "No CloudTrail trail data to sync" in caplog.text
533
+
534
+ @patch("regscale.integrations.commercial.aws.cloudtrail_evidence.CloudTrailControlMapper")
535
+ @patch("regscale.integrations.commercial.aws.cloudtrail_evidence.Api")
536
+ def test_sync_compliance_data_with_trails(self, mock_api, mock_mapper):
537
+ """Test sync_compliance_data with trails."""
538
+ mock_mapper_instance = MagicMock()
539
+ mock_mapper.return_value = mock_mapper_instance
540
+ mock_mapper_instance.assess_trail_compliance.return_value = {"AU-2": "PASS"}
541
+ mock_mapper_instance.assess_all_trails_compliance.return_value = {"AU-2": "PASS"}
542
+
543
+ integration = AWSCloudTrailEvidenceIntegration(plan_id=123, create_evidence=False, create_ssp_attachment=False)
544
+
545
+ trail_data = [{"Name": "test-trail"}]
546
+
547
+ with patch.object(integration, "fetch_compliance_data", return_value=trail_data):
548
+ with patch.object(integration, "_create_evidence_artifacts") as mock_create_evidence:
549
+ integration.sync_compliance_data()
550
+
551
+ assert len(integration.trails) == 1
552
+ mock_create_evidence.assert_not_called()
553
+
554
+ @patch("regscale.integrations.commercial.aws.cloudtrail_evidence.CloudTrailControlMapper")
555
+ @patch("regscale.integrations.commercial.aws.cloudtrail_evidence.Api")
556
+ def test_sync_compliance_data_with_evidence(self, mock_api, mock_mapper):
557
+ """Test sync_compliance_data with evidence creation."""
558
+ mock_mapper_instance = MagicMock()
559
+ mock_mapper.return_value = mock_mapper_instance
560
+ mock_mapper_instance.assess_trail_compliance.return_value = {"AU-2": "PASS"}
561
+ mock_mapper_instance.assess_all_trails_compliance.return_value = {"AU-2": "PASS"}
562
+
563
+ integration = AWSCloudTrailEvidenceIntegration(plan_id=123, create_evidence=True)
564
+
565
+ trail_data = [{"Name": "test-trail"}]
566
+
567
+ with patch.object(integration, "fetch_compliance_data", return_value=trail_data):
568
+ with patch.object(integration, "_create_evidence_artifacts") as mock_create_evidence:
569
+ integration.sync_compliance_data()
570
+
571
+ mock_create_evidence.assert_called_once()
572
+
573
+ @patch("regscale.integrations.commercial.aws.cloudtrail_evidence.CloudTrailControlMapper")
574
+ @patch("regscale.integrations.commercial.aws.cloudtrail_evidence.Api")
575
+ def test_create_evidence_file(self, mock_api, mock_mapper):
576
+ """Test creating evidence file."""
577
+ integration = AWSCloudTrailEvidenceIntegration(plan_id=123, region="us-east-1", account_id="123456789012")
578
+ integration.trails = [
579
+ CloudTrailComplianceItem(
580
+ {
581
+ "Name": "test-trail",
582
+ "TrailARN": "arn:aws:cloudtrail:us-east-1:123456789012:trail/test-trail",
583
+ "S3BucketName": "test-bucket",
584
+ "IsMultiRegionTrail": True,
585
+ "IsOrganizationTrail": False,
586
+ "LogFileValidationEnabled": True,
587
+ "KmsKeyId": "test-key",
588
+ "CloudWatchLogsLogGroupArn": "test-log-group",
589
+ "SnsTopicARN": "test-topic",
590
+ "Status": {"IsLogging": True},
591
+ "EventSelectors": [{"IncludeManagementEvents": True}],
592
+ "Tags": {"Environment": "Test"},
593
+ }
594
+ )
595
+ ]
596
+
597
+ compliance_results = {
598
+ "overall": {"AU-2": "PASS", "AU-3": "FAIL"},
599
+ "trails": [{"trail_name": "test-trail", "controls": {"AU-2": "PASS"}}],
600
+ }
601
+
602
+ evidence_file = integration._create_evidence_file(compliance_results)
603
+
604
+ assert os.path.exists(evidence_file)
605
+ assert evidence_file.endswith(".jsonl.gz")
606
+
607
+ # Verify file contents
608
+ with gzip.open(evidence_file, "rt", encoding="utf-8") as f:
609
+ lines = f.readlines()
610
+
611
+ assert len(lines) == 3 # metadata, summary, trail record
612
+
613
+ # Parse and verify metadata
614
+ metadata = json.loads(lines[0])
615
+ assert metadata["type"] == "metadata"
616
+ assert metadata["region"] == "us-east-1"
617
+ assert metadata["account_id"] == "123456789012"
618
+ assert metadata["trail_count"] == 1
619
+
620
+ # Parse and verify summary
621
+ summary = json.loads(lines[1])
622
+ assert summary["type"] == "compliance_summary"
623
+ assert summary["results"]["AU-2"] == "PASS"
624
+
625
+ # Parse and verify trail record
626
+ trail_record = json.loads(lines[2])
627
+ assert trail_record["type"] == "trail_configuration"
628
+ assert trail_record["trail_name"] == "test-trail"
629
+ assert trail_record["multi_region"] is True
630
+
631
+ # Cleanup
632
+ os.remove(evidence_file)
633
+
634
+ @patch("regscale.integrations.commercial.aws.cloudtrail_evidence.CloudTrailControlMapper")
635
+ @patch("regscale.integrations.commercial.aws.cloudtrail_evidence.Api")
636
+ def test_create_evidence_file_error(self, mock_api, mock_mapper):
637
+ """Test creating evidence file with error."""
638
+ integration = AWSCloudTrailEvidenceIntegration(plan_id=123)
639
+ integration.trails = []
640
+
641
+ compliance_results = {"overall": {}, "trails": []}
642
+
643
+ with patch("gzip.open", side_effect=OSError("Permission denied")):
644
+ with pytest.raises(OSError):
645
+ integration._create_evidence_file(compliance_results)
646
+
647
+ @patch("regscale.integrations.commercial.aws.cloudtrail_evidence.File")
648
+ @patch("regscale.integrations.commercial.aws.cloudtrail_evidence.CloudTrailControlMapper")
649
+ @patch("regscale.integrations.commercial.aws.cloudtrail_evidence.Api")
650
+ def test_create_ssp_attachment_with_evidence(self, mock_api, mock_mapper, mock_file):
651
+ """Test creating SSP attachment with evidence."""
652
+ mock_file.upload_file_to_regscale.return_value = True
653
+
654
+ integration = AWSCloudTrailEvidenceIntegration(plan_id=123, region="us-east-1")
655
+
656
+ # Create temporary evidence file
657
+ with tempfile.NamedTemporaryFile(suffix=".jsonl.gz", delete=False) as tmp_file:
658
+ evidence_file_path = tmp_file.name
659
+ with gzip.open(tmp_file, "wt") as f:
660
+ f.write("{}\n")
661
+
662
+ with patch.object(integration, "check_for_existing_evidence", return_value=False):
663
+ integration._create_ssp_attachment_with_evidence(evidence_file_path)
664
+
665
+ mock_file.upload_file_to_regscale.assert_called_once()
666
+ call_kwargs = mock_file.upload_file_to_regscale.call_args[1]
667
+ assert call_kwargs["parent_id"] == 123
668
+ assert call_kwargs["parent_module"] == "securityplans"
669
+ assert "cloudtrail_evidence" in call_kwargs["file_name"]
670
+ assert "aws,cloudtrail,audit,logging,compliance,automated" == call_kwargs["tags"]
671
+
672
+ # Cleanup
673
+ os.remove(evidence_file_path)
674
+
675
+ @patch("regscale.integrations.commercial.aws.cloudtrail_evidence.File")
676
+ @patch("regscale.integrations.commercial.aws.cloudtrail_evidence.CloudTrailControlMapper")
677
+ @patch("regscale.integrations.commercial.aws.cloudtrail_evidence.Api")
678
+ def test_create_ssp_attachment_duplicate_check(self, mock_api, mock_mapper, mock_file, caplog):
679
+ """Test creating SSP attachment with duplicate check."""
680
+ integration = AWSCloudTrailEvidenceIntegration(plan_id=123, region="us-west-2")
681
+
682
+ evidence_file_path = "/tmp/test_evidence.jsonl.gz"
683
+
684
+ with caplog.at_level(logging.INFO):
685
+ with patch.object(integration, "check_for_existing_evidence", return_value=True):
686
+ integration._create_ssp_attachment_with_evidence(evidence_file_path)
687
+
688
+ mock_file.upload_file_to_regscale.assert_not_called()
689
+ assert "already exists for today" in caplog.text
690
+
691
+ @patch("regscale.integrations.commercial.aws.cloudtrail_evidence.File")
692
+ @patch("regscale.integrations.commercial.aws.cloudtrail_evidence.CloudTrailControlMapper")
693
+ @patch("regscale.integrations.commercial.aws.cloudtrail_evidence.Api")
694
+ def test_create_ssp_attachment_upload_failure(self, mock_api, mock_mapper, mock_file, caplog):
695
+ """Test creating SSP attachment with upload failure."""
696
+ mock_file.upload_file_to_regscale.return_value = False
697
+
698
+ integration = AWSCloudTrailEvidenceIntegration(plan_id=123, region="us-east-1")
699
+
700
+ # Create temporary evidence file
701
+ with tempfile.NamedTemporaryFile(suffix=".jsonl.gz", delete=False) as tmp_file:
702
+ evidence_file_path = tmp_file.name
703
+ with gzip.open(tmp_file, "wt") as f:
704
+ f.write("{}\n")
705
+
706
+ with caplog.at_level(logging.ERROR):
707
+ with patch.object(integration, "check_for_existing_evidence", return_value=False):
708
+ integration._create_ssp_attachment_with_evidence(evidence_file_path)
709
+
710
+ assert "Failed to upload CloudTrail evidence file" in caplog.text
711
+
712
+ # Cleanup
713
+ os.remove(evidence_file_path)
714
+
715
+ @patch("regscale.integrations.commercial.aws.cloudtrail_evidence.File")
716
+ @patch("regscale.integrations.commercial.aws.cloudtrail_evidence.CloudTrailControlMapper")
717
+ @patch("regscale.integrations.commercial.aws.cloudtrail_evidence.Api")
718
+ def test_create_ssp_attachment_error_handling(self, mock_api, mock_mapper, mock_file, caplog):
719
+ """Test creating SSP attachment with error."""
720
+ integration = AWSCloudTrailEvidenceIntegration(plan_id=123)
721
+
722
+ evidence_file_path = "/tmp/nonexistent.jsonl.gz"
723
+
724
+ with caplog.at_level(logging.ERROR):
725
+ with patch.object(integration, "check_for_existing_evidence", return_value=False):
726
+ integration._create_ssp_attachment_with_evidence(evidence_file_path)
727
+
728
+ assert "Failed to create SSP attachment" in caplog.text
729
+
730
+ @patch("regscale.integrations.commercial.aws.cloudtrail_evidence.CloudTrailControlMapper")
731
+ @patch("regscale.integrations.commercial.aws.cloudtrail_evidence.Api")
732
+ def test_create_evidence_artifacts(self, mock_api, mock_mapper):
733
+ """Test creating evidence artifacts."""
734
+ integration = AWSCloudTrailEvidenceIntegration(plan_id=123, create_ssp_attachment=True)
735
+ integration.trails = []
736
+
737
+ compliance_results = {"overall": {}, "trails": []}
738
+
739
+ with patch.object(integration, "_create_evidence_file", return_value="/tmp/test.jsonl.gz") as mock_create:
740
+ with patch.object(integration, "_create_ssp_attachment_with_evidence") as mock_upload:
741
+ with patch("os.path.exists", return_value=True):
742
+ with patch("os.remove") as mock_remove:
743
+ integration._create_evidence_artifacts(compliance_results)
744
+
745
+ mock_create.assert_called_once_with(compliance_results)
746
+ mock_upload.assert_called_once()
747
+ mock_remove.assert_called_once_with("/tmp/test.jsonl.gz")
748
+
749
+ @patch("regscale.integrations.commercial.aws.cloudtrail_evidence.CloudTrailControlMapper")
750
+ @patch("regscale.integrations.commercial.aws.cloudtrail_evidence.Api")
751
+ def test_create_evidence_artifacts_no_ssp_attachment(self, mock_api, mock_mapper):
752
+ """Test creating evidence artifacts without SSP attachment."""
753
+ integration = AWSCloudTrailEvidenceIntegration(plan_id=123, create_ssp_attachment=False)
754
+ integration.trails = []
755
+
756
+ compliance_results = {"overall": {}, "trails": []}
757
+
758
+ with patch.object(integration, "_create_evidence_file", return_value="/tmp/test.jsonl.gz"):
759
+ with patch.object(integration, "_create_ssp_attachment_with_evidence") as mock_upload:
760
+ with patch("os.path.exists", return_value=True):
761
+ with patch("os.remove"):
762
+ integration._create_evidence_artifacts(compliance_results)
763
+
764
+ mock_upload.assert_not_called()
765
+
766
+ @patch("regscale.integrations.commercial.aws.cloudtrail_evidence.CloudTrailControlMapper")
767
+ @patch("regscale.integrations.commercial.aws.cloudtrail_evidence.Api")
768
+ def test_create_evidence_artifacts_cleanup(self, mock_api, mock_mapper):
769
+ """Test evidence artifacts cleanup."""
770
+ integration = AWSCloudTrailEvidenceIntegration(plan_id=123, create_ssp_attachment=True)
771
+ integration.trails = []
772
+
773
+ compliance_results = {"overall": {}, "trails": []}
774
+
775
+ # Create actual temp file to test cleanup
776
+ with tempfile.NamedTemporaryFile(suffix=".jsonl.gz", delete=False) as tmp_file:
777
+ evidence_file_path = tmp_file.name
778
+
779
+ with patch.object(integration, "_create_evidence_file", return_value=evidence_file_path):
780
+ with patch.object(integration, "_create_ssp_attachment_with_evidence"):
781
+ integration._create_evidence_artifacts(compliance_results)
782
+
783
+ # Verify file was cleaned up
784
+ assert not os.path.exists(evidence_file_path)
785
+
786
+
787
+ if __name__ == "__main__":
788
+ pytest.main([__file__, "-v"])