#!/usr/bin/bash
# fixfiles
#
# Script to restore labels on a SELinux box
#
# Copyright (C) 2004-2013 Red Hat, Inc.
# Authors: Dan Walsh <dwalsh@redhat.com>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA

set -o nounset

#
# seclabel support was added in 2.6.30.  This function will return a positive
# number if the current kernel version is greater than 2.6.30, a negative
# number if the current is less than 2.6.30 and 0 if they are the same.
#
function useseclabel {
	VER=`uname -r`
	SUP=2.6.30
	expr '(' "$VER" : '\([^.]*\)' ')' '-' '(' "$SUP" : '\([^.]*\)' ')' '|' \
	     '(' "$VER.0" : '[^.]*[.]\([^.]*\)' ')' '-' '(' "$SUP.0" : '[^.]*[.]\([^.]*\)' ')' '|' \
	     '(' "$VER.0.0" : '[^.]*[.][^.]*[.]\([^.]*\)' ')' '-' '(' "$SUP.0.0" : '[^.]*[.][^.]*[.]\([^.]*\)' ')'
}

#
# Get all mount points that support labeling.  Use the 'seclabel' field if it
# is available.  Else fall back to known fs types which likely support xattrs
# and we know were not context mounted.
#
get_all_labeled_mounts() {
FS="`cat /proc/self/mounts | sort | uniq | awk '{print $2}'`"
for i in $FS; do
	if [ `useseclabel` -ge 0 ]
	then
		grep " $i " /proc/self/mounts | awk '{print $4}' | grep -E --silent '(^|,)seclabel(,|$)' && echo $i
	else
		grep " $i " /proc/self/mounts | grep -v "context=" | grep -E --silent '(ext[234]| ext4dev | gfs2 | xfs | jfs | btrfs )' && echo $i
	fi
done
}

get_rw_labeled_mounts() {
FS=`get_all_labeled_mounts | sort | uniq`
for i in $FS; do
	grep " $i " /proc/self/mounts | awk '{print $4}' | grep -E --silent '(^|,)rw(,|$)' && echo $i
done
}

get_ro_labeled_mounts() {
FS=`get_all_labeled_mounts | sort | uniq`
for i in $FS; do
	grep " $i " /proc/self/mounts | awk '{print $4}' | grep -E --silent '(^|,)ro(,|$)' && echo $i
done
}

#
# Get the default label returned from the kernel for a file with a label the
# kernel does not understand
#
get_undefined_type() {
	SELINUXMNT=`grep selinuxfs /proc/self/mountinfo | head -1 | awk '{ print $5 }'`
	cat ${SELINUXMNT}/initial_contexts/unlabeled | secon -t
}

#
# Get the default label for a file without a label
#
get_unlabeled_type() {
	SELINUXMNT=`grep selinuxfs /proc/self/mountinfo | head -1 | awk '{ print $5 }'`
	cat $SELINUXMNT/initial_contexts/file | secon -t
}

exclude_dirs_from_relabelling() {
    exclude_from_relabelling=
    if [ -e /etc/selinux/fixfiles_exclude_dirs ]
    then
	while read i
	do
	  # skip blank line and comment
	  # skip not absolute path
	  # skip not directory
	  [ -z "${i}" ] && continue
	  [[ "${i}" =~ ^[[:blank:]]*# ]] && continue
	  [[ ! "${i}" =~ ^/.* ]] && continue
	  [[ ! -d "${i}" ]] && continue
	  exclude_from_relabelling="$exclude_from_relabelling -e $i"
	done < /etc/selinux/fixfiles_exclude_dirs
    fi
    echo "$exclude_from_relabelling"
}

#
# Set global Variables
#
fullFlag=0
BOOTTIME=""
VERBOSE="-p"
[ -t 1 ] || VERBOSE=""
FORCEFLAG=""
THREADS=""
RPMFILES=""
PREFC=""
RESTORE_MODE=""
BIND_MOUNT_FILESYSTEMS=""
SETFILES=/sbin/setfiles
RESTORECON=/sbin/restorecon
FILESYSTEMSRW=`get_rw_labeled_mounts`
FILESYSTEMSRO=`get_ro_labeled_mounts`
SELINUXTYPE="targeted"
if [ -e /etc/selinux/config ]; then
    . /etc/selinux/config
    FC=/etc/selinux/${SELINUXTYPE}/contexts/files/file_contexts
else
    FC=/etc/security/selinux/file_contexts
fi

#
# Log all Read Only file systems
#
LogReadOnly() {
if [ ! -z "$FILESYSTEMSRO" ]; then
    echo "Warning: Skipping the following R/O filesystems:"
    echo "$FILESYSTEMSRO"
fi
}

#
# Log directories excluded from relabelling by configuration file
#
LogExcluded() {
for i in ${EXCLUDEDIRS//-e / }; do
    echo "skipping the directory $i"
done
}

#
# Find files newer then the passed in date and fix the label
#
newer() {
    DATE=$1
    shift
    LogReadOnly
    for m in `echo $FILESYSTEMSRW`; do
	find $m -mount -newermt $DATE -print0 2>/dev/null | ${RESTORECON} ${FORCEFLAG} ${VERBOSE} ${THREADS} $* -i -0 -f -
    done;
}

#
# Compare PREVious File Context to currently installed File Context and
# run restorecon on all files affected by the differences.
#
diff_filecontext() {
EXCLUDEDIRS="`exclude_dirs_from_relabelling`"
for i in /sys /proc /mnt /var/tmp /var/lib/BackupPC /home /root /tmp; do
    [ -e $i ]  && EXCLUDEDIRS="${EXCLUDEDIRS} -e $i";
done
LogExcluded

if [ -f ${PREFC} -a -x /usr/bin/diff ]; then
	TEMPFILE=`mktemp ${FC}.XXXXXXXXXX`
	test -z "$TEMPFILE" && exit
	PREFCTEMPFILE=`mktemp ${PREFC}.XXXXXXXXXX`
	sed -r -e 's,:s0, ,g' $PREFC | sort -u > ${PREFCTEMPFILE}
	sed -r -e 's,:s0, ,g' $FC | sort -u | \
	/usr/bin/diff -b ${PREFCTEMPFILE} - | \
	    grep '^[<>]'|cut -c3-| grep ^/ | \
	    grep -Ev '(^/home|^/root|^/tmp)' |\
	sed -r -e 's,[[:blank:]].*,,g' \
	       -e 's|\(([/[:alnum:]]+)\)\?|{\1,}|g' \
	       -e 's|([/[:alnum:]])\?|{\1,}|g' \
	       -e 's|\?.*|*|g' \
	       -e 's|\{.*|*|g' \
	       -e 's|\(.*|*|g' \
	       -e 's|\[.*|*|g' \
	       -e 's|\.\*.*|*|g' \
	       -e 's|\.\+.*|*|g' | \
	    # These two sorts need to be separate commands \
	sort -u | \
	sort -d | \
	while read pattern ; \
	    do if ! echo "$pattern" | grep -q -f ${TEMPFILE} 2>/dev/null; then \
		  echo "$pattern"; \
		  case "$pattern" in *"*") \
		       echo "$pattern" | sed -e 's,^,^,' -e 's,\*$,,g' >> ${TEMPFILE};;
		  esac; \
	       fi; \
	    done | \
	${RESTORECON} ${VERBOSE} ${EXCLUDEDIRS} ${FORCEFLAG} ${THREADS} $* -i -R -f -; \
	rm -f ${TEMPFILE} ${PREFCTEMPFILE}
fi
}

rpmlist() {
rpm -q --qf '[%{FILESTATES} %{FILENAMES}\n]' "$1" | grep '^0 ' | cut -f2- -d ' '
[ ${PIPESTATUS[0]} != 0 ] && echo "$1 not found" >/dev/stderr
}

# unmount tmp bind mount before exit
umount_TMP_MOUNT() {
	if [ -n "$TMP_MOUNT" ]; then
	     umount "${TMP_MOUNT}${m}" || exit 130
	     rm -rf "${TMP_MOUNT}" || echo "Error cleaning up."
	fi
	exit 130
}

fix_labels_on_mountpoint() {
	test -z ${TMP_MOUNT+x} && echo "Unable to find temporary directory!" && exit 1
	mkdir -p "${TMP_MOUNT}${m}" || exit 1
	mount --bind "${m}" "${TMP_MOUNT}${m}" || exit 1
	${SETFILES} ${VERBOSE} ${EXCLUDEDIRS} ${FORCEFLAG} ${THREADS} $* -q ${FC} -r "${TMP_MOUNT}" "${TMP_MOUNT}${m}"
	umount "${TMP_MOUNT}${m}" || exit 1
	rm -rf "${TMP_MOUNT}" || echo "Error cleaning up."
}
export -f fix_labels_on_mountpoint

#
# restore
# if called with -n will only check file context
#
restore () {
OPTION=$1
shift

# [-B | -N time ]
if [ -n "$BOOTTIME" ]; then
	newer $BOOTTIME $*
	return
fi

# -C PREVIOUS_FILECONTEXT
if [ "$RESTORE_MODE" == PREFC ]; then
	diff_filecontext $*
	return
fi

[ -x /usr/sbin/genhomedircon ] && /usr/sbin/genhomedircon

EXCLUDEDIRS="`exclude_dirs_from_relabelling`"
LogExcluded

case "$RESTORE_MODE" in
    RPMFILES)
	for i in `echo "$RPMFILES" | sed 's/,/ /g'`; do
	    rpmlist $i | ${RESTORECON} ${VERBOSE} ${EXCLUDEDIRS} ${FORCEFLAG} ${THREADS} $* -i -R -f -
	done
    ;;
    FILEPATH)
	${RESTORECON} ${VERBOSE} ${EXCLUDEDIRS} ${FORCEFLAG} ${THREADS} $* -R -- "$FILEPATH"
    ;;
    *)
	if [ -n "${FILESYSTEMSRW}" ]; then
	    LogReadOnly
	    echo "${OPTION}ing `echo ${FILESYSTEMSRW}`"

	    if [ -z "$BIND_MOUNT_FILESYSTEMS" ]; then
	        ${SETFILES} ${VERBOSE} ${EXCLUDEDIRS} ${FORCEFLAG} $* -q ${THREADS} ${FC} ${FILESYSTEMSRW}
	    else
	        # we bind mount so we can fix the labels of files that have already been
	        # mounted over
	        for m in `echo $FILESYSTEMSRW`; do
	            TMP_MOUNT="$(mktemp -p /run -d fixfiles.XXXXXXXXXX)"
	            export SETFILES VERBOSE EXCLUDEDIRS FORCEFLAG THREADS FC TMP_MOUNT m
	            if type unshare &> /dev/null; then
	                unshare -m bash -c "fix_labels_on_mountpoint $*" || exit $?
	            else
	                trap umount_TMP_MOUNT EXIT
	                fix_labels_on_mountpoint $*
	                trap EXIT
	            fi
	        done;
	    fi
	else
	    echo >&2 "fixfiles: No suitable file systems found"
	fi
	if [ ${OPTION} != "Relabel" ]; then
	    return
	fi
	echo "Cleaning up labels on /tmp"
	rm -rf /tmp/gconfd-* /tmp/pulse-* /tmp/orbit-*

	UNDEFINED=`get_undefined_type` || exit $?
	UNLABELED=`get_unlabeled_type` || exit $?
	find /tmp \( -context "*:${UNLABELED}*" -o -context "*:${UNDEFINED}*" \) \( -type s -o -type p \) -delete
	find /tmp \( -context "*:${UNLABELED}*" -o -context "*:${UNDEFINED}*" \) -exec chcon --no-dereference --reference /tmp {} \;
	find /var/tmp \( -context "*:${UNLABELED}*" -o -context "*:${UNDEFINED}*" \) -exec chcon --no-dereference --reference /var/tmp {} \;
	find /var/run \( -context "*:${UNLABELED}*" -o -context "*:${UNDEFINED}*" \) -exec chcon --no-dereference --reference /var/run {} \;
	[ ! -e /var/lib/debug ] || find /var/lib/debug \( -context "*:${UNLABELED}*" -o -context "*:${UNDEFINED}*" \) -exec chcon --no-dereference --reference /lib {} \;
    ;;
esac
}

fullrelabel() {
    echo "Cleaning out /tmp"
    find /tmp/ -mindepth 1 -delete
    restore Relabel
}


relabel() {
    if [ -n "$RESTORE_MODE" -a "$RESTORE_MODE" != DEFAULT ]; then
	usage
	exit 1
    fi

    if [ $fullFlag == 1  ]; then
	fullrelabel
	return
    fi

    echo -n "
    Files in the /tmp directory may be labeled incorrectly, this command
    can remove all files in /tmp.  If you choose to remove files from /tmp,
    a reboot will be required after completion.

    Do you wish to clean out the /tmp directory [N]? "
    read answer
    if [ "$answer" = y -o  "$answer" = Y ]; then
	fullrelabel
    else
	restore Relabel
    fi
}

process() {
#
# Make sure they specified one of the three valid commands
#
case "$1" in
    restore) restore Relabel;;
    check) VERBOSE="-v"; restore Check -n;;
    verify) VERBOSE="-v"; restore Verify -n;;
    relabel) relabel;;
    onboot)
	if [ -n "$RESTORE_MODE" -a "$RESTORE_MODE" != DEFAULT ]; then
	    usage
	    exit 1
	fi
	> /.autorelabel || exit $?
	[ -z "$FORCEFLAG" ] || echo -n "$FORCEFLAG " >> /.autorelabel
	[ -z "$BOOTTIME" ] || echo -n "-N $BOOTTIME " >> /.autorelabel
	[ -z "$BIND_MOUNT_FILESYSTEMS" ] || echo -n "-M " >> /.autorelabel
	[ -z "$THREADS" ] || echo -n "$THREADS " >> /.autorelabel
	# Force full relabel if SELinux is not enabled
	selinuxenabled || echo -F > /.autorelabel
	echo "System will relabel on next boot"
	;;
    *)
    usage
    exit 1
esac
}
usage() {
	echo $"""
Usage: $0 [-v] [-F] [-M] [-f] [-T nthreads] relabel
or
Usage: $0 [-v] [-F] [-B | -N time ] [-T nthreads] { check | restore | verify }
or
Usage: $0 [-v] [-F] [-T nthreads] { check | restore | verify } dir/file ...
or
Usage: $0 [-v] [-F] [-T nthreads] -R rpmpackage[,rpmpackage...] { check | restore | verify }
or
Usage: $0 [-v] [-F] [-T nthreads] -C PREVIOUS_FILECONTEXT { check | restore | verify }
or
Usage: $0 [-F] [-M] [-B] [-T nthreads] onboot
"""
}

if [ $# -eq 0 ]; then
	usage
	exit 1
fi

set_restore_mode() {
	if [ -n "$RESTORE_MODE" ]; then
		# can't specify two different modes
		usage
		exit 1
	fi
	RESTORE_MODE="$1"
}

# See how we were called.
while getopts "N:BC:FfR:l:vMT:" i; do
    case "$i" in
	B)
		BOOTTIME=`/bin/who -b | awk '{print $3}'`
		set_restore_mode DEFAULT
		;;
	N)
		BOOTTIME=$OPTARG
		set_restore_mode BOOTTIME
		;;
	R)
		RPMFILES=$OPTARG
		set_restore_mode RPMFILES
		;;
	C)
		PREFC=$OPTARG
		set_restore_mode PREFC
		;;
	v)
		VERBOSE="-v"
		;;
	l)
		# Old scripts use obsolete option `-l logfile`
		echo "Redirecting output to $OPTARG"
		exec >>"$OPTARG" 2>&1
		;;
	M)
		BIND_MOUNT_FILESYSTEMS="-M"
		;;
	F)
		FORCEFLAG="-F"
		;;
	f)
		fullFlag=1
		;;
	T)
		THREADS="-T $OPTARG"
		;;
	*)
	    usage
	    exit 1
esac
done
# Move out processed options from arguments
shift $(( OPTIND - 1 ))

# Check for the command
if [ $# -eq 0 ]; then
    usage
    exit 1
fi
command="$1"

# Move out command from arguments
shift

if [ $# -gt 0 ]; then
    set_restore_mode FILEPATH
    while [ $# -gt 0 ]; do
	FILEPATH="$1"
	process "$command" || exit $?
	shift
    done
else
    process "$command"
fi

