#!/usr/bin/env bash set -u if [[ $# -ne 3 ]]; then echo "usage: bastionguard-webui-inotify-runner " >&2; exit 64; fi user="$1"; pathsfile="$2"; log="$3" case "$user" in ""|*[!A-Za-z0-9_-]*) echo "invalid user" >&2; exit 64 ;; esac [[ -r "$pathsfile" ]] || { echo "pathsfile not readable: $pathsfile" >&2; exit 66; } alert_file=/var/lib/bastionguard-webui/alerts.tsv runtime_dir=/run/bastionguard-webui/inotify-runtime mkdir -p /var/lib/bastionguard-webui "$(dirname "$log")" "$runtime_dir" 2>/dev/null || true ts_now(){ date -Is; } append_alert() { local type="$1" family="$2" path="$3" source="$4" ts id bpath bfamily owner 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" >> "$alert_file" 2>/dev/null || true owner="$(stat -c '%u:%g' /var/lib/bastionguard-webui 2>/dev/null || true)" [[ -n "$owner" ]] && chown "$owner" "$alert_file" 2>/dev/null || true chmod 0640 "$alert_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 } is_build_artifact() { case "$1" in *.o|*.obj|*.a|*.lo|*.la|*.pyc|*.pyo|*.class|*.d|*.gch|*.pch|*.mod|*.swp|*.swo|*.rlib|*.rmeta|*.tmp|*.bc|*.ll) return 0 ;; esac return 1 } is_noisy_dir() { case "$1" in */.git/*|*/.cache/*|*/.local/*|*/build/*|*/CMakeFiles/*|*/target/*|*/node_modules/*|*/dist/*|*/out/*|*/bin/*|*/obj/*|*/.gradle/*|*/.m2/*|*/.cargo/*|*/__pycache__/*|*/.pytest_cache/*|*/.next/*|*/.nuxt/*|*/.svelte-kit/*|*/.tox/*|*/.venv/*|*/venv/*|*/Debug/*|*/Release/*) return 0 ;; esac return 1 } 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 } recent_key() { printf '%s' "$1|$(stat -c '%s:%Y' "$1" 2>/dev/null || echo 0:0)" | sha256sum | awk '{print $1}'; } recent_seen() { local f="$1" key stamp now key="$(recent_key "$f")"; stamp="$runtime_dir/$key"; now="$(date +%s)" find "$runtime_dir" -type f -mmin +5 -delete 2>/dev/null || true [[ -f "$stamp" ]] && return 0 : > "$stamp" 2>/dev/null || true return 1 } cloud_key() { local home conf home="$(getent passwd "$user" | cut -d: -f6)"; conf="$home/.config/BastionGuard/cloud.conf" [[ -r "$conf" ]] || return 0 awk -F= '/^[[:space:]]*malware_bazaar_api_key[[:space:]]*=/{gsub(/^[ \t]+|[ \t]+$/, "", $2); print $2; exit}' "$conf" 2>/dev/null || true } cloud_check_virustotal() { local f="$1" key sha out malicious suspicious command -v curl >/dev/null 2>&1 || return 0 command -v sha256sum >/dev/null 2>&1 || return 0 key="$(cloud_key)" [[ -n "$key" ]] || return 0 sha="$(sha256sum -- "$f" 2>/dev/null | awk '{print $1}')" [[ -n "$sha" ]] || return 0 out="$(curl -fsS --max-time 12 -H "x-apikey: $key" "https://www.virustotal.com/api/v3/files/$sha" 2>/dev/null || true)" [[ -n "$out" ]] || return 0 malicious="$(printf '%s' "$out" | grep -o '"malicious"[[:space:]]*:[[:space:]]*[0-9]\+' | head -1 | grep -o '[0-9]\+$' || true)" suspicious="$(printf '%s' "$out" | grep -o '"suspicious"[[:space:]]*:[[:space:]]*[0-9]\+' | head -1 | grep -o '[0-9]\+$' || true)" malicious="${malicious:-0}"; suspicious="${suspicious:-0}" if [[ "$malicious" =~ ^[0-9]+$ && "$suspicious" =~ ^[0-9]+$ ]] && (( malicious > 0 || suspicious > 0 )); then echo "[$(ts_now)] CLOUD VirusTotal match malicious=$malicious suspicious=$suspicious $f" >> "$log" append_alert malware "VirusTotal.Match" "$f" inotify-cloud fi } import_native_ransomware_realtime_events() { local native=/var/log/BastionGuard/antiransom_inotify.log state line key payload path fam rest [[ -r "$native" ]] || return 0 state="$runtime_dir/native-ransomware.seen" touch "$state" 2>/dev/null || true tail -n 300 "$native" 2>/dev/null | 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 key="$(printf '%s' "$line" | sha256sum | awk '{print $1}')" grep -qx "$key" "$state" 2>/dev/null && continue printf '%s ' "$key" >> "$state" 2>/dev/null || true append_alert ransomware "${fam:-BastionGuard.RansomwareRealtime}" "$path" ransomware-realtime echo "[$(ts_now)] imported BastionGuard-ransomware-realtime alert ${fam:-BastionGuard.RansomwareRealtime} $path" >> "$log" done # Keep cache small. tail -n 1000 "$state" > "$state.tmp" 2>/dev/null && mv "$state.tmp" "$state" 2>/dev/null || true } scan_ransomware_file() { local f="$1" scanner out rc fam scanner="$(ransomware_scanner_bin)" [[ -n "$scanner" ]] || return 0 out="$($scanner --scan "$f" 2>&1)"; rc=$? printf '%s\n' "$out" >> "$log" 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="$(printf '%s\n' "$out" | sed -n 's/.*\(YARA[^ ]*\).*/\1/p' | head -1)" append_alert ransomware "${fam:-BastionGuard.YARA.Ransomware}" "$f" inotify fi fi return 0 } scan_file() { local f="$1" event="${2:-EVENT}" out rc fam atype size [[ -f "$f" ]] || return 0 case "$f" in *.part|*.crdownload) return 0 ;; esac is_noisy_dir "$f" && return 0 is_build_artifact "$f" && return 0 if webui_trusted_script_path "$f"; then echo "[$(ts_now)] skip trusted BastionGuard WebUI helper script $f" >> "$log" return 0 fi # Source ScanPage.cpp waits for close/move/attrib and then de-duplicates the same file. recent_seen "$f" && return 0 echo "[$(ts_now)] $event scan $f" >> "$log" # Run ransomware checks before ClamAV/cloud so ransomware events are imported promptly. scan_ransomware_file "$f" || true import_native_ransomware_realtime_events || true size="$(stat -c '%s' "$f" 2>/dev/null || echo 0)" if [[ "$size" =~ ^[0-9]+$ ]] && (( size < 64 )); then echo "[$(ts_now)] skip ClamAV/cloud tiny file size=$size $f" >> "$log" return 0 fi if command -v clamdscan >/dev/null 2>&1; then out="$(clamdscan --fdpass --no-summary --infected "$f" 2>&1)"; rc=$?; elif command -v clamscan >/dev/null 2>&1; then out="$(clamscan --infected "$f" 2>&1)"; rc=$?; else echo "[$(ts_now)] WARN clamdscan/clamscan not found; ransomware checks still run" >> "$log"; out=""; rc=0; fi [[ -n "$out" ]] && printf '%s\n' "$out" >> "$log" if printf '%s\n' "$out" | grep -q ' FOUND'; then fam="$(printf '%s\n' "$out" | sed -n 's/^.*: \(.*\) FOUND.*/\1/p' | head -1)" atype="malware"; [[ "${fam,,}" == *ransom* || "${f,,}" == *ransom* ]] && atype="ransomware" append_alert "$atype" "${fam:-Malware}" "$f" inotify elif [[ "$rc" == "0" ]]; then cloud_check_virustotal "$f" || true fi return 0 } scan_directory_once() { local d="$1" [[ -d "$d" ]] || return 0 echo "[$(ts_now)] new directory detected; scanning regular files under $d" >> "$log" find "$d" -type f -maxdepth 3 -print0 2>/dev/null | while IFS= read -r -d '' f; do scan_file "$f" "DIR"; done } export -f ts_now append_alert ransomware_scanner_bin is_build_artifact is_noisy_dir webui_trusted_script_path recent_key recent_seen cloud_key cloud_check_virustotal import_native_ransomware_realtime_events scan_ransomware_file scan_file scan_directory_once while true; do import_native_ransomware_realtime_events || true # Restart periodically so newly-created subdirectories are picked up by the recursive watch set. timeout 30s inotifywait -m -r -e create,moved_to,close_write,attrib --format '%e|%w%f' --fromfile "$pathsfile" 2>>"$log" | while IFS='|' read -r event file; do [[ -n "$file" ]] || continue if [[ "$event" == *ISDIR* ]]; then case "$event" in *CREATE*|*MOVED_TO*) scan_directory_once "$file" ;; esac continue fi case "$event" in *CLOSE_WRITE*|*MOVED_TO*|*ATTRIB*) scan_file "$file" "$event" ;; esac done echo "[$(ts_now)] inotifywait refreshed" >> "$log" sleep 1 done