#!/bin/sh
#
# ipfw2dshield: a DShield client for ipfw logs.

version="0.5"

###########################################################################
#
#    Copyright (C) 2002, 2004  Frank W. Josellis
#
#    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 tis program; if not, write to the Free Software
#    Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111, USA.
#
###########################################################################
#
###############################################################################
# Handy shell functions.
###############################################################################

usage(){
    echo "Usage: $prog [-t] [-d logdir] [-b logbase] [-s stampfile]"
    echo "       $prog [-h|-v]"
    echo -e "
 -t\tToggle test mode. [Sets dshield id to 0, mails report
\tto root@localhost, and doesn't update the stampfile.]\n
 -d logdir
\tThe directory containing the logfiles to be inspected. 
\t[default: $_logdir]\n
 -b logbase
\tThe basename of the logfiles to be inspected, i.e.,
\twithout the possible suffixes inherited from log rotation.
\t[default: $_logbase]\n
 -s stampfile
\tThe stampfile in /var/tmp used for keeping track of the
\treports to prevent multiple submission.
\t[default: $_stampfile]\n
 -h\tShow this help.\n
 -v\tVersion info.\n"
}

unixdate(){
    udate=`strpdate -f "%Y %b %e %T" "$year $1"`
    [ $udate -gt $now ] && \
	udate=`strpdate -f "%Y %b %e %T" "$last_year $1"`
}		

#
# Collect all matching records respecting the date limits. Attempt to choose
# a reasonable value for 'year' and convert each record's timestamp to Unix.
#
filter1(){
    repeated="last message repeated"
    read input
    record=""
    while [ "$input" ]; do
	if [ "`echo $input | grep \"$search\"`" ]; then
	    # Match according to search pattern.
	    logdate=`echo $input | cut -f1-3 -d' '`
	    unixdate "$logdate"
	    if [ $udate -ge $ref_date -a $udate -lt $now ]; then
		record="$udate `echo $input | cut -f9- -d' '`"
		echo $record
	    else
		record=""
	    fi
	elif [ "$record" -a "`echo $input | grep \"$repeated\"`" ]; then
	    # Match according to "last message repeated r times".
	    logdate=`echo $input | cut -f1-3 -d' '`
	    unixdate "$logdate"
	    if [ $udate -ge $ref_date -a $udate -lt $now ]; then
		record="$udate `echo $record | cut -f2- -d' '`"
		r=`echo $input | cut -f8 -d ' '`
		while [ $r -gt 0 ]; do
		    echo $record
		    r=`expr $r - 1`
		done
	    else
		record=""
	    fi
	else
	    # No match.
	    record=""
	fi
	    
	read input
    done
}


#
# Count the multiplicity of the previously collected records and discard
# identical entries. Convert a record's timestamp to the dshield format.
#
filter2(){
    date_cmd="date"
    case $utc_timestamps in
	[Yy][Ee][Ss])
	date_cmd="date -u"
	;;
    esac

    read input
    while [ "$input" ]; do
	date=`echo $input | cut -f1 -d' '`
	gmtoff=`$date_cmd -r $date +%z`
	gmtoff_H=`expr $gmtoff : '\(.\{3\}\).\{2\}'`
	gmtoff_M=`expr $gmtoff : '.\{3\}\(.\{2\}\)'`
	offset="$gmtoff_H:$gmtoff_M"
	fdate=`$date_cmd -r $date +"%Y-%m-%d %T"`
	
	record="$fdate $offset `echo $input | cut -f2- -d' '`"
	if [ "`grep \"$record\" $tmpfile2`" ]; then
	    read input
	    continue
	fi
	
	count="`grep \"$input\" $tmpfile1 | wc -l | tr -d [:space:]`"
	echo $count
	echo $record

	read input
    done
}	
   
#
# Read the multiplicity of a record and then the record. Rewrite the 
# input according to the dshield requirements. Currently recognized
# protocols are TCP, UDP, ICMP.
#
filter3(){
    read count
    while [ "$count" ]; do
	read input
	
	fdate="`echo $input | cut -f1-3 -d' '`"
	proto="`echo $input | cut -f4 -d' '`"
	source="`echo $input | cut -f5 -d' '`"
	target="`echo $input | cut -f6 -d' '`"

	case $proto in
	    ICMP*)  
		    s_addr=$source
		    t_addr=$target
		    ports="`echo $proto | cut -f2 -d:`"
		    s_port="`echo $ports | cut -f1 -d.`"
		    t_port="`echo $ports | cut -f2 -d.`"
		    proto=ICMP
		    ;;
	    
	    TCP|UDP)  
		    s_addr="`echo $source | cut -f1 -d:`"
		    s_port="`echo $source | cut -f2 -d:`"
		    t_addr="`echo $target | cut -f1 -d:`"
		    t_port="`echo $target | cut -f2 -d:`"
		    ;;
	    
	    *)	    read count
		    continue
		    ;;
	esac
	
	check_addr source $proto $s_addr $s_port
	if [ $? -ne 0 ]; then
	    read count
	    continue
	fi

	check_addr target $proto $t_addr $t_port
	if [ $? -ne 0 ]; then
	    read count
	    continue
	fi
	    
	ip="$s_addr\t$s_port\t$t_addr\t$t_port\t$proto"
	echo -e "$fdate\t$userid\t$count\t$ip"

	read count
    done
}


#
# Check if an IP address matches an exclusion list.
#
check_addr(){
    eval droplist1=\$drop_$1
    eval droplist2=\$drop_$1_$2
    for drop in $droplist1 $droplist2; do
	drop_a=`echo $drop: | cut -f1 -d:`
	drop_p=`echo $drop: | cut -f2 -d: | tr , ' '`

	if [ "$drop_a" = "$3" ]; then
	    if [ "$drop_p" ]; then
		for port_spec in $drop_p; do
		    p0=`echo $port_spec | cut -f1 -d-`
		    p1=`echo $port_spec | cut -f2 -d-`
		    [ "$p1" -a ! "$p0" ] && p0=0
		    [ "$p0" -a ! "$p1" ] && p1=65535
		    [ $4 -ge $p0 -a $4 -le $p1 ] && return 1
		done		    
	    else
		return 1
	    fi
	fi

	[ "$ipaddr" ] || continue
	mask=`echo $drop_a/32 | cut -f2 -d/`
	if [ $mask -lt 32 -a $mask -ge 0 ]; then
	    if [ "$drop_a" = "`$ipaddr $3/$mask | grep CIDR | cut -f3`" ]; then
		if [ "$drop_p" ]; then
		    for port_spec in $drop_p; do
			p0=`echo $port_spec | cut -f1 -d-`
			p1=`echo $port_spec | cut -f2 -d-`
			[ "$p1" -a ! "$p0" ] && p0=0
			[ "$p0" -a ! "$p1" ] && p1=65535
			[ $4 -ge $p0 -a $4 -le $p1 ] && return 1
		    done
		else
		    return 1
		fi
	    fi
	fi		
    done
    return 0
}


#
# Update the reference time.
#
update_ref(){
    echo $now > $ref_tm
    touch -t `date -r $now +%Y%m%d%H%M.%S` $ref_tm
}


#
# Clean termination.
#
bye(){
    echo "$prog: $1" >&2
    rm -f $tmpfile1 $tmpfile2 $tmpfile3
    exit 1
}


###############################################################################
#
# Main section.
#
prog="`basename $0`"

[ `id -u` -eq 0 ] || bye "You need to be root to run this program."

uname=`uname`
[ "$uname" = "FreeBSD" -o "$uname" = "Darwin" ] || \
    bye "You can't run this program on $uname." 

# Check if strpdate is installed.
[ -x "`which strpdate`" ] || \
    bye "Can't find 'strpdate' on this system."

# See if ipaddr is installed.
if [ -x "`which ipaddr`" ]; then
    ipaddr="`which ipaddr`"
else
    echo "$prog: Warning: Can't find 'ipaddr' on this system." >&2
fi

# Suck in the config file.
rc=~/.$prog.rc
[ -r $rc ] || bye "Permission denied: $rc"
. $rc
_logdir=$logdir
_logbase=$logbase
_stampfile=$stampfile

# Get options.
args=`getopt b:d:s:thv $*`
if [ $? != 0 ]; then
    usage
    exit 1
fi
set -- $args
for i; do 
    case $i in
	-b)
	logbase="$2"
	shift; shift
	;;
	-d)
	logdir="$2"
	shift; shift
	;;
	-s)
	stampfile="`basename $2`"
	shift; shift
	;;
	-t)
	mode="TEST"
	userid="0"
	mailto="root@localhost"
	shift
	;;
	-h)
	usage
	exit 1
	;;
	-v)
	echo $prog version $version
	exit 0
	;;
    esac
done

# The chronologically ordered logfiles.
_logfiles=`echo $logdir/$logbase | tr -s "/"`
logfiles=`ls -rt $_logfiles* 2> /dev/null`
[ "$logfiles" ] || \
    bye "No such logfiles found: $_logfiles*"

now=`date +%s`
year=`date -r $now +%Y`
last_year=`expr $year - 1`

# The reference time -- normally the date of the previous inspection.
ref_tm=/var/tmp/$stampfile

# If a reference time is not given set it to Unix Epoch.
if [ ! -r $ref_tm ]; then
    echo 0 > $ref_tm && touch -t `date -r 0 +%Y%m%d%H%M.%S` $ref_tm
fi
ref_date=`cat $ref_tm`

# Use mktemp if possible (missing on older Darwin releases).
if [ -x `which mktemp` ]; then
    for i in 1 2 3; do
	eval tmpfile$i=`mktemp /var/tmp/$prog.XXXXXX`
    done
else
    for i in 1 2 3; do
	eval tmpfile$i=/var/tmp/$prog\_$$_$i
	eval rm -f \$tmpfile$i
	eval touch \$tmpfile$i \&\& chmod 600 \$tmpfile$i
    done
fi


#
# Step 1
#
for log in $logfiles; do
    # Skip log if it is older than 6 months.
    [ "`ls -l $log | tr -s [:space:] | cut -f8 -d' ' | grep :`" ] || continue
    
    # Skip log if it is older than our reference time.
    [ $log -ot $ref_tm ] && continue
    
    # Check for compressed/gzipped/bzipped2 or plain ASCII log data.
    if [ "`file $log | grep compress`" ]; then
	if [ "`file $log | grep bzip2`" ]; then
	    cat_cmd=bzcat
	else	    
	    cat_cmd=zcat
	fi
    else
	cat_cmd=cat
    fi
    
    echo -n "$prog: Reading from $log ... "
    $cat_cmd $log | filter1 >> $tmpfile1
    echo "done."
done


# Go home if no new entries have been found.
[ -s $tmpfile1 ] || bye "Nothing to update."


#
# Step 2
#
echo -n "$prog: Processing records ... "
filter2 < $tmpfile1 >> $tmpfile2 
echo "done." && rm $tmpfile1


#
# Step 3
#
echo -n "$prog: Formatting output ... "
filter3 < $tmpfile2 >> $tmpfile3
echo "done." && rm $tmpfile2


#
# Mail the report and clean up.
#
subject="FORMAT DSHIELD USERID $userid"
[ "$mailcc" ] && mail_opts="-c $mailcc"
[ "$mailbcc" ] && mail_opts="$mail_opts -b $mailbcc"
[ "$sender" ] && sendmail_opts="-f $sender"
cat $tmpfile3 | mail -s "$subject" $mail_opts $mailto $sendmail_opts && \
    echo "$prog: Report sent to $mailto"
rm $tmpfile3
[ "$mode" != "TEST" ] && update_ref

exit 0
