An automated malware scan after updates closes the gap between "patched" and "clean". Attackers often leave a payload behind before you install the fix, so a scan that runs minutes after the patch lands catches what the patch alone does not remove. This guide hooks ClamAV into apt and dnf on Linux, and triggers a Windows Defender scan from Task Scheduler on Windows Server.
The approach is the same on both platforms: let the package manager or the update service finish, then start a scan as a separate, low-priority job so it never blocks the update itself.
Before you start
- Root or Administrator access to the server. On Linux, all commands below assume
sudo. - A working update path. If you have not set one up yet, read Linux Update Strategy: Testing Security Patches Before Production or Windows Update on a VPS: patches and maintenance windows first.
- Roughly 2 GB of free disk space on Linux for ClamAV signatures and logs.
- A maintenance window. A full filesystem scan reads every file, so it competes with your workload for disk I/O.
Warning: A full scan on a busy server can push disk I/O to 100% for a long time. Schedule it for a quiet period, and never run it inside the same synchronous step as the update.
Run an automated malware scan after updates on Linux
1. Install ClamAV and the daemon
On Ubuntu 22.04/24.04 and Debian 12:
sudo apt-get update
sudo apt-get install -y clamav clamav-daemon
sudo systemctl enable --now clamav-freshclam clamav-daemonOn AlmaLinux or Rocky 8/9:
sudo dnf install -y clamav clamav-update clamd
sudo systemctl enable --now clamd@scanCheck that the daemon is up before you go further:
systemctl status clamav-daemon --no-pager2. Create the scan script
The script uses clamdscan, which reuses the already-loaded signature database and is far faster than clamscan. It also takes a lock so overlapping update events cannot start two scans at once.
sudo tee /usr/local/sbin/post-update-scan.sh >/dev/null <<'EOF'
#!/usr/bin/env bash
set -uo pipefail
LOG=/var/log/post-update-scan.log
REPORT=/var/log/clamav/post-update-scan.txt
exec 9>/run/post-update-scan.lock
flock -n 9 || { echo "scan already running, skipping"; exit 0; }
mkdir -p "$(dirname "$REPORT")"
exec >>"$LOG" 2>&1
echo "=== scan started $(date -Is) ==="
freshclam --quiet || echo "freshclam failed, using existing signatures"
rc=0
clamdscan --multiscan --fdpass --infected / \
--exclude-dir='^/(proc|sys|dev|run)' \
--log="$REPORT" || rc=$?
echo "scan finished $(date -Is), exit code: $rc"
echo "=== end ==="
exit "$rc"
EOF
sudo chmod 0750 /usr/local/sbin/post-update-scan.shAn exit code of 1 means infected files were found. 2 means an error. Anything else is clean.
3. Hook it into apt
apt runs DPkg::Post-Invoke commands after every dpkg transaction. Detach the scan with systemd-run so apt returns immediately.
sudo tee /etc/apt/apt.conf.d/99-post-update-scan >/dev/null <<'EOF'
DPkg::Post-Invoke { "systemd-run --unit=post-update-scan --collect --nice=15 /usr/local/sbin/post-update-scan.sh"; };
EOFConfirm apt parsed the file:
apt-config dump | grep -i post-invoke4. Hook it into dnf
Install the post-transaction plugin and add an action file:
sudo dnf install -y dnf-plugin-post-transaction-actions
sudo tee /etc/dnf/plugins/post-transaction-actions.d/scan.action >/dev/null <<'EOF'
*:any:/usr/local/sbin/post-update-scan.sh
EOFThe plugin fires once per matching package, so the lock in the script is what stops a hundred parallel scans. If you prefer one scan per transaction, replace the action with a systemd timer that runs shortly after dnf-automatic finishes.
5. Add a weekly backstop
Update hooks only fire when something is installed. Add a timer so a quiet week still gets scanned:
sudo systemctl edit --force --full post-update-scan.service <<'EOF'
[Unit]
Description=Post-update malware scan
[Service]
Type=oneshot
Nice=15
IOSchedulingClass=idle
ExecStart=/usr/local/sbin/post-update-scan.sh
EOF
sudo systemctl edit --force --full post-update-scan.timer <<'EOF'
[Unit]
Description=Weekly post-update malware scan
[Timer]
OnCalendar=Sun 03:30
Persistent=true
[Install]
WantedBy=timers.target
EOF
sudo systemctl enable --now post-update-scan.timerSchedule a Defender scan after Windows Update
Windows Update Client writes event ID 19 to Microsoft-Windows-WindowsUpdateClient/Operational when an update installs successfully. Task Scheduler can trigger on that event.
1. Create the wrapper script
MpCmdRun does not write a log file on its own, so wrap it:
$dir = 'C:\ProgramData\Maintenance'
New-Item -ItemType Directory -Path $dir -Force | Out-Null
@'
$log = 'C:\ProgramData\Maintenance\post-update-scan.log'
"=== scan started $(Get-Date -Format o) ===" | Add-Content $log
& "$env:ProgramFiles\Windows Defender\MpCmdRun.exe" -Scan -ScanType 2 *>> $log
"exit code: $LASTEXITCODE" | Add-Content $log
'@ | Set-Content -Path "$dir\post-update-scan.ps1" -Encoding UTF8-ScanType 2 is a full scan. Use -ScanType 1 for a quick scan if full scans are too heavy for the server's role.
2. Register the scheduled task
$action = New-ScheduledTaskAction -Execute 'powershell.exe' `
-Argument '-NoProfile -ExecutionPolicy Bypass -File C:\ProgramData\Maintenance\post-update-scan.ps1'
$cim = Get-CimClass -Namespace root/Microsoft/Windows/TaskScheduler -ClassName MSFT_TaskEventTrigger
$trigger = New-CimInstance -CimClass $cim -ClientOnly
$trigger.Enabled = $true
$trigger.Subscription = '<QueryList><Query Id="0" Path="Microsoft-Windows-WindowsUpdateClient/Operational"><Select Path="Microsoft-Windows-WindowsUpdateClient/Operational">*[System[EventID=19]]</Select></Query></QueryList>'
$principal = New-ScheduledTaskPrincipal -UserId 'SYSTEM' -LogonType ServiceAccount -RunLevel Highest
Register-ScheduledTask -TaskName 'Post-Update Malware Scan' `
-Action $action -Trigger $trigger -Principal $principal `
-Settings (New-ScheduledTaskSettingsSet -StartWhenAvailable -ExecutionTimeLimit (New-TimeSpan -Hours 4))Note: Event ID 19 means a successful install. Some updates report under a different ID depending on the Windows Server build, so verify the current event IDs in Microsoft's documentation before you rely on the trigger.
3. Keep signatures current
Defender updates signatures on its own, but a forced update before the scan avoids scanning with stale definitions:
& "$env:ProgramFiles\Windows Defender\MpCmdRun.exe" -SignatureUpdateAdd that line to the top of post-update-scan.ps1 if your server has long gaps between updates.
Check it works
On Linux, run the script by hand and read the log:
sudo /usr/local/sbin/post-update-scan.sh; echo "exit: $?"
sudo tail -n 20 /var/log/post-update-scan.logThen install a trivial package and confirm the hook fired:
sudo apt-get install -y --reinstall tzdata
systemctl status post-update-scan --no-pagerOn Windows, start the task manually and check the history:
Start-ScheduledTask -TaskName 'Post-Update Malware Scan'
Get-Content C:\ProgramData\Maintenance\post-update-scan.log -Tail 20
Get-ScheduledTaskInfo -TaskName 'Post-Update Malware Scan'A clean Defender scan returns exit code 0. Code 2 means the scan could not complete.
Troubleshooting
- The apt hook never runs. The file must end in
.confinside/etc/apt/apt.conf.d/and contain a validDPkg::Post-Invokestanza. Check withapt-config dump | grep -i post-invoke; if nothing prints, the syntax is wrong. clamdscan: Could not connect to clamd. The daemon is stopped or its socket path changed. Runsystemctl status clamav-daemonand, if it will not start, fall back toclamscan -rin the script until you fix the service.- The update job hangs or the server becomes unresponsive. The scan is running in the foreground. Make sure the apt hook uses
systemd-runand that the systemd unit setsNice=15andIOSchedulingClass=idle. - The Task Scheduler event trigger never fires. The event log path or event ID is wrong for your build. Confirm with
Get-WinEvent -LogName 'Microsoft-Windows-WindowsUpdateClient/Operational' -MaxEvents 20and adjust the subscription. - Defender reports exit code 2. Signatures are missing or the scan was interrupted. Run
MpCmdRun.exe -SignatureUpdateand check that the task's execution time limit is long enough. - ClamAV quarantines files in your web root. Do not delete blindly. Review the report, confirm the detection, and see Malware Scanning on Linux: Configuring ClamAV for Web Root Protection for exclusions and safe handling.
Next steps
- Add integrity checking alongside signature scanning with Malware Scanning on Linux: Rootkit Detection and System Integrity Checks.
- Review what to do when a scan finds something: Incident Response: Preserving Forensic Evidence from a Compromised Linux Server.
- Tighten the accounts that can run these jobs: Linux Security: Hardening the sudoers File for Least Privilege and Advanced Windows Security: Restricting PowerShell Execution Policies.
