feat: logs endpoint annotation, alignment, descending sort

- fw/wg events: raw_ip → resolved_name annotation (dim)
- fw events: endpoint column with pre-resolved names (two-pass render)
- fw events: raw IP:port dim suffix after service name
- wg events: endpoint annotation in logs (same as watch)
- fw/wg: descending sort default, --ascending/--descending flags
- wg events: gap/offline indicator, threshold * 2 for offline label
- fw_row: no-endpoint rows show dim — placeholder for alignment
- section headers: dynamic width via tput cols
This commit is contained in:
Nuno Duque Nunes 2026-05-26 03:07:57 +00:00
parent d5de344d99
commit fb33aa1b6d
5 changed files with 176 additions and 60 deletions

View file

@ -228,31 +228,62 @@ function cmd::logs::show_fw_events() {
[[ -z "$data" ]] && return 0 [[ -z "$data" ]] && return 0
# Measure column widths # ── Pass 1: resolve endpoints and measure widths ──
local w_client=16 w_dest=20 local w_client=16 w_dest=20 w_endpoint=0
while IFS='|' read -r ts client dest_ip dest_port proto svc count; do local resolved_data=""
[[ -z "$ts" ]] && continue
(( ${#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})"
else
[[ -n "$dest_port" ]] && dest_display="${dest_ip}:${dest_port}/${proto}" || dest_display="${dest_ip} (${proto})"
fi
(( ${#dest_display} > w_dest )) && w_dest=${#dest_display}
done <<< "$data"
(( w_client += 2 ))
(( w_dest += 2 ))
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}
# 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" ]] && svc_display="${dest_ip}:${dest_port}/${proto}" \
|| svc_display="${dest_ip} (${proto})"
fi
# 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_endpoint" -gt 0 ]] && (( w_endpoint += 2 ))
# ── Pass 2: render ──
ui::logs::fw_section_header 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 [[ -z "$ts" ]] && continue
ui::logs::fw_row "$ts" "$client" "$dest_ip" "$dest_port" \ ui::logs::fw_row "$ts" "$client" "$dest_ip" "$dest_port" \
"$proto" "$svc" "$count" "$w_client" "$w_dest" "$proto" "$svc" "$count" "$w_client" "$w_dest" \
done <<< "$data" "$src_endpoint" "$src_resolved" "$w_endpoint"
done <<< "$resolved_data"
printf "\n" printf "\n"
} }
@ -277,21 +308,26 @@ function cmd::logs::show_wg_events() {
local resolved_data="" local resolved_data=""
while IFS='|' read -r ts client endpoint event count gap_seconds; do while IFS='|' read -r ts client endpoint event count gap_seconds; do
[[ -z "$ts" ]] && continue [[ -z "$ts" ]] && continue
local endpoint_display (( ${#client} > w_client )) && w_client=${#client}
endpoint_display=$(resolve::ip "$endpoint") local ep_len=${#endpoint}
[[ -z "$endpoint_display" ]] && endpoint_display="$endpoint" [[ -z "$endpoint" ]] && ep_len=1
resolved_data+="${ts}|${client}|${endpoint_display}|${event}|${count}|${gap_seconds}"$'\n' (( ep_len > w_endpoint )) && w_endpoint=$ep_len
(( ${#client} > w_client )) && w_client=${#client}
(( ${#endpoint_display} > w_endpoint )) && w_endpoint=${#endpoint_display} # 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" done <<< "$data"
(( w_client += 2 )) (( w_client += 2 ))
(( w_endpoint += 2 )) (( w_endpoint += 18 ))
ui::logs::wg_section_header 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 [[ -z "$ts" ]] && continue
ui::logs::wg_row "$ts" "$client" "$endpoint" "$event" \ 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" done <<< "$resolved_data"
printf "\n" printf "\n"
} }

View file

@ -1690,7 +1690,8 @@ commands = {
args[7] if len(args) > 7 else '', args[7] if len(args) > 7 else '',
args[8] if len(args) > 8 else '', args[8] if len(args) > 8 else '',
args[9] if len(args) > 9 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( 'wg_events': lambda args: __import__('lib.events', fromlist=['wg_events']).wg_events(
args[0], args[1], args[2], args[0], args[1], args[2],
args[3] if len(args) > 3 else '50', args[3] if len(args) > 3 else '50',

View file

@ -22,7 +22,8 @@ from lib.util import (
# ────────────────────────────────────────── # ──────────────────────────────────────────
def fw_events(file, filter_ip, filter_type, clients_dir, net_file, 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. 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) net_data = load_net_data(net_file)
hosts_data = load_hosts_data(None) # hosts lookup done in bash for now 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 since_dt = parse_since(since) if since else None
def _reverse(dest_ip, dest_port, proto): 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)) output = list(reversed(output))
for hour_key, dt in output: for hour_key, dt in output:
client, dst, port, proto, svc_name, _ = hour_key client, dst, port, proto, svc_name, _ = hour_key
count = hourly[hour_key] count = hourly[hour_key]
ts_fmt = fmt_ts_hour(dt.isoformat()) ts_fmt = fmt_ts_hour(hourly_ts[hour_key].isoformat())
print(f"{ts_fmt}|{client}|{dst}|{port}|{proto}|{svc_name}|{count}") src_endpoint = endpoint_cache.get(client, '')
print(f"{ts_fmt}|{client}|{dst}|{port}|{proto}|{svc_name}|{count}|{src_endpoint}")
else: else:
# Detailed — consecutive dedup only # 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_num = int(e.get('ip.protocol', 0))
proto = PROTO_MAP.get(proto_num, str(proto_num)) proto = PROTO_MAP.get(proto_num, str(proto_num))
client = ip_to_name.get(src, src) client = ip_to_name.get(src, src)
svc_name = _reverse(dst, port, proto) svc_name = reverse_lookup(net_data, dst, port, proto)
ts_fmt = fmt_ts(e.get('timestamp', '')) src_endpoint = endpoint_cache.get(client, '')
print(f"{ts_fmt}|{client}|{dst}|{port}|{proto}|{svc_name}|{count}") 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}")
# ────────────────────────────────────────── # ──────────────────────────────────────────

View file

@ -11,13 +11,15 @@ function ui::logs::build_dest() {
} }
function ui::logs::fw_section_header() { function ui::logs::fw_section_header() {
local cols=$(( $(tput cols 2>/dev/null || echo 80) - 4 ))
printf " Firewall Drops\n" 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() { function ui::logs::wg_section_header() {
local cols=$(( $(tput cols 2>/dev/null || echo 80) - 4 ))
printf " WireGuard Events\n" 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() { function ui::logs::fw_section_header_table() {
@ -35,17 +37,67 @@ function ui::logs::wg_section_header_table() {
function ui::logs::fw_row() { function ui::logs::fw_row() {
local ts="${1:-}" client="${2:-}" dest_ip="${3:-}" dest_port="${4:-}" \ local ts="${1:-}" client="${2:-}" dest_ip="${3:-}" dest_port="${4:-}" \
proto="${5:-}" svc_name="${6:-}" count="${7:-1}" \ proto="${5:-}" svc_name="${6:-}" count="${7:-1}" \
w_client="${8:-20}" w_dest="${9:-30}" w_client="${8:-20}" w_dest="${9:-30}" \
local dest_display src_endpoint="${10:-}" src_resolved="${11:-}" w_endpoint="${12:-0}"
dest_display=$(ui::logs::build_dest "$dest_ip" "$dest_port" "$proto" "$svc_name")
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="" local count_suffix=""
[[ "$count" -gt 1 ]] && count_suffix=" \033[2m(x${count})\033[0m" [[ "$count" -gt 1 ]] && count_suffix=" \033[2m(x${count})\033[0m"
local client_pad dest_pad_n
client_pad=$(printf "%-${w_client}s" "$client") # ── Render ──
dest_pad_n=$(( w_dest - ${#dest_display} )) if [[ "$w_endpoint" -gt 0 ]]; then
[[ $dest_pad_n -lt 0 ]] && dest_pad_n=0 printf " %s %s %b \033[1;31m→\033[0m %s%b%*s%b\n" \
printf " %s %s \033[1;31m→\033[0m %s%*s%b\n" \ "$ts_pad" "$client_pad" \
"$ts" "$client_pad" "$dest_display" "$dest_pad_n" "" "$count_suffix" "$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() { function ui::logs::fw_row_table() {
@ -58,7 +110,7 @@ function ui::logs::fw_row_table() {
function ui::logs::wg_row() { function ui::logs::wg_row() {
local ts="${1:-}" client="${2:-}" endpoint="${3:-}" event="${4:-}" \ local ts="${1:-}" client="${2:-}" endpoint="${3:-}" event="${4:-}" \
count="${5:-1}" w_client="${6:-20}" w_endpoint="${7:-20}" \ count="${5:-1}" w_client="${6:-20}" w_endpoint="${7:-20}" \
gap_seconds="${8:-}" gap_seconds="${8:-}" resolved="${9:-}"
local event_color local event_color
case "$event" in case "$event" in
@ -70,13 +122,13 @@ function ui::logs::wg_row() {
local count_suffix="" local count_suffix=""
[[ "$count" -gt 1 ]] && count_suffix=" \033[2m(x${count})\033[0m" [[ "$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="" local gap_suffix=""
if [[ "$event" == "handshake" && -n "$gap_seconds" && "$gap_seconds" -gt 0 ]]; then if [[ "$event" == "handshake" && -n "$gap_seconds" && "$gap_seconds" -gt 0 ]]; then
local gap_int="$gap_seconds" local gap_int="$gap_seconds"
local threshold="${WG_HANDSHAKE_CHECK_TIME_SEC:-300}" local threshold="${WG_HANDSHAKE_CHECK_TIME_SEC:-300}"
local offline_label="" local offline_label=""
[[ "$gap_int" -gt "$threshold" ]] && offline_label=" offline" [[ "$gap_int" -gt $(( threshold * 2 )) ]] && offline_label=" offline"
if (( gap_int >= 3600 )); then if (( gap_int >= 3600 )); then
gap_suffix=" \033[2m↑ $(( gap_int / 3600 ))h${offline_label}\033[0m" gap_suffix=" \033[2m↑ $(( gap_int / 3600 ))h${offline_label}\033[0m"
elif (( gap_int >= 60 )); then elif (( gap_int >= 60 )); then
@ -84,13 +136,25 @@ function ui::logs::wg_row() {
fi fi
fi fi
local client_pad endpoint_pad_n # Build endpoint display: raw_ip [dim → resolved]
client_pad=$(printf "%-${w_client}s" "$client") # Use ui::pad_mb so ANSI annotation doesn't affect column alignment
endpoint_pad_n=$(( w_endpoint - ${#endpoint} )) local endpoint_raw="${endpoint:--}"
[[ $endpoint_pad_n -lt 0 ]] && endpoint_pad_n=0 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")
printf " %s %s %s%*s %b%s\033[0m%b%b\n" \ local ts_pad client_pad
"$ts" "$client_pad" "$endpoint" "$endpoint_pad_n" "" \ ts_pad=$(printf "%-11s" "$ts")
client_pad=$(printf "%-${w_client}s" "$client")
printf " %s %s %b %b%s\033[0m%b%b\n" \
"$ts_pad" "$client_pad" \
"$endpoint_padded" \
"$event_color" "$event" "$count_suffix" "$gap_suffix" "$event_color" "$event" "$count_suffix" "$gap_suffix"
} }