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,370 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ElastiCache encryption compliance test.
|
|
3
|
+
|
|
4
|
+
Checks that all ElastiCache clusters have both at-rest and in-transit encryption enabled.
|
|
5
|
+
|
|
6
|
+
ISO 27001 Control: A.8.24 - Use of cryptography
|
|
7
|
+
Requirement: All data at rest and in transit must be encrypted
|
|
8
|
+
|
|
9
|
+
Example:
|
|
10
|
+
>>> from complio.connectors.aws.client import AWSConnector
|
|
11
|
+
>>> from complio.tests_library.storage.elasticache_encryption import ElastiCacheEncryptionTest
|
|
12
|
+
>>>
|
|
13
|
+
>>> connector = AWSConnector("production", "us-east-1")
|
|
14
|
+
>>> connector.connect()
|
|
15
|
+
>>>
|
|
16
|
+
>>> test = ElastiCacheEncryptionTest(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 ElastiCacheEncryptionTest(ComplianceTest):
|
|
35
|
+
"""Test for ElastiCache encryption compliance.
|
|
36
|
+
|
|
37
|
+
Verifies that all ElastiCache clusters have both at-rest and in-transit
|
|
38
|
+
encryption enabled.
|
|
39
|
+
|
|
40
|
+
Compliance Requirements:
|
|
41
|
+
- All cache clusters must have AtRestEncryptionEnabled=True
|
|
42
|
+
- All cache clusters must have TransitEncryptionEnabled=True
|
|
43
|
+
- Both encryption types are required for full compliance
|
|
44
|
+
|
|
45
|
+
Scoring:
|
|
46
|
+
- 100% if all clusters have both encryption types
|
|
47
|
+
- Proportional score based on compliant/total ratio
|
|
48
|
+
- 0% if no clusters are compliant
|
|
49
|
+
|
|
50
|
+
Example:
|
|
51
|
+
>>> test = ElastiCacheEncryptionTest(connector)
|
|
52
|
+
>>> result = test.execute()
|
|
53
|
+
>>> for finding in result.findings:
|
|
54
|
+
... print(f"{finding.resource_id}: {finding.title}")
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
def __init__(self, connector: AWSConnector) -> None:
|
|
58
|
+
"""Initialize ElastiCache encryption test.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
connector: AWS connector instance
|
|
62
|
+
"""
|
|
63
|
+
super().__init__(
|
|
64
|
+
test_id="elasticache_encryption",
|
|
65
|
+
test_name="ElastiCache Encryption Check",
|
|
66
|
+
description="Verify all ElastiCache clusters have at-rest and in-transit encryption",
|
|
67
|
+
control_id="A.8.24",
|
|
68
|
+
connector=connector,
|
|
69
|
+
scope="regional",
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
def execute(self) -> TestResult:
|
|
73
|
+
"""Execute ElastiCache encryption compliance test.
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
TestResult with findings for clusters without full encryption
|
|
77
|
+
|
|
78
|
+
Example:
|
|
79
|
+
>>> test = ElastiCacheEncryptionTest(connector)
|
|
80
|
+
>>> result = test.execute()
|
|
81
|
+
>>> print(result.score)
|
|
82
|
+
100.0
|
|
83
|
+
"""
|
|
84
|
+
result = TestResult(
|
|
85
|
+
test_id=self.test_id,
|
|
86
|
+
test_name=self.test_name,
|
|
87
|
+
status=TestStatus.PASSED,
|
|
88
|
+
passed=True,
|
|
89
|
+
score=100.0,
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
try:
|
|
93
|
+
# Get ElastiCache client
|
|
94
|
+
elasticache_client = self.connector.get_client("elasticache")
|
|
95
|
+
|
|
96
|
+
# List all replication groups (for Redis)
|
|
97
|
+
self.logger.info("listing_elasticache_replication_groups")
|
|
98
|
+
replication_groups = []
|
|
99
|
+
|
|
100
|
+
try:
|
|
101
|
+
response = elasticache_client.describe_replication_groups()
|
|
102
|
+
replication_groups = response.get("ReplicationGroups", [])
|
|
103
|
+
except ClientError as e:
|
|
104
|
+
error_code = e.response.get("Error", {}).get("Code")
|
|
105
|
+
self.logger.warning("replication_groups_list_error", error_code=error_code)
|
|
106
|
+
|
|
107
|
+
# List all cache clusters (standalone nodes)
|
|
108
|
+
self.logger.info("listing_elasticache_cache_clusters")
|
|
109
|
+
cache_clusters = []
|
|
110
|
+
|
|
111
|
+
try:
|
|
112
|
+
response = elasticache_client.describe_cache_clusters()
|
|
113
|
+
cache_clusters = response.get("CacheClusters", [])
|
|
114
|
+
except ClientError as e:
|
|
115
|
+
error_code = e.response.get("Error", {}).get("Code")
|
|
116
|
+
self.logger.warning("cache_clusters_list_error", error_code=error_code)
|
|
117
|
+
|
|
118
|
+
total_resources = len(replication_groups) + len(cache_clusters)
|
|
119
|
+
|
|
120
|
+
if total_resources == 0:
|
|
121
|
+
self.logger.info("no_elasticache_resources_found")
|
|
122
|
+
result.metadata["message"] = "No ElastiCache resources found in region"
|
|
123
|
+
return result
|
|
124
|
+
|
|
125
|
+
self.logger.info(
|
|
126
|
+
"elasticache_resources_found",
|
|
127
|
+
replication_groups=len(replication_groups),
|
|
128
|
+
cache_clusters=len(cache_clusters)
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
compliant_count = 0
|
|
132
|
+
|
|
133
|
+
# Check replication groups
|
|
134
|
+
for rep_group in replication_groups:
|
|
135
|
+
rep_group_id = rep_group["ReplicationGroupId"]
|
|
136
|
+
result.resources_scanned += 1
|
|
137
|
+
|
|
138
|
+
at_rest_enabled = rep_group.get("AtRestEncryptionEnabled", False)
|
|
139
|
+
transit_enabled = rep_group.get("TransitEncryptionEnabled", False)
|
|
140
|
+
fully_encrypted = at_rest_enabled and transit_enabled
|
|
141
|
+
|
|
142
|
+
# Get replication group details
|
|
143
|
+
status = rep_group.get("Status", "unknown")
|
|
144
|
+
engine = rep_group.get("CacheNodeType", "unknown")
|
|
145
|
+
num_node_groups = len(rep_group.get("NodeGroups", []))
|
|
146
|
+
|
|
147
|
+
# Create evidence
|
|
148
|
+
evidence = self.create_evidence(
|
|
149
|
+
resource_id=rep_group_id,
|
|
150
|
+
resource_type="elasticache_replication_group",
|
|
151
|
+
data={
|
|
152
|
+
"replication_group_id": rep_group_id,
|
|
153
|
+
"at_rest_encryption_enabled": at_rest_enabled,
|
|
154
|
+
"transit_encryption_enabled": transit_enabled,
|
|
155
|
+
"fully_encrypted": fully_encrypted,
|
|
156
|
+
"status": status,
|
|
157
|
+
"engine": engine,
|
|
158
|
+
"num_node_groups": num_node_groups,
|
|
159
|
+
"kms_key_id": rep_group.get("KmsKeyId"),
|
|
160
|
+
}
|
|
161
|
+
)
|
|
162
|
+
result.add_evidence(evidence)
|
|
163
|
+
|
|
164
|
+
if fully_encrypted:
|
|
165
|
+
compliant_count += 1
|
|
166
|
+
self.logger.debug(
|
|
167
|
+
"elasticache_replication_group_encrypted",
|
|
168
|
+
rep_group_id=rep_group_id
|
|
169
|
+
)
|
|
170
|
+
else:
|
|
171
|
+
# Create finding for incomplete encryption
|
|
172
|
+
missing_encryption = []
|
|
173
|
+
if not at_rest_enabled:
|
|
174
|
+
missing_encryption.append("at-rest encryption")
|
|
175
|
+
if not transit_enabled:
|
|
176
|
+
missing_encryption.append("in-transit encryption")
|
|
177
|
+
|
|
178
|
+
finding = self.create_finding(
|
|
179
|
+
resource_id=rep_group_id,
|
|
180
|
+
resource_type="elasticache_replication_group",
|
|
181
|
+
severity=Severity.HIGH,
|
|
182
|
+
title=f"ElastiCache replication group missing {', '.join(missing_encryption)}",
|
|
183
|
+
description=f"Redis replication group '{rep_group_id}' with {num_node_groups} node group(s) "
|
|
184
|
+
f"is missing {', '.join(missing_encryption)}. "
|
|
185
|
+
f"At-rest encryption: {'enabled' if at_rest_enabled else 'disabled'}. "
|
|
186
|
+
f"In-transit encryption: {'enabled' if transit_enabled else 'disabled'}. "
|
|
187
|
+
"This violates ISO 27001 A.8.24 requirement for comprehensive data encryption.",
|
|
188
|
+
remediation=(
|
|
189
|
+
f"Encryption cannot be enabled on existing replication groups. To remediate:\n"
|
|
190
|
+
"1. Create a backup of the replication group:\n"
|
|
191
|
+
f" aws elasticache create-snapshot --replication-group-id {rep_group_id} "
|
|
192
|
+
f"--snapshot-name {rep_group_id}-backup\n"
|
|
193
|
+
"2. Create a new replication group with encryption enabled:\n"
|
|
194
|
+
f" aws elasticache create-replication-group \\\n"
|
|
195
|
+
f" --replication-group-id {rep_group_id}-encrypted \\\n"
|
|
196
|
+
" --replication-group-description 'Encrypted replacement' \\\n"
|
|
197
|
+
f" --cache-node-type {engine} \\\n"
|
|
198
|
+
" --at-rest-encryption-enabled \\\n"
|
|
199
|
+
" --transit-encryption-enabled \\\n"
|
|
200
|
+
" --auth-token <strong-password>\n"
|
|
201
|
+
"3. Migrate data using Redis replication or application-level migration\n"
|
|
202
|
+
"4. Update application connection strings (enable TLS)\n"
|
|
203
|
+
f"5. Delete old replication group '{rep_group_id}'\n\n"
|
|
204
|
+
"Note: This requires downtime and application updates for TLS support."
|
|
205
|
+
),
|
|
206
|
+
evidence=evidence
|
|
207
|
+
)
|
|
208
|
+
result.add_finding(finding)
|
|
209
|
+
|
|
210
|
+
self.logger.warning(
|
|
211
|
+
"elasticache_replication_group_not_fully_encrypted",
|
|
212
|
+
rep_group_id=rep_group_id,
|
|
213
|
+
at_rest=at_rest_enabled,
|
|
214
|
+
transit=transit_enabled
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
# Check standalone cache clusters
|
|
218
|
+
for cluster in cache_clusters:
|
|
219
|
+
cluster_id = cluster["CacheClusterId"]
|
|
220
|
+
|
|
221
|
+
# Skip clusters that are part of replication groups (already checked)
|
|
222
|
+
if cluster.get("ReplicationGroupId"):
|
|
223
|
+
continue
|
|
224
|
+
|
|
225
|
+
result.resources_scanned += 1
|
|
226
|
+
|
|
227
|
+
# For standalone clusters, check if encryption is possible
|
|
228
|
+
at_rest_enabled = cluster.get("AtRestEncryptionEnabled", False)
|
|
229
|
+
transit_enabled = cluster.get("TransitEncryptionEnabled", False)
|
|
230
|
+
fully_encrypted = at_rest_enabled and transit_enabled
|
|
231
|
+
|
|
232
|
+
# Get cluster details
|
|
233
|
+
engine = cluster.get("Engine", "unknown")
|
|
234
|
+
engine_version = cluster.get("EngineVersion", "unknown")
|
|
235
|
+
cache_node_type = cluster.get("CacheNodeType", "unknown")
|
|
236
|
+
|
|
237
|
+
# Create evidence
|
|
238
|
+
evidence = self.create_evidence(
|
|
239
|
+
resource_id=cluster_id,
|
|
240
|
+
resource_type="elasticache_cluster",
|
|
241
|
+
data={
|
|
242
|
+
"cluster_id": cluster_id,
|
|
243
|
+
"at_rest_encryption_enabled": at_rest_enabled,
|
|
244
|
+
"transit_encryption_enabled": transit_enabled,
|
|
245
|
+
"fully_encrypted": fully_encrypted,
|
|
246
|
+
"engine": engine,
|
|
247
|
+
"engine_version": engine_version,
|
|
248
|
+
"cache_node_type": cache_node_type,
|
|
249
|
+
}
|
|
250
|
+
)
|
|
251
|
+
result.add_evidence(evidence)
|
|
252
|
+
|
|
253
|
+
if fully_encrypted:
|
|
254
|
+
compliant_count += 1
|
|
255
|
+
self.logger.debug(
|
|
256
|
+
"elasticache_cluster_encrypted",
|
|
257
|
+
cluster_id=cluster_id
|
|
258
|
+
)
|
|
259
|
+
else:
|
|
260
|
+
# Create finding for incomplete encryption
|
|
261
|
+
missing_encryption = []
|
|
262
|
+
if not at_rest_enabled:
|
|
263
|
+
missing_encryption.append("at-rest encryption")
|
|
264
|
+
if not transit_enabled:
|
|
265
|
+
missing_encryption.append("in-transit encryption")
|
|
266
|
+
|
|
267
|
+
finding = self.create_finding(
|
|
268
|
+
resource_id=cluster_id,
|
|
269
|
+
resource_type="elasticache_cluster",
|
|
270
|
+
severity=Severity.HIGH,
|
|
271
|
+
title=f"ElastiCache cluster missing {', '.join(missing_encryption)}",
|
|
272
|
+
description=f"{engine} cluster '{cluster_id}' (version {engine_version}) "
|
|
273
|
+
f"is missing {', '.join(missing_encryption)}. "
|
|
274
|
+
"This violates ISO 27001 A.8.24 requirement for comprehensive data encryption.",
|
|
275
|
+
remediation=(
|
|
276
|
+
f"Encryption cannot be enabled on existing clusters. To remediate:\n"
|
|
277
|
+
"1. Create a backup:\n"
|
|
278
|
+
f" aws elasticache create-snapshot --cache-cluster-id {cluster_id} "
|
|
279
|
+
f"--snapshot-name {cluster_id}-backup\n"
|
|
280
|
+
"2. Create a new cluster with encryption:\n"
|
|
281
|
+
f" aws elasticache create-cache-cluster \\\n"
|
|
282
|
+
f" --cache-cluster-id {cluster_id}-encrypted \\\n"
|
|
283
|
+
f" --engine {engine} \\\n"
|
|
284
|
+
f" --cache-node-type {cache_node_type} \\\n"
|
|
285
|
+
" --at-rest-encryption-enabled \\\n"
|
|
286
|
+
" --transit-encryption-enabled\n"
|
|
287
|
+
"3. Migrate data and update application\n"
|
|
288
|
+
f"4. Delete old cluster '{cluster_id}'"
|
|
289
|
+
),
|
|
290
|
+
evidence=evidence
|
|
291
|
+
)
|
|
292
|
+
result.add_finding(finding)
|
|
293
|
+
|
|
294
|
+
self.logger.warning(
|
|
295
|
+
"elasticache_cluster_not_fully_encrypted",
|
|
296
|
+
cluster_id=cluster_id,
|
|
297
|
+
at_rest=at_rest_enabled,
|
|
298
|
+
transit=transit_enabled
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
# Calculate compliance score
|
|
302
|
+
if total_resources > 0:
|
|
303
|
+
result.score = (compliant_count / total_resources) * 100
|
|
304
|
+
|
|
305
|
+
# Determine pass/fail
|
|
306
|
+
result.passed = compliant_count == total_resources
|
|
307
|
+
result.status = TestStatus.PASSED if result.passed else TestStatus.FAILED
|
|
308
|
+
|
|
309
|
+
# Add metadata
|
|
310
|
+
result.metadata = {
|
|
311
|
+
"total_resources": total_resources,
|
|
312
|
+
"replication_groups": len(replication_groups),
|
|
313
|
+
"standalone_clusters": len([c for c in cache_clusters if not c.get("ReplicationGroupId")]),
|
|
314
|
+
"fully_encrypted": compliant_count,
|
|
315
|
+
"not_fully_encrypted": total_resources - compliant_count,
|
|
316
|
+
"compliance_percentage": result.score,
|
|
317
|
+
"region": self.connector.region,
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
self.logger.info(
|
|
321
|
+
"elasticache_encryption_test_completed",
|
|
322
|
+
total=total_resources,
|
|
323
|
+
compliant=compliant_count,
|
|
324
|
+
score=result.score,
|
|
325
|
+
passed=result.passed
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
except ClientError as e:
|
|
329
|
+
error_code = e.response.get("Error", {}).get("Code")
|
|
330
|
+
self.logger.error("elasticache_encryption_test_error", error_code=error_code, error=str(e))
|
|
331
|
+
result.status = TestStatus.ERROR
|
|
332
|
+
result.passed = False
|
|
333
|
+
result.score = 0.0
|
|
334
|
+
result.error_message = f"AWS API Error: {error_code} - {str(e)}"
|
|
335
|
+
|
|
336
|
+
except Exception as e:
|
|
337
|
+
self.logger.error("elasticache_encryption_test_error", error=str(e))
|
|
338
|
+
result.status = TestStatus.ERROR
|
|
339
|
+
result.passed = False
|
|
340
|
+
result.score = 0.0
|
|
341
|
+
result.error_message = str(e)
|
|
342
|
+
|
|
343
|
+
return result
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
# ============================================================================
|
|
347
|
+
# CONVENIENCE FUNCTION
|
|
348
|
+
# ============================================================================
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
def run_elasticache_encryption_test(connector: AWSConnector) -> TestResult:
|
|
352
|
+
"""Run ElastiCache encryption compliance test.
|
|
353
|
+
|
|
354
|
+
Convenience function for running the test.
|
|
355
|
+
|
|
356
|
+
Args:
|
|
357
|
+
connector: AWS connector
|
|
358
|
+
|
|
359
|
+
Returns:
|
|
360
|
+
TestResult
|
|
361
|
+
|
|
362
|
+
Example:
|
|
363
|
+
>>> from complio.connectors.aws.client import AWSConnector
|
|
364
|
+
>>> connector = AWSConnector("production", "us-east-1")
|
|
365
|
+
>>> connector.connect()
|
|
366
|
+
>>> result = run_elasticache_encryption_test(connector)
|
|
367
|
+
>>> print(f"Score: {result.score}%")
|
|
368
|
+
"""
|
|
369
|
+
test = ElastiCacheEncryptionTest(connector)
|
|
370
|
+
return test.execute()
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Redshift cluster encryption compliance test.
|
|
3
|
+
|
|
4
|
+
Checks that all Redshift clusters have encryption enabled.
|
|
5
|
+
|
|
6
|
+
ISO 27001 Control: A.8.24 - Use of cryptography
|
|
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.storage.redshift_encryption import RedshiftEncryptionTest
|
|
12
|
+
>>>
|
|
13
|
+
>>> connector = AWSConnector("production", "us-east-1")
|
|
14
|
+
>>> connector.connect()
|
|
15
|
+
>>>
|
|
16
|
+
>>> test = RedshiftEncryptionTest(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 RedshiftEncryptionTest(ComplianceTest):
|
|
35
|
+
"""Test for Redshift cluster encryption compliance.
|
|
36
|
+
|
|
37
|
+
Verifies that all Redshift clusters have encryption enabled.
|
|
38
|
+
|
|
39
|
+
Compliance Requirements:
|
|
40
|
+
- All Redshift clusters must have Encrypted=True
|
|
41
|
+
- Unencrypted clusters expose sensitive data warehouse data
|
|
42
|
+
- Clusters without encryption are non-compliant
|
|
43
|
+
|
|
44
|
+
Scoring:
|
|
45
|
+
- 100% if all clusters are encrypted
|
|
46
|
+
- Proportional score based on encrypted/total ratio
|
|
47
|
+
- 0% if no clusters are encrypted
|
|
48
|
+
|
|
49
|
+
Example:
|
|
50
|
+
>>> test = RedshiftEncryptionTest(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 Redshift encryption test.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
connector: AWS connector instance
|
|
61
|
+
"""
|
|
62
|
+
super().__init__(
|
|
63
|
+
test_id="redshift_encryption",
|
|
64
|
+
test_name="Redshift Cluster Encryption Check",
|
|
65
|
+
description="Verify all Redshift clusters have encryption enabled",
|
|
66
|
+
control_id="A.8.24",
|
|
67
|
+
connector=connector,
|
|
68
|
+
scope="regional",
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
def execute(self) -> TestResult:
|
|
72
|
+
"""Execute Redshift encryption compliance test.
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
TestResult with findings for non-encrypted clusters
|
|
76
|
+
|
|
77
|
+
Example:
|
|
78
|
+
>>> test = RedshiftEncryptionTest(connector)
|
|
79
|
+
>>> result = test.execute()
|
|
80
|
+
>>> print(result.score)
|
|
81
|
+
100.0
|
|
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 Redshift client
|
|
93
|
+
redshift_client = self.connector.get_client("redshift")
|
|
94
|
+
|
|
95
|
+
# List all clusters
|
|
96
|
+
self.logger.info("listing_redshift_clusters")
|
|
97
|
+
response = redshift_client.describe_clusters()
|
|
98
|
+
clusters = response.get("Clusters", [])
|
|
99
|
+
|
|
100
|
+
if not clusters:
|
|
101
|
+
self.logger.info("no_redshift_clusters_found")
|
|
102
|
+
result.metadata["message"] = "No Redshift clusters found in region"
|
|
103
|
+
return result
|
|
104
|
+
|
|
105
|
+
self.logger.info("redshift_clusters_found", count=len(clusters))
|
|
106
|
+
|
|
107
|
+
# Check each cluster for encryption
|
|
108
|
+
encrypted_count = 0
|
|
109
|
+
total_count = len(clusters)
|
|
110
|
+
|
|
111
|
+
for cluster in clusters:
|
|
112
|
+
cluster_id = cluster["ClusterIdentifier"]
|
|
113
|
+
encrypted = cluster.get("Encrypted", False)
|
|
114
|
+
result.resources_scanned += 1
|
|
115
|
+
|
|
116
|
+
# Get cluster details
|
|
117
|
+
node_type = cluster.get("NodeType", "unknown")
|
|
118
|
+
number_of_nodes = cluster.get("NumberOfNodes", 0)
|
|
119
|
+
cluster_status = cluster.get("ClusterStatus", "unknown")
|
|
120
|
+
db_name = cluster.get("DBName", "unknown")
|
|
121
|
+
|
|
122
|
+
# Create evidence
|
|
123
|
+
evidence = self.create_evidence(
|
|
124
|
+
resource_id=cluster_id,
|
|
125
|
+
resource_type="redshift_cluster",
|
|
126
|
+
data={
|
|
127
|
+
"cluster_id": cluster_id,
|
|
128
|
+
"encrypted": encrypted,
|
|
129
|
+
"node_type": node_type,
|
|
130
|
+
"number_of_nodes": number_of_nodes,
|
|
131
|
+
"cluster_status": cluster_status,
|
|
132
|
+
"db_name": db_name,
|
|
133
|
+
"kms_key_id": cluster.get("KmsKeyId"),
|
|
134
|
+
"availability_zone": cluster.get("AvailabilityZone"),
|
|
135
|
+
}
|
|
136
|
+
)
|
|
137
|
+
result.add_evidence(evidence)
|
|
138
|
+
|
|
139
|
+
if encrypted:
|
|
140
|
+
encrypted_count += 1
|
|
141
|
+
self.logger.debug(
|
|
142
|
+
"redshift_cluster_encrypted",
|
|
143
|
+
cluster_id=cluster_id,
|
|
144
|
+
kms_key_id=cluster.get("KmsKeyId")
|
|
145
|
+
)
|
|
146
|
+
else:
|
|
147
|
+
# Create finding for non-encrypted cluster
|
|
148
|
+
finding = self.create_finding(
|
|
149
|
+
resource_id=cluster_id,
|
|
150
|
+
resource_type="redshift_cluster",
|
|
151
|
+
severity=Severity.HIGH,
|
|
152
|
+
title="Redshift cluster encryption not enabled",
|
|
153
|
+
description=f"Redshift cluster '{cluster_id}' ({node_type}, {number_of_nodes} nodes) "
|
|
154
|
+
"does not have encryption enabled. Data warehouse data is stored unencrypted. "
|
|
155
|
+
"This violates ISO 27001 A.8.24 requirement for data-at-rest encryption.",
|
|
156
|
+
remediation=(
|
|
157
|
+
"Redshift clusters cannot be encrypted after creation. To remediate:\n"
|
|
158
|
+
"1. Create a snapshot of the unencrypted cluster:\n"
|
|
159
|
+
f" aws redshift create-cluster-snapshot --cluster-identifier {cluster_id} "
|
|
160
|
+
f"--snapshot-identifier {cluster_id}-snapshot\n"
|
|
161
|
+
"2. Copy the snapshot with encryption enabled:\n"
|
|
162
|
+
f" aws redshift copy-cluster-snapshot "
|
|
163
|
+
f"--source-snapshot-identifier {cluster_id}-snapshot "
|
|
164
|
+
f"--target-snapshot-identifier {cluster_id}-encrypted-snapshot "
|
|
165
|
+
"--kms-key-id <your-kms-key-id>\n"
|
|
166
|
+
"3. Restore a new cluster from the encrypted snapshot:\n"
|
|
167
|
+
f" aws redshift restore-from-cluster-snapshot "
|
|
168
|
+
f"--cluster-identifier {cluster_id}-encrypted "
|
|
169
|
+
f"--snapshot-identifier {cluster_id}-encrypted-snapshot\n"
|
|
170
|
+
"4. Update application connection strings\n"
|
|
171
|
+
"5. Delete the old unencrypted cluster\n\n"
|
|
172
|
+
"Note: This will cause downtime during the migration.\n"
|
|
173
|
+
"For future clusters, enable encryption at creation time."
|
|
174
|
+
),
|
|
175
|
+
evidence=evidence
|
|
176
|
+
)
|
|
177
|
+
result.add_finding(finding)
|
|
178
|
+
|
|
179
|
+
self.logger.warning(
|
|
180
|
+
"redshift_cluster_not_encrypted",
|
|
181
|
+
cluster_id=cluster_id,
|
|
182
|
+
number_of_nodes=number_of_nodes
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
# Calculate compliance score
|
|
186
|
+
if total_count > 0:
|
|
187
|
+
result.score = (encrypted_count / total_count) * 100
|
|
188
|
+
|
|
189
|
+
# Determine pass/fail
|
|
190
|
+
result.passed = encrypted_count == total_count
|
|
191
|
+
result.status = TestStatus.PASSED if result.passed else TestStatus.FAILED
|
|
192
|
+
|
|
193
|
+
# Add metadata
|
|
194
|
+
result.metadata = {
|
|
195
|
+
"total_clusters": total_count,
|
|
196
|
+
"encrypted_clusters": encrypted_count,
|
|
197
|
+
"non_encrypted_clusters": total_count - encrypted_count,
|
|
198
|
+
"compliance_percentage": result.score,
|
|
199
|
+
"region": self.connector.region,
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
self.logger.info(
|
|
203
|
+
"redshift_encryption_test_completed",
|
|
204
|
+
total=total_count,
|
|
205
|
+
encrypted=encrypted_count,
|
|
206
|
+
score=result.score,
|
|
207
|
+
passed=result.passed
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
except ClientError as e:
|
|
211
|
+
error_code = e.response.get("Error", {}).get("Code")
|
|
212
|
+
self.logger.error("redshift_encryption_test_error", error_code=error_code, error=str(e))
|
|
213
|
+
result.status = TestStatus.ERROR
|
|
214
|
+
result.passed = False
|
|
215
|
+
result.score = 0.0
|
|
216
|
+
result.error_message = f"AWS API Error: {error_code} - {str(e)}"
|
|
217
|
+
|
|
218
|
+
except Exception as e:
|
|
219
|
+
self.logger.error("redshift_encryption_test_error", error=str(e))
|
|
220
|
+
result.status = TestStatus.ERROR
|
|
221
|
+
result.passed = False
|
|
222
|
+
result.score = 0.0
|
|
223
|
+
result.error_message = str(e)
|
|
224
|
+
|
|
225
|
+
return result
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
# ============================================================================
|
|
229
|
+
# CONVENIENCE FUNCTION
|
|
230
|
+
# ============================================================================
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def run_redshift_encryption_test(connector: AWSConnector) -> TestResult:
|
|
234
|
+
"""Run Redshift encryption compliance test.
|
|
235
|
+
|
|
236
|
+
Convenience function for running the test.
|
|
237
|
+
|
|
238
|
+
Args:
|
|
239
|
+
connector: AWS connector
|
|
240
|
+
|
|
241
|
+
Returns:
|
|
242
|
+
TestResult
|
|
243
|
+
|
|
244
|
+
Example:
|
|
245
|
+
>>> from complio.connectors.aws.client import AWSConnector
|
|
246
|
+
>>> connector = AWSConnector("production", "us-east-1")
|
|
247
|
+
>>> connector.connect()
|
|
248
|
+
>>> result = run_redshift_encryption_test(connector)
|
|
249
|
+
>>> print(f"Score: {result.score}%")
|
|
250
|
+
"""
|
|
251
|
+
test = RedshiftEncryptionTest(connector)
|
|
252
|
+
return test.execute()
|