#!/usr/bin/perl -w
eval 'exec perl -S $0 "$@"'
    if 0;
#
# authsumm.pl - Produce summaries of authorization entries in logfiles
#	Copyright (C) 1998-2004 by James S. Seymour (jseymour@LinxNet.com)
#	(See "License", below.)  Release 0.0.12
#
#
# Usage:
#    authsumm [-q] [-d <today|yesterday>] [file1 [file2 [filen [...]]]]
#
# Options:
#    -d today means just today
#    -d yesterday means just "yesterday"
#
#    -q quiet - don't print headings for empty reports
#
#    If no file(s) specified, reads from stdin.
#
#    Unmatched entries go into a report that is automatically suppressed
#    if there's nothing in it.
#
# Typical usage:
#    Produce a report of previous day's activities:
#        authsumm.pl -d yesterday /var/log/messages /var/log/secure 
#    A report of prior week's activities (after logs rotated):
#        authsumm.pl /var/log/messages.1 /var/log/secure.1
#    What's happened so far today:
#        authsumm.pl -d today /var/log/messages /var/log/secure
#
# TBD:
#    date ranges, "lastweek", etc.?)
#
# License:
#    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 may have received a copy of the GNU General Public License
#    along with this program; if not, write to the Free Software
#    Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
#    
#    An on-line copy of the GNU General Public License can be found
#    http://www.fsf.org/copyleft/gpl.html.

use Getopt::Std;
use strict;

# Known DHCPD mac addresses
# (Would be nice to just snag these out of dhcpd.conf)
my %knownMacs = (
    'xx:xx:xx:xx:xx:xx' => '1',
);

my $hostName = `uname -n`; chomp($hostName);	# like this, perhaps?

my $usageMsg = "usage: authsumm [-q] [-d <today|yesterday>]";

use vars qw($opt_q $opt_d);

my (@suSuccess, @suFailure,
    @logins, @failedLogins,
    @anonFtpLogins, @ftpLogins, @failedFtpLogins, @failedAnonFtpLogins,
    @refusedFtpConns,
    @portSentryAdminMsgs, @portSentryAttackMsgs, @portSentryUnkMsgs,
    @fwMon,
    @sshFailedLogins, @sshGoodLogins, @sshRefusedConns, @sshMiscMsgs,
    %dhcpKnown, @dhcpUnknown,
    @unMatched
);

my $lastMsgIgnored = 0;

$opt_q = 0;
getopts('qd:') ||
    die "$usageMsg\n";

my $dateStr = get_datestr($opt_d) if(defined($opt_d));

while(<>) {
    chomp;
    next if(defined($dateStr) && ! /^$dateStr/);

    if(/^(.*) $hostName last message repeated \d+ times?/o && $lastMsgIgnored) {
	$lastMsgIgnored = 0;
	next;
    }
    $lastMsgIgnored = 0;

    if(/^(.*) $hostName .* \(su\) session opened for user ([^ ]+) by ([^(]+)/o) {
	push(@suSuccess, "$1 $3 -> $2");
    } elsif(/^(.*) $hostName su: 'su ([^']+)' succeeded for ([^ ]+) on /o) {
	push(@suSuccess, "$1 $3 -> $2");
    } elsif(/^(.*) $hostName .* auth.*failure; ([^(]+).* -> ([^ ]+) for su/o) {
	push(@suFailure, "$1 $2 -> $3");
    } elsif(/^(.*) $hostName su: 'su ([^']+)' failed for ([^ ]+) on /o) {
	push(@suFailure, "$1 $2 -> $3");
    } elsif(/^(.*) $hostName .* LOGIN ON ([^ ]+) BY ([^ ]+) FROM ([^ ]+)/o) {
	push(@logins, "$1 $3 ($2/$4)");
    } elsif(/^(.*) $hostName .* LOGIN ON ([^ ]+) BY ([^ ]+)/o) {
	push(@logins, "$1 $3 ($2)");
    } elsif(/^(.*) $hostName login: (ROOT) LOGIN ([^ ]+)/o) {
	push(@logins, "$1 $2 ($3)");
    } elsif(/^(.*) $hostName .* FAILED LOGIN [0-9] FROM ([^ ]+) FOR ([^,]+)/o) {
	push(@failedLogins, "$1 $3 ($2)");
    } elsif(/^(.*) $hostName .* ANONYMOUS FTP LOGIN FROM ([^,]+), ([^ ]+)/o) {
	push(@anonFtpLogins, "$1 $3 ($2)");
    } elsif(/^(.*) $hostName proftpd\[.* \(([^\)]+)\) - ANON (anonymous|ftp):/o) {
	push(@anonFtpLogins, "$1 $3 ($2)");
    } elsif(/^(.*) $hostName .* FTP LOGIN FROM ([^,]+), ([^ ]+)/o) {
	push(@ftpLogins, "$1 $3 ($2)");
    } elsif(/^(.*) $hostName proftpd\[.* \(([^\)]+)\) - USER ([^:]+): Login successful/o) {
	push(@ftpLogins, "$1 $3 ($2)");
    } elsif(/^(.*) $hostName ftpd.* failed login from ([^,]+), ([^ ]+)/o) {
	push(@failedFtpLogins, "$1 $3 ($2)");
    } elsif(/^(.*) $hostName proftpd\[.* \(([^\)]+)\) - USER ([^:]+) \(Login failed\):/o) {
	push(@failedFtpLogins, "$1 $3 ($2)");
    } elsif(/^(.*) $hostName proftpd\[.* \(([^\)]+)\) - no such user '([^']+)'/o) {
	push(@failedFtpLogins, "$1 $3 ($2)");
    } elsif(/^(.*) $hostName proftpd\[.* \(([^\)]+)\) - SECURITY VIOLATION: (\S+) login attempted\./o) {
	push(@failedFtpLogins, "$1 $3 ($2)");
    } elsif(/^(.*) $hostName ftpd.* FTP ACCESS REFUSED \(([^\)]+)\) from (.+)$/o) {
	push(@failedAnonFtpLogins, "$1 $2 ($3)");
    } elsif(/^(.*) $hostName proftpd\[\d+\]: refused connect from ((\d{1,3}\.){3}\d{1,3})$/o) {
	push(@refusedFtpConns, "$1 $2");
    } elsif(/^(.*) $hostName proftpd\[\d+\]: .+ \((.*?\[(:?[0-9]+\.){3}[0-9]+\])\) - Connection refused \((max clients \d+)\)\.$/o) {
	push(@refusedFtpConns, "$1 $2 - $4");
    } elsif(/^(.*) $hostName portsentry\[[0-9]+\]: (\w+): (.+)$/o) {
	unless($3 =~ /^Going into listen mode on /o) {
	    if($2 eq "adminalert" ) {
		unless($3 =~ /^PortSentry is now active and listening\./o) {
		    push(@portSentryAdminMsgs, "$1 $3");
		}
	    } elsif($2 eq "attackalert") {
		unless($3 =~ /Host [\d\.]+ has been blocked via /o ||
		       $3 =~ /Host: [\d\.]+ is already blocked\. Ignoring$/o ||
		       $3 =~ /External command run for host: [\d\.]+ /o) {
		    push(@portSentryAttackMsgs, "$1 $3");
		}
	    } else {
		push(@portSentryUnkMsgs, "$1 $2: $3");
	    }
	}
    } elsif(/^(.*) $hostName fwmon\[[0-9]+\]: (.+)$/o) {
	unless($2 =~ /^system ok, rechecking in /o) {
	    push(@fwMon, "$1 $2");
	}
    } elsif(/^(.*) $hostName sshd\[[0-9]+\]: (.+)$/o) {
	my $tTime = $1;
	my $tMsg = $2;
	if($tMsg =~ /Wrong response to RSA authentication challenge\./o ||
	   $tMsg =~ /Failed rsa for .+ from /o ||
	   $tMsg =~ /Failed password for .+ from .+ port .+ ssh2/o)
	{
	    $tMsg =~ s/ port .+$//o;
	    push(@sshFailedLogins, "$tTime $tMsg");
	} elsif($tMsg =~ /Accepted rsa for .+ from /o ||
		$tMsg =~ /Accepted publickey for .+ from .+ port .+ ssh2/o) {
	    $tMsg =~ s/ port .+$//o;
	    push(@sshGoodLogins, "$tTime $tMsg");
	} elsif($tMsg =~ s/refused connect from //o) {
	    push(@sshRefusedConns, "$tTime $tMsg");
	} elsif(
		$tMsg =~ /^Closing connection to .+$/o ||
		$tMsg =~ /^Connection closed by .+$/o ||
		$tMsg =~ /^Connection closed by remote host\.$/o ||
		$tMsg =~ /^Enabling compatibility mode for protocol 2\.0$/o ||
		$tMsg =~ /^Failed none for .+ from .+ port [0-9]+ ssh2$/o ||
		$tMsg =~ /^Generating new [0-9]+ bit RSA key\.$/o ||
		$tMsg =~ /^RSA key generation complete\.$/o ||
		$tMsg =~ /^WARNING: .+\/primes does not exist, using old prime$/o
	       ) {
	    next;
	} else {
	    push(@sshMiscMsgs, "$tTime $tMsg");
	}
    } elsif(/^(.*) $hostName dhcpd: (.+)$/o) {
	my $tTime = $1;
	(my $tMsg = $2) =~ s/^(DHCP|BOOT)//o;
	if($tMsg =~ /^(?:DISCOVER|REQUEST) from ((?:[a-f0-9]{2}:){5}[a-f0-9]{2}) via /o ||
	   $tMsg =~ /^(?:REQUEST|ACK|OFFER|NAK|RELEASE) (?:on|for|of) (?:[0-9]{1,3}\.){3}[0-9]{1,3} (?:from|to) ((?:[a-f0-9]{2}:){5}[a-f0-9]{2}) via /o ||
	   $tMsg =~ /^REPLY for (?:[0-9]{1,3}\.){3}[0-9]{1,3} to .+? \(((?:[a-f0-9]{2}:){5}[a-f0-9]{2})\) via /o)
	{
	    if($knownMacs{$1}) {
		++$dhcpKnown{$tMsg};
	    } else {
		push(@dhcpUnknown, "$tTime $tMsg");
	    }
	}else{
	    push(@dhcpUnknown, "$tTime $tMsg");
	}
    } elsif(
            /^.{15} $hostName ftpd\[[0-9]+\]: FTP session closed$/o ||
            /^.{15} $hostName identd\[[0-9]+\]: connect from /o ||
            /^.{15} $hostName in\.ftpd\[[0-9]+\]: connect from /o ||
            /^.{15} $hostName in\.tftpd\[[0-9]+\]: connect from 172\.16\.104\.80/o ||
            /^.{15} $hostName in\.telnetd\[[0-9]+\]: connect from /o ||
	    /^.{15} $hostName login: DIALUP AT ttyS0 BY /o ||
	    /^.{15} $hostName mgetty\[\d+\]: data dev=ttyS0, pid=\d+, caller='none', conn='[^']+', name='', cmd='\/bin\/login', user='[^']+'/o ||
	    /^.{15} $hostName named\[[0-9]+\]:\s+([XN]STATS|USAGE|Cleaned cache of|listening on|deleting interface|dangling CNAME|Lame server|bad referral) /o ||
	    /^.{15} $hostName named\[\d+\]: ns_(forw|resp): (query\(\S+\) All possible A RR's lame|TCP truncated: )/o ||
	    /^.{15} $hostName named\[\d+\]: .*(points to (a )?CNAME|A RR negative cache entry) /o ||
            /^.{15} $hostName PAM_pwdb\[[0-9]+\]: /o ||
	    /^.{15} $hostName postfix((\/\w+)?\[[0-9]+\]|-script):\s+/o ||
	    /^.{15} $hostName proftpd\[[0-9]+\]: .* - FTP (no transfer timeout|login timed out), disconnected\.\s*$/o ||
	    /^.{15} $hostName proftpd\[[0-9]+\]: .* - FTP session (opened|closed)\.\s*$/o ||
	    /^.{15} $hostName proftpd\[[0-9]+\]: .+? - PAM\(.+?\): Authentication fail(ure|ed)\.\s*$/o ||
	    /^.{15} $hostName proftpd\[[0-9]+\]: pam_authenticate: error Authentication fail(ure|ed)\s*$/o ||
	    /^.{15} $hostName proftpd\[[0-9]+\]: .+? - USER .+?: no such user found from /o ||
	    /^.{15} $hostName proftpd\[[0-9]+\]:\s+connect from ([0-9]+\.){3}[0-9]+\s*$/o ||
            /^.{15} $hostName rpc\.rstatd\[[0-9]+\]: connect from 127.0.0.1/o ||
	    /^.{15} $hostName su: pam_authenticate: error Authentication failed$/o ||
	    /^.{15} $hostName syslogd .+: restart\.$/o ||
            /^.{15} $hostName xntpd\[[0-9]+\]: /o
	   ) {
	++$lastMsgIgnored;
	next;
    } else {
	push(@unMatched, "$_");
    }
}

if(defined($dateStr)) {
    print "Auth summaries for $dateStr\n";
}



print_summary(\@suFailure, "Failed \"su\"s");
print_summary(\@suSuccess, "Successful \"su\"s");
print_summary(\@failedLogins, "Failed logins");
print_summary(\@logins, "Successful logins");
print_summary(\@sshFailedLogins, "SSHD Failed Logins");
print_summary(\@sshGoodLogins, "SSHD Successful Logins");
print_summary(\@sshRefusedConns, "SSHD Refused Connects");
print_summary(\@sshMiscMsgs, "SSHD Misc Messages");
print_summary(\@failedFtpLogins, "Failed ftp logins");
print_summary(\@ftpLogins, "Real ftp logins");
print_summary(\@anonFtpLogins, "Anonymous ftp logins");
print_summary(\@failedAnonFtpLogins, "Failed anonymous ftp logins");
print_summary(\@refusedFtpConns, "Refused ftp connections");
print_summary(\@portSentryAttackMsgs, "PortSentry Attack Detections");
print_summary(\@portSentryAdminMsgs, "PortSentry Admin Messages");
print_summary(\@portSentryUnkMsgs, "PortSentry Misc Messages");
print_summary(\@fwMon, "Firewall Monitor Messages");
print_hash_summary(\%dhcpKnown, "DHCP Known Activity");
print_summary(\@dhcpUnknown, "DHCP Unknown Activity");

print_summary(\@unMatched, "Unprocessed Log Entries") if(@unMatched);


# print array info
sub print_summary {
    my($arrName, $title) = @_;
    my $dottedLine;
    unless(@$arrName) {
	return if($opt_q);
	$dottedLine = ": none";
    } else {
	$dottedLine = "\n" . "-" x length($title);
    }
    print "\n$title$dottedLine\n";
    foreach (@$arrName) {
	print "    $_\n";
    }
}

sub print_hash_summary {
    my($hashRef, $title) = @_;
    my $dottedLine;
    unless(%$hashRef) {
	return if($opt_q);
	$dottedLine = ": none";
    } else {
	$dottedLine = "\n" . "-" x length($title);
    }
    print "\n$title$dottedLine\n";
    foreach (sort { $hashRef->{$b} <=> $hashRef->{$a} } keys %$hashRef) {
	printf("    %4d %s\n", $hashRef->{$_}, $_);
    }
}

# return a date string to match in log
sub get_datestr {
    my @monthNames = qw(Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec);
    my $aDay = 60 * 60 * 24;

    my $dateOpt = $_[0];

    my $time = time();
    if($dateOpt eq "yesterday") {
	$time -= $aDay;
    } elsif($dateOpt ne "today") {
	die "$usageMsg\n";
    }
    my ($t_mday, $t_mon) = (localtime($time))[3,4];

    return sprintf("%s %2d", $monthNames[$t_mon], $t_mday);
}
