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,280 @@
|
|
|
1
|
+
"""
|
|
2
|
+
DynamoDB table encryption compliance test.
|
|
3
|
+
|
|
4
|
+
Checks that all DynamoDB tables have server-side encryption enabled.
|
|
5
|
+
|
|
6
|
+
ISO 27001 Control: A.8.24 - Use of cryptography
|
|
7
|
+
Requirement: All data at rest must be encrypted
|
|
8
|
+
|
|
9
|
+
Example:
|
|
10
|
+
>>> from complio.connectors.aws.client import AWSConnector
|
|
11
|
+
>>> from complio.tests_library.storage.dynamodb_encryption import DynamoDBEncryptionTest
|
|
12
|
+
>>>
|
|
13
|
+
>>> connector = AWSConnector("production", "us-east-1")
|
|
14
|
+
>>> connector.connect()
|
|
15
|
+
>>>
|
|
16
|
+
>>> test = DynamoDBEncryptionTest(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 DynamoDBEncryptionTest(ComplianceTest):
|
|
35
|
+
"""Test for DynamoDB table encryption compliance.
|
|
36
|
+
|
|
37
|
+
Verifies that all DynamoDB tables have server-side encryption enabled.
|
|
38
|
+
|
|
39
|
+
Compliance Requirements:
|
|
40
|
+
- All DynamoDB tables must have SSEDescription.Status=ENABLED
|
|
41
|
+
- Tables can use AWS managed or customer managed KMS keys
|
|
42
|
+
- Tables without SSE are non-compliant
|
|
43
|
+
|
|
44
|
+
Scoring:
|
|
45
|
+
- 100% if all tables are encrypted
|
|
46
|
+
- Proportional score based on encrypted/total ratio
|
|
47
|
+
- 0% if no tables are encrypted
|
|
48
|
+
|
|
49
|
+
Example:
|
|
50
|
+
>>> test = DynamoDBEncryptionTest(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 DynamoDB encryption test.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
connector: AWS connector instance
|
|
61
|
+
"""
|
|
62
|
+
super().__init__(
|
|
63
|
+
test_id="dynamodb_encryption",
|
|
64
|
+
test_name="DynamoDB Table Encryption Check",
|
|
65
|
+
description="Verify all DynamoDB tables have server-side encryption enabled",
|
|
66
|
+
control_id="A.8.24",
|
|
67
|
+
connector=connector,
|
|
68
|
+
scope="regional",
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
def execute(self) -> TestResult:
|
|
72
|
+
"""Execute DynamoDB encryption compliance test.
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
TestResult with findings for non-encrypted tables
|
|
76
|
+
|
|
77
|
+
Example:
|
|
78
|
+
>>> test = DynamoDBEncryptionTest(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 DynamoDB client
|
|
93
|
+
dynamodb_client = self.connector.get_client("dynamodb")
|
|
94
|
+
|
|
95
|
+
# List all tables
|
|
96
|
+
self.logger.info("listing_dynamodb_tables")
|
|
97
|
+
table_names = []
|
|
98
|
+
|
|
99
|
+
# Handle pagination for list_tables
|
|
100
|
+
paginator = dynamodb_client.get_paginator("list_tables")
|
|
101
|
+
for page in paginator.paginate():
|
|
102
|
+
table_names.extend(page.get("TableNames", []))
|
|
103
|
+
|
|
104
|
+
if not table_names:
|
|
105
|
+
self.logger.info("no_dynamodb_tables_found")
|
|
106
|
+
result.metadata["message"] = "No DynamoDB tables found in region"
|
|
107
|
+
return result
|
|
108
|
+
|
|
109
|
+
self.logger.info("dynamodb_tables_found", count=len(table_names))
|
|
110
|
+
|
|
111
|
+
# Check each table for encryption
|
|
112
|
+
encrypted_count = 0
|
|
113
|
+
total_count = len(table_names)
|
|
114
|
+
|
|
115
|
+
for table_name in table_names:
|
|
116
|
+
result.resources_scanned += 1
|
|
117
|
+
|
|
118
|
+
# Describe table to get encryption details
|
|
119
|
+
try:
|
|
120
|
+
table_response = dynamodb_client.describe_table(TableName=table_name)
|
|
121
|
+
table = table_response.get("Table", {})
|
|
122
|
+
|
|
123
|
+
# Check SSEDescription
|
|
124
|
+
sse_description = table.get("SSEDescription", {})
|
|
125
|
+
sse_status = sse_description.get("Status", "DISABLED")
|
|
126
|
+
sse_type = sse_description.get("SSEType", "None")
|
|
127
|
+
kms_key_arn = sse_description.get("KMSMasterKeyArn")
|
|
128
|
+
|
|
129
|
+
# Encryption is enabled if Status=ENABLED or ENABLING
|
|
130
|
+
encrypted = sse_status in ["ENABLED", "ENABLING"]
|
|
131
|
+
|
|
132
|
+
# Get table details
|
|
133
|
+
table_status = table.get("TableStatus", "unknown")
|
|
134
|
+
item_count = table.get("ItemCount", 0)
|
|
135
|
+
table_size_bytes = table.get("TableSizeBytes", 0)
|
|
136
|
+
billing_mode = table.get("BillingModeSummary", {}).get("BillingMode", "PROVISIONED")
|
|
137
|
+
|
|
138
|
+
# Create evidence
|
|
139
|
+
evidence = self.create_evidence(
|
|
140
|
+
resource_id=table_name,
|
|
141
|
+
resource_type="dynamodb_table",
|
|
142
|
+
data={
|
|
143
|
+
"table_name": table_name,
|
|
144
|
+
"encrypted": encrypted,
|
|
145
|
+
"sse_status": sse_status,
|
|
146
|
+
"sse_type": sse_type,
|
|
147
|
+
"kms_key_arn": kms_key_arn,
|
|
148
|
+
"table_status": table_status,
|
|
149
|
+
"item_count": item_count,
|
|
150
|
+
"table_size_bytes": table_size_bytes,
|
|
151
|
+
"billing_mode": billing_mode,
|
|
152
|
+
}
|
|
153
|
+
)
|
|
154
|
+
result.add_evidence(evidence)
|
|
155
|
+
|
|
156
|
+
if encrypted:
|
|
157
|
+
encrypted_count += 1
|
|
158
|
+
self.logger.debug(
|
|
159
|
+
"dynamodb_table_encrypted",
|
|
160
|
+
table_name=table_name,
|
|
161
|
+
sse_type=sse_type,
|
|
162
|
+
sse_status=sse_status
|
|
163
|
+
)
|
|
164
|
+
else:
|
|
165
|
+
# Create finding for non-encrypted table
|
|
166
|
+
size_mb = table_size_bytes / (1024 * 1024) if table_size_bytes > 0 else 0
|
|
167
|
+
finding = self.create_finding(
|
|
168
|
+
resource_id=table_name,
|
|
169
|
+
resource_type="dynamodb_table",
|
|
170
|
+
severity=Severity.HIGH,
|
|
171
|
+
title="DynamoDB table encryption not enabled",
|
|
172
|
+
description=f"DynamoDB table '{table_name}' with {item_count} items ({size_mb:.2f}MB) "
|
|
173
|
+
f"does not have server-side encryption enabled (SSE Status: {sse_status}). "
|
|
174
|
+
"Table data is stored unencrypted. "
|
|
175
|
+
"This violates ISO 27001 A.8.24 requirement for data-at-rest encryption.",
|
|
176
|
+
remediation=(
|
|
177
|
+
f"Enable server-side encryption for table '{table_name}':\n\n"
|
|
178
|
+
"Using AWS managed key (easier):\n"
|
|
179
|
+
f"aws dynamodb update-table --table-name {table_name} \\\n"
|
|
180
|
+
" --sse-specification Enabled=true\n\n"
|
|
181
|
+
"Or using customer managed KMS key (more control):\n"
|
|
182
|
+
f"aws dynamodb update-table --table-name {table_name} \\\n"
|
|
183
|
+
" --sse-specification Enabled=true,SSEType=KMS,KMSMasterKeyId=<your-kms-key-id>\n\n"
|
|
184
|
+
"Or use AWS Console:\n"
|
|
185
|
+
"1. Go to DynamoDB → Tables\n"
|
|
186
|
+
f"2. Select table '{table_name}'\n"
|
|
187
|
+
"3. Go to 'Additional settings' tab\n"
|
|
188
|
+
"4. Under 'Encryption at rest', click 'Manage encryption'\n"
|
|
189
|
+
"5. Select 'AWS owned key' or 'AWS managed key' or 'Customer managed key'\n"
|
|
190
|
+
"6. Click 'Save changes'\n\n"
|
|
191
|
+
"Note: Encryption can be enabled on existing tables without downtime."
|
|
192
|
+
),
|
|
193
|
+
evidence=evidence
|
|
194
|
+
)
|
|
195
|
+
result.add_finding(finding)
|
|
196
|
+
|
|
197
|
+
self.logger.warning(
|
|
198
|
+
"dynamodb_table_not_encrypted",
|
|
199
|
+
table_name=table_name,
|
|
200
|
+
sse_status=sse_status
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
except ClientError as e:
|
|
204
|
+
error_code = e.response.get("Error", {}).get("Code")
|
|
205
|
+
self.logger.error(
|
|
206
|
+
"dynamodb_table_describe_error",
|
|
207
|
+
table_name=table_name,
|
|
208
|
+
error_code=error_code
|
|
209
|
+
)
|
|
210
|
+
# Skip this table and continue
|
|
211
|
+
continue
|
|
212
|
+
|
|
213
|
+
# Calculate compliance score
|
|
214
|
+
if total_count > 0:
|
|
215
|
+
result.score = (encrypted_count / total_count) * 100
|
|
216
|
+
|
|
217
|
+
# Determine pass/fail
|
|
218
|
+
result.passed = encrypted_count == total_count
|
|
219
|
+
result.status = TestStatus.PASSED if result.passed else TestStatus.FAILED
|
|
220
|
+
|
|
221
|
+
# Add metadata
|
|
222
|
+
result.metadata = {
|
|
223
|
+
"total_tables": total_count,
|
|
224
|
+
"encrypted_tables": encrypted_count,
|
|
225
|
+
"non_encrypted_tables": total_count - encrypted_count,
|
|
226
|
+
"compliance_percentage": result.score,
|
|
227
|
+
"region": self.connector.region,
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
self.logger.info(
|
|
231
|
+
"dynamodb_encryption_test_completed",
|
|
232
|
+
total=total_count,
|
|
233
|
+
encrypted=encrypted_count,
|
|
234
|
+
score=result.score,
|
|
235
|
+
passed=result.passed
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
except ClientError as e:
|
|
239
|
+
error_code = e.response.get("Error", {}).get("Code")
|
|
240
|
+
self.logger.error("dynamodb_encryption_test_error", error_code=error_code, error=str(e))
|
|
241
|
+
result.status = TestStatus.ERROR
|
|
242
|
+
result.passed = False
|
|
243
|
+
result.score = 0.0
|
|
244
|
+
result.error_message = f"AWS API Error: {error_code} - {str(e)}"
|
|
245
|
+
|
|
246
|
+
except Exception as e:
|
|
247
|
+
self.logger.error("dynamodb_encryption_test_error", error=str(e))
|
|
248
|
+
result.status = TestStatus.ERROR
|
|
249
|
+
result.passed = False
|
|
250
|
+
result.score = 0.0
|
|
251
|
+
result.error_message = str(e)
|
|
252
|
+
|
|
253
|
+
return result
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
# ============================================================================
|
|
257
|
+
# CONVENIENCE FUNCTION
|
|
258
|
+
# ============================================================================
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def run_dynamodb_encryption_test(connector: AWSConnector) -> TestResult:
|
|
262
|
+
"""Run DynamoDB encryption compliance test.
|
|
263
|
+
|
|
264
|
+
Convenience function for running the test.
|
|
265
|
+
|
|
266
|
+
Args:
|
|
267
|
+
connector: AWS connector
|
|
268
|
+
|
|
269
|
+
Returns:
|
|
270
|
+
TestResult
|
|
271
|
+
|
|
272
|
+
Example:
|
|
273
|
+
>>> from complio.connectors.aws.client import AWSConnector
|
|
274
|
+
>>> connector = AWSConnector("production", "us-east-1")
|
|
275
|
+
>>> connector.connect()
|
|
276
|
+
>>> result = run_dynamodb_encryption_test(connector)
|
|
277
|
+
>>> print(f"Score: {result.score}%")
|
|
278
|
+
"""
|
|
279
|
+
test = DynamoDBEncryptionTest(connector)
|
|
280
|
+
return test.execute()
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
"""
|
|
2
|
+
EFS (Elastic File System) encryption compliance test.
|
|
3
|
+
|
|
4
|
+
Checks that all EFS file systems have encryption enabled.
|
|
5
|
+
|
|
6
|
+
ISO 27001 Control: A.8.24 - Use of cryptography
|
|
7
|
+
Requirement: All data at rest must be encrypted
|
|
8
|
+
|
|
9
|
+
Example:
|
|
10
|
+
>>> from complio.connectors.aws.client import AWSConnector
|
|
11
|
+
>>> from complio.tests_library.storage.efs_encryption import EFSEncryptionTest
|
|
12
|
+
>>>
|
|
13
|
+
>>> connector = AWSConnector("production", "us-east-1")
|
|
14
|
+
>>> connector.connect()
|
|
15
|
+
>>>
|
|
16
|
+
>>> test = EFSEncryptionTest(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 EFSEncryptionTest(ComplianceTest):
|
|
35
|
+
"""Test for EFS file system encryption compliance.
|
|
36
|
+
|
|
37
|
+
Verifies that all EFS file systems have encryption enabled.
|
|
38
|
+
|
|
39
|
+
Compliance Requirements:
|
|
40
|
+
- All EFS file systems must have Encrypted=True
|
|
41
|
+
- Unencrypted file systems expose shared data
|
|
42
|
+
- File systems without encryption are non-compliant
|
|
43
|
+
|
|
44
|
+
Scoring:
|
|
45
|
+
- 100% if all file systems are encrypted
|
|
46
|
+
- Proportional score based on encrypted/total ratio
|
|
47
|
+
- 0% if no file systems are encrypted
|
|
48
|
+
|
|
49
|
+
Example:
|
|
50
|
+
>>> test = EFSEncryptionTest(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 EFS encryption test.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
connector: AWS connector instance
|
|
61
|
+
"""
|
|
62
|
+
super().__init__(
|
|
63
|
+
test_id="efs_encryption",
|
|
64
|
+
test_name="EFS File System Encryption Check",
|
|
65
|
+
description="Verify all EFS file systems have encryption enabled",
|
|
66
|
+
control_id="A.8.24",
|
|
67
|
+
connector=connector,
|
|
68
|
+
scope="regional",
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
def execute(self) -> TestResult:
|
|
72
|
+
"""Execute EFS encryption compliance test.
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
TestResult with findings for non-encrypted file systems
|
|
76
|
+
|
|
77
|
+
Example:
|
|
78
|
+
>>> test = EFSEncryptionTest(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 EFS client
|
|
93
|
+
efs_client = self.connector.get_client("efs")
|
|
94
|
+
|
|
95
|
+
# List all file systems
|
|
96
|
+
self.logger.info("listing_efs_file_systems")
|
|
97
|
+
response = efs_client.describe_file_systems()
|
|
98
|
+
file_systems = response.get("FileSystems", [])
|
|
99
|
+
|
|
100
|
+
if not file_systems:
|
|
101
|
+
self.logger.info("no_efs_file_systems_found")
|
|
102
|
+
result.metadata["message"] = "No EFS file systems found in region"
|
|
103
|
+
return result
|
|
104
|
+
|
|
105
|
+
self.logger.info("efs_file_systems_found", count=len(file_systems))
|
|
106
|
+
|
|
107
|
+
# Check each file system for encryption
|
|
108
|
+
encrypted_count = 0
|
|
109
|
+
total_count = len(file_systems)
|
|
110
|
+
|
|
111
|
+
for fs in file_systems:
|
|
112
|
+
fs_id = fs["FileSystemId"]
|
|
113
|
+
encrypted = fs.get("Encrypted", False)
|
|
114
|
+
result.resources_scanned += 1
|
|
115
|
+
|
|
116
|
+
# Get file system details
|
|
117
|
+
name = fs.get("Name", "unnamed")
|
|
118
|
+
life_cycle_state = fs.get("LifeCycleState", "unknown")
|
|
119
|
+
size_in_bytes = fs.get("SizeInBytes", {}).get("Value", 0)
|
|
120
|
+
number_of_mount_targets = fs.get("NumberOfMountTargets", 0)
|
|
121
|
+
performance_mode = fs.get("PerformanceMode", "unknown")
|
|
122
|
+
|
|
123
|
+
# Create evidence
|
|
124
|
+
evidence = self.create_evidence(
|
|
125
|
+
resource_id=fs_id,
|
|
126
|
+
resource_type="efs_file_system",
|
|
127
|
+
data={
|
|
128
|
+
"file_system_id": fs_id,
|
|
129
|
+
"name": name,
|
|
130
|
+
"encrypted": encrypted,
|
|
131
|
+
"life_cycle_state": life_cycle_state,
|
|
132
|
+
"size_bytes": size_in_bytes,
|
|
133
|
+
"number_of_mount_targets": number_of_mount_targets,
|
|
134
|
+
"performance_mode": performance_mode,
|
|
135
|
+
"kms_key_id": fs.get("KmsKeyId"),
|
|
136
|
+
"creation_token": fs.get("CreationToken"),
|
|
137
|
+
}
|
|
138
|
+
)
|
|
139
|
+
result.add_evidence(evidence)
|
|
140
|
+
|
|
141
|
+
if encrypted:
|
|
142
|
+
encrypted_count += 1
|
|
143
|
+
self.logger.debug(
|
|
144
|
+
"efs_file_system_encrypted",
|
|
145
|
+
fs_id=fs_id,
|
|
146
|
+
name=name,
|
|
147
|
+
kms_key_id=fs.get("KmsKeyId")
|
|
148
|
+
)
|
|
149
|
+
else:
|
|
150
|
+
# Create finding for non-encrypted file system
|
|
151
|
+
size_mb = size_in_bytes / (1024 * 1024) if size_in_bytes > 0 else 0
|
|
152
|
+
finding = self.create_finding(
|
|
153
|
+
resource_id=fs_id,
|
|
154
|
+
resource_type="efs_file_system",
|
|
155
|
+
severity=Severity.HIGH,
|
|
156
|
+
title="EFS file system encryption not enabled",
|
|
157
|
+
description=f"EFS file system '{name}' ({fs_id}) with {number_of_mount_targets} mount targets "
|
|
158
|
+
f"and {size_mb:.2f}MB of data does not have encryption enabled. "
|
|
159
|
+
"Shared file system data is stored unencrypted. "
|
|
160
|
+
"This violates ISO 27001 A.8.24 requirement for data-at-rest encryption.",
|
|
161
|
+
remediation=(
|
|
162
|
+
"EFS file systems cannot be encrypted after creation. To remediate:\n"
|
|
163
|
+
"1. Create a new EFS file system with encryption enabled:\n"
|
|
164
|
+
" aws efs create-file-system --encrypted \\\n"
|
|
165
|
+
" --kms-key-id <your-kms-key-id> \\\n"
|
|
166
|
+
f" --performance-mode {performance_mode} \\\n"
|
|
167
|
+
" --tags Key=Name,Value=encrypted-replacement\n"
|
|
168
|
+
"2. Create mount targets in the same subnets:\n"
|
|
169
|
+
" aws efs create-mount-target --file-system-id <new-fs-id> \\\n"
|
|
170
|
+
" --subnet-id <subnet-id> --security-groups <sg-id>\n"
|
|
171
|
+
"3. Use AWS DataSync or rsync to migrate data:\n"
|
|
172
|
+
" # Mount both file systems, then:\n"
|
|
173
|
+
" rsync -av /mnt/old-efs/ /mnt/new-efs/\n"
|
|
174
|
+
"4. Update application mount points\n"
|
|
175
|
+
f"5. Delete the old unencrypted file system '{fs_id}'\n\n"
|
|
176
|
+
"Note: This will cause temporary downtime during data migration.\n"
|
|
177
|
+
"For future file systems, enable encryption at creation time."
|
|
178
|
+
),
|
|
179
|
+
evidence=evidence
|
|
180
|
+
)
|
|
181
|
+
result.add_finding(finding)
|
|
182
|
+
|
|
183
|
+
self.logger.warning(
|
|
184
|
+
"efs_file_system_not_encrypted",
|
|
185
|
+
fs_id=fs_id,
|
|
186
|
+
name=name,
|
|
187
|
+
size_mb=size_mb
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
# Calculate compliance score
|
|
191
|
+
if total_count > 0:
|
|
192
|
+
result.score = (encrypted_count / total_count) * 100
|
|
193
|
+
|
|
194
|
+
# Determine pass/fail
|
|
195
|
+
result.passed = encrypted_count == total_count
|
|
196
|
+
result.status = TestStatus.PASSED if result.passed else TestStatus.FAILED
|
|
197
|
+
|
|
198
|
+
# Add metadata
|
|
199
|
+
result.metadata = {
|
|
200
|
+
"total_file_systems": total_count,
|
|
201
|
+
"encrypted_file_systems": encrypted_count,
|
|
202
|
+
"non_encrypted_file_systems": total_count - encrypted_count,
|
|
203
|
+
"compliance_percentage": result.score,
|
|
204
|
+
"region": self.connector.region,
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
self.logger.info(
|
|
208
|
+
"efs_encryption_test_completed",
|
|
209
|
+
total=total_count,
|
|
210
|
+
encrypted=encrypted_count,
|
|
211
|
+
score=result.score,
|
|
212
|
+
passed=result.passed
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
except ClientError as e:
|
|
216
|
+
error_code = e.response.get("Error", {}).get("Code")
|
|
217
|
+
self.logger.error("efs_encryption_test_error", error_code=error_code, error=str(e))
|
|
218
|
+
result.status = TestStatus.ERROR
|
|
219
|
+
result.passed = False
|
|
220
|
+
result.score = 0.0
|
|
221
|
+
result.error_message = f"AWS API Error: {error_code} - {str(e)}"
|
|
222
|
+
|
|
223
|
+
except Exception as e:
|
|
224
|
+
self.logger.error("efs_encryption_test_error", error=str(e))
|
|
225
|
+
result.status = TestStatus.ERROR
|
|
226
|
+
result.passed = False
|
|
227
|
+
result.score = 0.0
|
|
228
|
+
result.error_message = str(e)
|
|
229
|
+
|
|
230
|
+
return result
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
# ============================================================================
|
|
234
|
+
# CONVENIENCE FUNCTION
|
|
235
|
+
# ============================================================================
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def run_efs_encryption_test(connector: AWSConnector) -> TestResult:
|
|
239
|
+
"""Run EFS encryption compliance test.
|
|
240
|
+
|
|
241
|
+
Convenience function for running the test.
|
|
242
|
+
|
|
243
|
+
Args:
|
|
244
|
+
connector: AWS connector
|
|
245
|
+
|
|
246
|
+
Returns:
|
|
247
|
+
TestResult
|
|
248
|
+
|
|
249
|
+
Example:
|
|
250
|
+
>>> from complio.connectors.aws.client import AWSConnector
|
|
251
|
+
>>> connector = AWSConnector("production", "us-east-1")
|
|
252
|
+
>>> connector.connect()
|
|
253
|
+
>>> result = run_efs_encryption_test(connector)
|
|
254
|
+
>>> print(f"Score: {result.score}%")
|
|
255
|
+
"""
|
|
256
|
+
test = EFSEncryptionTest(connector)
|
|
257
|
+
return test.execute()
|