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,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()