#!/usr/bin/perl
#
# Snare Audit Dispatcher for Linux
# (c) Copyright 2006 InterSect Alliance Pty Ltd
#
# This application will integrate into the native linux audit subsystem,
# and translate linux auditd log format data into something that can be
# parsed appropriately by apps that prefer one-line-per-event, such as the
# Snare audit daemon (for delivery to the Snare Server), or logwatch.
#
#
# INSTALLATION:
#
#	* Save this file to /usr/local/bin/SnareDispatcher.pl
#	* Add the following line to your /etc/auditd.conf file:
#		dispatcher = /usr/local/bin/SnareDispatcher.pl
#
#
# Although the items=x field should provide us with a reasonable guestimate
# of how many lines to expect for a particular syscall event, items=x isn't available
# for all events. As such, as well as a items=x test, the code uses a timeout to determine
# when to send an event, as we can't easily establish exactly how many
# event-lines might potentially contribute to a single event without items=x (even then,
# we are assuming that the PATH line will be the last to be sent). Yuck.
#
# How the hell am I going to convert eventid=11 to eventid=execve without
# hardcoding, or writing my own C-header-preprocessor (c2ph wont help)? Gack.
#

use POSIX qw(strftime);
use Net::Domain qw(hostname hostfqdn hostdomain);

$DEBUG=1;
if($DEBUG) {
	open(DEBUG,">>/var/log/SnareDebug.log");
}

# How often should we flush our buffer?
$FLUSHINTERVAL=10;	# in seconds
$FLUSH=0;
$FLUSHNOW=-1;

# What system am I running on?
# Try and grab the full name
my $hostname = hostfqdn();

# Declare a UID/GID cache so we don't wind up getting into recursive
# audit event generation.
my %uidcache;
my %gidcache;

%event=();

$CONTINUE=1;

$rin = '';
vec($rin,fileno(STDIN),1) = 1;

$eventsent=0;
$lasteventnum=0;

$timecheck=time();

sub Handler {
	local($sig)=@_;
	if($DEBUG) { DebugMsg("SIG$sig Signal Received"); }
	$CONTINUE=0;
}

$SIG{PIPE}='Handler';
$SIG{HUP}='Handler';
$SIG{INT}='Handler';
$SIG{QUIT}='Handler';
$SIG{CHLD}='Handler';
$SIG{TERM}='Handler';

while($CONTINUE==1) {
	$now=time();
	# Every 10 seconds
	if($now-$timecheck > $FLUSHINTERVAL) {
		# Flush any old data
		SendEvents($FLUSH);
		$timecheck=$now;
	}

	$nfound=select($rout=$rin,undef,undef,1);
	if($nfound) {
		if($DEBUG) { DebugMsg("Found some data! Grab it.."); }
		# Grab the version number
		sysread(STDIN,$tversion,4);
		sysread(STDIN,$theadersize,4);
		($version)=unpack("I",$tversion);
		($headersize)=unpack("I",$theadersize);

		# Eventtype 1302 = PATH
		# Eventtype 1300 = SYSCALL
		# Eventtype 1307 = CWD
		# 8k sanity check. Hopefully, we'll resynchronise.
		if($headersize > 0 && $headersize < 8192) {
			sysread(STDIN,$temp,$headersize-8);
			($eventtype,$contentsize)=unpack("II",$temp);
			if($contentsize > 0) {
				sysread(STDIN,$line,$contentsize);
			} else {
				next;
			}
		} else {
			next;
		}
	} else {
		if($DEBUG) { DebugMsg("NO DATA (timout)!"); }
		next;
	}

	chomp($line);
	# Kill off internal newlines.
	$line=~s/\n//g;

	if($DEBUG) { DebugMsg("\nLINE: $line"); }

	$eventnum=0;
	# Pull out the date/time and eventID
	($null,$head,$datetime,$eventnum,$tail)=split(/^([a-zA-Z0-9]+)\(([0-9\.]+):([0-9]+)\): (.*)/,$line);

	if($DEBUG) { DebugMsg("Head: $head datetime: $datetime eventnum: $eventnum tail: $tail"); }

	if($head ne "audit" || $datetime==0 || !$tail || !$eventnum) {
		# Not interested
		next;
	}
	$line=$tail;

	if($DEBUG) { DebugMsg("DATE TIME = $datetime EVENTID = $eventnum eventtype = $eventtype"); }


	# Lets split this line up into element/content pairs.
	# First, break apart by spaces that aren't inside inverted commas.
	@elements=split(/\s(?=(?:[^"]*"[^"]*")*[^"]*\z)/,$line);  # comment helper """
	%data=();
	if($DEBUG) { DebugMsg("Elements contains " . @elements . " elements"); }
	foreach $element (@elements) {
		if($element=~/=/) {
			# Then, by the equals sign.
			($key,$val)=split(/=/,$element,2);
			$val =~ s/^"//;
			$val =~ s/"$//;
			$data{$key}=$val;
		}
	}

	# Ok, we now have an array of key/value pairs.

	$eventsent=0;

	if(!$event{$eventnum}{"datetime"}) {
		@ltime=localtime($datetime);
		$sdatetime=strftime("%Y%m%d %T",@ltime);
		$event{$eventnum}{"datetime"}=$sdatetime;
	}
	# Unix time..
	$event{$eventnum}{"utime"}=$datetime;

	foreach $key (keys %data) {
		$count=0;
		while(defined($event{$eventnum}{$key . ($count==0?"":":$count")})) {
			$count++;
		}
		$event{$eventnum}{$key . ($count==0?"":":$count")}=$data{$key};

		if($key eq "uid" || $key eq "euid" || $key eq "ouid") {
			$event{$eventnum}{$key . "name" . ($count==0?"":":$count")}=getuid($data{$key});
		} elsif($key eq "gid" || $key eq "egid" || $key eq "ogid") {
			$event{$eventnum}{$key . "name" . ($count==0?"":":$count")}=getgid($data{$key});
		}
	}

	# Eventtype 1302 = "PATH"
	# This should tell us when the end of a record comes through.
	if($eventtype == 1302) {
		$items=$event{$eventnum}{"items"};
		if($items > 0) {
			$pathcount=$event{$eventnum}{"pathcount"};
			$pathcount++;
			if($pathcount == $items) {
				# We have all the data we need! Send this event out the door.
				# Clear our counter first.
				delete $event{$eventnum}{"pathcount"};
				if($DEBUG) { DebugMsg("Sending out Event $eventnum - we've had $pathcount items ($items)!"); }
				SendEvents($eventnum);
			} else {
				if($DEBUG) { DebugMsg("Found another PATH component for event $eventnum."); }
				$event{$eventnum}{"pathcount"}++;
			}
		}
	}
}

# Flush our buffers
SendEvents($FLUSHNOW);

if($DEBUG) {
	close(DEBUG);
}
exit;

sub SendEvents {
	($eventnumber)=@_;

	$now=time();
	# If we are sent a particular event number, flush it out.
	if($eventnumber != $FLUSH && $eventnumber != $FLUSHNOW) {
		@sendevents=($eventnumber);
	} else {
		# if eventnumber is zero, flush any events that are older than 10 seconds.
		# otherwise, if eventnumber is <0, flush all events we have. We're going down.
		foreach $eventnum (sort keys %event) {
			$utime=$event{$eventnum}{"utime"};
			if(($now-$utime < $FLUSHINTERVAL) && $eventnumber==$FLUSH) {
				# Since we're sorted, we can break out as soon as we find a event in the last 5 secs.
				last;
			}
			delete $event{$eventnum}{"utime"};
			push(@sendevents,$eventnum);
		}
	}

	# Ok, we now have an array of events to send out.
	foreach $eventnum (@sendevents) {
		if($DEBUG) { DebugMsg("DEBUG: Eventnum is $eventnum"); }

		# If the event doesn't satisfy the basic requirements, dump it
		if(!defined($event{$eventnum}{"syscall"}) ||
		   !defined($event{$eventnum}{"datetime"}) ||
		   !defined($event{$eventnum}{"uid"}) ||
		   !defined($event{$eventnum}{"gid"}) ||
		   !defined($event{$eventnum}{"pid"}) ||
		   !defined($event{$eventnum}{"comm"}) ||
		   !defined($event{$eventnum}{"exit"}) ||
		   !defined($event{$eventnum}{"success"})) {
			if($DEBUG) {
				DebugMsg(":::::::: Event $eventnum does not contain all required data");
				@elements=keys %{$event{$eventnum}};
				@values=values %{$event{$eventnum}};
				DebugMsg(":::::::: [@elements] [@values]");
			}

			delete $event{$eventnum};

			next;
		}

		if($event{$eventnum}{"name"}) {
			$tpath=$event{$eventnum}{"name"};
		} elsif($event{$eventnum}{"exe"}) {
			$tpath=$event{$eventnum}{"exe"};
		}

		if($tpath !~ /^\//) {
			$path=$event{$eventnum}{"cwd"} . "/" . $tpath;
		} else {
			$path=$tpath;
		}

		# Note: We don't want to use realpath here, since it will potentially
		# cause file-open events, and we may end up in an endless loop.
		$path=resolve_path($path);

		$eventstring = "$hostname	LinuxKAudit	event," .
			$event{$eventnum}{"syscall"} .  "," . $event{$eventnum}{"datetime"} .
			"	" . "user," .
			$event{$eventnum}{"uid"} .  "(" . $event{$eventnum}{"uidname"} . "),"  . 
			$event{$eventnum}{"gid"} .  "(" . $event{$eventnum}{"gidname"} . "),"  . 
			$event{$eventnum}{"euid"} .  "(" . $event{$eventnum}{"euidname"} . "),"  . 
			$event{$eventnum}{"egid"} .  "(" . $event{$eventnum}{"egidname"} . ")	"  . 
			"process," . $event{$eventnum}{"pid"} . "," . $event{$eventnum}{"comm"} . "	" .
			"path,$path	" .
			"return," . $event{$eventnum}{"exit"} . "," . $event{$eventnum}{"success"};

		delete $event{$eventnum}{"syscall"};
		delete $event{$eventnum}{"datetime"};
		delete $event{$eventnum}{"uid"};
		delete $event{$eventnum}{"uidname"};
		delete $event{$eventnum}{"gid"};
		delete $event{$eventnum}{"gidname"};
		delete $event{$eventnum}{"euid"};
		delete $event{$eventnum}{"euidname"};
		delete $event{$eventnum}{"egid"};
		delete $event{$eventnum}{"egidname"};
		delete $event{$eventnum}{"pid"};
		delete $event{$eventnum}{"exit"};
		delete $event{$eventnum}{"success"};
		delete $event{$eventnum}{"comm"};

		foreach $key (sort keys %{$event{$eventnum}} ) {
			$eventstring .= "	$key," . $event{$eventnum}{$key};
			if($DEBUG) { DebugMsg("  KEY: $key VAL: " . $event{$eventnum}{$key}); }
		}
		delete $event{$eventnum};
		# Send this out. For the moment, throw into /var/log/
		DebugMsg($eventstring);
	}
}

sub resolve_path {
	my($path)=@_;
	my(@pathparts)=split(/\//,$path);
	my(@newpath)=();

	foreach $part (@pathparts) {
		if($part eq "..") {
			if(@newpath) {
				pop(@newpath);
			}
		} elsif($part eq ".") {
			# Do nothing
		} elsif($part eq "") {
			# Do nothing
		} else {
			push(@newpath,$part);
		}
	}
	$resolvedpath="";
	if($path =~ /^\//) {
		$resolvedpath = "/";
	}
	$resolvedpath .= join("/",@newpath);

	return($resolvedpath);
}


sub getuid {
	my($id)=@_;

	if($uidcache{$id}) {
		return($uidcache{$id});
	} else {
		$name=getpwuid($id);
		if($name) {
			$uidcache{$id}=$name;
			return($name);
		}
		return("Unknown");
	}
}

sub getgid {
	my($id)=@_;
	if($gidcache{$id}) {
		return($gidcache{$id});
	} else {
		$name=getgrgid($id);
		if($name) {
			$gidcache{$id}=$name;
			return($name);
		}
		return("Unknown");
	}
}

sub DebugMsg {
	my($string)=@_;
	print DEBUG "$string\n";

	$old_fh=select(DEBUG);
	$|=1;
	select($old_fh);
}
