diff --git a/commands/logs.command.sh b/commands/logs.command.sh index de5762d..7957c63 100644 --- a/commands/logs.command.sh +++ b/commands/logs.command.sh @@ -225,34 +225,65 @@ function cmd::logs::show_fw_events() { "$filter_dest_ip" "$filter_dest_port" \ "$sort_order" \ 2>/dev/null) - + [[ -z "$data" ]] && return 0 - - # Measure column widths - local w_client=16 w_dest=20 - while IFS='|' read -r ts client dest_ip dest_port proto svc count; do + + # ── Pass 1: resolve endpoints and measure widths ── + local w_client=16 w_dest=20 w_endpoint=0 + local resolved_data="" + + while IFS='|' read -r ts client dest_ip dest_port proto svc count src_endpoint; do [[ -z "$ts" ]] && continue + + # Measure client (( ${#client} > w_client )) && w_client=${#client} - local dest_display host_name - host_name=$(hosts::resolve_ip "$dest_ip") - if [[ -n "$host_name" ]]; then - dest_display="$host_name" - elif [[ -n "$svc" ]]; then - [[ -n "$dest_port" ]] && dest_display="${svc}/${proto}" || dest_display="${svc} (${proto})" + + # Build svc_display (for w_dest measurement) + local svc_display="" + if [[ -n "$svc" ]]; then + [[ -n "$dest_port" ]] && svc_display="${svc}/${proto}" \ + || svc_display="${svc} (${proto})" else - [[ -n "$dest_port" ]] && dest_display="${dest_ip}:${dest_port}/${proto}" || dest_display="${dest_ip} (${proto})" + [[ -n "$dest_port" ]] && svc_display="${dest_ip}:${dest_port}/${proto}" \ + || svc_display="${dest_ip} (${proto})" fi - (( ${#dest_display} > w_dest )) && w_dest=${#dest_display} + + # Build raw_suffix plain (no ANSI) for w_dest measurement + local raw_plain="" + if [[ -n "$svc" ]]; then + [[ -n "$dest_port" ]] && raw_plain=" (${dest_ip}:${dest_port})" \ + || raw_plain=" (${dest_ip})" + fi + + local full_dest_len=$(( ${#svc_display} + ${#raw_plain} )) + (( full_dest_len > w_dest )) && w_dest=$full_dest_len + + # Resolve endpoint once + local src_resolved="" + if [[ -n "$src_endpoint" ]]; then + src_resolved=$(resolve::ip "$src_endpoint" 2>/dev/null || true) + [[ "$src_resolved" == "$src_endpoint" ]] && src_resolved="" + # Measure endpoint column: raw IP + " → resolved" + local ep_display_len=${#src_endpoint} + [[ -n "$src_resolved" ]] && ep_display_len=$(( ep_display_len + 4 + ${#src_resolved} )) + (( ep_display_len > w_endpoint )) && w_endpoint=$ep_display_len + fi + + resolved_data+="${ts}|${client}|${dest_ip}|${dest_port}|${proto}|${svc}|${count}|${src_endpoint}|${src_resolved}"$'\n' done <<< "$data" - (( w_client += 2 )) - (( w_dest += 2 )) - + + (( w_client += 2 )) + (( w_dest += 2 )) + [[ "$w_endpoint" -gt 0 ]] && (( w_endpoint += 2 )) + + # ── Pass 2: render ── ui::logs::fw_section_header - while IFS='|' read -r ts client dest_ip dest_port proto svc count; do + while IFS='|' read -r ts client dest_ip dest_port proto svc count src_endpoint src_resolved; do [[ -z "$ts" ]] && continue ui::logs::fw_row "$ts" "$client" "$dest_ip" "$dest_port" \ - "$proto" "$svc" "$count" "$w_client" "$w_dest" - done <<< "$data" + "$proto" "$svc" "$count" "$w_client" "$w_dest" \ + "$src_endpoint" "$src_resolved" "$w_endpoint" + done <<< "$resolved_data" printf "\n" } @@ -277,21 +308,26 @@ function cmd::logs::show_wg_events() { local resolved_data="" while IFS='|' read -r ts client endpoint event count gap_seconds; do [[ -z "$ts" ]] && continue - local endpoint_display - endpoint_display=$(resolve::ip "$endpoint") - [[ -z "$endpoint_display" ]] && endpoint_display="$endpoint" - resolved_data+="${ts}|${client}|${endpoint_display}|${event}|${count}|${gap_seconds}"$'\n' - (( ${#client} > w_client )) && w_client=${#client} - (( ${#endpoint_display} > w_endpoint )) && w_endpoint=${#endpoint_display} + (( ${#client} > w_client )) && w_client=${#client} + local ep_len=${#endpoint} + [[ -z "$endpoint" ]] && ep_len=1 + (( ep_len > w_endpoint )) && w_endpoint=$ep_len + + # Resolve endpoint + local resolved="" + [[ -n "$endpoint" ]] && resolved=$(resolve::ip "$endpoint" 2>/dev/null || true) + [[ "$resolved" == "$endpoint" ]] && resolved="" + + resolved_data+="${ts}|${client}|${endpoint}|${event}|${count}|${gap_seconds}|${resolved}"$'\n' done <<< "$data" (( w_client += 2 )) - (( w_endpoint += 2 )) + (( w_endpoint += 18 )) ui::logs::wg_section_header - while IFS='|' read -r ts client endpoint event count gap_seconds; do + while IFS='|' read -r ts client endpoint event count gap_seconds resolved; do [[ -z "$ts" ]] && continue ui::logs::wg_row "$ts" "$client" "$endpoint" "$event" \ - "$count" "$w_client" "$w_endpoint" "$gap_seconds" + "$count" "$w_client" "$w_endpoint" "$gap_seconds" "$resolved" done <<< "$resolved_data" printf "\n" } diff --git a/core/json_helper.py b/core/json_helper.py index cbe6669..dd084b0 100644 --- a/core/json_helper.py +++ b/core/json_helper.py @@ -1690,7 +1690,8 @@ commands = { args[7] if len(args) > 7 else '', args[8] if len(args) > 8 else '', args[9] if len(args) > 9 else '', - args[10] if len(args) > 10 else 'desc'), + args[10] if len(args) > 10 else 'desc', + args[11] if len(args) > 11 else ''), 'wg_events': lambda args: __import__('lib.events', fromlist=['wg_events']).wg_events( args[0], args[1], args[2], args[3] if len(args) > 3 else '50', diff --git a/core/lib/__pycache__/events.cpython-311.pyc b/core/lib/__pycache__/events.cpython-311.pyc index 0152321..98fb0ac 100644 Binary files a/core/lib/__pycache__/events.cpython-311.pyc and b/core/lib/__pycache__/events.cpython-311.pyc differ diff --git a/core/lib/events.py b/core/lib/events.py index 1ce065f..81cc1b5 100644 --- a/core/lib/events.py +++ b/core/lib/events.py @@ -22,7 +22,8 @@ from lib.util import ( # ────────────────────────────────────────── def fw_events(file, filter_ip, filter_type, clients_dir, net_file, - limit, collapse='1', since='', filter_dest_ip='', filter_dest_port='', sort_order='desc'): + limit, collapse='1', since='', filter_dest_ip='', + filter_dest_port='', sort_order='desc', endpoint_cache_file=''): """ Format firewall drop events with dedup, counts, and service annotation. @@ -42,6 +43,14 @@ def fw_events(file, filter_ip, filter_type, clients_dir, net_file, net_data = load_net_data(net_file) hosts_data = load_hosts_data(None) # hosts lookup done in bash for now + endpoint_cache = {} + if endpoint_cache_file and os.path.exists(endpoint_cache_file): + try: + with open(endpoint_cache_file) as f: + endpoint_cache = json.load(f) + except Exception: + pass + since_dt = parse_since(since) if since else None def _reverse(dest_ip, dest_port, proto): @@ -129,9 +138,10 @@ def fw_events(file, filter_ip, filter_type, clients_dir, net_file, output = list(reversed(output)) for hour_key, dt in output: client, dst, port, proto, svc_name, _ = hour_key - count = hourly[hour_key] - ts_fmt = fmt_ts_hour(dt.isoformat()) - print(f"{ts_fmt}|{client}|{dst}|{port}|{proto}|{svc_name}|{count}") + count = hourly[hour_key] + ts_fmt = fmt_ts_hour(hourly_ts[hour_key].isoformat()) + src_endpoint = endpoint_cache.get(client, '') + print(f"{ts_fmt}|{client}|{dst}|{port}|{proto}|{svc_name}|{count}|{src_endpoint}") else: # Detailed — consecutive dedup only @@ -172,9 +182,14 @@ def fw_events(file, filter_ip, filter_type, clients_dir, net_file, proto_num = int(e.get('ip.protocol', 0)) proto = PROTO_MAP.get(proto_num, str(proto_num)) client = ip_to_name.get(src, src) - svc_name = _reverse(dst, port, proto) - ts_fmt = fmt_ts(e.get('timestamp', '')) - print(f"{ts_fmt}|{client}|{dst}|{port}|{proto}|{svc_name}|{count}") + svc_name = reverse_lookup(net_data, dst, port, proto) + src_endpoint = endpoint_cache.get(client, '') + try: + dt = datetime.fromisoformat(e.get('timestamp', '')) + ts_fmt = dt.strftime(DATETIME_FMT) + except Exception: + ts_fmt = e.get('timestamp', '') + print(f"{ts_fmt}|{client}|{dst}|{port}|{proto}|{svc_name}|{count}|{src_endpoint}") # ────────────────────────────────────────── diff --git a/modules/ui/logs.module.sh b/modules/ui/logs.module.sh index f8dddd4..c2d479e 100644 --- a/modules/ui/logs.module.sh +++ b/modules/ui/logs.module.sh @@ -11,13 +11,15 @@ function ui::logs::build_dest() { } function ui::logs::fw_section_header() { + local cols=$(( $(tput cols 2>/dev/null || echo 80) - 4 )) printf " Firewall Drops\n" - printf " %s\n" "$(printf '─%.0s' {1..42})" + printf " %s\n" "$(printf '─%.0s' $(seq 1 $cols))" } function ui::logs::wg_section_header() { + local cols=$(( $(tput cols 2>/dev/null || echo 80) - 4 )) printf " WireGuard Events\n" - printf " %s\n" "$(printf '─%.0s' {1..42})" + printf " %s\n" "$(printf '─%.0s' $(seq 1 $cols))" } function ui::logs::fw_section_header_table() { @@ -35,17 +37,67 @@ function ui::logs::wg_section_header_table() { function ui::logs::fw_row() { local ts="${1:-}" client="${2:-}" dest_ip="${3:-}" dest_port="${4:-}" \ proto="${5:-}" svc_name="${6:-}" count="${7:-1}" \ - w_client="${8:-20}" w_dest="${9:-30}" - local dest_display - dest_display=$(ui::logs::build_dest "$dest_ip" "$dest_port" "$proto" "$svc_name") + w_client="${8:-20}" w_dest="${9:-30}" \ + src_endpoint="${10:-}" src_resolved="${11:-}" w_endpoint="${12:-0}" + + local ts_pad client_pad + ts_pad=$(printf "%-11s" "$ts") + client_pad=$(printf "%-${w_client}s" "$client") + + # ── Source endpoint — always render at w_endpoint width ── + local src_padded="" + if [[ "$w_endpoint" -gt 0 ]]; then + if [[ -n "$src_endpoint" ]]; then + local src_colored="$src_endpoint" + [[ -n "$src_resolved" ]] && \ + src_colored="${src_endpoint} \033[2m→ ${src_resolved}\033[0m" + src_padded=$(ui::pad_mb "$src_colored" "$w_endpoint") + else + # No endpoint — use dim dash padded to w_endpoint + src_padded=$(ui::pad_mb "\033[2m—\033[0m" "$w_endpoint") + fi + fi + + # ── Destination ── + local svc_display="" + local raw_suffix="" + if [[ -n "$svc_name" ]]; then + [[ -n "$dest_port" ]] && svc_display="${svc_name}/${proto}" \ + || svc_display="${svc_name} (${proto})" + [[ -n "$dest_port" ]] && raw_suffix=" \033[2m(${dest_ip}:${dest_port})\033[0m" \ + || raw_suffix=" \033[2m(${dest_ip})\033[0m" + else + [[ -n "$dest_port" ]] && svc_display="${dest_ip}:${dest_port}/${proto}" \ + || svc_display="${dest_ip} (${proto})" + fi + + # Pad so count aligns — based on full dest (svc + raw_suffix plain length) + local raw_plain="" + [[ -n "$svc_name" && -n "$dest_port" ]] && raw_plain=" (${dest_ip}:${dest_port})" + [[ -n "$svc_name" && -z "$dest_port" ]] && raw_plain=" (${dest_ip})" + local full_dest_len=$(( ${#svc_display} + ${#raw_plain} )) + local dest_pad_n=$(( w_dest - full_dest_len )) + [[ $dest_pad_n -lt 0 ]] && dest_pad_n=0 + + # ── Count ── local count_suffix="" [[ "$count" -gt 1 ]] && count_suffix=" \033[2m(x${count})\033[0m" - local client_pad dest_pad_n - client_pad=$(printf "%-${w_client}s" "$client") - dest_pad_n=$(( w_dest - ${#dest_display} )) - [[ $dest_pad_n -lt 0 ]] && dest_pad_n=0 - printf " %s %s \033[1;31m→\033[0m %s%*s%b\n" \ - "$ts" "$client_pad" "$dest_display" "$dest_pad_n" "" "$count_suffix" + + # ── Render ── + if [[ "$w_endpoint" -gt 0 ]]; then + printf " %s %s %b \033[1;31m→\033[0m %s%b%*s%b\n" \ + "$ts_pad" "$client_pad" \ + "$src_padded" \ + "$svc_display" "$raw_suffix" \ + "$dest_pad_n" "" \ + "$count_suffix" + else + printf " %s %s \033[1;31m→\033[0m %s%b%*s%b\n" \ + "$ts_pad" "$client_pad" \ + "$svc_display" "$raw_suffix" \ + "$dest_pad_n" "" \ + "$count_suffix" + fi } function ui::logs::fw_row_table() { @@ -58,39 +110,51 @@ function ui::logs::fw_row_table() { function ui::logs::wg_row() { local ts="${1:-}" client="${2:-}" endpoint="${3:-}" event="${4:-}" \ count="${5:-1}" w_client="${6:-20}" w_endpoint="${7:-20}" \ - gap_seconds="${8:-}" - + gap_seconds="${8:-}" resolved="${9:-}" + local event_color case "$event" in handshake) event_color="\033[1;32m" ;; attempt) event_color="\033[1;31m" ;; *) event_color="\033[0;37m" ;; esac - + local count_suffix="" [[ "$count" -gt 1 ]] && count_suffix=" \033[2m(x${count})\033[0m" - - # Gap suffix — only for handshakes with a meaningful gap + + # Gap suffix — offline label only when gap > threshold * 2 local gap_suffix="" if [[ "$event" == "handshake" && -n "$gap_seconds" && "$gap_seconds" -gt 0 ]]; then local gap_int="$gap_seconds" local threshold="${WG_HANDSHAKE_CHECK_TIME_SEC:-300}" local offline_label="" - [[ "$gap_int" -gt "$threshold" ]] && offline_label=" offline" + [[ "$gap_int" -gt $(( threshold * 2 )) ]] && offline_label=" offline" if (( gap_int >= 3600 )); then gap_suffix=" \033[2m↑ $(( gap_int / 3600 ))h${offline_label}\033[0m" elif (( gap_int >= 60 )); then gap_suffix=" \033[2m↑ $(( gap_int / 60 ))m${offline_label}\033[0m" fi fi - - local client_pad endpoint_pad_n + + # Build endpoint display: raw_ip [dim → resolved] + # Use ui::pad_mb so ANSI annotation doesn't affect column alignment + local endpoint_raw="${endpoint:--}" + local endpoint_colored + if [[ -n "$resolved" && -n "$endpoint" ]]; then + endpoint_colored="${endpoint} \033[2m→ ${resolved}\033[0m" + else + endpoint_colored="$endpoint_raw" + fi + local endpoint_padded + endpoint_padded=$(ui::pad_mb "$endpoint_colored" "$w_endpoint") + + local ts_pad client_pad + ts_pad=$(printf "%-11s" "$ts") client_pad=$(printf "%-${w_client}s" "$client") - endpoint_pad_n=$(( w_endpoint - ${#endpoint} )) - [[ $endpoint_pad_n -lt 0 ]] && endpoint_pad_n=0 - - printf " %s %s %s%*s %b%s\033[0m%b%b\n" \ - "$ts" "$client_pad" "$endpoint" "$endpoint_pad_n" "" \ + + printf " %s %s %b %b%s\033[0m%b%b\n" \ + "$ts_pad" "$client_pad" \ + "$endpoint_padded" \ "$event_color" "$event" "$count_suffix" "$gap_suffix" }