2025-07-25 21:45:07 -04:00
|
|
|
|
"""
|
|
|
|
|
|
Main GUI window for StreamLens
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
import sys
|
|
|
|
|
|
import os
|
2025-07-26 00:02:25 -04:00
|
|
|
|
from typing import Optional, List, TYPE_CHECKING, Dict
|
2025-07-25 21:45:07 -04:00
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
from PySide6.QtWidgets import (
|
|
|
|
|
|
QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QSplitter,
|
|
|
|
|
|
QTableWidget, QTableWidgetItem, QTextEdit, QMenuBar, QMenu,
|
|
|
|
|
|
QFileDialog, QMessageBox, QProgressBar, QStatusBar, QLabel,
|
2025-07-26 00:02:25 -04:00
|
|
|
|
QHeaderView, QPushButton, QGroupBox, QDockWidget, QToolBar,
|
|
|
|
|
|
QButtonGroup, QComboBox, QSpinBox
|
2025-07-25 21:45:07 -04:00
|
|
|
|
)
|
|
|
|
|
|
from PySide6.QtCore import Qt, QThread, Signal, QTimer
|
2025-07-26 00:02:25 -04:00
|
|
|
|
from PySide6.QtGui import QAction, QIcon, QFont, QKeySequence, QActionGroup
|
|
|
|
|
|
|
|
|
|
|
|
from .dock_panels import FlowListDockWidget, FlowDetailDockWidget
|
2025-07-25 21:45:07 -04:00
|
|
|
|
|
|
|
|
|
|
# Matplotlib integration - lazy loaded
|
|
|
|
|
|
matplotlib = None
|
|
|
|
|
|
FigureCanvas = None
|
|
|
|
|
|
NavigationToolbar = None
|
|
|
|
|
|
Figure = None
|
|
|
|
|
|
plt = None
|
|
|
|
|
|
|
|
|
|
|
|
def _ensure_matplotlib_gui_loaded():
|
|
|
|
|
|
"""Lazy load matplotlib for GUI mode"""
|
|
|
|
|
|
global matplotlib, FigureCanvas, NavigationToolbar, Figure, plt
|
|
|
|
|
|
|
|
|
|
|
|
if matplotlib is not None:
|
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
import matplotlib as mpl
|
|
|
|
|
|
matplotlib = mpl
|
|
|
|
|
|
matplotlib.use('Qt5Agg') # Use Qt backend for matplotlib
|
|
|
|
|
|
|
|
|
|
|
|
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FC
|
|
|
|
|
|
from matplotlib.backends.backend_qt5agg import NavigationToolbar2QT as NT
|
|
|
|
|
|
from matplotlib.figure import Figure as Fig
|
|
|
|
|
|
import matplotlib.pyplot as pyplot
|
|
|
|
|
|
|
2025-07-26 00:02:25 -04:00
|
|
|
|
# Turn off interactive mode to prevent floating windows
|
|
|
|
|
|
pyplot.ioff()
|
|
|
|
|
|
|
|
|
|
|
|
# Ensure no figure windows are created
|
|
|
|
|
|
mpl.rcParams['figure.max_open_warning'] = 0
|
|
|
|
|
|
mpl.rcParams['backend'] = 'Qt5Agg'
|
|
|
|
|
|
|
|
|
|
|
|
# Prevent any automatic figure display
|
|
|
|
|
|
mpl.pyplot.show = lambda *args, **kwargs: None
|
|
|
|
|
|
pyplot.show = lambda *args, **kwargs: None
|
|
|
|
|
|
|
|
|
|
|
|
# Also prevent figure.show()
|
|
|
|
|
|
original_figure_show = Fig.show
|
|
|
|
|
|
Fig.show = lambda self, *args, **kwargs: None
|
|
|
|
|
|
|
2025-07-25 21:45:07 -04:00
|
|
|
|
FigureCanvas = FC
|
|
|
|
|
|
NavigationToolbar = NT
|
|
|
|
|
|
Figure = Fig
|
|
|
|
|
|
plt = pyplot
|
|
|
|
|
|
|
|
|
|
|
|
return True
|
|
|
|
|
|
except ImportError as e:
|
|
|
|
|
|
print(f"Matplotlib GUI integration not available: {e}")
|
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
except ImportError as e:
|
|
|
|
|
|
print(f"GUI dependencies not available: {e}")
|
|
|
|
|
|
sys.exit(1)
|
|
|
|
|
|
|
|
|
|
|
|
if TYPE_CHECKING:
|
|
|
|
|
|
from ..analysis.core import EthernetAnalyzer
|
|
|
|
|
|
from ..models.flow_stats import FlowStats
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class PCAPLoadThread(QThread):
|
|
|
|
|
|
"""Thread for loading PCAP files without blocking UI"""
|
|
|
|
|
|
|
|
|
|
|
|
progress_updated = Signal(int)
|
|
|
|
|
|
loading_finished = Signal(object) # Emits the analyzer
|
|
|
|
|
|
error_occurred = Signal(str)
|
|
|
|
|
|
|
|
|
|
|
|
def __init__(self, file_path: str, parent=None):
|
|
|
|
|
|
super().__init__(parent)
|
|
|
|
|
|
self.file_path = file_path
|
|
|
|
|
|
|
|
|
|
|
|
def run(self):
|
|
|
|
|
|
try:
|
|
|
|
|
|
from ..analysis.core import EthernetAnalyzer
|
|
|
|
|
|
from ..utils.pcap_loader import PCAPLoader
|
|
|
|
|
|
|
|
|
|
|
|
# Create analyzer
|
|
|
|
|
|
analyzer = EthernetAnalyzer()
|
|
|
|
|
|
|
|
|
|
|
|
# Load PCAP
|
|
|
|
|
|
loader = PCAPLoader(self.file_path)
|
|
|
|
|
|
if not loader.validate_file():
|
|
|
|
|
|
self.error_occurred.emit(f"Invalid PCAP file: {self.file_path}")
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
packets = loader.load_all()
|
|
|
|
|
|
analyzer.all_packets = packets
|
|
|
|
|
|
|
|
|
|
|
|
# Process packets with progress updates
|
|
|
|
|
|
total_packets = len(packets)
|
|
|
|
|
|
for i, packet in enumerate(packets, 1):
|
|
|
|
|
|
analyzer._process_single_packet(packet, i)
|
|
|
|
|
|
|
|
|
|
|
|
# Update progress every 1000 packets
|
|
|
|
|
|
if i % 1000 == 0 or i == total_packets:
|
|
|
|
|
|
progress = int((i / total_packets) * 100)
|
|
|
|
|
|
self.progress_updated.emit(progress)
|
|
|
|
|
|
|
|
|
|
|
|
# Calculate statistics
|
|
|
|
|
|
analyzer.calculate_statistics()
|
|
|
|
|
|
|
|
|
|
|
|
self.loading_finished.emit(analyzer)
|
|
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
self.error_occurred.emit(str(e))
|
|
|
|
|
|
|
|
|
|
|
|
|
2025-07-26 00:02:25 -04:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2025-07-25 21:45:07 -04:00
|
|
|
|
class StreamLensMainWindow(QMainWindow):
|
2025-07-26 00:02:25 -04:00
|
|
|
|
"""Main application window with docking panels"""
|
2025-07-25 21:45:07 -04:00
|
|
|
|
|
|
|
|
|
|
def __init__(self):
|
|
|
|
|
|
super().__init__()
|
|
|
|
|
|
self.analyzer: Optional['EthernetAnalyzer'] = None
|
|
|
|
|
|
self.current_file = None
|
|
|
|
|
|
self.loading_thread = None
|
2025-07-26 00:02:25 -04:00
|
|
|
|
self._initializing = True # Flag to prevent early event triggers
|
2025-07-25 21:45:07 -04:00
|
|
|
|
|
|
|
|
|
|
self.setWindowTitle("StreamLens - Ethernet Traffic Analyzer")
|
2025-07-26 00:02:25 -04:00
|
|
|
|
self.setGeometry(100, 100, 1600, 1000) # Larger default size for docking
|
|
|
|
|
|
|
|
|
|
|
|
# Apply modern dark theme
|
|
|
|
|
|
self.apply_dark_theme()
|
2025-07-25 21:45:07 -04:00
|
|
|
|
|
|
|
|
|
|
self.setup_menus()
|
2025-07-26 00:02:25 -04:00
|
|
|
|
self.setup_toolbar()
|
|
|
|
|
|
self.setup_docking_ui()
|
2025-07-25 21:45:07 -04:00
|
|
|
|
self.setup_status_bar()
|
|
|
|
|
|
|
2025-07-26 00:02:25 -04:00
|
|
|
|
# Initialization complete
|
|
|
|
|
|
self._initializing = False
|
2025-07-25 21:45:07 -04:00
|
|
|
|
|
2025-07-26 00:02:25 -04:00
|
|
|
|
def apply_dark_theme(self):
|
|
|
|
|
|
"""Apply modern dark theme to the entire application"""
|
|
|
|
|
|
dark_stylesheet = """
|
|
|
|
|
|
/* Main Window Dark Theme */
|
|
|
|
|
|
QMainWindow {
|
|
|
|
|
|
background-color: #1e1e1e;
|
|
|
|
|
|
color: #ffffff;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Menu Bar */
|
|
|
|
|
|
QMenuBar {
|
|
|
|
|
|
background-color: #2d2d2d;
|
|
|
|
|
|
color: #ffffff;
|
|
|
|
|
|
border-bottom: 1px solid #404040;
|
|
|
|
|
|
padding: 2px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
QMenuBar::item {
|
|
|
|
|
|
background-color: transparent;
|
|
|
|
|
|
padding: 8px 12px;
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
QMenuBar::item:selected {
|
|
|
|
|
|
background-color: #404040;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
QMenuBar::item:pressed {
|
|
|
|
|
|
background-color: #505050;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Menu Dropdown */
|
|
|
|
|
|
QMenu {
|
|
|
|
|
|
background-color: #2d2d2d;
|
|
|
|
|
|
color: #ffffff;
|
|
|
|
|
|
border: 1px solid #404040;
|
|
|
|
|
|
border-radius: 6px;
|
|
|
|
|
|
padding: 4px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
QMenu::item {
|
|
|
|
|
|
padding: 8px 20px;
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
QMenu::item:selected {
|
|
|
|
|
|
background-color: #404040;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Toolbar */
|
|
|
|
|
|
QToolBar {
|
|
|
|
|
|
background-color: #2d2d2d;
|
|
|
|
|
|
border: none;
|
|
|
|
|
|
padding: 4px;
|
|
|
|
|
|
spacing: 8px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
QToolButton {
|
|
|
|
|
|
background-color: #404040;
|
|
|
|
|
|
color: #ffffff;
|
|
|
|
|
|
border: 1px solid #505050;
|
|
|
|
|
|
border-radius: 6px;
|
|
|
|
|
|
padding: 8px 12px;
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
QToolButton:hover {
|
|
|
|
|
|
background-color: #505050;
|
|
|
|
|
|
border-color: #606060;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
QToolButton:pressed {
|
|
|
|
|
|
background-color: #353535;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Status Bar */
|
|
|
|
|
|
QStatusBar {
|
|
|
|
|
|
background-color: #2d2d2d;
|
|
|
|
|
|
color: #cccccc;
|
|
|
|
|
|
border-top: 1px solid #404040;
|
|
|
|
|
|
padding: 4px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Progress Bar */
|
|
|
|
|
|
QProgressBar {
|
|
|
|
|
|
background-color: #404040;
|
|
|
|
|
|
border: 1px solid #505050;
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
color: #ffffff;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
QProgressBar::chunk {
|
|
|
|
|
|
background-color: #0078d4;
|
|
|
|
|
|
border-radius: 3px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Spin Box */
|
|
|
|
|
|
QSpinBox {
|
|
|
|
|
|
background-color: #404040;
|
|
|
|
|
|
color: #ffffff;
|
|
|
|
|
|
border: 1px solid #505050;
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
padding: 4px 8px;
|
|
|
|
|
|
min-width: 50px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
QSpinBox:hover {
|
|
|
|
|
|
border-color: #606060;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
QSpinBox:focus {
|
|
|
|
|
|
border-color: #0078d4;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
QSpinBox::up-button, QSpinBox::down-button {
|
|
|
|
|
|
background-color: #505050;
|
|
|
|
|
|
border: none;
|
|
|
|
|
|
width: 16px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
QSpinBox::up-button:hover, QSpinBox::down-button:hover {
|
|
|
|
|
|
background-color: #606060;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Labels */
|
|
|
|
|
|
QLabel {
|
|
|
|
|
|
color: #ffffff;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Dock Widgets */
|
|
|
|
|
|
QDockWidget {
|
|
|
|
|
|
background-color: #1e1e1e;
|
|
|
|
|
|
color: #ffffff;
|
|
|
|
|
|
titlebar-close-icon: none;
|
|
|
|
|
|
titlebar-normal-icon: none;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
QDockWidget::title {
|
|
|
|
|
|
background-color: #2d2d2d;
|
|
|
|
|
|
color: #ffffff;
|
|
|
|
|
|
padding: 8px;
|
|
|
|
|
|
border-bottom: 1px solid #404040;
|
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Splitter */
|
|
|
|
|
|
QSplitter::handle {
|
|
|
|
|
|
background-color: #404040;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
QSplitter::handle:horizontal {
|
|
|
|
|
|
width: 2px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
QSplitter::handle:vertical {
|
|
|
|
|
|
height: 2px;
|
|
|
|
|
|
}
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
self.setStyleSheet(dark_stylesheet)
|
|
|
|
|
|
|
|
|
|
|
|
def setup_docking_ui(self):
|
|
|
|
|
|
"""Set up the UI layout with integrated plots"""
|
|
|
|
|
|
# Create docking panels
|
|
|
|
|
|
self.flow_list_dock = FlowListDockWidget(self)
|
|
|
|
|
|
self.flow_detail_dock = FlowDetailDockWidget(self)
|
|
|
|
|
|
|
|
|
|
|
|
# Set flow list as central widget to take full width
|
|
|
|
|
|
self.setCentralWidget(self.flow_list_dock)
|
|
|
|
|
|
|
|
|
|
|
|
# Add detail dock at bottom
|
|
|
|
|
|
self.addDockWidget(Qt.BottomDockWidgetArea, self.flow_detail_dock)
|
|
|
|
|
|
|
|
|
|
|
|
# Connect signals
|
|
|
|
|
|
self.flow_list_dock.flows_table.itemSelectionChanged.connect(self.on_flow_selection_changed)
|
|
|
|
|
|
|
|
|
|
|
|
# Set detail dock height
|
|
|
|
|
|
self.resizeDocks([self.flow_detail_dock], [150], Qt.Vertical)
|
2025-07-25 21:45:07 -04:00
|
|
|
|
|
2025-07-26 00:02:25 -04:00
|
|
|
|
|
|
|
|
|
|
def on_flow_selection_changed(self):
|
|
|
|
|
|
"""Handle flow selection change to update detail panel"""
|
|
|
|
|
|
if not hasattr(self, 'flow_list_dock') or not self.analyzer:
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
selected_rows = self.flow_list_dock.flows_table.selectionModel().selectedRows()
|
2025-07-25 21:45:07 -04:00
|
|
|
|
|
2025-07-26 00:02:25 -04:00
|
|
|
|
if not selected_rows:
|
|
|
|
|
|
self.flow_detail_dock.update_flow_details(None, None)
|
|
|
|
|
|
return
|
2025-07-25 21:45:07 -04:00
|
|
|
|
|
2025-07-26 00:02:25 -04:00
|
|
|
|
# Get selected flow
|
|
|
|
|
|
row = selected_rows[0].row()
|
|
|
|
|
|
flow_item = self.flow_list_dock.flows_table.item(row, 0)
|
|
|
|
|
|
flow = flow_item.data(Qt.UserRole)
|
2025-07-25 21:45:07 -04:00
|
|
|
|
|
2025-07-26 00:02:25 -04:00
|
|
|
|
if flow:
|
|
|
|
|
|
self.flow_detail_dock.update_flow_details(flow, self.analyzer)
|
2025-07-25 21:45:07 -04:00
|
|
|
|
|
|
|
|
|
|
def setup_menus(self):
|
|
|
|
|
|
"""Set up application menus"""
|
|
|
|
|
|
menubar = self.menuBar()
|
|
|
|
|
|
|
|
|
|
|
|
# File menu
|
2025-07-26 00:02:25 -04:00
|
|
|
|
file_menu = menubar.addMenu("&File")
|
2025-07-25 21:45:07 -04:00
|
|
|
|
|
2025-07-26 00:02:25 -04:00
|
|
|
|
# Open PCAP
|
|
|
|
|
|
open_action = QAction("&Open PCAP...", self)
|
|
|
|
|
|
open_action.setShortcut(QKeySequence.Open)
|
|
|
|
|
|
open_action.setStatusTip("Open a PCAP file for analysis")
|
2025-07-25 21:45:07 -04:00
|
|
|
|
open_action.triggered.connect(self.open_pcap_file)
|
|
|
|
|
|
file_menu.addAction(open_action)
|
|
|
|
|
|
|
2025-07-26 00:02:25 -04:00
|
|
|
|
# Monitor NIC
|
|
|
|
|
|
monitor_action = QAction("&Monitor NIC...", self)
|
|
|
|
|
|
monitor_action.setShortcut("Ctrl+M")
|
|
|
|
|
|
monitor_action.setStatusTip("Start live network monitoring")
|
|
|
|
|
|
monitor_action.triggered.connect(self.monitor_nic)
|
|
|
|
|
|
file_menu.addAction(monitor_action)
|
|
|
|
|
|
|
2025-07-25 21:45:07 -04:00
|
|
|
|
file_menu.addSeparator()
|
|
|
|
|
|
|
2025-07-26 00:02:25 -04:00
|
|
|
|
# Exit
|
|
|
|
|
|
exit_action = QAction("E&xit", self)
|
|
|
|
|
|
exit_action.setShortcut(QKeySequence.Quit)
|
|
|
|
|
|
exit_action.setStatusTip("Exit StreamLens")
|
2025-07-25 21:45:07 -04:00
|
|
|
|
exit_action.triggered.connect(self.close)
|
|
|
|
|
|
file_menu.addAction(exit_action)
|
|
|
|
|
|
|
|
|
|
|
|
# View menu
|
2025-07-26 00:02:25 -04:00
|
|
|
|
view_menu = menubar.addMenu("&View")
|
2025-07-25 21:45:07 -04:00
|
|
|
|
|
2025-07-26 00:02:25 -04:00
|
|
|
|
# Refresh
|
|
|
|
|
|
refresh_action = QAction("&Refresh", self)
|
|
|
|
|
|
refresh_action.setShortcut(QKeySequence.Refresh)
|
|
|
|
|
|
refresh_action.setStatusTip("Refresh current data")
|
2025-07-25 21:45:07 -04:00
|
|
|
|
refresh_action.triggered.connect(self.refresh_data)
|
|
|
|
|
|
view_menu.addAction(refresh_action)
|
2025-07-26 00:02:25 -04:00
|
|
|
|
|
|
|
|
|
|
view_menu.addSeparator()
|
|
|
|
|
|
|
|
|
|
|
|
# Show/Hide panels
|
|
|
|
|
|
show_flow_details_action = QAction("Show Flow &Details", self)
|
|
|
|
|
|
show_flow_details_action.setCheckable(True)
|
|
|
|
|
|
show_flow_details_action.setChecked(True)
|
|
|
|
|
|
show_flow_details_action.triggered.connect(lambda checked: self.flow_detail_dock.setVisible(checked))
|
|
|
|
|
|
view_menu.addAction(show_flow_details_action)
|
|
|
|
|
|
|
|
|
|
|
|
# Help menu
|
|
|
|
|
|
help_menu = menubar.addMenu("&Help")
|
|
|
|
|
|
|
|
|
|
|
|
about_action = QAction("&About StreamLens...", self)
|
|
|
|
|
|
about_action.setStatusTip("About StreamLens")
|
|
|
|
|
|
about_action.triggered.connect(self.show_about)
|
|
|
|
|
|
help_menu.addAction(about_action)
|
|
|
|
|
|
|
|
|
|
|
|
def setup_toolbar(self):
|
|
|
|
|
|
"""Set up application toolbar"""
|
|
|
|
|
|
toolbar = QToolBar("Main Toolbar")
|
|
|
|
|
|
toolbar.setMovable(False)
|
|
|
|
|
|
self.addToolBar(toolbar)
|
|
|
|
|
|
|
|
|
|
|
|
# File operations
|
|
|
|
|
|
open_action = QAction("Open", self)
|
|
|
|
|
|
open_action.setShortcut(QKeySequence.Open)
|
|
|
|
|
|
open_action.setStatusTip("Open PCAP file")
|
|
|
|
|
|
open_action.triggered.connect(self.open_pcap_file)
|
|
|
|
|
|
toolbar.addAction(open_action)
|
|
|
|
|
|
|
|
|
|
|
|
monitor_action = QAction("Monitor", self)
|
|
|
|
|
|
monitor_action.setStatusTip("Monitor network interface")
|
|
|
|
|
|
monitor_action.triggered.connect(self.monitor_nic)
|
|
|
|
|
|
toolbar.addAction(monitor_action)
|
|
|
|
|
|
|
|
|
|
|
|
toolbar.addSeparator()
|
|
|
|
|
|
|
|
|
|
|
|
# Outlier threshold control
|
|
|
|
|
|
toolbar.addWidget(QLabel("Outlier Threshold: "))
|
|
|
|
|
|
self.outlier_threshold_spin = QSpinBox()
|
|
|
|
|
|
self.outlier_threshold_spin.setRange(1, 10)
|
|
|
|
|
|
self.outlier_threshold_spin.setValue(3)
|
|
|
|
|
|
self.outlier_threshold_spin.setSuffix("σ")
|
|
|
|
|
|
self.outlier_threshold_spin.setStatusTip("Set sigma threshold for outlier detection")
|
|
|
|
|
|
self.outlier_threshold_spin.valueChanged.connect(self.on_outlier_threshold_changed)
|
|
|
|
|
|
toolbar.addWidget(self.outlier_threshold_spin)
|
2025-07-25 21:45:07 -04:00
|
|
|
|
|
|
|
|
|
|
def setup_status_bar(self):
|
|
|
|
|
|
"""Set up status bar"""
|
|
|
|
|
|
self.status_bar = QStatusBar()
|
|
|
|
|
|
self.setStatusBar(self.status_bar)
|
|
|
|
|
|
|
|
|
|
|
|
self.progress_bar = QProgressBar()
|
|
|
|
|
|
self.progress_bar.setVisible(False)
|
|
|
|
|
|
self.status_bar.addPermanentWidget(self.progress_bar)
|
|
|
|
|
|
|
|
|
|
|
|
self.status_bar.showMessage("Ready")
|
|
|
|
|
|
|
|
|
|
|
|
def open_pcap_file(self):
|
|
|
|
|
|
"""Open PCAP file dialog"""
|
|
|
|
|
|
file_path, _ = QFileDialog.getOpenFileName(
|
|
|
|
|
|
self,
|
|
|
|
|
|
"Open PCAP File",
|
|
|
|
|
|
"",
|
|
|
|
|
|
"PCAP Files (*.pcap *.pcapng);;All Files (*)"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
if file_path:
|
|
|
|
|
|
self.load_pcap_file(file_path)
|
|
|
|
|
|
|
|
|
|
|
|
def load_pcap_file(self, file_path: str):
|
|
|
|
|
|
"""Load PCAP file in background thread"""
|
|
|
|
|
|
if self.loading_thread and self.loading_thread.isRunning():
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
self.current_file = file_path
|
|
|
|
|
|
self.status_bar.showMessage(f"Loading {os.path.basename(file_path)}...")
|
|
|
|
|
|
self.progress_bar.setVisible(True)
|
|
|
|
|
|
self.progress_bar.setValue(0)
|
|
|
|
|
|
|
|
|
|
|
|
# Disable UI during loading
|
2025-07-26 00:02:25 -04:00
|
|
|
|
self.flow_list_dock.flows_table.setEnabled(False)
|
2025-07-25 21:45:07 -04:00
|
|
|
|
|
|
|
|
|
|
# Start loading thread
|
|
|
|
|
|
self.loading_thread = PCAPLoadThread(file_path)
|
|
|
|
|
|
self.loading_thread.progress_updated.connect(self.progress_bar.setValue)
|
|
|
|
|
|
self.loading_thread.loading_finished.connect(self.on_pcap_loaded)
|
|
|
|
|
|
self.loading_thread.error_occurred.connect(self.on_loading_error)
|
|
|
|
|
|
self.loading_thread.start()
|
|
|
|
|
|
|
|
|
|
|
|
def on_pcap_loaded(self, analyzer: 'EthernetAnalyzer'):
|
|
|
|
|
|
"""Handle PCAP loading completion"""
|
|
|
|
|
|
self.analyzer = analyzer
|
|
|
|
|
|
self.progress_bar.setVisible(False)
|
2025-07-26 00:02:25 -04:00
|
|
|
|
self.flow_list_dock.flows_table.setEnabled(True)
|
2025-07-25 21:45:07 -04:00
|
|
|
|
|
2025-07-26 00:02:25 -04:00
|
|
|
|
# Update flow list dock with analyzer and file info
|
|
|
|
|
|
self.flow_list_dock.set_analyzer(analyzer)
|
|
|
|
|
|
self.flow_list_dock.update_file_info(self.current_file)
|
2025-07-25 21:45:07 -04:00
|
|
|
|
|
2025-07-26 00:02:25 -04:00
|
|
|
|
# Populate flows table with embedded plots (synchronous)
|
|
|
|
|
|
self.flow_list_dock.populate_flows_table()
|
2025-07-25 21:45:07 -04:00
|
|
|
|
|
2025-07-26 00:02:25 -04:00
|
|
|
|
summary = analyzer.get_summary()
|
|
|
|
|
|
self.status_bar.showMessage(f"Loaded {summary['total_packets']:,} packets with embedded plots")
|
|
|
|
|
|
|
2025-07-25 21:45:07 -04:00
|
|
|
|
|
|
|
|
|
|
def on_loading_error(self, error_message: str):
|
|
|
|
|
|
"""Handle loading error"""
|
|
|
|
|
|
self.progress_bar.setVisible(False)
|
2025-07-26 00:02:25 -04:00
|
|
|
|
self.flow_list_dock.flows_table.setEnabled(True)
|
2025-07-25 21:45:07 -04:00
|
|
|
|
|
|
|
|
|
|
QMessageBox.critical(self, "Loading Error", f"Error loading PCAP file:\n{error_message}")
|
|
|
|
|
|
self.status_bar.showMessage("Error loading file")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def get_flow_packets(self, flow: 'FlowStats') -> List:
|
|
|
|
|
|
"""Get all packets for a specific flow"""
|
|
|
|
|
|
if not self.analyzer or not self.analyzer.all_packets:
|
|
|
|
|
|
return []
|
|
|
|
|
|
|
|
|
|
|
|
flow_packets = []
|
|
|
|
|
|
|
|
|
|
|
|
for packet in self.analyzer.all_packets:
|
|
|
|
|
|
try:
|
|
|
|
|
|
if hasattr(packet, 'haslayer'):
|
|
|
|
|
|
from scapy.all import IP
|
|
|
|
|
|
if packet.haslayer(IP):
|
|
|
|
|
|
ip_layer = packet[IP]
|
|
|
|
|
|
if ip_layer.src == flow.src_ip and ip_layer.dst == flow.dst_ip:
|
|
|
|
|
|
flow_packets.append(packet)
|
|
|
|
|
|
except:
|
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
|
|
return flow_packets
|
|
|
|
|
|
|
|
|
|
|
|
def refresh_data(self):
|
|
|
|
|
|
"""Refresh the current data"""
|
|
|
|
|
|
if self.current_file:
|
|
|
|
|
|
self.load_pcap_file(self.current_file)
|
|
|
|
|
|
|
|
|
|
|
|
def closeEvent(self, event):
|
|
|
|
|
|
"""Handle application close"""
|
|
|
|
|
|
if self.loading_thread and self.loading_thread.isRunning():
|
|
|
|
|
|
self.loading_thread.quit()
|
|
|
|
|
|
self.loading_thread.wait()
|
|
|
|
|
|
|
2025-07-26 00:02:25 -04:00
|
|
|
|
event.accept()
|
|
|
|
|
|
|
|
|
|
|
|
# Menu and toolbar action handlers
|
|
|
|
|
|
|
|
|
|
|
|
def monitor_nic(self):
|
|
|
|
|
|
"""Start network interface monitoring"""
|
|
|
|
|
|
# TODO: Implement NIC monitoring dialog
|
|
|
|
|
|
QMessageBox.information(
|
|
|
|
|
|
self,
|
|
|
|
|
|
"Monitor NIC",
|
|
|
|
|
|
"Network interface monitoring will be implemented in a future version."
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
def show_about(self):
|
|
|
|
|
|
"""Show about dialog"""
|
|
|
|
|
|
QMessageBox.about(
|
|
|
|
|
|
self,
|
|
|
|
|
|
"About StreamLens",
|
|
|
|
|
|
"""<h2>StreamLens v1.0</h2>
|
|
|
|
|
|
<p>Advanced Ethernet Traffic Analyzer</p>
|
|
|
|
|
|
<p>Specialized protocol dissection for aviation and industrial networks
|
|
|
|
|
|
with sigma-based outlier identification and interactive signal visualization.</p>
|
|
|
|
|
|
<p><b>Features:</b></p>
|
|
|
|
|
|
<ul>
|
|
|
|
|
|
<li>Chapter 10 (IRIG 106) telemetry protocol support</li>
|
|
|
|
|
|
<li>PTP (IEEE 1588) and IENA protocol dissection</li>
|
|
|
|
|
|
<li>Statistical outlier detection</li>
|
|
|
|
|
|
<li>Interactive signal visualization</li>
|
|
|
|
|
|
<li>Docking panel interface</li>
|
|
|
|
|
|
</ul>
|
|
|
|
|
|
<p>Built with PySide6 and matplotlib</p>"""
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def on_outlier_threshold_changed(self, value: int):
|
|
|
|
|
|
"""Handle outlier threshold change"""
|
|
|
|
|
|
if self.analyzer:
|
|
|
|
|
|
# Update analyzer's outlier threshold
|
|
|
|
|
|
self.analyzer.statistics_engine.sigma_threshold = float(value)
|
|
|
|
|
|
|
|
|
|
|
|
# Refresh the flow data with new threshold
|
|
|
|
|
|
if hasattr(self, 'flow_list_dock'):
|
|
|
|
|
|
self.flow_list_dock.populate_flows_table()
|
|
|
|
|
|
|
|
|
|
|
|
self.status_bar.showMessage(f"Outlier threshold updated to {value}σ")
|