awslabs.well-architected-security-mcp-server 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.
- awslabs/well_architected_security_mcp_server/__init__.py +17 -0
- awslabs/well_architected_security_mcp_server/consts.py +113 -0
- awslabs/well_architected_security_mcp_server/server.py +1174 -0
- awslabs/well_architected_security_mcp_server/util/__init__.py +42 -0
- awslabs/well_architected_security_mcp_server/util/network_security.py +1251 -0
- awslabs/well_architected_security_mcp_server/util/prompt_utils.py +173 -0
- awslabs/well_architected_security_mcp_server/util/resource_utils.py +109 -0
- awslabs/well_architected_security_mcp_server/util/security_services.py +1618 -0
- awslabs/well_architected_security_mcp_server/util/storage_security.py +1126 -0
- awslabs_well_architected_security_mcp_server-0.1.1.dist-info/METADATA +258 -0
- awslabs_well_architected_security_mcp_server-0.1.1.dist-info/RECORD +13 -0
- awslabs_well_architected_security_mcp_server-0.1.1.dist-info/WHEEL +4 -0
- awslabs_well_architected_security_mcp_server-0.1.1.dist-info/entry_points.txt +5 -0
|
@@ -0,0 +1,1251 @@
|
|
|
1
|
+
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
"""Utility functions for checking AWS network services for data-in-transit security."""
|
|
16
|
+
|
|
17
|
+
from typing import Any, Dict, List
|
|
18
|
+
|
|
19
|
+
import boto3
|
|
20
|
+
import botocore.exceptions
|
|
21
|
+
from botocore.config import Config
|
|
22
|
+
from mcp.server.fastmcp import Context
|
|
23
|
+
|
|
24
|
+
from awslabs.well_architected_security_mcp_server import __version__
|
|
25
|
+
|
|
26
|
+
# User agent configuration for AWS API calls
|
|
27
|
+
USER_AGENT_CONFIG = Config(
|
|
28
|
+
user_agent_extra=f"awslabs/mcp/well-architected-security-mcp-server/{__version__}"
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
# Acceptable TLS versions for secure data in transit
|
|
32
|
+
ACCEPTABLE_TLS_VERSIONS = ["TLSv1.2", "TLSv1.3"]
|
|
33
|
+
|
|
34
|
+
# Minimum TLS version for secure data in transit
|
|
35
|
+
MIN_TLS_VERSION = "TLSv1.2"
|
|
36
|
+
|
|
37
|
+
# Insecure protocols that should be avoided
|
|
38
|
+
INSECURE_PROTOCOLS = ["TLSv1.0", "TLSv1.1", "SSLv3", "SSLv2"]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
async def check_network_security(
|
|
42
|
+
region: str,
|
|
43
|
+
services: List[str],
|
|
44
|
+
session: boto3.Session,
|
|
45
|
+
ctx: Context,
|
|
46
|
+
include_non_compliant_only: bool = False,
|
|
47
|
+
) -> Dict[str, Any]:
|
|
48
|
+
"""Check AWS network resources for data-in-transit security best practices.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
region: AWS region to check
|
|
52
|
+
services: List of network services to check
|
|
53
|
+
session: boto3 Session for AWS API calls
|
|
54
|
+
ctx: MCP context for error reporting
|
|
55
|
+
include_non_compliant_only: Whether to include only non-compliant resources in the results
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
Dictionary with network security status
|
|
59
|
+
"""
|
|
60
|
+
results = {
|
|
61
|
+
"region": region,
|
|
62
|
+
"services_checked": services,
|
|
63
|
+
"resources_checked": 0,
|
|
64
|
+
"compliant_resources": 0,
|
|
65
|
+
"non_compliant_resources": 0,
|
|
66
|
+
"compliance_by_service": {},
|
|
67
|
+
"resource_details": [],
|
|
68
|
+
"recommendations": [],
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
# Find all network resources using Resource Explorer
|
|
72
|
+
network_resources = await find_network_resources(region, session, services, ctx)
|
|
73
|
+
|
|
74
|
+
# Check each service as requested
|
|
75
|
+
if "elb" in services:
|
|
76
|
+
# Check classic load balancers
|
|
77
|
+
elb_client = session.client("elb", region_name=region, config=USER_AGENT_CONFIG)
|
|
78
|
+
elb_results = await check_classic_load_balancers(
|
|
79
|
+
region, elb_client, ctx, network_resources
|
|
80
|
+
)
|
|
81
|
+
await _update_results(results, elb_results, "elb", include_non_compliant_only)
|
|
82
|
+
|
|
83
|
+
# Check application and network load balancers
|
|
84
|
+
elbv2_client = session.client("elbv2", region_name=region, config=USER_AGENT_CONFIG)
|
|
85
|
+
elbv2_results = await check_elbv2_load_balancers(
|
|
86
|
+
region, elbv2_client, ctx, network_resources
|
|
87
|
+
)
|
|
88
|
+
await _update_results(results, elbv2_results, "elbv2", include_non_compliant_only)
|
|
89
|
+
|
|
90
|
+
if "vpc" in services:
|
|
91
|
+
vpc_client = session.client("ec2", region_name=region, config=USER_AGENT_CONFIG)
|
|
92
|
+
vpc_results = await check_vpc_endpoints(region, vpc_client, ctx, network_resources)
|
|
93
|
+
await _update_results(results, vpc_results, "vpc", include_non_compliant_only)
|
|
94
|
+
|
|
95
|
+
# Check security groups
|
|
96
|
+
sg_results = await check_security_groups(region, vpc_client, ctx, network_resources)
|
|
97
|
+
await _update_results(results, sg_results, "security_groups", include_non_compliant_only)
|
|
98
|
+
|
|
99
|
+
if "apigateway" in services:
|
|
100
|
+
apigw_client = session.client("apigateway", region_name=region, config=USER_AGENT_CONFIG)
|
|
101
|
+
apigw_results = await check_api_gateway(region, apigw_client, ctx, network_resources)
|
|
102
|
+
await _update_results(results, apigw_results, "apigateway", include_non_compliant_only)
|
|
103
|
+
|
|
104
|
+
if "cloudfront" in services:
|
|
105
|
+
# CloudFront is a global service, but we'll check it if requested
|
|
106
|
+
if region == "us-east-1":
|
|
107
|
+
cf_client = session.client("cloudfront", region_name=region, config=USER_AGENT_CONFIG)
|
|
108
|
+
cf_results = await check_cloudfront_distributions(
|
|
109
|
+
region, cf_client, ctx, network_resources
|
|
110
|
+
)
|
|
111
|
+
await _update_results(results, cf_results, "cloudfront", include_non_compliant_only)
|
|
112
|
+
|
|
113
|
+
# Generate overall recommendations based on findings
|
|
114
|
+
results["recommendations"] = await generate_recommendations(results)
|
|
115
|
+
|
|
116
|
+
return results
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
async def _update_results(
|
|
120
|
+
main_results: Dict[str, Any],
|
|
121
|
+
service_results: Dict[str, Any],
|
|
122
|
+
service_name: str,
|
|
123
|
+
include_non_compliant_only: bool,
|
|
124
|
+
) -> None:
|
|
125
|
+
"""Update the main results dictionary with service-specific results."""
|
|
126
|
+
# Update resource counts
|
|
127
|
+
main_results["resources_checked"] += service_results.get("resources_checked", 0)
|
|
128
|
+
main_results["compliant_resources"] += service_results.get("compliant_resources", 0)
|
|
129
|
+
main_results["non_compliant_resources"] += service_results.get("non_compliant_resources", 0)
|
|
130
|
+
|
|
131
|
+
# Add service-specific compliance info
|
|
132
|
+
main_results["compliance_by_service"][service_name] = {
|
|
133
|
+
"resources_checked": service_results.get("resources_checked", 0),
|
|
134
|
+
"compliant_resources": service_results.get("compliant_resources", 0),
|
|
135
|
+
"non_compliant_resources": service_results.get("non_compliant_resources", 0),
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
# Add resource details
|
|
139
|
+
for resource in service_results.get("resource_details", []):
|
|
140
|
+
if not include_non_compliant_only or not resource.get("compliant", True):
|
|
141
|
+
main_results["resource_details"].append(resource)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
async def generate_recommendations(results: Dict[str, Any]) -> List[str]:
|
|
145
|
+
"""Generate recommendations based on the scan results."""
|
|
146
|
+
recommendations = []
|
|
147
|
+
|
|
148
|
+
# Check ELB recommendations
|
|
149
|
+
if "elb" in results.get("compliance_by_service", {}) or "elbv2" in results.get(
|
|
150
|
+
"compliance_by_service", {}
|
|
151
|
+
):
|
|
152
|
+
elb_non_compliant = (
|
|
153
|
+
results.get("compliance_by_service", {})
|
|
154
|
+
.get("elb", {})
|
|
155
|
+
.get("non_compliant_resources", 0)
|
|
156
|
+
)
|
|
157
|
+
elbv2_non_compliant = (
|
|
158
|
+
results.get("compliance_by_service", {})
|
|
159
|
+
.get("elbv2", {})
|
|
160
|
+
.get("non_compliant_resources", 0)
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
if elb_non_compliant > 0 or elbv2_non_compliant > 0:
|
|
164
|
+
recommendations.append("Configure all load balancers to use HTTPS/TLS listeners")
|
|
165
|
+
recommendations.append("Update security policies to use TLS 1.2 or later")
|
|
166
|
+
recommendations.append(
|
|
167
|
+
"Use AWS Certificate Manager (ACM) to provision and manage certificates"
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
# Check VPC endpoint recommendations
|
|
171
|
+
if (
|
|
172
|
+
"vpc" in results.get("compliance_by_service", {})
|
|
173
|
+
and results.get("compliance_by_service", {})
|
|
174
|
+
.get("vpc", {})
|
|
175
|
+
.get("non_compliant_resources", 0)
|
|
176
|
+
> 0
|
|
177
|
+
):
|
|
178
|
+
recommendations.append(
|
|
179
|
+
"Configure interface VPC endpoints to use TLS for all communications"
|
|
180
|
+
)
|
|
181
|
+
recommendations.append("Enable private DNS for interface endpoints where applicable")
|
|
182
|
+
|
|
183
|
+
# Check security group recommendations
|
|
184
|
+
if (
|
|
185
|
+
"security_groups" in results.get("compliance_by_service", {})
|
|
186
|
+
and results.get("compliance_by_service", {})
|
|
187
|
+
.get("security_groups", {})
|
|
188
|
+
.get("non_compliant_resources", 0)
|
|
189
|
+
> 0
|
|
190
|
+
):
|
|
191
|
+
recommendations.append("Restrict inbound traffic to necessary ports only")
|
|
192
|
+
recommendations.append("Avoid allowing unrestricted access (0.0.0.0/0) to sensitive ports")
|
|
193
|
+
recommendations.append("Use security groups to enforce encryption in transit")
|
|
194
|
+
|
|
195
|
+
# Check API Gateway recommendations
|
|
196
|
+
if (
|
|
197
|
+
"apigateway" in results.get("compliance_by_service", {})
|
|
198
|
+
and results.get("compliance_by_service", {})
|
|
199
|
+
.get("apigateway", {})
|
|
200
|
+
.get("non_compliant_resources", 0)
|
|
201
|
+
> 0
|
|
202
|
+
):
|
|
203
|
+
recommendations.append("Configure API Gateway to enforce HTTPS endpoints only")
|
|
204
|
+
recommendations.append("Set a minimum TLS version of 1.2 for all API Gateway APIs")
|
|
205
|
+
|
|
206
|
+
# Check CloudFront recommendations
|
|
207
|
+
if (
|
|
208
|
+
"cloudfront" in results.get("compliance_by_service", {})
|
|
209
|
+
and results.get("compliance_by_service", {})
|
|
210
|
+
.get("cloudfront", {})
|
|
211
|
+
.get("non_compliant_resources", 0)
|
|
212
|
+
> 0
|
|
213
|
+
):
|
|
214
|
+
recommendations.append("Configure CloudFront distributions to redirect HTTP to HTTPS")
|
|
215
|
+
recommendations.append("Use TLS 1.2 or later for viewer and origin connections")
|
|
216
|
+
recommendations.append(
|
|
217
|
+
"Use Origin Access Identity (OAI) or Origin Access Control (OAC) for S3 origins"
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
# General recommendations
|
|
221
|
+
recommendations.append("Implement a centralized certificate management process")
|
|
222
|
+
recommendations.append("Regularly rotate and audit TLS certificates")
|
|
223
|
+
recommendations.append("Monitor for expiring certificates and insecure protocol usage")
|
|
224
|
+
|
|
225
|
+
return recommendations
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
async def find_network_resources(
|
|
229
|
+
region: str, session: boto3.Session, services: List[str], ctx: Context
|
|
230
|
+
) -> Dict[str, Any]:
|
|
231
|
+
"""Find network resources using Resource Explorer."""
|
|
232
|
+
try:
|
|
233
|
+
print(
|
|
234
|
+
f"[DEBUG:NetworkSecurity] Finding network resources in {region} using Resource Explorer"
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
# Initialize resource explorer client
|
|
238
|
+
resource_explorer = session.client(
|
|
239
|
+
"resource-explorer-2", region_name=region, config=USER_AGENT_CONFIG
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
# Try to get the default view for Resource Explorer
|
|
243
|
+
print("[DEBUG:NetworkSecurity] Listing Resource Explorer views...")
|
|
244
|
+
views = resource_explorer.list_views()
|
|
245
|
+
print(f"[DEBUG:NetworkSecurity] Found {len(views.get('Views', []))} views")
|
|
246
|
+
|
|
247
|
+
default_view = None
|
|
248
|
+
# Find the default view
|
|
249
|
+
for view in views.get("Views", []):
|
|
250
|
+
print(f"[DEBUG:NetworkSecurity] View: {view.get('ViewArn')}")
|
|
251
|
+
if view.get("Filters", {}).get("FilterString", "") == "":
|
|
252
|
+
default_view = view.get("ViewArn")
|
|
253
|
+
print(f"[DEBUG:NetworkSecurity] Found default view: {default_view}")
|
|
254
|
+
break
|
|
255
|
+
|
|
256
|
+
if not default_view:
|
|
257
|
+
print("[DEBUG:NetworkSecurity] No default view found. Cannot use Resource Explorer.")
|
|
258
|
+
await ctx.warning(
|
|
259
|
+
"No default Resource Explorer view found. Will fall back to direct service API calls."
|
|
260
|
+
)
|
|
261
|
+
return {"error": "No default Resource Explorer view found"}
|
|
262
|
+
|
|
263
|
+
# Build filter strings for each service
|
|
264
|
+
service_filters = []
|
|
265
|
+
|
|
266
|
+
if "elb" in services:
|
|
267
|
+
service_filters.append("service:elasticloadbalancing")
|
|
268
|
+
if "vpc" in services:
|
|
269
|
+
service_filters.append("service:ec2 resourcetype:ec2:vpc")
|
|
270
|
+
service_filters.append("service:ec2 resourcetype:ec2:vpc-endpoint")
|
|
271
|
+
service_filters.append("service:ec2 resourcetype:ec2:security-group")
|
|
272
|
+
if "apigateway" in services:
|
|
273
|
+
service_filters.append("service:apigateway")
|
|
274
|
+
if "cloudfront" in services and region == "us-east-1":
|
|
275
|
+
service_filters.append("service:cloudfront")
|
|
276
|
+
|
|
277
|
+
# Combine with OR
|
|
278
|
+
filter_string = " OR ".join(service_filters)
|
|
279
|
+
print(f"[DEBUG:NetworkSecurity] Using filter string: {filter_string}")
|
|
280
|
+
|
|
281
|
+
# Get resources
|
|
282
|
+
resources = []
|
|
283
|
+
paginator = resource_explorer.get_paginator("list_resources")
|
|
284
|
+
page_iterator = paginator.paginate(
|
|
285
|
+
Filters={"FilterString": filter_string}, MaxResults=100, ViewArn=default_view
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
for page in page_iterator:
|
|
289
|
+
resources.extend(page.get("Resources", []))
|
|
290
|
+
|
|
291
|
+
print(f"[DEBUG:NetworkSecurity] Found {len(resources)} total network resources")
|
|
292
|
+
|
|
293
|
+
# Organize by service
|
|
294
|
+
resources_by_service = {}
|
|
295
|
+
|
|
296
|
+
for resource in resources:
|
|
297
|
+
arn = resource.get("Arn", "")
|
|
298
|
+
if ":" in arn:
|
|
299
|
+
service = arn.split(":")[2]
|
|
300
|
+
|
|
301
|
+
# Map elasticloadbalancing to 'elb'
|
|
302
|
+
if service == "elasticloadbalancing":
|
|
303
|
+
service = "elb"
|
|
304
|
+
|
|
305
|
+
# Map ec2 VPC endpoints to 'vpc_endpoints'
|
|
306
|
+
if service == "ec2" and "vpc-endpoint" in arn:
|
|
307
|
+
service = "vpc_endpoints"
|
|
308
|
+
|
|
309
|
+
# Map ec2 security groups to 'security_groups'
|
|
310
|
+
if service == "ec2" and "security-group" in arn:
|
|
311
|
+
service = "security_groups"
|
|
312
|
+
|
|
313
|
+
# Map ec2 VPCs to 'vpc'
|
|
314
|
+
if service == "ec2" and "vpc/" in arn and "vpc-endpoint" not in arn:
|
|
315
|
+
service = "vpc"
|
|
316
|
+
|
|
317
|
+
if service not in resources_by_service:
|
|
318
|
+
resources_by_service[service] = []
|
|
319
|
+
|
|
320
|
+
resources_by_service[service].append(resource)
|
|
321
|
+
|
|
322
|
+
# Print summary
|
|
323
|
+
for service, svc_resources in resources_by_service.items():
|
|
324
|
+
print(f"[DEBUG:NetworkSecurity] {service}: {len(svc_resources)} resources")
|
|
325
|
+
|
|
326
|
+
return {
|
|
327
|
+
"total_resources": len(resources),
|
|
328
|
+
"resources_by_service": resources_by_service,
|
|
329
|
+
"resources": resources,
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
except botocore.exceptions.BotoCoreError as e:
|
|
333
|
+
print(f"[DEBUG:NetworkSecurity] Error finding network resources: {e}")
|
|
334
|
+
await ctx.error(f"Error finding network resources: {e}")
|
|
335
|
+
return {"error": str(e), "resources_by_service": {}}
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
async def check_classic_load_balancers(
|
|
339
|
+
region: str, elb_client: Any, ctx: Context, network_resources: Dict[str, Any]
|
|
340
|
+
) -> Dict[str, Any]:
|
|
341
|
+
"""Check Classic Load Balancers for data-in-transit security best practices."""
|
|
342
|
+
print(f"[DEBUG:NetworkSecurity] Checking Classic Load Balancers in {region}")
|
|
343
|
+
|
|
344
|
+
results = {
|
|
345
|
+
"service": "elb",
|
|
346
|
+
"resources_checked": 0,
|
|
347
|
+
"compliant_resources": 0,
|
|
348
|
+
"non_compliant_resources": 0,
|
|
349
|
+
"resource_details": [],
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
try:
|
|
353
|
+
# Get load balancer list - either from Resource Explorer or directly
|
|
354
|
+
load_balancers = []
|
|
355
|
+
|
|
356
|
+
if "error" not in network_resources and "elb" in network_resources.get(
|
|
357
|
+
"resources_by_service", {}
|
|
358
|
+
):
|
|
359
|
+
# Use Resource Explorer results
|
|
360
|
+
elb_resources = network_resources["resources_by_service"]["elb"]
|
|
361
|
+
for resource in elb_resources:
|
|
362
|
+
arn = resource.get("Arn", "")
|
|
363
|
+
# Filter for classic load balancers
|
|
364
|
+
if ":loadbalancer/app/" not in arn and ":loadbalancer/net/" not in arn:
|
|
365
|
+
lb_name = arn.split("/")[-1]
|
|
366
|
+
load_balancers.append(lb_name)
|
|
367
|
+
else:
|
|
368
|
+
# Fall back to direct API call
|
|
369
|
+
response = elb_client.describe_load_balancers()
|
|
370
|
+
for lb in response["LoadBalancerDescriptions"]:
|
|
371
|
+
load_balancers.append(lb["LoadBalancerName"])
|
|
372
|
+
|
|
373
|
+
print(
|
|
374
|
+
f"[DEBUG:NetworkSecurity] Found {len(load_balancers)} Classic Load Balancers in region {region}"
|
|
375
|
+
)
|
|
376
|
+
results["resources_checked"] = len(load_balancers)
|
|
377
|
+
|
|
378
|
+
# Check each load balancer
|
|
379
|
+
for lb_name in load_balancers:
|
|
380
|
+
lb_result = {
|
|
381
|
+
"name": lb_name,
|
|
382
|
+
"arn": f"arn:aws:elasticloadbalancing:{region}:loadbalancer/{lb_name}",
|
|
383
|
+
"type": "classic_load_balancer",
|
|
384
|
+
"compliant": True,
|
|
385
|
+
"issues": [],
|
|
386
|
+
"checks": {},
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
# Get load balancer details
|
|
390
|
+
lb_details = elb_client.describe_load_balancers(LoadBalancerNames=[lb_name])[
|
|
391
|
+
"LoadBalancerDescriptions"
|
|
392
|
+
][0]
|
|
393
|
+
|
|
394
|
+
# Check for HTTPS listeners
|
|
395
|
+
listeners = lb_details.get("ListenerDescriptions", [])
|
|
396
|
+
https_listeners = [
|
|
397
|
+
lst for lst in listeners if lst["Listener"].get("Protocol") in ["HTTPS", "SSL"]
|
|
398
|
+
]
|
|
399
|
+
http_listeners = [
|
|
400
|
+
lst for lst in listeners if lst["Listener"].get("Protocol") in ["HTTP", "TCP"]
|
|
401
|
+
]
|
|
402
|
+
|
|
403
|
+
lb_result["checks"]["https_listeners"] = {
|
|
404
|
+
"count": len(https_listeners),
|
|
405
|
+
"total_listeners": len(listeners),
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
# Check if all listeners are secure
|
|
409
|
+
all_secure = len(http_listeners) == 0 and len(https_listeners) > 0
|
|
410
|
+
lb_result["checks"]["all_listeners_secure"] = all_secure
|
|
411
|
+
|
|
412
|
+
if not all_secure:
|
|
413
|
+
lb_result["compliant"] = False
|
|
414
|
+
lb_result["issues"].append(f"Found {len(http_listeners)} non-encrypted listeners")
|
|
415
|
+
|
|
416
|
+
# Check SSL policies for each HTTPS listener
|
|
417
|
+
for https_listener in https_listeners:
|
|
418
|
+
ssl_policy = None
|
|
419
|
+
try:
|
|
420
|
+
policy_response = elb_client.describe_load_balancer_policies(
|
|
421
|
+
LoadBalancerName=lb_name,
|
|
422
|
+
PolicyNames=[https_listener["PolicyNames"][0]]
|
|
423
|
+
if https_listener.get("PolicyNames")
|
|
424
|
+
else [],
|
|
425
|
+
)
|
|
426
|
+
|
|
427
|
+
if policy_response.get("PolicyDescriptions"):
|
|
428
|
+
ssl_policy = policy_response["PolicyDescriptions"][0]
|
|
429
|
+
|
|
430
|
+
# Check for secure protocols
|
|
431
|
+
protocol_policies = [
|
|
432
|
+
attr
|
|
433
|
+
for attr in ssl_policy.get("PolicyAttributeDescriptions", [])
|
|
434
|
+
if attr["AttributeName"].startswith("Protocol-")
|
|
435
|
+
and attr["AttributeValue"] == "true"
|
|
436
|
+
]
|
|
437
|
+
|
|
438
|
+
insecure_protocols = [
|
|
439
|
+
p["AttributeName"].replace("Protocol-", "")
|
|
440
|
+
for p in protocol_policies
|
|
441
|
+
if p["AttributeName"].replace("Protocol-", "") in INSECURE_PROTOCOLS
|
|
442
|
+
]
|
|
443
|
+
|
|
444
|
+
if insecure_protocols:
|
|
445
|
+
lb_result["compliant"] = False
|
|
446
|
+
lb_result["issues"].append(
|
|
447
|
+
f"Using insecure protocols: {', '.join(insecure_protocols)}"
|
|
448
|
+
)
|
|
449
|
+
|
|
450
|
+
if "ssl_policies" not in lb_result["checks"]:
|
|
451
|
+
lb_result["checks"]["ssl_policies"] = []
|
|
452
|
+
|
|
453
|
+
lb_result["checks"]["ssl_policies"].append(
|
|
454
|
+
{
|
|
455
|
+
"name": ssl_policy.get("PolicyName"),
|
|
456
|
+
"insecure_protocols": insecure_protocols,
|
|
457
|
+
}
|
|
458
|
+
)
|
|
459
|
+
except Exception as e:
|
|
460
|
+
print(f"[DEBUG:NetworkSecurity] Error checking SSL policy for {lb_name}: {e}")
|
|
461
|
+
lb_result["issues"].append("Error checking SSL policy")
|
|
462
|
+
|
|
463
|
+
# Generate remediation steps
|
|
464
|
+
lb_result["remediation"] = []
|
|
465
|
+
|
|
466
|
+
if not all_secure:
|
|
467
|
+
lb_result["remediation"].append("Replace HTTP listeners with HTTPS listeners")
|
|
468
|
+
|
|
469
|
+
if lb_result["checks"].get("ssl_policies") and any(
|
|
470
|
+
p.get("insecure_protocols") for p in lb_result["checks"].get("ssl_policies", [])
|
|
471
|
+
):
|
|
472
|
+
lb_result["remediation"].append("Update SSL policy to use only TLSv1.2 or later")
|
|
473
|
+
|
|
474
|
+
# Update counts
|
|
475
|
+
if lb_result["compliant"]:
|
|
476
|
+
results["compliant_resources"] += 1
|
|
477
|
+
else:
|
|
478
|
+
results["non_compliant_resources"] += 1
|
|
479
|
+
|
|
480
|
+
results["resource_details"].append(lb_result)
|
|
481
|
+
|
|
482
|
+
return results
|
|
483
|
+
|
|
484
|
+
except botocore.exceptions.BotoCoreError as e:
|
|
485
|
+
print(f"[DEBUG:NetworkSecurity] Error checking Classic Load Balancers: {e}")
|
|
486
|
+
await ctx.error(f"Error checking Classic Load Balancers: {e}")
|
|
487
|
+
return {
|
|
488
|
+
"service": "elb",
|
|
489
|
+
"error": str(e),
|
|
490
|
+
"resources_checked": 0,
|
|
491
|
+
"compliant_resources": 0,
|
|
492
|
+
"non_compliant_resources": 0,
|
|
493
|
+
"resource_details": [],
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
|
|
497
|
+
async def check_elbv2_load_balancers(
|
|
498
|
+
region: str, elbv2_client: Any, ctx: Context, network_resources: Dict[str, Any]
|
|
499
|
+
) -> Dict[str, Any]:
|
|
500
|
+
"""Check Application and Network Load Balancers for data-in-transit security best practices."""
|
|
501
|
+
print(f"[DEBUG:NetworkSecurity] Checking ALB/NLB Load Balancers in {region}")
|
|
502
|
+
|
|
503
|
+
results = {
|
|
504
|
+
"service": "elbv2",
|
|
505
|
+
"resources_checked": 0,
|
|
506
|
+
"compliant_resources": 0,
|
|
507
|
+
"non_compliant_resources": 0,
|
|
508
|
+
"resource_details": [],
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
try:
|
|
512
|
+
# Get load balancer list - either from Resource Explorer or directly
|
|
513
|
+
load_balancers = []
|
|
514
|
+
|
|
515
|
+
if "error" not in network_resources and "elb" in network_resources.get(
|
|
516
|
+
"resources_by_service", {}
|
|
517
|
+
):
|
|
518
|
+
# Use Resource Explorer results
|
|
519
|
+
elb_resources = network_resources["resources_by_service"]["elb"]
|
|
520
|
+
for resource in elb_resources:
|
|
521
|
+
arn = resource.get("Arn", "")
|
|
522
|
+
# Filter for ALB/NLB load balancers
|
|
523
|
+
if ":loadbalancer/app/" in arn or ":loadbalancer/net/" in arn:
|
|
524
|
+
load_balancers.append(arn)
|
|
525
|
+
else:
|
|
526
|
+
# Fall back to direct API call
|
|
527
|
+
response = elbv2_client.describe_load_balancers()
|
|
528
|
+
for lb in response["LoadBalancers"]:
|
|
529
|
+
load_balancers.append(lb["LoadBalancerArn"])
|
|
530
|
+
|
|
531
|
+
print(
|
|
532
|
+
f"[DEBUG:NetworkSecurity] Found {len(load_balancers)} ALB/NLB Load Balancers in region {region}"
|
|
533
|
+
)
|
|
534
|
+
results["resources_checked"] = len(load_balancers)
|
|
535
|
+
|
|
536
|
+
# Check each load balancer
|
|
537
|
+
for lb_arn in load_balancers:
|
|
538
|
+
lb_name = (
|
|
539
|
+
lb_arn.split("/")[-2]
|
|
540
|
+
if "/app/" in lb_arn or "/net/" in lb_arn
|
|
541
|
+
else lb_arn.split("/")[-1]
|
|
542
|
+
)
|
|
543
|
+
lb_type = (
|
|
544
|
+
"application"
|
|
545
|
+
if "/app/" in lb_arn
|
|
546
|
+
else "network"
|
|
547
|
+
if "/net/" in lb_arn
|
|
548
|
+
else "unknown"
|
|
549
|
+
)
|
|
550
|
+
|
|
551
|
+
lb_result = {
|
|
552
|
+
"name": lb_name,
|
|
553
|
+
"arn": lb_arn,
|
|
554
|
+
"type": f"{lb_type}_load_balancer",
|
|
555
|
+
"compliant": True,
|
|
556
|
+
"issues": [],
|
|
557
|
+
"checks": {},
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
# Get listeners
|
|
561
|
+
try:
|
|
562
|
+
listeners_response = elbv2_client.describe_listeners(LoadBalancerArn=lb_arn)
|
|
563
|
+
listeners = listeners_response.get("Listeners", [])
|
|
564
|
+
|
|
565
|
+
# For ALBs, check for HTTPS listeners
|
|
566
|
+
if lb_type == "application":
|
|
567
|
+
https_listeners = [lst for lst in listeners if lst.get("Protocol") == "HTTPS"]
|
|
568
|
+
http_listeners = [lst for lst in listeners if lst.get("Protocol") == "HTTP"]
|
|
569
|
+
|
|
570
|
+
lb_result["checks"]["https_listeners"] = {
|
|
571
|
+
"count": len(https_listeners),
|
|
572
|
+
"total_listeners": len(listeners),
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
# Check if all listeners are secure
|
|
576
|
+
all_secure = len(http_listeners) == 0 and len(https_listeners) > 0
|
|
577
|
+
lb_result["checks"]["all_listeners_secure"] = all_secure
|
|
578
|
+
|
|
579
|
+
if not all_secure:
|
|
580
|
+
lb_result["compliant"] = False
|
|
581
|
+
lb_result["issues"].append(
|
|
582
|
+
f"Found {len(http_listeners)} non-encrypted HTTP listeners"
|
|
583
|
+
)
|
|
584
|
+
|
|
585
|
+
# Check SSL policies for each HTTPS listener
|
|
586
|
+
for https_listener in https_listeners:
|
|
587
|
+
ssl_policy = https_listener.get("SslPolicy")
|
|
588
|
+
|
|
589
|
+
# Check if the SSL policy is secure
|
|
590
|
+
if (
|
|
591
|
+
ssl_policy
|
|
592
|
+
and not ssl_policy.startswith("ELBSecurityPolicy-TLS-1-2")
|
|
593
|
+
and not ssl_policy.startswith("ELBSecurityPolicy-FS-1-2")
|
|
594
|
+
):
|
|
595
|
+
lb_result["compliant"] = False
|
|
596
|
+
lb_result["issues"].append(
|
|
597
|
+
f"Using potentially insecure SSL policy: {ssl_policy}"
|
|
598
|
+
)
|
|
599
|
+
|
|
600
|
+
if "ssl_policies" not in lb_result["checks"]:
|
|
601
|
+
lb_result["checks"]["ssl_policies"] = []
|
|
602
|
+
|
|
603
|
+
lb_result["checks"]["ssl_policies"].append(
|
|
604
|
+
{
|
|
605
|
+
"name": ssl_policy,
|
|
606
|
+
"listener_arn": https_listener.get("ListenerArn"),
|
|
607
|
+
}
|
|
608
|
+
)
|
|
609
|
+
|
|
610
|
+
# For NLBs, check for TLS listeners
|
|
611
|
+
elif lb_type == "network":
|
|
612
|
+
tls_listeners = [lst for lst in listeners if lst.get("Protocol") == "TLS"]
|
|
613
|
+
# tcp_listeners = [lst for lst in listeners if lst.get('Protocol') == 'TCP']
|
|
614
|
+
|
|
615
|
+
lb_result["checks"]["tls_listeners"] = {
|
|
616
|
+
"count": len(tls_listeners),
|
|
617
|
+
"total_listeners": len(listeners),
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
# For NLBs, we don't require all listeners to be TLS
|
|
621
|
+
# but we check the SSL policies of TLS listeners
|
|
622
|
+
for tls_listener in tls_listeners:
|
|
623
|
+
ssl_policy = tls_listener.get("SslPolicy")
|
|
624
|
+
|
|
625
|
+
# Check if the SSL policy is secure
|
|
626
|
+
if (
|
|
627
|
+
ssl_policy
|
|
628
|
+
and not ssl_policy.startswith("ELBSecurityPolicy-TLS-1-2")
|
|
629
|
+
and not ssl_policy.startswith("ELBSecurityPolicy-FS-1-2")
|
|
630
|
+
):
|
|
631
|
+
lb_result["compliant"] = False
|
|
632
|
+
lb_result["issues"].append(
|
|
633
|
+
f"Using potentially insecure SSL policy: {ssl_policy}"
|
|
634
|
+
)
|
|
635
|
+
|
|
636
|
+
if "ssl_policies" not in lb_result["checks"]:
|
|
637
|
+
lb_result["checks"]["ssl_policies"] = []
|
|
638
|
+
|
|
639
|
+
lb_result["checks"]["ssl_policies"].append(
|
|
640
|
+
{
|
|
641
|
+
"name": ssl_policy,
|
|
642
|
+
"listener_arn": tls_listener.get("ListenerArn"),
|
|
643
|
+
}
|
|
644
|
+
)
|
|
645
|
+
|
|
646
|
+
except Exception as e:
|
|
647
|
+
print(f"[DEBUG:NetworkSecurity] Error checking listeners for {lb_arn}: {e}")
|
|
648
|
+
lb_result["issues"].append("Error checking listeners")
|
|
649
|
+
lb_result["compliant"] = False
|
|
650
|
+
|
|
651
|
+
# Generate remediation steps
|
|
652
|
+
lb_result["remediation"] = []
|
|
653
|
+
|
|
654
|
+
if lb_type == "application" and not lb_result["checks"].get(
|
|
655
|
+
"all_listeners_secure", True
|
|
656
|
+
):
|
|
657
|
+
lb_result["remediation"].append("Replace HTTP listeners with HTTPS listeners")
|
|
658
|
+
lb_result["remediation"].append("Configure HTTP to HTTPS redirection")
|
|
659
|
+
|
|
660
|
+
if lb_result["checks"].get("ssl_policies"):
|
|
661
|
+
lb_result["remediation"].append(
|
|
662
|
+
"Update SSL policy to ELBSecurityPolicy-TLS-1-2-2017-01 or newer"
|
|
663
|
+
)
|
|
664
|
+
|
|
665
|
+
# Update counts
|
|
666
|
+
if lb_result["compliant"]:
|
|
667
|
+
results["compliant_resources"] += 1
|
|
668
|
+
else:
|
|
669
|
+
results["non_compliant_resources"] += 1
|
|
670
|
+
|
|
671
|
+
results["resource_details"].append(lb_result)
|
|
672
|
+
|
|
673
|
+
return results
|
|
674
|
+
|
|
675
|
+
except botocore.exceptions.BotoCoreError as e:
|
|
676
|
+
print(f"[DEBUG:NetworkSecurity] Error checking ALB/NLB Load Balancers: {e}")
|
|
677
|
+
await ctx.error(f"Error checking ALB/NLB Load Balancers: {e}")
|
|
678
|
+
return {
|
|
679
|
+
"service": "elbv2",
|
|
680
|
+
"error": str(e),
|
|
681
|
+
"resources_checked": 0,
|
|
682
|
+
"compliant_resources": 0,
|
|
683
|
+
"non_compliant_resources": 0,
|
|
684
|
+
"resource_details": [],
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
|
|
688
|
+
async def check_vpc_endpoints(
|
|
689
|
+
region: str, ec2_client: Any, ctx: Context, network_resources: Dict[str, Any]
|
|
690
|
+
) -> Dict[str, Any]:
|
|
691
|
+
"""Check VPC endpoints for data-in-transit security best practices."""
|
|
692
|
+
print(f"[DEBUG:NetworkSecurity] Checking VPC endpoints in {region}")
|
|
693
|
+
|
|
694
|
+
results = {
|
|
695
|
+
"service": "vpc",
|
|
696
|
+
"resources_checked": 0,
|
|
697
|
+
"compliant_resources": 0,
|
|
698
|
+
"non_compliant_resources": 0,
|
|
699
|
+
"resource_details": [],
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
try:
|
|
703
|
+
# Get VPC endpoint list - either from Resource Explorer or directly
|
|
704
|
+
vpc_endpoints = []
|
|
705
|
+
|
|
706
|
+
if "error" not in network_resources and "vpc_endpoints" in network_resources.get(
|
|
707
|
+
"resources_by_service", {}
|
|
708
|
+
):
|
|
709
|
+
# Use Resource Explorer results
|
|
710
|
+
endpoint_resources = network_resources["resources_by_service"]["vpc_endpoints"]
|
|
711
|
+
for resource in endpoint_resources:
|
|
712
|
+
vpc_endpoints.append(resource.get("Arn", ""))
|
|
713
|
+
else:
|
|
714
|
+
# Fall back to direct API call
|
|
715
|
+
response = ec2_client.describe_vpc_endpoints()
|
|
716
|
+
vpc_endpoints = [
|
|
717
|
+
endpoint["VpcEndpointId"] for endpoint in response.get("VpcEndpoints", [])
|
|
718
|
+
]
|
|
719
|
+
|
|
720
|
+
print(
|
|
721
|
+
f"[DEBUG:NetworkSecurity] Found {len(vpc_endpoints)} VPC endpoints in region {region}"
|
|
722
|
+
)
|
|
723
|
+
results["resources_checked"] = len(vpc_endpoints)
|
|
724
|
+
|
|
725
|
+
# Check each VPC endpoint
|
|
726
|
+
for endpoint_id in vpc_endpoints:
|
|
727
|
+
# Extract endpoint ID from ARN if necessary
|
|
728
|
+
if endpoint_id.startswith("arn:"):
|
|
729
|
+
endpoint_id = endpoint_id.split("/")[-1]
|
|
730
|
+
|
|
731
|
+
# Get endpoint details
|
|
732
|
+
endpoint_response = ec2_client.describe_vpc_endpoints(VpcEndpointIds=[endpoint_id])
|
|
733
|
+
|
|
734
|
+
if not endpoint_response.get("VpcEndpoints"):
|
|
735
|
+
continue
|
|
736
|
+
|
|
737
|
+
endpoint = endpoint_response["VpcEndpoints"][0]
|
|
738
|
+
|
|
739
|
+
endpoint_result = {
|
|
740
|
+
"id": endpoint_id,
|
|
741
|
+
"arn": f"arn:aws:ec2:{region}:{endpoint.get('OwnerId', '')}:vpc-endpoint/{endpoint_id}",
|
|
742
|
+
"type": "vpc_endpoint",
|
|
743
|
+
"service": endpoint.get("ServiceName", "").split(".")[-1],
|
|
744
|
+
"endpoint_type": endpoint.get("VpcEndpointType", ""),
|
|
745
|
+
"compliant": True,
|
|
746
|
+
"issues": [],
|
|
747
|
+
"checks": {},
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
# Check endpoint type
|
|
751
|
+
endpoint_type = endpoint.get("VpcEndpointType", "")
|
|
752
|
+
endpoint_result["checks"]["endpoint_type"] = endpoint_type
|
|
753
|
+
|
|
754
|
+
# For interface endpoints, check if private DNS is enabled
|
|
755
|
+
if endpoint_type == "Interface":
|
|
756
|
+
private_dns_enabled = endpoint.get("PrivateDnsEnabled", False)
|
|
757
|
+
endpoint_result["checks"]["private_dns_enabled"] = private_dns_enabled
|
|
758
|
+
|
|
759
|
+
# For interface endpoints, private DNS should be enabled for secure access
|
|
760
|
+
if not private_dns_enabled:
|
|
761
|
+
endpoint_result["compliant"] = False
|
|
762
|
+
endpoint_result["issues"].append(
|
|
763
|
+
"Private DNS not enabled for interface endpoint"
|
|
764
|
+
)
|
|
765
|
+
|
|
766
|
+
# Check security groups for interface endpoints
|
|
767
|
+
if endpoint_type == "Interface" and "Groups" in endpoint:
|
|
768
|
+
security_groups = endpoint.get("Groups", [])
|
|
769
|
+
endpoint_result["checks"]["security_groups"] = [
|
|
770
|
+
sg["GroupId"] for sg in security_groups
|
|
771
|
+
]
|
|
772
|
+
|
|
773
|
+
# We don't fail compliance here, but we'll check the security groups separately
|
|
774
|
+
|
|
775
|
+
# Generate remediation steps
|
|
776
|
+
endpoint_result["remediation"] = []
|
|
777
|
+
|
|
778
|
+
if endpoint_type == "Interface" and not endpoint.get("PrivateDnsEnabled", False):
|
|
779
|
+
endpoint_result["remediation"].append("Enable private DNS for interface endpoint")
|
|
780
|
+
|
|
781
|
+
# Update counts
|
|
782
|
+
if endpoint_result["compliant"]:
|
|
783
|
+
results["compliant_resources"] += 1
|
|
784
|
+
else:
|
|
785
|
+
results["non_compliant_resources"] += 1
|
|
786
|
+
|
|
787
|
+
results["resource_details"].append(endpoint_result)
|
|
788
|
+
|
|
789
|
+
return results
|
|
790
|
+
|
|
791
|
+
except botocore.exceptions.BotoCoreError as e:
|
|
792
|
+
print(f"[DEBUG:NetworkSecurity] Error checking VPC endpoints: {e}")
|
|
793
|
+
await ctx.error(f"Error checking VPC endpoints: {e}")
|
|
794
|
+
return {
|
|
795
|
+
"service": "vpc",
|
|
796
|
+
"error": str(e),
|
|
797
|
+
"resources_checked": 0,
|
|
798
|
+
"compliant_resources": 0,
|
|
799
|
+
"non_compliant_resources": 0,
|
|
800
|
+
"resource_details": [],
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
|
|
804
|
+
async def check_security_groups(
|
|
805
|
+
region: str, ec2_client: Any, ctx: Context, network_resources: Dict[str, Any]
|
|
806
|
+
) -> Dict[str, Any]:
|
|
807
|
+
"""Check security groups for data-in-transit security best practices."""
|
|
808
|
+
print(f"[DEBUG:NetworkSecurity] Checking security groups in {region}")
|
|
809
|
+
|
|
810
|
+
results = {
|
|
811
|
+
"service": "security_groups",
|
|
812
|
+
"resources_checked": 0,
|
|
813
|
+
"compliant_resources": 0,
|
|
814
|
+
"non_compliant_resources": 0,
|
|
815
|
+
"resource_details": [],
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
try:
|
|
819
|
+
# Get security group list - either from Resource Explorer or directly
|
|
820
|
+
security_groups = []
|
|
821
|
+
|
|
822
|
+
if "error" not in network_resources and "security_groups" in network_resources.get(
|
|
823
|
+
"resources_by_service", {}
|
|
824
|
+
):
|
|
825
|
+
# Use Resource Explorer results
|
|
826
|
+
sg_resources = network_resources["resources_by_service"]["security_groups"]
|
|
827
|
+
for resource in sg_resources:
|
|
828
|
+
sg_id = resource.get("Arn", "").split("/")[-1]
|
|
829
|
+
security_groups.append(sg_id)
|
|
830
|
+
else:
|
|
831
|
+
# Fall back to direct API call
|
|
832
|
+
response = ec2_client.describe_security_groups()
|
|
833
|
+
security_groups = [sg["GroupId"] for sg in response.get("SecurityGroups", [])]
|
|
834
|
+
|
|
835
|
+
print(
|
|
836
|
+
f"[DEBUG:NetworkSecurity] Found {len(security_groups)} security groups in region {region}"
|
|
837
|
+
)
|
|
838
|
+
results["resources_checked"] = len(security_groups)
|
|
839
|
+
|
|
840
|
+
# Define sensitive ports for data in transit
|
|
841
|
+
sensitive_ports = {
|
|
842
|
+
80: "HTTP",
|
|
843
|
+
23: "Telnet",
|
|
844
|
+
21: "FTP",
|
|
845
|
+
20: "FTP-Data",
|
|
846
|
+
25: "SMTP",
|
|
847
|
+
110: "POP3",
|
|
848
|
+
143: "IMAP",
|
|
849
|
+
69: "TFTP",
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
# Check each security group
|
|
853
|
+
for sg_id in security_groups:
|
|
854
|
+
# Get security group details
|
|
855
|
+
sg_response = ec2_client.describe_security_groups(GroupIds=[sg_id])
|
|
856
|
+
|
|
857
|
+
if not sg_response.get("SecurityGroups"):
|
|
858
|
+
continue
|
|
859
|
+
|
|
860
|
+
sg = sg_response["SecurityGroups"][0]
|
|
861
|
+
|
|
862
|
+
sg_result = {
|
|
863
|
+
"id": sg_id,
|
|
864
|
+
"name": sg.get("GroupName", ""),
|
|
865
|
+
"arn": f"arn:aws:ec2:{region}:{sg.get('OwnerId', '')}:security-group/{sg_id}",
|
|
866
|
+
"type": "security_group",
|
|
867
|
+
"vpc_id": sg.get("VpcId", ""),
|
|
868
|
+
"compliant": True,
|
|
869
|
+
"issues": [],
|
|
870
|
+
"checks": {},
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
# Check for open sensitive ports
|
|
874
|
+
open_sensitive_ports = []
|
|
875
|
+
|
|
876
|
+
for rule in sg.get("IpPermissions", []):
|
|
877
|
+
from_port = rule.get("FromPort")
|
|
878
|
+
to_port = rule.get("ToPort")
|
|
879
|
+
|
|
880
|
+
# Skip if ports are not defined
|
|
881
|
+
if from_port is None or to_port is None:
|
|
882
|
+
continue
|
|
883
|
+
|
|
884
|
+
# Check for sensitive ports
|
|
885
|
+
for port in range(from_port, to_port + 1):
|
|
886
|
+
if port in sensitive_ports:
|
|
887
|
+
# Check if open to the world (0.0.0.0/0)
|
|
888
|
+
for ip_range in rule.get("IpRanges", []):
|
|
889
|
+
if ip_range.get("CidrIp") == "0.0.0.0/0":
|
|
890
|
+
open_sensitive_ports.append(
|
|
891
|
+
{
|
|
892
|
+
"port": port,
|
|
893
|
+
"service": sensitive_ports[port],
|
|
894
|
+
"cidr": "0.0.0.0/0",
|
|
895
|
+
}
|
|
896
|
+
)
|
|
897
|
+
break
|
|
898
|
+
|
|
899
|
+
sg_result["checks"]["open_sensitive_ports"] = open_sensitive_ports
|
|
900
|
+
|
|
901
|
+
if open_sensitive_ports:
|
|
902
|
+
sg_result["compliant"] = False
|
|
903
|
+
for port_info in open_sensitive_ports:
|
|
904
|
+
sg_result["issues"].append(
|
|
905
|
+
f"Port {port_info['port']} ({port_info['service']}) open to the world"
|
|
906
|
+
)
|
|
907
|
+
|
|
908
|
+
# Generate remediation steps
|
|
909
|
+
sg_result["remediation"] = []
|
|
910
|
+
|
|
911
|
+
if open_sensitive_ports:
|
|
912
|
+
sg_result["remediation"].append(
|
|
913
|
+
"Restrict access to sensitive ports to specific IP ranges"
|
|
914
|
+
)
|
|
915
|
+
sg_result["remediation"].append(
|
|
916
|
+
"Replace insecure protocols with secure alternatives (e.g., HTTPS instead of HTTP)"
|
|
917
|
+
)
|
|
918
|
+
sg_result["remediation"].append(
|
|
919
|
+
"Use security group source references instead of CIDR blocks where possible"
|
|
920
|
+
)
|
|
921
|
+
|
|
922
|
+
# Update counts
|
|
923
|
+
if sg_result["compliant"]:
|
|
924
|
+
results["compliant_resources"] += 1
|
|
925
|
+
else:
|
|
926
|
+
results["non_compliant_resources"] += 1
|
|
927
|
+
|
|
928
|
+
results["resource_details"].append(sg_result)
|
|
929
|
+
|
|
930
|
+
return results
|
|
931
|
+
|
|
932
|
+
except botocore.exceptions.BotoCoreError as e:
|
|
933
|
+
print(f"[DEBUG:NetworkSecurity] Error checking security groups: {e}")
|
|
934
|
+
await ctx.error(f"Error checking security groups: {e}")
|
|
935
|
+
return {
|
|
936
|
+
"service": "security_groups",
|
|
937
|
+
"error": str(e),
|
|
938
|
+
"resources_checked": 0,
|
|
939
|
+
"compliant_resources": 0,
|
|
940
|
+
"non_compliant_resources": 0,
|
|
941
|
+
"resource_details": [],
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
|
|
945
|
+
async def check_api_gateway(
|
|
946
|
+
region: str, apigw_client: Any, ctx: Context, network_resources: Dict[str, Any]
|
|
947
|
+
) -> Dict[str, Any]:
|
|
948
|
+
"""Check API Gateway for data-in-transit security best practices."""
|
|
949
|
+
print(f"[DEBUG:NetworkSecurity] Checking API Gateway in {region}")
|
|
950
|
+
|
|
951
|
+
results = {
|
|
952
|
+
"service": "apigateway",
|
|
953
|
+
"resources_checked": 0,
|
|
954
|
+
"compliant_resources": 0,
|
|
955
|
+
"non_compliant_resources": 0,
|
|
956
|
+
"resource_details": [],
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
try:
|
|
960
|
+
# Get API list - either from Resource Explorer or directly
|
|
961
|
+
apis = []
|
|
962
|
+
|
|
963
|
+
if "error" not in network_resources and "apigateway" in network_resources.get(
|
|
964
|
+
"resources_by_service", {}
|
|
965
|
+
):
|
|
966
|
+
# Use Resource Explorer results
|
|
967
|
+
api_resources = network_resources["resources_by_service"]["apigateway"]
|
|
968
|
+
for resource in api_resources:
|
|
969
|
+
api_id = resource.get("Arn", "").split("/")[-1]
|
|
970
|
+
apis.append(api_id)
|
|
971
|
+
else:
|
|
972
|
+
# Fall back to direct API call
|
|
973
|
+
response = apigw_client.get_rest_apis()
|
|
974
|
+
apis = [api["id"] for api in response.get("items", [])]
|
|
975
|
+
|
|
976
|
+
print(f"[DEBUG:NetworkSecurity] Found {len(apis)} APIs in region {region}")
|
|
977
|
+
results["resources_checked"] = len(apis)
|
|
978
|
+
|
|
979
|
+
# Check each API
|
|
980
|
+
for api_id in apis:
|
|
981
|
+
# Get API details
|
|
982
|
+
api_response = apigw_client.get_rest_api(restApiId=api_id)
|
|
983
|
+
|
|
984
|
+
api_result = {
|
|
985
|
+
"id": api_id,
|
|
986
|
+
"name": api_response.get("name", ""),
|
|
987
|
+
"arn": f"arn:aws:apigateway:{region}::/restapis/{api_id}",
|
|
988
|
+
"type": "api_gateway",
|
|
989
|
+
"compliant": True,
|
|
990
|
+
"issues": [],
|
|
991
|
+
"checks": {},
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
# Check for HTTPS enforcement
|
|
995
|
+
try:
|
|
996
|
+
stages_response = apigw_client.get_stages(restApiId=api_id)
|
|
997
|
+
stages = stages_response.get("item", [])
|
|
998
|
+
|
|
999
|
+
for stage in stages:
|
|
1000
|
+
stage_name = stage.get("stageName", "")
|
|
1001
|
+
|
|
1002
|
+
# Check if HTTPS is enforced
|
|
1003
|
+
method_settings = stage.get("methodSettings", {})
|
|
1004
|
+
|
|
1005
|
+
# Default to not enforced
|
|
1006
|
+
https_enforced = False
|
|
1007
|
+
|
|
1008
|
+
# Check if there's a '*/*' setting that enforces HTTPS
|
|
1009
|
+
if "*/*" in method_settings:
|
|
1010
|
+
https_enforced = method_settings["*/*"].get("requireHttps", False)
|
|
1011
|
+
|
|
1012
|
+
if not https_enforced:
|
|
1013
|
+
api_result["compliant"] = False
|
|
1014
|
+
api_result["issues"].append(f"HTTPS not enforced for stage: {stage_name}")
|
|
1015
|
+
|
|
1016
|
+
if "stages" not in api_result["checks"]:
|
|
1017
|
+
api_result["checks"]["stages"] = []
|
|
1018
|
+
|
|
1019
|
+
api_result["checks"]["stages"].append(
|
|
1020
|
+
{"name": stage_name, "https_enforced": https_enforced}
|
|
1021
|
+
)
|
|
1022
|
+
except Exception as e:
|
|
1023
|
+
print(f"[DEBUG:NetworkSecurity] Error checking stages for API {api_id}: {e}")
|
|
1024
|
+
api_result["issues"].append("Error checking API stages")
|
|
1025
|
+
|
|
1026
|
+
# Check for custom domain names with secure TLS
|
|
1027
|
+
try:
|
|
1028
|
+
domains_response = apigw_client.get_domain_names()
|
|
1029
|
+
domains = domains_response.get("items", [])
|
|
1030
|
+
|
|
1031
|
+
for domain in domains:
|
|
1032
|
+
domain_name = domain.get("domainName", "")
|
|
1033
|
+
|
|
1034
|
+
# Check if this domain is mapped to our API
|
|
1035
|
+
mappings_response = apigw_client.get_base_path_mappings(domainName=domain_name)
|
|
1036
|
+
mappings = mappings_response.get("items", [])
|
|
1037
|
+
|
|
1038
|
+
for mapping in mappings:
|
|
1039
|
+
if mapping.get("restApiId") == api_id:
|
|
1040
|
+
# Check TLS version
|
|
1041
|
+
security_policy = domain.get("securityPolicy", "")
|
|
1042
|
+
|
|
1043
|
+
if security_policy != "TLS_1_2":
|
|
1044
|
+
api_result["compliant"] = False
|
|
1045
|
+
api_result["issues"].append(
|
|
1046
|
+
f"Domain {domain_name} using insecure TLS policy: {security_policy}"
|
|
1047
|
+
)
|
|
1048
|
+
|
|
1049
|
+
if "domains" not in api_result["checks"]:
|
|
1050
|
+
api_result["checks"]["domains"] = []
|
|
1051
|
+
|
|
1052
|
+
api_result["checks"]["domains"].append(
|
|
1053
|
+
{"name": domain_name, "security_policy": security_policy}
|
|
1054
|
+
)
|
|
1055
|
+
except Exception as e:
|
|
1056
|
+
print(f"[DEBUG:NetworkSecurity] Error checking domains for API {api_id}: {e}")
|
|
1057
|
+
# Don't fail compliance just because we couldn't check domains
|
|
1058
|
+
|
|
1059
|
+
# Generate remediation steps
|
|
1060
|
+
api_result["remediation"] = []
|
|
1061
|
+
|
|
1062
|
+
if "stages" in api_result["checks"] and any(
|
|
1063
|
+
not stage.get("https_enforced", False)
|
|
1064
|
+
for stage in api_result["checks"].get("stages", [])
|
|
1065
|
+
):
|
|
1066
|
+
api_result["remediation"].append("Enable 'Require HTTPS' in method settings")
|
|
1067
|
+
|
|
1068
|
+
if "domains" in api_result["checks"] and any(
|
|
1069
|
+
domain.get("security_policy") != "TLS_1_2"
|
|
1070
|
+
for domain in api_result["checks"].get("domains", [])
|
|
1071
|
+
):
|
|
1072
|
+
api_result["remediation"].append("Update custom domain security policy to TLS_1_2")
|
|
1073
|
+
|
|
1074
|
+
# Update counts
|
|
1075
|
+
if api_result["compliant"]:
|
|
1076
|
+
results["compliant_resources"] += 1
|
|
1077
|
+
else:
|
|
1078
|
+
results["non_compliant_resources"] += 1
|
|
1079
|
+
|
|
1080
|
+
results["resource_details"].append(api_result)
|
|
1081
|
+
|
|
1082
|
+
return results
|
|
1083
|
+
|
|
1084
|
+
except botocore.exceptions.BotoCoreError as e:
|
|
1085
|
+
print(f"[DEBUG:NetworkSecurity] Error checking API Gateway: {e}")
|
|
1086
|
+
await ctx.error(f"Error checking API Gateway: {e}")
|
|
1087
|
+
return {
|
|
1088
|
+
"service": "apigateway",
|
|
1089
|
+
"error": str(e),
|
|
1090
|
+
"resources_checked": 0,
|
|
1091
|
+
"compliant_resources": 0,
|
|
1092
|
+
"non_compliant_resources": 0,
|
|
1093
|
+
"resource_details": [],
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
|
|
1097
|
+
async def check_cloudfront_distributions(
|
|
1098
|
+
region: str, cf_client: Any, ctx: Context, network_resources: Dict[str, Any]
|
|
1099
|
+
) -> Dict[str, Any]:
|
|
1100
|
+
"""Check CloudFront distributions for data-in-transit security best practices."""
|
|
1101
|
+
print("[DEBUG:NetworkSecurity] Checking CloudFront distributions")
|
|
1102
|
+
|
|
1103
|
+
results = {
|
|
1104
|
+
"service": "cloudfront",
|
|
1105
|
+
"resources_checked": 0,
|
|
1106
|
+
"compliant_resources": 0,
|
|
1107
|
+
"non_compliant_resources": 0,
|
|
1108
|
+
"resource_details": [],
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
try:
|
|
1112
|
+
# Get distribution list - either from Resource Explorer or directly
|
|
1113
|
+
distributions = []
|
|
1114
|
+
|
|
1115
|
+
if "error" not in network_resources and "cloudfront" in network_resources.get(
|
|
1116
|
+
"resources_by_service", {}
|
|
1117
|
+
):
|
|
1118
|
+
# Use Resource Explorer results
|
|
1119
|
+
cf_resources = network_resources["resources_by_service"]["cloudfront"]
|
|
1120
|
+
for resource in cf_resources:
|
|
1121
|
+
dist_id = resource.get("Arn", "").split("/")[-1]
|
|
1122
|
+
distributions.append(dist_id)
|
|
1123
|
+
else:
|
|
1124
|
+
# Fall back to direct API call
|
|
1125
|
+
response = cf_client.list_distributions()
|
|
1126
|
+
if "DistributionList" in response and "Items" in response["DistributionList"]:
|
|
1127
|
+
distributions = [dist["Id"] for dist in response["DistributionList"]["Items"]]
|
|
1128
|
+
|
|
1129
|
+
print(f"[DEBUG:NetworkSecurity] Found {len(distributions)} CloudFront distributions")
|
|
1130
|
+
results["resources_checked"] = len(distributions)
|
|
1131
|
+
|
|
1132
|
+
# Check each distribution
|
|
1133
|
+
for dist_id in distributions:
|
|
1134
|
+
# Get distribution details
|
|
1135
|
+
dist_response = cf_client.get_distribution(Id=dist_id)
|
|
1136
|
+
|
|
1137
|
+
if "Distribution" not in dist_response:
|
|
1138
|
+
continue
|
|
1139
|
+
|
|
1140
|
+
dist = dist_response["Distribution"]
|
|
1141
|
+
config = dist.get("DistributionConfig", {})
|
|
1142
|
+
|
|
1143
|
+
dist_result = {
|
|
1144
|
+
"id": dist_id,
|
|
1145
|
+
"arn": f"arn:aws:cloudfront::{dist.get('Id', '')}:distribution/{dist_id}",
|
|
1146
|
+
"domain_name": dist.get("DomainName", ""),
|
|
1147
|
+
"type": "cloudfront_distribution",
|
|
1148
|
+
"compliant": True,
|
|
1149
|
+
"issues": [],
|
|
1150
|
+
"checks": {},
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
# Check if HTTPS is required
|
|
1154
|
+
viewer_protocol_policy = None
|
|
1155
|
+
default_cache_behavior = config.get("DefaultCacheBehavior", {})
|
|
1156
|
+
if default_cache_behavior:
|
|
1157
|
+
viewer_protocol_policy = default_cache_behavior.get("ViewerProtocolPolicy")
|
|
1158
|
+
|
|
1159
|
+
dist_result["checks"]["viewer_protocol_policy"] = viewer_protocol_policy
|
|
1160
|
+
|
|
1161
|
+
if (
|
|
1162
|
+
viewer_protocol_policy != "redirect-to-https"
|
|
1163
|
+
and viewer_protocol_policy != "https-only"
|
|
1164
|
+
):
|
|
1165
|
+
dist_result["compliant"] = False
|
|
1166
|
+
dist_result["issues"].append(
|
|
1167
|
+
f"Viewer protocol policy not enforcing HTTPS: {viewer_protocol_policy}"
|
|
1168
|
+
)
|
|
1169
|
+
|
|
1170
|
+
# Check TLS version
|
|
1171
|
+
ssl_protocol_version = config.get("ViewerCertificate", {}).get(
|
|
1172
|
+
"MinimumProtocolVersion"
|
|
1173
|
+
)
|
|
1174
|
+
dist_result["checks"]["minimum_tls_version"] = ssl_protocol_version
|
|
1175
|
+
|
|
1176
|
+
if ssl_protocol_version and ssl_protocol_version not in [
|
|
1177
|
+
"TLSv1.2_2018",
|
|
1178
|
+
"TLSv1.2_2019",
|
|
1179
|
+
"TLSv1.2_2021",
|
|
1180
|
+
]:
|
|
1181
|
+
dist_result["compliant"] = False
|
|
1182
|
+
dist_result["issues"].append(f"Using outdated TLS version: {ssl_protocol_version}")
|
|
1183
|
+
|
|
1184
|
+
# Check origin protocol policy for S3 origins
|
|
1185
|
+
origins = config.get("Origins", {}).get("Items", [])
|
|
1186
|
+
s3_origins_without_oai = []
|
|
1187
|
+
|
|
1188
|
+
for origin in origins:
|
|
1189
|
+
if "s3" in origin.get("DomainName", "").lower():
|
|
1190
|
+
# Check if using OAI or OAC
|
|
1191
|
+
has_oai = (
|
|
1192
|
+
"S3OriginConfig" in origin
|
|
1193
|
+
and "OriginAccessIdentity" in origin["S3OriginConfig"]
|
|
1194
|
+
and origin["S3OriginConfig"]["OriginAccessIdentity"]
|
|
1195
|
+
)
|
|
1196
|
+
has_oac = "OriginAccessControlId" in origin
|
|
1197
|
+
|
|
1198
|
+
if not has_oai and not has_oac:
|
|
1199
|
+
s3_origins_without_oai.append(origin.get("Id", "unknown"))
|
|
1200
|
+
|
|
1201
|
+
if s3_origins_without_oai:
|
|
1202
|
+
dist_result["compliant"] = False
|
|
1203
|
+
dist_result["issues"].append(
|
|
1204
|
+
f"S3 origins without OAI/OAC: {', '.join(s3_origins_without_oai)}"
|
|
1205
|
+
)
|
|
1206
|
+
dist_result["checks"]["s3_origins_without_oai"] = s3_origins_without_oai
|
|
1207
|
+
|
|
1208
|
+
# Generate remediation steps
|
|
1209
|
+
dist_result["remediation"] = []
|
|
1210
|
+
|
|
1211
|
+
if (
|
|
1212
|
+
viewer_protocol_policy != "redirect-to-https"
|
|
1213
|
+
and viewer_protocol_policy != "https-only"
|
|
1214
|
+
):
|
|
1215
|
+
dist_result["remediation"].append(
|
|
1216
|
+
"Set ViewerProtocolPolicy to 'redirect-to-https' or 'https-only'"
|
|
1217
|
+
)
|
|
1218
|
+
|
|
1219
|
+
if ssl_protocol_version and ssl_protocol_version not in [
|
|
1220
|
+
"TLSv1.2_2018",
|
|
1221
|
+
"TLSv1.2_2019",
|
|
1222
|
+
"TLSv1.2_2021",
|
|
1223
|
+
]:
|
|
1224
|
+
dist_result["remediation"].append("Update MinimumProtocolVersion to TLSv1.2_2021")
|
|
1225
|
+
|
|
1226
|
+
if s3_origins_without_oai:
|
|
1227
|
+
dist_result["remediation"].append(
|
|
1228
|
+
"Configure Origin Access Identity (OAI) or Origin Access Control (OAC) for S3 origins"
|
|
1229
|
+
)
|
|
1230
|
+
|
|
1231
|
+
# Update counts
|
|
1232
|
+
if dist_result["compliant"]:
|
|
1233
|
+
results["compliant_resources"] += 1
|
|
1234
|
+
else:
|
|
1235
|
+
results["non_compliant_resources"] += 1
|
|
1236
|
+
|
|
1237
|
+
results["resource_details"].append(dist_result)
|
|
1238
|
+
|
|
1239
|
+
return results
|
|
1240
|
+
|
|
1241
|
+
except botocore.exceptions.BotoCoreError as e:
|
|
1242
|
+
print(f"[DEBUG:NetworkSecurity] Error checking CloudFront distributions: {e}")
|
|
1243
|
+
await ctx.error(f"Error checking CloudFront distributions: {e}")
|
|
1244
|
+
return {
|
|
1245
|
+
"service": "cloudfront",
|
|
1246
|
+
"error": str(e),
|
|
1247
|
+
"resources_checked": 0,
|
|
1248
|
+
"compliant_resources": 0,
|
|
1249
|
+
"non_compliant_resources": 0,
|
|
1250
|
+
"resource_details": [],
|
|
1251
|
+
}
|