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,792 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """Unit tests for AWS Security Hub collector."""
4
+
5
+ from datetime import datetime
6
+ from unittest.mock import MagicMock, patch
7
+
8
+ import pytest
9
+ from botocore.exceptions import ClientError
10
+
11
+ from regscale.integrations.commercial.aws.inventory.resources.securityhub import SecurityHubCollector
12
+
13
+
14
+ class TestSecurityHubCollector:
15
+ """Test suite for AWS Security Hub collector."""
16
+
17
+ @pytest.fixture
18
+ def mock_session(self):
19
+ """Create a mock AWS session."""
20
+ session = MagicMock()
21
+ return session
22
+
23
+ @pytest.fixture
24
+ def mock_client(self):
25
+ """Create a mock Security Hub client."""
26
+ client = MagicMock()
27
+ return client
28
+
29
+ @pytest.fixture
30
+ def collector_no_account(self, mock_session):
31
+ """Create a SecurityHub collector without account_id."""
32
+ return SecurityHubCollector(session=mock_session, region="us-east-1", account_id=None)
33
+
34
+ @pytest.fixture
35
+ def collector_with_account(self, mock_session):
36
+ """Create a SecurityHub collector with account_id."""
37
+ return SecurityHubCollector(session=mock_session, region="us-east-1", account_id="123456789012")
38
+
39
+ def test_initialization_without_account_id(self, mock_session):
40
+ """Test initialization without account_id."""
41
+ collector = SecurityHubCollector(session=mock_session, region="us-west-2")
42
+
43
+ assert collector.session == mock_session
44
+ assert collector.region == "us-west-2"
45
+ assert collector.account_id is None
46
+
47
+ def test_initialization_with_account_id(self, mock_session):
48
+ """Test initialization with account_id."""
49
+ collector = SecurityHubCollector(session=mock_session, region="us-west-2", account_id="123456789012")
50
+
51
+ assert collector.session == mock_session
52
+ assert collector.region == "us-west-2"
53
+ assert collector.account_id == "123456789012"
54
+
55
+ @patch.object(SecurityHubCollector, "_get_client")
56
+ @patch.object(SecurityHubCollector, "_describe_hub")
57
+ @patch.object(SecurityHubCollector, "_get_enabled_standards")
58
+ @patch.object(SecurityHubCollector, "_describe_standards")
59
+ @patch.object(SecurityHubCollector, "_list_security_controls")
60
+ @patch.object(SecurityHubCollector, "_get_findings")
61
+ @patch.object(SecurityHubCollector, "_get_insights")
62
+ @patch.object(SecurityHubCollector, "_list_members")
63
+ def test_collect_success(
64
+ self,
65
+ mock_list_members,
66
+ mock_get_insights,
67
+ mock_get_findings,
68
+ mock_list_controls,
69
+ mock_describe_standards,
70
+ mock_get_enabled_standards,
71
+ mock_describe_hub,
72
+ mock_get_client,
73
+ collector_no_account,
74
+ mock_client,
75
+ ):
76
+ """Test successful collection of all Security Hub resources."""
77
+ mock_get_client.return_value = mock_client
78
+
79
+ # Setup mock returns
80
+ mock_hub_config = {"HubArn": "arn:aws:securityhub:us-east-1:123456789012:hub/default", "Region": "us-east-1"}
81
+ mock_describe_hub.return_value = mock_hub_config
82
+
83
+ mock_enabled_stds = [
84
+ {
85
+ "StandardsArn": "arn:aws:securityhub:us-east-1::standards/aws-foundational-security-best-practices/v/1.0.0"
86
+ }
87
+ ]
88
+ mock_get_enabled_standards.return_value = mock_enabled_stds
89
+
90
+ mock_standards = [
91
+ {
92
+ "StandardsArn": "arn:aws:securityhub:us-east-1::standards/aws-foundational-security-best-practices/v/1.0.0",
93
+ "Name": "AWS Foundational Security Best Practices",
94
+ }
95
+ ]
96
+ mock_describe_standards.return_value = mock_standards
97
+
98
+ mock_controls = [
99
+ {"SecurityControlId": "IAM.1", "Title": "IAM policies should not allow full '*' administrative privileges"}
100
+ ]
101
+ mock_list_controls.return_value = mock_controls
102
+
103
+ mock_findings = [{"Id": "finding-1", "Title": "Test Finding", "Region": "us-east-1"}]
104
+ mock_get_findings.return_value = mock_findings
105
+
106
+ mock_insights = [
107
+ {
108
+ "InsightArn": "arn:aws:securityhub:us-east-1:123456789012:insight/123456789012/custom/abc123",
109
+ "Name": "Test Insight",
110
+ }
111
+ ]
112
+ mock_get_insights.return_value = mock_insights
113
+
114
+ mock_members = [{"AccountId": "123456789012", "Email": "test@example.com"}]
115
+ mock_list_members.return_value = mock_members
116
+
117
+ # Execute
118
+ result = collector_no_account.collect()
119
+
120
+ # Verify structure
121
+ assert "Findings" in result
122
+ assert "Standards" in result
123
+ assert "EnabledStandards" in result
124
+ assert "SecurityControls" in result
125
+ assert "HubConfiguration" in result
126
+ assert "Members" in result
127
+ assert "Insights" in result
128
+
129
+ # Verify content
130
+ assert result["HubConfiguration"] == mock_hub_config
131
+ assert result["EnabledStandards"] == mock_enabled_stds
132
+ assert result["Standards"] == mock_standards
133
+ assert result["SecurityControls"] == mock_controls
134
+ assert result["Findings"] == mock_findings
135
+ assert result["Insights"] == mock_insights
136
+ assert result["Members"] == mock_members
137
+
138
+ # Verify method calls
139
+ mock_get_client.assert_called_once_with("securityhub")
140
+ mock_describe_hub.assert_called_once()
141
+ mock_get_enabled_standards.assert_called_once()
142
+ mock_describe_standards.assert_called_once()
143
+ mock_list_controls.assert_called_once()
144
+ mock_get_findings.assert_called_once()
145
+ mock_get_insights.assert_called_once()
146
+ mock_list_members.assert_called_once()
147
+
148
+ def test_describe_hub_success(self, collector_no_account, mock_client):
149
+ """Test successful hub configuration retrieval."""
150
+ mock_response = {
151
+ "HubArn": "arn:aws:securityhub:us-east-1:123456789012:hub/default",
152
+ "SubscribedAt": datetime(2024, 1, 1, 12, 0, 0),
153
+ "AutoEnableControls": True,
154
+ "ControlFindingGenerator": "SECURITY_CONTROL",
155
+ }
156
+ mock_client.describe_hub.return_value = mock_response
157
+
158
+ result = collector_no_account._describe_hub(mock_client)
159
+
160
+ assert result["Region"] == "us-east-1"
161
+ assert result["HubArn"] == "arn:aws:securityhub:us-east-1:123456789012:hub/default"
162
+ assert result["AutoEnableControls"] is True
163
+ assert result["ControlFindingGenerator"] == "SECURITY_CONTROL"
164
+ assert "2024-01-01" in result["SubscribedAt"]
165
+ mock_client.describe_hub.assert_called_once()
166
+
167
+ def test_describe_hub_invalid_access_exception(self, collector_no_account, mock_client):
168
+ """Test hub configuration retrieval with InvalidAccessException."""
169
+ mock_client.describe_hub.side_effect = ClientError(
170
+ {"Error": {"Code": "InvalidAccessException", "Message": "Account is not subscribed"}},
171
+ "DescribeHub",
172
+ )
173
+
174
+ result = collector_no_account._describe_hub(mock_client)
175
+
176
+ assert result == {}
177
+ mock_client.describe_hub.assert_called_once()
178
+
179
+ def test_describe_hub_resource_not_found_exception(self, collector_no_account, mock_client):
180
+ """Test hub configuration retrieval with ResourceNotFoundException."""
181
+ mock_client.describe_hub.side_effect = ClientError(
182
+ {"Error": {"Code": "ResourceNotFoundException", "Message": "Hub not found"}},
183
+ "DescribeHub",
184
+ )
185
+
186
+ result = collector_no_account._describe_hub(mock_client)
187
+
188
+ assert result == {}
189
+ mock_client.describe_hub.assert_called_once()
190
+
191
+ def test_describe_hub_other_error(self, collector_no_account, mock_client):
192
+ """Test hub configuration retrieval with other ClientError."""
193
+ mock_client.describe_hub.side_effect = ClientError(
194
+ {"Error": {"Code": "InternalError", "Message": "Internal error"}},
195
+ "DescribeHub",
196
+ )
197
+
198
+ result = collector_no_account._describe_hub(mock_client)
199
+
200
+ assert result == {}
201
+ mock_client.describe_hub.assert_called_once()
202
+
203
+ def test_get_enabled_standards_success_single_page(self, collector_no_account, mock_client):
204
+ """Test successful enabled standards retrieval (single page)."""
205
+ mock_response = {
206
+ "StandardsSubscriptions": [
207
+ {
208
+ "StandardsSubscriptionArn": "arn:aws:securityhub:us-east-1:123456789012:subscription/aws-foundational-security-best-practices/v/1.0.0",
209
+ "StandardsArn": "arn:aws:securityhub:us-east-1::standards/aws-foundational-security-best-practices/v/1.0.0",
210
+ "StandardsInput": {},
211
+ "StandardsStatus": "READY",
212
+ }
213
+ ]
214
+ }
215
+ mock_client.get_enabled_standards.return_value = mock_response
216
+
217
+ result = collector_no_account._get_enabled_standards(mock_client)
218
+
219
+ assert len(result) == 1
220
+ assert result[0]["Region"] == "us-east-1"
221
+ assert result[0]["StandardsStatus"] == "READY"
222
+ assert "StandardsArn" in result[0]
223
+ mock_client.get_enabled_standards.assert_called_once_with()
224
+
225
+ def test_get_enabled_standards_pagination(self, collector_no_account, mock_client):
226
+ """Test enabled standards retrieval with pagination."""
227
+ mock_response_page1 = {
228
+ "StandardsSubscriptions": [
229
+ {
230
+ "StandardsSubscriptionArn": "arn:aws:securityhub:us-east-1:123456789012:subscription/standard1",
231
+ "StandardsArn": "arn:aws:securityhub:us-east-1::standards/standard1",
232
+ "StandardsInput": {},
233
+ "StandardsStatus": "READY",
234
+ }
235
+ ],
236
+ "NextToken": "token123",
237
+ }
238
+ mock_response_page2 = {
239
+ "StandardsSubscriptions": [
240
+ {
241
+ "StandardsSubscriptionArn": "arn:aws:securityhub:us-east-1:123456789012:subscription/standard2",
242
+ "StandardsArn": "arn:aws:securityhub:us-east-1::standards/standard2",
243
+ "StandardsInput": {},
244
+ "StandardsStatus": "READY",
245
+ }
246
+ ]
247
+ }
248
+ mock_client.get_enabled_standards.side_effect = [mock_response_page1, mock_response_page2]
249
+
250
+ result = collector_no_account._get_enabled_standards(mock_client)
251
+
252
+ assert len(result) == 2
253
+ assert result[0]["StandardsArn"] == "arn:aws:securityhub:us-east-1::standards/standard1"
254
+ assert result[1]["StandardsArn"] == "arn:aws:securityhub:us-east-1::standards/standard2"
255
+ assert mock_client.get_enabled_standards.call_count == 2
256
+
257
+ def test_get_enabled_standards_invalid_access_exception(self, collector_no_account, mock_client):
258
+ """Test enabled standards retrieval with InvalidAccessException."""
259
+ mock_client.get_enabled_standards.side_effect = ClientError(
260
+ {"Error": {"Code": "InvalidAccessException", "Message": "Access denied"}},
261
+ "GetEnabledStandards",
262
+ )
263
+
264
+ result = collector_no_account._get_enabled_standards(mock_client)
265
+
266
+ assert result == []
267
+ mock_client.get_enabled_standards.assert_called_once()
268
+
269
+ def test_describe_standards_success_single_page(self, collector_no_account, mock_client):
270
+ """Test successful standards description (single page)."""
271
+ mock_response = {
272
+ "Standards": [
273
+ {
274
+ "StandardsArn": "arn:aws:securityhub:us-east-1::standards/aws-foundational-security-best-practices/v/1.0.0",
275
+ "Name": "AWS Foundational Security Best Practices",
276
+ "Description": "AWS Foundational Security Best Practices standard",
277
+ "EnabledByDefault": True,
278
+ }
279
+ ]
280
+ }
281
+ mock_client.describe_standards.return_value = mock_response
282
+
283
+ result = collector_no_account._describe_standards(mock_client)
284
+
285
+ assert len(result) == 1
286
+ assert result[0]["Region"] == "us-east-1"
287
+ assert result[0]["Name"] == "AWS Foundational Security Best Practices"
288
+ assert result[0]["EnabledByDefault"] is True
289
+ mock_client.describe_standards.assert_called_once_with()
290
+
291
+ def test_describe_standards_pagination(self, collector_no_account, mock_client):
292
+ """Test standards description with pagination."""
293
+ mock_response_page1 = {
294
+ "Standards": [
295
+ {
296
+ "StandardsArn": "arn:aws:securityhub:us-east-1::standards/standard1",
297
+ "Name": "Standard 1",
298
+ "Description": "First standard",
299
+ "EnabledByDefault": True,
300
+ }
301
+ ],
302
+ "NextToken": "token123",
303
+ }
304
+ mock_response_page2 = {
305
+ "Standards": [
306
+ {
307
+ "StandardsArn": "arn:aws:securityhub:us-east-1::standards/standard2",
308
+ "Name": "Standard 2",
309
+ "Description": "Second standard",
310
+ "EnabledByDefault": False,
311
+ }
312
+ ]
313
+ }
314
+ mock_client.describe_standards.side_effect = [mock_response_page1, mock_response_page2]
315
+
316
+ result = collector_no_account._describe_standards(mock_client)
317
+
318
+ assert len(result) == 2
319
+ assert result[0]["Name"] == "Standard 1"
320
+ assert result[1]["Name"] == "Standard 2"
321
+ assert mock_client.describe_standards.call_count == 2
322
+
323
+ def test_describe_standards_invalid_access_exception(self, collector_no_account, mock_client):
324
+ """Test standards description with InvalidAccessException."""
325
+ mock_client.describe_standards.side_effect = ClientError(
326
+ {"Error": {"Code": "InvalidAccessException", "Message": "Access denied"}},
327
+ "DescribeStandards",
328
+ )
329
+
330
+ result = collector_no_account._describe_standards(mock_client)
331
+
332
+ assert result == []
333
+ mock_client.describe_standards.assert_called_once()
334
+
335
+ def test_list_security_controls_success_single_page(self, collector_no_account, mock_client):
336
+ """Test successful security controls listing (single page)."""
337
+ mock_response = {
338
+ "SecurityControlDefinitions": [
339
+ {
340
+ "SecurityControlId": "IAM.1",
341
+ "Title": "IAM policies should not allow full '*' administrative privileges",
342
+ "Description": "This control checks whether the IAM policies allow full '*' administrative privileges",
343
+ "RemediationUrl": "https://docs.aws.amazon.com/console/securityhub/IAM.1/remediation",
344
+ "SeverityRating": "HIGH",
345
+ "CurrentRegionAvailability": "AVAILABLE",
346
+ }
347
+ ]
348
+ }
349
+ mock_client.list_security_control_definitions.return_value = mock_response
350
+
351
+ result = collector_no_account._list_security_controls(mock_client)
352
+
353
+ assert len(result) == 1
354
+ assert result[0]["Region"] == "us-east-1"
355
+ assert result[0]["SecurityControlId"] == "IAM.1"
356
+ assert result[0]["SeverityRating"] == "HIGH"
357
+ mock_client.list_security_control_definitions.assert_called_once_with(MaxResults=100)
358
+
359
+ def test_list_security_controls_pagination(self, collector_no_account, mock_client):
360
+ """Test security controls listing with pagination."""
361
+ mock_response_page1 = {
362
+ "SecurityControlDefinitions": [
363
+ {
364
+ "SecurityControlId": "IAM.1",
365
+ "Title": "Control 1",
366
+ "Description": "Description 1",
367
+ "RemediationUrl": "https://example.com/1",
368
+ "SeverityRating": "HIGH",
369
+ "CurrentRegionAvailability": "AVAILABLE",
370
+ }
371
+ ],
372
+ "NextToken": "token123",
373
+ }
374
+ mock_response_page2 = {
375
+ "SecurityControlDefinitions": [
376
+ {
377
+ "SecurityControlId": "IAM.2",
378
+ "Title": "Control 2",
379
+ "Description": "Description 2",
380
+ "RemediationUrl": "https://example.com/2",
381
+ "SeverityRating": "MEDIUM",
382
+ "CurrentRegionAvailability": "AVAILABLE",
383
+ }
384
+ ]
385
+ }
386
+ mock_client.list_security_control_definitions.side_effect = [mock_response_page1, mock_response_page2]
387
+
388
+ result = collector_no_account._list_security_controls(mock_client)
389
+
390
+ assert len(result) == 2
391
+ assert result[0]["SecurityControlId"] == "IAM.1"
392
+ assert result[1]["SecurityControlId"] == "IAM.2"
393
+ assert mock_client.list_security_control_definitions.call_count == 2
394
+
395
+ def test_list_security_controls_invalid_access_exception(self, collector_no_account, mock_client):
396
+ """Test security controls listing with InvalidAccessException."""
397
+ mock_client.list_security_control_definitions.side_effect = ClientError(
398
+ {"Error": {"Code": "InvalidAccessException", "Message": "Access denied"}},
399
+ "ListSecurityControlDefinitions",
400
+ )
401
+
402
+ result = collector_no_account._list_security_controls(mock_client)
403
+
404
+ assert result == []
405
+ mock_client.list_security_control_definitions.assert_called_once_with(MaxResults=100)
406
+
407
+ def test_get_findings_success_single_page_no_account_filter(self, collector_no_account, mock_client):
408
+ """Test successful findings retrieval (single page, no account filter)."""
409
+ mock_response = {
410
+ "Findings": [
411
+ {
412
+ "Id": "finding-1",
413
+ "Title": "Test Finding 1",
414
+ "Description": "Test Description",
415
+ "Severity": {"Label": "HIGH"},
416
+ }
417
+ ]
418
+ }
419
+ mock_client.get_findings.return_value = mock_response
420
+
421
+ result = collector_no_account._get_findings(mock_client)
422
+
423
+ assert len(result) == 1
424
+ assert result[0]["Id"] == "finding-1"
425
+ assert result[0]["Region"] == "us-east-1" # Verify Region tagging
426
+ mock_client.get_findings.assert_called_once_with(MaxResults=100)
427
+
428
+ def test_get_findings_success_with_account_filter(self, collector_with_account, mock_client):
429
+ """Test successful findings retrieval with account filter."""
430
+ mock_response = {
431
+ "Findings": [
432
+ {
433
+ "Id": "finding-1",
434
+ "Title": "Test Finding 1",
435
+ "AwsAccountId": "123456789012",
436
+ "Severity": {"Label": "HIGH"},
437
+ }
438
+ ]
439
+ }
440
+ mock_client.get_findings.return_value = mock_response
441
+
442
+ result = collector_with_account._get_findings(mock_client)
443
+
444
+ assert len(result) == 1
445
+ assert result[0]["Region"] == "us-east-1" # Verify Region tagging
446
+ # Verify account filter was applied
447
+ call_args = mock_client.get_findings.call_args
448
+ assert "Filters" in call_args[1]
449
+ assert call_args[1]["Filters"]["AwsAccountId"][0]["Value"] == "123456789012"
450
+ assert call_args[1]["Filters"]["AwsAccountId"][0]["Comparison"] == "EQUALS"
451
+
452
+ def test_get_findings_pagination(self, collector_no_account, mock_client):
453
+ """Test findings retrieval with pagination."""
454
+ mock_response_page1 = {
455
+ "Findings": [{"Id": "finding-1", "Title": "Finding 1"}],
456
+ "NextToken": "token123",
457
+ }
458
+ mock_response_page2 = {"Findings": [{"Id": "finding-2", "Title": "Finding 2"}]}
459
+ mock_client.get_findings.side_effect = [mock_response_page1, mock_response_page2]
460
+
461
+ result = collector_no_account._get_findings(mock_client)
462
+
463
+ assert len(result) == 2
464
+ assert result[0]["Id"] == "finding-1"
465
+ assert result[0]["Region"] == "us-east-1" # Verify Region tagging
466
+ assert result[1]["Id"] == "finding-2"
467
+ assert result[1]["Region"] == "us-east-1" # Verify Region tagging
468
+ assert mock_client.get_findings.call_count == 2
469
+
470
+ def test_get_findings_invalid_access_exception(self, collector_no_account, mock_client):
471
+ """Test findings retrieval with InvalidAccessException."""
472
+ mock_client.get_findings.side_effect = ClientError(
473
+ {"Error": {"Code": "InvalidAccessException", "Message": "Access denied"}},
474
+ "GetFindings",
475
+ )
476
+
477
+ result = collector_no_account._get_findings(mock_client)
478
+
479
+ assert result == []
480
+ mock_client.get_findings.assert_called_once()
481
+
482
+ def test_get_insights_success_single_page(self, collector_no_account, mock_client):
483
+ """Test successful insights retrieval (single page)."""
484
+ mock_response = {
485
+ "Insights": [
486
+ {
487
+ "InsightArn": "arn:aws:securityhub:us-east-1:123456789012:insight/123456789012/custom/abc123",
488
+ "Name": "Test Insight",
489
+ "Filters": {"SeverityLabel": [{"Value": "CRITICAL", "Comparison": "EQUALS"}]},
490
+ "GroupByAttribute": "ResourceType",
491
+ }
492
+ ]
493
+ }
494
+ mock_client.get_insights.return_value = mock_response
495
+
496
+ result = collector_no_account._get_insights(mock_client)
497
+
498
+ assert len(result) == 1
499
+ assert result[0]["Region"] == "us-east-1"
500
+ assert result[0]["Name"] == "Test Insight"
501
+ assert result[0]["GroupByAttribute"] == "ResourceType"
502
+ mock_client.get_insights.assert_called_once_with(MaxResults=100)
503
+
504
+ def test_get_insights_pagination(self, collector_no_account, mock_client):
505
+ """Test insights retrieval with pagination."""
506
+ mock_response_page1 = {
507
+ "Insights": [
508
+ {
509
+ "InsightArn": "arn:aws:securityhub:us-east-1:123456789012:insight/123456789012/custom/abc123",
510
+ "Name": "Insight 1",
511
+ "Filters": {},
512
+ "GroupByAttribute": "ResourceType",
513
+ }
514
+ ],
515
+ "NextToken": "token123",
516
+ }
517
+ mock_response_page2 = {
518
+ "Insights": [
519
+ {
520
+ "InsightArn": "arn:aws:securityhub:us-east-1:123456789012:insight/123456789012/custom/def456",
521
+ "Name": "Insight 2",
522
+ "Filters": {},
523
+ "GroupByAttribute": "SeverityLabel",
524
+ }
525
+ ]
526
+ }
527
+ mock_client.get_insights.side_effect = [mock_response_page1, mock_response_page2]
528
+
529
+ result = collector_no_account._get_insights(mock_client)
530
+
531
+ assert len(result) == 2
532
+ assert result[0]["Name"] == "Insight 1"
533
+ assert result[1]["Name"] == "Insight 2"
534
+ assert mock_client.get_insights.call_count == 2
535
+
536
+ def test_get_insights_invalid_access_exception(self, collector_no_account, mock_client):
537
+ """Test insights retrieval with InvalidAccessException."""
538
+ mock_client.get_insights.side_effect = ClientError(
539
+ {"Error": {"Code": "InvalidAccessException", "Message": "Access denied"}},
540
+ "GetInsights",
541
+ )
542
+
543
+ result = collector_no_account._get_insights(mock_client)
544
+
545
+ assert result == []
546
+ mock_client.get_insights.assert_called_once()
547
+
548
+ def test_list_members_success_single_page_no_account_filter(self, collector_no_account, mock_client):
549
+ """Test successful members listing (single page, no account filter)."""
550
+ mock_response = {
551
+ "Members": [
552
+ {
553
+ "AccountId": "123456789012",
554
+ "Email": "account1@example.com",
555
+ "MasterId": "999999999999",
556
+ "AdministratorId": "999999999999",
557
+ "MemberStatus": "ENABLED",
558
+ "InvitedAt": datetime(2024, 1, 1, 12, 0, 0),
559
+ "UpdatedAt": datetime(2024, 1, 2, 12, 0, 0),
560
+ }
561
+ ]
562
+ }
563
+ mock_client.list_members.return_value = mock_response
564
+
565
+ result = collector_no_account._list_members(mock_client)
566
+
567
+ assert len(result) == 1
568
+ assert result[0]["Region"] == "us-east-1"
569
+ assert result[0]["AccountId"] == "123456789012"
570
+ assert result[0]["MemberStatus"] == "ENABLED"
571
+ assert "2024-01-01" in result[0]["InvitedAt"]
572
+ assert "2024-01-02" in result[0]["UpdatedAt"]
573
+ mock_client.list_members.assert_called_once_with(MaxResults=50)
574
+
575
+ def test_list_members_with_account_filter(self, collector_with_account, mock_client):
576
+ """Test members listing with account filter."""
577
+ mock_response = {
578
+ "Members": [
579
+ {
580
+ "AccountId": "123456789012",
581
+ "Email": "account1@example.com",
582
+ "MemberStatus": "ENABLED",
583
+ },
584
+ {
585
+ "AccountId": "999999999999",
586
+ "Email": "account2@example.com",
587
+ "MemberStatus": "ENABLED",
588
+ },
589
+ ]
590
+ }
591
+ mock_client.list_members.return_value = mock_response
592
+
593
+ result = collector_with_account._list_members(mock_client)
594
+
595
+ # Only the account matching the filter should be returned
596
+ assert len(result) == 1
597
+ assert result[0]["AccountId"] == "123456789012"
598
+ assert result[0]["Region"] == "us-east-1"
599
+
600
+ def test_list_members_pagination(self, collector_no_account, mock_client):
601
+ """Test members listing with pagination."""
602
+ mock_response_page1 = {
603
+ "Members": [
604
+ {
605
+ "AccountId": "123456789012",
606
+ "Email": "account1@example.com",
607
+ "MemberStatus": "ENABLED",
608
+ }
609
+ ],
610
+ "NextToken": "token123",
611
+ }
612
+ mock_response_page2 = {
613
+ "Members": [
614
+ {
615
+ "AccountId": "999999999999",
616
+ "Email": "account2@example.com",
617
+ "MemberStatus": "ENABLED",
618
+ }
619
+ ]
620
+ }
621
+ mock_client.list_members.side_effect = [mock_response_page1, mock_response_page2]
622
+
623
+ result = collector_no_account._list_members(mock_client)
624
+
625
+ assert len(result) == 2
626
+ assert result[0]["AccountId"] == "123456789012"
627
+ assert result[1]["AccountId"] == "999999999999"
628
+ assert mock_client.list_members.call_count == 2
629
+
630
+ def test_list_members_invalid_access_exception(self, collector_no_account, mock_client):
631
+ """Test members listing with InvalidAccessException."""
632
+ mock_client.list_members.side_effect = ClientError(
633
+ {"Error": {"Code": "InvalidAccessException", "Message": "Access denied"}},
634
+ "ListMembers",
635
+ )
636
+
637
+ result = collector_no_account._list_members(mock_client)
638
+
639
+ assert result == []
640
+ mock_client.list_members.assert_called_once()
641
+
642
+ def test_list_members_no_invited_at(self, collector_no_account, mock_client):
643
+ """Test members listing with no InvitedAt or UpdatedAt fields."""
644
+ mock_response = {
645
+ "Members": [
646
+ {
647
+ "AccountId": "123456789012",
648
+ "Email": "account1@example.com",
649
+ "MemberStatus": "ENABLED",
650
+ }
651
+ ]
652
+ }
653
+ mock_client.list_members.return_value = mock_response
654
+
655
+ result = collector_no_account._list_members(mock_client)
656
+
657
+ assert len(result) == 1
658
+ assert result[0]["InvitedAt"] is None
659
+ assert result[0]["UpdatedAt"] is None
660
+
661
+ @patch.object(SecurityHubCollector, "_get_client")
662
+ @patch.object(SecurityHubCollector, "_describe_hub")
663
+ @patch.object(SecurityHubCollector, "_get_enabled_standards")
664
+ @patch.object(SecurityHubCollector, "_describe_standards")
665
+ @patch.object(SecurityHubCollector, "_list_security_controls")
666
+ @patch.object(SecurityHubCollector, "_get_findings")
667
+ @patch.object(SecurityHubCollector, "_get_insights")
668
+ @patch.object(SecurityHubCollector, "_list_members")
669
+ @patch.object(SecurityHubCollector, "_handle_error")
670
+ def test_collect_handles_client_error(
671
+ self,
672
+ mock_handle_error,
673
+ mock_list_members,
674
+ mock_get_insights,
675
+ mock_get_findings,
676
+ mock_list_controls,
677
+ mock_describe_standards,
678
+ mock_get_enabled_standards,
679
+ mock_describe_hub,
680
+ mock_get_client,
681
+ collector_no_account,
682
+ ):
683
+ """Test that collect properly handles ClientError."""
684
+ mock_client = MagicMock()
685
+ mock_get_client.return_value = mock_client
686
+
687
+ error = ClientError(
688
+ {"Error": {"Code": "AccessDeniedException", "Message": "Access denied"}},
689
+ "DescribeHub",
690
+ )
691
+ mock_describe_hub.side_effect = error
692
+
693
+ # Mock other methods to return empty data
694
+ mock_get_enabled_standards.return_value = []
695
+ mock_describe_standards.return_value = []
696
+ mock_list_controls.return_value = []
697
+ mock_get_findings.return_value = []
698
+ mock_get_insights.return_value = []
699
+ mock_list_members.return_value = []
700
+
701
+ result = collector_no_account.collect()
702
+
703
+ # Should return empty structure
704
+ assert "Findings" in result
705
+ assert "Standards" in result
706
+ assert result["Findings"] == []
707
+ mock_handle_error.assert_called_once()
708
+
709
+ @patch.object(SecurityHubCollector, "_get_client")
710
+ @patch.object(SecurityHubCollector, "_describe_hub")
711
+ @patch.object(SecurityHubCollector, "_get_enabled_standards")
712
+ @patch.object(SecurityHubCollector, "_describe_standards")
713
+ @patch.object(SecurityHubCollector, "_list_security_controls")
714
+ @patch.object(SecurityHubCollector, "_get_findings")
715
+ @patch.object(SecurityHubCollector, "_get_insights")
716
+ @patch.object(SecurityHubCollector, "_list_members")
717
+ def test_collect_handles_unexpected_error(
718
+ self,
719
+ mock_list_members,
720
+ mock_get_insights,
721
+ mock_get_findings,
722
+ mock_list_controls,
723
+ mock_describe_standards,
724
+ mock_get_enabled_standards,
725
+ mock_describe_hub,
726
+ mock_get_client,
727
+ collector_no_account,
728
+ ):
729
+ """Test that collect properly handles unexpected errors."""
730
+ mock_client = MagicMock()
731
+ mock_get_client.return_value = mock_client
732
+
733
+ # Cause an unexpected error
734
+ mock_describe_hub.side_effect = ValueError("Unexpected error")
735
+
736
+ # Mock other methods to return empty data
737
+ mock_get_enabled_standards.return_value = []
738
+ mock_describe_standards.return_value = []
739
+ mock_list_controls.return_value = []
740
+ mock_get_findings.return_value = []
741
+ mock_get_insights.return_value = []
742
+ mock_list_members.return_value = []
743
+
744
+ result = collector_no_account.collect()
745
+
746
+ # Should return empty structure and not raise
747
+ assert "Findings" in result
748
+ assert "Standards" in result
749
+ assert result["Findings"] == []
750
+
751
+ def test_region_tagging_in_all_methods(self, collector_no_account, mock_client):
752
+ """Test that all methods properly tag resources with Region."""
753
+ # Test _describe_hub
754
+ mock_client.describe_hub.return_value = {"HubArn": "arn:test"}
755
+ hub_result = collector_no_account._describe_hub(mock_client)
756
+ assert hub_result["Region"] == "us-east-1"
757
+
758
+ # Test _get_enabled_standards
759
+ mock_client.get_enabled_standards.return_value = {
760
+ "StandardsSubscriptions": [{"StandardsArn": "arn:test", "StandardsStatus": "READY"}]
761
+ }
762
+ standards_result = collector_no_account._get_enabled_standards(mock_client)
763
+ assert all(s["Region"] == "us-east-1" for s in standards_result)
764
+
765
+ # Test _describe_standards
766
+ mock_client.describe_standards.return_value = {"Standards": [{"StandardsArn": "arn:test", "Name": "Test"}]}
767
+ desc_standards_result = collector_no_account._describe_standards(mock_client)
768
+ assert all(s["Region"] == "us-east-1" for s in desc_standards_result)
769
+
770
+ # Test _list_security_controls
771
+ mock_client.list_security_control_definitions.return_value = {
772
+ "SecurityControlDefinitions": [{"SecurityControlId": "TEST.1", "Title": "Test"}]
773
+ }
774
+ controls_result = collector_no_account._list_security_controls(mock_client)
775
+ assert all(c["Region"] == "us-east-1" for c in controls_result)
776
+
777
+ # Test _get_findings
778
+ mock_client.get_findings.return_value = {"Findings": [{"Id": "finding-1"}]}
779
+ findings_result = collector_no_account._get_findings(mock_client)
780
+ assert all(f["Region"] == "us-east-1" for f in findings_result)
781
+
782
+ # Test _get_insights
783
+ mock_client.get_insights.return_value = {"Insights": [{"InsightArn": "arn:test", "Name": "Test"}]}
784
+ insights_result = collector_no_account._get_insights(mock_client)
785
+ assert all(i["Region"] == "us-east-1" for i in insights_result)
786
+
787
+ # Test _list_members
788
+ mock_client.list_members.return_value = {
789
+ "Members": [{"AccountId": "123456789012", "Email": "test@example.com"}]
790
+ }
791
+ members_result = collector_no_account._list_members(mock_client)
792
+ assert all(m["Region"] == "us-east-1" for m in members_result)