primo commit

This commit is contained in:
VALLONGOL 2025-11-18 15:53:15 +01:00
parent fcefcedc86
commit ddd7ff76d9
15 changed files with 438 additions and 21 deletions

10
.gitignore vendored
View File

@ -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
View 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
View 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
View 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()

View 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

View 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
View 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
View 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

View File

@ -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()