BastionGuard/webui/scripts/bastionguard-webui-admin
specialworld83 f0f913a209 Release 2.0
2026-07-15 10:52:22 +02:00

2472 lines
109 KiB
Bash
Executable file

#!/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 <<EOF
Uso:
$0 ensure-configs <utente>
$0 first-run-config <utente> <base64-json>
$0 list-configs <utente>
$0 read-config-b64 <chiave> [utente]
$0 write-config <chiave> <base64-content> [utente]
$0 scan-path-b64 <base64-path>
$0 samba-scan-b64 <base64-path> <utente>
$0 quarantine-path-b64 <base64-path> <utente|->
$0 restore-quarantine-b64 <base64-name> <base64-restore-dir> <utente|->
$0 delete-quarantine-b64 <base64-name> <utente|->
$0 inotify-start <utente> <base64-paths-newline-separated>
$0 inotify-save <utente> <base64-paths-newline-separated>
$0 inotify-service-start <utente> <base64-paths-newline-separated>
$0 inotify-service-stop <utente>
$0 inotify-service-status <utente>
$0 inotify-start-saved <utente>
$0 inotify-stop <utente>
$0 inotify-status <utente>
$0 inotify-events <utente> <lines>
$0 ransomware-realtime-start <utente>
$0 import-ransomware-events
$0 journal <system|user> <utente|-> <lines> <unit...>
$0 service-action <system|user> <utente|-> <start|stop|restart|enable|disable> <unit>
$0 update-yara <utente>
$0 update-sanesecurity <utente>
$0 update-phishing <utente>
$0 update-banks <utente> [source-url]
$0 phish-auto-update <enable|disable>
$0 read-version
$0 read-data-file-b64 <banks> <utente>
$0 write-system-file-b64 <key> <base64-content>
$0 desktop-mode <utente>
$0 apply-web-config <http_port> <https_port> [auto|distro]
$0 wizard <utente> <http_port> <https_port> [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 <<EOF
BastionGuard WebUI safe-desktop guard: blocked $action for $unit.
Reason: graphical desktop detected for user '$user' ($summary).
The GTK desktop UI must own proxy/browser configuration.
CEF/PAC, Webcam/Privacy and USB are always disabled in the server WebUI and must be managed by the GTK/desktop UI or locally on the host.
EOF
exit 75
fi
echo "BastionGuard WebUI safe-desktop guard: no GUI detected for '$user'; WebUI may manage $unit. ($summary)"
}
make_bastionguard_nginx_conf() {
local http="$1" https="$2" root_dir="${3:-/srv/http/webui}"
cat <<EOFNGINX
# BastionGuard Local Web Server
server {
listen ${http};
listen ${https} ssl;
server_name localhost 127.0.0.1;
root ${root_dir};
index index.php index.html;
location / {
try_files \$uri \$uri/ /index.php?\$query_string;
}
location ~ \.php$ {
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME \$document_root\$fastcgi_script_name;
fastcgi_pass unix:/run/php-fpm/php-fpm.sock;
}
}
EOFNGINX
}
config_path_for_key() {
local key="$1" user="${2:-}" home rel path scope
[[ "$key" =~ ^[a-z_]+$ ]] || { echo "Chiave non valida" >&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 <<EOF
{
"name": "com.BastionGuard.native_host",
"description": "BastionGuard native messaging host",
"path": "$bin_path",
"type": "stdio",
"allowed_origins": ["chrome-extension://$extension_id/"]
}
EOF
)
else
chrome_json=$(cat <<EOF
{
"name": "com.BastionGuard.native_host",
"description": "BastionGuard native messaging host",
"path": "$bin_path",
"type": "stdio",
"allowed_origins": []
}
EOF
)
fi
firefox_json=$(cat <<EOF
{
"name": "com.BastionGuard.native_host",
"description": "BastionGuard native messaging host",
"path": "$bin_path",
"type": "stdio",
"allowed_extensions": ["BastionGuard@local"]
}
EOF
)
echo "[BastionGuard] Installing native messaging host manifests"
if [[ -n "$user" ]] && valid_user "$user"; then
write_user_file "$user" .config/google-chrome/NativeMessagingHosts/com.BastionGuard.native_host.json 0644 "$chrome_json" || true
write_user_file "$user" .config/chromium/NativeMessagingHosts/com.BastionGuard.native_host.json 0644 "$chrome_json" || true
write_user_file "$user" .config/BraveSoftware/Brave-Browser/NativeMessagingHosts/com.BastionGuard.native_host.json 0644 "$chrome_json" || true
write_user_file "$user" .config/microsoft-edge/NativeMessagingHosts/com.BastionGuard.native_host.json 0644 "$chrome_json" || true
write_user_file "$user" .mozilla/native-messaging-hosts/com.BastionGuard.native_host.json 0644 "$firefox_json" || true
write_user_file "$user" .librewolf/native-messaging-hosts/com.BastionGuard.native_host.json 0644 "$firefox_json" || true
fi
if can_write_dir /etc; then
write_system_file /etc/opt/chrome/native-messaging-hosts/com.BastionGuard.native_host.json 0644 "$chrome_json" || true
write_system_file /etc/chromium/native-messaging-hosts/com.BastionGuard.native_host.json 0644 "$chrome_json" || true
else
echo "skipped: system native host manifests under /etc (read-only or not writable)"
fi
if can_write_dir /usr/lib; then
write_system_file /usr/lib/mozilla/native-messaging-hosts/com.BastionGuard.native_host.json 0644 "$firefox_json" || true
else
echo "skipped: Firefox system native host manifest under /usr/lib (read-only or not writable)"
fi
}
setup_phishing_blacklist() {
local user="$1" src=/usr/share/BastionGuard/data/phishing-blacklist.txt
if [[ -f "$src" ]]; then
write_user_file "$user" .local/share/BastionGuard/phishing/blacklist.txt 0644 "$(cat "$src")" || true
echo "phishing blacklist copied to ~/.local/share/BastionGuard/phishing/blacklist.txt"
else
echo "skipped: $src (not found)"
fi
}
setup_useragent() {
local script=/usr/share/BastionGuard/data/scripts/enable-user-agents.sh
[[ -x "$script" || -f "$script" ]] && /bin/sh "$script" || echo "Script enable-user-agents.sh non presente, salto."
}
setup_banks() {
local user="$1"
update_banks "$user" || true
}
setup_services() {
local user="$1" u gui_summary
local system_units=(BastionGuard-phishing-scanner.service BastionGuard-phishing-updater.service BastionGuard-phishing-updater.timer Bastionguard-privhelper.service bastionguard-sanesecurity.service bastionguard-sanesecurity.timer BastionGuard-ransomware-realtime.service clamav-daemon.service clamav-freshclam.service clamav-clamonacc.service)
local user_units=(BastionGuard-ransomware-alert.service BastionGuard-ransomware-realtime-alert.service BastionGuard-ransomware-scanner.service BastionGuard-useragent.service BastionGuard-mailproxy.service)
for u in "${system_units[@]}"; do systemctl list-unit-files "$u" >/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: created<TAB>tags<TAB>comments<TAB>type. 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