regscale-cli 6.27.3.0__py3-none-any.whl → 6.28.1.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 (113) 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/commercial/synqly/ticketing.py +27 -0
  59. regscale/integrations/compliance_integration.py +308 -38
  60. regscale/integrations/due_date_handler.py +3 -0
  61. regscale/integrations/scanner_integration.py +399 -84
  62. regscale/models/integration_models/cisa_kev_data.json +65 -5
  63. regscale/models/integration_models/synqly_models/capabilities.json +1 -1
  64. regscale/models/integration_models/synqly_models/connectors/vulnerabilities.py +17 -9
  65. regscale/models/regscale_models/assessment.py +2 -1
  66. regscale/models/regscale_models/control_objective.py +74 -5
  67. regscale/models/regscale_models/file.py +2 -0
  68. regscale/models/regscale_models/issue.py +2 -5
  69. {regscale_cli-6.27.3.0.dist-info → regscale_cli-6.28.1.0.dist-info}/METADATA +1 -1
  70. {regscale_cli-6.27.3.0.dist-info → regscale_cli-6.28.1.0.dist-info}/RECORD +113 -34
  71. tests/regscale/integrations/commercial/aws/__init__.py +0 -0
  72. tests/regscale/integrations/commercial/aws/test_audit_manager_compliance.py +1304 -0
  73. tests/regscale/integrations/commercial/aws/test_audit_manager_evidence_aggregation.py +341 -0
  74. tests/regscale/integrations/commercial/aws/test_aws_audit_manager_collector.py +1155 -0
  75. tests/regscale/integrations/commercial/aws/test_aws_cloudtrail_collector.py +534 -0
  76. tests/regscale/integrations/commercial/aws/test_aws_config_collector.py +400 -0
  77. tests/regscale/integrations/commercial/aws/test_aws_guardduty_collector.py +315 -0
  78. tests/regscale/integrations/commercial/aws/test_aws_iam_collector.py +458 -0
  79. tests/regscale/integrations/commercial/aws/test_aws_inspector_collector.py +353 -0
  80. tests/regscale/integrations/commercial/aws/test_aws_inventory_integration.py +530 -0
  81. tests/regscale/integrations/commercial/aws/test_aws_kms_collector.py +919 -0
  82. tests/regscale/integrations/commercial/aws/test_aws_s3_collector.py +722 -0
  83. tests/regscale/integrations/commercial/aws/test_aws_scanner_integration.py +722 -0
  84. tests/regscale/integrations/commercial/aws/test_aws_securityhub_collector.py +792 -0
  85. tests/regscale/integrations/commercial/aws/test_aws_systems_manager_collector.py +918 -0
  86. tests/regscale/integrations/commercial/aws/test_aws_vpc_collector.py +996 -0
  87. tests/regscale/integrations/commercial/aws/test_cli_evidence.py +431 -0
  88. tests/regscale/integrations/commercial/aws/test_cloudtrail_control_mappings.py +452 -0
  89. tests/regscale/integrations/commercial/aws/test_cloudtrail_evidence.py +788 -0
  90. tests/regscale/integrations/commercial/aws/test_config_compliance.py +298 -0
  91. tests/regscale/integrations/commercial/aws/test_conformance_pack_mappings.py +200 -0
  92. tests/regscale/integrations/commercial/aws/test_evidence_generator.py +386 -0
  93. tests/regscale/integrations/commercial/aws/test_guardduty_control_mappings.py +564 -0
  94. tests/regscale/integrations/commercial/aws/test_guardduty_evidence.py +1041 -0
  95. tests/regscale/integrations/commercial/aws/test_iam_control_mappings.py +718 -0
  96. tests/regscale/integrations/commercial/aws/test_iam_evidence.py +1375 -0
  97. tests/regscale/integrations/commercial/aws/test_kms_control_mappings.py +656 -0
  98. tests/regscale/integrations/commercial/aws/test_kms_evidence.py +1163 -0
  99. tests/regscale/integrations/commercial/aws/test_ocsf_mapper.py +370 -0
  100. tests/regscale/integrations/commercial/aws/test_org_control_mappings.py +546 -0
  101. tests/regscale/integrations/commercial/aws/test_org_evidence.py +1240 -0
  102. tests/regscale/integrations/commercial/aws/test_s3_control_mappings.py +672 -0
  103. tests/regscale/integrations/commercial/aws/test_s3_evidence.py +987 -0
  104. tests/regscale/integrations/commercial/aws/test_scanner_evidence.py +373 -0
  105. tests/regscale/integrations/commercial/aws/test_security_hub_config_filtering.py +539 -0
  106. tests/regscale/integrations/commercial/aws/test_session_manager.py +516 -0
  107. tests/regscale/integrations/commercial/aws/test_ssm_control_mappings.py +588 -0
  108. tests/regscale/integrations/commercial/aws/test_ssm_evidence.py +735 -0
  109. tests/regscale/integrations/commercial/test_aws.py +55 -56
  110. {regscale_cli-6.27.3.0.dist-info → regscale_cli-6.28.1.0.dist-info}/LICENSE +0 -0
  111. {regscale_cli-6.27.3.0.dist-info → regscale_cli-6.28.1.0.dist-info}/WHEEL +0 -0
  112. {regscale_cli-6.27.3.0.dist-info → regscale_cli-6.28.1.0.dist-info}/entry_points.txt +0 -0
  113. {regscale_cli-6.27.3.0.dist-info → regscale_cli-6.28.1.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,735 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """Unit tests for AWS Systems Manager Evidence Integration."""
4
+
5
+ import gzip
6
+ import json
7
+ import os
8
+ import tempfile
9
+ from datetime import datetime, timedelta
10
+ from pathlib import Path
11
+ from unittest.mock import MagicMock, Mock, patch, mock_open
12
+
13
+ import pytest
14
+
15
+ from regscale.integrations.commercial.aws.ssm_evidence import (
16
+ SSMComplianceItem,
17
+ AWSSSMEvidenceIntegration,
18
+ )
19
+
20
+ PATH = "regscale.integrations.commercial.aws.ssm_evidence"
21
+
22
+
23
+ class TestSSMComplianceItem:
24
+ """Test SSMComplianceItem class."""
25
+
26
+ def test_init_with_complete_data(self):
27
+ """Test initialization with complete SSM data."""
28
+ ssm_data = {
29
+ "ManagedInstances": [
30
+ {"InstanceId": "i-1234567890abcdef0", "PingStatus": "Online"},
31
+ {"InstanceId": "i-1234567890abcdef1", "PingStatus": "Online"},
32
+ ],
33
+ "Parameters": [{"Name": "param1"}, {"Name": "param2"}],
34
+ "Documents": [{"Name": "doc1"}],
35
+ "PatchBaselines": [{"BaselineId": "pb-123"}],
36
+ "MaintenanceWindows": [{"WindowId": "mw-123"}],
37
+ "Associations": [{"AssociationId": "assoc-123"}],
38
+ "InventoryEntries": [{"TypeName": "AWS:Application"}],
39
+ "ComplianceSummary": {"TotalCompliant": 10, "TotalNonCompliant": 2},
40
+ }
41
+
42
+ item = SSMComplianceItem(ssm_data)
43
+
44
+ assert len(item.managed_instances) == 2
45
+ assert len(item.parameters) == 2
46
+ assert len(item.documents) == 1
47
+ assert len(item.patch_baselines) == 1
48
+ assert len(item.maintenance_windows) == 1
49
+ assert len(item.associations) == 1
50
+ assert len(item.inventory_entries) == 1
51
+ assert item.compliance_summary["TotalCompliant"] == 10
52
+ assert item.raw_data == ssm_data
53
+
54
+ def test_init_with_minimal_data(self):
55
+ """Test initialization with minimal SSM data."""
56
+ ssm_data = {}
57
+
58
+ item = SSMComplianceItem(ssm_data)
59
+
60
+ assert item.managed_instances == []
61
+ assert item.parameters == []
62
+ assert item.documents == []
63
+ assert item.patch_baselines == []
64
+ assert item.maintenance_windows == []
65
+ assert item.associations == []
66
+ assert item.inventory_entries == []
67
+ assert item.compliance_summary == {}
68
+
69
+ def test_to_dict(self):
70
+ """Test conversion to dictionary."""
71
+ ssm_data = {
72
+ "ManagedInstances": [{"InstanceId": "i-123"}],
73
+ "Parameters": [{"Name": "param1"}],
74
+ }
75
+
76
+ item = SSMComplianceItem(ssm_data)
77
+ result = item.to_dict()
78
+
79
+ assert result == ssm_data
80
+ assert result is item.raw_data
81
+
82
+
83
+ class TestAWSSSMEvidenceIntegration:
84
+ """Test AWSSSMEvidenceIntegration class."""
85
+
86
+ @patch(f"{PATH}.SSMControlMapper")
87
+ @patch(f"{PATH}.Api")
88
+ def test_init_with_defaults(self, mock_api, mock_mapper):
89
+ """Test initialization with default parameters."""
90
+ integration = AWSSSMEvidenceIntegration(plan_id=123)
91
+
92
+ assert integration.plan_id == 123
93
+ assert integration.region == "us-east-1"
94
+ assert integration.account_id is None
95
+ assert integration.tags == {}
96
+ assert integration.create_evidence is False
97
+ assert integration.create_ssp_attachment is True
98
+ assert integration.evidence_control_ids == []
99
+ assert integration.force_refresh is False
100
+ assert integration.aws_profile is None
101
+ assert integration.aws_access_key_id is None
102
+ assert integration.aws_secret_access_key is None
103
+ assert integration.aws_session_token is None
104
+ assert integration.session is None
105
+ assert integration.collector is None
106
+ assert integration.cache_ttl_hours == 4
107
+ assert integration.raw_ssm_data == {}
108
+ assert integration.ssm_item is None
109
+ mock_api.assert_called_once()
110
+ mock_mapper.assert_called_once_with(framework="NIST800-53R5")
111
+
112
+ @patch(f"{PATH}.SSMControlMapper")
113
+ @patch(f"{PATH}.Api")
114
+ def test_init_with_custom_parameters(self, mock_api, mock_mapper):
115
+ """Test initialization with custom parameters."""
116
+ integration = AWSSSMEvidenceIntegration(
117
+ plan_id=456,
118
+ region="us-west-2",
119
+ account_id="123456789012",
120
+ tags={"Environment": "Production"},
121
+ create_evidence=True,
122
+ create_ssp_attachment=False,
123
+ evidence_control_ids=["CM-2", "CM-6"],
124
+ force_refresh=True,
125
+ aws_profile="test-profile",
126
+ aws_access_key_id="AKIATEST",
127
+ aws_secret_access_key="secret",
128
+ aws_session_token="token",
129
+ )
130
+
131
+ assert integration.plan_id == 456
132
+ assert integration.region == "us-west-2"
133
+ assert integration.account_id == "123456789012"
134
+ assert integration.tags == {"Environment": "Production"}
135
+ assert integration.create_evidence is True
136
+ assert integration.create_ssp_attachment is False
137
+ assert integration.evidence_control_ids == ["CM-2", "CM-6"]
138
+ assert integration.force_refresh is True
139
+ assert integration.aws_profile == "test-profile"
140
+ assert integration.aws_access_key_id == "AKIATEST"
141
+ assert integration.aws_secret_access_key == "secret"
142
+ assert integration.aws_session_token == "token"
143
+
144
+ @patch(f"{PATH}.SSMControlMapper")
145
+ @patch(f"{PATH}.Api")
146
+ def test_get_cache_file_path(self, mock_api, mock_mapper):
147
+ """Test cache file path generation."""
148
+ integration = AWSSSMEvidenceIntegration(plan_id=123, region="us-east-1", account_id="123456789012")
149
+
150
+ cache_path = integration._get_cache_file_path()
151
+
152
+ assert isinstance(cache_path, Path)
153
+ assert cache_path.name == "ssm_data_us-east-1_123456789012.json"
154
+ assert "regscale" in str(cache_path)
155
+ assert "aws_ssm_cache" in str(cache_path)
156
+
157
+ @patch(f"{PATH}.SSMControlMapper")
158
+ @patch(f"{PATH}.Api")
159
+ def test_get_cache_file_path_without_account_id(self, mock_api, mock_mapper):
160
+ """Test cache file path generation without account ID."""
161
+ integration = AWSSSMEvidenceIntegration(plan_id=123, region="us-west-2")
162
+
163
+ cache_path = integration._get_cache_file_path()
164
+
165
+ assert cache_path.name == "ssm_data_us-west-2_default.json"
166
+
167
+ @patch(f"{PATH}.SSMControlMapper")
168
+ @patch(f"{PATH}.Api")
169
+ def test_is_cache_valid_no_cache(self, mock_api, mock_mapper):
170
+ """Test cache validation when cache file doesn't exist."""
171
+ integration = AWSSSMEvidenceIntegration(plan_id=123)
172
+
173
+ cache_file = integration._get_cache_file_path()
174
+ if cache_file.exists():
175
+ cache_file.unlink()
176
+
177
+ is_valid = integration._is_cache_valid()
178
+
179
+ assert is_valid is False
180
+
181
+ @patch(f"{PATH}.SSMControlMapper")
182
+ @patch(f"{PATH}.Api")
183
+ def test_is_cache_valid_expired(self, mock_api, mock_mapper):
184
+ """Test cache validation when cache is expired."""
185
+ integration = AWSSSMEvidenceIntegration(plan_id=123)
186
+ cache_file = integration._get_cache_file_path()
187
+
188
+ cache_file.parent.mkdir(parents=True, exist_ok=True)
189
+ cache_file.write_text("{}")
190
+
191
+ five_hours_ago = datetime.now() - timedelta(hours=5)
192
+ os.utime(cache_file, (five_hours_ago.timestamp(), five_hours_ago.timestamp()))
193
+
194
+ is_valid = integration._is_cache_valid()
195
+
196
+ assert is_valid is False
197
+
198
+ cache_file.unlink()
199
+
200
+ @patch(f"{PATH}.SSMControlMapper")
201
+ @patch(f"{PATH}.Api")
202
+ def test_is_cache_valid_fresh(self, mock_api, mock_mapper):
203
+ """Test cache validation when cache is fresh."""
204
+ integration = AWSSSMEvidenceIntegration(plan_id=123)
205
+ cache_file = integration._get_cache_file_path()
206
+
207
+ cache_file.parent.mkdir(parents=True, exist_ok=True)
208
+ cache_file.write_text("{}")
209
+
210
+ is_valid = integration._is_cache_valid()
211
+
212
+ assert is_valid is True
213
+
214
+ cache_file.unlink()
215
+
216
+ @patch(f"{PATH}.SSMControlMapper")
217
+ @patch(f"{PATH}.Api")
218
+ def test_save_cache(self, mock_api, mock_mapper):
219
+ """Test saving data to cache."""
220
+ integration = AWSSSMEvidenceIntegration(plan_id=123)
221
+ cache_file = integration._get_cache_file_path()
222
+
223
+ test_data = {"ManagedInstances": [{"InstanceId": "i-123"}]}
224
+
225
+ integration._save_cache(test_data)
226
+
227
+ assert cache_file.exists()
228
+ with open(cache_file) as f:
229
+ loaded_data = json.load(f)
230
+ assert loaded_data == test_data
231
+
232
+ cache_file.unlink()
233
+
234
+ @patch(f"{PATH}.SSMControlMapper")
235
+ @patch(f"{PATH}.Api")
236
+ def test_save_cache_error(self, mock_api, mock_mapper, caplog):
237
+ """Test saving cache with error."""
238
+ integration = AWSSSMEvidenceIntegration(plan_id=123)
239
+
240
+ with patch("builtins.open", side_effect=OSError("Permission denied")):
241
+ integration._save_cache({"test": "data"})
242
+
243
+ assert "Failed to save cache" in caplog.text
244
+
245
+ @patch(f"{PATH}.SSMControlMapper")
246
+ @patch(f"{PATH}.Api")
247
+ def test_load_cached_data(self, mock_api, mock_mapper):
248
+ """Test loading data from cache."""
249
+ integration = AWSSSMEvidenceIntegration(plan_id=123)
250
+ cache_file = integration._get_cache_file_path()
251
+
252
+ test_data = {"ManagedInstances": [{"InstanceId": "i-123"}]}
253
+
254
+ cache_file.parent.mkdir(parents=True, exist_ok=True)
255
+ with open(cache_file, "w") as f:
256
+ json.dump(test_data, f)
257
+
258
+ loaded_data = integration._load_cached_data()
259
+
260
+ assert loaded_data == test_data
261
+
262
+ cache_file.unlink()
263
+
264
+ @patch(f"{PATH}.SSMControlMapper")
265
+ @patch(f"{PATH}.Api")
266
+ def test_load_cached_data_error(self, mock_api, mock_mapper, caplog):
267
+ """Test loading cache with error."""
268
+ integration = AWSSSMEvidenceIntegration(plan_id=123)
269
+
270
+ result = integration._load_cached_data()
271
+
272
+ assert result is None
273
+ assert "Failed to load cache" in caplog.text
274
+
275
+ @patch(f"{PATH}.SSMControlMapper")
276
+ @patch(f"{PATH}.Api")
277
+ def test_get_cache_age_hours_no_cache(self, mock_api, mock_mapper):
278
+ """Test getting cache age when no cache exists."""
279
+ integration = AWSSSMEvidenceIntegration(plan_id=123)
280
+
281
+ age = integration._get_cache_age_hours()
282
+
283
+ assert age == float("inf")
284
+
285
+ @patch(f"{PATH}.SSMControlMapper")
286
+ @patch(f"{PATH}.Api")
287
+ def test_get_cache_age_hours_with_cache(self, mock_api, mock_mapper):
288
+ """Test getting cache age with existing cache."""
289
+ integration = AWSSSMEvidenceIntegration(plan_id=123)
290
+ cache_file = integration._get_cache_file_path()
291
+
292
+ cache_file.parent.mkdir(parents=True, exist_ok=True)
293
+ cache_file.write_text("{}")
294
+
295
+ two_hours_ago = datetime.now() - timedelta(hours=2)
296
+ os.utime(cache_file, (two_hours_ago.timestamp(), two_hours_ago.timestamp()))
297
+
298
+ age = integration._get_cache_age_hours()
299
+
300
+ assert 1.9 < age < 2.1
301
+
302
+ cache_file.unlink()
303
+
304
+ @patch(f"{PATH}.boto3.Session")
305
+ @patch(f"{PATH}.SSMControlMapper")
306
+ @patch(f"{PATH}.Api")
307
+ def test_initialize_aws_session_with_keys(self, mock_api, mock_mapper, mock_boto_session):
308
+ """Test AWS session initialization with access keys."""
309
+ mock_session = MagicMock()
310
+ mock_boto_session.return_value = mock_session
311
+
312
+ integration = AWSSSMEvidenceIntegration(
313
+ plan_id=123,
314
+ region="us-west-2",
315
+ aws_access_key_id="AKIATEST",
316
+ aws_secret_access_key="secret",
317
+ aws_session_token="token",
318
+ )
319
+
320
+ integration._initialize_aws_session()
321
+
322
+ mock_boto_session.assert_called_once_with(
323
+ aws_access_key_id="AKIATEST",
324
+ aws_secret_access_key="secret",
325
+ aws_session_token="token",
326
+ region_name="us-west-2",
327
+ )
328
+ assert integration.session == mock_session
329
+
330
+ @patch(f"{PATH}.boto3.Session")
331
+ @patch(f"{PATH}.SSMControlMapper")
332
+ @patch(f"{PATH}.Api")
333
+ def test_initialize_aws_session_with_profile(self, mock_api, mock_mapper, mock_boto_session):
334
+ """Test AWS session initialization with profile."""
335
+ mock_session = MagicMock()
336
+ mock_boto_session.return_value = mock_session
337
+
338
+ integration = AWSSSMEvidenceIntegration(plan_id=123, region="us-east-1", aws_profile="test-profile")
339
+
340
+ integration._initialize_aws_session()
341
+
342
+ mock_boto_session.assert_called_once_with(profile_name="test-profile", region_name="us-east-1")
343
+ assert integration.session == mock_session
344
+
345
+ @patch(f"{PATH}.boto3.Session")
346
+ @patch(f"{PATH}.SSMControlMapper")
347
+ @patch(f"{PATH}.Api")
348
+ def test_initialize_aws_session_default(self, mock_api, mock_mapper, mock_boto_session):
349
+ """Test AWS session initialization with default credentials."""
350
+ mock_session = MagicMock()
351
+ mock_boto_session.return_value = mock_session
352
+
353
+ integration = AWSSSMEvidenceIntegration(plan_id=123, region="us-east-1")
354
+
355
+ integration._initialize_aws_session()
356
+
357
+ mock_boto_session.assert_called_once_with(region_name="us-east-1")
358
+ assert integration.session == mock_session
359
+
360
+ @patch(f"{PATH}.SSMControlMapper")
361
+ @patch(f"{PATH}.Api")
362
+ def test_fetch_compliance_data_from_cache(self, mock_api, mock_mapper):
363
+ """Test fetching compliance data from cache."""
364
+ integration = AWSSSMEvidenceIntegration(plan_id=123)
365
+
366
+ cached_data = {"ManagedInstances": [{"InstanceId": "i-123"}]}
367
+
368
+ with patch.object(integration, "_is_cache_valid", return_value=True):
369
+ with patch.object(integration, "_load_cached_data", return_value=cached_data):
370
+ result = integration.fetch_compliance_data()
371
+
372
+ assert result == cached_data
373
+
374
+ @patch(f"{PATH}.SSMControlMapper")
375
+ @patch(f"{PATH}.Api")
376
+ def test_fetch_compliance_data_force_refresh(self, mock_api, mock_mapper):
377
+ """Test fetching compliance data with force refresh."""
378
+ integration = AWSSSMEvidenceIntegration(plan_id=123, force_refresh=True)
379
+
380
+ fresh_data = {"ManagedInstances": [{"InstanceId": "i-fresh"}]}
381
+
382
+ with patch.object(integration, "_fetch_fresh_ssm_data", return_value=fresh_data):
383
+ result = integration.fetch_compliance_data()
384
+
385
+ assert result == fresh_data
386
+
387
+ @patch(f"{PATH}.SystemsManagerCollector")
388
+ @patch(f"{PATH}.SSMControlMapper")
389
+ @patch(f"{PATH}.Api")
390
+ def test_fetch_fresh_ssm_data(self, mock_api, mock_mapper, mock_collector_class):
391
+ """Test fetching fresh SSM data."""
392
+ mock_collector = MagicMock()
393
+ mock_collector_class.return_value = mock_collector
394
+ mock_collector.collect.return_value = {
395
+ "ManagedInstances": [{"InstanceId": "i-1"}, {"InstanceId": "i-2"}],
396
+ "Parameters": [{"Name": "param1"}],
397
+ "Documents": [{"Name": "doc1"}],
398
+ "PatchBaselines": [{"BaselineId": "pb-1"}],
399
+ }
400
+
401
+ integration = AWSSSMEvidenceIntegration(plan_id=123, region="us-east-1")
402
+
403
+ mock_session = MagicMock()
404
+ integration.session = mock_session
405
+
406
+ with patch.object(integration, "_save_cache"):
407
+ result = integration._fetch_fresh_ssm_data()
408
+
409
+ assert len(result["ManagedInstances"]) == 2
410
+ assert len(result["Parameters"]) == 1
411
+ mock_collector_class.assert_called_once_with(session=mock_session, region="us-east-1", account_id=None, tags={})
412
+
413
+ @patch(f"{PATH}.SSMControlMapper")
414
+ @patch(f"{PATH}.Api")
415
+ def test_fetch_fresh_ssm_data_initializes_session(self, mock_api, mock_mapper):
416
+ """Test that fetch_fresh_ssm_data initializes session if needed."""
417
+ integration = AWSSSMEvidenceIntegration(plan_id=123)
418
+
419
+ with patch.object(integration, "_initialize_aws_session") as mock_init:
420
+ with patch(f"{PATH}.SystemsManagerCollector"):
421
+ integration._fetch_fresh_ssm_data()
422
+
423
+ mock_init.assert_called_once()
424
+
425
+ @patch(f"{PATH}.SSMControlMapper")
426
+ @patch(f"{PATH}.Api")
427
+ def test_create_compliance_item(self, mock_api, mock_mapper):
428
+ """Test creating a compliance item from raw data."""
429
+ integration = AWSSSMEvidenceIntegration(plan_id=123)
430
+
431
+ raw_data = {
432
+ "ManagedInstances": [{"InstanceId": "i-123"}],
433
+ "Parameters": [],
434
+ }
435
+
436
+ result = integration.create_compliance_item(raw_data)
437
+
438
+ assert isinstance(result, SSMComplianceItem)
439
+ assert len(result.managed_instances) == 1
440
+
441
+ @patch(f"{PATH}.SSMControlMapper")
442
+ @patch(f"{PATH}.Api")
443
+ def test_assess_compliance(self, mock_api, mock_mapper):
444
+ """Test assessing compliance."""
445
+ mock_mapper_instance = MagicMock()
446
+ mock_mapper.return_value = mock_mapper_instance
447
+
448
+ mock_mapper_instance.assess_ssm_compliance.return_value = {
449
+ "CM-2": "PASS",
450
+ "CM-6": "FAIL",
451
+ }
452
+
453
+ integration = AWSSSMEvidenceIntegration(plan_id=123)
454
+ integration.ssm_item = SSMComplianceItem(
455
+ {
456
+ "ManagedInstances": [{"InstanceId": "i-1"}],
457
+ "Parameters": [{"Name": "param1"}],
458
+ "Documents": [{"Name": "doc1"}],
459
+ "PatchBaselines": [{"BaselineId": "pb-1"}],
460
+ }
461
+ )
462
+
463
+ result = integration._assess_compliance()
464
+
465
+ assert "overall" in result
466
+ assert result["overall"] == {"CM-2": "PASS", "CM-6": "FAIL"}
467
+
468
+ @patch(f"{PATH}.SSMControlMapper")
469
+ @patch(f"{PATH}.Api")
470
+ def test_sync_compliance_data_no_data(self, mock_api, mock_mapper, caplog):
471
+ """Test sync_compliance_data with no SSM data."""
472
+ integration = AWSSSMEvidenceIntegration(plan_id=123)
473
+
474
+ with patch.object(integration, "fetch_compliance_data", return_value=None):
475
+ integration.sync_compliance_data()
476
+
477
+ assert "No Systems Manager data to sync" in caplog.text
478
+
479
+ @patch(f"{PATH}.SSMControlMapper")
480
+ @patch(f"{PATH}.Api")
481
+ def test_sync_compliance_data_with_data(self, mock_api, mock_mapper):
482
+ """Test sync_compliance_data with SSM data."""
483
+ mock_mapper_instance = MagicMock()
484
+ mock_mapper.return_value = mock_mapper_instance
485
+ mock_mapper_instance.assess_ssm_compliance.return_value = {"CM-2": "PASS"}
486
+
487
+ integration = AWSSSMEvidenceIntegration(plan_id=123, create_evidence=False, create_ssp_attachment=False)
488
+
489
+ ssm_data = {"ManagedInstances": [{"InstanceId": "i-123"}]}
490
+
491
+ with patch.object(integration, "fetch_compliance_data", return_value=ssm_data):
492
+ with patch.object(integration, "_create_evidence_artifacts") as mock_create_evidence:
493
+ integration.sync_compliance_data()
494
+
495
+ assert integration.ssm_item is not None
496
+ mock_create_evidence.assert_not_called()
497
+
498
+ @patch(f"{PATH}.SSMControlMapper")
499
+ @patch(f"{PATH}.Api")
500
+ def test_sync_compliance_data_with_evidence(self, mock_api, mock_mapper):
501
+ """Test sync_compliance_data with evidence creation."""
502
+ mock_mapper_instance = MagicMock()
503
+ mock_mapper.return_value = mock_mapper_instance
504
+ mock_mapper_instance.assess_ssm_compliance.return_value = {"CM-2": "PASS"}
505
+
506
+ integration = AWSSSMEvidenceIntegration(plan_id=123, create_evidence=True)
507
+
508
+ ssm_data = {"ManagedInstances": [{"InstanceId": "i-123"}]}
509
+
510
+ with patch.object(integration, "fetch_compliance_data", return_value=ssm_data):
511
+ with patch.object(integration, "_create_evidence_artifacts") as mock_create_evidence:
512
+ integration.sync_compliance_data()
513
+
514
+ mock_create_evidence.assert_called_once()
515
+
516
+ @patch(f"{PATH}.SSMControlMapper")
517
+ @patch(f"{PATH}.Api")
518
+ def test_create_evidence_file(self, mock_api, mock_mapper):
519
+ """Test creating evidence file."""
520
+ integration = AWSSSMEvidenceIntegration(plan_id=123, region="us-east-1", account_id="123456789012")
521
+ integration.ssm_item = SSMComplianceItem(
522
+ {
523
+ "ManagedInstances": [
524
+ {
525
+ "InstanceId": "i-123",
526
+ "PingStatus": "Online",
527
+ "PlatformName": "Amazon Linux",
528
+ "AgentVersion": "3.0.0",
529
+ "PatchSummary": {"InstalledCount": 10},
530
+ }
531
+ ],
532
+ "Parameters": [{"Name": "param1"}],
533
+ "Documents": [{"Name": "doc1"}],
534
+ "PatchBaselines": [
535
+ {
536
+ "BaselineId": "pb-123",
537
+ "BaselineName": "test-baseline",
538
+ "OperatingSystem": "AMAZON_LINUX",
539
+ "DefaultBaseline": True,
540
+ }
541
+ ],
542
+ "MaintenanceWindows": [
543
+ {
544
+ "WindowId": "mw-123",
545
+ "Name": "test-window",
546
+ "Enabled": True,
547
+ "Schedule": "cron(0 2 ? * SUN *)",
548
+ }
549
+ ],
550
+ "ComplianceSummary": {
551
+ "TotalCompliant": 10,
552
+ "TotalNonCompliant": 2,
553
+ "ComplianceTypes": ["Association", "Patch"],
554
+ },
555
+ }
556
+ )
557
+
558
+ compliance_results = {
559
+ "overall": {"CM-2": "PASS", "CM-6": "FAIL"},
560
+ }
561
+
562
+ evidence_file = integration._create_evidence_file(compliance_results)
563
+
564
+ assert os.path.exists(evidence_file)
565
+ assert evidence_file.endswith(".jsonl.gz")
566
+
567
+ with gzip.open(evidence_file, "rt", encoding="utf-8") as f:
568
+ lines = f.readlines()
569
+
570
+ assert len(lines) >= 2
571
+
572
+ metadata = json.loads(lines[0])
573
+ assert metadata["type"] == "metadata"
574
+ assert metadata["region"] == "us-east-1"
575
+ assert metadata["account_id"] == "123456789012"
576
+ assert metadata["managed_instances_count"] == 1
577
+
578
+ summary = json.loads(lines[1])
579
+ assert summary["type"] == "compliance_summary"
580
+ assert summary["results"]["CM-2"] == "PASS"
581
+
582
+ os.remove(evidence_file)
583
+
584
+ @patch(f"{PATH}.SSMControlMapper")
585
+ @patch(f"{PATH}.Api")
586
+ def test_create_evidence_file_error(self, mock_api, mock_mapper):
587
+ """Test creating evidence file with error."""
588
+ integration = AWSSSMEvidenceIntegration(plan_id=123)
589
+ integration.ssm_item = SSMComplianceItem({})
590
+
591
+ compliance_results = {"overall": {}}
592
+
593
+ with patch("gzip.open", side_effect=OSError("Permission denied")):
594
+ with pytest.raises(OSError):
595
+ integration._create_evidence_file(compliance_results)
596
+
597
+ @patch(f"{PATH}.File")
598
+ @patch(f"{PATH}.SSMControlMapper")
599
+ @patch(f"{PATH}.Api")
600
+ def test_create_ssp_attachment_with_evidence(self, mock_api, mock_mapper, mock_file):
601
+ """Test creating SSP attachment with evidence."""
602
+ mock_file.upload_file_to_regscale.return_value = True
603
+
604
+ integration = AWSSSMEvidenceIntegration(plan_id=123, region="us-east-1")
605
+
606
+ with tempfile.NamedTemporaryFile(suffix=".jsonl.gz", delete=False) as tmp_file:
607
+ evidence_file_path = tmp_file.name
608
+ with gzip.open(tmp_file, "wt") as f:
609
+ f.write("{}\n")
610
+
611
+ compliance_results = {"overall": {"CM-2": "PASS"}}
612
+
613
+ with patch.object(integration, "check_for_existing_evidence", return_value=False):
614
+ integration._create_ssp_attachment_with_evidence(evidence_file_path, compliance_results)
615
+
616
+ mock_file.upload_file_to_regscale.assert_called_once()
617
+ call_kwargs = mock_file.upload_file_to_regscale.call_args[1]
618
+ assert call_kwargs["parent_id"] == 123
619
+ assert call_kwargs["parent_module"] == "securityplans"
620
+ assert "ssm_evidence" in call_kwargs["file_name"]
621
+ assert "aws,ssm,systems-manager,patch,config,compliance,automated" == call_kwargs["tags"]
622
+
623
+ os.remove(evidence_file_path)
624
+
625
+ @patch(f"{PATH}.File")
626
+ @patch(f"{PATH}.SSMControlMapper")
627
+ @patch(f"{PATH}.Api")
628
+ def test_create_ssp_attachment_duplicate_check(self, mock_api, mock_mapper, mock_file, caplog):
629
+ """Test creating SSP attachment with duplicate check."""
630
+ integration = AWSSSMEvidenceIntegration(plan_id=123, region="us-west-2")
631
+
632
+ evidence_file_path = "/tmp/test_evidence.jsonl.gz"
633
+ compliance_results = {"overall": {"CM-2": "PASS"}}
634
+
635
+ with patch.object(integration, "check_for_existing_evidence", return_value=True):
636
+ integration._create_ssp_attachment_with_evidence(evidence_file_path, compliance_results)
637
+
638
+ mock_file.upload_file_to_regscale.assert_not_called()
639
+ assert "already exists for today" in caplog.text
640
+
641
+ @patch(f"{PATH}.File")
642
+ @patch(f"{PATH}.SSMControlMapper")
643
+ @patch(f"{PATH}.Api")
644
+ def test_create_ssp_attachment_upload_failure(self, mock_api, mock_mapper, mock_file, caplog):
645
+ """Test creating SSP attachment with upload failure."""
646
+ mock_file.upload_file_to_regscale.return_value = False
647
+
648
+ integration = AWSSSMEvidenceIntegration(plan_id=123, region="us-east-1")
649
+
650
+ with tempfile.NamedTemporaryFile(suffix=".jsonl.gz", delete=False) as tmp_file:
651
+ evidence_file_path = tmp_file.name
652
+ with gzip.open(tmp_file, "wt") as f:
653
+ f.write("{}\n")
654
+
655
+ compliance_results = {"overall": {"CM-2": "PASS"}}
656
+
657
+ with patch.object(integration, "check_for_existing_evidence", return_value=False):
658
+ integration._create_ssp_attachment_with_evidence(evidence_file_path, compliance_results)
659
+
660
+ assert "Failed to upload Systems Manager evidence file" in caplog.text
661
+
662
+ os.remove(evidence_file_path)
663
+
664
+ @patch(f"{PATH}.File")
665
+ @patch(f"{PATH}.SSMControlMapper")
666
+ @patch(f"{PATH}.Api")
667
+ def test_create_ssp_attachment_error_handling(self, mock_api, mock_mapper, mock_file, caplog):
668
+ """Test creating SSP attachment with error."""
669
+ integration = AWSSSMEvidenceIntegration(plan_id=123)
670
+
671
+ evidence_file_path = "/tmp/nonexistent.jsonl.gz"
672
+ compliance_results = {"overall": {}}
673
+
674
+ with patch.object(integration, "check_for_existing_evidence", return_value=False):
675
+ integration._create_ssp_attachment_with_evidence(evidence_file_path, compliance_results)
676
+
677
+ assert "Failed to create SSP attachment" in caplog.text
678
+
679
+ @patch(f"{PATH}.SSMControlMapper")
680
+ @patch(f"{PATH}.Api")
681
+ def test_create_evidence_artifacts(self, mock_api, mock_mapper):
682
+ """Test creating evidence artifacts."""
683
+ integration = AWSSSMEvidenceIntegration(plan_id=123, create_ssp_attachment=True)
684
+ integration.ssm_item = SSMComplianceItem({})
685
+
686
+ compliance_results = {"overall": {}}
687
+
688
+ with patch.object(integration, "_create_evidence_file", return_value="/tmp/test.jsonl.gz") as mock_create:
689
+ with patch.object(integration, "_create_ssp_attachment_with_evidence") as mock_upload:
690
+ with patch("os.path.exists", return_value=True):
691
+ with patch("os.remove") as mock_remove:
692
+ integration._create_evidence_artifacts(compliance_results)
693
+
694
+ mock_create.assert_called_once_with(compliance_results)
695
+ mock_upload.assert_called_once()
696
+ mock_remove.assert_called_once_with("/tmp/test.jsonl.gz")
697
+
698
+ @patch(f"{PATH}.SSMControlMapper")
699
+ @patch(f"{PATH}.Api")
700
+ def test_create_evidence_artifacts_no_ssp_attachment(self, mock_api, mock_mapper):
701
+ """Test creating evidence artifacts without SSP attachment."""
702
+ integration = AWSSSMEvidenceIntegration(plan_id=123, create_ssp_attachment=False)
703
+ integration.ssm_item = SSMComplianceItem({})
704
+
705
+ compliance_results = {"overall": {}}
706
+
707
+ with patch.object(integration, "_create_evidence_file", return_value="/tmp/test.jsonl.gz"):
708
+ with patch.object(integration, "_create_ssp_attachment_with_evidence") as mock_upload:
709
+ with patch("os.path.exists", return_value=True):
710
+ with patch("os.remove"):
711
+ integration._create_evidence_artifacts(compliance_results)
712
+
713
+ mock_upload.assert_not_called()
714
+
715
+ @patch(f"{PATH}.SSMControlMapper")
716
+ @patch(f"{PATH}.Api")
717
+ def test_create_evidence_artifacts_cleanup(self, mock_api, mock_mapper):
718
+ """Test evidence artifacts cleanup."""
719
+ integration = AWSSSMEvidenceIntegration(plan_id=123, create_ssp_attachment=True)
720
+ integration.ssm_item = SSMComplianceItem({})
721
+
722
+ compliance_results = {"overall": {}}
723
+
724
+ with tempfile.NamedTemporaryFile(suffix=".jsonl.gz", delete=False) as tmp_file:
725
+ evidence_file_path = tmp_file.name
726
+
727
+ with patch.object(integration, "_create_evidence_file", return_value=evidence_file_path):
728
+ with patch.object(integration, "_create_ssp_attachment_with_evidence"):
729
+ integration._create_evidence_artifacts(compliance_results)
730
+
731
+ assert not os.path.exists(evidence_file_path)
732
+
733
+
734
+ if __name__ == "__main__":
735
+ pytest.main([__file__, "-v"])