ClamAV - Hardening version 4
Update the ClamAV file to make it work for the following:
- Debian, MintOS and Bazzite
- Fix the email notification on service failure problem
Result of dry-run Debian
sudo ./setup-clamav-v4.sh --dry-run
[sudo] password for braedach:
[INFO] Detected OS family: debian (ID=linuxmint)
[INFO] Dry-run mode enabled: no changes will be applied.
>>> Stopping ClamAV services
[INFO] [DRY] Would stop clamav-daemon and clamav-freshclam services
[INFO] [DRY] Would kill any stale freshclam processes
[INFO] [DRY] Would remove stale PID files
>>> Backing up existing configuration
[INFO] [DRY] Would back up configs to /etc/clamav/backups/20260517_100500
>>> Resetting configuration files to vendor baseline
[INFO] [DRY] Would run dpkg-reconfigure clamav-daemon and clamav-freshclam
>>> Socket Permissions
[INFO] [DRY] Would update: LocalSocketMode → 660 (/etc/clamav/clamd.conf)
[INFO] [DRY] Would update: LocalSocketGroup → clamav (/etc/clamav/clamd.conf)
[INFO] [DRY] Would update: PidFile → /var/run/clamav/clamd.pid (/etc/clamav/clamd.conf)
>>> Logging
[INFO] [DRY] Would update: LogFile → /var/log/clamav/clamav.log (/etc/clamav/clamd.conf)
[INFO] [DRY] Would update: LogFileMaxSize → 52428800 (/etc/clamav/clamd.conf)
[INFO] [DRY] Would update: LogTime → yes (/etc/clamav/clamd.conf)
[INFO] [DRY] Would update: LogVerbose → yes (/etc/clamav/clamd.conf)
[INFO] [DRY] Would update: LogSyslog → yes (/etc/clamav/clamd.conf)
[INFO] [DRY] Would update: LogFacility → LOG_LOCAL6 (/etc/clamav/clamd.conf)
[INFO] [DRY] Would update: LogRotate → yes (/etc/clamav/clamd.conf)
>>> ExcludePath Directives
[INFO] Already present: ExcludePath /proc/
[INFO] Already present: ExcludePath /sys/
[INFO] Already present: ExcludePath /dev/
[INFO] Already present: ExcludePath /run/
[INFO] Already present: ExcludePath /var/lib/docker/
[INFO] Already present: ExcludePath /var/lib/containers/
[INFO] Already present: ExcludePath /var/lib/mysql/
[INFO] Already present: ExcludePath /var/lib/postgresql/
[INFO] Already present: ExcludePath /var/cache/apt/archives/
[INFO] Already present: ExcludePath /var/quarantine/clamav/
>>> Alert Settings
[INFO] [DRY] Would update: AlertEncryptedArchive → yes (/etc/clamav/clamd.conf)
[INFO] [DRY] Would update: AlertEncryptedDoc → yes (/etc/clamav/clamd.conf)
[INFO] [DRY] Would update: AlertOLE2Macros → yes (/etc/clamav/clamd.conf)
[INFO] [DRY] Would update: AlertPhishingSSLMismatch → yes (/etc/clamav/clamd.conf)
[INFO] [DRY] Would update: AlertPhishingCloak → yes (/etc/clamav/clamd.conf)
[INFO] [DRY] Would update: AlertBrokenExecutables → yes (/etc/clamav/clamd.conf)
[INFO] [DRY] Would update: AlertBrokenMedia → yes (/etc/clamav/clamd.conf)
[INFO] [DRY] Would update: AlertPartitionIntersection → yes (/etc/clamav/clamd.conf)
>>> Database Hygiene
[INFO] [DRY] Would update: OfficialDatabaseOnly → no (/etc/clamav/clamd.conf)
[INFO] [DRY] Would update: FailIfCvdOlderThan → 7 (/etc/clamav/clamd.conf)
[INFO] [DRY] Would update: ConcurrentDatabaseReload → yes (/etc/clamav/clamd.conf)
[INFO] [DRY] Would update: SelfCheck → 1800 (/etc/clamav/clamd.conf)
>>> Detection Refinements
[INFO] [DRY] Would update: DetectPUA → yes (/etc/clamav/clamd.conf)
[INFO] [DRY] Would update: HeuristicAlerts → yes (/etc/clamav/clamd.conf)
[INFO] [DRY] Would update: HeuristicScanPrecedence → yes (/etc/clamav/clamd.conf)
>>> File and Scan Limits
[INFO] [DRY] Would update: MaxScanSize → 209715200 (/etc/clamav/clamd.conf)
[INFO] [DRY] Would update: MaxFileSize → 52428800 (/etc/clamav/clamd.conf)
[INFO] [DRY] Would update: MaxRecursion → 16 (/etc/clamav/clamd.conf)
[INFO] [DRY] Would update: MaxFiles → 15000 (/etc/clamav/clamd.conf)
[INFO] [DRY] Would update: MaxEmbeddedPE → 10485760 (/etc/clamav/clamd.conf)
[INFO] [DRY] Would update: MaxHTMLNormalize → 10485760 (/etc/clamav/clamd.conf)
[INFO] [DRY] Would update: MaxHTMLNoTags → 2097152 (/etc/clamav/clamd.conf)
[INFO] [DRY] Would update: MaxScriptNormalize → 5242880 (/etc/clamav/clamd.conf)
[INFO] [DRY] Would update: MaxZipTypeRcg → 1048576 (/etc/clamav/clamd.conf)
[INFO] [DRY] Would update: OnAccessMaxFileSize → 26214400 (/etc/clamav/clamd.conf)
>>> On-Access Scanning (Home Directory Protection)
[INFO] Already present: OnAccessIncludePath /home
[INFO] Already present: OnAccessIncludePath /tmp
[INFO] Already present: OnAccessIncludePath /var/tmp
[INFO] Already present: OnAccessIncludePath /root
[INFO] [DRY] Would update: OnAccessPrevention → yes (/etc/clamav/clamd.conf)
[INFO] [DRY] Would update: OnAccessExcludeUID → 0 (/etc/clamav/clamd.conf)
[INFO] Already present: OnAccessExcludePath /proc
[INFO] Already present: OnAccessExcludePath /sys
[INFO] Already present: OnAccessExcludePath /dev
[INFO] Already present: OnAccessExcludePath /run
[INFO] Already present: OnAccessExcludePath /var/quarantine/clamav
>>> Freshclam Settings
[INFO] [DRY] Would update: Checks → 6 (/etc/clamav/freshclam.conf)
[INFO] [DRY] Would update: MaxAttempts → 5 (/etc/clamav/freshclam.conf)
[INFO] [DRY] Would update: ConnectTimeout → 30 (/etc/clamav/freshclam.conf)
[INFO] [DRY] Would update: ReceiveTimeout → 30 (/etc/clamav/freshclam.conf)
[INFO] [DRY] Would update: LogVerbose → yes (/etc/clamav/freshclam.conf)
[INFO] [DRY] Would update: LogSyslog → yes (/etc/clamav/freshclam.conf)
[INFO] [DRY] Would update: LogFacility → LOG_LOCAL6 (/etc/clamav/freshclam.conf)
[INFO] [DRY] Would update: NotifyClamd → /etc/clamav/clamd.conf (/etc/clamav/freshclam.conf)
>>> Third-Party Signatures (DatabaseCustomURL)
[INFO] Already present: DatabaseCustomURL https://mirror.rollernet.us/sanesecurity/sanesecurity.ftm
[INFO] Already present: DatabaseCustomURL https://mirror.rollernet.us/sanesecurity/sigwhitelist.ign2
[INFO] Already present: DatabaseCustomURL https://mirror.rollernet.us/sanesecurity/junk.ndb
[INFO] Already present: DatabaseCustomURL https://mirror.rollernet.us/sanesecurity/jurlbl.ndb
[INFO] Already present: DatabaseCustomURL https://mirror.rollernet.us/sanesecurity/phish.ndb
[INFO] Already present: DatabaseCustomURL https://mirror.rollernet.us/sanesecurity/rogue.hdb
[INFO] Already present: DatabaseCustomURL https://mirror.rollernet.us/sanesecurity/scam.ndb
[INFO] Already present: DatabaseCustomURL https://mirror.rollernet.us/sanesecurity/spamimg.hdb
[INFO] Already present: DatabaseCustomURL https://mirror.rollernet.us/sanesecurity/spamattach.hdb
[INFO] Already present: DatabaseCustomURL https://mirror.rollernet.us/sanesecurity/blurl.ndb
[INFO] Already present: DatabaseCustomURL https://mirror.rollernet.us/sanesecurity/malwarehash.hsb
[INFO] Already present: DatabaseCustomURL https://mirror.rollernet.us/sanesecurity/hackingteam.hsb
[INFO] Already present: DatabaseCustomURL https://mirror.rollernet.us/sanesecurity/foxhole_generic.cdb
[INFO] Already present: DatabaseCustomURL https://mirror.rollernet.us/sanesecurity/foxhole_filename.cdb
[INFO] Already present: DatabaseCustomURL https://mirror.rollernet.us/sanesecurity/winnow_malware.hdb
[INFO] Already present: DatabaseCustomURL https://mirror.rollernet.us/sanesecurity/winnow_malware_links.ndb
[INFO] Already present: DatabaseCustomURL https://mirror.rollernet.us/sanesecurity/winnow_extended_malware.hdb
[INFO] Already present: DatabaseCustomURL https://mirror.rollernet.us/sanesecurity/winnow.attachments.hdb
[INFO] Already present: DatabaseCustomURL https://mirror.rollernet.us/sanesecurity/winnow_bad_cw.hdb
[INFO] Already present: DatabaseCustomURL https://urlhaus.abuse.ch/downloads/urlhaus.ndb
>>> Removing legacy cron job (if present)
[INFO] No legacy cron file found — nothing to remove.
>>> Enabling and starting ClamAV services
[INFO] [DRY] Would enable and start clamav-freshclam.service
[INFO] [DRY] Would run an initial freshclam update
[INFO] [DRY] Would enable and start clamav-daemon.service
>>> EICAR Functional Test
[INFO] [DRY] Would run EICAR detection test against clamd.
>>> Systemd Failure Notification
[INFO] [DRY] Would install notify helper script at /usr/local/lib/clamav-notify-failure.sh
[INFO] [DRY] Would install notify-failure@.service at /etc/systemd/system/notify-failure@.service
[INFO] [DRY] Would install OnFailure drop-in for clamav-daemon.service at /etc/systemd/system/clamav-daemon.service.d/notify-on-failure.conf
[INFO] [DRY] Would install OnFailure drop-in for clamav-freshclam.service at /etc/systemd/system/clamav-freshclam.service.d/notify-on-failure.conf
[INFO] [DRY] Would run systemctl daemon-reload
>>> Setup Complete
┌─────────────────────────────────────────────────────────────┐
│ ClamAV — Debian / LMDE │
├─────────────────────────────────────────────────────────────┤
│ Services: clamav-daemon clamav-freshclam │
│ On-access: /home /tmp /var/tmp /root (BLOCKING) │
│ Quarantine: /var/quarantine/clamav │
│ Signatures: Every 4 hours via freshclam service │
│ Notify: /usr/local/lib/clamav-notify-failure.sh │
├─────────────────────────────────────────────────────────────┤
│ Useful commands: │
│ systemctl status clamav-daemon │
│ systemctl status clamav-freshclam │
│ clamdscan --fdpass ~/ │
│ journalctl -u clamav-daemon -f │
└─────────────────────────────────────────────────────────────┘
Tests run on script
- Script on MintOS LMDE 7 - no errors
- Script run on Bazzite OS - whoops - fix - no errors
- Script run on stagnant MintOS LMDE 7 - no errors
ClamAV Hardening Script
Find below the hardening script. Please read the script summary before running.
Warning
- Bazzite implementation of ClamAV via distrobox - seriously ugly
- Pulled that code from the script
- This script still has the code as I have run out of time
- I will fix the code but need to wait 5 hours as AI spat the dummy
- Problem has been identified and solution found but need more time
#!/bin/bash
# ─────────────────────────────────────────────────────────────────────────────
# setup-clamav.sh
# Harden ClamAV configuration for Debian/LMDE and Bazzite OS systems
# ─────────────────────────────────────────────────────────────────────────────
#
# PURPOSE
# Applies a hardened, best-practice ClamAV configuration focused on
# real-time workstation protection, home directory defence, and
# scheduled full-system sweeps.
#
# Supports two distinct operating system families:
#
# DEBIAN / LMDE (debian)
# Full daemon-based setup: clamd + freshclam as systemd services,
# on-access scanning via fanotify, quarantine, Sanesecurity +
# URLhaus third-party signature databases.
# Packages: clamav-daemon, clamav-freshclam (via apt)
# Config: /etc/clamav/clamd.conf, /etc/clamav/freshclam.conf
#
# BAZZITE OS (bazzite)
# Immutable Fedora Atomic base — root filesystem is read-only.
# ClamAV installed via Homebrew into the Homebrew prefix.
# No clamd systemd service; on-demand scanning only (clamscan).
# freshclam config bootstrapped from the Homebrew sample.
# Signature updates via a user-level systemd timer (no root required).
# On-access scanning is NOT available in this mode.
# Config: $(brew --prefix)/etc/clamav/
#
# BEHAVIOUR
# - Auto-detects OS from /etc/os-release (ID and ID_LIKE fields)
# - Self-contained: resets config files to a known-good baseline before
# applying settings — safe for first run and re-runs alike
# - Idempotent: safely re-runnable
# - Supports --dry-run mode: validates what would change without touching
# any files or services → ./setup-clamav.sh --dry-run
# - Debian mode must be run as root
# - Bazzite mode must NOT be run as root (Homebrew requirement)
#
# UPDATE STRATEGY (Debian)
# Signature updates managed by clamav-freshclam systemd service — NOT cron.
# This avoids log-lock conflicts caused by multiple freshclam processes.
#
# UPDATE STRATEGY (Bazzite)
# A user-level systemd timer (~/.config/systemd/user/) runs freshclam
# daily. No root required. clamd daemon is not installed or started.
#
# PRE-REQUISITES (Debian)
# - clamav-daemon, clamav-freshclam installed
# - Kernel with fanotify support (Linux Mint 20+ / kernel 5.4+)
# - Local MTA (Postfix) installed and configured for email notifications
#
# PRE-REQUISITES (Bazzite)
# - Homebrew installed (https://brew.sh)
# - Run as a normal user (not root)
#
# PROTECTION COVERAGE (Debian)
# Real-time (on-access, BLOCKING): /home /tmp /var/tmp /root
# Quarantine destination: /var/quarantine/clamav (mode 0700)
# Signature updates: Every 4 hours via freshclam systemd service
#
# PROTECTION COVERAGE (Bazzite)
# On-demand scanning: clamscan --recursive --infected ~/
# Signature updates: Daily via user systemd timer
# On-access (real-time) scanning: NOT available (Homebrew / no clamd)
#
# Version: 4.0.0
# Updated: 17-05-2026
#
# Change Log:
# v4.0.0 — 17-05-2026
# - ADDED: Bazzite OS support (Homebrew-based, user-level, no daemon)
# - ADDED: Auto OS detection from /etc/os-release
# - ADDED: Bazzite freshclam config bootstrap from Homebrew sample
# - ADDED: User-level systemd timer for daily freshclam on Bazzite
# - FIXED: notify-failure@.service — blank Host/Unit/Time/Journal in emails
# Root cause: fragile inline bash -c one-liner with broken %i expansion
# and hostname -f failures. Fix: ExecStart now calls an installed helper
# script (/usr/local/lib/clamav-notify-failure.sh) that runs in a clean
# shell environment with no systemd escaping issues.
# - FIXED: notify-failure@.service subject line showed "[ClamAV FAILURE]
# failed on" with blank unit and host — same root cause as above.
# - UPDATED: Version bump to 4.0.0, header and change log updated
# ─────────────────────────────────────────────────────────────────────────────
set -euo pipefail
# --- Output helpers ---
info() { echo -e "\033[0;32m[INFO]\033[0m $*"; }
warn() { echo -e "\033[0;33m[WARN]\033[0m $*" >&2; }
error() { echo -e "\033[0;31m[ERROR]\033[0m $*" >&2; }
section() { echo -e "\n\033[1;34m>>> $* \033[0m"; }
# ─────────────────────────────────────────────────────────────────────────────
# OS DETECTION
# ─────────────────────────────────────────────────────────────────────────────
detect_os() {
if [[ ! -f /etc/os-release ]]; then
error "/etc/os-release not found — cannot detect OS."
exit 1
fi
local os_id os_id_like
os_id=$(grep -oP '(?<=^ID=)[^\n]+' /etc/os-release | tr -d '"' | tr '[:upper:]' '[:lower:]')
os_id_like=$(grep -oP '(?<=^ID_LIKE=)[^\n]+' /etc/os-release | tr -d '"' | tr '[:upper:]' '[:lower:]' || true)
if [[ "$os_id" == "bazzite" ]] || echo "$os_id_like" | grep -q "bazzite"; then
OS_FAMILY="bazzite"
elif [[ "$os_id" == "debian" || "$os_id" == "linuxmint" ]] || \
echo "$os_id_like" | grep -qE "debian|ubuntu"; then
OS_FAMILY="debian"
else
error "Unsupported OS: ID='$os_id' ID_LIKE='$os_id_like'"
error "This script supports Debian, Linux Mint (LMDE), and Bazzite OS only."
exit 1
fi
info "Detected OS family: $OS_FAMILY (ID=$os_id)"
}
detect_os
DRY_RUN=false
if [[ "${1:-}" == "--dry-run" ]]; then
DRY_RUN=true
info "Dry-run mode enabled: no changes will be applied."
fi
# ─────────────────────────────────────────────────────────────────────────────
# ROOT / USER PRIVILEGE CHECKS
# ─────────────────────────────────────────────────────────────────────────────
if [[ "$OS_FAMILY" == "debian" ]]; then
if [[ "${EUID}" -ne 0 ]]; then
error "Debian/LMDE mode must be run as root."
exit 1
fi
elif [[ "$OS_FAMILY" == "bazzite" ]]; then
if [[ "${EUID}" -eq 0 ]]; then
error "Bazzite mode must NOT be run as root — Homebrew requires a normal user."
exit 1
fi
fi
# ─────────────────────────────────────────────────────────────────────────────
# SHARED HELPERS
# ─────────────────────────────────────────────────────────────────────────────
# ensure_config: idempotently set a directive in a config file.
ensure_config() {
local file="$1" key="$2" value="$3"
if grep -qE "^[[:space:]]*${key}[[:space:]]" "$file"; then
if $DRY_RUN; then
info "[DRY] Would update: $key → $value ($file)"
else
sed -i "s|^[[:space:]]*${key}[[:space:]].*|${key} ${value}|" "$file"
info "Updated: $key = $value"
fi
else
if $DRY_RUN; then
info "[DRY] Would add: $key $value ($file)"
else
echo "${key} ${value}" >> "$file"
info "Added: $key = $value"
fi
fi
}
# ensure_multivalue: idempotently add a directive that may appear multiple times.
ensure_multivalue() {
local file="$1" directive="$2" value="$3"
if ! grep -qF "${directive} ${value}" "$file"; then
if $DRY_RUN; then
info "[DRY] Would add: ${directive} ${value}"
else
echo "${directive} ${value}" >> "$file"
info "Added: ${directive} ${value}"
fi
else
info "Already present: ${directive} ${value}"
fi
}
# ═════════════════════════════════════════════════════════════════════════════
# BAZZITE PATH
# ═════════════════════════════════════════════════════════════════════════════
if [[ "$OS_FAMILY" == "bazzite" ]]; then
section "Bazzite OS — Homebrew ClamAV Setup"
# ─────────────────────────────────────────────────────────────────────────
# Verify Homebrew is present
# ─────────────────────────────────────────────────────────────────────────
if ! command -v brew &>/dev/null; then
error "Homebrew not found. Install it first: https://brew.sh"
exit 1
fi
BREW_PREFIX="$(brew --prefix)"
BREW_CLAMAV_CONF_DIR="${BREW_PREFIX}/etc/clamav"
FRESHCLAM_CONF="${BREW_CLAMAV_CONF_DIR}/freshclam.conf"
CLAMD_CONF="${BREW_CLAMAV_CONF_DIR}/clamd.conf"
# ─────────────────────────────────────────────────────────────────────────
# SECTION B1 — Install ClamAV via Homebrew
# ─────────────────────────────────────────────────────────────────────────
section "Installing ClamAV via Homebrew"
if brew list clamav &>/dev/null; then
info "ClamAV already installed via Homebrew."
if $DRY_RUN; then
info "[DRY] Would run: brew upgrade clamav"
else
info "Upgrading to latest version..."
brew upgrade clamav || info "Already at latest version."
fi
else
if $DRY_RUN; then
info "[DRY] Would run: brew install clamav"
else
info "Installing ClamAV..."
brew install clamav
fi
fi
# ─────────────────────────────────────────────────────────────────────────
# SECTION B2 — Bootstrap freshclam.conf from Homebrew sample
#
# Homebrew ships a .sample file. The sample contains "Example" at the top
# which deliberately breaks freshclam until removed. This section creates
# a working freshclam.conf from the sample if one doesn't already exist,
# then applies hardened settings via ensure_config.
# ─────────────────────────────────────────────────────────────────────────
section "Bootstrapping freshclam.conf"
SAMPLE_CONF="${BREW_CLAMAV_CONF_DIR}/freshclam.conf.sample"
if [[ ! -f "$SAMPLE_CONF" ]]; then
error "Homebrew sample config not found at $SAMPLE_CONF"
error "Try: brew reinstall clamav"
exit 1
fi
if [[ ! -f "$FRESHCLAM_CONF" ]]; then
if $DRY_RUN; then
info "[DRY] Would copy $SAMPLE_CONF → $FRESHCLAM_CONF"
info "[DRY] Would remove 'Example' line from freshclam.conf"
else
cp "$SAMPLE_CONF" "$FRESHCLAM_CONF"
# Remove the "Example" line that deliberately breaks freshclam
sed -i '/^Example/d' "$FRESHCLAM_CONF"
info "Created $FRESHCLAM_CONF from sample."
fi
else
info "freshclam.conf already exists — applying/updating settings only."
# Ensure the Example line is gone even on re-runs
if ! $DRY_RUN && grep -q "^Example" "$FRESHCLAM_CONF" 2>/dev/null; then
sed -i '/^Example/d' "$FRESHCLAM_CONF"
info "Removed stale 'Example' directive."
fi
fi
# Bootstrap clamd.conf from sample (needed for clamscan defaults even
# without running the daemon)
if [[ -f "${BREW_CLAMAV_CONF_DIR}/clamd.conf.sample" ]] && \
[[ ! -f "$CLAMD_CONF" ]]; then
if $DRY_RUN; then
info "[DRY] Would copy clamd.conf.sample → clamd.conf"
else
cp "${BREW_CLAMAV_CONF_DIR}/clamd.conf.sample" "$CLAMD_CONF"
sed -i '/^Example/d' "$CLAMD_CONF"
info "Created $CLAMD_CONF from sample."
fi
fi
# ─────────────────────────────────────────────────────────────────────────
# SECTION B3 — Apply freshclam settings
# ─────────────────────────────────────────────────────────────────────────
section "Applying freshclam settings"
if ! $DRY_RUN; then
ensure_config "$FRESHCLAM_CONF" "Checks" "6"
ensure_config "$FRESHCLAM_CONF" "MaxAttempts" "5"
ensure_config "$FRESHCLAM_CONF" "ConnectTimeout" "30"
ensure_config "$FRESHCLAM_CONF" "ReceiveTimeout" "30"
ensure_config "$FRESHCLAM_CONF" "LogVerbose" "yes"
# Homebrew log path (writable by current user, no clamav system user)
BREW_LOG_DIR="${BREW_PREFIX}/var/log/clamav"
mkdir -p "$BREW_LOG_DIR"
ensure_config "$FRESHCLAM_CONF" "UpdateLogFile" "${BREW_LOG_DIR}/freshclam.log"
else
info "[DRY] Would apply freshclam settings to $FRESHCLAM_CONF"
fi
# ─────────────────────────────────────────────────────────────────────────
# SECTION B4 — Third-party signatures (Sanesecurity + URLhaus)
# Same low-FP databases as Debian mode — freshclam handles these the
# same way regardless of OS.
# ─────────────────────────────────────────────────────────────────────────
section "Third-Party Signatures (DatabaseCustomURL)"
SANESEC_MIRROR="https://mirror.rollernet.us/sanesecurity"
if ! $DRY_RUN; then
for db in \
"${SANESEC_MIRROR}/sanesecurity.ftm" \
"${SANESEC_MIRROR}/sigwhitelist.ign2" \
"${SANESEC_MIRROR}/junk.ndb" \
"${SANESEC_MIRROR}/jurlbl.ndb" \
"${SANESEC_MIRROR}/phish.ndb" \
"${SANESEC_MIRROR}/rogue.hdb" \
"${SANESEC_MIRROR}/scam.ndb" \
"${SANESEC_MIRROR}/spamimg.hdb" \
"${SANESEC_MIRROR}/spamattach.hdb" \
"${SANESEC_MIRROR}/blurl.ndb" \
"${SANESEC_MIRROR}/malwarehash.hsb" \
"${SANESEC_MIRROR}/hackingteam.hsb" \
"${SANESEC_MIRROR}/foxhole_generic.cdb" \
"${SANESEC_MIRROR}/foxhole_filename.cdb" \
"${SANESEC_MIRROR}/winnow_malware.hdb" \
"${SANESEC_MIRROR}/winnow_malware_links.ndb" \
"${SANESEC_MIRROR}/winnow_extended_malware.hdb" \
"${SANESEC_MIRROR}/winnow.attachments.hdb" \
"${SANESEC_MIRROR}/winnow_bad_cw.hdb" \
"https://urlhaus.abuse.ch/downloads/urlhaus.ndb"; do
ensure_multivalue "$FRESHCLAM_CONF" "DatabaseCustomURL" "$db"
done
else
info "[DRY] Would add Sanesecurity and URLhaus DatabaseCustomURL entries"
fi
# ─────────────────────────────────────────────────────────────────────────
# SECTION B5 — Initial signature update
# ─────────────────────────────────────────────────────────────────────────
section "Running initial freshclam update"
if $DRY_RUN; then
info "[DRY] Would run: freshclam"
else
info "Updating virus definitions (this may take a moment on first run)..."
freshclam || warn "freshclam update failed — check log at ${BREW_LOG_DIR}/freshclam.log"
fi
# ─────────────────────────────────────────────────────────────────────────
# SECTION B6 — User-level systemd timer for daily freshclam
#
# Bazzite uses systemd. A user-level timer (~/.config/systemd/user/)
# runs freshclam daily without requiring root. The timer survives OS
# image updates because it lives in the user's home directory.
# ─────────────────────────────────────────────────────────────────────────
section "Installing user systemd timer for daily freshclam"
USER_SYSTEMD_DIR="${HOME}/.config/systemd/user"
FRESHCLAM_SERVICE="${USER_SYSTEMD_DIR}/clamav-freshclam.service"
FRESHCLAM_TIMER="${USER_SYSTEMD_DIR}/clamav-freshclam.timer"
if $DRY_RUN; then
info "[DRY] Would install user systemd service: $FRESHCLAM_SERVICE"
info "[DRY] Would install user systemd timer: $FRESHCLAM_TIMER"
info "[DRY] Would enable and start clamav-freshclam.timer"
else
mkdir -p "$USER_SYSTEMD_DIR"
cat > "$FRESHCLAM_SERVICE" <<EOF
[Unit]
Description=ClamAV signature update (freshclam)
After=network-online.target
Wants=network-online.target
[Service]
Type=oneshot
ExecStart=${BREW_PREFIX}/bin/freshclam
StandardOutput=journal
StandardError=journal
EOF
cat > "$FRESHCLAM_TIMER" <<EOF
[Unit]
Description=Daily ClamAV signature update
[Timer]
OnCalendar=daily
RandomizedDelaySec=1h
Persistent=true
[Install]
WantedBy=timers.target
EOF
systemctl --user daemon-reload
systemctl --user enable --now clamav-freshclam.timer
info "User timer installed and started."
systemctl --user status clamav-freshclam.timer --no-pager || true
fi
# ─────────────────────────────────────────────────────────────────────────
# SECTION B7 — EICAR functional test
# ─────────────────────────────────────────────────────────────────────────
section "EICAR Functional Test"
if $DRY_RUN; then
info "[DRY] Would run EICAR test via clamscan"
else
EICAR_FILE="/tmp/eicar_test_$$.txt"
python3 -c "open('${EICAR_FILE}','w').write('X5O!P%@AP[4\\PZX54(P^)7CC)7}\$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!\$H+H*')"
SCAN_RESULT=$(clamscan --no-summary "$EICAR_FILE" 2>&1 || true)
if echo "$SCAN_RESULT" | grep -q "FOUND"; then
info "EICAR test: PASSED — clamscan detected test signature correctly."
else
warn "EICAR test: FAILED or definitions not yet downloaded."
warn "Scan output: $SCAN_RESULT"
warn "Manual test: clamscan /tmp/eicar_test.txt"
fi
rm -f "$EICAR_FILE"
fi
# ─────────────────────────────────────────────────────────────────────────
# SECTION B8 — Bazzite usage summary
# ─────────────────────────────────────────────────────────────────────────
section "Bazzite ClamAV Setup Complete"
echo ""
echo " ┌─────────────────────────────────────────────────────────────┐"
echo " │ ClamAV — Bazzite OS │"
echo " ├─────────────────────────────────────────────────────────────┤"
echo " │ Mode: On-demand scanning only (no clamd daemon) │"
echo " │ Config: ${BREW_CLAMAV_CONF_DIR}/ │"
echo " │ Logs: ${BREW_PREFIX}/var/log/clamav/ │"
echo " │ Updates: Daily via user systemd timer │"
echo " ├─────────────────────────────────────────────────────────────┤"
echo " │ Useful commands: │"
echo " │ clamscan -r --infected ~/ # scan home dir │"
echo " │ freshclam # update definitions │"
echo " │ systemctl --user status clamav-freshclam.timer │"
echo " │ systemctl --user list-timers │"
echo " └─────────────────────────────────────────────────────────────┘"
echo ""
exit 0
fi
# ═════════════════════════════════════════════════════════════════════════════
# DEBIAN / LMDE PATH (everything below is Debian only)
# ═════════════════════════════════════════════════════════════════════════════
# --- Paths ---
CLAMD_CONF="/etc/clamav/clamd.conf"
FRESHCLAM_CONF="/etc/clamav/freshclam.conf"
LOGFILE="/var/log/clamav/harden_clamav.log"
QUARANTINE_DIR="/var/quarantine/clamav"
SCAN_LOG="/var/log/clamav/daily_scan.log"
BACKUP_DIR="/etc/clamav/backups/$(date +%Y%m%d_%H%M%S)"
# --- Ensure log dir and quarantine dir exist ---
if ! $DRY_RUN; then
mkdir -p "$(dirname "$LOGFILE")" "$QUARANTINE_DIR"
chmod 0700 "$QUARANTINE_DIR"
chown root:root "$QUARANTINE_DIR"
touch "$LOGFILE" "$SCAN_LOG"
chown clamav:clamav "$LOGFILE" "$SCAN_LOG"
chmod 0640 "$LOGFILE" "$SCAN_LOG"
fi
# ─────────────────────────────────────────────────────────────────────────────
# SECTION 0 — Stop services and back up existing config
# ─────────────────────────────────────────────────────────────────────────────
section "Stopping ClamAV services"
if ! $DRY_RUN; then
systemctl stop clamav-daemon.service 2>/dev/null || true
systemctl stop clamav-freshclam.service 2>/dev/null || true
sleep 1
if pgrep -x freshclam >/dev/null 2>&1; then
warn "Stale freshclam process found — terminating..."
pkill -x freshclam || true
sleep 1
fi
rm -f /var/run/clamav/freshclam.pid
rm -f /var/run/clamav/clamd.pid
info "Stale PID files cleared."
else
info "[DRY] Would stop clamav-daemon and clamav-freshclam services"
info "[DRY] Would kill any stale freshclam processes"
info "[DRY] Would remove stale PID files"
fi
section "Backing up existing configuration"
if ! $DRY_RUN; then
mkdir -p "$BACKUP_DIR"
[[ -f "$CLAMD_CONF" ]] && cp "$CLAMD_CONF" "${BACKUP_DIR}/clamd.conf.bak"
[[ -f "$FRESHCLAM_CONF" ]] && cp "$FRESHCLAM_CONF" "${BACKUP_DIR}/freshclam.conf.bak"
info "Backed up configs to $BACKUP_DIR"
else
info "[DRY] Would back up configs to $BACKUP_DIR"
fi
section "Resetting configuration files to vendor baseline"
if ! $DRY_RUN; then
DEBIAN_FRONTEND=noninteractive dpkg-reconfigure clamav-daemon 2>/dev/null || true
DEBIAN_FRONTEND=noninteractive dpkg-reconfigure clamav-freshclam 2>/dev/null || true
info "Config files reset to vendor defaults."
else
info "[DRY] Would run dpkg-reconfigure clamav-daemon and clamav-freshclam"
fi
# ─────────────────────────────────────────────────────────────────────────────
# SECTION 1 — Socket Permissions
# ─────────────────────────────────────────────────────────────────────────────
section "Socket Permissions"
ensure_config "$CLAMD_CONF" "LocalSocketMode" "660"
ensure_config "$CLAMD_CONF" "LocalSocketGroup" "clamav"
ensure_config "$CLAMD_CONF" "PidFile" "/var/run/clamav/clamd.pid"
# ─────────────────────────────────────────────────────────────────────────────
# SECTION 2 — Logging
# ─────────────────────────────────────────────────────────────────────────────
section "Logging"
ensure_config "$CLAMD_CONF" "LogFile" "/var/log/clamav/clamav.log"
ensure_config "$CLAMD_CONF" "LogFileMaxSize" "52428800"
ensure_config "$CLAMD_CONF" "LogTime" "yes"
ensure_config "$CLAMD_CONF" "LogVerbose" "yes"
ensure_config "$CLAMD_CONF" "LogSyslog" "yes"
ensure_config "$CLAMD_CONF" "LogFacility" "LOG_LOCAL6"
ensure_config "$CLAMD_CONF" "LogRotate" "yes"
# ─────────────────────────────────────────────────────────────────────────────
# SECTION 3 — ExcludePath (daemon)
# ─────────────────────────────────────────────────────────────────────────────
section "ExcludePath Directives"
for path in \
"/proc/" \
"/sys/" \
"/dev/" \
"/run/" \
"/var/lib/docker/" \
"/var/lib/containers/" \
"/var/lib/mysql/" \
"/var/lib/postgresql/" \
"/var/cache/apt/archives/" \
"$QUARANTINE_DIR/"; do
ensure_multivalue "$CLAMD_CONF" "ExcludePath" "$path"
done
# ─────────────────────────────────────────────────────────────────────────────
# SECTION 4 — Alert Settings
# ─────────────────────────────────────────────────────────────────────────────
section "Alert Settings"
for key in \
AlertEncryptedArchive \
AlertEncryptedDoc \
AlertOLE2Macros \
AlertPhishingSSLMismatch \
AlertPhishingCloak \
AlertBrokenExecutables \
AlertBrokenMedia \
AlertPartitionIntersection; do
ensure_config "$CLAMD_CONF" "$key" "yes"
done
# ─────────────────────────────────────────────────────────────────────────────
# SECTION 5 — Database Hygiene
# ─────────────────────────────────────────────────────────────────────────────
section "Database Hygiene"
ensure_config "$CLAMD_CONF" "OfficialDatabaseOnly" "no"
ensure_config "$CLAMD_CONF" "FailIfCvdOlderThan" "7"
ensure_config "$CLAMD_CONF" "ConcurrentDatabaseReload" "yes"
ensure_config "$CLAMD_CONF" "SelfCheck" "1800"
# ─────────────────────────────────────────────────────────────────────────────
# SECTION 6 — Detection Refinements
# ─────────────────────────────────────────────────────────────────────────────
section "Detection Refinements"
ensure_config "$CLAMD_CONF" "DetectPUA" "yes"
ensure_config "$CLAMD_CONF" "HeuristicAlerts" "yes"
ensure_config "$CLAMD_CONF" "HeuristicScanPrecedence" "yes"
# ─────────────────────────────────────────────────────────────────────────────
# SECTION 7 — File & Scan Limits
# ─────────────────────────────────────────────────────────────────────────────
section "File and Scan Limits"
ensure_config "$CLAMD_CONF" "MaxScanSize" "209715200"
ensure_config "$CLAMD_CONF" "MaxFileSize" "52428800"
ensure_config "$CLAMD_CONF" "MaxRecursion" "16"
ensure_config "$CLAMD_CONF" "MaxFiles" "15000"
ensure_config "$CLAMD_CONF" "MaxEmbeddedPE" "10485760"
ensure_config "$CLAMD_CONF" "MaxHTMLNormalize" "10485760"
ensure_config "$CLAMD_CONF" "MaxHTMLNoTags" "2097152"
ensure_config "$CLAMD_CONF" "MaxScriptNormalize" "5242880"
ensure_config "$CLAMD_CONF" "MaxZipTypeRcg" "1048576"
ensure_config "$CLAMD_CONF" "OnAccessMaxFileSize" "26214400"
# ─────────────────────────────────────────────────────────────────────────────
# SECTION 8 — On-Access Scanning (real-time workstation protection)
#
# OnAccessPrevention BLOCKS access to detected files rather than just alerting.
# Requires kernel fanotify support (Linux Mint 20+ / kernel 5.4+).
# clamav-daemon must run as root for fanotify to work.
#
# NOTE: OnAccessExtraScanning permanently disabled in ClamAV 0.100.2 —
# omitted to avoid spurious warnings.
# ─────────────────────────────────────────────────────────────────────────────
section "On-Access Scanning (Home Directory Protection)"
if ! $DRY_RUN; then
if grep -qE "^[[:space:]]*OnAccessIncludePath[[:space:]]" "$CLAMD_CONF"; then
sed -i "/^[[:space:]]*OnAccessIncludePath[[:space:]]/d" "$CLAMD_CONF"
info "Cleared existing OnAccessIncludePath entries for clean rewrite."
fi
fi
for oa_path in "/home" "/tmp" "/var/tmp" "/root"; do
ensure_multivalue "$CLAMD_CONF" "OnAccessIncludePath" "$oa_path"
done
ensure_config "$CLAMD_CONF" "OnAccessPrevention" "yes"
ensure_config "$CLAMD_CONF" "OnAccessExcludeUID" "0"
for path in "/proc" "/sys" "/dev" "/run" "$QUARANTINE_DIR"; do
ensure_multivalue "$CLAMD_CONF" "OnAccessExcludePath" "$path"
done
# ─────────────────────────────────────────────────────────────────────────────
# SECTION 9 — Freshclam (signature updates via systemd service)
#
# Checks 6 = freshclam checks for new signatures 6 times per day (every 4 hours).
# ─────────────────────────────────────────────────────────────────────────────
section "Freshclam Settings"
ensure_config "$FRESHCLAM_CONF" "Checks" "6"
ensure_config "$FRESHCLAM_CONF" "MaxAttempts" "5"
ensure_config "$FRESHCLAM_CONF" "ConnectTimeout" "30"
ensure_config "$FRESHCLAM_CONF" "ReceiveTimeout" "30"
ensure_config "$FRESHCLAM_CONF" "LogVerbose" "yes"
ensure_config "$FRESHCLAM_CONF" "LogSyslog" "yes"
ensure_config "$FRESHCLAM_CONF" "LogFacility" "LOG_LOCAL6"
ensure_config "$FRESHCLAM_CONF" "NotifyClamd" "$CLAMD_CONF"
# ─────────────────────────────────────────────────────────────────────────────
# SECTION 9b — Third-Party Signatures via DatabaseCustomURL
# ─────────────────────────────────────────────────────────────────────────────
section "Third-Party Signatures (DatabaseCustomURL)"
SANESEC_MIRROR="https://mirror.rollernet.us/sanesecurity"
ensure_multivalue "$FRESHCLAM_CONF" "DatabaseCustomURL" "${SANESEC_MIRROR}/sanesecurity.ftm"
ensure_multivalue "$FRESHCLAM_CONF" "DatabaseCustomURL" "${SANESEC_MIRROR}/sigwhitelist.ign2"
ensure_multivalue "$FRESHCLAM_CONF" "DatabaseCustomURL" "${SANESEC_MIRROR}/junk.ndb"
ensure_multivalue "$FRESHCLAM_CONF" "DatabaseCustomURL" "${SANESEC_MIRROR}/jurlbl.ndb"
ensure_multivalue "$FRESHCLAM_CONF" "DatabaseCustomURL" "${SANESEC_MIRROR}/phish.ndb"
ensure_multivalue "$FRESHCLAM_CONF" "DatabaseCustomURL" "${SANESEC_MIRROR}/rogue.hdb"
ensure_multivalue "$FRESHCLAM_CONF" "DatabaseCustomURL" "${SANESEC_MIRROR}/scam.ndb"
ensure_multivalue "$FRESHCLAM_CONF" "DatabaseCustomURL" "${SANESEC_MIRROR}/spamimg.hdb"
ensure_multivalue "$FRESHCLAM_CONF" "DatabaseCustomURL" "${SANESEC_MIRROR}/spamattach.hdb"
ensure_multivalue "$FRESHCLAM_CONF" "DatabaseCustomURL" "${SANESEC_MIRROR}/blurl.ndb"
ensure_multivalue "$FRESHCLAM_CONF" "DatabaseCustomURL" "${SANESEC_MIRROR}/malwarehash.hsb"
ensure_multivalue "$FRESHCLAM_CONF" "DatabaseCustomURL" "${SANESEC_MIRROR}/hackingteam.hsb"
ensure_multivalue "$FRESHCLAM_CONF" "DatabaseCustomURL" "${SANESEC_MIRROR}/foxhole_generic.cdb"
ensure_multivalue "$FRESHCLAM_CONF" "DatabaseCustomURL" "${SANESEC_MIRROR}/foxhole_filename.cdb"
ensure_multivalue "$FRESHCLAM_CONF" "DatabaseCustomURL" "${SANESEC_MIRROR}/winnow_malware.hdb"
ensure_multivalue "$FRESHCLAM_CONF" "DatabaseCustomURL" "${SANESEC_MIRROR}/winnow_malware_links.ndb"
ensure_multivalue "$FRESHCLAM_CONF" "DatabaseCustomURL" "${SANESEC_MIRROR}/winnow_extended_malware.hdb"
ensure_multivalue "$FRESHCLAM_CONF" "DatabaseCustomURL" "${SANESEC_MIRROR}/winnow.attachments.hdb"
ensure_multivalue "$FRESHCLAM_CONF" "DatabaseCustomURL" "${SANESEC_MIRROR}/winnow_bad_cw.hdb"
ensure_multivalue "$FRESHCLAM_CONF" "DatabaseCustomURL" "https://urlhaus.abuse.ch/downloads/urlhaus.ndb"
# --- MEDIUM FP RISK — disabled by default, uncomment to enable ---
# DatabaseCustomURL ${SANESEC_MIRROR}/jurlbla.ndb
# DatabaseCustomURL ${SANESEC_MIRROR}/lott.ndb
# DatabaseCustomURL ${SANESEC_MIRROR}/spam.ldb
# DatabaseCustomURL ${SANESEC_MIRROR}/badmacro.ndb
# DatabaseCustomURL ${SANESEC_MIRROR}/shelter.ldb
# DatabaseCustomURL ${SANESEC_MIRROR}/spear.ndb
# DatabaseCustomURL ${SANESEC_MIRROR}/spearl.ndb
# DatabaseCustomURL ${SANESEC_MIRROR}/foxhole_js.cdb
# DatabaseCustomURL ${SANESEC_MIRROR}/foxhole_js.ndb
# DatabaseCustomURL ${SANESEC_MIRROR}/winnow_spam_complete.ndb
# DatabaseCustomURL ${SANESEC_MIRROR}/winnow_phish_complete_url.ndb
# DatabaseCustomURL ${SANESEC_MIRROR}/winnow.complex.patterns.ldb
# DatabaseCustomURL ${SANESEC_MIRROR}/winnow_extended_malware_links.ndb
# --- HIGH FP RISK — not recommended for workstations ---
# DatabaseCustomURL ${SANESEC_MIRROR}/foxhole_all.cdb
# DatabaseCustomURL ${SANESEC_MIRROR}/foxhole_all.ndb
# DatabaseCustomURL ${SANESEC_MIRROR}/foxhole_mail.cdb
# DatabaseCustomURL ${SANESEC_MIRROR}/winnow_phish_complete.ndb
# ─────────────────────────────────────────────────────────────────────────────
# SECTION 10 — Remove legacy cron job (if present from earlier script versions)
# ─────────────────────────────────────────────────────────────────────────────
section "Removing legacy cron job (if present)"
LEGACY_CRON="/etc/cron.d/clamav-scheduled"
if [[ -f "$LEGACY_CRON" ]]; then
if $DRY_RUN; then
warn "[DRY] Would remove legacy cron file: $LEGACY_CRON"
else
rm -f "$LEGACY_CRON"
warn "Removed legacy cron file: $LEGACY_CRON"
fi
else
info "No legacy cron file found — nothing to remove."
fi
# ─────────────────────────────────────────────────────────────────────────────
# SECTION 11 — Enable and start services
# ─────────────────────────────────────────────────────────────────────────────
section "Enabling and starting ClamAV services"
if $DRY_RUN; then
info "[DRY] Would enable and start clamav-freshclam.service"
info "[DRY] Would run an initial freshclam update"
info "[DRY] Would enable and start clamav-daemon.service"
else
info "Enabling clamav-freshclam.service..."
systemctl enable clamav-freshclam.service
info "Running initial freshclam update (this may take a moment)..."
freshclam --quiet || warn "Initial freshclam update failed — daemon may start with existing DB if present."
info "Starting clamav-freshclam.service..."
systemctl start clamav-freshclam.service
sleep 2
if systemctl is-active --quiet clamav-freshclam.service; then
info "SUCCESS: clamav-freshclam.service is active."
else
warn "clamav-freshclam.service did not start cleanly."
systemctl status clamav-freshclam.service || true
fi
info "Enabling clamav-daemon.service..."
systemctl enable clamav-daemon.service
info "Starting clamav-daemon.service..."
if systemctl start clamav-daemon.service; then
sleep 2
if systemctl is-active --quiet clamav-daemon.service; then
info "SUCCESS: clamav-daemon.service is active."
else
error "clamav-daemon failed to start."
systemctl status clamav-daemon.service
exit 1
fi
else
error "Failed to start clamav-daemon."
exit 1
fi
fi
# ─────────────────────────────────────────────────────────────────────────────
# SECTION 12 — EICAR test (functional validation)
# ─────────────────────────────────────────────────────────────────────────────
section "EICAR Functional Test"
if $DRY_RUN; then
info "[DRY] Would run EICAR detection test against clamd."
else
EICAR_FILE="/tmp/eicar_test_$$.txt"
printf 'X5O!P%%@AP[4\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*' > "${EICAR_FILE}"
sleep 1
info "Running EICAR test against clamd..."
SCAN_RESULT=$(clamdscan --fdpass --no-summary "$EICAR_FILE" 2>&1 || true)
if echo "$SCAN_RESULT" | grep -q "FOUND"; then
info "EICAR test: PASSED — clamd detected test signature correctly."
else
warn "EICAR test: FAILED or clamd not yet responding."
warn "Scan output: $SCAN_RESULT"
warn "Manual test: clamdscan --fdpass /tmp/eicar_test.txt"
fi
rm -f "$EICAR_FILE"
fi
# ─────────────────────────────────────────────────────────────────────────────
# SECTION 13 — Systemd Failure Notification
#
# FIX (v4.0.0): The previous version embedded all notification logic as an
# inline bash -c one-liner inside the systemd unit's ExecStart. This caused
# three distinct failures:
#
# 1. %i (systemd instance specifier) was not expanding because the unit
# was written with the literal string %i rather than being passed through
# systemd's specifier substitution at the ExecStart level.
#
# 2. hostname -f failed silently on LMDE where /etc/hosts lacks a proper
# FQDN entry, producing an empty HOST variable.
#
# 3. The %%Y-%%m-%%d double-escaping for systemd was correct in the unit
# file but the printf format string inside the bash -c layer consumed
# the escapes before the date command received them, producing blank
# timestamps.
#
# SOLUTION: ExecStart now calls a standalone helper script installed at
# /usr/local/lib/clamav-notify-failure.sh. The script runs in a clean bash
# environment with no systemd specifier or printf escaping complications.
# The unit passes the failed service name as $1 via the systemd %i specifier.
# hostname -s is used as a reliable fallback when hostname -f returns empty.
#
# The template remains reusable — any unit can add:
# OnFailure=notify-failure@%n.service
# ─────────────────────────────────────────────────────────────────────────────
section "Systemd Failure Notification"
NOTIFY_HELPER="/usr/local/lib/clamav-notify-failure.sh"
NOTIFY_UNIT="/etc/systemd/system/notify-failure@.service"
# --- Install the helper script ---
# This runs in a plain bash context — no systemd escaping, no printf layers.
# $1 is the failed unit name, passed via systemd %i specifier.
if $DRY_RUN; then
info "[DRY] Would install notify helper script at $NOTIFY_HELPER"
else
cat > "$NOTIFY_HELPER" << 'HELPER_EOF'
#!/bin/bash
# /usr/local/lib/clamav-notify-failure.sh
# Called by notify-failure@.service with the failed unit name as $1
# Sends a failure email via the local MTA (Postfix/sendmail)
UNIT="${1:-unknown-unit}"
# Use hostname -s as reliable fallback; -f can fail if FQDN is not configured
HOST="$(hostname -f 2>/dev/null || hostname -s 2>/dev/null || echo 'unknown-host')"
TIMESTAMP="$(date '+%Y-%m-%d %H:%M:%S')"
JOURNAL="$(journalctl -u "${UNIT}" -n 50 --no-pager 2>/dev/null || echo 'Journal unavailable')"
/usr/sbin/sendmail -t <<MAIL_EOF
Subject: [ClamAV FAILURE] ${UNIT} failed on ${HOST}
To: root
Content-Type: text/plain
Service failure alert
======================
Host: ${HOST}
Unit: ${UNIT}
Time: ${TIMESTAMP}
Last 50 journal lines:
${JOURNAL}
MAIL_EOF
HELPER_EOF
chmod 0755 "$NOTIFY_HELPER"
info "Installed: $NOTIFY_HELPER"
fi
# --- Install the systemd unit template ---
if $DRY_RUN; then
info "[DRY] Would install notify-failure@.service at $NOTIFY_UNIT"
else
cat > "$NOTIFY_UNIT" << 'UNIT_EOF'
[Unit]
Description=Failure notification for %i
After=network.target
[Service]
Type=oneshot
ExecStart=/usr/local/lib/clamav-notify-failure.sh %i
UNIT_EOF
chmod 0644 "$NOTIFY_UNIT"
info "Installed: $NOTIFY_UNIT"
fi
# --- Install OnFailure= drop-in overrides for both ClamAV units ---
for UNIT in clamav-daemon.service clamav-freshclam.service; do
DROPIN_DIR="/etc/systemd/system/${UNIT}.d"
DROPIN_FILE="${DROPIN_DIR}/notify-on-failure.conf"
if $DRY_RUN; then
info "[DRY] Would install OnFailure drop-in for ${UNIT} at ${DROPIN_FILE}"
else
mkdir -p "$DROPIN_DIR"
cat > "$DROPIN_FILE" << 'DROPIN_EOF'
[Unit]
OnFailure=notify-failure@%n.service
DROPIN_EOF
chmod 0644 "$DROPIN_FILE"
info "Installed drop-in: $DROPIN_FILE"
fi
done
# Reload systemd to pick up the new unit and drop-ins
if $DRY_RUN; then
info "[DRY] Would run systemctl daemon-reload"
else
systemctl daemon-reload
info "systemctl daemon-reload complete."
fi
# ─────────────────────────────────────────────────────────────────────────────
# COMPLETE
# ─────────────────────────────────────────────────────────────────────────────
section "Setup Complete"
echo ""
echo " ┌─────────────────────────────────────────────────────────────┐"
echo " │ ClamAV — Debian / LMDE │"
echo " ├─────────────────────────────────────────────────────────────┤"
echo " │ Services: clamav-daemon clamav-freshclam │"
echo " │ On-access: /home /tmp /var/tmp /root (BLOCKING) │"
echo " │ Quarantine: /var/quarantine/clamav │"
echo " │ Signatures: Every 4 hours via freshclam service │"
echo " │ Notify: /usr/local/lib/clamav-notify-failure.sh │"
echo " ├─────────────────────────────────────────────────────────────┤"
echo " │ Useful commands: │"
echo " │ systemctl status clamav-daemon │"
echo " │ systemctl status clamav-freshclam │"
echo " │ clamdscan --fdpass ~/ │"
echo " │ journalctl -u clamav-daemon -f │"
echo " └─────────────────────────────────────────────────────────────┘"
echo ""Update done
#enoughsaid