#!/usr/bin/env bash set -euo pipefail # BastionGuard WebUI privileged helper. # Installare root:root 0755 in /usr/local/sbin e consentire via sudoers solo agli utenti web. usage() { cat >&2 < $0 first-run-config $0 list-configs $0 read-config-b64 [utente] $0 write-config [utente] $0 scan-path-b64 $0 samba-scan-b64 $0 quarantine-path-b64 $0 restore-quarantine-b64 $0 delete-quarantine-b64 $0 inotify-start $0 inotify-save $0 inotify-service-start $0 inotify-service-stop $0 inotify-service-status $0 inotify-start-saved $0 inotify-stop $0 inotify-status $0 inotify-events $0 ransomware-realtime-start $0 import-ransomware-events $0 journal $0 service-action $0 update-yara $0 update-sanesecurity $0 update-phishing $0 update-banks [source-url] $0 phish-auto-update $0 read-version $0 read-data-file-b64 $0 write-system-file-b64 $0 desktop-mode $0 apply-web-config [auto|distro] $0 wizard [resolv dnsmasq firewall nftables certs webconf native useragent banks services] EOF exit 64 } valid_user() { [[ "${1:-}" =~ ^[A-Za-z_][A-Za-z0-9_-]*[$]?$ ]] && id "$1" >/dev/null 2>&1; } valid_port() { [[ "${1:-}" =~ ^[0-9]+$ ]] && (( $1 >= 1 && $1 <= 65535 )); } user_home() { getent passwd "$1" | cut -d: -f6; } user_uid() { id -u "$1"; } user_gid() { id -g "$1"; } q() { printf '%q' "$1"; } can_write_dir() { local dir="$1" tmp mkdir -p "$dir" 2>/dev/null || return 1 tmp="$(mktemp "$dir/.bastionguard-webui.XXXXXX" 2>/dev/null)" || return 1 rm -f "$tmp" return 0 } # Same detection semantics used by src/SettingsPage.cpp for Settings -> Web configuration. detect_distro_src() { local id="" result="" line if command -v lsb_release >/dev/null 2>&1; then result="$(lsb_release -si 2>/dev/null | tr -d '\r\n' || true)" if [[ -n "$result" && "$result" != *n/a* && "$result" != *N/A* ]]; then id="$result" fi fi if [[ -z "$id" && -r /etc/os-release ]]; then while IFS= read -r line; do if [[ "$line" == ID=* ]]; then id="${line#ID=}" id="${id%\"}"; id="${id#\"}" break fi done < /etc/os-release fi if [[ -z "$id" ]]; then [[ -e /etc/arch-release ]] && id=arch [[ -z "$id" && -e /etc/debian_version ]] && id=debian [[ -z "$id" && -e /etc/redhat-release ]] && id=fedora [[ -z "$id" && -e /etc/gentoo-release ]] && id=gentoo [[ -z "$id" && -e /etc/slackware-version ]] && id=slackware [[ -z "$id" && -e /usr/local/etc/rc.conf ]] && id=bsd fi id="${id,,}" case "$id" in *debian*|*ubuntu*|*mint*) echo debian ;; *fedora*|*rhel*|*centos*|*alma*|*rocky*) echo fedora ;; *arch*|*manjaro*|*endeavour*) echo arch ;; *suse*) echo opensuse ;; *gentoo*) echo gentoo ;; *slackware*) echo slackware ;; *bsd*) echo bsd ;; *) echo auto ;; esac } detect_webserver_src() { if systemctl is-active --quiet apache2 2>/dev/null || systemctl is-active --quiet httpd 2>/dev/null || command -v apache2 >/dev/null 2>&1 || command -v httpd >/dev/null 2>&1; then echo apache; return 0 fi if systemctl is-active --quiet nginx 2>/dev/null || command -v nginx >/dev/null 2>&1; then echo nginx; return 0 fi if systemctl is-active --quiet lshttpd 2>/dev/null || systemctl is-active --quiet lsws 2>/dev/null || command -v lsws >/dev/null 2>&1 || command -v lshttpd >/dev/null 2>&1; then echo litespeed; return 0 fi local out out="$(ps -eo comm 2>/dev/null | grep -E 'apache2|httpd|nginx|lshttpd|lsws' | grep -v grep || true)" [[ "$out" == *apache2* || "$out" == *httpd* ]] && { echo apache; return 0; } [[ "$out" == *nginx* ]] && { echo nginx; return 0; } [[ "$out" == *lshttpd* || "$out" == *lsws* ]] && { echo litespeed; return 0; } echo unknown } # Desktop arbitration for proxy-owning components. # CEF and PAC are never owned by the WebUI: they are GTK/desktop proxy/browser # components and can rewrite system/browser proxy state. webui_disabled_unit() { case "${1,,}" in bastionguard-cef.service|bastionguard-cef|bastionguard-pacd.service|bastionguard-pacd|bastionguard-privacyd.service|bastionguard-privacyd|bastionguard-usbd.service|bastionguard-usbd) return 0 ;; *) return 1 ;; esac } proxy_sensitive_unit() { return 1 } desktop_session_summary() { local user="${1:-}" uid="" sid name active state type class remote desktop leader comm details="" valid_user "$user" || { echo "invalid-user"; return 1; } uid="$(user_uid "$user")" if command -v loginctl >/dev/null 2>&1; then while read -r sid _rest; do [[ -n "$sid" ]] || continue name="$(loginctl show-session "$sid" -p Name --value 2>/dev/null || true)" [[ "$name" == "$user" ]] || continue active="$(loginctl show-session "$sid" -p Active --value 2>/dev/null || true)" state="$(loginctl show-session "$sid" -p State --value 2>/dev/null || true)" type="$(loginctl show-session "$sid" -p Type --value 2>/dev/null || true)" class="$(loginctl show-session "$sid" -p Class --value 2>/dev/null || true)" remote="$(loginctl show-session "$sid" -p Remote --value 2>/dev/null || true)" desktop="$(loginctl show-session "$sid" -p Desktop --value 2>/dev/null || true)" leader="$(loginctl show-session "$sid" -p Leader --value 2>/dev/null || true)" comm="" [[ "$leader" =~ ^[0-9]+$ ]] && comm="$(ps -p "$leader" -o comm= 2>/dev/null || true)" details="session=$sid active=${active:-?} state=${state:-?} type=${type:-?} class=${class:-?} remote=${remote:-?} desktop=${desktop:-?} leader=${comm:-$leader}" case "$type" in x11|wayland|mir) echo "gui $details"; return 0 ;; esac if [[ "$active" == yes && "$class" == user && "$remote" != yes ]]; then case "$desktop $comm" in *plasma*|*gnome*|*xfce*|*cinnamon*|*mate*|*lxqt*|*budgie*) echo "gui $details"; return 0 ;; esac fi done < <(loginctl list-sessions --no-legend 2>/dev/null || true) fi # Fallbacks for systems without complete logind metadata. if [[ -S "/run/user/$uid/bus" ]]; then if pgrep -u "$user" -x plasmashell >/dev/null 2>&1 || pgrep -u "$user" -x gnome-shell >/dev/null 2>&1 || pgrep -u "$user" -x xfce4-session >/dev/null 2>&1 || pgrep -u "$user" -x cinnamon-session >/dev/null 2>&1 || pgrep -u "$user" -x mate-session >/dev/null 2>&1 || pgrep -u "$user" -x lxqt-session >/dev/null 2>&1; then echo "gui process-detected user=$user uid=$uid" return 0 fi if compgen -G "/run/user/$uid/wayland-*" >/dev/null 2>&1; then echo "gui wayland-socket user=$user uid=$uid" return 0 fi fi echo "headless no-active-graphical-session user=$user uid=$uid" return 1 } is_desktop_gui_active() { local summary summary="$(desktop_session_summary "$1" 2>/dev/null || true)" [[ "$summary" == gui* ]] } guard_proxy_desktop_conflict() { local user="$1" unit="$2" action="${3:-manage}" valid_user "$user" || { echo "Invalid user: $user" >&2; exit 67; } proxy_sensitive_unit "$unit" || return 0 case "$action" in start|restart|enable|enable--now|install|manage) ;; stop|disable|disable--now) return 0 ;; esac local summary summary="$(desktop_session_summary "$user" 2>/dev/null || true)" if [[ "$summary" == gui* ]]; then cat >&2 <&2; exit 64; } case "$key" in webports) path=/etc/BastionGuard/webports.conf; scope=system ;; dnsmasq) path=/etc/dnsmasq.d/BastionGuard.conf; scope=system ;; app_config) rel=.config/BastionGuard/config.json; scope=user ;; scan_config) rel=.local/share/BastionGuard/config.json; scope=user ;; cloud) rel=.config/BastionGuard/cloud.conf; scope=user ;; cred) rel=.config/BastionGuard/cred.conf; scope=user ;; firewall) rel=.config/BastionGuard/firewall.conf; scope=user ;; identity_leak) rel=.config/BastionGuard/identity_leak.json; scope=user ;; scanner) rel=.config/BastionGuard/scanner.conf; scope=user ;; googlesafe) rel=.config/BastionGuard/googlesafe.conf; scope=user ;; mail) rel=.config/BastionGuard/mail.json; scope=user ;; payments) rel=.config/BastionGuard/payments.json; scope=user ;; proxy_bypass) rel=.config/BastionGuard/proxy_bypass.json; scope=user ;; samba) rel=.config/BastionGuard/samba.conf; scope=user ;; settings) rel=.config/BastionGuard/settings.json; scope=user ;; theme) rel=.config/BastionGuard/theme.conf; scope=user ;; whitelist) rel=.config/BastionGuard/whitelist.json; scope=user ;; lang) rel=.config/BastionGuard/lang.conf; scope=user ;; sources) rel=.config/BastionGuard/sources.conf; scope=user ;; first_run_services_done) rel=.config/BastionGuard/first-run-services-done; scope=user ;; *) echo "Chiave non consentita: $key" >&2; exit 64 ;; esac if [[ "$scope" == user ]]; then [[ -n "$user" ]] || { echo "Missing user" >&2; exit 64; } valid_user "$user" || { echo "Invalid user: $user" >&2; exit 67; } home="$(user_home "$user")" path="$home/$rel" fi printf '%s\t%s\n' "$scope" "$path" } list_configs() { local user="$1" home dir uid gid name path type readable size mtime valid_user "$user" || { echo "Invalid user: $user" >&2; exit 67; } home="$(user_home "$user")" dir="$home/.config/BastionGuard" printf 'DIR\t%s\t%s\n' "$dir" "$( [[ -d "$dir" ]] && echo 1 || echo 0 )" [[ -d "$dir" ]] || exit 0 shopt -s nullglob dotglob for path in "$dir"/*; do name="$(basename "$path")" [[ "$name" == "." || "$name" == ".." ]] && continue if [[ -d "$path" ]]; then type=directory; size=0; else type=file; size="$(stat -c '%s' "$path" 2>/dev/null || echo 0)"; fi [[ -r "$path" ]] && readable=1 || readable=0 mtime="$(stat -c '%Y' "$path" 2>/dev/null || echo 0)" printf 'ITEM\t%s\t%s\t%s\t%s\t%s\n' "$(printf '%s' "$name" | base64 -w0)" "$type" "$readable" "$size" "$mtime" done } read_config_b64() { local key="$1" user="${2:-}" meta scope path meta="$(config_path_for_key "$key" "$user")" scope="${meta%%$'\t'*}"; path="${meta#*$'\t'}" [[ -e "$path" ]] || { echo "MISSING $path" >&2; exit 66; } [[ ! -d "$path" ]] || { echo "DIRECTORY $path" >&2; exit 68; } [[ -f "$path" ]] || { echo "NOTFILE $path" >&2; exit 65; } [[ -r "$path" ]] || { echo "UNREADABLE $path" >&2; exit 13; } base64 -w0 "$path" } write_user_file() { local user="$1" rel="$2" mode="$3" content="$4" local home uid gid path dir tmp home="$(user_home "$user")"; uid="$(user_uid "$user")"; gid="$(user_gid "$user")" path="$home/$rel"; dir="$(dirname "$path")" mkdir -p "$dir" tmp="$(mktemp)" printf '%s' "$content" > "$tmp" install -o "$uid" -g "$gid" -m "$mode" "$tmp" "$path" rm -f "$tmp" echo "created: $path" } write_system_file() { local path="$1" mode="$2" content="$3" local dir tmp err rc dir="$(dirname "$path")" if ! mkdir -p "$dir" 2>/tmp/bastionguard-webui-mkdir.err; then err="$(cat /tmp/bastionguard-webui-mkdir.err 2>/dev/null || true)" echo "skipped: $path (${err:-cannot create directory})" return 1 fi tmp="$(mktemp)" printf '%s' "$content" > "$tmp" err="$(install -o root -g root -m "$mode" "$tmp" "$path" 2>&1)" || { rc=$? rm -f "$tmp" case "$err" in *'Read-only file system'*) echo "skipped: $path (read-only file system)" ;; *'Permission denied'*) echo "skipped: $path (permission denied)" ;; *) echo "skipped: $path (${err:-install failed})" ;; esac return "$rc" } rm -f "$tmp" echo "created: $path" } ensure_user_dirs() { local user="$1" home uid gid home="$(user_home "$user")"; uid="$(user_uid "$user")"; gid="$(user_gid "$user")" install -d -o "$uid" -g "$gid" -m 0750 "$home/.config/BastionGuard" install -d -o "$uid" -g "$gid" -m 0750 "$home/.local/share/BastionGuard" install -d -o "$uid" -g "$gid" -m 0750 "$home/.local/share/BastionGuard/logs" install -d -o "$uid" -g "$gid" -m 0750 "$home/.local/share/BastionGuard/quarantine" install -d -o "$uid" -g "$gid" -m 0750 "$home/.local/share/BastionGuard/phishing" } ensure_configs() { local user="$1" home valid_user "$user" || { echo "Invalid user: $user" >&2; exit 67; } home="$(user_home "$user")" ensure_user_dirs "$user" [[ -f /etc/BastionGuard/webports.conf ]] || write_system_file /etc/BastionGuard/webports.conf 0644 $'81 444\n' [[ -f /etc/dnsmasq.d/BastionGuard.conf ]] || write_system_file /etc/dnsmasq.d/BastionGuard.conf 0644 $'# Interfacce di ascolto per BastionGuard DNS Protection\nlisten-address=127.0.0.1,127.0.0.2\n\n# Facoltativo: riduce warning di bind\nbind-interfaces\n' # Non marchiamo il wizard come completato qui: config.json completato viene scritto solo dal first-run wizard. [[ -f "$home/.config/BastionGuard/config.json" ]] || true [[ -f "$home/.local/share/BastionGuard/config.json" ]] || write_user_file "$user" .local/share/BastionGuard/config.json 0640 $'{\n "auto_scan_enabled": true\n}\n' [[ -f "$home/.config/BastionGuard/cloud.conf" ]] || write_user_file "$user" .config/BastionGuard/cloud.conf 0640 $'malware_bazaar_api_key=\n' [[ -f "$home/.config/BastionGuard/scanner.conf" ]] || write_user_file "$user" .config/BastionGuard/scanner.conf 0640 $'# --- BastionGuard Anti-Ransomware Config ---\nsuspicious_only=0\nignore_paths=/proc;/sys;/dev;/run;/tmp\nignore_ext=.tmp;.log;.cache\nsuspicious_ext=.locked;.encrypted;.crypt;.enc;.encrypted\n\n# --- Ransomware Scanner ---\nenable_yara=1\nenable_sanesecurity=1\nscan_interval=60\nscan_path=/home\n' [[ -f "$home/.config/BastionGuard/mail.json" ]] || write_user_file "$user" .config/BastionGuard/mail.json 0640 $'{\n "version": 2,\n "enabled": false,\n "scan_outgoing": true,\n "inject_signature": false,\n "local_smtp_host": "127.0.0.1",\n "local_smtp_port": 2525,\n "local_smtp_tls_port": 2465,\n "local_submission_port": 2587,\n "advertise_starttls": true,\n "enable_implicit_tls_listener": true,\n "default_profile_id": "default",\n "profiles": [\n {\n "id": "default",\n "label": "Default",\n "match_from": [],\n "match_from_domain": [],\n "smtp_host": "",\n "smtp_port": 587,\n "starttls": true,\n "implicit_tls": false,\n "username": "",\n "password": "",\n "signature": {\n "display_name": "",\n "job_title": "",\n "company": "",\n "phone": "",\n "website": "",\n "logo_path": "/usr/share/BastionGuard/data/logo.png"\n }\n }\n ]\n}\n' [[ -f "$home/.config/BastionGuard/payments.json" ]] || write_user_file "$user" .config/BastionGuard/payments.json 0640 $'{\n "version": 1,\n "enabled": false,\n "list": [\n "paypal.com",\n "stripe.com",\n "adyen.com",\n "klarna.com",\n "checkout.com",\n "worldpay.com",\n "braintreepayments.com",\n "braintreegateway.com",\n "amazonpay.com",\n "pay.google.com",\n "pay.apple.com",\n "amazon.com"\n ]\n}\n' [[ -f "$home/.config/BastionGuard/whitelist.json" ]] || write_user_file "$user" .config/BastionGuard/whitelist.json 0640 $'{\n "domains": []\n}\n' [[ -f "$home/.config/BastionGuard/lang.conf" ]] || write_user_file "$user" .config/BastionGuard/lang.conf 0640 $'language=it\n' [[ -f "$home/.config/BastionGuard/googlesafe.conf" ]] || write_user_file "$user" .config/BastionGuard/googlesafe.conf 0640 $'# Google Safe Browsing configuration\ngoogle_safe_enabled=false\ngoogle_safe_key=\n' [[ -f "$home/.config/BastionGuard/sources.conf" ]] || write_user_file "$user" .config/BastionGuard/sources.conf 0640 $'clamav=1\nsanesecurity=1\n' [[ -f "$home/.config/BastionGuard/cred.conf" ]] || write_user_file "$user" .config/BastionGuard/cred.conf 0640 $'# Credenziali locali BastionGuard\n' [[ -f "$home/.config/BastionGuard/firewall.conf" ]] || write_user_file "$user" .config/BastionGuard/firewall.conf 0640 $'enabled=1\nmode=auto\n' [[ -f "$home/.config/BastionGuard/identity_leak.json" ]] || write_user_file "$user" .config/BastionGuard/identity_leak.json 0640 $'{\n "emails": [],\n "providers": []\n}\n' [[ -f "$home/.config/BastionGuard/proxy_bypass.json" ]] || write_user_file "$user" .config/BastionGuard/proxy_bypass.json 0640 $'{\n "domains": ["localhost", "127.0.0.1"]\n}\n' [[ -f "$home/.config/BastionGuard/samba.conf" ]] || write_user_file "$user" .config/BastionGuard/samba.conf 0640 $'scan_enabled=1\nquarantine_enabled=1\n' [[ -f "$home/.config/BastionGuard/settings.json" ]] || write_user_file "$user" .config/BastionGuard/settings.json 0640 $'{\n "version": 1\n}\n' [[ -f "$home/.config/BastionGuard/theme.conf" ]] || write_user_file "$user" .config/BastionGuard/theme.conf 0640 $'theme=system\n' } first_run_config() { local user="$1" b64="$2" home cfg content uid gid valid_user "$user" || { echo "Invalid user: $user" >&2; exit 67; } content="$(printf '%s' "$b64" | base64 -d)" || { echo "Base64 non valido" >&2; exit 64; } if ! printf '%s' "$content" | grep -q '"wizard_completed"[[:space:]]*:[[:space:]]*true'; then echo "config.json rifiutato: wizard_completed deve essere true" >&2 exit 64 fi home="$(user_home "$user")"; uid="$(user_uid "$user")"; gid="$(user_gid "$user")" cfg="$home/.config/BastionGuard/config.json" mkdir -p "$(dirname "$cfg")" chown "$uid:$gid" "$(dirname "$cfg")" 2>/dev/null || true if [[ -f "$cfg" ]] && grep -q '"wizard_completed"[[:space:]]*:[[:space:]]*true' "$cfg" && grep -q '"services"' "$cfg"; then echo "config.json already completed: no changes." exit 0 fi write_user_file "$user" .config/BastionGuard/config.json 0640 "$content " : > "$home/.config/BastionGuard/first-run-services-done" chown "$uid:$gid" "$home/.config/BastionGuard/first-run-services-done" 2>/dev/null || true chmod 0640 "$home/.config/BastionGuard/first-run-services-done" 2>/dev/null || true } write_config() { local key="$1" b64="$2" user="${3:-}" path scope mode content home uid gid [[ "$key" =~ ^[a-z_]+$ ]] || { echo "Chiave non valida" >&2; exit 64; } content="$(printf '%s' "$b64" | base64 -d)" || { echo "Base64 non valido" >&2; exit 64; } case "$key" in webports) path=/etc/BastionGuard/webports.conf; scope=system; mode=0644 ;; dnsmasq) path=/etc/dnsmasq.d/BastionGuard.conf; scope=system; mode=0644 ;; app_config) rel=.config/BastionGuard/config.json; scope=user; mode=0640 ;; config_ini) rel=.config/BastionGuard/config.ini; scope=user; mode=0640 ;; scan_config) rel=.local/share/BastionGuard/config.json; scope=user; mode=0640 ;; cloud) rel=.config/BastionGuard/cloud.conf; scope=user; mode=0640 ;; cred) rel=.config/BastionGuard/cred.conf; scope=user; mode=0640 ;; firewall) rel=.config/BastionGuard/firewall.conf; scope=user; mode=0640 ;; identity_leak) rel=.config/BastionGuard/identity_leak.json; scope=user; mode=0640 ;; scanner) rel=.config/BastionGuard/scanner.conf; scope=user; mode=0640 ;; googlesafe) rel=.config/BastionGuard/googlesafe.conf; scope=user; mode=0640 ;; allowlist) rel=.config/BastionGuard/allowlist.txt; scope=user; mode=0640 ;; mail) rel=.config/BastionGuard/mail.json; scope=user; mode=0640 ;; payments) rel=.config/BastionGuard/payments.json; scope=user; mode=0640 ;; proxy_bypass) rel=.config/BastionGuard/proxy_bypass.json; scope=user; mode=0640 ;; samba) rel=.config/BastionGuard/samba.conf; scope=user; mode=0640 ;; settings) rel=.config/BastionGuard/settings.json; scope=user; mode=0640 ;; theme) rel=.config/BastionGuard/theme.conf; scope=user; mode=0640 ;; whitelist) rel=.config/BastionGuard/whitelist.json; scope=user; mode=0640 ;; lang) rel=.config/BastionGuard/lang.conf; scope=user; mode=0640 ;; sources) rel=.config/BastionGuard/sources.conf; scope=user; mode=0640 ;; first_run_services_done) rel=.config/BastionGuard/first-run-services-done; scope=user; mode=0640 ;; *) echo "Chiave non consentita: $key" >&2; exit 64 ;; esac if [[ "$scope" == system ]]; then write_system_file "$path" "$mode" "$content" else [[ -n "$user" ]] || { echo "Missing user" >&2; exit 64; } valid_user "$user" || { echo "Invalid user: $user" >&2; exit 67; } write_user_file "$user" "$rel" "$mode" "$content" fi } run_as_user() { local user="$1"; shift local uid runtime bus uid="$(user_uid "$user")"; runtime="/run/user/$uid"; bus="unix:path=$runtime/bus" if command -v runuser >/dev/null 2>&1; then runuser -u "$user" -- env XDG_RUNTIME_DIR="$runtime" DBUS_SESSION_BUS_ADDRESS="$bus" "$@" else sudo -n -u "$user" env XDG_RUNTIME_DIR="$runtime" DBUS_SESSION_BUS_ADDRESS="$bus" "$@" fi } setup_net() { local script=/usr/share/BastionGuard/data/scripts/BastionGuard-net-setup.sh if [[ -f "$script" ]]; then echo "running: $script" /usr/bin/env bash "$script" || echo "warning: BastionGuard-net-setup.sh failed or incomplete" else echo "skipped: $script (not found)" fi } setup_dnsmasq() { if ! can_write_dir /etc; then echo "skipped: /etc/dnsmasq.conf (read-only or not writable)" write_system_file /etc/dnsmasq.d/BastionGuard.conf 0644 $'# Interfacce di ascolto per BastionGuard DNS Protection\nlisten-address=127.0.0.1,127.0.0.2\n\n# Facoltativo: riduce warning di bind\nbind-interfaces\n' || true return 0 fi mkdir -p /etc/dnsmasq.d || { echo "skipped: /etc/dnsmasq.d (cannot create directory)"; return 0; } if [[ -f /etc/dnsmasq.conf ]]; then [[ -f /etc/dnsmasq.conf.bastionguard.bak ]] || cp -a /etc/dnsmasq.conf /etc/dnsmasq.conf.bastionguard.bak || true if grep -Eq '^[[:space:]]*#?[[:space:]]*conf-dir=/etc/dnsmasq\.d/.*,\*\.conf[[:space:]]*$' /etc/dnsmasq.conf; then sed -i -E 's|^[[:space:]]*#?[[:space:]]*(conf-dir=/etc/dnsmasq\.d/.*,\*\.conf)[[:space:]]*$|\1|' /etc/dnsmasq.conf || echo "skipped: /etc/dnsmasq.conf sed update failed" else printf '\nconf-dir=/etc/dnsmasq.d/,*.conf\n' >> /etc/dnsmasq.conf || echo "skipped: /etc/dnsmasq.conf append failed" fi else printf 'conf-dir=/etc/dnsmasq.d/,*.conf\n' > /etc/dnsmasq.conf || echo "skipped: /etc/dnsmasq.conf create failed" fi write_system_file /etc/dnsmasq.d/BastionGuard.conf 0644 $'# Interfacce di ascolto per BastionGuard DNS Protection\nlisten-address=127.0.0.1,127.0.0.2\n\n# Facoltativo: riduce warning di bind\nbind-interfaces\n' || true systemctl try-restart dnsmasq.service 2>/dev/null || true } setup_browser_policies() { local ff='{ "policies": { "DNSOverHTTPS": { "Enabled": true, "ProviderURL": "https://127.0.0.1:4443/dns-query", "Locked": true } } } ' local chrom='{ "DnsOverHttpsMode": "secure", "DnsOverHttpsTemplates": ["https://127.0.0.1:4443/dns-query"] } ' write_system_file /usr/lib/firefox/distribution/policies.json 0644 "$ff" || true write_system_file /etc/chromium/policies/managed/BastionGuard-dns.json 0644 "$chrom" || true write_system_file /etc/opt/chrome/policies/managed/BastionGuard-dns.json 0644 "$chrom" || true write_system_file /etc/opt/edge/policies/managed/BastionGuard-dns.json 0644 "$chrom" || true } setup_certs() { if ! can_write_dir /etc/BastionGuard; then echo "skipped: /etc/BastionGuard/certs (read-only or not writable)"; return 0; fi mkdir -p /etc/BastionGuard/certs && chmod 755 /etc/BastionGuard/certs [[ -f /etc/BastionGuard/certs/BastionGuard-ca.key.pem ]] || openssl genrsa -out /etc/BastionGuard/certs/BastionGuard-ca.key.pem 4096 [[ -f /etc/BastionGuard/certs/BastionGuard-ca.crt.pem ]] || openssl req -x509 -new -nodes -key /etc/BastionGuard/certs/BastionGuard-ca.key.pem -sha256 -days 3650 -subj '/CN=BastionGuard Local CA' -out /etc/BastionGuard/certs/BastionGuard-ca.crt.pem [[ -f /etc/BastionGuard/certs/local-warning.key.pem ]] || openssl genrsa -out /etc/BastionGuard/certs/local-warning.key.pem 2048 openssl req -new -key /etc/BastionGuard/certs/local-warning.key.pem -subj '/CN=localhost' -out /etc/BastionGuard/certs/local-warning.csr.pem printf 'subjectAltName = DNS:localhost,IP:127.0.0.2\n' > /etc/BastionGuard/certs/v3ext.cnf openssl x509 -req -in /etc/BastionGuard/certs/local-warning.csr.pem -CA /etc/BastionGuard/certs/BastionGuard-ca.crt.pem -CAkey /etc/BastionGuard/certs/BastionGuard-ca.key.pem -CAcreateserial -out /etc/BastionGuard/certs/local-warning.crt.pem -days 825 -sha256 -extfile /etc/BastionGuard/certs/v3ext.cnf rm -f /etc/BastionGuard/certs/local-warning.csr.pem /etc/BastionGuard/certs/BastionGuard-ca.srl /etc/BastionGuard/certs/v3ext.cnf || true chmod 600 /etc/BastionGuard/certs/*key.pem || true chmod 644 /etc/BastionGuard/certs/*.crt.pem || true if command -v update-ca-certificates >/dev/null 2>&1; then install -m 0644 /etc/BastionGuard/certs/BastionGuard-ca.crt.pem /usr/local/share/ca-certificates/BastionGuard-ca.crt && update-ca-certificates || true elif command -v trust >/dev/null 2>&1; then trust anchor --store /etc/BastionGuard/certs/BastionGuard-ca.crt.pem || true elif command -v update-ca-trust >/dev/null 2>&1; then install -m 0644 /etc/BastionGuard/certs/BastionGuard-ca.crt.pem /etc/pki/ca-trust/source/anchors/BastionGuard-ca.crt && update-ca-trust extract || true fi } setup_source_presteps() { local clamav_script=/usr/share/BastionGuard/data/scripts/BastionGuard-setup-clamav-daemon.sh local locale_script=/usr/share/BastionGuard/data/scripts/BastionGuard-locale.sh if [[ -f "$clamav_script" ]]; then echo "[BastionGuard] Setup ClamAV daemon pre-step" /usr/bin/env bash "$clamav_script" || echo "warning: BastionGuard-setup-clamav-daemon.sh failed or incomplete" else echo "skipped: $clamav_script (not found)" fi if [[ -f "$locale_script" ]]; then echo "[BastionGuard] Locale pre-step" /bin/sh "$locale_script" || echo "warning: BastionGuard-locale.sh failed or incomplete" else echo "skipped: $locale_script (not found)" fi } restart_phishing_scanner() { echo "[BastionGuard] Restarting BastionGuard-phishing-scanner.service" if command -v systemctl >/dev/null 2>&1 && systemctl list-unit-files BastionGuard-phishing-scanner.service >/dev/null 2>&1; then systemctl restart BastionGuard-phishing-scanner.service || true else echo "skipped: BastionGuard-phishing-scanner.service unit not found" fi } setup_firewall() { local user="${1:-}" local ports=(53/udp 81/tcp 444/tcp 4443/tcp) echo "[BastionGuard] Opening required firewall ports: 53/udp, 81/tcp, 444/tcp, 4443/tcp" if command -v firewall-cmd >/dev/null 2>&1 && systemctl is-active --quiet firewalld.service; then firewall-cmd --permanent --add-port=53/udp || true firewall-cmd --permanent --add-port=81/tcp || true firewall-cmd --permanent --add-port=444/tcp || true firewall-cmd --permanent --add-port=4443/tcp || true firewall-cmd --reload || true echo "firewalld: rules applied" elif command -v ufw >/dev/null 2>&1; then for p in "${ports[@]}"; do ufw allow "$p" || true; done echo "ufw: rules applied" elif command -v nft >/dev/null 2>&1; then modprobe nf_nat 2>/dev/null || true mkdir -p /etc/modules-load.d 2>/dev/null || true printf 'nf_nat\n' > /etc/modules-load.d/BastionGuard.conf 2>/dev/null || true nft add table inet BastionGuard_filter 2>/dev/null || true nft 'add chain inet BastionGuard_filter input { type filter hook input priority 0; policy accept; }' 2>/dev/null || true nft add rule inet BastionGuard_filter input udp dport 53 accept 2>/dev/null || true nft add rule inet BastionGuard_filter input tcp dport 81 accept 2>/dev/null || true nft add rule inet BastionGuard_filter input tcp dport 444 accept 2>/dev/null || true nft add rule inet BastionGuard_filter input tcp dport 4443 accept 2>/dev/null || true echo "nftables: best-effort rules applied" elif command -v iptables >/dev/null 2>&1; then iptables -I INPUT -p udp --dport 53 -j ACCEPT || true iptables -I INPUT -p tcp --dport 81 -j ACCEPT || true iptables -I INPUT -p tcp --dport 444 -j ACCEPT || true iptables -I INPUT -p tcp --dport 4443 -j ACCEPT || true echo "iptables: best-effort rules applied" else echo "warning: no supported firewall tool detected; open ports manually if needed" fi # Coerente con src/wizard/wizard_setup.cpp: aggiunge l'utente ai gruppi utili per log/journal. if [[ -n "$user" ]] && valid_user "$user"; then if getent group adm >/dev/null 2>&1; then usermod -aG adm "$user" || true; fi if getent group system-journal >/dev/null 2>&1; then usermod -aG system-journal "$user" || true; fi echo "groups: user $user added to adm/system-journal when available" fi } setup_nftables() { command -v nft >/dev/null 2>&1 || { echo "skipped: nft command not found"; return 0; } local src_conf=/usr/share/BastionGuard/data/config/nftables.conf local dst_conf=/etc/nftables.conf local net_script=/usr/share/BastionGuard/data/scripts/BastionGuard-net-setup.sh echo "[BastionGuard] Configuring nftables using source wizard logic" if [[ -f "$src_conf" ]]; then if can_write_dir /etc; then install -m 0644 "$src_conf" "$dst_conf" || echo "warning: cannot install $dst_conf" nft -f "$dst_conf" 2>/dev/null || true if ! systemctl is-active --quiet ufw 2>/dev/null && ! systemctl is-active --quiet firewalld 2>/dev/null; then systemctl enable --now nftables.service 2>/dev/null || true echo "nftables.service enabled when no UFW/firewalld was active" else echo "UFW or firewalld detected: existing firewall kept" fi else echo "skipped: $dst_conf (read-only or not writable)" fi else echo "skipped: $src_conf (not found)" fi if [[ -f "$net_script" ]]; then echo "running: $net_script" /usr/bin/env bash "$net_script" || echo "warning: BastionGuard-net-setup.sh failed or incomplete" else echo "skipped: $net_script (not found)" fi } setup_webconf() { local http="$1" https="$2" distro distro="$(detect_distro_src)" echo "[detect_distro] Distribuzione rilevata: $distro" apply_web_config "$http" "$https" "$distro" } setup_native() { local user="${1:-}" extension_id="${2:-}" local bin_path=/usr/bin/BastionGuard-native-host local chrome_json firefox_json [[ -x "$bin_path" || -f "$bin_path" ]] || { echo "warning: native host binary not found: $bin_path"; } chmod a+rx "$bin_path" 2>/dev/null || true if [[ -n "$extension_id" ]]; then chrome_json=$(cat </dev/null 2>&1 && systemctl enable --now "$u" || true; done gui_summary="$(desktop_session_summary "$user" 2>/dev/null || true)" for u in "${user_units[@]}"; do if webui_disabled_unit "$u"; then echo "webui-default: skipped $u enable/start because proxy/browser daemons are GTK/desktop-only" continue fi if proxy_sensitive_unit "$u" && [[ "$gui_summary" == gui* ]]; then echo "safe-desktop: skipped $u enable/start because GTK owns proxy while desktop is active ($gui_summary)" continue fi run_as_user "$user" systemctl --user enable --now "$u" 2>/dev/null || true done } wizard() { local user="$1" http="$2" https="$3"; shift 3 valid_user "$user" || { echo "Invalid user: $user" >&2; exit 67; } valid_port "$http" || { echo "Porta HTTP non valida" >&2; exit 64; } valid_port "$https" || { echo "Porta HTTPS non valida" >&2; exit 64; } ensure_configs "$user" # src/wizard on_finish(): run_net_setup() is executed before the selected privileged actions. setup_net # src/wizard worker_run(): ClamAV daemon and locale scripts are pre-steps before certs/web/blacklist. setup_source_presteps local flags=" $* " [[ "$flags" == *' resolv '* ]] && setup_browser_policies [[ "$flags" == *' dnsmasq '* ]] && setup_dnsmasq [[ "$flags" == *' firewall '* ]] && setup_firewall "$user" [[ "$flags" == *' nftables '* ]] && setup_nftables [[ "$flags" == *' certs '* ]] && setup_certs setup_phishing_blacklist "$user" [[ "$flags" == *' webconf '* ]] && setup_webconf "$http" "$https" [[ "$flags" == *' native '* ]] && setup_native "$user" [[ "$flags" == *' useragent '* ]] && setup_useragent [[ "$flags" == *' banks '* ]] && setup_banks "$user" [[ "$flags" == *' services '* ]] && setup_services "$user" # src/wizard worker_run(): always restart phishing scanner after blacklist/pre-step commands. restart_phishing_scanner echo "BastionGuard system wizard actions completed. config.json is managed by the WebPanel first-run wizard." } download_to_owned_file() { local user="$1" url="$2" dest="$3" min_size="${4:-10}" local uid gid dir tmp size uid="$(user_uid "$user")"; gid="$(user_gid "$user")"; dir="$(dirname "$dest")" install -d -o "$uid" -g "$gid" -m 0750 "$dir" tmp="$(mktemp)" if ! curl -fsSL --max-time 30 "$url" -o "$tmp"; then rm -f "$tmp"; echo "failed: $url"; return 1; fi size="$(stat -c '%s' "$tmp" 2>/dev/null || echo 0)" if (( size < min_size )); then rm -f "$tmp"; echo "failed-small: $url"; return 1; fi install -o "$uid" -g "$gid" -m 0640 "$tmp" "$dest" rm -f "$tmp" echo "updated: $dest" } install_copy_system_data() { local src="$1" dest="$2" dir dir="$(dirname "$dest")" if ! mkdir -p "$dir" 2>/dev/null; then echo "skipped: $dest (cannot create directory)"; return 0; fi if install -o root -g root -m 0644 "$src" "$dest" 2>/tmp/bastionguard-webui-install.err; then echo "installed: $dest" else local err; err="$(cat /tmp/bastionguard-webui-install.err 2>/dev/null || true)" case "$err" in *'Read-only file system'*) echo "skipped: $dest (read-only file system)" ;; *'Permission denied'*) echo "skipped: $dest (permission denied)" ;; *) echo "skipped: $dest (${err:-install failed})" ;; esac fi } update_yara() { local user="$1" home base sysdir success=0 fail=0 f url dest valid_user "$user" || { echo "Invalid user: $user" >&2; exit 67; } command -v curl >/dev/null 2>&1 || { echo "curl not found" >&2; exit 127; } home="$(user_home "$user")"; base="$home/.local/share/BastionGuard/data/yara"; sysdir="/usr/share/BastionGuard/data/yara" local files=( ByteCode.MSIL.Ransomware.Apis.yara ByteCode.MSIL.Ransomware.Thanos.yara Linux.Ransomware.KillDisk.yara Win32.Ransomware.WannaCry.yara Win32.Ransomware.LockBit.yara Win64.Ransomware.BlackBasta.yara ByteCode.MSIL.Ransomware.ChupaCabra.yara ByteCode.MSIL.Ransomware.Cring.yara ByteCode.MSIL.Ransomware.Dusk.yara ByteCode.MSIL.Ransomware.EAF.yara ByteCode.MSIL.Ransomware.Eternity.yara ByteCode.MSIL.Ransomware.Fantom.yara ByteCode.MSIL.Ransomware.GhosTEncryptor.yara ByteCode.MSIL.Ransomware.Ghostbin.yara ByteCode.MSIL.Ransomware.GoodWill.yara ByteCode.MSIL.Ransomware.HarpoonLocker.yara ByteCode.MSIL.Ransomware.Hog.yara ByteCode.MSIL.Ransomware.Invert.yara ByteCode.MSIL.Ransomware.Janelle.yara ByteCode.MSIL.Ransomware.Khonsari.yara ByteCode.MSIL.Ransomware.McBurglar.yara ByteCode.MSIL.Ransomware.Moisha.yara ByteCode.MSIL.Ransomware.Namaste.yara ByteCode.MSIL.Ransomware.Oct.yara ByteCode.MSIL.Ransomware.Pacman.yara ByteCode.MSIL.Ransomware.PoliceRecords.yara ByteCode.MSIL.Ransomware.Povlsomware.yara ByteCode.MSIL.Ransomware.Retis.yara ByteCode.MSIL.Ransomware.TaRRaK.yara ByteCode.MSIL.Ransomware.TimeCrypt.yara ByteCode.MSIL.Ransomware.TimeTime.yara ByteCode.MSIL.Ransomware.Venom.yara ByteCode.MSIL.Ransomware.WildFire.yara ByteCode.MSIL.Ransomware.WormLocker.yara ByteCode.MSIL.Ransomware.ZeroLocker.yara Bytecode.MSIL.Ransomware.CobraLocker.yara Linux.Ransomware.GwisinLocker.yara Linux.Ransomware.Helldown.yara Linux.Ransomware.LuckyJoe.yara Linux.Ransomware.RedAlert.yara Win32.Ransomware.Conti.yara Win32.Ransomware.BlackCat.yara Win32.Ransomware.Clop.yara Win32.Ransomware.Ryuk.yara Win64.Ransomware.Cactus.yara Win64.Ransomware.Pandora.yara Win64.Ransomware.HermeticRansom.yara ) echo "Updating YARA rules..." for f in "${files[@]}"; do url="https://raw.githubusercontent.com/reversinglabs/reversinglabs-yara-rules/develop/yara/ransomware/$f" dest="$base/$f" if download_to_owned_file "$user" "$url" "$dest" 100; then ((success++)); else ((fail++)); fi done if (( success > 0 )); then for f in "$base"/*.yara; do [[ -f "$f" ]] && install_copy_system_data "$f" "$sysdir/$(basename "$f")"; done fi echo "YARA updated: success=$success fail=$fail user_dir=$base" (( success > 0 )) || exit 1 } update_sanesecurity() { local user="$1" home base sysdir success=0 fail=0 db url dest valid_user "$user" || { echo "Invalid user: $user" >&2; exit 67; } command -v curl >/dev/null 2>&1 || { echo "curl not found" >&2; exit 127; } home="$(user_home "$user")"; base="$home/.local/share/BastionGuard/data/sanesecurity"; sysdir="/usr/share/BastionGuard/data/sanesecurity" local dbs=(scamsigs.hdb scam.ndb phish.ndb rogue.hdb ransomware.ndb) echo "Updating Sanesecurity DB..." for db in "${dbs[@]}"; do url="https://sanesecurity.com/clamav/$db"; dest="$base/$db" if download_to_owned_file "$user" "$url" "$dest" 100; then ((success++)); install_copy_system_data "$dest" "$sysdir/$db"; else ((fail++)); fi done echo "Sanesecurity updated: success=$success fail=$fail user_dir=$base" (( success > 0 )) || exit 1 } update_phishing() { local user="$1" home uid gid base tmp final reduce sysdir src count=0 valid_user "$user" || { echo "Invalid user: $user" >&2; exit 67; } command -v curl >/dev/null 2>&1 || { echo "curl not found" >&2; exit 127; } home="$(user_home "$user")"; uid="$(user_uid "$user")"; gid="$(user_gid "$user")" base="$home/.local/share/BastionGuard/data/phishing"; sysdir="/usr/share/BastionGuard/data/phishing" install -d -o "$uid" -g "$gid" -m 0750 "$base" tmp="$(mktemp)"; final="$(mktemp)"; reduce="$(mktemp)" trap 'rm -f "$tmp" "$final" "$reduce"' RETURN local sources=( https://phishing.army/download/phishing_army_blocklist.txt https://openphish.com/feed.txt https://urlhaus.abuse.ch/downloads/text/ ) echo "Updating phishing blacklist..." : > "$tmp" for src in "${sources[@]}"; do echo "source: $src" curl -fsSL --max-time 45 "$src" >> "$tmp" || echo "warning: failed $src" printf '\n' >> "$tmp" done sed -E 's/#.*$//; s/[[:space:]]+$//; s/^[[:space:]]+//' "$tmp" | awk 'length($0)>0' | sort -u > "$final" awk 'length($0)>0 && $0 !~ /^[0-9.]+$/ && $0 !~ /:/ {print}' "$final" | sort -u > "$reduce" count="$(wc -l < "$final" | tr -d ' ')" if (( count < 10 )); then echo "Downloaded blacklist is empty or too small" >&2; exit 1; fi install -o "$uid" -g "$gid" -m 0640 "$final" "$base/blacklist.txt" install -o "$uid" -g "$gid" -m 0640 "$reduce" "$base/blacklist-reduce.txt" install_copy_system_data "$final" "$sysdir/blacklist.txt" install_copy_system_data "$reduce" "$sysdir/blacklist-reduce.txt" echo "Phishing blacklist updated: $count entries user_dir=$base" } read_version() { local path=/usr/share/BastionGuard/data/version.bs if [[ -r "$path" && -f "$path" ]]; then cat "$path" else echo "version=?.?" echo "build=?" fi } valid_data_key() { case "${1:-}" in banks) return 0 ;; *) return 1 ;; esac } data_file_path() { local key="$1" user="$2" home valid_data_key "$key" || { echo "Invalid data file key: $key" >&2; exit 64; } valid_user "$user" || { echo "Invalid user: $user" >&2; exit 67; } home="$(user_home "$user")" case "$key" in banks) printf '%s\n' "$home/.local/share/BastionGuard/banks.json" ;; esac } read_data_file_b64() { local key="$1" user="$2" path path="$(data_file_path "$key" "$user")" [[ -e "$path" ]] || { echo "MISSING $path" >&2; exit 66; } [[ -f "$path" ]] || { echo "NOTFILE $path" >&2; exit 65; } [[ -r "$path" ]] || { echo "UNREADABLE $path" >&2; exit 13; } base64 -w0 "$path" } update_banks() { local user="$1" url="${2:-https://raw.githubusercontent.com/MISP/misp-warninglists/main/lists/bank-website/list.json}" local home uid gid out tmp size valid_user "$user" || { echo "Invalid user: $user" >&2; exit 67; } [[ "$url" =~ ^https:// ]] || { echo "Invalid bank-list URL. HTTPS only." >&2; exit 64; } command -v curl >/dev/null 2>&1 || { echo "curl not found" >&2; exit 127; } home="$(user_home "$user")"; uid="$(user_uid "$user")"; gid="$(user_gid "$user")" out="$home/.local/share/BastionGuard/banks.json" install -d -o "$uid" -g "$gid" -m 0750 "$(dirname "$out")" tmp="$(mktemp)" if ! curl -fsSL --max-time 60 "$url" -o "$tmp"; then rm -f "$tmp"; echo "failed: $url" >&2; exit 1; fi size="$(stat -c '%s' "$tmp" 2>/dev/null || echo 0)" if (( size < 100 )); then rm -f "$tmp"; echo "downloaded bank list is empty or too small" >&2; exit 1; fi install -o "$uid" -g "$gid" -m 0640 "$tmp" "$out" rm -f "$tmp" echo "updated: $out" } system_file_path_for_key() { local key="$1" case "$key" in webports) printf '%s\n' /etc/BastionGuard/webports.conf ;; dnsmasq_bastionguard) printf '%s\n' /etc/dnsmasq.d/BastionGuard.conf ;; firefox_policy) printf '%s\n' /usr/lib/firefox/distribution/policies.json ;; chromium_policy) printf '%s\n' /etc/chromium/policies/managed/BastionGuard-dns.json ;; chrome_policy) printf '%s\n' /etc/opt/chrome/policies/managed/BastionGuard-dns.json ;; edge_policy) printf '%s\n' /etc/opt/edge/policies/managed/BastionGuard-dns.json ;; phishing_custom) printf '%s\n' /usr/share/BastionGuard/data/phishing/blacklist_custom.txt ;; dnsmasq_custom_blacklist) printf '%s\n' /etc/dnsmasq.d/BastionGuard-custom-blacklist.conf ;; nginx_webui) printf '%s\n' /etc/nginx/conf.d/BastionGuard-webui.conf ;; nftables_bastionguard) printf '%s\n' /etc/nftables.d/BastionGuard.nft ;; *) echo "System file key not allowed: $key" >&2; exit 64 ;; esac } inotify_start_saved() { local user="$1" uid gid saved tmp valid_user "$user" || { echo "Invalid user: $user" >&2; exit 67; } uid="$(id -u "$user")"; gid="$(id -g "$user")" saved="$(inotify_saved_paths_file "$user")" tmp="$(mktemp)" if [[ -s "$saved" ]]; then cat "$saved" > "$tmp"; else inotify_default_paths "$user" > "$tmp"; fi [[ -s "$tmp" ]] || { echo "No valid realtime paths configured. Add paths from the WebPanel Scan page." >&2; rm -f "$tmp"; exit 64; } chown "$uid:$gid" "$tmp" 2>/dev/null || true inotify_start "$user" "$(base64 -w0 < "$tmp")" rm -f "$tmp" } write_system_file_b64() { local key="$1" b64="$2" path content path="$(system_file_path_for_key "$key")" content="$(printf '%s' "$b64" | base64 -d)" || { echo "Invalid base64" >&2; exit 64; } write_system_file "$path" 0644 "$content" } phish_auto_update() { local action="$1" case "$action" in enable) systemctl enable --now BastionGuard-phishing-updater.timer ;; disable) systemctl disable --now BastionGuard-phishing-updater.timer ;; *) echo "Invalid phish-auto-update action" >&2; exit 64 ;; esac } backup_fmt_bytes() { local bytes="$1" if [[ ! "$bytes" =~ ^[0-9]+$ ]]; then echo "?"; return 0; fi if (( bytes >= 1073741824 )); then awk -v b="$bytes" 'BEGIN{printf "%.1f GB", b/1073741824}'; elif (( bytes >= 1048576 )); then awk -v b="$bytes" 'BEGIN{printf "%.0f MB", b/1048576}'; else awk -v b="$bytes" 'BEGIN{printf "%.0f KB", b/1024}'; fi } backup_bin_path() { local bin="" bin="$(command -v bastionguard-backup 2>/dev/null || true)" [[ -n "$bin" ]] || bin="/usr/bin/bastionguard-backup" printf '%s' "$bin" } backup_prepare_root_runtime() { # The WebUI helper is expected to run through sudo as root. Prepare the same # runtime shape expected by the native backup tool, but do not fail if a # container/chroot has a read-only /dev or no device-mapper support. export HOME=/root export USER=root export LOGNAME=root export PATH=/usr/sbin:/usr/bin:/sbin:/bin:${PATH:-} unset DBUS_SESSION_BUS_ADDRESS XDG_RUNTIME_DIR WAYLAND_DISPLAY DISPLAY mkdir -p /run/bastionguard-backup 2>/dev/null || true mkdir -p /dev/mapper 2>/dev/null || true } backup_cli() { local bin backup_prepare_root_runtime bin="$(backup_bin_path)" [[ -x "$bin" ]] || { echo "Missing BastionGuard Backup CLI: $bin" >&2; exit 66; } "$bin" "$@" } backup_read_json_key() { local file="$1" key="$2" [[ -r "$file" ]] || return 1 python3 - "$file" "$key" <<'PYJSONKEY' 2>/dev/null || true import json, sys try: with open(sys.argv[1], 'r', encoding='utf-8') as fh: data = json.load(fh) value = data.get(sys.argv[2], '') if isinstance(value, bool): print('true' if value else 'false') elif value is None: print('') else: print(str(value)) except Exception: pass PYJSONKEY } backup_resolve_device() { local value="${1:-}" dev="" value="${value//[$'\r\n\t ']/}" [[ -n "$value" ]] || return 1 if [[ "$value" == /dev/* && -b "$value" ]]; then printf '%s\n' "$value"; return 0; fi if [[ "$value" == /dev/disk/by-uuid/* && -e "$value" ]]; then readlink -f -- "$value"; return 0; fi if [[ -e "/dev/disk/by-uuid/$value" ]]; then readlink -f -- "/dev/disk/by-uuid/$value"; return 0; fi if command -v blkid >/dev/null 2>&1; then dev="$(blkid -U "$value" 2>/dev/null || true)" [[ -n "$dev" && -b "$dev" ]] && { printf '%s\n' "$dev"; return 0; } fi if command -v lsblk >/dev/null 2>&1; then dev="$(lsblk -nrpo NAME,UUID 2>/dev/null | awk -v u="$value" '$2==u{print $1; exit}')" [[ -n "$dev" && -b "$dev" ]] && { printf '%s\n' "$dev"; return 0; } fi return 1 } backup_device_arg_if_block() { local value="${1:-}" dev="" [[ -n "$value" ]] || return 1 dev="$(backup_resolve_device "$value" 2>/dev/null || true)" [[ -n "$dev" && -b "$dev" ]] || return 1 printf '%s\n' "$dev" } backup_configured_device_value() { local cfg cfg="$(backup_config_path)" backup_read_json_key "$cfg" backup_device_uuid } backup_configured_mode() { local cfg v cfg="$(backup_config_path)" v="$(backup_read_json_key "$cfg" btrfs_mode)" case "${v,,}" in true|1|yes|on) echo btrfs ;; *) echo rsync ;; esac } backup_find_mount_for_source() { local source="${1:-}" target="" [[ -n "$source" ]] || return 1 if command -v findmnt >/dev/null 2>&1; then target="$(findmnt -rn -S "$source" -o TARGET 2>/dev/null | head -n1 || true)" [[ -n "$target" ]] && { printf '%s\n' "$target"; return 0; } fi return 1 } backup_mount_ro_device() { local dev="$1" mp="" [[ -b "$dev" ]] || return 1 mp="$(backup_find_mount_for_source "$dev" 2>/dev/null || true)" if [[ -n "$mp" && -d "$mp" ]]; then printf '%s\n' "$mp"; return 0; fi mp="/run/bastionguard-backup/webui-list-$$-$(basename "$dev")" mkdir -p "$mp" 2>/dev/null || return 1 if mount -o ro "$dev" "$mp" 2>/dev/null; then printf '%s\n' "$mp" return 0 fi rmdir "$mp" 2>/dev/null || true return 1 } backup_snapshot_emit_dir() { local snap="$1" repo="$2" mode="$3" name path_b64 repo_b64 created tags comments comments_b64 type [[ -d "$snap" ]] || return 0 name="$(basename -- "$snap")" created=""; tags=""; comments=""; type="$mode" if [[ -r "$snap/info.json" ]]; then # Output: createdtagscommentstype. Comments are base64 encoded after this block. IFS=$'\t' read -r created tags comments type < <(python3 - "$snap/info.json" <<'PYSNAP' 2>/dev/null || true import json, sys try: with open(sys.argv[1], 'r', encoding='utf-8') as fh: data = json.load(fh) print('\t'.join(str(data.get(k, '')) for k in ('created','tags','comments','type'))) except Exception: pass PYSNAP ) fi [[ -n "$created" ]] || created="$(stat -c '%Y' "$snap" 2>/dev/null || echo 0)" [[ -n "$type" ]] || type="$mode" path_b64="$(printf '%s' "$snap" | base64 -w0)" repo_b64="$(printf '%s' "$repo" | base64 -w0)" comments_b64="$(printf '%s' "$comments" | base64 -w0)" printf 'SNAP\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' "$name" "$type" "$path_b64" "${created:-0}" "$tags" "$comments_b64" "$repo_b64" } backup_snapshot_scan_root() { local root="$1" repo mode snapdir snap [[ -d "$root" ]] || return 0 for repo in \ "$root/bastionguard-backup/snapshots" \ "$root/timeshift/snapshots" \ "$root/bastionguard-backup-btrfs/snapshots" \ "$root/timeshift-btrfs/snapshots"; do [[ -d "$repo" ]] || continue mode="rsync" case "$repo" in *btrfs*) mode="btrfs" ;; esac find "$repo" -mindepth 1 -maxdepth 1 -type d -print0 2>/dev/null | sort -z | while IFS= read -r -d '' snap; do [[ "$(basename -- "$snap")" == ".sync" ]] && continue backup_snapshot_emit_dir "$snap" "$repo" "$mode" done done } backup_snapshot_index() { backup_prepare_root_runtime local cfg_value dev mp tmp_mounted="" root declare -A seen=() cfg_value="$(backup_configured_device_value 2>/dev/null || true)" if [[ -n "$cfg_value" ]]; then dev="$(backup_resolve_device "$cfg_value" 2>/dev/null || true)" if [[ -n "$dev" ]]; then mp="$(backup_mount_ro_device "$dev" 2>/dev/null || true)" if [[ -n "$mp" ]]; then backup_snapshot_scan_root "$mp" case "$mp" in /run/bastionguard-backup/webui-list-*) tmp_mounted="$mp" ;; esac else printf 'WARN\t%s\n' "Configured backup device is present but could not be mounted read-only: $dev" fi else printf 'WARN\t%s\n' "Configured backup device was not found: $cfg_value" fi fi # Scan already-mounted filesystems and common removable/media roots. This is # deliberately independent from the native CLI so the WebUI can show existing # snapshots even when the configured device is full or cannot be mounted by # the CLI for creation/restore. { command -v findmnt >/dev/null 2>&1 && findmnt -rn -o TARGET 2>/dev/null || true printf '%s\n' / /mnt /media /run/media /run/bastionguard-backup find /mnt /media /run/media /run/bastionguard-backup -mindepth 1 -maxdepth 3 -type d 2>/dev/null || true } | while IFS= read -r root; do [[ -n "$root" && -d "$root" ]] || continue case "$root" in /proc*|/sys*|/dev*|/run/user*|/tmp*) continue ;; esac # avoid scanning the temporary mount twice through /run/bastionguard-backup if [[ -n "$tmp_mounted" && "$root" == "$tmp_mounted" ]]; then continue; fi backup_snapshot_scan_root "$root" done | awk '!seen[$0]++' if [[ -n "$tmp_mounted" ]]; then umount "$tmp_mounted" 2>/dev/null || true rmdir "$tmp_mounted" 2>/dev/null || true fi } backup_config_path() { printf '%s' "/etc/bastionguard-backup/bastionguard-backup.json"; } backup_default_config() { cat <<'JSONDEFAULT' { "backup_device_uuid": "", "parent_device_uuid": "", "do_first_run": "false", "btrfs_mode": "false", "include_btrfs_home_for_backup": "false", "include_btrfs_home_for_restore": "false", "stop_cron_emails": "true", "schedule_monthly": "false", "schedule_weekly": "false", "schedule_daily": "true", "schedule_hourly": "false", "schedule_boot": "false", "count_monthly": "2", "count_weekly": "3", "count_daily": "5", "count_hourly": "6", "count_boot": "5", "snapshot_size": "0", "snapshot_count": "0", "date_format": "%Y-%m-%d %H:%M:%S", "exclude": [], "exclude-apps": [] } JSONDEFAULT } backup_config_read() { local cfg cfg="$(backup_config_path)" if [[ -f "$cfg" ]]; then cat "$cfg"; else backup_default_config; fi } backup_config_save_b64() { local b64="$1" cfg tmp dir cfg="$(backup_config_path)" dir="$(dirname "$cfg")" install -d -o root -g root -m 0755 "$dir" tmp="$(mktemp)" printf '%s' "$b64" | base64 -d > "$tmp" || { rm -f "$tmp"; echo "Invalid base64 config" >&2; exit 64; } if command -v python3 >/dev/null 2>&1; then python3 - "$tmp" <<'PYCFG' || { rm -f "$tmp"; echo "Invalid JSON config" >&2; exit 64; } import json, sys path = sys.argv[1] with open(path, 'r', encoding='utf-8') as fh: data = json.load(fh) if not isinstance(data, dict): raise SystemExit(1) allowed = { 'backup_device_uuid','parent_device_uuid','do_first_run','btrfs_mode','include_btrfs_home','include_btrfs_home_for_backup','include_btrfs_home_for_restore','stop_cron_emails', 'schedule_monthly','schedule_weekly','schedule_daily','schedule_hourly','schedule_boot','count_monthly','count_weekly','count_daily','count_hourly','count_boot', 'snapshot_size','snapshot_count','date_format','exclude','exclude-apps' } for key in list(data.keys()): if key not in allowed: data.pop(key, None) if not isinstance(data.get('exclude', []), list): data['exclude'] = [] if not isinstance(data.get('exclude-apps', []), list): data['exclude-apps'] = [] with open(path, 'w', encoding='utf-8') as fh: json.dump(data, fh, ensure_ascii=False, indent=2) fh.write(' ') PYCFG fi install -o root -g root -m 0644 "$tmp" "$cfg" rm -f "$tmp" echo "BastionGuard Backup config saved: $cfg" } backup_status() { local squash="/boot/bastionguard-recovery/live/filesystem.squashfs" local kernel="/boot/bastionguard-recovery/vmlinuz" local grub="/etc/grub.d/42_bastionguard-recovery" local check_path="/boot" free="0" size="0" bin cfg [[ -d "$check_path" ]] || check_path="/" free="$(df -PB1 "$check_path" 2>/dev/null | awk 'NR==2{print $4}' || echo 0)" [[ -f "$squash" ]] && size="$(stat -c '%s' "$squash" 2>/dev/null || echo 0)" bin="$(backup_bin_path)" cfg="$(backup_config_path)" printf 'RECOVERY_DIR %s ' "/boot/bastionguard-recovery" printf 'SQUASHFS %s %s %s ' "$squash" "$( [[ -f "$squash" ]] && echo 1 || echo 0 )" "$(backup_fmt_bytes "$size")" printf 'KERNEL %s %s ' "$kernel" "$( [[ -f "$kernel" ]] && echo 1 || echo 0 )" printf 'GRUB %s %s ' "$grub" "$( [[ -f "$grub" ]] && echo 1 || echo 0 )" printf 'SPACE %s %s %s ' "$check_path" "${free:-0}" "$(backup_fmt_bytes "${free:-0}")" printf 'CLI %s %s ' "$bin" "$( [[ -x "$bin" ]] && echo 1 || echo 0 )" printf 'CONFIG %s %s ' "$cfg" "$( [[ -f "$cfg" ]] && echo 1 || echo 0 )" } backup_devices_tsv() { backup_prepare_root_runtime if command -v lsblk >/dev/null 2>&1 && command -v python3 >/dev/null 2>&1; then python3 - <<'PYDEVS' import json, os, subprocess, sys COLUMN_SETS = [ 'NAME,KNAME,PATH,TYPE,FSTYPE,SIZE,LABEL,UUID,MOUNTPOINT,TRAN,RM,RO,PKNAME,MODEL,VENDOR,HOTPLUG', 'NAME,KNAME,PATH,TYPE,FSTYPE,SIZE,LABEL,UUID,MOUNTPOINT,TRAN,RM,RO,PKNAME,MODEL,VENDOR', 'NAME,KNAME,PATH,TYPE,FSTYPE,SIZE,LABEL,UUID,MOUNTPOINT,RM,RO,PKNAME', 'NAME,TYPE,FSTYPE,SIZE,LABEL,UUID,MOUNTPOINT', ] def run_lsblk(): for cols in COLUMN_SETS: try: out = subprocess.check_output(['lsblk', '-J', '-o', cols], stderr=subprocess.DEVNULL, text=True) data = json.loads(out) return data.get('blockdevices', []) or [] except Exception: continue return [] def read_file(path): try: with open(path, 'r', encoding='utf-8', errors='ignore') as fh: return fh.read().strip() except Exception: return '' def boolish(value): if isinstance(value, bool): return value if value is None: return False return str(value).strip().lower() in ('1', 'true', 'yes', 'on') def kname_of(dev): return (dev.get('kname') or dev.get('name') or '').replace('/dev/', '').strip() def dev_path(dev): path = (dev.get('path') or '').strip() if path: return path name = (dev.get('name') or '').strip() if name.startswith('/dev/'): return name if name: return '/dev/' + name return '' def sys_parent_disk(kname): if not kname: return '' base = os.path.basename(kname) # /sys/class/block/sde/sde1/partition exists for partitions; PKNAME is better, # but this fallback keeps ancient lsblk output usable. parent = os.path.realpath(f'/sys/class/block/{base}') cur = parent for _ in range(8): if os.path.exists(os.path.join(cur, 'removable')): return os.path.basename(cur) nxt = os.path.dirname(cur) if nxt == cur: break cur = nxt return base def sys_is_usb(kname, pkname=''): names = [x for x in (kname, pkname, sys_parent_disk(kname)) if x] for name in names: rp = os.path.realpath(f'/sys/class/block/{os.path.basename(name)}') if '/usb' in rp.lower(): return True cur = rp for _ in range(12): modalias = read_file(os.path.join(cur, 'modalias')).lower() if modalias.startswith('usb:') or 'usb' in modalias: return True nxt = os.path.dirname(cur) if nxt == cur: break cur = nxt return False def sys_removable(kname, pkname=''): for name in (pkname, sys_parent_disk(kname), kname): if not name: continue v = read_file(f'/sys/class/block/{os.path.basename(name)}/removable') if v == '1': return True return False def flatten(devs, parent=None, inherited=None): inherited = dict(inherited or {}) rows = [] for dev in devs: kname = kname_of(dev) path = dev_path(dev) typ = (dev.get('type') or '').strip() pkname = (dev.get('pkname') or inherited.get('kname') or '').replace('/dev/', '').strip() tran = (dev.get('tran') or inherited.get('tran') or '').strip() removable = boolish(dev.get('rm')) or boolish(dev.get('hotplug')) or boolish(inherited.get('rm')) or sys_removable(kname, pkname) usb = (tran.lower() == 'usb') or boolish(dev.get('hotplug')) or boolish(inherited.get('usb')) or sys_is_usb(kname, pkname) row = { 'path': path, 'type': typ, 'fstype': (dev.get('fstype') or '').strip(), 'size': str(dev.get('size') or '').strip(), 'label': str(dev.get('label') or '').strip(), 'uuid': str(dev.get('uuid') or '').strip(), 'mountpoint': str(dev.get('mountpoint') or '').strip(), 'tran': tran, 'rm': '1' if removable else '0', 'ro': '1' if boolish(dev.get('ro')) else '0', 'model': str(dev.get('model') or inherited.get('model') or '').strip(), 'vendor': str(dev.get('vendor') or inherited.get('vendor') or '').strip(), 'hotplug': '1' if boolish(dev.get('hotplug')) else '0', 'parent': ('/dev/' + pkname) if pkname and not pkname.startswith('/dev/') else pkname, 'usb': '1' if usb else '0', } rows.append(row) child_inherited = { 'kname': kname, 'tran': tran, 'rm': '1' if removable else '0', 'usb': '1' if usb else '0', 'model': row['model'], 'vendor': row['vendor'], } rows.extend(flatten(dev.get('children') or [], dev, child_inherited)) return rows linux_fs = { 'ext2','ext3','ext4','xfs','btrfs','f2fs','jfs','reiserfs','nilfs2','bcachefs', 'crypto_luks','LVM2_member','linux_raid_member' } flat_devices = flatten(run_lsblk()) if not flat_devices: # Last-resort fallback: blkid can still report partitions in restricted # environments where lsblk cannot read sysfs. Metadata is limited, but the # WebUI can still offer a selectable device instead of an empty list. try: out = subprocess.check_output(['blkid', '-o', 'export'], stderr=subprocess.DEVNULL, text=True) item = {} for line in out.splitlines() + ['']: if not line.strip(): if item.get('DEVNAME'): path = item.get('DEVNAME', '') kname = os.path.basename(path) flat_devices.append({ 'path': path, 'type': 'part', 'fstype': item.get('TYPE', ''), 'size': '', 'label': item.get('LABEL', ''), 'uuid': item.get('UUID', ''), 'mountpoint': '', 'tran': '', 'rm': '1' if sys_removable(kname) else '0', 'ro': '0', 'model': '', 'vendor': '', 'hotplug': '0', 'parent': '', 'usb': '1' if sys_is_usb(kname) else '0', }) item = {} continue if '=' in line: k, v = line.split('=', 1) item[k] = v except Exception: pass rows = [] seen = set() for r in flat_devices: path = r['path'] if not path or path in seen or not os.path.exists(path): continue seen.add(path) typ = r['type'] if typ in ('loop','rom') or path.startswith('/dev/loop') or path.startswith('/dev/sr'): continue fstype = r['fstype'] # Keep disks, partitions and mapper/lvm/crypt nodes. The UI can select USB # partitions for backup and disk nodes for GRUB/restore targets. if typ not in ('disk','part','crypt','lvm','raid','md'): continue eligible = '1' if (fstype in linux_fs or fstype.startswith('ext') or fstype == 'btrfs' or typ == 'disk') else '0' if r['ro'] == '1': reason = 'read-only' elif eligible == '1': reason = 'ok' elif fstype: reason = 'non-linux-fs' else: reason = 'no-filesystem' print('\t'.join([ r['path'], r['type'], r['fstype'], r['size'], r['label'], r['uuid'], r['mountpoint'], r['tran'], r['rm'], r['ro'], r['model'], r['vendor'], r['hotplug'], r['parent'], r['usb'], eligible, reason ])) PYDEVS return 0 fi if ! command -v lsblk >/dev/null 2>&1; then return 0; fi # Fallback for very small systems without python3/lsblk JSON. lsblk -nrPpo NAME,TYPE,FSTYPE,SIZE,LABEL,UUID,MOUNTPOINT 2>/dev/null | while IFS= read -r line; do local NAME="" TYPE="" FSTYPE="" SIZE="" LABEL="" UUID="" MOUNTPOINT="" eval "$line" 2>/dev/null || true [[ -n "${NAME:-}" ]] || continue case "${TYPE:-}" in loop|rom) continue ;; esac [[ -e "$NAME" ]] || continue printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' \ "$NAME" "${TYPE:-}" "${FSTYPE:-}" "${SIZE:-}" "${LABEL:-}" "${UUID:-}" "${MOUNTPOINT:-}" \ "" "0" "0" "" "" "0" "" "0" "1" "ok" done } backup_list_snapshots() { local mode="${1:-config}" device_b64="${2:-}" raw_device="" device=""; local -a args [[ -n "$device_b64" ]] && raw_device="$(printf '%s' "$device_b64" | base64 -d 2>/dev/null || true)" args=(--list-snapshots) case "$mode" in rsync) args+=(--rsync);; btrfs) args+=(--btrfs);; esac # The native CLI expects /dev/xxx for --snapshot-device, not a UUID. If the # WebUI/config contains a UUID, resolve it before passing it. If it cannot be # resolved, omit the override and let the native config path handle/report it. device="$(backup_device_arg_if_block "$raw_device" 2>/dev/null || true)" [[ -n "$device" ]] && args+=(--snapshot-device "$device") backup_cli "${args[@]}" } backup_create_snapshot() { local comment_b64="$1" tags="${2:-O}" mode="${3:-config}" device_b64="${4:-}" comment raw_device device; local -a args comment="$(printf '%s' "$comment_b64" | base64 -d 2>/dev/null || true)" raw_device="$(printf '%s' "$device_b64" | base64 -d 2>/dev/null || true)" device="$(backup_device_arg_if_block "$raw_device" 2>/dev/null || true)" [[ "$tags" =~ ^[OBHDWM,]+$ ]] || tags="O" args=(--create --scripted --yes --comments "$comment" --tags "$tags") case "$mode" in rsync) args+=(--rsync);; btrfs) args+=(--btrfs);; esac [[ -n "$device" ]] && args+=(--snapshot-device "$device") backup_cli "${args[@]}" } backup_delete_snapshot() { local snap_b64="$1" device_b64="${2:-}" snap raw_device device; local -a args snap="$(printf '%s' "$snap_b64" | base64 -d 2>/dev/null || true)" raw_device="$(printf '%s' "$device_b64" | base64 -d 2>/dev/null || true)" device="$(backup_device_arg_if_block "$raw_device" 2>/dev/null || true)" [[ -n "$snap" ]] || { echo "Snapshot name is required" >&2; exit 64; } args=(--delete --scripted --yes --snapshot "$snap") [[ -n "$device" ]] && args+=(--snapshot-device "$device") backup_cli "${args[@]}" } backup_delete_all() { local device_b64="${1:-}" raw_device device; local -a args raw_device="$(printf '%s' "$device_b64" | base64 -d 2>/dev/null || true)" device="$(backup_device_arg_if_block "$raw_device" 2>/dev/null || true)" args=(--delete-all --scripted --yes) [[ -n "$device" ]] && args+=(--snapshot-device "$device") backup_cli "${args[@]}" } backup_restore_snapshot() { local snap_b64="$1" target_b64="$2" grub_b64="${3:-}" skip_grub="${4:-0}" device_b64="${5:-}" local snap raw_target target raw_grub grub raw_device device; local -a args snap="$(printf '%s' "$snap_b64" | base64 -d 2>/dev/null || true)" raw_target="$(printf '%s' "$target_b64" | base64 -d 2>/dev/null || true)" raw_grub="$(printf '%s' "$grub_b64" | base64 -d 2>/dev/null || true)" raw_device="$(printf '%s' "$device_b64" | base64 -d 2>/dev/null || true)" target="$(backup_device_arg_if_block "$raw_target" 2>/dev/null || true)" grub="$(backup_device_arg_if_block "$raw_grub" 2>/dev/null || true)" device="$(backup_device_arg_if_block "$raw_device" 2>/dev/null || true)" [[ -n "$snap" ]] || { echo "Snapshot name is required" >&2; exit 64; } [[ -n "$target" ]] || { echo "Target block device is required or could not be resolved: $raw_target" >&2; exit 64; } args=(--restore --scripted --yes --snapshot "$snap" --target-device "$target") if [[ "$skip_grub" == "1" ]]; then args+=(--skip-grub); elif [[ -n "$grub" ]]; then args+=(--grub-device "$grub"); fi [[ -n "$device" ]] && args+=(--snapshot-device "$device") backup_cli "${args[@]}" } backup_action() { local action="${1:-}"; shift || true local build_script="/usr/share/bastionguard-backup/scripts/build-recovery-squashfs.sh" local grub_script="/usr/share/bastionguard-backup/scripts/install-grub-entry.sh" case "$action" in status) backup_status ;; config-read) backup_config_read ;; config-save-b64) [[ $# -eq 1 ]] || { echo "config-save-b64 requires one argument" >&2; exit 64; }; backup_config_save_b64 "$1" ;; devices-tsv) backup_devices_tsv ;; list-devices) backup_devices_tsv ;; snapshot-index) backup_snapshot_index ;; list-snapshots) backup_list_snapshots "${1:-config}" "${2:-}" ;; create-snapshot-b64) [[ $# -ge 4 ]] || { echo "create-snapshot-b64 requires comment, tags, mode, device" >&2; exit 64; }; backup_create_snapshot "$1" "$2" "$3" "$4" ;; delete-snapshot-b64) [[ $# -ge 1 ]] || { echo "delete-snapshot-b64 requires snapshot" >&2; exit 64; }; backup_delete_snapshot "$1" "${2:-}" ;; delete-all-b64) backup_delete_all "${1:-}" ;; restore-snapshot-b64) [[ $# -ge 5 ]] || { echo "restore-snapshot-b64 requires snapshot, target, grub, skip, device" >&2; exit 64; }; backup_restore_snapshot "$1" "$2" "$3" "$4" "$5" ;; launch-manager) echo "BastionGuard WebUI: graphical backup manager launch is disabled. The WebUI backup manager uses the headless CLI and scripts." >&2 exit 76 ;; build-recovery) [[ -f "$build_script" ]] || { echo "Missing script: $build_script" >&2; exit 66; } echo "==> Running recovery build script: $build_script" bash "$build_script" ;; install-grub) [[ -f "$grub_script" ]] || { echo "Missing script: $grub_script" >&2; exit 66; } echo "==> Running GRUB install script: $grub_script" bash "$grub_script" ;; remove-grub) echo "==> Removing BastionGuard recovery GRUB entry" rm -f /etc/grub.d/42_bastionguard-recovery if command -v update-grub >/dev/null 2>&1; then update-grub elif command -v grub-mkconfig >/dev/null 2>&1; then grub-mkconfig -o /boot/grub/grub.cfg else echo "Warning: no update-grub or grub-mkconfig found" >&2 fi ;; *) echo "Invalid backup action: $action" >&2; exit 64 ;; esac } scan_path_b64() { local b64="$1" target real scanner target="$(printf '%s' "$b64" | base64 -d)" || { echo "Base64 non valido" >&2; exit 64; } [[ -n "$target" ]] || { echo "Percorso vuoto" >&2; exit 64; } [[ -e "$target" ]] || { echo "Percorso inesistente: $target" >&2; exit 66; } if command -v realpath >/dev/null 2>&1; then real="$(realpath -m -- "$target")"; else real="$target"; fi case "$real" in /home|/home/*|/srv|/srv/*|/mnt|/mnt/*|/media|/media/*|/run/media|/run/media/*|/var/www|/var/www/*|/var/lib/bastionguard-webui/*|/tmp/bastionguard-webui-data/*|/tmp|/tmp/*) ;; *) echo "Percorso non consentito dal profilo scan WebUI: $real" >&2; exit 64 ;; esac run_clam_scan_and_alert "$real" manual } valid_unit_name() { [[ "${1:-}" =~ ^[A-Za-z0-9@_.:-]+\.service$ || "${1:-}" =~ ^[A-Za-z0-9@_.:-]+\.timer$ ]] } service_action() { local mode="$1" user="$2" action="$3" unit="$4" verb guard_action [[ "$mode" == "system" || "$mode" == "user" ]] || { echo "Invalid service mode" >&2; exit 64; } case "$action" in start|stop|restart|enable|disable) ;; *) echo "Invalid service action" >&2; exit 64 ;; esac valid_unit_name "$unit" || { echo "Invalid unit: $unit" >&2; exit 64; } verb="$action" guard_action="$action" if [[ "$action" == "enable" ]]; then verb="enable --now"; guard_action="enable--now"; fi if [[ "$action" == "disable" ]]; then verb="disable --now"; guard_action="disable--now"; fi if webui_disabled_unit "$unit"; then case "$guard_action" in stop|disable|disable--now) ;; *) echo "BastionGuard WebUI: blocked $guard_action for $unit. CEF/PAC, Webcam/Privacy and USB are disabled in the server WebUI." >&2; exit 76 ;; esac fi if [[ "$mode" == "user" ]]; then valid_user "$user" || { echo "Invalid user: $user" >&2; exit 67; } guard_proxy_desktop_conflict "$user" "$unit" "$guard_action" # shellcheck disable=SC2086 run_as_user "$user" systemctl --user $verb "$unit" else # shellcheck disable=SC2086 systemctl $verb "$unit" fi } append_alert_tsv() { local type="$1" family="$2" path="$3" source="$4" dir=/var/lib/bastionguard-webui file ts id bpath bfamily owner mkdir -p "$dir" 2>/dev/null || return 0 owner="$(stat -c '%u:%g' "$dir" 2>/dev/null || true)" chmod 0750 "$dir" 2>/dev/null || true file="$dir/alerts.tsv" ts="$(date +%s)" id="$(printf '%s|%s|%s|%s' "$ts" "$type" "$family" "$path" | sha256sum | awk '{print $1}')" bpath="$(printf '%s' "$path" | base64 -w0)" bfamily="$(printf '%s' "$family" | base64 -w0)" printf '%s\t%s\t%s\t%s\t%s\t%s\n' "$ts" "$id" "$type" "$bfamily" "$bpath" "$source" >> "$file" || true [[ -n "$owner" ]] && chown "$owner" "$file" 2>/dev/null || true chmod 0640 "$file" 2>/dev/null || true } ransomware_scanner_bin() { local c for c in /usr/bin/BastionGuard-ransomware-scanner /usr/local/bin/BastionGuard-ransomware-scanner /bin/BastionGuard-ransomware-scanner; do [[ -x "$c" ]] && { printf '%s\n' "$c"; return 0; } done command -v BastionGuard-ransomware-scanner 2>/dev/null || true } append_ransomware_alert_for_file() { local path="$1" family="${2:-BastionGuard.Ransomware.Signal}" source="${3:-ransomware}" [[ -n "$path" ]] || return 0 append_alert_tsv ransomware "$family" "$path" "$source" } scan_ransomware_path_and_alert() { local target="$1" source="${2:-ransomware}" scanner out rc=0 found=0 event_count_before=0 event_count_after=0 line path fam [[ -e "$target" ]] || return 0 scanner="$(ransomware_scanner_bin)" if [[ -n "$scanner" ]]; then if [[ -d "$target" ]]; then event_count_before=$(wc -l < /tmp/BastionGuard-ransomware-events.log 2>/dev/null || echo 0) set +e out="$($scanner --scan-dir "$target" 2>&1)" rc=$? set -e else event_count_before=$(wc -l < /tmp/BastionGuard-ransomware-events.log 2>/dev/null || echo 0) set +e out="$($scanner --scan "$target" 2>&1)" rc=$? set -e fi printf '%s\n' "$out" if printf '%s\n' "$out" | grep -Eiq '(Ransomware rilevato|Ransomware detected|\[YARA\]|YARA_SAMBA|YARA_FILE_MATCH|RILEVATO|MATCH:)'; then if ! printf '%s\n' "$out" | grep -Eiq '(Nessuna|No rule|No YARA|nessuna regola|No match)'; then fam="BastionGuard.YARA.Ransomware" [[ -f "$target" ]] && append_ransomware_alert_for_file "$target" "$fam" "$source" && found=1 fi fi # Import events written by the native scanner, if any were appended during this run. event_count_after=$(wc -l < /tmp/BastionGuard-ransomware-events.log 2>/dev/null || echo 0) if [[ "$event_count_after" =~ ^[0-9]+$ && "$event_count_before" =~ ^[0-9]+$ && $event_count_after -gt $event_count_before ]]; then tail -n $((event_count_after-event_count_before)) /tmp/BastionGuard-ransomware-events.log 2>/dev/null | while IFS='|' read -r ts path fam rest; do [[ -n "$path" ]] || continue case "$fam" in SUPPRESSED_ALLOWLIST*) continue ;; esac if [[ "$fam" == YARA* || "$fam" == *RANSOM* || "$fam" == *ransom* ]]; then append_ransomware_alert_for_file "$path" "${fam:-BastionGuard.Ransomware}" "$source" fi done found=1 fi return "$rc" fi return "$found" } import_ransomware_events() { local f=/tmp/BastionGuard-ransomware-events.log native=/var/log/BastionGuard/antiransom_inotify.log line ts path fam rest payload imported=0 if [[ -r "$f" ]]; then tail -n 200 "$f" | while IFS='|' read -r ts path fam rest; do [[ -n "$path" && -n "$fam" ]] || continue case "$fam" in SUPPRESSED_ALLOWLIST*) continue ;; esac if [[ "$fam" == YARA* || "$fam" == *RANSOM* || "$fam" == *ransom* ]]; then append_ransomware_alert_for_file "$path" "${fam:-BastionGuard.Ransomware}" "ransomware-realtime" fi done imported=1 fi if [[ -r "$native" ]]; then tail -n 300 "$native" | while IFS= read -r line; do [[ -n "$line" ]] || continue path=""; fam="" case "$line" in *"Alert inviato:"*" | "*) payload="${line#*Alert inviato: }" path="${payload%% | *}" fam="${payload##* | }" ;; *"ARCHIVE THREAT SIGNAL:"*) rest="${line#*ARCHIVE THREAT SIGNAL: }" path="${rest%% risk=*}" fam="BastionGuard.ARCHIVE_THREAT" ;; *) continue ;; esac [[ -n "$path" ]] || continue append_ransomware_alert_for_file "$path" "${fam:-BastionGuard.RansomwareRealtime}" "ransomware-realtime" done imported=1 fi if [[ "$imported" == 0 ]]; then echo "No ransomware event log found: $f or $native" else echo "ransomware events imported" fi } parse_scan_alerts() { local source="$1" line f fam while IFS= read -r line; do [[ "$line" == *" FOUND"* ]] || continue f="${line%%:*}" fam="${line#*: }"; fam="${fam% FOUND*}" if [[ -n "$f" ]]; then local atype="malware"; [[ "${fam,,}" == *ransom* || "${f,,}" == *ransom* ]] && atype="ransomware" append_alert_tsv "$atype" "${fam:-Malware}" "$f" "$source" fi done } webui_trusted_script_path() { local p="${1:-}" base base="$(basename -- "$p" 2>/dev/null || true)" case "$p" in /usr/local/sbin/bastionguard-webui-*|/usr/local/libexec/bastionguard-webui-*|/usr/share/bastionguard-webui/scripts/*|/srv/http/webui/scripts/*|/srv/www/webui/scripts/*|/var/www/*/webui/scripts/*|*/webui/scripts/bastionguard-webui-*|*/webui/scripts/install-webui-helpers.sh|*/webui/scripts/install-user-service-helper.sh|*/webui/scripts/vendor-cantarell-font.sh) case "$base" in bastionguard-webui-*|install-webui-helpers.sh|install-user-service-helper.sh|vendor-cantarell-font.sh) return 0 ;; esac ;; esac return 1 } run_clam_scan_and_alert() { local real="$1" source="$2" out rc scanner=() if [[ -f "$real" ]] && webui_trusted_script_path "$real"; then echo "SKIP trusted BastionGuard WebUI helper script: $real" return 0 fi if command -v clamdscan >/dev/null 2>&1; then scanner=(clamdscan --fdpass --multiscan --no-summary "$real") elif command -v clamscan >/dev/null 2>&1; then scanner=(clamscan -r --infected "$real") else echo "WARN: clamdscan/clamscan non trovati; eseguo comunque i controlli anti-ransomware" >&2 scanner=() fi if [[ ${#scanner[@]} -gt 0 ]]; then echo "Comando: ${scanner[*]}" set +e out="$("${scanner[@]}" 2>&1)" rc=$? set -e printf '%s\n' "$out" printf '%s\n' "$out" | parse_scan_alerts "$source" else rc=0 fi # The monitored paths must also run the BastionGuard anti-ransomware checks, # not only ClamAV. This keeps manual scans, Samba scans and inotify events aligned. scan_ransomware_path_and_alert "$real" "$source" || true return "$rc" } smb_cred_for() { local key="$1" user="$2" home cred cur line u p enc home="$(user_home "$user")"; cred="$home/.config/BastionGuard/cred.conf" [[ -r "$cred" ]] || return 0 while IFS= read -r line || [[ -n "$line" ]]; do [[ -z "$line" ]] && continue if [[ "$line" == \[*\] ]]; then cur="${line#[}"; cur="${cur%]}"; continue; fi [[ "$cur" == "$key" ]] || continue if [[ "$line" == user=* ]]; then u="$(printf '%s' "${line#user=}" | base64 -d 2>/dev/null || true)"; fi if [[ "$line" == pass=* ]]; then enc="${line#pass=}" if command -v python3 >/dev/null 2>&1; then p="$(python3 - "$enc" <<'PYDEC' 2>/dev/null || true import base64,sys key=b'BastionGuard0101SecretKey' try: data=base64.b64decode(sys.argv[1]) print(bytes([b ^ key[i % len(key)] for i,b in enumerate(data)]).decode('utf-8','ignore')) except Exception: pass PYDEC )" else p="$(printf '%s' "$enc" | base64 -d 2>/dev/null || true)" fi fi done < "$cred" printf '%s\t%s\n' "$u" "$p" } run_samba_clam_scan_and_alert() { local tmp="$1" remote_base="$2" out rc scanner=() line f fam rel remote atype if command -v clamdscan >/dev/null 2>&1; then scanner=(clamdscan --fdpass --multiscan --no-summary "$tmp") elif command -v clamscan >/dev/null 2>&1; then scanner=(clamscan -r --infected "$tmp") else echo "clamdscan/clamscan non trovati" >&2 return 127 fi echo "Comando: ${scanner[*]}" set +e out="$("${scanner[@]}" 2>&1)" rc=$? set -e printf '%s\n' "$out" while IFS= read -r line; do [[ "$line" == *" FOUND"* ]] || continue f="${line%%:*}" fam="${line#*: }"; fam="${fam% FOUND*}" rel="${f#$tmp/}" if [[ "$rel" == "$f" ]]; then remote="$remote_base"; else remote="${remote_base%/}/$rel"; fi atype="malware"; [[ "${fam,,}" == *ransom* || "${remote,,}" == *ransom* ]] && atype="ransomware" append_alert_tsv "$atype" "${fam:-Malware}" "$remote" samba done <<< "$out" return "$rc" } samba_scan_b64() { local b64="$1" user="$2" target server share rpath key cred smb_user smb_pass tmp cmd rc valid_user "$user" || { echo "Invalid user: $user" >&2; exit 67; } target="$(printf '%s' "$b64" | base64 -d)" || { echo "Base64 non valido" >&2; exit 64; } [[ -n "$target" ]] || { echo "Percorso vuoto" >&2; exit 64; } if [[ "$target" != smb://* ]]; then scan_path_b64 "$b64" return $? fi command -v smbclient >/dev/null 2>&1 || { echo "smbclient non trovato: installa smbclient per scansionare URL smb://" >&2; exit 127; } local rest="${target#smb://}" server="${rest%%/*}" rest="${rest#*/}" share="${rest%%/*}" if [[ "$rest" == "$share" ]]; then rpath=""; else rpath="${rest#*/}"; fi [[ -n "$server" && -n "$share" ]] || { echo "URL SMB non valido: $target" >&2; exit 64; } key="smb://$server/$share" cred="$(smb_cred_for "$key" "$user")" smb_user="${cred%%$'\t'*}"; smb_pass="${cred#*$'\t'}" tmp="$(mktemp -d /tmp/BastionGuard_samba_XXXXXX)" chmod 0755 "$tmp" || true cmd="$(mktemp)" { echo 'prompt OFF' echo 'recurse ON' echo "lcd $tmp" if [[ -n "$rpath" ]]; then printf 'cd %q\n' "$rpath"; fi echo 'mget *' } > "$cmd" echo "=== Scansione Samba: $target ===" echo "Temp dir: $tmp" echo "Share: //$server/$share" set +e if [[ -n "$smb_user" ]]; then smbclient "//$server/$share" -U "$smb_user%$smb_pass" -m SMB3 -E < "$cmd" 2>&1 else smbclient "//$server/$share" -N -m SMB3 -E < "$cmd" 2>&1 fi rc=$? set -e rm -f "$cmd" if [[ $rc -ne 0 ]]; then echo "ERRORE: impossibile copiare la share SMB. Verifica URL, credenziali in cred.conf e permessi. [code $rc]" rm -rf "$tmp" exit "$rc" fi echo "File copiati temporaneamente: $(find "$tmp" -type f 2>/dev/null | wc -l)" rc=0 run_samba_clam_scan_and_alert "$tmp" "$target" || rc=$? rm -rf "$tmp" echo "=== Scansione Samba completata ===" return "$rc" } allowed_restore_path() { local p="$1" case "$p" in /home|/home/*|/srv|/srv/*|/mnt|/mnt/*|/media|/media/*|/run/media|/run/media/*|/var/www|/var/www/*|/var/lib/bastionguard-webui/*|/tmp|/tmp/*) return 0 ;; *) return 1 ;; esac } quarantine_owner() { stat -c '%u:%g' /var/lib/bastionguard-webui 2>/dev/null || true } safe_quarantine_name() { local name="$1" [[ -n "$name" && "$name" != "." && "$name" != ".." && "$name" != */* ]] || { echo "Invalid quarantine item name" >&2; exit 64; } } quarantine_metadata_dir() { printf '%s\n' /var/lib/bastionguard-webui/quarantine/.metadata } write_quarantine_metadata() { local dest="$1" original="$2" user="$3" meta_dir meta name owner uid gid mode mtime meta_dir="$(quarantine_metadata_dir)" mkdir -p "$meta_dir" name="$(basename "$dest")" uid="$(stat -c '%u' "$dest" 2>/dev/null || echo 0)" gid="$(stat -c '%g' "$dest" 2>/dev/null || echo 0)" mode="$(stat -c '%a' "$dest" 2>/dev/null || echo 0640)" mtime="$(stat -c '%Y' "$dest" 2>/dev/null || date +%s)" meta="$meta_dir/$name.json" python3 - "$meta" "$original" "$dest" "$user" "$uid" "$gid" "$mode" "$mtime" <<'PYMETA' import json, sys, time meta, original, dest, user, uid, gid, mode, mtime = sys.argv[1:] data = { "original_path": original, "quarantine_path": dest, "quarantined_at": time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime()), "desktop_user": user if user != '-' else '', "uid": int(uid) if uid.isdigit() else 0, "gid": int(gid) if gid.isdigit() else 0, "mode": mode, "mtime": int(mtime) if str(mtime).isdigit() else int(time.time()), } with open(meta, 'w', encoding='utf-8') as fh: json.dump(data, fh, indent=2, ensure_ascii=False) fh.write('\n') PYMETA owner="$(quarantine_owner)" [[ -n "$owner" ]] && chown "$owner" "$meta_dir" "$meta" 2>/dev/null || true chmod 0750 "$meta_dir" 2>/dev/null || true chmod 0640 "$meta" 2>/dev/null || true } quarantine_path_b64() { local b64="$1" user="${2:-}" target real qdir dest base n owner uid gid mode target="$(printf '%s' "$b64" | base64 -d)" || { echo "Base64 non valido" >&2; exit 64; } [[ "$target" != smb://* ]] || { echo "Remote SMB path cannot be quarantined from WebPanel; remove or isolate it on the SMB server." >&2; exit 65; } [[ -e "$target" && -f "$target" ]] || { echo "File non trovato: $target" >&2; exit 66; } if command -v realpath >/dev/null 2>&1; then real="$(realpath -m -- "$target")"; else real="$target"; fi allowed_restore_path "$real" || { echo "Path non consentito: $real" >&2; exit 64; } uid="$(stat -c '%u' "$real" 2>/dev/null || echo 0)" gid="$(stat -c '%g' "$real" 2>/dev/null || echo 0)" mode="$(stat -c '%a' "$real" 2>/dev/null || echo 0640)" qdir=/var/lib/bastionguard-webui/quarantine mkdir -p "$qdir" owner="$(quarantine_owner)" [[ -n "$owner" ]] && chown "$owner" "$qdir" 2>/dev/null || true chmod 0750 "$qdir" || true base="$(basename "$real")"; dest="$qdir/$base"; n=0 while [[ -e "$dest" ]]; do n=$((n+1)); dest="$qdir/${base}.$n"; done mv -- "$real" "$dest" chown "$uid:$gid" "$dest" 2>/dev/null || true chmod "$mode" "$dest" 2>/dev/null || chmod 0640 "$dest" || true write_quarantine_metadata "$dest" "$real" "${user:-}" [[ -n "$owner" ]] && chown "$owner" "$dest" 2>/dev/null || true chmod 0640 "$dest" 2>/dev/null || true echo "quarantined: $dest" } restore_quarantine_b64() { local name_b64="$1" restore_b64="$2" user="${3:-}" name restore_dir qdir src meta dest original owner uid gid mode n base dir name="$(printf '%s' "$name_b64" | base64 -d)" || { echo "Invalid quarantine file encoding" >&2; exit 64; } restore_dir="$(printf '%s' "$restore_b64" | base64 -d)" || { echo "Invalid restore directory encoding" >&2; exit 64; } safe_quarantine_name "$name" qdir=/var/lib/bastionguard-webui/quarantine src="$qdir/$name" [[ -f "$src" ]] || { echo "Quarantine item not found: $name" >&2; exit 66; } meta="$(quarantine_metadata_dir)/$name.json" original="" uid=""; gid=""; mode="" if [[ -f "$meta" ]]; then original="$(python3 - <<'PYMETA' "$meta" import json,sys try: d=json.load(open(sys.argv[1], encoding='utf-8')) print(d.get('original_path','')) except Exception: print('') PYMETA )" uid="$(python3 - <<'PYMETA' "$meta" import json,sys try: print(json.load(open(sys.argv[1], encoding='utf-8')).get('uid','')) except Exception: print('') PYMETA )" gid="$(python3 - <<'PYMETA' "$meta" import json,sys try: print(json.load(open(sys.argv[1], encoding='utf-8')).get('gid','')) except Exception: print('') PYMETA )" mode="$(python3 - <<'PYMETA' "$meta" import json,sys try: print(json.load(open(sys.argv[1], encoding='utf-8')).get('mode','')) except Exception: print('') PYMETA )" fi if [[ -n "$original" ]]; then if command -v realpath >/dev/null 2>&1; then dest="$(realpath -m -- "$original")"; else dest="$original"; fi else [[ -n "$restore_dir" ]] || restore_dir=/tmp/bastionguard-restore if command -v realpath >/dev/null 2>&1; then restore_dir="$(realpath -m -- "$restore_dir")"; fi allowed_restore_path "$restore_dir" || { echo "Restore directory not allowed: $restore_dir" >&2; exit 64; } mkdir -p "$restore_dir" dest="$restore_dir/$name" fi allowed_restore_path "$dest" || { echo "Restore path not allowed: $dest" >&2; exit 64; } dir="$(dirname "$dest")" mkdir -p "$dir" base="$(basename "$dest")" if [[ -e "$dest" ]]; then n=0 while [[ -e "$dir/$base.restored.$n" ]]; do n=$((n+1)); done dest="$dir/$base.restored.$n" fi mv -- "$src" "$dest" if [[ "$uid" =~ ^[0-9]+$ && "$gid" =~ ^[0-9]+$ ]]; then chown "$uid:$gid" "$dest" 2>/dev/null || true elif [[ -n "$user" && "$user" != "-" ]] && valid_user "$user"; then chown "$(user_uid "$user"):$(user_gid "$user")" "$dest" 2>/dev/null || true fi [[ "$mode" =~ ^[0-7]{3,4}$ ]] && chmod "$mode" "$dest" 2>/dev/null || chmod 0640 "$dest" 2>/dev/null || true rm -f -- "$meta" 2>/dev/null || true owner="$(quarantine_owner)"; [[ -n "$owner" ]] && chown "$owner" "$qdir" "$(quarantine_metadata_dir)" 2>/dev/null || true echo "restored: $dest" } delete_quarantine_b64() { local name_b64="$1" user="${2:-}" name qdir src meta name="$(printf '%s' "$name_b64" | base64 -d)" || { echo "Invalid quarantine file encoding" >&2; exit 64; } safe_quarantine_name "$name" qdir=/var/lib/bastionguard-webui/quarantine src="$qdir/$name" meta="$(quarantine_metadata_dir)/$name.json" [[ -f "$src" ]] || { echo "Quarantine item not found: $name" >&2; exit 66; } rm -f -- "$src" "$meta" echo "deleted: $name" } inotify_pid_file() { printf '/run/bastionguard-webui/inotify-%s.pid\n' "$1"; } inotify_log_file() { local home; home="$(user_home "$1")"; printf '%s/.local/share/BastionGuard/logs/webui-inotify.log\n' "$home"; } inotify_saved_paths_file() { local home; home="$(user_home "$1")"; printf '%s/.local/share/BastionGuard/webui-inotify.paths\n' "$home"; } inotify_conf_file() { local home; home="$(user_home "$1")"; printf '%s/.config/BastionGuard/webui-inotify.conf\n' "$home"; } inotify_default_paths() { local user="$1" home p home="$(user_home "$user")" for p in "$home/Downloads" "$home/public_html" "$home/www" /home /var/www /var/www/html /srv/http /srv/www /tmp; do [[ -d "$p" ]] && printf '%s\n' "$p" done } inotify_path_allowed() { local p="$1" case "$p" in /home|/home/*|/srv|/srv/*|/mnt|/mnt/*|/media|/media/*|/run/media|/run/media/*|/var/www|/var/www/*|/tmp|/tmp/*|/root|/root/*|/etc|/etc/*|/var/lib|/var/lib/*|/var/log|/var/log/*) return 0 ;; *) return 1 ;; esac } ransomware_realtime_start() { local user="${1:-}" unit bin pidfile log pid [[ -z "$user" || "$user" == "-" ]] || valid_user "$user" || { echo "Invalid user: $user" >&2; exit 67; } for unit in BastionGuard-ransomware-realtime.service bastionguard-ransomware-realtime.service; do if systemctl list-unit-files "$unit" >/dev/null 2>&1 || systemctl status "$unit" >/dev/null 2>&1; then systemctl enable --now "$unit" 2>/dev/null || systemctl start "$unit" 2>/dev/null || true echo "ransomware realtime: systemd unit requested: $unit" return 0 fi done for bin in /usr/bin/BastionGuard-ransomware-realtime /usr/local/bin/BastionGuard-ransomware-realtime /bin/BastionGuard-ransomware-realtime; do [[ -x "$bin" ]] && break || true done if [[ ! -x "${bin:-}" ]]; then bin="$(command -v BastionGuard-ransomware-realtime 2>/dev/null || true)" fi if [[ -z "${bin:-}" || ! -x "$bin" ]]; then echo "ransomware realtime: BastionGuard-ransomware-realtime not installed; WebUI inotify will still run clamdscan and BastionGuard-ransomware-scanner when available" return 0 fi pidfile=/run/BastionGuard-ransomware-realtime-webui.pid log=/var/log/BastionGuard/antiransom_inotify.log mkdir -p /var/log/BastionGuard /run 2>/dev/null || true if [[ -f "$pidfile" ]]; then pid="$(cat "$pidfile" 2>/dev/null || true)" [[ -n "$pid" ]] && kill -0 "$pid" 2>/dev/null && { echo "ransomware realtime: already running pid=$pid"; return 0; } fi nohup "$bin" >> "$log" 2>&1 & echo "$!" > "$pidfile" chmod 0644 "$log" 2>/dev/null || true echo "ransomware realtime: started $bin pid=$(cat "$pidfile") log=$log" } inotify_system_unit_exists() { [[ -f /etc/systemd/system/bastionguard-webui-inotify.service ]] || systemctl list-unit-files bastionguard-webui-inotify.service >/dev/null 2>&1 } inotify_service_start() { local user="$1" b64="$2" valid_user "$user" || { echo "Invalid user: $user" >&2; exit 67; } inotify_save_paths "$user" "$b64" ransomware_realtime_start "$user" || true if inotify_system_unit_exists; then systemctl daemon-reload 2>/dev/null || true systemctl enable bastionguard-webui-inotify.service 2>/dev/null || true systemctl restart bastionguard-webui-inotify.service systemctl is-active --quiet bastionguard-webui-inotify.service && echo "root service active: bastionguard-webui-inotify.service" || true else echo "warning: root systemd unit not installed; falling back to direct root monitor. Re-run scripts/install-webui-helpers.sh." inotify_start_saved "$user" fi } inotify_service_stop() { local user="$1" valid_user "$user" || { echo "Invalid user: $user" >&2; exit 67; } if inotify_system_unit_exists; then systemctl stop bastionguard-webui-inotify.service 2>/dev/null || true fi inotify_stop "$user" } inotify_service_status() { local user="$1" active enabled valid_user "$user" || { echo "Invalid user: $user" >&2; exit 67; } if inotify_system_unit_exists; then active="$(systemctl is-active bastionguard-webui-inotify.service 2>/dev/null || true)" enabled="$(systemctl is-enabled bastionguard-webui-inotify.service 2>/dev/null || true)" printf 'systemd=%s enabled=%s ' "${active:-unknown}" "${enabled:-unknown}" else printf 'systemd=missing ' fi inotify_status "$user" } inotify_status() { local user="$1" pidfile oldpidfile pid log valid_user "$user" || { echo "Invalid user: $user" >&2; exit 67; } pidfile="$(inotify_pid_file "$user")"; oldpidfile="/run/bastionguard-webui-inotify-$user.pid"; log="$(inotify_log_file "$user")" if [[ -f "$pidfile" ]]; then pid="$(cat "$pidfile" 2>/dev/null || true)" if [[ -n "$pid" ]] && kill -0 "$pid" 2>/dev/null; then echo "running pid=$pid log=$log runner=/usr/local/libexec/bastionguard-webui-inotify-runner"; exit 0; fi fi if [[ -f "$oldpidfile" ]]; then pid="$(cat "$oldpidfile" 2>/dev/null || true)" if [[ -n "$pid" ]] && kill -0 "$pid" 2>/dev/null; then echo "legacy-runtime-script-running pid=$pid log=$log; stop/start to migrate"; exit 0; fi fi echo "stopped log=$log" } inotify_stop() { local user="$1" pidfile oldpidfile pid stopped=0 valid_user "$user" || { echo "Invalid user: $user" >&2; exit 67; } pidfile="$(inotify_pid_file "$user")" oldpidfile="/run/bastionguard-webui-inotify-$user.pid" for pf in "$pidfile" "$oldpidfile"; do [[ -f "$pf" ]] || continue pid="$(cat "$pf" 2>/dev/null || true)" [[ -n "$pid" ]] && kill "$pid" 2>/dev/null || true rm -f "$pf" stopped=1 done rm -f -- "/run/bastionguard-webui-inotify-$user.sh" "/run/bastionguard-webui-inotify-$user.paths" 2>/dev/null || true if [[ "$stopped" == "1" ]]; then echo "stopped"; else echo "not running"; fi } inotify_events() { local user="$1" lines="${2:-120}" log valid_user "$user" || { echo "Invalid user: $user" >&2; exit 67; } [[ "$lines" =~ ^[0-9]+$ ]] || lines=120 (( lines > 500 )) && lines=500 log="$(inotify_log_file "$user")" [[ -f "$log" ]] || { echo "No realtime inotify log yet: $log"; exit 0; } tail -n "$lines" "$log" } inotify_save_paths() { local user="$1" b64="$2" home uid gid pathsfile p real saved conf log valid_user "$user" || { echo "Invalid user: $user" >&2; exit 67; } home="$(user_home "$user")"; uid="$(user_uid "$user")"; gid="$(user_gid "$user")" pathsfile="$(mktemp)" printf '%s' "$b64" | base64 -d > "$pathsfile" || { echo "Base64 paths invalid" >&2; rm -f "$pathsfile"; exit 64; } while IFS= read -r p || [[ -n "$p" ]]; do [[ -z "$p" ]] && continue [[ -e "$p" ]] || { echo "Path not found: $p" >&2; rm -f "$pathsfile"; exit 66; } if command -v realpath >/dev/null 2>&1; then real="$(realpath -m -- "$p")"; else real="$p"; fi if ! inotify_path_allowed "$real"; then echo "Path not allowed for inotify monitor: $real" >&2; rm -f "$pathsfile"; exit 64; fi done < "$pathsfile" saved="$(inotify_saved_paths_file "$user")"; conf="$(inotify_conf_file "$user")"; log="$(inotify_log_file "$user")" install -d -o "$uid" -g "$gid" -m 0750 "$(dirname "$saved")" "$(dirname "$conf")" "$(dirname "$log")" 2>/dev/null || true install -o "$uid" -g "$gid" -m 0640 "$pathsfile" "$saved" 2>/dev/null || cp "$pathsfile" "$saved" 2>/dev/null || true printf 'enabled=1\npaths_file=%s\nlog_file=%s\n' "$saved" "$log" > "$conf.tmp" 2>/dev/null || true if [[ -f "$conf.tmp" ]]; then install -o "$uid" -g "$gid" -m 0640 "$conf.tmp" "$conf" 2>/dev/null || cp "$conf.tmp" "$conf" 2>/dev/null || true; rm -f "$conf.tmp"; fi rm -f "$pathsfile" echo "saved paths=$saved" } inotify_start() { local user="$1" b64="$2" home uid gid pidfile log pathsfile runner pid p real saved conf runtime_dir old_script valid_user "$user" || { echo "Invalid user: $user" >&2; exit 67; } command -v inotifywait >/dev/null 2>&1 || { echo "inotifywait non trovato: installa inotify-tools." >&2; exit 127; } runner=/usr/local/libexec/bastionguard-webui-inotify-runner [[ -x "$runner" ]] || { echo "Missing static realtime runner: $runner. Re-run scripts/install-webui-helpers.sh." >&2; exit 127; } home="$(user_home "$user")"; uid="$(user_uid "$user")"; gid="$(user_gid "$user")" pidfile="$(inotify_pid_file "$user")"; log="$(inotify_log_file "$user")" if [[ -f "$pidfile" ]]; then pid="$(cat "$pidfile" 2>/dev/null || true)"; [[ -n "$pid" ]] && kill -0 "$pid" 2>/dev/null && { echo "already running pid=$pid"; exit 0; }; fi install -d -o "$uid" -g "$gid" -m 0750 "$(dirname "$log")" runtime_dir=/run/bastionguard-webui install -d -o root -g root -m 0755 "$runtime_dir" pathsfile="$runtime_dir/inotify-$user.paths" old_script="/run/bastionguard-webui-inotify-$user.sh" rm -f -- "$old_script" 2>/dev/null || true printf '%s' "$b64" | base64 -d > "$pathsfile" || { echo "Base64 paths invalid" >&2; rm -f "$pathsfile"; exit 64; } chmod 0640 "$pathsfile" 2>/dev/null || true chown root:root "$pathsfile" 2>/dev/null || true while IFS= read -r p || [[ -n "$p" ]]; do [[ -z "$p" ]] && continue [[ -e "$p" ]] || { echo "Path not found: $p" >&2; rm -f "$pathsfile"; exit 66; } if command -v realpath >/dev/null 2>&1; then real="$(realpath -m -- "$p")"; else real="$p"; fi if ! inotify_path_allowed "$real"; then echo "Path not allowed for inotify monitor: $real" >&2; rm -f "$pathsfile"; exit 64; fi done < "$pathsfile" saved="$(inotify_saved_paths_file "$user")"; conf="$(inotify_conf_file "$user")" install -d -o "$uid" -g "$gid" -m 0750 "$(dirname "$saved")" "$(dirname "$conf")" 2>/dev/null || true install -o "$uid" -g "$gid" -m 0640 "$pathsfile" "$saved" 2>/dev/null || cp "$pathsfile" "$saved" 2>/dev/null || true printf 'enabled=1\npaths_file=%s\nlog_file=%s\nrunner=%s\n' "$saved" "$log" "$runner" > "$conf.tmp" 2>/dev/null || true if [[ -f "$conf.tmp" ]]; then install -o "$uid" -g "$gid" -m 0640 "$conf.tmp" "$conf" 2>/dev/null || cp "$conf.tmp" "$conf" 2>/dev/null || true; rm -f "$conf.tmp"; fi nohup "$runner" "$user" "$pathsfile" "$log" >> "$log" 2>&1 & pid=$! echo "$pid" > "$pidfile" chmod 0644 "$pidfile" 2>/dev/null || true chown "$uid:$gid" "$log" 2>/dev/null || true echo "started pid=$pid log=$log runner=$runner" } clamd_conf_path() { local candidates=(/etc/clamd/clamd.conf /etc/clamav/clamd.conf /etc/clamd.d/clamd.conf /etc/clamd.conf) local p d for p in "${candidates[@]}"; do [[ -f "$p" ]] && { printf '%s\n' "$p"; return 0; }; done for p in "${candidates[@]}"; do d="$(dirname "$p")"; [[ -d "$d" ]] && { printf '%s\n' "$p"; return 0; }; done printf '%s\n' /etc/clamav/clamd.conf } read_clamd_b64() { local p; p="$(clamd_conf_path)" [[ -e "$p" ]] || { echo "MISSING $p" >&2; exit 66; } [[ -f "$p" ]] || { echo "NOTFILE $p" >&2; exit 65; } [[ -r "$p" ]] || { echo "UNREADABLE $p" >&2; exit 13; } base64 -w0 "$p" } write_clamd_b64() { local b64="$1" p tmp content p="$(clamd_conf_path)" content="$(printf '%s' "$b64" | base64 -d)" || { echo "Invalid base64" >&2; exit 64; } tmp="$(mktemp)" printf '%s' "$content" > "$tmp" if [[ -f "$p" ]]; then cp -a "$p" "$p.bak.$(date +%s)" 2>/dev/null || true; fi write_system_file "$p" 0644 "$(cat "$tmp")" rm -f "$tmp" systemctl try-restart clamav-daemon.service 2>/dev/null || systemctl try-restart clamd.service 2>/dev/null || true systemctl try-restart clamav-clamonacc.service 2>/dev/null || systemctl try-restart clamonacc.service 2>/dev/null || true echo "written: $p" } install_secure_ca() { local helper=/usr/share/BastionGuard/data/scripts/install-ca-system.sh local user_or_ca="${1:-}" ca="/etc/BastionGuard/certs/BastionGuard-ca.crt.pem" if [[ -n "$user_or_ca" && "$user_or_ca" != /* ]] && valid_user "$user_or_ca"; then echo "BastionGuard WebUI: secure browsing / CEF bootstrap is disabled. Use the GTK desktop UI for CEF/browser proxy setup." >&2 exit 76 elif [[ -n "$user_or_ca" ]]; then ca="$user_or_ca" fi if [[ -x "$helper" || -f "$helper" ]]; then /usr/bin/env bash "$helper" "$ca" else echo "skipped: $helper not found" fi } install_thunderbird_extension() { local user="$1" script=/usr/share/BastionGuard/data/extension/bastionguard-tb-extension/install-tb-extension.sh valid_user "$user" || { echo "Invalid user: $user" >&2; exit 67; } if [[ -f "$script" ]]; then run_as_user "$user" /usr/bin/env bash "$script" else echo "skipped: $script not found" fi } apply_web_config() { local http="$1" https="$2" os="${3:-auto}" distro websrv tmpfile target_conf site_avail site_enable valid_port "$http" || { echo "Invalid HTTP port" >&2; exit 64; } valid_port "$https" || { echo "Invalid HTTPS port" >&2; exit 64; } if [[ -z "$os" || "$os" == auto ]]; then distro="$(detect_distro_src)" else distro="${os,,}" fi websrv="$(detect_webserver_src)" echo "[detect_distro] Distribuzione rilevata: $distro" echo "[detect_webserver] Server rilevato: $websrv" if [[ "$websrv" != nginx && ! -x /usr/sbin/nginx && ! -x /usr/bin/nginx ]] && ! command -v nginx >/dev/null 2>&1; then echo "Nessun server NGINX rilevato sul sistema." >&2 exit 69 fi echo "[BastionGuard] Applicazione configurazione NGINX..." write_system_file /etc/BastionGuard/webports.conf 0644 "$http $https " tmpfile="$(mktemp /tmp/BastionGuard_nginx.XXXXXX.conf)" trap 'rm -f "$tmpfile"' RETURN make_bastionguard_nginx_conf "$http" "$https" "/srv/http/webui" > "$tmpfile" if [[ "$distro" == arch || "$distro" == manjaro || "$distro" == endeavour ]]; then site_avail=/etc/nginx/sites-available/default site_enable=/etc/nginx/sites-enabled/default echo "[BastionGuard] Installo configurazione Arch-based..." mkdir -p /etc/nginx/sites-available /etc/nginx/sites-enabled install -m 0644 "$tmpfile" "$site_avail" ln -sf "$site_avail" "$site_enable" target_conf="$site_avail" else echo "[BastionGuard] Verifica configurazione esistente..." if [[ -f /etc/nginx/nginx.conf ]] && ! grep -q 'BastionGuard Local Web Server' /etc/nginx/nginx.conf 2>/dev/null; then cp -a /etc/nginx/nginx.conf "/etc/nginx/nginx.conf.bak.$(date +%s)" 2>/dev/null || true echo "[BastionGuard] Backup nginx.conf creato; installo il vhost BastionGuard separato." fi if [[ -d /etc/nginx/conf.d ]]; then target_conf=/etc/nginx/conf.d/BastionGuard.conf else mkdir -p /etc/nginx/sites-enabled target_conf=/etc/nginx/sites-enabled/BastionGuard.conf fi install -m 0644 "$tmpfile" "$target_conf" fi if nginx -t; then systemctl restart nginx echo "Configurazione NGINX aggiornata e servizio riavviato: $target_conf" else echo "Errore: nginx -t non riuscito; configurazione scritta in $target_conf" >&2 exit 70 fi } restart_user_service() { local user="$1" unit="$2" valid_user "$user" || { echo "Invalid user: $user" >&2; exit 67; } valid_unit_name "$unit" || { echo "Invalid unit: $unit" >&2; exit 64; } if webui_disabled_unit "$unit"; then echo "BastionGuard WebUI: blocked restart for $unit. the unit is disabled in the server WebUI." >&2 exit 76 fi guard_proxy_desktop_conflict "$user" "$unit" restart run_as_user "$user" systemctl --user restart "$unit" || true } journal_logs() { local mode="$1" user="$2" lines="$3" unit out shift 3 [[ "$mode" == "system" || "$mode" == "user" ]] || { echo "Invalid journal mode" >&2; exit 64; } [[ "$lines" =~ ^[0-9]+$ ]] || { echo "Invalid line count" >&2; exit 64; } (( lines >= 1 )) || lines=1 (( lines <= 2000 )) || lines=2000 if [[ "$mode" == "user" ]]; then valid_user "$user" || { echo "Invalid user: $user" >&2; exit 67; } fi [[ $# -ge 1 ]] || { echo "No journal unit specified" >&2; exit 64; } command -v journalctl >/dev/null 2>&1 || { echo "journalctl not found" >&2; exit 127; } for unit in "$@"; do valid_unit_name "$unit" || { echo "Invalid unit: $unit" >&2; continue; } printf '### %s\n' "$unit" if [[ "$mode" == "user" ]]; then out="$(run_as_user "$user" journalctl --user -u "$unit" -n "$lines" --no-pager --output=short-iso 2>&1 || true)" else out="$(journalctl -u "$unit" -n "$lines" --no-pager --output=short-iso 2>&1 || true)" fi if [[ -n "$out" ]]; then printf '%s\n' "$out" else printf '(no log entries)\n' fi printf '\n' done } [[ $# -ge 1 ]] || usage cmd="$1"; shift case "$cmd" in ensure-configs) [[ $# -eq 1 ]] || usage; ensure_configs "$1" ;; first-run-config) [[ $# -eq 2 ]] || usage; first_run_config "$1" "$2" ;; list-configs) [[ $# -eq 1 ]] || usage; list_configs "$1" ;; read-config-b64) [[ $# -ge 1 && $# -le 2 ]] || usage; read_config_b64 "$@" ;; write-config) [[ $# -ge 2 && $# -le 3 ]] || usage; write_config "$@" ;; scan-path-b64) [[ $# -eq 1 ]] || usage; scan_path_b64 "$1" ;; samba-scan-b64) [[ $# -eq 2 ]] || usage; samba_scan_b64 "$1" "$2" ;; quarantine-path-b64) [[ $# -eq 2 ]] || usage; quarantine_path_b64 "$1" "$2" ;; restore-quarantine-b64) [[ $# -eq 3 ]] || usage; restore_quarantine_b64 "$1" "$2" "$3" ;; delete-quarantine-b64) [[ $# -eq 2 ]] || usage; delete_quarantine_b64 "$1" "$2" ;; inotify-start) [[ $# -eq 2 ]] || usage; inotify_start "$1" "$2" ;; inotify-save) [[ $# -eq 2 ]] || usage; inotify_save_paths "$1" "$2" ;; inotify-service-start) [[ $# -eq 2 ]] || usage; inotify_service_start "$1" "$2" ;; inotify-service-stop) [[ $# -eq 1 ]] || usage; inotify_service_stop "$1" ;; inotify-service-status) [[ $# -eq 1 ]] || usage; inotify_service_status "$1" ;; inotify-start-saved) [[ $# -eq 1 ]] || usage; inotify_start_saved "$1" ;; inotify-stop) [[ $# -eq 1 ]] || usage; inotify_stop "$1" ;; inotify-status) [[ $# -eq 1 ]] || usage; inotify_status "$1" ;; inotify-events) [[ $# -eq 2 ]] || usage; inotify_events "$1" "$2" ;; ransomware-realtime-start) [[ $# -eq 1 ]] || usage; ransomware_realtime_start "$1" ;; import-ransomware-events) [[ $# -eq 0 ]] || usage; import_ransomware_events ;; backup-action) echo "BastionGuard WebUI: backup is disabled on the server WebUI. Use the GTK desktop backup client." >&2; exit 76 ;; journal) [[ $# -ge 4 ]] || usage; journal_logs "$@" ;; service-action) [[ $# -eq 4 ]] || usage; service_action "$@" ;; update-yara) [[ $# -eq 1 ]] || usage; update_yara "$1" ;; update-sanesecurity) [[ $# -eq 1 ]] || usage; update_sanesecurity "$1" ;; update-phishing) [[ $# -eq 1 ]] || usage; update_phishing "$1" ;; update-banks) [[ $# -ge 1 && $# -le 2 ]] || usage; update_banks "$@" ;; phish-auto-update) [[ $# -eq 1 ]] || usage; phish_auto_update "$1" ;; read-version) [[ $# -eq 0 ]] || usage; read_version ;; desktop-mode) [[ $# -eq 1 ]] || usage; desktop_session_summary "$1" ;; read-data-file-b64) [[ $# -eq 2 ]] || usage; read_data_file_b64 "$@" ;; write-system-file-b64) [[ $# -eq 2 ]] || usage; write_system_file_b64 "$@" ;; read-clamd-b64) [[ $# -eq 0 ]] || usage; read_clamd_b64 ;; write-clamd-b64) [[ $# -eq 1 ]] || usage; write_clamd_b64 "$1" ;; install-secure-ca) [[ $# -ge 0 && $# -le 1 ]] || usage; install_secure_ca "${1:-}" ;; install-thunderbird-extension) [[ $# -eq 1 ]] || usage; install_thunderbird_extension "$1" ;; apply-web-config) [[ $# -ge 2 && $# -le 3 ]] || usage; apply_web_config "$@" ;; restart-user-service) [[ $# -eq 2 ]] || usage; restart_user_service "$1" "$2" ;; wizard) [[ $# -ge 3 ]] || usage; wizard "$@" ;; *) usage ;; esac