aws-cis-controls-assessment 1.0.3__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 +11 -0
- aws_cis_assessment/cli/__init__.py +3 -0
- aws_cis_assessment/cli/examples.py +274 -0
- aws_cis_assessment/cli/main.py +1259 -0
- aws_cis_assessment/cli/utils.py +356 -0
- aws_cis_assessment/config/__init__.py +1 -0
- aws_cis_assessment/config/config_loader.py +328 -0
- aws_cis_assessment/config/rules/cis_controls_ig1.yaml +590 -0
- aws_cis_assessment/config/rules/cis_controls_ig2.yaml +412 -0
- aws_cis_assessment/config/rules/cis_controls_ig3.yaml +100 -0
- aws_cis_assessment/controls/__init__.py +1 -0
- aws_cis_assessment/controls/base_control.py +400 -0
- aws_cis_assessment/controls/ig1/__init__.py +239 -0
- aws_cis_assessment/controls/ig1/control_1_1.py +586 -0
- aws_cis_assessment/controls/ig1/control_2_2.py +231 -0
- aws_cis_assessment/controls/ig1/control_3_3.py +718 -0
- aws_cis_assessment/controls/ig1/control_3_4.py +235 -0
- aws_cis_assessment/controls/ig1/control_4_1.py +461 -0
- aws_cis_assessment/controls/ig1/control_access_keys.py +310 -0
- aws_cis_assessment/controls/ig1/control_advanced_security.py +512 -0
- aws_cis_assessment/controls/ig1/control_backup_recovery.py +510 -0
- aws_cis_assessment/controls/ig1/control_cloudtrail_logging.py +197 -0
- aws_cis_assessment/controls/ig1/control_critical_security.py +422 -0
- aws_cis_assessment/controls/ig1/control_data_protection.py +898 -0
- aws_cis_assessment/controls/ig1/control_iam_advanced.py +573 -0
- aws_cis_assessment/controls/ig1/control_iam_governance.py +493 -0
- aws_cis_assessment/controls/ig1/control_iam_policies.py +383 -0
- aws_cis_assessment/controls/ig1/control_instance_optimization.py +100 -0
- aws_cis_assessment/controls/ig1/control_network_enhancements.py +203 -0
- aws_cis_assessment/controls/ig1/control_network_security.py +672 -0
- aws_cis_assessment/controls/ig1/control_s3_enhancements.py +173 -0
- aws_cis_assessment/controls/ig1/control_s3_security.py +422 -0
- aws_cis_assessment/controls/ig1/control_vpc_security.py +235 -0
- aws_cis_assessment/controls/ig2/__init__.py +172 -0
- aws_cis_assessment/controls/ig2/control_3_10.py +698 -0
- aws_cis_assessment/controls/ig2/control_3_11.py +1330 -0
- aws_cis_assessment/controls/ig2/control_5_2.py +393 -0
- aws_cis_assessment/controls/ig2/control_advanced_encryption.py +355 -0
- aws_cis_assessment/controls/ig2/control_codebuild_security.py +263 -0
- aws_cis_assessment/controls/ig2/control_encryption_rest.py +382 -0
- aws_cis_assessment/controls/ig2/control_encryption_transit.py +382 -0
- aws_cis_assessment/controls/ig2/control_network_ha.py +467 -0
- aws_cis_assessment/controls/ig2/control_remaining_encryption.py +426 -0
- aws_cis_assessment/controls/ig2/control_remaining_rules.py +363 -0
- aws_cis_assessment/controls/ig2/control_service_logging.py +402 -0
- aws_cis_assessment/controls/ig3/__init__.py +49 -0
- aws_cis_assessment/controls/ig3/control_12_8.py +395 -0
- aws_cis_assessment/controls/ig3/control_13_1.py +467 -0
- aws_cis_assessment/controls/ig3/control_3_14.py +523 -0
- aws_cis_assessment/controls/ig3/control_7_1.py +359 -0
- aws_cis_assessment/core/__init__.py +1 -0
- aws_cis_assessment/core/accuracy_validator.py +425 -0
- aws_cis_assessment/core/assessment_engine.py +1266 -0
- aws_cis_assessment/core/audit_trail.py +491 -0
- aws_cis_assessment/core/aws_client_factory.py +313 -0
- aws_cis_assessment/core/error_handler.py +607 -0
- aws_cis_assessment/core/models.py +166 -0
- aws_cis_assessment/core/scoring_engine.py +459 -0
- aws_cis_assessment/reporters/__init__.py +8 -0
- aws_cis_assessment/reporters/base_reporter.py +454 -0
- aws_cis_assessment/reporters/csv_reporter.py +835 -0
- aws_cis_assessment/reporters/html_reporter.py +2162 -0
- aws_cis_assessment/reporters/json_reporter.py +561 -0
- aws_cis_controls_assessment-1.0.3.dist-info/METADATA +248 -0
- aws_cis_controls_assessment-1.0.3.dist-info/RECORD +77 -0
- aws_cis_controls_assessment-1.0.3.dist-info/WHEEL +5 -0
- aws_cis_controls_assessment-1.0.3.dist-info/entry_points.txt +2 -0
- aws_cis_controls_assessment-1.0.3.dist-info/licenses/LICENSE +21 -0
- aws_cis_controls_assessment-1.0.3.dist-info/top_level.txt +2 -0
- docs/README.md +94 -0
- docs/assessment-logic.md +766 -0
- docs/cli-reference.md +698 -0
- docs/config-rule-mappings.md +393 -0
- docs/developer-guide.md +858 -0
- docs/installation.md +299 -0
- docs/troubleshooting.md +634 -0
- docs/user-guide.md +487 -0
|
@@ -0,0 +1,467 @@
|
|
|
1
|
+
"""Control 13.1: Centralize Security Event Alerting assessments."""
|
|
2
|
+
|
|
3
|
+
from typing import Dict, List, Any
|
|
4
|
+
import logging
|
|
5
|
+
import json
|
|
6
|
+
from botocore.exceptions import ClientError
|
|
7
|
+
|
|
8
|
+
from aws_cis_assessment.controls.base_control import BaseConfigRuleAssessment
|
|
9
|
+
from aws_cis_assessment.core.models import ComplianceResult, ComplianceStatus
|
|
10
|
+
from aws_cis_assessment.core.aws_client_factory import AWSClientFactory
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class RestrictedIncomingTrafficAssessment(BaseConfigRuleAssessment):
|
|
16
|
+
"""Assessment for restricted-incoming-traffic Config rule."""
|
|
17
|
+
|
|
18
|
+
def __init__(self):
|
|
19
|
+
"""Initialize restricted incoming traffic assessment."""
|
|
20
|
+
super().__init__(
|
|
21
|
+
rule_name="restricted-incoming-traffic",
|
|
22
|
+
control_id="13.1",
|
|
23
|
+
resource_types=["AWS::EC2::SecurityGroup"],
|
|
24
|
+
parameters={
|
|
25
|
+
"blockedPort1": "20",
|
|
26
|
+
"blockedPort2": "21",
|
|
27
|
+
"blockedPort3": "3389",
|
|
28
|
+
"blockedPort4": "3306",
|
|
29
|
+
"blockedPort5": "4333"
|
|
30
|
+
}
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
def _get_resources(self, aws_factory: AWSClientFactory, resource_type: str, region: str) -> List[Dict[str, Any]]:
|
|
34
|
+
"""Get all security groups in the region."""
|
|
35
|
+
if resource_type != "AWS::EC2::SecurityGroup":
|
|
36
|
+
return []
|
|
37
|
+
|
|
38
|
+
try:
|
|
39
|
+
ec2_client = aws_factory.get_client('ec2', region)
|
|
40
|
+
|
|
41
|
+
response = aws_factory.aws_api_call_with_retry(
|
|
42
|
+
lambda: ec2_client.describe_security_groups()
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
security_groups = []
|
|
46
|
+
for sg in response.get('SecurityGroups', []):
|
|
47
|
+
security_groups.append({
|
|
48
|
+
'GroupId': sg.get('GroupId'),
|
|
49
|
+
'GroupName': sg.get('GroupName'),
|
|
50
|
+
'Description': sg.get('Description'),
|
|
51
|
+
'VpcId': sg.get('VpcId'),
|
|
52
|
+
'IpPermissions': sg.get('IpPermissions', []),
|
|
53
|
+
'IpPermissionsEgress': sg.get('IpPermissionsEgress', []),
|
|
54
|
+
'Tags': sg.get('Tags', [])
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
logger.debug(f"Found {len(security_groups)} security groups in region {region}")
|
|
58
|
+
return security_groups
|
|
59
|
+
|
|
60
|
+
except ClientError as e:
|
|
61
|
+
logger.error(f"Error retrieving security groups in region {region}: {e}")
|
|
62
|
+
raise
|
|
63
|
+
except Exception as e:
|
|
64
|
+
logger.error(f"Unexpected error retrieving security groups in region {region}: {e}")
|
|
65
|
+
raise
|
|
66
|
+
|
|
67
|
+
def _evaluate_resource_compliance(self, resource: Dict[str, Any], aws_factory: AWSClientFactory, region: str) -> ComplianceResult:
|
|
68
|
+
"""Evaluate if security group restricts incoming traffic on blocked ports."""
|
|
69
|
+
group_id = resource.get('GroupId', 'unknown')
|
|
70
|
+
group_name = resource.get('GroupName', 'unknown')
|
|
71
|
+
ip_permissions = resource.get('IpPermissions', [])
|
|
72
|
+
|
|
73
|
+
# Get blocked ports from parameters
|
|
74
|
+
blocked_ports = set()
|
|
75
|
+
for i in range(1, 6): # blockedPort1 through blockedPort5
|
|
76
|
+
port_param = self.parameters.get(f'blockedPort{i}')
|
|
77
|
+
if port_param:
|
|
78
|
+
try:
|
|
79
|
+
blocked_ports.add(int(port_param))
|
|
80
|
+
except ValueError:
|
|
81
|
+
logger.warning(f"Invalid blocked port parameter: {port_param}")
|
|
82
|
+
|
|
83
|
+
# Check for rules allowing unrestricted access to blocked ports
|
|
84
|
+
violations = []
|
|
85
|
+
|
|
86
|
+
for rule in ip_permissions:
|
|
87
|
+
ip_protocol = rule.get('IpProtocol', '')
|
|
88
|
+
from_port = rule.get('FromPort')
|
|
89
|
+
to_port = rule.get('ToPort')
|
|
90
|
+
ip_ranges = rule.get('IpRanges', [])
|
|
91
|
+
ipv6_ranges = rule.get('Ipv6Ranges', [])
|
|
92
|
+
|
|
93
|
+
# Check if rule allows access from anywhere (0.0.0.0/0 or ::/0)
|
|
94
|
+
allows_public_access = False
|
|
95
|
+
for ip_range in ip_ranges:
|
|
96
|
+
if ip_range.get('CidrIp') == '0.0.0.0/0':
|
|
97
|
+
allows_public_access = True
|
|
98
|
+
break
|
|
99
|
+
|
|
100
|
+
if not allows_public_access:
|
|
101
|
+
for ipv6_range in ipv6_ranges:
|
|
102
|
+
if ipv6_range.get('CidrIpv6') == '::/0':
|
|
103
|
+
allows_public_access = True
|
|
104
|
+
break
|
|
105
|
+
|
|
106
|
+
# If rule allows public access, check if it includes blocked ports
|
|
107
|
+
if allows_public_access:
|
|
108
|
+
if ip_protocol == '-1': # All protocols
|
|
109
|
+
violations.extend([{'port': port, 'protocol': 'All'} for port in blocked_ports])
|
|
110
|
+
elif ip_protocol.lower() in ['tcp', 'udp'] and from_port is not None and to_port is not None:
|
|
111
|
+
# Check if any blocked ports fall within the range
|
|
112
|
+
for port in blocked_ports:
|
|
113
|
+
if from_port <= port <= to_port:
|
|
114
|
+
violations.append({
|
|
115
|
+
'port': port,
|
|
116
|
+
'protocol': ip_protocol,
|
|
117
|
+
'from_port': from_port,
|
|
118
|
+
'to_port': to_port
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
if violations:
|
|
122
|
+
compliance_status = ComplianceStatus.NON_COMPLIANT
|
|
123
|
+
violation_ports = set(v['port'] for v in violations)
|
|
124
|
+
evaluation_reason = f"Security group {group_name} ({group_id}) allows unrestricted access to blocked ports: {sorted(violation_ports)}"
|
|
125
|
+
else:
|
|
126
|
+
compliance_status = ComplianceStatus.COMPLIANT
|
|
127
|
+
evaluation_reason = f"Security group {group_name} ({group_id}) properly restricts access to blocked ports: {sorted(blocked_ports)}"
|
|
128
|
+
|
|
129
|
+
return ComplianceResult(
|
|
130
|
+
resource_id=group_id,
|
|
131
|
+
resource_type="AWS::EC2::SecurityGroup",
|
|
132
|
+
compliance_status=compliance_status,
|
|
133
|
+
evaluation_reason=evaluation_reason,
|
|
134
|
+
config_rule_name=self.rule_name,
|
|
135
|
+
region=region
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
def _get_rule_remediation_steps(self) -> List[str]:
|
|
139
|
+
"""Get specific remediation steps for restricting incoming traffic."""
|
|
140
|
+
return [
|
|
141
|
+
"Remove security group rules allowing unrestricted access to blocked ports",
|
|
142
|
+
"For each non-compliant security group:",
|
|
143
|
+
" 1. Review inbound rules allowing public access (0.0.0.0/0 or ::/0)",
|
|
144
|
+
" 2. Remove rules allowing access to commonly attacked ports (20, 21, 3389, 3306, 4333)",
|
|
145
|
+
" 3. Replace with more restrictive CIDR blocks if access is needed",
|
|
146
|
+
" 4. Use bastion hosts or VPN for administrative access",
|
|
147
|
+
"Remove unrestricted access to FTP (port 20, 21):",
|
|
148
|
+
"aws ec2 revoke-security-group-ingress --group-id <sg-id> --protocol tcp --port 20 --cidr 0.0.0.0/0",
|
|
149
|
+
"aws ec2 revoke-security-group-ingress --group-id <sg-id> --protocol tcp --port 21 --cidr 0.0.0.0/0",
|
|
150
|
+
"Remove unrestricted access to RDP (port 3389):",
|
|
151
|
+
"aws ec2 revoke-security-group-ingress --group-id <sg-id> --protocol tcp --port 3389 --cidr 0.0.0.0/0",
|
|
152
|
+
"Remove unrestricted access to MySQL (port 3306):",
|
|
153
|
+
"aws ec2 revoke-security-group-ingress --group-id <sg-id> --protocol tcp --port 3306 --cidr 0.0.0.0/0",
|
|
154
|
+
"Remove unrestricted access to other blocked ports:",
|
|
155
|
+
"aws ec2 revoke-security-group-ingress --group-id <sg-id> --protocol tcp --port 4333 --cidr 0.0.0.0/0",
|
|
156
|
+
"For necessary access, use specific CIDR blocks:",
|
|
157
|
+
"aws ec2 authorize-security-group-ingress --group-id <sg-id> --protocol tcp --port <port> --cidr <specific-cidr>",
|
|
158
|
+
"Implement alternative secure access methods:",
|
|
159
|
+
" - Use AWS Systems Manager Session Manager for server access",
|
|
160
|
+
" - Set up VPN or Direct Connect for administrative access",
|
|
161
|
+
" - Use bastion hosts in public subnets for secure access",
|
|
162
|
+
" - Implement Application Load Balancer for web applications",
|
|
163
|
+
"Monitor security group changes with CloudTrail",
|
|
164
|
+
"Set up Config rules to detect unauthorized security group changes",
|
|
165
|
+
"Regularly audit security group rules for compliance"
|
|
166
|
+
]
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
class IncomingSSHDisabledAssessment(BaseConfigRuleAssessment):
|
|
170
|
+
"""Assessment for incoming-ssh-disabled Config rule."""
|
|
171
|
+
|
|
172
|
+
def __init__(self):
|
|
173
|
+
"""Initialize incoming SSH disabled assessment."""
|
|
174
|
+
super().__init__(
|
|
175
|
+
rule_name="incoming-ssh-disabled",
|
|
176
|
+
control_id="13.1",
|
|
177
|
+
resource_types=["AWS::EC2::SecurityGroup"]
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
def _get_resources(self, aws_factory: AWSClientFactory, resource_type: str, region: str) -> List[Dict[str, Any]]:
|
|
181
|
+
"""Get all security groups in the region."""
|
|
182
|
+
if resource_type != "AWS::EC2::SecurityGroup":
|
|
183
|
+
return []
|
|
184
|
+
|
|
185
|
+
try:
|
|
186
|
+
ec2_client = aws_factory.get_client('ec2', region)
|
|
187
|
+
|
|
188
|
+
response = aws_factory.aws_api_call_with_retry(
|
|
189
|
+
lambda: ec2_client.describe_security_groups()
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
security_groups = []
|
|
193
|
+
for sg in response.get('SecurityGroups', []):
|
|
194
|
+
security_groups.append({
|
|
195
|
+
'GroupId': sg.get('GroupId'),
|
|
196
|
+
'GroupName': sg.get('GroupName'),
|
|
197
|
+
'Description': sg.get('Description'),
|
|
198
|
+
'VpcId': sg.get('VpcId'),
|
|
199
|
+
'IpPermissions': sg.get('IpPermissions', []),
|
|
200
|
+
'IpPermissionsEgress': sg.get('IpPermissionsEgress', []),
|
|
201
|
+
'Tags': sg.get('Tags', [])
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
logger.debug(f"Found {len(security_groups)} security groups in region {region}")
|
|
205
|
+
return security_groups
|
|
206
|
+
|
|
207
|
+
except ClientError as e:
|
|
208
|
+
logger.error(f"Error retrieving security groups in region {region}: {e}")
|
|
209
|
+
raise
|
|
210
|
+
except Exception as e:
|
|
211
|
+
logger.error(f"Unexpected error retrieving security groups in region {region}: {e}")
|
|
212
|
+
raise
|
|
213
|
+
|
|
214
|
+
def _evaluate_resource_compliance(self, resource: Dict[str, Any], aws_factory: AWSClientFactory, region: str) -> ComplianceResult:
|
|
215
|
+
"""Evaluate if security group disables unrestricted SSH access."""
|
|
216
|
+
group_id = resource.get('GroupId', 'unknown')
|
|
217
|
+
group_name = resource.get('GroupName', 'unknown')
|
|
218
|
+
ip_permissions = resource.get('IpPermissions', [])
|
|
219
|
+
|
|
220
|
+
# Check for rules allowing unrestricted SSH access (port 22)
|
|
221
|
+
ssh_violations = []
|
|
222
|
+
|
|
223
|
+
for rule in ip_permissions:
|
|
224
|
+
ip_protocol = rule.get('IpProtocol', '')
|
|
225
|
+
from_port = rule.get('FromPort')
|
|
226
|
+
to_port = rule.get('ToPort')
|
|
227
|
+
ip_ranges = rule.get('IpRanges', [])
|
|
228
|
+
ipv6_ranges = rule.get('Ipv6Ranges', [])
|
|
229
|
+
|
|
230
|
+
# Check if rule allows access from anywhere (0.0.0.0/0 or ::/0)
|
|
231
|
+
allows_public_access = False
|
|
232
|
+
public_cidrs = []
|
|
233
|
+
|
|
234
|
+
for ip_range in ip_ranges:
|
|
235
|
+
if ip_range.get('CidrIp') == '0.0.0.0/0':
|
|
236
|
+
allows_public_access = True
|
|
237
|
+
public_cidrs.append('0.0.0.0/0')
|
|
238
|
+
|
|
239
|
+
for ipv6_range in ipv6_ranges:
|
|
240
|
+
if ipv6_range.get('CidrIpv6') == '::/0':
|
|
241
|
+
allows_public_access = True
|
|
242
|
+
public_cidrs.append('::/0')
|
|
243
|
+
|
|
244
|
+
# If rule allows public access, check if it includes SSH (port 22)
|
|
245
|
+
if allows_public_access:
|
|
246
|
+
if ip_protocol == '-1': # All protocols
|
|
247
|
+
ssh_violations.append({
|
|
248
|
+
'protocol': 'All',
|
|
249
|
+
'port_range': 'All',
|
|
250
|
+
'cidrs': public_cidrs
|
|
251
|
+
})
|
|
252
|
+
elif ip_protocol.lower() == 'tcp' and from_port is not None and to_port is not None:
|
|
253
|
+
# Check if SSH port (22) falls within the range
|
|
254
|
+
if from_port <= 22 <= to_port:
|
|
255
|
+
ssh_violations.append({
|
|
256
|
+
'protocol': ip_protocol,
|
|
257
|
+
'port_range': f"{from_port}-{to_port}" if from_port != to_port else str(from_port),
|
|
258
|
+
'cidrs': public_cidrs
|
|
259
|
+
})
|
|
260
|
+
|
|
261
|
+
if ssh_violations:
|
|
262
|
+
compliance_status = ComplianceStatus.NON_COMPLIANT
|
|
263
|
+
violation_details = []
|
|
264
|
+
for violation in ssh_violations:
|
|
265
|
+
violation_details.append(f"{violation['protocol']}:{violation['port_range']} from {', '.join(violation['cidrs'])}")
|
|
266
|
+
evaluation_reason = f"Security group {group_name} ({group_id}) allows unrestricted SSH access: {'; '.join(violation_details)}"
|
|
267
|
+
else:
|
|
268
|
+
compliance_status = ComplianceStatus.COMPLIANT
|
|
269
|
+
evaluation_reason = f"Security group {group_name} ({group_id}) does not allow unrestricted SSH access"
|
|
270
|
+
|
|
271
|
+
return ComplianceResult(
|
|
272
|
+
resource_id=group_id,
|
|
273
|
+
resource_type="AWS::EC2::SecurityGroup",
|
|
274
|
+
compliance_status=compliance_status,
|
|
275
|
+
evaluation_reason=evaluation_reason,
|
|
276
|
+
config_rule_name=self.rule_name,
|
|
277
|
+
region=region
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
def _get_rule_remediation_steps(self) -> List[str]:
|
|
281
|
+
"""Get specific remediation steps for disabling unrestricted SSH access."""
|
|
282
|
+
return [
|
|
283
|
+
"Remove security group rules allowing unrestricted SSH access",
|
|
284
|
+
"For each non-compliant security group:",
|
|
285
|
+
" 1. Identify rules allowing SSH access from 0.0.0.0/0 or ::/0",
|
|
286
|
+
" 2. Remove unrestricted SSH access rules",
|
|
287
|
+
" 3. Implement secure alternatives for SSH access",
|
|
288
|
+
" 4. Use specific CIDR blocks if SSH access is required",
|
|
289
|
+
"Remove unrestricted SSH access:",
|
|
290
|
+
"aws ec2 revoke-security-group-ingress --group-id <sg-id> --protocol tcp --port 22 --cidr 0.0.0.0/0",
|
|
291
|
+
"aws ec2 revoke-security-group-ingress --group-id <sg-id> --protocol tcp --port 22 --cidr ::/0",
|
|
292
|
+
"Implement secure SSH access alternatives:",
|
|
293
|
+
"1. Use AWS Systems Manager Session Manager:",
|
|
294
|
+
" - No need for SSH keys or open ports",
|
|
295
|
+
" - Centralized access logging and auditing",
|
|
296
|
+
" - aws ssm start-session --target <instance-id>",
|
|
297
|
+
"2. Set up bastion host in public subnet:",
|
|
298
|
+
" - Create dedicated bastion host with restricted access",
|
|
299
|
+
" - Allow SSH only from specific IP ranges",
|
|
300
|
+
" - aws ec2 authorize-security-group-ingress --group-id <bastion-sg-id> --protocol tcp --port 22 --cidr <office-ip>/32",
|
|
301
|
+
"3. Use VPN or AWS Direct Connect:",
|
|
302
|
+
" - Establish secure network connection",
|
|
303
|
+
" - Allow SSH only from VPN/Direct Connect IP ranges",
|
|
304
|
+
"4. If specific CIDR access is required:",
|
|
305
|
+
" aws ec2 authorize-security-group-ingress --group-id <sg-id> --protocol tcp --port 22 --cidr <specific-ip>/32",
|
|
306
|
+
"Best practices for SSH security:",
|
|
307
|
+
" - Use SSH key pairs instead of passwords",
|
|
308
|
+
" - Implement SSH key rotation policies",
|
|
309
|
+
" - Enable SSH logging and monitoring",
|
|
310
|
+
" - Use non-standard SSH ports if necessary",
|
|
311
|
+
" - Implement fail2ban or similar intrusion prevention",
|
|
312
|
+
"Monitor SSH access with CloudTrail and VPC Flow Logs",
|
|
313
|
+
"Set up CloudWatch alarms for SSH connection attempts",
|
|
314
|
+
"Regularly audit SSH access patterns and security group changes"
|
|
315
|
+
]
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
class VPCFlowLogsEnabledAssessment(BaseConfigRuleAssessment):
|
|
319
|
+
"""Assessment for VPC Flow Logs enabled for network monitoring."""
|
|
320
|
+
|
|
321
|
+
def __init__(self):
|
|
322
|
+
"""Initialize VPC Flow Logs enabled assessment."""
|
|
323
|
+
super().__init__(
|
|
324
|
+
rule_name="vpc-flow-logs-enabled",
|
|
325
|
+
control_id="13.1",
|
|
326
|
+
resource_types=["AWS::EC2::VPC"]
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
def _get_resources(self, aws_factory: AWSClientFactory, resource_type: str, region: str) -> List[Dict[str, Any]]:
|
|
330
|
+
"""Get all VPCs in the region."""
|
|
331
|
+
if resource_type != "AWS::EC2::VPC":
|
|
332
|
+
return []
|
|
333
|
+
|
|
334
|
+
try:
|
|
335
|
+
ec2_client = aws_factory.get_client('ec2', region)
|
|
336
|
+
|
|
337
|
+
# Get all VPCs
|
|
338
|
+
vpcs_response = aws_factory.aws_api_call_with_retry(
|
|
339
|
+
lambda: ec2_client.describe_vpcs()
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
# Get all flow logs
|
|
343
|
+
flow_logs_response = aws_factory.aws_api_call_with_retry(
|
|
344
|
+
lambda: ec2_client.describe_flow_logs()
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
# Create mapping of VPC ID to flow logs
|
|
348
|
+
vpc_flow_logs = {}
|
|
349
|
+
for flow_log in flow_logs_response.get('FlowLogs', []):
|
|
350
|
+
resource_id = flow_log.get('ResourceId')
|
|
351
|
+
if resource_id and resource_id.startswith('vpc-'):
|
|
352
|
+
if resource_id not in vpc_flow_logs:
|
|
353
|
+
vpc_flow_logs[resource_id] = []
|
|
354
|
+
vpc_flow_logs[resource_id].append(flow_log)
|
|
355
|
+
|
|
356
|
+
vpcs = []
|
|
357
|
+
for vpc in vpcs_response.get('Vpcs', []):
|
|
358
|
+
vpc_id = vpc.get('VpcId')
|
|
359
|
+
vpcs.append({
|
|
360
|
+
'VpcId': vpc_id,
|
|
361
|
+
'State': vpc.get('State'),
|
|
362
|
+
'CidrBlock': vpc.get('CidrBlock'),
|
|
363
|
+
'IsDefault': vpc.get('IsDefault', False),
|
|
364
|
+
'Tags': vpc.get('Tags', []),
|
|
365
|
+
'FlowLogs': vpc_flow_logs.get(vpc_id, [])
|
|
366
|
+
})
|
|
367
|
+
|
|
368
|
+
logger.debug(f"Found {len(vpcs)} VPCs in region {region}")
|
|
369
|
+
return vpcs
|
|
370
|
+
|
|
371
|
+
except ClientError as e:
|
|
372
|
+
logger.error(f"Error retrieving VPCs and flow logs in region {region}: {e}")
|
|
373
|
+
raise
|
|
374
|
+
except Exception as e:
|
|
375
|
+
logger.error(f"Unexpected error retrieving VPCs and flow logs in region {region}: {e}")
|
|
376
|
+
raise
|
|
377
|
+
|
|
378
|
+
def _evaluate_resource_compliance(self, resource: Dict[str, Any], aws_factory: AWSClientFactory, region: str) -> ComplianceResult:
|
|
379
|
+
"""Evaluate if VPC has flow logs enabled."""
|
|
380
|
+
vpc_id = resource.get('VpcId', 'unknown')
|
|
381
|
+
vpc_state = resource.get('State', 'unknown')
|
|
382
|
+
is_default = resource.get('IsDefault', False)
|
|
383
|
+
flow_logs = resource.get('FlowLogs', [])
|
|
384
|
+
|
|
385
|
+
# Skip deleted VPCs
|
|
386
|
+
if vpc_state != 'available':
|
|
387
|
+
return ComplianceResult(
|
|
388
|
+
resource_id=vpc_id,
|
|
389
|
+
resource_type="AWS::EC2::VPC",
|
|
390
|
+
compliance_status=ComplianceStatus.NOT_APPLICABLE,
|
|
391
|
+
evaluation_reason=f"VPC {vpc_id} is in state '{vpc_state}' and not available for evaluation",
|
|
392
|
+
config_rule_name=self.rule_name,
|
|
393
|
+
region=region
|
|
394
|
+
)
|
|
395
|
+
|
|
396
|
+
# Check for active flow logs
|
|
397
|
+
active_flow_logs = []
|
|
398
|
+
for flow_log in flow_logs:
|
|
399
|
+
flow_log_status = flow_log.get('FlowLogStatus', '')
|
|
400
|
+
if flow_log_status == 'ACTIVE':
|
|
401
|
+
active_flow_logs.append({
|
|
402
|
+
'id': flow_log.get('FlowLogId'),
|
|
403
|
+
'traffic_type': flow_log.get('TrafficType', 'ALL'),
|
|
404
|
+
'log_destination_type': flow_log.get('LogDestinationType', 'cloud-watch-logs'),
|
|
405
|
+
'log_destination': flow_log.get('LogDestination', '')
|
|
406
|
+
})
|
|
407
|
+
|
|
408
|
+
if active_flow_logs:
|
|
409
|
+
compliance_status = ComplianceStatus.COMPLIANT
|
|
410
|
+
flow_log_details = []
|
|
411
|
+
for fl in active_flow_logs:
|
|
412
|
+
flow_log_details.append(f"{fl['id']} ({fl['traffic_type']} traffic to {fl['log_destination_type']})")
|
|
413
|
+
|
|
414
|
+
vpc_type = "default VPC" if is_default else "VPC"
|
|
415
|
+
evaluation_reason = f"{vpc_type} {vpc_id} has {len(active_flow_logs)} active flow log(s): {', '.join(flow_log_details)}"
|
|
416
|
+
else:
|
|
417
|
+
compliance_status = ComplianceStatus.NON_COMPLIANT
|
|
418
|
+
vpc_type = "Default VPC" if is_default else "VPC"
|
|
419
|
+
if flow_logs:
|
|
420
|
+
inactive_count = len(flow_logs)
|
|
421
|
+
evaluation_reason = f"{vpc_type} {vpc_id} has {inactive_count} inactive flow log(s) but no active flow logs for network monitoring"
|
|
422
|
+
else:
|
|
423
|
+
evaluation_reason = f"{vpc_type} {vpc_id} has no flow logs enabled for network monitoring"
|
|
424
|
+
|
|
425
|
+
return ComplianceResult(
|
|
426
|
+
resource_id=vpc_id,
|
|
427
|
+
resource_type="AWS::EC2::VPC",
|
|
428
|
+
compliance_status=compliance_status,
|
|
429
|
+
evaluation_reason=evaluation_reason,
|
|
430
|
+
config_rule_name=self.rule_name,
|
|
431
|
+
region=region
|
|
432
|
+
)
|
|
433
|
+
|
|
434
|
+
def _get_rule_remediation_steps(self) -> List[str]:
|
|
435
|
+
"""Get specific remediation steps for enabling VPC Flow Logs."""
|
|
436
|
+
return [
|
|
437
|
+
"Enable VPC Flow Logs for network monitoring and security analysis",
|
|
438
|
+
"For each non-compliant VPC:",
|
|
439
|
+
" 1. Create CloudWatch Log Group for flow logs (recommended)",
|
|
440
|
+
" 2. Create IAM role for flow logs delivery",
|
|
441
|
+
" 3. Enable flow logs for the VPC",
|
|
442
|
+
" 4. Configure appropriate traffic type (ALL, ACCEPT, or REJECT)",
|
|
443
|
+
"Create CloudWatch Log Group:",
|
|
444
|
+
"aws logs create-log-group --log-group-name /aws/vpc/flowlogs",
|
|
445
|
+
"Create IAM role for VPC Flow Logs:",
|
|
446
|
+
"aws iam create-role --role-name flowlogsRole --assume-role-policy-document file://trust-policy.json",
|
|
447
|
+
"aws iam put-role-policy --role-name flowlogsRole --policy-name flowlogsDeliveryRolePolicy --policy-document file://delivery-policy.json",
|
|
448
|
+
"Enable VPC Flow Logs to CloudWatch:",
|
|
449
|
+
"aws ec2 create-flow-logs --resource-type VPC --resource-ids <vpc-id> --traffic-type ALL --log-destination-type cloud-watch-logs --log-group-name /aws/vpc/flowlogs --deliver-logs-permission-arn arn:aws:iam::<account-id>:role/flowlogsRole",
|
|
450
|
+
"Alternative: Enable VPC Flow Logs to S3:",
|
|
451
|
+
"aws ec2 create-flow-logs --resource-type VPC --resource-ids <vpc-id> --traffic-type ALL --log-destination-type s3 --log-destination arn:aws:s3:::<bucket-name>/vpc-flow-logs/",
|
|
452
|
+
"Configure flow log format (optional):",
|
|
453
|
+
"aws ec2 create-flow-logs --resource-type VPC --resource-ids <vpc-id> --traffic-type ALL --log-destination-type cloud-watch-logs --log-group-name /aws/vpc/flowlogs --deliver-logs-permission-arn <role-arn> --log-format '${srcaddr} ${dstaddr} ${srcport} ${dstport} ${protocol} ${packets} ${bytes} ${windowstart} ${windowend} ${action}'",
|
|
454
|
+
"Best practices for VPC Flow Logs:",
|
|
455
|
+
" - Enable for ALL traffic types for comprehensive monitoring",
|
|
456
|
+
" - Use CloudWatch Logs for real-time analysis and alerting",
|
|
457
|
+
" - Use S3 for long-term storage and cost optimization",
|
|
458
|
+
" - Set up CloudWatch alarms for suspicious traffic patterns",
|
|
459
|
+
" - Implement automated analysis with Lambda functions",
|
|
460
|
+
"Monitor flow logs with CloudWatch Insights:",
|
|
461
|
+
" - Analyze traffic patterns and identify anomalies",
|
|
462
|
+
" - Create dashboards for network visibility",
|
|
463
|
+
" - Set up alerts for security events",
|
|
464
|
+
"Consider enabling flow logs at subnet level for granular monitoring",
|
|
465
|
+
"Regularly review flow log data for security insights",
|
|
466
|
+
"Implement log retention policies to manage costs"
|
|
467
|
+
]
|