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.
Files changed (79) hide show
  1. CHANGELOG.md +208 -0
  2. README.md +343 -0
  3. complio/__init__.py +48 -0
  4. complio/cli/__init__.py +0 -0
  5. complio/cli/banner.py +87 -0
  6. complio/cli/commands/__init__.py +0 -0
  7. complio/cli/commands/history.py +439 -0
  8. complio/cli/commands/scan.py +700 -0
  9. complio/cli/main.py +115 -0
  10. complio/cli/output.py +338 -0
  11. complio/config/__init__.py +17 -0
  12. complio/config/settings.py +333 -0
  13. complio/connectors/__init__.py +9 -0
  14. complio/connectors/aws/__init__.py +0 -0
  15. complio/connectors/aws/client.py +342 -0
  16. complio/connectors/base.py +135 -0
  17. complio/core/__init__.py +10 -0
  18. complio/core/registry.py +228 -0
  19. complio/core/runner.py +351 -0
  20. complio/py.typed +0 -0
  21. complio/reporters/__init__.py +7 -0
  22. complio/reporters/generator.py +417 -0
  23. complio/tests_library/__init__.py +0 -0
  24. complio/tests_library/base.py +492 -0
  25. complio/tests_library/identity/__init__.py +0 -0
  26. complio/tests_library/identity/access_key_rotation.py +302 -0
  27. complio/tests_library/identity/mfa_enforcement.py +327 -0
  28. complio/tests_library/identity/root_account_protection.py +470 -0
  29. complio/tests_library/infrastructure/__init__.py +0 -0
  30. complio/tests_library/infrastructure/cloudtrail_encryption.py +286 -0
  31. complio/tests_library/infrastructure/cloudtrail_log_validation.py +274 -0
  32. complio/tests_library/infrastructure/cloudtrail_logging.py +400 -0
  33. complio/tests_library/infrastructure/ebs_encryption.py +244 -0
  34. complio/tests_library/infrastructure/ec2_security_groups.py +321 -0
  35. complio/tests_library/infrastructure/iam_password_policy.py +460 -0
  36. complio/tests_library/infrastructure/nacl_security.py +356 -0
  37. complio/tests_library/infrastructure/rds_encryption.py +252 -0
  38. complio/tests_library/infrastructure/s3_encryption.py +301 -0
  39. complio/tests_library/infrastructure/s3_public_access.py +369 -0
  40. complio/tests_library/infrastructure/secrets_manager_encryption.py +248 -0
  41. complio/tests_library/infrastructure/vpc_flow_logs.py +287 -0
  42. complio/tests_library/logging/__init__.py +0 -0
  43. complio/tests_library/logging/cloudwatch_alarms.py +354 -0
  44. complio/tests_library/logging/cloudwatch_logs_encryption.py +281 -0
  45. complio/tests_library/logging/cloudwatch_retention.py +252 -0
  46. complio/tests_library/logging/config_enabled.py +393 -0
  47. complio/tests_library/logging/eventbridge_rules.py +460 -0
  48. complio/tests_library/logging/guardduty_enabled.py +436 -0
  49. complio/tests_library/logging/security_hub_enabled.py +416 -0
  50. complio/tests_library/logging/sns_encryption.py +273 -0
  51. complio/tests_library/network/__init__.py +0 -0
  52. complio/tests_library/network/alb_nlb_security.py +421 -0
  53. complio/tests_library/network/api_gateway_security.py +452 -0
  54. complio/tests_library/network/cloudfront_https.py +332 -0
  55. complio/tests_library/network/direct_connect_security.py +343 -0
  56. complio/tests_library/network/nacl_configuration.py +367 -0
  57. complio/tests_library/network/network_firewall.py +355 -0
  58. complio/tests_library/network/transit_gateway_security.py +318 -0
  59. complio/tests_library/network/vpc_endpoints_security.py +339 -0
  60. complio/tests_library/network/vpn_security.py +333 -0
  61. complio/tests_library/network/waf_configuration.py +428 -0
  62. complio/tests_library/security/__init__.py +0 -0
  63. complio/tests_library/security/kms_key_rotation.py +314 -0
  64. complio/tests_library/storage/__init__.py +0 -0
  65. complio/tests_library/storage/backup_encryption.py +288 -0
  66. complio/tests_library/storage/dynamodb_encryption.py +280 -0
  67. complio/tests_library/storage/efs_encryption.py +257 -0
  68. complio/tests_library/storage/elasticache_encryption.py +370 -0
  69. complio/tests_library/storage/redshift_encryption.py +252 -0
  70. complio/tests_library/storage/s3_versioning.py +264 -0
  71. complio/utils/__init__.py +26 -0
  72. complio/utils/errors.py +179 -0
  73. complio/utils/exceptions.py +151 -0
  74. complio/utils/history.py +243 -0
  75. complio/utils/logger.py +391 -0
  76. complio-0.1.1.dist-info/METADATA +385 -0
  77. complio-0.1.1.dist-info/RECORD +79 -0
  78. complio-0.1.1.dist-info/WHEEL +4 -0
  79. 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()