mcpcap 0.4.7__py3-none-any.whl → 0.5.0__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.0.dist-info/METADATA +371 -0
- mcpcap-0.5.0.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.0.dist-info}/WHEEL +0 -0
- {mcpcap-0.4.7.dist-info → mcpcap-0.5.0.dist-info}/entry_points.txt +0 -0
- {mcpcap-0.4.7.dist-info → mcpcap-0.5.0.dist-info}/licenses/LICENSE +0 -0
- {mcpcap-0.4.7.dist-info → mcpcap-0.5.0.dist-info}/top_level.txt +0 -0
mcpcap/_version.py
CHANGED
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
|
|
28
28
|
commit_id: COMMIT_ID
|
29
29
|
__commit_id__: COMMIT_ID
|
30
30
|
|
31
|
-
__version__ = version = '0.
|
32
|
-
__version_tuple__ = version_tuple = (0,
|
31
|
+
__version__ = version = '0.5.0'
|
32
|
+
__version_tuple__ = version_tuple = (0, 5, 0)
|
33
33
|
|
34
34
|
__commit_id__ = commit_id = None
|
mcpcap/cli.py
CHANGED
@@ -20,27 +20,16 @@ def main():
|
|
20
20
|
int: Exit code (0 for success, 1 for error)
|
21
21
|
|
22
22
|
Raises:
|
23
|
-
ValueError: If the provided PCAP path is invalid
|
24
23
|
KeyboardInterrupt: If the user interrupts the server
|
25
24
|
Exception: For any unexpected errors during server operation
|
26
25
|
"""
|
27
26
|
parser = argparse.ArgumentParser(description="mcpcap MCP Server")
|
28
27
|
|
29
|
-
# PCAP source options (mutually exclusive)
|
30
|
-
source_group = parser.add_mutually_exclusive_group(required=True)
|
31
|
-
source_group.add_argument(
|
32
|
-
"--pcap-path", help="Path to PCAP file or directory containing PCAP files"
|
33
|
-
)
|
34
|
-
source_group.add_argument(
|
35
|
-
"--pcap-url",
|
36
|
-
help="HTTP URL to PCAP file (direct link) or directory containing PCAP files",
|
37
|
-
)
|
38
|
-
|
39
28
|
# Analysis options
|
40
29
|
parser.add_argument(
|
41
30
|
"--modules",
|
42
|
-
help="Comma-separated list of modules to load (default: dns)",
|
43
|
-
default="dns",
|
31
|
+
help="Comma-separated list of modules to load (default: dns,dhcp,icmp)",
|
32
|
+
default="dns,dhcp,icmp",
|
44
33
|
)
|
45
34
|
parser.add_argument(
|
46
35
|
"--max-packets",
|
@@ -52,13 +41,11 @@ def main():
|
|
52
41
|
|
53
42
|
try:
|
54
43
|
# Parse modules and automatically set protocols to match
|
55
|
-
modules = args.modules.split(",") if args.modules else ["dns"]
|
44
|
+
modules = args.modules.split(",") if args.modules else ["dns", "dhcp", "icmp"]
|
56
45
|
protocols = modules # Protocols automatically match loaded modules
|
57
46
|
|
58
47
|
# Initialize configuration
|
59
48
|
config = Config(
|
60
|
-
pcap_path=args.pcap_path,
|
61
|
-
pcap_url=args.pcap_url,
|
62
49
|
modules=modules,
|
63
50
|
protocols=protocols,
|
64
51
|
max_packets=args.max_packets,
|
mcpcap/core/config.py
CHANGED
@@ -1,18 +1,11 @@
|
|
1
1
|
"""Configuration management for mcpcap."""
|
2
2
|
|
3
|
-
import os
|
4
|
-
from urllib.parse import urljoin, urlparse
|
5
|
-
|
6
|
-
import requests
|
7
|
-
|
8
3
|
|
9
4
|
class Config:
|
10
5
|
"""Configuration management for mcpcap server."""
|
11
6
|
|
12
7
|
def __init__(
|
13
8
|
self,
|
14
|
-
pcap_path: str | None = None,
|
15
|
-
pcap_url: str | None = None,
|
16
9
|
modules: list[str] | None = None,
|
17
10
|
protocols: list[str] | None = None,
|
18
11
|
max_packets: int | None = None,
|
@@ -20,211 +13,17 @@ class Config:
|
|
20
13
|
"""Initialize configuration.
|
21
14
|
|
22
15
|
Args:
|
23
|
-
pcap_path: Path to directory containing PCAP files
|
24
|
-
pcap_url: HTTP server URL containing PCAP files
|
25
16
|
modules: List of modules to load
|
26
17
|
protocols: List of protocols to analyze
|
27
18
|
max_packets: Maximum number of packets to analyze per file
|
28
19
|
"""
|
29
|
-
self.
|
30
|
-
self.
|
31
|
-
self.modules = modules or ["dns"]
|
32
|
-
self.protocols = protocols or ["dns"]
|
20
|
+
self.modules = modules or ["dns", "dhcp"]
|
21
|
+
self.protocols = protocols or ["dns", "dhcp"]
|
33
22
|
self.max_packets = max_packets
|
34
|
-
self.is_remote = pcap_url is not None
|
35
|
-
self.is_direct_file_url = False # Will be set during validation
|
36
|
-
self.is_direct_file_path = (
|
37
|
-
False # Will be set during validation for local files
|
38
|
-
)
|
39
23
|
|
40
24
|
self._validate_configuration()
|
41
25
|
|
42
26
|
def _validate_configuration(self) -> None:
|
43
27
|
"""Validate the configuration parameters."""
|
44
|
-
if not self.pcap_path and not self.pcap_url:
|
45
|
-
raise ValueError("Either --pcap-path or --pcap-url must be specified")
|
46
|
-
|
47
|
-
if self.pcap_path and self.pcap_url:
|
48
|
-
raise ValueError("Cannot specify both --pcap-path and --pcap-url")
|
49
|
-
|
50
|
-
if self.pcap_path:
|
51
|
-
self._validate_pcap_path()
|
52
|
-
|
53
|
-
if self.pcap_url:
|
54
|
-
self._validate_pcap_url()
|
55
|
-
|
56
28
|
if self.max_packets is not None and self.max_packets <= 0:
|
57
29
|
raise ValueError("max_packets must be a positive integer")
|
58
|
-
|
59
|
-
def _validate_pcap_path(self) -> None:
|
60
|
-
"""Validate that the PCAP path exists and is either a directory or a PCAP file."""
|
61
|
-
if not os.path.exists(self.pcap_path):
|
62
|
-
raise ValueError(f"PCAP path '{self.pcap_path}' does not exist")
|
63
|
-
|
64
|
-
if os.path.isfile(self.pcap_path):
|
65
|
-
# Check if it's a PCAP file
|
66
|
-
if not self.pcap_path.lower().endswith((".pcap", ".pcapng", ".cap")):
|
67
|
-
raise ValueError(
|
68
|
-
f"File '{self.pcap_path}' is not a supported PCAP file (.pcap/.pcapng/.cap)"
|
69
|
-
)
|
70
|
-
self.is_direct_file_path = True
|
71
|
-
elif os.path.isdir(self.pcap_path):
|
72
|
-
self.is_direct_file_path = False
|
73
|
-
else:
|
74
|
-
raise ValueError(f"'{self.pcap_path}' is neither a file nor a directory")
|
75
|
-
|
76
|
-
def _validate_pcap_url(self) -> None:
|
77
|
-
"""Validate that the PCAP URL is accessible."""
|
78
|
-
try:
|
79
|
-
parsed = urlparse(self.pcap_url)
|
80
|
-
if not parsed.scheme or not parsed.netloc:
|
81
|
-
raise ValueError(f"Invalid URL format: {self.pcap_url}")
|
82
|
-
|
83
|
-
# Determine if this is a direct file URL or directory URL
|
84
|
-
self.is_direct_file_url = self._is_direct_file_url()
|
85
|
-
|
86
|
-
# Test connectivity with a HEAD request
|
87
|
-
response = requests.head(self.pcap_url, timeout=10)
|
88
|
-
if response.status_code >= 400:
|
89
|
-
raise ValueError(
|
90
|
-
f"Cannot access PCAP URL: {self.pcap_url} (HTTP {response.status_code})"
|
91
|
-
)
|
92
|
-
|
93
|
-
except requests.RequestException as e:
|
94
|
-
raise ValueError(
|
95
|
-
f"Cannot connect to PCAP URL '{self.pcap_url}': {str(e)}"
|
96
|
-
) from e
|
97
|
-
|
98
|
-
def _is_direct_file_url(self) -> bool:
|
99
|
-
"""Determine if the URL points directly to a PCAP file."""
|
100
|
-
parsed = urlparse(self.pcap_url)
|
101
|
-
path = parsed.path.lower()
|
102
|
-
|
103
|
-
# Check if URL ends with a PCAP file extension
|
104
|
-
return (
|
105
|
-
path.endswith(".pcap") or path.endswith(".pcapng") or path.endswith(".cap")
|
106
|
-
)
|
107
|
-
|
108
|
-
def get_pcap_file_path(self, pcap_file: str) -> str:
|
109
|
-
"""Get full path or URL to a PCAP file.
|
110
|
-
|
111
|
-
Args:
|
112
|
-
pcap_file: Filename or relative path to PCAP file
|
113
|
-
|
114
|
-
Returns:
|
115
|
-
Full path or URL to the PCAP file
|
116
|
-
"""
|
117
|
-
if self.is_remote:
|
118
|
-
# If it's already a full URL, return as-is
|
119
|
-
if pcap_file.startswith("http"):
|
120
|
-
return pcap_file
|
121
|
-
|
122
|
-
# If this is a direct file URL, return the URL directly
|
123
|
-
if self.is_direct_file_url:
|
124
|
-
return self.pcap_url
|
125
|
-
|
126
|
-
# Otherwise, treat as directory and join with filename
|
127
|
-
return urljoin(self.pcap_url.rstrip("/") + "/", pcap_file)
|
128
|
-
else:
|
129
|
-
# Local file handling
|
130
|
-
if os.path.isabs(pcap_file):
|
131
|
-
return pcap_file
|
132
|
-
|
133
|
-
# If this is a direct file path, return it directly
|
134
|
-
if self.is_direct_file_path:
|
135
|
-
return self.pcap_path
|
136
|
-
|
137
|
-
# Otherwise, join with directory
|
138
|
-
return os.path.join(self.pcap_path, pcap_file)
|
139
|
-
|
140
|
-
def list_pcap_files(self) -> list[str]:
|
141
|
-
"""List all PCAP files in the configured directory or remote URL.
|
142
|
-
|
143
|
-
Returns:
|
144
|
-
List of PCAP filenames
|
145
|
-
"""
|
146
|
-
if self.is_remote:
|
147
|
-
return self._list_remote_pcap_files()
|
148
|
-
else:
|
149
|
-
if self.is_direct_file_path:
|
150
|
-
# Return just the filename from the direct file path
|
151
|
-
return [os.path.basename(self.pcap_path)]
|
152
|
-
else:
|
153
|
-
# List files in directory
|
154
|
-
try:
|
155
|
-
return [
|
156
|
-
f
|
157
|
-
for f in os.listdir(self.pcap_path)
|
158
|
-
if f.endswith((".pcap", ".pcapng", ".cap"))
|
159
|
-
]
|
160
|
-
except Exception:
|
161
|
-
return []
|
162
|
-
|
163
|
-
def _list_remote_pcap_files(self) -> list[str]:
|
164
|
-
"""List PCAP files from a remote HTTP server.
|
165
|
-
|
166
|
-
Returns:
|
167
|
-
List of PCAP filenames found on the remote server
|
168
|
-
"""
|
169
|
-
# If this is a direct file URL, return just that filename
|
170
|
-
if self.is_direct_file_url:
|
171
|
-
filename = os.path.basename(urlparse(self.pcap_url).path)
|
172
|
-
return [filename] if filename else []
|
173
|
-
|
174
|
-
# Otherwise try to parse directory listing
|
175
|
-
try:
|
176
|
-
response = requests.get(self.pcap_url, timeout=30)
|
177
|
-
response.raise_for_status()
|
178
|
-
|
179
|
-
# Parse HTML to find .pcap and .pcapng files
|
180
|
-
# This is a simple implementation that looks for href attributes
|
181
|
-
import re
|
182
|
-
|
183
|
-
pcap_files = []
|
184
|
-
|
185
|
-
# Look for links to .pcap, .pcapng, and .cap files
|
186
|
-
pattern = r'href=["\']([^"\']*\.(?:pcap|pcapng|cap))["\']'
|
187
|
-
matches = re.findall(pattern, response.text, re.IGNORECASE)
|
188
|
-
|
189
|
-
for match in matches:
|
190
|
-
# Extract just the filename, not the full path
|
191
|
-
filename = os.path.basename(match)
|
192
|
-
if filename and filename not in pcap_files:
|
193
|
-
pcap_files.append(filename)
|
194
|
-
|
195
|
-
return sorted(pcap_files)
|
196
|
-
|
197
|
-
except requests.RequestException:
|
198
|
-
return []
|
199
|
-
|
200
|
-
def download_pcap_file(self, pcap_file: str, local_path: str) -> str:
|
201
|
-
"""Download a remote PCAP file to local storage.
|
202
|
-
|
203
|
-
Args:
|
204
|
-
pcap_file: Name of the PCAP file to download
|
205
|
-
local_path: Local path to save the file
|
206
|
-
|
207
|
-
Returns:
|
208
|
-
Local path to the downloaded file
|
209
|
-
"""
|
210
|
-
if not self.is_remote:
|
211
|
-
raise ValueError("Cannot download file: not using remote source")
|
212
|
-
|
213
|
-
url = self.get_pcap_file_path(pcap_file)
|
214
|
-
|
215
|
-
try:
|
216
|
-
response = requests.get(url, timeout=60, stream=True)
|
217
|
-
response.raise_for_status()
|
218
|
-
|
219
|
-
os.makedirs(os.path.dirname(local_path), exist_ok=True)
|
220
|
-
|
221
|
-
with open(local_path, "wb") as f:
|
222
|
-
for chunk in response.iter_content(chunk_size=8192):
|
223
|
-
f.write(chunk)
|
224
|
-
|
225
|
-
return local_path
|
226
|
-
|
227
|
-
except requests.RequestException as e:
|
228
|
-
raise ValueError(
|
229
|
-
f"Failed to download PCAP file '{pcap_file}': {str(e)}"
|
230
|
-
) from e
|
mcpcap/core/server.py
CHANGED
@@ -4,6 +4,7 @@ from fastmcp import FastMCP
|
|
4
4
|
|
5
5
|
from ..modules.dhcp import DHCPModule
|
6
6
|
from ..modules.dns import DNSModule
|
7
|
+
from ..modules.icmp import ICMPModule
|
7
8
|
from .config import Config
|
8
9
|
|
9
10
|
|
@@ -25,6 +26,8 @@ class MCPServer:
|
|
25
26
|
self.modules["dns"] = DNSModule(config)
|
26
27
|
if "dhcp" in self.config.modules:
|
27
28
|
self.modules["dhcp"] = DHCPModule(config)
|
29
|
+
if "icmp" in self.config.modules:
|
30
|
+
self.modules["icmp"] = ICMPModule(config)
|
28
31
|
|
29
32
|
# Register tools
|
30
33
|
self._register_tools()
|
@@ -38,25 +41,21 @@ class MCPServer:
|
|
38
41
|
# Register tools for each loaded module
|
39
42
|
for module_name, module in self.modules.items():
|
40
43
|
if module_name == "dns":
|
41
|
-
self.mcp.tool(module.
|
44
|
+
self.mcp.tool(module.analyze_dns_packets)
|
42
45
|
elif module_name == "dhcp":
|
43
|
-
self.mcp.tool(module.
|
44
|
-
|
45
|
-
|
46
|
-
if self.modules:
|
47
|
-
# Use the first available module for listing PCAP files
|
48
|
-
first_module = next(iter(self.modules.values()))
|
49
|
-
self.mcp.tool(first_module.list_pcap_files)
|
46
|
+
self.mcp.tool(module.analyze_dhcp_packets)
|
47
|
+
elif module_name == "icmp":
|
48
|
+
self.mcp.tool(module.analyze_icmp_packets)
|
50
49
|
|
51
50
|
def run(self) -> None:
|
52
51
|
"""Start the MCP server."""
|
53
52
|
import sys
|
54
53
|
|
55
54
|
# Log to stderr to avoid breaking MCP JSON-RPC protocol
|
56
|
-
|
57
|
-
|
55
|
+
enabled_modules = ", ".join(self.config.modules)
|
56
|
+
print(
|
57
|
+
f"Starting mcpcap MCP server with modules: {enabled_modules}",
|
58
|
+
file=sys.stderr,
|
58
59
|
)
|
59
|
-
source_type = "remote URL" if self.config.is_remote else "directory"
|
60
|
-
print(f"Starting MCP server with PCAP {source_type}: {source}", file=sys.stderr)
|
61
60
|
|
62
61
|
self.mcp.run()
|
mcpcap/modules/base.py
CHANGED
@@ -1,5 +1,7 @@
|
|
1
1
|
"""Base module interface for protocol analyzers."""
|
2
2
|
|
3
|
+
import os
|
4
|
+
import tempfile
|
3
5
|
from abc import ABC, abstractmethod
|
4
6
|
from typing import Any
|
5
7
|
|
@@ -17,20 +19,115 @@ class BaseModule(ABC):
|
|
17
19
|
"""
|
18
20
|
self.config = config
|
19
21
|
|
22
|
+
@property
|
20
23
|
@abstractmethod
|
21
|
-
def
|
22
|
-
"""
|
24
|
+
def protocol_name(self) -> str:
|
25
|
+
"""Return the name of the protocol this module analyzes."""
|
26
|
+
pass
|
27
|
+
|
28
|
+
@abstractmethod
|
29
|
+
def _analyze_protocol_file(self, pcap_file: str) -> dict[str, Any]:
|
30
|
+
"""Analyze a local PCAP file for this protocol.
|
31
|
+
|
32
|
+
This method should be implemented by each module to perform
|
33
|
+
the actual protocol-specific analysis.
|
23
34
|
|
24
35
|
Args:
|
25
|
-
pcap_file: Path to
|
36
|
+
pcap_file: Path to local PCAP file
|
26
37
|
|
27
38
|
Returns:
|
28
|
-
Analysis results
|
39
|
+
Analysis results dictionary
|
29
40
|
"""
|
30
41
|
pass
|
31
42
|
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
43
|
+
def analyze_packets(self, pcap_file: str) -> dict[str, Any]:
|
44
|
+
"""Analyze packets from a PCAP file (local or remote).
|
45
|
+
|
46
|
+
Args:
|
47
|
+
pcap_file: Path to local PCAP file or HTTP URL to remote PCAP file
|
48
|
+
|
49
|
+
Returns:
|
50
|
+
A structured dictionary containing packet analysis results
|
51
|
+
"""
|
52
|
+
# Check if this is a remote URL or local file
|
53
|
+
if pcap_file.startswith(("http://", "https://")):
|
54
|
+
return self._handle_remote_analysis(pcap_file)
|
55
|
+
else:
|
56
|
+
return self._handle_local_analysis(pcap_file)
|
57
|
+
|
58
|
+
def _handle_remote_analysis(self, pcap_url: str) -> dict[str, Any]:
|
59
|
+
"""Handle remote PCAP file analysis."""
|
60
|
+
try:
|
61
|
+
# Download remote file to temporary location
|
62
|
+
with tempfile.NamedTemporaryFile(suffix=".pcap", delete=False) as tmp_file:
|
63
|
+
temp_path = tmp_file.name
|
64
|
+
|
65
|
+
local_path = self._download_pcap_file(pcap_url, temp_path)
|
66
|
+
result = self._analyze_protocol_file(local_path)
|
67
|
+
|
68
|
+
# Clean up temporary file
|
69
|
+
try:
|
70
|
+
os.unlink(local_path)
|
71
|
+
except OSError:
|
72
|
+
pass # Ignore cleanup errors
|
73
|
+
|
74
|
+
return result
|
75
|
+
|
76
|
+
except Exception as e:
|
77
|
+
return {
|
78
|
+
"error": f"Failed to download PCAP file '{pcap_url}': {str(e)}",
|
79
|
+
"pcap_url": pcap_url,
|
80
|
+
}
|
81
|
+
|
82
|
+
def _handle_local_analysis(self, pcap_file: str) -> dict[str, Any]:
|
83
|
+
"""Handle local PCAP file analysis."""
|
84
|
+
# Validate file exists
|
85
|
+
if not os.path.exists(pcap_file):
|
86
|
+
return {
|
87
|
+
"error": f"PCAP file not found: {pcap_file}",
|
88
|
+
"pcap_file": pcap_file,
|
89
|
+
}
|
90
|
+
|
91
|
+
# Validate file extension
|
92
|
+
if not pcap_file.lower().endswith((".pcap", ".pcapng", ".cap")):
|
93
|
+
return {
|
94
|
+
"error": f"File '{pcap_file}' is not a supported PCAP file (.pcap/.pcapng/.cap)",
|
95
|
+
"pcap_file": pcap_file,
|
96
|
+
}
|
97
|
+
|
98
|
+
try:
|
99
|
+
return self._analyze_protocol_file(pcap_file)
|
100
|
+
except Exception as e:
|
101
|
+
return {
|
102
|
+
"error": f"Failed to analyze PCAP file '{pcap_file}': {str(e)}",
|
103
|
+
"pcap_file": pcap_file,
|
104
|
+
}
|
105
|
+
|
106
|
+
def _download_pcap_file(self, pcap_url: str, local_path: str) -> str:
|
107
|
+
"""Download a remote PCAP file to local storage.
|
108
|
+
|
109
|
+
Args:
|
110
|
+
pcap_url: URL of the PCAP file to download
|
111
|
+
local_path: Local path to save the file
|
112
|
+
|
113
|
+
Returns:
|
114
|
+
Local path to the downloaded file
|
115
|
+
"""
|
116
|
+
import requests
|
117
|
+
|
118
|
+
try:
|
119
|
+
response = requests.get(pcap_url, timeout=60, stream=True)
|
120
|
+
response.raise_for_status()
|
121
|
+
|
122
|
+
os.makedirs(os.path.dirname(local_path), exist_ok=True)
|
123
|
+
|
124
|
+
with open(local_path, "wb") as f:
|
125
|
+
for chunk in response.iter_content(chunk_size=8192):
|
126
|
+
f.write(chunk)
|
127
|
+
|
128
|
+
return local_path
|
129
|
+
|
130
|
+
except requests.RequestException as e:
|
131
|
+
raise ValueError(
|
132
|
+
f"Failed to download PCAP file '{pcap_url}': {str(e)}"
|
133
|
+
) from e
|
mcpcap/modules/dhcp.py
CHANGED
@@ -1,7 +1,5 @@
|
|
1
1
|
"""DHCP analysis module."""
|
2
2
|
|
3
|
-
import os
|
4
|
-
import tempfile
|
5
3
|
from typing import Any
|
6
4
|
|
7
5
|
from fastmcp import FastMCP
|
@@ -18,129 +16,20 @@ class DHCPModule(BaseModule):
|
|
18
16
|
"""Return the name of the protocol this module analyzes."""
|
19
17
|
return "DHCP"
|
20
18
|
|
21
|
-
def
|
19
|
+
def analyze_dhcp_packets(self, pcap_file: str) -> dict[str, Any]:
|
22
20
|
"""
|
23
|
-
Analyze DHCP packets from a PCAP file and return
|
21
|
+
Analyze DHCP packets from a PCAP file and return comprehensive analysis results.
|
24
22
|
|
25
23
|
Args:
|
26
|
-
pcap_file: Path to
|
27
|
-
or when using the first available file in local directories.
|
24
|
+
pcap_file: Path to local PCAP file or HTTP URL to remote PCAP file
|
28
25
|
|
29
26
|
Returns:
|
30
27
|
A structured dictionary containing DHCP packet analysis results
|
31
28
|
"""
|
32
|
-
|
33
|
-
if self.config.is_remote:
|
34
|
-
# For direct file URLs, always use the URL file (ignore pcap_file parameter)
|
35
|
-
if self.config.is_direct_file_url:
|
36
|
-
available_files = self.config.list_pcap_files()
|
37
|
-
if not available_files:
|
38
|
-
return {
|
39
|
-
"error": "No PCAP file found at the provided URL",
|
40
|
-
"pcap_url": self.config.pcap_url,
|
41
|
-
}
|
42
|
-
pcap_file = available_files[0] # Use the actual filename from URL
|
43
|
-
elif not pcap_file:
|
44
|
-
# For directory URLs, if no file specified, use the first available
|
45
|
-
available_files = self.config.list_pcap_files()
|
46
|
-
if not available_files:
|
47
|
-
return {
|
48
|
-
"error": "No PCAP files found at the provided URL",
|
49
|
-
"pcap_url": self.config.pcap_url,
|
50
|
-
"available_files": [],
|
51
|
-
}
|
52
|
-
pcap_file = available_files[0]
|
53
|
-
|
54
|
-
# Download remote file to temporary location
|
55
|
-
try:
|
56
|
-
with tempfile.NamedTemporaryFile(
|
57
|
-
suffix=".pcap", delete=False
|
58
|
-
) as tmp_file:
|
59
|
-
temp_path = tmp_file.name
|
60
|
-
|
61
|
-
local_path = self.config.download_pcap_file(pcap_file, temp_path)
|
62
|
-
result = self.analyze_packets(local_path)
|
63
|
-
|
64
|
-
# Clean up temporary file
|
65
|
-
try:
|
66
|
-
os.unlink(local_path)
|
67
|
-
except OSError:
|
68
|
-
pass # Ignore cleanup errors
|
69
|
-
|
70
|
-
return result
|
71
|
-
|
72
|
-
except Exception as e:
|
73
|
-
# List available PCAP files for help
|
74
|
-
available_files = self.config.list_pcap_files()
|
75
|
-
return {
|
76
|
-
"error": f"Failed to download PCAP file '{pcap_file}': {str(e)}",
|
77
|
-
"pcap_url": self.config.pcap_url,
|
78
|
-
"available_files": available_files,
|
79
|
-
}
|
80
|
-
|
81
|
-
else:
|
82
|
-
# Handle local files
|
83
|
-
if not pcap_file:
|
84
|
-
# If no file specified, use the first available 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]
|
29
|
+
return self.analyze_packets(pcap_file)
|
93
30
|
|
94
|
-
|
95
|
-
|
96
|
-
# Check if local file exists
|
97
|
-
if not os.path.exists(full_path):
|
98
|
-
available_files = self.config.list_pcap_files()
|
99
|
-
return {
|
100
|
-
"error": f"PCAP file '{pcap_file}' not found",
|
101
|
-
"file_path": full_path,
|
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 DHCP packets in a PCAP file.
|
137
|
-
|
138
|
-
Args:
|
139
|
-
pcap_file: Path to the PCAP file to analyze
|
140
|
-
|
141
|
-
Returns:
|
142
|
-
Dictionary containing DHCP packet analysis results
|
143
|
-
"""
|
31
|
+
def _analyze_protocol_file(self, pcap_file: str) -> dict[str, Any]:
|
32
|
+
"""Perform the actual DHCP packet analysis on a local PCAP file."""
|
144
33
|
try:
|
145
34
|
packets = rdpcap(pcap_file)
|
146
35
|
dhcp_packets = [pkt for pkt in packets if pkt.haslayer(BOOTP)]
|