Security

Malware Scanning: Integrating Virus Scans into Automated Update Cycles

Wire malware scanning into your update cycle so every security patch is followed by a virus scan. Covers ClamAV on Linux and Defender on Windows.

Intermediate7 min read

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

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:

Bash
sudo apt-get update
sudo apt-get install -y clamav clamav-daemon
sudo systemctl enable --now clamav-freshclam clamav-daemon

On AlmaLinux or Rocky 8/9:

Bash
sudo dnf install -y clamav clamav-update clamd
sudo systemctl enable --now clamd@scan

Check that the daemon is up before you go further:

Bash
systemctl status clamav-daemon --no-pager

2. 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.

Bash
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.sh

An 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.

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

Confirm apt parsed the file:

Bash
apt-config dump | grep -i post-invoke

4. Hook it into dnf

Install the post-transaction plugin and add an action file:

Bash
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
EOF

The 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:

Bash
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.timer

Schedule 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:

PowerShell
$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

PowerShell
$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:

PowerShell
& "$env:ProgramFiles\Windows Defender\MpCmdRun.exe" -SignatureUpdate

Add 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:

Bash
sudo /usr/local/sbin/post-update-scan.sh; echo "exit: $?"
sudo tail -n 20 /var/log/post-update-scan.log

Then install a trivial package and confirm the hook fired:

Bash
sudo apt-get install -y --reinstall tzdata
systemctl status post-update-scan --no-pager

On Windows, start the task manually and check the history:

PowerShell
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 .conf inside /etc/apt/apt.conf.d/ and contain a valid DPkg::Post-Invoke stanza. Check with apt-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. Run systemctl status clamav-daemon and, if it will not start, fall back to clamscan -r in 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-run and that the systemd unit sets Nice=15 and IOSchedulingClass=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 20 and adjust the subscription.
  • Defender reports exit code 2. Signatures are missing or the scan was interrupted. Run MpCmdRun.exe -SignatureUpdate and 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

Was this guide helpful?

Still stuck?

Ask MdmHost AI for a quick answer, or open a ticket and our team will take a look. We are here 24/7.