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