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,248 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Secrets Manager encryption compliance test.
|
|
3
|
+
|
|
4
|
+
Checks that all secrets use customer-managed KMS keys for encryption.
|
|
5
|
+
|
|
6
|
+
ISO 27001 Control: A.8.24 - Use of cryptography
|
|
7
|
+
Requirement: Sensitive data must be encrypted with customer-controlled keys
|
|
8
|
+
|
|
9
|
+
Example:
|
|
10
|
+
>>> from complio.connectors.aws.client import AWSConnector
|
|
11
|
+
>>> from complio.tests_library.infrastructure.secrets_manager_encryption import SecretsManagerEncryptionTest
|
|
12
|
+
>>>
|
|
13
|
+
>>> connector = AWSConnector("production", "us-east-1")
|
|
14
|
+
>>> connector.connect()
|
|
15
|
+
>>>
|
|
16
|
+
>>> test = SecretsManagerEncryptionTest(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 SecretsManagerEncryptionTest(ComplianceTest):
|
|
35
|
+
"""Test for Secrets Manager encryption compliance.
|
|
36
|
+
|
|
37
|
+
Verifies that all secrets use customer-managed KMS keys for encryption
|
|
38
|
+
rather than the default AWS-managed key.
|
|
39
|
+
|
|
40
|
+
Compliance Requirements:
|
|
41
|
+
- All secrets should use customer-managed KMS keys
|
|
42
|
+
- AWS-managed keys provide less control over rotation and access
|
|
43
|
+
- Secrets without custom KMS keys are flagged as medium severity
|
|
44
|
+
|
|
45
|
+
Scoring:
|
|
46
|
+
- 100% if all secrets use customer-managed keys
|
|
47
|
+
- Proportional score based on compliant/total ratio
|
|
48
|
+
- 0% if no secrets use customer-managed keys
|
|
49
|
+
|
|
50
|
+
Example:
|
|
51
|
+
>>> test = SecretsManagerEncryptionTest(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 Secrets Manager encryption test.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
connector: AWS connector instance
|
|
62
|
+
"""
|
|
63
|
+
super().__init__(
|
|
64
|
+
test_id="secrets_manager_encryption",
|
|
65
|
+
test_name="Secrets Manager KMS Encryption Check",
|
|
66
|
+
description="Verify all secrets use customer-managed KMS keys for encryption",
|
|
67
|
+
control_id="A.8.24",
|
|
68
|
+
connector=connector,
|
|
69
|
+
scope="regional",
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
def execute(self) -> TestResult:
|
|
73
|
+
"""Execute Secrets Manager encryption compliance test.
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
TestResult with findings for secrets using AWS-managed keys
|
|
77
|
+
|
|
78
|
+
Example:
|
|
79
|
+
>>> test = SecretsManagerEncryptionTest(connector)
|
|
80
|
+
>>> result = test.execute()
|
|
81
|
+
>>> print(result.score)
|
|
82
|
+
85.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 Secrets Manager client
|
|
94
|
+
sm_client = self.connector.get_client("secretsmanager")
|
|
95
|
+
|
|
96
|
+
# List all secrets
|
|
97
|
+
self.logger.info("listing_secrets")
|
|
98
|
+
secrets = []
|
|
99
|
+
paginator = sm_client.get_paginator("list_secrets")
|
|
100
|
+
|
|
101
|
+
for page in paginator.paginate():
|
|
102
|
+
secrets.extend(page.get("SecretList", []))
|
|
103
|
+
|
|
104
|
+
if not secrets:
|
|
105
|
+
self.logger.info("no_secrets_found")
|
|
106
|
+
result.metadata["message"] = "No secrets found in region"
|
|
107
|
+
return result
|
|
108
|
+
|
|
109
|
+
self.logger.info("secrets_found", count=len(secrets))
|
|
110
|
+
|
|
111
|
+
# Check each secret for customer-managed KMS key
|
|
112
|
+
compliant_count = 0
|
|
113
|
+
total_count = len(secrets)
|
|
114
|
+
|
|
115
|
+
for secret in secrets:
|
|
116
|
+
secret_name = secret["Name"]
|
|
117
|
+
secret_arn = secret["ARN"]
|
|
118
|
+
kms_key_id = secret.get("KmsKeyId")
|
|
119
|
+
result.resources_scanned += 1
|
|
120
|
+
|
|
121
|
+
# Check if using customer-managed key
|
|
122
|
+
# If KmsKeyId is not present or is alias/aws/secretsmanager, it's using AWS-managed key
|
|
123
|
+
using_customer_key = (
|
|
124
|
+
kms_key_id is not None and
|
|
125
|
+
"alias/aws/secretsmanager" not in str(kms_key_id) and
|
|
126
|
+
kms_key_id != ""
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
# Create evidence
|
|
130
|
+
evidence = self.create_evidence(
|
|
131
|
+
resource_id=secret_name,
|
|
132
|
+
resource_type="secrets_manager_secret",
|
|
133
|
+
data={
|
|
134
|
+
"secret_name": secret_name,
|
|
135
|
+
"secret_arn": secret_arn,
|
|
136
|
+
"kms_key_id": kms_key_id,
|
|
137
|
+
"using_customer_managed_key": using_customer_key,
|
|
138
|
+
"last_changed_date": secret.get("LastChangedDate").isoformat() if secret.get("LastChangedDate") else None,
|
|
139
|
+
"last_accessed_date": secret.get("LastAccessedDate").isoformat() if secret.get("LastAccessedDate") else None,
|
|
140
|
+
}
|
|
141
|
+
)
|
|
142
|
+
result.add_evidence(evidence)
|
|
143
|
+
|
|
144
|
+
if using_customer_key:
|
|
145
|
+
compliant_count += 1
|
|
146
|
+
self.logger.debug(
|
|
147
|
+
"secret_using_customer_key",
|
|
148
|
+
secret_name=secret_name,
|
|
149
|
+
kms_key_id=kms_key_id
|
|
150
|
+
)
|
|
151
|
+
else:
|
|
152
|
+
# Create finding for secret using AWS-managed key
|
|
153
|
+
finding = self.create_finding(
|
|
154
|
+
resource_id=secret_name,
|
|
155
|
+
resource_type="secrets_manager_secret",
|
|
156
|
+
severity=Severity.MEDIUM,
|
|
157
|
+
title="Secret using AWS-managed KMS key",
|
|
158
|
+
description=f"Secret '{secret_name}' is using the default AWS-managed KMS key instead of a "
|
|
159
|
+
"customer-managed key. This reduces control over key rotation, access policies, "
|
|
160
|
+
"and audit trails. ISO 27001 A.8.24 recommends customer-controlled encryption keys.",
|
|
161
|
+
remediation=(
|
|
162
|
+
"Update the secret to use a customer-managed KMS key:\n"
|
|
163
|
+
"1. Create a customer-managed KMS key (if not already created):\n"
|
|
164
|
+
" aws kms create-key --description 'Secrets Manager encryption key'\n"
|
|
165
|
+
"2. Update the secret to use the customer-managed key:\n"
|
|
166
|
+
f" aws secretsmanager update-secret --secret-id {secret_name} "
|
|
167
|
+
"--kms-key-id <your-kms-key-id>\n\n"
|
|
168
|
+
"Note: Ensure your IAM roles/users have permission to use the KMS key.\n"
|
|
169
|
+
"Add a key policy that allows the Secrets Manager service to use the key."
|
|
170
|
+
),
|
|
171
|
+
evidence=evidence
|
|
172
|
+
)
|
|
173
|
+
result.add_finding(finding)
|
|
174
|
+
|
|
175
|
+
self.logger.warning(
|
|
176
|
+
"secret_using_aws_managed_key",
|
|
177
|
+
secret_name=secret_name,
|
|
178
|
+
kms_key_id=kms_key_id
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
# Calculate compliance score
|
|
182
|
+
if total_count > 0:
|
|
183
|
+
result.score = (compliant_count / total_count) * 100
|
|
184
|
+
|
|
185
|
+
# Determine pass/fail
|
|
186
|
+
result.passed = compliant_count == total_count
|
|
187
|
+
result.status = TestStatus.PASSED if result.passed else TestStatus.FAILED
|
|
188
|
+
|
|
189
|
+
# Add metadata
|
|
190
|
+
result.metadata = {
|
|
191
|
+
"total_secrets": total_count,
|
|
192
|
+
"secrets_with_customer_keys": compliant_count,
|
|
193
|
+
"secrets_with_aws_managed_keys": total_count - compliant_count,
|
|
194
|
+
"compliance_percentage": result.score,
|
|
195
|
+
"region": self.connector.region,
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
self.logger.info(
|
|
199
|
+
"secrets_manager_encryption_test_completed",
|
|
200
|
+
total=total_count,
|
|
201
|
+
compliant=compliant_count,
|
|
202
|
+
score=result.score,
|
|
203
|
+
passed=result.passed
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
except ClientError as e:
|
|
207
|
+
error_code = e.response.get("Error", {}).get("Code")
|
|
208
|
+
self.logger.error("secrets_manager_test_error", error_code=error_code, error=str(e))
|
|
209
|
+
result.status = TestStatus.ERROR
|
|
210
|
+
result.passed = False
|
|
211
|
+
result.score = 0.0
|
|
212
|
+
result.error_message = f"AWS API Error: {error_code} - {str(e)}"
|
|
213
|
+
|
|
214
|
+
except Exception as e:
|
|
215
|
+
self.logger.error("secrets_manager_test_error", error=str(e))
|
|
216
|
+
result.status = TestStatus.ERROR
|
|
217
|
+
result.passed = False
|
|
218
|
+
result.score = 0.0
|
|
219
|
+
result.error_message = str(e)
|
|
220
|
+
|
|
221
|
+
return result
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
# ============================================================================
|
|
225
|
+
# CONVENIENCE FUNCTION
|
|
226
|
+
# ============================================================================
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def run_secrets_manager_encryption_test(connector: AWSConnector) -> TestResult:
|
|
230
|
+
"""Run Secrets Manager encryption compliance test.
|
|
231
|
+
|
|
232
|
+
Convenience function for running the test.
|
|
233
|
+
|
|
234
|
+
Args:
|
|
235
|
+
connector: AWS connector
|
|
236
|
+
|
|
237
|
+
Returns:
|
|
238
|
+
TestResult
|
|
239
|
+
|
|
240
|
+
Example:
|
|
241
|
+
>>> from complio.connectors.aws.client import AWSConnector
|
|
242
|
+
>>> connector = AWSConnector("production", "us-east-1")
|
|
243
|
+
>>> connector.connect()
|
|
244
|
+
>>> result = run_secrets_manager_encryption_test(connector)
|
|
245
|
+
>>> print(f"Score: {result.score}%")
|
|
246
|
+
"""
|
|
247
|
+
test = SecretsManagerEncryptionTest(connector)
|
|
248
|
+
return test.execute()
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
"""
|
|
2
|
+
VPC Flow Logs compliance test.
|
|
3
|
+
|
|
4
|
+
Checks that all VPCs have flow logs enabled for network traffic monitoring.
|
|
5
|
+
|
|
6
|
+
ISO 27001 Control: A.8.16 - Monitoring of network traffic
|
|
7
|
+
Requirement: Network traffic must be logged for security monitoring
|
|
8
|
+
|
|
9
|
+
Example:
|
|
10
|
+
>>> from complio.connectors.aws.client import AWSConnector
|
|
11
|
+
>>> from complio.tests_library.infrastructure.vpc_flow_logs import VPCFlowLogsTest
|
|
12
|
+
>>>
|
|
13
|
+
>>> connector = AWSConnector("production", "us-east-1")
|
|
14
|
+
>>> connector.connect()
|
|
15
|
+
>>>
|
|
16
|
+
>>> test = VPCFlowLogsTest(connector)
|
|
17
|
+
>>> result = test.run()
|
|
18
|
+
>>> print(f"Passed: {result.passed}, Score: {result.score}")
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from typing import Any, Dict, List
|
|
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 VPCFlowLogsTest(ComplianceTest):
|
|
35
|
+
"""Test for VPC Flow Logs compliance.
|
|
36
|
+
|
|
37
|
+
Verifies that all VPCs have flow logs enabled to capture network
|
|
38
|
+
traffic information for security monitoring and troubleshooting.
|
|
39
|
+
|
|
40
|
+
Compliance Requirements:
|
|
41
|
+
- Each VPC must have at least one flow log configured
|
|
42
|
+
- Flow logs should capture accepted, rejected, or all traffic
|
|
43
|
+
- VPCs without flow logs are non-compliant
|
|
44
|
+
|
|
45
|
+
Scoring:
|
|
46
|
+
- 100% if all VPCs have flow logs
|
|
47
|
+
- Proportional score based on compliant/total ratio
|
|
48
|
+
- 0% if no VPCs have flow logs
|
|
49
|
+
|
|
50
|
+
Example:
|
|
51
|
+
>>> test = VPCFlowLogsTest(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 VPC Flow Logs test.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
connector: AWS connector instance
|
|
62
|
+
"""
|
|
63
|
+
super().__init__(
|
|
64
|
+
test_id="vpc_flow_logs",
|
|
65
|
+
test_name="VPC Flow Logs Check",
|
|
66
|
+
description="Verify all VPCs have flow logs enabled for network traffic monitoring",
|
|
67
|
+
control_id="A.8.16",
|
|
68
|
+
connector=connector,
|
|
69
|
+
scope="regional",
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
def execute(self) -> TestResult:
|
|
73
|
+
"""Execute VPC Flow Logs compliance test.
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
TestResult with findings for VPCs without flow logs
|
|
77
|
+
|
|
78
|
+
Example:
|
|
79
|
+
>>> test = VPCFlowLogsTest(connector)
|
|
80
|
+
>>> result = test.execute()
|
|
81
|
+
>>> print(result.score)
|
|
82
|
+
80.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 EC2 client
|
|
94
|
+
ec2_client = self.connector.get_client("ec2")
|
|
95
|
+
|
|
96
|
+
# List all VPCs
|
|
97
|
+
self.logger.info("listing_vpcs")
|
|
98
|
+
vpcs_response = ec2_client.describe_vpcs()
|
|
99
|
+
vpcs = vpcs_response.get("Vpcs", [])
|
|
100
|
+
|
|
101
|
+
if not vpcs:
|
|
102
|
+
self.logger.info("no_vpcs_found")
|
|
103
|
+
result.metadata["message"] = "No VPCs found in region"
|
|
104
|
+
return result
|
|
105
|
+
|
|
106
|
+
self.logger.info("vpcs_found", count=len(vpcs))
|
|
107
|
+
|
|
108
|
+
# Get all flow logs in the region
|
|
109
|
+
self.logger.info("listing_flow_logs")
|
|
110
|
+
flow_logs_response = ec2_client.describe_flow_logs()
|
|
111
|
+
flow_logs = flow_logs_response.get("FlowLogs", [])
|
|
112
|
+
|
|
113
|
+
# Create a map of VPC ID -> flow logs
|
|
114
|
+
vpc_flow_logs_map: Dict[str, List[Dict[str, Any]]] = {}
|
|
115
|
+
for flow_log in flow_logs:
|
|
116
|
+
resource_id = flow_log.get("ResourceId")
|
|
117
|
+
if resource_id and resource_id.startswith("vpc-"):
|
|
118
|
+
if resource_id not in vpc_flow_logs_map:
|
|
119
|
+
vpc_flow_logs_map[resource_id] = []
|
|
120
|
+
vpc_flow_logs_map[resource_id].append(flow_log)
|
|
121
|
+
|
|
122
|
+
# Check each VPC for flow logs
|
|
123
|
+
compliant_count = 0
|
|
124
|
+
total_count = len(vpcs)
|
|
125
|
+
|
|
126
|
+
for vpc in vpcs:
|
|
127
|
+
vpc_id = vpc["VpcId"]
|
|
128
|
+
is_default = vpc.get("IsDefault", False)
|
|
129
|
+
cidr_block = vpc.get("CidrBlock", "unknown")
|
|
130
|
+
result.resources_scanned += 1
|
|
131
|
+
|
|
132
|
+
# Get VPC name from tags
|
|
133
|
+
vpc_name = "unnamed"
|
|
134
|
+
for tag in vpc.get("Tags", []):
|
|
135
|
+
if tag.get("Key") == "Name":
|
|
136
|
+
vpc_name = tag.get("Value", "unnamed")
|
|
137
|
+
break
|
|
138
|
+
|
|
139
|
+
# Check if VPC has flow logs
|
|
140
|
+
vpc_flow_logs = vpc_flow_logs_map.get(vpc_id, [])
|
|
141
|
+
has_flow_logs = len(vpc_flow_logs) > 0
|
|
142
|
+
|
|
143
|
+
# Create evidence
|
|
144
|
+
evidence = self.create_evidence(
|
|
145
|
+
resource_id=vpc_id,
|
|
146
|
+
resource_type="vpc",
|
|
147
|
+
data={
|
|
148
|
+
"vpc_id": vpc_id,
|
|
149
|
+
"vpc_name": vpc_name,
|
|
150
|
+
"is_default": is_default,
|
|
151
|
+
"cidr_block": cidr_block,
|
|
152
|
+
"has_flow_logs": has_flow_logs,
|
|
153
|
+
"flow_logs_count": len(vpc_flow_logs),
|
|
154
|
+
"flow_logs": [
|
|
155
|
+
{
|
|
156
|
+
"flow_log_id": fl.get("FlowLogId"),
|
|
157
|
+
"traffic_type": fl.get("TrafficType"),
|
|
158
|
+
"log_destination_type": fl.get("LogDestinationType"),
|
|
159
|
+
"log_destination": fl.get("LogDestination"),
|
|
160
|
+
"log_group_name": fl.get("LogGroupName"),
|
|
161
|
+
}
|
|
162
|
+
for fl in vpc_flow_logs
|
|
163
|
+
] if vpc_flow_logs else [],
|
|
164
|
+
}
|
|
165
|
+
)
|
|
166
|
+
result.add_evidence(evidence)
|
|
167
|
+
|
|
168
|
+
if has_flow_logs:
|
|
169
|
+
compliant_count += 1
|
|
170
|
+
self.logger.debug(
|
|
171
|
+
"vpc_has_flow_logs",
|
|
172
|
+
vpc_id=vpc_id,
|
|
173
|
+
vpc_name=vpc_name,
|
|
174
|
+
flow_logs_count=len(vpc_flow_logs)
|
|
175
|
+
)
|
|
176
|
+
else:
|
|
177
|
+
# Create finding for VPC without flow logs
|
|
178
|
+
finding = self.create_finding(
|
|
179
|
+
resource_id=vpc_id,
|
|
180
|
+
resource_type="vpc",
|
|
181
|
+
severity=Severity.HIGH,
|
|
182
|
+
title="VPC Flow Logs not enabled",
|
|
183
|
+
description=f"VPC '{vpc_name}' ({vpc_id}, {cidr_block}) does not have flow logs enabled. "
|
|
184
|
+
"Flow logs capture information about IP traffic going to and from network interfaces. "
|
|
185
|
+
"Without flow logs, network security monitoring and troubleshooting are severely limited. "
|
|
186
|
+
"This violates ISO 27001 A.8.16 requirement for network traffic monitoring.",
|
|
187
|
+
remediation=(
|
|
188
|
+
f"Enable VPC Flow Logs for VPC '{vpc_id}':\n"
|
|
189
|
+
"1. Create a CloudWatch log group (if using CloudWatch Logs):\n"
|
|
190
|
+
f" aws logs create-log-group --log-group-name /aws/vpc/flowlogs/{vpc_id}\n"
|
|
191
|
+
"2. Create an IAM role for flow logs:\n"
|
|
192
|
+
" (See AWS documentation for required trust policy and permissions)\n"
|
|
193
|
+
"3. Create the flow log:\n"
|
|
194
|
+
f" aws ec2 create-flow-logs --resource-type VPC \\\n"
|
|
195
|
+
f" --resource-ids {vpc_id} \\\n"
|
|
196
|
+
" --traffic-type ALL \\\n"
|
|
197
|
+
" --log-destination-type cloud-watch-logs \\\n"
|
|
198
|
+
f" --log-group-name /aws/vpc/flowlogs/{vpc_id} \\\n"
|
|
199
|
+
" --deliver-logs-permission-arn <iam-role-arn>\n\n"
|
|
200
|
+
"Or use AWS Console:\n"
|
|
201
|
+
"1. Go to VPC → Your VPCs\n"
|
|
202
|
+
f"2. Select VPC '{vpc_id}'\n"
|
|
203
|
+
"3. Actions → Create flow log\n"
|
|
204
|
+
"4. Configure destination (CloudWatch Logs or S3)\n"
|
|
205
|
+
"5. Set traffic type to 'All'\n"
|
|
206
|
+
"6. Click Create flow log"
|
|
207
|
+
),
|
|
208
|
+
evidence=evidence
|
|
209
|
+
)
|
|
210
|
+
result.add_finding(finding)
|
|
211
|
+
|
|
212
|
+
self.logger.warning(
|
|
213
|
+
"vpc_without_flow_logs",
|
|
214
|
+
vpc_id=vpc_id,
|
|
215
|
+
vpc_name=vpc_name,
|
|
216
|
+
is_default=is_default
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
# Calculate compliance score
|
|
220
|
+
if total_count > 0:
|
|
221
|
+
result.score = (compliant_count / total_count) * 100
|
|
222
|
+
|
|
223
|
+
# Determine pass/fail
|
|
224
|
+
result.passed = compliant_count == total_count
|
|
225
|
+
result.status = TestStatus.PASSED if result.passed else TestStatus.FAILED
|
|
226
|
+
|
|
227
|
+
# Add metadata
|
|
228
|
+
result.metadata = {
|
|
229
|
+
"total_vpcs": total_count,
|
|
230
|
+
"vpcs_with_flow_logs": compliant_count,
|
|
231
|
+
"vpcs_without_flow_logs": total_count - compliant_count,
|
|
232
|
+
"total_flow_logs": len(flow_logs),
|
|
233
|
+
"compliance_percentage": result.score,
|
|
234
|
+
"region": self.connector.region,
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
self.logger.info(
|
|
238
|
+
"vpc_flow_logs_test_completed",
|
|
239
|
+
total=total_count,
|
|
240
|
+
compliant=compliant_count,
|
|
241
|
+
score=result.score,
|
|
242
|
+
passed=result.passed
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
except ClientError as e:
|
|
246
|
+
error_code = e.response.get("Error", {}).get("Code")
|
|
247
|
+
self.logger.error("vpc_flow_logs_test_error", error_code=error_code, error=str(e))
|
|
248
|
+
result.status = TestStatus.ERROR
|
|
249
|
+
result.passed = False
|
|
250
|
+
result.score = 0.0
|
|
251
|
+
result.error_message = f"AWS API Error: {error_code} - {str(e)}"
|
|
252
|
+
|
|
253
|
+
except Exception as e:
|
|
254
|
+
self.logger.error("vpc_flow_logs_test_error", error=str(e))
|
|
255
|
+
result.status = TestStatus.ERROR
|
|
256
|
+
result.passed = False
|
|
257
|
+
result.score = 0.0
|
|
258
|
+
result.error_message = str(e)
|
|
259
|
+
|
|
260
|
+
return result
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
# ============================================================================
|
|
264
|
+
# CONVENIENCE FUNCTION
|
|
265
|
+
# ============================================================================
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def run_vpc_flow_logs_test(connector: AWSConnector) -> TestResult:
|
|
269
|
+
"""Run VPC Flow Logs compliance test.
|
|
270
|
+
|
|
271
|
+
Convenience function for running the test.
|
|
272
|
+
|
|
273
|
+
Args:
|
|
274
|
+
connector: AWS connector
|
|
275
|
+
|
|
276
|
+
Returns:
|
|
277
|
+
TestResult
|
|
278
|
+
|
|
279
|
+
Example:
|
|
280
|
+
>>> from complio.connectors.aws.client import AWSConnector
|
|
281
|
+
>>> connector = AWSConnector("production", "us-east-1")
|
|
282
|
+
>>> connector.connect()
|
|
283
|
+
>>> result = run_vpc_flow_logs_test(connector)
|
|
284
|
+
>>> print(f"Score: {result.score}%")
|
|
285
|
+
"""
|
|
286
|
+
test = VPCFlowLogsTest(connector)
|
|
287
|
+
return test.execute()
|
|
File without changes
|