complio 0.1.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- CHANGELOG.md +208 -0
- README.md +343 -0
- complio/__init__.py +48 -0
- complio/cli/__init__.py +0 -0
- complio/cli/banner.py +87 -0
- complio/cli/commands/__init__.py +0 -0
- complio/cli/commands/history.py +439 -0
- complio/cli/commands/scan.py +700 -0
- complio/cli/main.py +115 -0
- complio/cli/output.py +338 -0
- complio/config/__init__.py +17 -0
- complio/config/settings.py +333 -0
- complio/connectors/__init__.py +9 -0
- complio/connectors/aws/__init__.py +0 -0
- complio/connectors/aws/client.py +342 -0
- complio/connectors/base.py +135 -0
- complio/core/__init__.py +10 -0
- complio/core/registry.py +228 -0
- complio/core/runner.py +351 -0
- complio/py.typed +0 -0
- complio/reporters/__init__.py +7 -0
- complio/reporters/generator.py +417 -0
- complio/tests_library/__init__.py +0 -0
- complio/tests_library/base.py +492 -0
- complio/tests_library/identity/__init__.py +0 -0
- complio/tests_library/identity/access_key_rotation.py +302 -0
- complio/tests_library/identity/mfa_enforcement.py +327 -0
- complio/tests_library/identity/root_account_protection.py +470 -0
- complio/tests_library/infrastructure/__init__.py +0 -0
- complio/tests_library/infrastructure/cloudtrail_encryption.py +286 -0
- complio/tests_library/infrastructure/cloudtrail_log_validation.py +274 -0
- complio/tests_library/infrastructure/cloudtrail_logging.py +400 -0
- complio/tests_library/infrastructure/ebs_encryption.py +244 -0
- complio/tests_library/infrastructure/ec2_security_groups.py +321 -0
- complio/tests_library/infrastructure/iam_password_policy.py +460 -0
- complio/tests_library/infrastructure/nacl_security.py +356 -0
- complio/tests_library/infrastructure/rds_encryption.py +252 -0
- complio/tests_library/infrastructure/s3_encryption.py +301 -0
- complio/tests_library/infrastructure/s3_public_access.py +369 -0
- complio/tests_library/infrastructure/secrets_manager_encryption.py +248 -0
- complio/tests_library/infrastructure/vpc_flow_logs.py +287 -0
- complio/tests_library/logging/__init__.py +0 -0
- complio/tests_library/logging/cloudwatch_alarms.py +354 -0
- complio/tests_library/logging/cloudwatch_logs_encryption.py +281 -0
- complio/tests_library/logging/cloudwatch_retention.py +252 -0
- complio/tests_library/logging/config_enabled.py +393 -0
- complio/tests_library/logging/eventbridge_rules.py +460 -0
- complio/tests_library/logging/guardduty_enabled.py +436 -0
- complio/tests_library/logging/security_hub_enabled.py +416 -0
- complio/tests_library/logging/sns_encryption.py +273 -0
- complio/tests_library/network/__init__.py +0 -0
- complio/tests_library/network/alb_nlb_security.py +421 -0
- complio/tests_library/network/api_gateway_security.py +452 -0
- complio/tests_library/network/cloudfront_https.py +332 -0
- complio/tests_library/network/direct_connect_security.py +343 -0
- complio/tests_library/network/nacl_configuration.py +367 -0
- complio/tests_library/network/network_firewall.py +355 -0
- complio/tests_library/network/transit_gateway_security.py +318 -0
- complio/tests_library/network/vpc_endpoints_security.py +339 -0
- complio/tests_library/network/vpn_security.py +333 -0
- complio/tests_library/network/waf_configuration.py +428 -0
- complio/tests_library/security/__init__.py +0 -0
- complio/tests_library/security/kms_key_rotation.py +314 -0
- complio/tests_library/storage/__init__.py +0 -0
- complio/tests_library/storage/backup_encryption.py +288 -0
- complio/tests_library/storage/dynamodb_encryption.py +280 -0
- complio/tests_library/storage/efs_encryption.py +257 -0
- complio/tests_library/storage/elasticache_encryption.py +370 -0
- complio/tests_library/storage/redshift_encryption.py +252 -0
- complio/tests_library/storage/s3_versioning.py +264 -0
- complio/utils/__init__.py +26 -0
- complio/utils/errors.py +179 -0
- complio/utils/exceptions.py +151 -0
- complio/utils/history.py +243 -0
- complio/utils/logger.py +391 -0
- complio-0.1.1.dist-info/METADATA +385 -0
- complio-0.1.1.dist-info/RECORD +79 -0
- complio-0.1.1.dist-info/WHEEL +4 -0
- complio-0.1.1.dist-info/entry_points.txt +3 -0
|
@@ -0,0 +1,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()
|