diff --git a/lxc-enroll.sh b/lxc-enroll.sh index 9baca52..986d097 100644 --- a/lxc-enroll.sh +++ b/lxc-enroll.sh @@ -6,14 +6,52 @@ umask 077 fail() { printf 'FEHLER: %s\n' "$*" >&2; exit 1; } say() { printf '%s\n' "$*"; } -ask() { printf '%s ' "$1" >&2; IFS= read -r REPLY <&3 || fail 'Eingabe abgebrochen.'; } +UI_TOOL=''; UI_HEIGHT=20; UI_WIDTH=76 +init_ui() { + local dimensions rows=24 columns=80 + UI_TOOL='' + if command -v whiptail >/dev/null 2>&1; then UI_TOOL=whiptail + elif command -v dialog >/dev/null 2>&1; then UI_TOOL=dialog; fi + dimensions=$(stty size <&3 2>/dev/null) || dimensions='' + if [[ $dimensions =~ ^([0-9]+)[[:space:]]+([0-9]+)$ ]]; then + rows=${BASH_REMATCH[1]}; columns=${BASH_REMATCH[2]} + fi + if [[ -z $UI_TOOL || ${TERM:-dumb} == dumb || $rows -lt 16 || $columns -lt 60 ]]; then + UI_TOOL='' + say 'Dialogoberflaeche nicht verfuegbar (whiptail/dialog oder ausreichend grosses Terminal fehlt); Texteingabe verwenden.' + return 0 + fi + UI_HEIGHT=$((rows-4)); UI_WIDTH=$((columns-4)) + if [[ $UI_HEIGHT -gt 30 ]]; then UI_HEIGHT=30; fi + if [[ $UI_WIDTH -gt 110 ]]; then UI_WIDTH=110; fi +} +ui_capture() { + # The UI uses the controlling terminal, even if this script's stdin is a pipe. + # Secrets/selections are captured on a separate descriptor, never stdout/logs. + REPLY=$("$UI_TOOL" --output-fd 4 "$@" 4>&1 1>&3 2>&3 <&3) +} +ask() { + if [[ -n $UI_TOOL ]]; then + ui_capture --title 'LXC Enrollment - Eingabe' --ok-button 'Weiter' --cancel-button 'Abbrechen' \ + --inputbox "$1" "$UI_HEIGHT" "$UI_WIDTH" "${2:-}" || fail 'Eingabe abgebrochen oder Dialog fehlgeschlagen.' + else + printf '%s ' "$1" >&2 + IFS= read -r REPLY <&3 || fail 'Eingabe abgebrochen.' + fi +} secret() { + if [[ -n $UI_TOOL ]]; then + ui_capture --title 'LXC Enrollment - Verdeckte Eingabe' --ok-button 'Weiter' --cancel-button 'Abbrechen' \ + --passwordbox "$1" "$UI_HEIGHT" "$UI_WIDTH" || fail 'Eingabe abgebrochen oder Dialog fehlgeschlagen.' + return 0 + fi printf '%s ' "$1" >&2 IFS= read -r -s REPLY <&3 || fail 'Eingabe abgebrochen.' printf '\n' >&2 } installed_label() { if [[ $1 == 1 ]]; then printf 'vorhanden'; else printf 'fehlt'; fi; } choice_hint() { if [[ $1 == 1 ]]; then printf '[J/n]'; else printf '[j/N]'; fi; } +yesno_label() { if [[ $1 == 1 ]]; then printf 'Ja'; else printf 'Nein'; fi; } LIST=0; DRY=0; MANAGER=''; VERSION=''; LOGIN_SERVER=''; SELECTION=''; VERSION_EXPLICIT=0 SSH_ENABLED=1; SSH_USER=''; SSH_SUDO=0; SSH_NOPASSWD=0; SAVE_SETTINGS=1 @@ -45,6 +83,8 @@ Nur laufende Debian-/Ubuntu-LXCs mit systemd. Vorhandene Tailscale-/Wazuh- Registrierungen bleiben unveraendert. Optional Tailscale SSH/Nutzer/Sudo fuer die Auswahl. Lokale Einstellungen: /etc/lxc-enroll/.vars (JSON, ohne Geheimnisse). Keine Host-Agent-Installation. Geheimnisse werden lokal abgefragt. +Dialogoberflaeche fuer Auswahl, Eingaben und Geheimnisse (whiptail/dialog). +LXC-Checkliste: Leertaste markiert. Ohne Dialogwerkzeug: Texteingabe. HELP exit 0 ;; *) fail "Unbekannte Option: $1" ;; @@ -70,7 +110,20 @@ trap 'exit 130' INT trap 'exit 143' TERM yes_no() { - local prompt=$1 default=$2 + local prompt=$1 default=$2 rc + local -a options=() + if [[ -n ${UI_TOOL:-} ]]; then + [[ $default != 0 ]] || options+=(--defaultno) + # Keep keyboard hints only in text mode; buttons show Ja/Nein in the UI. + prompt=${prompt% \[J/n\]}; prompt=${prompt% \[j/N\]} + if ui_capture --title 'LXC Enrollment - Auswahl' --yes-button 'Ja' --no-button 'Nein' \ + "${options[@]}" --yesno "$prompt" "$UI_HEIGHT" "$UI_WIDTH"; then REPLY=1 + else + rc=$? + case "$rc" in 1) REPLY=0;; *) fail 'Auswahl abgebrochen oder Dialog fehlgeschlagen.';; esac + fi + return 0 + fi ask "$prompt" case "$REPLY" in '' ) REPLY=$default;; 1|j|J|ja|Ja|y|Y|yes|Yes) REPLY=1;; 0|n|N|nein|Nein|no|No) REPLY=0;; *) fail 'Bitte j oder n eingeben (auch 1/0 moeglich).';; esac } @@ -166,6 +219,36 @@ PROBE done } +choose_lxcs() { + local menu_height id rc + local -a items=() + if [[ -z $UI_TOOL ]]; then + ask 'CTIDs auswaehlen (Leerzeichen, oder all; leer = abbrechen):' + SELECTION=$REPLY + [[ -n $SELECTION ]] + return + fi + menu_height=$((UI_HEIGHT-9)) + if [[ $menu_height -gt ${#IDS[@]} ]]; then menu_height=${#IDS[@]}; fi + for id in "${IDS[@]}"; do + items+=("$id" "${NAMES[$id]} | Tailscale: $(installed_label "${TS[$id]}") | Wazuh: $(installed_label "${WZ[$id]}") | TUN: $(installed_label "${TUN[$id]}")" OFF) + done + # UI reads/writes the controlling terminal. Only CTIDs go to captured fd 4. + # Never eval/source the dialog output; the normal selection validation follows. + if ui_capture --title 'LXC Enrollment - Container auswaehlen' \ + --ok-button 'Weiter' --cancel-button 'Abbrechen' \ + --separate-output --checklist \ + 'Pfeile: navigieren | Leertaste: markieren | Tab: Schaltflaeche | Enter: bestaetigen' \ + "$UI_HEIGHT" "$UI_WIDTH" "$menu_height" "${items[@]}"; then + SELECTION=${REPLY//$'\n'/ } + [[ -n $SELECTION ]] + return + else + rc=$? + case "$rc" in 1|255) return 1;; *) fail "LXC-Checkliste fehlgeschlagen (Status $rc).";; esac + fi +} + ensure_tun() { local id=$1 cfg slot=-1 n cfg=$(pct config "$id") || return 1 @@ -359,16 +442,17 @@ deploy_one() { } main() { - local id input wanted found need_ts=0 need_wz=0 restart_ids='' ok='' failed='' auto_version password + local id input wanted found need_ts=0 need_wz=0 restart_ids='' ok='' failed='' auto_version password plan='' scan [[ $LIST == 0 ]] || return 0 [[ ${#IDS[@]} -gt 0 ]] || { say 'Keine passenden LXCs gefunden.'; return 0; } exec 3<>/dev/tty || fail 'Interaktives Terminal fehlt. --list kann ohne Terminal laufen.' + init_ui load_settings yes_no "Tailscale SSH fuer alle ausgewaehlten LXCs aktivieren? $(choice_hint "$SSH_ENABLED")" "$SSH_ENABLED" SSH_ENABLED=$REPLY if [[ $SSH_ENABLED == 1 ]]; then - ask "SSH-Nutzername${SSH_USER:+ [$SSH_USER]}:" + ask "SSH-Nutzername${SSH_USER:+ [$SSH_USER]}:" "$SSH_USER" SSH_USER=${REPLY:-$SSH_USER} [[ $SSH_USER =~ ^[a-z_][a-z0-9_-]{0,30}$ && $SSH_USER != root ]] || fail 'Bitte einen normalen Linux-Nutzernamen verwenden.' yes_no "Nutzer zu sudo hinzufuegen? $(choice_hint "$SSH_SUDO")" "$SSH_SUDO"; SSH_SUDO=$REPLY @@ -377,7 +461,9 @@ main() { fi fi yes_no 'Einstellungen ohne Geheimnisse lokal in .vars speichern? [J/n]' 1; SAVE_SETTINGS=$REPLY - if [[ -z $SELECTION ]]; then ask 'CTIDs auswaehlen (Leerzeichen, oder all; leer = abbrechen):'; SELECTION=$REPLY; fi + if [[ -z $SELECTION ]]; then + choose_lxcs || { say 'Abgebrochen; keine Container ausgewaehlt.'; return 0; } + fi [[ -n $SELECTION ]] || return 0 if [[ $SELECTION == all ]]; then SELECTED=("${IDS[@]}"); else SELECTION=${SELECTION//,/ } @@ -393,6 +479,7 @@ main() { say ''; say 'Geplante Aenderungen:' for id in "${SELECTED[@]}"; do say "LXC $id ${NAMES[$id]}: Tailscale $(installed_label "${TS[$id]}"), Wazuh $(installed_label "${WZ[$id]}")." + plan+="LXC $id ${NAMES[$id]}: Tailscale $(installed_label "${TS[$id]}"), Wazuh $(installed_label "${WZ[$id]}")"$'\n' if [[ ${TS[$id]} == 0 || $RESUME_TS == 1 ]]; then need_ts=1 if [[ ${TUN[$id]} == 0 ]]; then restart_ids+=" $id"; fi @@ -411,7 +498,7 @@ main() { exec 8>/run/lxc-enroll.lock flock -n 8 || fail 'Ein anderer Enrollment-Lauf ist bereits aktiv.' if [[ $need_wz == 1 ]]; then - if [[ -z $MANAGER ]]; then ask 'Erreichbare Wazuh-Manager-Adresse (LAN-IP oder Headscale-IP):'; MANAGER=$REPLY; fi + if [[ -z $MANAGER ]]; then ask 'Erreichbare Wazuh-Manager-Adresse (IP oder DNS-Name):'; MANAGER=$REPLY; fi [[ $MANAGER =~ ^[A-Za-z0-9][A-Za-z0-9.:-]*$ ]] || fail 'Manager-Adresse ungueltig.' if [[ ${#MANAGERS[@]} == 1 ]]; then auto_version=$(pct exec "${MANAGERS[0]}" -- dpkg-query -W '-f=${Version}' wazuh-manager) || fail 'Manager-Version nicht lesbar.' @@ -435,7 +522,19 @@ main() { else say 'Neue Clients behalten lokales DNS bei. Der Manager-Name muss ueber diesen Resolver aufloesbar sein.' fi - ask 'Nur diese fehlenden Komponenten installieren? Bitte INSTALL eingeben:' + if [[ -n $UI_TOOL ]]; then + local -a scroll_option=() + if [[ $UI_TOOL == whiptail ]]; then scroll_option=(--scrolltext); else scroll_option=(--scrollbar); fi + plan+=$'\n'"Wazuh-Manager: ${MANAGER:-nicht benoetigt}; neue Agent-Version: ${VERSION:-keine}"$'\n' + plan+="Headscale: ${LOGIN_SERVER:-nicht benoetigt}; DNS uebernehmen: $(yesno_label "$TS_ACCEPT_DNS")"$'\n' + plan+="Tailscale SSH: $(yesno_label "$SSH_ENABLED"); Nutzer: ${SSH_USER:-keiner}; Sudo: $(yesno_label "$SSH_SUDO"); Sudo ohne Passwort: $(yesno_label "$SSH_NOPASSWD")"$'\n' + if [[ $SSH_ENABLED == 1 ]]; then plan+='Tailscale-SSH-Sitzungen koennen unterbrochen werden. Zugriff braucht eine passende Headscale-Policy.'$'\n'; fi + plan+="TUN ergaenzen und neu starten:${restart_ids:- keine}"$'\n' + if [[ $RESUME_TS == 1 ]]; then plan+='Wiederaufnahme: noch nicht angemeldetes Tailscale anmelden.'$'\n'; fi + ui_capture --title 'LXC Enrollment - Geplante Aenderungen' --ok-button 'Pruefen und weiter' \ + "${scroll_option[@]}" --msgbox "$plan" "$UI_HEIGHT" "$UI_WIDTH" || fail 'Pruefung abgebrochen.' + fi + ask 'Nur diese fehlenden Komponenten und gewaehlten Einstellungen einrichten? Bitte INSTALL eingeben:' [[ $REPLY == INSTALL ]] || { say 'Abgebrochen.'; return 0; } if [[ -n $restart_ids ]]; then ask "Diese Container werden kurz unterbrochen:$restart_ids. Bitte NEUSTART eingeben:"