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,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()
|