#!/bin/bash
#
# Copyright (C) 2004	  Debian Logcheck Team 
#                         <logcheck-devel@alioth.lists.debian.org>
# Copyright (C) 2002,2003 Jonathan Middleton <jjm@ixtab.org.uk>
# Copyright (C) 1999-2002 Rene Mayrhofer <rmayr@debian.org>
# Copyright (C) 1996-1997 Craig Rowland <crowland@psionic.com>

# This file is part of Logcheck

# Logcheck 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.

# Logcheck 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 Logcheck; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA

# $Id: logcheck,v 1.99 2004/11/09 08:40:29 ttroxell Exp $

if [ $UID == 0 ]; then
    echo "logcheck should not be run as root. Use su to invoke logcheck:"
    echo "su -s /bin/bash -c \"/usr/sbin/logcheck [options]\" logcheck"
    # you may want to uncomment that hack to let logcheck invoke itself.
    # su -s /bin/bash -c  "$0 $*" logcheck
    exit 1
fi

# Set the umask
umask 077

# Set the flag variables
SYSTEM=0
SECURITY=0
ATTACK=0

# Set the getopts string
GETOPTS="c:dhH:l:L:m:opr:RsS:tTuvw"

# Get the details for the email message
# Hostname either fully qualified or not.
if [ -z "$FQDN" ]; then
    HOSTNAME="$(hostname --short)"
else
    HOSTNAME="$(hostname --fqdn)"    
fi

DATE="$(date +'%Y-%m-%d %H:%M')"
VERSION="1.2.32"

# Set the default report level
REPORTLEVEL="server"

# Set the default subject lines
ATTACKSUBJECT="Security Alerts"
SECURITYSUBJECT="Security Events"
EVENTSSUBJECT="System Events"
ADDTAG="no"

# Set the default paths
RULEDIR="/etc/logcheck"
CONFFILE="/etc/logcheck/logcheck.conf"
STATEDIR="/var/lib/logcheck"
LOGFILES_LIST="/etc/logcheck/logcheck.logfiles"
LOGTAIL="/usr/sbin/logtail"
CAT="/bin/cat"
SYSLOG_SUMMARY="/usr/bin/syslog-summary"

# Set the options defaults
INTRO=1
LOGCHECKDEBUG=0
MAILOUT=0
NOCLEANUP=0
REBOOT=0
SORTUNIQ=0
SUPPORT_CRACKING_IGNORE=0
SYSLOGSUMMARY=0
LOCKFILE="/var/lock/logcheck"

# Carry out the clean up tasks
cleanup() {

    if [ -n "$LOCK" ]; then
        debug "cleanup: Killing lockfile-touch - $LOCK"
	kill $LOCK && unset LOCK
    fi

    if [ -f "$LOCKFILE.lock" ]; then
        debug "cleanup: Removing lockfile: $LOCKFILE.lock"
	lockfile-remove $LOCKFILE
    fi

    if [ -d "$TMPDIR" ]; then
        # Remove the tmp directory
        if [ $NOCLEANUP -eq 0 ];then 
    	    debug "Cleanup: Removing - $TMPDIR" 
    	    rm -r $TMPDIR
        else
    	    debug "cleanup: Not removing - $TMPDIR"
        fi
    fi
}

# Log debug output to standard error
debug() {
    if [ $LOGCHECKDEBUG -eq 1 ]; then
	echo "D: [$(date +%s)] $1" >&2 
    fi
}

# Mail error message to sysadmin
error() {
    message=$1

    if [ "$2" = "noclean" ]; then
	debug "error: Not removing lockfile"
    else
        if [ -n "$LOCK" ]; then
	    debug "error: Killing lockfile-touch - $LOCK"
	    kill $LOCK && unset LOCK
	fi

       if [ -f "$LOCKFILE.lock" ]; then
           debug "error: Removing lockfile: $LOCKFILE.lock"
           lockfile-remove $LOCKFILE
       fi
	
    fi

    debug "Error: $message"

    if [ $MAILOUT -eq 0 ]; then
	{
	    cat<<EOF
Warning: If you are seeing this message, your log files may not have been
checked!

Details:
$message

Check temporary directory: $TMPDIR

$(export)
EOF
	} | mail -s "Logcheck: $HOSTNAME $DATE exiting due to errors" \
	    $SENDMAILTO
    fi
    exit 1
}

# Add an identification line at the beginning of the sent mail
setintro() {
    if [ -f /etc/logcheck/header.txt -a -r /etc/logcheck/header.txt ] ; then
       $CAT /etc/logcheck/header.txt >> $TMPDIR/report \
           || error "Could not append header to $TMPDIR/report  Disk full?"
    fi
}


# Add a footer to the report.
setfooter() {
    if [ -f /etc/logcheck/footer.txt -a -r /etc/logcheck/footer.txt ] ; then
       $CAT /etc/logcheck/footer.txt >> $TMPDIR/report \
           || error "Could not append footer to $TMPDIR/report Disk full?"
    fi
}


# Clean a directory (or single file) to a cleaned tmp version
# takes two args: directory and cleaned file
cleanrules() {
    dir=$1
    cleaned=$2

    if [ -d $dir ]; then 
        if [ ! -d $cleaned ]; then
	    mkdir $cleaned \
	        || error "Could not make dir $cleaned for cleaned rulefiles."
	fi
	for rulefile in $(run-parts --list $dir); do
	    rulefile=$(basename $rulefile)
	    if [ -f ${dir}/${rulefile} ]; then
		debug "cleanrules: ${dir}/${rulefile}"
		# pipe to cat on greps to get usable exit status
		egrep --text -v '^[[:space:]]*$|^#' $dir/$rulefile | cat \
		    >> $cleaned/$rulefile \
		    || error "Couldn't append to $cleaned/$rulefile. Disk Full?"
	    fi
	done
    elif [ -f $dir ]; then
	error "cleanrules: '$dir' is a file, not a directory"
    elif [ -z $dir ]; then
	error "cleanrules: called without argument"
    fi
}

# Add any events to the report
report() {
    if [ -s $TMPDIR/checked ]; then
	printheader "$*" >> $TMPDIR/report \
	    || error "Could not append to report.  Disk Full?"
	if [ $SYSLOGSUMMARY -eq 1 ] && [ -x $SYSLOG_SUMMARY ]; then
	    debug "report: running syslog-summary - $*"
	    $SYSLOG_SUMMARY $TMPDIR/checked | \
		egrep -v "^Summarizing "| cat >> $TMPDIR/report \
		    || error "Could not append to report.  Disk Full?"
	else
	    if [ $SYSLOGSUMMARY -eq 1 ] && [ ! -x $SYSLOG_SUMMARY ]; then
	    	debug "report : WARNING : can't exec $SYSLOG_SUMMARY. Running without summary"
	    fi
	    debug "report: cat'ing - $*"
	    cat $TMPDIR/checked >> $TMPDIR/report \
	        || error "Could not append to report.  Disk full?"
	fi
	echo >> $TMPDIR/report \
	    || error "Could not append to report.  Disk full?"
	return 0
    else
	return 1
    fi
}

# Add eventual section titles to the report
printheader() {
    char="="
    header="$1"
    number="$(echo $header | wc -c)"
    num=1
    line=""

    while [ "$num" -lt "$number" ]; do
        line="${line}${char}"
        if [ "$char" = "=" ]; then
            char="-"
        else
            char="="
        fi
        num=$(($num + 1))
    done
    echo "$header"
    echo "$line"
}

# Mail the report
sendreport() {
    if [ $REBOOT -eq 1 ]; then
	subject="Reboot: $HOSTNAME $DATE $*"
    else
	subject="$HOSTNAME $DATE $*"
    fi
    if [ $ADDTAG = "yes" ]; then
        subject="[logcheck] $subject"
    fi

    if [ $MAILOUT -eq 1 ]; then
	debug "Sending report to STDOUT"
	cat $TMPDIR/report
	debug "Sent report to STDOUT"
    else
	debug "Sending report: '$subject' to $SENDMAILTO"
	cat $TMPDIR/report | mail -s "$subject" $SENDMAILTO
    fi
}

# Clean the report to level for type
greplogoutput() {
    raise="$1"
    sectionstring="$2"
    ignore="$3"
    ignorehigher="$4"

    RETURN=1

    for grepfile in $(ls -1 $raise); do
	debug "greplogoutput: $grepfile"

	# Raise entries that match
	egrep --text -f $raise/$grepfile $TMPDIR/logoutput-sorted | cat \
	    > $TMPDIR/checked \
	    || error "Could not output to $TMPDIR/checked.  Disk Full?"

	# apply different ignore rules
	if [ -s $TMPDIR/checked ]; then
	    debug "greplogoutput: Entries in checked"
	    
	    if [ -n "$ignore" -a -f "$ignore/$(basename $grepfile)" ]; then
		cleanchecked "$ignore/$(basename $grepfile)"
	    fi

	    # quick and dirty fix for ignoring logcheck-foo files 
	    # in the case logcheck itself has no raised entry
	    if [ -n "$ignore" -a -f "$ignore/logcheck-$(basename $grepfile)" ]; then
		cleanchecked "$ignore/logcheck-$(basename $grepfile)"
	    fi
	    
	    # If it's the logcheck file, we do something special
	    if [ "$(basename $grepfile)" = "logcheck" ]; then 

		# Now ignore all entries from the ignore dir
		# old logcheck versions only ignored logcheck-<package> files
		if [ -n "$ignore" ]; then
		    debug "Applying Logcheck override files"
		    for file in $(ls -1 $ignore/) ; do
		        debug "clean logcheck-<package>: $file"
			cleanchecked "$ignore/$file"
		    done
		else
		    debug "No Logcheck override files" 
		fi
 
		debug "Cleaning logcheck"
		# Remove any entries already reported
		for file in $(ls $raise/ | grep -v '^logcheck') ; do
		    debug "Cleaning logcheck: $file"
		    cleanchecked "$raise/$file"
		done
	    fi

	    if [ -n "$ignorehigher" ]; then 
		if [ -d $ignorehigher -a -s $TMPDIR/checked ]; then
		    cleanchecked "$ignorehigher"
		fi
	    fi

	    # Apply local rules before we report
	    if [ -n "$ignore" ]; then
	        if [ -f $ignore/local -a -s $TMPDIR/checked ]; then
		    cleanchecked "$ignore/local"
	        fi

	        # Now apply any local-* files
	        for file in $(ls -1 $ignore/ | grep '^local-') ; do
	    	    cleanchecked "$ignore/$file"
	        done
	    fi

	    if [ "$(basename $grepfile)" = "logcheck" ]; then
		report "${sectionstring}" && RETURN=0
	    else
		report "${sectionstring} for $(basename $grepfile)" \
		    && RETURN=0
	    fi
	fi
    done
    debug "greplogoutput: returning $RETURN"
    return $RETURN
}

# Process a logfile snippet with egrep.
cleanchecked() {
    clean=$1

    if [ -f $clean ]; then 
	debug "cleanchecked - file: $clean"
        egrep --text -v -f $clean $TMPDIR/checked | cat >> $TMPDIR/checked.1  \
	    || error "Could not output to $TMPDIR/checked.1 Disk full?"
	mv $TMPDIR/checked.1 $TMPDIR/checked \
	    || error "Could not move $TMPDIR/checked.1 to $TMPDIR/checked"
    elif [ -d $clean ]; then
	debug "cleanchecked - dir - $clean"
	for file in $(ls -1 $clean/); do
	debug "cleanchecked - dir - $clean/$file"
	    egrep --text -v -f $clean/$file $TMPDIR/checked | cat \
		>> $TMPDIR/checked.1 \
		    || error "Could not output to TMPDIR/checked.1 Disk Full?"
	    mv $TMPDIR/checked.1 $TMPDIR/checked \
	        || error "Could not move $TMPDIR/checked.1 to $TMPDIR/checked"
	done
    else
	error "cleanchecked: Not a file or a directory $clean"
    fi
}

# Get the yet unseen part of one logfile.
logoutput() {
    file=$1

    # There are some problems with this section.
    debug "logoutput called with file: $file"
    if [ -f $file ]; then
	offsetfile="$STATEDIR/offset$(echo $file|tr / .)"
	if [ -s $offsetfile -a -r $offsetfile ]; then
	    if [[ $(wc -c < $file) -lt $(tail -n 1  $offsetfile) ]]; then
		if [ -e $file.0 ]; then
	        # assume the log is rotated by savelog(8)
		    debug "Running logtail on rotated: $file.0"
		    $LOGTAIL -f $file.0 -o $offsetfile $LOGTAIL_OPTS > \
			$TMPDIR/logoutput/$(basename $file) \
			|| error "Could not run logtail or save output"
		    rm -f $offsetfile \
		        || error "Could not remove $offsetfile"
		elif [ -e $file.1 ]; then
		# assume the log is rotated by logrotate(8)
		    debug "Running logtail on rotated: $file.1"
		    $LOGTAIL -f $file.1 -o $offsetfile $LOGTAIL_OPTS > \
			$TMPDIR/logoutput/$(basename $file) \
			|| error "Could not run logtail or save output"
		    rm -f $offsetfile \
		        || error "Could not remove $offsetfile"
		fi
	    fi
	fi
	debug "Running logtail: $file"
	$LOGTAIL -f $file -o $offsetfile $LOGTAIL_OPTS \
	    >> $TMPDIR/logoutput/$(basename $file) \
	    || error "Could not run logtail or save output"
    else
	echo "E: File could not be read: $file" >> $TMPDIR/errors \
	    || error "Could not output to $TMPDIR/errors  Disk Full?"
    fi
}

# Show all the cli options to our users.
usage() {
    debug "usage: Printing usage and exiting"
    cat<<EOF
usage: logcheck [-c CFG] [-d] [-h] [-H HOST] [-l LOG] [-L CFG] [-m MAIL] [-o] 
                [-r DIR] [-s|-p|-w] [-R] [-S DIR] [-t] [-T] [-u]
 -c CFG       = overrule default configuration file
 -d           = debug mode
 -h           = print this usage guide and exit
 -H HOST      = use this hostname for the mail
 -l LOG       = run logfile through logcheck
 -L CFG       = overrule default logfiles list
 -m MAIL      = email the report to recipient
 -o           = stdout mode, not sending mail
 -p           = runlevel "paranoid"
 -r DIR       = overrule default rules directory
 -R           = adds Reboot: to email subject
 -s           = runlevel "server"
 -S DIR       = overrule default state directory
 -t           = testing mode
 -T           = do not remove the TMPDIR 
 -u           = enable syslog-summary
 -v           = print version
 -w           = runlevel "workstation"
EOF
}

# Check the commandline options for a change to the config file option
while getopts $GETOPTS opt; do
    case "$opt" in
	c)
	    debug "Setting CONFFILE to $OPTARG"
	    CONFFILE="$OPTARG"
	    ;;
	d)
	    LOGCHECKDEBUG=1
	    debug "Turning debug mode on"
	    ;;
	h)
	    usage
	    exit 0
	    ;;
	T)
	    debug "Setting NOCLEANUP to 1"
	    NOCLEANUP=1
	    ;;
	v)
	    echo "logcheck $VERSION"
	    exit 0
	    ;;
	\?)
	    usage
	    exit 1
	    ;;
    esac
done

# Now reset $OPTIND to 1 
OPTIND=1

debug "Sourcing - $CONFFILE"

# Now source the config file - before things that should not be changed
. $CONFFILE

# Setup the compatibility for the old style of setting $INTRO
# And handle it being set to ""
if [ -z "$INTRO" ]; then
	INTRO=1
else
    if [ "$INTRO" = "no" ]; then
	INTRO=0
    elif [ "$INTRO" = "yes" ]; then
	INTRO=1
    fi
fi

# Use sort -u or -k 1,3 -s 
if [ $SORTUNIQ -eq 1 ];then
    SORT="sort -u"
else
    SORT="sort -k 1,3 -s"
fi

# Now check for the other options
while getopts $GETOPTS opt; do
    case "$opt" in
    	H)
	   debug "Setting HOSTNAME to $OPTARG"
	   HOSTNAME="$OPTARG"
	   ;;
	l)
	    debug "Setting LOGFILE to $OPTARG"
	    LOGFILE="$OPTARG"
	    ;;
	L)
	    debug "Setting LOGFILES_LIST to $OPTARG"
	    LOGFILES_LIST="$OPTARG"
	    ;;
	m)
	    debug "Setting SENDMAILTO to $OPTARG"
	    SENDMAILTO="$OPTARG"
	    ;;
	o)
	    debug "Setting MAILOUT to 1"
	    MAILOUT="1"
	    ;;
	p)
	    debug "Setting REPORTLEVEL to paranoid"
	    REPORTLEVEL="paranoid"
	    ;;
	r)
	    debug "Setting RULEDIR to $OPTARG"
	    RULEDIR="$OPTARG"
	    ;;
	R)
	    debug "Setting REBOOT to 1"
	    REBOOT=1
	    ;;
	s)
	    debug "Setting REPORTLEVEL to server"
	    REPORTLEVEL="server"
	    ;;
	S)
	    debug "Setting STATEDIR to $OPTARG"
	    STATEDIR="$OPTARG"
	    ;;
	u)
	    debug "Setting SYSLOGSUMMARY to 1"
	    SYSLOGSUMMARY="1"
	    ;;
	t)
	    debug "Setting LOGTAIL_OPTS to -t"
	    LOGTAIL_OPTS=' -t'
	    ;;
	w)
	    debug "Setting REPORTLEVEL to workstation"
	    REPORTLEVEL="workstation"
	    ;;
	\?)
	    usage
	    exit 1
	    ;;
    esac
done
debug "Finished getopts $GETOPTS"
shift `expr $OPTIND - 1`

if [ $REPORTLEVEL = "workstation" ]; then
    REPORTLEVELS="workstation server paranoid"
elif [ $REPORTLEVEL = "server" ]; then
    REPORTLEVELS="server paranoid"
elif [ $REPORTLEVEL = "paranoid" ]; then
    REPORTLEVELS="paranoid"
else
    error "REPORTLEVEL is set to an unknown value" "noclean"
fi

trap 'cleanup' 0
                                                                                
debug "Trying to get lockfile: $LOCKFILE.lock"
lockfile-create --retry 1 $LOCKFILE > /dev/null 2>&1


if [ $? -eq 1 ]; then 
    error "Failed to get lockfile: $LOCKFILE.lock" "noclean"
else 
    debug "Running lockfile-touch $LOCKFILE.lock"
    lockfile-touch $LOCKFILE &
    LOCK="$!"
fi

# Create the secure temporary directory or exit
TMPDIR=$(mktemp -d -p /tmp logcheck.XXXXXX) \
    || TMPDIR=$(mktemp -d -p /var/tmp logcheck.XXXXXX) \
    || error "Could not create temporary directory"

# Now clean the rulefiles in the directories
cleanrules $RULEDIR/cracking.d $TMPDIR/cracking
cleanrules $RULEDIR/violations.d $TMPDIR/violations
cleanrules $RULEDIR/violations.ignore.d $TMPDIR/violations-ignore

# Now clean the ignore rulefiles for the report levels
for level in $REPORTLEVELS; do 
    cleanrules $RULEDIR/ignore.d.$level $TMPDIR/ignore
done

# The following cracking.ignore directory will only be used if
# $SUPPORT_CRACKING_IGNORE is set to 1 in the configuration file.
# This is *only* for local admin use.
if [ $SUPPORT_CRACKING_IGNORE -eq 1 ]; then
    cleanrules $RULEDIR/cracking.ignore.d $TMPDIR/cracking-ignore
fi

# Get the list of log files from config file
# Handle log rotation correctly, idea taken from Wiktor Niesiobedzki.
mkdir $TMPDIR/logoutput \
    || error "Could not mkdir for log files"
if [ ! $LOGFILE ]; then
    for file in $(egrep --text -v "(^#|^[[:space:]]*$)" $LOGFILES_LIST); do
	logoutput "$file"
    done 
elif [ $LOGFILE ]; then
    if [ -f $LOGFILE ] && [ -r $LOGFILE ]; then
        logoutput "$LOGFILE"
    else
    	error "$LOGFILE don't exist or we do not have permissions to read it"
    fi
fi

# First sort the logs to remove duplicate lines (from different logfiles with
# the same lines) and reduce CPU and memory usage afterwards.
debug "Sorting logs"
$SORT $TMPDIR/logoutput/* | sed -e 's/[[:space:]]\+$//' | cat  \
    > $TMPDIR/logoutput-sorted \
        || error "Could not output to $TMPDIR/logoutput-sorted Disk Full?"
 

# See if the tmp file exists and actually has data to check,
# if it doesn't we should erase it and exit as our job is done.
if [ ! -s $TMPDIR/logoutput-sorted -a ! -f $TMPDIR/errors ]; then
    debug "Nothing to report"
    exit 0
elif [ ! -s $TMPDIR/logoutput-sorted -a -f $TMPDIR/errors ]; then
    error "$(cat $TMPDIR/errors)"
fi

if [ $INTRO -eq 1 ]; then
    debug "Setting the Intro"
    setintro
else
    debug "Not setting the Intro"
fi


if [ -f $TMPDIR/errors ]; then
    { 
	cat<<EOF

$(cat $TMPDIR/errors)

EOF
    } >> $TMPDIR/report \
    || error "Could not output to $TMPDIR/report Disk Full?"
fi

# Check for blatant cracking attempts
if [ -d $TMPDIR/cracking ]; then
    if [ $SUPPORT_CRACKING_IGNORE -eq 1 ]; then
	debug "Checking for security alerts and using cracking-ignore"
	if [ -d $TMPDIR/cracking-ignore ]; then
	    greplogoutput $TMPDIR/cracking "$ATTACKSUBJECT" \
		$TMPDIR/cracking-ignore && ATTACK="1"
	fi
    else
	debug "Checking for security alerts"
	greplogoutput $TMPDIR/cracking "$ATTACKSUBJECT" \
	    && ATTACK="1"
    fi
fi

# Check for security events
if [ -d $TMPDIR/violations ]; then
    debug "Checking for security events"
    rm -f $TMPDIR/checked 

    if [ $ATTACK -eq 1 ]; then
	greplogoutput $TMPDIR/violations "$SECURITYSUBJECT" \
	    $TMPDIR/violations-ignore $TMPDIR/cracking && SECURITY="1"
    else
	greplogoutput $TMPDIR/violations "$SECURITYSUBJECT" \
	    $TMPDIR/violations-ignore && SECURITY="1"
    fi
fi

# Do reverse grep on patterns we want to ignore
if [ -d $TMPDIR/ignore ]; then
    debug "Checking for system events"
    cp $TMPDIR/logoutput-sorted $TMPDIR/checked \
    || error "Could not copy $TMPDIR/logoutput-sorted to $TMPDIR/checked"
    cleanchecked $TMPDIR/ignore

    if [ -s $TMPDIR/checked ]; then
	debug "Removing alerts from system events"
	cleanchecked $TMPDIR/cracking
    fi
    if [ -s $TMPDIR/checked ]; then 	
	debug "Removing violations from system events"
	cleanchecked $TMPDIR/violations
    fi
    report "$EVENTSSUBJECT" && SYSTEM="1"
fi

# Include the footer if present.
if [ $INTRO -eq 1 ]; then
    debug "Setting the footer text"
    setfooter
else
    debug "Not setting the footer text"
fi

# If there are results, mail them to sysadmin
if [ $ATTACK -eq 1 ]; then
    sendreport "$ATTACKSUBJECT"
elif [ $SECURITY -eq 1 ]; then
    sendreport "$SECURITYSUBJECT"
elif [ $SYSTEM -eq 1 ]; then
    sendreport "$EVENTSSUBJECT"
fi
