Emergency server help: get in touch

Server Inventory Report

A one-page server inventory report for Linux: OS, hardware, disks, IPs, control panel, web server, PHP, database and mail versions, firewall, backups, open ports and pending updates.

Version
1.0.1
Last updated
October 6, 2026
Language
Bash
Tested on
AlmaLinux 9.8 with DirectAdmin 1.712 (lab test, 6 Oct 2026); AlmaLinux 9.8 with cPanel & WHM 11.138 (lab test, 5 Oct 2026); Ubuntu 24.04
License
MIT
Pricing
Free

The first hour on an unfamiliar server goes on questions that should take a minute: which OS, how much RAM, which panel, Apache or LiteSpeed, which PHP versions, MariaDB or MySQL, is there a firewall, is anything backing it up, what is listening on the public IP. The answers are spread over twenty commands with twenty output formats. This server inventory report runs them all and prints one page you can read, paste into a ticket, or save as JSON to compare next month.

What the server inventory report covers

  • System: host name, OS release, kernel, uptime, last boot, whether a reboot is pending (/var/run/reboot-required, needs-restarting -r, or a newer installed kernel), virtualization from systemd-detect-virt, hardware vendor and model.
  • CPU and memory: CPU model, threads and sockets, load average, RAM and swap.
  • Storage: physical disks with size and SSD or rotational, software RAID arrays, and every real filesystem with its usage.
  • Network: IPv4 and IPv6 addresses per interface, default gateways, DNS resolvers (the real upstream servers when systemd-resolved is in front).
  • Hosting stack: cPanel & WHM, DirectAdmin or Plesk with version (plus CyberPanel, CWP, HestiaCP, Webmin); Apache, LiteSpeed, Nginx or Caddy; every PHP version installed through EasyApache, CloudLinux alt-php, Remi, DirectAdmin, LSPHP or the distribution; MariaDB or MySQL, PostgreSQL and Redis; Exim, Postfix and Dovecot.
  • Security and backups: CSF (and whether it is still in TESTING mode), firewalld, ufw, nftables or iptables rule counts, fail2ban, Imunify360, LFD, cPHulk; cPanel backups enabled or not, JetBackup, DirectAdmin admin backups, Acronis, restic, borg, rsnapshot, duplicity, rclone, Veeam, Bacula.
  • Listening services: every listening TCP and UDP port with the process and the addresses it is bound to.
  • Updates: pending package updates from the local dnf, yum or apt cache.

Usage

curl -fsSL https://srvscripts.com/get/server-inventory-report/ -o server-inventory-report.sh
bash server-inventory-report.sh                     # plain text
bash server-inventory-report.sh --markdown          # tables for a ticket, wiki or handover note
bash server-inventory-report.sh --json > inventory-$(hostname)-$(date +%F).json
bash server-inventory-report.sh --no-updates        # skip the package manager query

Sample output

== System ==
  Hostname               web1.example.com
  OS                     AlmaLinux 9.4 (Seafoam Ocelot)
  Kernel                 5.14.0-427.13.1.el9_4.x86_64 (x86_64)
  Uptime                 212d 4h 51m
  Reboot required        yes (needs-restarting -r)
  Virtualization         kvm

== Hosting stack ==
  Control panel          cPanel & WHM 11.126.0.9
  Web server             LiteSpeed/6.3.1 Enterprise
  PHP versions           7.4.33 (ea-php74), 8.1.30 (ea-php81), 8.2.24 (ea-php82), 8.3.12 (ea-php83)
  Databases              MariaDB 10.6.19
  Mail                   Exim 4.98, Dovecot 2.3.21

== Security and backups ==
  Firewall               csf: v14.22 (cPanel), TESTING mode
  Intrusion prevention   cPHulk
  Backup tools           cPanel backups (disabled), JetBackup

== Listening services ==
  22/tcp                 sshd on 0.0.0.0 [::]
  3306/tcp               mariadbd on 0.0.0.0
  11211/tcp              memcached on 0.0.0.0

== Updates ==
  Pending updates        143 (from the local dnf cache)

Read top to bottom, this is a handover list: 212 days without a reboot and a pending kernel, PHP 7.4 still installed (check which accounts use it before removing it), CSF left in TESTING mode, which means its rules are cleared every few minutes, and MariaDB and Memcached listening on every interface. Server Security Audit goes deeper on the firewall and exposed services.

Tested on a real server

We ran this script on our lab server on 5 October 2026: AlmaLinux 9.8 with cPanel & WHM 11.138, MariaDB 10.11 and three WordPress test accounts. The screenshot is the real terminal output; only IP addresses are masked, and long output is cut where marked.

Terminal output of bash server-inventory-report.sh on AlmaLinux 9.8 with cPanel and WHM 11.138
bash server-inventory-report.sh — exit code 0, 5.8 s. AlmaLinux 9.8, cPanel & WHM 11.138, 5 Oct 2026. IP addresses masked; long output cut where marked.

Second run, on our DirectAdmin test server (DirectAdmin 1.712, AlmaLinux 9.8, one vCPU, 6 October 2026). It detects the panel, Apache, both PHP-FPM versions, MariaDB, Exim and Dovecot and CSF.

Terminal output of server-inventory-report.sh on DirectAdmin 1.712
bash server-inventory-report.sh on DirectAdmin 1.712 — exit code 0. AlmaLinux 9.8, 6 Oct 2026. IP addresses masked.

What the test changed: Our lab test found two display issues, fixed in 1.0.1: the free ImunifyAV was listed as Imunify360, and cPanel’s cpsrvd appeared as "cpsrvd (SSL". The screenshot shows the fixed version.

Options

  • --markdown — one ## Section and a two-column table per section.
  • --json — { "system": { "hostname": "..." }, ... } with lower-case keys; mount points and ports are kept as they are.
  • --no-updates — skip the pending-updates count.
  • --no-color — plain text even on a terminal.

Notes

The report is read-only and makes no network calls. Pending updates come from the package manager’s local cache (dnf -C check-update, apt-get -s upgrade), so they are as fresh as the last dnf makecache or apt update, and a server without a dnf cache shows “unknown” instead of reaching out to the mirrors. Versions come from the binaries themselves (httpd -v, php -r, mariadbd --version, exim -bV), not from package names, so a hand-compiled build is reported correctly. Without root, process names in the listening list and firewall rule counts are missing. Saving the JSON after every change gives you a history of what a server looked like before an upgrade or a migration.

Server inventory report at a glance

Server Inventory Report summary card: The first hour on an unfamiliar server goes on questions that should take a minute: which OS, how much RAM, which…
In short: The first hour on an unfamiliar server goes on questions that should take a minute: which OS, how much RAM, which panel, Apache or LiteSpeed, which PHP versions, MariaDB or MySQL, is there a firewall, is anything backing it up, what is…
Server Inventory Report sections: What the server inventory report covers, Usage, Sample output and Options
Covers: What the server inventory report covers, Usage, Sample output and Options.
Server Inventory Report questions answered: Does it work on DirectAdmin and Plesk servers? Can I collect the report from many servers?
Answers: Does it work on DirectAdmin and Plesk servers? Can I collect the report from many servers?

Official documentation: DirectAdmin documentation, cPanel & WHM documentation, AlmaLinux wiki.

Related guides: Best VPS for cPanel and DirectAdmin in 2026: What to Check · Disk full on a production server: recovery runbook · CSF Fork 2026: Which Reliable Replacement After ConfigServer?.

The script

server-inventory-report.shDownload
#!/usr/bin/env bash
# Server Inventory Report (v1.0.1) - from srvScripts.com
# Source, docs and updates: https://srvscripts.com/scripts/server-inventory-report/
# Copyright (c) 2026 srvScripts.com. MIT licence: if you copy, share or adapt this script, keep this notice and credit srvScripts.com.
# server-inventory-report.sh — one-page inventory of a Linux server: hardware, network, panel, stack, security, backups
# https://srvscripts.com/scripts/server-inventory-report/   License: MIT
#
# Read-only, no network calls. Detects cPanel, DirectAdmin and Plesk, Apache/LiteSpeed/Nginx,
# every installed PHP version, the database and mail servers, firewall, backup tools, listening
# services, pending updates (from the local package cache) and whether a reboot is required.
#   bash server-inventory-report.sh                 # plain text
#   bash server-inventory-report.sh --markdown      # paste into a ticket or wiki
#   bash server-inventory-report.sh --json > inventory-$(hostname).json
# Exit codes: 0 report printed, 2 usage error. Run as root for process names and firewall rules.
set -uo pipefail
export LC_ALL=C

FORMAT=text; COLOR=1; UPDATES=1
usage() {
  cat <<'EOF'
Usage: server-inventory-report.sh [options]
  --markdown     Markdown tables (tickets, wikis, handover notes)
  --json         JSON object: { "section": { "item": "value" } }
  --no-updates   skip the pending-updates count (it reads the dnf/yum/apt cache)
  --no-color     plain text output
  -h, --help     this help
EOF
}
while [[ $# -gt 0 ]]; do
  case "$1" in
    --markdown) FORMAT=markdown; shift ;;
    --json) FORMAT=json; shift ;;
    --no-updates) UPDATES=0; shift ;;
    --no-color) COLOR=0; shift ;;
    -h|--help) usage; exit 0 ;;
    *) echo "Unknown option: $1 (try --help)" >&2; exit 2 ;;
  esac
done
[[ -t 1 && "$FORMAT" == text ]] || COLOR=0

# ---- storage: parallel arrays of section / item / value ----------------------------------------
declare -a SEC=() KEY=() VAL=()
CUR=""
section() { CUR=$1; }
add() { [[ -n "${2:-}" ]] || return 0; SEC+=("$CUR"); KEY+=("$1"); VAL+=("$2"); }
have() { command -v "$1" >/dev/null 2>&1; }
first() { head -n 1 | sed 's/^[[:space:]]*//; s/[[:space:]]*$//'; }
join_lines() { awk 'NR > 1 { printf ", " } { printf "%s", $0 } END { if (NR) print "" }'; }
gib() { awk -v k="$1" 'BEGIN{ printf "%.1fG", k / 1048576 }'; }   # kB -> GiB
meminfo() { awk -v k="$1:" '$1 == k { print $2 }' /proc/meminfo; }

# ---- System ------------------------------------------------------------------------------------------
section "System"
add "Hostname" "$(hostname -f 2>/dev/null || hostname)"
add "OS" "$(. /etc/os-release 2>/dev/null && echo "${PRETTY_NAME:-$NAME}")"
add "Kernel" "$(uname -r) ($(uname -m))"
read -r up _ < /proc/uptime; up=${up%.*}
add "Uptime" "$((up / 86400))d $((up % 86400 / 3600))h $((up % 3600 / 60))m"
add "Last boot" "$(uptime -s 2>/dev/null || who -b 2>/dev/null | awk '{ print $3, $4 }')"
if [[ -f /var/run/reboot-required ]]; then
  reboot="yes ($(join_lines < /var/run/reboot-required.pkgs 2>/dev/null || echo 'see /var/run/reboot-required'))"
elif have needs-restarting; then
  if needs-restarting -r >/dev/null 2>&1; then reboot="no"; else reboot="yes (needs-restarting -r)"; fi
else
  newest=$(ls -1 /lib/modules 2>/dev/null | sort -V | tail -n 1)
  if [[ -n "$newest" && "$newest" != "$(uname -r)" ]]; then reboot="likely (newest installed kernel is $newest)"; else reboot="no"; fi
fi
add "Reboot required" "$reboot"
virt=$(systemd-detect-virt 2>/dev/null); [[ -n "$virt" ]] || virt="unknown"
[[ "$virt" == none ]] && virt="none (bare metal)"
add "Virtualization" "$virt"
add "Hardware" "$(cat /sys/class/dmi/id/sys_vendor /sys/class/dmi/id/product_name 2>/dev/null | paste -sd' ' -)"

# ---- CPU and memory ------------------------------------------------------------------------------
section "CPU and memory"
add "CPU model" "$(grep -m1 -E '^(model name|Model|cpu model)' /proc/cpuinfo | cut -d: -f2 | first)"
sockets=$(lscpu 2>/dev/null | awk -F: '/^Socket\(s\)/ { gsub(/ /, "", $2); print $2 }')
add "CPU threads" "$(nproc)${sockets:+ ($sockets socket(s))}"
add "Load average" "$(cut -d' ' -f1-3 /proc/loadavg)"
add "RAM" "$(gib "$(meminfo MemTotal)") total, $(gib "$(meminfo MemAvailable)") available"
st=$(meminfo SwapTotal)
if (( ${st:-0} > 0 )); then add "Swap" "$(gib "$st") total, $(gib $(( st - $(meminfo SwapFree) ))) used"; else add "Swap" "none"; fi

# ---- Storage ---------------------------------------------------------------------------------------
section "Storage"
if have lsblk; then
  while read -r name size type rota model; do
    [[ "$type" == disk && "$name" != zram* && "$size" != 0B ]] || continue
    add "/dev/$name" "$size $([[ "$rota" == 1 ]] && echo rotational || echo ssd)${model:+ $model}"
  done < <(lsblk -dn -o NAME,SIZE,TYPE,ROTA,MODEL 2>/dev/null)
fi
[[ -r /proc/mdstat ]] && while read -r md _ state level rest; do
  add "$md" "software RAID $state $level: $rest"
done < <(grep '^md' /proc/mdstat)
while read -r target fstype size used pcent; do
  [[ "$target" == /home/virtfs/* ]] && continue
  add "$target" "$fstype $size, $pcent used ($used)"
done < <(df -h --output=target,fstype,size,used,pcent -x tmpfs -x devtmpfs -x overlay -x squashfs -x efivarfs 2>/dev/null | tail -n +2)

# ---- Network -----------------------------------------------------------------------------------------
section "Network"
if have ip; then
  add "IPv4" "$(ip -o -4 addr show scope global 2>/dev/null | awk '{ print $4 " (" $2 ")" }' | join_lines)"
  add "IPv6" "$(ip -o -6 addr show scope global 2>/dev/null | awk '{ print $4 " (" $2 ")" }' | join_lines)"
  add "Default gateway" "$(ip route show default 2>/dev/null | awk '{ print $3 " (" $5 ")" }' | join_lines)"
  add "IPv6 gateway" "$(ip -6 route show default 2>/dev/null | awk '{ print $3 " (" $5 ")" }' | join_lines)"
else
  add "IP addresses" "$(hostname -I 2>/dev/null)"
fi
dns=$(awk '$1 == "nameserver" { print $2 }' /etc/resolv.conf 2>/dev/null | join_lines)
if [[ "$dns" == 127.0.0.53 && -r /run/systemd/resolve/resolv.conf ]]; then
  dns="$(awk '$1 == "nameserver" { print $2 }' /run/systemd/resolve/resolv.conf | join_lines) (via systemd-resolved)"
fi
add "DNS resolvers" "${dns:-none}"

# ---- Control panel and web stack -----------------------------------------------------------------------
section "Hosting stack"
panel=""
[[ -r /usr/local/cpanel/version ]] && panel="cPanel & WHM $(cat /usr/local/cpanel/version)"
if [[ -x /usr/local/directadmin/directadmin ]]; then
  da=$( { timeout 5 /usr/local/directadmin/directadmin version || timeout 5 /usr/local/directadmin/directadmin v; } 2>/dev/null | grep -m1 -Eo 'v?\.?[0-9]+\.[0-9.]+')
  panel="DirectAdmin ${da:-(version unknown)}"
fi
[[ -r /usr/local/psa/version ]] && panel="Plesk $(awk '{ print $1 }' /usr/local/psa/version)"
[[ -z "$panel" && -d /usr/local/CyberCP ]] && panel="CyberPanel"
[[ -z "$panel" && -d /usr/local/cwpsrv ]] && panel="CentOS Web Panel"
[[ -z "$panel" && -d /usr/local/hestia ]] && panel="HestiaCP"
[[ -z "$panel" && -r /etc/webmin/version ]] && panel="Webmin $(cat /etc/webmin/version)"
add "Control panel" "${panel:-none detected}"

web=()
for b in httpd apache2 /usr/sbin/httpd /usr/local/apache/bin/httpd; do
  have "$b" || continue
  v=$("$b" -v 2>/dev/null | sed -n 's/^Server version: //p'); [[ -n "$v" ]] && { web+=("$v"); break; }
done
[[ -x /usr/local/lsws/bin/lshttpd ]] && web+=("$( (timeout 5 /usr/local/lsws/bin/lshttpd -v 2>&1 | first) || cat /usr/local/lsws/VERSION)")
have nginx && web+=("$(nginx -v 2>&1 | sed 's/^nginx version: //' | first)")
have caddy && web+=("Caddy $(caddy version 2>/dev/null | awk '{ print $1 }')")
add "Web server" "$( ((${#web[@]})) && printf '%s\n' "${web[@]}" | join_lines || echo 'none detected')"

declare -A PHPSEEN=()
php=()
for b in /opt/cpanel/ea-php*/root/usr/bin/php /opt/alt/php*/usr/bin/php /opt/remi/php*/root/usr/bin/php \
         /usr/local/php*/bin/php /usr/local/lsws/lsphp*/bin/php /usr/bin/php[0-9]* /usr/bin/php /usr/local/bin/php; do
  [[ -x "$b" ]] || continue
  real=$(readlink -f "$b"); [[ -n "${PHPSEEN[$real]:-}" ]] && continue; PHPSEEN[$real]=1
  v=$(timeout 5 "$b" -n -r 'echo PHP_VERSION;' 2>/dev/null) || continue
  label=$(sed -E 's#^/opt/cpanel/(ea-php[0-9]+)/.*#\1#; s#^/opt/alt/(php[0-9]+)/.*#alt-\1#; s#^/opt/remi/(php[0-9]+)/.*#remi-\1#;
                s#^/usr/local/lsws/(lsphp[0-9]+)/.*#\1#; s#^/usr/local/(php[0-9]+)/.*#\1#; s#^/usr/(local/)?bin/##' <<< "$b")
  php+=("$v ($label)")
done
add "PHP versions" "$( ((${#php[@]})) && printf '%s\n' "${php[@]}" | join_lines || echo 'none found')"
have php && add "PHP default CLI" "$(php -n -r 'echo PHP_VERSION;' 2>/dev/null)"

db=()
for b in /usr/sbin/mariadbd /usr/libexec/mariadbd /usr/sbin/mysqld /usr/libexec/mysqld; do
  [[ -x "$b" ]] || continue
  v=$("$b" --version 2>/dev/null | sed -n 's/.*Ver \([^ ]*\).*/\1/p')
  [[ -n "$v" ]] && { [[ "$v" == *MariaDB* ]] && db+=("MariaDB ${v%%-MariaDB*}") || db+=("MySQL $v"); break; }
done
for b in /usr/pgsql-*/bin/postgres /usr/lib/postgresql/*/bin/postgres; do
  [[ -x "$b" ]] && db+=("PostgreSQL $("$b" --version 2>/dev/null | sed -n 's/.*(PostgreSQL) \([^ ]*\).*/\1/p')")
done
have redis-server && db+=("Redis $(redis-server --version 2>/dev/null | sed -n 's/.* v=\([^ ]*\).*/\1/p')")
add "Databases" "$( ((${#db[@]})) && printf '%s\n' "${db[@]}" | join_lines || echo 'none detected')"

mail=()
have exim && mail+=("Exim $(exim -bV 2>/dev/null | sed -n 's/^Exim version \([^ ]*\).*/\1/p' | first)")
have postconf && mail+=("Postfix $(postconf -h mail_version 2>/dev/null)")
have dovecot && mail+=("Dovecot $(dovecot --version 2>/dev/null | awk '{ print $1 }')")
add "Mail" "$( ((${#mail[@]})) && printf '%s\n' "${mail[@]}" | join_lines || echo 'none detected')"

# ---- Security and backups ----------------------------------------------------------------------------
section "Security and backups"
fw=()
if have csf; then
  c=$(csf -v 2>/dev/null | first)
  grep -qE '^TESTING *= *"1"' /etc/csf/csf.conf 2>/dev/null && c="$c, TESTING mode"
  fw+=("$c")
fi
have firewall-cmd && fw+=("firewalld $(firewall-cmd --state 2>&1 | first)")
have ufw && fw+=("ufw $(ufw status 2>/dev/null | sed -n 's/^Status: //p')")
have nft && n=$(nft list ruleset 2>/dev/null | grep -cE '^\s+(ip|tcp|udp|ct|iif|oif|meta|counter|accept|drop|reject)') && (( n > 0 )) && fw+=("nftables ($n rules)")
have iptables && n=$(iptables -S 2>/dev/null | grep -c '^-A') && (( n > 0 )) && fw+=("iptables ($n rules)")
add "Firewall" "$( ((${#fw[@]})) && printf '%s\n' "${fw[@]}" | join_lines || echo 'none detected (or not root)')"
ips=()
have fail2ban-client && ips+=("fail2ban$(pgrep -f fail2ban-server >/dev/null && echo ' (running)')")
if have imunify360-agent; then   # the free ImunifyAV ships the same command; Imunify360 has its own service unit
  imv=$(imunify360-agent version 2>/dev/null | first)
  if systemctl list-unit-files imunify360.service 2>/dev/null | grep -q '^imunify360\.service' || { have rpm && rpm -q imunify360-firewall >/dev/null 2>&1; }; then ips+=("Imunify360 $imv")
  else ips+=("ImunifyAV $imv (malware scanner only)"); fi
fi
pgrep -x lfd >/dev/null && ips+=("LFD (running)")
[[ -d /usr/local/cpanel ]] && [[ -e /var/cpanel/hulkd/enabled ]] && ips+=("cPHulk")
add "Intrusion prevention" "$( ((${#ips[@]})) && printf '%s\n' "${ips[@]}" | join_lines || echo 'none detected')"

bk=()
if [[ -r /var/cpanel/backups/config ]]; then
  grep -qE "^BACKUPENABLE: '?yes" /var/cpanel/backups/config && bk+=("cPanel backups (enabled)") || bk+=("cPanel backups (disabled)")
fi
{ have jetbackup5api || [[ -d /usr/local/jetapps/usr/bin ]]; } && bk+=("JetBackup")
[[ -s /usr/local/directadmin/data/admin/backup_crons.list ]] && bk+=("DirectAdmin admin backups")
[[ -d /usr/lib/Acronis ]] && bk+=("Acronis agent")
for t in restic borg rsnapshot duplicity rclone veeamconfig bacula-fd; do have "$t" && bk+=("$t"); done
[[ -r /etc/srvscripts/restic.conf ]] && bk+=("restic-offsite-backup config")
add "Backup tools" "$( ((${#bk[@]})) && printf '%s\n' "${bk[@]}" | join_lines || echo 'none detected')"

# ---- Listening services ----------------------------------------------------------------------------
section "Listening services"
if have ss; then
  while IFS=$'\t' read -r port procs addrs; do
    add "$port" "$procs on $addrs"
  done < <(ss -ltnup 2>/dev/null | awk 'NR > 1 {
      local = $5; p = local; sub(/.*:/, "", p); a = local; sub(/:[^:]*$/, "", a); sub(/%.*/, "", a)
      name = "?"; if (match($0, /users:\(\("[^"]+"/)) name = substr($0, RSTART + 9, RLENGTH - 10)
      sub(/ \(.*$/, "", name); sub(/ - .*$/, "", name)   # process titles like "cpsrvd (SSL) - ..." -> cpsrvd
      k = p "/" $1
      if (!(k in n)) n[k] = name; else if (index(n[k], name) == 0) n[k] = n[k] "," name
      if (!(k in ad)) ad[k] = a; else if (index(ad[k], a) == 0) ad[k] = ad[k] " " a
    } END { for (k in n) print k "\t" n[k] "\t" ad[k] }' | sort -t/ -k1,1n)
else
  add "ss" "skipped: ss not installed"
fi

# ---- Updates -----------------------------------------------------------------------------------------
section "Updates"
if (( UPDATES )); then
  if have dnf || have yum; then
    pm=$(command -v dnf || command -v yum)
    out=$(timeout 120 "$pm" -q -C check-update 2>/dev/null); rc=$?
    case $rc in
      0) add "Pending updates" "0 (from the local ${pm##*/} cache)" ;;
      100) add "Pending updates" "$(awk '/^Obsoleting/ { exit } NF == 3 && $1 ~ /\./ { n++ } END { print n + 0 }' <<< "$out") (from the local ${pm##*/} cache)" ;;
      *) add "Pending updates" "unknown (no ${pm##*/} cache yet: run ${pm##*/} makecache)" ;;
    esac
  elif have apt-get; then
    out=$(apt-get -s -o Debug::NoLocking=1 upgrade 2>/dev/null | grep '^Inst')
    add "Pending updates" "$(grep -c . <<< "$out") ($(grep -ci security <<< "$out") security; as of the last apt update)"
  else
    add "Pending updates" "skipped: no dnf, yum or apt"
  fi
fi

# ---- output ------------------------------------------------------------------------------------------
jstr() { local s=$1; s=${s//\\/\\\\}; s=${s//\"/\\\"}; s=${s//$'\t'/ }; s=${s//$'\n'/ }; printf '"%s"' "$s"; }
jkey() { if [[ "$1" =~ ^[A-Za-z] ]]; then tr 'A-Z' 'a-z' <<< "$1" | sed -E 's/[^a-z0-9]+/_/g; s/_$//'; else printf '%s' "$1"; fi; }
if (( COLOR )); then H=$'\e[1m'; N=$'\e[0m'; else H=""; N=""; fi
last=""; n=${#KEY[@]}
case $FORMAT in
  text)
    printf 'Server inventory: %s   (%s)\n' "$(hostname)" "$(date '+%Y-%m-%d %H:%M %Z')"
    for ((i = 0; i < n; i++)); do
      [[ "${SEC[i]}" != "$last" ]] && { printf '\n%s== %s ==%s\n' "$H" "${SEC[i]}" "$N"; last=${SEC[i]}; }
      printf '  %-22s %s\n' "${KEY[i]}" "${VAL[i]}"
    done ;;
  markdown)
    printf '# Server inventory: %s\n\nGenerated %s by server-inventory-report.sh\n' "$(hostname)" "$(date '+%Y-%m-%d %H:%M %Z')"
    for ((i = 0; i < n; i++)); do
      [[ "${SEC[i]}" != "$last" ]] && { printf '\n## %s\n\n| Item | Value |\n|---|---|\n' "${SEC[i]}"; last=${SEC[i]}; }
      printf '| %s | %s |\n' "${KEY[i]//|/\\|}" "${VAL[i]//|/\\|}"
    done ;;
  json)
    printf '{\n  "generated": %s' "$(jstr "$(date -u '+%Y-%m-%dT%H:%M:%SZ')")"
    for ((i = 0; i < n; i++)); do
      if [[ "${SEC[i]}" != "$last" ]]; then
        [[ -n "$last" ]] && printf '\n  }'
        printf ',\n  %s: {\n' "$(jstr "$(jkey "${SEC[i]}")")"; last=${SEC[i]}
      else printf ',\n'; fi
      printf '    %s: %s' "$(jstr "$(jkey "${KEY[i]}")")" "$(jstr "${VAL[i]}")"
    done
    [[ -n "$last" ]] && printf '\n  }'
    printf '\n}\n' ;;
esac
exit 0
Version 1.0.1 · SHA-256 eaabeba1ac068ed7e368f04c22b9feca2dd7aaca90b84a4a033c8d19cd031d7b
Download and verify on Linux or macOS
curl -fsSL -o server-inventory-report.sh https://scr.srvscripts.com/server-inventory-report/server-inventory-report.sh && curl -fsSL https://scr.srvscripts.com/server-inventory-report/server-inventory-report.sh.sha256 | sha256sum -c
Download and verify in Windows PowerShell
Invoke-WebRequest -Uri 'https://scr.srvscripts.com/server-inventory-report/server-inventory-report.sh' -OutFile 'server-inventory-report.sh'; if ((Get-FileHash 'server-inventory-report.sh' -Algorithm SHA256).Hash -eq 'EAABEBA1AC068ED7E368F04C22B9FECA2DD7AACA90B84A4A033C8D19CD031D7B') { 'OK: the file is intact' } else { 'MISMATCH: do not run this file' }
Copy the whole line. In Windows PowerShell, curl and sha256sum are not the Linux tools, so use the PowerShell line there.
Also on GitHub: github.com/srvscripts/scripts

Frequently asked questions

Does it work on DirectAdmin and Plesk servers?

Yes. It reads the DirectAdmin binary’s version and /usr/local/psa/version for Plesk, and finds DirectAdmin’s PHP builds in /usr/local/php*.

Can I collect the report from many servers?

Run it over SSH with --json and save one file per host, for example ssh root@web1 'bash -s -- --json' < server-inventory-report.sh > web1.json.

Why is “Pending updates” unknown?

On dnf and yum systems the count comes from the local cache only. Run dnf makecache once, or let dnf-makecache.timer do it.

Does it send the inventory anywhere?

No. It prints to standard output and nothing else; where the report goes is up to you.

Changelog

  • 1.0.1 — ImunifyAV is reported as ImunifyAV (malware scanner only) instead of Imunify360; process titles with spaces, such as cpsrvd (SSL), are shortened to the program name. Found in our lab test, 5 Oct 2026.
  • 1.0.0 — Initial release

Free website test

Is your website set up right?

Check SSL, security headers, redirects, robots.txt, sitemap, llms.txt and security.txt in one test. It takes about 30 seconds.