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

183 lines
No EOL
9 KiB
Bash
Executable file

#!/usr/bin/env bash
set -u
if [[ $# -ne 3 ]]; then echo "usage: bastionguard-webui-inotify-runner <user> <pathsfile> <log>" >&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