Modern TUI with Enhanced Protocol Hierarchy Interface
Major Features: - Complete modern TUI interface with three focused views - Enhanced multi-column layout: Source | Proto | Destination | Extended | Frame Type | Metrics - Simplified navigation with 1/2/3 hotkeys instead of F1/F2/F3 - Protocol hierarchy: Transport (TCP/UDP) → Extended (CH10/PTP) → Frame Types - Classic TUI preserved with --classic flag Views Implemented: 1. Flow Analysis View: Enhanced multi-column flow overview with protocol detection 2. Packet Decoder View: Three-panel deep inspection (Flows | Frames | Fields) 3. Statistical Analysis View: Four analysis modes with timing and quality metrics Technical Improvements: - Left-aligned text columns with IP:port precision - Transport protocol separation from extended protocols - Frame type identification (CH10-Data, TMATS, PTP Sync) - Cross-view communication with persistent flow selection - Context-sensitive help and status bars - Comprehensive error handling with console fallback
This commit is contained in:
9
analyzer/tui/modern_views/__init__.py
Normal file
9
analyzer/tui/modern_views/__init__.py
Normal file
@@ -0,0 +1,9 @@
|
||||
"""
|
||||
Modern TUI Views for StreamLens
|
||||
"""
|
||||
|
||||
from .flow_analysis import FlowAnalysisView
|
||||
from .packet_decoder import PacketDecoderView
|
||||
from .statistical_analysis import StatisticalAnalysisView
|
||||
|
||||
__all__ = ['FlowAnalysisView', 'PacketDecoderView', 'StatisticalAnalysisView']
|
||||
318
analyzer/tui/modern_views/flow_analysis.py
Normal file
318
analyzer/tui/modern_views/flow_analysis.py
Normal file
@@ -0,0 +1,318 @@
|
||||
"""
|
||||
Flow Analysis View - Visual flow overview with protocol detection
|
||||
Focuses on understanding communication patterns and flow characteristics
|
||||
"""
|
||||
|
||||
import curses
|
||||
from typing import TYPE_CHECKING, List, Optional, Tuple
|
||||
from ...models import FlowStats
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ...analysis.core import EthernetAnalyzer
|
||||
|
||||
|
||||
class FlowAnalysisView:
|
||||
"""
|
||||
Flow Analysis View - F1
|
||||
|
||||
Primary view for understanding network flows:
|
||||
- Flow overview with visual indicators
|
||||
- Protocol detection and classification
|
||||
- Traffic patterns and volume analysis
|
||||
- Enhanced decoder availability
|
||||
"""
|
||||
|
||||
def __init__(self, analyzer: 'EthernetAnalyzer'):
|
||||
self.analyzer = analyzer
|
||||
self.selected_flow = 0
|
||||
self.scroll_offset = 0
|
||||
self.show_frame_types = True
|
||||
|
||||
def draw(self, stdscr, selected_flow_key: Optional[str]):
|
||||
"""Draw the Flow Analysis view"""
|
||||
height, width = stdscr.getmaxyx()
|
||||
start_y = 3 # After header
|
||||
max_height = height - 2 # Reserve for status bar
|
||||
|
||||
flows_list = self._get_flows_list()
|
||||
|
||||
if not flows_list:
|
||||
stdscr.addstr(start_y + 2, 4, "No network flows detected", curses.A_DIM)
|
||||
stdscr.addstr(start_y + 3, 4, "Waiting for packets...", curses.A_DIM)
|
||||
return
|
||||
|
||||
# Flow summary header
|
||||
summary = self.analyzer.get_summary()
|
||||
summary_text = (f"Flows: {summary['unique_flows']} | "
|
||||
f"Packets: {summary['total_packets']} | "
|
||||
f"Endpoints: {summary['unique_ips']}")
|
||||
|
||||
if self.analyzer.is_live:
|
||||
rt_summary = self.analyzer.statistics_engine.get_realtime_summary()
|
||||
summary_text += f" | Outliers: {rt_summary.get('total_outliers', 0)}"
|
||||
|
||||
stdscr.addstr(start_y, 4, summary_text, curses.A_BOLD)
|
||||
|
||||
# Flow analysis area
|
||||
flow_start_y = start_y + 2
|
||||
self._draw_flow_overview(stdscr, flow_start_y, width, max_height - flow_start_y, flows_list)
|
||||
|
||||
def _draw_flow_overview(self, stdscr, start_y: int, width: int, max_height: int, flows_list: List[FlowStats]):
|
||||
"""Draw comprehensive flow overview"""
|
||||
|
||||
# Header
|
||||
stdscr.addstr(start_y, 4, "FLOW ANALYSIS", curses.A_BOLD | curses.A_UNDERLINE)
|
||||
current_y = start_y + 2
|
||||
|
||||
# Column headers with visual indicators
|
||||
headers = (
|
||||
f"{'#':>2} "
|
||||
f"{'Source':20} "
|
||||
f"{'Proto':6} "
|
||||
f"{'Destination':20} "
|
||||
f"{'Extended':10} "
|
||||
f"{'Frame Type':12} "
|
||||
f"{'Pkts':>6} "
|
||||
f"{'Volume':>8} "
|
||||
f"{'Timing':>8} "
|
||||
f"{'Quality':>8}"
|
||||
)
|
||||
stdscr.addstr(current_y, 4, headers, curses.A_UNDERLINE)
|
||||
current_y += 1
|
||||
|
||||
# Calculate visible range
|
||||
visible_flows = max_height - (current_y - start_y) - 2
|
||||
start_idx = self.scroll_offset
|
||||
end_idx = min(start_idx + visible_flows, len(flows_list))
|
||||
|
||||
# Draw flows
|
||||
for i in range(start_idx, end_idx):
|
||||
flow = flows_list[i]
|
||||
display_idx = i - start_idx
|
||||
|
||||
# Flow selection
|
||||
is_selected = (i == self.selected_flow)
|
||||
attr = curses.A_REVERSE if is_selected else curses.A_NORMAL
|
||||
|
||||
# Flow line
|
||||
flow_line = self._format_flow_line(i + 1, flow)
|
||||
stdscr.addstr(current_y + display_idx, 4, flow_line[:width-8], attr)
|
||||
|
||||
# Enhanced indicator
|
||||
if flow.enhanced_analysis.decoder_type != "Standard":
|
||||
stdscr.addstr(current_y + display_idx, 2, "●", curses.A_BOLD | curses.color_pair(1))
|
||||
|
||||
# Frame types sub-display (if selected and enabled)
|
||||
if is_selected and self.show_frame_types and flow.frame_types:
|
||||
sub_y = current_y + display_idx + 1
|
||||
if sub_y < current_y + visible_flows:
|
||||
self._draw_frame_types_compact(stdscr, sub_y, width, flow)
|
||||
|
||||
# Scroll indicators
|
||||
if start_idx > 0:
|
||||
stdscr.addstr(current_y, width - 10, "↑ More", curses.A_DIM)
|
||||
if end_idx < len(flows_list):
|
||||
stdscr.addstr(current_y + visible_flows - 1, width - 10, "↓ More", curses.A_DIM)
|
||||
|
||||
def _format_flow_line(self, flow_num: int, flow: FlowStats) -> str:
|
||||
"""Format a single flow line with comprehensive information"""
|
||||
|
||||
# Source with port (left-aligned)
|
||||
source = f"{flow.src_ip}:{flow.src_port}"
|
||||
if len(source) > 18:
|
||||
source = f"{flow.src_ip[:10]}…:{flow.src_port}"
|
||||
|
||||
# Transport protocol (TCP, UDP, ICMP, IGMP, etc.)
|
||||
protocol = flow.transport_protocol
|
||||
|
||||
# Destination with port (left-aligned)
|
||||
destination = f"{flow.dst_ip}:{flow.dst_port}"
|
||||
if len(destination) > 18:
|
||||
destination = f"{flow.dst_ip[:10]}…:{flow.dst_port}"
|
||||
|
||||
# Extended protocol (Chapter 10, PTP, IENA, etc.)
|
||||
extended_protocol = self._get_extended_protocol(flow)
|
||||
|
||||
# Frame type (most common frame type in this flow)
|
||||
frame_type = self._get_primary_frame_type(flow)
|
||||
|
||||
# Packet count
|
||||
pkt_count = f"{flow.frame_count}"
|
||||
|
||||
# Volume with units
|
||||
volume = self._format_bytes(flow.total_bytes)
|
||||
|
||||
# Timing quality
|
||||
if flow.avg_inter_arrival > 0:
|
||||
timing = f"{flow.avg_inter_arrival*1000:.1f}ms"
|
||||
else:
|
||||
timing = "N/A"
|
||||
|
||||
# Quality score
|
||||
if flow.enhanced_analysis.decoder_type != "Standard":
|
||||
if flow.enhanced_analysis.avg_frame_quality > 0:
|
||||
quality = f"{flow.enhanced_analysis.avg_frame_quality:.0f}%"
|
||||
else:
|
||||
quality = "Enhanced"
|
||||
else:
|
||||
# Check for outliers
|
||||
outlier_pct = len(flow.outlier_frames) / flow.frame_count * 100 if flow.frame_count > 0 else 0
|
||||
if outlier_pct > 5:
|
||||
quality = f"{outlier_pct:.0f}% Out"
|
||||
else:
|
||||
quality = "Normal"
|
||||
|
||||
return (f"{flow_num:>2} "
|
||||
f"{source:20} "
|
||||
f"{protocol:6} "
|
||||
f"{destination:20} "
|
||||
f"{extended_protocol:10} "
|
||||
f"{frame_type:12} "
|
||||
f"{pkt_count:>6} "
|
||||
f"{volume:>8} "
|
||||
f"{timing:>8} "
|
||||
f"{quality:>8}")
|
||||
|
||||
def _draw_frame_types_compact(self, stdscr, y: int, width: int, flow: FlowStats):
|
||||
"""Draw compact frame type breakdown for selected flow"""
|
||||
frame_types = sorted(flow.frame_types.items(), key=lambda x: x[1].count, reverse=True)
|
||||
|
||||
# Compact frame type display
|
||||
type_summary = []
|
||||
for frame_type, ft_stats in frame_types[:4]: # Show top 4
|
||||
type_summary.append(f"{frame_type}({ft_stats.count})")
|
||||
|
||||
if len(frame_types) > 4:
|
||||
type_summary.append(f"+{len(frame_types)-4} more")
|
||||
|
||||
frame_line = f" └─ Frame Types: {', '.join(type_summary)}"
|
||||
stdscr.addstr(y, 4, frame_line[:width-8], curses.A_DIM)
|
||||
|
||||
def _get_primary_protocol(self, flow: FlowStats) -> str:
|
||||
"""Get the most relevant protocol for display"""
|
||||
# Prioritize enhanced protocols
|
||||
if flow.detected_protocol_types:
|
||||
enhanced_protocols = {'CHAPTER10', 'PTP', 'IENA', 'CH10'}
|
||||
found_enhanced = flow.detected_protocol_types & enhanced_protocols
|
||||
if found_enhanced:
|
||||
return list(found_enhanced)[0]
|
||||
|
||||
# Use first detected protocol
|
||||
return list(flow.detected_protocol_types)[0]
|
||||
|
||||
# Fallback to transport protocol
|
||||
return flow.transport_protocol
|
||||
|
||||
def _get_extended_protocol(self, flow: FlowStats) -> str:
|
||||
"""Get extended protocol (Chapter 10, PTP, IENA, etc.)"""
|
||||
if flow.detected_protocol_types:
|
||||
# Look for specialized protocols
|
||||
enhanced_protocols = {'CHAPTER10', 'CH10', 'PTP', 'IENA'}
|
||||
found_enhanced = flow.detected_protocol_types & enhanced_protocols
|
||||
if found_enhanced:
|
||||
protocol = list(found_enhanced)[0]
|
||||
# Simplify display names
|
||||
if protocol in ['CHAPTER10', 'CH10']:
|
||||
return 'CH10'
|
||||
return protocol
|
||||
|
||||
# Check for other common protocols
|
||||
if flow.detected_protocol_types and 'NTP' in flow.detected_protocol_types:
|
||||
return 'NTP'
|
||||
|
||||
return '-'
|
||||
|
||||
def _get_primary_frame_type(self, flow: FlowStats) -> str:
|
||||
"""Get the most common frame type in this flow"""
|
||||
if not flow.frame_types:
|
||||
return '-'
|
||||
|
||||
# Find the frame type with the most packets
|
||||
most_common = max(flow.frame_types.items(), key=lambda x: x[1].count)
|
||||
frame_type = most_common[0]
|
||||
|
||||
# Simplify frame type names for display
|
||||
if frame_type == 'CH10-Data':
|
||||
return 'CH10-Data'
|
||||
elif frame_type == 'TMATS':
|
||||
return 'TMATS'
|
||||
elif frame_type.startswith('PTP-'):
|
||||
return frame_type.replace('PTP-', 'PTP ')[:11] # PTP Sync, PTP Signal
|
||||
elif frame_type == 'UDP':
|
||||
return 'UDP'
|
||||
elif frame_type == 'IGMP':
|
||||
return 'IGMP'
|
||||
else:
|
||||
return frame_type[:11] # Truncate to fit column
|
||||
|
||||
def _format_bytes(self, bytes_count: int) -> str:
|
||||
"""Format byte count with appropriate units"""
|
||||
if bytes_count >= 1_000_000_000:
|
||||
return f"{bytes_count / 1_000_000_000:.1f}GB"
|
||||
elif bytes_count >= 1_000_000:
|
||||
return f"{bytes_count / 1_000_000:.1f}MB"
|
||||
elif bytes_count >= 1_000:
|
||||
return f"{bytes_count / 1_000:.1f}KB"
|
||||
else:
|
||||
return f"{bytes_count}B"
|
||||
|
||||
def _get_flows_list(self) -> List[FlowStats]:
|
||||
"""Get flows sorted by importance for flow analysis"""
|
||||
flows_list = list(self.analyzer.flows.values())
|
||||
|
||||
# Sort by: Enhanced protocols first, then outliers, then packet count
|
||||
flows_list.sort(key=lambda x: (
|
||||
x.enhanced_analysis.decoder_type != "Standard",
|
||||
len(x.outlier_frames),
|
||||
x.frame_count
|
||||
), reverse=True)
|
||||
|
||||
return flows_list
|
||||
|
||||
def handle_input(self, key: int, flows_list: List[FlowStats]) -> str:
|
||||
"""Handle input for Flow Analysis view"""
|
||||
if key == curses.KEY_UP:
|
||||
self.selected_flow = max(0, self.selected_flow - 1)
|
||||
self._adjust_scroll()
|
||||
return 'selection_change'
|
||||
elif key == curses.KEY_DOWN:
|
||||
self.selected_flow = min(len(flows_list) - 1, self.selected_flow + 1)
|
||||
self._adjust_scroll()
|
||||
return 'selection_change'
|
||||
elif key == curses.KEY_PPAGE: # Page Up
|
||||
self.selected_flow = max(0, self.selected_flow - 10)
|
||||
self._adjust_scroll()
|
||||
return 'selection_change'
|
||||
elif key == curses.KEY_NPAGE: # Page Down
|
||||
self.selected_flow = min(len(flows_list) - 1, self.selected_flow + 10)
|
||||
self._adjust_scroll()
|
||||
return 'selection_change'
|
||||
elif key == ord('\n') or key == curses.KEY_ENTER:
|
||||
# Select flow for detailed analysis
|
||||
if flows_list and self.selected_flow < len(flows_list):
|
||||
selected_flow = flows_list[self.selected_flow]
|
||||
# Signal flow selection for other views
|
||||
return 'flow_selected'
|
||||
elif key == ord('v') or key == ord('V'):
|
||||
# Visualize selected flow
|
||||
return 'visualize'
|
||||
elif key == ord('d') or key == ord('D'):
|
||||
# Switch to decoder view for selected flow
|
||||
return 'decode_flow'
|
||||
elif key == ord('t') or key == ord('T'):
|
||||
# Toggle frame types display
|
||||
self.show_frame_types = not self.show_frame_types
|
||||
return 'toggle_frame_types'
|
||||
|
||||
return 'none'
|
||||
|
||||
def _adjust_scroll(self):
|
||||
"""Adjust scroll position to keep selected item visible"""
|
||||
# This would be implemented based on visible area calculations
|
||||
pass
|
||||
|
||||
def get_selected_flow(self, flows_list: List[FlowStats]) -> Optional[FlowStats]:
|
||||
"""Get currently selected flow"""
|
||||
if flows_list and 0 <= self.selected_flow < len(flows_list):
|
||||
return flows_list[self.selected_flow]
|
||||
return None
|
||||
307
analyzer/tui/modern_views/packet_decoder.py
Normal file
307
analyzer/tui/modern_views/packet_decoder.py
Normal file
@@ -0,0 +1,307 @@
|
||||
"""
|
||||
Packet Decoder View - Deep protocol inspection and field extraction
|
||||
Focuses on understanding packet contents and protocol compliance
|
||||
"""
|
||||
|
||||
import curses
|
||||
from typing import TYPE_CHECKING, List, Optional, Dict, Any
|
||||
from ...models import FlowStats
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ...analysis.core import EthernetAnalyzer
|
||||
|
||||
|
||||
class PacketDecoderView:
|
||||
"""
|
||||
Packet Decoder View - F2
|
||||
|
||||
Deep packet inspection interface:
|
||||
- Protocol field extraction and display
|
||||
- Frame-by-frame analysis
|
||||
- Enhanced decoder output
|
||||
- Field value inspection
|
||||
"""
|
||||
|
||||
def __init__(self, analyzer: 'EthernetAnalyzer'):
|
||||
self.analyzer = analyzer
|
||||
self.selected_flow = 0
|
||||
self.selected_frame = 0
|
||||
self.selected_field = 0
|
||||
self.scroll_offset = 0
|
||||
self.field_scroll_offset = 0
|
||||
self.current_panel = 0 # 0=flows, 1=frames, 2=fields
|
||||
|
||||
def draw(self, stdscr, selected_flow_key: Optional[str]):
|
||||
"""Draw the Packet Decoder view"""
|
||||
height, width = stdscr.getmaxyx()
|
||||
start_y = 3
|
||||
max_height = height - 2
|
||||
|
||||
flows_list = self._get_enhanced_flows()
|
||||
|
||||
if not flows_list:
|
||||
stdscr.addstr(start_y + 2, 4, "No enhanced decodable flows detected", curses.A_DIM)
|
||||
stdscr.addstr(start_y + 3, 4, "Switch to Flow Analysis (F1) to see all flows", curses.A_DIM)
|
||||
return
|
||||
|
||||
# Decoder view header
|
||||
stdscr.addstr(start_y, 4, "PACKET DECODER - Enhanced Protocol Analysis", curses.A_BOLD)
|
||||
|
||||
# Three-panel layout: Flows | Frames | Fields
|
||||
panel_width = width // 3
|
||||
|
||||
# Panel 1: Enhanced Flows (left)
|
||||
self._draw_flows_panel(stdscr, start_y + 2, 4, panel_width - 2, max_height - start_y - 2, flows_list)
|
||||
|
||||
# Separator
|
||||
for y in range(start_y + 2, max_height):
|
||||
stdscr.addstr(y, panel_width, "│", curses.A_DIM)
|
||||
|
||||
# Panel 2: Frame Details (center)
|
||||
selected_flow = flows_list[self.selected_flow] if flows_list else None
|
||||
self._draw_frames_panel(stdscr, start_y + 2, panel_width + 2, panel_width - 2, max_height - start_y - 2, selected_flow)
|
||||
|
||||
# Separator
|
||||
for y in range(start_y + 2, max_height):
|
||||
stdscr.addstr(y, 2 * panel_width, "│", curses.A_DIM)
|
||||
|
||||
# Panel 3: Field Inspection (right)
|
||||
self._draw_fields_panel(stdscr, start_y + 2, 2 * panel_width + 2, panel_width - 2, max_height - start_y - 2, selected_flow)
|
||||
|
||||
def _draw_flows_panel(self, stdscr, start_y: int, start_x: int, width: int, height: int, flows_list: List[FlowStats]):
|
||||
"""Draw enhanced flows panel"""
|
||||
panel_attr = curses.A_BOLD if self.current_panel == 0 else curses.A_NORMAL
|
||||
|
||||
# Panel header
|
||||
header = "Enhanced Flows"
|
||||
stdscr.addstr(start_y, start_x, header, panel_attr | curses.A_UNDERLINE)
|
||||
|
||||
current_y = start_y + 2
|
||||
available_height = height - 2
|
||||
|
||||
# Flow list
|
||||
for i, flow in enumerate(flows_list[:available_height]):
|
||||
is_selected = (i == self.selected_flow and self.current_panel == 0)
|
||||
attr = curses.A_REVERSE if is_selected else curses.A_NORMAL
|
||||
|
||||
# Flow summary line
|
||||
flow_line = self._format_flow_summary(flow, width - 2)
|
||||
stdscr.addstr(current_y + i, start_x, flow_line, attr)
|
||||
|
||||
# Decoder indicator
|
||||
decoder_indicator = "●" if flow.enhanced_analysis.decoder_type != "Standard" else "○"
|
||||
stdscr.addstr(current_y + i, start_x + width - 3, decoder_indicator,
|
||||
curses.A_BOLD if flow.enhanced_analysis.decoder_type != "Standard" else curses.A_DIM)
|
||||
|
||||
def _draw_frames_panel(self, stdscr, start_y: int, start_x: int, width: int, height: int, flow: Optional[FlowStats]):
|
||||
"""Draw frame details panel"""
|
||||
panel_attr = curses.A_BOLD if self.current_panel == 1 else curses.A_NORMAL
|
||||
|
||||
# Panel header
|
||||
header = "Frame Analysis"
|
||||
stdscr.addstr(start_y, start_x, header, panel_attr | curses.A_UNDERLINE)
|
||||
|
||||
if not flow:
|
||||
stdscr.addstr(start_y + 2, start_x, "No flow selected", curses.A_DIM)
|
||||
return
|
||||
|
||||
current_y = start_y + 2
|
||||
|
||||
# Flow details
|
||||
stdscr.addstr(current_y, start_x, f"Flow: {flow.src_ip} → {flow.dst_ip}", curses.A_BOLD)
|
||||
current_y += 1
|
||||
stdscr.addstr(current_y, start_x, f"Decoder: {flow.enhanced_analysis.decoder_type}")
|
||||
current_y += 2
|
||||
|
||||
# Sample frames
|
||||
if flow.enhanced_analysis.sample_decoded_fields:
|
||||
stdscr.addstr(current_y, start_x, "Decoded Frames:", curses.A_UNDERLINE)
|
||||
current_y += 1
|
||||
|
||||
for i, (frame_key, frame_data) in enumerate(flow.enhanced_analysis.sample_decoded_fields.items()):
|
||||
is_selected = (i == self.selected_frame and self.current_panel == 1)
|
||||
attr = curses.A_REVERSE if is_selected else curses.A_NORMAL
|
||||
|
||||
frame_line = f"{frame_key}: {len(frame_data)} fields"
|
||||
stdscr.addstr(current_y + i, start_x, frame_line[:width-2], attr)
|
||||
else:
|
||||
stdscr.addstr(current_y, start_x, "No decoded frame data available", curses.A_DIM)
|
||||
current_y += 1
|
||||
stdscr.addstr(current_y, start_x, "Decoder may not be implemented", curses.A_DIM)
|
||||
|
||||
def _draw_fields_panel(self, stdscr, start_y: int, start_x: int, width: int, height: int, flow: Optional[FlowStats]):
|
||||
"""Draw field inspection panel"""
|
||||
panel_attr = curses.A_BOLD if self.current_panel == 2 else curses.A_NORMAL
|
||||
|
||||
# Panel header
|
||||
header = "Field Inspector"
|
||||
stdscr.addstr(start_y, start_x, header, panel_attr | curses.A_UNDERLINE)
|
||||
|
||||
if not flow or not flow.enhanced_analysis.sample_decoded_fields:
|
||||
stdscr.addstr(start_y + 2, start_x, "No field data available", curses.A_DIM)
|
||||
return
|
||||
|
||||
current_y = start_y + 2
|
||||
|
||||
# Get selected frame data
|
||||
frame_items = list(flow.enhanced_analysis.sample_decoded_fields.items())
|
||||
if self.selected_frame < len(frame_items):
|
||||
frame_key, frame_data = frame_items[self.selected_frame]
|
||||
|
||||
stdscr.addstr(current_y, start_x, f"Frame: {frame_key}", curses.A_BOLD)
|
||||
current_y += 2
|
||||
|
||||
# Field list with values
|
||||
available_height = height - 4
|
||||
field_items = list(frame_data.items())
|
||||
|
||||
start_field = self.field_scroll_offset
|
||||
end_field = min(start_field + available_height, len(field_items))
|
||||
|
||||
for i in range(start_field, end_field):
|
||||
field_name, field_value = field_items[i]
|
||||
display_idx = i - start_field
|
||||
|
||||
is_selected = (i == self.selected_field and self.current_panel == 2)
|
||||
attr = curses.A_REVERSE if is_selected else curses.A_NORMAL
|
||||
|
||||
# Format field line
|
||||
field_line = self._format_field_line(field_name, field_value, width - 2)
|
||||
stdscr.addstr(current_y + display_idx, start_x, field_line, attr)
|
||||
|
||||
# Scroll indicators
|
||||
if start_field > 0:
|
||||
stdscr.addstr(current_y, start_x + width - 5, "↑", curses.A_DIM)
|
||||
if end_field < len(field_items):
|
||||
stdscr.addstr(current_y + available_height - 1, start_x + width - 5, "↓", curses.A_DIM)
|
||||
|
||||
def _format_flow_summary(self, flow: FlowStats, max_width: int) -> str:
|
||||
"""Format flow summary for flows panel"""
|
||||
# Format as "src:port → dst:port | protocol"
|
||||
source = f"{flow.src_ip}:{flow.src_port}"
|
||||
destination = f"{flow.dst_ip}:{flow.dst_port}"
|
||||
|
||||
protocol = flow.enhanced_analysis.decoder_type
|
||||
if protocol == "Standard":
|
||||
protocol = self._get_primary_protocol(flow)
|
||||
|
||||
# Calculate available space for src and dst
|
||||
protocol_space = len(protocol) + 3 # " | " + protocol
|
||||
available_space = max_width - protocol_space
|
||||
src_space = available_space // 2 - 2 # Account for " → "
|
||||
dst_space = available_space - src_space - 3 # " → "
|
||||
|
||||
if len(source) > src_space:
|
||||
source = f"{flow.src_ip[:src_space-6]}…:{flow.src_port}"
|
||||
if len(destination) > dst_space:
|
||||
destination = f"{flow.dst_ip[:dst_space-6]}…:{flow.dst_port}"
|
||||
|
||||
return f"{source} → {destination} | {protocol}"[:max_width]
|
||||
|
||||
def _format_field_line(self, field_name: str, field_value: Any, max_width: int) -> str:
|
||||
"""Format field name and value for display"""
|
||||
# Clean up field name
|
||||
display_name = field_name.replace('_', ' ').title()
|
||||
|
||||
# Format value based on type
|
||||
if isinstance(field_value, bool):
|
||||
display_value = "Yes" if field_value else "No"
|
||||
elif isinstance(field_value, float):
|
||||
if "timestamp" in field_name.lower():
|
||||
display_value = f"{field_value:.6f}s"
|
||||
else:
|
||||
display_value = f"{field_value:.3f}"
|
||||
elif field_value is None:
|
||||
display_value = "N/A"
|
||||
else:
|
||||
display_value = str(field_value)
|
||||
|
||||
# Truncate if needed
|
||||
available_name_width = max_width // 2
|
||||
available_value_width = max_width - available_name_width - 3
|
||||
|
||||
if len(display_name) > available_name_width:
|
||||
display_name = display_name[:available_name_width-1] + "…"
|
||||
if len(display_value) > available_value_width:
|
||||
display_value = display_value[:available_value_width-1] + "…"
|
||||
|
||||
return f"{display_name:<{available_name_width}} : {display_value}"
|
||||
|
||||
def _get_enhanced_flows(self) -> List[FlowStats]:
|
||||
"""Get flows with enhanced decoders available"""
|
||||
flows_list = []
|
||||
for flow in self.analyzer.flows.values():
|
||||
if (flow.enhanced_analysis.decoder_type != "Standard" or
|
||||
"CHAPTER10" in flow.detected_protocol_types or
|
||||
"PTP" in flow.detected_protocol_types or
|
||||
"IENA" in flow.detected_protocol_types):
|
||||
flows_list.append(flow)
|
||||
|
||||
# Sort by decoder quality and packet count
|
||||
flows_list.sort(key=lambda x: (
|
||||
x.enhanced_analysis.decoder_type != "Standard",
|
||||
len(x.enhanced_analysis.sample_decoded_fields),
|
||||
x.frame_count
|
||||
), reverse=True)
|
||||
|
||||
return flows_list
|
||||
|
||||
def _get_primary_protocol(self, flow: FlowStats) -> str:
|
||||
"""Get primary protocol for display"""
|
||||
if flow.detected_protocol_types:
|
||||
enhanced_protocols = {'CHAPTER10', 'PTP', 'IENA', 'CH10'}
|
||||
found_enhanced = flow.detected_protocol_types & enhanced_protocols
|
||||
if found_enhanced:
|
||||
return list(found_enhanced)[0]
|
||||
return list(flow.detected_protocol_types)[0]
|
||||
return flow.transport_protocol
|
||||
|
||||
def handle_input(self, key: int, flows_list: List[FlowStats]) -> str:
|
||||
"""Handle input for Packet Decoder view"""
|
||||
enhanced_flows = self._get_enhanced_flows()
|
||||
|
||||
if key == ord('\t'): # Tab to switch panels
|
||||
self.current_panel = (self.current_panel + 1) % 3
|
||||
return 'panel_switch'
|
||||
elif key == curses.KEY_UP:
|
||||
if self.current_panel == 0: # Flows panel
|
||||
self.selected_flow = max(0, self.selected_flow - 1)
|
||||
elif self.current_panel == 1: # Frames panel
|
||||
self.selected_frame = max(0, self.selected_frame - 1)
|
||||
elif self.current_panel == 2: # Fields panel
|
||||
self.selected_field = max(0, self.selected_field - 1)
|
||||
self._adjust_field_scroll()
|
||||
return 'selection_change'
|
||||
elif key == curses.KEY_DOWN:
|
||||
if self.current_panel == 0: # Flows panel
|
||||
self.selected_flow = min(len(enhanced_flows) - 1, self.selected_flow + 1)
|
||||
elif self.current_panel == 1: # Frames panel
|
||||
if enhanced_flows and self.selected_flow < len(enhanced_flows):
|
||||
flow = enhanced_flows[self.selected_flow]
|
||||
max_frames = len(flow.enhanced_analysis.sample_decoded_fields) - 1
|
||||
self.selected_frame = min(max_frames, self.selected_frame + 1)
|
||||
elif self.current_panel == 2: # Fields panel
|
||||
if enhanced_flows and self.selected_flow < len(enhanced_flows):
|
||||
flow = enhanced_flows[self.selected_flow]
|
||||
if flow.enhanced_analysis.sample_decoded_fields:
|
||||
frame_items = list(flow.enhanced_analysis.sample_decoded_fields.items())
|
||||
if self.selected_frame < len(frame_items):
|
||||
frame_data = frame_items[self.selected_frame][1]
|
||||
max_fields = len(frame_data) - 1
|
||||
self.selected_field = min(max_fields, self.selected_field + 1)
|
||||
self._adjust_field_scroll()
|
||||
return 'selection_change'
|
||||
elif key == ord('e') or key == ord('E'):
|
||||
return 'export_fields'
|
||||
elif key == ord('c') or key == ord('C'):
|
||||
return 'copy_field'
|
||||
|
||||
return 'none'
|
||||
|
||||
def _adjust_field_scroll(self):
|
||||
"""Adjust field scroll to keep selected field visible"""
|
||||
# Simple scroll adjustment - could be enhanced
|
||||
if self.selected_field < self.field_scroll_offset:
|
||||
self.field_scroll_offset = self.selected_field
|
||||
elif self.selected_field >= self.field_scroll_offset + 10: # Assume 10 visible fields
|
||||
self.field_scroll_offset = self.selected_field - 9
|
||||
432
analyzer/tui/modern_views/statistical_analysis.py
Normal file
432
analyzer/tui/modern_views/statistical_analysis.py
Normal file
@@ -0,0 +1,432 @@
|
||||
"""
|
||||
Statistical Analysis View - Timing analysis, outliers, and quality metrics
|
||||
Focuses on understanding network performance and data quality
|
||||
"""
|
||||
|
||||
import curses
|
||||
import statistics
|
||||
from typing import TYPE_CHECKING, List, Optional, Dict, Tuple
|
||||
from ...models import FlowStats
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ...analysis.core import EthernetAnalyzer
|
||||
|
||||
|
||||
class StatisticalAnalysisView:
|
||||
"""
|
||||
Statistical Analysis View - F3
|
||||
|
||||
Performance and quality analysis interface:
|
||||
- Timing statistics and outlier detection
|
||||
- Quality metrics and trends
|
||||
- Performance indicators
|
||||
- Network health assessment
|
||||
"""
|
||||
|
||||
def __init__(self, analyzer: 'EthernetAnalyzer'):
|
||||
self.analyzer = analyzer
|
||||
self.selected_flow = 0
|
||||
self.analysis_mode = 0 # 0=overview, 1=outliers, 2=quality, 3=timing
|
||||
self.scroll_offset = 0
|
||||
|
||||
def draw(self, stdscr, selected_flow_key: Optional[str]):
|
||||
"""Draw the Statistical Analysis view"""
|
||||
height, width = stdscr.getmaxyx()
|
||||
start_y = 3
|
||||
max_height = height - 2
|
||||
|
||||
flows_list = self._get_flows_list()
|
||||
|
||||
if not flows_list:
|
||||
stdscr.addstr(start_y + 2, 4, "No flows available for statistical analysis", curses.A_DIM)
|
||||
return
|
||||
|
||||
# Statistical analysis header
|
||||
mode_names = ["Overview", "Outlier Analysis", "Quality Metrics", "Timing Analysis"]
|
||||
current_mode = mode_names[self.analysis_mode]
|
||||
stdscr.addstr(start_y, 4, f"STATISTICAL ANALYSIS - {current_mode}", curses.A_BOLD)
|
||||
|
||||
# Mode selector
|
||||
mode_line = start_y + 1
|
||||
for i, mode_name in enumerate(mode_names):
|
||||
x_pos = 4 + i * 20
|
||||
if i == self.analysis_mode:
|
||||
stdscr.addstr(mode_line, x_pos, f"[{mode_name}]", curses.A_REVERSE)
|
||||
else:
|
||||
stdscr.addstr(mode_line, x_pos, f" {mode_name} ", curses.A_DIM)
|
||||
|
||||
# Analysis content area
|
||||
content_y = start_y + 3
|
||||
content_height = max_height - content_y
|
||||
|
||||
if self.analysis_mode == 0:
|
||||
self._draw_overview(stdscr, content_y, width, content_height, flows_list)
|
||||
elif self.analysis_mode == 1:
|
||||
self._draw_outlier_analysis(stdscr, content_y, width, content_height, flows_list)
|
||||
elif self.analysis_mode == 2:
|
||||
self._draw_quality_metrics(stdscr, content_y, width, content_height, flows_list)
|
||||
elif self.analysis_mode == 3:
|
||||
self._draw_timing_analysis(stdscr, content_y, width, content_height, flows_list)
|
||||
|
||||
def _draw_overview(self, stdscr, start_y: int, width: int, height: int, flows_list: List[FlowStats]):
|
||||
"""Draw statistical overview"""
|
||||
current_y = start_y
|
||||
|
||||
# Overall statistics
|
||||
total_packets = sum(flow.frame_count for flow in flows_list)
|
||||
total_outliers = sum(len(flow.outlier_frames) for flow in flows_list)
|
||||
outlier_percentage = (total_outliers / total_packets * 100) if total_packets > 0 else 0
|
||||
|
||||
stdscr.addstr(current_y, 4, "NETWORK PERFORMANCE SUMMARY", curses.A_UNDERLINE)
|
||||
current_y += 2
|
||||
|
||||
# Key metrics
|
||||
metrics = [
|
||||
("Total Flows", str(len(flows_list))),
|
||||
("Total Packets", f"{total_packets:,}"),
|
||||
("Total Outliers", f"{total_outliers:,} ({outlier_percentage:.2f}%)"),
|
||||
("Enhanced Flows", str(sum(1 for f in flows_list if f.enhanced_analysis.decoder_type != "Standard"))),
|
||||
]
|
||||
|
||||
for metric_name, metric_value in metrics:
|
||||
stdscr.addstr(current_y, 4, f"{metric_name:20}: {metric_value}")
|
||||
current_y += 1
|
||||
|
||||
current_y += 1
|
||||
|
||||
# Flow performance table
|
||||
stdscr.addstr(current_y, 4, "FLOW PERFORMANCE RANKING", curses.A_UNDERLINE)
|
||||
current_y += 2
|
||||
|
||||
# Table header
|
||||
header = f"{'Rank':>4} {'Flow':30} {'Packets':>8} {'Outliers':>9} {'Avg Δt':>10} {'Jitter':>8} {'Score':>6}"
|
||||
stdscr.addstr(current_y, 4, header, curses.A_BOLD)
|
||||
current_y += 1
|
||||
|
||||
# Rank flows by performance
|
||||
ranked_flows = self._rank_flows_by_performance(flows_list)
|
||||
|
||||
visible_flows = min(height - (current_y - start_y) - 2, len(ranked_flows))
|
||||
for i in range(visible_flows):
|
||||
flow, score = ranked_flows[i]
|
||||
|
||||
is_selected = (i == self.selected_flow)
|
||||
attr = curses.A_REVERSE if is_selected else curses.A_NORMAL
|
||||
|
||||
# Format flow line
|
||||
flow_desc = f"{flow.src_ip}:{flow.src_port} → {flow.dst_ip}:{flow.dst_port}"
|
||||
if len(flow_desc) > 28:
|
||||
flow_desc = f"{flow.src_ip[:8]}…:{flow.src_port} → {flow.dst_ip[:8]}…:{flow.dst_port}"
|
||||
|
||||
outliers = len(flow.outlier_frames)
|
||||
outlier_pct = f"{outliers/flow.frame_count*100:.1f}%" if flow.frame_count > 0 else "0%"
|
||||
|
||||
avg_timing = f"{flow.avg_inter_arrival*1000:.1f}ms" if flow.avg_inter_arrival > 0 else "N/A"
|
||||
jitter = f"{flow.std_inter_arrival*1000:.1f}ms" if flow.std_inter_arrival > 0 else "N/A"
|
||||
|
||||
line = f"{i+1:>4} {flow_desc:30} {flow.frame_count:>8} {outlier_pct:>9} {avg_timing:>10} {jitter:>8} {score:>6.1f}"
|
||||
stdscr.addstr(current_y + i, 4, line[:width-8], attr)
|
||||
|
||||
def _draw_outlier_analysis(self, stdscr, start_y: int, width: int, height: int, flows_list: List[FlowStats]):
|
||||
"""Draw detailed outlier analysis"""
|
||||
current_y = start_y
|
||||
|
||||
stdscr.addstr(current_y, 4, "OUTLIER ANALYSIS", curses.A_UNDERLINE)
|
||||
current_y += 2
|
||||
|
||||
# Find flows with outliers
|
||||
outlier_flows = [(flow, len(flow.outlier_frames)) for flow in flows_list if flow.outlier_frames]
|
||||
outlier_flows.sort(key=lambda x: x[1], reverse=True)
|
||||
|
||||
if not outlier_flows:
|
||||
stdscr.addstr(current_y, 4, "No outliers detected in any flows", curses.A_DIM)
|
||||
stdscr.addstr(current_y + 1, 4, "All packet timing appears normal", curses.A_DIM)
|
||||
return
|
||||
|
||||
# Outlier summary
|
||||
total_outliers = sum(count for _, count in outlier_flows)
|
||||
stdscr.addstr(current_y, 4, f"Flows with outliers: {len(outlier_flows)}")
|
||||
current_y += 1
|
||||
stdscr.addstr(current_y, 4, f"Total outlier packets: {total_outliers}")
|
||||
current_y += 2
|
||||
|
||||
# Detailed outlier breakdown
|
||||
stdscr.addstr(current_y, 4, "OUTLIER DETAILS", curses.A_BOLD)
|
||||
current_y += 1
|
||||
|
||||
header = f"{'Flow':35} {'Outliers':>9} {'Rate':>8} {'Max Σ':>8} {'Timing':>12}"
|
||||
stdscr.addstr(current_y, 4, header, curses.A_UNDERLINE)
|
||||
current_y += 1
|
||||
|
||||
visible_flows = min(height - (current_y - start_y) - 2, len(outlier_flows))
|
||||
for i in range(visible_flows):
|
||||
flow, outlier_count = outlier_flows[i]
|
||||
|
||||
is_selected = (i == self.selected_flow)
|
||||
attr = curses.A_REVERSE if is_selected else curses.A_NORMAL
|
||||
|
||||
flow_desc = f"{flow.src_ip}:{flow.src_port} → {flow.dst_ip}:{flow.dst_port}"
|
||||
if len(flow_desc) > 33:
|
||||
flow_desc = f"{flow.src_ip[:10]}…:{flow.src_port} → {flow.dst_ip[:10]}…:{flow.dst_port}"
|
||||
|
||||
outlier_rate = f"{outlier_count/flow.frame_count*100:.1f}%" if flow.frame_count > 0 else "0%"
|
||||
max_sigma = self.analyzer.statistics_engine.get_max_sigma_deviation(flow)
|
||||
timing_info = f"{flow.avg_inter_arrival*1000:.1f}±{flow.std_inter_arrival*1000:.1f}ms"
|
||||
|
||||
line = f"{flow_desc:35} {outlier_count:>9} {outlier_rate:>8} {max_sigma:>7.1f}σ {timing_info:>12}"
|
||||
stdscr.addstr(current_y + i, 4, line[:width-8], attr)
|
||||
|
||||
# Selected flow outlier details
|
||||
if outlier_flows and self.selected_flow < len(outlier_flows):
|
||||
selected_flow, _ = outlier_flows[self.selected_flow]
|
||||
self._draw_selected_flow_outliers(stdscr, current_y + visible_flows + 1, width,
|
||||
height - (current_y + visible_flows + 1 - start_y), selected_flow)
|
||||
|
||||
def _draw_quality_metrics(self, stdscr, start_y: int, width: int, height: int, flows_list: List[FlowStats]):
|
||||
"""Draw quality metrics analysis"""
|
||||
current_y = start_y
|
||||
|
||||
stdscr.addstr(current_y, 4, "QUALITY METRICS", curses.A_UNDERLINE)
|
||||
current_y += 2
|
||||
|
||||
# Enhanced flows quality
|
||||
enhanced_flows = [f for f in flows_list if f.enhanced_analysis.decoder_type != "Standard"]
|
||||
|
||||
if enhanced_flows:
|
||||
stdscr.addstr(current_y, 4, "ENHANCED DECODER QUALITY", curses.A_BOLD)
|
||||
current_y += 1
|
||||
|
||||
header = f"{'Flow':30} {'Decoder':15} {'Quality':>8} {'Drift':>10} {'Errors':>8}"
|
||||
stdscr.addstr(current_y, 4, header, curses.A_UNDERLINE)
|
||||
current_y += 1
|
||||
|
||||
for i, flow in enumerate(enhanced_flows[:height - (current_y - start_y) - 5]):
|
||||
is_selected = (i == self.selected_flow)
|
||||
attr = curses.A_REVERSE if is_selected else curses.A_NORMAL
|
||||
|
||||
flow_desc = f"{flow.src_ip}:{flow.src_port} → {flow.dst_ip}:{flow.dst_port}"
|
||||
if len(flow_desc) > 28:
|
||||
flow_desc = f"{flow.src_ip[:8]}…:{flow.src_port} → {flow.dst_ip[:8]}…:{flow.dst_port}"
|
||||
|
||||
enhanced = flow.enhanced_analysis
|
||||
decoder_type = enhanced.decoder_type.replace("_Enhanced", "")
|
||||
quality = f"{enhanced.avg_frame_quality:.1f}%" if enhanced.avg_frame_quality > 0 else "N/A"
|
||||
drift = f"{enhanced.avg_clock_drift_ppm:.1f}ppm" if enhanced.avg_clock_drift_ppm != 0 else "N/A"
|
||||
|
||||
error_count = (enhanced.rtc_sync_errors + enhanced.format_errors +
|
||||
enhanced.overflow_errors + enhanced.sequence_gaps)
|
||||
|
||||
line = f"{flow_desc:30} {decoder_type:15} {quality:>8} {drift:>10} {error_count:>8}"
|
||||
stdscr.addstr(current_y + i, 4, line[:width-8], attr)
|
||||
|
||||
current_y += len(enhanced_flows) + 2
|
||||
|
||||
# General quality indicators
|
||||
stdscr.addstr(current_y, 4, "GENERAL QUALITY INDICATORS", curses.A_BOLD)
|
||||
current_y += 1
|
||||
|
||||
# Calculate network health metrics
|
||||
health_metrics = self._calculate_health_metrics(flows_list)
|
||||
|
||||
for metric_name, metric_value, status in health_metrics:
|
||||
status_color = curses.A_BOLD if status == "GOOD" else curses.A_DIM if status == "WARNING" else curses.A_REVERSE
|
||||
stdscr.addstr(current_y, 4, f"{metric_name:25}: {metric_value:15} [{status}]", status_color)
|
||||
current_y += 1
|
||||
|
||||
def _draw_timing_analysis(self, stdscr, start_y: int, width: int, height: int, flows_list: List[FlowStats]):
|
||||
"""Draw detailed timing analysis"""
|
||||
current_y = start_y
|
||||
|
||||
stdscr.addstr(current_y, 4, "TIMING ANALYSIS", curses.A_UNDERLINE)
|
||||
current_y += 2
|
||||
|
||||
# Timing distribution summary
|
||||
all_inter_arrivals = []
|
||||
for flow in flows_list:
|
||||
all_inter_arrivals.extend(flow.inter_arrival_times)
|
||||
|
||||
if all_inter_arrivals:
|
||||
mean_timing = statistics.mean(all_inter_arrivals)
|
||||
median_timing = statistics.median(all_inter_arrivals)
|
||||
std_timing = statistics.stdev(all_inter_arrivals) if len(all_inter_arrivals) > 1 else 0
|
||||
|
||||
stdscr.addstr(current_y, 4, "NETWORK TIMING DISTRIBUTION", curses.A_BOLD)
|
||||
current_y += 1
|
||||
|
||||
timing_stats = [
|
||||
("Mean Inter-arrival", f"{mean_timing*1000:.3f} ms"),
|
||||
("Median Inter-arrival", f"{median_timing*1000:.3f} ms"),
|
||||
("Standard Deviation", f"{std_timing*1000:.3f} ms"),
|
||||
("Coefficient of Variation", f"{std_timing/mean_timing:.3f}" if mean_timing > 0 else "N/A"),
|
||||
]
|
||||
|
||||
for stat_name, stat_value in timing_stats:
|
||||
stdscr.addstr(current_y, 4, f"{stat_name:25}: {stat_value}")
|
||||
current_y += 1
|
||||
|
||||
current_y += 1
|
||||
|
||||
# Per-flow timing details
|
||||
stdscr.addstr(current_y, 4, "PER-FLOW TIMING ANALYSIS", curses.A_BOLD)
|
||||
current_y += 1
|
||||
|
||||
header = f"{'Flow':30} {'Mean':>10} {'Std Dev':>10} {'CV':>8} {'Range':>12}"
|
||||
stdscr.addstr(current_y, 4, header, curses.A_UNDERLINE)
|
||||
current_y += 1
|
||||
|
||||
# Sort flows by timing variability
|
||||
timing_flows = [(flow, flow.std_inter_arrival / flow.avg_inter_arrival if flow.avg_inter_arrival > 0 else 0)
|
||||
for flow in flows_list if flow.inter_arrival_times]
|
||||
timing_flows.sort(key=lambda x: x[1], reverse=True)
|
||||
|
||||
visible_flows = min(height - (current_y - start_y) - 2, len(timing_flows))
|
||||
for i in range(visible_flows):
|
||||
flow, cv = timing_flows[i]
|
||||
|
||||
is_selected = (i == self.selected_flow)
|
||||
attr = curses.A_REVERSE if is_selected else curses.A_NORMAL
|
||||
|
||||
flow_desc = f"{flow.src_ip}:{flow.src_port} → {flow.dst_ip}:{flow.dst_port}"
|
||||
if len(flow_desc) > 28:
|
||||
flow_desc = f"{flow.src_ip[:8]}…:{flow.src_port} → {flow.dst_ip[:8]}…:{flow.dst_port}"
|
||||
|
||||
mean_ms = f"{flow.avg_inter_arrival*1000:.1f}ms"
|
||||
std_ms = f"{flow.std_inter_arrival*1000:.1f}ms"
|
||||
cv_str = f"{cv:.3f}"
|
||||
|
||||
if flow.inter_arrival_times:
|
||||
range_ms = f"{(max(flow.inter_arrival_times) - min(flow.inter_arrival_times))*1000:.1f}ms"
|
||||
else:
|
||||
range_ms = "N/A"
|
||||
|
||||
line = f"{flow_desc:30} {mean_ms:>10} {std_ms:>10} {cv_str:>8} {range_ms:>12}"
|
||||
stdscr.addstr(current_y + i, 4, line[:width-8], attr)
|
||||
|
||||
def _rank_flows_by_performance(self, flows_list: List[FlowStats]) -> List[Tuple[FlowStats, float]]:
|
||||
"""Rank flows by performance score (lower is better)"""
|
||||
ranked = []
|
||||
|
||||
for flow in flows_list:
|
||||
score = 0.0
|
||||
|
||||
# Outlier penalty (higher percentage = higher score)
|
||||
if flow.frame_count > 0:
|
||||
outlier_rate = len(flow.outlier_frames) / flow.frame_count
|
||||
score += outlier_rate * 100 # 0-100 points
|
||||
|
||||
# Timing variability penalty
|
||||
if flow.avg_inter_arrival > 0:
|
||||
cv = flow.std_inter_arrival / flow.avg_inter_arrival
|
||||
score += cv * 50 # 0-50+ points
|
||||
|
||||
# Enhanced decoder bonus (negative score)
|
||||
if flow.enhanced_analysis.decoder_type != "Standard":
|
||||
score -= 10
|
||||
if flow.enhanced_analysis.avg_frame_quality > 80:
|
||||
score -= 5 # Good quality bonus
|
||||
|
||||
ranked.append((flow, score))
|
||||
|
||||
ranked.sort(key=lambda x: x[1]) # Lower scores first (better performance)
|
||||
return ranked
|
||||
|
||||
def _calculate_health_metrics(self, flows_list: List[FlowStats]) -> List[Tuple[str, str, str]]:
|
||||
"""Calculate network health metrics"""
|
||||
metrics = []
|
||||
|
||||
# Overall outlier rate
|
||||
total_packets = sum(flow.frame_count for flow in flows_list)
|
||||
total_outliers = sum(len(flow.outlier_frames) for flow in flows_list)
|
||||
outlier_rate = (total_outliers / total_packets * 100) if total_packets > 0 else 0
|
||||
|
||||
outlier_status = "GOOD" if outlier_rate < 1.0 else "WARNING" if outlier_rate < 5.0 else "CRITICAL"
|
||||
metrics.append(("Network Outlier Rate", f"{outlier_rate:.2f}%", outlier_status))
|
||||
|
||||
# Enhanced decoder coverage
|
||||
enhanced_count = sum(1 for f in flows_list if f.enhanced_analysis.decoder_type != "Standard")
|
||||
coverage = (enhanced_count / len(flows_list) * 100) if flows_list else 0
|
||||
coverage_status = "GOOD" if coverage > 50 else "WARNING" if coverage > 0 else "NONE"
|
||||
metrics.append(("Enhanced Coverage", f"{coverage:.1f}%", coverage_status))
|
||||
|
||||
# Timing consistency
|
||||
all_cvs = []
|
||||
for flow in flows_list:
|
||||
if flow.avg_inter_arrival > 0:
|
||||
cv = flow.std_inter_arrival / flow.avg_inter_arrival
|
||||
all_cvs.append(cv)
|
||||
|
||||
if all_cvs:
|
||||
avg_cv = statistics.mean(all_cvs)
|
||||
timing_status = "GOOD" if avg_cv < 0.1 else "WARNING" if avg_cv < 0.5 else "CRITICAL"
|
||||
metrics.append(("Timing Consistency", f"CV={avg_cv:.3f}", timing_status))
|
||||
|
||||
return metrics
|
||||
|
||||
def _draw_selected_flow_outliers(self, stdscr, start_y: int, width: int, height: int, flow: FlowStats):
|
||||
"""Draw outlier details for selected flow"""
|
||||
if height < 3:
|
||||
return
|
||||
|
||||
stdscr.addstr(start_y, 4, f"OUTLIER DETAILS: {flow.src_ip}:{flow.src_port} → {flow.dst_ip}:{flow.dst_port}", curses.A_BOLD)
|
||||
current_y = start_y + 1
|
||||
|
||||
if flow.outlier_details:
|
||||
header = f"{'Frame#':>8} {'Inter-arrival':>15} {'Deviation':>12}"
|
||||
stdscr.addstr(current_y, 4, header, curses.A_UNDERLINE)
|
||||
current_y += 1
|
||||
|
||||
visible_outliers = min(height - 3, len(flow.outlier_details))
|
||||
for i in range(visible_outliers):
|
||||
frame_num, timing = flow.outlier_details[i]
|
||||
|
||||
# Calculate sigma deviation
|
||||
if flow.avg_inter_arrival > 0 and flow.std_inter_arrival > 0:
|
||||
sigma = abs(timing - flow.avg_inter_arrival) / flow.std_inter_arrival
|
||||
deviation = f"{sigma:.1f}σ"
|
||||
else:
|
||||
deviation = "N/A"
|
||||
|
||||
outlier_line = f"{frame_num:>8} {timing*1000:>12.3f}ms {deviation:>12}"
|
||||
stdscr.addstr(current_y + i, 4, outlier_line)
|
||||
|
||||
def _get_flows_list(self) -> List[FlowStats]:
|
||||
"""Get flows sorted for statistical analysis"""
|
||||
flows_list = list(self.analyzer.flows.values())
|
||||
|
||||
# Sort by statistical interest: outliers first, then enhanced, then packet count
|
||||
flows_list.sort(key=lambda x: (
|
||||
len(x.outlier_frames),
|
||||
x.enhanced_analysis.decoder_type != "Standard",
|
||||
x.frame_count
|
||||
), reverse=True)
|
||||
|
||||
return flows_list
|
||||
|
||||
def handle_input(self, key: int, flows_list: List[FlowStats]) -> str:
|
||||
"""Handle input for Statistical Analysis view"""
|
||||
if key == curses.KEY_UP:
|
||||
self.selected_flow = max(0, self.selected_flow - 1)
|
||||
return 'selection_change'
|
||||
elif key == curses.KEY_DOWN:
|
||||
max_flows = len(flows_list) - 1
|
||||
self.selected_flow = min(max_flows, self.selected_flow + 1)
|
||||
return 'selection_change'
|
||||
elif key == curses.KEY_LEFT:
|
||||
self.analysis_mode = max(0, self.analysis_mode - 1)
|
||||
self.selected_flow = 0 # Reset selection when changing modes
|
||||
return 'mode_change'
|
||||
elif key == curses.KEY_RIGHT:
|
||||
self.analysis_mode = min(3, self.analysis_mode + 1)
|
||||
self.selected_flow = 0 # Reset selection when changing modes
|
||||
return 'mode_change'
|
||||
elif key >= ord('1') and key <= ord('4'):
|
||||
self.analysis_mode = key - ord('1')
|
||||
self.selected_flow = 0
|
||||
return 'mode_change'
|
||||
elif key == ord('r') or key == ord('R'):
|
||||
return 'refresh_stats'
|
||||
elif key == ord('o') or key == ord('O'):
|
||||
self.analysis_mode = 1 # Switch to outlier analysis
|
||||
return 'show_outliers'
|
||||
|
||||
return 'none'
|
||||
Reference in New Issue
Block a user