# Process Windows security events logged to a server, using Snare Agent or
# similar.

########################################################
## Copyright (c) 2008-2014 Orion Poplawski
## Covered under the included MIT/X-Consortium License:
##    http://www.opensource.org/licenses/mit-license.php
## All modifications and contributions by other persons to
## this script are assumed to have been donated to the
## Logwatch project and thus assume the above copyright
## and licensing terms.  If you want to make contributions
## under your own copyright or a different license this
## must be explicitly stated in the contribution an the
## Logwatch project reserves the right to not accept such
## contributions.  If you have made significant
## contributions to this script and want to claim
## copyright please contact logwatch-devel@lists.sourceforge.net.
#########################################################

use strict;
use URI::URL;

my $Detail = $ENV{'LOGWATCH_DETAIL_LEVEL'} || 0;
my $Ignore_messages = $ENV{'ignore_messages'} || '^$';

my $SuccessAudits = 0;
my %SuccessAuditUsers;
my %FailureAudits;
my %SuccessAudits;
my %ClockSkew;
my %Errors;
my %Information;
my %UnknownUser;
my %UnknownClient;
my %BadPasswords;
my %TicketExpired;
my %AccessDenied;
my %AccountChanged;
my %AccountCreated;
my %AccountDeleted;
my %AccountDisabled;
my %AccountEnabled;
my %AccountLocked;
my %AuditPolicyChanged;
my %ExpiredPassword;
my %PasswordChanged;
my %Logon;
my %PrivilegedLogon;
my %Logoff;
my %WorkstationLocked;
my %WorkstationUnlocked;
my %OtherList;

while (defined(my $ThisLine = <STDIN>)) {
   # User specified ignore messages, lower cased
   next if $ThisLine =~ /$Ignore_messages/i;

   my ($Hostname,$Criticality,$SourceName,$DateTime,$EventID,$SourceName2,$UserName,$SIDType,$EventLogType,$CategoryString,$DataString,$ExpandedString,$Extra);
   #Determine format
   if ($ThisLine =~ /MSWinEventLog\[/) {  # Snare 4
      #Parse
      ($Criticality,$SourceName,$DateTime,$EventID,$SourceName2,$UserName,$SIDType,$EventLogType,$Hostname,$CategoryString,$DataString,$ExpandedString,$Extra) =
         ($ThisLine =~ /MSWinEventLog\[(\d+)\]:(\w+)\t\d+\t([^\t]+)\t(\d+)\t([^\t]+)\t([^\t]+)\t([^\t]+)\t([^\t]+)\t([^\t]+)\t?([^\t]*)\t?([^\t]*)\t?([^\t]*)\t?([^\t]*)/);
   } elsif ($ThisLine =~ /MSWinEventLog\t/) { # Snare 3
      #Parse
      ($Criticality,$SourceName,$DateTime,$EventID,$SourceName2,$UserName,$SIDType,$EventLogType,$Hostname,$CategoryString,$DataString,$ExpandedString,$Extra) =
         ($ThisLine =~ /MSWinEventLog\t(\d+)\t(\w+)\t\d+\t([^\t]+)\t(\d+)\t([^\t]+)\t([^\t]+)\t([^\t]+)\t([^\t]+)\t([^\t]+)\t?([^\t]*)\t?([^\t]*)\t?([^\t]*)\t?([^\t]*)/);
   }
   if (!defined($Hostname)) {
      print STDERR "Cannot parse $ThisLine";
      next;
   }

   # Modify some items that prevent de-duplication
   if ($Detail < 10) {
      $ExpandedString =~ s/Filter Run-Time ID: \d+/Filter Run-Time ID: XXX/;
      $ExpandedString =~ s/(Key Name:)\s+\{[0-9A-F\-]+\}/$1 {XXX}/g;
      $ExpandedString =~ s/Logon ID:\s+0x[0-9A-F]+/Logon ID:  0xXXX/;
   }
   if ($Detail < 5) {
      # Reduce ephemeral ports
      $ExpandedString =~ s/(Client|Destination|Source) Port:(\s+)[3-6]\d{4}/$1 Port:${2}XXXXX/g;
      $ExpandedString =~ s/Relative Target Name: ([^\t]+)/Relative Target Name: XXX/;
   }

   my $url = URI::URL->new("https://www.ultimatewindowssecurity.com/securitylog/encyclopedia/event.aspx?eventID=$EventID");
   if ($EventID == 4673
       or $EventID == 4674) {
      # An operation was attempted on a privileged object.
      # These are basically noise
      # https://www.ultimatewindowssecurity.com/securitylog/encyclopedia/event.aspx?eventID=4673
      # https://www.ultimatewindowssecurity.com/securitylog/encyclopedia/event.aspx?eventID=4674
      # Ignore
   } elsif ($EventLogType eq "Success Audit") {
      if ($EventID == 4608    # Windows is starting up. (startups logged by evtsystem)
          or $EventID == 4688 # A new process has been created.
          or $EventID == 4689 # A process has exited.
         ) {
         # Ignore
      } elsif ($EventID == 4624 or $EventID == 4648) {
         $Logon{"$Hostname $UserName"}++ if $Detail >= 5;
      } elsif ($EventID == 4634 or $EventID == 4647) {
         $Logoff{"$Hostname $UserName"}++ if $Detail >= 5;
      } elsif ($EventID == 4672) {
         $PrivilegedLogon{"$Hostname $UserName"}++ if $Detail > 0;
      } elsif ($EventID == 4719) {
         $AuditPolicyChanged{$Hostname}++;
      } elsif ($EventID == 4720) {
         $AccountCreated{$UserName}++;
      } elsif ($EventID == 4722) {
         $AccountEnabled{$UserName}++;
      } elsif ($EventID == 4723) {
         $PasswordChanged{$UserName}++;
      } elsif ($EventID == 4725) {
         $AccountDisabled{$UserName}++;
      } elsif ($EventID == 4726) {
         $AccountDeleted{$UserName}++;
      } elsif ($EventID == 4738 or $EventID == 4742) {
         $AccountChanged{$UserName}++;
      } elsif ($EventID == 4800) {
         $WorkstationLocked{"$Hostname $UserName"}++ if $Detail >= 10;
      } elsif ($EventID == 4801) {
         $WorkstationUnlocked{"$Hostname $UserName"}++ if $Detail >= 10;
      } else {
         $SuccessAudits++;
         $SuccessAuditUsers{$UserName}++;
         $SuccessAudits{"$Hostname $ExpandedString\n$url"}++ if $Detail >= 10;
      }
   }
   elsif ($EventLogType eq "Failure Audit") {
      if ($EventID == 4625) {
         # An account failed to log on
         if (my ($account,$domain,$reason) = ($ExpandedString =~ /Account For Which Logon Failed:.*Account Name:\s+(\S+)\s+Account Domain:\s+(\S+).*Failure Reason:\s+(.+)\s+Status:.*Sub Status:/)) {
            $FailureAudits{"$Hostname Log On Failure for $domain\\$account: $reason"}++;
         } elsif (my ($account,$domain,$reason,$process) = ($ExpandedString =~ /Account Name:\s+(\S+)\s+Account Domain:\s+(\S+).*Failure Reason:\s+(.+)\s+Status:.*Sub Status:.*Caller Process Name:\s+(.*)\s+Network Informaion:/)) {
            $FailureAudits{"$Hostname Log On Failure for $domain\\$account by $process: $reason"}++;
         }
      } elsif (my ($account,$domain,$process) = ($ExpandedString =~ /^A privileged service was called\..*Account Name:\s+(\S+)\s+Account Domain:\s+(\S+).*Process Name:\s+(.+)\sService/)) {
         $FailureAudits{"$Hostname Privileged service called for $domain\\$account: $process"}++ if $Detail;
      } elsif ($EventID == 4768) {
         # A Kerberos authentication ticket (TGT) was requested
         my ($Account,$Realm,$Client,$FailureCode) = $ExpandedString =~ /Account Name:\s+(\S*)\s.*Supplied Realm Name:\s+(\S*)\s.*Client Address:\s+(\S+)\s.*Result Code:\s+(\w+)/;
         if ($FailureCode eq "0x6") {
            # Client not found in Kerberos database
            $UnknownClient{"$Account\\$Realm $Client"}++;
         } elsif ($FailureCode eq "0x12") {
            $AccountDisabled{"$Account\@$Realm $Client"}++;
         } elsif ($FailureCode eq "0x17") {
            #   Password has expired
            $ExpiredPassword{"$UserName"}++;
         } else {
            $FailureAudits{"$Hostname $ExpandedString\n$url"}++;
         }
      } elsif ($EventID == 4769) {
         # A Kerberos service ticket was requested
         my ($Client,$FailureCode) = $ExpandedString =~ /Client Address:\s+(\S+)\s.*Failure Code:\s+(\w+)/;
#print STDER "EventID=$EventID Client=$Client FailureCode=$FailureCode ExpandedString=$ExpandedString\n";
         if ($FailureCode eq "0x12") {
            $AccountDisabled{"$Client"}++;
         } elsif ($FailureCode eq "0x1B") {
            # KDC_ERR_MUST_USE_USER2USER Server principal valid for user-to-user only
            # This is an informational response and not an issue
         } elsif ($FailureCode eq "0x20") {
            # Ticket expired
            $TicketExpired{$Client}++;
         } elsif ($FailureCode eq "0x25") {
            # Clock skew too great
            $ClockSkew{$Client}++;
         } else {
            $FailureAudits{"$Hostname $ExpandedString\n$url"}++;
         }
      } elsif ($EventID == 4771) {
         # Kerberos pre-authentication failed
         my ($Account,$Client,$FailureCode) = $ExpandedString =~ /Account Name:\s+(\S+)\s.*Client Address:\s+(\S+)\s.*Failure Code:\s+(\w+)/;
         if ($FailureCode eq "0x12") {
            #Clients credentials have been revoked      Account disabled, expired, locked out, logon hours.
            $AccountLocked{"$Account $Client"}++;
         } elsif ($FailureCode eq "0x18") {
            #Pre-authentication information was invalid - bad password
            $BadPasswords{"$Account $Client"}++;
         } elsif ($FailureCode eq "0x25") {
            # Clock skew too great
            $ClockSkew{$Client}++;
         } else {
            $FailureAudits{"$Hostname $ExpandedString\n$url"}++;
         }
      } elsif ($EventID == 4776) {
         # The domain controller attempted to validate the credentials for an account
         my ($Account,$Client,$FailureCode) = $ExpandedString =~ /Logon Account:\s+(\S+)\s+Source Workstation:\s+(\S*)\s.*Error Code:\s+(\w+)/;
         if (lc($FailureCode) eq "0xc0000064") {
            # user name does not exist
            $UnknownUser{"$Account $Client"}++;
         } elsif (lc($FailureCode) eq "0xc000006a") {
            # user name is correct but the password is wrong
            $BadPasswords{"$Account $Client"}++;
         } elsif (lc($FailureCode) eq "0xc0000071") {
            # expired password
            $ExpiredPassword{"$Account $Client"}++;
         } elsif (lc($FailureCode) eq "0xc0000234") {
            # account locked
            $AccountLocked{"$UserName $Client"}++;
         } else {
            $FailureAudits{"$Hostname $ExpandedString\n$url"}++;
         }
      } elsif ($EventID == 4957 and $ExpandedString =~ /resolved to an empty set/) {
         # Windows Firewall did not apply the following rule - because it was not applicable
      } elsif ($EventID == 6273) {
         my ($account,$domain,$client) = ($ExpandedString =~ /Account Name:\s+(\S+)\s+Account Domain:\s+(\S+).*Client Friendly Name:\s+(\S+)/);
         $AccessDenied{"$account\\$domain $client"}++;
      } else {
         $FailureAudits{"$Hostname $ExpandedString\n$url"}++;
      }
   }
   elsif ($EventLogType eq "Error") {
      $ExpandedString =~ s/\s+\d+\s+\d+//;
      $Errors{"$Hostname $ExpandedString\n$url"}++;
   }
   elsif ($EventLogType eq "Information") {
      next if $ExpandedString =~ /The event logging service has shut down/;
      next if $Detail < 5;
      $Information{"$Hostname $ExpandedString\n$url"}++;
   }
   else {
      # Report any unmatched entries...
      chomp($ThisLine);
      $OtherList{"Type=$EventLogType $ThisLine"}++;
   }
}

if (keys %Errors) {
   print "\nERRORS:\n";
   foreach my $Error (sort keys %Errors) {
      print "    $Error : $Errors{$Error} Times\n";
   }
}

if (keys %ClockSkew) {
   print "\nClock skew too great\n";
   foreach my $Client (sort keys %ClockSkew) {
      print "    $Client : $ClockSkew{$Client} Times\n";
   }
}

if (keys %AccountCreated) {
   print "\nAccount Created\n";
   foreach my $Account (sort keys %AccountCreated) {
      print "    $Account : $AccountCreated{$Account} Times\n";
   }
}

if (keys %AccountDeleted) {
   print "\nAccount Deleted\n";
   foreach my $Account (sort keys %AccountDeleted) {
      print "    $Account : $AccountDeleted{$Account} Times\n";
   }
}

if (keys %AccountDisabled) {
   print "\nAccount Disabled\n";
   foreach my $Account (sort keys %AccountDisabled) {
      print "    $Account : $AccountDisabled{$Account} Times\n";
   }
}

if (keys %AccountEnabled) {
   print "\nAccount Enabled\n";
   foreach my $Account (sort keys %AccountEnabled) {
      print "    $Account : $AccountEnabled{$Account} Times\n";
   }
}

if (keys %AccountChanged) {
   print "\nAccount Changed\n";
   foreach my $Account (sort keys %AccountChanged) {
      print "    $Account : $AccountChanged{$Account} Times\n";
   }
}

if (keys %PasswordChanged) {
   print "\nPassword Changed\n";
   foreach my $Account (sort keys %PasswordChanged) {
      print "    $Account : $PasswordChanged{$Account} Times\n";
   }
}

if (keys %AccountLocked) {
   print "\nAccount Locked\n";
   foreach my $Account (sort keys %AccountLocked) {
      print "    $Account : $AccountLocked{$Account} Times\n";
   }
}

if (keys %ExpiredPassword) {
   print "\nPassword Expired\n";
   foreach my $Account (sort keys %ExpiredPassword) {
      print "    $Account : $ExpiredPassword{$Account} Times\n";
   }
}

if (keys %AccessDenied) {
   print "\nAccess Denied\n";
   foreach my $Item (sort keys %AccessDenied) {
      print "    $Item : $AccessDenied{$Item} Times\n";
   }
}

if (keys %UnknownUser) {
   print "\nUnknown Users\n";
   foreach my $Account (sort keys %UnknownUser) {
      print "    $Account : $UnknownUser{$Account} Times\n";
   }
}

if (keys %UnknownClient) {
   print "\nUnknown Clients\n";
   foreach my $Account (sort keys %UnknownClient) {
      print "    $Account : $UnknownClient{$Account} Times\n";
   }
}

if (keys %BadPasswords) {
   print "\nBad Passwords\n";
   foreach my $Account (sort keys %BadPasswords) {
      print "    $Account : $BadPasswords{$Account} Times\n";
   }
}

if (keys %TicketExpired) {
   print "\nTicket Expired\n";
   foreach my $Client (sort keys %TicketExpired) {
      print "    $Client : $TicketExpired{$Client} Times\n";
   }
}

if (keys %FailureAudits) {
   print "\nFailure Audits\n";
   foreach my $Error (sort keys %FailureAudits) {
      print "    $Error : $FailureAudits{$Error} Times\n";
   }
}

if (keys %AuditPolicyChanged) {
   print "\nAudit Policy Changed\n";
   foreach my $Hostname (sort keys %AuditPolicyChanged) {
      print "    $Hostname : $AuditPolicyChanged{$Hostname} Times\n";
   }
}

# Detail > 0
if (keys %PrivilegedLogon) {
   print "\nPrivileged Logons\n";
   foreach my $User (sort keys %PrivilegedLogon) {
      print "    $User : $PrivilegedLogon{$User} Times\n";
   }
}

# Detail >= 5
if (keys %Logon) {
   print "\nLogons\n";
   foreach my $User (sort keys %Logon) {
      print "    $User : $Logon{$User} Times\n";
   }
}

# Detail >= 5
if (keys %Logoff) {
   print "\nLogoffs\n";
   foreach my $User (sort keys %Logoff) {
      print "    $User : $Logoff{$User} Times\n";
   }
}

# Detail >= 10
if (keys %WorkstationLocked) {
   print "\nWorkstation Locked\n";
   foreach my $User (sort keys %WorkstationLocked) {
      print "    $User : $WorkstationLocked{$User} Times\n";
   }
}

# Detail >= 10
if (keys %WorkstationUnlocked) {
   print "\nWorkstation Unlocked\n";
   foreach my $User (sort keys %WorkstationUnlocked) {
      print "    $User : $WorkstationUnlocked{$User} Times\n";
   }
}

# Detail >= 5
if ($SuccessAudits and ($Detail >= 5) ) {
   print "\nSuccess Audits " . $SuccessAudits . " Time(s)\n";
   foreach my $User (keys %SuccessAuditUsers) {
      print "    $User : $SuccessAuditUsers{$User} Times\n";
   }
   if ($Detail >= 10) {
      print "\nSuccess Audits\n";
      foreach my $Error (sort keys %SuccessAudits) {
         print "    $Error : $SuccessAudits{$Error} Times\n";
      }
   }
}

if (keys %Information) {
   print "\nInformational Messages:\n";
   foreach my $Item (sort keys %Information) {
      print "    $Item : $Information{$Item} Times\n";
   }
}

if (keys %OtherList) {
   print "\n**** Unmatched entries ****\n";
   foreach my $Error (keys %OtherList) {
      print "    $Error : $OtherList{$Error} Times\n";
   }
}

exit(0);

# vi: shiftwidth=3 tabstop=3 syntax=perl et
# Local Variables:
# mode: perl
# perl-indent-level: 3
# indent-tabs-mode: nil
# End:
