#!/usr/bin/env bash
#
# net-audit.sh — Netzwerksicherheits-Audit für Linux (read-only)
#
# Modi:
#   ./net-audit.sh                       lokaler Audit
#   ./net-audit.sh --scan-network CIDR   zusätzlich LAN-Scan (reines Bash, /dev/tcp)
#
# Das Skript ändert NICHTS am System. Alle Checks sind lesend.
# Ohne Root laufen einzelne Checks eingeschränkt und werden als SKIP markiert;
# voller Umfang mit: sudo ./net-audit.sh
#
# Distributionen: volle Unterstützung (echte Checks statt SKIP) für
#   - Debian/Ubuntu (dpkg, ufw)
#   - RHEL-Familie: RHEL/CentOS/Fedora/Rocky/AlmaLinux/Oracle/Amazon Linux
#     (rpm, firewalld)
# Andere Distributionen laufen mit den portablen Checks (Ports, sysctl,
# sshd -T, LAN-Scan); Paket- und Firewall-Prüfungen werden dort als SKIP
# markiert statt falsch-grüne Ergebnisse zu liefern.
#
# Referenzen je Check sind im Output angegeben:
#   - Lynis (github.com/CISOfy/lynis), Test-IDs wie NETW-3012, SSH-7408
#     (distro-unabhängig)
#   - CIS Ubuntu Linux 24.04 LTS Benchmark v1.0.0, Abschnittsnummern wie
#     2.1.22 (nur auf Debian/Ubuntu; auf der RHEL-Familie automatisch
#     ausgeblendet, da nicht gegen einen CIS-RHEL-Benchmark verifiziert)

set -u

# ---------------------------------------------------------------------------
# Globale Einstellungen und Ausgabe-Helfer
# ---------------------------------------------------------------------------

SCAN_NETWORK=0
SCAN_CIDR=""
IS_ROOT=0
[ "$(id -u)" -eq 0 ] && IS_ROOT=1

# --- Distributions-Guard ---------------------------------------------------
# Paket- (Bereich 4/6) und Firewall-Prüfungen (Bereich 2) unterscheiden sich
# je nach Distributions-Familie (dpkg/ufw auf Debian/Ubuntu, rpm/firewalld
# auf RHEL/Fedora/CentOS/Rocky/AlmaLinux/Oracle/Amazon Linux). Auf anderen
# Distributionen fehlen die passenden Tools, wodurch die Paket-Checks sonst
# fälschlich "nicht installiert" = OK melden würden (gefährliches
# Fehl-Grün). Deshalb hier erkennen und betroffene Checks bei unbekannter
# Familie als SKIP markieren.
# /etc/os-release wird geparst (nicht gesourct — keine Codeausführung).
OS_ID=""; OS_ID_LIKE=""; OS_PRETTY=""
if [ -r /etc/os-release ]; then
    OS_ID=$(sed -n 's/^ID=//p' /etc/os-release | tr -d '"' | head -1)
    OS_ID_LIKE=$(sed -n 's/^ID_LIKE=//p' /etc/os-release | tr -d '"' | head -1)
    OS_PRETTY=$(sed -n 's/^PRETTY_NAME=//p' /etc/os-release | tr -d '"' | head -1)
fi
OS_IS_DEBIAN=0
case " $OS_ID $OS_ID_LIKE " in
    *" debian "*|*" ubuntu "*) OS_IS_DEBIAN=1 ;;
esac
OS_IS_RHEL=0
case " $OS_ID $OS_ID_LIKE " in
    *" rhel "*|*" fedora "*|*" centos "*|*" rocky "*|*" almalinux "*|*" ol "*|*" amzn "*) OS_IS_RHEL=1 ;;
esac
HAVE_DPKG=0
command -v dpkg-query >/dev/null 2>&1 && HAVE_DPKG=1
HAVE_RPM=0
command -v rpm >/dev/null 2>&1 && HAVE_RPM=1
# Paketprüfung nur belastbar, wenn Distro-Familie UND passendes Tool passen
PKG_FAMILY_OK=0
{ [ "$OS_IS_DEBIAN" -eq 1 ] && [ "$HAVE_DPKG" -eq 1 ]; } && PKG_FAMILY_OK=1
{ [ "$OS_IS_RHEL" -eq 1 ] && [ "$HAVE_RPM" -eq 1 ]; } && PKG_FAMILY_OK=1

# Zähler für die Zusammenfassung
CNT_OK=0 CNT_WARN=0 CNT_RISK=0 CNT_INFO=0 CNT_SKIP=0

# Farben nur auf Terminal und wenn NO_COLOR nicht gesetzt ist
if [ -t 1 ] && [ -z "${NO_COLOR:-}" ]; then
    C_OK=$'\033[32m'; C_WARN=$'\033[33m'; C_RISK=$'\033[31m'
    C_INFO=$'\033[36m'; C_SKIP=$'\033[90m'; C_HEAD=$'\033[1m'; C_RST=$'\033[0m'
else
    C_OK="" C_WARN="" C_RISK="" C_INFO="" C_SKIP="" C_HEAD="" C_RST=""
fi

header() {
    printf '\n%s== %s ==%s\n' "$C_HEAD" "$1" "$C_RST"
}

# CIS-Ubuntu-24.04-Textteile aus einer Referenz entfernen (für Nicht-Debian-
# Familien, wo diese Abschnittsnummern nicht verifiziert sind). Lynis-IDs
# bleiben stehen, da sie distro-unabhängig sind. Wird leer die Referenz
# komplett, macht das der Aufrufer (report()) sichtbar statt sie zu verstecken.
strip_cis_ubuntu_ref() {
    local r="$1"
    r=$(printf '%s' "$r" | sed -E 's/CIS Ubuntu 24\.04( v[0-9]+\.[0-9]+\.[0-9]+)? [0-9x]+(\.[0-9x]+)*([/–-][0-9x]+(\.[0-9x]+)*)*//g')
    r=$(printf '%s' "$r" | sed -E 's/^; *//; s/; *$//; s/; *;/;/g; s/^ +//; s/ +$//')
    printf '%s' "$r"
}

# report STATUS "Beschreibung" "Referenz" ["Detailzeile" ...]
# STATUS: OK | WARN | RISK | INFO | SKIP
report() {
    local status="$1" text="$2" ref="$3" color
    shift 3
    if [ "$OS_IS_DEBIAN" -ne 1 ] && [ -n "$ref" ]; then
        ref="$(strip_cis_ubuntu_ref "$ref")"
        [ -z "$ref" ] && ref="kein CIS/Lynis-Bezug für diese Distro-Familie verifiziert"
    fi
    case "$status" in
        OK)   color=$C_OK;   CNT_OK=$((CNT_OK + 1)) ;;
        WARN) color=$C_WARN; CNT_WARN=$((CNT_WARN + 1)) ;;
        RISK) color=$C_RISK; CNT_RISK=$((CNT_RISK + 1)) ;;
        INFO) color=$C_INFO; CNT_INFO=$((CNT_INFO + 1)) ;;
        SKIP) color=$C_SKIP; CNT_SKIP=$((CNT_SKIP + 1)) ;;
        *)    color="" ;;
    esac
    printf '%s[%-4s]%s %s\n' "$color" "$status" "$C_RST" "$text"
    [ -n "$ref" ] && printf '       Referenz: %s\n' "$ref"
    local line
    for line in "$@"; do
        printf '       %s\n' "$line"
    done
}

need_cmd() {
    command -v "$1" >/dev/null 2>&1
}

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

# ---------------------------------------------------------------------------
# Argumente
# ---------------------------------------------------------------------------

while [ $# -gt 0 ]; do
    case "$1" in
        --scan-network)
            SCAN_NETWORK=1
            if [ $# -gt 1 ] && [ "${2#-}" = "$2" ]; then
                SCAN_CIDR="$2"; shift   # genutzt in Prüfbereich 7 (LAN-Scan)
            fi
            ;;
        -h|--help) usage ;;
        *) printf 'Unbekannte Option: %s (siehe --help)\n' "$1" >&2; exit 2 ;;
    esac
    shift
done

# Nicht-Loopback-Ports aus Bereich 1, genutzt für den Firewall-Abgleich
# (CIS 4.2.6). Format: ein Port pro Zeile, dedupliziert.
NONLOOP_PORTS=""

# ---------------------------------------------------------------------------
# Prüfbereich 1: Lauschende Ports und Dienste
# Referenz: Lynis NETW-3012 (Check listening ports),
#           CIS Ubuntu 24.04 v1.0.0 2.1.22 (Ensure only approved services are
#           listening on a network interface)
# ---------------------------------------------------------------------------

check_listening_ports() {
    header "1. Lauschende Ports und Dienste"

    if ! need_cmd ss; then
        report SKIP "ss (iproute2) nicht gefunden — Port-Check nicht möglich" \
            "Lynis NETW-3012"
        return
    fi

    if [ "$IS_ROOT" -ne 1 ]; then
        report SKIP "Ohne Root fehlen Prozessnamen fremder Dienste (sudo für vollen Umfang)" \
            ""
    fi

    # -H: keine Kopfzeile, -tu: TCP+UDP, -l: listening, -n: numerisch, -p: Prozess
    local ss_out
    ss_out=$(ss -tulnpH 2>/dev/null)

    if [ -z "$ss_out" ]; then
        report SKIP "ss lieferte keine Ausgabe" "Lynis NETW-3012"
        return
    fi

    local total=0 wildcard=0 loopback=0 internal=0
    local proto addr proc laddr port scope label status

    # Format pro Zeile: proto state recvq sendq local:port peer:port [process]
    # state (Spalte 2) ist UNCONN bei UDP, LISTEN bei TCP — beides Listener
    while read -r proto _ _ _ addr _ proc; do
        [ -z "$addr" ] && continue
        total=$((total + 1))

        # Adresse und Port trennen ([::]:22, 0.0.0.0:80, 127.0.0.1:53, *:443)
        port="${addr##*:}"
        laddr="${addr%:*}"

        case "$laddr" in
            127.*|"[::1]")
                scope="nur localhost"; label=OK; loopback=$((loopback + 1)) ;;
            0.0.0.0|\*|"[::]")
                scope="ALLE Interfaces"; label=INFO; wildcard=$((wildcard + 1))
                NONLOOP_PORTS="$NONLOOP_PORTS$port"$'\n' ;;
            *)
                scope="Interface $laddr"; label=OK; internal=$((internal + 1))
                NONLOOP_PORTS="$NONLOOP_PORTS$port"$'\n' ;;
        esac

        # Prozessname aus users:(("name",pid=..,fd=..)) extrahieren
        local pname="?"
        case "$proc" in
            users:*) pname="${proc#*\"}"; pname="${pname%%\"*}" ;;
        esac
        [ "$pname" = "?" ] && [ "$IS_ROOT" -ne 1 ] && pname="? (root nötig)"

        status=$label
        report "$status" \
            "$(printf '%-4s Port %-5s %-18s Prozess: %s' "$proto" "$port" "($scope)" "$pname")" \
            ""
    done <<< "$ss_out"

    report INFO "Gesamt: $total Listener — $wildcard auf allen Interfaces, $loopback nur localhost, $internal interface-gebunden" \
        "Lynis NETW-3012; CIS Ubuntu 24.04 2.1.22" \
        "CIS 2.1.22 verlangt manuelle Freigabe: jeder Listener auf einer Netz-Adresse muss ein gewollter Dienst sein." \
        "Auf allen Interfaces lauschende Dienste von außen erreichbar, sofern die Firewall sie nicht filtert (siehe Bereich 2)."
}

# ---------------------------------------------------------------------------
# Prüfbereich 2: Firewall
# Referenzen (ufw/Debian-Familie): CIS Ubuntu 24.04 v1.0.0
#             4.2.1/4.2.3/4.2.4/4.2.6/4.2.7, Lynis FIRE-4508 (chain
#             policies), FIRE-4512 (ruleset), FIRE-4538 (nftables config),
#             FIRE-4590 (mind. eine FW aktiv). Für firewalld (RHEL-Familie)
#             liegt kein CIS-RHEL-Benchmark vor und Lynis' Testdateien haben
#             keine dedizierte firewalld-Test-ID — dort wird nur FIRE-4590
#             (generisch: irgendeine Firewall aktiv) zitiert, Zonen-/
#             Port-Details laufen unter "Least-Exposure-Prinzip" offen
#             gekennzeichnet ohne Benchmark-Nummer.
# Sprachunabhängigkeit: Status/Policies kommen aus Konfigdateien
# (/etc/ufw/ufw.conf, /etc/default/ufw) bzw. systemctl-Schlüsselwörtern,
# Regel-Ports als numerische Tokens aus `ufw status` — keine übersetzbaren
# Statustexte werden geparst. firewalld liefert über `--list-all`/
# `--info-service` ebenfalls maschinenlesbare, unlokalisierte Schlüssel.
# ---------------------------------------------------------------------------

check_firewall() {
    header "2. Firewall"

    local fw_active=0

    if need_cmd ufw; then
        report OK "ufw ist installiert" "CIS Ubuntu 24.04 4.2.1"

        # Boot-Konfiguration und Dienststatus (sprachunabhängig)
        local ufw_conf_enabled="unbekannt" svc_enabled svc_active
        [ -r /etc/ufw/ufw.conf ] && \
            ufw_conf_enabled=$(sed -n 's/^ENABLED=//p' /etc/ufw/ufw.conf | tail -1)
        svc_enabled=$(systemctl is-enabled ufw 2>/dev/null || true)
        svc_active=$(systemctl is-active ufw 2>/dev/null || true)

        if [ "$ufw_conf_enabled" = "yes" ] && [ "$svc_active" = "active" ]; then
            fw_active=1
            report OK "ufw ist aktiv (ufw.conf ENABLED=yes, Dienst: $svc_active/$svc_enabled)" \
                "CIS Ubuntu 24.04 4.2.3"
        else
            report RISK "ufw ist NICHT aktiv (ENABLED=$ufw_conf_enabled, Dienst: $svc_active/$svc_enabled)" \
                "CIS Ubuntu 24.04 4.2.3; Lynis FIRE-4590"
        fi

        # Default-Policies aus /etc/default/ufw (world-readable, sprachunabhängig)
        if [ -r /etc/default/ufw ]; then
            local pol_in pol_out pol_fwd
            pol_in=$(sed -n 's/^DEFAULT_INPUT_POLICY="\(.*\)"/\1/p' /etc/default/ufw)
            pol_out=$(sed -n 's/^DEFAULT_OUTPUT_POLICY="\(.*\)"/\1/p' /etc/default/ufw)
            pol_fwd=$(sed -n 's/^DEFAULT_FORWARD_POLICY="\(.*\)"/\1/p' /etc/default/ufw)
            case "$pol_in" in
                DROP|REJECT)
                    report OK "Default-Policy eingehend: $pol_in (deny by default)" \
                        "CIS Ubuntu 24.04 4.2.7; Lynis FIRE-4508" ;;
                *)
                    report RISK "Default-Policy eingehend: ${pol_in:-?} — sollte DROP oder REJECT sein" \
                        "CIS Ubuntu 24.04 4.2.7; Lynis FIRE-4508" ;;
            esac
            report INFO "Default-Policy ausgehend: ${pol_out:-?}, Forward: ${pol_fwd:-?}" \
                "CIS Ubuntu 24.04 4.2.5 (manuell: ausgehende Regeln nach Site-Policy)"
        fi

        # Loopback-Regeln (CIS 4.2.4) — before.rules ist nur mit Root lesbar
        if [ -r /etc/ufw/before.rules ]; then
            if grep -q -- '-A ufw-before-input -i lo -j ACCEPT' /etc/ufw/before.rules && \
               grep -q -- '-A ufw-before-output -o lo -j ACCEPT' /etc/ufw/before.rules; then
                report OK "Loopback-Verkehr wird akzeptiert (before.rules)" \
                    "CIS Ubuntu 24.04 4.2.4"
            else
                report WARN "Loopback-ACCEPT-Regeln in before.rules nicht gefunden" \
                    "CIS Ubuntu 24.04 4.2.4"
            fi
        else
            report SKIP "Loopback-Regeln (before.rules) nicht lesbar — Root nötig" \
                "CIS Ubuntu 24.04 4.2.4"
        fi

        # Regel-Abgleich: jeder Nicht-Loopback-Listener braucht eine bewusste
        # Entscheidung (Allow-Regel oder Blockade durch Default-Policy)
        if [ "$IS_ROOT" -eq 1 ] && [ "$fw_active" -eq 1 ]; then
            # Ports aus der ufw-Regeltabelle: erste Spalte, numerische Tokens
            # (auch Kommalisten wie 80,443), Rest der Zeile wird ignoriert
            local ufw_ports
            ufw_ports=$(ufw status 2>/dev/null | grep -oE '^[0-9][0-9,]*' | tr ',' '\n' | sort -un)
            local p covered=0 uncovered=""
            while read -r p; do
                [ -z "$p" ] && continue
                if printf '%s\n' "$ufw_ports" | grep -qx "$p"; then
                    covered=$((covered + 1))
                else
                    uncovered="$uncovered $p"
                fi
            done <<< "$(printf '%s' "$NONLOOP_PORTS" | sort -un)"
            if [ -z "$uncovered" ]; then
                report OK "Alle offenen Ports haben eine explizite ufw-Regel ($covered Ports)" \
                    "CIS Ubuntu 24.04 4.2.6"
            else
                report INFO "Offene Ports OHNE eigene ufw-Regel:$uncovered — werden von der Default-Policy blockiert; prüfen, ob das gewollt ist" \
                    "CIS Ubuntu 24.04 4.2.6"
            fi
        else
            report SKIP "Abgleich offene Ports vs. ufw-Regeln braucht Root und aktives ufw" \
                "CIS Ubuntu 24.04 4.2.6"
        fi
    elif need_cmd firewall-cmd; then
        report OK "firewalld ist installiert" ""

        # Dienststatus (world-readable via systemd, funktioniert ohne Root)
        local svc_enabled svc_active
        svc_enabled=$(systemctl is-enabled firewalld 2>/dev/null || true)
        svc_active=$(systemctl is-active firewalld 2>/dev/null || true)

        if [ "$svc_active" = "active" ]; then
            fw_active=1
            report OK "firewalld ist aktiv (Dienst: $svc_active/$svc_enabled)" \
                "Lynis FIRE-4590"
        else
            report RISK "firewalld ist NICHT aktiv (Dienst: ${svc_active:-inaktiv}/${svc_enabled:-?})" \
                "Lynis FIRE-4590"
        fi

        # firewall-cmd verweigert JEDE Abfrage (auch reines Auslesen) ohne
        # Root mit "NotAuthorizedException" — echt getestet auf Rocky Linux 9
        # (2026-07-27); anders als ufw sind hier auch ohne Root lesbare
        # Config-Dateien nicht vorgesehen (/etc/firewalld ist 0750 root:root).
        if [ "$IS_ROOT" -eq 1 ] && [ "$fw_active" -eq 1 ]; then
            local zone target
            zone=$(firewall-cmd --get-default-zone 2>/dev/null)
            target=$(firewall-cmd --zone="$zone" --list-all 2>/dev/null | sed -n 's/^ *target: *//p')
            case "$target" in
                DROP|%%REJECT%%|REJECT|default)
                    report OK "Default-Zone '$zone', Target: $target (deny by default)" \
                        "Least-Exposure-Prinzip; kein CIS/Lynis-Bezug für firewalld-Zonen-Targets verifiziert" ;;
                ACCEPT)
                    report RISK "Default-Zone '$zone', Target: ACCEPT — nicht deny-by-default" \
                        "Least-Exposure-Prinzip; kein CIS/Lynis-Bezug für firewalld-Zonen-Targets verifiziert" ;;
                *)
                    report WARN "Default-Zone '$zone', Target: ${target:-unbekannt} — nicht als deny-by-default erkannt" \
                        "Least-Exposure-Prinzip; kein CIS/Lynis-Bezug für firewalld-Zonen-Targets verifiziert" ;;
            esac

            # Port-Abgleich: numerische --list-ports UND über --list-services
            # freigegebene Dienste (Portzuordnung je Dienst via --info-service)
            local fw_ports svc s p covered=0 uncovered=""
            fw_ports=$(firewall-cmd --zone="$zone" --list-ports 2>/dev/null | tr ' ' '\n' | grep -oE '^[0-9]+')
            for svc in $(firewall-cmd --zone="$zone" --list-services 2>/dev/null); do
                s=$(firewall-cmd --info-service="$svc" 2>/dev/null | sed -n 's/^ *ports: *//p' | tr ' ' '\n' | grep -oE '^[0-9]+')
                fw_ports="$fw_ports
$s"
            done
            fw_ports=$(printf '%s\n' "$fw_ports" | sort -un)
            while read -r p; do
                [ -z "$p" ] && continue
                if printf '%s\n' "$fw_ports" | grep -qx "$p"; then
                    covered=$((covered + 1))
                else
                    uncovered="$uncovered $p"
                fi
            done <<< "$(printf '%s' "$NONLOOP_PORTS" | sort -un)"
            if [ -z "$uncovered" ]; then
                report OK "Alle offenen Ports sind in der Default-Zone '$zone' freigegeben ($covered Ports)" \
                    "Least-Exposure-Prinzip; kein CIS/Lynis-Bezug für firewalld-Portabdeckung verifiziert"
            else
                report INFO "Offene Ports OHNE Freigabe in Zone '$zone':$uncovered — werden vom Zonen-Target blockiert; prüfen, ob das gewollt ist" \
                    "Least-Exposure-Prinzip; kein CIS/Lynis-Bezug für firewalld-Portabdeckung verifiziert"
            fi
        elif [ "$fw_active" -eq 1 ]; then
            report SKIP "Zonen-/Target-/Port-Details brauchen Root — firewall-cmd verweigert Abfragen ohne Root komplett" \
                "Lynis FIRE-4590"
        fi
    else
        report INFO "Weder ufw noch firewalld installiert — prüfe nftables/iptables direkt" \
            "CIS Ubuntu 24.04 4.2.1"
    fi

    # Fallback/Zusatz: nftables-Ruleset (ufw nutzt auf Ubuntu das nft-Backend)
    if [ "$fw_active" -ne 1 ]; then
        if [ "$IS_ROOT" -eq 1 ] && need_cmd nft; then
            local nft_rules
            nft_rules=$(nft list ruleset 2>/dev/null | grep -c '^' || true)
            if [ "${nft_rules:-0}" -gt 5 ]; then
                fw_active=1
                report OK "nftables-Ruleset vorhanden ($nft_rules Zeilen)" \
                    "Lynis FIRE-4538/FIRE-4512"
            else
                report RISK "Kein nennenswertes nftables-Ruleset geladen" \
                    "Lynis FIRE-4538/FIRE-4512"
            fi
        elif need_cmd nft; then
            report SKIP "nftables-Ruleset nicht prüfbar ohne Root" "Lynis FIRE-4538"
        fi
    fi

    if [ "$fw_active" -ne 1 ] && [ "$IS_ROOT" -eq 1 ]; then
        report RISK "Keine aktive Host-Firewall erkannt" "Lynis FIRE-4590"
    fi

    # Docker umgeht ufw/firewalld: veröffentlichte Container-Ports werden
    # über eine eigene DOCKER-Chain direkt in iptables/nftables erlaubt,
    # bevor die Frontend-Regeln (ufw-Regeln bzw. firewalld-Zonen) greifen.
    if need_cmd docker || [ "$(systemctl is-active docker 2>/dev/null)" = "active" ]; then
        report INFO "Docker ist aktiv: per -p veröffentlichte Container-Ports umgehen ufw/firewalld (eigene DOCKER-Chain)" \
            "Docker-Doku: docs.docker.com/engine/network/packet-filtering-firewalls" \
            "Container-Ports im Zweifel nur an 127.0.0.1 oder interne Interfaces binden."
    fi
}

# ---------------------------------------------------------------------------
# Prüfbereich 3: SSH-Härtung
# Referenzen: CIS Ubuntu 24.04 v1.0.0 5.1.4–5.1.22 (Werte wörtlich aus dem
#             Benchmark), Lynis SSH-7408 (Härtungsstufen gut/mittel/schwach)
# Methode: effektive Konfiguration via `sshd -T` (löst Includes und Defaults
# auf; braucht Root). Ausgabeformat ist maschinenlesbar und sprachunabhängig:
# ein kleingeschriebener Schlüssel + Wert pro Zeile.
# ---------------------------------------------------------------------------

SSHD_CFG=""

# Wert einer sshd-Option aus der gecachten `sshd -T`-Ausgabe (kleingeschrieben)
sshval() {
    printf '%s\n' "$SSHD_CFG" | awk -v k="$1" '$1 == k { $1 = ""; sub(/^ /, ""); print; exit }'
}

# ssh_eq OPTION ERLAUBT(Pipe-getrennt) SEVERITY REFERENZ
ssh_eq() {
    local opt="$1" allowed="$2" sev="$3" ref="$4" val
    val=$(sshval "$opt")
    if [ -z "$val" ]; then
        report SKIP "$opt: nicht in sshd -T enthalten (OpenSSH-Version?)" "$ref"
    elif printf '%s' "$val" | grep -qixE "$allowed"; then
        report OK "$opt = $val" "$ref"
    else
        report "$sev" "$opt = $val — empfohlen: ${allowed//|/ oder }" "$ref"
    fi
}

# ssh_max OPTION MAXWERT SEVERITY REFERENZ [ZUSATZ]
ssh_max() {
    local opt="$1" max="$2" sev="$3" ref="$4" note="${5:-}" val
    val=$(sshval "$opt")
    case "$val" in
        ''|*[!0-9]*)
            report SKIP "$opt: kein numerischer Wert ermittelbar ('$val')" "$ref" ;;
        *)
            if [ "$val" -le "$max" ]; then
                report OK "$opt = $val (≤ $max)" "$ref" ${note:+"$note"}
            else
                report "$sev" "$opt = $val — empfohlen: ≤ $max" "$ref" ${note:+"$note"}
            fi ;;
    esac
}

check_ssh() {
    header "3. SSH-Härtung (sshd)"

    if ! need_cmd sshd; then
        report INFO "OpenSSH-Server nicht installiert — Bereich entfällt" ""
        return
    fi

    SSHD_CFG=$(sshd -T 2>/dev/null)
    if [ -z "$SSHD_CFG" ]; then
        report SKIP "Effektive Konfiguration (sshd -T) nicht lesbar — Root nötig (sshd_config.d/ und Hostkeys sind root-only)" \
            "CIS Ubuntu 24.04 5.1.x; Lynis SSH-7408"
        return
    fi

    # --- Authentisierung / Zugriff -------------------------------------
    local prl
    prl=$(sshval permitrootlogin)
    case "$prl" in
        no)
            report OK "permitrootlogin = no" "CIS Ubuntu 24.04 5.1.20; Lynis SSH-7408" ;;
        prohibit-password|without-password|forced-commands-only)
            report WARN "permitrootlogin = $prl — CIS verlangt 'no'; Lynis stuft '$prl' als akzeptabel ein (kein Passwort-Login für root)" \
                "CIS Ubuntu 24.04 5.1.20; Lynis SSH-7408" ;;
        *)
            report RISK "permitrootlogin = ${prl:-?} — Root-Login per SSH erlaubt" \
                "CIS Ubuntu 24.04 5.1.20; Lynis SSH-7408" ;;
    esac

    ssh_eq permitemptypasswords "no"  RISK "CIS Ubuntu 24.04 5.1.19"
    ssh_eq hostbasedauthentication "no" WARN "CIS Ubuntu 24.04 5.1.10"
    ssh_eq gssapiauthentication "no"  WARN "CIS Ubuntu 24.04 5.1.9"
    ssh_eq ignorerhosts "yes"         WARN "CIS Ubuntu 24.04 5.1.11; Lynis SSH-7408"
    ssh_eq permituserenvironment "no" WARN "CIS Ubuntu 24.04 5.1.21; Lynis SSH-7408"
    ssh_eq usepam "yes"               WARN "CIS Ubuntu 24.04 5.1.22"

    # AllowUsers/AllowGroups/DenyUsers/DenyGroups (CIS 5.1.4, manuelle Bewertung)
    local acc="" k
    for k in allowusers allowgroups denyusers denygroups; do
        [ -n "$(sshval $k)" ] && acc="$acc $k=$(sshval $k)"
    done
    if [ -n "$acc" ]; then
        report OK "SSH-Zugriff eingeschränkt:$acc" "CIS Ubuntu 24.04 5.1.4"
    else
        report INFO "Kein AllowUsers/AllowGroups/DenyUsers/DenyGroups gesetzt — jeder Systemnutzer darf SSH (nach Site-Policy bewerten)" \
            "CIS Ubuntu 24.04 5.1.4 (manuell)"
    fi

    # --- Limits / Protokollierung ---------------------------------------
    ssh_max maxauthtries 4 WARN "CIS Ubuntu 24.04 5.1.16 (≤4); Lynis SSH-7408: ≤3 ideal"
    ssh_max maxsessions 10 WARN "CIS Ubuntu 24.04 5.1.17 (≤10); Lynis SSH-7408: ≤2 ideal"

    local lgt
    lgt=$(sshval logingracetime)
    if [ -n "$lgt" ] && [ "$lgt" -ge 1 ] 2>/dev/null && [ "$lgt" -le 60 ]; then
        report OK "logingracetime = $lgt (1–60s)" "CIS Ubuntu 24.04 5.1.13"
    else
        report WARN "logingracetime = ${lgt:-?} — empfohlen: 1–60 Sekunden" "CIS Ubuntu 24.04 5.1.13"
    fi

    ssh_eq loglevel "VERBOSE|INFO" WARN "CIS Ubuntu 24.04 5.1.14; Lynis SSH-7408: VERBOSE ideal"

    local cai cacm
    cai=$(sshval clientaliveinterval); cacm=$(sshval clientalivecountmax)
    if [ "${cai:-0}" -gt 0 ] 2>/dev/null && [ "${cacm:-0}" -gt 0 ] 2>/dev/null; then
        report OK "clientaliveinterval = $cai, clientalivecountmax = $cacm (beide > 0)" \
            "CIS Ubuntu 24.04 5.1.7"
    else
        report WARN "clientaliveinterval = ${cai:-?}, clientalivecountmax = ${cacm:-?} — beide müssen > 0 sein (tote Sessions beenden)" \
            "CIS Ubuntu 24.04 5.1.7"
    fi

    # MaxStartups: drei Felder start:rate:full, jeweils ≤ 10:30:60
    local ms ms1 ms2 ms3
    ms=$(sshval maxstartups)
    IFS=: read -r ms1 ms2 ms3 <<< "$ms"
    if [ "${ms1:-99}" -le 10 ] 2>/dev/null && [ "${ms2:-99}" -le 30 ] 2>/dev/null && [ "${ms3:-999}" -le 60 ] 2>/dev/null; then
        report OK "maxstartups = $ms (≤ 10:30:60)" "CIS Ubuntu 24.04 5.1.18"
    else
        report WARN "maxstartups = ${ms:-?} — empfohlen: ≤ 10:30:60 (Schutz vor Verbindungs-Flut)" \
            "CIS Ubuntu 24.04 5.1.18"
    fi

    local banner
    banner=$(sshval banner)
    if [ -n "$banner" ] && [ "$banner" != "none" ]; then
        report OK "banner = $banner" "CIS Ubuntu 24.04 5.1.5"
    else
        report INFO "Kein SSH-Banner konfiguriert (Warnhinweis vor Login, nach Site-Policy)" \
            "CIS Ubuntu 24.04 5.1.5"
    fi

    # --- Kryptographie ---------------------------------------------------
    # Schwache Algorithmen wörtlich aus den CIS-Audit-Regexen übernommen
    local weak
    weak=$(sshval ciphers | tr ',' '\n' | grep -iE '(^|-)(3des|arcfour|rijndael)|.-cbc$' | paste -sd, -)
    if [ -n "$weak" ]; then
        report WARN "Schwache Ciphers aktiv: $weak (CBC/3DES/RC4)" "CIS Ubuntu 24.04 5.1.6"
    else
        report OK "Keine schwachen Ciphers (CBC/3DES/RC4) aktiv" "CIS Ubuntu 24.04 5.1.6"
    fi
    if sshval ciphers | grep -q 'chacha20-poly1305@openssh.com'; then
        report INFO "chacha20-poly1305 aktiv — nur unbedenklich, wenn CVE-2023-48795 (Terrapin) gepatcht ist; Ubuntu 24.04 liefert gepatchtes OpenSSH" \
            "CIS Ubuntu 24.04 5.1.6"
    fi

    weak=$(sshval kexalgorithms | tr ',' '\n' | grep -ixE 'diffie-hellman-group1-sha1|diffie-hellman-group14-sha1|diffie-hellman-group-exchange-sha1' | paste -sd, -)
    if [ -n "$weak" ]; then
        report WARN "Schwache Kex-Algorithmen aktiv: $weak" "CIS Ubuntu 24.04 5.1.12"
    else
        report OK "Keine schwachen Kex-Algorithmen (SHA1-DH-Gruppen) aktiv" "CIS Ubuntu 24.04 5.1.12"
    fi

    # CIS 5.1.15 listet zwei Gruppen: kryptografisch schwache MACs (MD5,
    # RIPEMD160, 96-bit, umac-64) und die ETM-MACs, die nur wegen
    # CVE-2023-48795 (Terrapin) gelistet sind. umac-128-etm ist für sich
    # stark und nur Terrapin-relevant — auf gepatchtem 24.04 unkritisch.
    local weak weak_etm
    weak=$(sshval macs | tr ',' '\n' | grep -ixE 'hmac-md5|hmac-md5-96|hmac-ripemd160|hmac-sha1-96|umac-64@openssh\.com|hmac-md5-etm@openssh\.com|hmac-md5-96-etm@openssh\.com|hmac-ripemd160-etm@openssh\.com|hmac-sha1-96-etm@openssh\.com|umac-64-etm@openssh\.com' | paste -sd, -)
    weak_etm=$(sshval macs | tr ',' '\n' | grep -ix 'umac-128-etm@openssh\.com' | paste -sd, -)
    if [ -n "$weak" ]; then
        report WARN "Schwache MACs aktiv: $weak (MD5/RIPEMD160/96-bit/umac-64 — kryptografisch schwach)" "CIS Ubuntu 24.04 5.1.15"
    else
        report OK "Keine kryptografisch schwachen MACs (MD5/RIPEMD160/96-bit/umac-64) aktiv" "CIS Ubuntu 24.04 5.1.15"
    fi
    if [ -n "$weak_etm" ]; then
        report INFO "umac-128-etm aktiv — von CIS 5.1.15 gelistet, aber nur wegen CVE-2023-48795 (Terrapin); auf gepatchtem OpenSSH (Ubuntu 24.04) effektiv unkritisch" "CIS Ubuntu 24.04 5.1.15"
    fi

    # --- Forwarding (CIS 5.1.8 verlangt pauschal DisableForwarding=yes) ---
    local dfw
    dfw=$(sshval disableforwarding)
    if [ "$dfw" = "yes" ]; then
        report OK "disableforwarding = yes (deaktiviert TCP-/X11-/Agent-Forwarding)" \
            "CIS Ubuntu 24.04 5.1.8"
    else
        report WARN "disableforwarding = ${dfw:-?} — CIS empfiehlt 'yes'; wenn SSH-Tunnel gebraucht werden, bewusst abweichen (Site-Policy)" \
            "CIS Ubuntu 24.04 5.1.8" \
            "Einzelwerte: allowtcpforwarding=$(sshval allowtcpforwarding), x11forwarding=$(sshval x11forwarding), allowagentforwarding=$(sshval allowagentforwarding)"
    fi

    # --- Lynis-Härtungsvorschläge ohne CIS-Pflicht (nur INFO) -------------
    local opt exp val findings=""
    for opt in tcpkeepalive:no usedns:no gatewayports:no permittunnel:no printlastlog:yes strictmodes:yes fingerprinthash:SHA256; do
        exp="${opt#*:}"; opt="${opt%%:*}"
        val=$(sshval "$opt")
        if [ -n "$val" ] && [ "${val,,}" != "${exp,,}" ]; then
            findings="$findings $opt=$val(→$exp)"
        fi
    done
    if [ -n "$findings" ]; then
        report INFO "Lynis-Härtungsvorschläge (optional):$findings" "Lynis SSH-7408"
    else
        report OK "Alle optionalen Lynis-Härtungswerte erfüllt (tcpkeepalive, usedns, gatewayports, permittunnel, printlastlog, strictmodes, fingerprinthash)" \
            "Lynis SSH-7408"
    fi

    local port
    port=$(sshval port)
    [ "$port" = "22" ] && report INFO "SSH auf Standard-Port 22 — Portwechsel ist laut Lynis optionale Verschleierung, kein Sicherheitsgewinn im engeren Sinn" \
        "Lynis SSH-7408 (Port != 22 als 'gut' gewertet)"
}

# ---------------------------------------------------------------------------
# Prüfbereich 4: Unverschlüsselte / Klartext-Dienste
# Referenzen: CIS Ubuntu 24.04 v1.0.0 2.1.x (Server-Dienste), 2.2.1–2.2.6
#             (Client-Pakete), 2.1.21 (MTA local-only),
#             Lynis INSE-8xxx (tests_insecure_services)
# Methode: Paketstatus über dpkg-Status-Tokens (stdout, feste Werte,
# sprachunabhängig), Dienststatus über systemctl-Schlüsselwörter,
# Klartext-Listener über die Portliste aus Bereich 1.
# ---------------------------------------------------------------------------

pkg_installed() {
    if [ "$HAVE_DPKG" -eq 1 ]; then
        dpkg-query -W -f='${Status}' "$1" 2>/dev/null | grep -q 'install ok installed'
    elif [ "$HAVE_RPM" -eq 1 ]; then
        rpm -q --quiet "$1" 2>/dev/null
    else
        return 1
    fi
}

check_plaintext_services() {
    header "4. Klartext-Dienste"

    local entry pkg ref found

    # --- Server-Pakete: sollten nicht installiert sein --------------------
    # Paketnamen sind je Distro-Familie verschieden (dpkg vs. rpm); ohne
    # passendes Tool zur erkannten Familie nicht belastbar prüfbar (Guard).
    # Debian/Ubuntu-Namen und RHEL-Namen (telnet-server, rsh-server,
    # tftp-server, net-snmp, ypserv/ypbind …) stammen aus Lynis'
    # PACKAGES=-Listen (tests_insecure_services) bzw. echtem Testlauf auf
    # Rocky Linux 9 (dnf/rpm) am 2026-07-27.
    if [ "$PKG_FAMILY_OK" -ne 1 ]; then
        report SKIP "Server-Altdienst-Pakete nicht geprüft — kein passender Paketmanager zur erkannten Distro-Familie (dpkg/rpm)" \
            "Guard: Paketprüfung nur auf Debian/Ubuntu (dpkg) oder RHEL-Familie (rpm) belastbar"
    else
    found=0
    if [ "$OS_IS_DEBIAN" -eq 1 ]; then
        for entry in \
            "telnetd|Lynis INSE-8322" \
            "telnetd-ssl|Lynis INSE-8322" \
            "inetutils-telnetd|Lynis INSE-8322" \
            "rsh-server|Lynis INSE-8304" \
            "rsh-redone-server|Lynis INSE-8304" \
            "vsftpd|CIS Ubuntu 24.04 2.1.6" \
            "proftpd-core|CIS Ubuntu 24.04 2.1.6" \
            "pure-ftpd|CIS Ubuntu 24.04 2.1.6" \
            "nis|CIS Ubuntu 24.04 2.1.10; Lynis INSE-8316" \
            "tftpd-hpa|CIS Ubuntu 24.04 2.1.16; Lynis INSE-8320" \
            "atftpd|CIS Ubuntu 24.04 2.1.16; Lynis INSE-8320" \
            "xinetd|CIS Ubuntu 24.04 2.1.19; Lynis INSE-8100" \
            "openbsd-inetd|Lynis INSE-8000" \
            "inetutils-inetd|Lynis INSE-8000" \
            "snmpd|CIS Ubuntu 24.04 2.1.15"; do
            pkg="${entry%%|*}"; ref="${entry#*|}"
            if pkg_installed "$pkg"; then
                found=1
                report RISK "Klartext-/Altdienst-Serverpaket installiert: $pkg" "$ref"
            fi
        done
    else
        for entry in \
            "telnet-server|Lynis INSE-8322" \
            "rsh-server|Lynis INSE-8304" \
            "vsftpd|CIS Ubuntu 24.04 2.1.6" \
            "proftpd|CIS Ubuntu 24.04 2.1.6" \
            "pure-ftpd|CIS Ubuntu 24.04 2.1.6" \
            "ypserv|CIS Ubuntu 24.04 2.1.10; Lynis INSE-8316" \
            "tftp-server|CIS Ubuntu 24.04 2.1.16; Lynis INSE-8320" \
            "xinetd|CIS Ubuntu 24.04 2.1.19; Lynis INSE-8100" \
            "net-snmp|CIS Ubuntu 24.04 2.1.15"; do
            pkg="${entry%%|*}"; ref="${entry#*|}"
            if pkg_installed "$pkg"; then
                found=1
                report RISK "Klartext-/Altdienst-Serverpaket installiert: $pkg" "$ref"
            fi
        done
    fi
    [ "$found" -eq 0 ] && report OK "Keine Klartext-Server installiert (telnet/rsh/ftp/nis/tftp/inetd/xinetd/snmpd)" \
        "CIS Ubuntu 24.04 2.1.6/2.1.10/2.1.15/2.1.16/2.1.19; Lynis INSE-8000–8322"
    fi

    # rsync als Dienst (Paket allein ist als Kopiertool üblich und harmlos).
    # Debian nennt den Dienst "rsync", ein manuell eingerichteter RHEL-Dienst
    # meist "rsyncd" (RHEL liefert dafür standardmäßig keine eigene Unit).
    local rsync_svc="" s
    for s in rsync rsyncd; do
        [ "$(systemctl is-active "$s" 2>/dev/null)" = "active" ] && rsync_svc="$s"
    done
    if [ -n "$rsync_svc" ]; then
        report WARN "rsync-Daemon ist aktiv ($rsync_svc) — überträgt ohne SSH-Wrapper unverschlüsselt" \
            "CIS Ubuntu 24.04 2.1.13"
    else
        report OK "Kein rsync-Daemon aktiv" "CIS Ubuntu 24.04 2.1.13"
    fi

    # --- Client-Pakete: laut CIS 2.2.x nicht installieren ------------------
    if [ "$PKG_FAMILY_OK" -ne 1 ]; then
        report SKIP "Klartext-Client-Pakete nicht geprüft — kein passender Paketmanager zur erkannten Distro-Familie (dpkg/rpm)" \
            "Guard: Paketprüfung nur auf Debian/Ubuntu (dpkg) oder RHEL-Familie (rpm) belastbar"
    else
    found=0
    if [ "$OS_IS_DEBIAN" -eq 1 ]; then
        for entry in \
            "nis|CIS Ubuntu 24.04 2.2.1" \
            "rsh-client|CIS Ubuntu 24.04 2.2.2; Lynis INSE-8300" \
            "rsh-redone-client|CIS Ubuntu 24.04 2.2.2; Lynis INSE-8300" \
            "talk|CIS Ubuntu 24.04 2.2.3" \
            "telnet|CIS Ubuntu 24.04 2.2.4; Lynis INSE-8310" \
            "inetutils-telnet|CIS Ubuntu 24.04 2.2.4; Lynis INSE-8310" \
            "ldap-utils|CIS Ubuntu 24.04 2.2.5" \
            "ftp|CIS Ubuntu 24.04 2.2.6" \
            "tnftp|CIS Ubuntu 24.04 2.2.6" \
            "tftp-hpa|Lynis INSE-8318"; do
            pkg="${entry%%|*}"; ref="${entry#*|}"
            # Übergangs-Dummys nicht doppelt melden (auf Ubuntu 24.04 sind
            # telnet→inetutils-telnet und ftp→tnftp reine Transitional-Pakete)
            case "$pkg" in
                telnet) pkg_installed inetutils-telnet && continue ;;
                ftp)    pkg_installed tnftp && continue ;;
            esac
            if pkg_installed "$pkg"; then
                found=1
                report WARN "Klartext-Client installiert: $pkg — CIS empfiehlt Deinstallation" "$ref"
            fi
        done
    else
        for entry in \
            "ypbind|CIS Ubuntu 24.04 2.2.1" \
            "rsh|CIS Ubuntu 24.04 2.2.2; Lynis INSE-8300" \
            "talk|CIS Ubuntu 24.04 2.2.3" \
            "telnet|CIS Ubuntu 24.04 2.2.4; Lynis INSE-8310" \
            "openldap-clients|CIS Ubuntu 24.04 2.2.5" \
            "ftp|CIS Ubuntu 24.04 2.2.6" \
            "tftp|Lynis INSE-8318"; do
            pkg="${entry%%|*}"; ref="${entry#*|}"
            if pkg_installed "$pkg"; then
                found=1
                report WARN "Klartext-Client installiert: $pkg — CIS empfiehlt Deinstallation" "$ref"
            fi
        done
    fi
    [ "$found" -eq 0 ] && report OK "Keine Klartext-Clients installiert (telnet/rsh/talk/ldap/ftp/tftp/nis)" \
        "CIS Ubuntu 24.04 2.2.1–2.2.6; Lynis INSE-8300–8318"
    fi

    # --- rsh-Vertrauensdateien (Lynis INSE-8302) ---------------------------
    if [ -s /etc/hosts.equiv ]; then
        report RISK "/etc/hosts.equiv existiert und ist nicht leer — rsh-artige Host-Vertrauensstellung" \
            "Lynis INSE-8302"
    else
        report OK "Keine /etc/hosts.equiv-Vertrauensdatei" "Lynis INSE-8302"
    fi

    # --- Klartext-Protokolle auf Nicht-Loopback-Ports (aus Bereich 1) ------
    local p
    while read -r p; do
        [ -z "$p" ] && continue
        case "$p" in
            21)  report RISK "Port 21 (FTP) lauscht auf Netz-Interface — Klartext-Anmeldedaten" "CIS Ubuntu 24.04 2.1.6" ;;
            23)  report RISK "Port 23 (Telnet) lauscht auf Netz-Interface — Klartext-Sitzungen" "Lynis INSE-8322" ;;
            69)  report RISK "Port 69 (TFTP) lauscht auf Netz-Interface — unauthentisierter Dateizugriff" "CIS Ubuntu 24.04 2.1.16" ;;
            110) report WARN "Port 110 (POP3) lauscht auf Netz-Interface — nur mit TLS (995/STARTTLS) betreiben" "CIS Ubuntu 24.04 2.1.8" ;;
            143) report WARN "Port 143 (IMAP) lauscht auf Netz-Interface — nur mit TLS (993/STARTTLS) betreiben" "CIS Ubuntu 24.04 2.1.8" ;;
            512|513|514) report RISK "Port $p (rexec/rlogin/rsh) lauscht auf Netz-Interface" "Lynis INSE-8304" ;;
            25)  report WARN "Port 25 (SMTP) lauscht auf Netz-Interface — MTA sollte laut CIS nur auf Loopback lauschen (sofern kein Mailserver)" "CIS Ubuntu 24.04 2.1.21" ;;
            80)  report INFO "Port 80 (HTTP, Klartext) offen — unbedenklich, wenn nur Redirect auf HTTPS ausgeliefert wird" "CIS Ubuntu 24.04 2.1.18 (Webserver nach Site-Policy)" ;;
        esac
    done <<< "$(printf '%s' "$NONLOOP_PORTS" | sort -un)"
    report OK "Klartext-Port-Prüfung abgeschlossen (21/23/25/69/110/143/512–514/80 gegen Bereich-1-Liste)" \
        "CIS Ubuntu 24.04 2.1.x; Lynis INSE-8xxx"
}

# ---------------------------------------------------------------------------
# Prüfbereich 5: Kernel-/Netzwerkparameter (sysctl)
# Referenz: CIS Ubuntu 24.04 v1.0.0 3.3.1–3.3.11 (Netzwerk), 1.5.1–1.5.3
#           (ASLR, ptrace_scope, Core-Dumps); Lynis KRNL-6000 (Sollwerte aus
#           default.prf des Lynis-Repos)
# Gelesen wird direkt aus /proc/sys (sprachunabhängig, ohne Root lesbar).
# Hinweis: Geprüft wird die LAUFENDE Konfiguration. CIS verlangt zusätzlich,
# dass die Werte persistent gesetzt sind (/etc/sysctl.d/ u. a.) — diese
# Persistenz-Prüfung macht das Skript nicht.
# ---------------------------------------------------------------------------

# Wert eines sysctl-Schlüssels aus /proc/sys lesen; leer, wenn nicht vorhanden
sysctl_val() {
    local f="/proc/sys/${1//./\/}"
    [ -r "$f" ] && cat "$f"
}

# sysctl_check SCHLÜSSEL SOLL REFERENZ [DETAILZEILE ...]
# SOLL darf mehrere erlaubte Werte enthalten, mit | getrennt (z. B. "1|2|3").
sysctl_check() {
    local key="$1" want="$2" ref="$3" val
    shift 3
    val="$(sysctl_val "$key")"
    if [ -z "$val" ]; then
        case "$key" in
            net.ipv6.*) report OK "$key nicht vorhanden (IPv6 deaktiviert)" "$ref" ;;
            *)          report SKIP "$key auf diesem Kernel nicht vorhanden" "$ref" ;;
        esac
        return
    fi
    if printf '%s' "$val" | grep -qxE "($want)"; then
        report OK "$key = $val" "$ref" "$@"
    else
        report WARN "$key = $val — Benchmark erwartet ${want//|/ oder }" "$ref" "$@"
    fi
}

check_sysctl() {
    header "5. Kernel-/Netzwerkparameter (sysctl)"

    report INFO "Geprüft wird die laufende Konfiguration (/proc/sys); CIS verlangt die Werte zusätzlich persistent (z. B. /etc/sysctl.d/)" \
        "CIS Ubuntu 24.04 3.3 (Audit umfasst running config UND Konfig-Dateien)"

    # --- CIS 3.3.1: IP-Forwarding (Docker-Sonderfall) ----------------------
    local docker_active=0 fwd
    if need_cmd docker || [ "$(systemctl is-active docker 2>/dev/null)" = "active" ]; then
        docker_active=1
    fi
    fwd="$(sysctl_val net.ipv4.ip_forward)"
    if [ "$fwd" = "0" ]; then
        report OK "net.ipv4.ip_forward = 0" "CIS Ubuntu 24.04 3.3.1; Lynis KRNL-6000"
    elif [ "$docker_active" -eq 1 ]; then
        report INFO "net.ipv4.ip_forward = ${fwd:-?} — von Docker für Container-Routing benötigt; CIS-Sollwert 0 gilt für Nicht-Router" \
            "CIS Ubuntu 24.04 3.3.1 (Impact-Abschnitt: Router-/CSP-Systeme benötigen Forwarding)"
    else
        report WARN "net.ipv4.ip_forward = ${fwd:-?} — System leitet Pakete weiter (Router-Rolle); CIS erwartet 0" \
            "CIS Ubuntu 24.04 3.3.1; Lynis KRNL-6000"
    fi
    sysctl_check net.ipv6.conf.all.forwarding 0 "CIS Ubuntu 24.04 3.3.1"

    # --- CIS 3.3.2–3.3.6, 3.3.8–3.3.11: feste Sollwert-Paare ---------------
    local entry key want ref
    for entry in \
        "net.ipv4.conf.all.send_redirects|0|CIS Ubuntu 24.04 3.3.2; Lynis KRNL-6000" \
        "net.ipv4.conf.default.send_redirects|0|CIS Ubuntu 24.04 3.3.2" \
        "net.ipv4.icmp_ignore_bogus_error_responses|1|CIS Ubuntu 24.04 3.3.3; Lynis KRNL-6000" \
        "net.ipv4.icmp_echo_ignore_broadcasts|1|CIS Ubuntu 24.04 3.3.4; Lynis KRNL-6000" \
        "net.ipv4.conf.all.accept_redirects|0|CIS Ubuntu 24.04 3.3.5; Lynis KRNL-6000" \
        "net.ipv4.conf.default.accept_redirects|0|CIS Ubuntu 24.04 3.3.5; Lynis KRNL-6000" \
        "net.ipv6.conf.all.accept_redirects|0|CIS Ubuntu 24.04 3.3.5; Lynis KRNL-6000" \
        "net.ipv6.conf.default.accept_redirects|0|CIS Ubuntu 24.04 3.3.5; Lynis KRNL-6000" \
        "net.ipv4.conf.all.secure_redirects|0|CIS Ubuntu 24.04 3.3.6" \
        "net.ipv4.conf.default.secure_redirects|0|CIS Ubuntu 24.04 3.3.6" \
        "net.ipv4.conf.all.accept_source_route|0|CIS Ubuntu 24.04 3.3.8; Lynis KRNL-6000" \
        "net.ipv4.conf.default.accept_source_route|0|CIS Ubuntu 24.04 3.3.8; Lynis KRNL-6000" \
        "net.ipv6.conf.all.accept_source_route|0|CIS Ubuntu 24.04 3.3.8; Lynis KRNL-6000" \
        "net.ipv6.conf.default.accept_source_route|0|CIS Ubuntu 24.04 3.3.8; Lynis KRNL-6000" \
        "net.ipv4.conf.all.log_martians|1|CIS Ubuntu 24.04 3.3.9; Lynis KRNL-6000" \
        "net.ipv4.conf.default.log_martians|1|CIS Ubuntu 24.04 3.3.9; Lynis KRNL-6000" \
        "net.ipv4.tcp_syncookies|1|CIS Ubuntu 24.04 3.3.10; Lynis KRNL-6000"; do
        IFS='|' read -r key want ref <<< "$entry"
        sysctl_check "$key" "$want" "$ref"
    done

    # 3.3.11: Router Advertisements — Hinweis, weil manche Hoster die
    # IPv6-Adresse per RA/SLAAC zuweisen (dann bricht accept_ra=0 IPv6)
    sysctl_check net.ipv6.conf.all.accept_ra 0 "CIS Ubuntu 24.04 3.3.11" \
        "Hinweis: Wenn der Hoster IPv6 per Router Advertisement (SLAAC) zuweist, ist accept_ra nötig — dann vor dem Ändern prüfen."
    sysctl_check net.ipv6.conf.default.accept_ra 0 "CIS Ubuntu 24.04 3.3.11"

    # --- CIS 3.3.7: Reverse-Path-Filter (Sonderfall Loose Mode) ------------
    local rpf
    for key in net.ipv4.conf.all.rp_filter net.ipv4.conf.default.rp_filter; do
        rpf="$(sysctl_val "$key")"
        case "$rpf" in
            1) report OK "$key = 1 (Strict Mode)" "CIS Ubuntu 24.04 3.3.7; Lynis KRNL-6000" ;;
            2) report WARN "$key = 2 (Loose Mode, Ubuntu-Standard) — CIS erwartet 1 (Strict Mode)" \
                   "CIS Ubuntu 24.04 3.3.7; Lynis KRNL-6000" \
                   "Hinweis: Strict Mode kann bei asymmetrischem Routing (Multi-Homing, teils Docker-Setups) legitime Pakete verwerfen." ;;
            *) report WARN "$key = ${rpf:-?} — Reverse-Path-Filter deaktiviert; CIS erwartet 1" \
                   "CIS Ubuntu 24.04 3.3.7; Lynis KRNL-6000" ;;
        esac
    done

    # --- CIS 1.5.1–1.5.3: Kernel-Härtung (ASLR, ptrace, Core-Dumps) --------
    sysctl_check kernel.randomize_va_space 2 "CIS Ubuntu 24.04 1.5.1; Lynis KRNL-6000"
    sysctl_check kernel.yama.ptrace_scope "1|2|3" "CIS Ubuntu 24.04 1.5.2; Lynis KRNL-6000"
    sysctl_check fs.suid_dumpable 0 "CIS Ubuntu 24.04 1.5.3; Lynis KRNL-6000" \
        "Hinweis: Wert 2 = 'suidsafe' (Dump nur über Pipe-Handler, auf Ubuntu meist apport); CIS verlangt dennoch 0."

    # --- Nur-Lynis-Empfehlungen (kein CIS-Pendant im Benchmark): gebündelt --
    # Sollwerte wörtlich aus Lynis default.prf (KRNL-6000, config-data=sysctl).
    # Lynis wertet Abweichungen als Vorschläge für den Hardening-Index,
    # nicht als Pflicht — daher hier gesammelt als INFO.
    local -a lynis_dev=()
    local val
    for entry in \
        "net.ipv4.conf.all.forwarding:0" \
        "net.ipv4.conf.all.bootp_relay:0" \
        "net.ipv4.conf.all.proxy_arp:0" \
        "net.ipv4.conf.all.mc_forwarding:0" \
        "net.ipv4.tcp_timestamps:0|1" \
        "net.ipv6.conf.all.send_redirects:0" \
        "kernel.sysrq:0" \
        "kernel.dmesg_restrict:1" \
        "kernel.kptr_restrict:2" \
        "kernel.unprivileged_bpf_disabled:1" \
        "kernel.core_uses_pid:1" \
        "kernel.ctrl-alt-del:0" \
        "kernel.perf_event_paranoid:2|3|4" \
        "kernel.modules_disabled:1" \
        "net.core.bpf_jit_harden:2" \
        "fs.protected_fifos:2" \
        "fs.protected_hardlinks:1" \
        "fs.protected_regular:2" \
        "fs.protected_symlinks:1" \
        "dev.tty.ldisc_autoload:0"; do
        key="${entry%%:*}"; want="${entry#*:}"
        val="$(sysctl_val "$key")"
        [ -z "$val" ] && continue   # Schlüssel existiert auf diesem Kernel nicht
        printf '%s' "$val" | grep -qxE "($want)" \
            || lynis_dev+=("$key = $val (Lynis-Sollwert: ${want//|/ oder })")
    done
    if [ "${#lynis_dev[@]}" -eq 0 ]; then
        report OK "Alle weiteren Lynis-Kernel-Empfehlungen erfüllt" "Lynis KRNL-6000 (default.prf)"
    else
        report INFO "${#lynis_dev[@]} weitere Lynis-Kernel-Empfehlungen weichen ab (Vorschläge, keine CIS-Pflicht):" \
            "Lynis KRNL-6000 (default.prf)" \
            "${lynis_dev[@]}"
    fi
}

# ---------------------------------------------------------------------------
# Prüfbereich 6: Netzwerk-Freigaben (NFS/Samba)
# Referenz: CIS Ubuntu 24.04 v1.0.0 2.1.9 (NFS-Server), 2.1.12 (rpcbind),
#           2.1.14 (Samba); Lynis STRG-1920 (NFS-Daemon), STRG-1926/1928
#           (/etc/exports), STRG-1930 (Client-Einschränkung der Exporte)
# CIS-Logik: Paket nicht installiert ODER — falls als Abhängigkeit nötig —
# Dienst gestoppt und maskiert.
# ---------------------------------------------------------------------------

check_shares() {
    header "6. Netzwerk-Freigaben (NFS/Samba)"

    # Paketprüfungen (NFS/rpcbind/Samba) brauchen den zur Distro-Familie
    # passenden Paketmanager (Guard). NFS-Serverpaket heißt auf Debian/Ubuntu
    # "nfs-kernel-server", auf der RHEL-Familie "nfs-utils" (echt getestet
    # auf Rocky Linux 9, 2026-07-27) — die Unit "nfs-server.service" ist auf
    # beiden Familien identisch.
    local nfs_pkg="nfs-kernel-server" samba_svc="smbd.service"
    if [ "$OS_IS_RHEL" -eq 1 ]; then
        nfs_pkg="nfs-utils"; samba_svc="smb.service"
    fi
    if [ "$PKG_FAMILY_OK" -ne 1 ]; then
        report SKIP "NFS-/rpcbind-/Samba-Paketprüfung übersprungen — kein passender Paketmanager zur erkannten Distro-Familie (dpkg/rpm)" \
            "Guard: Paketprüfung nur auf Debian/Ubuntu (dpkg) oder RHEL-Familie (rpm) belastbar"
    else
    # --- NFS-Server (CIS 2.1.9; Lynis STRG-1920) ---------------------------
    if pkg_installed "$nfs_pkg"; then
        if [ "$(systemctl is-active nfs-server.service 2>/dev/null)" = "active" ]; then
            report WARN "NFS-Server installiert und aktiv — CIS: entfernen, wenn nicht benötigt" \
                "CIS Ubuntu 24.04 2.1.9; Lynis STRG-1920"
        else
            report INFO "$nfs_pkg installiert, Dienst nfs-server aber nicht aktiv — CIS: Paket entfernen oder Dienst maskieren" \
                "CIS Ubuntu 24.04 2.1.9"
        fi
        # /etc/exports auswerten (Lynis STRG-1926/1928/1930)
        if [ -r /etc/exports ]; then
            local exports line
            exports="$(grep -vE '^[[:space:]]*(#|$)' /etc/exports 2>/dev/null)"
            if [ -z "$exports" ]; then
                report INFO "/etc/exports enthält keine Freigaben" "Lynis STRG-1926/1928"
            else
                while IFS= read -r line; do
                    if printf '%s' "$line" | grep -q '\*'; then
                        report RISK "NFS-Export ohne/mit Wildcard-Client-Einschränkung: $line" \
                            "Lynis STRG-1930 (erlaubte Clients explizit angeben)"
                    else
                        report INFO "NFS-Export: $line" "Lynis STRG-1926"
                    fi
                done <<< "$exports"
            fi
        elif [ -f /etc/exports ]; then
            report SKIP "/etc/exports nicht lesbar (root nötig)" "Lynis STRG-1926"
        fi
    else
        report OK "Kein NFS-Server installiert ($nfs_pkg)" "CIS Ubuntu 24.04 2.1.9"
    fi

    # --- rpcbind/Portmapper (CIS 2.1.12) -----------------------------------
    if pkg_installed rpcbind; then
        if [ "$(systemctl is-active rpcbind.service 2>/dev/null)" = "active" ] \
            || [ "$(systemctl is-active rpcbind.socket 2>/dev/null)" = "active" ]; then
            report WARN "rpcbind installiert und aktiv (Portmapper, Port 111) — laut CIS DDoS-Verstärker; entfernen oder stoppen+maskieren" \
                "CIS Ubuntu 24.04 2.1.12"
        else
            report INFO "rpcbind installiert, aber inaktiv — CIS: Paket entfernen oder Dienst und Socket maskieren" \
                "CIS Ubuntu 24.04 2.1.12"
        fi
    else
        report OK "Kein rpcbind/Portmapper installiert" "CIS Ubuntu 24.04 2.1.12"
    fi

    # --- Samba (CIS 2.1.14) ------------------------------------------------
    # Servicename: Debian/Ubuntu "smbd.service", RHEL-Familie "smb.service"
    # (echt getestet auf Rocky Linux 9, 2026-07-27)
    if pkg_installed samba; then
        if [ "$(systemctl is-active "$samba_svc" 2>/dev/null)" = "active" ]; then
            report WARN "Samba installiert und $samba_svc aktiv — CIS: entfernen, wenn keine SMB-Freigaben benötigt werden" \
                "CIS Ubuntu 24.04 2.1.14"
        else
            report INFO "Samba installiert, $samba_svc aber nicht aktiv — CIS: Paket entfernen oder $samba_svc maskieren" \
                "CIS Ubuntu 24.04 2.1.14"
        fi
        # Definierte Freigaben nur auflisten (reine Information; ob eine
        # Freigabe nötig ist, ist Site-Policy — CIS bewertet nur "in use")
        if [ -r /etc/samba/smb.conf ]; then
            local -a shares=()
            mapfile -t shares < <(grep -oE '^[[:space:]]*\[[^]]+\]' /etc/samba/smb.conf \
                | sed -E 's/^[[:space:]]*\[//; s/\]$//' | grep -vix global || true)
            if [ "${#shares[@]}" -gt 0 ]; then
                report INFO "In smb.conf definierte Freigabe-Abschnitte (Bewertung nach Site-Policy):" \
                    "CIS Ubuntu 24.04 2.1.14" "${shares[@]}"
            fi
        elif [ -f /etc/samba/smb.conf ]; then
            report SKIP "/etc/samba/smb.conf nicht lesbar" "CIS Ubuntu 24.04 2.1.14"
        fi
    else
        report OK "Kein Samba-Server installiert (Paket samba)" "CIS Ubuntu 24.04 2.1.14"
    fi
    fi

    # --- Freigabe-Ports auf Netz-Interfaces (aus Bereich 1) ----------------
    local p found=0
    while read -r p; do
        [ -z "$p" ] && continue
        case "$p" in
            111)     found=1; report RISK "Port 111 (rpcbind/Portmapper) lauscht auf Netz-Interface — DDoS-Verstärkung, RPC-Enumeration" "CIS Ubuntu 24.04 2.1.12" ;;
            2049)    found=1; report RISK "Port 2049 (NFS) lauscht auf Netz-Interface" "CIS Ubuntu 24.04 2.1.9" ;;
            139|445) found=1; report RISK "Port $p (SMB/NetBIOS) lauscht auf Netz-Interface" "CIS Ubuntu 24.04 2.1.14" ;;
        esac
    done <<< "$(printf '%s' "$NONLOOP_PORTS" | sort -un)"
    [ "$found" -eq 0 ] && report OK "Keine Freigabe-Ports (111/2049/139/445) auf Netz-Interfaces" \
        "CIS Ubuntu 24.04 2.1.9/2.1.12/2.1.14"
}

# ---------------------------------------------------------------------------
# Prüfbereich 7: Netzwerk-Scan (LAN, aktiv) — Modus 2 via --scan-network
#
# WICHTIG: Dies ist KEINE Benchmark-Prüfung (weder CIS noch Lynis bewerten
# fremde Hosts), sondern ein Inventar-/Discovery-Werkzeug. Es sendet aktiv
# TCP-Verbindungsversuche an andere Geräte. Erlaubt ist das NUR im eigenen
# bzw. selbst administrierten Netz. Der Scanner scannt daher ausschließlich
# private Netze (RFC 1918); öffentliche Adressbereiche werden abgelehnt.
#
# Umsetzung: reines Bash über /dev/tcp (kein nmap), optional ping zur
# Host-Erkennung. Gefundene Klartext-/Risiko-Ports werden mit derselben
# Begründung wie in Prüfbereich 4 markiert (als Ferndiagnose gekennzeichnet).
# ---------------------------------------------------------------------------

SCAN7_MAX_HOSTS=1024
SCAN7_BATCH=48
SCAN7_DISCOVERY_PORTS="22 80 443 445 3389 8080"
SCAN7_PORTS="21 22 23 25 53 80 110 111 135 139 143 443 445 3306 3389 5432 5900 6379 8080 8443 9200 27017"
SCAN7_HAVE_PING=0
need_cmd ping && SCAN7_HAVE_PING=1

ip_to_int() {
    local IFS=. a b c d
    read -r a b c d <<< "$1"
    printf '%s' "$(( (a << 24) + (b << 16) + (c << 8) + d ))"
}

int_to_ip() {
    local n="$1"
    printf '%d.%d.%d.%d' "$(( (n >> 24) & 255 ))" "$(( (n >> 16) & 255 ))" \
        "$(( (n >> 8) & 255 ))" "$(( n & 255 ))"
}

# Strikte CIDR-Validierung (Eingabe-Absicherung)
valid_cidr() {
    local cidr="$1" ip prefix a b c d rest o
    case "$cidr" in */*) ;; *) return 1 ;; esac
    ip="${cidr%/*}"; prefix="${cidr#*/}"
    [ -n "$prefix" ] || return 1
    case "$prefix" in *[!0-9]*) return 1 ;; esac
    [ "$prefix" -le 32 ] || return 1
    IFS=. read -r a b c d rest <<< "$ip"
    [ -z "$rest" ] || return 1
    for o in "$a" "$b" "$c" "$d"; do
        [ -n "$o" ] || return 1
        case "$o" in *[!0-9]*) return 1 ;; esac
        [ "$o" -le 255 ] || return 1
    done
    return 0
}

# Privates Netz nach RFC 1918? (nur solche werden gescannt)
is_private_cidr() {
    local ip="${1%/*}" a b
    IFS=. read -r a b _ _ <<< "$ip"
    [ "$a" -eq 10 ] && return 0
    [ "$a" -eq 192 ] && [ "$b" -eq 168 ] && return 0
    [ "$a" -eq 172 ] && [ "$b" -ge 16 ] && [ "$b" -le 31 ] && return 0
    return 1
}

# TCP-Connect-Test: tcp_open HOST PORT TIMEOUT  -> 0 wenn offen
tcp_open() {
    timeout "$3" bash -c ": >/dev/tcp/$1/$2" 2>/dev/null
}

scan7_host_up() {
    local host="$1" p
    if [ "$SCAN7_HAVE_PING" -eq 1 ] && ping -c1 -W1 "$host" >/dev/null 2>&1; then
        return 0
    fi
    for p in $SCAN7_DISCOVERY_PORTS; do
        tcp_open "$host" "$p" 0.7 && return 0
    done
    return 1
}

# Läuft im Hintergrund je Host; schreibt "ip port port ..." in die Ergebnisdatei
scan7_host() {
    local host="$1" outfile="$2" p open=""
    scan7_host_up "$host" || return 0
    for p in $SCAN7_PORTS; do
        tcp_open "$host" "$p" 1 && open="$open $p"
    done
    printf '%s%s\n' "$host" "$open" > "$outfile"
}

# Ein gefundener offener Port -> Bewertung (Ferndiagnose)
scan7_classify() {
    local host="$1" port="$2"
    case "$port" in
        23)      report RISK "  $host:23 Telnet erreichbar — Klartext-Login" "Lynis INSE-8322 (aus der Ferne beobachtet)" ;;
        21)      report RISK "  $host:21 FTP erreichbar — Klartext-Login" "CIS Ubuntu 24.04 2.1.6 (aus der Ferne beobachtet)" ;;
        110)     report WARN "  $host:110 POP3 erreichbar — nur mit TLS (995) betreiben" "CIS Ubuntu 24.04 2.1.8 (Ferndiagnose)" ;;
        143)     report WARN "  $host:143 IMAP erreichbar — nur mit TLS (993) betreiben" "CIS Ubuntu 24.04 2.1.8 (Ferndiagnose)" ;;
        445|139) report WARN "  $host:$port SMB/NetBIOS erreichbar" "CIS Ubuntu 24.04 2.1.14 (Ferndiagnose)" ;;
        111)     report WARN "  $host:111 rpcbind/Portmapper erreichbar" "CIS Ubuntu 24.04 2.1.12 (Ferndiagnose)" ;;
        3389)    report WARN "  $host:3389 RDP erreichbar — gehört nicht offen ins Netz" "Least-Exposure-Prinzip; keine spezifische CIS-Ubuntu-Nummer" ;;
        5900)    report WARN "  $host:5900 VNC erreichbar — oft schwache/keine Auth" "Least-Exposure-Prinzip; keine spezifische CIS-Ubuntu-Nummer" ;;
        3306)    report WARN "  $host:3306 MySQL/MariaDB erreichbar" "Least-Exposure; DB sollte nicht offen im Netz lauschen (keine spezifische CIS-Nummer)" ;;
        5432)    report WARN "  $host:5432 PostgreSQL erreichbar" "Least-Exposure (keine spezifische CIS-Nummer)" ;;
        6379)    report WARN "  $host:6379 Redis erreichbar — Standard ohne Auth" "Least-Exposure (keine spezifische CIS-Nummer)" ;;
        27017)   report WARN "  $host:27017 MongoDB erreichbar" "Least-Exposure (keine spezifische CIS-Nummer)" ;;
        9200)    report WARN "  $host:9200 Elasticsearch erreichbar" "Least-Exposure (keine spezifische CIS-Nummer)" ;;
        22)      report INFO "  $host:22 SSH" "" ;;
        80|8080) report INFO "  $host:$port HTTP" "" ;;
        443|8443) report INFO "  $host:$port HTTPS" "" ;;
        25)      report INFO "  $host:25 SMTP" "" ;;
        53)      report INFO "  $host:53 DNS" "" ;;
        135)     report INFO "  $host:135 MSRPC" "" ;;
        *)       report INFO "  $host:$port offen" "" ;;
    esac
}

# Einen validierten, privaten CIDR-Bereich scannen
scan7_range() {
    local cidr="$1" ip prefix ipint mask net bcast first last count i tmp rf host ports port up
    ip="${cidr%/*}"; prefix="${cidr#*/}"
    ipint="$(ip_to_int "$ip")"
    if [ "$prefix" -ge 31 ]; then
        # /31 (RFC 3021) und /32: keine reservierte Netz-/Broadcast-Adresse
        net=$(( ipint & (0xFFFFFFFF << (32 - prefix) & 0xFFFFFFFF) ))
        first="$net"
        [ "$prefix" -eq 32 ] && last="$net" || last="$(( net + 1 ))"
    else
        mask=$(( (0xFFFFFFFF << (32 - prefix)) & 0xFFFFFFFF ))
        net=$(( ipint & mask ))
        bcast=$(( net | (0xFFFFFFFF & ~mask) ))
        first=$(( net + 1 )); last=$(( bcast - 1 ))
    fi
    count=$(( last - first + 1 ))
    if [ "$count" -gt "$SCAN7_MAX_HOSTS" ]; then
        report INFO "$cidr übersprungen — $count Hosts überschreiten das Limit ($SCAN7_MAX_HOSTS); Bereich enger fassen" \
            "Schutz vor versehentlich riesigen Scans"
        return
    fi

    report INFO "Scanne $cidr ($count Hosts, ${SCAN7_HAVE_PING:+ping+}TCP-Connect, ${SCAN7_PORTS// /,} Ports) …" ""
    tmp="$(mktemp -d)" || { report SKIP "Konnte kein temporäres Verzeichnis anlegen" ""; return; }
    mkdir -p "$tmp/results"

    i="$first"
    local running=0
    while [ "$i" -le "$last" ]; do
        scan7_host "$(int_to_ip "$i")" "$tmp/results/$(printf '%010d' "$i")" &
        running=$(( running + 1 ))
        if [ "$running" -ge "$SCAN7_BATCH" ]; then wait; running=0; fi
        i=$(( i + 1 ))
    done
    wait

    up=0
    shopt -s nullglob
    for rf in "$tmp"/results/*; do
        up=$(( up + 1 ))
        IFS=' ' read -r host ports < "$rf"
        if [ -z "$ports" ]; then
            report INFO "Host aktiv: $host (antwortet, keiner der geprüften Ports offen)" ""
        else
            report INFO "Host aktiv: $host — offene Ports: $ports" ""
            for port in $ports; do
                scan7_classify "$host" "$port"
            done
        fi
    done
    shopt -u nullglob
    rm -rf "$tmp"
    [ "$up" -eq 0 ] && report INFO "Keine aktiven Hosts in $cidr gefunden" ""
}

check_lan_scan() {
    header "7. Netzwerk-Scan (LAN, aktiv)"
    report INFO "Aktiver Scan: sendet TCP-Verbindungsversuche an andere Geräte — nur im eigenen/administrierten Netz zulässig" \
        "Rechtlicher/ethischer Hinweis — dies ist ein Inventar-Werkzeug, keine CIS/Lynis-Benchmark-Prüfung"

    if ! need_cmd timeout; then
        report SKIP "'timeout' nicht vorhanden — LAN-Scan benötigt es für saubere Zeitlimits" ""
        return
    fi

    local -a targets=()
    if [ -n "$SCAN_CIDR" ]; then
        if ! valid_cidr "$SCAN_CIDR"; then
            report WARN "Ungültige CIDR-Angabe: $SCAN_CIDR (erwartet z. B. 192.168.1.0/24)" ""
            return
        fi
        if ! is_private_cidr "$SCAN_CIDR"; then
            report WARN "$SCAN_CIDR ist kein privates Netz (RFC 1918) — abgelehnt. Der Scanner prüft nur eigene/lokale Netze" \
                "Schutz vor unautorisiertem Scannen fremder Hosts"
            return
        fi
        targets=("$SCAN_CIDR")
    else
        if ! need_cmd ip; then
            report SKIP "'ip' nicht vorhanden — keine automatische Netz-Erkennung möglich; CIDR mit --scan-network <CIDR> angeben" ""
            return
        fi
        local cidr
        while read -r cidr; do
            [ -z "$cidr" ] && continue
            if ! is_private_cidr "$cidr"; then
                report INFO "Überspringe $cidr — kein privates Netz (RFC 1918); vermutlich öffentliche/Provider-Adresse, nicht dein LAN" \
                    "Nur private Netze werden automatisch gescannt"
                continue
            fi
            targets+=("$cidr")
        done < <(ip -o -f inet addr show scope global 2>/dev/null | awk '{print $4}')
    fi

    if [ "${#targets[@]}" -eq 0 ]; then
        report INFO "Keine privaten Netze zum Scannen gefunden (CIDR ggf. mit --scan-network <CIDR> angeben)" ""
        return
    fi

    local t
    for t in "${targets[@]}"; do
        scan7_range "$t"
    done
}

# ---------------------------------------------------------------------------
# Zusammenfassung
# ---------------------------------------------------------------------------

summary() {
    header "Zusammenfassung"
    printf '  %sOK: %d%s  %sWARN: %d%s  %sRISK: %d%s  %sINFO: %d%s  %sSKIP: %d%s\n' \
        "$C_OK" "$CNT_OK" "$C_RST" "$C_WARN" "$CNT_WARN" "$C_RST" \
        "$C_RISK" "$CNT_RISK" "$C_RST" "$C_INFO" "$CNT_INFO" "$C_RST" \
        "$C_SKIP" "$CNT_SKIP" "$C_RST"
    if [ "$IS_ROOT" -ne 1 ] && [ "$CNT_SKIP" -gt 0 ]; then
        printf '  Hinweis: für vollen Umfang mit sudo ausführen.\n'
    fi
}

# ---------------------------------------------------------------------------
# Ablauf
# ---------------------------------------------------------------------------

printf '%snet-audit.sh — lokaler Netzwerksicherheits-Audit (read-only)%s\n' "$C_HEAD" "$C_RST"
printf 'Host: %s | Datum: %s | Root: %s\n' "$(hostname)" "$(date '+%F %T')" "$([ "$IS_ROOT" -eq 1 ] && echo ja || echo nein)"
printf 'System: %s\n' "${OS_PRETTY:-unbekannt}"
if [ "$OS_IS_DEBIAN" -eq 1 ]; then
    :
elif [ "$OS_IS_RHEL" -eq 1 ]; then
    printf '%sHinweis:%s Distro-Familie: RHEL (dnf/rpm, firewalld). Paket- und Firewall-Prüfungen\n' "$C_INFO" "$C_RST"
    printf '  nutzen die passenden RHEL-Namen/-Tools. CIS-Referenzen im Output stammen aus dem\n'
    printf '  CIS-Ubuntu-24.04-Benchmark und gelten NICHT für diese Distro — sie werden daher\n'
    printf '  ausgeblendet; es bleiben Lynis-Test-IDs (distro-unabhängig) oder ein offener\n'
    printf '  Hinweis "kein CIS/Lynis-Bezug für diese Distro-Familie verifiziert".\n'
else
    printf '%s! Hinweis:%s Unbekannte Distro-Familie (weder Debian/Ubuntu noch RHEL erkannt).\n' "$C_WARN" "$C_RST"
    printf '  Paket-Prüfungen (Bereich 4/6) und Teile der Firewall-Prüfung (Bereich 2) werden\n'
    printf '  übersprungen (SKIP), da dpkg/rpm bzw. ufw/firewalld fehlen könnten. Ergebnisse auf\n'
    printf '  dieser Distribution sind nur eingeschränkt aussagekräftig.\n'
fi

check_listening_ports
check_firewall
check_ssh
check_plaintext_services
check_sysctl
check_shares

if [ "$SCAN_NETWORK" -eq 1 ]; then
    check_lan_scan
fi

summary
