#!/usr/bin/env bash set -Eeuo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" ENV_FILE="${ENV_FILE:-$SCRIPT_DIR/.env}" SRC_DIR="$SCRIPT_DIR/src" log(){ printf '[malware-watch] %s\n' "$*"; } die(){ printf '[malware-watch] ERROR: %s\n' "$*" >&2; exit 1; } if [[ ${EUID:-$(id -u)} -ne 0 ]]; then exec sudo -E bash "$0" "$@" fi [[ -r "$ENV_FILE" ]] || die "Missing $ENV_FILE" set -a . "$ENV_FILE" set +a : "${DISCORD_WEBHOOK:?DISCORD_WEBHOOK is required in .env}" : "${ABUSECH_AUTH_KEY:?ABUSECH_AUTH_KEY is required in .env}" [[ "$DISCORD_WEBHOOK" != "디스코드웹훅URL" ]] || die "Replace DISCORD_WEBHOOK placeholder in .env" [[ "$ABUSECH_AUTH_KEY" != "abuse.ch api 키" ]] || die "Replace ABUSECH_AUTH_KEY placeholder in .env" CPU_THRESHOLD="${CPU_THRESHOLD:-150}" ALERT_SCORE="${ALERT_SCORE:-60}" CRITICAL_SCORE="${CRITICAL_SCORE:-100}" COOLDOWN_SECONDS="${COOLDOWN_SECONDS:-1800}" CLAMAV_VERSION="${CLAMAV_VERSION:-1.4.6}" CLAMAV_URL="${CLAMAV_URL:-}" INSTALL_CLAMAV="${INSTALL_CLAMAV:-1}" RUN_FRESHCLAM_NOW="${RUN_FRESHCLAM_NOW:-0}" SEND_INSTALL_TEST="${SEND_INSTALL_TEST:-0}" case "$DISCORD_WEBHOOK$ABUSECH_AUTH_KEY" in *"'"*) die "Secrets containing a single quote are not supported by this installer" ;; esac command -v systemctl >/dev/null || die "systemd is required" command -v apt-get >/dev/null || die "This installer currently supports Debian/Ubuntu (apt)" for f in malware-watch.py malware-quarantine.py malware-feed-update.py clamav-watch-scan.sh block-malware-iocs; do [[ -f "$SRC_DIR/$f" ]] || die "Missing $SRC_DIR/$f" done log "Installing base dependencies" apt-get update -y DEBIAN_FRONTEND=noninteractive apt-get install -y \ ca-certificates curl python3 iproute2 procps util-linux iptables coreutils passwd install -d -m 700 /etc/malware-watch /var/lib/malware-watch /var/lib/malware-watch/feeds install -d -m 700 /root/security-evidence/malware-watch /root/security-evidence/quarantine BACKUP="/root/security-evidence/malware-watch/install-backup-$(date +%Y%m%d-%H%M%S)" install -d -m 700 "$BACKUP" for f in \ /etc/malware-watch/config \ /usr/local/sbin/malware-watch.py \ /usr/local/sbin/malware-quarantine.py \ /usr/local/sbin/malware-feed-update.py \ /usr/local/sbin/clamav-watch-scan.sh \ /usr/local/sbin/block-malware-iocs; do [[ -e "$f" ]] && cp -a "$f" "$BACKUP/" || true done umask 077 { printf "CPU_THRESHOLD='%s'\n" "$CPU_THRESHOLD" printf "ALERT_SCORE='%s'\n" "$ALERT_SCORE" printf "CRITICAL_SCORE='%s'\n" "$CRITICAL_SCORE" printf "COOLDOWN_SECONDS='%s'\n" "$COOLDOWN_SECONDS" printf "ABUSECH_AUTH_KEY='%s'\n" "$ABUSECH_AUTH_KEY" printf "DISCORD_WEBHOOK='%s'\n" "$DISCORD_WEBHOOK" } > /etc/malware-watch/config chmod 600 /etc/malware-watch/config for f in malware-watch.py malware-quarantine.py malware-feed-update.py clamav-watch-scan.sh block-malware-iocs; do install -o root -g root -m 700 "$SRC_DIR/$f" "/usr/local/sbin/$f" done if [[ "$INSTALL_CLAMAV" == "1" ]]; then arch="$(uname -m)" case "$arch" in aarch64|arm64) clam_arch="aarch64" ;; x86_64|amd64) clam_arch="x86_64" ;; *) die "Unsupported ClamAV architecture: $arch" ;; esac if ! /usr/local/bin/clamscan --version 2>/dev/null | grep -q "ClamAV $CLAMAV_VERSION"; then log "Installing official ClamAV $CLAMAV_VERSION for $clam_arch" deb="$(mktemp /tmp/clamav.XXXXXX.deb)" url="${CLAMAV_URL:-https://www.clamav.net/downloads/production/clamav-${CLAMAV_VERSION}.linux.${clam_arch}.deb}" curl -fL --retry 3 --retry-delay 2 -o "$deb" "$url" systemctl disable --now clamav-freshclam 2>/dev/null || true DEBIAN_FRONTEND=noninteractive apt-get remove -y \ clamav clamav-freshclam clamav-base libclamav9 libclamav11 2>/dev/null || true DEBIAN_FRONTEND=noninteractive apt-get install -y "$deb" rm -f "$deb" fi [[ -x /usr/local/bin/clamscan ]] || die "Official ClamAV clamscan not found after installation" [[ -f /usr/local/etc/freshclam.conf.sample ]] || die "freshclam.conf.sample not found" if ! getent group clamav >/dev/null 2>&1; then groupadd --system clamav fi if ! id -u clamav >/dev/null 2>&1; then useradd --system --gid clamav --home-dir /var/lib/clamav --shell /usr/sbin/nologin clamav fi install -d -o clamav -g clamav -m 755 /var/lib/clamav cp /usr/local/etc/freshclam.conf.sample /usr/local/etc/freshclam.conf sed -i 's/^Example/#Example/' /usr/local/etc/freshclam.conf cat >> /usr/local/etc/freshclam.conf <<'EOF_FRESH' DatabaseDirectory /var/lib/clamav DatabaseOwner clamav Checks 12 EOF_FRESH chmod 644 /usr/local/etc/freshclam.conf fi cat > /etc/systemd/system/malware-watch.service <<'EOF_UNIT' [Unit] Description=Malware Watch realtime detector After=network-online.target docker.service Wants=network-online.target [Service] Type=oneshot ExecStart=/usr/local/sbin/malware-watch.py User=root Nice=10 IOSchedulingClass=best-effort IOSchedulingPriority=7 EOF_UNIT cat > /etc/systemd/system/malware-watch.timer <<'EOF_UNIT' [Unit] Description=Run Malware Watch every minute [Timer] OnBootSec=2min OnUnitActiveSec=1min AccuracySec=5s Persistent=true [Install] WantedBy=timers.target EOF_UNIT cat > /etc/systemd/system/malware-feed-update.service <<'EOF_UNIT' [Unit] Description=Malware Watch abuse.ch feed updater After=network-online.target Wants=network-online.target [Service] Type=oneshot EnvironmentFile=/etc/malware-watch/config ExecStart=/usr/local/sbin/malware-feed-update.py User=root EOF_UNIT cat > /etc/systemd/system/malware-feed-update.timer <<'EOF_UNIT' [Unit] Description=Update malware threat feeds every 6 hours [Timer] OnBootSec=5min OnUnitActiveSec=6h RandomizedDelaySec=5min Persistent=true [Install] WantedBy=timers.target EOF_UNIT cat > /etc/systemd/system/block-malware-iocs.service <<'EOF_UNIT' [Unit] Description=Restore malware IOC firewall blocks After=network-online.target docker.service Wants=network-online.target [Service] Type=oneshot ExecStart=/usr/local/sbin/block-malware-iocs RemainAfterExit=yes [Install] WantedBy=multi-user.target EOF_UNIT if [[ "$INSTALL_CLAMAV" == "1" ]]; then cat > /etc/systemd/system/clamav-freshclam-local.service < /etc/systemd/system/clamav-freshclam-local.timer <<'EOF_UNIT' [Unit] Description=Update ClamAV signatures every 2 hours [Timer] OnBootSec=15min OnUnitActiveSec=2h RandomizedDelaySec=5min Persistent=true [Install] WantedBy=timers.target EOF_UNIT cat > /etc/systemd/system/clamav-risk-scan.service <<'EOF_UNIT' [Unit] Description=ClamAV high-risk path scan After=local-fs.target [Service] Type=oneshot ExecStart=/usr/local/sbin/clamav-watch-scan.sh quick User=root Nice=15 IOSchedulingClass=idle SuccessExitStatus=1 EOF_UNIT cat > /etc/systemd/system/clamav-risk-scan.timer <<'EOF_UNIT' [Unit] Description=Hourly ClamAV scan of high-risk temporary paths [Timer] OnBootSec=20min OnUnitActiveSec=1h RandomizedDelaySec=5min Persistent=true [Install] WantedBy=timers.target EOF_UNIT cat > /etc/systemd/system/clamav-deep-scan.service <<'EOF_UNIT' [Unit] Description=ClamAV daily low-priority server scan After=local-fs.target [Service] Type=oneshot ExecStart=/usr/local/sbin/clamav-watch-scan.sh deep User=root Nice=19 IOSchedulingClass=idle SuccessExitStatus=1 EOF_UNIT cat > /etc/systemd/system/clamav-deep-scan.timer <<'EOF_UNIT' [Unit] Description=Daily ClamAV low-priority server scan [Timer] OnCalendar=*-*-* 04:30:00 RandomizedDelaySec=15min Persistent=true [Install] WantedBy=timers.target EOF_UNIT fi systemctl daemon-reload systemctl enable --now malware-watch.timer malware-feed-update.timer block-malware-iocs.service if [[ "$INSTALL_CLAMAV" == "1" ]]; then systemctl enable --now clamav-freshclam-local.timer clamav-risk-scan.timer clamav-deep-scan.timer fi log "Updating abuse.ch feeds" if ! systemctl start malware-feed-update.service; then log "WARNING: abuse.ch feed update failed; timer will retry" fi if [[ "$INSTALL_CLAMAV" == "1" && "$RUN_FRESHCLAM_NOW" == "1" ]]; then log "Trying initial ClamAV signature update" systemctl start clamav-freshclam-local.service || log "WARNING: freshclam failed; timer will retry" fi systemctl start malware-watch.service || true if [[ "$SEND_INSTALL_TEST" == "1" ]]; then payload="$(python3 - <<'PYMSG' import json,socket print(json.dumps({'content':f'malware-watch install OK | server: {socket.gethostname()}'},ensure_ascii=False)) PYMSG )" curl -sS -o /dev/null -A 'Mozilla/5.0 (compatible; malware-watch/2.0)' \ -H 'Content-Type: application/json' --data "$payload" "$DISCORD_WEBHOOK" || true fi log "Installation complete" /usr/local/sbin/malware-watch.py || true if [[ -x "$SCRIPT_DIR/verify.sh" ]]; then "$SCRIPT_DIR/verify.sh" || true fi