complio 0.1.1__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.
- CHANGELOG.md +208 -0
- README.md +343 -0
- complio/__init__.py +48 -0
- complio/cli/__init__.py +0 -0
- complio/cli/banner.py +87 -0
- complio/cli/commands/__init__.py +0 -0
- complio/cli/commands/history.py +439 -0
- complio/cli/commands/scan.py +700 -0
- complio/cli/main.py +115 -0
- complio/cli/output.py +338 -0
- complio/config/__init__.py +17 -0
- complio/config/settings.py +333 -0
- complio/connectors/__init__.py +9 -0
- complio/connectors/aws/__init__.py +0 -0
- complio/connectors/aws/client.py +342 -0
- complio/connectors/base.py +135 -0
- complio/core/__init__.py +10 -0
- complio/core/registry.py +228 -0
- complio/core/runner.py +351 -0
- complio/py.typed +0 -0
- complio/reporters/__init__.py +7 -0
- complio/reporters/generator.py +417 -0
- complio/tests_library/__init__.py +0 -0
- complio/tests_library/base.py +492 -0
- complio/tests_library/identity/__init__.py +0 -0
- complio/tests_library/identity/access_key_rotation.py +302 -0
- complio/tests_library/identity/mfa_enforcement.py +327 -0
- complio/tests_library/identity/root_account_protection.py +470 -0
- complio/tests_library/infrastructure/__init__.py +0 -0
- complio/tests_library/infrastructure/cloudtrail_encryption.py +286 -0
- complio/tests_library/infrastructure/cloudtrail_log_validation.py +274 -0
- complio/tests_library/infrastructure/cloudtrail_logging.py +400 -0
- complio/tests_library/infrastructure/ebs_encryption.py +244 -0
- complio/tests_library/infrastructure/ec2_security_groups.py +321 -0
- complio/tests_library/infrastructure/iam_password_policy.py +460 -0
- complio/tests_library/infrastructure/nacl_security.py +356 -0
- complio/tests_library/infrastructure/rds_encryption.py +252 -0
- complio/tests_library/infrastructure/s3_encryption.py +301 -0
- complio/tests_library/infrastructure/s3_public_access.py +369 -0
- complio/tests_library/infrastructure/secrets_manager_encryption.py +248 -0
- complio/tests_library/infrastructure/vpc_flow_logs.py +287 -0
- complio/tests_library/logging/__init__.py +0 -0
- complio/tests_library/logging/cloudwatch_alarms.py +354 -0
- complio/tests_library/logging/cloudwatch_logs_encryption.py +281 -0
- complio/tests_library/logging/cloudwatch_retention.py +252 -0
- complio/tests_library/logging/config_enabled.py +393 -0
- complio/tests_library/logging/eventbridge_rules.py +460 -0
- complio/tests_library/logging/guardduty_enabled.py +436 -0
- complio/tests_library/logging/security_hub_enabled.py +416 -0
- complio/tests_library/logging/sns_encryption.py +273 -0
- complio/tests_library/network/__init__.py +0 -0
- complio/tests_library/network/alb_nlb_security.py +421 -0
- complio/tests_library/network/api_gateway_security.py +452 -0
- complio/tests_library/network/cloudfront_https.py +332 -0
- complio/tests_library/network/direct_connect_security.py +343 -0
- complio/tests_library/network/nacl_configuration.py +367 -0
- complio/tests_library/network/network_firewall.py +355 -0
- complio/tests_library/network/transit_gateway_security.py +318 -0
- complio/tests_library/network/vpc_endpoints_security.py +339 -0
- complio/tests_library/network/vpn_security.py +333 -0
- complio/tests_library/network/waf_configuration.py +428 -0
- complio/tests_library/security/__init__.py +0 -0
- complio/tests_library/security/kms_key_rotation.py +314 -0
- complio/tests_library/storage/__init__.py +0 -0
- complio/tests_library/storage/backup_encryption.py +288 -0
- complio/tests_library/storage/dynamodb_encryption.py +280 -0
- complio/tests_library/storage/efs_encryption.py +257 -0
- complio/tests_library/storage/elasticache_encryption.py +370 -0
- complio/tests_library/storage/redshift_encryption.py +252 -0
- complio/tests_library/storage/s3_versioning.py +264 -0
- complio/utils/__init__.py +26 -0
- complio/utils/errors.py +179 -0
- complio/utils/exceptions.py +151 -0
- complio/utils/history.py +243 -0
- complio/utils/logger.py +391 -0
- complio-0.1.1.dist-info/METADATA +385 -0
- complio-0.1.1.dist-info/RECORD +79 -0
- complio-0.1.1.dist-info/WHEEL +4 -0
- complio-0.1.1.dist-info/entry_points.txt +3 -0
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
"""
|
|
2
|
+
S3 bucket encryption compliance test.
|
|
3
|
+
|
|
4
|
+
Checks that all S3 buckets have default encryption enabled.
|
|
5
|
+
|
|
6
|
+
ISO 27001 Control: A.10.1.1 - Cryptographic Controls
|
|
7
|
+
Requirement: All data at rest must be encrypted
|
|
8
|
+
|
|
9
|
+
Example:
|
|
10
|
+
>>> from complio.connectors.aws.client import AWSConnector
|
|
11
|
+
>>> from complio.tests_library.infrastructure.s3_encryption import S3EncryptionTest
|
|
12
|
+
>>>
|
|
13
|
+
>>> connector = AWSConnector("production", "us-east-1")
|
|
14
|
+
>>> connector.connect()
|
|
15
|
+
>>>
|
|
16
|
+
>>> test = S3EncryptionTest(connector)
|
|
17
|
+
>>> result = test.run()
|
|
18
|
+
>>> print(f"Passed: {result.passed}, Score: {result.score}")
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from typing import Any, Dict, List
|
|
22
|
+
|
|
23
|
+
from botocore.exceptions import ClientError
|
|
24
|
+
|
|
25
|
+
from complio.connectors.aws.client import AWSConnector
|
|
26
|
+
from complio.tests_library.base import (
|
|
27
|
+
ComplianceTest,
|
|
28
|
+
Severity,
|
|
29
|
+
TestResult,
|
|
30
|
+
TestStatus,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class S3EncryptionTest(ComplianceTest):
|
|
35
|
+
"""Test for S3 bucket encryption compliance.
|
|
36
|
+
|
|
37
|
+
Verifies that all S3 buckets have default encryption enabled.
|
|
38
|
+
|
|
39
|
+
Compliance Requirements:
|
|
40
|
+
- All S3 buckets must have default encryption configured
|
|
41
|
+
- Encryption can be SSE-S3, SSE-KMS, or SSE-C
|
|
42
|
+
- Buckets without encryption are non-compliant
|
|
43
|
+
|
|
44
|
+
Scoring:
|
|
45
|
+
- 100% if all buckets are encrypted
|
|
46
|
+
- Proportional score based on encrypted/total ratio
|
|
47
|
+
- 0% if no buckets are encrypted
|
|
48
|
+
|
|
49
|
+
Example:
|
|
50
|
+
>>> test = S3EncryptionTest(connector)
|
|
51
|
+
>>> result = test.execute()
|
|
52
|
+
>>> for finding in result.findings:
|
|
53
|
+
... print(f"{finding.resource_id}: {finding.title}")
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
def __init__(self, connector: AWSConnector) -> None:
|
|
57
|
+
"""Initialize S3 encryption test.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
connector: AWS connector instance
|
|
61
|
+
"""
|
|
62
|
+
super().__init__(
|
|
63
|
+
test_id="s3_encryption",
|
|
64
|
+
test_name="S3 Bucket Encryption Check",
|
|
65
|
+
description="Verify all S3 buckets have default encryption enabled (scans all regions)",
|
|
66
|
+
control_id="A.10.1.1",
|
|
67
|
+
connector=connector,
|
|
68
|
+
scope="global",
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
def execute(self) -> TestResult:
|
|
72
|
+
"""Execute S3 encryption compliance test.
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
TestResult with findings for non-encrypted buckets
|
|
76
|
+
|
|
77
|
+
Example:
|
|
78
|
+
>>> test = S3EncryptionTest(connector)
|
|
79
|
+
>>> result = test.execute()
|
|
80
|
+
>>> print(result.score)
|
|
81
|
+
85.5
|
|
82
|
+
"""
|
|
83
|
+
result = TestResult(
|
|
84
|
+
test_id=self.test_id,
|
|
85
|
+
test_name=self.test_name,
|
|
86
|
+
status=TestStatus.PASSED,
|
|
87
|
+
passed=True,
|
|
88
|
+
score=100.0,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
try:
|
|
92
|
+
# Get S3 client
|
|
93
|
+
s3_client = self.connector.get_client("s3")
|
|
94
|
+
|
|
95
|
+
# List all buckets
|
|
96
|
+
self.logger.info("listing_s3_buckets")
|
|
97
|
+
response = s3_client.list_buckets()
|
|
98
|
+
buckets = response.get("Buckets", [])
|
|
99
|
+
|
|
100
|
+
if not buckets:
|
|
101
|
+
self.logger.info("no_s3_buckets_found")
|
|
102
|
+
result.metadata["message"] = "No S3 buckets found in account"
|
|
103
|
+
return result
|
|
104
|
+
|
|
105
|
+
self.logger.info("s3_buckets_found", count=len(buckets))
|
|
106
|
+
|
|
107
|
+
# Check each bucket for encryption
|
|
108
|
+
encrypted_count = 0
|
|
109
|
+
total_count = len(buckets)
|
|
110
|
+
|
|
111
|
+
for bucket in buckets:
|
|
112
|
+
bucket_name = bucket["Name"]
|
|
113
|
+
result.resources_scanned += 1
|
|
114
|
+
|
|
115
|
+
# Check encryption status
|
|
116
|
+
encryption_status = self._check_bucket_encryption(s3_client, bucket_name)
|
|
117
|
+
|
|
118
|
+
# Create evidence
|
|
119
|
+
evidence = self.create_evidence(
|
|
120
|
+
resource_id=bucket_name,
|
|
121
|
+
resource_type="s3_bucket",
|
|
122
|
+
data={
|
|
123
|
+
"bucket_name": bucket_name,
|
|
124
|
+
"encryption": encryption_status,
|
|
125
|
+
"creation_date": bucket["CreationDate"].isoformat(),
|
|
126
|
+
}
|
|
127
|
+
)
|
|
128
|
+
result.add_evidence(evidence)
|
|
129
|
+
|
|
130
|
+
if encryption_status["enabled"]:
|
|
131
|
+
encrypted_count += 1
|
|
132
|
+
self.logger.debug(
|
|
133
|
+
"bucket_encrypted",
|
|
134
|
+
bucket=bucket_name,
|
|
135
|
+
algorithm=encryption_status.get("algorithm")
|
|
136
|
+
)
|
|
137
|
+
else:
|
|
138
|
+
# Create finding for non-encrypted bucket
|
|
139
|
+
finding = self.create_finding(
|
|
140
|
+
resource_id=bucket_name,
|
|
141
|
+
resource_type="s3_bucket",
|
|
142
|
+
severity=Severity.HIGH,
|
|
143
|
+
title="S3 bucket encryption not enabled",
|
|
144
|
+
description=f"Bucket '{bucket_name}' does not have default encryption enabled. "
|
|
145
|
+
"This violates ISO 27001 A.10.1.1 requirement for data-at-rest encryption.",
|
|
146
|
+
remediation=(
|
|
147
|
+
"Enable default encryption for the bucket:\n"
|
|
148
|
+
"1. Go to AWS Console → S3 → Select bucket\n"
|
|
149
|
+
"2. Go to Properties → Default encryption\n"
|
|
150
|
+
"3. Enable either SSE-S3 or SSE-KMS encryption\n"
|
|
151
|
+
"Or use AWS CLI:\n"
|
|
152
|
+
f"aws s3api put-bucket-encryption --bucket {bucket_name} "
|
|
153
|
+
"--server-side-encryption-configuration '{\"Rules\":[{\"ApplyServerSideEncryptionByDefault\":{\"SSEAlgorithm\":\"AES256\"}}]}'"
|
|
154
|
+
),
|
|
155
|
+
evidence=evidence
|
|
156
|
+
)
|
|
157
|
+
result.add_finding(finding)
|
|
158
|
+
|
|
159
|
+
self.logger.warning(
|
|
160
|
+
"bucket_not_encrypted",
|
|
161
|
+
bucket=bucket_name
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
# Calculate compliance score
|
|
165
|
+
if total_count > 0:
|
|
166
|
+
result.score = (encrypted_count / total_count) * 100
|
|
167
|
+
|
|
168
|
+
# Determine pass/fail
|
|
169
|
+
result.passed = encrypted_count == total_count
|
|
170
|
+
result.status = TestStatus.PASSED if result.passed else TestStatus.FAILED
|
|
171
|
+
|
|
172
|
+
# Add metadata
|
|
173
|
+
result.metadata = {
|
|
174
|
+
"total_buckets": total_count,
|
|
175
|
+
"encrypted_buckets": encrypted_count,
|
|
176
|
+
"non_encrypted_buckets": total_count - encrypted_count,
|
|
177
|
+
"compliance_percentage": result.score,
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
self.logger.info(
|
|
181
|
+
"s3_encryption_test_completed",
|
|
182
|
+
total=total_count,
|
|
183
|
+
encrypted=encrypted_count,
|
|
184
|
+
score=result.score,
|
|
185
|
+
passed=result.passed
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
except Exception as e:
|
|
189
|
+
self.logger.error("s3_encryption_test_error", error=str(e))
|
|
190
|
+
result.status = TestStatus.ERROR
|
|
191
|
+
result.passed = False
|
|
192
|
+
result.score = 0.0
|
|
193
|
+
result.error_message = str(e)
|
|
194
|
+
|
|
195
|
+
return result
|
|
196
|
+
|
|
197
|
+
def _check_bucket_encryption(self, s3_client: Any, bucket_name: str) -> Dict[str, Any]:
|
|
198
|
+
"""Check if bucket has encryption enabled.
|
|
199
|
+
|
|
200
|
+
Args:
|
|
201
|
+
s3_client: Boto3 S3 client
|
|
202
|
+
bucket_name: Name of the bucket to check
|
|
203
|
+
|
|
204
|
+
Returns:
|
|
205
|
+
Dictionary with encryption status:
|
|
206
|
+
{
|
|
207
|
+
"enabled": True/False,
|
|
208
|
+
"algorithm": "AES256" | "aws:kms",
|
|
209
|
+
"kms_key_id": "..." (if SSE-KMS)
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
Example:
|
|
213
|
+
>>> status = self._check_bucket_encryption(s3_client, "my-bucket")
|
|
214
|
+
>>> print(status["enabled"])
|
|
215
|
+
True
|
|
216
|
+
"""
|
|
217
|
+
try:
|
|
218
|
+
response = s3_client.get_bucket_encryption(Bucket=bucket_name)
|
|
219
|
+
|
|
220
|
+
# Extract encryption configuration
|
|
221
|
+
rules = response.get("ServerSideEncryptionConfiguration", {}).get("Rules", [])
|
|
222
|
+
|
|
223
|
+
if not rules:
|
|
224
|
+
return {"enabled": False}
|
|
225
|
+
|
|
226
|
+
# Get first rule (usually only one)
|
|
227
|
+
rule = rules[0]
|
|
228
|
+
sse_default = rule.get("ApplyServerSideEncryptionByDefault", {})
|
|
229
|
+
|
|
230
|
+
encryption_status = {
|
|
231
|
+
"enabled": True,
|
|
232
|
+
"algorithm": sse_default.get("SSEAlgorithm"),
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
# Add KMS key ID if using KMS
|
|
236
|
+
if sse_default.get("SSEAlgorithm") == "aws:kms":
|
|
237
|
+
encryption_status["kms_key_id"] = sse_default.get("KMSMasterKeyID")
|
|
238
|
+
|
|
239
|
+
return encryption_status
|
|
240
|
+
|
|
241
|
+
except ClientError as e:
|
|
242
|
+
error_code = e.response.get("Error", {}).get("Code")
|
|
243
|
+
|
|
244
|
+
if error_code == "ServerSideEncryptionConfigurationNotFoundError":
|
|
245
|
+
# No encryption configured
|
|
246
|
+
return {"enabled": False}
|
|
247
|
+
elif error_code == "NoSuchBucket":
|
|
248
|
+
# Bucket doesn't exist (race condition)
|
|
249
|
+
self.logger.warning("bucket_not_found", bucket=bucket_name)
|
|
250
|
+
return {"enabled": False, "error": "Bucket not found"}
|
|
251
|
+
elif error_code == "AccessDenied":
|
|
252
|
+
# No permission to check encryption
|
|
253
|
+
self.logger.warning("encryption_check_access_denied", bucket=bucket_name)
|
|
254
|
+
return {
|
|
255
|
+
"enabled": False,
|
|
256
|
+
"error": "Access denied - insufficient permissions"
|
|
257
|
+
}
|
|
258
|
+
else:
|
|
259
|
+
# Other error
|
|
260
|
+
self.logger.error(
|
|
261
|
+
"encryption_check_error",
|
|
262
|
+
bucket=bucket_name,
|
|
263
|
+
error_code=error_code,
|
|
264
|
+
error=str(e)
|
|
265
|
+
)
|
|
266
|
+
return {"enabled": False, "error": str(e)}
|
|
267
|
+
|
|
268
|
+
except Exception as e:
|
|
269
|
+
self.logger.error(
|
|
270
|
+
"encryption_check_unexpected_error",
|
|
271
|
+
bucket=bucket_name,
|
|
272
|
+
error=str(e)
|
|
273
|
+
)
|
|
274
|
+
return {"enabled": False, "error": str(e)}
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
# ============================================================================
|
|
278
|
+
# CONVENIENCE FUNCTION
|
|
279
|
+
# ============================================================================
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def run_s3_encryption_test(connector: AWSConnector) -> TestResult:
|
|
283
|
+
"""Run S3 encryption compliance test.
|
|
284
|
+
|
|
285
|
+
Convenience function for running the test.
|
|
286
|
+
|
|
287
|
+
Args:
|
|
288
|
+
connector: AWS connector
|
|
289
|
+
|
|
290
|
+
Returns:
|
|
291
|
+
TestResult
|
|
292
|
+
|
|
293
|
+
Example:
|
|
294
|
+
>>> from complio.connectors.aws.client import AWSConnector
|
|
295
|
+
>>> connector = AWSConnector("production", "us-east-1")
|
|
296
|
+
>>> connector.connect()
|
|
297
|
+
>>> result = run_s3_encryption_test(connector)
|
|
298
|
+
>>> print(f"Score: {result.score}%")
|
|
299
|
+
"""
|
|
300
|
+
test = S3EncryptionTest(connector)
|
|
301
|
+
return test.run()
|
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
"""
|
|
2
|
+
S3 public access block compliance test.
|
|
3
|
+
|
|
4
|
+
Checks that S3 public access block is enabled at both account and bucket levels.
|
|
5
|
+
|
|
6
|
+
ISO 27001 Control: A.8.11 - Secure configuration
|
|
7
|
+
Requirement: Data must be protected from unauthorized public access
|
|
8
|
+
|
|
9
|
+
Example:
|
|
10
|
+
>>> from complio.connectors.aws.client import AWSConnector
|
|
11
|
+
>>> from complio.tests_library.infrastructure.s3_public_access import S3PublicAccessBlockTest
|
|
12
|
+
>>>
|
|
13
|
+
>>> connector = AWSConnector("production", "us-east-1")
|
|
14
|
+
>>> connector.connect()
|
|
15
|
+
>>>
|
|
16
|
+
>>> test = S3PublicAccessBlockTest(connector)
|
|
17
|
+
>>> result = test.run()
|
|
18
|
+
>>> print(f"Passed: {result.passed}, Score: {result.score}")
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from typing import Any, Dict
|
|
22
|
+
|
|
23
|
+
from botocore.exceptions import ClientError
|
|
24
|
+
|
|
25
|
+
from complio.connectors.aws.client import AWSConnector
|
|
26
|
+
from complio.tests_library.base import (
|
|
27
|
+
ComplianceTest,
|
|
28
|
+
Severity,
|
|
29
|
+
TestResult,
|
|
30
|
+
TestStatus,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class S3PublicAccessBlockTest(ComplianceTest):
|
|
35
|
+
"""Test for S3 public access block compliance.
|
|
36
|
+
|
|
37
|
+
Verifies that S3 public access block is enabled at both account-level
|
|
38
|
+
and individual bucket levels to prevent unintended public data exposure.
|
|
39
|
+
|
|
40
|
+
Compliance Requirements:
|
|
41
|
+
- Account-level public access block should be enabled
|
|
42
|
+
- All four settings should be enabled (Block Public ACLs, Ignore Public ACLs,
|
|
43
|
+
Block Public Policy, Restrict Public Buckets)
|
|
44
|
+
- Buckets without all four settings enabled are non-compliant
|
|
45
|
+
|
|
46
|
+
Scoring:
|
|
47
|
+
- Account-level check contributes 30% of score
|
|
48
|
+
- Bucket-level checks contribute 70% of score
|
|
49
|
+
- All four settings must be enabled for full compliance
|
|
50
|
+
|
|
51
|
+
Example:
|
|
52
|
+
>>> test = S3PublicAccessBlockTest(connector)
|
|
53
|
+
>>> result = test.execute()
|
|
54
|
+
>>> for finding in result.findings:
|
|
55
|
+
... print(f"{finding.resource_id}: {finding.title}")
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
def __init__(self, connector: AWSConnector) -> None:
|
|
59
|
+
"""Initialize S3 public access block test.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
connector: AWS connector instance
|
|
63
|
+
"""
|
|
64
|
+
super().__init__(
|
|
65
|
+
test_id="s3_public_access_block",
|
|
66
|
+
test_name="S3 Public Access Block Check",
|
|
67
|
+
description="Verify S3 public access block is enabled at account and bucket levels",
|
|
68
|
+
control_id="A.8.11",
|
|
69
|
+
connector=connector,
|
|
70
|
+
scope="global",
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
def execute(self) -> TestResult:
|
|
74
|
+
"""Execute S3 public access block compliance test.
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
TestResult with findings for misconfigured public access blocks
|
|
78
|
+
|
|
79
|
+
Example:
|
|
80
|
+
>>> test = S3PublicAccessBlockTest(connector)
|
|
81
|
+
>>> result = test.execute()
|
|
82
|
+
>>> print(result.score)
|
|
83
|
+
95.0
|
|
84
|
+
"""
|
|
85
|
+
result = TestResult(
|
|
86
|
+
test_id=self.test_id,
|
|
87
|
+
test_name=self.test_name,
|
|
88
|
+
status=TestStatus.PASSED,
|
|
89
|
+
passed=True,
|
|
90
|
+
score=100.0,
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
try:
|
|
94
|
+
# Get S3 and S3Control clients
|
|
95
|
+
s3_client = self.connector.get_client("s3")
|
|
96
|
+
s3control_client = self.connector.get_client("s3control")
|
|
97
|
+
|
|
98
|
+
# Get account ID for S3 Control API
|
|
99
|
+
sts_client = self.connector.get_client("sts")
|
|
100
|
+
account_id = sts_client.get_caller_identity()["Account"]
|
|
101
|
+
|
|
102
|
+
# Check account-level public access block
|
|
103
|
+
account_score = 0.0
|
|
104
|
+
try:
|
|
105
|
+
self.logger.info("checking_account_level_public_access_block")
|
|
106
|
+
response = s3control_client.get_public_access_block(
|
|
107
|
+
AccountId=account_id
|
|
108
|
+
)
|
|
109
|
+
pab_config = response.get("PublicAccessBlockConfiguration", {})
|
|
110
|
+
|
|
111
|
+
account_compliant = all([
|
|
112
|
+
pab_config.get("BlockPublicAcls", False),
|
|
113
|
+
pab_config.get("IgnorePublicAcls", False),
|
|
114
|
+
pab_config.get("BlockPublicPolicy", False),
|
|
115
|
+
pab_config.get("RestrictPublicBuckets", False),
|
|
116
|
+
])
|
|
117
|
+
|
|
118
|
+
if account_compliant:
|
|
119
|
+
account_score = 30.0
|
|
120
|
+
self.logger.info("account_level_public_access_block_enabled")
|
|
121
|
+
else:
|
|
122
|
+
# Create finding for account-level misconfiguration
|
|
123
|
+
finding = self.create_finding(
|
|
124
|
+
resource_id=f"account-{account_id}",
|
|
125
|
+
resource_type="s3_account",
|
|
126
|
+
severity=Severity.HIGH,
|
|
127
|
+
title="Account-level S3 public access block not fully enabled",
|
|
128
|
+
description=f"Account-level S3 public access block settings are not fully enabled. "
|
|
129
|
+
f"Current settings: BlockPublicAcls={pab_config.get('BlockPublicAcls', False)}, "
|
|
130
|
+
f"IgnorePublicAcls={pab_config.get('IgnorePublicAcls', False)}, "
|
|
131
|
+
f"BlockPublicPolicy={pab_config.get('BlockPublicPolicy', False)}, "
|
|
132
|
+
f"RestrictPublicBuckets={pab_config.get('RestrictPublicBuckets', False)}. "
|
|
133
|
+
"This violates ISO 27001 A.8.11 requirement for secure configuration.",
|
|
134
|
+
remediation=(
|
|
135
|
+
"Enable all S3 public access block settings at account level:\n"
|
|
136
|
+
f"aws s3control put-public-access-block --account-id {account_id} \\\n"
|
|
137
|
+
" --public-access-block-configuration \\\n"
|
|
138
|
+
" 'BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true'\n\n"
|
|
139
|
+
"Or use AWS Console:\n"
|
|
140
|
+
"1. Go to S3 → Block Public Access settings for this account\n"
|
|
141
|
+
"2. Click Edit\n"
|
|
142
|
+
"3. Enable all four settings\n"
|
|
143
|
+
"4. Click Save changes"
|
|
144
|
+
),
|
|
145
|
+
evidence=self.create_evidence(
|
|
146
|
+
resource_id=f"account-{account_id}",
|
|
147
|
+
resource_type="s3_account",
|
|
148
|
+
data={"public_access_block_configuration": pab_config}
|
|
149
|
+
)
|
|
150
|
+
)
|
|
151
|
+
result.add_finding(finding)
|
|
152
|
+
|
|
153
|
+
except ClientError as e:
|
|
154
|
+
error_code = e.response.get("Error", {}).get("Code")
|
|
155
|
+
if error_code == "NoSuchPublicAccessBlockConfiguration":
|
|
156
|
+
self.logger.warning("account_level_public_access_block_not_configured")
|
|
157
|
+
# Create finding for missing account-level configuration
|
|
158
|
+
finding = self.create_finding(
|
|
159
|
+
resource_id=f"account-{account_id}",
|
|
160
|
+
resource_type="s3_account",
|
|
161
|
+
severity=Severity.HIGH,
|
|
162
|
+
title="Account-level S3 public access block not configured",
|
|
163
|
+
description="Account-level S3 public access block is not configured. "
|
|
164
|
+
"This leaves all buckets vulnerable to accidental public exposure.",
|
|
165
|
+
remediation=(
|
|
166
|
+
"Enable S3 public access block at account level:\n"
|
|
167
|
+
f"aws s3control put-public-access-block --account-id {account_id} \\\n"
|
|
168
|
+
" --public-access-block-configuration \\\n"
|
|
169
|
+
" 'BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true'"
|
|
170
|
+
),
|
|
171
|
+
evidence=self.create_evidence(
|
|
172
|
+
resource_id=f"account-{account_id}",
|
|
173
|
+
resource_type="s3_account",
|
|
174
|
+
data={"error": "NoSuchPublicAccessBlockConfiguration"}
|
|
175
|
+
)
|
|
176
|
+
)
|
|
177
|
+
result.add_finding(finding)
|
|
178
|
+
else:
|
|
179
|
+
raise
|
|
180
|
+
|
|
181
|
+
# List all buckets
|
|
182
|
+
self.logger.info("listing_s3_buckets")
|
|
183
|
+
buckets_response = s3_client.list_buckets()
|
|
184
|
+
buckets = buckets_response.get("Buckets", [])
|
|
185
|
+
|
|
186
|
+
if not buckets:
|
|
187
|
+
# No buckets, only account-level score applies
|
|
188
|
+
result.score = account_score / 0.3 if account_score > 0 else 0.0
|
|
189
|
+
result.metadata["message"] = "No S3 buckets found, only account-level check performed"
|
|
190
|
+
return result
|
|
191
|
+
|
|
192
|
+
self.logger.info("s3_buckets_found", count=len(buckets))
|
|
193
|
+
|
|
194
|
+
# Check each bucket for public access block
|
|
195
|
+
compliant_buckets = 0
|
|
196
|
+
total_buckets = len(buckets)
|
|
197
|
+
|
|
198
|
+
for bucket in buckets:
|
|
199
|
+
bucket_name = bucket["Name"]
|
|
200
|
+
result.resources_scanned += 1
|
|
201
|
+
|
|
202
|
+
# Check public access block for bucket
|
|
203
|
+
bucket_compliant = self._check_bucket_public_access_block(s3_client, bucket_name, result)
|
|
204
|
+
|
|
205
|
+
if bucket_compliant:
|
|
206
|
+
compliant_buckets += 1
|
|
207
|
+
|
|
208
|
+
# Calculate total score (30% account + 70% buckets)
|
|
209
|
+
bucket_score = (compliant_buckets / total_buckets) * 70.0 if total_buckets > 0 else 0.0
|
|
210
|
+
result.score = account_score + bucket_score
|
|
211
|
+
|
|
212
|
+
# Determine pass/fail (requires 100% compliance)
|
|
213
|
+
result.passed = (result.score >= 99.9) # Allow for floating point imprecision
|
|
214
|
+
result.status = TestStatus.PASSED if result.passed else TestStatus.FAILED
|
|
215
|
+
|
|
216
|
+
# Add metadata
|
|
217
|
+
result.metadata = {
|
|
218
|
+
"account_level_compliant": account_score > 0,
|
|
219
|
+
"total_buckets": total_buckets,
|
|
220
|
+
"compliant_buckets": compliant_buckets,
|
|
221
|
+
"non_compliant_buckets": total_buckets - compliant_buckets,
|
|
222
|
+
"compliance_percentage": result.score,
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
self.logger.info(
|
|
226
|
+
"s3_public_access_test_completed",
|
|
227
|
+
total_buckets=total_buckets,
|
|
228
|
+
compliant_buckets=compliant_buckets,
|
|
229
|
+
score=result.score,
|
|
230
|
+
passed=result.passed
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
except ClientError as e:
|
|
234
|
+
error_code = e.response.get("Error", {}).get("Code")
|
|
235
|
+
self.logger.error("s3_public_access_test_error", error_code=error_code, error=str(e))
|
|
236
|
+
result.status = TestStatus.ERROR
|
|
237
|
+
result.passed = False
|
|
238
|
+
result.score = 0.0
|
|
239
|
+
result.error_message = f"AWS API Error: {error_code} - {str(e)}"
|
|
240
|
+
|
|
241
|
+
except Exception as e:
|
|
242
|
+
self.logger.error("s3_public_access_test_error", error=str(e))
|
|
243
|
+
result.status = TestStatus.ERROR
|
|
244
|
+
result.passed = False
|
|
245
|
+
result.score = 0.0
|
|
246
|
+
result.error_message = str(e)
|
|
247
|
+
|
|
248
|
+
return result
|
|
249
|
+
|
|
250
|
+
def _check_bucket_public_access_block(self, s3_client: Any, bucket_name: str, result: TestResult) -> bool:
|
|
251
|
+
"""Check if bucket has public access block fully enabled.
|
|
252
|
+
|
|
253
|
+
Args:
|
|
254
|
+
s3_client: Boto3 S3 client
|
|
255
|
+
bucket_name: Name of the bucket
|
|
256
|
+
result: TestResult object to add findings to
|
|
257
|
+
|
|
258
|
+
Returns:
|
|
259
|
+
True if bucket is compliant, False otherwise
|
|
260
|
+
"""
|
|
261
|
+
try:
|
|
262
|
+
response = s3_client.get_public_access_block(Bucket=bucket_name)
|
|
263
|
+
pab_config = response.get("PublicAccessBlockConfiguration", {})
|
|
264
|
+
|
|
265
|
+
compliant = all([
|
|
266
|
+
pab_config.get("BlockPublicAcls", False),
|
|
267
|
+
pab_config.get("IgnorePublicAcls", False),
|
|
268
|
+
pab_config.get("BlockPublicPolicy", False),
|
|
269
|
+
pab_config.get("RestrictPublicBuckets", False),
|
|
270
|
+
])
|
|
271
|
+
|
|
272
|
+
# Create evidence
|
|
273
|
+
evidence = self.create_evidence(
|
|
274
|
+
resource_id=bucket_name,
|
|
275
|
+
resource_type="s3_bucket",
|
|
276
|
+
data={
|
|
277
|
+
"bucket_name": bucket_name,
|
|
278
|
+
"public_access_block_configuration": pab_config,
|
|
279
|
+
"compliant": compliant,
|
|
280
|
+
}
|
|
281
|
+
)
|
|
282
|
+
result.add_evidence(evidence)
|
|
283
|
+
|
|
284
|
+
if compliant:
|
|
285
|
+
self.logger.debug("bucket_public_access_block_enabled", bucket=bucket_name)
|
|
286
|
+
return True
|
|
287
|
+
else:
|
|
288
|
+
# Create finding
|
|
289
|
+
finding = self.create_finding(
|
|
290
|
+
resource_id=bucket_name,
|
|
291
|
+
resource_type="s3_bucket",
|
|
292
|
+
severity=Severity.HIGH,
|
|
293
|
+
title="S3 bucket public access block not fully enabled",
|
|
294
|
+
description=f"Bucket '{bucket_name}' does not have all public access block settings enabled. "
|
|
295
|
+
f"Current settings: BlockPublicAcls={pab_config.get('BlockPublicAcls', False)}, "
|
|
296
|
+
f"IgnorePublicAcls={pab_config.get('IgnorePublicAcls', False)}, "
|
|
297
|
+
f"BlockPublicPolicy={pab_config.get('BlockPublicPolicy', False)}, "
|
|
298
|
+
f"RestrictPublicBuckets={pab_config.get('RestrictPublicBuckets', False)}.",
|
|
299
|
+
remediation=(
|
|
300
|
+
f"Enable all public access block settings for bucket '{bucket_name}':\n"
|
|
301
|
+
f"aws s3api put-public-access-block --bucket {bucket_name} \\\n"
|
|
302
|
+
" --public-access-block-configuration \\\n"
|
|
303
|
+
" 'BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true'"
|
|
304
|
+
),
|
|
305
|
+
evidence=evidence
|
|
306
|
+
)
|
|
307
|
+
result.add_finding(finding)
|
|
308
|
+
self.logger.warning("bucket_public_access_block_incomplete", bucket=bucket_name)
|
|
309
|
+
return False
|
|
310
|
+
|
|
311
|
+
except ClientError as e:
|
|
312
|
+
error_code = e.response.get("Error", {}).get("Code")
|
|
313
|
+
|
|
314
|
+
if error_code == "NoSuchPublicAccessBlockConfiguration":
|
|
315
|
+
# No public access block configured for bucket
|
|
316
|
+
finding = self.create_finding(
|
|
317
|
+
resource_id=bucket_name,
|
|
318
|
+
resource_type="s3_bucket",
|
|
319
|
+
severity=Severity.HIGH,
|
|
320
|
+
title="S3 bucket public access block not configured",
|
|
321
|
+
description=f"Bucket '{bucket_name}' does not have public access block configured.",
|
|
322
|
+
remediation=(
|
|
323
|
+
f"Enable public access block for bucket '{bucket_name}':\n"
|
|
324
|
+
f"aws s3api put-public-access-block --bucket {bucket_name} \\\n"
|
|
325
|
+
" --public-access-block-configuration \\\n"
|
|
326
|
+
" 'BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true'"
|
|
327
|
+
),
|
|
328
|
+
evidence=self.create_evidence(
|
|
329
|
+
resource_id=bucket_name,
|
|
330
|
+
resource_type="s3_bucket",
|
|
331
|
+
data={"error": "NoSuchPublicAccessBlockConfiguration"}
|
|
332
|
+
)
|
|
333
|
+
)
|
|
334
|
+
result.add_finding(finding)
|
|
335
|
+
return False
|
|
336
|
+
else:
|
|
337
|
+
self.logger.error(
|
|
338
|
+
"bucket_public_access_check_error",
|
|
339
|
+
bucket=bucket_name,
|
|
340
|
+
error_code=error_code
|
|
341
|
+
)
|
|
342
|
+
return False
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
# ============================================================================
|
|
346
|
+
# CONVENIENCE FUNCTION
|
|
347
|
+
# ============================================================================
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def run_s3_public_access_block_test(connector: AWSConnector) -> TestResult:
|
|
351
|
+
"""Run S3 public access block compliance test.
|
|
352
|
+
|
|
353
|
+
Convenience function for running the test.
|
|
354
|
+
|
|
355
|
+
Args:
|
|
356
|
+
connector: AWS connector
|
|
357
|
+
|
|
358
|
+
Returns:
|
|
359
|
+
TestResult
|
|
360
|
+
|
|
361
|
+
Example:
|
|
362
|
+
>>> from complio.connectors.aws.client import AWSConnector
|
|
363
|
+
>>> connector = AWSConnector("production", "us-east-1")
|
|
364
|
+
>>> connector.connect()
|
|
365
|
+
>>> result = run_s3_public_access_block_test(connector)
|
|
366
|
+
>>> print(f"Score: {result.score}%")
|
|
367
|
+
"""
|
|
368
|
+
test = S3PublicAccessBlockTest(connector)
|
|
369
|
+
return test.execute()
|