{ config, lib, pkgs, ... }: with lib; let cfg = config.gortium.clamav; clamavPkg = pkgs.clamav; clamdConfig = pkgs.writeText "clamd.conf" '' LogFile /var/log/clamav/clamd.log LogTime yes LogVerbose yes LogSyslog yes LocalSocket /run/clamav/clamd.sock TCPSocket 3310 TCPAddr 127.0.0.1 User clamav AllowSupplementaryGroups yes ${cfg.clamdExtraConfig} ''; freshclamConfig = pkgs.writeText "freshclam.conf" '' DatabaseDirectory /var/lib/clamav LogFile /var/log/clamav/freshclam.log LogTime yes LogVerbose yes LogSyslog yes User clamav AllowSupplementaryGroups yes ${cfg.freshclamExtraConfig} ''; # Daily scan — logging only, no auto-quarantine/delete scanScript = pkgs.writeShellScript "clamav-daily-scan" '' set -e PATHS="${concatStringsSep " " cfg.scanPaths}" if [ -z "$PATHS" ]; then echo "No paths configured for daily scan" exit 0 fi echo "=== ClamAV daily scan started: $(date) ===" ${clamavPkg}/bin/clamdscan --fdpass --log=/var/log/clamav/daily-scan.log --no-summary $PATHS echo "=== ClamAV daily scan finished: $(date) ===" ''; in { ##### Options ##### options.gortium.clamav = { enable = mkEnableOption "ClamAV antivirus — installs clamav CLI tools"; enableDaemon = mkOption { type = types.bool; default = true; description = '' Run clamd daemon + freshclam updater + daily scheduled scan. Set to false on machines where you only want the CLI tools (clamscan, clamdscan) for manual on-demand scanning. ''; }; onAccessScanning = mkOption { type = types.bool; default = false; description = '' Enable on-access scanning via clamonacc (fanotify-based). Resource-heavy; server use only. Requires enableDaemon = true. ''; }; scanPaths = mkOption { type = types.listOf types.str; default = [ "/home" "/nix/store" "/var/lib" "/etc" "/tmp" "/var/tmp" ]; description = "Paths for the daily scheduled scan."; }; dailyScanTime = mkOption { type = types.str; default = "daily"; description = '' When to run the daily scan. systemd calendar expression or shortcuts like "daily", "weekly", "04:00". ''; }; clamdExtraConfig = mkOption { type = types.lines; default = ""; description = "Extra lines appended to clamd.conf"; }; freshclamExtraConfig = mkOption { type = types.lines; default = ""; description = "Extra lines appended to freshclam.conf"; }; }; ##### Implementation ##### config = mkIf cfg.enable { # 1. Package — always installed when enable = true environment.systemPackages = [ clamavPkg ]; # Everything below uses mkIf cfg.enableDaemon — conditionalized per attribute # 2. Users/groups (only if daemon runs) users.users.clamav = mkIf cfg.enableDaemon { isSystemUser = true; group = "clamav"; home = "/var/lib/clamav"; createHome = true; description = "ClamAV daemon user"; }; users.groups.clamav = mkIf cfg.enableDaemon {}; # 3. Directories (only if daemon runs) systemd.tmpfiles.rules = mkIf cfg.enableDaemon [ "d /var/lib/clamav 0750 clamav clamav -" "d /var/log/clamav 0750 clamav clamav -" "d /run/clamav 0755 clamav clamav -" ]; # 4. ClamAV daemon (clamd) systemd.services.clamav-daemon = mkIf cfg.enableDaemon { description = "ClamAV Anti-Virus Daemon"; after = [ "network.target" ]; wantedBy = [ "multi-user.target" ]; path = [ clamavPkg ]; preStart = '' mkdir -p /var/lib/clamav /var/log/clamav /run/clamav chown clamav:clamav /var/lib/clamav /var/log/clamav /run/clamav ''; serviceConfig = { Type = "simple"; ExecStart = "${clamavPkg}/bin/clamd --config-file=${clamdConfig}"; ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID"; Restart = "on-failure"; RestartSec = "10s"; User = "clamav"; Group = "clamav"; RuntimeDirectory = "clamav"; RuntimeDirectoryMode = "0755"; StateDirectory = "clamav"; StateDirectoryMode = "0750"; LogsDirectory = "clamav"; LogsDirectoryMode = "0750"; PrivateTmp = true; ProtectSystem = "strict"; ProtectHome = true; ReadWritePaths = [ "/var/lib/clamav" "/var/log/clamav" "/run/clamav" ]; NoNewPrivileges = true; }; }; # 5. freshclam (database updater) — hourly via timer systemd.services.clamav-freshclam = mkIf cfg.enableDaemon { description = "ClamAV Virus Database Updater"; after = [ "network.target" ]; wantedBy = [ "multi-user.target" ]; path = [ clamavPkg pkgs.curl ]; serviceConfig = { Type = "oneshot"; ExecStart = "${clamavPkg}/bin/freshclam --config-file=${freshclamConfig} --daemon-notify=${clamdConfig}"; User = "clamav"; Group = "clamav"; PrivateTmp = true; ProtectSystem = "full"; NoNewPrivileges = true; }; }; systemd.timers.clamav-freshclam = mkIf cfg.enableDaemon { description = "ClamAV database update timer"; wantedBy = [ "timers.target" ]; timerConfig = { OnCalendar = "hourly"; Persistent = true; RandomizedDelaySec = "1800"; }; }; # 6. Daily scan — logging only, no auto-quarantine systemd.services.clamav-daily-scan = mkIf cfg.enableDaemon { description = "ClamAV Daily Scheduled Scan"; after = [ "clamav-daemon.service" ]; requires = [ "clamav-daemon.service" ]; path = [ clamavPkg ]; serviceConfig = { Type = "oneshot"; ExecStart = "${scanScript}"; User = "clamav"; Group = "clamav"; PrivateTmp = true; ProtectSystem = "strict"; ReadWritePaths = [ "/var/log/clamav" ]; }; }; systemd.timers.clamav-daily-scan = mkIf cfg.enableDaemon { description = "ClamAV daily scan timer"; wantedBy = [ "timers.target" ]; timerConfig = { OnCalendar = cfg.dailyScanTime; Persistent = true; }; }; # 7. On-access scanning (clamonacc) — needs enableDaemon systemd.services.clamav-onaccess = mkIf (cfg.enableDaemon && cfg.onAccessScanning) { description = "ClamAV On-Access Scanner (clamonacc)"; after = [ "clamav-daemon.service" ]; requires = [ "clamav-daemon.service" ]; wantedBy = [ "multi-user.target" ]; path = [ clamavPkg ]; serviceConfig = { Type = "simple"; ExecStart = "${clamavPkg}/bin/clamonacc --config-file=${clamdConfig} --fdpass --log=/var/log/clamav/clamonacc.log"; Restart = "on-failure"; RestartSec = "10s"; User = "root"; # clamonacc needs root for fanotify Group = "root"; PrivateTmp = true; NoNewPrivileges = true; }; }; }; }