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,919 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """Unit tests for AWS KMS Collector."""
4
+
5
+ import logging
6
+ from datetime import datetime
7
+ from unittest.mock import MagicMock, patch
8
+
9
+ import pytest
10
+ from botocore.exceptions import ClientError
11
+
12
+ from regscale.integrations.commercial.aws.inventory.resources.kms import KMSCollector
13
+
14
+ logger = logging.getLogger("regscale")
15
+
16
+
17
+ class TestKMSCollector:
18
+ """Test suite for KMSCollector class."""
19
+
20
+ @pytest.fixture
21
+ def mock_session(self):
22
+ """Create a mock boto3 session."""
23
+ session = MagicMock()
24
+ return session
25
+
26
+ @pytest.fixture
27
+ def mock_kms_client(self):
28
+ """Create a mock KMS client."""
29
+ client = MagicMock()
30
+ return client
31
+
32
+ @pytest.fixture
33
+ def collector(self, mock_session):
34
+ """Create a KMSCollector instance without account filtering."""
35
+ return KMSCollector(session=mock_session, region="us-east-1", account_id=None)
36
+
37
+ @pytest.fixture
38
+ def collector_with_account_filter(self, mock_session):
39
+ """Create a KMSCollector instance with account filtering."""
40
+ return KMSCollector(session=mock_session, region="us-east-1", account_id="123456789012")
41
+
42
+ @pytest.fixture
43
+ def sample_key_metadata(self):
44
+ """Create sample KMS key metadata."""
45
+ return {
46
+ "KeyId": "1234abcd-12ab-34cd-56ef-1234567890ab",
47
+ "Arn": "arn:aws:kms:us-east-1:123456789012:key/1234abcd-12ab-34cd-56ef-1234567890ab",
48
+ "Description": "Test key",
49
+ "Enabled": True,
50
+ "KeyState": "Enabled",
51
+ "CreationDate": datetime(2023, 1, 1, 0, 0, 0),
52
+ "DeletionDate": None,
53
+ "Origin": "AWS_KMS",
54
+ "KeyManager": "CUSTOMER",
55
+ "KeySpec": "SYMMETRIC_DEFAULT",
56
+ "KeyUsage": "ENCRYPT_DECRYPT",
57
+ "MultiRegion": False,
58
+ "MultiRegionConfiguration": None,
59
+ }
60
+
61
+ @pytest.fixture
62
+ def sample_key_info(self, sample_key_metadata):
63
+ """Create sample key info dictionary."""
64
+ return {
65
+ "KeyId": sample_key_metadata["KeyId"],
66
+ "Arn": sample_key_metadata["Arn"],
67
+ "Description": sample_key_metadata["Description"],
68
+ "Enabled": sample_key_metadata["Enabled"],
69
+ "KeyState": sample_key_metadata["KeyState"],
70
+ "CreationDate": str(sample_key_metadata["CreationDate"]),
71
+ "DeletionDate": None,
72
+ "Origin": sample_key_metadata["Origin"],
73
+ "KeyManager": sample_key_metadata["KeyManager"],
74
+ "KeySpec": sample_key_metadata["KeySpec"],
75
+ "KeyUsage": sample_key_metadata["KeyUsage"],
76
+ "MultiRegion": sample_key_metadata["MultiRegion"],
77
+ "MultiRegionConfiguration": sample_key_metadata["MultiRegionConfiguration"],
78
+ }
79
+
80
+ @pytest.fixture
81
+ def sample_alias(self):
82
+ """Create sample KMS alias."""
83
+ return {
84
+ "AliasName": "alias/test-key",
85
+ "AliasArn": "arn:aws:kms:us-east-1:123456789012:alias/test-key",
86
+ "TargetKeyId": "1234abcd-12ab-34cd-56ef-1234567890ab",
87
+ }
88
+
89
+ # Test initialization
90
+ def test_collector_initialization_without_account_id(self, mock_session):
91
+ """Test collector initialization without account ID filtering."""
92
+ collector = KMSCollector(session=mock_session, region="us-west-2", account_id=None)
93
+ assert collector.session == mock_session
94
+ assert collector.region == "us-west-2"
95
+ assert collector.account_id is None
96
+
97
+ def test_collector_initialization_with_account_id(self, mock_session):
98
+ """Test collector initialization with account ID filtering."""
99
+ collector = KMSCollector(session=mock_session, region="us-east-1", account_id="123456789012")
100
+ assert collector.session == mock_session
101
+ assert collector.region == "us-east-1"
102
+ assert collector.account_id == "123456789012"
103
+
104
+ # Test collect() method
105
+ @patch.object(KMSCollector, "_get_client")
106
+ @patch.object(KMSCollector, "_list_keys")
107
+ @patch.object(KMSCollector, "_list_aliases")
108
+ def test_collect_success(self, mock_list_aliases, mock_list_keys, mock_get_client, collector, sample_key_info):
109
+ """Test successful collection of KMS resources."""
110
+ mock_client = MagicMock()
111
+ mock_get_client.return_value = mock_client
112
+
113
+ mock_list_keys.return_value = [sample_key_info]
114
+ mock_list_aliases.return_value = [
115
+ {
116
+ "Region": "us-east-1",
117
+ "AliasName": "alias/test-key",
118
+ "AliasArn": "arn:aws:kms:us-east-1:123456789012:alias/test-key",
119
+ "TargetKeyId": "1234abcd-12ab-34cd-56ef-1234567890ab",
120
+ }
121
+ ]
122
+
123
+ result = collector.collect()
124
+
125
+ assert "Keys" in result
126
+ assert "Aliases" in result
127
+ assert len(result["Keys"]) == 1
128
+ assert len(result["Aliases"]) == 1
129
+ assert result["Keys"][0]["KeyId"] == sample_key_info["KeyId"]
130
+ assert result["Aliases"][0]["AliasName"] == "alias/test-key"
131
+
132
+ mock_get_client.assert_called_once_with("kms")
133
+ mock_list_keys.assert_called_once_with(mock_client)
134
+ mock_list_aliases.assert_called_once_with(mock_client)
135
+
136
+ @patch.object(KMSCollector, "_get_client")
137
+ @patch.object(KMSCollector, "_handle_error")
138
+ def test_collect_client_error(self, mock_handle_error, mock_get_client, collector):
139
+ """Test collect method handles ClientError."""
140
+ mock_get_client.side_effect = ClientError(
141
+ {"Error": {"Code": "AccessDeniedException", "Message": "Access denied"}}, "ListKeys"
142
+ )
143
+
144
+ result = collector.collect()
145
+
146
+ assert result == {"Keys": [], "Aliases": []}
147
+ mock_handle_error.assert_called_once()
148
+
149
+ @patch.object(KMSCollector, "_get_client")
150
+ def test_collect_unexpected_error(self, mock_get_client, collector, caplog):
151
+ """Test collect method handles unexpected errors."""
152
+ mock_get_client.side_effect = Exception("Unexpected error")
153
+
154
+ with caplog.at_level(logging.ERROR):
155
+ result = collector.collect()
156
+
157
+ assert result == {"Keys": [], "Aliases": []}
158
+ assert "Unexpected error collecting KMS resources" in caplog.text
159
+
160
+ # Test _list_keys() method
161
+ @patch.object(KMSCollector, "_describe_key")
162
+ @patch.object(KMSCollector, "_get_key_rotation_status")
163
+ @patch.object(KMSCollector, "_get_key_policy")
164
+ @patch.object(KMSCollector, "_list_grants")
165
+ @patch.object(KMSCollector, "_list_resource_tags")
166
+ def test_list_keys_success(
167
+ self,
168
+ mock_list_tags,
169
+ mock_list_grants,
170
+ mock_get_policy,
171
+ mock_get_rotation,
172
+ mock_describe_key,
173
+ collector,
174
+ mock_kms_client,
175
+ sample_key_info,
176
+ ):
177
+ """Test successful listing of KMS keys."""
178
+ # Setup paginator
179
+ mock_paginator = MagicMock()
180
+ mock_kms_client.get_paginator.return_value = mock_paginator
181
+ mock_paginator.paginate.return_value = [{"Keys": [{"KeyId": "1234abcd-12ab-34cd-56ef-1234567890ab"}]}]
182
+
183
+ # Setup method returns
184
+ mock_describe_key.return_value = sample_key_info
185
+ mock_get_rotation.return_value = True
186
+ mock_get_policy.return_value = '{"Version": "2012-10-17"}'
187
+ mock_list_grants.return_value = [{"GrantId": "grant1"}]
188
+ mock_list_tags.return_value = [{"TagKey": "Environment", "TagValue": "Test"}]
189
+
190
+ result = collector._list_keys(mock_kms_client)
191
+
192
+ assert len(result) == 1
193
+ assert result[0]["KeyId"] == "1234abcd-12ab-34cd-56ef-1234567890ab"
194
+ assert result[0]["Region"] == "us-east-1"
195
+ assert result[0]["RotationEnabled"] is True
196
+ assert result[0]["Policy"] == '{"Version": "2012-10-17"}'
197
+ assert result[0]["GrantCount"] == 1
198
+ assert len(result[0]["Tags"]) == 1
199
+
200
+ mock_kms_client.get_paginator.assert_called_once_with("list_keys")
201
+ mock_describe_key.assert_called_once()
202
+ mock_get_rotation.assert_called_once()
203
+ mock_get_policy.assert_called_once()
204
+ mock_list_grants.assert_called_once()
205
+ mock_list_tags.assert_called_once()
206
+
207
+ @patch.object(KMSCollector, "_describe_key")
208
+ def test_list_keys_skip_none_key_info(self, mock_describe_key, collector, mock_kms_client, sample_key_info):
209
+ """Test listing keys skips keys with no metadata."""
210
+ # Setup paginator
211
+ mock_paginator = MagicMock()
212
+ mock_kms_client.get_paginator.return_value = mock_paginator
213
+ mock_paginator.paginate.return_value = [{"Keys": [{"KeyId": "key1"}, {"KeyId": "key2"}]}]
214
+
215
+ # First key returns None, second returns valid info
216
+ mock_describe_key.side_effect = [None, sample_key_info]
217
+
218
+ result = collector._list_keys(mock_kms_client)
219
+
220
+ # Should only get one key (the second one)
221
+ assert len(result) == 1
222
+ assert mock_describe_key.call_count == 2
223
+
224
+ @patch.object(KMSCollector, "_describe_key")
225
+ @patch.object(KMSCollector, "_matches_account_id")
226
+ def test_list_keys_with_account_filter(
227
+ self, mock_matches_account, mock_describe_key, collector_with_account_filter, mock_kms_client, sample_key_info
228
+ ):
229
+ """Test listing keys with account ID filtering."""
230
+ # Setup paginator
231
+ mock_paginator = MagicMock()
232
+ mock_kms_client.get_paginator.return_value = mock_paginator
233
+ mock_paginator.paginate.return_value = [{"Keys": [{"KeyId": "key1"}, {"KeyId": "key2"}]}]
234
+
235
+ # Both keys return valid info
236
+ mock_describe_key.return_value = sample_key_info
237
+ # First key doesn't match account, second does
238
+ mock_matches_account.side_effect = [False, True]
239
+
240
+ result = collector_with_account_filter._list_keys(mock_kms_client)
241
+
242
+ # Should only get one key (the second one that matches)
243
+ assert len(result) == 1
244
+ assert mock_matches_account.call_count == 2
245
+
246
+ def test_list_keys_handle_not_found_exception(self, collector, mock_kms_client, caplog):
247
+ """Test listing keys handles NotFoundException gracefully."""
248
+ # Setup paginator
249
+ mock_paginator = MagicMock()
250
+ mock_kms_client.get_paginator.return_value = mock_paginator
251
+ mock_paginator.paginate.return_value = [{"Keys": [{"KeyId": "key1"}]}]
252
+
253
+ # describe_key raises NotFoundException
254
+ mock_kms_client.describe_key.side_effect = ClientError(
255
+ {"Error": {"Code": "NotFoundException", "Message": "Not found"}}, "DescribeKey"
256
+ )
257
+
258
+ with caplog.at_level(logging.DEBUG):
259
+ result = collector._list_keys(mock_kms_client)
260
+
261
+ # Should return empty list and not log error
262
+ assert len(result) == 0
263
+ assert "Error getting details for key" not in caplog.text
264
+
265
+ def test_list_keys_handle_access_denied_exception(self, collector, mock_kms_client, caplog):
266
+ """Test listing keys handles AccessDeniedException gracefully."""
267
+ # Setup paginator
268
+ mock_paginator = MagicMock()
269
+ mock_kms_client.get_paginator.return_value = mock_paginator
270
+ mock_paginator.paginate.return_value = [{"Keys": [{"KeyId": "key1"}]}]
271
+
272
+ # describe_key raises AccessDeniedException
273
+ mock_kms_client.describe_key.side_effect = ClientError(
274
+ {"Error": {"Code": "AccessDeniedException", "Message": "Access denied"}}, "DescribeKey"
275
+ )
276
+
277
+ with caplog.at_level(logging.DEBUG):
278
+ result = collector._list_keys(mock_kms_client)
279
+
280
+ # Should return empty list and not log error
281
+ assert len(result) == 0
282
+ assert "Error getting details for key" not in caplog.text
283
+
284
+ @patch.object(KMSCollector, "_describe_key")
285
+ def test_list_keys_handle_other_client_error(self, mock_describe_key, collector, mock_kms_client, caplog):
286
+ """Test listing keys logs other ClientErrors."""
287
+ # Setup paginator
288
+ mock_paginator = MagicMock()
289
+ mock_kms_client.get_paginator.return_value = mock_paginator
290
+ mock_paginator.paginate.return_value = [{"Keys": [{"KeyId": "key1"}]}]
291
+
292
+ # _describe_key raises different error (not NotFoundException or AccessDeniedException)
293
+ mock_describe_key.side_effect = ClientError(
294
+ {"Error": {"Code": "InternalException", "Message": "Internal error"}}, "DescribeKey"
295
+ )
296
+
297
+ with caplog.at_level(logging.ERROR):
298
+ result = collector._list_keys(mock_kms_client)
299
+
300
+ # Should return empty list and log error through the except handler
301
+ assert len(result) == 0
302
+
303
+ def test_list_keys_pagination_error_access_denied(self, collector, mock_kms_client, caplog):
304
+ """Test listing keys handles pagination AccessDeniedException."""
305
+ # Setup paginator to raise AccessDeniedException
306
+ mock_paginator = MagicMock()
307
+ mock_kms_client.get_paginator.return_value = mock_paginator
308
+ mock_paginator.paginate.side_effect = ClientError(
309
+ {"Error": {"Code": "AccessDeniedException", "Message": "Access denied"}}, "ListKeys"
310
+ )
311
+
312
+ with caplog.at_level(logging.WARNING):
313
+ result = collector._list_keys(mock_kms_client)
314
+
315
+ assert len(result) == 0
316
+ assert "Access denied to list KMS keys" in caplog.text
317
+
318
+ def test_list_keys_pagination_error_other(self, collector, mock_kms_client, caplog):
319
+ """Test listing keys handles other pagination errors."""
320
+ # Setup paginator to raise different error
321
+ mock_paginator = MagicMock()
322
+ mock_kms_client.get_paginator.return_value = mock_paginator
323
+ mock_paginator.paginate.side_effect = ClientError(
324
+ {"Error": {"Code": "InternalException", "Message": "Internal error"}}, "ListKeys"
325
+ )
326
+
327
+ with caplog.at_level(logging.ERROR):
328
+ result = collector._list_keys(mock_kms_client)
329
+
330
+ assert len(result) == 0
331
+ assert "Error listing KMS keys" in caplog.text
332
+
333
+ # Test _describe_key() method
334
+ def test_describe_key_success(self, collector, mock_kms_client, sample_key_metadata):
335
+ """Test successful key description."""
336
+ mock_kms_client.describe_key.return_value = {"KeyMetadata": sample_key_metadata}
337
+
338
+ result = collector._describe_key(mock_kms_client, "1234abcd-12ab-34cd-56ef-1234567890ab")
339
+
340
+ assert result is not None
341
+ assert result["KeyId"] == sample_key_metadata["KeyId"]
342
+ assert result["Arn"] == sample_key_metadata["Arn"]
343
+ assert result["Description"] == sample_key_metadata["Description"]
344
+ assert result["Enabled"] == sample_key_metadata["Enabled"]
345
+ assert result["KeyState"] == sample_key_metadata["KeyState"]
346
+ assert result["CreationDate"] == str(sample_key_metadata["CreationDate"])
347
+ assert result["DeletionDate"] is None
348
+ assert result["Origin"] == sample_key_metadata["Origin"]
349
+ assert result["KeyManager"] == sample_key_metadata["KeyManager"]
350
+ assert result["KeySpec"] == sample_key_metadata["KeySpec"]
351
+ assert result["KeyUsage"] == sample_key_metadata["KeyUsage"]
352
+ assert result["MultiRegion"] == sample_key_metadata["MultiRegion"]
353
+ assert result["MultiRegionConfiguration"] == sample_key_metadata["MultiRegionConfiguration"]
354
+
355
+ mock_kms_client.describe_key.assert_called_once_with(KeyId="1234abcd-12ab-34cd-56ef-1234567890ab")
356
+
357
+ def test_describe_key_with_deletion_date(self, collector, mock_kms_client, sample_key_metadata):
358
+ """Test key description with deletion date."""
359
+ sample_key_metadata["DeletionDate"] = datetime(2024, 12, 31, 23, 59, 59)
360
+ mock_kms_client.describe_key.return_value = {"KeyMetadata": sample_key_metadata}
361
+
362
+ result = collector._describe_key(mock_kms_client, "key-id")
363
+
364
+ assert result is not None
365
+ assert result["DeletionDate"] == str(sample_key_metadata["DeletionDate"])
366
+
367
+ def test_describe_key_not_found(self, collector, mock_kms_client):
368
+ """Test describe key handles NotFoundException."""
369
+ mock_kms_client.describe_key.side_effect = ClientError(
370
+ {"Error": {"Code": "NotFoundException", "Message": "Not found"}}, "DescribeKey"
371
+ )
372
+
373
+ result = collector._describe_key(mock_kms_client, "non-existent-key")
374
+
375
+ assert result is None
376
+
377
+ def test_describe_key_access_denied(self, collector, mock_kms_client):
378
+ """Test describe key handles AccessDeniedException."""
379
+ mock_kms_client.describe_key.side_effect = ClientError(
380
+ {"Error": {"Code": "AccessDeniedException", "Message": "Access denied"}}, "DescribeKey"
381
+ )
382
+
383
+ result = collector._describe_key(mock_kms_client, "key-id")
384
+
385
+ assert result is None
386
+
387
+ def test_describe_key_other_error(self, collector, mock_kms_client, caplog):
388
+ """Test describe key logs other errors."""
389
+ mock_kms_client.describe_key.side_effect = ClientError(
390
+ {"Error": {"Code": "InternalException", "Message": "Internal error"}}, "DescribeKey"
391
+ )
392
+
393
+ with caplog.at_level(logging.ERROR):
394
+ result = collector._describe_key(mock_kms_client, "key-id")
395
+
396
+ assert result is None
397
+ assert "Error describing key" in caplog.text
398
+
399
+ # Test _get_key_rotation_status() method
400
+ def test_get_key_rotation_status_enabled(self, collector, mock_kms_client):
401
+ """Test getting rotation status when enabled."""
402
+ mock_kms_client.get_key_rotation_status.return_value = {"KeyRotationEnabled": True}
403
+
404
+ result = collector._get_key_rotation_status(mock_kms_client, "key-id")
405
+
406
+ assert result is True
407
+ mock_kms_client.get_key_rotation_status.assert_called_once_with(KeyId="key-id")
408
+
409
+ def test_get_key_rotation_status_disabled(self, collector, mock_kms_client):
410
+ """Test getting rotation status when disabled."""
411
+ mock_kms_client.get_key_rotation_status.return_value = {"KeyRotationEnabled": False}
412
+
413
+ result = collector._get_key_rotation_status(mock_kms_client, "key-id")
414
+
415
+ assert result is False
416
+
417
+ def test_get_key_rotation_status_not_found(self, collector, mock_kms_client):
418
+ """Test getting rotation status handles NotFoundException."""
419
+ mock_kms_client.get_key_rotation_status.side_effect = ClientError(
420
+ {"Error": {"Code": "NotFoundException", "Message": "Not found"}}, "GetKeyRotationStatus"
421
+ )
422
+
423
+ result = collector._get_key_rotation_status(mock_kms_client, "key-id")
424
+
425
+ assert result is False
426
+
427
+ def test_get_key_rotation_status_access_denied(self, collector, mock_kms_client):
428
+ """Test getting rotation status handles AccessDeniedException."""
429
+ mock_kms_client.get_key_rotation_status.side_effect = ClientError(
430
+ {"Error": {"Code": "AccessDeniedException", "Message": "Access denied"}}, "GetKeyRotationStatus"
431
+ )
432
+
433
+ result = collector._get_key_rotation_status(mock_kms_client, "key-id")
434
+
435
+ assert result is False
436
+
437
+ def test_get_key_rotation_status_unsupported_operation(self, collector, mock_kms_client):
438
+ """Test getting rotation status handles UnsupportedOperationException."""
439
+ mock_kms_client.get_key_rotation_status.side_effect = ClientError(
440
+ {"Error": {"Code": "UnsupportedOperationException", "Message": "Unsupported"}}, "GetKeyRotationStatus"
441
+ )
442
+
443
+ result = collector._get_key_rotation_status(mock_kms_client, "key-id")
444
+
445
+ assert result is False
446
+
447
+ def test_get_key_rotation_status_other_error(self, collector, mock_kms_client):
448
+ """Test getting rotation status logs other errors."""
449
+ mock_kms_client.get_key_rotation_status.side_effect = ClientError(
450
+ {"Error": {"Code": "InternalException", "Message": "Internal error"}}, "GetKeyRotationStatus"
451
+ )
452
+
453
+ result = collector._get_key_rotation_status(mock_kms_client, "key-id")
454
+
455
+ assert result is False
456
+
457
+ # Test _get_key_policy() method
458
+ def test_get_key_policy_success(self, collector, mock_kms_client):
459
+ """Test successful key policy retrieval."""
460
+ policy = '{"Version": "2012-10-17", "Statement": []}'
461
+ mock_kms_client.get_key_policy.return_value = {"Policy": policy}
462
+
463
+ result = collector._get_key_policy(mock_kms_client, "key-id")
464
+
465
+ assert result == policy
466
+ mock_kms_client.get_key_policy.assert_called_once_with(KeyId="key-id", PolicyName="default")
467
+
468
+ def test_get_key_policy_not_found(self, collector, mock_kms_client):
469
+ """Test getting key policy handles NotFoundException."""
470
+ mock_kms_client.get_key_policy.side_effect = ClientError(
471
+ {"Error": {"Code": "NotFoundException", "Message": "Not found"}}, "GetKeyPolicy"
472
+ )
473
+
474
+ result = collector._get_key_policy(mock_kms_client, "key-id")
475
+
476
+ assert result is None
477
+
478
+ def test_get_key_policy_access_denied(self, collector, mock_kms_client):
479
+ """Test getting key policy handles AccessDeniedException."""
480
+ mock_kms_client.get_key_policy.side_effect = ClientError(
481
+ {"Error": {"Code": "AccessDeniedException", "Message": "Access denied"}}, "GetKeyPolicy"
482
+ )
483
+
484
+ result = collector._get_key_policy(mock_kms_client, "key-id")
485
+
486
+ assert result is None
487
+
488
+ def test_get_key_policy_other_error(self, collector, mock_kms_client):
489
+ """Test getting key policy logs other errors."""
490
+ mock_kms_client.get_key_policy.side_effect = ClientError(
491
+ {"Error": {"Code": "InternalException", "Message": "Internal error"}}, "GetKeyPolicy"
492
+ )
493
+
494
+ result = collector._get_key_policy(mock_kms_client, "key-id")
495
+
496
+ assert result is None
497
+
498
+ # Test _list_grants() method
499
+ def test_list_grants_success(self, collector, mock_kms_client):
500
+ """Test successful grant listing."""
501
+ mock_paginator = MagicMock()
502
+ mock_kms_client.get_paginator.return_value = mock_paginator
503
+ mock_paginator.paginate.return_value = [
504
+ {"Grants": [{"GrantId": "grant1"}, {"GrantId": "grant2"}]},
505
+ {"Grants": [{"GrantId": "grant3"}]},
506
+ ]
507
+
508
+ result = collector._list_grants(mock_kms_client, "key-id")
509
+
510
+ assert len(result) == 3
511
+ assert result[0]["GrantId"] == "grant1"
512
+ assert result[1]["GrantId"] == "grant2"
513
+ assert result[2]["GrantId"] == "grant3"
514
+
515
+ mock_kms_client.get_paginator.assert_called_once_with("list_grants")
516
+ mock_paginator.paginate.assert_called_once_with(KeyId="key-id")
517
+
518
+ def test_list_grants_empty(self, collector, mock_kms_client):
519
+ """Test listing grants when none exist."""
520
+ mock_paginator = MagicMock()
521
+ mock_kms_client.get_paginator.return_value = mock_paginator
522
+ mock_paginator.paginate.return_value = [{"Grants": []}]
523
+
524
+ result = collector._list_grants(mock_kms_client, "key-id")
525
+
526
+ assert len(result) == 0
527
+
528
+ def test_list_grants_not_found(self, collector, mock_kms_client):
529
+ """Test listing grants handles NotFoundException."""
530
+ mock_paginator = MagicMock()
531
+ mock_kms_client.get_paginator.return_value = mock_paginator
532
+ mock_paginator.paginate.side_effect = ClientError(
533
+ {"Error": {"Code": "NotFoundException", "Message": "Not found"}}, "ListGrants"
534
+ )
535
+
536
+ result = collector._list_grants(mock_kms_client, "key-id")
537
+
538
+ assert len(result) == 0
539
+
540
+ def test_list_grants_access_denied(self, collector, mock_kms_client):
541
+ """Test listing grants handles AccessDeniedException."""
542
+ mock_paginator = MagicMock()
543
+ mock_kms_client.get_paginator.return_value = mock_paginator
544
+ mock_paginator.paginate.side_effect = ClientError(
545
+ {"Error": {"Code": "AccessDeniedException", "Message": "Access denied"}}, "ListGrants"
546
+ )
547
+
548
+ result = collector._list_grants(mock_kms_client, "key-id")
549
+
550
+ assert len(result) == 0
551
+
552
+ def test_list_grants_other_error(self, collector, mock_kms_client):
553
+ """Test listing grants logs other errors."""
554
+ mock_paginator = MagicMock()
555
+ mock_kms_client.get_paginator.return_value = mock_paginator
556
+ mock_paginator.paginate.side_effect = ClientError(
557
+ {"Error": {"Code": "InternalException", "Message": "Internal error"}}, "ListGrants"
558
+ )
559
+
560
+ result = collector._list_grants(mock_kms_client, "key-id")
561
+
562
+ assert len(result) == 0
563
+
564
+ # Test _list_resource_tags() method
565
+ def test_list_resource_tags_success(self, collector, mock_kms_client):
566
+ """Test successful tag listing."""
567
+ tags = [{"TagKey": "Environment", "TagValue": "Production"}, {"TagKey": "Owner", "TagValue": "Team"}]
568
+ mock_kms_client.list_resource_tags.return_value = {"Tags": tags}
569
+
570
+ result = collector._list_resource_tags(mock_kms_client, "key-id")
571
+
572
+ assert len(result) == 2
573
+ assert result[0]["TagKey"] == "Environment"
574
+ assert result[0]["TagValue"] == "Production"
575
+ assert result[1]["TagKey"] == "Owner"
576
+ assert result[1]["TagValue"] == "Team"
577
+
578
+ mock_kms_client.list_resource_tags.assert_called_once_with(KeyId="key-id")
579
+
580
+ def test_list_resource_tags_empty(self, collector, mock_kms_client):
581
+ """Test listing tags when none exist."""
582
+ mock_kms_client.list_resource_tags.return_value = {"Tags": []}
583
+
584
+ result = collector._list_resource_tags(mock_kms_client, "key-id")
585
+
586
+ assert len(result) == 0
587
+
588
+ def test_list_resource_tags_not_found(self, collector, mock_kms_client):
589
+ """Test listing tags handles NotFoundException."""
590
+ mock_kms_client.list_resource_tags.side_effect = ClientError(
591
+ {"Error": {"Code": "NotFoundException", "Message": "Not found"}}, "ListResourceTags"
592
+ )
593
+
594
+ result = collector._list_resource_tags(mock_kms_client, "key-id")
595
+
596
+ assert len(result) == 0
597
+
598
+ def test_list_resource_tags_access_denied(self, collector, mock_kms_client):
599
+ """Test listing tags handles AccessDeniedException."""
600
+ mock_kms_client.list_resource_tags.side_effect = ClientError(
601
+ {"Error": {"Code": "AccessDeniedException", "Message": "Access denied"}}, "ListResourceTags"
602
+ )
603
+
604
+ result = collector._list_resource_tags(mock_kms_client, "key-id")
605
+
606
+ assert len(result) == 0
607
+
608
+ def test_list_resource_tags_other_error(self, collector, mock_kms_client):
609
+ """Test listing tags logs other errors."""
610
+ mock_kms_client.list_resource_tags.side_effect = ClientError(
611
+ {"Error": {"Code": "InternalException", "Message": "Internal error"}}, "ListResourceTags"
612
+ )
613
+
614
+ result = collector._list_resource_tags(mock_kms_client, "key-id")
615
+
616
+ assert len(result) == 0
617
+
618
+ # Test _list_aliases() method
619
+ def test_list_aliases_success_without_filter(self, collector, mock_kms_client):
620
+ """Test successful alias listing without account filtering."""
621
+ mock_paginator = MagicMock()
622
+ mock_kms_client.get_paginator.return_value = mock_paginator
623
+ mock_paginator.paginate.return_value = [
624
+ {
625
+ "Aliases": [
626
+ {
627
+ "AliasName": "alias/test-key-1",
628
+ "AliasArn": "arn:aws:kms:us-east-1:123456789012:alias/test-key-1",
629
+ "TargetKeyId": "key1",
630
+ },
631
+ {
632
+ "AliasName": "alias/aws/s3",
633
+ "AliasArn": "arn:aws:kms:us-east-1:123456789012:alias/aws/s3",
634
+ "TargetKeyId": "key2",
635
+ },
636
+ ]
637
+ }
638
+ ]
639
+
640
+ result = collector._list_aliases(mock_kms_client)
641
+
642
+ # Should include both aliases when no account filtering
643
+ assert len(result) == 2
644
+ assert result[0]["AliasName"] == "alias/test-key-1"
645
+ assert result[0]["Region"] == "us-east-1"
646
+ assert result[1]["AliasName"] == "alias/aws/s3"
647
+
648
+ mock_kms_client.get_paginator.assert_called_once_with("list_aliases")
649
+
650
+ @patch.object(KMSCollector, "_describe_key")
651
+ @patch.object(KMSCollector, "_matches_account_id")
652
+ def test_list_aliases_with_account_filter_skip_aws_managed(
653
+ self, mock_matches_account, mock_describe_key, collector_with_account_filter, mock_kms_client, sample_key_info
654
+ ):
655
+ """Test alias listing with account filter skips AWS managed aliases."""
656
+ mock_paginator = MagicMock()
657
+ mock_kms_client.get_paginator.return_value = mock_paginator
658
+ mock_paginator.paginate.return_value = [
659
+ {
660
+ "Aliases": [
661
+ {
662
+ "AliasName": "alias/test-key-1",
663
+ "AliasArn": "arn:aws:kms:us-east-1:123456789012:alias/test-key-1",
664
+ "TargetKeyId": "key1",
665
+ },
666
+ {
667
+ "AliasName": "alias/aws/s3",
668
+ "AliasArn": "arn:aws:kms:us-east-1:123456789012:alias/aws/s3",
669
+ "TargetKeyId": "key2",
670
+ },
671
+ ]
672
+ }
673
+ ]
674
+
675
+ mock_describe_key.return_value = sample_key_info
676
+ mock_matches_account.return_value = True
677
+
678
+ result = collector_with_account_filter._list_aliases(mock_kms_client)
679
+
680
+ # Should skip AWS managed alias
681
+ assert len(result) == 1
682
+ assert result[0]["AliasName"] == "alias/test-key-1"
683
+
684
+ @patch.object(KMSCollector, "_describe_key")
685
+ @patch.object(KMSCollector, "_matches_account_id")
686
+ def test_list_aliases_with_account_filter_check_target_key(
687
+ self, mock_matches_account, mock_describe_key, collector_with_account_filter, mock_kms_client, sample_key_info
688
+ ):
689
+ """Test alias listing with account filter checks target key account."""
690
+ mock_paginator = MagicMock()
691
+ mock_kms_client.get_paginator.return_value = mock_paginator
692
+ mock_paginator.paginate.return_value = [
693
+ {
694
+ "Aliases": [
695
+ {
696
+ "AliasName": "alias/key-in-account",
697
+ "AliasArn": "arn:aws:kms:us-east-1:123456789012:alias/key-in-account",
698
+ "TargetKeyId": "key1",
699
+ },
700
+ {
701
+ "AliasName": "alias/key-different-account",
702
+ "AliasArn": "arn:aws:kms:us-east-1:123456789012:alias/key-different-account",
703
+ "TargetKeyId": "key2",
704
+ },
705
+ ]
706
+ }
707
+ ]
708
+
709
+ mock_describe_key.return_value = sample_key_info
710
+ # First key matches account, second doesn't
711
+ mock_matches_account.side_effect = [True, False]
712
+
713
+ result = collector_with_account_filter._list_aliases(mock_kms_client)
714
+
715
+ # Should only include alias for matching account
716
+ assert len(result) == 1
717
+ assert result[0]["AliasName"] == "alias/key-in-account"
718
+ assert mock_describe_key.call_count == 2
719
+ assert mock_matches_account.call_count == 2
720
+
721
+ def test_list_aliases_without_target_key_id(self, collector_with_account_filter, mock_kms_client):
722
+ """Test alias listing when alias has no target key ID."""
723
+ mock_paginator = MagicMock()
724
+ mock_kms_client.get_paginator.return_value = mock_paginator
725
+ mock_paginator.paginate.return_value = [
726
+ {
727
+ "Aliases": [
728
+ {
729
+ "AliasName": "alias/no-target",
730
+ "AliasArn": "arn:aws:kms:us-east-1:123456789012:alias/no-target",
731
+ }
732
+ ]
733
+ }
734
+ ]
735
+
736
+ result = collector_with_account_filter._list_aliases(mock_kms_client)
737
+
738
+ # Should include alias even without target key
739
+ assert len(result) == 1
740
+ assert result[0]["AliasName"] == "alias/no-target"
741
+ assert result[0]["TargetKeyId"] is None
742
+
743
+ def test_list_aliases_access_denied(self, collector, mock_kms_client, caplog):
744
+ """Test listing aliases handles AccessDeniedException."""
745
+ mock_paginator = MagicMock()
746
+ mock_kms_client.get_paginator.return_value = mock_paginator
747
+ mock_paginator.paginate.side_effect = ClientError(
748
+ {"Error": {"Code": "AccessDeniedException", "Message": "Access denied"}}, "ListAliases"
749
+ )
750
+
751
+ with caplog.at_level(logging.WARNING):
752
+ result = collector._list_aliases(mock_kms_client)
753
+
754
+ assert len(result) == 0
755
+ assert "Access denied to list KMS aliases" in caplog.text
756
+
757
+ def test_list_aliases_other_error(self, collector, mock_kms_client, caplog):
758
+ """Test listing aliases logs other errors."""
759
+ mock_paginator = MagicMock()
760
+ mock_kms_client.get_paginator.return_value = mock_paginator
761
+ mock_paginator.paginate.side_effect = ClientError(
762
+ {"Error": {"Code": "InternalException", "Message": "Internal error"}}, "ListAliases"
763
+ )
764
+
765
+ with caplog.at_level(logging.ERROR):
766
+ result = collector._list_aliases(mock_kms_client)
767
+
768
+ assert len(result) == 0
769
+ assert "Error listing KMS aliases" in caplog.text
770
+
771
+ # Test _matches_account_id() method
772
+ def test_matches_account_id_no_filter(self, collector):
773
+ """Test account matching when no filter is specified."""
774
+ result = collector._matches_account_id("arn:aws:kms:us-east-1:999999999999:key/test")
775
+
776
+ # Should return True when no account_id filter
777
+ assert result is True
778
+
779
+ def test_matches_account_id_matching(self, collector_with_account_filter):
780
+ """Test account matching with matching account ID."""
781
+ arn = "arn:aws:kms:us-east-1:123456789012:key/1234abcd-12ab-34cd-56ef-1234567890ab"
782
+
783
+ result = collector_with_account_filter._matches_account_id(arn)
784
+
785
+ assert result is True
786
+
787
+ def test_matches_account_id_not_matching(self, collector_with_account_filter):
788
+ """Test account matching with non-matching account ID."""
789
+ arn = "arn:aws:kms:us-east-1:999999999999:key/1234abcd-12ab-34cd-56ef-1234567890ab"
790
+
791
+ result = collector_with_account_filter._matches_account_id(arn)
792
+
793
+ assert result is False
794
+
795
+ def test_matches_account_id_invalid_arn_format(self, collector_with_account_filter):
796
+ """Test account matching with invalid ARN format."""
797
+ invalid_arn = "invalid:arn:format"
798
+
799
+ result = collector_with_account_filter._matches_account_id(invalid_arn)
800
+
801
+ # Invalid format won't have enough parts, returns False
802
+ assert result is False
803
+
804
+ def test_matches_account_id_short_arn(self, collector_with_account_filter):
805
+ """Test account matching with short ARN."""
806
+ short_arn = "arn:aws:kms"
807
+
808
+ result = collector_with_account_filter._matches_account_id(short_arn)
809
+
810
+ # Short ARN won't have enough parts (< 5), returns False
811
+ assert result is False
812
+
813
+ def test_matches_account_id_none_arn(self, collector_with_account_filter):
814
+ """Test account matching with None ARN."""
815
+ result = collector_with_account_filter._matches_account_id(None)
816
+
817
+ # None ARN is caught by AttributeError in try/except, returns False
818
+ assert result is False
819
+
820
+ # Integration tests
821
+ @patch.object(KMSCollector, "_get_client")
822
+ def test_full_collection_workflow_with_pagination(
823
+ self, mock_get_client, collector, sample_key_metadata, sample_alias
824
+ ):
825
+ """Test full collection workflow with multiple pages."""
826
+ mock_client = MagicMock()
827
+ mock_get_client.return_value = mock_client
828
+
829
+ # Setup paginators
830
+ key_paginator = MagicMock()
831
+ key_paginator.paginate.return_value = [
832
+ {"Keys": [{"KeyId": "key1"}]},
833
+ {"Keys": [{"KeyId": "key2"}]},
834
+ ]
835
+
836
+ grants_paginator1 = MagicMock()
837
+ grants_paginator1.paginate.return_value = [{"Grants": []}]
838
+
839
+ grants_paginator2 = MagicMock()
840
+ grants_paginator2.paginate.return_value = [{"Grants": []}]
841
+
842
+ alias_paginator = MagicMock()
843
+ alias_paginator.paginate.return_value = [{"Aliases": [sample_alias]}]
844
+
845
+ # Return paginators in order they're called
846
+ mock_client.get_paginator.side_effect = [
847
+ key_paginator, # For list_keys
848
+ grants_paginator1, # For list_grants (key1)
849
+ grants_paginator2, # For list_grants (key2)
850
+ alias_paginator, # For list_aliases
851
+ ]
852
+
853
+ # Setup describe_key responses
854
+ mock_client.describe_key.side_effect = [
855
+ {"KeyMetadata": {**sample_key_metadata, "KeyId": "key1"}},
856
+ {"KeyMetadata": {**sample_key_metadata, "KeyId": "key2"}},
857
+ ]
858
+
859
+ # Setup other responses
860
+ mock_client.get_key_rotation_status.return_value = {"KeyRotationEnabled": True}
861
+ mock_client.get_key_policy.return_value = {"Policy": "{}"}
862
+ mock_client.list_resource_tags.return_value = {"Tags": []}
863
+
864
+ result = collector.collect()
865
+
866
+ assert len(result["Keys"]) == 2
867
+ assert len(result["Aliases"]) == 1
868
+ assert result["Keys"][0]["KeyId"] == "key1"
869
+ assert result["Keys"][1]["KeyId"] == "key2"
870
+
871
+ @patch.object(KMSCollector, "_get_client")
872
+ def test_full_collection_workflow_with_multi_region_key(self, mock_get_client, collector, sample_key_metadata):
873
+ """Test collection of multi-region key."""
874
+ mock_client = MagicMock()
875
+ mock_get_client.return_value = mock_client
876
+
877
+ # Setup multi-region key metadata
878
+ multi_region_metadata = {
879
+ **sample_key_metadata,
880
+ "MultiRegion": True,
881
+ "MultiRegionConfiguration": {
882
+ "MultiRegionKeyType": "PRIMARY",
883
+ "PrimaryKey": {"Arn": sample_key_metadata["Arn"], "Region": "us-east-1"},
884
+ "ReplicaKeys": [{"Arn": "arn:aws:kms:us-west-2:123456789012:key/replica", "Region": "us-west-2"}],
885
+ },
886
+ }
887
+
888
+ # Setup key pagination
889
+ key_paginator = MagicMock()
890
+ mock_client.get_paginator.side_effect = [
891
+ key_paginator, # For list_keys
892
+ MagicMock(), # For list_grants
893
+ MagicMock(), # For list_aliases
894
+ ]
895
+
896
+ key_paginator.paginate.return_value = [{"Keys": [{"KeyId": "multi-region-key"}]}]
897
+
898
+ # Setup describe_key response
899
+ mock_client.describe_key.return_value = {"KeyMetadata": multi_region_metadata}
900
+
901
+ # Setup other responses
902
+ mock_client.get_key_rotation_status.return_value = {"KeyRotationEnabled": True}
903
+ mock_client.get_key_policy.return_value = {"Policy": "{}"}
904
+ mock_client.list_resource_tags.return_value = {"Tags": []}
905
+
906
+ # Setup grants pagination
907
+ grants_paginator = MagicMock()
908
+ grants_paginator.paginate.return_value = [{"Grants": []}]
909
+
910
+ # Setup aliases pagination
911
+ alias_paginator = MagicMock()
912
+ alias_paginator.paginate.return_value = [{"Aliases": []}]
913
+
914
+ result = collector.collect()
915
+
916
+ assert len(result["Keys"]) == 1
917
+ assert result["Keys"][0]["MultiRegion"] is True
918
+ assert result["Keys"][0]["MultiRegionConfiguration"]["MultiRegionKeyType"] == "PRIMARY"
919
+ assert len(result["Keys"][0]["MultiRegionConfiguration"]["ReplicaKeys"]) == 1