183 lines
No EOL
9 KiB
Bash
Executable file
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 |