Thorough local machine audit: security, processes, disk, network, users, services, compromise indicators.
Setup: REPORT_DIR=$(mktemp -d /tmp/machine-health-XXXXX) && chmod 700 "$REPORT_DIR" && echo "Report: $REPORT_DIR"
| $ARGUMENTS | Behavior |
|---|---|
| (empty) | Full assessment, all phases |
network disk security users processes services docker |
Deep-dive that area, baseline others |
quick |
Phases 1, 3, 4, 5 only — fast triage |
Detect environment before running phases:
# Init system
pidof systemd >/dev/null && echo "systemd" || echo "other"
# Firewall backend
nft list ruleset &>/dev/null && echo "nftables" || (iptables -L &>/dev/null && echo "iptables" || echo "none")
# Container runtime
command -v docker &>/dev/null && echo "docker" || (command -v podman &>/dev/null && echo "podman" || echo "none")
# Sudo
sudo -n true 2>/dev/null && echo "sudo:yes" || echo "sudo:limited"
# Package manager
command -v apt-get &>/dev/null && echo "apt" || (command -v dnf &>/dev/null && echo "dnf" || echo "other")Record capabilities. Note any limitations in the report.
Collect in parallel:
| Check | Command |
|---|---|
| Uptime & load | uptime |
| Kernel | uname -r |
| OS | head -4 /etc/os-release |
| RAM | free -h |
| Swap usage | swapon --show |
| CPU count | nproc |
| Hostname | hostname -f |
| Time sync | timedatectl status 2>/dev/null | rg -i 'synchro|service|time zone' |
| Clock source | chronyc tracking 2>/dev/null || ntpq -pn 2>/dev/null |
| Kernel taint | cat /proc/sys/kernel/tainted (0 = clean) |
| Recent kernel errors | journalctl -p err --no-pager --since '24h ago' 2>/dev/null | tail -20 |
| OOM kills | journalctl -k --no-pager -g 'oom|Out of memory' --since '7 days ago' 2>/dev/null | tail -10 |
| DNS resolution | timeout 5 dig +short example.com && echo "DNS OK" || echo "DNS FAIL" |
Flag: load average > nproc, swap > 50% used, uptime < 1 day (unexpected reboot?), NTP not synchronized, kernel tainted (non-zero), OOM kills present, DNS resolution failure
Collect in parallel:
| Check | Command |
|---|---|
| Currently logged in | who |
| UID 0 accounts | awk -F: '$3 == 0' /etc/passwd |
| Interactive users (UID >= 1000) | awk -F: '$3 >= 1000 && $7 !~ /nologin|false/' /etc/passwd |
| Recent logins | last -15 |
| Failed logins | sudo lastb -15 2>/dev/null |
| SSH authorized_keys | For each interactive user, check ~user/.ssh/authorized_keys — count keys, show fingerprints with ssh-keygen -lf |
| Sudoers | sudo grep -v '^#|^$' /etc/sudoers 2>/dev/null; sudo cat /etc/sudoers.d/* 2>/dev/null | grep -v '^#|^$' |
| Password status | For each interactive user: sudo passwd -S <user> |
Flag:
- Multiple UID 0 accounts
- Unknown interactive users
- SSH keys you don't recognize (show fingerprints)
- Unexpected sudoers entries
- Logins from unknown IPs (whois + reverse DNS on any unfamiliar IP)
- Failed login bursts from same IP
Collect in parallel:
| Check | Command |
|---|---|
| Top CPU consumers | ps aux --sort=-%cpu | head -20 |
| Top MEM consumers | ps aux --sort=-%mem | head -20 |
| Zombie processes | ps aux | awk '$8 ~ /Z/' |
| Long-running processes | ps -eo pid,etime,comm --sort=-etime | head -20 |
| Kernel threads (D state) | ps aux | awk '$8 ~ /D/' |
| Deleted binaries | ls -l /proc/*/exe 2>/dev/null | grep deleted |
| FD counts (top 10) | for p in $(ls /proc/ | rg '^\d+$' | head -200); do echo "$(ls /proc/$p/fd 2>/dev/null | wc -l) $p $(cat /proc/$p/comm 2>/dev/null)"; done | sort -rn | head -10 |
Flag:
- Any process using > 80% CPU sustained
- Zombies (parent not reaping)
- Suspicious process names (crypto miners: xmrig, kdevtmpfsi, kinsing, solr, etc.)
- Unknown processes running as root
- Processes with deleted binaries = CRITICAL
- Any process with > 10,000 open FDs (leak)
- For every suspicious process: check
ls -l /proc/<pid>/exe,cat /proc/<pid>/cmdline, fd count
Review findings from Phases 1-3. If ANY of these are true:
- Unknown UID 0 accounts or unauthorized SSH keys
- Processes with deleted binaries or known miner signatures
- Outbound connections to C2 ports or unknown IPs with high traffic
→ Escalate: Log evidence to $REPORT_DIR/evidence/, note timestamps, do NOT terminate suspicious processes (preserves forensic state). Tag report verdict as COMPROMISED early. Continue remaining phases with forensic lens.
→ Otherwise: Continue normally.
Collect in parallel:
| Check | Command |
|---|---|
| Listening ports | ss -tlnp |
| Established connections | ss -tnp state established |
| UDP listeners | ss -ulnp |
| Firewall (nftables) | sudo nft list ruleset 2>/dev/null |
| Firewall (iptables fallback) | sudo iptables -L -n --line-numbers 2>/dev/null; sudo ip6tables -L -n --line-numbers 2>/dev/null |
| DNS config | cat /etc/resolv.conf |
| /etc/hosts | cat /etc/hosts |
| Routing | ip route show |
| Interfaces | ip -br addr |
| ARP table | ip neigh show |
Flag:
- Unexpected listening ports (cross-reference with known services, don't just flag >8000)
- Outbound connections to unknown IPs (whois suspicious ones)
- No firewall rules at all (nft + iptables both empty)
- Connections to known-bad ports: 1337, 4444, 5555, 6666, 9090 (common C2)
- DNS pointing to unexpected resolvers
- /etc/hosts entries pointing known domains to unexpected IPs = CRITICAL
- ARP anomalies (duplicate MACs, unexpected gateways)
Collect in parallel:
| Check | Command |
|---|---|
| Disk usage | df -h --total | grep -v tmpfs |
| Inode usage | df -i | grep -v tmpfs |
| Top space consumers | timeout 30 dust -n 15 -d 3 / 2>/dev/null || du -sh /* 2>/dev/null | sort -rh | head -15 |
| /tmp total + contents | du -sh /tmp/ 2>/dev/null; du -sh /tmp/* 2>/dev/null | sort -rh | head -15 |
| /tmp live files | sudo lsof +D /tmp 2>/dev/null | awk '{print $NF}' | sort -u | rg '^/tmp/' |
| /tmp stale (>7d) | find /tmp -maxdepth 1 -mindepth 1 -mtime +7 ! -name 'systemd-private-*' -exec du -sh {} + 2>/dev/null | sort -rh |
| /var/log size | du -sh /var/log/ && du -sh /var/log/* 2>/dev/null | sort -rh | head -10 |
| Large files (7d) | find / -xdev -type f -mtime -7 -size +100M 2>/dev/null | head -20 |
| Open deleted files | sudo lsof +L1 2>/dev/null | head -20 |
| World-writable dirs | find / -xdev -type d -perm -0002 ! -path '/tmp/*' ! -path '/var/tmp/*' ! -path '/dev/*' ! -path '/proc/*' ! -path '/sys/*' 2>/dev/null | head -10 |
| Filesystem layout | lsblk -o NAME,SIZE,FSTYPE,MOUNTPOINT,RO |
Flag:
- Any partition > 85% used
- Inode exhaustion (> 90%)
- /tmp total > 500MB = investigate. Enumerate every entry, cross-reference with lsof live list
- For each /tmp entry: if NOT in lsof live list AND older than 7 days → mark as STALE, include in cleanup recommendation with exact size
- Present stale /tmp items as an explicit itemized cleanup list in the report, not a vague "some files in /tmp"
- Deleted files still held open (space not reclaimed)
- Unexpected large files in user directories
Collect in parallel:
| Check | Command |
|---|---|
| Failed systemd units | systemctl list-units --state=failed --no-pager |
| Enabled services | systemctl list-unit-files --state=enabled --no-pager |
| User crontabs | for u in $(awk -F: '$3>=1000{print $1}' /etc/passwd); do echo "=== $u ==="; sudo crontab -u $u -l 2>/dev/null; done |
| Root crontab | sudo crontab -l 2>/dev/null |
| System cron | ls /etc/cron.d/ /etc/cron.daily/ /etc/cron.hourly/ /etc/cron.weekly/ /etc/cron.monthly/ 2>/dev/null |
| Systemd timers | systemctl list-timers --no-pager |
| At jobs | atq 2>/dev/null |
| Systemd overrides | find /etc/systemd/system -name 'override.conf' -o -name '*.d' -type d 2>/dev/null |
| Non-standard services | find /etc/systemd/system /run/systemd/system -name '*.service' -newer /etc/os-release 2>/dev/null |
| Init scripts | ls /etc/init.d/ 2>/dev/null |
| rc.local | cat /etc/rc.local 2>/dev/null |
| Profile backdoors | rg -l 'curl|wget|nc |python.*-c|bash.*-i|/dev/tcp' /etc/profile.d/ /etc/environment ~/.bashrc ~/.bash_profile ~/.profile 2>/dev/null |
| Motd scripts | ls -la /etc/update-motd.d/ 2>/dev/null |
| Udev rules | ls /etc/udev/rules.d/ 2>/dev/null |
Flag:
- Failed units (especially security: fail2ban, ufw, apparmor, auditd)
- Unknown cron entries
- Cron jobs downloading or executing from URLs
- Timers running unexpected scripts
- Services created after OS install (newer than /etc/os-release)
- ExecStart pointing outside /usr/ paths
- Shell profiles containing curl/wget/nc/python -c patterns = CRITICAL
- rc.local with non-trivial content
Collect in parallel:
| Check | Command |
|---|---|
| SSH config (full) | rg -v '^\s*#|^\s*$' /etc/ssh/sshd_config /etc/ssh/sshd_config.d/*.conf 2>/dev/null |
| fail2ban status | sudo fail2ban-client status 2>/dev/null && for j in $(sudo fail2ban-client status 2>/dev/null | grep 'Jail list' | sed 's/.*:\s*//;s/,/ /g'); do sudo fail2ban-client status $j; done |
| SUID binaries | find / -xdev -perm -4000 -type f 2>/dev/null | head -30 |
| SGID binaries | find / -xdev -perm -2000 -type f 2>/dev/null | head -20 |
| ASLR | cat /proc/sys/kernel/randomize_va_space (should be 2) |
| Kernel hardening | sysctl kernel.kptr_restrict kernel.dmesg_restrict kernel.yama.ptrace_scope net.ipv4.tcp_syncookies net.ipv4.conf.all.rp_filter fs.protected_hardlinks fs.protected_symlinks 2>/dev/null |
| Unattended upgrades | dpkg -l unattended-upgrades 2>/dev/null | tail -1; cat /etc/apt/apt.conf.d/20auto-upgrades 2>/dev/null |
| Pending updates | apt-get -s upgrade 2>/dev/null | grep ^Inst | head -20 |
| AppArmor/SELinux | aa-status 2>/dev/null || getenforce 2>/dev/null || echo "No MAC" |
| Package integrity | debsums -c 2>/dev/null | head -30 |
| Log integrity | wc -l /var/log/auth.log /var/log/syslog 2>/dev/null; find /var/log -name '*.log' -empty 2>/dev/null; journalctl --verify 2>&1 | tail -5 |
| Log permissions | stat -c '%a %U %G %n' /var/log/auth.log /var/log/syslog 2>/dev/null |
Flag:
- SSH: PermitRootLogin yes, PasswordAuthentication yes (without fail2ban), MaxAuthTries > 4, AllowTcpForwarding yes, X11Forwarding yes on servers, AuthorizedKeysCommand pointing to unexpected binary
- fail2ban not running
- Unusual SUID binaries outside /usr/bin, /usr/sbin, /usr/lib
- ASLR disabled (randomize_va_space != 2)
- Kernel hardening params at insecure defaults
- Pending security patches
- No unattended-upgrades configured
- No MAC (AppArmor/SELinux) active
- Package integrity failures on security binaries (sshd, sudo, login, su, passwd) = CRITICAL
- auth.log < 100 lines on server with uptime > 7 days = logs likely cleared
- Empty .log files that should have content
- Journal verification failures
Collect in parallel:
| Check | Command |
|---|---|
| LD_PRELOAD hijack | cat /etc/ld.so.preload 2>/dev/null; rg LD_PRELOAD /etc/environment /etc/profile.d/ ~/.bashrc 2>/dev/null |
| Shared library config | rg -v '^#|^$' /etc/ld.so.conf.d/*.conf 2>/dev/null |
| PAM integrity | dpkg -V libpam-modules 2>/dev/null; ls -lt /etc/pam.d/ | head -15 |
| Kernel modules | lsmod | head -30 |
| Rootkit scan | sudo rkhunter --check --skip-keypress --report-warnings-only 2>/dev/null | tail -30 |
| /dev/shm contents | ls -la /dev/shm/ |
| Hidden files in / | ls -la / | grep '^\.' |
| Process vs /proc count | echo "ps: $(ps -e --no-headers | wc -l) /proc: $(ls -d /proc/[0-9]* | wc -l)" |
| Recently modified sys bins | find /usr/bin /usr/sbin -xdev -mtime -7 -type f 2>/dev/null | head -20 |
| Unusual /usr/local | ls -lt /usr/local/bin/ /usr/local/sbin/ 2>/dev/null | head -20 |
| Immutable files | lsattr -R /etc/ 2>/dev/null | rg '\-i\-' | head -10 |
| Cert expiry (30d) | find /etc/ssl /etc/letsencrypt -name '*.pem' -o -name '*.crt' 2>/dev/null | head -20 | xargs -I{} sh -c 'openssl x509 -enddate -noout -in "{}" 2>/dev/null && echo " {}"' |
| Audit log (recent) | sudo ausearch -ts recent --raw 2>/dev/null | aureport -au --summary 2>/dev/null | tail -20; sudo auditctl -l 2>/dev/null |
Flag:
- ANY content in /etc/ld.so.preload = CRITICAL
- LD_PRELOAD in environment or profiles = CRITICAL
- /dev/shm containing executables or scripts = CRITICAL
- PAM module integrity failures = CRITICAL
- Process count discrepancy > 5 = investigate hidden processes
- Recently modified binaries in /usr/bin, /usr/sbin (not from apt) = CRITICAL
- Unknown kernel modules (compare against
dpkg -Sfor each) - rkhunter warnings (rootkit signatures, modified binaries, hidden ports)
- Certificates expiring within 30 days
- Audit rules missing or auditd not running (no syscall visibility)
Only run if command -v docker &>/dev/null || command -v podman &>/dev/null:
| Check | Command |
|---|---|
| Running containers | docker ps --format 'table {{.Names}}\t{{.Status}}\t{{.Ports}}' |
| All containers | docker ps -a --format 'table {{.Names}}\t{{.Status}}\t{{.Image}}' |
| Images | docker images --format 'table {{.Repository}}\t{{.Tag}}\t{{.Size}}' |
| Disk usage | docker system df |
| Privileged | docker ps -q | xargs -I{} docker inspect {} --format '{{.Name}} privileged={{.HostConfig.Privileged}}' |
| Socket mounts | docker ps -q | xargs -I{} docker inspect {} --format '{{.Name}} {{range .Mounts}}{{if eq .Source "/var/run/docker.sock"}}DOCKER_SOCKET{{end}}{{end}}' |
| Host namespaces | docker ps -q | xargs -I{} docker inspect {} --format '{{.Name}} pid={{.HostConfig.PidMode}} ipc={{.HostConfig.IpcMode}} net={{.HostConfig.NetworkMode}}' |
| Container user | docker ps -q | xargs -I{} docker inspect {} --format '{{.Name}} user={{.Config.User}}' |
| Daemon config | cat /etc/docker/daemon.json 2>/dev/null | jq -c '.' |
| Restart loops | docker ps -a --format '{{.Names}} {{.Status}}' | rg -i 'restarting' |
Flag:
- Containers running as privileged = CRITICAL
- Docker socket mounted into container = CRITICAL
- Host PID/IPC namespace = HIGH
- Old stopped containers wasting space
- Dangling images/volumes
- Unknown images
- Insecure registries in daemon.json
- Containers in restart loop
Write to $REPORT_DIR/report.md:
## Machine Health Report — [hostname] — [date]
### Verdict: [CLEAN / NEEDS ATTENTION / COMPROMISED]
### Summary
| Area | Status | Issues |
|------|--------|--------|
| System | OK/WARN/CRIT | ... |
| Users & Access | OK/WARN/CRIT | ... |
| Processes | OK/WARN/CRIT | ... |
| Network | OK/WARN/CRIT | ... |
| Disk | OK/WARN/CRIT | ... |
| Services | OK/WARN/CRIT | ... |
| Security | OK/WARN/CRIT | ... |
| Compromise | OK/WARN/CRIT | ... |
| Containers | OK/WARN/CRIT/N/A | ... |
### Findings (by severity)
#### CRITICAL
[findings requiring immediate action]
#### WARNING
[findings requiring attention]
#### INFO
[observations, no action needed]
### Recommended Actions
[ordered list of fixes, most urgent first]
### Methodology
- Phases executed: [list]
- Focus area: [from $ARGUMENTS or "full"]
- Sudo available: [yes/no — which checks were skipped]
- Report: $REPORT_DIR
Also display the report inline.
- Run all checks — never skip a phase (unless
quickmode) - Maximize parallel execution within each phase
- Use
sudoonly when needed, note if sudo is unavailable and which checks were skipped - For every unknown IP found: run
whois+ reverse DNS - For every suspicious process: check binary path (
ls -l /proc/<pid>/exe), open files, and cmdline - Compare SUID binaries against known-good Debian/Ubuntu defaults — flag anything unusual
- Don't just collect data — interpret it. Every finding needs a severity and actionable recommendation
- If $ARGUMENTS is provided, focus on that area but still run baseline checks on other areas
- Use compact CLI flags per CLAUDE.md conventions
- Use local tools (fd, rg, dust, procs) when available, fall back to standard tools. Do NOT use fd for permission-based filtering — use find with -perm instead
- Wrap any filesystem-wide scan in
timeout 30to prevent hangs on NFS/large storage. Always use-xdevwith find from / - OPSEC: If compromise is suspected mid-assessment, avoid commands that generate outbound network traffic (whois, apt update). Flag IPs for offline analysis instead. Do not terminate suspicious processes — preserve forensic state
- Respect Gate 1 — if early phases indicate compromise, switch to forensic lens for remaining phases