mcpcap 0.4.7__py3-none-any.whl → 0.5.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mcpcap/_version.py +2 -2
- mcpcap/cli.py +3 -16
- mcpcap/core/config.py +2 -203
- mcpcap/core/server.py +11 -12
- mcpcap/modules/base.py +106 -9
- mcpcap/modules/dhcp.py +6 -117
- mcpcap/modules/dns.py +6 -116
- mcpcap/modules/icmp.py +327 -0
- mcpcap-0.5.2.dist-info/METADATA +371 -0
- mcpcap-0.5.2.dist-info/RECORD +17 -0
- mcpcap-0.4.7.dist-info/METADATA +0 -345
- mcpcap-0.4.7.dist-info/RECORD +0 -16
- {mcpcap-0.4.7.dist-info → mcpcap-0.5.2.dist-info}/WHEEL +0 -0
- {mcpcap-0.4.7.dist-info → mcpcap-0.5.2.dist-info}/entry_points.txt +0 -0
- {mcpcap-0.4.7.dist-info → mcpcap-0.5.2.dist-info}/licenses/LICENSE +0 -0
- {mcpcap-0.4.7.dist-info → mcpcap-0.5.2.dist-info}/top_level.txt +0 -0
mcpcap/modules/dns.py
CHANGED
@@ -1,7 +1,5 @@
|
|
1
1
|
"""DNS analysis module."""
|
2
2
|
|
3
|
-
import os
|
4
|
-
import tempfile
|
5
3
|
from datetime import datetime
|
6
4
|
from typing import Any
|
7
5
|
|
@@ -19,128 +17,20 @@ class DNSModule(BaseModule):
|
|
19
17
|
"""Return the name of the protocol this module analyzes."""
|
20
18
|
return "DNS"
|
21
19
|
|
22
|
-
def
|
20
|
+
def analyze_dns_packets(self, pcap_file: str) -> dict[str, Any]:
|
23
21
|
"""
|
24
|
-
Analyze DNS packets from a PCAP file and return
|
22
|
+
Analyze DNS packets from a PCAP file and return comprehensive analysis results.
|
25
23
|
|
26
24
|
Args:
|
27
|
-
pcap_file: Path to
|
28
|
-
or when using the first available file in local directories.
|
25
|
+
pcap_file: Path to local PCAP file or HTTP URL to remote PCAP file
|
29
26
|
|
30
27
|
Returns:
|
31
28
|
A structured dictionary containing DNS packet analysis results
|
32
29
|
"""
|
33
|
-
|
34
|
-
if self.config.is_remote:
|
35
|
-
# For direct file URLs, always use the URL file (ignore pcap_file parameter)
|
36
|
-
if self.config.is_direct_file_url:
|
37
|
-
available_files = self.config.list_pcap_files()
|
38
|
-
if not available_files:
|
39
|
-
return {
|
40
|
-
"error": "No PCAP file found at the provided URL",
|
41
|
-
"pcap_url": self.config.pcap_url,
|
42
|
-
}
|
43
|
-
pcap_file = available_files[0] # Use the actual filename from URL
|
44
|
-
elif not pcap_file:
|
45
|
-
# For directory URLs, if no file specified, use the first available
|
46
|
-
available_files = self.config.list_pcap_files()
|
47
|
-
if not available_files:
|
48
|
-
return {
|
49
|
-
"error": "No PCAP files found at the provided URL",
|
50
|
-
"pcap_url": self.config.pcap_url,
|
51
|
-
"available_files": [],
|
52
|
-
}
|
53
|
-
pcap_file = available_files[0]
|
54
|
-
|
55
|
-
# Download remote file to temporary location
|
56
|
-
try:
|
57
|
-
with tempfile.NamedTemporaryFile(
|
58
|
-
suffix=".pcap", delete=False
|
59
|
-
) as tmp_file:
|
60
|
-
temp_path = tmp_file.name
|
61
|
-
|
62
|
-
local_path = self.config.download_pcap_file(pcap_file, temp_path)
|
63
|
-
result = self.analyze_packets(local_path)
|
64
|
-
|
65
|
-
# Clean up temporary file
|
66
|
-
try:
|
67
|
-
os.unlink(local_path)
|
68
|
-
except OSError:
|
69
|
-
pass # Ignore cleanup errors
|
70
|
-
|
71
|
-
return result
|
30
|
+
return self.analyze_packets(pcap_file)
|
72
31
|
|
73
|
-
|
74
|
-
|
75
|
-
available_files = self.config.list_pcap_files()
|
76
|
-
return {
|
77
|
-
"error": f"Failed to download PCAP file '{pcap_file}': {str(e)}",
|
78
|
-
"available_files": available_files,
|
79
|
-
"pcap_source": self.config.pcap_url,
|
80
|
-
}
|
81
|
-
else:
|
82
|
-
# Local file handling
|
83
|
-
if not pcap_file:
|
84
|
-
# If no file specified, use the first available local file
|
85
|
-
available_files = self.config.list_pcap_files()
|
86
|
-
if not available_files:
|
87
|
-
return {
|
88
|
-
"error": "No PCAP files found in directory",
|
89
|
-
"pcap_directory": self.config.pcap_path,
|
90
|
-
"available_files": [],
|
91
|
-
}
|
92
|
-
pcap_file = available_files[0]
|
93
|
-
|
94
|
-
full_path = self.config.get_pcap_file_path(pcap_file)
|
95
|
-
|
96
|
-
# Check if local file exists
|
97
|
-
if not os.path.exists(full_path):
|
98
|
-
# List available PCAP files for help
|
99
|
-
available_files = self.config.list_pcap_files()
|
100
|
-
return {
|
101
|
-
"error": f"PCAP file '{pcap_file}' not found",
|
102
|
-
"available_files": available_files,
|
103
|
-
"pcap_directory": self.config.pcap_path,
|
104
|
-
}
|
105
|
-
|
106
|
-
return self.analyze_packets(full_path)
|
107
|
-
|
108
|
-
def list_pcap_files(self) -> str:
|
109
|
-
"""
|
110
|
-
List all available PCAP files in the configured directory or remote URL.
|
111
|
-
|
112
|
-
Returns:
|
113
|
-
A list of available PCAP files that can be analyzed
|
114
|
-
"""
|
115
|
-
files = self.config.list_pcap_files()
|
116
|
-
source = (
|
117
|
-
self.config.pcap_url if self.config.is_remote else self.config.pcap_path
|
118
|
-
)
|
119
|
-
|
120
|
-
if files:
|
121
|
-
if self.config.is_remote and self.config.is_direct_file_url:
|
122
|
-
return f"Direct PCAP file URL: {source}\\n- {files[0]}"
|
123
|
-
elif not self.config.is_remote and self.config.is_direct_file_path:
|
124
|
-
return f"Direct PCAP file path: {source}\\n- {files[0]}"
|
125
|
-
else:
|
126
|
-
source_type = "remote server" if self.config.is_remote else "directory"
|
127
|
-
return (
|
128
|
-
f"Available PCAP files in {source_type} {source}:\\n"
|
129
|
-
+ "\\n".join(f"- {f}" for f in sorted(files))
|
130
|
-
)
|
131
|
-
else:
|
132
|
-
source_type = "remote server" if self.config.is_remote else "directory"
|
133
|
-
return f"No PCAP files found in {source_type} {source}"
|
134
|
-
|
135
|
-
def analyze_packets(self, pcap_file: str) -> dict[str, Any]:
|
136
|
-
"""Analyze DNS packets in a PCAP file.
|
137
|
-
|
138
|
-
Args:
|
139
|
-
pcap_file: Full path to the PCAP file
|
140
|
-
|
141
|
-
Returns:
|
142
|
-
Analysis results as a dictionary
|
143
|
-
"""
|
32
|
+
def _analyze_protocol_file(self, pcap_file: str) -> dict[str, Any]:
|
33
|
+
"""Perform the actual DNS packet analysis on a local PCAP file."""
|
144
34
|
try:
|
145
35
|
packets = rdpcap(pcap_file)
|
146
36
|
dns_packets = [pkt for pkt in packets if pkt.haslayer(DNS)]
|
mcpcap/modules/icmp.py
ADDED
@@ -0,0 +1,327 @@
|
|
1
|
+
"""ICMP analysis module."""
|
2
|
+
|
3
|
+
from datetime import datetime
|
4
|
+
from typing import Any
|
5
|
+
|
6
|
+
from fastmcp import FastMCP
|
7
|
+
from scapy.all import ICMP, IP, IPv6, rdpcap
|
8
|
+
|
9
|
+
from .base import BaseModule
|
10
|
+
|
11
|
+
|
12
|
+
class ICMPModule(BaseModule):
|
13
|
+
"""Module for analyzing ICMP packets in PCAP files."""
|
14
|
+
|
15
|
+
@property
|
16
|
+
def protocol_name(self) -> str:
|
17
|
+
"""Return the name of the protocol this module analyzes."""
|
18
|
+
return "ICMP"
|
19
|
+
|
20
|
+
def analyze_icmp_packets(self, pcap_file: str) -> dict[str, Any]:
|
21
|
+
"""
|
22
|
+
Analyze ICMP packets from a PCAP file and return comprehensive analysis results.
|
23
|
+
|
24
|
+
Args:
|
25
|
+
pcap_file: Path to local PCAP file or HTTP URL to remote PCAP file
|
26
|
+
|
27
|
+
Returns:
|
28
|
+
A structured dictionary containing ICMP packet analysis results
|
29
|
+
"""
|
30
|
+
return self.analyze_packets(pcap_file)
|
31
|
+
|
32
|
+
def _analyze_protocol_file(self, pcap_file: str) -> dict[str, Any]:
|
33
|
+
"""Perform the actual ICMP packet analysis on a local PCAP file."""
|
34
|
+
try:
|
35
|
+
packets = rdpcap(pcap_file)
|
36
|
+
icmp_packets = [pkt for pkt in packets if pkt.haslayer(ICMP)]
|
37
|
+
|
38
|
+
if not icmp_packets:
|
39
|
+
return {
|
40
|
+
"file": pcap_file,
|
41
|
+
"total_packets": len(packets),
|
42
|
+
"icmp_packets_found": 0,
|
43
|
+
"message": "No ICMP packets found in this capture",
|
44
|
+
}
|
45
|
+
|
46
|
+
# Apply max_packets limit if specified
|
47
|
+
packets_to_analyze = icmp_packets
|
48
|
+
limited = False
|
49
|
+
if self.config.max_packets and len(icmp_packets) > self.config.max_packets:
|
50
|
+
packets_to_analyze = icmp_packets[: self.config.max_packets]
|
51
|
+
limited = True
|
52
|
+
|
53
|
+
packet_details = []
|
54
|
+
for i, pkt in enumerate(packets_to_analyze, 1):
|
55
|
+
packet_info = self._analyze_icmp_packet(pkt, i)
|
56
|
+
packet_details.append(packet_info)
|
57
|
+
|
58
|
+
# Generate statistics
|
59
|
+
stats = self._generate_statistics(packet_details)
|
60
|
+
|
61
|
+
result = {
|
62
|
+
"file": pcap_file,
|
63
|
+
"analysis_timestamp": datetime.now().isoformat(),
|
64
|
+
"total_packets": len(packets),
|
65
|
+
"icmp_packets_found": len(icmp_packets),
|
66
|
+
"icmp_packets_analyzed": len(packet_details),
|
67
|
+
"statistics": stats,
|
68
|
+
"packets": packet_details,
|
69
|
+
}
|
70
|
+
|
71
|
+
if limited:
|
72
|
+
result["note"] = (
|
73
|
+
f"Analysis limited to first {self.config.max_packets} ICMP packets due to --max-packets setting"
|
74
|
+
)
|
75
|
+
|
76
|
+
return result
|
77
|
+
|
78
|
+
except Exception as e:
|
79
|
+
return {
|
80
|
+
"error": f"Error reading PCAP file '{pcap_file}': {str(e)}",
|
81
|
+
"file": pcap_file,
|
82
|
+
}
|
83
|
+
|
84
|
+
def _analyze_icmp_packet(self, packet, packet_num: int) -> dict[str, Any]:
|
85
|
+
"""Analyze a single ICMP packet and extract relevant information."""
|
86
|
+
info = {
|
87
|
+
"packet_number": packet_num,
|
88
|
+
"timestamp": datetime.fromtimestamp(float(packet.time)).isoformat(),
|
89
|
+
}
|
90
|
+
|
91
|
+
# Basic IP information
|
92
|
+
if packet.haslayer(IP):
|
93
|
+
ip_layer = packet[IP]
|
94
|
+
info.update(
|
95
|
+
{
|
96
|
+
"src_ip": ip_layer.src,
|
97
|
+
"dst_ip": ip_layer.dst,
|
98
|
+
"ip_version": 4,
|
99
|
+
"ttl": ip_layer.ttl,
|
100
|
+
"packet_size": len(packet),
|
101
|
+
}
|
102
|
+
)
|
103
|
+
elif packet.haslayer(IPv6):
|
104
|
+
ipv6_layer = packet[IPv6]
|
105
|
+
info.update(
|
106
|
+
{
|
107
|
+
"src_ip": ipv6_layer.src,
|
108
|
+
"dst_ip": ipv6_layer.dst,
|
109
|
+
"ip_version": 6,
|
110
|
+
"hop_limit": ipv6_layer.hlim,
|
111
|
+
"packet_size": len(packet),
|
112
|
+
}
|
113
|
+
)
|
114
|
+
|
115
|
+
# ICMP information
|
116
|
+
if packet.haslayer(ICMP):
|
117
|
+
icmp_layer = packet[ICMP]
|
118
|
+
|
119
|
+
# Map ICMP types to human-readable names
|
120
|
+
icmp_types = {
|
121
|
+
0: "Echo Reply",
|
122
|
+
3: "Destination Unreachable",
|
123
|
+
4: "Source Quench",
|
124
|
+
5: "Redirect",
|
125
|
+
8: "Echo Request",
|
126
|
+
11: "Time Exceeded",
|
127
|
+
12: "Parameter Problem",
|
128
|
+
13: "Timestamp Request",
|
129
|
+
14: "Timestamp Reply",
|
130
|
+
15: "Information Request",
|
131
|
+
16: "Information Reply",
|
132
|
+
}
|
133
|
+
|
134
|
+
# Map destination unreachable codes
|
135
|
+
dest_unreach_codes = {
|
136
|
+
0: "Network Unreachable",
|
137
|
+
1: "Host Unreachable",
|
138
|
+
2: "Protocol Unreachable",
|
139
|
+
3: "Port Unreachable",
|
140
|
+
4: "Fragmentation Required",
|
141
|
+
5: "Source Route Failed",
|
142
|
+
}
|
143
|
+
|
144
|
+
# Map time exceeded codes
|
145
|
+
time_exceeded_codes = {
|
146
|
+
0: "TTL Exceeded in Transit",
|
147
|
+
1: "Fragment Reassembly Time Exceeded",
|
148
|
+
}
|
149
|
+
|
150
|
+
icmp_type = icmp_layer.type
|
151
|
+
icmp_code = icmp_layer.code
|
152
|
+
|
153
|
+
info.update(
|
154
|
+
{
|
155
|
+
"icmp_type": icmp_type,
|
156
|
+
"icmp_code": icmp_code,
|
157
|
+
"icmp_type_name": icmp_types.get(
|
158
|
+
icmp_type, f"Unknown Type ({icmp_type})"
|
159
|
+
),
|
160
|
+
"icmp_id": getattr(icmp_layer, "id", None),
|
161
|
+
"icmp_seq": getattr(icmp_layer, "seq", None),
|
162
|
+
"checksum": icmp_layer.chksum,
|
163
|
+
}
|
164
|
+
)
|
165
|
+
|
166
|
+
# Add code descriptions for specific types
|
167
|
+
if icmp_type == 3: # Destination Unreachable
|
168
|
+
info["icmp_code_name"] = dest_unreach_codes.get(
|
169
|
+
icmp_code, f"Unknown Code ({icmp_code})"
|
170
|
+
)
|
171
|
+
elif icmp_type == 11: # Time Exceeded
|
172
|
+
info["icmp_code_name"] = time_exceeded_codes.get(
|
173
|
+
icmp_code, f"Unknown Code ({icmp_code})"
|
174
|
+
)
|
175
|
+
else:
|
176
|
+
info["icmp_code_name"] = f"Code {icmp_code}"
|
177
|
+
|
178
|
+
return info
|
179
|
+
|
180
|
+
def _generate_statistics(self, packets: list[dict[str, Any]]) -> dict[str, Any]:
|
181
|
+
"""Generate statistics from analyzed ICMP packets."""
|
182
|
+
stats = {
|
183
|
+
"icmp_types": {},
|
184
|
+
"unique_sources": set(),
|
185
|
+
"unique_destinations": set(),
|
186
|
+
"echo_pairs": {},
|
187
|
+
"unreachable_destinations": set(),
|
188
|
+
}
|
189
|
+
|
190
|
+
for pkt in packets:
|
191
|
+
# Count ICMP types
|
192
|
+
if "icmp_type_name" in pkt:
|
193
|
+
type_name = pkt["icmp_type_name"]
|
194
|
+
stats["icmp_types"][type_name] = (
|
195
|
+
stats["icmp_types"].get(type_name, 0) + 1
|
196
|
+
)
|
197
|
+
|
198
|
+
# Track unique IPs
|
199
|
+
if "src_ip" in pkt:
|
200
|
+
stats["unique_sources"].add(pkt["src_ip"])
|
201
|
+
if "dst_ip" in pkt:
|
202
|
+
stats["unique_destinations"].add(pkt["dst_ip"])
|
203
|
+
|
204
|
+
# Track echo request/reply pairs
|
205
|
+
if pkt.get("icmp_type") in [0, 8] and pkt.get("icmp_id") is not None:
|
206
|
+
echo_id = pkt["icmp_id"]
|
207
|
+
if echo_id not in stats["echo_pairs"]:
|
208
|
+
stats["echo_pairs"][echo_id] = {"requests": 0, "replies": 0}
|
209
|
+
|
210
|
+
if pkt["icmp_type"] == 8: # Echo Request
|
211
|
+
stats["echo_pairs"][echo_id]["requests"] += 1
|
212
|
+
elif pkt["icmp_type"] == 0: # Echo Reply
|
213
|
+
stats["echo_pairs"][echo_id]["replies"] += 1
|
214
|
+
|
215
|
+
# Track unreachable destinations
|
216
|
+
if pkt.get("icmp_type") == 3: # Destination Unreachable
|
217
|
+
if "dst_ip" in pkt:
|
218
|
+
stats["unreachable_destinations"].add(pkt["dst_ip"])
|
219
|
+
|
220
|
+
# Convert sets to lists for JSON serialization
|
221
|
+
return {
|
222
|
+
"icmp_type_counts": stats["icmp_types"],
|
223
|
+
"unique_sources_count": len(stats["unique_sources"]),
|
224
|
+
"unique_destinations_count": len(stats["unique_destinations"]),
|
225
|
+
"unique_sources": list(stats["unique_sources"]),
|
226
|
+
"unique_destinations": list(stats["unique_destinations"]),
|
227
|
+
"echo_sessions": len(stats["echo_pairs"]),
|
228
|
+
"echo_pairs": stats["echo_pairs"],
|
229
|
+
"unreachable_destinations_count": len(stats["unreachable_destinations"]),
|
230
|
+
"unreachable_destinations": list(stats["unreachable_destinations"]),
|
231
|
+
}
|
232
|
+
|
233
|
+
def setup_prompts(self, mcp: FastMCP) -> None:
|
234
|
+
"""Set up ICMP-specific analysis prompts for the MCP server."""
|
235
|
+
|
236
|
+
@mcp.prompt
|
237
|
+
def icmp_network_diagnostics():
|
238
|
+
"""Prompt for analyzing ICMP traffic from a network diagnostics perspective"""
|
239
|
+
return """You are a network engineer analyzing ICMP traffic for network diagnostics. Focus on:
|
240
|
+
|
241
|
+
1. **Connectivity Testing:**
|
242
|
+
- Analyze ping (echo request/reply) patterns and success rates
|
243
|
+
- Identify network reachability issues from failed pings
|
244
|
+
- Check ping response times and latency patterns
|
245
|
+
- Look for asymmetric routing issues
|
246
|
+
|
247
|
+
2. **Network Path Analysis:**
|
248
|
+
- Examine TTL values and time exceeded messages for traceroute data
|
249
|
+
- Identify network hops and routing paths
|
250
|
+
- Look for routing loops or suboptimal paths
|
251
|
+
- Check for fragmentation issues
|
252
|
+
|
253
|
+
3. **Error Diagnostics:**
|
254
|
+
- Analyze destination unreachable messages and their causes
|
255
|
+
- Identify network, host, protocol, or port unreachability
|
256
|
+
- Look for fragmentation required messages
|
257
|
+
- Check source quench messages for congestion
|
258
|
+
|
259
|
+
4. **Network Health Assessment:**
|
260
|
+
- Monitor ICMP message frequencies and patterns
|
261
|
+
- Identify potential network congestion indicators
|
262
|
+
- Look for unusual ICMP traffic that might indicate problems
|
263
|
+
- Assess overall network responsiveness
|
264
|
+
|
265
|
+
Provide specific recommendations for network troubleshooting and optimization."""
|
266
|
+
|
267
|
+
@mcp.prompt
|
268
|
+
def icmp_security_analysis():
|
269
|
+
"""Prompt for analyzing ICMP traffic from a security perspective"""
|
270
|
+
return """You are a security analyst examining ICMP traffic for threats and anomalies. Focus on:
|
271
|
+
|
272
|
+
1. **Reconnaissance Detection:**
|
273
|
+
- Identify ping sweeps and network scanning activities
|
274
|
+
- Look for systematic probing of network ranges
|
275
|
+
- Check for unusual ping patterns that might indicate reconnaissance
|
276
|
+
- Monitor for traceroute-based network mapping
|
277
|
+
|
278
|
+
2. **Covert Channel Analysis:**
|
279
|
+
- Examine ICMP payloads for potential data exfiltration
|
280
|
+
- Look for unusual ICMP packet sizes or timing patterns
|
281
|
+
- Check for non-standard ICMP types or codes
|
282
|
+
- Identify potential ICMP tunneling activities
|
283
|
+
|
284
|
+
3. **DoS Attack Detection:**
|
285
|
+
- Monitor for ICMP flood attacks (ping floods)
|
286
|
+
- Look for smurf attack patterns (broadcast ping amplification)
|
287
|
+
- Check for fragmentation attacks using ICMP
|
288
|
+
- Identify potential death of death or similar attacks
|
289
|
+
|
290
|
+
4. **Policy Compliance:**
|
291
|
+
- Verify ICMP traffic matches network security policies
|
292
|
+
- Check for unauthorized ICMP types (if policy restricts certain types)
|
293
|
+
- Monitor for ICMP traffic from unexpected sources
|
294
|
+
- Identify potential firewall bypass attempts
|
295
|
+
|
296
|
+
Provide threat assessment and recommended security controls."""
|
297
|
+
|
298
|
+
@mcp.prompt
|
299
|
+
def icmp_forensic_investigation():
|
300
|
+
"""Prompt for forensic analysis of ICMP traffic"""
|
301
|
+
return """You are conducting a digital forensics investigation involving ICMP traffic. Approach systematically:
|
302
|
+
|
303
|
+
1. **Timeline Reconstruction:**
|
304
|
+
- Create chronological sequence of ICMP events
|
305
|
+
- Map ping activities to potential user or system actions
|
306
|
+
- Correlate ICMP traffic with incident timeframes
|
307
|
+
- Track network connectivity patterns over time
|
308
|
+
|
309
|
+
2. **Attribution and Tracking:**
|
310
|
+
- Trace ICMP traffic to source systems and networks
|
311
|
+
- Identify systems involved in ping exchanges
|
312
|
+
- Map network topology from traceroute data
|
313
|
+
- Document unique identifiers in ICMP packets
|
314
|
+
|
315
|
+
3. **Evidence Collection:**
|
316
|
+
- Preserve all ICMP packet details with precise timestamps
|
317
|
+
- Document network paths and hop information
|
318
|
+
- Record error messages and their contexts
|
319
|
+
- Note any unusual or suspicious ICMP characteristics
|
320
|
+
|
321
|
+
4. **Impact Assessment:**
|
322
|
+
- Determine scope of network reconnaissance or scanning
|
323
|
+
- Assess potential information leakage through ICMP
|
324
|
+
- Identify systems that may have been probed
|
325
|
+
- Evaluate ongoing security implications
|
326
|
+
|
327
|
+
Present findings with precise timestamps, evidence preservation notes, and clear documentation suitable for legal proceedings."""
|