ClamAV - Hardening version 4

ClamAV - Hardening version 4
Linux Hardening

Update the ClamAV file to make it work for the following:

  1. Debian, MintOS and Bazzite
  2. 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

  1. Script on MintOS LMDE 7 - no errors
  2. Script run on Bazzite OS - whoops - fix - no errors
  3. 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

  1. Bazzite implementation of ClamAV via distrobox - seriously ugly
  2. Pulled that code from the script
  3. This script still has the code as I have run out of time
  4. I will fix the code but need to wait 5 hours as AI spat the dummy
  5. 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