#!/usr/bin/env bash ############################################################################### # Targeted SCA Remediation — Based on sca-verify.sh diagnostic output # # Fixes all FAIL items from the diagnostic. Does NOT touch items that # already PASS. Safe to re-run (idempotent). # # Usage: chmod +x sca-fix.sh && sudo ./sca-fix.sh ############################################################################### set -euo pipefail export DEBIAN_FRONTEND=noninteractive if [[ $EUID -ne 0 ]]; then echo "Run as root." >&2; exit 1; fi banner() { printf '\n\e[1;36m>>> %s\e[0m\n' "$1"; } ADMIN_USER="${ADMIN_USER:-chris}" ############################################################################### banner "FIX 1: Filesystem module blacklist (35509) — config file MISSING" ############################################################################### cat > /etc/modprobe.d/cis-filesystems.conf <<'EOF' install afs /bin/false install ceph /bin/false install cifs /bin/false install exfat /bin/false install fat /bin/false install fscache /bin/false install fuse /bin/false install gfs2 /bin/false install nfs_common /bin/false install nfsd /bin/false install smbfs_common /bin/false install cramfs /bin/false install freevxfs /bin/false install jffs2 /bin/false install hfs /bin/false install hfsplus /bin/false install squashfs /bin/false install udf /bin/false blacklist afs blacklist ceph blacklist cifs blacklist exfat blacklist fat blacklist fscache blacklist fuse blacklist gfs2 blacklist nfs_common blacklist nfsd blacklist smbfs_common blacklist cramfs blacklist freevxfs blacklist jffs2 blacklist hfs blacklist hfsplus blacklist squashfs blacklist udf EOF echo " ✓ /etc/modprobe.d/cis-filesystems.conf created" ############################################################################### banner "FIX 2: Network protocol module blacklist (35604-35607) — config file MISSING" ############################################################################### cat > /etc/modprobe.d/cis-network-protocols.conf <<'EOF' install dccp /bin/false install sctp /bin/false install rds /bin/false install tipc /bin/false blacklist dccp blacklist sctp blacklist rds blacklist tipc EOF echo " ✓ /etc/modprobe.d/cis-network-protocols.conf created" ############################################################################### banner "FIX 3: USB storage (uses /bin/true, should be /bin/false)" ############################################################################### cat > /etc/modprobe.d/cis-usb-storage.conf <<'EOF' install usb-storage /bin/false blacklist usb-storage EOF echo " ✓ /etc/modprobe.d/cis-usb-storage.conf created" ############################################################################### banner "FIX 4: Rebuild initramfs with new module blacklists" ############################################################################### update-initramfs -u echo " ✓ initramfs rebuilt" ############################################################################### banner "FIX 5: Sysctl — ip_forward and log_martians (35608, 35616)" ############################################################################### # Docker overrides ip_forward at runtime. To keep it disabled while # allowing Docker networking, we need to tell Docker not to touch it # and handle forwarding via iptables only. # However, if Docker is actively in use, disabling ip_forward breaks # container networking. The pragmatic fix: if command -v docker &>/dev/null; then echo " Docker is installed. ip_forward=1 is required for container networking." echo " This is a known conflict — document as compensating control." echo " Creating /etc/sysctl.d/99-docker-override.conf for non-forwarding params only." # Don't override ip_forward if Docker is present, but DO fix log_martians cat > /etc/sysctl.d/99-docker-override.conf <<'EOF' # Docker requires ip_forward=1 — accepted as compensating control # These params are safe to enforce alongside Docker: net.ipv4.conf.all.log_martians = 1 net.ipv4.conf.default.log_martians = 1 EOF else # No Docker — enforce everything sysctl -w net.ipv4.ip_forward=0 fi # Apply log_martians immediately regardless sysctl -w net.ipv4.conf.all.log_martians=1 sysctl -w net.ipv4.conf.default.log_martians=1 sysctl --system > /dev/null 2>&1 echo " ✓ log_martians enabled" echo " ✓ sysctl reloaded" ############################################################################### banner "FIX 6: Purge nftables package (35626, 35634)" ############################################################################### if dpkg -s nftables &>/dev/null; then apt-get purge -y nftables echo " ✓ nftables purged" else echo " ✓ nftables already absent" fi ############################################################################### banner "FIX 7: SSH config — merge drop-in into main config (35644-35661)" ############################################################################### # Wazuh scans /etc/ssh/sshd_config directly and doesn't parse drop-in files. # All settings PASS via sshd -T but FAIL in Wazuh because they're only in the drop-in. # Fix: append drop-in contents to main config and remove the drop-in. DROPIN="/etc/ssh/sshd_config.d/nist-hardening.conf" MAIN="/etc/ssh/sshd_config" if [[ -f "$DROPIN" ]]; then # Remove any existing lines in main config that we're about to add # (avoid duplicates) while IFS= read -r line; do # Skip comments and empty lines [[ "$line" =~ ^#|^$ ]] && continue param=$(echo "$line" | awk '{print $1}') # Remove existing uncommented instances of this param from main config sed -i "/^\s*${param}\b/d" "$MAIN" 2>/dev/null || true done < "$DROPIN" # Append the hardening config echo "" >> "$MAIN" echo "# ===== NIST 800-53 SSH Hardening (merged from drop-in) =====" >> "$MAIN" cat "$DROPIN" >> "$MAIN" # Remove the drop-in rm -f "$DROPIN" # Test and restart if sshd -t; then systemctl restart ssh echo " ✓ SSH config merged into $MAIN, drop-in removed, SSH restarted" else echo " ERROR: sshd config test failed. Check $MAIN" exit 1 fi else echo " ✓ No drop-in to merge (already in main config)" fi ############################################################################### banner "FIX 8: Password aging for root and nobody (35694, 35695, 35698)" ############################################################################### # root — set aging but keep locked (no password login) chage -M 60 -m 1 -W 14 -I 30 root echo " ✓ root: max=60 min=1 warn=14 inactive=30" # nobody — system account, set aging for compliance chage -M 60 -m 1 -W 14 -I 30 nobody echo " ✓ nobody: max=60 min=1 warn=14 inactive=30" ############################################################################### banner "FIX 9: Journald MaxRetentionSec (35708)" ############################################################################### JOURNALD_DROPIN="/etc/systemd/journald.conf.d/nist.conf" if [[ -f "$JOURNALD_DROPIN" ]]; then if ! grep -q 'MaxRetentionSec' "$JOURNALD_DROPIN"; then echo "MaxRetentionSec=1month" >> "$JOURNALD_DROPIN" echo " ✓ MaxRetentionSec added to drop-in" fi fi # Also add to main config since Wazuh may only read the main file JOURNALD_MAIN="/etc/systemd/journald.conf" if ! grep -q '^\s*SystemMaxUse=' "$JOURNALD_MAIN"; then # If the main config doesn't have our settings, add them sed -i '/^\[Journal\]/a SystemMaxUse=1G\nSystemKeepFree=1G\nSystemMaxFileSize=100M\nMaxRetentionSec=1month' "$JOURNALD_MAIN" echo " ✓ Journald settings added to main config for Wazuh" else if ! grep -q '^\s*MaxRetentionSec=' "$JOURNALD_MAIN"; then sed -i '/^\[Journal\]/a MaxRetentionSec=1month' "$JOURNALD_MAIN" echo " ✓ MaxRetentionSec added to main journald.conf" fi fi systemctl restart systemd-journald echo " ✓ journald restarted" ############################################################################### banner "FIX 10: Log file permissions (35722)" ############################################################################### # Fix dpkg.log and sysstat files find /var/log -type f -exec chmod g-wx,o-rwx {} + find /var/log -type d -exec chmod g-wx,o-rwx {} + echo " ✓ /var/log permissions tightened" # Prevent sysstat from creating world-readable files in the future if [[ -f /etc/sysstat/sysstat ]]; then # Set umask for sysstat if ! grep -q 'umask' /etc/sysstat/sysstat; then echo 'UMASK="0027"' >> /etc/sysstat/sysstat fi fi # Also set default umask in rsyslog if [[ -f /etc/rsyslog.d/99-nist-perms.conf ]]; then if ! grep -q 'DirCreateMode' /etc/rsyslog.d/99-nist-perms.conf; then echo '$DirCreateMode 0750' >> /etc/rsyslog.d/99-nist-perms.conf echo '$Umask 0027' >> /etc/rsyslog.d/99-nist-perms.conf fi fi echo " ✓ Future log file creation modes tightened" ############################################################################### banner "FIX 11: Audit rules file (35731-35748) — rules file MISSING" ############################################################################### # Rules are loaded (50 rules) but the file Wazuh scans doesn't exist. # The rules seem to be in /etc/audit/rules.d/cis-nist.rules instead of # nist-800-53.rules. Wazuh scans the rules.d directory, so we need to # ensure the rules file is there with the correct content. cat > /etc/audit/rules.d/nist-800-53.rules <<'AUDITRULES' ## ---- Remove any existing rules ---- -D ## ---- Buffer size ---- -b 8192 ## ---- Failure mode ---- -f 1 ## ---- Time changes (35734) ---- -a always,exit -F arch=b64 -S adjtimex,settimeofday,clock_settime -k time-change -a always,exit -F arch=b32 -S adjtimex,settimeofday,clock_settime -k time-change -w /etc/localtime -p wa -k time-change ## ---- User/group changes (35737) ---- -w /etc/passwd -p wa -k identity -w /etc/group -p wa -k identity -w /etc/shadow -p wa -k identity -w /etc/gshadow -p wa -k identity -w /etc/security/opasswd -p wa -k identity ## ---- Network configuration (35735) ---- -a always,exit -F arch=b64 -S sethostname,setdomainname -k network-config -a always,exit -F arch=b32 -S sethostname,setdomainname -k network-config -w /etc/hosts -p wa -k network-config -w /etc/hostname -p wa -k network-config -w /etc/netplan/ -p wa -k network-config -w /etc/network/ -p wa -k network-config -w /etc/networks -p wa -k network-config ## ---- Login/logout events (35741) ---- -w /var/log/lastlog -p wa -k logins -w /var/log/faillog -p wa -k logins -w /var/log/wtmp -p wa -k logins -w /var/log/btmp -p wa -k logins ## ---- Session initiation (35740) ---- -w /var/run/utmp -p wa -k session ## ---- Sudo scope changes (35731) ---- -w /etc/sudoers -p wa -k scope -w /etc/sudoers.d -p wa -k scope ## ---- Sudo log (35733) ---- -w /var/log/sudo.log -p wa -k actions ## ---- Actions as another user (35732) ---- -a always,exit -F arch=b64 -S execve -C euid!=uid -F auid!=unset -k user_emulation -a always,exit -F arch=b32 -S execve -C euid!=uid -F auid!=unset -k user_emulation ## ---- Privileged commands ---- -a always,exit -F arch=b64 -S execve -F euid=0 -F auid>=1000 -F auid!=4294967295 -k privileged -a always,exit -F arch=b32 -S execve -F euid=0 -F auid>=1000 -F auid!=4294967295 -k privileged ## ---- Unsuccessful file access (35736) ---- -a always,exit -F arch=b64 -S open,openat,creat,truncate,ftruncate -F exit=-EACCES -F auid>=1000 -F auid!=4294967295 -k access -a always,exit -F arch=b64 -S open,openat,creat,truncate,ftruncate -F exit=-EPERM -F auid>=1000 -F auid!=4294967295 -k access -a always,exit -F arch=b32 -S open,openat,creat,truncate,ftruncate -F exit=-EACCES -F auid>=1000 -F auid!=4294967295 -k access -a always,exit -F arch=b32 -S open,openat,creat,truncate,ftruncate -F exit=-EPERM -F auid>=1000 -F auid!=4294967295 -k access ## ---- DAC permission changes (35738) ---- -a always,exit -F arch=b64 -S chmod,fchmod,fchmodat -F auid>=1000 -F auid!=4294967295 -k perm_mod -a always,exit -F arch=b64 -S chown,fchown,lchown,fchownat -F auid>=1000 -F auid!=4294967295 -k perm_mod -a always,exit -F arch=b64 -S setxattr,lsetxattr,fsetxattr,removexattr,lremovexattr,fremovexattr -F auid>=1000 -F auid!=4294967295 -k perm_mod -a always,exit -F arch=b32 -S chmod,fchmod,fchmodat -F auid>=1000 -F auid!=4294967295 -k perm_mod -a always,exit -F arch=b32 -S chown,fchown,lchown,fchownat -F auid>=1000 -F auid!=4294967295 -k perm_mod -a always,exit -F arch=b32 -S setxattr,lsetxattr,fsetxattr,removexattr,lremovexattr,fremovexattr -F auid>=1000 -F auid!=4294967295 -k perm_mod ## ---- File system mounts (35739) ---- -a always,exit -F arch=b64 -S mount -F auid>=1000 -F auid!=4294967295 -k mounts -a always,exit -F arch=b32 -S mount -F auid>=1000 -F auid!=4294967295 -k mounts ## ---- File deletion events (35742) ---- -a always,exit -F arch=b64 -S unlink,unlinkat,rename,renameat -F auid>=1000 -F auid!=4294967295 -k delete -a always,exit -F arch=b32 -S unlink,unlinkat,rename,renameat -F auid>=1000 -F auid!=4294967295 -k delete ## ---- Mandatory access control changes (35743) ---- -w /etc/apparmor/ -p wa -k MAC-policy -w /etc/apparmor.d/ -p wa -k MAC-policy ## ---- chcon (35744) ---- -a always,exit -F path=/usr/bin/chcon -F perm=x -F auid>=1000 -F auid!=4294967295 -k perm_chng ## ---- setfacl (35745) ---- -a always,exit -F path=/usr/bin/setfacl -F perm=x -F auid>=1000 -F auid!=4294967295 -k perm_chng ## ---- chacl (35746) ---- -a always,exit -F path=/usr/bin/chacl -F perm=x -F auid>=1000 -F auid!=4294967295 -k perm_chng ## ---- usermod (35747) ---- -a always,exit -F path=/usr/sbin/usermod -F perm=x -F auid>=1000 -F auid!=4294967295 -k usermod ## ---- Kernel module loading (35748) ---- -a always,exit -F arch=b64 -S init_module,finit_module,delete_module,create_module,query_module -k kernel_modules -a always,exit -F arch=b32 -S init_module,finit_module,delete_module,create_module,query_module -k kernel_modules -w /sbin/insmod -p x -k kernel_modules -w /sbin/rmmod -p x -k kernel_modules -w /sbin/modprobe -p x -k kernel_modules -w /bin/kmod -p x -k kernel_modules ## ---- Make configuration immutable ---- -e 2 AUDITRULES chmod 640 /etc/audit/rules.d/nist-800-53.rules # Remove old rules file if it exists and is different if [[ -f /etc/audit/rules.d/cis-nist.rules ]]; then rm -f /etc/audit/rules.d/cis-nist.rules echo " ✓ Removed old cis-nist.rules" fi # Load the new rules augenrules --load 2>/dev/null || true systemctl restart auditd 2>/dev/null || true echo " ✓ nist-800-53.rules created and loaded" echo " Rules loaded: $(auditctl -l 2>/dev/null | wc -l)" ############################################################################### banner "FIX 12: AIDE audit tool coverage (35760)" ############################################################################### AIDE_CUSTOM="/etc/aide/aide.conf.d/99_nist_custom" if [[ -f "$AIDE_CUSTOM" ]]; then # Check if audit tools are already covered if ! grep -q 'auditctl' "$AIDE_CUSTOM"; then cat >> "$AIDE_CUSTOM" <<'EOF' # CIS 4.1.4.11 - Audit tool integrity /sbin/auditctl p+i+n+u+g+s+b+acl+xattrs+sha512 /sbin/auditd p+i+n+u+g+s+b+acl+xattrs+sha512 /sbin/ausearch p+i+n+u+g+s+b+acl+xattrs+sha512 /sbin/aureport p+i+n+u+g+s+b+acl+xattrs+sha512 /sbin/autrace p+i+n+u+g+s+b+acl+xattrs+sha512 /sbin/augenrules p+i+n+u+g+s+b+acl+xattrs+sha512 EOF echo " ✓ Audit tool integrity rules added to AIDE config" else echo " ✓ Audit tools already in AIDE config" fi else # Create the file cat > "$AIDE_CUSTOM" <<'EOF' # NIST 800-53 SI-7: File integrity monitoring /boot Full /bin Full /sbin Full /lib Full /lib64 Full /usr/bin Full /usr/sbin Full /usr/lib Full /etc Full !/etc/mtab !/etc/adjtime !/var/log !/var/spool !/var/cache # CIS 4.1.4.11 - Audit tool integrity /sbin/auditctl p+i+n+u+g+s+b+acl+xattrs+sha512 /sbin/auditd p+i+n+u+g+s+b+acl+xattrs+sha512 /sbin/ausearch p+i+n+u+g+s+b+acl+xattrs+sha512 /sbin/aureport p+i+n+u+g+s+b+acl+xattrs+sha512 /sbin/autrace p+i+n+u+g+s+b+acl+xattrs+sha512 /sbin/augenrules p+i+n+u+g+s+b+acl+xattrs+sha512 EOF echo " ✓ AIDE custom config created with audit tool coverage" fi echo " NOTE: Run 'sudo aideinit -y -f' to rebuild the database (takes 10-15 min)" ############################################################################### banner "FIX 13: at.allow — add admin user (35601)" ############################################################################### if [[ -f /etc/at.allow ]]; then if ! grep -q "$ADMIN_USER" /etc/at.allow; then echo "$ADMIN_USER" >> /etc/at.allow echo " ✓ $ADMIN_USER added to /etc/at.allow" else echo " ✓ $ADMIN_USER already in /etc/at.allow" fi fi # Same for cron.allow if [[ -f /etc/cron.allow ]]; then if ! grep -q "$ADMIN_USER" /etc/cron.allow; then echo "$ADMIN_USER" >> /etc/cron.allow echo " ✓ $ADMIN_USER added to /etc/cron.allow" fi fi ############################################################################### banner "COMPLETE" ############################################################################### cat <<'DONE' ╔══════════════════════════════════════════════════════════════════╗ ║ SCA Remediation Complete ║ ╠══════════════════════════════════════════════════════════════════╣ ║ ║ ║ Fixed: ║ ║ • Filesystem module blacklist file created ║ ║ • Network protocol module blacklist file created ║ ║ • USB storage module fixed (/bin/false) ║ ║ • initramfs rebuilt with blacklists ║ ║ • log_martians enabled at runtime ║ ║ • nftables package purged ║ ║ • SSH config merged from drop-in into main sshd_config ║ ║ • Password aging set for root and nobody ║ ║ • Journald MaxRetentionSec added ║ ║ • Log file permissions tightened ║ ║ • Full audit ruleset created in nist-800-53.rules ║ ║ • AIDE audit tool integrity rules added ║ ║ • at.allow / cron.allow updated ║ ║ ║ ║ Known exceptions (suppress in Wazuh): ║ ║ • 35540 GRUB password (manual step) ║ ║ • 35589 systemd-timesyncd (chrony is active) ║ ║ • 35608 ip_forward=1 (Docker requires it) ║ ║ • 35710 journal-upload (using Wazuh instead) ║ ║ • 35720 remote syslog (using Wazuh instead) ║ ║ • 35626-35639 firewall cross-checks (using UFW only) ║ ║ ║ ║ Still needed: ║ ║ • Rebuild AIDE database: sudo aideinit -y -f ║ ║ • Reboot to apply module blacklists ║ ║ • Re-run sca-verify.sh to confirm fixes ║ ║ • Re-run Wazuh SCA scan ║ ║ ║ ╚══════════════════════════════════════════════════════════════════╝ DONE