#!/usr/local/bin/perl

# Copyright (C) 2006-2014 Ganael LAPLANCHE - Institut Pasteur

# 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 should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.

# Latest version on http://contribs.martymac.org

use strict ;
use warnings ;

use Encode qw(decode encode) ;
use Getopt::Std ;
use IO::File ;

my $VERSION = 'evtViewer v0.6' ;
$Getopt::Std::STANDARD_HELP_VERSION = 1 ;

### MS stuff - ID declarations ###
my %eventIdCodes = (
    '512' => 'System Restart',
    '513' => 'System Shutdown',
    '514' => 'Authentication Package Load',
    '515' => 'Logon Process Registered',
    '516' => 'Some Audit Event Records Discarded',
    '517' => 'Audit Log Cleared',
    '518' => 'Notification package loaded by the Security Account Manager',
    '519' => 'A process is using an invalid local procedure call (LPC) port',
    '520' => 'The system time was changed',
    '528' => 'Successful Logon',
    '529' => 'Unknown Username or Bad Password',
    '530' => 'Account logon time restriction',
    '531' => 'Account Currently Disabled',
    '532' => 'User account has expired',
    '533' => "User can't log on to this computer",
    '534' => 'Logon Type Restricted',
    '535' => 'Password Expired',
    '536' => 'The NetLogon component is not active',
    '537' => 'Unexpected error',
    '538' => 'User Logoff',
    '539' => 'Account locked out',
    '540' => 'Successful network logon',
    '541' => 'IPSec security association established',
    '542' => 'IPSec security association ended. Mode: Data Protection (Quick mode)',
    '543' => 'IPSec security association ended. Mode: Key Exchange (Main mode)',
    '544' => 'IPSec security association establishment failed because peer could not authenticate',
    '545' => 'IPSec peer authentication failed',
    '546' => 'IPSec security association establishment failed because peer sent invalid proposal',
    '547' => 'IPSec security association negotiation failed',
    '551' => 'User initiated logoff',
    '552' => 'Logon attempt using explicit credentials',
    '560' => 'Object Open',
    '561' => 'Handle Allocated',
    '562' => 'Handle Closed',
    '563' => 'Object open for deletion',
    '564' => 'Object Deleted',
    '565' => 'Object Open (Active Directory)',
    '566' => 'Object Operation (W3 Active Directory)',
    '567' => 'Object Access Attempt',
    '576' => 'Special Privilege Assigned',
    '577' => 'Privileged Service Called',
    '578' => 'Priviledge Object Operation',
    '592' => 'New Process Has Been Created',
    '593' => 'Process Has Exited',
    '594' => 'Handle Duplicated',
    '595' => 'Indirect Access to Object',
    '600' => 'A process was assigned a primary token',
    '601' => 'Attempt to install service',
    '602' => 'Scheduled Task created',
    '608' => 'User Right Assigned',
    '609' => 'User Right Removed',
    '610' => 'New Trusted Domain',
    '611' => 'Removing Trusted Domain',
    '612' => 'Audit Policy Change',
    '613' => 'IPSec policy agent started',
    '614' => 'IPSec policy agent disabled',
    '615' => 'IPSEC Policy Changed',
    '616' => 'IPSec policy agent agent encountered a potentially serious failure',
    '617' => 'Kerberos Policy Changed',
    '618' => 'Encrypted Data Recovery Policy Changed',
    '619' => 'Quality of Service Policy Changed',
    '620' => 'Trusted Domain Information Modified',
    '621' => 'System Security Access Granted',
    '622' => 'System Security Access Removed',
    '623' => 'Per User Audit Policy was refreshed',
    '624' => 'User Account Created',
    '625' => 'User Account Type Change',
    '626' => 'User Account Enabled',
    '627' => 'Change Password Attempt',
    '628' => 'User Account password set',
    '629' => 'User Account Disabled',
    '630' => 'User Account Deleted',
    '631' => 'Global Group Created',
    '632' => 'Global Group Member Added',
    '633' => 'Global Group Member Removed',
    '634' => 'Global Group Deleted',
    '635' => 'Local Group Created',
    '636' => 'Local Group Member Added',
    '637' => 'Local Group Member Removed',
    '638' => 'Local Group Deleted',
    '639' => 'Local Group Changed',
    '640' => 'General Account Database Change',
    '641' => 'Global Group Changed',
    '642' => 'User Account Changed',
    '643' => 'Domain Policy Changed',
    '644' => 'User Account Locked Out',
    '645' => 'Computer Account Created',
    '646' => 'Computer Account Changed',
    '647' => 'Computer Account Deleted',
    '648' => 'Security Disabled Local Group Created',
    '649' => 'Security Disabled Local Group Changed',
    '650' => 'Security Disabled Local Group Member Added',
    '651' => 'Security Disabled Local Group Member Removed',
    '652' => 'Security Disabled Local Group Deleted',
    '653' => 'Security Disabled Global Group Created',
    '654' => 'Security Disabled Global Group Changed',
    '655' => 'Security Disabled Global Group Member Added',
    '656' => 'Security Disabled Global Group Member Removed',
    '657' => 'Security Disabled Global Group Deleted',
    '658' => 'Security Enabled Universal Group Created',
    '659' => 'Security Enabled Universal Group Changed',
    '660' => 'Security Enabled Universal Group Member Added',
    '661' => 'Security Enabled Universal Group Member Removed',
    '662' => 'Security Enabled Universal Group Deleted',
    '663' => 'Security Disabled Universal Group Created',
    '664' => 'Security Disabled Universal Group Changed',
    '665' => 'Security Disabled Universal Group Member Added',
    '666' => 'Security Disabled Universal Group Member Removed',
    '667' => 'Security Disabled Universal Group Deleted',
    '668' => 'Group Type Changed',
    '669' => 'Add SID History (Success)',
    '670' => 'Add SID History (Failure)',
    '671' => 'User Account Unlocked',
    '672' => 'Authentication Ticket Granted',
    '673' => 'Service Ticket Granted',
    '674' => 'Ticket Granted Renewed',
    '675' => 'Pre-authentication failed',
    '676' => 'Authentication Ticket Request Failed',
    '677' => 'Service Ticket Request Failed',
    '678' => 'Account Mapped for Logon',
    '679' => 'Account could not be mapped for logon',
    '680' => 'Account Used for Logon',
    '681' => 'The logon to account: client name by: source from workstation: workstation failed. The error code was: error',
    '682' => 'Session reconnected to winstation',
    '683' => 'Session disconnected from winstation',
    '684' => 'Set the security descriptor of members of administrative groups',
    '685' => 'Account Name Changed',
    '686' => 'Password of the following user accessed',
    '687' => 'Application group operation',
    '688' => 'Application group operation',
    '689' => 'Application group operation',
    '690' => 'Application group operation',
    '691' => 'Application group operation',
    '692' => 'Application group operation',
    '693' => 'Application group operation',
    '694' => 'Application group operation',
    '695' => 'Application group operation',
    '696' => 'Application group operation',
    '768' => 'Collision detected between a namespace element in one forest and a namespace element in another forest',
    '806' => 'Per User Audit Policy was refreshed',
    '807' => 'Per user auditing policy set for user'
) ;

# idCodes-related string meanings
my %eventIdStrings = (
    '528' => [ 'User Name', 'Domain', 'Logon ID', 'Logon Type', 'Logon Process', 'Auth. Package', 'Workstation Name', 'Logon GUID' ],
    '529' => [ 'User Name', 'Domain', 'Logon Type', 'Logon Process', 'Auth. Package', 'Workstation Name' ],
    '538' => [ 'User Name', 'Domain', 'Logon ID', 'Logon Type' ],
    '540' => [ 'User Name', 'Domain', 'Logon ID', 'Logon Type', 'Logon Process', 'Auth. Package', 'Workstation Name', 'Logon GUID' ],
    '551' => [ 'User Name', 'Domain', 'Logon ID' ]
) ;

my %eventCategoryCodes = (
    '0' => 'None',
    '1' => 'System',
    '2' => 'Logon/Logoff',
    '3' => 'Object access',
    '4' => 'Privilege use',
    '5' => 'Process tracking',
    '6' => 'Policy change',
    '7' => 'Account Management',
    '8' => 'Directory service access', # Not verified
    '9' => 'Account logon'
) ;

my %eventTypeCodes = (
    '0'  => 'Success',
    '1'  => 'Error',
    '2'  => 'Warning',
    '4'  => 'Information',
    '8'  => 'Success Audit',
    '16' => 'Failure Audit'
) ;

my %wellKnownSIDs = (
    'S-1-0'        => 'Null Authority',
    'S-1-0-0'      => 'Nobody',
    'S-1-1'        => 'World Authority',
    'S-1-1-0'      => 'Everyone',
    'S-1-2'        => 'Local Authority',
    'S-1-3'        => 'Creator Authority',
    'S-1-3-0'      => 'Creator Owner',
    'S-1-3-1'      => 'Creator Group',
    'S-1-3-2'      => 'Creator Owner Server',
    'S-1-3-3'      => 'Creator Group Server',
    'S-1-4'        => 'Non-unique Authority',
    'S-1-5'        => 'NT Authority',
    'S-1-5-1'      => 'Dialup',
    'S-1-5-2'      => 'Network',
    'S-1-5-3'      => 'Batch',
    'S-1-5-4'      => 'Interactive',
    'S-1-5-5'      => 'Logon Session',   # Should have -X-Y here
    'S-1-5-6'      => 'Service',
    'S-1-5-7'      => 'Anonymous',
    'S-1-5-8'      => 'Proxy',
    'S-1-5-9'      => 'Enterprise Domain Controllers',
    'S-1-5-10'     => 'Principal Self',
    'S-1-5-11'     => 'Authenticated Users',
    'S-1-5-12'     => 'Restricted Code',
    'S-1-5-13'     => 'Terminal Server Users',
    'S-1-5-18'     => 'Local System',
    'S-1-5-19'     => 'NT Authority',
    'S-1-5-20'     => 'NT Authority',
    'S-1-5-32-544' => 'Administrators',
    'S-1-5-32-545' => 'Users',
    'S-1-5-32-546' => 'Guests',
    'S-1-5-32-547' => 'Power Users',
    'S-1-5-32-548' => 'Account Operators',
    'S-1-5-32-549' => 'Server Operators',
    'S-1-5-32-550' => 'Print Operators',
    'S-1-5-32-551' => 'Backup Operators',
    'S-1-5-32-552' => 'Replicators',
    'S-1-5-32-554' => 'BUILTIN\Pre-Windows 2000 Compatible Access',
    'S-1-5-32-555' => 'BUILTIN\Remote Desktop Users',
    'S-1-5-32-556' => 'BUILTIN\Network Configuration Operators',
    'S-1-5-32-557' => 'BUILTIN\Incoming Forest Trust Builders',
    'S-1-5-32-557' => 'BUILTIN\Incoming Forest Trust Builders',
    'S-1-5-32-558' => 'BUILTIN\Performance Monitor Users',
    'S-1-5-32-559' => 'BUILTIN\Performance Log Users',
    'S-1-5-32-560' => 'BUILTIN\Windows Authorization Access Group',
    'S-1-5-32-561' => 'BUILTIN\Terminal Server License Servers'
) ;

my %wellKnownRIDs = (
    '500' => 'Administrator',
    '501' => 'Guest',
    '502' => 'KRBTGT',
    '512' => 'Admins',
    '513' => 'Users',
    '514' => 'Guests',
    '515' => 'Computers',
    '516' => 'Controllers',
    '517' => 'Cert Publishers',
    '518' => 'Schema Admins',
    '519' => 'Enterprise Admins',
    '520' => 'Group Policy Creator Owners',
    '533' => 'RAS and IAS Servers',
    '544' => 'Builtin Admins',
    '545' => 'Builtin users',
    '546' => 'Builtin Guests',
    '547' => 'Builtin Power Users',
    '548' => 'Builtin Account Operators',
    '549' => 'Builtin System Operators',
    '550' => 'Builtin Print Operators',
    '551' => 'Builtin Backup Operators',
    '552' => 'Builtin Replicator',
    '553' => 'Builtin RAS Servers'
) ;
### End of ID declarations ###

# Record header length (in bytes)
my $HEADER_LENGTH = 56 ;

# Tests is a string is NULL
# ARG1 = string to test
sub isNull($)
{
  my ($string) = @_ ;
  if (defined($string) && ($string !~ /^\s*$/)) { return 0 ; }
  return 1 ;
}

# Converts a binary SID to a string
# ARG1 = binary sid
# Based on Win32::Lanman::SidToString
sub SidToString($)
{
    my ($binarySid) = @_ ;

    if (not defined($binarySid) or ($binarySid eq ''))
    {
        return 'N/A' ;
    }

    my $version = unpack('C', substr($binarySid, 0, 1)) ;
    my $numDashes = unpack('C', substr($binarySid, 1, 1)) ;

    if (length($binarySid) != (8 + 4 * $numDashes))
    {
        return 'Invalid' ;
    }

    my $stringSid = "S-$version-" . unpack('V', substr($binarySid, 4, 4)) ;
    for my $i (0 .. ($numDashes - 1))
    {
          $stringSid .= '-' . unpack('V', substr($binarySid, 8 + 4 * $i, 4)) ;
    }
    return $stringSid ;
}

# Returns next raw entry from the given file descriptor
# ARG1 = file descriptor
# returns '' if a wrong record has been read
# returns 'EOF' if EOF has been reached
sub getNextRawEntryFromFile($)
{
    my ($file) = @_ ;

    my $buffer = '' ;
    my $size = 0 ;

    while ($buffer ne 'LfLe')
    {
            $size = read($file, $buffer, 4) ; # read DWORDs sequencially to find a record
            if ($size <= 0) { return 'EOF' ; }
    }
    seek($file, -8, 1) ; # go back to the start of the record

    $size = read($file, $buffer, 4) ; # read the length of the record
    if ($size < 4 ) { return 'EOF' ; }

    my $recordLength = unpack('V', $buffer) ; # and convert it

    if ($recordLength > $HEADER_LENGTH)
    {
        seek($file, -4, 1) ; # go back to the start of the record
        $size = read($file, $buffer, $recordLength) ; # read the whole record
        if ($size < $recordLength ) { return 'EOF' ; }
        return $buffer ;
    }
    else
    {
        seek($file, 8, 1) ; # skip the wrong record and return
        return '' ;
    }
}

# Decodes a raw entry and splits values
# ARG1 = raw entry reference
# ARG2 = target character encoding
# returns a %decodedEntry, human readable hash
sub getDecodedEntryFromRaw($$)
{
    my ($rawEntry, $enc) = @_ ;
    my %decodedEntry = () ;

    # Split header / body and keep them
    $decodedEntry{rawHeader} = substr($$rawEntry, 0, $HEADER_LENGTH) ;
    $decodedEntry{rawBody} = substr($$rawEntry, $HEADER_LENGTH) ;

    ############### Header part ###############
    # See      : http://msdn.microsoft.com/en-us/library/cc231412.aspx
    # See also : http://www.forensicswiki.org/wiki/EVT
    # See also : http://www.d-fence.be

    (my $length, my $reserved, my $recordNumber, my $timeGenerated, my $timeWritten, my $eventId, my $eventType, my $numStrings, my $eventCategory, my $reservedFlags, my $closingRecordNumber, my $stringOffset, my $userSIDLength, my $userSIDOffset, my $dataLength, my $dataOffset) = unpack('VVVVVVvvvvVVVVVV', $$rawEntry) ;

    ############## Header part (56 bytes)
    $decodedEntry{hdr_length} = $length ;                               # 4 bytes
    $decodedEntry{hdr_reserved} = $reserved ;                           # 4 bytes - 'LfLe' flag
    $decodedEntry{hdr_recordNumber} = $recordNumber ;                   # 4 bytes
    $decodedEntry{hdr_timeGenerated} = $timeGenerated ;                 # 4 bytes
    $decodedEntry{hdr_timeWritten} = $timeWritten ;                     # 4 bytes
    $decodedEntry{hdr_eventId} = $eventId ;                             # 4 bytes
    $decodedEntry{hdr_eventType} = $eventType ;                         # 2 bytes
    $decodedEntry{hdr_numStrings} = $numStrings ;                       # 2 bytes
    $decodedEntry{hdr_eventCategory} = $eventCategory ;                 # 2 bytes
    $decodedEntry{hdr_reservedFlags} = $reservedFlags ;                 # 2 bytes
    $decodedEntry{hdr_closingRecordNumber} = $closingRecordNumber ;     # 4 bytes
    $decodedEntry{hdr_stringOffset} = $stringOffset ;                   # 4 bytes
    $decodedEntry{hdr_userSIDLength} = $userSIDLength ;                 # 4 bytes
    $decodedEntry{hdr_userSIDOffset} = $userSIDOffset ;                 # 4 bytes
    $decodedEntry{hdr_dataLength} = $dataLength ;                       # 4 bytes
    $decodedEntry{hdr_dataOffset} = $dataOffset ;                       # 4 bytes
    ############## Body part
    # SourceName                                                        # (variable)
    # Computername                                                      # (variable)
    # UserSidPadding                                                    # (variable)
    # UserSid                                                           # (variable)
    # Strings                                                           # (variable)
    # Data                                                              # (variable)
    # Padding                                                           # (variable)
    # Length2                                                           # 4 bytes

    ############### Handle body part ###############

    # Event source (Unicode, \0\0 terminated) and computer name (Unicode, \0\0 terminated)
    my $tmpData = substr($$rawEntry, $HEADER_LENGTH) ;
    $tmpData = Encode::decode('UCS-2LE', $tmpData) ;
    ($decodedEntry{bdy_eventSource}, $decodedEntry{bdy_computer}) = split(/\0/, $tmpData) ;
    if (isNull($decodedEntry{bdy_eventSource}))
    {
        $decodedEntry{bdy_eventSource} = "<unknown>" ;
    }
    if (isNull($decodedEntry{bdy_computer}))
    {
        $decodedEntry{bdy_computer} = "<unknown>" ;
    }
    if (not isNull($enc))
    {
        $decodedEntry{bdy_eventSource} = Encode::encode($enc, $decodedEntry{bdy_eventSource}) ;
        $decodedEntry{bdy_computer} = Encode::encode($enc, $decodedEntry{bdy_computer}) ;
    }

    # User SID, 5x4 bytes
    if ($userSIDOffset + $userSIDLength > $decodedEntry{hdr_length})
    {
        $decodedEntry{bdy_sid} = "<corrupt record>" ;
    }
    else
    {
        $decodedEntry{bdy_sid} = SidToString(substr($$rawEntry, $userSIDOffset, $userSIDLength)) ;
    }

    # Strings start (should be stringOffset, Unicode, \0\0 terminated)
    my @bdy_strings = () ;
    if ($stringOffset >= $decodedEntry{hdr_length})
    {
        push(@bdy_strings, "<corrupt record>") ;
    }
    else
    {
        $tmpData = substr($$rawEntry, $stringOffset) ;
        $tmpData = Encode::decode('UCS-2LE', $tmpData) ;
        @bdy_strings = split(/\0/, $tmpData) ;
        # Keep $numStrings elements only
        splice(@bdy_strings, $numStrings) ;
        if (not isNull($enc))
        {
            for (my $i = 0 ; $i < @bdy_strings ; $i++)
            {
                # Convert \r and \n within string to space
                $bdy_strings[$i] =~ tr/\r\n/  /s ;
                # Trim resulting string
                $bdy_strings[$i] =~ s/^\s+|\s+$//g ;
                $bdy_strings[$i] = Encode::encode($enc, $bdy_strings[$i]) ;
            }
        }
    }
    $decodedEntry{bdy_strings} = \@bdy_strings ;

    # Data start (should be dataOffset)
    if ($dataOffset + $dataLength > $decodedEntry{hdr_length})
    {
        $decodedEntry{bdy_data} = "" ;
    }
    else
    {
        $decodedEntry{bdy_data} = substr($$rawEntry, $dataOffset, $dataLength) ;
    }

    return %decodedEntry ;
}

# Initializes a decodedEntryPool array from a file
# ARG1 = @decodedEntryPool array reference
# ARG2 = file descriptor
# ARG3 = target character encoding
# returns 0
sub getDecodedEntriesFromFile($$$)
{
    my ($decodedEntryPool, $file, $enc) = @_ ;

    my $rawEntry = getNextRawEntryFromFile($file) ;
    while ($rawEntry ne 'EOF')
    {
        if ($rawEntry ne '')
        {
            my %decodedEntry = getDecodedEntryFromRaw(\$rawEntry, $enc) ;
            push(@{$decodedEntryPool}, \%decodedEntry) ;
        }
        $rawEntry = getNextRawEntryFromFile($file) ;
    }

    return 0 ;
}

# Prints a decoded entry
# ARG1 = %decodedEntry hash reference
# ARG2 = first column size
# ARG3 = options hash reference
# returns 0 or 1 if error
sub printDecodedEntry($$$)
{
    my ($decodedEntry, $firstColumnLength, $options) = @_ ;

    if ($options->{l} == 0)
    {
        ############### Raw part ###############
        #print('Header : ' . $decodedEntry->{rawHeader} . "\n") ;
        #print('Body   : ' . $decodedEntry->{rawBody} . "\n") ;
        #print("\n") ;
    
        ############### Header part ###############
        print("[Header part]\n") ;
        if ($options->{v} == 1)
        {
            printf("%-${firstColumnLength}s: ", 'Length') ; print($decodedEntry->{hdr_length} . "\n") ;
            printf("%-${firstColumnLength}s: ", 'Reserved') ; print($decodedEntry->{hdr_reserved} . " (\"LfLe\" flag)\n") ;
        }
        printf("%-${firstColumnLength}s: ", 'Record number') ; print($decodedEntry->{hdr_recordNumber} . "\n") ;
        printf("%-${firstColumnLength}s: ", 'Time generated') ; print(localtime($decodedEntry->{hdr_timeGenerated}) . ' local (' . gmtime($decodedEntry->{hdr_timeGenerated}) . " GMT)\n") ;
        if ($options->{v} == 1)
        {
            printf("%-${firstColumnLength}s: ", 'Time written') ; print(localtime($decodedEntry->{hdr_timeWritten}) . ' local (' . gmtime($decodedEntry->{hdr_timeWritten}) . " GMT)\n") ;
        }
        printf("%-${firstColumnLength}s: ", 'Event ID') ; print($decodedEntry->{hdr_eventId} . ' (' . (defined($eventIdCodes{$decodedEntry->{hdr_eventId}}) ? $eventIdCodes{$decodedEntry->{hdr_eventId}} : 'no description') . ")\n") ;
        printf("%-${firstColumnLength}s: ", 'Event type') ; print($decodedEntry->{hdr_eventType} . ' (' . (defined($eventTypeCodes{$decodedEntry->{hdr_eventType}}) ? $eventTypeCodes{$decodedEntry->{hdr_eventType}} : 'no description') . ")\n") ;
        if ($options->{v} == 1)
        {
            printf("%-${firstColumnLength}s: ", 'Num. of strings') ; print($decodedEntry->{hdr_numStrings} . "\n") ;
        }
        printf("%-${firstColumnLength}s: ", 'Event category') ; print($decodedEntry->{hdr_eventCategory} . ' (' . (defined($eventCategoryCodes{$decodedEntry->{hdr_eventCategory}}) ? $eventCategoryCodes{$decodedEntry->{hdr_eventCategory}} : 'no description') . ")\n") ;
        if ($options->{v} == 1)
        {
            printf("%-${firstColumnLength}s: ", 'Reserved flags') ; print($decodedEntry->{hdr_reservedFlags} . "\n") ;
            printf("%-${firstColumnLength}s: ", 'Closing record num.') ; print($decodedEntry->{hdr_closingRecordNumber} . "\n") ;
            printf("%-${firstColumnLength}s: ", 'String offset') ; print($decodedEntry->{hdr_stringOffset} . "\n") ;
            printf("%-${firstColumnLength}s: ", 'User SID length') ; print($decodedEntry->{hdr_userSIDLength} . "\n") ;
            printf("%-${firstColumnLength}s: ", 'User SID offset') ; print($decodedEntry->{hdr_userSIDOffset} . "\n") ;
            printf("%-${firstColumnLength}s: ", 'Data length') ; print($decodedEntry->{hdr_dataLength} . "\n") ;
            printf("%-${firstColumnLength}s: ", 'Data offset') ; print($decodedEntry->{hdr_dataOffset} . "\n") ;
        }
        print("\n") ;
    
        ############### Body part ###############
        print("[Body part]\n") ;
        printf("%-${firstColumnLength}s: ", 'Event source') ; print($decodedEntry->{bdy_eventSource} . "\n") ;
        printf("%-${firstColumnLength}s: ", 'Computer name') ; print($decodedEntry->{bdy_computer} . "\n") ;
        # SID
        printf("%-${firstColumnLength}s: ", 'User SID') ; print($decodedEntry->{bdy_sid}) ;
        if (defined($wellKnownSIDs{$decodedEntry->{bdy_sid}}))
        {
            print(' (' . $wellKnownSIDs{$decodedEntry->{bdy_sid}} . ')') ;
        }
        else
        {
            my @tmpIds = split(/-/, $decodedEntry->{bdy_sid}) ; # Get IDs, $tmpIds[-1] is the RID
            if (defined($tmpIds[-1]) and defined($wellKnownRIDs{$tmpIds[-1]}))
            {
                print(' (' . $wellKnownRIDs{$tmpIds[-1]} . ')') ;
            }
            else
            {
                if (($tmpIds[-1] ne 'N/A') and ($tmpIds[-1] ne 'Invalid'))
                {
                    print(' (unresolved)') ;
                }
            }
        }
        print("\n") ;
        # Strings
        my $i = 0 ;
        if (defined(@{$decodedEntry->{bdy_strings}}[$i]))
        {
            print("\n") ;
            print("[Strings part]\n") ;
            while (defined(@{$decodedEntry->{bdy_strings}}[$i]))
            {
                printf("%-${firstColumnLength}s: ", defined(@{$eventIdStrings{$decodedEntry->{hdr_eventId}}}[$i]) ? @{$eventIdStrings{$decodedEntry->{hdr_eventId}}}[$i] : 'String') ;
                print(@{$decodedEntry->{bdy_strings}}[$i] . "\n") ;
                $i++ ;
            }
        }
    
        print("\n") ;
        # Data
        if ($options->{v} == 1)
        {
            print("[Data part]\n") ;
            printf("%-${firstColumnLength}s: ", 'Data') ; print((($decodedEntry->{bdy_data} eq '') ? 'N/A' : $decodedEntry->{bdy_data}) . "\n") ;
            print("\n") ;
        }
    }
    else # log-like display
    {
        # Local time
        my ($sec, $min, $hour, $mday, $mon, $year) = localtime($decodedEntry->{hdr_timeGenerated}) ;
        $mon += 1 ;
        $year += 1900 ;
        print("$year/" . ($mon < 10 ? '0' : '') . "$mon/$mday ") ;
        print(($hour < 10 ? '0' : '') . $hour . ':' ) ;
        print(($min < 10 ? '0' : '') . $min . ':' ) ;
        print(($sec < 10 ? '0' : '') . $sec . ' ' ) ;

        # Record number
        print($decodedEntry->{hdr_recordNumber} . '; ') ;

        # Event source / Computer name / User SID
        print($decodedEntry->{bdy_computer} . ' ') ;
        print($decodedEntry->{bdy_eventSource} . '; ') ;
        print($decodedEntry->{bdy_sid} . '; ') ;

        # Event ID, Type and Category
        print($decodedEntry->{hdr_eventId} . ' (' . (defined($eventIdCodes{$decodedEntry->{hdr_eventId}}) ? $eventIdCodes{$decodedEntry->{hdr_eventId}} : 'no description') . '); ') ;
        print($decodedEntry->{hdr_eventType} . ' (' . (defined($eventTypeCodes{$decodedEntry->{hdr_eventType}}) ? $eventTypeCodes{$decodedEntry->{hdr_eventType}} : 'no description') . '); ') ;
        print($decodedEntry->{hdr_eventCategory} . ' (' . (defined($eventCategoryCodes{$decodedEntry->{hdr_eventCategory}}) ? $eventCategoryCodes{$decodedEntry->{hdr_eventCategory}} : 'no description') . ')') ;

        # Strings
        my $i = 0 ;
        if (defined(@{$decodedEntry->{bdy_strings}}[$i])) { print('; ') ; }
        while (defined(@{$decodedEntry->{bdy_strings}}[$i]))
        {
            print(@{$decodedEntry->{bdy_strings}}[$i]) ;
            if (defined(@{$decodedEntry->{bdy_strings}}[$i+1])) { print(', ') ; }
            $i++ ;
        }

        print("\n") ;
    }

    return 0 ;
}

# Removes bad entries from the entry pool given the specified options
# ARG1 = @decodedEntryPool array reference
# ARG2 = options hash reference
# returns 0
sub filterDecodedEntries($$)
{
    my ($decodedEntryPool, $options) = @_ ;

    my $i = 0 ;
    while (defined($decodedEntryPool->[$i]))
    {
        # Manage -n (record number)
        if (defined($options->{n}) and ($options->{n} != $decodedEntryPool->[$i]->{hdr_recordNumber}))
        {
            splice(@{$decodedEntryPool}, $i, 1) ;
            next ;
        }
        # Manage -i (id)
        if (defined($options->{i}) and ($options->{i} != $decodedEntryPool->[$i]->{hdr_eventId}))
        {
            splice(@{$decodedEntryPool}, $i, 1) ;
            next ;
        }
        # Manage -t (type)
        if (defined($options->{t}) and ($options->{t} != $decodedEntryPool->[$i]->{hdr_eventType}))
        {
            splice(@{$decodedEntryPool}, $i, 1) ;
            next ;
        }
        # Manage -c (category)
        if (defined($options->{c}) and ($options->{c} != $decodedEntryPool->[$i]->{hdr_eventCategory}))
        {
            splice(@{$decodedEntryPool}, $i, 1) ;
            next ;
        }

        $i++ ;
    }

    # Truncate results
    if (($options->{x} > 0) and ($options->{x} < @{$decodedEntryPool}))
    {
        splice(@{$decodedEntryPool}, $options->{x}, @{$decodedEntryPool} - $options->{x}) ;
    }
    if (($options->{y} > 0) and ($options->{y} < @{$decodedEntryPool}))
    {
        splice(@{$decodedEntryPool}, 0, @{$decodedEntryPool} - $options->{y}) ;
    }

    # Display result #n
    # Should return a result only if the given number is 1 <= number <= @{$decodedEntryPool}
    # Else empty the array (nothing to return)
    if (defined($options->{r}))
    {
        if (($options->{r} > 0) and ($options->{r} <= @{$decodedEntryPool}))
        {
            @{$decodedEntryPool} = ($decodedEntryPool->[$options->{r} - 1]) ;
        }
        else
        {
            @{$decodedEntryPool} = () ;
        }
    }

    return 0 ;
}

# Print a decoded entry pool
# ARG1 = @decodedEntryPool array reference
# ARG2 = first column size
# ARG3 = options hash reference
# returns 0
sub printDecodedEntries($$$)
{
    my ($decodedEntryPool, $firstColumnLength, $options) = @_ ;

    # Print header
    if (($options->{l} == 1) and ($options->{v} == 1))
    {
        print STDERR
        (
            "Date Time RecordNum; ComputerName EvtSource; UserSID; EvtID; EvtType; EvtCategory; String[, String2[, ...]]" . "\n"
        ) ;
    }

    my $i = 0 ;
    while (defined($decodedEntryPool->[$i]))
    {
        my %decodedEntry = %{$decodedEntryPool->[$i]} ;
        if ($options->{l} == 0)
        {
            print('--') ;
            if ($options->{v} == 1) { print(' Entry #' . ($i + 1) . ' --') ; }
            print("\n") ;
        }
        printDecodedEntry(\%decodedEntry, $firstColumnLength, $options) ;
        $i++ ;
    }

    if ($i == 0)
    {
        print STDERR ("No entry found.\n") ;
    }
    else
    {
        if ($options->{v} == 1)
        {
            if ($options->{l} == 0) { print STDERR ("--\n") ; }
            print STDERR ($i . " entr" . ($i > 1 ? "ies" : "y") . " found.\n") ;
        }
    }

    return 0 ;
}

# Prints help
sub HELP_MESSAGE()
{
    print("Usage: evtViewer [OPTIONS] [filename.evt [filename2.evt...]]\n") ;
    print("\n") ;
    print("== Filters ==\n") ;
    print("  -n num :  show only events matching this record number\n") ;
    print("  -i num :  show only events matching this event id\n") ;
    print("  -t num :  show only events matching this event type\n") ;
    print("  -c num :  show only events matching this event category\n") ;
    print("== Limits ==\n") ;
    print("  -x num :  show only the first num entries (0 = unlimited, default)\n") ;
    print("  -y num :  show only the last num entries (0 = unlimited, default)\n") ;
    print("  -r num :  show only result number num\n") ;
    print("== Display ==\n") ;
    print("  -l     :  log-like display mode\n") ;
    print("  -e     :  target character encoding to use when converting strings\n") ;
    print("            (default: 'ascii', set to '' to skip re-encoding)\n") ;
    print("  -v     :  verbose mode\n") ;
    print("  -V     :  print version\n") ;
}

# Prints version
sub VERSION_MESSAGE()
{
    print("$VERSION\n") ;
}

##############
#### Main ####
##############

my $firstColumnLength = 20 ;

## Options ##

my %options=() ;
Getopt::Std::getopts("n:i:t:c:x:y:r:le:vVh", \%options) ;

if (defined($options{V}))
{
    VERSION_MESSAGE() ;
    exit 0 ;
}
if (defined($options{h}))
{
    VERSION_MESSAGE() ;
    HELP_MESSAGE() ;
    exit 0 ;
}
if (defined($options{n}) and ($options{n} !~ /^[0-9]+$/)) { undef $options{n} ; }
if (defined($options{i}) and ($options{i} !~ /^[0-9]+$/)) { undef $options{i} ; }
if (defined($options{t}) and ($options{t} !~ /^[0-9]+$/)) { undef $options{t} ; }
if (defined($options{c}) and ($options{c} !~ /^[0-9]+$/)) { undef $options{c} ; }
if (not defined($options{x}) or (defined($options{x}) and ($options{x} !~ /^[0-9]+$/))) { $options{x} = 0 ; }
if (not defined($options{y}) or (defined($options{y}) and ($options{y} !~ /^[0-9]+$/))) { $options{y} = 0 ; }
if (defined($options{r}) and ($options{r} !~ /^[0-9]+$/)) { undef $options{r} ; }
if (not defined($options{l})) { $options{l} = 0 ; } else { $options{l} = 1 ; }
if (not defined($options{e})) { $options{e} = 'ascii' ; }
if ((not isNull($options{e})) and (not grep(/^$options{e}$/, Encode->encodings(":all"))))
{
    print STDERR ("Unsupported target character encoding: '" . $options{e} . "'\n") ;
    exit 1 ;
}
if (not defined($options{v})) { $options{v} = 0 ; } else { $options{v} = 1 ; }

## Manage input (file or STDIN ?) ##

my @decodedEntryPool = () ;

if (@ARGV > 0)
{
    foreach my $fileName (@ARGV)
    {
        my $inputFile = IO::File->new() ;
        if ((-f $fileName) and $inputFile->open("$fileName", 'r'))
        {
            binmode($inputFile) ;
            getDecodedEntriesFromFile(\@decodedEntryPool, $inputFile, $options{e}) ;
            $inputFile->close() ;
        }
        else
        {
            print STDERR ("Cannot open input file $fileName\n") ;
        }
    }
}
else # no parameter, read from 'STDIN'
{
    my $fileName = "/tmp/evtViewer.$$.tmp" ;

    my $inputFile = IO::File->new() ;
    $inputFile->open("$fileName", 'w+') or die("Cannot create output temporary file $fileName.\n") ;
    binmode($inputFile) ;

    # Copy STDIN data to the tmp file (need seek ability)
    while (<STDIN>) { print $inputFile ($_) ; }
    seek($inputFile, 0, 0) ;
    getDecodedEntriesFromFile(\@decodedEntryPool, $inputFile, $options{e}) ;

    # Close and delete temporary file
    $inputFile->close() ;
    unlink($fileName) ;
}

## Filter and print entries ##

filterDecodedEntries(\@decodedEntryPool, \%options) ;
printDecodedEntries(\@decodedEntryPool, $firstColumnLength, \%options) ;

exit ((@decodedEntryPool > 0) ? 0 : 1) ;
