#!/bin/sh
# This software is GPL (GNU GENERAL PUBLIC LICENSE Version 2, June 1991) and free to use.
# For support please see rkhunter-users at lists dot sourceforge dot net.
# Please send new hashes for inclusion in Rootkit Hunter to unspawn at users dot sourceforge dot net.
# Please send comments and updates (diff -u please) of hashupd to unspawn at users dot sourceforge dot net.
# 
# Contributors: 
# John Horne <john dot horne at plymouth dot ac dot uk>
# 
# Purpose: Add-on for Rootkit Hunter. Update or add hashes for Linux distributions/releases.
# Args: config, dbpath, tmpdir, emailaddr, verbosity.
# Deps: Bash, GNU utils, Rootkit Hunter.
# Run from: manually.

# 0. TODO
# * Only tested on Fedora Core 2,4 and 5: test more.
# - Only generate changed sums.
# - Allow rpmdb or rpm proto://remote/rpms/some.rpm to generate sums.

# 1. Variables 'n stuff.
progn=hashupd
# Set sane PATH.
PATH="/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/sbin:/usr/local/bin"
export PATH
# Set umask.
umask 027
# Be verbose unless told otherwise.
V="Yes, gimme verbose output"
# Don't email unless told otherwise.
EMAIL=""
# Install defaults
DBDIR=/usr/local/rkhunter/lib/rkhunter/db
# Check for prelinking
grep -q "^PRELINKING=yes" /etc/sysconfig/prelink 2>/dev/null; MUNGED="$?"

# 2. Functions. Easy readable code.
show_help() { echo -en "hashupd for Rootkit Hunter\nargs: \n -q: quiet, only errors shown,\n -c <filename>: supply path and configuration file,\n -d <directory name>: supply path to database directory (if not found in rkhunter.conf),\n -t <directory name>: supply path to temp directory (if not found in rkhunter.conf),\n -m <email_account@domain>: email new or changed sums to address so you can send it to the maintainer.\n* ${progn} is an unofficial, but community-supported, helper application for adding and changing\nRootkit Hunter database information.\nFor support please see rkhunter-users@lists.sourceforge.net\nPlease send new hashes for inclusion in Rootkit Hunter to unspawn@users.sourceforge.net\n\n"; }
is_writable() { case "$1" in d) check_int="7";; f) check_int="6";; esac; perm=$(stat -c %a "$2" 2>/dev/null); case "${#perm}" in 3) check_pos="${perm:0:1}";; 4) check_pos="${perm:1:1}";; esac; if [ "$check_pos" != "$check_int" ]; then echo "[FATAL] "$2" not writable, exiting."; exit 1; fi; }
sum_md5() { case "$MUNGED" in 0) sum="prelink -y --md5";; *) sum=md5sum;; esac; md5=($($sum "$1" 2>/dev/null)); }
sum_sha1() { case "$MUNGED" in 0) sum="prelink -y --sha";; *) sum=sha1sum;; esac; sha1=($($sum "$1" 2>/dev/null)); }


# 3. Pre-flight checks.
while getopts c:d:m:t:qh OPT; do
 case "$OPT" in
  q) unset V
     ;;
  c) CONF="${OPTARG}"; [ -f "${CONF}" ] && [ -n "$V" ] && echo "[INFO] Using \"${CONF}\" as config."
     ;;
  d) DBDIR="${OPTARG}"; [ -d "${DBDIR}" ] && [ -n "$V" ] && echo "[INFO] Using \"${DBDIR}\" as database directory."
     ;;
  t) TMPDIR="${OPTARG}"; [ -d "${TMPDIR}" ] && [ -n "$V" ] && echo "[INFO] Using \"${TMPDIR}\" as directory for temporary files."
     ;;
  m) EMAIL="${OPTARG}"; [ -n "$V" ] && { echo "[INFO] Send sums to \"${EMAIL}\"."; MYHOSTNAME=$(hostname); eval echo "${MYHOSTNAME:=$(uname -n)}" 2>&1>/dev/null; }
     ;;
  h) show_help; exit 1
     ;;
 esac
done

#if [ "$(id -u 2>/dev/null)" != "0" ]; then
#	echo "[FATAL] Please run as root, exiting."
#	exit 127
#fi

# Queso rkhunter.conf?
if [ -z "${CONF}" ]; then
	if [ -f "/etc/rkhunter.conf" ]; then
		CONF="/etc/rkhunter.conf"
	elif [ -f "/usr/local/etc/rkhunter.conf" ]; then
		CONF="/usr/local/etc/rkhunter.conf"
	else
		type locate 2>/dev/null && CONF_loc=($(locate -r "/rkhunter\.conf$"))
		if [ "${#CONF_loc}" = "1" ]; then
			CONF=${CONF_loc[0]}
			[ -n "$V" ] && echo "[INFO] Using RKH config found as ${CONF}."
		else
			echo "[FATAL] Found ${#CONF_loc} rkhunter.conf's using locate."
			echo "[FATAL] Please rerun and supply rkhunter.conf location using \"-c\" switch, exiting."
			exit 1
		fi
	fi
fi

# Queso uh, database dir?
if [ -z "$DBDIR" -o ! -d "$DBDIR" ]; then
	DBDIR=$(grep "^DBDIR" "${CONF}"|head -1|cut -d "=" -f 2)
fi
if [ -z "$DBDIR" -o ! -d "$DBDIR" ]; then
	echo "[FATAL] Could not find database directory \"${DBDIR}\"."
	echo "[FATAL] Please rerun and supply database directory name using \"-d\" switch, exiting."
	exit 1
else
	is_writable d "${DBDIR}"
	for f in os.dat defaulthashes.dat; do
		if [ -f "${DBDIR}/${f}" ]; then
			is_writable f "${DBDIR}/${f}"
		else
			echo "[FATAL] Could not find "$f" in \"${DBDIR}\", exiting."
			exit 1
		fi
	done
fi


# temp dir?
if [ -z "$TMPDIR" -o ! -d "$TMPDIR" ]; then
	TMPDIR=$(grep "^TMPDIR" "${CONF}"|head -1|cut -d "=" -f 2)
fi

if [ -z "$TMPDIR" -o ! -d "$TMPDIR" ]; then
	export TMPDIR="/var/tmp"
	echo "[WARN] Could not find usable directory for temp files. Default to ${TMPDIR}."
fi


# 4. Main.
# Check if we have a release file.

###
if [ -e "/etc/redhat-release" ]; then
	if [ -e "/etc/mandrake-release" ]; then
		if [ -e "/etc/pclinuxos-release" ]; then
			rel_str=$(cat /etc/pclinuxos-release)
		else rel_str=$(cat /etc/mandrake-release)
		fi
	fi
	if [ -e "/etc/fedora-release" ]; then rel_str=$(cat /etc/redhat-release)
		uname_model=$(uname -m); case "$uname_model" in
		 i[0-9]86) architecture=i386;; x86_64) architecture=x86_64;; esac
		rel_str="${rel_str} (${architecture})"
	fi

	if [ -e "/etc/aurora-release" ]; then
		rel_str=$(cat /etc/aurora-release); uname_model=$(uname -m)
	fi

	if [ -e "/etc/release" ]; then TRUSTIX=$(cat /etc/release | grep Trustix)
		if [ -n "${TRUSTIX}" ]; then rel_str=$(cat /etc/release); fi
	fi

	if [ -e "/etc/tao-release" ]; then TAOREL=$(cat /etc/tao-release | grep 'Tao Linux')
		if [ -n "${TAOREL}" ]; then rel_str=$(cat /etc/tao-release); fi
	fi
	    
	if [ -f /etc/redhat-release -a -z "$rel_str" ]; then rel_str=$(cat /etc/redhat-release)
	fi
fi

if [ -e "/etc/debian_version" ]; then
	version=$(cat /etc/debian_version)
	uname_model=$(uname -m); case "$uname_model" in
	 i[0-9]86) architecture=i386;; sun4u|sparc64) architecture=sparc64;;
	 arm*) architecture=arm;; ppc) architecture=powerpc;;
	 x86_64) architecture=x86_64;; esac
	if [ -n "${version}" -a -n "${architecture}" = "" ]; then
		rel_str="Debian ${version} (${architecture})"
	fi
fi

if [ -e "/etc/pld-release" ]; then
	version=$(cat /etc/pld-release)
	uname_model=$(uname -m); case "$uname_model" in
	 i[0-9]86) architecture=i386;; sun4u|sparc64) architecture=sparc64;;
	 arm*) architecture=arm;; ppc) architecture=powerpc;; esac

	if [ -n "${version}" -a -n "${architecture}"  ]; then
		rel_str="${version} (${architecture})"
	fi
fi

if [ -e "/etc/cobalt-release" ]; then version=$(cat /etc/cobalt-release); rel_str="${version}"
fi

if [ -e "/etc/cpub-release" ]; then version=$(cat /etc/cpub-release); rel_str="${version}"
fi

if [ -e "/etc/e-smith-release" ]; then version=$(cat /etc/e-smith-release); rel_str="${version}"
fi

if [ -e "/etc/SuSE-release" ]; then version=$(cat /etc/SuSE-release | grep -i "SuSE Linux")
	rel_str="${version}"
fi

if [ -e "/etc/SLOX-release" ]; then version=$(cat /etc/SLOX-release | grep "SuSE Linux")
	rel_str="${version}"
fi

if [ -e "/etc/turbolinux-release" ]; then rel_str=$(cat /etc/turbolinux-release)
fi

if [ -e "/etc/slackware-version" ]; then rel_str=$(cat /etc/slackware-version)
fi

if [ -e "/etc/yellowdog-release" ]; then rel_str=$(cat /etc/yellowdog-release)
fi

if [ -e "/etc/caos-release" ]; then rel_str=$(cat /etc/caos-release | sed 's/^cAos: //')
fi

if [ -e "/etc/gentoo-release" ]; then
	GENTOO=1; version=$(cat /etc/gentoo-release | awk '{ print $5 }' | cut -d '.' -f1,2)
	uname_model=$(uname -m); case "$uname_model" in
	 i[0-9]86) architecture=i386;; ppc) architecture=powerpc;;
	 sparc) architecture=sparc;; sparc64) architecture=sparc64;;
	 x86_64) architecture=x86_64;; esac
	rel_str="Gentoo Linux ${version} (${architecture})"
fi
###


if [ -n "$rel_str" ]; then
	[ -n "$V" ] && echo "[INFO] Found release: \"${rel_str}\""
else
	echo "[REQ] Enter your *EXACT* release string including architecture:"
	read rel_str
	echo 
fi

# Determine if the release is in the os db.
rel_nfo=($(grep "^[0-9]\{3\}:${rel_str}" "${DBDIR}/os.dat"|sed -e "s/:/ /g"))

# Check that hashes do exist for the O/S.
nohashexist=0
if [ -n "${rel_nfo[0]}" ]; then
	grep "^${rel_nfo[0]}:" "${DBDIR}/defaulthashes.dat"|head -1 >/dev/null 2>&1
	nohashexist=$?
	if [ ${nohashexist} -eq 1 ]; then
		nohashexist=${rel_nfo[0]}
		rel_nfo=
	fi
fi


# If we have the release we'll use the number else get a new one.
if [ -z "${rel_nfo[0]}" ]; then
	[ -n "$V" ] && [ ${nohashexist} -eq 0 ] && echo "[INFO] \"${rel_str}\" wasn't found in "${DBDIR}/os.dat"."
	os_last=$(sort -t ":" -k1 -r "${DBDIR}/os.dat"|grep -v ^OS|head -1|cut -d ":" -f 1)
	os_new=$[$os_last+1]
	[ ${nohashexist} -gt 0 ] && os_new=${nohashexist}
	[ -n "$V" ] && echo "[INFO] \"${rel_str}\" has local number ${os_new}."
	prefix="${os_new}:"
	# Check for loc md5sum
	md5sum_loc=$(which md5sum 2>/dev/null)
	if [ -z "${md5sum_loc}" ]; then
		echo "[REQ] Enter location of your md5sum binary and necessary args (-q): "
		read md5sum_loc
	else
		[ -n "$V" ] && echo "[INFO] Found md5sum at ${md5sum_loc}"
	fi
	# Check for loc sha1sum
	sha1sum_loc=$(which sha1sum 2>/dev/null)
	if [ -z "${sha1sum_loc}" ]; then
		echo "[REQ] Enter location of your sha1sum binary and necessary args (-q): "
		read sha1sum_loc
	else
		[ -n "$V" ] && echo "[INFO] Found sha1sum at ${sha1sum_loc}"
	fi

	# Put info in os.dat
	os_new_str="${os_new}:${rel_str}:${md5sum_loc}:/bin:"
	if [ ${nohashexist} -eq 0 ]; then
		[ -n "$V" ] && echo "[INFO] Adding distribution/release \"${rel_str}\" to \"${DBDIR}/os.dat\""
		echo "${os_new_str}" >> "${DBDIR}/os.dat"
	fi

	# grep for mktemp v 1.5 switches
	mktemp --help 2>&1|grep -q "dqtu.*-p"; case "$?" in 
		0) MKTEMPARGS="-p "${TMPDIR}" -t";;
		*) # Doesn't recognise prefix switch, template switch probably neither
		   MKTEMPARGS="";;
	esac

	TMPFILE=$(mktemp ${MKTEMPARGS} defaulthashesnew.XXXXXXXXXX) && {
		echo "os.dat:\"${os_new_str}\"" > "${TMPFILE}"
		# Stupid way to generate list of *all* Linux OS binaries, but OK.	
		os=$(grep -i linux "${DBDIR}/os.dat" | awk -F ":" '{print $1}')
		hashfiles=($(for n in $os; do grep ^${n} "${DBDIR}/defaulthashes.dat"\
		|awk -F ":" '{print $2}'; done|\sort|\uniq))
		[ -n "$V" ] && echo "[INFO] Looking for ${#hashfiles[@]} hashes."
		for f in ${hashfiles[@]}; do
			file "$f" 2>/dev/null|egrep -qvie "(sym|script|text)"
			bintest="$?"
			if [ "$bintest" = "0" -a -x "$f" ]; then
			 sum_md5 "${f}"; sum_sha1 "${f}"
			 echo "${prefix}${md5[1]:=NO_BINARY_FOUND}:${md5[0]}:${sha1[0]}" >> "${TMPFILE}"
			fi
		done
		newhashcount=($(wc -l "${TMPFILE}"))
		newhashcount=$[$newhashcount-1]
		if [ "${newhashcount[0]}" != "${#hashfiles[@]}" ]; then
			errorcount=$(grep -c "NO_BINARY_FOUND" "${TMPFILE}")
			echo "[WARN] Found "${newhashcount[0]}" of "${#hashfiles[@]}" hashes, ${errorcount} errors found."
			#echo "[FATAL] Please examine tempfile ${TMPFILE}. Exiting now."; exit 1
		fi
		egrep -v "(^os.dat|NO_BINARY_FOUND)" "${TMPFILE}" >> "${DBDIR}/defaulthashes.dat"
		[ -n "$V" ] && echo "[INFO] added new hashes."
		[ -n "${EMAIL}" ] && cat "${TMPFILE}" | mail -s "Rootkit Hunter hashupd: new hashes added on host ${MYHOSTNAME}." ${EMAIL}
		rm -f "${TMPFILE}"
	}
	exit 0

else
	[ -n "$V" ] && echo "[INFO] \"${rel_str}\" is seq nr ${rel_nfo[0]}"
	prefix="${rel_nfo[0]}:"
	TMPFILE=$(mktemp ${MKTEMPARGS} defaulthashesupd.XXXXXXXXXX) && {
		# Prune
		perms=($(stat --format="%a %u %g" "${DBDIR}/defaulthashes.dat" 2>/dev/null))
		grep -v "^${rel_nfo[0]}:" "${DBDIR}/defaulthashes.dat" > "${TMPFILE}"
		# Rip release-related list of binaries and replace:
		hashfiles=($(grep "^${rel_nfo[0]}:" "${DBDIR}/defaulthashes.dat"|awk -F ":" '{print $2}'))
		# Graft
		for f in ${hashfiles[@]}; do
			sum_md5 "${f}"; sum_sha1 "${f}"
			echo "${prefix}${md5[1]:=NO_BINARY_FOUND}:${md5[0]}:${sha1[0]}" >> "${TMPFILE}"
		done
		install "${TMPFILE}" -m ${perms[0]} -o ${perms[1]} -g ${perms[2]} "${DBDIR}/defaulthashes.dat"
		[ -n "$V" ] && echo "[INFO] updated hashes."
		[ -n "${EMAIL}" ] && cat "${TMPFILE}" | mail -s "Rootkit Hunter hashupd: hashes updated on host ${MYHOSTNAME}." ${EMAIL}
		rm -f "${TMPFILE}"
        }
	exit 0
fi

exit 0
