ClamAV - Hardening version 2

ClamAV - Hardening version 2
Linux Hardening

Well its that time of year to review all this again.

Linux is not getting any safer. Below is the script after a few teething bugs, put through the AI and further enhanced. It runs on all laptops within this house.

Please note that the script hardens an existing installation and does not do the installation itself. Please refer to standard documentation on how to do that.

Hardening Script

As per below

#!/bin/bash

# ─────────────────────────────────────────────────────────────────────────────
# setup-clamav.sh
# Harden ClamAV configuration for Debian / Linux Mint systems
# ─────────────────────────────────────────────────────────────────────────────
#
# PURPOSE
#   Applies a hardened, best-practice ClamAV configuration focused on
#   real-time workstation protection, home directory defence, and
#   scheduled full-system sweeps. Designed for Linux Mint laptops.
#
# BEHAVIOUR
#   - Idempotent: safely re-runnable; only applies changes that are missing
#     or differ from the desired state — will not duplicate directives
#   - Supports --dry-run mode: validates what would change without touching
#     any files or services  →  ./setup-clamav.sh --dry-run
#   - Must be run as root
#   - Audit-style section comments included for compliance traceability
#
# PROTECTION COVERAGE
#   Real-time (on-access, BLOCKING):  /home  /tmp  /var/tmp  /root
#   Daily scheduled scan:             /home                   02:30 AM
#   Weekly scheduled scan:            /tmp  /var/tmp          Sun 03:00 AM
#   Quarantine destination:           /var/quarantine/clamav  (mode 0700)
#   Signature updates:                Every 6 hours via cron
#   Freshclam self-check cadence:     Every 1 hour
#
# Updated: 11-02-2026
# Version: 2.0.0
# ─────────────────────────────────────────────────────────────────────────────

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"; }

# --- Root privilege check ---
if [[ "${EUID}" -ne 0 ]]; then
    error "This script must be run as root."
    exit 1
fi

DRY_RUN=false
if [[ "${1:-}" == "--dry-run" ]]; then
    DRY_RUN=true
    info "Dry-run mode enabled: no changes will be applied."
fi

# --- Paths ---
CLAMD_CONF="/etc/clamav/clamd.conf"
FRESHCLAM_CONF="/etc/clamav/freshclam.conf"
LOGFILE="/var/log/clamav/harden_clamav.log"
CRON_FILE="/etc/cron.d/clamav-scheduled"
QUARANTINE_DIR="/var/quarantine/clamav"
SCAN_LOG="/var/log/clamav/daily_scan.log"

# --- 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

# ─────────────────────────────────────────────────────────────────────────────
# ensure_config: idempotently set a directive in a config file.
# Handles the case where the key already exists (replaces) or is absent (appends).
# ─────────────────────────────────────────────────────────────────────────────
ensure_config() {
    local file="$1"
    local key="$2"
    local 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
# (e.g. OnAccessIncludePath, ExcludePath, OnAccessExcludePath).
# Uses exact string matching — will not add a duplicate line.
# ─────────────────────────────────────────────────────────────────────────────
ensure_multivalue() {
    local file="$1"
    local directive="$2"
    local 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
}

# ─────────────────────────────────────────────────────────────────────────────
# SECTION 0 — Purge Stale / Invalid Directives
# Actively removes directives that no longer exist or have been renamed.
# Prevents parse failures on re-runs against configs written by older scripts.
# ─────────────────────────────────────────────────────────────────────────────
section "Purging stale/invalid directives"

STALE_DIRECTIVES=(
    "LogCleanFiles"          # removed in ClamAV 0.99
    "AlertXXE"               # never a valid directive — erroneously added in script v2.0
    "AlgorithmicDetection"   # deprecated 0.101, superseded by HeuristicAlerts
    "OnAccessExtraScanning"  # permanently disabled since 0.100.2 due to kernel bug
)

for directive in "${STALE_DIRECTIVES[@]}"; do
    if grep -qE "^[[:space:]]*${directive}[[:space:]]" "$CLAMD_CONF"; then
        if $DRY_RUN; then
            info "[DRY] Would purge stale directive: $directive"
        else
            sed -i "/^[[:space:]]*${directive}[[:space:]]/d" "$CLAMD_CONF"
            warn "Purged stale directive: $directive"
        fi
    else
        info "Stale directive not present (clean): $directive"
    fi
done

# OnAccessIncludePath is multi-value — remove ALL existing instances so the
# ensure_multivalue calls below write them cleanly without duplicates
if grep -qE "^[[:space:]]*OnAccessIncludePath[[:space:]]" "$CLAMD_CONF"; then
    if $DRY_RUN; then
        info "[DRY] Would remove all existing OnAccessIncludePath lines for clean rewrite"
    else
        sed -i "/^[[:space:]]*OnAccessIncludePath[[:space:]]/d" "$CLAMD_CONF"
        warn "Cleared all OnAccessIncludePath lines — will be rewritten cleanly below"
    fi
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"   # 50 MB
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"
# NOTE: LogCleanFiles was removed in ClamAV 0.99 — do not add it

# ─────────────────────────────────────────────────────────────────────────────
# SECTION 3 — ExcludePath (scheduled scan and daemon)
# Excludes virtual/kernel filesystems and container storage to avoid false
# positives and unnecessary CPU load. These are NOT excluded from on-access.
# ─────────────────────────────────────────────────────────────────────────────
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
# NOTE: /var/infected/ removed — replaced by structured $QUARANTINE_DIR above

# ─────────────────────────────────────────────────────────────────────────────
# 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
# NOTE: OfficialDatabaseOnly is set to 'no' here to allow third-party
# signature databases (e.g. MalwarePatrol, Sanesecurity). If you use only
# official signatures, change this to 'yes'.
# ─────────────────────────────────────────────────────────────────────────────
section "Database Hygiene"
# CHANGE from v1: was 'yes', now 'no' to allow third-party signature databases
# Comment out the line below and set to 'yes' if you use ONLY official DBs
ensure_config "$CLAMD_CONF" "OfficialDatabaseOnly"     "no"
ensure_config "$CLAMD_CONF" "FailIfCvdOlderThan"       "3"    # TIGHTENED: was 7 days
ensure_config "$CLAMD_CONF" "ConcurrentDatabaseReload" "yes"
ensure_config "$CLAMD_CONF" "SelfCheck"                "1800" # TIGHTENED: was 3600 (30 min)

# ─────────────────────────────────────────────────────────────────────────────
# SECTION 6 — Detection Refinements
# ─────────────────────────────────────────────────────────────────────────────
section "Detection Refinements"
# NOTE: AlgorithmicDetection was deprecated in ClamAV 0.101 and replaced by HeuristicAlerts
# Do NOT set AlgorithmicDetection — it will be removed by the purge block above
ensure_config "$CLAMD_CONF" "DetectPUA"               "yes"
ensure_config "$CLAMD_CONF" "HeuristicAlerts"          "yes"   # replaces AlgorithmicDetection
ensure_config "$CLAMD_CONF" "HeuristicScanPrecedence"  "yes"

# ─────────────────────────────────────────────────────────────────────────────
# SECTION 7 — File & Scan Limits
# Balances coverage vs performance on typical laptop workloads
# ─────────────────────────────────────────────────────────────────────────────
section "File and Scan Limits"
ensure_config "$CLAMD_CONF" "MaxScanSize"          "209715200" # INCREASED: 200 MB (was 100 MB)
ensure_config "$CLAMD_CONF" "MaxFileSize"           "52428800"  # INCREASED: 50 MB (was 25 MB)
ensure_config "$CLAMD_CONF" "MaxRecursion"          "16"
ensure_config "$CLAMD_CONF" "MaxFiles"              "15000"     # INCREASED: was 10000
ensure_config "$CLAMD_CONF" "MaxEmbeddedPE"         "10485760"  # 10 MB — PE executables in archives
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"  # INCREASED: 25 MB (was 10 MB)

# ─────────────────────────────────────────────────────────────────────────────
# SECTION 8 — On-Access Scanning (real-time workstation protection)
#
# This is the critical section for home directory protection.
# OnAccessPrevention BLOCKS access to detected files rather than just alerting.
# OnAccessExtraScanning catches more events (chmod, attr changes etc.)
# Requires kernel fanotify support (Linux Mint 20+ uses kernel 5.4+, supported).
#
# NOTE: clamav-daemon must be run as root for fanotify to work.
# ─────────────────────────────────────────────────────────────────────────────
section "On-Access Scanning (Home Directory Protection)"

# Include all home directories and common download/staging targets
# OnAccessIncludePath is a MULTI-VALUE directive — must use ensure_multivalue,
# NOT ensure_config (which would collapse all entries into one line via sed)
for oa_path in "/home" "/tmp" "/var/tmp" "/root"; do
    ensure_multivalue "$CLAMD_CONF" "OnAccessIncludePath" "$oa_path"
done

# NEW: Block access to infected files (not just alert)
ensure_config "$CLAMD_CONF" "OnAccessPrevention"      "yes"

# NOTE: OnAccessExtraScanning was permanently disabled in ClamAV 0.100.2 due to a
# kernel resource cleanup bug and does nothing even if set — omitted to avoid warnings

# NEW: Disable scanning of files written/read by clamd itself (prevents loops)
ensure_config "$CLAMD_CONF" "OnAccessExcludeUID"      "0"

# Exclude virtual filesystems from on-access
for path in "/proc" "/sys" "/dev" "/run" "$QUARANTINE_DIR"; do
    ensure_multivalue "$CLAMD_CONF" "OnAccessExcludePath" "$path"
done

# ─────────────────────────────────────────────────────────────────────────────
# SECTION 9 — Freshclam (signature update frequency)
# ─────────────────────────────────────────────────────────────────────────────
section "Freshclam Settings"
# 24 checks/day = every hour; safe for laptops, stays within ClamAV rate limits
ensure_config "$FRESHCLAM_CONF" "Checks"              "24"     # INCREASED: was 12
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"  # triggers live DB reload

# ─────────────────────────────────────────────────────────────────────────────
# SECTION 9b — Third-Party Signatures via DatabaseCustomURL
#
# Freshclam natively supports pulling third-party signature databases via
# DatabaseCustomURL — no wrapper scripts, no pip, no external tooling.
# Freshclam downloads, verifies and manages these files exactly like official
# signatures. OfficialDatabaseOnly must be 'no' (already set in Section 5).
#
# SOURCES USED:
#   Sanesecurity  — active since 2006, hourly updates, free. Covers phishing,
#                   malware, macros, spam and zero-day threats.
#                   Mirror: https://mirror.rollernet.us/sanesecurity/
#                   (Community mirror — primary rsync.sanesecurity.net is
#                    rsync-only which freshclam cannot use directly)
#   URLhaus       — abuse.ch project, updated every minute, covers active
#                   malware distribution URLs. Free, no account required.
#                   https://urlhaus.abuse.ch/downloads/urlhaus.ndb
#
# FALSE POSITIVE RISK (sourced from sanesecurity.com/usage/signatures/):
#   Low risk DBs only are included below. Medium/High FP DBs are commented
#   out with their risk noted — enable only if you understand the trade-off.
#
# MEMORY NOTE: Each database increases clamd RAM usage. The selection below
# adds approximately 150-250 MB. Monitor with: systemctl status clamav-daemon
# ─────────────────────────────────────────────────────────────────────────────
section "Third-Party Signatures (DatabaseCustomURL)"

SANESEC_MIRROR="https://mirror.rollernet.us/sanesecurity"

# --- Required Sanesecurity support files (must be loaded first) ---
ensure_multivalue "$FRESHCLAM_CONF" "DatabaseCustomURL" "${SANESEC_MIRROR}/sanesecurity.ftm"
ensure_multivalue "$FRESHCLAM_CONF" "DatabaseCustomURL" "${SANESEC_MIRROR}/sigwhitelist.ign2"

# --- Low FP risk: Sanesecurity core databases ---
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"

# --- Low FP risk: Foxhole (executable/archive content detection) ---
ensure_multivalue "$FRESHCLAM_CONF" "DatabaseCustomURL" "${SANESEC_MIRROR}/foxhole_generic.cdb"
ensure_multivalue "$FRESHCLAM_CONF" "DatabaseCustomURL" "${SANESEC_MIRROR}/foxhole_filename.cdb"

# --- Low FP risk: OITC winnow databases (distributed via Sanesecurity) ---
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"

# --- URLhaus: active malware distribution URLs (abuse.ch) — Low FP risk ---
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          # Junk URLs autogenerated — Med FP
# DatabaseCustomURL ${SANESEC_MIRROR}/lott.ndb             # Lottery spam — Med FP
# DatabaseCustomURL ${SANESEC_MIRROR}/spam.ldb             # Spam logical sigs — Med FP
# DatabaseCustomURL ${SANESEC_MIRROR}/badmacro.ndb         # Office macros — Med FP
# DatabaseCustomURL ${SANESEC_MIRROR}/shelter.ldb          # Phishing/malware — Med FP
# DatabaseCustomURL ${SANESEC_MIRROR}/spear.ndb            # Spear phishing — Med FP
# DatabaseCustomURL ${SANESEC_MIRROR}/spearl.ndb           # Spear phishing URLs — Med FP
# DatabaseCustomURL ${SANESEC_MIRROR}/foxhole_js.cdb       # JS content — Med FP
# DatabaseCustomURL ${SANESEC_MIRROR}/foxhole_js.ndb       # JS content — Med FP
# DatabaseCustomURL ${SANESEC_MIRROR}/winnow_spam_complete.ndb       # Spam — Med FP
# DatabaseCustomURL ${SANESEC_MIRROR}/winnow_phish_complete_url.ndb  # Phishing URLs — Med FP
# DatabaseCustomURL ${SANESEC_MIRROR}/winnow.complex.patterns.ldb    # Malware patterns — Med FP
# DatabaseCustomURL ${SANESEC_MIRROR}/winnow_extended_malware_links.ndb  # Malware links — Med FP

# --- HIGH FP RISK — not recommended for workstations, do not enable ---
# DatabaseCustomURL ${SANESEC_MIRROR}/foxhole_all.cdb      # All content types — High FP
# DatabaseCustomURL ${SANESEC_MIRROR}/foxhole_all.ndb      # All content types — High FP
# DatabaseCustomURL ${SANESEC_MIRROR}/foxhole_mail.cdb     # Mail content — High FP
# DatabaseCustomURL ${SANESEC_MIRROR}/winnow_phish_complete.ndb  # Phishing — High FP


# Cron manages: freshclam updates + daily home directory full scan
# ─────────────────────────────────────────────────────────────────────────────
section "Cron Schedule"

CRON_CONTENT="# Managed by setup-clamav.sh — do not edit manually
SHELL=/bin/bash
PATH=/usr/sbin:/usr/bin:/sbin:/bin

# ClamAV signature updates (every 6 hours)
0 */6 * * *   root  /usr/bin/freshclam --quiet 2>&1 | logger -t freshclam

# Daily full scan of all home directories (02:30 AM)
# Moves infected files to quarantine; logs to $SCAN_LOG
30 2 * * *    root  /usr/bin/clamdscan --fdpass --recursive --move=${QUARANTINE_DIR} /home >> ${SCAN_LOG} 2>&1

# Weekly full scan of /tmp and /var/tmp (Sunday 03:00 AM)
0 3 * * 0     root  /usr/bin/clamdscan --fdpass --recursive --move=${QUARANTINE_DIR} /tmp /var/tmp >> ${SCAN_LOG} 2>&1
"

if $DRY_RUN; then
    info "[DRY] Would write cron file at $CRON_FILE"
    echo "--- Cron content preview ---"
    echo "$CRON_CONTENT"
    echo "---"
else
    printf "%s" "$CRON_CONTENT" > "$CRON_FILE"
    chmod 0644 "$CRON_FILE"
    chown root:root "$CRON_FILE"
    info "Installed cron schedule: $CRON_FILE"
fi

# ─────────────────────────────────────────────────────────────────────────────
# SECTION 11 — Logrotate for ClamAV logs
# ─────────────────────────────────────────────────────────────────────────────
section "Logrotate Configuration"

LOGROTATE_FILE="/etc/logrotate.d/clamav-custom"
LOGROTATE_CONTENT="/var/log/clamav/*.log {
    weekly
    rotate 8
    compress
    delaycompress
    missingok
    notifempty
    create 0640 clamav clamav
    sharedscripts
    postrotate
        systemctl reload clamav-daemon.service 2>/dev/null || true
    endscript
}
"

if $DRY_RUN; then
    info "[DRY] Would write logrotate config at $LOGROTATE_FILE"
else
    printf "%s" "$LOGROTATE_CONTENT" > "$LOGROTATE_FILE"
    chmod 0644 "$LOGROTATE_FILE"
    info "Installed logrotate config: $LOGROTATE_FILE"
fi

# ─────────────────────────────────────────────────────────────────────────────
# SECTION 12 — Disable systemd freshclam service (cron manages updates)
# ─────────────────────────────────────────────────────────────────────────────
section "Disabling systemd freshclam (cron takes over)"

FRESHCLAM_UNIT="clamav-freshclam.service"

if $DRY_RUN; then
    info "[DRY] Would disable and stop $FRESHCLAM_UNIT (cron manages updates)"
else
    if systemctl is-enabled --quiet "$FRESHCLAM_UNIT" 2>/dev/null; then
        info "Disabling $FRESHCLAM_UNIT — cron manages updates..."
        systemctl disable --now "$FRESHCLAM_UNIT" || warn "Could not disable $FRESHCLAM_UNIT (continuing)"
    else
        info "$FRESHCLAM_UNIT already disabled."
        systemctl stop "$FRESHCLAM_UNIT" 2>/dev/null || true
    fi
fi

# ─────────────────────────────────────────────────────────────────────────────
# SECTION 13 — Restart and validate clamd
# ─────────────────────────────────────────────────────────────────────────────
section "Restarting clamd"

if $DRY_RUN; then
    info "Dry-run complete. No services were restarted."
else
    if systemctl restart clamav-daemon.service; then
        sleep 2
        if systemctl is-active --quiet clamav-daemon.service; then
            info "SUCCESS: clamav-daemon is active."
        else
            error "clamav-daemon failed to start after restart."
            systemctl status clamav-daemon.service
            exit 1
        fi
    else
        error "Failed to restart clamav-daemon."
        exit 1
    fi

    # Validate on-access scanning is operational
    section "Validating On-Access Scanning"
    if systemctl is-active --quiet clamav-daemon.service; then
        OA_STATUS=$(clamconf 2>/dev/null | grep -i "OnAccess" || echo "clamconf not available")
        info "On-access config status: $OA_STATUS"
    fi

    # Quick test: scan EICAR test string in /tmp (non-destructive)
    # Written as a Python one-liner to avoid shell escaping issues with the EICAR string
    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*')"
    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 responding."
        warn "Scan output: $SCAN_RESULT"
        warn "Check: clamdscan --fdpass /tmp/eicar_test.txt  (manual test)"
    fi
    rm -f "$EICAR_FILE"
fi

# ─────────────────────────────────────────────────────────────────────────────
# SECTION 14 — Post-install Summary
# ─────────────────────────────────────────────────────────────────────────────
section "Summary"
cat <<'EOF'
  ClamAV Hardening Complete
  ─────────────────────────────────────────────────────────────────
  On-Access Scanning:   /home, /tmp, /var/tmp, /root  [BLOCKING]
  Daily Scheduled Scan: /home                          02:30 AM
  Weekly Scheduled Scan: /tmp, /var/tmp                Sun 03:00 AM
  Quarantine Directory: /var/quarantine/clamav
  Signature Updates:    Every 6 hours (cron)
  Freshclam checks:     Every 1 hour (daemon self-scheduling)
  ─────────────────────────────────────────────────────────────────
  Third-Party Signatures (via freshclam DatabaseCustomURL):
    Sanesecurity  — phishing, malware, macros, spam, zero-day
                    (mirror.rollernet.us — community mirror)
    URLhaus       — active malware distribution URLs (abuse.ch)
    All Low FP risk databases enabled. Medium/High FP databases
    are present in freshclam.conf as commented-out entries.
  ─────────────────────────────────────────────────────────────────
  IMPORTANT: On-access scanning requires clamav-daemon to run as
  root and the kernel to support fanotify (Linux Mint 20+ / kernel
  5.4+). Verify with: systemctl status clamav-daemon
  ─────────────────────────────────────────────────────────────────
  clamav-unofficial-sigs (extremeshok) is ABANDONED — do not use.
  Fangfrisch is the maintained alternative if DatabaseCustomURL
  proves insufficient: https://github.com/rseichter/fangfrisch
  ─────────────────────────────────────────────────────────────────
EOF

# End of script

Warning

If you let the official ClamAV database, get older than 3 days - the daemon won't start

It might be wiser to modify that parameter above, however, to fix run the following commands. This will pull a fresh version of the database and restart the daemon.

  1. sudo freshclam
  2. sudo systemctl start clamav-daemon.service && systemctl status clamav-daemon.service

Hope this helps

Now back to the LXC Application server. I found another hole. Rather annoyed.

On the plus side - the LXC proxy script worked a treat. About 15 minutes to rebuild and restore from the Zoraxy backup file. Version bumped to latest.

#enoughsaid