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.
@@ -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
+ }