etc/security
author jmmv <jmmv@NetBSD.org>
Fri, 02 Apr 2004 13:13:47 +0000
branchtrunk
changeset 125761 d6de26b19154
parent 123879 45e2bba5ae2d
child 125980 3ed943f94165
permissions -rw-r--r--
Introduce and use the rcvar_manpage variable, which contains the manual page name where the user should look at for documentation about rcvar. It defaults to 'rc.subr(5)', as rc.subr is mainly used by rc.d scripts. This variable is useful to let the daily, weekly, monthly and security scripts tune the warning message shown when any of the variables they handle is not properly set. Closes PR misc/23908.

#!/bin/sh -
#
#	$NetBSD: security,v 1.89 2004/04/02 13:13:47 jmmv Exp $
#	from: @(#)security	8.1 (Berkeley) 6/9/93
#

PATH=/sbin:/usr/sbin:/bin:/usr/bin

rcvar_manpage='security.conf(5)'

if [ -f /etc/rc.subr ]; then
	. /etc/rc.subr
else
	echo "Can't read /etc/rc.subr; aborting."
	exit 1;
fi

umask 077
TZ=UTC; export TZ

if [ -s /etc/security.conf ]; then
	. /etc/security.conf
fi

# Set reasonable defaults (if they're not set in security.conf)
#
backup_dir=${backup_dir:-/var/backups}
pkgdb_dir=${pkgdb_dir:-/var/db/pkg}
max_loginlen=${max_loginlen:-8}
max_grouplen=${max_grouplen:-8}

# Other configurable variables
#
special_files="/etc/mtree/special /etc/mtree/special.local"
MP=/etc/master.passwd
CHANGELIST=""
work_dir=$backup_dir/work

if [ ! -d "$work_dir" ]; then
	mkdir -p "$work_dir"
fi

SECUREDIR=`mktemp -d /tmp/_securedir.XXXXXX` || exit 1

trap "/bin/rm -rf $SECUREDIR ; exit 0" EXIT INT QUIT PIPE

if ! cd "$SECUREDIR"; then
	echo "Can not cd to $SECUREDIR".
	exit 1
fi

ERR=secure1.$$
TMP1=secure2.$$
TMP2=secure3.$$
MPBYUID=secure4.$$
MPBYPATH=secure5.$$
LIST=secure6.$$
OUTPUT=secure7.$$
LABELS=secure8.$$
PKGS=secure9.$$
CHANGEFILES=secure10.$$


# migrate_file old new
#	Determine if the "${old}" path name needs to be migrated to the
#	"${new}" path. Also checks if "${old}.current" needs migrating,
#	and if so, migrate it and possibly "${old}.current,v" and
#	"${old}.backup".
#
migrate_file()
{
	_old=$1
	_new=$2
	if [ -z "$_old" -o -z "$_new" ]; then
		err 3 "USAGE: migrate_file old new"
	fi
	if [ ! -d "${_new%/*}" ]; then
		mkdir -p "${_new%/*}"
	fi
	if [ -f "${_old}" -a ! -f "${_new}" ]; then
		echo "==> migrating ${_old}"
		echo "           to ${_new}"
		mv "${_old}" "${_new}"
	fi
	if [ -f "${_old}.current" -a ! -f "${_new}.current" ]; then
		echo "==> migrating ${_old}.current"
		echo "           to ${_new}.current"
		mv "${_old}.current" "${_new}.current"
		if [ -f "${_old}.current,v" -a ! -f "${_new}.current,v" ]; then
			echo "==> migrating ${_old}.current,v"
			echo "           to ${_new}.current,v"
			mv "${_old}.current,v" "${_new}.current,v"
		fi
		if [ -f "${_old}.backup" -a ! -f "${_new}.backup" ]; then
			echo "==> migrating ${_old}.backup"
			echo "           to ${_new}.backup"
			mv "${_old}.backup" "${_new}.backup"
		fi
	fi
}


# backup_and_diff file printdiff
#	Determine if file needs backing up, and if so, do it.
#	If printdiff is yes, display the diffs, otherwise 
#	just print a message saying "[changes omitted]".
#
backup_and_diff()
{
	_file=$1
	_printdiff=$2
	if [ -z "$_file" -o -z "$_printdiff" ]; then
		err 3 "USAGE: backup_and_diff file printdiff"
	fi
	! checkyesno _printdiff
	_printdiff=$?

	_old=$backup_dir/${_file##*/}
	case "$_file" in
	$work_dir/*)
		_new=$_file
		migrate_file "$backup_dir/$_old" "$_new"
		migrate_file "$_old" "$_new"
		;;
	*)
		_new=$backup_dir/$_file
		migrate_file "$_old" "$_new"
		;;
	esac
	CUR=${_new}.current
	BACK=${_new}.backup
	if [ -f $_file ]; then
		if [ -f $CUR ] ; then
			if [ "$_printdiff" -ne 0 ]; then
				diff ${diff_options} $CUR $_file > $OUTPUT
			else
				if ! cmp -s $CUR $_file; then
					echo "[changes omitted]"
				fi > $OUTPUT
			fi
			if [ -s $OUTPUT ] ; then
				printf \
			"\n======\n%s diffs (OLD < > NEW)\n======\n" $_file
				cat $OUTPUT
				backup_file update $_file $CUR $BACK
			fi
		else
			printf "\n======\n%s added\n======\n" $_file
			if [ "$_printdiff" -ne 0 ]; then
				diff ${diff_options} /dev/null $_file
			else
				echo "[changes omitted]"
			fi
			backup_file add $_file $CUR $BACK
		fi
	else
		if [ -f $CUR ]; then
			printf "\n======\n%s removed\n======\n" $_file
			if [ "$_printdiff" -ne 0 ]; then
				diff ${diff_options} $CUR /dev/null
			else
				echo "[changes omitted]"
			fi
			backup_file remove $_file $CUR $BACK
		fi
	fi
}


# These are used several times.
#
awk -F: '!/^+/ { print $1 " " $3 }' $MP | sort -k2n > $MPBYUID
awk -F: '{ print $1 " " $9 }' $MP | sort -k2 > $MPBYPATH


# Check the master password file syntax.
#
if checkyesno check_passwd; then
        # XXX: the sense of permit_star is reversed; the code works as
        # implemented, but usage needs to be negated.
	checkyesno check_passwd_permit_star && permit_star=0 || permit_star=1
	awk -v "len=$max_loginlen" \
	    -v "nowarn_shells_list=$check_passwd_nowarn_shells" \
	    -v "nowarn_users_list=$check_passwd_nowarn_users" \
	    -v "permit_star=$permit_star" '
	BEGIN {
		while ( getline < "/etc/shells" > 0 ) {
			if ($0 ~ /^\#/ || $0 ~ /^$/ )
				continue;
			shells[$1]++;
		}
		split(nowarn_shells_list, a);
		for (i in a) nowarn_shells[a[i]]++;
		split(nowarn_users_list, a);
		for (i in a) nowarn_users[a[i]]++;
		uid0_users_list="root toor"
		split(uid0_users_list, a);
		for (i in a) uid0_users[a[i]]++;
		FS=":";
	}

	{
		if ($0 ~ /^[	 ]*$/) {
			printf "Line %d is a blank line.\n", NR;
			next;
		}
		if (NF != 10 && ($1 != "+" || NF != 1))
			printf "Line %d has the wrong number of fields.\n", NR;
		if ($1 == "+" )  {
			if (NF != 1 && $3 == 0)
			    printf "Line %d includes entries with uid 0.\n",
			        NR;
			next;
		}
		if ($1 !~ /^[A-Za-z0-9]([-A-Za-z0-9]*[A-Za-z0-9])*$/)
			printf "Login %s has non-alphanumeric characters.\n",
			    $1;
		if (length($1) > len)
			printf "Login %s has more than "len" characters.\n",
			    $1;
		if ($2 == "" && !nowarn_users[$1])
			    printf "Login %s has no password.\n", $1;
		if (!nowarn_shells[$10] && !nowarn_users[$1]) {
		    if (length($2) != 13 &&
		    	length($2) != 20 &&
		    	$2 !~ /^\$1/ &&
		    	$2 !~ /^\$2/ &&
		    	$2 != "" &&
			(permit_star || $2 != "*") &&
		    	$2 !~ /^\*[A-z-]+$/ &&
			$1 != "toor") {
		    	    if ($10 == "" || shells[$10])
				printf "Login %s is off but still has "\
				  "a valid shell (%s)\n", $1, $10;
		    } else if (! shells[$10])
		    	    printf "Login %s does not have a valid "\
			    "shell (%s)\n", $1, $10;
		}
		if ($3 == 0 && !uid0_users[$1] && !nowarn_users[$1])
			printf "Login %s has a user id of 0.\n", $1;
		if ($3 < 0)
			printf "Login %s has a negative user id.\n", $1;
		if ($4 < 0)
			printf "Login %s has a negative group id.\n", $1;
	}' < $MP > $OUTPUT
	if [ -s $OUTPUT ] ; then
		printf "\nChecking the $MP file:\n"
		cat $OUTPUT
	fi

	awk -F: '{ print $1 }' $MP | sort | uniq -d > $OUTPUT
	if [ -s $OUTPUT ] ; then
		printf "\n$MP has duplicate user names.\n"
		column $OUTPUT
	fi

# To not exclude 'toor', a standard duplicate root account, from the duplicate
# account test, uncomment the line below (without egrep in it)and comment
# out the line (with egrep in it) below it.
#
#	< $MPBYUID uniq -d -f 1 | awk '{ print $2 }' > $TMP2
	< $MPBYUID egrep -v '^toor ' | uniq -d -f 1 | awk '{ print $2 }' > $TMP2
	if [ -s $TMP2 ] ; then
		printf "\n$MP has duplicate user id's.\n"
		while read uid; do
			grep -w $uid $MPBYUID
		done < $TMP2 | column
	fi
fi

# Check the group file syntax.
#
if checkyesno check_group; then
	GRP=/etc/group
	awk -F: -v "len=$max_grouplen" '{
		if ($0 ~ /^[	 ]*$/) {
			printf "Line %d is a blank line.\n", NR;
			next;
		}
		if (NF != 4 && ($1 != "+" || NF != 1))
			printf "Line %d has the wrong number of fields.\n", NR;
		if ($1 == "+" )  {
			next;
		}
		if ($1 !~ /^[A-Za-z0-9]([-A-Za-z0-9]*[A-Za-z0-9])*$/)
			printf "Group %s has non-alphanumeric characters.\n",
			    $1;
		if (length($1) > len)
			printf "Group %s has more than "len" characters.\n", $1;
		if ($3 !~ /[0-9]*/)
			printf "Login %s has a negative group id.\n", $1;
	}' < $GRP > $OUTPUT
	if [ -s $OUTPUT ] ; then
		printf "\nChecking the $GRP file:\n"
		cat $OUTPUT
	fi

	awk -F: '{ print $1 }' $GRP | sort | uniq -d > $OUTPUT
	if [ -s $OUTPUT ] ; then
		printf "\n$GRP has duplicate group names.\n"
		column $OUTPUT
	fi
fi

# Check for root paths, umask values in startup files.
# The check for the root paths is problematical -- it's likely to fail
# in other environments.  Once the shells have been modified to warn
# of '.' in the path, the path tests should go away.
#
if checkyesno check_rootdotfiles; then
	rhome=~root
	umaskset=no
	list="/etc/csh.cshrc /etc/csh.login ${rhome}/.cshrc ${rhome}/.login"
	for i in $list ; do
		if [ -f $i ] ; then
			if egrep '^[ \t]*umask[ \t]+[0-7]+' $i > /dev/null ;
			then
				umaskset=yes
			fi
			# Double check the umask value itself; ensure that
			# both the group and other write bits are set.
			#
			egrep '^[ \t]*umask[ \t]+[0-7]+' $i |
			awk '{
				if ($2 ~ /^.$/ || $2 ~! /[^2367].$/) {
					print "\tRoot umask is group writable"
				}
				if ($2 ~ /[^2367]$/) {
					print "\tRoot umask is other writable"
			    	}
			    }' | sort -u
			SAVE_PATH=$PATH
			unset PATH
			/bin/csh -f -s << end-of-csh > /dev/null 2>&1
				source $i
				/bin/ls -ldgT \$path > $TMP1
end-of-csh
			export PATH=$SAVE_PATH
			awk '{
				if ($10 ~ /^\.$/) {
					print "\tThe root path includes .";
					next;
				}
			     }
			     $1 ~ /^d....w/ \
		{ print "\tRoot path directory " $10 " is group writable." } \
			     $1 ~ /^d.......w/ \
		{ print "\tRoot path directory " $10 " is other writable." }' \
			< $TMP1
		fi
	done > $OUTPUT
	if [ $umaskset = "no" -o -s $OUTPUT ] ; then
		printf "\nChecking root csh paths, umask values:\n$list\n\n"
		if [ -s $OUTPUT ]; then
			cat $OUTPUT
		fi
		if [ $umaskset = "no" ] ; then
		    printf "\tRoot csh startup files do not set the umask.\n"
		fi
	fi

	umaskset=no
	list="/etc/profile ${rhome}/.profile"
	for i in $list; do
		if [ -f $i ] ; then
			if egrep umask $i > /dev/null ; then
				umaskset=yes
			fi
			egrep umask $i |
			awk '$2 ~ /^.$/ || $2 ~ /[^2367].$/ \
				{ print "\tRoot umask is group writable" } \
			     $2 ~ /[^2367]$/ \
				{ print "\tRoot umask is other writable" }'
			SAVE_PATH=$PATH
			unset PATH
			/bin/sh << end-of-sh > /dev/null 2>&1
				. $i
				list=\`echo \$PATH | /usr/bin/sed -e \
				    's/^:/.:/;s/:$/:./;s/::/:.:/g;s/:/ /g'\`
				/bin/ls -ldgT \$list > $TMP1
end-of-sh
			export PATH=$SAVE_PATH
			awk '{
				if ($10 ~ /^\.$/) {
					print "\tThe root path includes .";
					next;
				}
			     }
			     $1 ~ /^d....w/ \
		{ print "\tRoot path directory " $10 " is group writable." } \
			     $1 ~ /^d.......w/ \
		{ print "\tRoot path directory " $10 " is other writable." }' \
			< $TMP1

		fi
	done > $OUTPUT
	if [ $umaskset = "no" -o -s $OUTPUT ] ; then
		printf "\nChecking root sh paths, umask values:\n$list\n"
		if [ -s $OUTPUT ]; then
			cat $OUTPUT
		fi
		if [ $umaskset = "no" ] ; then
			printf "\tRoot sh startup files do not set the umask.\n"
		fi
	fi
fi

# Root and uucp should both be in /etc/ftpusers.
#
if checkyesno check_ftpusers; then
	list="uucp "`awk '$2 == 0 { print $1 }' $MPBYUID`
	for i in $list; do
		if /usr/libexec/ftpd -C $i ; then
			printf "\t$i is not denied\n"
		fi
	done > $OUTPUT
	if [ -s $OUTPUT ]; then
		printf "\nChecking the /etc/ftpusers configuration:\n"
		cat $OUTPUT
	fi
fi

# Uudecode should not be in the /etc/mail/aliases file.
#
if checkyesno check_aliases; then
	for f in /etc/mail/aliases /etc/aliases; do
		if [ -f $f ] && egrep '^[^#]*(uudecode|decode).*\|' $f; then
			printf "\nEntry for uudecode in $f file.\n"
		fi
	done
fi

# Files that should not have + signs.
#
if checkyesno check_rhosts; then
	list="/etc/hosts.equiv /etc/hosts.lpd"
	for f in $list ; do
		if [ -f $f ] && egrep '\+' $f > /dev/null ; then
			printf "\nPlus sign in $f file.\n"
		fi
	done

	# Check for special users with .rhosts files.  Only root and toor should
	# have .rhosts files.  Also, .rhosts files should not have plus signs.
	awk -F: '$1 != "root" && $1 != "toor" && \
		($3 < 100 || $1 == "ftp" || $1 == "uucp") \
			{ print $1 " " $9 }' $MP |
	sort -k2 |
	while read uid homedir; do
		if [ -f ${homedir}/.rhosts ] ; then
			rhost=`ls -ldgT ${homedir}/.rhosts`
			printf -- "$uid: $rhost\n"
		fi
	done > $OUTPUT
	if [ -s $OUTPUT ] ; then
		printf "\nChecking for special users with .rhosts files.\n"
		cat $OUTPUT
	fi

	while read uid homedir; do
		if [ -f ${homedir}/.rhosts -a -r ${homedir}/.rhosts ] && \
		    cat -f ${homedir}/.rhosts | egrep '\+' > /dev/null ; then
			printf -- "$uid: + in .rhosts file.\n"
		fi
	done < $MPBYPATH > $OUTPUT
	if [ -s $OUTPUT ] ; then
		printf "\nChecking .rhosts files syntax.\n"
		cat $OUTPUT
	fi
fi

# Check home directories.  Directories should not be owned by someone else
# or writable.
#
if checkyesno check_homes; then
	checkyesno check_homes_permit_usergroups && \
		permit_usergroups=1 || permit_usergroups=0
	while read uid homedir; do
		if [ -d ${homedir}/ ] ; then
			file=`ls -ldgT ${homedir}`
			printf -- "$uid $file\n"
		fi
	done < $MPBYPATH |
	awk -v "usergroups=$permit_usergroups" '
	     $1 != $4 && $4 != "root" \
		{ print "user " $1 " home directory is owned by " $4 }
	     $2 ~ /^-....w/ && (!usergroups || $5 != $1) \
		{ print "user " $1 " home directory is group writable" }
	     $2 ~ /^-.......w/ \
		{ print "user " $1 " home directory is other writable" }' \
	    > $OUTPUT
	if [ -s $OUTPUT ] ; then
		printf "\nChecking home directories.\n"
		cat $OUTPUT
	fi

	# Files that should not be owned by someone else or readable.
	list=".Xauthority .netrc .ssh/id_dsa .ssh/id_rsa .ssh/identity"
	while read uid homedir; do
		for f in $list ; do
			file=${homedir}/${f}
			if [ -f $file ] ; then
				printf -- "$uid $f `ls -ldgT $file`\n"
			fi
		done
	done < $MPBYPATH |
	awk  -v "usergroups=$permit_usergroups" '
	     $1 != $5 && $5 != "root" \
		{ print "user " $1 " " $2 " file is owned by " $5 }
	     $3 ~ /^-...r/ && (!usergroups || $6 != $1) \
		{ print "user " $1 " " $2 " file is group readable" }
	     $3 ~ /^-......r/ \
		{ print "user " $1 " " $2 " file is other readable" }
	     $3 ~ /^-....w/ && (!usergroups || $6 != $1) \
		{ print "user " $1 " " $2 " file is group writable" }
	     $3 ~ /^-.......w/ \
		{ print "user " $1 " " $2 " file is other writable" }' \
	    > $OUTPUT

	# Files that should not be owned by someone else or writable.
	list=".bash_history .bash_login .bash_logout .bash_profile .bashrc \
	      .cshrc .emacs .exrc .forward .history .k5login .klogin .login \
	      .logout .profile .qmail .rc_history .rhosts .shosts ssh .tcshrc \
	      .twmrc .xinitrc .xsession .ssh/authorized_keys \
	      .ssh/authorized_keys2 .ssh/config .ssh/id_dsa.pub \
	      .ssh/id_rsa.pub .ssh/identity.pub .ssh/known_hosts \
	      .ssh/known_hosts2"
	while read uid homedir; do
		for f in $list ; do
			file=${homedir}/${f}
			if [ -f $file ] ; then
				printf -- "$uid $f `ls -ldgT $file`\n"
			fi
		done
	done < $MPBYPATH |
	awk -v "usergroups=$permit_usergroups" '
	     $1 != $5 && $5 != "root" \
		{ print "user " $1 " " $2 " file is owned by " $5 }
	     $3 ~ /^-....w/ && (!usergroups || $6 != $1) \
		{ print "user " $1 " " $2 " file is group writable" }
	     $3 ~ /^-.......w/ \
		{ print "user " $1 " " $2 " file is other writable" }' \
	    >> $OUTPUT
	if [ -s $OUTPUT ] ; then
		printf "\nChecking dot files.\n"
		cat $OUTPUT
	fi
fi

# Mailboxes should be owned by user and unreadable.
#
if checkyesno check_varmail; then
	ls -lA /var/mail | \
	awk '	NR == 1 { next; }
		$9 ~ /^\./ {next; }
	    	$3 != $9 {
			print "user " $9 " mailbox is owned by " $3
		}
		$1 != "-rw-------" {
			print "user " $9 " mailbox is " $1 ", group " $4
		}' > $OUTPUT
	if [ -s $OUTPUT ] ; then
		printf "\nChecking mailbox ownership.\n"
		cat $OUTPUT
	fi
fi

# NFS exports shouldn't be globally exported
#
if checkyesno check_nfs && [ -f /etc/exports ]; then
	awk '{
		# ignore comments and blank lines
		if ($0 ~ /^\#/ || $0 ~ /^$/ )
			next;

		readonly = 0;
		for (i = 2; i <= NF; ++i) {
			if ($i ~ /-ro/)
				readonly = 1;
			else if ($i !~ /^-/)
				next;
		}
		if (readonly)
			print "File system " $1 " globally exported, read-only."
		else
			print "File system " $1 " globally exported, read-write."
	}' < /etc/exports > $OUTPUT
	if [ -s $OUTPUT ] ; then
		printf "\nChecking for globally exported file systems.\n"
		cat $OUTPUT
	fi
fi

# Display any changes in setuid files and devices.
#
if checkyesno check_devices; then
	> $ERR
	(find / \( ! -fstype local -o -fstype fdesc -o -fstype kernfs \
			-o -fstype null \
			-o -fstype procfs \) -a -prune -o \
	    \( \( -perm -u+s -a ! -type d \) -o \
	       \( -perm -g+s -a ! -type d \) -o \
	       -type b -o -type c \) -print0 | \
	xargs -0 ls -ldgTq | sort +9 > $LIST) 2> $OUTPUT

	# Display any errors that occurred during system file walk.
	if [ -s $OUTPUT ] ; then
		printf "Setuid/device find errors:\n" >> $ERR
		cat $OUTPUT >> $ERR
		printf "\n" >> $ERR
	fi

	# Display any changes in the setuid file list.
	egrep -v '^[bc]' $LIST > $TMP1
	if [ -s $TMP1 ] ; then
		# Check to make sure uudecode isn't setuid.
		if grep -w uudecode $TMP1 > /dev/null ; then
			printf "\nUudecode is setuid.\n" >> $ERR
		fi

		file=$work_dir/setuid
		migrate_file "$backup_dir/setuid" "$file"
		CUR=${file}.current
		BACK=${file}.backup
		if [ -s $CUR ] ; then
			if cmp -s $CUR $TMP1 ; then
				:
			else
				> $TMP2
				join -110 -210 -v2 $CUR $TMP1 > $OUTPUT
				if [ -s $OUTPUT ] ; then
					printf "Setuid additions:\n" >> $ERR
					tee -a $TMP2 < $OUTPUT >> $ERR
					printf "\n" >> $ERR
				fi

				join -110 -210 -v1 $CUR $TMP1 > $OUTPUT
				if [ -s $OUTPUT ] ; then
					printf "Setuid deletions:\n" >> $ERR
					tee -a $TMP2 < $OUTPUT >> $ERR
					printf "\n" >> $ERR
				fi

				sort -k10 $TMP2 $CUR $TMP1 | \
				    sed -e 's/[	 ][	 ]*/ /g' | \
				    uniq -u > $OUTPUT
				if [ -s $OUTPUT ] ; then
					printf "Setuid changes:\n" >> $ERR
					column -t $OUTPUT >> $ERR
					printf "\n" >> $ERR
				fi

				backup_file update $TMP1 $CUR $BACK
			fi
		else
			printf "Setuid additions:\n" >> $ERR
			column -t $TMP1 >> $ERR
			printf "\n" >> $ERR
			backup_file add $TMP1 $CUR $BACK
		fi
	fi

	# Check for block and character disk devices that are readable or
	# writable or not owned by root.operator.
	>$TMP1
	DISKLIST="ccd ch hk hp ld md ra raid rb rd rl rx \
	    sd se ss uk up vnd wd xd xy"
#	DISKLIST="$DISKLIST ct mt st wt"
	for i in $DISKLIST; do
		egrep "^b.*/${i}[0-9][0-9]*[a-p]$"  $LIST >> $TMP1
		egrep "^c.*/r${i}[0-9][0-9]*[a-p]$"  $LIST >> $TMP1
	done

	awk '$3 != "root" || $4 != "operator" || $1 !~ /.rw-r-----/ \
		{ printf "Disk %s is user %s, group %s, permissions %s.\n", \
		    $11, $3, $4, $1; }' < $TMP1 > $OUTPUT
	if [ -s $OUTPUT ] ; then
		printf "\nChecking disk ownership and permissions.\n" >> $ERR
		cat $OUTPUT >> $ERR
		printf "\n" >> $ERR
	fi

	# Display any changes in the device file list.
	egrep '^[bc]' $LIST | sort -k11 > $TMP1
	if [ -s $TMP1 ] ; then
		file=$work_dir/device
		migrate_file "$backup_dir/device" "$file"
		CUR=${file}.current
		BACK=${file}.backup

		if [ -s $CUR ] ; then
			if cmp -s $CUR $TMP1 ; then
				:
			else
				> $TMP2
				join -111 -211 -v2 $CUR $TMP1 > $OUTPUT
				if [ -s $OUTPUT ] ; then
					printf "Device additions:\n" >> $ERR
					tee -a $TMP2 < $OUTPUT >> $ERR
					printf "\n" >> $ERR
				fi

				join -111 -211 -v1 $CUR $TMP1 > $OUTPUT
				if [ -s $OUTPUT ] ; then
					printf "Device deletions:\n" >> $ERR
					tee -a $TMP2 < $OUTPUT >> $ERR
					printf "\n" >> $ERR
				fi

				# Report any block device change. Ignore
				# character devices, only the name is
				# significant.
				cat $TMP2 $CUR $TMP1 | \
				    sed -e '/^c/d' | \
				    sort -k11 | \
				    sed -e 's/[	 ][	 ]*/ /g' | \
				    uniq -u > $OUTPUT
				if [ -s $OUTPUT ] ; then
					printf "Block device changes:\n" >> $ERR
					column -t $OUTPUT >> $ERR
					printf "\n" >> $ERR
				fi

				backup_file update $TMP1 $CUR $BACK
			fi
		else
			printf "Device additions:\n" >> $ERR
			column -t $TMP1 >> $ERR
			printf "\n" >> $ERR
			backup_file add $TMP1 $CUR $BACK >> $ERR
		fi
	fi
	if [ -s $ERR ] ; then
		printf "\nChecking setuid files and devices:\n"
		cat $ERR
		printf "\n"
	fi
fi

# Check special files.
# Check system binaries.
#
# Create the mtree tree specifications using:
#	mtree -cx -pDIR -kmd5,uid,gid,mode,nlink,size,link,time > DIR.secure
#	chown root:wheel DIR.secure
#	chmod u+r,go= DIR.secure
#
# Note, this is not complete protection against Trojan horsed binaries, as
# the hacker can modify the tree specification to match the replaced binary.
# For details on really protecting yourself against modified binaries, see
# the mtree(8) manual page.
#
if checkyesno check_mtree; then
	if checkyesno check_mtree_follow_symlinks; then
		check_mtree_flags="-L"
	else
		check_mtree_flags=""
	fi
	for file in $special_files; do
		[ ! -s $file ] && continue
		mtree -e -l -p / $check_mtree_flags -f $file
	done 3>&1 >$OUTPUT 2>&3 |
		grep -v '^mtree: dev/tty: Device not configured$' >&2
	if [ -s $OUTPUT ]; then
		printf "\nChecking special files and directories.\n"
		cat $OUTPUT
	fi

	for file in /etc/mtree/*.secure; do
		[ $file = '/etc/mtree/*.secure' ] && continue
		tree=`sed -n -e '3s/.* //p' -e 3q $file`
		mtree $check_mtree_flags -f $file -p $tree > $TMP1
		if [ -s $TMP1 ]; then
			printf "\nChecking $tree:\n"
			cat $TMP1
		fi
	done > $OUTPUT
	if [ -s $OUTPUT ]; then
		printf "\nChecking system binaries:\n"
		cat $OUTPUT
	fi
fi

# Backup disklabels of available disks
#
if checkyesno check_disklabels; then
		# migrate old disklabels
	for file in `ls -1d $backup_dir/$backup_dir/disklabel.* \
	    $backup_dir/disklabel.* 2>/dev/null`; do
		migrate_file "$file" "$work_dir/${file##*/}"
	done

		# generate list of old disklabels & fdisks and remove them
	ls -1d $work_dir/disklabel.* $work_dir/fdisk.* 2>/dev/null |
	    egrep -v '\.(backup|current)(,v)?$' > $LABELS
	xargs rm < $LABELS

		# generate disklabels of all disks excluding:	cd fd md
	disks=`iostat -x | awk 'NR > 1 && $1 !~ /^[cfm]d/ { print $1; }'`
	for i in $disks; do
		disklabel $i > "$work_dir/disklabel.$i" 2>/dev/null
	done

		# if fdisk is available, generate fdisks for:	ed ld sd wd
	if [ -x /sbin/fdisk ]; then
		disks=`iostat -x| awk 'NR > 1 && $1 ~ /^[elsw]d/ { print $1; }'`
		for i in $disks; do
			/sbin/fdisk $i > "$work_dir/fdisk.$i" 2>/dev/null
		done
	fi

		# append list of new disklabels and fdisks
	ls -1d $work_dir/disklabel.* $work_dir/fdisk.* 2>/dev/null |
	    egrep -v '\.(backup|current)(,v)?$' >> $LABELS
	CHANGELIST="$LABELS $CHANGELIST"
fi

# Check for changes in the list of installed pkgs
#
if checkyesno check_pkgs && [ -d $pkgdb_dir ]; then
	pkgs=$work_dir/pkgs
	migrate_file "$backup_dir/pkgs" "$pkgs"
	(	cd $pkgdb_dir
		pkg_info | sort
		echo ""
		find . \( -name +REQUIRED_BY -o -name +CONTENTS \) -print0 |
			xargs -0 ls -ldgTq | sort -t. +1 | sed -e 's, \./, ,'
	 ) > $pkgs
	echo "$pkgs" > $PKGS
	CHANGELIST="$PKGS $CHANGELIST"
fi

# List of files that get backed up and checked for any modifications.
# Any changes cause the files to rotate.
#
if checkyesno check_changelist ; then
	for file in $special_files; do
		[ ! -s $file ] && continue
		mtree -D -k type -f $file -E exclude |
		    sed '/^type=file/!d ; s/type=file \.//'
	done > $CHANGEFILES

	(
		# Add other files which might dynamically exist:
		#	/etc/ifconfig.*
		#	/etc/raid*.conf
		#	/etc/rc.d/*
		#	/etc/rc.conf.d/*
		#
		echo "/etc/ifconfig.*"
		echo "/etc/raid*.conf"
		echo "/etc/rc.d/*"
		echo "/etc/rc.conf.d/*"

		# Add /etc/changelist
		#
		if [ -s /etc/changelist ]; then
			grep -v '^#' /etc/changelist
		fi
	) | while read file; do
		case "$file" in
		*[\*\?\[]*)	# If changelist line is a glob ...
				# ... expand possible backup files
				#
			ls -1d $(echo $backup_dir/${file}.current) 2>/dev/null \
			    | sed "s,^$backup_dir/,, ; s,\.current$,,"
				
				# ... expand possible files
				#
			ls -1d $(echo $file) 2>/dev/null
			;;
		*)
				# Otherwise, just print the filename
			echo $file
			;;
		esac
	done >> $CHANGEFILES
	CHANGELIST="$CHANGEFILES $CHANGELIST"
fi

# Special case backups, including the master password file and
# ssh private host keys. The normal backup mechanisms for
# $check_changelist (see below) also print out the actual file
# differences and we don't want to do that for these files
#
echo $MP > $TMP1			# always add /etc/master.passwd
for file in $special_files; do
	[ ! -s $file ] && continue
	mtree -D -k type -f $file -I nodiff |
	    sed '/^type=file/!d ; s/type=file \.//'
done >> $TMP1
grep -v '^$' $TMP1 | sort -u > $TMP2

while read file; do
	backup_and_diff "$file" no
done < $TMP2


if [ -n "$CHANGELIST" ]; then
	grep -h -v '^$' $CHANGELIST | sort -u > $TMP1
	comm -23 $TMP1 $TMP2 | while read file; do
		backup_and_diff "$file" yes
	done
fi

if [ -f /etc/security.local ]; then
	. /etc/security.local > $OUTPUT
	if [ -s $OUTPUT ] ; then
		printf "\nRunning /etc/security.local:\n"
		cat $OUTPUT
	fi
fi