#!/usr/bin/ksh
#
# Script to find unused or "dormant" user accounts
#
# Written by Thomas Sluyter (nagiosATkilalaDOTnl)
# By request of Unixerius, the Netherlands
# Last Modified: 16-03-2010
#
# Description:
#   Users come and users go and likewise user accounts get created and destroyed. However,
# sometimes your HR-processes fail and accounts get forgotten and left behind. It may not
# be obvious, but these forgotten accounts can actually form a threat to your security and
# should be cleaned up. Many companies even go out and lock or remove accounts of people
# who actively employed if they go unused for an extended period of time. 
#   This script will help you find these forgotten user accounts, so you can then decide
# what to do with them.
#
# Usage: 
# ./check_boks_dormant [[-u UC] [-H HG] [-h HOST] | -A] [-M MON] [-x UC] [-X HG]  [-d -o FILE] [-f FILE]
#
# -u UCLASS		Check only accounts with profile UCLASS. Multiple -u entries allowed.
# -H HGROUP		Check only accounts from HOSTGROUP. Multiple -H entries allowed.
# -h HOST		Check ALL accounts involved with HOST. Multiple -h entries allowed.
# -A 			Check ALL user accounts.
# -M MON		Minimum amount of months that accounts must be dormant. Default is 6.
# -x EXCLUDEUC	Exclude all accounts with profile UCLASS. Multiple -x entries allowed.
# -X EXCLUDEHG	Exclude all accounts from HOSTGROUP. Multiple -X entries allowed.
# -S 			Exclude all accounts who can authenticate with SSH_PK. See "other notes" below.
# -f FILE		Log file that contains all dormant accounts. Default logs into $BOKS_var.
# -d 			Debug mode. Provides error logging. 
# -o FILE		Output file for debugging logs. Required when -d is passed.
#
# When using the -h option, a list will be made of all user accounts involved with this server
# regardless of user class or host group. One can exclude certain classes or groups by using
# the -x and -X parameters.
#
# Example: 
# ./check_boks_dormant.ksh -h solaris1 -x RootUsers -x DataTransfer
# ./check_boks_dormant.ksh -u OracleDBA
# ./check_boks_dormant.ksh -A -d -o /tmp/foobar
#
# Limitations:
# * This script requires Perl for some minor date magic.
# * This script must be run on a BoKS master or replica server.
# * This script must be run as root from the BoKS shell.
# * This script was tested with BoKS 6.0.x and 6.5.x.
# * If the script malfunctions, you may want to verify the platform specific
#   variables under "SETTING THINGS UP". 
#
# Output:
#   The script does not output to stdout. Instead, all dormant accounts are logged in
# $BOKS_var/check_boks_dormant.ksh.DATE or another file specified with -f. 
#
# Other notes:
# * Use the debugging option to find out the cause of your problems if this
#   script ever misbehaves. Use it with the -n dry run option if you like want
#   to test the script from within Tivoli, as opposed to on the command line.
# * When using the -h option, a list will be made of all user accounts involved with this 
#   server regardless of user class or host group. One can exclude certain classes or groups 
# 	by using the -x and -X parameters.
# * When using the -S option the script will exclude users who are allowed to authenticate 
#   using SSH_PK. It has been found that using SSH public key authentication does NOT update
#   the LASTLOGIN field when logging in. A bug report has been filed with FoxT for this matter
#   and a hotfix was created for 6.5.x: 081208-213828. Unfortunately 6.0 will NOT get the hotfix.
#   Similarly the LASTPSWCHG field can often be ignored for these users as they do not update
#   their BoKS passwords, favoring their SSH key passwords instead.
# * Similar to the point above, we have also found that logging in using SCP or SFTP does NOT
#   update the LASTLOGIN field for a user either. Hence this script may not give 100% accurate
#   results for those accounts used primarily for data transfer.
#
#


### SETTING THINGS UP ###

PROGNAME=$(basename $0)
PATH="/usr/bin:/usr/sbin:/bin:/sbin:$PATH"

TEMPDIR="/tmp/$PROGNAME.$(date +%Y%m%d)"; mkdir $TEMPDIR >/dev/null 2>&1; rm $TEMPDIR/* >/dev/null 2>&1; chmod 600 $TEMPDIR
ACCTLIST="$TEMPDIR/ACCTLIST"
ACCTEXCLUDELIST="$TEMPDIR/ACCTEXCLUDELIST"
UCEXCLUDELIST="$TEMPDIR/UCEXCLUDELIST"
HGEXCLUDELIST="$TEMPDIR/HGEXCLUDELIST"

HOSTNAMES="" 			# Default if -h parameter is not passed.
HOSTGROUPS=""			# Default if -H parameter is not passed.
USERCLASSES=""			# Default if -u parameter is not passed.
ALL="0"					# Default if -A parameter is not passed.
MONTHS="6"				# Default if -M parameter is not passed.
EXCLUDEUCS=""           # Default if -x parameter is not passed.
EXCLUDEHGS=""           # Default if -X parameter is not passed.
NOSSHPK="0"				# Default if -S parameter is not passed.
DEBUG="0"				# Default if -d parameter is not passed.
DEBUGFILE=""			# Default if -o parameter is not passed.
LOGFILE="$BOKS_var/$PROGNAME.$(date +%Y%m%d)"	# Default if -f is not passed.


# Setting up platform specific options

case $(uname) in
	AIX) BOKSSBIN="/opt/boksm/sbin";
		BOKSBIN="/opt/boksm/bin";
		BOKSLIB="/opt/boksm/lib";
		BOKSTMP="/var/opt/boksm/tmp";
		BOKSETC="/etc/opt/boksm";
		AWK="/usr/bin/awk";
		GREP="/usr/bin/grep";
		PING="/bin/ping -c 1";
		PINGPARAM="";;
	Linux) BOKSSBIN="/opt/boksm/sbin";
		BOKSBIN="/opt/boksm/bin";
		BOKSLIB="/opt/boksm/lib";
		BOKSTMP="/var/opt/boksm/tmp";
		BOKSETC="/etc/opt/boksm";
		AWK="/bin/awk";
		GREP="/bin/grep";
		PING="/bin/ping -c 1";
		PINGPARAM="";;
	SunOS) BOKSSBIN="/opt/boksm/sbin";
		BOKSBIN="/opt/boksm/bin";
		BOKSLIB="/opt/boksm/lib";
		BOKSETC="/etc/opt/boksm";
		BOKSTMP="/var/opt/boksm/tmp";
		AWK="/usr/xpg4/bin/awk";
		GREP="/usr/xpg4/bin/grep";
		PING="/usr/sbin/ping";
		PINGPARAM="1";;
	*)  echo "\nERROR: OS is not supported."; exit 1 ;;
esac


# Quick check to see if you're running as root with the BoKS shell

if [[ ( -z $BOKS_etc ) || ( -z $(id | $GREP "uid=0") ) ]]
then
        echo "ERROR: Script must be run as root, from the BoKS shell.\n"
        exit 1
fi


### REQUISITE COMMAND LINE STUFF ###

print_usage() {
	echo "\nUsage:"
	echo "./check_boks_dormant [[-u UC] [-H HG] [-h HOST] | -A] [-M MON] [-x UC] [-X HG] [-S] [-d -o FILE] [-f FILE]"
	echo "\n-u UCLASS\tCheck only accounts with profile UCLASS. Multiple -u entries allowed."
	echo "-H HGROUP\tCheck only accounts from HOSTGROUP. Multiple -H entries allowed."
	echo "-h HOST\t\tCheck ALL accounts involved with HOST. Multiple -h entries allowed."
	echo "-A\t\tCheck ALL user accounts."
	echo "-M MON\t\tMinimum amount of months that accounts must be dormant. Default is 6."
	echo "-x EXCLUDEUC\tExclude all accounts with profile UCLASS. Multiple -x entries allowed."
	echo "-X EXCLUDEHG\tExclude all accounts from HOSTGROUP. Multiple -X entries allowed."
	echo "-S\t\tExclude all accounts who can authenticate with SSH_PK. See README."
	echo "-f FILE\t\tLog file that contains all dormant accounts. Default logs into \$BOKS_var."
	echo "-d\t\tDebug mode. Provides error logging."
	echo "-o FILE\t\tOutput file for debugging logs. Required when -d is passed."
	echo "\nExamples:"
	echo "./check_boks_dormant.ksh -h solaris1 -x RootUsers -x DataTransfer"
	echo "./check_boks_dormant.ksh -u OracleDBA"
	echo "./check_boks_dormant.ksh -A -d -o /tmp/foobar\n"
	exit 1
}


[[ $# -eq 0 ]] && print_usage

while [[ $# -gt 0 ]]
do
    case "$1" in
	-h) if [[ (-z $(echo $2 | $GREP ^-)) && (! -z $2) ]]
	    then
			if [[ $($BOKSSBIN/hostadm -l -h $2 | wc -l) -gt 0 ]]
			then
				HOSTNAMES="$HOSTNAMES $2"; shift
			else
				echo "\nERROR: Specified host $2 does NOT exist.\n\n"; exit 1
			fi
	    else
		echo "\nERROR: Hostname incorrectly specified. Use -h.\n\n"
		print_usage
	    fi;;
	-H) if [[ (-z $(echo $2 | $GREP ^-)) && (! -z $2) ]]
	    then
			if [[ $($BOKSSBIN/hgrpadm -l -g $2 | wc -l) -gt 0 ]]
			then
				HOSTGROUPS="$HOSTGROUPS $2"; shift
			else
				echo "\nERROR: Specified host group $2 does NOT exist.\n\n"; exit 1
			fi
	    else
		echo "\nERROR: Hostgroup incorrectly specified. Use -H.\n\n"
		print_usage
	    fi;;
	-u) if [[ (-z $(echo $2 | $GREP ^-)) && (! -z $2) ]]
	    then
			if [[ $($BOKSSBIN/classadm -l -u $2 | wc -l) -gt 0 ]]
			then
				USERCLASSES="$USERCLASSES $2"; shift
			else
				echo "\nERROR: Specified user class $2 does NOT exist.\n\n"; exit 1
			fi
	    else
		echo "\nERROR: Userclass incorrectly specified. Use -u.\n\n"
		print_usage
	    fi;;
	-A) ALL="1";;
	-M) if [[ (-z $(echo $2 | $GREP ^-)) && (! -z $2) && (-z $(echo $2 | $GREP [A-z])) && (! -z $(echo $2 | $GREP [0-9])) ]]
	    then
		MONTHS="$2"; shift
	    else
		echo "\nERROR: Amount of months incorrectly specified. Use -M.\n\n"; exit 1
	    fi;;
	-x) if [[ (-z $(echo $2 | $GREP ^-)) && (! -z $2) ]]
	    then
			if [[ $($BOKSSBIN/classadm -l -u $2 | wc -l) -gt 0 ]]
			then
				EXCLUDEUCS="$EXCLUDEUCS $2"; shift
			else
				echo "\nERROR: Specified user class $2 does NOT exist.\n\n"; exit 1
			fi
	    else
		echo "\nERROR: Userclass to exclude incorrectly specified. Use -x.\n\n"
		print_usage
	    fi;;
	-X) if [[ (-z $(echo $2 | $GREP ^-)) && (! -z $2) ]]
	    then
			if [[ $($BOKSSBIN/hgrpadm -l -g $2 | wc -l) -gt 0 ]]
			then
				EXCLUDEHGS="$EXCLUDEHGS $2"; shift
			else
				echo "\nERROR: Specified host group $2 does NOT exist.\n\n"; exit 1
			fi
	    else
		echo "\nERROR: Hostgroup to exclude incorrectly specified. Use -X.\n\n"
		print_usage
	    fi;;
	-S) NOSSHPK="1";;
	-f) if [[ (-z $(echo $2 | $GREP ^-)) && (! -z $2) ]]
	    then
		if [[ -d $(dirname $2) ]]
		then
		    LOGFILE="$2"
		    shift
		else
		    echo "\nERROR: Output directory for log file does not exist. Use -f.\n\n"; exit 1
		fi
	    else
		echo "\nERROR: Log file not specified. Use -f.\n\n"
		print_usage
	    fi;;
	-d) DEBUG="1";;
	-o) if [[ (-z $(echo $2 | $GREP ^-)) && (! -z $2) ]]
	    then
		if [[ -d $(dirname $2) ]]
		then
		    DEBUGFILE="$2"
		    echo > $DEBUGFILE; chown root:root $DEBUGFILE; chmod 0600 $DEBUGFILE
		    shift
		else
		    echo "\nERROR: Output directory for debug log does not exist. Use -o.\n\n"; exit 1
		fi
	    else
		echo "\nERROR: Debug log output file not specified. Use -o.\n\n"
		print_usage
	    fi;;
	-*) print_usage;;
	*)  print_usage;;	
    esac
    shift
done


# Some final checks and initialisation

if [[ $DEBUG -gt 0 ]]
then
	echo "=== SETUP ===" >> $DEBUGFILE
	echo "OS name is $(uname)" >> $DEBUGFILE
	echo "BOKSSBIN is $BOKSSBIN" >> $DEBUGFILE
	echo "BOKSBIN is $BOKSBIN" >> $DEBUGFILE
	echo "BOKSLIB is $BOKSLIB" >> $DEBUGFILE
	echo "BOKSETC is $BOKSETC \n" >> $DEBUGFILE
	echo "Parameters passed to script are:" >> $DEBUGFILE
	echo "HOSTNAMES = $HOSTNAMES" >> $DEBUGFILE
	echo "HOSTGROUPS = $HOSTGROUPS" >> $DEBUGFILE
	echo "USERCLASSES = $USERCLASSES" >> $DEBUGFILE
	echo "ALL = $ALL" >> $DEBUGFILE
	echo "EXCLUDEUCS = $EXCLUDEUCS" >> $DEBUGFILE
	echo "EXCLUDEHGS = $EXCLUDEHGS" >> $DEBUGFILE
	echo "NOSSHPK = $NOSSHPK" >> $DEBUGFILE
	echo "DEBUG = $DEBUG" >> $DEBUGFILE
	echo "DEBUGFILE = $DEBUGFILE \n" >> $DEBUGFILE
	echo "LOGFILE = $LOGFILE \n" >> $DEBUGFILE
fi



### DEFINING SUBROUTINES ###


function ExitScript
{
        EXIT="$1"

        rm -rf $TEMPDIR
        echo $EXIT
        exit $EXIT
}


EpochToDMY()
{
	echo $(echo $1 | perl -ne '
		chomp; $epochdate = $_;
		my ($sec, $min, $hour, $day,$month,$year) = (localtime($time))[0,1,2,3,4,5,6]; 
		print $day."-".($month+1)."-".($year+1900);
	')
}


GenerateAccountList()
{
	[[ $DEBUG -gt 0 ]] && echo "\n=== STARTING GenerateAccountList ===\n" >> $DEBUGFILE

	$BOKSLIB/dumpbase -t1 > $TEMPDIR/table1
	[[ $? -gt 0 ]] && ExitScript Failed

	rm $ACCTLIST 2>/dev/null; touch $ACCTLIST
	if [[ ! -f $ACCTLIST ]] 
	then
	    echo "ERROR: Failed to create list of accounts in $TEMPDIR."
	    ExitScript Failed
	fi
	
	if [[ $ALL -gt 0 ]] 
	then
        [[ $DEBUG -gt 0 ]] && echo "Parameter -A passed. Adding all users to ACCTLIST." >> $DEBUGFILE
		grep ^RLOGNAME $TEMPDIR/table1 >> $ACCTLIST
	else
	    if [[ $HOSTNAMES != "" ]]
	    then
      	 	[[ $DEBUG -gt 0 ]] && echo "Hostnames passed with -h parameter. Resolving to host groups for -H." >> $DEBUGFILE
			for HOST in $(echo $HOSTNAMES)
			do 
				grep ^"RLOGNAME=\"$HOST:" $TEMPDIR/table1 >> $ACCTLIST
				for HG in $(hgrpadm -l | grep $HOST | $AWK '{print $1}'); do HOSTGROUPS="$HOSTGROUPS $HG"; done
			done
	    fi
	
	    if [[ $HOSTGROUPS != "" ]]
	    then
        	[[ $DEBUG -gt 0 ]] && echo "Hostgroups passed with -H parameter. Resolving and adding to account list." >> $DEBUGFILE
			for HG in $(echo $HOSTGROUPS); do grep ^"RLOGNAME=\"$HG:" $TEMPDIR/table1  >> $ACCTLIST; done
	    fi
	
	    if [[ $USERCLASSES != "" ]]
	    then
        	[[ $DEBUG -gt 0 ]] && echo "List of userclasses passed with -u parameter. Adding to account list." >> $DEBUGFILE
			for UC in $(echo $USERCLASSES)
			do
				for USER in $(classadm -L -u $UC); do grep ^"RLOGNAME=\"$USER:" $TEMPDIR/table1  >> $ACCTLIST; done
			done
	    fi
	fi

	cat $ACCTLIST | sort -u > $ACCTLIST.temp; mv $ACCTLIST.temp $ACCTLIST
	NUMUSERS=$(wc -l $ACCTLIST | $AWK '{print $1}')

	[[ $DEBUG -gt 0 ]] && echo "Account list is $NUMUSERS lines." >> $DEBUGFILE
}


GenerateAccountExcludeList()
{
	[[ $DEBUG -gt 0 ]] && echo "\n=== STARTING GenerateAccountExcludeList ===\n" >> $DEBUGFILE

	$BOKSLIB/dumpbase -t31 > $TEMPDIR/table31
	[[ $? -gt 0 ]] && ExitScript Failed

	rm $ACCTEXCLUDELIST 2>/dev/null; touch $ACCTEXCLUDELIST
	if [[ ! -f $ACCTEXCLUDELIST ]] 
	then
	    echo "ERROR: Failed to create exclude list in $TEMPDIR."
	    ExitScript Failed
	fi
	
	if [[ $EXCLUDEUCS != "" ]]
	then
       	[[ $DEBUG -gt 0 ]] && echo "Userclasses passed with -x parameter. Resolving and adding to exclude list." >> $DEBUGFILE
		for UC in $(echo $EXCLUDEUCS); do classadm -L -u $UC >> $ACCTEXCLUDELIST; done
	fi
	
	if [[ $EXCLUDEHGS != "" ]]
	then
        [[ $DEBUG -gt 0 ]] && echo "Hostgroups passed with -X parameter. Resolving and adding to exclude list." >> $DEBUGFILE
	    for HG in $(echo $EXCLUDEHGS); do lsbks -l $HG:* >> $ACCTEXCLUDELIST; done
	fi

	if [[ $NOSSHPK -gt 0 ]]
	then
        [[ $DEBUG -gt 0 ]] && echo "Excluding SSH_PK users due to -S parameter. Resolving and adding to exclude list." >> $DEBUGFILE
		grep "ssh_pk" $TEMPDIR/table31 | $AWK -F\" '{print $2}' >> $ACCTEXCLUDELIST
	fi

	cat $ACCTEXCLUDELIST | sort -u > $ACCTEXCLUDELIST.temp; mv $ACCTEXCLUDELIST.temp $ACCTEXCLUDELIST
	NUMEXCLUDE=$(wc -l $ACCTEXCLUDELIST | $AWK '{print $1}')

	[[ $DEBUG -gt 0 ]] && echo "Exclude list is $NUMEXCLUDE lines." >> $DEBUGFILE
}


SearchDormantAccounts()
{
	[[ $DEBUG -gt 0 ]] && echo "\n=== STARTING SearchDormantAccounts ===\n" >> $DEBUGFILE
	
	rm $LOGFILE 2>/dev/null; touch $LOGFILE; chown root:root $LOGFILE; chmod 0644 $LOGFILE
	
	MONTH=2592000
	let TIME=$MONTHS*$MONTH
	let NOW=$(perl -e "print time")
	let CUTOFF=$NOW-$TIME
	
	[[ $DEBUG -gt 0 ]] && echo "CUTOFF = $CUTOFF = $NOW - ( $TIME = $MONTHS * $MONTH ).\n" >> $DEBUGFILE
	
	cat $ACCTLIST | while read LINE
	do
		eval $LINE
		if [[ $DEBUG -gt 0 ]] 
		then
			echo "\nRead new line: $RLOGNAME:$UID:$PROFILE:$PSWLASTCHANGE:$LASTLOGIN:$REALNAME" >> $DEBUGFILE
			[[ $(grep $RLOGNAME $ACCTEXCLUDELIST | wc -l) -gt 0 ]] && echo "USER IS ON EXCLUDE LIST!" >> $DEBUGFILE
			echo "If $PSWLASTCHANGE -gt $CUTOFF then skip." >> $DEBUGFILE
			echo "If $LASTLOGIN -gt $CUTOFF then skip." >> $DEBUGFILE
		fi
		
		[[ $(grep $RLOGNAME $ACCTEXCLUDELIST | wc -l) -gt 0 ]] && continue		
        [[(-z $RLOGNAME) || (-z $PROFILE) || (-z $LASTLOGIN) || (-z $PSWLASTCHANGE)]] && continue
        [[ ($PSWLASTCHANGE -gt $CUTOFF) || ($LASTLOGIN -gt $CUTOFF) ]] && continue

        PRINTLOG=$(EpochToDMY $LASTLOGIN)
        PRINTPSW=$(EpochToDMY $PSWLASTCHANGE)

		[[ $DEBUG -gt 0 ]] && echo "Last login and pswchg are $PRINTLOG and $PRINTPSW." >> $DEBUGFILE
				
        echo "$RLOGNAME%$UID%$REALNAME%$PROFILE%$PRINTLOG%$PRINTPSW" >> $LOGFILE
	done
	
	# Unfortunately a trim of the final list of dormant accounts is needed because sometimes
	# user accounts from the excluded user classes slip through. I guess it's a bug in classadm.
	# I also run the list of accounts through sort -u because sometimes duplicates show up.
	DORMANT=$(wc -l $LOGFILE | $AWK '{print $1}')
	[[ $DEBUG -gt 0 ]] && echo "Dormant list is $DORMANT lines before sort -u and re-trim of user classes." >> $DEBUGFILE
	cat $LOGFILE | sort -u > $LOGFILE.temp; mv $LOGFILE.temp $LOGFILE
	for UC in $(echo $EXCLUDEUCS); do grep -v $UC $LOGFILE > $LOGFILE.temp; mv $LOGFILE.temp $LOGFILE; done
	DORMANT=$(wc -l $LOGFILE | $AWK '{print $1}')
	[[ $DEBUG -gt 0 ]] && echo "Dormant list is $DORMANT lines." >> $DEBUGFILE
}



### FINALLY, THE MAIN ROUTINE ###

[[ $DEBUG -gt 0 ]] && echo "\n=== STARTING MAIN PHASE ===\n $(date) \n" >> $DEBUGFILE

GenerateAccountList
GenerateAccountExcludeList

[[ -z $(wc -l $ACCTLIST) ]] && ExitScript EmptyList

SearchDormantAccounts

if [[ $DEBUG -gt 0 ]] 
then
	echo "\n\n=== SCRIPT FINISHED ===\n\n" >> $DEBUGFILE
	echo "Ran for $COUNT user accounts ($NUMUSERS total, $NUMEXCLUDE excluded, $DORMANT users dormant.).\n" >> $DEBUGFILE
	date >> $DEBUGFILE
fi

echo "\nRan for $NUMUSERS user accounts ($NUMUSERS total, $NUMEXCLUDE excluded, $DORMANT users dormant.).\n"

ExitScript Done

