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,722 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""Unit tests for AWS S3 Collector in RegScale CLI."""
|
|
4
|
+
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from unittest.mock import MagicMock, patch
|
|
7
|
+
|
|
8
|
+
import pytest
|
|
9
|
+
from botocore.exceptions import ClientError
|
|
10
|
+
|
|
11
|
+
from regscale.integrations.commercial.aws.inventory.resources.s3 import S3Collector
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class TestS3Collector:
|
|
15
|
+
"""Test suite for AWS S3 Collector."""
|
|
16
|
+
|
|
17
|
+
@pytest.fixture
|
|
18
|
+
def mock_session(self):
|
|
19
|
+
"""Create a mock AWS session."""
|
|
20
|
+
session = MagicMock()
|
|
21
|
+
return session
|
|
22
|
+
|
|
23
|
+
@pytest.fixture
|
|
24
|
+
def mock_s3_client(self):
|
|
25
|
+
"""Create a mock S3 client."""
|
|
26
|
+
client = MagicMock()
|
|
27
|
+
return client
|
|
28
|
+
|
|
29
|
+
@pytest.fixture
|
|
30
|
+
def s3_collector(self, mock_session):
|
|
31
|
+
"""Create an S3Collector instance for testing."""
|
|
32
|
+
return S3Collector(session=mock_session, region="us-east-1", account_id="123456789012")
|
|
33
|
+
|
|
34
|
+
@pytest.fixture
|
|
35
|
+
def s3_collector_no_account(self, mock_session):
|
|
36
|
+
"""Create an S3Collector instance without account_id."""
|
|
37
|
+
return S3Collector(session=mock_session, region="us-west-2")
|
|
38
|
+
|
|
39
|
+
# Test 1: Initialization with account_id
|
|
40
|
+
def test_initialization_with_account_id(self, s3_collector):
|
|
41
|
+
"""Should initialize S3Collector with account_id."""
|
|
42
|
+
assert s3_collector.region == "us-east-1"
|
|
43
|
+
assert s3_collector.account_id == "123456789012"
|
|
44
|
+
|
|
45
|
+
# Test 2: Initialization without account_id
|
|
46
|
+
def test_initialization_without_account_id(self, s3_collector_no_account):
|
|
47
|
+
"""Should initialize S3Collector without account_id."""
|
|
48
|
+
assert s3_collector_no_account.region == "us-west-2"
|
|
49
|
+
assert s3_collector_no_account.account_id is None
|
|
50
|
+
|
|
51
|
+
# Test 3: Successful collection of buckets with full details
|
|
52
|
+
def test_collect_buckets_successfully(self, s3_collector, mock_s3_client):
|
|
53
|
+
"""Should successfully collect S3 buckets with full details."""
|
|
54
|
+
# Mock the _get_client method
|
|
55
|
+
s3_collector._get_client = MagicMock(return_value=mock_s3_client)
|
|
56
|
+
|
|
57
|
+
# Mock list_buckets response
|
|
58
|
+
mock_s3_client.list_buckets.return_value = {
|
|
59
|
+
"Buckets": [
|
|
60
|
+
{"Name": "test-bucket-1", "CreationDate": datetime(2023, 1, 1, 12, 0, 0)},
|
|
61
|
+
{"Name": "test-bucket-2", "CreationDate": datetime(2023, 2, 1, 12, 0, 0)},
|
|
62
|
+
]
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
# Mock bucket location (both in us-east-1)
|
|
66
|
+
mock_s3_client.get_bucket_location.side_effect = [
|
|
67
|
+
{"LocationConstraint": None}, # us-east-1 returns None
|
|
68
|
+
{"LocationConstraint": None},
|
|
69
|
+
]
|
|
70
|
+
|
|
71
|
+
# Mock encryption configuration
|
|
72
|
+
mock_s3_client.get_bucket_encryption.side_effect = [
|
|
73
|
+
{
|
|
74
|
+
"ServerSideEncryptionConfiguration": {
|
|
75
|
+
"Rules": [
|
|
76
|
+
{
|
|
77
|
+
"ApplyServerSideEncryptionByDefault": {
|
|
78
|
+
"SSEAlgorithm": "AES256",
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
]
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
"ServerSideEncryptionConfiguration": {
|
|
86
|
+
"Rules": [
|
|
87
|
+
{
|
|
88
|
+
"ApplyServerSideEncryptionByDefault": {
|
|
89
|
+
"SSEAlgorithm": "aws:kms",
|
|
90
|
+
"KMSMasterKeyID": "arn:aws:kms:us-east-1:123456789012:key/12345678-1234-1234-1234-123456789012",
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
]
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
]
|
|
97
|
+
|
|
98
|
+
# Mock versioning configuration
|
|
99
|
+
mock_s3_client.get_bucket_versioning.side_effect = [
|
|
100
|
+
{"Status": "Enabled", "MFADelete": "Disabled"},
|
|
101
|
+
{"Status": "Disabled", "MFADelete": "Disabled"},
|
|
102
|
+
]
|
|
103
|
+
|
|
104
|
+
# Mock public access block
|
|
105
|
+
mock_s3_client.get_public_access_block.side_effect = [
|
|
106
|
+
{
|
|
107
|
+
"PublicAccessBlockConfiguration": {
|
|
108
|
+
"BlockPublicAcls": True,
|
|
109
|
+
"IgnorePublicAcls": True,
|
|
110
|
+
"BlockPublicPolicy": True,
|
|
111
|
+
"RestrictPublicBuckets": True,
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
"PublicAccessBlockConfiguration": {
|
|
116
|
+
"BlockPublicAcls": False,
|
|
117
|
+
"IgnorePublicAcls": False,
|
|
118
|
+
"BlockPublicPolicy": False,
|
|
119
|
+
"RestrictPublicBuckets": False,
|
|
120
|
+
}
|
|
121
|
+
},
|
|
122
|
+
]
|
|
123
|
+
|
|
124
|
+
# Mock bucket policy status
|
|
125
|
+
mock_s3_client.get_bucket_policy_status.side_effect = [
|
|
126
|
+
{"PolicyStatus": {"IsPublic": False}},
|
|
127
|
+
{"PolicyStatus": {"IsPublic": True}},
|
|
128
|
+
]
|
|
129
|
+
|
|
130
|
+
# Mock bucket ACL
|
|
131
|
+
mock_s3_client.get_bucket_acl.side_effect = [
|
|
132
|
+
{
|
|
133
|
+
"Owner": {"DisplayName": "owner1", "ID": "owner-id-1"},
|
|
134
|
+
"Grants": [{"Grantee": {"Type": "CanonicalUser"}, "Permission": "FULL_CONTROL"}],
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
"Owner": {"DisplayName": "owner2", "ID": "owner-id-2"},
|
|
138
|
+
"Grants": [
|
|
139
|
+
{"Grantee": {"Type": "CanonicalUser"}, "Permission": "FULL_CONTROL"},
|
|
140
|
+
{"Grantee": {"Type": "Group"}, "Permission": "READ"},
|
|
141
|
+
],
|
|
142
|
+
},
|
|
143
|
+
]
|
|
144
|
+
|
|
145
|
+
# Mock bucket tagging
|
|
146
|
+
mock_s3_client.get_bucket_tagging.side_effect = [
|
|
147
|
+
{"TagSet": [{"Key": "Environment", "Value": "Production"}, {"Key": "Owner", "Value": "TeamA"}]},
|
|
148
|
+
{"TagSet": [{"Key": "Environment", "Value": "Development"}]},
|
|
149
|
+
]
|
|
150
|
+
|
|
151
|
+
# Mock bucket logging
|
|
152
|
+
mock_s3_client.get_bucket_logging.side_effect = [
|
|
153
|
+
{"LoggingEnabled": {"TargetBucket": "log-bucket", "TargetPrefix": "logs/"}},
|
|
154
|
+
{},
|
|
155
|
+
]
|
|
156
|
+
|
|
157
|
+
result = s3_collector.collect()
|
|
158
|
+
|
|
159
|
+
assert "Buckets" in result
|
|
160
|
+
assert len(result["Buckets"]) == 2
|
|
161
|
+
|
|
162
|
+
# Verify first bucket
|
|
163
|
+
bucket1 = result["Buckets"][0]
|
|
164
|
+
assert bucket1["Name"] == "test-bucket-1"
|
|
165
|
+
assert bucket1["Region"] == "us-east-1"
|
|
166
|
+
assert bucket1["Location"] == "us-east-1"
|
|
167
|
+
assert bucket1["Encryption"]["Enabled"] is True
|
|
168
|
+
assert bucket1["Encryption"]["Algorithm"] == "AES256"
|
|
169
|
+
assert bucket1["Versioning"]["Status"] == "Enabled"
|
|
170
|
+
assert bucket1["PublicAccessBlock"]["BlockPublicAcls"] is True
|
|
171
|
+
assert bucket1["PolicyStatus"]["IsPublic"] is False
|
|
172
|
+
assert bucket1["ACL"]["GrantCount"] == 1
|
|
173
|
+
assert len(bucket1["Tags"]) == 2
|
|
174
|
+
assert bucket1["Logging"]["Enabled"] is True
|
|
175
|
+
|
|
176
|
+
# Verify second bucket
|
|
177
|
+
bucket2 = result["Buckets"][1]
|
|
178
|
+
assert bucket2["Name"] == "test-bucket-2"
|
|
179
|
+
assert bucket2["Encryption"]["Algorithm"] == "aws:kms"
|
|
180
|
+
assert bucket2["PublicAccessBlock"]["BlockPublicAcls"] is False
|
|
181
|
+
assert bucket2["PolicyStatus"]["IsPublic"] is True
|
|
182
|
+
assert bucket2["ACL"]["GrantCount"] == 2
|
|
183
|
+
assert len(bucket2["Tags"]) == 1
|
|
184
|
+
assert bucket2["Logging"]["Enabled"] is False
|
|
185
|
+
|
|
186
|
+
# Test 4: Bucket location retrieval
|
|
187
|
+
def test_get_bucket_location_us_east_1(self, s3_collector, mock_s3_client):
|
|
188
|
+
"""Should return us-east-1 when LocationConstraint is None."""
|
|
189
|
+
mock_s3_client.get_bucket_location.return_value = {"LocationConstraint": None}
|
|
190
|
+
|
|
191
|
+
location = s3_collector._get_bucket_location(mock_s3_client, "test-bucket")
|
|
192
|
+
|
|
193
|
+
assert location == "us-east-1"
|
|
194
|
+
|
|
195
|
+
def test_get_bucket_location_other_region(self, s3_collector, mock_s3_client):
|
|
196
|
+
"""Should return correct region when LocationConstraint is set."""
|
|
197
|
+
mock_s3_client.get_bucket_location.return_value = {"LocationConstraint": "us-west-2"}
|
|
198
|
+
|
|
199
|
+
location = s3_collector._get_bucket_location(mock_s3_client, "test-bucket")
|
|
200
|
+
|
|
201
|
+
assert location == "us-west-2"
|
|
202
|
+
|
|
203
|
+
def test_get_bucket_location_error(self, s3_collector, mock_s3_client):
|
|
204
|
+
"""Should return unknown when ClientError occurs."""
|
|
205
|
+
mock_s3_client.get_bucket_location.side_effect = ClientError(
|
|
206
|
+
{"Error": {"Code": "AccessDenied", "Message": "Access Denied"}}, "get_bucket_location"
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
location = s3_collector._get_bucket_location(mock_s3_client, "test-bucket")
|
|
210
|
+
|
|
211
|
+
assert location == "unknown"
|
|
212
|
+
|
|
213
|
+
# Test 5: Bucket encryption configuration (enabled)
|
|
214
|
+
def test_get_bucket_encryption_aes256(self, s3_collector, mock_s3_client):
|
|
215
|
+
"""Should return encryption configuration with AES256 algorithm."""
|
|
216
|
+
mock_s3_client.get_bucket_encryption.return_value = {
|
|
217
|
+
"ServerSideEncryptionConfiguration": {
|
|
218
|
+
"Rules": [{"ApplyServerSideEncryptionByDefault": {"SSEAlgorithm": "AES256"}}]
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
encryption = s3_collector._get_bucket_encryption(mock_s3_client, "test-bucket")
|
|
223
|
+
|
|
224
|
+
assert encryption["Enabled"] is True
|
|
225
|
+
assert encryption["Algorithm"] == "AES256"
|
|
226
|
+
assert encryption["KMSMasterKeyID"] is None
|
|
227
|
+
|
|
228
|
+
def test_get_bucket_encryption_kms(self, s3_collector, mock_s3_client):
|
|
229
|
+
"""Should return encryption configuration with KMS algorithm."""
|
|
230
|
+
kms_key_id = "arn:aws:kms:us-east-1:123456789012:key/12345678-1234-1234-1234-123456789012"
|
|
231
|
+
mock_s3_client.get_bucket_encryption.return_value = {
|
|
232
|
+
"ServerSideEncryptionConfiguration": {
|
|
233
|
+
"Rules": [
|
|
234
|
+
{
|
|
235
|
+
"ApplyServerSideEncryptionByDefault": {
|
|
236
|
+
"SSEAlgorithm": "aws:kms",
|
|
237
|
+
"KMSMasterKeyID": kms_key_id,
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
]
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
encryption = s3_collector._get_bucket_encryption(mock_s3_client, "test-bucket")
|
|
245
|
+
|
|
246
|
+
assert encryption["Enabled"] is True
|
|
247
|
+
assert encryption["Algorithm"] == "aws:kms"
|
|
248
|
+
assert encryption["KMSMasterKeyID"] == kms_key_id
|
|
249
|
+
|
|
250
|
+
# Test 6: Bucket encryption configuration (disabled)
|
|
251
|
+
def test_get_bucket_encryption_not_found(self, s3_collector, mock_s3_client):
|
|
252
|
+
"""Should return Enabled False when ServerSideEncryptionConfigurationNotFoundError occurs."""
|
|
253
|
+
mock_s3_client.get_bucket_encryption.side_effect = ClientError(
|
|
254
|
+
{"Error": {"Code": "ServerSideEncryptionConfigurationNotFoundError", "Message": "Not found"}},
|
|
255
|
+
"get_bucket_encryption",
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
encryption = s3_collector._get_bucket_encryption(mock_s3_client, "test-bucket")
|
|
259
|
+
|
|
260
|
+
assert encryption["Enabled"] is False
|
|
261
|
+
|
|
262
|
+
def test_get_bucket_encryption_empty_rules(self, s3_collector, mock_s3_client):
|
|
263
|
+
"""Should return Enabled False when encryption rules are empty."""
|
|
264
|
+
mock_s3_client.get_bucket_encryption.return_value = {"ServerSideEncryptionConfiguration": {"Rules": []}}
|
|
265
|
+
|
|
266
|
+
encryption = s3_collector._get_bucket_encryption(mock_s3_client, "test-bucket")
|
|
267
|
+
|
|
268
|
+
assert encryption["Enabled"] is False
|
|
269
|
+
|
|
270
|
+
def test_get_bucket_encryption_other_error(self, s3_collector, mock_s3_client):
|
|
271
|
+
"""Should return empty dict for other ClientError."""
|
|
272
|
+
mock_s3_client.get_bucket_encryption.side_effect = ClientError(
|
|
273
|
+
{"Error": {"Code": "AccessDenied", "Message": "Access Denied"}}, "get_bucket_encryption"
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
encryption = s3_collector._get_bucket_encryption(mock_s3_client, "test-bucket")
|
|
277
|
+
|
|
278
|
+
assert encryption == {}
|
|
279
|
+
|
|
280
|
+
# Test 7: Bucket versioning configuration
|
|
281
|
+
def test_get_bucket_versioning_enabled(self, s3_collector, mock_s3_client):
|
|
282
|
+
"""Should return versioning status as Enabled."""
|
|
283
|
+
mock_s3_client.get_bucket_versioning.return_value = {"Status": "Enabled", "MFADelete": "Enabled"}
|
|
284
|
+
|
|
285
|
+
versioning = s3_collector._get_bucket_versioning(mock_s3_client, "test-bucket")
|
|
286
|
+
|
|
287
|
+
assert versioning["Status"] == "Enabled"
|
|
288
|
+
assert versioning["MFADelete"] == "Enabled"
|
|
289
|
+
|
|
290
|
+
def test_get_bucket_versioning_disabled(self, s3_collector, mock_s3_client):
|
|
291
|
+
"""Should return versioning status as Disabled when not set."""
|
|
292
|
+
mock_s3_client.get_bucket_versioning.return_value = {}
|
|
293
|
+
|
|
294
|
+
versioning = s3_collector._get_bucket_versioning(mock_s3_client, "test-bucket")
|
|
295
|
+
|
|
296
|
+
assert versioning["Status"] == "Disabled"
|
|
297
|
+
assert versioning["MFADelete"] == "Disabled"
|
|
298
|
+
|
|
299
|
+
def test_get_bucket_versioning_error(self, s3_collector, mock_s3_client):
|
|
300
|
+
"""Should return empty dict when ClientError occurs."""
|
|
301
|
+
mock_s3_client.get_bucket_versioning.side_effect = ClientError(
|
|
302
|
+
{"Error": {"Code": "AccessDenied", "Message": "Access Denied"}}, "get_bucket_versioning"
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
versioning = s3_collector._get_bucket_versioning(mock_s3_client, "test-bucket")
|
|
306
|
+
|
|
307
|
+
assert versioning == {}
|
|
308
|
+
|
|
309
|
+
# Test 8: Public access block configuration (enabled)
|
|
310
|
+
def test_get_public_access_block_enabled(self, s3_collector, mock_s3_client):
|
|
311
|
+
"""Should return public access block configuration when enabled."""
|
|
312
|
+
mock_s3_client.get_public_access_block.return_value = {
|
|
313
|
+
"PublicAccessBlockConfiguration": {
|
|
314
|
+
"BlockPublicAcls": True,
|
|
315
|
+
"IgnorePublicAcls": True,
|
|
316
|
+
"BlockPublicPolicy": True,
|
|
317
|
+
"RestrictPublicBuckets": True,
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
public_access_block = s3_collector._get_public_access_block(mock_s3_client, "test-bucket")
|
|
322
|
+
|
|
323
|
+
assert public_access_block["BlockPublicAcls"] is True
|
|
324
|
+
assert public_access_block["IgnorePublicAcls"] is True
|
|
325
|
+
assert public_access_block["BlockPublicPolicy"] is True
|
|
326
|
+
assert public_access_block["RestrictPublicBuckets"] is True
|
|
327
|
+
|
|
328
|
+
# Test 9: Public access block configuration (disabled)
|
|
329
|
+
def test_get_public_access_block_disabled(self, s3_collector, mock_s3_client):
|
|
330
|
+
"""Should return all False when NoSuchPublicAccessBlockConfiguration occurs."""
|
|
331
|
+
mock_s3_client.get_public_access_block.side_effect = ClientError(
|
|
332
|
+
{"Error": {"Code": "NoSuchPublicAccessBlockConfiguration", "Message": "Not found"}},
|
|
333
|
+
"get_public_access_block",
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
public_access_block = s3_collector._get_public_access_block(mock_s3_client, "test-bucket")
|
|
337
|
+
|
|
338
|
+
assert public_access_block["BlockPublicAcls"] is False
|
|
339
|
+
assert public_access_block["IgnorePublicAcls"] is False
|
|
340
|
+
assert public_access_block["BlockPublicPolicy"] is False
|
|
341
|
+
assert public_access_block["RestrictPublicBuckets"] is False
|
|
342
|
+
|
|
343
|
+
def test_get_public_access_block_other_error(self, s3_collector, mock_s3_client):
|
|
344
|
+
"""Should return empty dict for other ClientError."""
|
|
345
|
+
mock_s3_client.get_public_access_block.side_effect = ClientError(
|
|
346
|
+
{"Error": {"Code": "AccessDenied", "Message": "Access Denied"}}, "get_public_access_block"
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
public_access_block = s3_collector._get_public_access_block(mock_s3_client, "test-bucket")
|
|
350
|
+
|
|
351
|
+
assert public_access_block == {}
|
|
352
|
+
|
|
353
|
+
# Test 10: Bucket policy status (public and private)
|
|
354
|
+
def test_get_bucket_policy_status_public(self, s3_collector, mock_s3_client):
|
|
355
|
+
"""Should return IsPublic True when bucket policy is public."""
|
|
356
|
+
mock_s3_client.get_bucket_policy_status.return_value = {"PolicyStatus": {"IsPublic": True}}
|
|
357
|
+
|
|
358
|
+
policy_status = s3_collector._get_bucket_policy_status(mock_s3_client, "test-bucket")
|
|
359
|
+
|
|
360
|
+
assert policy_status["IsPublic"] is True
|
|
361
|
+
|
|
362
|
+
def test_get_bucket_policy_status_private(self, s3_collector, mock_s3_client):
|
|
363
|
+
"""Should return IsPublic False when bucket policy is private."""
|
|
364
|
+
mock_s3_client.get_bucket_policy_status.return_value = {"PolicyStatus": {"IsPublic": False}}
|
|
365
|
+
|
|
366
|
+
policy_status = s3_collector._get_bucket_policy_status(mock_s3_client, "test-bucket")
|
|
367
|
+
|
|
368
|
+
assert policy_status["IsPublic"] is False
|
|
369
|
+
|
|
370
|
+
def test_get_bucket_policy_status_no_policy(self, s3_collector, mock_s3_client):
|
|
371
|
+
"""Should return IsPublic False when NoSuchBucketPolicy error occurs."""
|
|
372
|
+
mock_s3_client.get_bucket_policy_status.side_effect = ClientError(
|
|
373
|
+
{"Error": {"Code": "NoSuchBucketPolicy", "Message": "Not found"}}, "get_bucket_policy_status"
|
|
374
|
+
)
|
|
375
|
+
|
|
376
|
+
policy_status = s3_collector._get_bucket_policy_status(mock_s3_client, "test-bucket")
|
|
377
|
+
|
|
378
|
+
assert policy_status["IsPublic"] is False
|
|
379
|
+
|
|
380
|
+
def test_get_bucket_policy_status_other_error(self, s3_collector, mock_s3_client):
|
|
381
|
+
"""Should return empty dict for other ClientError."""
|
|
382
|
+
mock_s3_client.get_bucket_policy_status.side_effect = ClientError(
|
|
383
|
+
{"Error": {"Code": "AccessDenied", "Message": "Access Denied"}}, "get_bucket_policy_status"
|
|
384
|
+
)
|
|
385
|
+
|
|
386
|
+
policy_status = s3_collector._get_bucket_policy_status(mock_s3_client, "test-bucket")
|
|
387
|
+
|
|
388
|
+
assert policy_status == {}
|
|
389
|
+
|
|
390
|
+
# Test 11: Bucket ACL retrieval
|
|
391
|
+
def test_get_bucket_acl_single_grant(self, s3_collector, mock_s3_client):
|
|
392
|
+
"""Should return ACL information with single grant."""
|
|
393
|
+
mock_s3_client.get_bucket_acl.return_value = {
|
|
394
|
+
"Owner": {"DisplayName": "owner1", "ID": "owner-id-1"},
|
|
395
|
+
"Grants": [{"Grantee": {"Type": "CanonicalUser"}, "Permission": "FULL_CONTROL"}],
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
acl = s3_collector._get_bucket_acl(mock_s3_client, "test-bucket")
|
|
399
|
+
|
|
400
|
+
assert acl["Owner"]["DisplayName"] == "owner1"
|
|
401
|
+
assert acl["Owner"]["ID"] == "owner-id-1"
|
|
402
|
+
assert acl["GrantCount"] == 1
|
|
403
|
+
|
|
404
|
+
def test_get_bucket_acl_multiple_grants(self, s3_collector, mock_s3_client):
|
|
405
|
+
"""Should return ACL information with multiple grants."""
|
|
406
|
+
mock_s3_client.get_bucket_acl.return_value = {
|
|
407
|
+
"Owner": {"DisplayName": "owner1", "ID": "owner-id-1"},
|
|
408
|
+
"Grants": [
|
|
409
|
+
{"Grantee": {"Type": "CanonicalUser"}, "Permission": "FULL_CONTROL"},
|
|
410
|
+
{"Grantee": {"Type": "Group"}, "Permission": "READ"},
|
|
411
|
+
{"Grantee": {"Type": "Group"}, "Permission": "WRITE"},
|
|
412
|
+
],
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
acl = s3_collector._get_bucket_acl(mock_s3_client, "test-bucket")
|
|
416
|
+
|
|
417
|
+
assert acl["GrantCount"] == 3
|
|
418
|
+
|
|
419
|
+
def test_get_bucket_acl_error(self, s3_collector, mock_s3_client):
|
|
420
|
+
"""Should return empty dict when ClientError occurs."""
|
|
421
|
+
mock_s3_client.get_bucket_acl.side_effect = ClientError(
|
|
422
|
+
{"Error": {"Code": "AccessDenied", "Message": "Access Denied"}}, "get_bucket_acl"
|
|
423
|
+
)
|
|
424
|
+
|
|
425
|
+
acl = s3_collector._get_bucket_acl(mock_s3_client, "test-bucket")
|
|
426
|
+
|
|
427
|
+
assert acl == {}
|
|
428
|
+
|
|
429
|
+
# Test 12: Bucket tagging (with tags)
|
|
430
|
+
def test_get_bucket_tagging_with_tags(self, s3_collector, mock_s3_client):
|
|
431
|
+
"""Should return bucket tags when tags exist."""
|
|
432
|
+
mock_s3_client.get_bucket_tagging.return_value = {
|
|
433
|
+
"TagSet": [
|
|
434
|
+
{"Key": "Environment", "Value": "Production"},
|
|
435
|
+
{"Key": "Owner", "Value": "TeamA"},
|
|
436
|
+
{"Key": "CostCenter", "Value": "Engineering"},
|
|
437
|
+
]
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
tags = s3_collector._get_bucket_tagging(mock_s3_client, "test-bucket")
|
|
441
|
+
|
|
442
|
+
assert len(tags) == 3
|
|
443
|
+
assert tags[0]["Key"] == "Environment"
|
|
444
|
+
assert tags[0]["Value"] == "Production"
|
|
445
|
+
assert tags[1]["Key"] == "Owner"
|
|
446
|
+
assert tags[1]["Value"] == "TeamA"
|
|
447
|
+
|
|
448
|
+
# Test 13: Bucket tagging (without tags)
|
|
449
|
+
def test_get_bucket_tagging_no_tags(self, s3_collector, mock_s3_client):
|
|
450
|
+
"""Should return empty list when NoSuchTagSet error occurs."""
|
|
451
|
+
mock_s3_client.get_bucket_tagging.side_effect = ClientError(
|
|
452
|
+
{"Error": {"Code": "NoSuchTagSet", "Message": "Not found"}}, "get_bucket_tagging"
|
|
453
|
+
)
|
|
454
|
+
|
|
455
|
+
tags = s3_collector._get_bucket_tagging(mock_s3_client, "test-bucket")
|
|
456
|
+
|
|
457
|
+
assert tags == []
|
|
458
|
+
|
|
459
|
+
def test_get_bucket_tagging_other_error(self, s3_collector, mock_s3_client):
|
|
460
|
+
"""Should return empty list for other ClientError."""
|
|
461
|
+
mock_s3_client.get_bucket_tagging.side_effect = ClientError(
|
|
462
|
+
{"Error": {"Code": "AccessDenied", "Message": "Access Denied"}}, "get_bucket_tagging"
|
|
463
|
+
)
|
|
464
|
+
|
|
465
|
+
tags = s3_collector._get_bucket_tagging(mock_s3_client, "test-bucket")
|
|
466
|
+
|
|
467
|
+
assert tags == []
|
|
468
|
+
|
|
469
|
+
# Test 14: Bucket logging configuration
|
|
470
|
+
def test_get_bucket_logging_enabled(self, s3_collector, mock_s3_client):
|
|
471
|
+
"""Should return logging configuration when logging is enabled."""
|
|
472
|
+
mock_s3_client.get_bucket_logging.return_value = {
|
|
473
|
+
"LoggingEnabled": {
|
|
474
|
+
"TargetBucket": "log-bucket",
|
|
475
|
+
"TargetPrefix": "logs/",
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
logging_config = s3_collector._get_bucket_logging(mock_s3_client, "test-bucket")
|
|
480
|
+
|
|
481
|
+
assert logging_config["Enabled"] is True
|
|
482
|
+
assert logging_config["TargetBucket"] == "log-bucket"
|
|
483
|
+
assert logging_config["TargetPrefix"] == "logs/"
|
|
484
|
+
|
|
485
|
+
def test_get_bucket_logging_disabled(self, s3_collector, mock_s3_client):
|
|
486
|
+
"""Should return Enabled False when logging is not configured."""
|
|
487
|
+
mock_s3_client.get_bucket_logging.return_value = {}
|
|
488
|
+
|
|
489
|
+
logging_config = s3_collector._get_bucket_logging(mock_s3_client, "test-bucket")
|
|
490
|
+
|
|
491
|
+
assert logging_config["Enabled"] is False
|
|
492
|
+
|
|
493
|
+
def test_get_bucket_logging_error(self, s3_collector, mock_s3_client):
|
|
494
|
+
"""Should return empty dict when ClientError occurs."""
|
|
495
|
+
mock_s3_client.get_bucket_logging.side_effect = ClientError(
|
|
496
|
+
{"Error": {"Code": "AccessDenied", "Message": "Access Denied"}}, "get_bucket_logging"
|
|
497
|
+
)
|
|
498
|
+
|
|
499
|
+
logging_config = s3_collector._get_bucket_logging(mock_s3_client, "test-bucket")
|
|
500
|
+
|
|
501
|
+
assert logging_config == {}
|
|
502
|
+
|
|
503
|
+
# Test 15: Region filtering
|
|
504
|
+
def test_region_filtering_excludes_other_regions(self, s3_collector, mock_s3_client):
|
|
505
|
+
"""Should only include buckets in the target region."""
|
|
506
|
+
s3_collector._get_client = MagicMock(return_value=mock_s3_client)
|
|
507
|
+
|
|
508
|
+
mock_s3_client.list_buckets.return_value = {
|
|
509
|
+
"Buckets": [
|
|
510
|
+
{"Name": "bucket-us-east-1", "CreationDate": datetime(2023, 1, 1, 12, 0, 0)},
|
|
511
|
+
{"Name": "bucket-us-west-2", "CreationDate": datetime(2023, 2, 1, 12, 0, 0)},
|
|
512
|
+
{"Name": "bucket-eu-west-1", "CreationDate": datetime(2023, 3, 1, 12, 0, 0)},
|
|
513
|
+
]
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
# First bucket is in us-east-1 (target region)
|
|
517
|
+
# Second bucket is in us-west-2
|
|
518
|
+
# Third bucket is in eu-west-1
|
|
519
|
+
mock_s3_client.get_bucket_location.side_effect = [
|
|
520
|
+
{"LocationConstraint": None}, # us-east-1
|
|
521
|
+
{"LocationConstraint": "us-west-2"},
|
|
522
|
+
{"LocationConstraint": "eu-west-1"},
|
|
523
|
+
]
|
|
524
|
+
|
|
525
|
+
# Mock other required calls for the bucket in target region
|
|
526
|
+
mock_s3_client.get_bucket_encryption.return_value = {"ServerSideEncryptionConfiguration": {"Rules": []}}
|
|
527
|
+
mock_s3_client.get_bucket_versioning.return_value = {"Status": "Disabled"}
|
|
528
|
+
mock_s3_client.get_public_access_block.return_value = {"PublicAccessBlockConfiguration": {}}
|
|
529
|
+
mock_s3_client.get_bucket_policy_status.return_value = {"PolicyStatus": {"IsPublic": False}}
|
|
530
|
+
mock_s3_client.get_bucket_acl.return_value = {"Owner": {}, "Grants": []}
|
|
531
|
+
mock_s3_client.get_bucket_tagging.side_effect = ClientError(
|
|
532
|
+
{"Error": {"Code": "NoSuchTagSet", "Message": "Not found"}}, "get_bucket_tagging"
|
|
533
|
+
)
|
|
534
|
+
mock_s3_client.get_bucket_logging.return_value = {}
|
|
535
|
+
|
|
536
|
+
result = s3_collector.collect()
|
|
537
|
+
|
|
538
|
+
# Only the bucket in us-east-1 should be included
|
|
539
|
+
assert len(result["Buckets"]) == 1
|
|
540
|
+
assert result["Buckets"][0]["Name"] == "bucket-us-east-1"
|
|
541
|
+
assert result["Buckets"][0]["Region"] == "us-east-1"
|
|
542
|
+
|
|
543
|
+
def test_region_filtering_verifies_region_tag(self, s3_collector, mock_s3_client):
|
|
544
|
+
"""Should verify Region tag is set for all buckets in target region."""
|
|
545
|
+
s3_collector._get_client = MagicMock(return_value=mock_s3_client)
|
|
546
|
+
|
|
547
|
+
mock_s3_client.list_buckets.return_value = {
|
|
548
|
+
"Buckets": [
|
|
549
|
+
{"Name": "test-bucket-1", "CreationDate": datetime(2023, 1, 1, 12, 0, 0)},
|
|
550
|
+
{"Name": "test-bucket-2", "CreationDate": datetime(2023, 2, 1, 12, 0, 0)},
|
|
551
|
+
]
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
mock_s3_client.get_bucket_location.side_effect = [
|
|
555
|
+
{"LocationConstraint": None}, # us-east-1
|
|
556
|
+
{"LocationConstraint": None}, # us-east-1
|
|
557
|
+
]
|
|
558
|
+
|
|
559
|
+
# Mock other required calls
|
|
560
|
+
mock_s3_client.get_bucket_encryption.return_value = {"ServerSideEncryptionConfiguration": {"Rules": []}}
|
|
561
|
+
mock_s3_client.get_bucket_versioning.return_value = {"Status": "Disabled"}
|
|
562
|
+
mock_s3_client.get_public_access_block.return_value = {"PublicAccessBlockConfiguration": {}}
|
|
563
|
+
mock_s3_client.get_bucket_policy_status.return_value = {"PolicyStatus": {"IsPublic": False}}
|
|
564
|
+
mock_s3_client.get_bucket_acl.return_value = {"Owner": {}, "Grants": []}
|
|
565
|
+
mock_s3_client.get_bucket_tagging.side_effect = [
|
|
566
|
+
ClientError({"Error": {"Code": "NoSuchTagSet", "Message": "Not found"}}, "get_bucket_tagging"),
|
|
567
|
+
ClientError({"Error": {"Code": "NoSuchTagSet", "Message": "Not found"}}, "get_bucket_tagging"),
|
|
568
|
+
]
|
|
569
|
+
mock_s3_client.get_bucket_logging.return_value = {}
|
|
570
|
+
|
|
571
|
+
result = s3_collector.collect()
|
|
572
|
+
|
|
573
|
+
# Verify all buckets have Region tag
|
|
574
|
+
assert len(result["Buckets"]) == 2
|
|
575
|
+
for bucket in result["Buckets"]:
|
|
576
|
+
assert "Region" in bucket
|
|
577
|
+
assert bucket["Region"] == "us-east-1"
|
|
578
|
+
|
|
579
|
+
# Test 16: Error handling - AccessDenied during list_buckets
|
|
580
|
+
def test_collect_handles_access_denied_list_buckets(self, s3_collector, mock_s3_client):
|
|
581
|
+
"""Should handle AccessDenied error when listing buckets."""
|
|
582
|
+
s3_collector._get_client = MagicMock(return_value=mock_s3_client)
|
|
583
|
+
|
|
584
|
+
mock_s3_client.list_buckets.side_effect = ClientError(
|
|
585
|
+
{"Error": {"Code": "AccessDenied", "Message": "Access Denied"}}, "list_buckets"
|
|
586
|
+
)
|
|
587
|
+
|
|
588
|
+
with patch("regscale.integrations.commercial.aws.inventory.resources.s3.logger") as mock_logger:
|
|
589
|
+
result = s3_collector.collect()
|
|
590
|
+
|
|
591
|
+
assert result["Buckets"] == []
|
|
592
|
+
mock_logger.warning.assert_called_once()
|
|
593
|
+
|
|
594
|
+
# Test 17: Error handling - NoSuchBucket during bucket details retrieval
|
|
595
|
+
def test_list_buckets_handles_nosuchbucket_error(self, s3_collector, mock_s3_client):
|
|
596
|
+
"""Should skip bucket when NoSuchBucket error occurs during details retrieval."""
|
|
597
|
+
s3_collector._get_client = MagicMock(return_value=mock_s3_client)
|
|
598
|
+
|
|
599
|
+
mock_s3_client.list_buckets.return_value = {
|
|
600
|
+
"Buckets": [
|
|
601
|
+
{"Name": "bucket-exists", "CreationDate": datetime(2023, 1, 1, 12, 0, 0)},
|
|
602
|
+
{"Name": "bucket-deleted", "CreationDate": datetime(2023, 2, 1, 12, 0, 0)},
|
|
603
|
+
]
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
# First bucket succeeds, second bucket throws NoSuchBucket
|
|
607
|
+
mock_s3_client.get_bucket_location.side_effect = [
|
|
608
|
+
{"LocationConstraint": None}, # us-east-1
|
|
609
|
+
ClientError({"Error": {"Code": "NoSuchBucket", "Message": "Bucket not found"}}, "get_bucket_location"),
|
|
610
|
+
]
|
|
611
|
+
|
|
612
|
+
# Mock other required calls for the first bucket
|
|
613
|
+
mock_s3_client.get_bucket_encryption.return_value = {"ServerSideEncryptionConfiguration": {"Rules": []}}
|
|
614
|
+
mock_s3_client.get_bucket_versioning.return_value = {"Status": "Disabled"}
|
|
615
|
+
mock_s3_client.get_public_access_block.return_value = {"PublicAccessBlockConfiguration": {}}
|
|
616
|
+
mock_s3_client.get_bucket_policy_status.return_value = {"PolicyStatus": {"IsPublic": False}}
|
|
617
|
+
mock_s3_client.get_bucket_acl.return_value = {"Owner": {}, "Grants": []}
|
|
618
|
+
mock_s3_client.get_bucket_tagging.side_effect = ClientError(
|
|
619
|
+
{"Error": {"Code": "NoSuchTagSet", "Message": "Not found"}}, "get_bucket_tagging"
|
|
620
|
+
)
|
|
621
|
+
mock_s3_client.get_bucket_logging.return_value = {}
|
|
622
|
+
|
|
623
|
+
result = s3_collector.collect()
|
|
624
|
+
|
|
625
|
+
# Only the first bucket should be included
|
|
626
|
+
assert len(result["Buckets"]) == 1
|
|
627
|
+
assert result["Buckets"][0]["Name"] == "bucket-exists"
|
|
628
|
+
|
|
629
|
+
# Test 18: Error handling - AccessDenied during bucket details retrieval
|
|
630
|
+
def test_list_buckets_handles_access_denied_details(self, s3_collector, mock_s3_client):
|
|
631
|
+
"""Should skip bucket when AccessDenied error occurs during details retrieval."""
|
|
632
|
+
s3_collector._get_client = MagicMock(return_value=mock_s3_client)
|
|
633
|
+
|
|
634
|
+
mock_s3_client.list_buckets.return_value = {
|
|
635
|
+
"Buckets": [
|
|
636
|
+
{"Name": "bucket-accessible", "CreationDate": datetime(2023, 1, 1, 12, 0, 0)},
|
|
637
|
+
{"Name": "bucket-restricted", "CreationDate": datetime(2023, 2, 1, 12, 0, 0)},
|
|
638
|
+
]
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
# First bucket succeeds, second bucket throws AccessDenied
|
|
642
|
+
mock_s3_client.get_bucket_location.side_effect = [
|
|
643
|
+
{"LocationConstraint": None}, # us-east-1
|
|
644
|
+
ClientError({"Error": {"Code": "AccessDenied", "Message": "Access Denied"}}, "get_bucket_location"),
|
|
645
|
+
]
|
|
646
|
+
|
|
647
|
+
# Mock other required calls for the first bucket
|
|
648
|
+
mock_s3_client.get_bucket_encryption.return_value = {"ServerSideEncryptionConfiguration": {"Rules": []}}
|
|
649
|
+
mock_s3_client.get_bucket_versioning.return_value = {"Status": "Disabled"}
|
|
650
|
+
mock_s3_client.get_public_access_block.return_value = {"PublicAccessBlockConfiguration": {}}
|
|
651
|
+
mock_s3_client.get_bucket_policy_status.return_value = {"PolicyStatus": {"IsPublic": False}}
|
|
652
|
+
mock_s3_client.get_bucket_acl.return_value = {"Owner": {}, "Grants": []}
|
|
653
|
+
mock_s3_client.get_bucket_tagging.side_effect = ClientError(
|
|
654
|
+
{"Error": {"Code": "NoSuchTagSet", "Message": "Not found"}}, "get_bucket_tagging"
|
|
655
|
+
)
|
|
656
|
+
mock_s3_client.get_bucket_logging.return_value = {}
|
|
657
|
+
|
|
658
|
+
result = s3_collector.collect()
|
|
659
|
+
|
|
660
|
+
# Only the first bucket should be included
|
|
661
|
+
assert len(result["Buckets"]) == 1
|
|
662
|
+
assert result["Buckets"][0]["Name"] == "bucket-accessible"
|
|
663
|
+
|
|
664
|
+
# Test 19: Error handling - Unexpected error during collection
|
|
665
|
+
def test_collect_handles_unexpected_error(self, s3_collector, mock_s3_client):
|
|
666
|
+
"""Should handle unexpected errors during collection."""
|
|
667
|
+
s3_collector._get_client = MagicMock(return_value=mock_s3_client)
|
|
668
|
+
|
|
669
|
+
mock_s3_client.list_buckets.side_effect = Exception("Unexpected error")
|
|
670
|
+
|
|
671
|
+
with patch("regscale.integrations.commercial.aws.inventory.resources.s3.logger") as mock_logger:
|
|
672
|
+
result = s3_collector.collect()
|
|
673
|
+
|
|
674
|
+
assert result["Buckets"] == []
|
|
675
|
+
mock_logger.error.assert_called_once()
|
|
676
|
+
|
|
677
|
+
# Test 20: Collect with ClientError during main operation
|
|
678
|
+
def test_collect_handles_client_error(self, s3_collector, mock_s3_client):
|
|
679
|
+
"""Should handle ClientError during main collection operation."""
|
|
680
|
+
s3_collector._get_client = MagicMock(
|
|
681
|
+
side_effect=ClientError({"Error": {"Code": "UnauthorizedOperation", "Message": "Unauthorized"}}, "client")
|
|
682
|
+
)
|
|
683
|
+
|
|
684
|
+
result = s3_collector.collect()
|
|
685
|
+
|
|
686
|
+
# The error should be handled by _handle_error method
|
|
687
|
+
assert result["Buckets"] == []
|
|
688
|
+
|
|
689
|
+
# Test 21: Error handling for other exceptions during bucket details retrieval
|
|
690
|
+
def test_list_buckets_handles_other_errors(self, s3_collector, mock_s3_client):
|
|
691
|
+
"""Should skip bucket when non-AccessDenied/NoSuchBucket error occurs during details retrieval."""
|
|
692
|
+
s3_collector._get_client = MagicMock(return_value=mock_s3_client)
|
|
693
|
+
|
|
694
|
+
mock_s3_client.list_buckets.return_value = {
|
|
695
|
+
"Buckets": [
|
|
696
|
+
{"Name": "bucket-ok", "CreationDate": datetime(2023, 1, 1, 12, 0, 0)},
|
|
697
|
+
{"Name": "bucket-error", "CreationDate": datetime(2023, 2, 1, 12, 0, 0)},
|
|
698
|
+
]
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
# First bucket succeeds, second bucket throws unexpected error
|
|
702
|
+
mock_s3_client.get_bucket_location.side_effect = [
|
|
703
|
+
{"LocationConstraint": None}, # us-east-1
|
|
704
|
+
ClientError({"Error": {"Code": "InternalError", "Message": "Internal Error"}}, "get_bucket_location"),
|
|
705
|
+
]
|
|
706
|
+
|
|
707
|
+
# Mock other required calls for the first bucket
|
|
708
|
+
mock_s3_client.get_bucket_encryption.return_value = {"ServerSideEncryptionConfiguration": {"Rules": []}}
|
|
709
|
+
mock_s3_client.get_bucket_versioning.return_value = {"Status": "Disabled"}
|
|
710
|
+
mock_s3_client.get_public_access_block.return_value = {"PublicAccessBlockConfiguration": {}}
|
|
711
|
+
mock_s3_client.get_bucket_policy_status.return_value = {"PolicyStatus": {"IsPublic": False}}
|
|
712
|
+
mock_s3_client.get_bucket_acl.return_value = {"Owner": {}, "Grants": []}
|
|
713
|
+
mock_s3_client.get_bucket_tagging.side_effect = ClientError(
|
|
714
|
+
{"Error": {"Code": "NoSuchTagSet", "Message": "Not found"}}, "get_bucket_tagging"
|
|
715
|
+
)
|
|
716
|
+
mock_s3_client.get_bucket_logging.return_value = {}
|
|
717
|
+
|
|
718
|
+
result = s3_collector.collect()
|
|
719
|
+
|
|
720
|
+
# Only the first bucket should be included (second bucket skipped due to error)
|
|
721
|
+
assert len(result["Buckets"]) == 1
|
|
722
|
+
assert result["Buckets"][0]["Name"] == "bucket-ok"
|