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,400 @@
1
+ """
2
+ CloudTrail logging compliance test.
3
+
4
+ Checks that AWS CloudTrail is enabled and properly configured for audit logging.
5
+
6
+ ISO 27001 Control: A.12.4.1 - Event Logging
7
+ Requirement: Event logs recording user activities, exceptions, faults and information
8
+ security events shall be produced, kept and regularly reviewed.
9
+
10
+ CloudTrail requirements:
11
+ - At least one trail enabled
12
+ - Multi-region trail enabled
13
+ - Log file validation enabled
14
+ - S3 bucket logging enabled
15
+ - CloudWatch Logs integration (optional but recommended)
16
+
17
+ Example:
18
+ >>> from complio.connectors.aws.client import AWSConnector
19
+ >>> from complio.tests_library.infrastructure.cloudtrail_logging import CloudTrailLoggingTest
20
+ >>>
21
+ >>> connector = AWSConnector("production", "us-east-1")
22
+ >>> connector.connect()
23
+ >>>
24
+ >>> test = CloudTrailLoggingTest(connector)
25
+ >>> result = test.run()
26
+ >>> print(f"Passed: {result.passed}, Score: {result.score}")
27
+ """
28
+
29
+ from typing import Any, Dict, List
30
+
31
+ from botocore.exceptions import ClientError
32
+
33
+ from complio.connectors.aws.client import AWSConnector
34
+ from complio.tests_library.base import (
35
+ ComplianceTest,
36
+ Evidence,
37
+ Finding,
38
+ Severity,
39
+ TestResult,
40
+ TestStatus,
41
+ )
42
+ from complio.utils.logger import get_logger
43
+
44
+
45
+ class CloudTrailLoggingTest(ComplianceTest):
46
+ """Test for CloudTrail logging compliance.
47
+
48
+ Verifies that AWS CloudTrail is properly configured for audit logging.
49
+
50
+ Compliance Requirements (ISO 27001 A.12.4.1):
51
+ - At least one trail must be enabled
52
+ - Multi-region trail should be configured
53
+ - Log file validation must be enabled
54
+ - Logs must be delivered to S3
55
+ - CloudWatch Logs integration recommended
56
+
57
+ Scoring:
58
+ - 100% if all requirements met
59
+ - 0% if no trails configured
60
+ - Deductions for missing features
61
+
62
+ Example:
63
+ >>> test = CloudTrailLoggingTest(connector)
64
+ >>> result = test.run()
65
+ >>> if not result.passed:
66
+ ... for finding in result.findings:
67
+ ... print(f"{finding.severity}: {finding.title}")
68
+ """
69
+
70
+ def __init__(self, connector: AWSConnector) -> None:
71
+ """Initialize CloudTrail logging test.
72
+
73
+ Args:
74
+ connector: AWS connector instance
75
+ """
76
+ super().__init__(
77
+ test_id="cloudtrail_logging",
78
+ test_name="CloudTrail Audit Logging",
79
+ description="Ensures CloudTrail is enabled with log file validation to maintain audit trails (checks trails in specified region)",
80
+ control_id="A.12.4.1",
81
+ connector=connector,
82
+ scope="regional",
83
+ )
84
+ self.logger = get_logger(__name__)
85
+
86
+ def execute(self) -> TestResult:
87
+ """Execute the CloudTrail logging compliance test.
88
+
89
+ Returns:
90
+ TestResult with findings and evidence
91
+
92
+ Raises:
93
+ AWSConnectionError: If unable to connect to AWS
94
+ AWSCredentialsError: If credentials are invalid
95
+ """
96
+ self.logger.info(
97
+ "starting_cloudtrail_logging_test",
98
+ region=self.connector.region,
99
+ )
100
+
101
+ findings: List[Finding] = []
102
+ evidence_list: List[Evidence] = []
103
+
104
+ try:
105
+ # Get CloudTrail client
106
+ cloudtrail_client = self.connector.get_client("cloudtrail")
107
+
108
+ # Describe all trails
109
+ response = cloudtrail_client.describe_trails()
110
+ trails = response.get("trailList", [])
111
+
112
+ # Get trail status for each trail
113
+ trail_statuses = []
114
+ for trail in trails:
115
+ trail_name = trail.get("Name", "unknown")
116
+ try:
117
+ status_response = cloudtrail_client.get_trail_status(Name=trail_name)
118
+ trail_statuses.append({
119
+ "trail": trail,
120
+ "status": status_response,
121
+ })
122
+ except ClientError:
123
+ # If we can't get status, trail might not exist in this region
124
+ continue
125
+
126
+ self.logger.info(
127
+ "cloudtrail_trails_found",
128
+ total_trails=len(trails),
129
+ active_trails=len(trail_statuses),
130
+ region=self.connector.region,
131
+ )
132
+
133
+ # Check if any trails exist
134
+ if not trail_statuses:
135
+ finding = Finding(
136
+ resource_id="aws-account",
137
+ resource_type="cloudtrail",
138
+ severity=Severity.CRITICAL,
139
+ title="No CloudTrail trails configured",
140
+ description=(
141
+ "AWS account has no CloudTrail trails configured. This means no audit "
142
+ "logs are being collected for AWS API calls. Without CloudTrail, you cannot "
143
+ "detect unauthorized access, track changes, or meet ISO 27001 A.12.4.1 requirements."
144
+ ),
145
+ remediation=(
146
+ "Enable CloudTrail:\n"
147
+ "1. Go to CloudTrail Console\n"
148
+ "2. Click 'Create trail'\n"
149
+ "3. Enable 'Apply trail to all regions'\n"
150
+ "4. Enable 'Log file validation'\n"
151
+ "5. Configure S3 bucket for log storage\n"
152
+ "6. Optionally configure CloudWatch Logs"
153
+ ),
154
+ iso27001_control="A.12.4.1",
155
+ )
156
+ findings.append(finding)
157
+
158
+ evidence = Evidence(
159
+ resource_id="aws-account",
160
+ resource_type="cloudtrail",
161
+ region=self.connector.region,
162
+ data={"trails_configured": 0, "trails_active": 0},
163
+ )
164
+ evidence_list.append(evidence)
165
+
166
+ return TestResult(
167
+ test_id=self.test_id,
168
+ test_name=self.test_name,
169
+ status=TestStatus.FAILED,
170
+ passed=False,
171
+ score=0.0,
172
+ findings=findings,
173
+ evidence=evidence_list,
174
+ metadata={
175
+ "region": self.connector.region,
176
+ "trails_configured": 0,
177
+ "iso27001_control": "A.12.4.1",
178
+ },
179
+ )
180
+
181
+ # Analyze each trail
182
+ has_multi_region_trail = False
183
+ has_log_file_validation = False
184
+ has_cloudwatch_logs = False
185
+ active_trails = 0
186
+
187
+ for trail_data in trail_statuses:
188
+ trail = trail_data["trail"]
189
+ status = trail_data["status"]
190
+
191
+ trail_name = trail.get("Name", "unknown")
192
+ trail_arn = trail.get("TrailARN", "N/A")
193
+ is_logging = status.get("IsLogging", False)
194
+ is_multi_region = trail.get("IsMultiRegionTrail", False)
195
+ log_validation = trail.get("LogFileValidationEnabled", False)
196
+ cloudwatch_logs_arn = trail.get("CloudWatchLogsLogGroupArn")
197
+
198
+ if is_logging:
199
+ active_trails += 1
200
+
201
+ if is_multi_region:
202
+ has_multi_region_trail = True
203
+
204
+ if log_validation:
205
+ has_log_file_validation = True
206
+
207
+ if cloudwatch_logs_arn:
208
+ has_cloudwatch_logs = True
209
+
210
+ # Create evidence for each trail
211
+ evidence = Evidence(
212
+ resource_id=trail_arn,
213
+ resource_type="cloudtrail",
214
+ region=self.connector.region,
215
+ data={
216
+ "trail_name": trail_name,
217
+ "is_logging": is_logging,
218
+ "is_multi_region": is_multi_region,
219
+ "log_file_validation": log_validation,
220
+ "s3_bucket": trail.get("S3BucketName", "N/A"),
221
+ "has_cloudwatch_logs": cloudwatch_logs_arn is not None,
222
+ },
223
+ )
224
+ evidence_list.append(evidence)
225
+
226
+ # Check for issues with this trail
227
+ if not is_logging:
228
+ findings.append(
229
+ Finding(
230
+ resource_id=trail_arn,
231
+ resource_type="cloudtrail",
232
+ severity=Severity.HIGH,
233
+ title=f"CloudTrail '{trail_name}' is not logging",
234
+ description=(
235
+ f"Trail '{trail_name}' is configured but not currently logging. "
236
+ f"This means audit events are not being recorded."
237
+ ),
238
+ remediation=f"Enable logging for trail '{trail_name}' using CloudTrail console or AWS CLI",
239
+ iso27001_control="A.12.4.1",
240
+ metadata={"trail_name": trail_name},
241
+ )
242
+ )
243
+
244
+ # Check for missing features across all trails
245
+ score = 100.0
246
+ score_deductions = 0
247
+
248
+ if not has_multi_region_trail:
249
+ score_deductions += 20
250
+ findings.append(
251
+ Finding(
252
+ resource_id="aws-account",
253
+ resource_type="cloudtrail",
254
+ severity=Severity.HIGH,
255
+ title="No multi-region CloudTrail trail configured",
256
+ description=(
257
+ "None of the CloudTrail trails are configured for multi-region logging. "
258
+ "This means API calls in other regions are not being logged, creating audit gaps."
259
+ ),
260
+ remediation=(
261
+ "Configure at least one trail as multi-region:\n"
262
+ "1. Go to CloudTrail Console\n"
263
+ "2. Select a trail or create new one\n"
264
+ "3. Enable 'Apply trail to all regions'"
265
+ ),
266
+ iso27001_control="A.12.4.1",
267
+ )
268
+ )
269
+
270
+ if not has_log_file_validation:
271
+ score_deductions += 15
272
+ findings.append(
273
+ Finding(
274
+ resource_id="aws-account",
275
+ resource_type="cloudtrail",
276
+ severity=Severity.MEDIUM,
277
+ title="Log file validation not enabled",
278
+ description=(
279
+ "None of the CloudTrail trails have log file validation enabled. "
280
+ "Without this, you cannot verify that log files haven't been tampered with."
281
+ ),
282
+ remediation=(
283
+ "Enable log file validation:\n"
284
+ "1. Go to CloudTrail Console\n"
285
+ "2. Select a trail\n"
286
+ "3. Edit trail settings\n"
287
+ "4. Enable 'Enable log file validation'"
288
+ ),
289
+ iso27001_control="A.12.4.1",
290
+ )
291
+ )
292
+
293
+ if not has_cloudwatch_logs:
294
+ score_deductions += 10
295
+ findings.append(
296
+ Finding(
297
+ resource_id="aws-account",
298
+ resource_type="cloudtrail",
299
+ severity=Severity.LOW,
300
+ title="CloudWatch Logs integration not configured",
301
+ description=(
302
+ "CloudTrail is not integrated with CloudWatch Logs. "
303
+ "While not required, this integration enables real-time monitoring "
304
+ "and alerting on suspicious API activity."
305
+ ),
306
+ remediation=(
307
+ "Configure CloudWatch Logs (optional but recommended):\n"
308
+ "1. Go to CloudTrail Console\n"
309
+ "2. Select a trail\n"
310
+ "3. Edit trail settings\n"
311
+ "4. Configure CloudWatch Logs log group\n"
312
+ "5. Create IAM role for CloudTrail to write to CloudWatch"
313
+ ),
314
+ iso27001_control="A.12.4.1",
315
+ )
316
+ )
317
+
318
+ if active_trails == 0:
319
+ score_deductions += 50 # Critical issue
320
+ findings.append(
321
+ Finding(
322
+ resource_id="aws-account",
323
+ resource_type="cloudtrail",
324
+ severity=Severity.CRITICAL,
325
+ title="No active CloudTrail logging",
326
+ description=(
327
+ f"{len(trails)} trail(s) configured but none are actively logging. "
328
+ f"No audit logs are being collected."
329
+ ),
330
+ remediation="Start logging on at least one CloudTrail trail",
331
+ iso27001_control="A.12.4.1",
332
+ )
333
+ )
334
+
335
+ score = max(0.0, score - score_deductions)
336
+
337
+ if score >= 90:
338
+ status = TestStatus.PASSED
339
+ passed = True
340
+ elif score >= 70:
341
+ status = TestStatus.WARNING
342
+ passed = False
343
+ else:
344
+ status = TestStatus.FAILED
345
+ passed = False
346
+
347
+ self.logger.info(
348
+ "cloudtrail_logging_test_complete",
349
+ total_trails=len(trails),
350
+ active_trails=active_trails,
351
+ has_multi_region=has_multi_region_trail,
352
+ score=score,
353
+ )
354
+
355
+ return TestResult(
356
+ test_id=self.test_id,
357
+ test_name=self.test_name,
358
+ status=status,
359
+ passed=passed,
360
+ score=score,
361
+ findings=findings,
362
+ evidence=evidence_list,
363
+ metadata={
364
+ "region": self.connector.region,
365
+ "total_trails": len(trails),
366
+ "active_trails": active_trails,
367
+ "has_multi_region_trail": has_multi_region_trail,
368
+ "has_log_file_validation": has_log_file_validation,
369
+ "has_cloudwatch_logs": has_cloudwatch_logs,
370
+ "iso27001_control": "A.12.4.1",
371
+ },
372
+ )
373
+
374
+ except ClientError as e:
375
+ self.logger.error(
376
+ "cloudtrail_logging_test_failed",
377
+ error=str(e),
378
+ error_code=e.response.get("Error", {}).get("Code"),
379
+ )
380
+
381
+ return TestResult(
382
+ test_id=self.test_id,
383
+ test_name=self.test_name,
384
+ status=TestStatus.ERROR,
385
+ passed=False,
386
+ score=0.0,
387
+ findings=[
388
+ Finding(
389
+ resource_id="aws-account",
390
+ resource_type="cloudtrail",
391
+ severity=Severity.HIGH,
392
+ title="Failed to check CloudTrail configuration",
393
+ description=f"Error accessing CloudTrail: {str(e)}",
394
+ remediation="Check AWS credentials and permissions. Ensure IAM policy allows cloudtrail:DescribeTrails and cloudtrail:GetTrailStatus",
395
+ iso27001_control="A.12.4.1",
396
+ )
397
+ ],
398
+ evidence=[],
399
+ metadata={"error": str(e)},
400
+ )
@@ -0,0 +1,244 @@
1
+ """
2
+ EBS volume encryption compliance test.
3
+
4
+ Checks that all EBS volumes 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.infrastructure.ebs_encryption import EBSEncryptionTest
12
+ >>>
13
+ >>> connector = AWSConnector("production", "us-east-1")
14
+ >>> connector.connect()
15
+ >>>
16
+ >>> test = EBSEncryptionTest(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 EBSEncryptionTest(ComplianceTest):
35
+ """Test for EBS volume encryption compliance.
36
+
37
+ Verifies that all EBS volumes have encryption enabled.
38
+
39
+ Compliance Requirements:
40
+ - All EBS volumes must be encrypted
41
+ - Both root and non-root volumes must be encrypted
42
+ - Volumes without encryption are non-compliant
43
+
44
+ Scoring:
45
+ - 100% if all volumes are encrypted
46
+ - Proportional score based on encrypted/total ratio
47
+ - 0% if no volumes are encrypted
48
+
49
+ Example:
50
+ >>> test = EBSEncryptionTest(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 EBS encryption test.
58
+
59
+ Args:
60
+ connector: AWS connector instance
61
+ """
62
+ super().__init__(
63
+ test_id="ebs_encryption",
64
+ test_name="EBS Volume Encryption Check",
65
+ description="Verify all EBS volumes have encryption enabled",
66
+ control_id="A.8.24",
67
+ connector=connector,
68
+ scope="regional",
69
+ )
70
+
71
+ def execute(self) -> TestResult:
72
+ """Execute EBS encryption compliance test.
73
+
74
+ Returns:
75
+ TestResult with findings for non-encrypted volumes
76
+
77
+ Example:
78
+ >>> test = EBSEncryptionTest(connector)
79
+ >>> result = test.execute()
80
+ >>> print(result.score)
81
+ 92.3
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 EC2 client
93
+ ec2_client = self.connector.get_client("ec2")
94
+
95
+ # List all volumes
96
+ self.logger.info("listing_ebs_volumes")
97
+ response = ec2_client.describe_volumes()
98
+ volumes = response.get("Volumes", [])
99
+
100
+ if not volumes:
101
+ self.logger.info("no_ebs_volumes_found")
102
+ result.metadata["message"] = "No EBS volumes found in region"
103
+ return result
104
+
105
+ self.logger.info("ebs_volumes_found", count=len(volumes))
106
+
107
+ # Check each volume for encryption
108
+ encrypted_count = 0
109
+ total_count = len(volumes)
110
+
111
+ for volume in volumes:
112
+ volume_id = volume["VolumeId"]
113
+ encrypted = volume.get("Encrypted", False)
114
+ result.resources_scanned += 1
115
+
116
+ # Get volume details
117
+ volume_size = volume.get("Size", 0)
118
+ volume_type = volume.get("VolumeType", "unknown")
119
+ state = volume.get("State", "unknown")
120
+ availability_zone = volume.get("AvailabilityZone", "unknown")
121
+
122
+ # Create evidence
123
+ evidence = self.create_evidence(
124
+ resource_id=volume_id,
125
+ resource_type="ebs_volume",
126
+ data={
127
+ "volume_id": volume_id,
128
+ "encrypted": encrypted,
129
+ "size_gb": volume_size,
130
+ "volume_type": volume_type,
131
+ "state": state,
132
+ "availability_zone": availability_zone,
133
+ "kms_key_id": volume.get("KmsKeyId"),
134
+ }
135
+ )
136
+ result.add_evidence(evidence)
137
+
138
+ if encrypted:
139
+ encrypted_count += 1
140
+ self.logger.debug(
141
+ "volume_encrypted",
142
+ volume_id=volume_id,
143
+ kms_key_id=volume.get("KmsKeyId")
144
+ )
145
+ else:
146
+ # Create finding for non-encrypted volume
147
+ finding = self.create_finding(
148
+ resource_id=volume_id,
149
+ resource_type="ebs_volume",
150
+ severity=Severity.HIGH,
151
+ title="EBS volume encryption not enabled",
152
+ description=f"Volume '{volume_id}' ({volume_size}GB, {volume_type}) does not have encryption enabled. "
153
+ "This violates ISO 27001 A.8.24 requirement for data-at-rest encryption.",
154
+ remediation=(
155
+ "EBS volumes cannot be encrypted after creation. To remediate:\n"
156
+ "1. Create a snapshot of the unencrypted volume\n"
157
+ "2. Copy the snapshot with encryption enabled:\n"
158
+ f" aws ec2 copy-snapshot --source-region {self.connector.region} "
159
+ f"--source-snapshot-id <snap-id> --encrypted\n"
160
+ "3. Create a new volume from the encrypted snapshot\n"
161
+ "4. Attach the new volume to the instance\n"
162
+ "5. Update /etc/fstab if needed\n"
163
+ "6. Delete the old unencrypted volume\n\n"
164
+ "For future volumes, enable encryption by default:\n"
165
+ "aws ec2 enable-ebs-encryption-by-default --region " + self.connector.region
166
+ ),
167
+ evidence=evidence
168
+ )
169
+ result.add_finding(finding)
170
+
171
+ self.logger.warning(
172
+ "volume_not_encrypted",
173
+ volume_id=volume_id,
174
+ size_gb=volume_size
175
+ )
176
+
177
+ # Calculate compliance score
178
+ if total_count > 0:
179
+ result.score = (encrypted_count / total_count) * 100
180
+
181
+ # Determine pass/fail
182
+ result.passed = encrypted_count == total_count
183
+ result.status = TestStatus.PASSED if result.passed else TestStatus.FAILED
184
+
185
+ # Add metadata
186
+ result.metadata = {
187
+ "total_volumes": total_count,
188
+ "encrypted_volumes": encrypted_count,
189
+ "non_encrypted_volumes": total_count - encrypted_count,
190
+ "compliance_percentage": result.score,
191
+ "region": self.connector.region,
192
+ }
193
+
194
+ self.logger.info(
195
+ "ebs_encryption_test_completed",
196
+ total=total_count,
197
+ encrypted=encrypted_count,
198
+ score=result.score,
199
+ passed=result.passed
200
+ )
201
+
202
+ except ClientError as e:
203
+ error_code = e.response.get("Error", {}).get("Code")
204
+ self.logger.error("ebs_encryption_test_error", error_code=error_code, error=str(e))
205
+ result.status = TestStatus.ERROR
206
+ result.passed = False
207
+ result.score = 0.0
208
+ result.error_message = f"AWS API Error: {error_code} - {str(e)}"
209
+
210
+ except Exception as e:
211
+ self.logger.error("ebs_encryption_test_error", error=str(e))
212
+ result.status = TestStatus.ERROR
213
+ result.passed = False
214
+ result.score = 0.0
215
+ result.error_message = str(e)
216
+
217
+ return result
218
+
219
+
220
+ # ============================================================================
221
+ # CONVENIENCE FUNCTION
222
+ # ============================================================================
223
+
224
+
225
+ def run_ebs_encryption_test(connector: AWSConnector) -> TestResult:
226
+ """Run EBS encryption compliance test.
227
+
228
+ Convenience function for running the test.
229
+
230
+ Args:
231
+ connector: AWS connector
232
+
233
+ Returns:
234
+ TestResult
235
+
236
+ Example:
237
+ >>> from complio.connectors.aws.client import AWSConnector
238
+ >>> connector = AWSConnector("production", "us-east-1")
239
+ >>> connector.connect()
240
+ >>> result = run_ebs_encryption_test(connector)
241
+ >>> print(f"Score: {result.score}%")
242
+ """
243
+ test = EBSEncryptionTest(connector)
244
+ return test.execute()