aws-cis-controls-assessment 1.1.4__py3-none-any.whl → 1.2.2__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.
- aws_cis_assessment/__init__.py +4 -4
- aws_cis_assessment/config/rules/cis_controls_ig1.yaml +365 -2
- aws_cis_assessment/controls/base_control.py +106 -24
- aws_cis_assessment/controls/ig1/__init__.py +144 -15
- aws_cis_assessment/controls/ig1/control_4_1.py +4 -4
- aws_cis_assessment/controls/ig1/control_access_analyzer.py +198 -0
- aws_cis_assessment/controls/ig1/control_access_asset_mgmt.py +360 -0
- aws_cis_assessment/controls/ig1/control_access_control.py +323 -0
- aws_cis_assessment/controls/ig1/control_backup_security.py +579 -0
- aws_cis_assessment/controls/ig1/control_cloudfront_logging.py +215 -0
- aws_cis_assessment/controls/ig1/control_configuration_mgmt.py +407 -0
- aws_cis_assessment/controls/ig1/control_data_classification.py +255 -0
- aws_cis_assessment/controls/ig1/control_dynamodb_encryption.py +279 -0
- aws_cis_assessment/controls/ig1/control_ebs_encryption.py +177 -0
- aws_cis_assessment/controls/ig1/control_efs_encryption.py +243 -0
- aws_cis_assessment/controls/ig1/control_elb_logging.py +195 -0
- aws_cis_assessment/controls/ig1/control_guardduty.py +156 -0
- aws_cis_assessment/controls/ig1/control_inspector.py +184 -0
- aws_cis_assessment/controls/ig1/control_inventory.py +511 -0
- aws_cis_assessment/controls/ig1/control_macie.py +165 -0
- aws_cis_assessment/controls/ig1/control_messaging_encryption.py +419 -0
- aws_cis_assessment/controls/ig1/control_mfa.py +485 -0
- aws_cis_assessment/controls/ig1/control_network_security.py +194 -619
- aws_cis_assessment/controls/ig1/control_patch_management.py +626 -0
- aws_cis_assessment/controls/ig1/control_rds_encryption.py +228 -0
- aws_cis_assessment/controls/ig1/control_s3_encryption.py +383 -0
- aws_cis_assessment/controls/ig1/control_tls_ssl.py +556 -0
- aws_cis_assessment/controls/ig1/control_version_mgmt.py +337 -0
- aws_cis_assessment/controls/ig1/control_vpc_flow_logs.py +205 -0
- aws_cis_assessment/controls/ig1/control_waf_logging.py +226 -0
- aws_cis_assessment/core/assessment_engine.py +160 -11
- aws_cis_assessment/core/aws_client_factory.py +17 -5
- aws_cis_assessment/core/models.py +20 -1
- aws_cis_assessment/core/scoring_engine.py +102 -1
- aws_cis_assessment/reporters/base_reporter.py +58 -13
- aws_cis_assessment/reporters/html_reporter.py +186 -9
- aws_cis_controls_assessment-1.2.2.dist-info/METADATA +320 -0
- {aws_cis_controls_assessment-1.1.4.dist-info → aws_cis_controls_assessment-1.2.2.dist-info}/RECORD +44 -20
- docs/developer-guide.md +204 -5
- docs/user-guide.md +137 -4
- aws_cis_controls_assessment-1.1.4.dist-info/METADATA +0 -404
- {aws_cis_controls_assessment-1.1.4.dist-info → aws_cis_controls_assessment-1.2.2.dist-info}/WHEEL +0 -0
- {aws_cis_controls_assessment-1.1.4.dist-info → aws_cis_controls_assessment-1.2.2.dist-info}/entry_points.txt +0 -0
- {aws_cis_controls_assessment-1.1.4.dist-info → aws_cis_controls_assessment-1.2.2.dist-info}/licenses/LICENSE +0 -0
- {aws_cis_controls_assessment-1.1.4.dist-info → aws_cis_controls_assessment-1.2.2.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,485 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CIS Control 2.6, 2.7, 2.15 - Multi-Factor Authentication Controls
|
|
3
|
+
Ensures MFA is enabled for administrative access and critical services.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import logging
|
|
7
|
+
from typing import List, Dict, Any
|
|
8
|
+
from botocore.exceptions import ClientError
|
|
9
|
+
|
|
10
|
+
from aws_cis_assessment.controls.base_control import BaseConfigRuleAssessment
|
|
11
|
+
from aws_cis_assessment.core.models import ComplianceResult, ComplianceStatus
|
|
12
|
+
from aws_cis_assessment.core.aws_client_factory import AWSClientFactory
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class IAMAdminMFARequiredAssessment(BaseConfigRuleAssessment):
|
|
18
|
+
"""
|
|
19
|
+
CIS Control 2.6 - Establish an Access Granting Process
|
|
20
|
+
AWS Config Rule: iam-admin-mfa-required
|
|
21
|
+
|
|
22
|
+
Ensures IAM users with administrative privileges have MFA enabled.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def __init__(self):
|
|
26
|
+
super().__init__(
|
|
27
|
+
rule_name="iam-admin-mfa-required",
|
|
28
|
+
control_id="2.6",
|
|
29
|
+
resource_types=["AWS::IAM::User"]
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
def _get_resources(self, aws_factory: AWSClientFactory, resource_type: str, region: str) -> List[Dict[str, Any]]:
|
|
33
|
+
"""Get all IAM users and check for admin privileges and MFA."""
|
|
34
|
+
if resource_type != "AWS::IAM::User":
|
|
35
|
+
return []
|
|
36
|
+
|
|
37
|
+
# IAM is global, only check in us-east-1
|
|
38
|
+
if region != 'us-east-1':
|
|
39
|
+
return []
|
|
40
|
+
|
|
41
|
+
try:
|
|
42
|
+
iam_client = aws_factory.get_client('iam', region)
|
|
43
|
+
|
|
44
|
+
users = []
|
|
45
|
+
paginator = iam_client.get_paginator('list_users')
|
|
46
|
+
|
|
47
|
+
for page in paginator.paginate():
|
|
48
|
+
for user in page.get('Users', []):
|
|
49
|
+
user_name = user.get('UserName', '')
|
|
50
|
+
|
|
51
|
+
# Check if user has MFA
|
|
52
|
+
try:
|
|
53
|
+
mfa_devices = iam_client.list_mfa_devices(UserName=user_name)
|
|
54
|
+
has_mfa = len(mfa_devices.get('MFADevices', [])) > 0
|
|
55
|
+
except ClientError:
|
|
56
|
+
has_mfa = False
|
|
57
|
+
|
|
58
|
+
# Check if user has admin policies
|
|
59
|
+
is_admin = self._is_admin_user(iam_client, user_name)
|
|
60
|
+
|
|
61
|
+
if is_admin: # Only include admin users
|
|
62
|
+
users.append({
|
|
63
|
+
'UserName': user_name,
|
|
64
|
+
'UserArn': user.get('Arn', ''),
|
|
65
|
+
'HasMFA': has_mfa,
|
|
66
|
+
'IsAdmin': is_admin
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
logger.debug(f"Found {len(users)} admin IAM users")
|
|
70
|
+
return users
|
|
71
|
+
|
|
72
|
+
except ClientError as e:
|
|
73
|
+
logger.error(f"Error retrieving IAM users: {e}")
|
|
74
|
+
return []
|
|
75
|
+
|
|
76
|
+
def _is_admin_user(self, iam_client, user_name: str) -> bool:
|
|
77
|
+
"""Check if user has administrative privileges."""
|
|
78
|
+
try:
|
|
79
|
+
# Check attached policies
|
|
80
|
+
attached_policies = iam_client.list_attached_user_policies(UserName=user_name)
|
|
81
|
+
for policy in attached_policies.get('AttachedPolicies', []):
|
|
82
|
+
policy_arn = policy.get('PolicyArn', '')
|
|
83
|
+
if 'AdministratorAccess' in policy_arn or 'PowerUserAccess' in policy_arn:
|
|
84
|
+
return True
|
|
85
|
+
|
|
86
|
+
# Check inline policies
|
|
87
|
+
inline_policies = iam_client.list_user_policies(UserName=user_name)
|
|
88
|
+
for policy_name in inline_policies.get('PolicyNames', []):
|
|
89
|
+
policy_doc = iam_client.get_user_policy(UserName=user_name, PolicyName=policy_name)
|
|
90
|
+
policy_document = policy_doc.get('PolicyDocument', {})
|
|
91
|
+
if self._has_admin_permissions(policy_document):
|
|
92
|
+
return True
|
|
93
|
+
|
|
94
|
+
# Check group memberships
|
|
95
|
+
groups = iam_client.list_groups_for_user(UserName=user_name)
|
|
96
|
+
for group in groups.get('Groups', []):
|
|
97
|
+
group_name = group.get('GroupName', '')
|
|
98
|
+
if 'admin' in group_name.lower():
|
|
99
|
+
return True
|
|
100
|
+
|
|
101
|
+
return False
|
|
102
|
+
except ClientError:
|
|
103
|
+
return False
|
|
104
|
+
|
|
105
|
+
def _has_admin_permissions(self, policy_document: Dict) -> bool:
|
|
106
|
+
"""Check if policy document grants admin permissions."""
|
|
107
|
+
statements = policy_document.get('Statement', [])
|
|
108
|
+
for statement in statements:
|
|
109
|
+
if statement.get('Effect') == 'Allow':
|
|
110
|
+
actions = statement.get('Action', [])
|
|
111
|
+
if isinstance(actions, str):
|
|
112
|
+
actions = [actions]
|
|
113
|
+
if '*' in actions or 'iam:*' in actions:
|
|
114
|
+
return True
|
|
115
|
+
return False
|
|
116
|
+
|
|
117
|
+
def _evaluate_resource_compliance(
|
|
118
|
+
self,
|
|
119
|
+
resource: Dict[str, Any],
|
|
120
|
+
aws_factory: AWSClientFactory,
|
|
121
|
+
region: str
|
|
122
|
+
) -> ComplianceResult:
|
|
123
|
+
"""Evaluate if admin user has MFA enabled."""
|
|
124
|
+
user_name = resource.get('UserName', 'unknown')
|
|
125
|
+
has_mfa = resource.get('HasMFA', False)
|
|
126
|
+
|
|
127
|
+
if has_mfa:
|
|
128
|
+
evaluation_reason = f"IAM admin user '{user_name}' has MFA enabled."
|
|
129
|
+
compliance_status = ComplianceStatus.COMPLIANT
|
|
130
|
+
else:
|
|
131
|
+
evaluation_reason = (
|
|
132
|
+
f"IAM admin user '{user_name}' does not have MFA enabled. "
|
|
133
|
+
f"MFA is required for all administrative users."
|
|
134
|
+
)
|
|
135
|
+
compliance_status = ComplianceStatus.NON_COMPLIANT
|
|
136
|
+
|
|
137
|
+
return ComplianceResult(
|
|
138
|
+
resource_id=user_name,
|
|
139
|
+
resource_type="AWS::IAM::User",
|
|
140
|
+
compliance_status=compliance_status,
|
|
141
|
+
evaluation_reason=evaluation_reason,
|
|
142
|
+
config_rule_name=self.rule_name,
|
|
143
|
+
region=region
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
def _get_rule_remediation_steps(self) -> List[str]:
|
|
147
|
+
"""Get remediation steps for enabling MFA for admin users."""
|
|
148
|
+
return [
|
|
149
|
+
"1. Enable MFA for IAM user via Console:",
|
|
150
|
+
" - Sign in to AWS Console",
|
|
151
|
+
" - Navigate to IAM > Users",
|
|
152
|
+
" - Select the user",
|
|
153
|
+
" - Click 'Security credentials' tab",
|
|
154
|
+
" - Under 'Multi-factor authentication (MFA)', click 'Assign MFA device'",
|
|
155
|
+
" - Choose MFA device type:",
|
|
156
|
+
" * Authenticator app (recommended)",
|
|
157
|
+
" * Security key (FIDO)",
|
|
158
|
+
" * Hardware TOTP token",
|
|
159
|
+
" - Follow the setup wizard",
|
|
160
|
+
"",
|
|
161
|
+
"2. Enable MFA via AWS CLI:",
|
|
162
|
+
" # For virtual MFA device",
|
|
163
|
+
" aws iam create-virtual-mfa-device \\",
|
|
164
|
+
" --virtual-mfa-device-name <user-name>-mfa \\",
|
|
165
|
+
" --outfile QRCode.png \\",
|
|
166
|
+
" --bootstrap-method QRCodePNG",
|
|
167
|
+
"",
|
|
168
|
+
" # Scan QR code with authenticator app, then enable",
|
|
169
|
+
" aws iam enable-mfa-device \\",
|
|
170
|
+
" --user-name <user-name> \\",
|
|
171
|
+
" --serial-number arn:aws:iam::<account-id>:mfa/<user-name>-mfa \\",
|
|
172
|
+
" --authentication-code-1 <code1> \\",
|
|
173
|
+
" --authentication-code-2 <code2>",
|
|
174
|
+
"",
|
|
175
|
+
"3. Enforce MFA with IAM policy:",
|
|
176
|
+
" {",
|
|
177
|
+
' "Version": "2012-10-17",',
|
|
178
|
+
' "Statement": [{',
|
|
179
|
+
' "Sid": "DenyAllExceptListedIfNoMFA",',
|
|
180
|
+
' "Effect": "Deny",',
|
|
181
|
+
' "NotAction": [',
|
|
182
|
+
' "iam:CreateVirtualMFADevice",',
|
|
183
|
+
' "iam:EnableMFADevice",',
|
|
184
|
+
' "iam:GetUser",',
|
|
185
|
+
' "iam:ListMFADevices",',
|
|
186
|
+
' "iam:ListVirtualMFADevices",',
|
|
187
|
+
' "iam:ResyncMFADevice",',
|
|
188
|
+
' "sts:GetSessionToken"',
|
|
189
|
+
" ],",
|
|
190
|
+
' "Resource": "*",',
|
|
191
|
+
' "Condition": {',
|
|
192
|
+
' "BoolIfExists": {"aws:MultiFactorAuthPresent": "false"}',
|
|
193
|
+
" }",
|
|
194
|
+
" }]",
|
|
195
|
+
" }",
|
|
196
|
+
"",
|
|
197
|
+
"4. Best practices:",
|
|
198
|
+
" - Require MFA for all admin users",
|
|
199
|
+
" - Use authenticator apps (Google Authenticator, Authy, etc.)",
|
|
200
|
+
" - Consider hardware security keys for highest security",
|
|
201
|
+
" - Store backup codes securely",
|
|
202
|
+
" - Regularly audit MFA compliance",
|
|
203
|
+
" - Disable console access for users without MFA",
|
|
204
|
+
"",
|
|
205
|
+
"Priority: CRITICAL - Admin access without MFA is a major security risk",
|
|
206
|
+
"Effort: Low - Can be enabled in minutes per user",
|
|
207
|
+
"",
|
|
208
|
+
"AWS Documentation:",
|
|
209
|
+
"https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_mfa.html"
|
|
210
|
+
]
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
class CognitoMFAEnabledAssessment(BaseConfigRuleAssessment):
|
|
214
|
+
"""
|
|
215
|
+
CIS Control 2.7 - Establish an Access Granting Process
|
|
216
|
+
AWS Config Rule: cognito-mfa-enabled
|
|
217
|
+
|
|
218
|
+
Ensures Cognito user pools have MFA enabled for user authentication.
|
|
219
|
+
"""
|
|
220
|
+
|
|
221
|
+
def __init__(self):
|
|
222
|
+
super().__init__(
|
|
223
|
+
rule_name="cognito-mfa-enabled",
|
|
224
|
+
control_id="2.7",
|
|
225
|
+
resource_types=["AWS::Cognito::UserPool"]
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
def _get_resources(self, aws_factory: AWSClientFactory, resource_type: str, region: str) -> List[Dict[str, Any]]:
|
|
229
|
+
"""Get all Cognito user pools and their MFA configuration."""
|
|
230
|
+
if resource_type != "AWS::Cognito::UserPool":
|
|
231
|
+
return []
|
|
232
|
+
|
|
233
|
+
try:
|
|
234
|
+
cognito_client = aws_factory.get_client('cognito-idp', region)
|
|
235
|
+
|
|
236
|
+
user_pools = []
|
|
237
|
+
paginator = cognito_client.get_paginator('list_user_pools')
|
|
238
|
+
|
|
239
|
+
for page in paginator.paginate(MaxResults=60):
|
|
240
|
+
for pool in page.get('UserPools', []):
|
|
241
|
+
pool_id = pool.get('Id', '')
|
|
242
|
+
pool_name = pool.get('Name', '')
|
|
243
|
+
|
|
244
|
+
try:
|
|
245
|
+
# Get detailed pool configuration
|
|
246
|
+
pool_details = cognito_client.describe_user_pool(UserPoolId=pool_id)
|
|
247
|
+
user_pool = pool_details.get('UserPool', {})
|
|
248
|
+
|
|
249
|
+
mfa_config = user_pool.get('MfaConfiguration', 'OFF')
|
|
250
|
+
|
|
251
|
+
user_pools.append({
|
|
252
|
+
'UserPoolId': pool_id,
|
|
253
|
+
'UserPoolName': pool_name,
|
|
254
|
+
'MfaConfiguration': mfa_config,
|
|
255
|
+
'MfaEnabled': mfa_config in ['ON', 'OPTIONAL']
|
|
256
|
+
})
|
|
257
|
+
except ClientError as e:
|
|
258
|
+
logger.warning(f"Error describing user pool {pool_id}: {e}")
|
|
259
|
+
continue
|
|
260
|
+
|
|
261
|
+
logger.debug(f"Found {len(user_pools)} Cognito user pools in {region}")
|
|
262
|
+
return user_pools
|
|
263
|
+
|
|
264
|
+
except ClientError as e:
|
|
265
|
+
logger.error(f"Error retrieving Cognito user pools from {region}: {e}")
|
|
266
|
+
return []
|
|
267
|
+
|
|
268
|
+
def _evaluate_resource_compliance(
|
|
269
|
+
self,
|
|
270
|
+
resource: Dict[str, Any],
|
|
271
|
+
aws_factory: AWSClientFactory,
|
|
272
|
+
region: str
|
|
273
|
+
) -> ComplianceResult:
|
|
274
|
+
"""Evaluate if Cognito user pool has MFA enabled."""
|
|
275
|
+
pool_id = resource.get('UserPoolId', 'unknown')
|
|
276
|
+
pool_name = resource.get('UserPoolName', '')
|
|
277
|
+
mfa_config = resource.get('MfaConfiguration', 'OFF')
|
|
278
|
+
mfa_enabled = resource.get('MfaEnabled', False)
|
|
279
|
+
|
|
280
|
+
if mfa_enabled:
|
|
281
|
+
evaluation_reason = (
|
|
282
|
+
f"Cognito user pool '{pool_name}' has MFA configured as '{mfa_config}'."
|
|
283
|
+
)
|
|
284
|
+
compliance_status = ComplianceStatus.COMPLIANT
|
|
285
|
+
else:
|
|
286
|
+
evaluation_reason = (
|
|
287
|
+
f"Cognito user pool '{pool_name}' does not have MFA enabled. "
|
|
288
|
+
f"Current configuration: {mfa_config}"
|
|
289
|
+
)
|
|
290
|
+
compliance_status = ComplianceStatus.NON_COMPLIANT
|
|
291
|
+
|
|
292
|
+
return ComplianceResult(
|
|
293
|
+
resource_id=pool_id,
|
|
294
|
+
resource_type="AWS::Cognito::UserPool",
|
|
295
|
+
compliance_status=compliance_status,
|
|
296
|
+
evaluation_reason=evaluation_reason,
|
|
297
|
+
config_rule_name=self.rule_name,
|
|
298
|
+
region=region
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
def _get_rule_remediation_steps(self) -> List[str]:
|
|
302
|
+
"""Get remediation steps for enabling Cognito MFA."""
|
|
303
|
+
return [
|
|
304
|
+
"1. Enable MFA for Cognito user pool via Console:",
|
|
305
|
+
" - Navigate to Amazon Cognito",
|
|
306
|
+
" - Select 'User pools'",
|
|
307
|
+
" - Select the user pool",
|
|
308
|
+
" - Click 'Sign-in experience' tab",
|
|
309
|
+
" - Under 'Multi-factor authentication', click 'Edit'",
|
|
310
|
+
" - Select MFA enforcement:",
|
|
311
|
+
" * Required - MFA required for all users",
|
|
312
|
+
" * Optional - Users can choose to enable MFA",
|
|
313
|
+
" - Select MFA methods:",
|
|
314
|
+
" * SMS text message",
|
|
315
|
+
" * Authenticator apps (TOTP)",
|
|
316
|
+
" - Click 'Save changes'",
|
|
317
|
+
"",
|
|
318
|
+
"2. Enable MFA via AWS CLI:",
|
|
319
|
+
" aws cognito-idp set-user-pool-mfa-config \\",
|
|
320
|
+
" --user-pool-id <pool-id> \\",
|
|
321
|
+
" --mfa-configuration ON \\",
|
|
322
|
+
" --software-token-mfa-configuration Enabled=true \\",
|
|
323
|
+
" --region <region>",
|
|
324
|
+
"",
|
|
325
|
+
"3. Enable SMS MFA:",
|
|
326
|
+
" aws cognito-idp set-user-pool-mfa-config \\",
|
|
327
|
+
" --user-pool-id <pool-id> \\",
|
|
328
|
+
" --mfa-configuration ON \\",
|
|
329
|
+
" --sms-mfa-configuration SmsConfiguration={SnsCallerArn=<sns-role-arn>} \\",
|
|
330
|
+
" --region <region>",
|
|
331
|
+
"",
|
|
332
|
+
"4. Best practices:",
|
|
333
|
+
" - Use 'Required' for production applications",
|
|
334
|
+
" - Prefer authenticator apps over SMS",
|
|
335
|
+
" - Configure both SMS and TOTP for flexibility",
|
|
336
|
+
" - Set up SNS for SMS delivery",
|
|
337
|
+
" - Monitor MFA usage with CloudWatch",
|
|
338
|
+
"",
|
|
339
|
+
"Priority: HIGH - MFA protects user accounts from compromise",
|
|
340
|
+
"Effort: Low - Can be enabled quickly",
|
|
341
|
+
"",
|
|
342
|
+
"AWS Documentation:",
|
|
343
|
+
"https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-settings-mfa.html"
|
|
344
|
+
]
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
class VPNMFAEnabledAssessment(BaseConfigRuleAssessment):
|
|
348
|
+
"""
|
|
349
|
+
CIS Control 2.15 - Secure Remote Access
|
|
350
|
+
AWS Config Rule: vpn-mfa-enabled
|
|
351
|
+
|
|
352
|
+
Ensures Client VPN endpoints require MFA for authentication.
|
|
353
|
+
"""
|
|
354
|
+
|
|
355
|
+
def __init__(self):
|
|
356
|
+
super().__init__(
|
|
357
|
+
rule_name="vpn-mfa-enabled",
|
|
358
|
+
control_id="2.15",
|
|
359
|
+
resource_types=["AWS::EC2::ClientVpnEndpoint"]
|
|
360
|
+
)
|
|
361
|
+
|
|
362
|
+
def _get_resources(self, aws_factory: AWSClientFactory, resource_type: str, region: str) -> List[Dict[str, Any]]:
|
|
363
|
+
"""Get all Client VPN endpoints and their MFA configuration."""
|
|
364
|
+
if resource_type != "AWS::EC2::ClientVpnEndpoint":
|
|
365
|
+
return []
|
|
366
|
+
|
|
367
|
+
try:
|
|
368
|
+
ec2_client = aws_factory.get_client('ec2', region)
|
|
369
|
+
|
|
370
|
+
vpn_endpoints = []
|
|
371
|
+
paginator = ec2_client.get_paginator('describe_client_vpn_endpoints')
|
|
372
|
+
|
|
373
|
+
for page in paginator.paginate():
|
|
374
|
+
for endpoint in page.get('ClientVpnEndpoints', []):
|
|
375
|
+
endpoint_id = endpoint.get('ClientVpnEndpointId', '')
|
|
376
|
+
|
|
377
|
+
# Check authentication options
|
|
378
|
+
auth_options = endpoint.get('AuthenticationOptions', [])
|
|
379
|
+
has_mfa = False
|
|
380
|
+
auth_types = []
|
|
381
|
+
|
|
382
|
+
for auth in auth_options:
|
|
383
|
+
auth_type = auth.get('Type', '')
|
|
384
|
+
auth_types.append(auth_type)
|
|
385
|
+
|
|
386
|
+
# Check if MFA is enabled for this auth method
|
|
387
|
+
if auth_type == 'federated-authentication':
|
|
388
|
+
# SAML-based auth can enforce MFA at IdP level
|
|
389
|
+
has_mfa = True
|
|
390
|
+
elif auth_type == 'directory-service-authentication':
|
|
391
|
+
# AD can enforce MFA
|
|
392
|
+
has_mfa = True
|
|
393
|
+
|
|
394
|
+
vpn_endpoints.append({
|
|
395
|
+
'ClientVpnEndpointId': endpoint_id,
|
|
396
|
+
'Description': endpoint.get('Description', ''),
|
|
397
|
+
'Status': endpoint.get('Status', {}).get('Code', ''),
|
|
398
|
+
'AuthenticationOptions': auth_types,
|
|
399
|
+
'HasMFA': has_mfa
|
|
400
|
+
})
|
|
401
|
+
|
|
402
|
+
logger.debug(f"Found {len(vpn_endpoints)} Client VPN endpoints in {region}")
|
|
403
|
+
return vpn_endpoints
|
|
404
|
+
|
|
405
|
+
except ClientError as e:
|
|
406
|
+
logger.error(f"Error retrieving Client VPN endpoints from {region}: {e}")
|
|
407
|
+
return []
|
|
408
|
+
|
|
409
|
+
def _evaluate_resource_compliance(
|
|
410
|
+
self,
|
|
411
|
+
resource: Dict[str, Any],
|
|
412
|
+
aws_factory: AWSClientFactory,
|
|
413
|
+
region: str
|
|
414
|
+
) -> ComplianceResult:
|
|
415
|
+
"""Evaluate if VPN endpoint has MFA enabled."""
|
|
416
|
+
endpoint_id = resource.get('ClientVpnEndpointId', 'unknown')
|
|
417
|
+
has_mfa = resource.get('HasMFA', False)
|
|
418
|
+
auth_options = resource.get('AuthenticationOptions', [])
|
|
419
|
+
|
|
420
|
+
if has_mfa:
|
|
421
|
+
evaluation_reason = (
|
|
422
|
+
f"Client VPN endpoint '{endpoint_id}' uses authentication methods that support MFA: "
|
|
423
|
+
f"{', '.join(auth_options)}"
|
|
424
|
+
)
|
|
425
|
+
compliance_status = ComplianceStatus.COMPLIANT
|
|
426
|
+
else:
|
|
427
|
+
evaluation_reason = (
|
|
428
|
+
f"Client VPN endpoint '{endpoint_id}' does not have MFA-capable authentication configured. "
|
|
429
|
+
f"Current auth: {', '.join(auth_options) if auth_options else 'None'}"
|
|
430
|
+
)
|
|
431
|
+
compliance_status = ComplianceStatus.NON_COMPLIANT
|
|
432
|
+
|
|
433
|
+
return ComplianceResult(
|
|
434
|
+
resource_id=endpoint_id,
|
|
435
|
+
resource_type="AWS::EC2::ClientVpnEndpoint",
|
|
436
|
+
compliance_status=compliance_status,
|
|
437
|
+
evaluation_reason=evaluation_reason,
|
|
438
|
+
config_rule_name=self.rule_name,
|
|
439
|
+
region=region
|
|
440
|
+
)
|
|
441
|
+
|
|
442
|
+
def _get_rule_remediation_steps(self) -> List[str]:
|
|
443
|
+
"""Get remediation steps for enabling VPN MFA."""
|
|
444
|
+
return [
|
|
445
|
+
"1. Configure MFA for Client VPN using SAML-based authentication:",
|
|
446
|
+
" - Set up SAML 2.0 identity provider (Okta, Azure AD, etc.)",
|
|
447
|
+
" - Configure MFA in your IdP",
|
|
448
|
+
" - Create Client VPN endpoint with federated authentication:",
|
|
449
|
+
" ",
|
|
450
|
+
" aws ec2 create-client-vpn-endpoint \\",
|
|
451
|
+
" --client-cidr-block 10.0.0.0/16 \\",
|
|
452
|
+
" --server-certificate-arn <cert-arn> \\",
|
|
453
|
+
" --authentication-options Type=federated-authentication,SAMLProviderArn=<saml-provider-arn> \\",
|
|
454
|
+
" --connection-log-options Enabled=true,CloudwatchLogGroup=<log-group> \\",
|
|
455
|
+
" --region <region>",
|
|
456
|
+
"",
|
|
457
|
+
"2. Configure MFA using AWS Directory Service:",
|
|
458
|
+
" - Set up AWS Managed Microsoft AD or AD Connector",
|
|
459
|
+
" - Enable MFA in Active Directory",
|
|
460
|
+
" - Create Client VPN with directory authentication:",
|
|
461
|
+
" ",
|
|
462
|
+
" aws ec2 create-client-vpn-endpoint \\",
|
|
463
|
+
" --client-cidr-block 10.0.0.0/16 \\",
|
|
464
|
+
" --server-certificate-arn <cert-arn> \\",
|
|
465
|
+
" --authentication-options Type=directory-service-authentication,ActiveDirectory={DirectoryId=<directory-id>} \\",
|
|
466
|
+
" --region <region>",
|
|
467
|
+
"",
|
|
468
|
+
"3. Modify existing VPN endpoint (requires recreation):",
|
|
469
|
+
" Note: You cannot modify authentication options on existing endpoints",
|
|
470
|
+
" You must create a new endpoint with MFA-enabled authentication",
|
|
471
|
+
"",
|
|
472
|
+
"4. Best practices:",
|
|
473
|
+
" - Use SAML-based authentication with enterprise IdP",
|
|
474
|
+
" - Enforce MFA at the IdP level",
|
|
475
|
+
" - Enable connection logging",
|
|
476
|
+
" - Use certificate-based authentication in addition to MFA",
|
|
477
|
+
" - Implement least privilege access",
|
|
478
|
+
" - Monitor VPN connections with CloudWatch",
|
|
479
|
+
"",
|
|
480
|
+
"Priority: CRITICAL - VPN access without MFA is a major security risk",
|
|
481
|
+
"Effort: High - Requires IdP setup and VPN endpoint recreation",
|
|
482
|
+
"",
|
|
483
|
+
"AWS Documentation:",
|
|
484
|
+
"https://docs.aws.amazon.com/vpn/latest/clientvpn-admin/client-authentication.html"
|
|
485
|
+
]
|