#!/bin/bash
if [[ "$1" == 'debug'  ]]; then set -x;		_DEBUG=$1; shift; fi
if [[ "$1" == 'debug2' ]]; then set -xvT;	_DEBUG=$1; shift; fi
if [[ "$1" == 'trace'  ]]; then 		_TRACE=$1; shift; fi
####################################################################
#############################################################
# - WatchMB - 
# Supporting WatchMX for mail break-ins POP, Imap, SASL, ...
#
# We are triggered by WatchMX that tracks the 'maillog'
# From WatchMX we only get suspicious lines that have 
# to do with break-in attempts to mailboxes & mail transport
# So we have to do the determination ourselves ...
# i.e. BANDITs IP addr, CLASS a.s.o.
#############################################################
#------------------------
REALPATH=`realpath $0`
WHERE=`dirname $REALPATH`
ME=`basename $REALPATH`
cd $WHERE
#----- Core stuff ---------------------
. ../../system.conf
. ../../watchermap.conf
. ../../common.conf
. ../../common.bashlib
#----- API stuff ----------------------
. ..api/bash/sql.bashlib
#----- Private stuff ------------------
. ../WatchMX/WatchMX.conf	# Listen to mother's voice
. ../WatchMX/private.bashlib
. $ME.conf

trap refresh			HUP		# React on a 'HUP' for the filter refresh
trap stopservice		TERM INT	# React on a termination request
trap rerun			KILL 9		# React on crash 
trap 'errorexit $? $_'		EXIT		# React on script errors

load_filter() {
# $1 .. filter status
# $2 .. compress flag
local funtag="[${FUNCNAME[0]}]"
	unset -f filter

	[[ -f filter 	   	]] 	&& mv filter filter.old
	[[ -f filter.compress 	]] 	&& rm -f filter.compress

	./mkfilter $1
	source filter	# Must load here at first for the compress to work

	if [ -f filter.old ]
	then	diff -c filter filter.old >filter-diffs
	else 	[[ -f filter-diffs ]] && rm -f filter-diffs
	fi

	if [ ! -z "$2" ]
	then	# If the 'compress filter' flag is set
		# Compress filter; i.e. remove comments for runtime
		compress_filter
		. filter.compress

		trace "$funtag Using filter compressed"
	else	trace "$funtag Using filter plain"
	fi
	
	#
	# Read map for superfluous stuff as well
	#
	SUPERFLUOUS=`mk_superfluous`
}

refresh() {
local funtag="[${FUNCNAME[0]}]"

	trace   "$funtag Reloading filters for $ME ..."
	logger  "$ME[$$]: $funtag Reloading filters for $ME ..."
	load_filter "$funtag `date --iso=seconds`" $COMPRESS_FILTER
}

errorexit() {
local	funtag="[${FUNCNAME[0]}]"

	logger	"$ME[$$]: $funtag Trapped with $1, Cmd: '$2' ... Loop: $loop"
	trace	"$funtag Trapped with $1, Cmd: '$2' ... Loop: $loop"
	trace   "$funtag BASH Source: $BASH_SOURCE"
	trace	"$funtag Caller     : $(caller)"
	trace	"$funtag BASH line 0: ${BASH_LINENO[0]}"
	trace	"$funtag BASH line 1: ${BASH_LINENO[1]}"
	trace	"$funtag BASH line 2: ${BASH_LINENO[2]}"
	exit 255
}

stopservice() {
local	funtag="[${FUNCNAME[0]}]"

	logger 	"$ME[$$]: $funtag Terminating on request ..."
	trace	"$funtag Terminating on request ..."
	exit
}

rerun() {
local	funtag="[${FUNCNAME[0]}]"

	log	"$ME: $funtag Abnormal termination, exit status: $?, Loop; $loop"
#	log	"Line was: '$LINE'"
#	log	"Rule was: $RULE"

	logger  "$ME[$$]: $funtag Restarting in a minute ..."
	trace	"$funtag Restarting in a minute ..."
	echo	"$WHERE/$ME" | at "now + 1 minute"
	exit
}

#
# - inject -
# Do all the dirty work here
# Determination, whitelisting, ...
#
inject() {
local	funtag="[${FUNCNAME[0]}]"
local	penalty=${1:-1}
local	retcode
local	haveit
local	isdropped
local	this_ip this_state this_affairs this_severity

	trace "$funtag Triggered by rule [$RULE] '$Pattern'"

	BANDIT=`get_bandit`
	
	if ipset -q test whitelist "$BANDIT"
	then return 3   # Flag whitelisted IP address and bailout
	fi

	validate_IP $BANDIT
	if [ $? -ne 0 ]
	then	trace "$funtag Got junk for bandit '$BANDIT' ... bailing out ..."
		return 255
	fi

	CLASS=`get_class $BANDIT`
	MB_CLASS="Breaker"
	
	trace "$funtag $CLASS $BANDIT, MB-Class: $MB_CLASS"

	sql_track=()
	sql_track[cmd]="
		select IP,state,affairs,severity  from $TABLE where IP='$BANDIT'
		;"
	exec_sql sql_track $ME

        this_ip=${resultset[IP]}
        this_state=${resultset[state]}
        this_affairs=${resultset[affairs]}
        this_severity=${resultset[severity]}
	isdropped="$this_state"

	#
	# See if this bandit is already known ...
	#
	haveit=${resultset[IP]}

	# Introduction of bandit?
	if [ -z "$haveit" ]
	then
		# NX domains have no business to do on MTAs and FAKEHOSTs
		# are suspicious anyway. These get their affairs set up
		# at 'MAX_AFFAIRS-1'; i.e. they die on 2nd attempt.
		# The first attempt is needed to identify and classify them.
		#
		if [[ "NXDOMAIN FAKEHOST" =~ "$CLASS" ]]
		then penalty=$(( $MAX_AFFAIRS - 1 ))
		fi

		AFFAIRS=$penalty

		if [ -n "$GEOTRACK" ]
		then do_geotrack $BANDIT $ME $funtag
		fi

		# Write IPSET first of all
		$IPSET -exist add tarpit $BANDIT timeout $((2**AFFAIRS * TIME_SLICE)) comment "tarpit-$funtag,$THIS_TYPE,$AFFAIRS"

		STATE='INTRO'
		ACTION="Initial $AFFAIRS/$MAX_AFFAIRS, Penalty: $penalty"
		Comment="Mailbox break-in"

		sql_track=()
		sql_track[cmd]="
			insert into $TABLE 	(IP,origin,rule,class,mb_class,state,severity,
						type,affairs,date_event,date_intro,comment)
			values ('$BANDIT','$ME','$RULE','$CLASS','$MB_CLASS','$STATE','Initial',
				'$THIS_TYPE',$AFFAIRS,
				datetime(current_timestamp,'localtime'),
				datetime(current_timestamp,'localtime'),'$Comment')
			;"
		exec_sql sql_track $ME

		retcode=1
	else 
		#
		# OK. Have this culprit already?
		#
		isdropped=`$SQL "select IP from $TABLE where IP='$BANDIT' and state='DROP';"`

		if [[ -n "$isdropped" ]]
		then
			trace "$funtag DROPed culprit $BANDIT re-occured ... taken into custody ..."
			$IPSET -exist add custody $BANDIT comment "custody-$funtag,$THIS_TYPE,$CLASS"
			return 1 
		fi

		#AFFAIRS=`$SQL "select affairs from $TABLE where IP='$BANDIT';"`
		AFFAIRS=$this_affairs
		((AFFAIRS++))

		#
		# Limit of affairs reached ?
		#
		if [ $AFFAIRS -ge $MAX_AFFAIRS ]
		then
			# punch IPSET first
			# Last attempt ... killed on next access ...
			$IPSET -exist add tarpit $BANDIT timeout $((2**AFFAIRS * TIME_SLICE)) comment "tarpit-$funtag,$THIS_TYPE,$AFFAIRS"

			# Drop if the limit is reached
			STATE='DROP'
			ACTION="Dropped @ $AFFAIRS"

			sql_track=()
			sql_track[cmd]="
				update $TABLE set 
					class	= '$CLASS',
					mb_class= '$MB_CLASS',
					severity= 'Dropped',
					origin	= '$ME',
					rule	= '$RULE',
					affairs	= $AFFAIRS,
					state	= '$STATE',
					comment	= '$ACTION',
					date_event=datetime(current_timestamp,'localtime')
				where IP='$BANDIT'
				;"
			exec_sql sql_track $ME

			retcode=1
		else
			# Punch IPSET first of all
			$IPSET -exist add tarpit $BANDIT timeout $((2**AFFAIRS * TIME_SLICE)) comment "tarpit-$funtag,$THIS_TYPE,$AFFAIRS"
			STATE='COUNT'
			ACTION="Count   $AFFAIRS/$MAX_AFFAIRS"

			sql_track=()
			sql_track[cmd]="
				update $TABLE set 
					affairs	=$AFFAIRS,
					rule	='$RULE',
					class	='$CLASS',
					severity='Counted',
					state	='$STATE',
					comment	='$ACTION',
					date_event=datetime(current_timestamp,'localtime')
				where IP = '$BANDIT'
				;"
			exec_sql sql_track $ME

			retcode=1
		fi
	fi

	write_affairs $BANDIT $AFFAIRS $RULE $ME

	clog	"$funtag" "$CLASS" "$BANDIT" "'$ACTION'" 
	trace	"$funtag $CLASS $BANDIT '$ACTION'"

	return $retcode
}

#------------------ Main -------------------------------
dump_runtime

# Set 'time slice' if missing or empty
# if [ -z "$TIME_SLICE" ]; then TIME_SLICE=60; fi
: "${TIME_SLICE:=60}"

if [ ! -p $PIPE ]
then mkfifo -m 600 $PIPE
fi

touch $LOG		;	 chmod 600 $LOG
touch $ME.trace		;	 chmod 600 $ME.trace
touch $ME.sql_trace	;	 chmod 600 $ME.sql_trace

log	"--- $ME started on $HOSTNAME ... $SYSTEM $SYSVERS"
trace 	"--- $ME started on $HOSTNAME ... $SYSTEM $SYSVERS"
trace   "Watcher: $PRODUCT, $REVISION $1"

load_filter Initial $COMPRESS_FILTER

logger	"$ME[$$]: Started listening on pipe $PIPE using dynamic filters"
log	"Started listening on pipe $PIPE using dynamic filters"
trace	"Started listening on pipe $PIPE using dynamic filters"

renice -10 $$
trace "Starting scanner ..."
while :
do

	read < $PIPE
	LOOP_START=`date +%s%3N`
	((loop++))
	: echo "-------- Loop: $loop ----------"
#	DYN_ADDR=`$DYN_CMD`	# self-lockout prevention not needed here

	if [ -z "$REPLY" ]
	then	: echo "Eeek! Empty line ..."
		continue
	fi

	STAGE="[prepro]"
	#------------ Internal rules ---------------------------
	#
	# Kick off superfluous stuff first of all
	#
	RULE=Superfluous
        #	result=`grep -oE "$SUPERFLUOUS" <<< $REPLY`
        #	if [ ! -z "$result" ]
		BASH_REMATCH=()
                [[ "$REPLY" =~ ($SUPERFLUOUS) ]] 
		if [ -n "${BASH_REMATCH[0]}" ]
        	then	: echo "Superfluous stuff ... ignored"
			continue
        	fi

#	Self-lockout prevention already accomplished by 
#	the companion process WatchMX -- not needed here

	#--------------------------------------------------------
	STAGE="[filter]"
	dump_loadrate
	trace	"[Loop: $loop] '$REPLY'"
	trace	"Debuging: Debug: '$_DEBUG', Trace: '$_TRACE'"

	FILT_START=`date +%s%3N`
	filter
	FILT_RESULT=$?
	FILT_END=`date +%s%3N`

	FILT_TIME=$(( FILT_END-FILT_START )) 
	LOOP_TIME=$(( FILT_END-LOOP_START )) 

	###################################################
	# Post processor ...
	###################################################
	STAGE="[postpro]"
	case $FILT_RESULT in 
		0)	trace "[postpro] $RULE ... fall through, $LOOP_TIME/$FILT_TIME ms"
		;;
		1)	trace "[postpro] Finished for rule [$RULE], $LOOP_TIME/$FILT_TIME ms"
			continue
		;;
		3)	trace "[postpro] $BANDIT white-listed ... nothing to do ..."
			continue

		#------ Return codes 240..255 for internal use only -------------------------
		;;
                241)    trace "[postpro] Debuging set '$Pattern'"
                        continue
                ;;
                242)    trace "[postpro] Missing ruleset '$RULESET'"
                        continue
		;;
		254)	trace "[postpro] Ignored ... [$RULE] '$Pattern'"
		        continue
		;;	
		255)	trace "[postpro] !!! Nasty things happend in [inject] by rule '$RULE' !!!"
		;;
		*)	trace "[postpro] Illegal return code ... fall through"
	;;
	esac

	#-------------[ Catch all before loop finishes ]-----------
	RULE=UNTREATED
		log	"[$RULE] '$REPLY'"
		trace	"[$RULE] '$REPLY'"
done

# DEAD-End ... Reaching here triggers a trap that restarts us 
rerun
