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,356 @@
1
+ """
2
+ Network ACL security compliance test.
3
+
4
+ Checks that Network ACLs don't allow unrestricted access to sensitive ports.
5
+
6
+ ISO 27001 Control: A.8.20 - Network security
7
+ Requirement: Network access controls must restrict access to sensitive services
8
+
9
+ Example:
10
+ >>> from complio.connectors.aws.client import AWSConnector
11
+ >>> from complio.tests_library.infrastructure.nacl_security import NACLSecurityTest
12
+ >>>
13
+ >>> connector = AWSConnector("production", "us-east-1")
14
+ >>> connector.connect()
15
+ >>>
16
+ >>> test = NACLSecurityTest(connector)
17
+ >>> result = test.run()
18
+ >>> print(f"Passed: {result.passed}, Score: {result.score}")
19
+ """
20
+
21
+ from typing import Any, Dict, List, Set
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
+ # Sensitive ports that should not be open to 0.0.0.0/0
35
+ SENSITIVE_PORTS = {
36
+ 22: "SSH",
37
+ 3389: "RDP",
38
+ 1433: "MS SQL Server",
39
+ 3306: "MySQL",
40
+ 5432: "PostgreSQL",
41
+ 27017: "MongoDB",
42
+ 6379: "Redis",
43
+ 9200: "Elasticsearch",
44
+ 5984: "CouchDB",
45
+ }
46
+
47
+
48
+ class NACLSecurityTest(ComplianceTest):
49
+ """Test for Network ACL security compliance.
50
+
51
+ Verifies that Network ACLs don't allow unrestricted access (0.0.0.0/0)
52
+ to sensitive ports commonly used by database and management services.
53
+
54
+ Compliance Requirements:
55
+ - NACLs should not allow 0.0.0.0/0 access to sensitive ports
56
+ - Sensitive ports include: SSH (22), RDP (3389), databases (1433, 3306, 5432, 27017)
57
+ - Unrestricted access to these ports is HIGH severity
58
+
59
+ Scoring:
60
+ - 100% if all NACLs are secure
61
+ - Proportional score based on secure/total ratio
62
+ - Each insecure rule reduces the score
63
+
64
+ Example:
65
+ >>> test = NACLSecurityTest(connector)
66
+ >>> result = test.execute()
67
+ >>> for finding in result.findings:
68
+ ... print(f"{finding.resource_id}: {finding.title}")
69
+ """
70
+
71
+ def __init__(self, connector: AWSConnector) -> None:
72
+ """Initialize Network ACL security test.
73
+
74
+ Args:
75
+ connector: AWS connector instance
76
+ """
77
+ super().__init__(
78
+ test_id="nacl_security",
79
+ test_name="Network ACL Security Check",
80
+ description="Verify Network ACLs don't allow unrestricted access to sensitive ports",
81
+ control_id="A.8.20",
82
+ connector=connector,
83
+ scope="regional",
84
+ )
85
+
86
+ def execute(self) -> TestResult:
87
+ """Execute Network ACL security compliance test.
88
+
89
+ Returns:
90
+ TestResult with findings for insecure NACL rules
91
+
92
+ Example:
93
+ >>> test = NACLSecurityTest(connector)
94
+ >>> result = test.execute()
95
+ >>> print(result.score)
96
+ 90.0
97
+ """
98
+ result = TestResult(
99
+ test_id=self.test_id,
100
+ test_name=self.test_name,
101
+ status=TestStatus.PASSED,
102
+ passed=True,
103
+ score=100.0,
104
+ )
105
+
106
+ try:
107
+ # Get EC2 client
108
+ ec2_client = self.connector.get_client("ec2")
109
+
110
+ # List all Network ACLs
111
+ self.logger.info("listing_network_acls")
112
+ response = ec2_client.describe_network_acls()
113
+ nacls = response.get("NetworkAcls", [])
114
+
115
+ if not nacls:
116
+ self.logger.info("no_nacls_found")
117
+ result.metadata["message"] = "No Network ACLs found in region"
118
+ return result
119
+
120
+ self.logger.info("nacls_found", count=len(nacls))
121
+
122
+ # Check each NACL for insecure rules
123
+ secure_nacls = 0
124
+ total_nacls = len(nacls)
125
+ total_insecure_rules = 0
126
+
127
+ for nacl in nacls:
128
+ nacl_id = nacl["NetworkAclId"]
129
+ vpc_id = nacl.get("VpcId", "unknown")
130
+ is_default = nacl.get("IsDefault", False)
131
+ result.resources_scanned += 1
132
+
133
+ # Get NACL name from tags
134
+ nacl_name = "unnamed"
135
+ for tag in nacl.get("Tags", []):
136
+ if tag.get("Key") == "Name":
137
+ nacl_name = tag.get("Value", "unnamed")
138
+ break
139
+
140
+ # Check inbound rules for insecure configurations
141
+ insecure_rules = self._check_nacl_rules(nacl)
142
+
143
+ # Create evidence
144
+ evidence = self.create_evidence(
145
+ resource_id=nacl_id,
146
+ resource_type="network_acl",
147
+ data={
148
+ "nacl_id": nacl_id,
149
+ "nacl_name": nacl_name,
150
+ "vpc_id": vpc_id,
151
+ "is_default": is_default,
152
+ "insecure_rules_count": len(insecure_rules),
153
+ "insecure_rules": insecure_rules,
154
+ "total_entries": len(nacl.get("Entries", [])),
155
+ }
156
+ )
157
+ result.add_evidence(evidence)
158
+
159
+ if len(insecure_rules) == 0:
160
+ secure_nacls += 1
161
+ self.logger.debug(
162
+ "nacl_secure",
163
+ nacl_id=nacl_id,
164
+ nacl_name=nacl_name
165
+ )
166
+ else:
167
+ total_insecure_rules += len(insecure_rules)
168
+
169
+ # Create finding for each insecure rule
170
+ for insecure_rule in insecure_rules:
171
+ rule_number = insecure_rule["rule_number"]
172
+ port_range = insecure_rule["port_range"]
173
+ protocol = insecure_rule["protocol"]
174
+ service_name = insecure_rule["service_name"]
175
+
176
+ finding = self.create_finding(
177
+ resource_id=nacl_id,
178
+ resource_type="network_acl",
179
+ severity=Severity.HIGH,
180
+ title=f"Network ACL allows unrestricted access to {service_name}",
181
+ description=f"Network ACL '{nacl_name}' ({nacl_id}) has rule #{rule_number} that allows "
182
+ f"unrestricted access (0.0.0.0/0) to {service_name} on port {port_range}. "
183
+ f"This exposes sensitive services to the entire internet. "
184
+ "ISO 27001 A.8.20 requires proper network access controls.",
185
+ remediation=(
186
+ f"Restrict access to {service_name} in NACL '{nacl_id}':\n"
187
+ "1. Go to VPC → Network ACLs\n"
188
+ f"2. Select NACL '{nacl_id}'\n"
189
+ "3. Go to Inbound Rules tab\n"
190
+ f"4. Edit rule #{rule_number}\n"
191
+ "5. Change source from 0.0.0.0/0 to specific IP ranges\n"
192
+ "6. Save changes\n\n"
193
+ "Or use AWS CLI:\n"
194
+ f"# First, delete the insecure rule:\n"
195
+ f"aws ec2 delete-network-acl-entry --network-acl-id {nacl_id} \\\n"
196
+ f" --rule-number {rule_number} --ingress\n\n"
197
+ f"# Then, add a secure rule with specific source:\n"
198
+ f"aws ec2 create-network-acl-entry --network-acl-id {nacl_id} \\\n"
199
+ f" --rule-number {rule_number} --protocol {protocol} \\\n"
200
+ f" --port-range From={port_range},To={port_range} \\\n"
201
+ " --cidr-block <your-ip-range>/32 --rule-action allow --ingress"
202
+ ),
203
+ evidence=evidence
204
+ )
205
+ result.add_finding(finding)
206
+
207
+ self.logger.warning(
208
+ "nacl_insecure",
209
+ nacl_id=nacl_id,
210
+ nacl_name=nacl_name,
211
+ insecure_rules_count=len(insecure_rules)
212
+ )
213
+
214
+ # Calculate compliance score
215
+ # Score is based on the ratio of secure NACLs to total NACLs
216
+ if total_nacls > 0:
217
+ result.score = (secure_nacls / total_nacls) * 100
218
+
219
+ # Determine pass/fail
220
+ result.passed = secure_nacls == total_nacls
221
+ result.status = TestStatus.PASSED if result.passed else TestStatus.FAILED
222
+
223
+ # Add metadata
224
+ result.metadata = {
225
+ "total_nacls": total_nacls,
226
+ "secure_nacls": secure_nacls,
227
+ "insecure_nacls": total_nacls - secure_nacls,
228
+ "total_insecure_rules": total_insecure_rules,
229
+ "compliance_percentage": result.score,
230
+ "region": self.connector.region,
231
+ }
232
+
233
+ self.logger.info(
234
+ "nacl_security_test_completed",
235
+ total=total_nacls,
236
+ secure=secure_nacls,
237
+ total_insecure_rules=total_insecure_rules,
238
+ score=result.score,
239
+ passed=result.passed
240
+ )
241
+
242
+ except ClientError as e:
243
+ error_code = e.response.get("Error", {}).get("Code")
244
+ self.logger.error("nacl_security_test_error", error_code=error_code, error=str(e))
245
+ result.status = TestStatus.ERROR
246
+ result.passed = False
247
+ result.score = 0.0
248
+ result.error_message = f"AWS API Error: {error_code} - {str(e)}"
249
+
250
+ except Exception as e:
251
+ self.logger.error("nacl_security_test_error", error=str(e))
252
+ result.status = TestStatus.ERROR
253
+ result.passed = False
254
+ result.score = 0.0
255
+ result.error_message = str(e)
256
+
257
+ return result
258
+
259
+ def _check_nacl_rules(self, nacl: Dict[str, Any]) -> List[Dict[str, Any]]:
260
+ """Check NACL rules for insecure configurations.
261
+
262
+ Args:
263
+ nacl: Network ACL dictionary from AWS API
264
+
265
+ Returns:
266
+ List of insecure rules with details
267
+ """
268
+ insecure_rules = []
269
+
270
+ for entry in nacl.get("Entries", []):
271
+ # Only check inbound ALLOW rules
272
+ if entry.get("Egress", True):
273
+ continue # Skip egress rules
274
+
275
+ if entry.get("RuleAction") != "allow":
276
+ continue # Skip deny rules
277
+
278
+ # Check if rule allows 0.0.0.0/0 or ::/0
279
+ cidr_block = entry.get("CidrBlock", "")
280
+ ipv6_cidr_block = entry.get("Ipv6CidrBlock", "")
281
+
282
+ if cidr_block != "0.0.0.0/0" and ipv6_cidr_block != "::/0":
283
+ continue # Not unrestricted, skip
284
+
285
+ # Get port range
286
+ port_range = entry.get("PortRange", {})
287
+ from_port = port_range.get("From") if port_range else None
288
+ to_port = port_range.get("To") if port_range else None
289
+
290
+ # Get protocol
291
+ protocol = entry.get("Protocol", "-1")
292
+
293
+ # Check if rule allows sensitive ports
294
+ if protocol == "-1":
295
+ # All protocols - check if this is a problem
296
+ # This is very permissive but not always wrong for NACLs
297
+ # We'll flag it if it's rule 100 or lower (high priority)
298
+ rule_number = entry.get("RuleNumber", 32767)
299
+ if rule_number <= 100:
300
+ for port, service_name in SENSITIVE_PORTS.items():
301
+ insecure_rules.append({
302
+ "rule_number": rule_number,
303
+ "port_range": f"All (includes {port})",
304
+ "protocol": "All",
305
+ "service_name": f"All Services (includes {service_name})",
306
+ "cidr_block": cidr_block or ipv6_cidr_block,
307
+ })
308
+ break # Only add once for "all protocols" rules
309
+ elif from_port is not None and to_port is not None:
310
+ # Check if any sensitive port is in the range
311
+ for port, service_name in SENSITIVE_PORTS.items():
312
+ if from_port <= port <= to_port:
313
+ protocol_name = {
314
+ "6": "TCP",
315
+ "17": "UDP",
316
+ "-1": "All",
317
+ }.get(protocol, f"Protocol {protocol}")
318
+
319
+ port_display = f"{from_port}-{to_port}" if from_port != to_port else str(from_port)
320
+
321
+ insecure_rules.append({
322
+ "rule_number": entry.get("RuleNumber", 32767),
323
+ "port_range": port_display,
324
+ "protocol": protocol_name,
325
+ "service_name": service_name,
326
+ "cidr_block": cidr_block or ipv6_cidr_block,
327
+ })
328
+
329
+ return insecure_rules
330
+
331
+
332
+ # ============================================================================
333
+ # CONVENIENCE FUNCTION
334
+ # ============================================================================
335
+
336
+
337
+ def run_nacl_security_test(connector: AWSConnector) -> TestResult:
338
+ """Run Network ACL security compliance test.
339
+
340
+ Convenience function for running the test.
341
+
342
+ Args:
343
+ connector: AWS connector
344
+
345
+ Returns:
346
+ TestResult
347
+
348
+ Example:
349
+ >>> from complio.connectors.aws.client import AWSConnector
350
+ >>> connector = AWSConnector("production", "us-east-1")
351
+ >>> connector.connect()
352
+ >>> result = run_nacl_security_test(connector)
353
+ >>> print(f"Score: {result.score}%")
354
+ """
355
+ test = NACLSecurityTest(connector)
356
+ return test.execute()
@@ -0,0 +1,252 @@
1
+ """
2
+ RDS instance encryption compliance test.
3
+
4
+ Checks that all RDS database instances have storage 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.infrastructure.rds_encryption import RDSEncryptionTest
12
+ >>>
13
+ >>> connector = AWSConnector("production", "us-east-1")
14
+ >>> connector.connect()
15
+ >>>
16
+ >>> test = RDSEncryptionTest(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 RDSEncryptionTest(ComplianceTest):
35
+ """Test for RDS instance encryption compliance.
36
+
37
+ Verifies that all RDS database instances have storage encryption enabled.
38
+
39
+ Compliance Requirements:
40
+ - All RDS instances must have StorageEncrypted=True
41
+ - Both primary and read replicas must be encrypted
42
+ - Instances without encryption are non-compliant
43
+
44
+ Scoring:
45
+ - 100% if all instances are encrypted
46
+ - Proportional score based on encrypted/total ratio
47
+ - 0% if no instances are encrypted
48
+
49
+ Example:
50
+ >>> test = RDSEncryptionTest(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 RDS encryption test.
58
+
59
+ Args:
60
+ connector: AWS connector instance
61
+ """
62
+ super().__init__(
63
+ test_id="rds_encryption",
64
+ test_name="RDS Instance Encryption Check",
65
+ description="Verify all RDS database instances have storage encryption enabled",
66
+ control_id="A.8.24",
67
+ connector=connector,
68
+ scope="regional",
69
+ )
70
+
71
+ def execute(self) -> TestResult:
72
+ """Execute RDS encryption compliance test.
73
+
74
+ Returns:
75
+ TestResult with findings for non-encrypted instances
76
+
77
+ Example:
78
+ >>> test = RDSEncryptionTest(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 RDS client
93
+ rds_client = self.connector.get_client("rds")
94
+
95
+ # List all DB instances
96
+ self.logger.info("listing_rds_instances")
97
+ response = rds_client.describe_db_instances()
98
+ instances = response.get("DBInstances", [])
99
+
100
+ if not instances:
101
+ self.logger.info("no_rds_instances_found")
102
+ result.metadata["message"] = "No RDS instances found in region"
103
+ return result
104
+
105
+ self.logger.info("rds_instances_found", count=len(instances))
106
+
107
+ # Check each instance for encryption
108
+ encrypted_count = 0
109
+ total_count = len(instances)
110
+
111
+ for instance in instances:
112
+ instance_id = instance["DBInstanceIdentifier"]
113
+ encrypted = instance.get("StorageEncrypted", False)
114
+ result.resources_scanned += 1
115
+
116
+ # Get instance details
117
+ engine = instance.get("Engine", "unknown")
118
+ engine_version = instance.get("EngineVersion", "unknown")
119
+ instance_class = instance.get("DBInstanceClass", "unknown")
120
+ storage_size = instance.get("AllocatedStorage", 0)
121
+ instance_status = instance.get("DBInstanceStatus", "unknown")
122
+
123
+ # Create evidence
124
+ evidence = self.create_evidence(
125
+ resource_id=instance_id,
126
+ resource_type="rds_instance",
127
+ data={
128
+ "instance_id": instance_id,
129
+ "encrypted": encrypted,
130
+ "engine": engine,
131
+ "engine_version": engine_version,
132
+ "instance_class": instance_class,
133
+ "storage_size_gb": storage_size,
134
+ "status": instance_status,
135
+ "kms_key_id": instance.get("KmsKeyId"),
136
+ "availability_zone": instance.get("AvailabilityZone"),
137
+ }
138
+ )
139
+ result.add_evidence(evidence)
140
+
141
+ if encrypted:
142
+ encrypted_count += 1
143
+ self.logger.debug(
144
+ "rds_instance_encrypted",
145
+ instance_id=instance_id,
146
+ engine=engine,
147
+ kms_key_id=instance.get("KmsKeyId")
148
+ )
149
+ else:
150
+ # Create finding for non-encrypted instance
151
+ finding = self.create_finding(
152
+ resource_id=instance_id,
153
+ resource_type="rds_instance",
154
+ severity=Severity.HIGH,
155
+ title="RDS instance storage encryption not enabled",
156
+ description=f"RDS instance '{instance_id}' ({engine} {engine_version}, {storage_size}GB) "
157
+ "does not have storage encryption enabled. "
158
+ "This violates ISO 27001 A.8.24 requirement for data-at-rest encryption.",
159
+ remediation=(
160
+ "RDS instances cannot be encrypted after creation. To remediate:\n"
161
+ "1. Create a snapshot of the unencrypted instance:\n"
162
+ f" aws rds create-db-snapshot --db-instance-identifier {instance_id} "
163
+ f"--db-snapshot-identifier {instance_id}-snapshot\n"
164
+ "2. Copy the snapshot with encryption enabled:\n"
165
+ f" aws rds copy-db-snapshot --source-db-snapshot-identifier {instance_id}-snapshot "
166
+ f"--target-db-snapshot-identifier {instance_id}-encrypted-snapshot --kms-key-id <key-id>\n"
167
+ "3. Restore a new instance from the encrypted snapshot:\n"
168
+ f" aws rds restore-db-instance-from-db-snapshot "
169
+ f"--db-instance-identifier {instance_id}-encrypted "
170
+ f"--db-snapshot-identifier {instance_id}-encrypted-snapshot\n"
171
+ "4. Update application connection strings\n"
172
+ "5. Delete the old unencrypted instance\n\n"
173
+ "Note: This will cause downtime during the migration."
174
+ ),
175
+ evidence=evidence
176
+ )
177
+ result.add_finding(finding)
178
+
179
+ self.logger.warning(
180
+ "rds_instance_not_encrypted",
181
+ instance_id=instance_id,
182
+ engine=engine
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_instances": total_count,
196
+ "encrypted_instances": encrypted_count,
197
+ "non_encrypted_instances": total_count - encrypted_count,
198
+ "compliance_percentage": result.score,
199
+ "region": self.connector.region,
200
+ }
201
+
202
+ self.logger.info(
203
+ "rds_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("rds_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("rds_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_rds_encryption_test(connector: AWSConnector) -> TestResult:
234
+ """Run RDS 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_rds_encryption_test(connector)
249
+ >>> print(f"Score: {result.score}%")
250
+ """
251
+ test = RDSEncryptionTest(connector)
252
+ return test.execute()