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,1618 @@
|
|
|
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 security services and retrieving findings."""
|
|
16
|
+
|
|
17
|
+
import datetime
|
|
18
|
+
import json
|
|
19
|
+
from typing import Any, Dict, List, Optional
|
|
20
|
+
|
|
21
|
+
import boto3
|
|
22
|
+
from botocore.config import Config
|
|
23
|
+
from mcp.server.fastmcp import Context
|
|
24
|
+
|
|
25
|
+
from awslabs.well_architected_security_mcp_server import __version__
|
|
26
|
+
|
|
27
|
+
# User agent configuration for AWS API calls
|
|
28
|
+
USER_AGENT_CONFIG = Config(
|
|
29
|
+
user_agent_extra=f"awslabs/mcp/well-architected-security-mcp-server/{__version__}"
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
async def get_analyzer_findings_count(
|
|
34
|
+
analyzer_arn: str, analyzer_client: Any, ctx: Context
|
|
35
|
+
) -> str:
|
|
36
|
+
"""Get the number of findings for an IAM Access Analyzer.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
analyzer_arn: ARN of the IAM Access Analyzer
|
|
40
|
+
analyzer_client: boto3 client for Access Analyzer
|
|
41
|
+
ctx: MCP context for error reporting
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
Count of findings as string, or "Unknown" if there was an error
|
|
45
|
+
"""
|
|
46
|
+
try:
|
|
47
|
+
response = analyzer_client.list_findings(analyzerArn=analyzer_arn)
|
|
48
|
+
return str(len(response.get("findings", [])))
|
|
49
|
+
except Exception as e:
|
|
50
|
+
await ctx.warning(f"Error getting findings count: {e}")
|
|
51
|
+
return "Unknown"
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
async def check_access_analyzer(region: str, session: boto3.Session, ctx: Context) -> Dict:
|
|
55
|
+
"""Check if IAM Access Analyzer is enabled in the specified region.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
region: AWS region to check
|
|
59
|
+
session: boto3 Session for AWS API calls
|
|
60
|
+
ctx: MCP context for error reporting
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
Dictionary with status information about IAM Access Analyzer
|
|
64
|
+
"""
|
|
65
|
+
try:
|
|
66
|
+
analyzer_client = session.client(
|
|
67
|
+
"accessanalyzer", region_name=region, config=USER_AGENT_CONFIG
|
|
68
|
+
)
|
|
69
|
+
response = analyzer_client.list_analyzers()
|
|
70
|
+
|
|
71
|
+
# Extract analyzers - verify the field exists to prevent KeyError
|
|
72
|
+
flag = True
|
|
73
|
+
if "analyzers" not in response:
|
|
74
|
+
flag = False
|
|
75
|
+
elif len(response["analyzers"]) == 0:
|
|
76
|
+
flag = False
|
|
77
|
+
|
|
78
|
+
if not flag:
|
|
79
|
+
return {
|
|
80
|
+
"enabled": False,
|
|
81
|
+
"analyzers": [],
|
|
82
|
+
"debug_info": {"raw_response": response},
|
|
83
|
+
"setup_instructions": """
|
|
84
|
+
# IAM Access Analyzer Setup Instructions
|
|
85
|
+
|
|
86
|
+
IAM Access Analyzer is not enabled in this region. To enable it:
|
|
87
|
+
|
|
88
|
+
1. Open the IAM console: https://console.aws.amazon.com/iam/
|
|
89
|
+
2. Choose Access analyzer
|
|
90
|
+
3. Choose Create analyzer
|
|
91
|
+
4. Enter a name for the analyzer
|
|
92
|
+
5. Choose the type of analyzer (account or organization)
|
|
93
|
+
6. Choose Create analyzer
|
|
94
|
+
|
|
95
|
+
This is strongly recommended before proceeding with the security review.
|
|
96
|
+
|
|
97
|
+
Learn more: https://docs.aws.amazon.com/IAM/latest/UserGuide/access-analyzer-getting-started.html
|
|
98
|
+
""",
|
|
99
|
+
"message": "IAM Access Analyzer is not enabled in this region.",
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
analyzers = response.get("analyzers", [])
|
|
103
|
+
|
|
104
|
+
# Check if any of the analyzers are active
|
|
105
|
+
active_analyzers = [a for a in analyzers if a.get("status") == "ACTIVE"]
|
|
106
|
+
|
|
107
|
+
# Access Analyzer is enabled if there's at least one analyzer, even if not all are ACTIVE
|
|
108
|
+
analyzer_details = []
|
|
109
|
+
for analyzer in analyzers:
|
|
110
|
+
analyzer_arn = analyzer.get("arn")
|
|
111
|
+
if analyzer_arn:
|
|
112
|
+
try:
|
|
113
|
+
findings_count = await get_analyzer_findings_count(
|
|
114
|
+
analyzer_arn, analyzer_client, ctx
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
except Exception:
|
|
118
|
+
findings_count = "Error"
|
|
119
|
+
else:
|
|
120
|
+
findings_count = "Unknown (No ARN)"
|
|
121
|
+
|
|
122
|
+
analyzer_details.append(
|
|
123
|
+
{
|
|
124
|
+
"name": analyzer.get("name"),
|
|
125
|
+
"type": analyzer.get("type"),
|
|
126
|
+
"status": analyzer.get("status"),
|
|
127
|
+
"created_at": str(analyzer.get("createdAt")),
|
|
128
|
+
"findings_count": findings_count,
|
|
129
|
+
}
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
# Consider IAM Access Analyzer enabled if there's at least one analyzer, even if not all are ACTIVE
|
|
133
|
+
return {
|
|
134
|
+
"enabled": True,
|
|
135
|
+
"analyzers": analyzer_details,
|
|
136
|
+
"message": f"IAM Access Analyzer is enabled with {len(analyzers)} analyzer(s) ({len(active_analyzers)} active).",
|
|
137
|
+
}
|
|
138
|
+
except Exception as e:
|
|
139
|
+
await ctx.error(f"Error checking IAM Access Analyzer status: {e}")
|
|
140
|
+
return {
|
|
141
|
+
"enabled": False,
|
|
142
|
+
"error": str(e),
|
|
143
|
+
"message": "Error checking IAM Access Analyzer status.",
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
async def check_security_hub(region: str, session: boto3.Session, ctx: Context) -> Dict:
|
|
148
|
+
"""Check if AWS Security Hub is enabled in the specified region.
|
|
149
|
+
|
|
150
|
+
Args:
|
|
151
|
+
region: AWS region to check
|
|
152
|
+
session: boto3 Session for AWS API calls
|
|
153
|
+
ctx: MCP context for error reporting
|
|
154
|
+
|
|
155
|
+
Returns:
|
|
156
|
+
Dictionary with status information about AWS Security Hub
|
|
157
|
+
"""
|
|
158
|
+
try:
|
|
159
|
+
# Create Security Hub client
|
|
160
|
+
securityhub_client = session.client(
|
|
161
|
+
"securityhub", region_name=region, config=USER_AGENT_CONFIG
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
try:
|
|
165
|
+
# Check if Security Hub is enabled
|
|
166
|
+
hub_response = securityhub_client.describe_hub()
|
|
167
|
+
|
|
168
|
+
# Security Hub is enabled, get enabled standards
|
|
169
|
+
try:
|
|
170
|
+
standards_response = securityhub_client.get_enabled_standards()
|
|
171
|
+
standards = standards_response.get("StandardsSubscriptions", [])
|
|
172
|
+
|
|
173
|
+
# Safely process standards with better error handling
|
|
174
|
+
processed_standards = []
|
|
175
|
+
for standard in standards:
|
|
176
|
+
try:
|
|
177
|
+
standard_name = standard.get("StandardsArn", "").split("/")[-1]
|
|
178
|
+
standard_status = standard.get("StandardsStatus", "UNKNOWN")
|
|
179
|
+
|
|
180
|
+
# Handle the nested structure carefully
|
|
181
|
+
enabled_at = ""
|
|
182
|
+
if "StandardsSubscriptionArn" in standard:
|
|
183
|
+
# Sometimes EnabledAt is in the root or might not exist
|
|
184
|
+
enabled_at = str(standard.get("EnabledAt", ""))
|
|
185
|
+
|
|
186
|
+
processed_standards.append(
|
|
187
|
+
{
|
|
188
|
+
"name": standard_name,
|
|
189
|
+
"status": standard_status,
|
|
190
|
+
"enabled_at": enabled_at,
|
|
191
|
+
}
|
|
192
|
+
)
|
|
193
|
+
except Exception:
|
|
194
|
+
pass
|
|
195
|
+
|
|
196
|
+
return {
|
|
197
|
+
"enabled": True,
|
|
198
|
+
"standards": processed_standards,
|
|
199
|
+
"message": f"Security Hub is enabled with {len(standards)} standards.",
|
|
200
|
+
"debug_info": {
|
|
201
|
+
"hub_arn": hub_response.get("HubArn", "Unknown"),
|
|
202
|
+
"standards_count": len(standards),
|
|
203
|
+
},
|
|
204
|
+
}
|
|
205
|
+
except Exception as std_ex:
|
|
206
|
+
# Security Hub is enabled but we couldn't get standards
|
|
207
|
+
return {
|
|
208
|
+
"enabled": True,
|
|
209
|
+
"standards": [],
|
|
210
|
+
"message": "Security Hub is enabled but there was an error retrieving standards.",
|
|
211
|
+
"debug_info": {
|
|
212
|
+
"hub_arn": hub_response.get("HubArn", "Unknown"),
|
|
213
|
+
"error_getting_standards": str(std_ex),
|
|
214
|
+
},
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
except securityhub_client.exceptions.InvalidAccessException:
|
|
218
|
+
# Security Hub is not enabled
|
|
219
|
+
return {
|
|
220
|
+
"enabled": False,
|
|
221
|
+
"standards": [],
|
|
222
|
+
"setup_instructions": """
|
|
223
|
+
# AWS Security Hub Setup Instructions
|
|
224
|
+
AWS Security Hub is not enabled in this region. To enable it:
|
|
225
|
+
https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-get-started.html
|
|
226
|
+
""",
|
|
227
|
+
"message": "AWS Security Hub is not enabled in this region.",
|
|
228
|
+
}
|
|
229
|
+
except securityhub_client.exceptions.ResourceNotFoundException:
|
|
230
|
+
# Hub not found - not enabled
|
|
231
|
+
return {
|
|
232
|
+
"enabled": False,
|
|
233
|
+
"standards": [],
|
|
234
|
+
"setup_instructions": """
|
|
235
|
+
# AWS Security Hub Setup Instructions
|
|
236
|
+
|
|
237
|
+
AWS Security Hub is not enabled in this region. To enable it:
|
|
238
|
+
|
|
239
|
+
1. Open the Security Hub console: https://console.aws.amazon.com/securityhub/
|
|
240
|
+
2. Choose Go to Security Hub
|
|
241
|
+
3. Configure your security standards
|
|
242
|
+
4. Choose Enable Security Hub
|
|
243
|
+
|
|
244
|
+
This is strongly recommended for maintaining security best practices.
|
|
245
|
+
|
|
246
|
+
Learn more: https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-get-started.html
|
|
247
|
+
""",
|
|
248
|
+
"message": "AWS Security Hub is not enabled in this region.",
|
|
249
|
+
}
|
|
250
|
+
except Exception as e:
|
|
251
|
+
return {
|
|
252
|
+
"enabled": False,
|
|
253
|
+
"error": str(e),
|
|
254
|
+
"message": "Error checking Security Hub status.",
|
|
255
|
+
"debug_info": {"exception": str(e), "exception_type": type(e).__name__},
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
async def check_guard_duty(region: str, session: boto3.Session, ctx: Context) -> Dict:
|
|
260
|
+
"""Check if Amazon GuardDuty is enabled in the specified region.
|
|
261
|
+
|
|
262
|
+
Args:
|
|
263
|
+
region: AWS region to check
|
|
264
|
+
session: boto3 Session for AWS API calls
|
|
265
|
+
ctx: MCP context for error reporting
|
|
266
|
+
|
|
267
|
+
Returns:
|
|
268
|
+
Dictionary with status information about Amazon GuardDuty
|
|
269
|
+
"""
|
|
270
|
+
try:
|
|
271
|
+
# Create GuardDuty client
|
|
272
|
+
guardduty_client = session.client(
|
|
273
|
+
"guardduty", region_name=region, config=USER_AGENT_CONFIG
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
# List detectors
|
|
277
|
+
detector_response = guardduty_client.list_detectors()
|
|
278
|
+
detector_ids = detector_response.get("DetectorIds", [])
|
|
279
|
+
|
|
280
|
+
if not detector_ids:
|
|
281
|
+
# GuardDuty is not enabled
|
|
282
|
+
return {
|
|
283
|
+
"enabled": False,
|
|
284
|
+
"detector_details": {},
|
|
285
|
+
"setup_instructions": """
|
|
286
|
+
# Amazon GuardDuty Setup Instructions
|
|
287
|
+
|
|
288
|
+
Amazon GuardDuty is not enabled in this region. To enable it:
|
|
289
|
+
|
|
290
|
+
1. Open the GuardDuty console: https://console.aws.amazon.com/guardduty/
|
|
291
|
+
2. Choose Get Started
|
|
292
|
+
3. Choose Enable GuardDuty
|
|
293
|
+
|
|
294
|
+
This is strongly recommended for detecting threats to your AWS environment.
|
|
295
|
+
|
|
296
|
+
Learn more: https://docs.aws.amazon.com/guardduty/latest/ug/guardduty_settingup.html
|
|
297
|
+
""",
|
|
298
|
+
"message": "Amazon GuardDuty is not enabled in this region.",
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
# GuardDuty is enabled, get detector details
|
|
302
|
+
detector_id = detector_ids[0] # Use the first detector
|
|
303
|
+
detector_details = guardduty_client.get_detector(DetectorId=detector_id)
|
|
304
|
+
|
|
305
|
+
return {
|
|
306
|
+
"enabled": True,
|
|
307
|
+
"detector_details": {
|
|
308
|
+
"id": detector_id,
|
|
309
|
+
"status": "ENABLED",
|
|
310
|
+
"finding_publishing_frequency": detector_details.get("FindingPublishingFrequency"),
|
|
311
|
+
"data_sources": detector_details.get("DataSources"),
|
|
312
|
+
"features": detector_details.get("Features", []),
|
|
313
|
+
},
|
|
314
|
+
"message": "Amazon GuardDuty is enabled and active.",
|
|
315
|
+
}
|
|
316
|
+
except Exception as e:
|
|
317
|
+
await ctx.error(f"Error checking GuardDuty status: {e}")
|
|
318
|
+
return {"enabled": False, "error": str(e), "message": "Error checking GuardDuty status."}
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
async def check_inspector(region: str, session: boto3.Session, ctx: Context) -> Dict:
|
|
322
|
+
"""Check if Amazon Inspector is enabled in the specified region.
|
|
323
|
+
|
|
324
|
+
Args:
|
|
325
|
+
region: AWS region to check
|
|
326
|
+
session: boto3 Session for AWS API calls
|
|
327
|
+
ctx: MCP context for error reporting
|
|
328
|
+
|
|
329
|
+
Returns:
|
|
330
|
+
Dictionary with status information about Amazon Inspector
|
|
331
|
+
"""
|
|
332
|
+
try:
|
|
333
|
+
# Create Inspector client (using inspector2)
|
|
334
|
+
inspector_client = session.client(
|
|
335
|
+
"inspector2", region_name=region, config=USER_AGENT_CONFIG
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
try:
|
|
339
|
+
# Get Inspector status
|
|
340
|
+
try:
|
|
341
|
+
# First try using get_status API
|
|
342
|
+
status_response = inspector_client.get_status()
|
|
343
|
+
print(
|
|
344
|
+
f"[DEBUG:Inspector] get_status() successful, raw response: {status_response}"
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
# If we can call get_status successfully, Inspector2 is enabled
|
|
348
|
+
# Now we need to determine which scan types are enabled
|
|
349
|
+
|
|
350
|
+
# The service exists and is enabled at this point, since get_status worked
|
|
351
|
+
is_enabled = True
|
|
352
|
+
|
|
353
|
+
# Attempt to extract status from different possible response structures
|
|
354
|
+
status = {}
|
|
355
|
+
|
|
356
|
+
# Check all possible paths where status might be located
|
|
357
|
+
if isinstance(status_response, dict):
|
|
358
|
+
# Direct status fields in response root
|
|
359
|
+
for scan_type in ["EC2", "ECR", "LAMBDA", "ec2", "ecr", "lambda"]:
|
|
360
|
+
# Try all possible field name patterns for each scan type
|
|
361
|
+
for field_pattern in [
|
|
362
|
+
f"{scan_type}Status",
|
|
363
|
+
f"{scan_type.lower()}Status",
|
|
364
|
+
f"{scan_type}_status",
|
|
365
|
+
f"{scan_type.lower()}_status",
|
|
366
|
+
scan_type,
|
|
367
|
+
scan_type.lower(),
|
|
368
|
+
]:
|
|
369
|
+
if field_pattern in status_response:
|
|
370
|
+
status[field_pattern] = status_response[field_pattern]
|
|
371
|
+
|
|
372
|
+
# Try the 'status' nested object too
|
|
373
|
+
if "status" in status_response and isinstance(status_response["status"], dict):
|
|
374
|
+
for key, value in status_response["status"].items():
|
|
375
|
+
# Avoid duplicates if we've already found this info
|
|
376
|
+
if key not in status:
|
|
377
|
+
status[key] = value
|
|
378
|
+
|
|
379
|
+
print(f"[DEBUG:Inspector] Extracted status fields: {status}")
|
|
380
|
+
|
|
381
|
+
# Check for enabled scan types
|
|
382
|
+
scan_types = ["EC2", "ECR", "LAMBDA"]
|
|
383
|
+
enabled_scans = []
|
|
384
|
+
|
|
385
|
+
for scan_type in scan_types:
|
|
386
|
+
found_enabled = False
|
|
387
|
+
# Check all possible status keys for this scan type
|
|
388
|
+
for status_key in [
|
|
389
|
+
f"{scan_type}Status",
|
|
390
|
+
f"{scan_type.lower()}Status",
|
|
391
|
+
f"{scan_type}_status",
|
|
392
|
+
f"{scan_type.lower()}_status",
|
|
393
|
+
scan_type,
|
|
394
|
+
scan_type.lower(),
|
|
395
|
+
]:
|
|
396
|
+
status_value = None
|
|
397
|
+
|
|
398
|
+
# Try direct key in status dictionary
|
|
399
|
+
if status_key in status:
|
|
400
|
+
status_value = status[status_key]
|
|
401
|
+
print(
|
|
402
|
+
f"[DEBUG:Inspector] Found status for {scan_type} via key {status_key}: {status_value}"
|
|
403
|
+
)
|
|
404
|
+
|
|
405
|
+
# Check if the status value indicates "enabled"
|
|
406
|
+
if status_value and (
|
|
407
|
+
(isinstance(status_value, str) and status_value.upper() == "ENABLED")
|
|
408
|
+
or (isinstance(status_value, bool) and status_value is True)
|
|
409
|
+
):
|
|
410
|
+
enabled_scans.append(scan_type)
|
|
411
|
+
found_enabled = True
|
|
412
|
+
print(f"[DEBUG:Inspector] {scan_type} scan type is ENABLED")
|
|
413
|
+
break
|
|
414
|
+
|
|
415
|
+
if not found_enabled:
|
|
416
|
+
# If we haven't found an "enabled" status for this scan type, try one more approach
|
|
417
|
+
# Looking for any key that contains the scan type name and has "enabled" value
|
|
418
|
+
for status_key, status_value in status.items():
|
|
419
|
+
if (
|
|
420
|
+
scan_type.lower() in status_key.lower()
|
|
421
|
+
and isinstance(status_value, str)
|
|
422
|
+
and "enable" in status_value.lower()
|
|
423
|
+
):
|
|
424
|
+
enabled_scans.append(scan_type)
|
|
425
|
+
print(
|
|
426
|
+
f"[DEBUG:Inspector] {scan_type} scan type is potentially enabled via fuzzy match"
|
|
427
|
+
)
|
|
428
|
+
break
|
|
429
|
+
|
|
430
|
+
print(f"[DEBUG:Inspector] Final enabled scan types: {enabled_scans}")
|
|
431
|
+
|
|
432
|
+
# Build the scan status dictionary
|
|
433
|
+
scan_status = {}
|
|
434
|
+
for scan_type in scan_types:
|
|
435
|
+
scan_found = False
|
|
436
|
+
scan_status_key = f"{scan_type.lower()}_status"
|
|
437
|
+
|
|
438
|
+
# Look for this scan type in the status dictionary
|
|
439
|
+
for status_key, status_value in status.items():
|
|
440
|
+
if scan_type.lower() in status_key.lower():
|
|
441
|
+
scan_status[scan_status_key] = status_value
|
|
442
|
+
scan_found = True
|
|
443
|
+
break
|
|
444
|
+
|
|
445
|
+
# If no matching key found, indicate unknown
|
|
446
|
+
if not scan_found:
|
|
447
|
+
scan_status[scan_status_key] = "UNKNOWN"
|
|
448
|
+
|
|
449
|
+
# By this point, if we successfully called get_status, the service itself is enabled
|
|
450
|
+
# Even if no scan types are explicitly shown as enabled
|
|
451
|
+
return {
|
|
452
|
+
"enabled": is_enabled,
|
|
453
|
+
"scan_status": scan_status,
|
|
454
|
+
"message": f"Amazon Inspector is enabled with the following scan types: {', '.join(enabled_scans) if enabled_scans else 'unknown'}",
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
except Exception as status_error:
|
|
458
|
+
# log the error but continue with the alternative checks
|
|
459
|
+
print(f"[DEBUG:Inspector] get_status() error: {status_error}")
|
|
460
|
+
await ctx.warning(f"Error calling Inspector2 get_status(): {status_error}")
|
|
461
|
+
|
|
462
|
+
# If get_status failed or didn't find scan types, try another approach
|
|
463
|
+
# Try calling batch_get_account_status which may give different information
|
|
464
|
+
try:
|
|
465
|
+
account_status = inspector_client.batch_get_account_status()
|
|
466
|
+
|
|
467
|
+
# If we get here, the service is enabled
|
|
468
|
+
if "accounts" in account_status and account_status["accounts"]:
|
|
469
|
+
account_info = account_status["accounts"][0]
|
|
470
|
+
resource_status = account_info.get("resourceStatus", {})
|
|
471
|
+
|
|
472
|
+
# Check which resources are enabled
|
|
473
|
+
ec2_enabled = resource_status.get("ec2", {}).get("status") == "ENABLED"
|
|
474
|
+
ecr_enabled = resource_status.get("ecr", {}).get("status") == "ENABLED"
|
|
475
|
+
lambda_enabled = resource_status.get("lambda", {}).get("status") == "ENABLED"
|
|
476
|
+
|
|
477
|
+
enabled_scans = []
|
|
478
|
+
if ec2_enabled:
|
|
479
|
+
enabled_scans.append("EC2")
|
|
480
|
+
if ecr_enabled:
|
|
481
|
+
enabled_scans.append("ECR")
|
|
482
|
+
if lambda_enabled:
|
|
483
|
+
enabled_scans.append("LAMBDA")
|
|
484
|
+
|
|
485
|
+
print(
|
|
486
|
+
f"[DEBUG:Inspector] From batch_get_account_status, enabled scans: {enabled_scans}"
|
|
487
|
+
)
|
|
488
|
+
|
|
489
|
+
# If we successfully called batch_get_account_status, treat Inspector as enabled
|
|
490
|
+
return {
|
|
491
|
+
"enabled": True,
|
|
492
|
+
"scan_status": {
|
|
493
|
+
"ec2_status": "ENABLED" if ec2_enabled else "DISABLED",
|
|
494
|
+
"ecr_status": "ENABLED" if ecr_enabled else "DISABLED",
|
|
495
|
+
"lambda_status": "ENABLED" if lambda_enabled else "DISABLED",
|
|
496
|
+
},
|
|
497
|
+
"message": f"Amazon Inspector is enabled with the following scan types: {', '.join(enabled_scans) if enabled_scans else 'none'}",
|
|
498
|
+
}
|
|
499
|
+
except Exception as account_error:
|
|
500
|
+
print(f"[DEBUG:Inspector] batch_get_account_status() error: {account_error}")
|
|
501
|
+
|
|
502
|
+
# As a last resort, try listing findings
|
|
503
|
+
# If this works, it means Inspector is enabled
|
|
504
|
+
try:
|
|
505
|
+
# Try listing a small number of findings just to test API access
|
|
506
|
+
findings_response = inspector_client.list_findings(maxResults=1)
|
|
507
|
+
flag = False
|
|
508
|
+
if findings_response:
|
|
509
|
+
flag = True
|
|
510
|
+
# If we can call list_findings, Inspector is definitely enabled
|
|
511
|
+
return {
|
|
512
|
+
"enabled": flag,
|
|
513
|
+
"scan_status": {
|
|
514
|
+
"ec2_status": "UNKNOWN",
|
|
515
|
+
"ecr_status": "UNKNOWN",
|
|
516
|
+
"lambda_status": "UNKNOWN",
|
|
517
|
+
},
|
|
518
|
+
"message": "Amazon Inspector is enabled, but specific scan types could not be determined.",
|
|
519
|
+
}
|
|
520
|
+
except Exception as findings_error:
|
|
521
|
+
print(f"[DEBUG:Inspector] list_findings() error: {findings_error}")
|
|
522
|
+
|
|
523
|
+
# If we get here, we've tried multiple methods but can't confirm Inspector is enabled
|
|
524
|
+
print("[DEBUG:Inspector] All detection methods failed, treating as not enabled")
|
|
525
|
+
return {
|
|
526
|
+
"enabled": False,
|
|
527
|
+
"scan_status": {
|
|
528
|
+
"ec2_status": "UNKNOWN",
|
|
529
|
+
"ecr_status": "UNKNOWN",
|
|
530
|
+
"lambda_status": "UNKNOWN",
|
|
531
|
+
},
|
|
532
|
+
"setup_instructions": """
|
|
533
|
+
# Amazon Inspector Setup Instructions
|
|
534
|
+
|
|
535
|
+
Amazon Inspector may not be fully enabled in this region. To enable it:
|
|
536
|
+
|
|
537
|
+
1. Open the Inspector console: https://console.aws.amazon.com/inspector/
|
|
538
|
+
2. Choose Settings
|
|
539
|
+
3. Enable the scan types you need (EC2, ECR, Lambda)
|
|
540
|
+
|
|
541
|
+
This is strongly recommended for identifying vulnerabilities in your workloads.
|
|
542
|
+
|
|
543
|
+
Learn more: https://docs.aws.amazon.com/inspector/latest/user/enabling-disable-scanning-account.html
|
|
544
|
+
""",
|
|
545
|
+
"message": "Amazon Inspector status could not be determined. Multiple detection methods failed.",
|
|
546
|
+
}
|
|
547
|
+
except inspector_client.exceptions.AccessDeniedException:
|
|
548
|
+
# Inspector is not enabled or permissions issue
|
|
549
|
+
return {
|
|
550
|
+
"enabled": False,
|
|
551
|
+
"setup_instructions": """
|
|
552
|
+
# Amazon Inspector Setup Instructions
|
|
553
|
+
Amazon Inspector is not enabled in this region. To enable it:
|
|
554
|
+
1. Open the Inspector console: https://console.aws.amazon.com/inspector/
|
|
555
|
+
2. Choose Get started
|
|
556
|
+
3. Choose Enable Amazon Inspector
|
|
557
|
+
4. Select the scan types to enable
|
|
558
|
+
""",
|
|
559
|
+
"message": "Amazon Inspector is not enabled in this region.",
|
|
560
|
+
}
|
|
561
|
+
except Exception as e:
|
|
562
|
+
await ctx.error(f"Error checking Inspector status: {e}")
|
|
563
|
+
return {"enabled": False, "error": str(e), "message": "Error checking Inspector status."}
|
|
564
|
+
|
|
565
|
+
|
|
566
|
+
# New functions to get findings from security services
|
|
567
|
+
|
|
568
|
+
|
|
569
|
+
async def get_guardduty_findings(
|
|
570
|
+
region: str,
|
|
571
|
+
session: boto3.Session,
|
|
572
|
+
ctx: Context,
|
|
573
|
+
max_findings: int = 100,
|
|
574
|
+
filter_criteria: Optional[Dict] = None,
|
|
575
|
+
) -> Dict:
|
|
576
|
+
"""Get findings from Amazon GuardDuty in the specified region.
|
|
577
|
+
|
|
578
|
+
Args:
|
|
579
|
+
region: AWS region to get findings from
|
|
580
|
+
session: boto3 Session for AWS API calls
|
|
581
|
+
ctx: MCP context for error reporting
|
|
582
|
+
max_findings: Maximum number of findings to return (default: 100)
|
|
583
|
+
filter_criteria: Optional filter criteria for findings
|
|
584
|
+
|
|
585
|
+
Returns:
|
|
586
|
+
Dictionary containing GuardDuty findings
|
|
587
|
+
"""
|
|
588
|
+
try:
|
|
589
|
+
# First check if GuardDuty is enabled
|
|
590
|
+
print(f"[DEBUG:GuardDuty] Checking if GuardDuty is enabled in {region}")
|
|
591
|
+
guardduty_status = await check_guard_duty(region, session, ctx)
|
|
592
|
+
if not guardduty_status.get("enabled", False):
|
|
593
|
+
print(f"[DEBUG:GuardDuty] GuardDuty is not enabled in {region}")
|
|
594
|
+
return {
|
|
595
|
+
"enabled": False,
|
|
596
|
+
"message": "Amazon GuardDuty is not enabled in this region",
|
|
597
|
+
"findings": [],
|
|
598
|
+
"debug_info": "GuardDuty is not enabled, no findings retrieved",
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
# Get detector ID
|
|
602
|
+
print("[DEBUG:GuardDuty] GuardDuty is enabled, retrieving detector ID")
|
|
603
|
+
detector_id = guardduty_status.get("detector_details", {}).get("id")
|
|
604
|
+
if not detector_id:
|
|
605
|
+
print("[DEBUG:GuardDuty] ERROR: No GuardDuty detector ID found")
|
|
606
|
+
await ctx.error("No GuardDuty detector ID found")
|
|
607
|
+
return {
|
|
608
|
+
"enabled": True,
|
|
609
|
+
"error": "No GuardDuty detector ID found",
|
|
610
|
+
"findings": [],
|
|
611
|
+
"debug_info": "GuardDuty is enabled but no detector ID was found",
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
print(f"[DEBUG:GuardDuty] Using detector ID: {detector_id}")
|
|
615
|
+
|
|
616
|
+
# Create GuardDuty client
|
|
617
|
+
guardduty_client = session.client(
|
|
618
|
+
"guardduty", region_name=region, config=USER_AGENT_CONFIG
|
|
619
|
+
)
|
|
620
|
+
|
|
621
|
+
# Set up default finding criteria if none provided
|
|
622
|
+
if filter_criteria is None:
|
|
623
|
+
print("[DEBUG:GuardDuty] No filter criteria provided, creating default criteria")
|
|
624
|
+
# By default, get findings from the last 30 days with high or medium severity
|
|
625
|
+
# Calculate timestamp in milliseconds (GuardDuty expects integer timestamp)
|
|
626
|
+
thirty_days_ago = int(
|
|
627
|
+
(datetime.datetime.now() - datetime.timedelta(days=30)).timestamp() * 1000
|
|
628
|
+
)
|
|
629
|
+
|
|
630
|
+
filter_criteria = {
|
|
631
|
+
"Criterion": {
|
|
632
|
+
"severity": {
|
|
633
|
+
"Eq": ["7", "5", "8"] # High (7), Medium (5), and Critical (8) findings
|
|
634
|
+
},
|
|
635
|
+
"updatedAt": {"GreaterThanOrEqual": thirty_days_ago},
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
print(
|
|
639
|
+
f"[DEBUG:GuardDuty] Created default filter criteria with timestamp: {thirty_days_ago} ({datetime.datetime.fromtimestamp(thirty_days_ago / 1000).isoformat()})"
|
|
640
|
+
)
|
|
641
|
+
else:
|
|
642
|
+
print(
|
|
643
|
+
f"[DEBUG:GuardDuty] Using provided filter criteria: {json.dumps(filter_criteria)}"
|
|
644
|
+
)
|
|
645
|
+
|
|
646
|
+
# List findings with the filter criteria
|
|
647
|
+
print(f"[DEBUG:GuardDuty] Calling list_findings with max results: {max_findings}")
|
|
648
|
+
findings_response = guardduty_client.list_findings(
|
|
649
|
+
DetectorId=detector_id, FindingCriteria=filter_criteria, MaxResults=max_findings
|
|
650
|
+
)
|
|
651
|
+
|
|
652
|
+
finding_ids = findings_response.get("FindingIds", [])
|
|
653
|
+
print(f"[DEBUG:GuardDuty] Retrieved {len(finding_ids)} finding IDs")
|
|
654
|
+
|
|
655
|
+
if not finding_ids:
|
|
656
|
+
print("[DEBUG:GuardDuty] No findings match the filter criteria")
|
|
657
|
+
return {
|
|
658
|
+
"enabled": True,
|
|
659
|
+
"message": "No GuardDuty findings match the filter criteria",
|
|
660
|
+
"findings": [],
|
|
661
|
+
"debug_info": "GuardDuty query returned no findings matching the criteria",
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
# Get finding details
|
|
665
|
+
print(f"[DEBUG:GuardDuty] Retrieving details for {len(finding_ids)} findings")
|
|
666
|
+
findings_details = guardduty_client.get_findings(
|
|
667
|
+
DetectorId=detector_id, FindingIds=finding_ids
|
|
668
|
+
)
|
|
669
|
+
|
|
670
|
+
# Process findings to clean up non-serializable objects (like datetime)
|
|
671
|
+
findings = []
|
|
672
|
+
raw_findings_count = len(findings_details.get("Findings", []))
|
|
673
|
+
print(
|
|
674
|
+
f"[DEBUG:GuardDuty] Processing {raw_findings_count} findings from get_findings response"
|
|
675
|
+
)
|
|
676
|
+
|
|
677
|
+
for finding in findings_details.get("Findings", []):
|
|
678
|
+
# Convert datetime objects to strings
|
|
679
|
+
finding = _clean_datetime_objects(finding)
|
|
680
|
+
findings.append(finding)
|
|
681
|
+
|
|
682
|
+
print(f"[DEBUG:GuardDuty] Successfully processed {len(findings)} findings")
|
|
683
|
+
|
|
684
|
+
# Generate summary
|
|
685
|
+
summary = _summarize_guardduty_findings(findings)
|
|
686
|
+
print(f"[DEBUG:GuardDuty] Generated summary with {summary['total_count']} findings")
|
|
687
|
+
print(
|
|
688
|
+
f"[DEBUG:GuardDuty] Severity breakdown: High={summary['severity_counts']['high']}, Medium={summary['severity_counts']['medium']}, Low={summary['severity_counts']['low']}"
|
|
689
|
+
)
|
|
690
|
+
|
|
691
|
+
return {
|
|
692
|
+
"enabled": True,
|
|
693
|
+
"message": f"Retrieved {len(findings)} GuardDuty findings",
|
|
694
|
+
"findings": findings,
|
|
695
|
+
"summary": summary,
|
|
696
|
+
"debug_info": {
|
|
697
|
+
"detector_id": detector_id,
|
|
698
|
+
"finding_ids_retrieved": len(finding_ids),
|
|
699
|
+
"findings_details_retrieved": raw_findings_count,
|
|
700
|
+
"findings_processed": len(findings),
|
|
701
|
+
"filter_criteria": filter_criteria,
|
|
702
|
+
},
|
|
703
|
+
}
|
|
704
|
+
except Exception as e:
|
|
705
|
+
await ctx.error(f"Error getting GuardDuty findings: {e}")
|
|
706
|
+
return {
|
|
707
|
+
"enabled": True,
|
|
708
|
+
"error": str(e),
|
|
709
|
+
"message": "Error getting GuardDuty findings",
|
|
710
|
+
"findings": [],
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
|
|
714
|
+
async def get_securityhub_findings(
|
|
715
|
+
region: str,
|
|
716
|
+
session: boto3.Session,
|
|
717
|
+
ctx: Context,
|
|
718
|
+
max_findings: int = 100,
|
|
719
|
+
filter_criteria: Optional[Dict] = None,
|
|
720
|
+
) -> Dict:
|
|
721
|
+
"""Get findings from AWS Security Hub in the specified region.
|
|
722
|
+
|
|
723
|
+
Args:
|
|
724
|
+
region: AWS region to get findings from
|
|
725
|
+
session: boto3 Session for AWS API calls
|
|
726
|
+
ctx: MCP context for error reporting
|
|
727
|
+
max_findings: Maximum number of findings to return (default: 100)
|
|
728
|
+
filter_criteria: Optional filter criteria for findings
|
|
729
|
+
|
|
730
|
+
Returns:
|
|
731
|
+
Dictionary containing Security Hub findings
|
|
732
|
+
"""
|
|
733
|
+
try:
|
|
734
|
+
# First check if Security Hub is enabled
|
|
735
|
+
securityhub_status = await check_security_hub(region, session, ctx)
|
|
736
|
+
if not securityhub_status.get("enabled", False):
|
|
737
|
+
return {
|
|
738
|
+
"enabled": False,
|
|
739
|
+
"message": "AWS Security Hub is not enabled in this region",
|
|
740
|
+
"findings": [],
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
# Create Security Hub client
|
|
744
|
+
securityhub_client = session.client(
|
|
745
|
+
"securityhub", region_name=region, config=USER_AGENT_CONFIG
|
|
746
|
+
)
|
|
747
|
+
|
|
748
|
+
# Set up default finding criteria if none provided
|
|
749
|
+
if filter_criteria is None:
|
|
750
|
+
# By default, get active findings from the last 30 days with high severity
|
|
751
|
+
filter_criteria = {
|
|
752
|
+
"RecordState": [{"Comparison": "EQUALS", "Value": "ACTIVE"}],
|
|
753
|
+
"WorkflowStatus": [{"Comparison": "EQUALS", "Value": "NEW"}],
|
|
754
|
+
"UpdatedAt": [
|
|
755
|
+
{
|
|
756
|
+
"Start": (datetime.datetime.now() - datetime.timedelta(days=30)).strftime(
|
|
757
|
+
"%Y-%m-%dT%H:%M:%S.%fZ"
|
|
758
|
+
),
|
|
759
|
+
"End": datetime.datetime.now().strftime("%Y-%m-%dT%H:%M:%S.%fZ"),
|
|
760
|
+
}
|
|
761
|
+
],
|
|
762
|
+
"SeverityLabel": [
|
|
763
|
+
{"Comparison": "EQUALS", "Value": "HIGH"},
|
|
764
|
+
{"Comparison": "EQUALS", "Value": "CRITICAL"},
|
|
765
|
+
],
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
# Get findings with the filter criteria
|
|
769
|
+
findings_response = securityhub_client.get_findings(
|
|
770
|
+
Filters=filter_criteria, MaxResults=max_findings
|
|
771
|
+
)
|
|
772
|
+
|
|
773
|
+
findings = findings_response.get("Findings", [])
|
|
774
|
+
|
|
775
|
+
if not findings:
|
|
776
|
+
return {
|
|
777
|
+
"enabled": True,
|
|
778
|
+
"message": "No Security Hub findings match the filter criteria",
|
|
779
|
+
"findings": [],
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
# Process findings to clean up non-serializable objects (like datetime)
|
|
783
|
+
processed_findings = []
|
|
784
|
+
for finding in findings:
|
|
785
|
+
# Convert datetime objects to strings
|
|
786
|
+
finding = _clean_datetime_objects(finding)
|
|
787
|
+
processed_findings.append(finding)
|
|
788
|
+
|
|
789
|
+
return {
|
|
790
|
+
"enabled": True,
|
|
791
|
+
"message": f"Retrieved {len(processed_findings)} Security Hub findings",
|
|
792
|
+
"findings": processed_findings,
|
|
793
|
+
"summary": _summarize_securityhub_findings(processed_findings),
|
|
794
|
+
}
|
|
795
|
+
except Exception as e:
|
|
796
|
+
await ctx.error(f"Error getting Security Hub findings: {e}")
|
|
797
|
+
return {
|
|
798
|
+
"enabled": True,
|
|
799
|
+
"error": str(e),
|
|
800
|
+
"message": "Error getting Security Hub findings",
|
|
801
|
+
"findings": [],
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
|
|
805
|
+
async def get_inspector_findings(
|
|
806
|
+
region: str,
|
|
807
|
+
session: boto3.Session,
|
|
808
|
+
ctx: Context,
|
|
809
|
+
max_findings: int = 100,
|
|
810
|
+
filter_criteria: Optional[Dict] = None,
|
|
811
|
+
) -> Dict:
|
|
812
|
+
"""Get findings from Amazon Inspector in the specified region.
|
|
813
|
+
|
|
814
|
+
Args:
|
|
815
|
+
region: AWS region to get findings from
|
|
816
|
+
session: boto3 Session for AWS API calls
|
|
817
|
+
ctx: MCP context for error reporting
|
|
818
|
+
max_findings: Maximum number of findings to return (default: 100)
|
|
819
|
+
filter_criteria: Optional filter criteria for findings
|
|
820
|
+
|
|
821
|
+
Returns:
|
|
822
|
+
Dictionary containing Inspector findings
|
|
823
|
+
"""
|
|
824
|
+
try:
|
|
825
|
+
# First check if Inspector is enabled
|
|
826
|
+
inspector_status = await check_inspector(region, session, ctx)
|
|
827
|
+
if not inspector_status.get("enabled", False):
|
|
828
|
+
return {
|
|
829
|
+
"enabled": False,
|
|
830
|
+
"message": "Amazon Inspector is not enabled in this region",
|
|
831
|
+
"findings": [],
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
# Create Inspector client
|
|
835
|
+
inspector_client = session.client(
|
|
836
|
+
"inspector2", region_name=region, config=USER_AGENT_CONFIG
|
|
837
|
+
)
|
|
838
|
+
|
|
839
|
+
# Set up default finding criteria if none provided
|
|
840
|
+
if filter_criteria is None:
|
|
841
|
+
# By default, get findings with high or critical severity
|
|
842
|
+
filter_criteria = {
|
|
843
|
+
"severities": [
|
|
844
|
+
{"comparison": "EQUALS", "value": "HIGH"},
|
|
845
|
+
{"comparison": "EQUALS", "value": "CRITICAL"},
|
|
846
|
+
],
|
|
847
|
+
"findingStatus": [{"comparison": "EQUALS", "value": "ACTIVE"}],
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
# List findings with the filter criteria
|
|
851
|
+
findings_response = inspector_client.list_findings(
|
|
852
|
+
filterCriteria=filter_criteria, maxResults=max_findings
|
|
853
|
+
)
|
|
854
|
+
|
|
855
|
+
findings = findings_response.get("findings", [])
|
|
856
|
+
|
|
857
|
+
if not findings:
|
|
858
|
+
return {
|
|
859
|
+
"enabled": True,
|
|
860
|
+
"message": "No Inspector findings match the filter criteria",
|
|
861
|
+
"findings": [],
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
# Process findings to clean up non-serializable objects (like datetime)
|
|
865
|
+
processed_findings = []
|
|
866
|
+
for finding in findings:
|
|
867
|
+
# Convert datetime objects to strings
|
|
868
|
+
finding = _clean_datetime_objects(finding)
|
|
869
|
+
processed_findings.append(finding)
|
|
870
|
+
|
|
871
|
+
return {
|
|
872
|
+
"enabled": True,
|
|
873
|
+
"message": f"Retrieved {len(processed_findings)} Inspector findings",
|
|
874
|
+
"findings": processed_findings,
|
|
875
|
+
"summary": _summarize_inspector_findings(processed_findings),
|
|
876
|
+
}
|
|
877
|
+
except Exception as e:
|
|
878
|
+
await ctx.error(f"Error getting Inspector findings: {e}")
|
|
879
|
+
return {
|
|
880
|
+
"enabled": True,
|
|
881
|
+
"error": str(e),
|
|
882
|
+
"message": "Error getting Inspector findings",
|
|
883
|
+
"findings": [],
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
|
|
887
|
+
async def get_access_analyzer_findings(
|
|
888
|
+
region: str, session: boto3.Session, ctx: Context, analyzer_arn: Optional[str] = None
|
|
889
|
+
) -> Dict:
|
|
890
|
+
"""Get findings from IAM Access Analyzer in the specified region.
|
|
891
|
+
|
|
892
|
+
Args:
|
|
893
|
+
region: AWS region to get findings from
|
|
894
|
+
session: boto3 Session for AWS API calls
|
|
895
|
+
ctx: MCP context for error reporting
|
|
896
|
+
analyzer_arn: Optional ARN of a specific analyzer to get findings from
|
|
897
|
+
|
|
898
|
+
Returns:
|
|
899
|
+
Dictionary containing IAM Access Analyzer findings
|
|
900
|
+
"""
|
|
901
|
+
try:
|
|
902
|
+
# First check if Access Analyzer is enabled
|
|
903
|
+
analyzer_status = await check_access_analyzer(region, session, ctx)
|
|
904
|
+
if not analyzer_status.get("enabled", False):
|
|
905
|
+
return {
|
|
906
|
+
"enabled": False,
|
|
907
|
+
"message": "IAM Access Analyzer is not enabled in this region",
|
|
908
|
+
"findings": [],
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
# Create Access Analyzer client
|
|
912
|
+
analyzer_client = session.client(
|
|
913
|
+
"accessanalyzer", region_name=region, config=USER_AGENT_CONFIG
|
|
914
|
+
)
|
|
915
|
+
|
|
916
|
+
analyzers = analyzer_status.get("analyzers", [])
|
|
917
|
+
if not analyzers:
|
|
918
|
+
return {
|
|
919
|
+
"enabled": True,
|
|
920
|
+
"message": "No IAM Access Analyzer analyzers found in this region",
|
|
921
|
+
"findings": [],
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
all_findings = []
|
|
925
|
+
|
|
926
|
+
# If analyzer_arn is provided, only get findings for that analyzer
|
|
927
|
+
if analyzer_arn:
|
|
928
|
+
analyzers = [a for a in analyzers if a.get("arn") == analyzer_arn]
|
|
929
|
+
|
|
930
|
+
# Get findings for each analyzer
|
|
931
|
+
for analyzer in analyzers:
|
|
932
|
+
analyzer_arn = analyzer.get("arn")
|
|
933
|
+
if not analyzer_arn:
|
|
934
|
+
continue
|
|
935
|
+
|
|
936
|
+
findings_response = analyzer_client.list_findings(
|
|
937
|
+
analyzerArn=analyzer_arn, maxResults=100
|
|
938
|
+
)
|
|
939
|
+
|
|
940
|
+
finding_ids = findings_response.get("findings", [])
|
|
941
|
+
|
|
942
|
+
# Get details for each finding
|
|
943
|
+
for finding_id in finding_ids:
|
|
944
|
+
finding_details = analyzer_client.get_finding(
|
|
945
|
+
analyzerArn=analyzer_arn, id=finding_id
|
|
946
|
+
)
|
|
947
|
+
|
|
948
|
+
# Clean up non-serializable objects
|
|
949
|
+
finding_details = _clean_datetime_objects(finding_details)
|
|
950
|
+
all_findings.append(finding_details)
|
|
951
|
+
|
|
952
|
+
if not all_findings:
|
|
953
|
+
return {
|
|
954
|
+
"enabled": True,
|
|
955
|
+
"message": "No IAM Access Analyzer findings found",
|
|
956
|
+
"findings": [],
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
return {
|
|
960
|
+
"enabled": True,
|
|
961
|
+
"message": f"Retrieved {len(all_findings)} IAM Access Analyzer findings",
|
|
962
|
+
"findings": all_findings,
|
|
963
|
+
"summary": _summarize_access_analyzer_findings(all_findings),
|
|
964
|
+
}
|
|
965
|
+
except Exception as e:
|
|
966
|
+
await ctx.error(f"Error getting IAM Access Analyzer findings: {e}")
|
|
967
|
+
return {
|
|
968
|
+
"enabled": True,
|
|
969
|
+
"error": str(e),
|
|
970
|
+
"message": "Error getting IAM Access Analyzer findings",
|
|
971
|
+
"findings": [],
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
|
|
975
|
+
# Helper functions for processing findings
|
|
976
|
+
|
|
977
|
+
|
|
978
|
+
def _clean_datetime_objects(obj: Any) -> Any:
|
|
979
|
+
"""Convert datetime objects in a nested dictionary to ISO format strings.
|
|
980
|
+
|
|
981
|
+
Args:
|
|
982
|
+
obj: Object that may contain datetime objects
|
|
983
|
+
|
|
984
|
+
Returns:
|
|
985
|
+
Object with datetime objects converted to strings
|
|
986
|
+
"""
|
|
987
|
+
if isinstance(obj, datetime.datetime):
|
|
988
|
+
return obj.isoformat()
|
|
989
|
+
elif isinstance(obj, list):
|
|
990
|
+
return [_clean_datetime_objects(item) for item in obj]
|
|
991
|
+
elif isinstance(obj, dict):
|
|
992
|
+
return {k: _clean_datetime_objects(v) for k, v in obj.items()}
|
|
993
|
+
else:
|
|
994
|
+
return obj
|
|
995
|
+
|
|
996
|
+
|
|
997
|
+
def _summarize_guardduty_findings(findings: List[Dict]) -> Dict:
|
|
998
|
+
"""Generate a summary of GuardDuty findings.
|
|
999
|
+
|
|
1000
|
+
Args:
|
|
1001
|
+
findings: List of GuardDuty finding dictionaries
|
|
1002
|
+
|
|
1003
|
+
Returns:
|
|
1004
|
+
Dictionary with summary information
|
|
1005
|
+
"""
|
|
1006
|
+
summary = {
|
|
1007
|
+
"total_count": len(findings),
|
|
1008
|
+
"severity_counts": {"high": 0, "medium": 0, "low": 0},
|
|
1009
|
+
"type_counts": {},
|
|
1010
|
+
"resource_counts": {},
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
for finding in findings:
|
|
1014
|
+
# Count by severity
|
|
1015
|
+
severity = finding.get("Severity", 0)
|
|
1016
|
+
if severity >= 7:
|
|
1017
|
+
summary["severity_counts"]["high"] += 1
|
|
1018
|
+
elif severity >= 4:
|
|
1019
|
+
summary["severity_counts"]["medium"] += 1
|
|
1020
|
+
else:
|
|
1021
|
+
summary["severity_counts"]["low"] += 1
|
|
1022
|
+
|
|
1023
|
+
# Count by finding type
|
|
1024
|
+
finding_type = finding.get("Type", "unknown")
|
|
1025
|
+
if finding_type in summary["type_counts"]:
|
|
1026
|
+
summary["type_counts"][finding_type] += 1
|
|
1027
|
+
else:
|
|
1028
|
+
summary["type_counts"][finding_type] = 1
|
|
1029
|
+
|
|
1030
|
+
# Count by resource type
|
|
1031
|
+
resource_type = finding.get("Resource", {}).get("ResourceType", "unknown")
|
|
1032
|
+
if resource_type in summary["resource_counts"]:
|
|
1033
|
+
summary["resource_counts"][resource_type] += 1
|
|
1034
|
+
else:
|
|
1035
|
+
summary["resource_counts"][resource_type] = 1
|
|
1036
|
+
|
|
1037
|
+
return summary
|
|
1038
|
+
|
|
1039
|
+
|
|
1040
|
+
def _summarize_securityhub_findings(findings: List[Dict]) -> Dict:
|
|
1041
|
+
"""Generate a summary of Security Hub findings.
|
|
1042
|
+
|
|
1043
|
+
Args:
|
|
1044
|
+
findings: List of Security Hub finding dictionaries
|
|
1045
|
+
|
|
1046
|
+
Returns:
|
|
1047
|
+
Dictionary with summary information
|
|
1048
|
+
"""
|
|
1049
|
+
summary = {
|
|
1050
|
+
"total_count": len(findings),
|
|
1051
|
+
"severity_counts": {"critical": 0, "high": 0, "medium": 0, "low": 0},
|
|
1052
|
+
"standard_counts": {},
|
|
1053
|
+
"resource_type_counts": {},
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
for finding in findings:
|
|
1057
|
+
# Count by severity
|
|
1058
|
+
severity = finding.get("Severity", {}).get("Label", "MEDIUM").upper()
|
|
1059
|
+
if severity == "CRITICAL":
|
|
1060
|
+
summary["severity_counts"]["critical"] += 1
|
|
1061
|
+
elif severity == "HIGH":
|
|
1062
|
+
summary["severity_counts"]["high"] += 1
|
|
1063
|
+
elif severity == "MEDIUM":
|
|
1064
|
+
summary["severity_counts"]["medium"] += 1
|
|
1065
|
+
else:
|
|
1066
|
+
summary["severity_counts"]["low"] += 1
|
|
1067
|
+
|
|
1068
|
+
# Count by compliance standard
|
|
1069
|
+
product_name = finding.get("ProductName", "unknown")
|
|
1070
|
+
if product_name in summary["standard_counts"]:
|
|
1071
|
+
summary["standard_counts"][product_name] += 1
|
|
1072
|
+
else:
|
|
1073
|
+
summary["standard_counts"][product_name] = 1
|
|
1074
|
+
|
|
1075
|
+
# Count by resource type
|
|
1076
|
+
resources = finding.get("Resources", [])
|
|
1077
|
+
for resource in resources:
|
|
1078
|
+
resource_type = resource.get("Type", "unknown")
|
|
1079
|
+
if resource_type in summary["resource_type_counts"]:
|
|
1080
|
+
summary["resource_type_counts"][resource_type] += 1
|
|
1081
|
+
else:
|
|
1082
|
+
summary["resource_type_counts"][resource_type] = 1
|
|
1083
|
+
|
|
1084
|
+
return summary
|
|
1085
|
+
|
|
1086
|
+
|
|
1087
|
+
def _summarize_inspector_findings(findings: List[Dict]) -> Dict:
|
|
1088
|
+
"""Generate a summary of Inspector findings.
|
|
1089
|
+
|
|
1090
|
+
Args:
|
|
1091
|
+
findings: List of Inspector finding dictionaries
|
|
1092
|
+
|
|
1093
|
+
Returns:
|
|
1094
|
+
Dictionary with summary information
|
|
1095
|
+
"""
|
|
1096
|
+
summary = {
|
|
1097
|
+
"total_count": len(findings),
|
|
1098
|
+
"severity_counts": {"critical": 0, "high": 0, "medium": 0, "low": 0},
|
|
1099
|
+
"type_counts": {},
|
|
1100
|
+
"resource_type_counts": {},
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
for finding in findings:
|
|
1104
|
+
# Count by severity
|
|
1105
|
+
severity = finding.get("severity", "MEDIUM")
|
|
1106
|
+
if severity == "CRITICAL":
|
|
1107
|
+
summary["severity_counts"]["critical"] += 1
|
|
1108
|
+
elif severity == "HIGH":
|
|
1109
|
+
summary["severity_counts"]["high"] += 1
|
|
1110
|
+
elif severity == "MEDIUM":
|
|
1111
|
+
summary["severity_counts"]["medium"] += 1
|
|
1112
|
+
else:
|
|
1113
|
+
summary["severity_counts"]["low"] += 1
|
|
1114
|
+
|
|
1115
|
+
# Count by finding type
|
|
1116
|
+
finding_type = finding.get("type", "unknown")
|
|
1117
|
+
if finding_type in summary["type_counts"]:
|
|
1118
|
+
summary["type_counts"][finding_type] += 1
|
|
1119
|
+
else:
|
|
1120
|
+
summary["type_counts"][finding_type] = 1
|
|
1121
|
+
|
|
1122
|
+
# Count by resource type
|
|
1123
|
+
resource_type = finding.get("resourceType", "unknown")
|
|
1124
|
+
if resource_type in summary["resource_type_counts"]:
|
|
1125
|
+
summary["resource_type_counts"][resource_type] += 1
|
|
1126
|
+
else:
|
|
1127
|
+
summary["resource_type_counts"][resource_type] = 1
|
|
1128
|
+
|
|
1129
|
+
return summary
|
|
1130
|
+
|
|
1131
|
+
|
|
1132
|
+
def _summarize_access_analyzer_findings(findings: List[Dict]) -> Dict:
|
|
1133
|
+
"""Generate a summary of IAM Access Analyzer findings.
|
|
1134
|
+
|
|
1135
|
+
Args:
|
|
1136
|
+
findings: List of IAM Access Analyzer finding dictionaries
|
|
1137
|
+
|
|
1138
|
+
Returns:
|
|
1139
|
+
Dictionary with summary information
|
|
1140
|
+
"""
|
|
1141
|
+
summary = {"total_count": len(findings), "resource_type_counts": {}, "action_counts": {}}
|
|
1142
|
+
|
|
1143
|
+
for finding in findings:
|
|
1144
|
+
# Count by resource type
|
|
1145
|
+
resource_type = finding.get("resourceType", "unknown")
|
|
1146
|
+
if resource_type in summary["resource_type_counts"]:
|
|
1147
|
+
summary["resource_type_counts"][resource_type] += 1
|
|
1148
|
+
else:
|
|
1149
|
+
summary["resource_type_counts"][resource_type] = 1
|
|
1150
|
+
|
|
1151
|
+
# Count by action
|
|
1152
|
+
actions = finding.get("action", [])
|
|
1153
|
+
for action in actions:
|
|
1154
|
+
if action in summary["action_counts"]:
|
|
1155
|
+
summary["action_counts"][action] += 1
|
|
1156
|
+
else:
|
|
1157
|
+
summary["action_counts"][action] = 1
|
|
1158
|
+
|
|
1159
|
+
return summary
|
|
1160
|
+
|
|
1161
|
+
|
|
1162
|
+
async def check_trusted_advisor(region: str, session: boto3.Session, ctx: Context) -> Dict:
|
|
1163
|
+
"""Check if AWS Trusted Advisor is accessible in the account.
|
|
1164
|
+
|
|
1165
|
+
Args:
|
|
1166
|
+
region: AWS region to check (Trusted Advisor is a global service, but API calls must be made to us-east-1)
|
|
1167
|
+
session: boto3 Session for AWS API calls
|
|
1168
|
+
ctx: MCP context for error reporting
|
|
1169
|
+
|
|
1170
|
+
Returns:
|
|
1171
|
+
Dictionary with status information about AWS Trusted Advisor
|
|
1172
|
+
|
|
1173
|
+
Note:
|
|
1174
|
+
Full Trusted Advisor functionality requires Business or Enterprise Support plan.
|
|
1175
|
+
"""
|
|
1176
|
+
try:
|
|
1177
|
+
print("[DEBUG:TrustedAdvisor] Starting Trusted Advisor check")
|
|
1178
|
+
|
|
1179
|
+
# Trusted Advisor API is only available in us-east-1
|
|
1180
|
+
support_client = session.client(
|
|
1181
|
+
"support", region_name="us-east-1", config=USER_AGENT_CONFIG
|
|
1182
|
+
)
|
|
1183
|
+
|
|
1184
|
+
try:
|
|
1185
|
+
# Try to describe Trusted Advisor checks to see if we have access
|
|
1186
|
+
print("[DEBUG:TrustedAdvisor] Calling describe_trusted_advisor_checks API")
|
|
1187
|
+
checks_response = support_client.describe_trusted_advisor_checks(language="en")
|
|
1188
|
+
|
|
1189
|
+
# If we get here, we have access to Trusted Advisor
|
|
1190
|
+
checks = checks_response.get("checks", [])
|
|
1191
|
+
print(
|
|
1192
|
+
f"[DEBUG:TrustedAdvisor] Successfully retrieved {len(checks)} Trusted Advisor checks"
|
|
1193
|
+
)
|
|
1194
|
+
|
|
1195
|
+
# Count checks by category
|
|
1196
|
+
category_counts = {}
|
|
1197
|
+
for check in checks:
|
|
1198
|
+
category = check.get("category", "unknown")
|
|
1199
|
+
if category in category_counts:
|
|
1200
|
+
category_counts[category] += 1
|
|
1201
|
+
else:
|
|
1202
|
+
category_counts[category] = 1
|
|
1203
|
+
|
|
1204
|
+
# Count security checks specifically
|
|
1205
|
+
security_checks = [check for check in checks if check.get("category") == "security"]
|
|
1206
|
+
print(f"[DEBUG:TrustedAdvisor] Found {len(security_checks)} security-related checks")
|
|
1207
|
+
|
|
1208
|
+
# Determine support tier based on number of checks
|
|
1209
|
+
# Basic support typically has 7 core checks, Business/Enterprise has 100+
|
|
1210
|
+
support_tier = "Basic" if len(checks) < 20 else "Business/Enterprise"
|
|
1211
|
+
|
|
1212
|
+
return {
|
|
1213
|
+
"enabled": True,
|
|
1214
|
+
"support_tier": support_tier,
|
|
1215
|
+
"total_checks": len(checks),
|
|
1216
|
+
"security_checks": len(security_checks),
|
|
1217
|
+
"category_counts": category_counts,
|
|
1218
|
+
"message": f"AWS Trusted Advisor is accessible with {support_tier} Support ({len(checks)} checks available, {len(security_checks)} security checks).",
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
except support_client.exceptions.SubscriptionRequiredException:
|
|
1222
|
+
# This exception means Trusted Advisor is not available with the current support plan
|
|
1223
|
+
return {
|
|
1224
|
+
"enabled": False,
|
|
1225
|
+
"support_tier": "Basic",
|
|
1226
|
+
"setup_instructions": """
|
|
1227
|
+
# AWS Trusted Advisor Full Access Requirements
|
|
1228
|
+
|
|
1229
|
+
Full access to AWS Trusted Advisor requires Business or Enterprise Support plan.
|
|
1230
|
+
|
|
1231
|
+
With your current support plan, you have limited access to Trusted Advisor.
|
|
1232
|
+
To get full access to all Trusted Advisor checks:
|
|
1233
|
+
|
|
1234
|
+
1. Open the AWS Support Center Console: https://console.aws.amazon.com/support/
|
|
1235
|
+
2. Choose Support Center
|
|
1236
|
+
3. Choose Compare or change your Support plan
|
|
1237
|
+
4. Upgrade to Business or Enterprise Support
|
|
1238
|
+
|
|
1239
|
+
Learn more: https://aws.amazon.com/premiumsupport/
|
|
1240
|
+
""",
|
|
1241
|
+
"message": "Full AWS Trusted Advisor functionality requires Business or Enterprise Support plan.",
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
except Exception as e:
|
|
1245
|
+
await ctx.error(f"Error checking Trusted Advisor status: {e}")
|
|
1246
|
+
return {
|
|
1247
|
+
"enabled": False,
|
|
1248
|
+
"error": str(e),
|
|
1249
|
+
"message": "Error checking Trusted Advisor status.",
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
|
|
1253
|
+
async def get_trusted_advisor_findings(
|
|
1254
|
+
region: str,
|
|
1255
|
+
session: boto3.Session,
|
|
1256
|
+
ctx: Context,
|
|
1257
|
+
max_findings: int = 100,
|
|
1258
|
+
status_filter: Optional[List[str]] = None,
|
|
1259
|
+
category_filter: Optional[str] = None,
|
|
1260
|
+
) -> Dict:
|
|
1261
|
+
"""Retrieve check results from AWS Trusted Advisor.
|
|
1262
|
+
|
|
1263
|
+
Args:
|
|
1264
|
+
region: AWS region (Trusted Advisor is global, but API calls must be made to us-east-1)
|
|
1265
|
+
session: boto3 Session for AWS API calls
|
|
1266
|
+
ctx: MCP context for error reporting
|
|
1267
|
+
max_findings: Maximum number of findings to return (default: 100)
|
|
1268
|
+
status_filter: Optional list of statuses to filter by (e.g., ['error', 'warning'])
|
|
1269
|
+
category_filter: Optional category to filter by (e.g., 'security')
|
|
1270
|
+
|
|
1271
|
+
Returns:
|
|
1272
|
+
Dictionary containing Trusted Advisor check results
|
|
1273
|
+
"""
|
|
1274
|
+
try:
|
|
1275
|
+
print("[DEBUG:TrustedAdvisor] Starting findings retrieval")
|
|
1276
|
+
|
|
1277
|
+
# Set default status filter if not provided
|
|
1278
|
+
if status_filter is None:
|
|
1279
|
+
status_filter = ["error", "warning"]
|
|
1280
|
+
|
|
1281
|
+
# First check if Trusted Advisor is accessible
|
|
1282
|
+
ta_status = await check_trusted_advisor(region, session, ctx)
|
|
1283
|
+
if not ta_status.get("enabled", False):
|
|
1284
|
+
print("[DEBUG:TrustedAdvisor] Trusted Advisor is not fully accessible")
|
|
1285
|
+
return {
|
|
1286
|
+
"enabled": False,
|
|
1287
|
+
"message": ta_status.get("message", "AWS Trusted Advisor is not accessible"),
|
|
1288
|
+
"findings": [],
|
|
1289
|
+
"support_tier": ta_status.get("support_tier", "Unknown"),
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
# Create Support client (Trusted Advisor API is only available in us-east-1)
|
|
1293
|
+
support_client = session.client(
|
|
1294
|
+
"support", region_name="us-east-1", config=USER_AGENT_CONFIG
|
|
1295
|
+
)
|
|
1296
|
+
|
|
1297
|
+
# Get all available checks
|
|
1298
|
+
print("[DEBUG:TrustedAdvisor] Getting all available checks")
|
|
1299
|
+
checks_response = support_client.describe_trusted_advisor_checks(language="en")
|
|
1300
|
+
all_checks = checks_response.get("checks", [])
|
|
1301
|
+
|
|
1302
|
+
# Filter checks by category if specified
|
|
1303
|
+
filtered_checks = all_checks
|
|
1304
|
+
if category_filter:
|
|
1305
|
+
filtered_checks = [
|
|
1306
|
+
check
|
|
1307
|
+
for check in all_checks
|
|
1308
|
+
if check.get("category", "").lower() == category_filter.lower()
|
|
1309
|
+
]
|
|
1310
|
+
print(
|
|
1311
|
+
f"[DEBUG:TrustedAdvisor] Filtered to {len(filtered_checks)} {category_filter} checks"
|
|
1312
|
+
)
|
|
1313
|
+
|
|
1314
|
+
# Limit the number of checks to process based on max_findings
|
|
1315
|
+
checks_to_process = filtered_checks[:max_findings]
|
|
1316
|
+
|
|
1317
|
+
# Get check results
|
|
1318
|
+
findings = []
|
|
1319
|
+
for check in checks_to_process:
|
|
1320
|
+
check_id = check.get("id", "unknown") # Initialize check_id outside try block
|
|
1321
|
+
try:
|
|
1322
|
+
result = support_client.describe_trusted_advisor_check_result(
|
|
1323
|
+
checkId=check_id, language="en"
|
|
1324
|
+
)
|
|
1325
|
+
|
|
1326
|
+
# Extract the result
|
|
1327
|
+
check_result = result.get("result", {})
|
|
1328
|
+
status = check_result.get("status", "").lower()
|
|
1329
|
+
|
|
1330
|
+
# Skip checks that don't match the status filter
|
|
1331
|
+
if status_filter and status not in status_filter:
|
|
1332
|
+
continue
|
|
1333
|
+
|
|
1334
|
+
# Format the finding
|
|
1335
|
+
finding = {
|
|
1336
|
+
"check_id": check_id,
|
|
1337
|
+
"name": check.get("name"),
|
|
1338
|
+
"description": check.get("description"),
|
|
1339
|
+
"category": check.get("category"),
|
|
1340
|
+
"status": status,
|
|
1341
|
+
"timestamp": check_result.get("timestamp"),
|
|
1342
|
+
"resources_flagged": check_result.get("resourcesSummary", {}).get(
|
|
1343
|
+
"resourcesFlagged", 0
|
|
1344
|
+
),
|
|
1345
|
+
"resources_processed": check_result.get("resourcesSummary", {}).get(
|
|
1346
|
+
"resourcesProcessed", 0
|
|
1347
|
+
),
|
|
1348
|
+
"resources_suppressed": check_result.get("resourcesSummary", {}).get(
|
|
1349
|
+
"resourcesSuppressed", 0
|
|
1350
|
+
),
|
|
1351
|
+
"flagged_resources": [],
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
# Add flagged resources
|
|
1355
|
+
flagged_resources = check_result.get("flaggedResources", [])
|
|
1356
|
+
for resource in flagged_resources:
|
|
1357
|
+
# Clean up the resource data
|
|
1358
|
+
resource_data = _clean_datetime_objects(resource)
|
|
1359
|
+
finding["flagged_resources"].append(resource_data)
|
|
1360
|
+
|
|
1361
|
+
findings.append(finding)
|
|
1362
|
+
print(
|
|
1363
|
+
f"[DEBUG:TrustedAdvisor] Added finding: {finding['name']} (status: {finding['status']}, resources: {finding['resources_flagged']})"
|
|
1364
|
+
)
|
|
1365
|
+
|
|
1366
|
+
except Exception as check_error:
|
|
1367
|
+
await ctx.warning(
|
|
1368
|
+
f"Error getting results for Trusted Advisor check {check_id}: {check_error}"
|
|
1369
|
+
)
|
|
1370
|
+
|
|
1371
|
+
# Generate summary
|
|
1372
|
+
summary = _summarize_trusted_advisor_findings(findings)
|
|
1373
|
+
|
|
1374
|
+
return {
|
|
1375
|
+
"enabled": True,
|
|
1376
|
+
"message": f"Retrieved {len(findings)} Trusted Advisor findings",
|
|
1377
|
+
"findings": findings,
|
|
1378
|
+
"summary": summary,
|
|
1379
|
+
"support_tier": ta_status.get("support_tier", "Unknown"),
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
except Exception as e:
|
|
1383
|
+
await ctx.error(f"Error getting Trusted Advisor findings: {e}")
|
|
1384
|
+
return {
|
|
1385
|
+
"enabled": True,
|
|
1386
|
+
"error": str(e),
|
|
1387
|
+
"message": "Error getting Trusted Advisor findings",
|
|
1388
|
+
"findings": [],
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
|
|
1392
|
+
def _summarize_trusted_advisor_findings(findings: List[Dict]) -> Dict:
|
|
1393
|
+
"""Generate a summary of Trusted Advisor findings.
|
|
1394
|
+
|
|
1395
|
+
Args:
|
|
1396
|
+
findings: List of Trusted Advisor finding dictionaries
|
|
1397
|
+
|
|
1398
|
+
Returns:
|
|
1399
|
+
Dictionary with summary information
|
|
1400
|
+
"""
|
|
1401
|
+
summary = {
|
|
1402
|
+
"total_count": len(findings),
|
|
1403
|
+
"status_counts": {"error": 0, "warning": 0, "ok": 0, "not_available": 0},
|
|
1404
|
+
"category_counts": {},
|
|
1405
|
+
"resources_flagged": 0,
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
for finding in findings:
|
|
1409
|
+
# Count by status
|
|
1410
|
+
status = finding.get("status", "").lower()
|
|
1411
|
+
if status in summary["status_counts"]:
|
|
1412
|
+
summary["status_counts"][status] += 1
|
|
1413
|
+
else:
|
|
1414
|
+
summary["status_counts"]["not_available"] += 1
|
|
1415
|
+
|
|
1416
|
+
# Count by category
|
|
1417
|
+
category = finding.get("category", "unknown")
|
|
1418
|
+
if category in summary["category_counts"]:
|
|
1419
|
+
summary["category_counts"][category] += 1
|
|
1420
|
+
else:
|
|
1421
|
+
summary["category_counts"][category] = 1
|
|
1422
|
+
|
|
1423
|
+
# Count total flagged resources
|
|
1424
|
+
summary["resources_flagged"] += finding.get("resources_flagged", 0)
|
|
1425
|
+
|
|
1426
|
+
return summary
|
|
1427
|
+
|
|
1428
|
+
|
|
1429
|
+
async def check_macie(region: str, session: boto3.Session, ctx: Context) -> Dict:
|
|
1430
|
+
"""Check if Amazon Macie is enabled in the specified region.
|
|
1431
|
+
|
|
1432
|
+
Args:
|
|
1433
|
+
region: AWS region to check
|
|
1434
|
+
session: boto3 Session for AWS API calls
|
|
1435
|
+
ctx: MCP context for error reporting
|
|
1436
|
+
|
|
1437
|
+
Returns:
|
|
1438
|
+
Dictionary with status information about Amazon Macie
|
|
1439
|
+
"""
|
|
1440
|
+
try:
|
|
1441
|
+
print(f"[DEBUG:Macie] Starting Macie check for region: {region}")
|
|
1442
|
+
# Create Macie client
|
|
1443
|
+
macie_client = session.client("macie2", region_name=region, config=USER_AGENT_CONFIG)
|
|
1444
|
+
|
|
1445
|
+
# Check if Macie is enabled
|
|
1446
|
+
try:
|
|
1447
|
+
print("[DEBUG:Macie] Calling get_macie_session() API")
|
|
1448
|
+
status = macie_client.get_macie_session()
|
|
1449
|
+
print(f"[DEBUG:Macie] get_macie_session() successful, status: {status.get('status')}")
|
|
1450
|
+
|
|
1451
|
+
# If we get here without exception, Macie is enabled
|
|
1452
|
+
return {
|
|
1453
|
+
"enabled": True,
|
|
1454
|
+
"status": status.get("status"),
|
|
1455
|
+
"created_at": str(status.get("createdAt")),
|
|
1456
|
+
"service_role": status.get("serviceRole"),
|
|
1457
|
+
"finding_publishing_frequency": status.get("findingPublishingFrequency"),
|
|
1458
|
+
"message": "Amazon Macie is enabled in this region.",
|
|
1459
|
+
}
|
|
1460
|
+
except macie_client.exceptions.AccessDeniedException:
|
|
1461
|
+
return {
|
|
1462
|
+
"enabled": False,
|
|
1463
|
+
"setup_instructions": """
|
|
1464
|
+
# Amazon Macie Setup Instructions
|
|
1465
|
+
|
|
1466
|
+
Amazon Macie is not enabled in this region. To enable it:
|
|
1467
|
+
|
|
1468
|
+
1. Open the Macie console: https://console.aws.amazon.com/macie/
|
|
1469
|
+
2. Choose Get Started
|
|
1470
|
+
3. Configure your settings and choose Enable Macie
|
|
1471
|
+
|
|
1472
|
+
This is recommended for discovering and protecting sensitive data in S3 buckets.
|
|
1473
|
+
|
|
1474
|
+
Learn more: https://docs.aws.amazon.com/macie/latest/user/getting-started.html
|
|
1475
|
+
""",
|
|
1476
|
+
"message": "Amazon Macie is not enabled in this region.",
|
|
1477
|
+
}
|
|
1478
|
+
except Exception as e:
|
|
1479
|
+
await ctx.error(f"Error checking Macie status: {e}")
|
|
1480
|
+
return {
|
|
1481
|
+
"enabled": False,
|
|
1482
|
+
"error": str(e),
|
|
1483
|
+
"message": "Error checking Macie status.",
|
|
1484
|
+
"debug_info": {"exception": str(e), "exception_type": type(e).__name__},
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1487
|
+
|
|
1488
|
+
async def get_macie_findings(
|
|
1489
|
+
region: str,
|
|
1490
|
+
session: boto3.Session,
|
|
1491
|
+
ctx: Context,
|
|
1492
|
+
max_findings: int = 100,
|
|
1493
|
+
filter_criteria: Optional[Dict] = None,
|
|
1494
|
+
) -> Dict:
|
|
1495
|
+
"""Get findings from Amazon Macie in the specified region.
|
|
1496
|
+
|
|
1497
|
+
Args:
|
|
1498
|
+
region: AWS region to get findings from
|
|
1499
|
+
session: boto3 Session for AWS API calls
|
|
1500
|
+
ctx: MCP context for error reporting
|
|
1501
|
+
max_findings: Maximum number of findings to return (default: 100)
|
|
1502
|
+
filter_criteria: Optional filter criteria for findings
|
|
1503
|
+
|
|
1504
|
+
Returns:
|
|
1505
|
+
Dictionary containing Macie findings
|
|
1506
|
+
"""
|
|
1507
|
+
try:
|
|
1508
|
+
print(f"[DEBUG:Macie] Starting findings retrieval for region: {region}")
|
|
1509
|
+
# First check if Macie is enabled
|
|
1510
|
+
macie_status = await check_macie(region, session, ctx)
|
|
1511
|
+
if not macie_status.get("enabled", False):
|
|
1512
|
+
print(f"[DEBUG:Macie] Macie is not enabled in {region}")
|
|
1513
|
+
return {
|
|
1514
|
+
"enabled": False,
|
|
1515
|
+
"message": "Amazon Macie is not enabled in this region",
|
|
1516
|
+
"findings": [],
|
|
1517
|
+
}
|
|
1518
|
+
|
|
1519
|
+
# Create Macie client
|
|
1520
|
+
macie_client = session.client("macie2", region_name=region, config=USER_AGENT_CONFIG)
|
|
1521
|
+
|
|
1522
|
+
# Set up default finding criteria if none provided
|
|
1523
|
+
if filter_criteria is None:
|
|
1524
|
+
filter_criteria = {"criterion": {"severity.score": {"gt": 7}}}
|
|
1525
|
+
|
|
1526
|
+
# List findings with the filter criteria
|
|
1527
|
+
findings_response = macie_client.list_findings(
|
|
1528
|
+
findingCriteria=filter_criteria, maxResults=max_findings
|
|
1529
|
+
)
|
|
1530
|
+
|
|
1531
|
+
finding_ids = findings_response.get("findingIds", [])
|
|
1532
|
+
print(f"[DEBUG:Macie] Retrieved {len(finding_ids)} finding IDs")
|
|
1533
|
+
|
|
1534
|
+
if not finding_ids:
|
|
1535
|
+
return {
|
|
1536
|
+
"enabled": True,
|
|
1537
|
+
"message": "No Macie findings match the filter criteria",
|
|
1538
|
+
"findings": [],
|
|
1539
|
+
}
|
|
1540
|
+
|
|
1541
|
+
# Get finding details
|
|
1542
|
+
print(f"[DEBUG:Macie] Retrieving details for {len(finding_ids)} findings")
|
|
1543
|
+
findings_details = macie_client.get_findings(findingIds=finding_ids)
|
|
1544
|
+
|
|
1545
|
+
# Process findings to clean up non-serializable objects (like datetime)
|
|
1546
|
+
findings = []
|
|
1547
|
+
raw_findings_count = len(findings_details.get("findings", []))
|
|
1548
|
+
print(f"[DEBUG:Macie] Processing {raw_findings_count} findings from get_findings response")
|
|
1549
|
+
|
|
1550
|
+
for finding in findings_details.get("findings", []):
|
|
1551
|
+
# Convert datetime objects to strings
|
|
1552
|
+
finding = _clean_datetime_objects(finding)
|
|
1553
|
+
findings.append(finding)
|
|
1554
|
+
|
|
1555
|
+
print(f"[DEBUG:Macie] Successfully processed {len(findings)} findings")
|
|
1556
|
+
|
|
1557
|
+
# Generate summary
|
|
1558
|
+
summary = _summarize_macie_findings(findings)
|
|
1559
|
+
print(f"[DEBUG:Macie] Generated summary with {summary['total_count']} findings")
|
|
1560
|
+
|
|
1561
|
+
return {
|
|
1562
|
+
"enabled": True,
|
|
1563
|
+
"message": f"Retrieved {len(findings)} Macie findings",
|
|
1564
|
+
"findings": findings,
|
|
1565
|
+
"summary": summary,
|
|
1566
|
+
}
|
|
1567
|
+
except Exception as e:
|
|
1568
|
+
await ctx.error(f"Error getting Macie findings: {e}")
|
|
1569
|
+
return {
|
|
1570
|
+
"enabled": True,
|
|
1571
|
+
"error": str(e),
|
|
1572
|
+
"message": "Error getting Macie findings",
|
|
1573
|
+
"findings": [],
|
|
1574
|
+
}
|
|
1575
|
+
|
|
1576
|
+
|
|
1577
|
+
def _summarize_macie_findings(findings: List[Dict]) -> Dict:
|
|
1578
|
+
"""Generate a summary of Macie findings.
|
|
1579
|
+
|
|
1580
|
+
Args:
|
|
1581
|
+
findings: List of Macie finding dictionaries
|
|
1582
|
+
|
|
1583
|
+
Returns:
|
|
1584
|
+
Dictionary with summary information
|
|
1585
|
+
"""
|
|
1586
|
+
summary = {
|
|
1587
|
+
"total_count": len(findings),
|
|
1588
|
+
"severity_counts": {"high": 0, "medium": 0, "low": 0},
|
|
1589
|
+
"type_counts": {},
|
|
1590
|
+
"bucket_counts": {},
|
|
1591
|
+
}
|
|
1592
|
+
|
|
1593
|
+
for finding in findings:
|
|
1594
|
+
# Count by severity
|
|
1595
|
+
severity = finding.get("severity", {}).get("score", 0)
|
|
1596
|
+
if severity >= 7:
|
|
1597
|
+
summary["severity_counts"]["high"] += 1
|
|
1598
|
+
elif severity >= 4:
|
|
1599
|
+
summary["severity_counts"]["medium"] += 1
|
|
1600
|
+
else:
|
|
1601
|
+
summary["severity_counts"]["low"] += 1
|
|
1602
|
+
|
|
1603
|
+
# Count by finding type
|
|
1604
|
+
finding_type = finding.get("type", "unknown")
|
|
1605
|
+
if finding_type in summary["type_counts"]:
|
|
1606
|
+
summary["type_counts"][finding_type] += 1
|
|
1607
|
+
else:
|
|
1608
|
+
summary["type_counts"][finding_type] = 1
|
|
1609
|
+
|
|
1610
|
+
# Count by S3 bucket
|
|
1611
|
+
resource = finding.get("resourcesAffected", {}).get("s3Bucket", {})
|
|
1612
|
+
bucket_name = resource.get("name", "unknown")
|
|
1613
|
+
if bucket_name in summary["bucket_counts"]:
|
|
1614
|
+
summary["bucket_counts"][bucket_name] += 1
|
|
1615
|
+
else:
|
|
1616
|
+
summary["bucket_counts"][bucket_name] = 1
|
|
1617
|
+
|
|
1618
|
+
return summary
|