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.
- regscale/_version.py +1 -1
- regscale/core/app/utils/app_utils.py +11 -2
- regscale/dev/cli.py +26 -0
- regscale/dev/version.py +72 -0
- regscale/integrations/commercial/__init__.py +15 -1
- regscale/integrations/commercial/amazon/amazon/__init__.py +0 -0
- regscale/integrations/commercial/amazon/amazon/common.py +204 -0
- regscale/integrations/commercial/amazon/common.py +48 -58
- regscale/integrations/commercial/aws/audit_manager_compliance.py +2671 -0
- regscale/integrations/commercial/aws/cli.py +3093 -55
- regscale/integrations/commercial/aws/cloudtrail_control_mappings.py +333 -0
- regscale/integrations/commercial/aws/cloudtrail_evidence.py +501 -0
- regscale/integrations/commercial/aws/cloudwatch_control_mappings.py +357 -0
- regscale/integrations/commercial/aws/cloudwatch_evidence.py +490 -0
- regscale/integrations/commercial/aws/config_compliance.py +914 -0
- regscale/integrations/commercial/aws/conformance_pack_mappings.py +198 -0
- regscale/integrations/commercial/aws/evidence_generator.py +283 -0
- regscale/integrations/commercial/aws/guardduty_control_mappings.py +340 -0
- regscale/integrations/commercial/aws/guardduty_evidence.py +1053 -0
- regscale/integrations/commercial/aws/iam_control_mappings.py +368 -0
- regscale/integrations/commercial/aws/iam_evidence.py +574 -0
- regscale/integrations/commercial/aws/inventory/__init__.py +223 -22
- regscale/integrations/commercial/aws/inventory/base.py +107 -5
- regscale/integrations/commercial/aws/inventory/resources/audit_manager.py +513 -0
- regscale/integrations/commercial/aws/inventory/resources/cloudtrail.py +315 -0
- regscale/integrations/commercial/aws/inventory/resources/cloudtrail_logs_metadata.py +476 -0
- regscale/integrations/commercial/aws/inventory/resources/cloudwatch.py +191 -0
- regscale/integrations/commercial/aws/inventory/resources/compute.py +66 -9
- regscale/integrations/commercial/aws/inventory/resources/config.py +464 -0
- regscale/integrations/commercial/aws/inventory/resources/containers.py +74 -9
- regscale/integrations/commercial/aws/inventory/resources/database.py +106 -31
- regscale/integrations/commercial/aws/inventory/resources/guardduty.py +286 -0
- regscale/integrations/commercial/aws/inventory/resources/iam.py +470 -0
- regscale/integrations/commercial/aws/inventory/resources/inspector.py +476 -0
- regscale/integrations/commercial/aws/inventory/resources/integration.py +175 -61
- regscale/integrations/commercial/aws/inventory/resources/kms.py +447 -0
- regscale/integrations/commercial/aws/inventory/resources/networking.py +103 -67
- regscale/integrations/commercial/aws/inventory/resources/s3.py +394 -0
- regscale/integrations/commercial/aws/inventory/resources/security.py +268 -72
- regscale/integrations/commercial/aws/inventory/resources/securityhub.py +473 -0
- regscale/integrations/commercial/aws/inventory/resources/storage.py +53 -29
- regscale/integrations/commercial/aws/inventory/resources/systems_manager.py +657 -0
- regscale/integrations/commercial/aws/inventory/resources/vpc.py +655 -0
- regscale/integrations/commercial/aws/kms_control_mappings.py +288 -0
- regscale/integrations/commercial/aws/kms_evidence.py +879 -0
- regscale/integrations/commercial/aws/ocsf/__init__.py +7 -0
- regscale/integrations/commercial/aws/ocsf/constants.py +115 -0
- regscale/integrations/commercial/aws/ocsf/mapper.py +435 -0
- regscale/integrations/commercial/aws/org_control_mappings.py +286 -0
- regscale/integrations/commercial/aws/org_evidence.py +666 -0
- regscale/integrations/commercial/aws/s3_control_mappings.py +356 -0
- regscale/integrations/commercial/aws/s3_evidence.py +632 -0
- regscale/integrations/commercial/aws/scanner.py +851 -206
- regscale/integrations/commercial/aws/security_hub.py +319 -0
- regscale/integrations/commercial/aws/session_manager.py +282 -0
- regscale/integrations/commercial/aws/ssm_control_mappings.py +291 -0
- regscale/integrations/commercial/aws/ssm_evidence.py +492 -0
- regscale/integrations/commercial/synqly/ticketing.py +27 -0
- regscale/integrations/compliance_integration.py +308 -38
- regscale/integrations/due_date_handler.py +3 -0
- regscale/integrations/scanner_integration.py +399 -84
- regscale/models/integration_models/cisa_kev_data.json +65 -5
- regscale/models/integration_models/synqly_models/capabilities.json +1 -1
- regscale/models/integration_models/synqly_models/connectors/vulnerabilities.py +17 -9
- regscale/models/regscale_models/assessment.py +2 -1
- regscale/models/regscale_models/control_objective.py +74 -5
- regscale/models/regscale_models/file.py +2 -0
- regscale/models/regscale_models/issue.py +2 -5
- {regscale_cli-6.27.3.0.dist-info → regscale_cli-6.28.1.0.dist-info}/METADATA +1 -1
- {regscale_cli-6.27.3.0.dist-info → regscale_cli-6.28.1.0.dist-info}/RECORD +113 -34
- tests/regscale/integrations/commercial/aws/__init__.py +0 -0
- tests/regscale/integrations/commercial/aws/test_audit_manager_compliance.py +1304 -0
- tests/regscale/integrations/commercial/aws/test_audit_manager_evidence_aggregation.py +341 -0
- tests/regscale/integrations/commercial/aws/test_aws_audit_manager_collector.py +1155 -0
- tests/regscale/integrations/commercial/aws/test_aws_cloudtrail_collector.py +534 -0
- tests/regscale/integrations/commercial/aws/test_aws_config_collector.py +400 -0
- tests/regscale/integrations/commercial/aws/test_aws_guardduty_collector.py +315 -0
- tests/regscale/integrations/commercial/aws/test_aws_iam_collector.py +458 -0
- tests/regscale/integrations/commercial/aws/test_aws_inspector_collector.py +353 -0
- tests/regscale/integrations/commercial/aws/test_aws_inventory_integration.py +530 -0
- tests/regscale/integrations/commercial/aws/test_aws_kms_collector.py +919 -0
- tests/regscale/integrations/commercial/aws/test_aws_s3_collector.py +722 -0
- tests/regscale/integrations/commercial/aws/test_aws_scanner_integration.py +722 -0
- tests/regscale/integrations/commercial/aws/test_aws_securityhub_collector.py +792 -0
- tests/regscale/integrations/commercial/aws/test_aws_systems_manager_collector.py +918 -0
- tests/regscale/integrations/commercial/aws/test_aws_vpc_collector.py +996 -0
- tests/regscale/integrations/commercial/aws/test_cli_evidence.py +431 -0
- tests/regscale/integrations/commercial/aws/test_cloudtrail_control_mappings.py +452 -0
- tests/regscale/integrations/commercial/aws/test_cloudtrail_evidence.py +788 -0
- tests/regscale/integrations/commercial/aws/test_config_compliance.py +298 -0
- tests/regscale/integrations/commercial/aws/test_conformance_pack_mappings.py +200 -0
- tests/regscale/integrations/commercial/aws/test_evidence_generator.py +386 -0
- tests/regscale/integrations/commercial/aws/test_guardduty_control_mappings.py +564 -0
- tests/regscale/integrations/commercial/aws/test_guardduty_evidence.py +1041 -0
- tests/regscale/integrations/commercial/aws/test_iam_control_mappings.py +718 -0
- tests/regscale/integrations/commercial/aws/test_iam_evidence.py +1375 -0
- tests/regscale/integrations/commercial/aws/test_kms_control_mappings.py +656 -0
- tests/regscale/integrations/commercial/aws/test_kms_evidence.py +1163 -0
- tests/regscale/integrations/commercial/aws/test_ocsf_mapper.py +370 -0
- tests/regscale/integrations/commercial/aws/test_org_control_mappings.py +546 -0
- tests/regscale/integrations/commercial/aws/test_org_evidence.py +1240 -0
- tests/regscale/integrations/commercial/aws/test_s3_control_mappings.py +672 -0
- tests/regscale/integrations/commercial/aws/test_s3_evidence.py +987 -0
- tests/regscale/integrations/commercial/aws/test_scanner_evidence.py +373 -0
- tests/regscale/integrations/commercial/aws/test_security_hub_config_filtering.py +539 -0
- tests/regscale/integrations/commercial/aws/test_session_manager.py +516 -0
- tests/regscale/integrations/commercial/aws/test_ssm_control_mappings.py +588 -0
- tests/regscale/integrations/commercial/aws/test_ssm_evidence.py +735 -0
- tests/regscale/integrations/commercial/test_aws.py +55 -56
- {regscale_cli-6.27.3.0.dist-info → regscale_cli-6.28.1.0.dist-info}/LICENSE +0 -0
- {regscale_cli-6.27.3.0.dist-info → regscale_cli-6.28.1.0.dist-info}/WHEEL +0 -0
- {regscale_cli-6.27.3.0.dist-info → regscale_cli-6.28.1.0.dist-info}/entry_points.txt +0 -0
- {regscale_cli-6.27.3.0.dist-info → regscale_cli-6.28.1.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
|