primo commit
This commit is contained in:
parent
fcefcedc86
commit
ddd7ff76d9
10
.gitignore
vendored
10
.gitignore
vendored
@ -27,10 +27,6 @@ share/python-wheels/
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a CI server in a temp folder.
|
||||
# Then everything is copied to shipping folder during release.
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
@ -148,3 +144,9 @@ dmypy.json
|
||||
# Temporary files
|
||||
*.swp
|
||||
*~
|
||||
|
||||
_dist/*
|
||||
_build/*
|
||||
*.pcapng
|
||||
|
||||
|
||||
|
||||
15
.vscode/launch.json
vendored
Normal file
15
.vscode/launch.json
vendored
Normal file
@ -0,0 +1,15 @@
|
||||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
|
||||
{
|
||||
"name": "Python Debugger: Module",
|
||||
"type": "debugpy",
|
||||
"request": "launch",
|
||||
"module": "netanalyzer"
|
||||
}
|
||||
]
|
||||
}
|
||||
39
netanalyzer.spec
Normal file
39
netanalyzer.spec
Normal file
@ -0,0 +1,39 @@
|
||||
# -*- mode: python ; coding: utf-8 -*-
|
||||
|
||||
block_cipher = None
|
||||
|
||||
a = Analysis(
|
||||
['netanalyzer/__main__.py'],
|
||||
pathex=['.'],
|
||||
binaries=[],
|
||||
# Usa project_icon_filename nella sezione datas
|
||||
datas=[('NetAnalyzer.ico', '.')],
|
||||
hiddenimports=[],
|
||||
hookspath=[],
|
||||
runtime_hooks=[],
|
||||
excludes=[],
|
||||
win_no_prefer_redirects=False,
|
||||
win_private_assemblies=False,
|
||||
cipher=block_cipher,
|
||||
noarchive=False
|
||||
)
|
||||
pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)
|
||||
|
||||
exe = EXE(
|
||||
pyz,
|
||||
a.scripts,
|
||||
a.binaries,
|
||||
a.zipfiles,
|
||||
a.datas,
|
||||
[],
|
||||
name='NetAnalyzer',
|
||||
debug=False,
|
||||
bootloader_ignore_signals=False,
|
||||
strip=False,
|
||||
upx=True,
|
||||
upx_exclude=[],
|
||||
runtime_tmpdir=None,
|
||||
console=True,
|
||||
# Usa project_icon_filename per l'opzione icon
|
||||
icon='NetAnalyzer.ico'
|
||||
)
|
||||
11
netanalyzer/__main__.py
Normal file
11
netanalyzer/__main__.py
Normal file
@ -0,0 +1,11 @@
|
||||
from .gui.main_view import MainView
|
||||
|
||||
def main():
|
||||
"""
|
||||
Punto di ingresso principale per l'applicazione NetPulseAnalyzer.
|
||||
"""
|
||||
app = MainView()
|
||||
app.mainloop()
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
80
netanalyzer/core/analyzer.py
Normal file
80
netanalyzer/core/analyzer.py
Normal file
@ -0,0 +1,80 @@
|
||||
import scapy.all as scapy
|
||||
import pandas as pd
|
||||
from typing import List, Dict, Any, Optional
|
||||
|
||||
class PacketAnalyzer:
|
||||
"""
|
||||
Main class for analyzing network packets from capture files.
|
||||
Uses Scapy for reading and Pandas for statistical processing.
|
||||
"""
|
||||
def __init__(self):
|
||||
self.packets: Optional[scapy.PacketList] = None
|
||||
self.filepath: Optional[str] = None
|
||||
|
||||
def load_pcap(self, filepath: str) -> int:
|
||||
"""
|
||||
Loads packets from a .pcapng or .pcap file.
|
||||
WARNING: This can be slow for very large files as it loads the entire file into memory.
|
||||
|
||||
Returns the total number of packets loaded.
|
||||
"""
|
||||
self.filepath = filepath
|
||||
print(f"Loading packets from {filepath}...")
|
||||
self.packets = scapy.rdpcap(filepath)
|
||||
print(f"Loaded {len(self.packets)} packets.")
|
||||
return len(self.packets)
|
||||
|
||||
def calculate_ab_processing_stats(self, port_a: int, port_b: int) -> Dict[str, Any]:
|
||||
"""
|
||||
Calculates the processing time between packets on port A and port B.
|
||||
"""
|
||||
if not self.packets:
|
||||
raise ValueError("No packets loaded. Call load_pcap() first.")
|
||||
|
||||
# Extract timestamps for packets on port A and port B (destination ports)
|
||||
times_a = [p.time for p in self.packets if p.haslayer(scapy.UDP) and p[scapy.UDP].dport == port_a]
|
||||
times_b = [p.time for p in self.packets if p.haslayer(scapy.UDP) and p[scapy.UDP].dport == port_b]
|
||||
|
||||
if not times_a or not times_b:
|
||||
return {"error": "Packets for one or both ports (A/B) were not found."}
|
||||
|
||||
min_len = min(len(times_a), len(times_b))
|
||||
if min_len == 0:
|
||||
return {"error": "No matching A/B packet pairs found."}
|
||||
|
||||
# Calculate the time deltas in milliseconds
|
||||
processing_times_ms = (pd.Series(times_b[:min_len]) - pd.Series(times_a[:min_len])) * 1000
|
||||
|
||||
# Use pandas to get a comprehensive set of statistics
|
||||
stats = processing_times_ms.describe(percentiles=[.25, .5, .75, .95, .99]).to_dict()
|
||||
|
||||
# Add raw data points for plotting
|
||||
stats['data_points'] = processing_times_ms.tolist()
|
||||
|
||||
return stats
|
||||
|
||||
def calculate_inter_packet_gap_stats(self, port: int) -> Dict[str, Any]:
|
||||
"""
|
||||
Calculates the interval (and jitter) between consecutive packets on a single port.
|
||||
"""
|
||||
if not self.packets:
|
||||
raise ValueError("No packets loaded.")
|
||||
|
||||
# Filter packets for the specified port (either source or destination)
|
||||
times = [p.time for p in self.packets if p.haslayer(scapy.UDP) and (p[scapy.UDP].sport == port or p[scapy.UDP].dport == port)]
|
||||
|
||||
if len(times) < 2:
|
||||
return {"error": f"Fewer than 2 packets found on port {port}. Cannot calculate interval."}
|
||||
|
||||
# Calculate the difference between each timestamp and the previous one
|
||||
gaps_ms = pd.Series(times).diff().dropna() * 1000
|
||||
|
||||
# Calculate statistics
|
||||
stats = gaps_ms.describe(percentiles=[.25, .5, .75, .95, .99]).to_dict()
|
||||
|
||||
# Jitter is often defined as the standard deviation of the packet delay variation
|
||||
stats['jitter'] = stats.get('std', 0.0)
|
||||
|
||||
stats['data_points'] = gaps_ms.tolist()
|
||||
|
||||
return stats
|
||||
262
netanalyzer/gui/main_view.py
Normal file
262
netanalyzer/gui/main_view.py
Normal file
@ -0,0 +1,262 @@
|
||||
# NetPulseAnalyzer/gui/main_view.py
|
||||
|
||||
import tkinter as tk
|
||||
from tkinter import ttk, filedialog, messagebox
|
||||
import threading
|
||||
import os
|
||||
import matplotlib.pyplot as plt
|
||||
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
|
||||
import pandas as pd
|
||||
from typing import Dict, Any
|
||||
|
||||
|
||||
from ..core.analyzer import PacketAnalyzer
|
||||
|
||||
class MainView(tk.Tk):
|
||||
"""
|
||||
Main window for the NetPulseAnalyzer application.
|
||||
"""
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.title("NetPulseAnalyzer")
|
||||
self.geometry("1200x800")
|
||||
|
||||
self.analyzer = PacketAnalyzer()
|
||||
|
||||
self._create_widgets()
|
||||
|
||||
def _create_widgets(self):
|
||||
main_frame = ttk.Frame(self, padding=10)
|
||||
main_frame.pack(fill=tk.BOTH, expand=True)
|
||||
|
||||
# --- Top Controls Section ---
|
||||
controls_frame = ttk.LabelFrame(main_frame, text="File Controls", padding=10)
|
||||
controls_frame.pack(fill=tk.X, pady=(0, 10))
|
||||
|
||||
self.filepath_label = ttk.Label(controls_frame, text="No file loaded.")
|
||||
self.filepath_label.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=5)
|
||||
self.load_button = ttk.Button(controls_frame, text="Open .pcapng File...", command=self._open_file)
|
||||
self.load_button.pack(side=tk.RIGHT, padx=5)
|
||||
|
||||
# --- Notebook for different analysis types ---
|
||||
self.results_notebook = ttk.Notebook(main_frame)
|
||||
self.results_notebook.pack(fill=tk.BOTH, expand=True)
|
||||
|
||||
# Create the two tabs
|
||||
self.tab_processing_time = ttk.Frame(self.results_notebook)
|
||||
self.tab_jitter = ttk.Frame(self.results_notebook)
|
||||
|
||||
self.results_notebook.add(self.tab_processing_time, text=" Processing Time (A -> B) ")
|
||||
self.results_notebook.add(self.tab_jitter, text=" Interval / Jitter Analysis ")
|
||||
|
||||
# Populate each tab with its specific widgets
|
||||
self._create_processing_time_tab(self.tab_processing_time)
|
||||
self._create_jitter_tab(self.tab_jitter)
|
||||
|
||||
def _create_processing_time_tab(self, parent_tab):
|
||||
controls = ttk.LabelFrame(parent_tab, text="Analysis Parameters", padding=10)
|
||||
controls.pack(fill=tk.X, padx=10, pady=10)
|
||||
|
||||
ttk.Label(controls, text="Port A (Start):").grid(row=0, column=0, padx=5, sticky='w')
|
||||
self.proc_port_a_var = tk.StringVar(value="55001")
|
||||
ttk.Entry(controls, textvariable=self.proc_port_a_var, width=10).grid(row=0, column=1)
|
||||
|
||||
ttk.Label(controls, text="Port B (End):").grid(row=0, column=2, padx=(20, 5), sticky='w')
|
||||
self.proc_port_b_var = tk.StringVar(value="55002")
|
||||
ttk.Entry(controls, textvariable=self.proc_port_b_var, width=10).grid(row=0, column=3)
|
||||
|
||||
self.proc_analyze_button = ttk.Button(controls, text="Analyze", command=self._run_processing_time_analysis, state=tk.DISABLED)
|
||||
self.proc_analyze_button.grid(row=0, column=4, padx=20)
|
||||
|
||||
pane = ttk.PanedWindow(parent_tab, orient=tk.HORIZONTAL)
|
||||
pane.pack(fill=tk.BOTH, expand=True, padx=10, pady=(0, 10))
|
||||
|
||||
stats_frame = ttk.LabelFrame(pane, text="Statistics (ms)", padding=10)
|
||||
pane.add(stats_frame, weight=1)
|
||||
|
||||
self.proc_stats_tree = ttk.Treeview(stats_frame, columns=("Metric", "Value"), show="headings")
|
||||
self.proc_stats_tree.heading("Metric", text="Metric")
|
||||
self.proc_stats_tree.heading("Value", text="Value (ms)")
|
||||
self.proc_stats_tree.column("Metric", width=120)
|
||||
self.proc_stats_tree.column("Value", width=100, anchor='e')
|
||||
self.proc_stats_tree.pack(fill=tk.BOTH, expand=True)
|
||||
|
||||
plot_frame = ttk.LabelFrame(pane, text="Plot", padding=10)
|
||||
pane.add(plot_frame, weight=3)
|
||||
|
||||
self.proc_fig = plt.Figure(figsize=(5, 4), dpi=100)
|
||||
self.proc_ax = self.proc_fig.add_subplot(111)
|
||||
self.proc_canvas = FigureCanvasTkAgg(self.proc_fig, master=plot_frame)
|
||||
self.proc_canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True)
|
||||
|
||||
def _create_jitter_tab(self, parent_tab):
|
||||
controls = ttk.LabelFrame(parent_tab, text="Analysis Parameters", padding=10)
|
||||
controls.pack(fill=tk.X, padx=10, pady=10)
|
||||
|
||||
ttk.Label(controls, text="Port to Analyze:").grid(row=0, column=0, padx=5, sticky='w')
|
||||
self.jitter_port_var = tk.StringVar(value="60012") # Default to server command port
|
||||
ttk.Entry(controls, textvariable=self.jitter_port_var, width=10).grid(row=0, column=1)
|
||||
|
||||
self.jitter_analyze_button = ttk.Button(controls, text="Analyze Jitter", command=self._run_jitter_analysis, state=tk.DISABLED)
|
||||
self.jitter_analyze_button.grid(row=0, column=2, padx=20)
|
||||
|
||||
pane = ttk.PanedWindow(parent_tab, orient=tk.HORIZONTAL)
|
||||
pane.pack(fill=tk.BOTH, expand=True, padx=10, pady=(0, 10))
|
||||
|
||||
stats_frame = ttk.LabelFrame(pane, text="Interval Statistics (ms)", padding=10)
|
||||
pane.add(stats_frame, weight=1)
|
||||
|
||||
self.jitter_stats_tree = ttk.Treeview(stats_frame, columns=("Metric", "Value"), show="headings")
|
||||
self.jitter_stats_tree.heading("Metric", text="Metric")
|
||||
self.jitter_stats_tree.heading("Value", text="Value (ms)")
|
||||
self.jitter_stats_tree.column("Metric", width=120)
|
||||
self.jitter_stats_tree.column("Value", width=100, anchor='e')
|
||||
self.jitter_stats_tree.pack(fill=tk.BOTH, expand=True)
|
||||
|
||||
plot_frame = ttk.LabelFrame(pane, text="Packet Interval (Jitter) Plot", padding=10)
|
||||
pane.add(plot_frame, weight=3)
|
||||
|
||||
self.jitter_fig = plt.Figure(figsize=(5, 4), dpi=100)
|
||||
self.jitter_ax = self.jitter_fig.add_subplot(111)
|
||||
self.jitter_canvas = FigureCanvasTkAgg(self.jitter_fig, master=plot_frame)
|
||||
self.jitter_canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True)
|
||||
|
||||
def _open_file(self):
|
||||
filepath = filedialog.askopenfilename(
|
||||
title="Select a capture file",
|
||||
filetypes=[("Wireshark/Scapy Captures", "*.pcapng *.pcap"), ("All files", "*.*")]
|
||||
)
|
||||
if not filepath:
|
||||
return
|
||||
|
||||
self.load_button.config(state=tk.DISABLED)
|
||||
self.proc_analyze_button.config(state=tk.DISABLED)
|
||||
self.jitter_analyze_button.config(state=tk.DISABLED)
|
||||
|
||||
progress_dialog = self._create_progress_dialog(filepath)
|
||||
|
||||
threading.Thread(
|
||||
target=self._load_file_thread,
|
||||
args=(filepath, progress_dialog),
|
||||
daemon=True
|
||||
).start()
|
||||
|
||||
def _create_progress_dialog(self, filepath: str) -> tk.Toplevel:
|
||||
"""Creates a modal dialog with an indeterminate progress bar."""
|
||||
dialog = tk.Toplevel(self)
|
||||
dialog.title("Loading...")
|
||||
dialog.geometry("400x150")
|
||||
dialog.transient(self)
|
||||
dialog.grab_set()
|
||||
dialog.resizable(False, False)
|
||||
|
||||
filename = os.path.basename(filepath)
|
||||
try:
|
||||
file_size_mb = os.path.getsize(filepath) / (1024 * 1024)
|
||||
size_str = f"({file_size_mb:.2f} MB)"
|
||||
except OSError:
|
||||
size_str = ""
|
||||
|
||||
ttk.Label(dialog, text="Loading capture file:", padding=10).pack()
|
||||
ttk.Label(dialog, text=f"{filename} {size_str}", font=('Helvetica', 10, 'bold')).pack()
|
||||
|
||||
ttk.Label(dialog, text="This may take a moment...", padding=5).pack()
|
||||
|
||||
progress_bar = ttk.Progressbar(dialog, mode='indeterminate', length=350)
|
||||
progress_bar.pack(pady=20)
|
||||
progress_bar.start(10)
|
||||
|
||||
self.update_idletasks()
|
||||
x = self.winfo_x() + (self.winfo_width() // 2) - (dialog.winfo_width() // 2)
|
||||
y = self.winfo_y() + (self.winfo_height() // 2) - (dialog.winfo_height() // 2)
|
||||
dialog.geometry(f"+{x}+{y}")
|
||||
|
||||
return dialog
|
||||
|
||||
def _load_file_thread(self, filepath: str, progress_dialog: tk.Toplevel):
|
||||
"""
|
||||
Background thread function to load the file. Closes the dialog when done.
|
||||
"""
|
||||
try:
|
||||
total_packets = self.analyzer.load_pcap(filepath)
|
||||
self.after(0, self._on_load_complete, filepath, total_packets)
|
||||
except Exception as e:
|
||||
self.after(0, messagebox.showerror, "Load Error", f"Failed to read file:\n{e}")
|
||||
self.after(0, self.filepath_label.config, {"text": "File load failed."})
|
||||
finally:
|
||||
self.after(0, progress_dialog.destroy)
|
||||
self.after(0, self.load_button.config, {"state": tk.NORMAL})
|
||||
|
||||
def _on_load_complete(self, filepath, total_packets):
|
||||
"""GUI thread callback for when file loading is complete."""
|
||||
self.filepath_label.config(text=f"File: {os.path.basename(filepath)} ({total_packets} packets)")
|
||||
self.proc_analyze_button.config(state=tk.NORMAL)
|
||||
self.jitter_analyze_button.config(state=tk.NORMAL)
|
||||
|
||||
def _run_processing_time_analysis(self):
|
||||
try:
|
||||
port_a = int(self.proc_port_a_var.get())
|
||||
port_b = int(self.proc_port_b_var.get())
|
||||
stats = self.analyzer.calculate_ab_processing_stats(port_a, port_b)
|
||||
if "error" in stats:
|
||||
messagebox.showerror("Analysis Error", stats["error"])
|
||||
return
|
||||
self._update_stats_table(self.proc_stats_tree, stats)
|
||||
self._update_plot(self.proc_ax, self.proc_fig, self.proc_canvas, stats['data_points'], "Processing Time", "Time (ms)")
|
||||
except Exception as e:
|
||||
messagebox.showerror("Error", f"Analysis failed: {e}")
|
||||
|
||||
def _run_jitter_analysis(self):
|
||||
try:
|
||||
port = int(self.jitter_port_var.get())
|
||||
stats = self.analyzer.calculate_inter_packet_gap_stats(port)
|
||||
if "error" in stats:
|
||||
messagebox.showerror("Analysis Error", stats["error"])
|
||||
return
|
||||
|
||||
# CORREZIONE: Usiamo la chiave 'std' per 'Jitter'
|
||||
stats['Jitter'] = stats.get('std', 0.0)
|
||||
|
||||
self._update_stats_table(self.jitter_stats_tree, stats)
|
||||
self._update_plot(self.jitter_ax, self.jitter_fig, self.jitter_canvas, stats['data_points'], f"Packet Interval (Port {port})", "Interval (ms)")
|
||||
except Exception as e:
|
||||
messagebox.showerror("Error", f"Analysis failed: {e}")
|
||||
|
||||
def _update_stats_table(self, tree: ttk.Treeview, stats: Dict[str, Any]):
|
||||
for item in tree.get_children():
|
||||
tree.delete(item)
|
||||
|
||||
# CORREZIONE: Usiamo 'std' ma lo etichettiamo come Jitter per chiarezza
|
||||
metric_map = {
|
||||
'count': 'Sample Count', 'mean': 'Mean', 'std': 'Std Dev (Jitter)',
|
||||
'min': 'Min', '25%': '25th Percentile', '50%': 'Median (50%)',
|
||||
'75%': '75th Percentile', '95%': '95th Percentile', '99%': '99th Percentile',
|
||||
'max': 'Max'
|
||||
}
|
||||
|
||||
for key, name in metric_map.items():
|
||||
if key in stats:
|
||||
value = stats[key]
|
||||
if key == 'count':
|
||||
tree.insert('', 'end', values=(name, f"{int(value)}"))
|
||||
else:
|
||||
tree.insert('', 'end', values=(name, f"{value:.4f}"))
|
||||
|
||||
def _update_plot(self, ax, fig, canvas, data_points, title, ylabel):
|
||||
ax.clear()
|
||||
ax.plot(data_points, marker='.', linestyle='-', markersize=4)
|
||||
|
||||
if data_points:
|
||||
s = pd.Series(data_points)
|
||||
mean_val = s.mean()
|
||||
p99_val = s.quantile(0.99)
|
||||
ax.axhline(y=mean_val, color='g', linestyle='--', label=f'Mean ({mean_val:.2f} ms)')
|
||||
ax.axhline(y=p99_val, color='r', linestyle='--', label=f'99th Percentile ({p99_val:.2f} ms)')
|
||||
|
||||
ax.set_title(title)
|
||||
ax.set_xlabel("Packet Sequence")
|
||||
ax.set_ylabel(ylabel)
|
||||
ax.grid(True)
|
||||
ax.legend()
|
||||
fig.tight_layout()
|
||||
canvas.draw()
|
||||
7
pyproject.toml
Normal file
7
pyproject.toml
Normal file
@ -0,0 +1,7 @@
|
||||
[build-system]
|
||||
requires = ["setuptools>=61.0"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "sxxxxxxx-netanalyzer"
|
||||
version = "0.1.0"
|
||||
18
requirements.txt
Normal file
18
requirements.txt
Normal file
@ -0,0 +1,18 @@
|
||||
# Requirements generated by DependencyAnalyzer for SXXXXXXX_NetAnalyzer
|
||||
# Python Version (analysis env): 3.13.9
|
||||
|
||||
# --- Standard Library Modules Used (part of Python 3.13.9) ---
|
||||
# threading (Used in: netanalyzer\gui\main_view.py)
|
||||
# tkinter (Used in: netanalyzer\gui\main_view.py)
|
||||
# typing (Used in: netanalyzer\core\analyzer.py, netanalyzer\gui\main_view.py)
|
||||
|
||||
# --- External Dependencies (for pip install) ---
|
||||
# Found in: netanalyzer\gui\main_view.py
|
||||
matplotlib==3.10.7
|
||||
|
||||
# Found in: netanalyzer\core\analyzer.py, netanalyzer\gui\main_view.py
|
||||
pandas==2.3.3
|
||||
|
||||
# Found in: netanalyzer\core\analyzer.py
|
||||
scapy==2.6.1
|
||||
|
||||
@ -1,17 +0,0 @@
|
||||
# sxxxxxxx_netanalyzer/__main__.py
|
||||
|
||||
# Example import assuming your main logic is in a 'main' function
|
||||
# within a 'app' module in your 'sxxxxxxx_netanalyzer.core' package.
|
||||
# from sxxxxxxx_netanalyzer.core.app import main as start_application
|
||||
#
|
||||
# Or, if you have a function in sxxxxxxx_netanalyzer.core.core:
|
||||
# from sxxxxxxx_netanalyzer.core.core import main_function
|
||||
|
||||
def main():
|
||||
print(f"Running SXXXXXXX_NetAnalyzer...")
|
||||
# Placeholder: Replace with your application's entry point
|
||||
# Example: start_application()
|
||||
print("To customize, edit 'sxxxxxxx_netanalyzer/__main__.py' and your core modules.")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Loading…
Reference in New Issue
Block a user