#!/usr/bin/env bash
#
# localnetscan.sh - lokales Netzwerk scannen und bekannte Geraete verwalten
#
# Optionen:
#   --add-known       scannen und ALLE gefundenen Geraete als "known" speichern
#   --exclude-known   scannen, aber bekannte Geraete NICHT anzeigen (nur Neue)
#   --check-known     nicht scannen, nur pruefen ob bekannte Geraete online sind
#   --known-file DATEI  abweichende known-hosts Datei benutzen
#   -h, --help        diese Hilfe anzeigen
#
# known-hosts Datei (Standard: ~/.config/localnetscan/known-hosts.csv)
#   Spalten: mac,label,added,last_ip,last_seen
#   Kann jederzeit von Hand editiert werden, z.B.:
#     vi ~/.config/localnetscan/known-hosts.csv
#
# Root-Rechte:
#   Die Geraete-Erkennung (nmap -sn) sendet rohe ARP-Frames und braucht
#   dafuer normalerweise Root. Um das dauerhaft OHNE sudo nutzen zu koennen,
#   einmalig ausfuehren:
#     sudo setcap cap_net_raw,cap_net_admin+eip "$(command -v nmap)"
#   Das Skript erkennt automatisch, ob die Capability gesetzt ist, und
#   fragt nur dann per sudo nach, wenn sie fehlt. Der Portscan selbst
#   (-sT, TCP-Connect) braucht ohnehin nie Root.
#   rechte wieder entfernen mit: sudo setcap -r "$(command -v nmap)"
#   (r für remove)

set -uo pipefail

PORTS="22,80,443,445,631,554"
KNOWN_FILE="${KNOWN_FILE:-$HOME/.config/localnetscan/known-hosts.csv}"
MODE="scan"

usage() {
    sed -n '2,27p' "$0" | sed 's/^# \{0,1\}//'
}

# ---------- Argumente ----------
while [ $# -gt 0 ]; do
    case "$1" in
        --add-known)     MODE="add" ;;
        --exclude-known) MODE="exclude" ;;
        --check-known)   MODE="check" ;;
        --known-file)    KNOWN_FILE="$2"; shift ;;
        -h|--help)       usage; exit 0 ;;
        *)
            echo "Unbekannte Option: $1" >&2
            usage
            exit 1
            ;;
    esac
    shift
done

# ---------- known-hosts Verwaltung ----------
ensure_known_file() {
    mkdir -p "$(dirname "$KNOWN_FILE")"
    if [ ! -f "$KNOWN_FILE" ]; then
        echo "mac,label,added,last_ip,last_seen" > "$KNOWN_FILE"
    fi
}

declare -A KNOWN
load_known() {
    KNOWN=()
    [ -f "$KNOWN_FILE" ] || return 0
    while IFS=, read -r mac label added last_ip last_seen; do
        [ "$mac" = "mac" ] && continue   # Kopfzeile
        [ -z "$mac" ] && continue
        KNOWN["$mac"]="$label"
    done < "$KNOWN_FILE"
}

is_known() {
    [ -n "${KNOWN[$1]:-}" ]
}

add_known() {
    local mac="$1" ip="$2" label="${3:--}"
    if [ -z "$mac" ] || [ "$mac" = "-" ]; then
        echo "Ueberspringe $ip: keine MAC-Adresse bekannt (kein Eintrag in known-hosts)." >&2
        return 1
    fi
    local now; now="$(date +%F_%T)"
    if is_known "$mac"; then
        local tmp; tmp="$(mktemp)"
        awk -F, -v OFS=, -v mac="$mac" -v ip="$ip" -v now="$now" \
            '$1==mac{$4=ip;$5=now} {print}' "$KNOWN_FILE" > "$tmp" && mv "$tmp" "$KNOWN_FILE"
    else
        echo "$mac,$label,$now,$ip,$now" >> "$KNOWN_FILE"
        KNOWN["$mac"]="$label"
    fi
}

# ---------- root-loser nmap-Wrapper ----------
nmap_sn() {
    local subnet="$1"
    local nmap_bin; nmap_bin="$(command -v nmap)"

    if [ "$(id -u)" -eq 0 ]; then
        nmap -sn -n "$subnet" 2>/dev/null
        return
    fi

    if command -v getcap >/dev/null 2>&1 \
        && getcap "$nmap_bin" 2>/dev/null | grep -q cap_net_raw; then
        # nmap erkennt gesetzte File-Capabilities nicht automatisch (es prueft
        # primaer geteuid()==0), daher hier explizit --privileged erzwingen -
        # sonst faellt es trotz vorhandener Rechte auf den ICMP/TCP-Fallback
        # zurueck (keine MAC-Adressen, kein Vendor).
        nmap -sn -n --privileged "$subnet" 2>/dev/null
        return
    fi

    echo "Hinweis: nmap hat kein cap_net_raw gesetzt, frage einmalig per sudo." >&2
    echo "Fuer root-lose Scans in Zukunft einmalig ausfuehren:" >&2
    echo "  sudo setcap cap_net_raw,cap_net_admin+eip \"$nmap_bin\"" >&2
    sudo nmap -sn -n "$subnet" 2>/dev/null
}

get_primary_iface() {
    ip -4 route show default 2>/dev/null \
        | awk '{for(i=1;i<=NF;i++) if($i=="dev"){print $(i+1); exit}}'
}

detect_subnet() {
    local iface; iface="$(get_primary_iface)"
    if [ -n "$iface" ]; then
        ip -4 -o addr show dev "$iface" scope global | awk '{print $4}' | head -n1
    else
        ip -4 -o addr show scope global | awk '{print $4}' | head -n1
    fi
}

# OUI-Praefix (erste 3 Bytes der MAC) in der nmap-eigenen Datenbank nachschlagen
lookup_vendor() {
    local mac="$1" prefix db result
    prefix=$(echo "$mac" | tr -d ':' | tr 'a-f' 'A-F' | cut -c1-6)
    [ -z "$prefix" ] && return
    for db in /usr/share/nmap/nmap-mac-prefixes; do
        [ -r "$db" ] || continue
        result=$(awk -v p="$prefix" 'toupper($1)==p{$1=""; sub(/^ /,""); print; exit}' "$db")
        if [ -n "$result" ]; then
            echo "$result"
            return
        fi
    done
}

# Eigene IP/MAC/Vendor JEDER lokalen Schnittstelle (Ethernet, WLAN, ...)
# ermitteln - nmap kann sich selbst nicht per ARP erreichen, daher taucht
# der scannende Rechner nie mit MAC in "nmap -sn" auf. Wird separat ueber
# "ip" ermittelt und pro Interface in OWN_MAC_BY_IP/OWN_VENDOR_BY_IP abgelegt.
declare -A OWN_MAC_BY_IP
declare -A OWN_VENDOR_BY_IP
get_own_info() {
    OWN_MAC_BY_IP=()
    OWN_VENDOR_BY_IP=()
    local line iface ip mac vendor
    while read -r line; do
        iface=$(echo "$line" | awk '{print $2}')
        ip=$(echo "$line" | awk '{print $4}' | cut -d/ -f1)
        [ -z "$iface" ] || [ -z "$ip" ] && continue

        mac=$(ip link show dev "$iface" 2>/dev/null | awk '/link\/ether/{print $2}')
        [ -z "$mac" ] && continue

        vendor="$(lookup_vendor "$mac")"
        [ -z "$vendor" ] && vendor="lokal"
        OWN_MAC_BY_IP["$ip"]="$mac"
        OWN_VENDOR_BY_IP["$ip"]="$vendor (lokal, $iface)"
    done < <(ip -4 -o addr show scope global)
}

port_scan() {
    local ip="$1"
    nmap -sT -Pn -n --open -p "$PORTS" "$ip" 2>/dev/null \
        | awk '/^[0-9]+\/tcp/{print $1}' | paste -sd, -
}

# nmap -sn Ausgabe in Zeilen "ip|mac|vendor" umwandeln
scan_hosts() {
    local subnet="$1"
    nmap_sn "$subnet" | awk '
        /^Nmap scan report for/ {
            if (ip != "") print ip "|" mac "|" vendor
            ip = $NF; gsub(/[()]/,"",ip); mac=""; vendor=""
        }
        /^MAC Address:/ {
            mac = $3
            vendor = $0
            sub(/^MAC Address: [^ ]+ \(/, "", vendor)
            sub(/\)$/, "", vendor)
        }
        END { if (ip != "") print ip "|" mac "|" vendor }
    '
}

# Fehlende MAC/Vendor einer Zeile anhand der eigenen Interfaces auffuellen.
# Erwartet/veraendert die Variablen ip, mac, vendor im Aufrufer-Scope.
fill_own_mac() {
    if { [ -z "$mac" ] || [ "$mac" = "-" ]; } && [ -n "${OWN_MAC_BY_IP[$ip]:-}" ]; then
        mac="${OWN_MAC_BY_IP[$ip]}"
        vendor="${OWN_VENDOR_BY_IP[$ip]}"
    fi
}

print_row() {
    printf "%-15s  %-12s  %-17s  %-24s  %s\n" "$1" "$2" "$3" "$4" "$5"
}

# ---------- Modi ----------
run_scan() {
    local subnet; subnet="$(detect_subnet)"
    if [ -z "$subnet" ]; then
        echo "Konnte lokales Subnetz nicht bestimmen." >&2
        exit 1
    fi

    ensure_known_file
    load_known
    get_own_info

    print_row "IP" "HOST" "MAC" "VENDOR" "OPEN PORTS"
    print_row "---" "----" "---" "------" "----------"

    while IFS='|' read -r ip mac vendor; do
        [ -z "$ip" ] && continue
        fill_own_mac
        [ -z "$vendor" ] && vendor="?"
        [ -z "$mac" ] && mac="-"

        if [ "$MODE" = "exclude" ] && [ "$mac" != "-" ] && is_known "$mac"; then
            continue
        fi

        host=$(getent hosts "$ip" | awk '{print $2}' | cut -d. -f1)
        [ -z "$host" ] && host="-"

        ports="$(port_scan "$ip")"
        [ -z "$ports" ] && ports="-"

        print_row "$ip" "$host" "$mac" "$vendor" "$ports"

        if [ "$MODE" = "add" ] && [ "$mac" != "-" ]; then
            add_known "$mac" "$ip" "$host"
        fi
    done < <(scan_hosts "$subnet")
}

run_check_known() {
    ensure_known_file
    load_known
    if [ "${#KNOWN[@]}" -eq 0 ]; then
        echo "Keine bekannten Geraete in $KNOWN_FILE"
        return
    fi

    local subnet; subnet="$(detect_subnet)"
    get_own_info
    declare -A SEEN
    while IFS='|' read -r ip mac vendor; do
        fill_own_mac
        [ -n "$mac" ] && [ "$mac" != "-" ] && SEEN["$mac"]="$ip"
    done < <(scan_hosts "$subnet")

    printf "%-17s  %-20s  %-8s  %s\n" "MAC" "LABEL" "STATUS" "IP"
    printf "%-17s  %-20s  %-8s  %s\n" "---" "-----" "------" "--"
    while IFS=, read -r mac label added last_ip last_seen; do
        [ "$mac" = "mac" ] && continue
        [ -z "$mac" ] && continue
        if [ -n "${SEEN[$mac]:-}" ]; then
            printf "%-17s  %-20s  %-8s  %s\n" "$mac" "$label" "online" "${SEEN[$mac]}"
        else
            printf "%-17s  %-20s  %-8s  %s\n" "$mac" "$label" "offline" "$last_ip"
        fi
    done < "$KNOWN_FILE"
}

case "$MODE" in
    check) run_check_known ;;
    *)     run_scan ;;
esac
