1#!/bin/sh
2#
3# ipfw2dshield: a DShield client for ipfw logs.
4
5version="0.5"
6
7###########################################################################
8#
9#    Copyright (C) 2002, 2004  Frank W. Josellis
10#
11#    This program is free software; you can redistribute it and/or modify
12#    it under the terms of the GNU General Public License as published by
13#    the Free Software Foundation; either version 2 of the License, or
14#    (at your option) any later version.
15#
16#    This program is distributed in the hope that it will be useful,
17#    but WITHOUT ANY WARRANTY; without even the implied warranty of
18#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19#    GNU General Public License for more details.
20#
21#    You should have received a copy of the GNU General Public License
22#    along with tis program; if not, write to the Free Software
23#    Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111, USA.
24#
25###########################################################################
26#
27###############################################################################
28# Handy shell functions.
29###############################################################################
30
31usage(){
32    echo "Usage: $prog [-t] [-d logdir] [-b logbase] [-s stampfile]"
33    echo "       $prog [-h|-v]"
34    echo -e "
35 -t\tToggle test mode. [Sets dshield id to 0, mails report
36\tto root@localhost, and doesn't update the stampfile.]\n
37 -d logdir
38\tThe directory containing the logfiles to be inspected.
39\t[default: $_logdir]\n
40 -b logbase
41\tThe basename of the logfiles to be inspected, i.e.,
42\twithout the possible suffixes inherited from log rotation.
43\t[default: $_logbase]\n
44 -s stampfile
45\tThe stampfile in /var/tmp used for keeping track of the
46\treports to prevent multiple submission.
47\t[default: $_stampfile]\n
48 -h\tShow this help.\n
49 -v\tVersion info.\n"
50}
51
52unixdate(){
53    udate=`strpdate -f "%Y %b %e %T" "$year $1"`
54    [ $udate -gt $now ] && \
55	udate=`strpdate -f "%Y %b %e %T" "$last_year $1"`
56}
57
58#
59# Collect all matching records respecting the date limits. Attempt to choose
60# a reasonable value for 'year' and convert each record's timestamp to Unix.
61#
62filter1(){
63    repeated="last message repeated"
64    read input
65    record=""
66    while [ "$input" ]; do
67	if [ "`echo $input | grep \"$search\"`" ]; then
68	    # Match according to search pattern.
69	    logdate=`echo $input | cut -f1-3 -d' '`
70	    unixdate "$logdate"
71	    if [ $udate -ge $ref_date -a $udate -lt $now ]; then
72		record="$udate `echo $input | cut -f9- -d' '`"
73		echo $record
74	    else
75		record=""
76	    fi
77	elif [ "$record" -a "`echo $input | grep \"$repeated\"`" ]; then
78	    # Match according to "last message repeated r times".
79	    logdate=`echo $input | cut -f1-3 -d' '`
80	    unixdate "$logdate"
81	    if [ $udate -ge $ref_date -a $udate -lt $now ]; then
82		record="$udate `echo $record | cut -f2- -d' '`"
83		r=`echo $input | cut -f8 -d ' '`
84		while [ $r -gt 0 ]; do
85		    echo $record
86		    r=`expr $r - 1`
87		done
88	    else
89		record=""
90	    fi
91	else
92	    # No match.
93	    record=""
94	fi
95
96	read input
97    done
98}
99
100
101#
102# Count the multiplicity of the previously collected records and discard
103# identical entries. Convert a record's timestamp to the dshield format.
104#
105filter2(){
106    date_cmd="date"
107    case $utc_timestamps in
108	[Yy][Ee][Ss])
109	date_cmd="date -u"
110	;;
111    esac
112
113    read input
114    while [ "$input" ]; do
115	date=`echo $input | cut -f1 -d' '`
116	gmtoff=`$date_cmd -r $date +%z`
117	gmtoff_H=`expr $gmtoff : '\(.\{3\}\).\{2\}'`
118	gmtoff_M=`expr $gmtoff : '.\{3\}\(.\{2\}\)'`
119	offset="$gmtoff_H:$gmtoff_M"
120	fdate=`$date_cmd -r $date +"%Y-%m-%d %T"`
121
122	record="$fdate $offset `echo $input | cut -f2- -d' '`"
123	if [ "`grep \"$record\" $tmpfile2`" ]; then
124	    read input
125	    continue
126	fi
127
128	count="`grep \"$input\" $tmpfile1 | wc -l | tr -d [:space:]`"
129	echo $count
130	echo $record
131
132	read input
133    done
134}
135
136#
137# Read the multiplicity of a record and then the record. Rewrite the
138# input according to the dshield requirements. Currently recognized
139# protocols are TCP, UDP, ICMP.
140#
141filter3(){
142    read count
143    while [ "$count" ]; do
144	read input
145
146	fdate="`echo $input | cut -f1-3 -d' '`"
147	proto="`echo $input | cut -f4 -d' '`"
148	source="`echo $input | cut -f5 -d' '`"
149	target="`echo $input | cut -f6 -d' '`"
150
151	case $proto in
152	    ICMP*)
153		    s_addr=$source
154		    t_addr=$target
155		    ports="`echo $proto | cut -f2 -d:`"
156		    s_port="`echo $ports | cut -f1 -d.`"
157		    t_port="`echo $ports | cut -f2 -d.`"
158		    proto=ICMP
159		    ;;
160
161	    TCP|UDP)
162		    s_addr="`echo $source | cut -f1 -d:`"
163		    s_port="`echo $source | cut -f2 -d:`"
164		    t_addr="`echo $target | cut -f1 -d:`"
165		    t_port="`echo $target | cut -f2 -d:`"
166		    ;;
167
168	    *)	    read count
169		    continue
170		    ;;
171	esac
172
173	check_addr source $proto $s_addr $s_port
174	if [ $? -ne 0 ]; then
175	    read count
176	    continue
177	fi
178
179	check_addr target $proto $t_addr $t_port
180	if [ $? -ne 0 ]; then
181	    read count
182	    continue
183	fi
184
185	ip="$s_addr\t$s_port\t$t_addr\t$t_port\t$proto"
186	echo -e "$fdate\t$userid\t$count\t$ip"
187
188	read count
189    done
190}
191
192
193#
194# Check if an IP address matches an exclusion list.
195#
196check_addr(){
197    eval droplist1=\$drop_$1
198    eval droplist2=\$drop_$1_$2
199    for drop in $droplist1 $droplist2; do
200	drop_a=`echo $drop: | cut -f1 -d:`
201	drop_p=`echo $drop: | cut -f2 -d: | tr , ' '`
202
203	if [ "$drop_a" = "$3" ]; then
204	    if [ "$drop_p" ]; then
205		for port_spec in $drop_p; do
206		    p0=`echo $port_spec | cut -f1 -d-`
207		    p1=`echo $port_spec | cut -f2 -d-`
208		    [ "$p1" -a ! "$p0" ] && p0=0
209		    [ "$p0" -a ! "$p1" ] && p1=65535
210		    [ $4 -ge $p0 -a $4 -le $p1 ] && return 1
211		done
212	    else
213		return 1
214	    fi
215	fi
216
217	[ "$ipaddr" ] || continue
218	mask=`echo $drop_a/32 | cut -f2 -d/`
219	if [ $mask -lt 32 -a $mask -ge 0 ]; then
220	    if [ "$drop_a" = "`$ipaddr $3/$mask | grep CIDR | cut -f3`" ]; then
221		if [ "$drop_p" ]; then
222		    for port_spec in $drop_p; do
223			p0=`echo $port_spec | cut -f1 -d-`
224			p1=`echo $port_spec | cut -f2 -d-`
225			[ "$p1" -a ! "$p0" ] && p0=0
226			[ "$p0" -a ! "$p1" ] && p1=65535
227			[ $4 -ge $p0 -a $4 -le $p1 ] && return 1
228		    done
229		else
230		    return 1
231		fi
232	    fi
233	fi
234    done
235    return 0
236}
237
238
239#
240# Update the reference time.
241#
242update_ref(){
243    echo $now > $ref_tm
244    touch -t `date -r $now +%Y%m%d%H%M.%S` $ref_tm
245}
246
247
248#
249# Clean termination.
250#
251bye(){
252    echo "$prog: $1" >&2
253    rm -f $tmpfile1 $tmpfile2 $tmpfile3
254    exit 1
255}
256
257
258###############################################################################
259#
260# Main section.
261#
262prog="`basename $0`"
263
264[ `id -u` -eq 0 ] || bye "You need to be root to run this program."
265
266uname=`uname`
267[ "$uname" = "FreeBSD" -o "$uname" = "Darwin" -o "$uname" = "DragonFly"] || \
268    bye "You can't run this program on $uname."
269
270# Check if strpdate is installed.
271[ -x "`which strpdate`" ] || \
272    bye "Can't find 'strpdate' on this system."
273
274# See if ipaddr is installed.
275if [ -x "`which ipaddr`" ]; then
276    ipaddr="`which ipaddr`"
277else
278    echo "$prog: Warning: Can't find 'ipaddr' on this system." >&2
279fi
280
281# Suck in the config file.
282rc=/usr/local/etc/$prog.rc
283[ -e ~/.$prog.rc ] && rc=~/.$prog.rc
284[ -r $rc ] || bye "Permission denied: $rc"
285. $rc
286_logdir=$logdir
287_logbase=$logbase
288_stampfile=$stampfile
289
290# Get options.
291args=`getopt b:d:s:thv $*`
292if [ $? != 0 ]; then
293    usage
294    exit 1
295fi
296set -- $args
297for i; do
298    case $i in
299	-b)
300	logbase="$2"
301	shift; shift
302	;;
303	-d)
304	logdir="$2"
305	shift; shift
306	;;
307	-s)
308	stampfile="`basename $2`"
309	shift; shift
310	;;
311	-t)
312	mode="TEST"
313	userid="0"
314	mailto="root@localhost"
315	shift
316	;;
317	-h)
318	usage
319	exit 1
320	;;
321	-v)
322	echo $prog version $version
323	exit 0
324	;;
325    esac
326done
327
328# The chronologically ordered logfiles.
329_logfiles=`echo $logdir/$logbase | tr -s "/"`
330logfiles=`ls -rt $_logfiles* 2> /dev/null`
331[ "$logfiles" ] || \
332    bye "No such logfiles found: $_logfiles*"
333
334now=`date +%s`
335year=`date -r $now +%Y`
336last_year=`expr $year - 1`
337
338# The reference time -- normally the date of the previous inspection.
339ref_tm=/var/tmp/$stampfile
340
341# If a reference time is not given set it to Unix Epoch.
342if [ ! -r $ref_tm ]; then
343    echo 0 > $ref_tm && touch -t `date -r 0 +%Y%m%d%H%M.%S` $ref_tm
344fi
345ref_date=`cat $ref_tm`
346
347# Use mktemp if possible (missing on older Darwin releases).
348if [ -x `which mktemp` ]; then
349    for i in 1 2 3; do
350	eval tmpfile$i=`mktemp /var/tmp/$prog.XXXXXX`
351    done
352else
353    for i in 1 2 3; do
354	eval tmpfile$i=/var/tmp/$prog\_$$_$i
355	eval rm -f \$tmpfile$i
356	eval touch \$tmpfile$i \&\& chmod 600 \$tmpfile$i
357    done
358fi
359
360
361#
362# Step 1
363#
364for log in $logfiles; do
365    # Skip log if it is older than 6 months.
366    [ "`ls -l $log | tr -s [:space:] | cut -f8 -d' ' | grep :`" ] || continue
367
368    # Skip log if it is older than our reference time.
369    [ $log -ot $ref_tm ] && continue
370
371    # Check for compressed/gzipped/bzipped2 or plain ASCII log data.
372    if [ "`file $log | grep compress`" ]; then
373	if [ "`file $log | grep bzip2`" ]; then
374	    cat_cmd=bzcat
375	else
376	    cat_cmd=zcat
377	fi
378    else
379	cat_cmd=cat
380    fi
381
382    echo -n "$prog: Reading from $log ... "
383    $cat_cmd $log | filter1 >> $tmpfile1
384    echo "done."
385done
386
387
388# Go home if no new entries have been found.
389[ -s $tmpfile1 ] || bye "Nothing to update."
390
391
392#
393# Step 2
394#
395echo -n "$prog: Processing records ... "
396filter2 < $tmpfile1 >> $tmpfile2
397echo "done." && rm $tmpfile1
398
399
400#
401# Step 3
402#
403echo -n "$prog: Formatting output ... "
404filter3 < $tmpfile2 >> $tmpfile3
405echo "done." && rm $tmpfile2
406
407
408#
409# Mail the report and clean up.
410#
411subject="FORMAT DSHIELD USERID $userid"
412[ "$mailcc" ] && mail_opts="-c $mailcc"
413[ "$mailbcc" ] && mail_opts="$mail_opts -b $mailbcc"
414[ "$sender" ] && sendmail_opts="-f $sender"
415cat $tmpfile3 | mail -s "$subject" $mail_opts $mailto $sendmail_opts && \
416    echo "$prog: Report sent to $mailto"
417rm $tmpfile3
418[ "$mode" != "TEST" ] && update_ref
419
420exit 0
421