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,314 @@
1
+ """
2
+ KMS key rotation compliance test.
3
+
4
+ Checks that all customer-managed KMS keys have automatic rotation enabled.
5
+
6
+ ISO 27001 Control: A.8.24 - Use of cryptography
7
+ Requirement: Cryptographic keys must be rotated regularly
8
+
9
+ Example:
10
+ >>> from complio.connectors.aws.client import AWSConnector
11
+ >>> from complio.tests_library.security.kms_key_rotation import KMSKeyRotationTest
12
+ >>>
13
+ >>> connector = AWSConnector("production", "us-east-1")
14
+ >>> connector.connect()
15
+ >>>
16
+ >>> test = KMSKeyRotationTest(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 KMSKeyRotationTest(ComplianceTest):
35
+ """Test for KMS key rotation compliance.
36
+
37
+ Verifies that all customer-managed KMS keys have automatic rotation enabled.
38
+ AWS-managed keys are automatically rotated and are skipped.
39
+
40
+ Compliance Requirements:
41
+ - Customer-managed keys must have KeyRotationEnabled=True
42
+ - AWS-managed keys are automatically rotated (not checked)
43
+ - Keys without rotation enabled are non-compliant
44
+
45
+ Scoring:
46
+ - 100% if all customer-managed keys have rotation enabled
47
+ - Proportional score based on compliant/total ratio
48
+ - 0% if no keys have rotation enabled
49
+
50
+ Example:
51
+ >>> test = KMSKeyRotationTest(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 KMS key rotation test.
59
+
60
+ Args:
61
+ connector: AWS connector instance
62
+ """
63
+ super().__init__(
64
+ test_id="kms_key_rotation",
65
+ test_name="KMS Key Rotation Check",
66
+ description="Verify all customer-managed KMS keys have automatic rotation enabled",
67
+ control_id="A.8.24",
68
+ connector=connector,
69
+ scope="regional",
70
+ )
71
+
72
+ def execute(self) -> TestResult:
73
+ """Execute KMS key rotation compliance test.
74
+
75
+ Returns:
76
+ TestResult with findings for keys without rotation
77
+
78
+ Example:
79
+ >>> test = KMSKeyRotationTest(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 KMS client
94
+ kms_client = self.connector.get_client("kms")
95
+
96
+ # List all keys
97
+ self.logger.info("listing_kms_keys")
98
+ key_ids = []
99
+
100
+ # Handle pagination
101
+ paginator = kms_client.get_paginator("list_keys")
102
+ for page in paginator.paginate():
103
+ for key in page.get("Keys", []):
104
+ key_ids.append(key["KeyId"])
105
+
106
+ if not key_ids:
107
+ self.logger.info("no_kms_keys_found")
108
+ result.metadata["message"] = "No KMS keys found in region"
109
+ return result
110
+
111
+ self.logger.info("kms_keys_found", count=len(key_ids))
112
+
113
+ # Filter customer-managed keys and check rotation
114
+ customer_managed_keys = []
115
+ rotation_enabled_count = 0
116
+
117
+ for key_id in key_ids:
118
+ try:
119
+ # Describe key to get metadata
120
+ key_metadata_response = kms_client.describe_key(KeyId=key_id)
121
+ key_metadata = key_metadata_response.get("KeyMetadata", {})
122
+
123
+ # Skip AWS-managed keys (they are automatically rotated)
124
+ key_manager = key_metadata.get("KeyManager")
125
+ if key_manager == "AWS":
126
+ self.logger.debug("skipping_aws_managed_key", key_id=key_id)
127
+ continue
128
+
129
+ # Skip keys that are not enabled
130
+ key_state = key_metadata.get("KeyState")
131
+ if key_state not in ["Enabled", "Disabled"]:
132
+ # Skip keys in PendingDeletion, PendingImport, etc.
133
+ self.logger.debug("skipping_key_in_state", key_id=key_id, state=key_state)
134
+ continue
135
+
136
+ # This is a customer-managed key
137
+ customer_managed_keys.append(key_id)
138
+ result.resources_scanned += 1
139
+
140
+ # Get key alias for better reporting
141
+ key_alias = "no-alias"
142
+ try:
143
+ aliases_response = kms_client.list_aliases(KeyId=key_id)
144
+ aliases = aliases_response.get("Aliases", [])
145
+ if aliases:
146
+ key_alias = aliases[0].get("AliasName", "no-alias")
147
+ except Exception:
148
+ pass
149
+
150
+ # Check rotation status
151
+ try:
152
+ rotation_response = kms_client.get_key_rotation_status(KeyId=key_id)
153
+ rotation_enabled = rotation_response.get("KeyRotationEnabled", False)
154
+ except ClientError as e:
155
+ error_code = e.response.get("Error", {}).get("Code")
156
+ if error_code == "UnsupportedOperationException":
157
+ # Asymmetric keys don't support automatic rotation
158
+ self.logger.debug("key_rotation_not_supported", key_id=key_id)
159
+ rotation_enabled = False
160
+ else:
161
+ raise
162
+
163
+ # Get key details
164
+ key_arn = key_metadata.get("Arn")
165
+ creation_date = key_metadata.get("CreationDate")
166
+ description = key_metadata.get("Description", "")
167
+ key_usage = key_metadata.get("KeyUsage", "unknown")
168
+ key_spec = key_metadata.get("KeySpec", "unknown")
169
+
170
+ # Create evidence
171
+ evidence = self.create_evidence(
172
+ resource_id=key_id,
173
+ resource_type="kms_key",
174
+ data={
175
+ "key_id": key_id,
176
+ "key_arn": key_arn,
177
+ "key_alias": key_alias,
178
+ "rotation_enabled": rotation_enabled,
179
+ "key_state": key_state,
180
+ "key_manager": key_manager,
181
+ "key_usage": key_usage,
182
+ "key_spec": key_spec,
183
+ "description": description,
184
+ "creation_date": creation_date.isoformat() if creation_date else None,
185
+ }
186
+ )
187
+ result.add_evidence(evidence)
188
+
189
+ if rotation_enabled:
190
+ rotation_enabled_count += 1
191
+ self.logger.debug(
192
+ "kms_key_rotation_enabled",
193
+ key_id=key_id,
194
+ key_alias=key_alias
195
+ )
196
+ else:
197
+ # Create finding for key without rotation
198
+ finding = self.create_finding(
199
+ resource_id=key_id,
200
+ resource_type="kms_key",
201
+ severity=Severity.HIGH,
202
+ title="KMS key automatic rotation not enabled",
203
+ description=f"Customer-managed KMS key '{key_alias}' ({key_id}) does not have "
204
+ f"automatic rotation enabled. Key is {key_state.lower()} and used for {key_usage}. "
205
+ "Without rotation, compromised keys remain valid indefinitely. "
206
+ "ISO 27001 A.8.24 requires regular key rotation to minimize risk.",
207
+ remediation=(
208
+ f"Enable automatic key rotation for KMS key '{key_id}':\n\n"
209
+ "Using AWS CLI:\n"
210
+ f"aws kms enable-key-rotation --key-id {key_id}\n\n"
211
+ "Or use AWS Console:\n"
212
+ "1. Go to AWS KMS → Customer managed keys\n"
213
+ f"2. Select key '{key_alias}' ({key_id})\n"
214
+ "3. Go to 'Key rotation' tab\n"
215
+ "4. Enable 'Automatic key rotation'\n"
216
+ "5. Click 'Save'\n\n"
217
+ "Note: KMS automatically rotates the key material every year.\n"
218
+ "Existing ciphertext can still be decrypted with old key material.\n"
219
+ "Asymmetric keys do not support automatic rotation."
220
+ ),
221
+ evidence=evidence
222
+ )
223
+ result.add_finding(finding)
224
+
225
+ self.logger.warning(
226
+ "kms_key_rotation_disabled",
227
+ key_id=key_id,
228
+ key_alias=key_alias,
229
+ key_state=key_state
230
+ )
231
+
232
+ except ClientError as e:
233
+ error_code = e.response.get("Error", {}).get("Code")
234
+ if error_code in ["AccessDeniedException", "NotFoundException"]:
235
+ self.logger.warning("key_access_denied", key_id=key_id, error_code=error_code)
236
+ continue
237
+ else:
238
+ raise
239
+
240
+ total_customer_keys = len(customer_managed_keys)
241
+
242
+ if total_customer_keys == 0:
243
+ self.logger.info("no_customer_managed_keys_found")
244
+ result.metadata["message"] = "No customer-managed KMS keys found in region"
245
+ return result
246
+
247
+ # Calculate compliance score
248
+ result.score = (rotation_enabled_count / total_customer_keys) * 100
249
+
250
+ # Determine pass/fail
251
+ result.passed = rotation_enabled_count == total_customer_keys
252
+ result.status = TestStatus.PASSED if result.passed else TestStatus.FAILED
253
+
254
+ # Add metadata
255
+ result.metadata = {
256
+ "total_keys_scanned": len(key_ids),
257
+ "customer_managed_keys": total_customer_keys,
258
+ "rotation_enabled": rotation_enabled_count,
259
+ "rotation_disabled": total_customer_keys - rotation_enabled_count,
260
+ "compliance_percentage": result.score,
261
+ "region": self.connector.region,
262
+ }
263
+
264
+ self.logger.info(
265
+ "kms_key_rotation_test_completed",
266
+ total_customer_keys=total_customer_keys,
267
+ rotation_enabled=rotation_enabled_count,
268
+ score=result.score,
269
+ passed=result.passed
270
+ )
271
+
272
+ except ClientError as e:
273
+ error_code = e.response.get("Error", {}).get("Code")
274
+ self.logger.error("kms_key_rotation_test_error", error_code=error_code, error=str(e))
275
+ result.status = TestStatus.ERROR
276
+ result.passed = False
277
+ result.score = 0.0
278
+ result.error_message = f"AWS API Error: {error_code} - {str(e)}"
279
+
280
+ except Exception as e:
281
+ self.logger.error("kms_key_rotation_test_error", error=str(e))
282
+ result.status = TestStatus.ERROR
283
+ result.passed = False
284
+ result.score = 0.0
285
+ result.error_message = str(e)
286
+
287
+ return result
288
+
289
+
290
+ # ============================================================================
291
+ # CONVENIENCE FUNCTION
292
+ # ============================================================================
293
+
294
+
295
+ def run_kms_key_rotation_test(connector: AWSConnector) -> TestResult:
296
+ """Run KMS key rotation compliance test.
297
+
298
+ Convenience function for running the test.
299
+
300
+ Args:
301
+ connector: AWS connector
302
+
303
+ Returns:
304
+ TestResult
305
+
306
+ Example:
307
+ >>> from complio.connectors.aws.client import AWSConnector
308
+ >>> connector = AWSConnector("production", "us-east-1")
309
+ >>> connector.connect()
310
+ >>> result = run_kms_key_rotation_test(connector)
311
+ >>> print(f"Score: {result.score}%")
312
+ """
313
+ test = KMSKeyRotationTest(connector)
314
+ return test.execute()
File without changes
@@ -0,0 +1,288 @@
1
+ """
2
+ AWS Backup encryption compliance test.
3
+
4
+ Checks that all AWS Backup recovery points use encryption.
5
+
6
+ ISO 27001 Control: A.8.24 - Use of cryptography
7
+ Requirement: Backup data must be encrypted at rest
8
+
9
+ Example:
10
+ >>> from complio.connectors.aws.client import AWSConnector
11
+ >>> from complio.tests_library.storage.backup_encryption import BackupEncryptionTest
12
+ >>>
13
+ >>> connector = AWSConnector("production", "us-east-1")
14
+ >>> connector.connect()
15
+ >>>
16
+ >>> test = BackupEncryptionTest(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 BackupEncryptionTest(ComplianceTest):
35
+ """Test for AWS Backup encryption compliance.
36
+
37
+ Verifies that all AWS Backup recovery points use encryption to protect
38
+ backup data at rest.
39
+
40
+ Compliance Requirements:
41
+ - All backup recovery points must be encrypted
42
+ - Encryption protects backup data from unauthorized access
43
+ - Applies to all supported AWS resources (EC2, RDS, EBS, EFS, DynamoDB, etc.)
44
+
45
+ Scoring:
46
+ - 100% if all recovery points are encrypted
47
+ - Proportional score based on compliant/total ratio
48
+ - 100% if no recovery points exist (no backups configured)
49
+
50
+ Example:
51
+ >>> test = BackupEncryptionTest(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 AWS Backup encryption test.
59
+
60
+ Args:
61
+ connector: AWS connector instance
62
+ """
63
+ super().__init__(
64
+ test_id="backup_encryption",
65
+ test_name="AWS Backup Encryption Check",
66
+ description="Verify all AWS Backup recovery points are encrypted",
67
+ control_id="A.8.24",
68
+ connector=connector,
69
+ scope="regional",
70
+ )
71
+
72
+ def execute(self) -> TestResult:
73
+ """Execute AWS Backup encryption compliance test.
74
+
75
+ Returns:
76
+ TestResult with findings for unencrypted recovery points
77
+
78
+ Example:
79
+ >>> test = BackupEncryptionTest(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 Backup client
94
+ backup_client = self.connector.get_client("backup")
95
+
96
+ # List all backup vaults
97
+ self.logger.info("listing_backup_vaults")
98
+ vaults_response = backup_client.list_backup_vaults()
99
+ vaults = vaults_response.get("BackupVaultList", [])
100
+
101
+ if not vaults:
102
+ self.logger.info("no_backup_vaults_found")
103
+ result.metadata["message"] = "No AWS Backup vaults found in region"
104
+ return result
105
+
106
+ self.logger.info("backup_vaults_found", count=len(vaults))
107
+
108
+ # Track recovery points across all vaults
109
+ total_recovery_points = 0
110
+ encrypted_recovery_points = 0
111
+
112
+ # Check recovery points in each vault
113
+ for vault in vaults:
114
+ vault_name = vault["BackupVaultName"]
115
+
116
+ try:
117
+ # List recovery points in vault
118
+ paginator = backup_client.get_paginator("list_recovery_points_by_backup_vault")
119
+
120
+ for page in paginator.paginate(BackupVaultName=vault_name):
121
+ recovery_points = page.get("RecoveryPoints", [])
122
+
123
+ for recovery_point in recovery_points:
124
+ recovery_point_arn = recovery_point.get("RecoveryPointArn")
125
+ resource_arn = recovery_point.get("ResourceArn")
126
+ resource_type = recovery_point.get("ResourceType")
127
+ is_encrypted = recovery_point.get("IsEncrypted", False)
128
+ creation_date = recovery_point.get("CreationDate")
129
+ status = recovery_point.get("Status")
130
+
131
+ # Only check completed recovery points
132
+ if status != "COMPLETED":
133
+ continue
134
+
135
+ total_recovery_points += 1
136
+ result.resources_scanned += 1
137
+
138
+ # Create evidence
139
+ evidence = self.create_evidence(
140
+ resource_id=recovery_point_arn,
141
+ resource_type="backup_recovery_point",
142
+ data={
143
+ "recovery_point_arn": recovery_point_arn,
144
+ "resource_arn": resource_arn,
145
+ "resource_type": resource_type,
146
+ "is_encrypted": is_encrypted,
147
+ "backup_vault_name": vault_name,
148
+ "creation_date": creation_date.isoformat() if creation_date else None,
149
+ "status": status,
150
+ }
151
+ )
152
+ result.add_evidence(evidence)
153
+
154
+ if is_encrypted:
155
+ encrypted_recovery_points += 1
156
+ self.logger.debug(
157
+ "recovery_point_encrypted",
158
+ recovery_point_arn=recovery_point_arn,
159
+ resource_type=resource_type
160
+ )
161
+ else:
162
+ # Create finding for unencrypted recovery point
163
+ finding = self.create_finding(
164
+ resource_id=recovery_point_arn,
165
+ resource_type="backup_recovery_point",
166
+ severity=Severity.HIGH,
167
+ title="AWS Backup recovery point not encrypted",
168
+ description=f"Backup recovery point for resource '{resource_arn}' "
169
+ f"(type: {resource_type}) in vault '{vault_name}' is not encrypted. "
170
+ "Unencrypted backups expose sensitive data to unauthorized access. "
171
+ "All backup data must be encrypted at rest to comply with "
172
+ "ISO 27001 A.8.24 cryptographic controls.",
173
+ remediation=(
174
+ "AWS Backup automatically encrypts recovery points based on the source resource encryption:\n\n"
175
+ "1. Enable encryption on source resources BEFORE backing them up:\n"
176
+ " - EBS volumes: Enable encryption\n"
177
+ " - RDS instances: Enable storage encryption\n"
178
+ " - EFS file systems: Enable encryption at rest\n"
179
+ " - DynamoDB tables: Enable encryption\n"
180
+ " - S3 buckets: Enable default encryption\n\n"
181
+ "2. Delete existing unencrypted recovery points:\n"
182
+ f" aws backup delete-recovery-point \\\n"
183
+ f" --backup-vault-name {vault_name} \\\n"
184
+ f" --recovery-point-arn {recovery_point_arn}\n\n"
185
+ "3. Create new backup after encrypting source resource:\n"
186
+ " aws backup start-backup-job \\\n"
187
+ f" --backup-vault-name {vault_name} \\\n"
188
+ f" --resource-arn {resource_arn} \\\n"
189
+ " --iam-role-arn <backup-role-arn>\n\n"
190
+ "Note: You cannot encrypt existing recovery points in-place. "
191
+ "You must encrypt the source resource and create new backups."
192
+ ),
193
+ evidence=evidence
194
+ )
195
+ result.add_finding(finding)
196
+
197
+ self.logger.warning(
198
+ "recovery_point_not_encrypted",
199
+ recovery_point_arn=recovery_point_arn,
200
+ resource_type=resource_type,
201
+ vault=vault_name
202
+ )
203
+
204
+ except ClientError as e:
205
+ error_code = e.response.get("Error", {}).get("Code")
206
+ if error_code in ["ResourceNotFoundException", "AccessDeniedException"]:
207
+ self.logger.warning(
208
+ "backup_vault_access_error",
209
+ vault=vault_name,
210
+ error_code=error_code
211
+ )
212
+ continue
213
+ else:
214
+ raise
215
+
216
+ # Handle case where no recovery points exist
217
+ if total_recovery_points == 0:
218
+ self.logger.info("no_recovery_points_found")
219
+ result.metadata["message"] = "No completed recovery points found in any backup vault"
220
+ return result
221
+
222
+ # Calculate compliance score
223
+ result.score = (encrypted_recovery_points / total_recovery_points) * 100
224
+
225
+ # Determine pass/fail
226
+ result.passed = encrypted_recovery_points == total_recovery_points
227
+ result.status = TestStatus.PASSED if result.passed else TestStatus.FAILED
228
+
229
+ # Add metadata
230
+ result.metadata = {
231
+ "total_vaults": len(vaults),
232
+ "total_recovery_points": total_recovery_points,
233
+ "encrypted_recovery_points": encrypted_recovery_points,
234
+ "unencrypted_recovery_points": total_recovery_points - encrypted_recovery_points,
235
+ "compliance_percentage": result.score,
236
+ }
237
+
238
+ self.logger.info(
239
+ "backup_encryption_test_completed",
240
+ total_recovery_points=total_recovery_points,
241
+ encrypted=encrypted_recovery_points,
242
+ score=result.score,
243
+ passed=result.passed
244
+ )
245
+
246
+ except ClientError as e:
247
+ error_code = e.response.get("Error", {}).get("Code")
248
+ self.logger.error("backup_encryption_test_error", error_code=error_code, error=str(e))
249
+ result.status = TestStatus.ERROR
250
+ result.passed = False
251
+ result.score = 0.0
252
+ result.error_message = f"AWS API Error: {error_code} - {str(e)}"
253
+
254
+ except Exception as e:
255
+ self.logger.error("backup_encryption_test_error", error=str(e))
256
+ result.status = TestStatus.ERROR
257
+ result.passed = False
258
+ result.score = 0.0
259
+ result.error_message = str(e)
260
+
261
+ return result
262
+
263
+
264
+ # ============================================================================
265
+ # CONVENIENCE FUNCTION
266
+ # ============================================================================
267
+
268
+
269
+ def run_backup_encryption_test(connector: AWSConnector) -> TestResult:
270
+ """Run AWS Backup encryption compliance test.
271
+
272
+ Convenience function for running the test.
273
+
274
+ Args:
275
+ connector: AWS connector
276
+
277
+ Returns:
278
+ TestResult
279
+
280
+ Example:
281
+ >>> from complio.connectors.aws.client import AWSConnector
282
+ >>> connector = AWSConnector("production", "us-east-1")
283
+ >>> connector.connect()
284
+ >>> result = run_backup_encryption_test(connector)
285
+ >>> print(f"Score: {result.score}%")
286
+ """
287
+ test = BackupEncryptionTest(connector)
288
+ return test.execute()