HoneyClient::Agent::Integrity::Filesystem - Perl extension to perform static


HoneyClient-Agent documentation Contained in the HoneyClient-Agent distribution.

Index


Code Index:

NAME

Top

HoneyClient::Agent::Integrity::Filesystem - Perl extension to perform static checks of the Windows OS filesystem.

VERSION

Top

This documentation refers to HoneyClient::Agent::Integrity::Filesystem version 0.98.

SYNOPSIS

Top

  use HoneyClient::Agent::Integrity::Filesystem;
  use Data::Dumper;

  # Create the filesystem object.  Upon creation, the object will
  # be initialized, by performing a baseline of the filesystem.
  my $filesystem = HoneyClient::Agent::Integrity::Filesystem->new();

  # ... Some time elapses ...

  # Check the filesystem, for any violations.
  my $changes = $filesystem->check();

  if (!defined($changes)) {
      print "No filesystem changes have occurred.\n";
  } else {
      print "Filesystem has changed:\n";
      print Dumper($changes);
  }

  # $changes refers to an array of hashtable references, where
  # each hashtable has the following format:
  #
  # $changes = [ {
  #     # Indicates if the filesystem entry was deleted,
  #     # added, or changed.
  #     'status' => $STATUS_DELETED | $STATUS_ADDED | $STATUS_MODIFIED,
  #     'name'  => 'C:\WINDOWS\SYSTEM32...',
  #     'mtime' => 'YYYY-MM-DD HH:MM:SS', # new mtime for added/modified files;
  #                                       # old mtime for deleted files
  #
  #     # content will only exist for added/modified files
  #     'content' => {
  #         'size' => 1263,                                       # size of new content 
  #         'type' => 'application/octect-stream',                # type of new content
  #         'md5'  => 'b1946ac92492d2347c6235b4d2611184',         # md5  of new content
  #         'sha1' => 'f572d396fae9206628714fb2ce00f72e94f2258f', # sha1 of new content
  #     },
  # }, ]

DESCRIPTION

Top

This library allows the Integrity module to easily baseline and check the Windows OS filesystem for any changes that may occur, while instrumenting a target application.

DEFAULT PARAMETER LIST

Top

When a Filesystem $object is instantiated using the new() function, the following parameters are supplied default values. Each value can be overridden by specifying the new (key => value) pair into the new() function, as arguments.

bypass_baseline

When set to 1, the object will forgo any type of initial baselining process, upon initialization. Otherwise, baselining will occur as normal, upon initialization.

baseline_analysis

An array of hashtables used to hold the file analysis information, for the baseline filesystem operation.

monitored_directories

The base list of drives, directories, and/or files to monitor.

ignored_entries

The list of regular expressions that match drives, directories, and/or files to exclude from analysis.

METHODS IMPLEMENTED

Top

The following functions have been implemented by any Filesystem object.

HoneyClient::Agent::Integrity::Filesystem->new($param => $value, ...)

Creates a new Filesystem object, which contains a hashtable containing any of the supplied "param => value" arguments. Upon creation, the Filesystem object performs a baseline of the Windows filesystem.

Inputs:$param is an optional parameter variable.$value is $param's corresponding value.

Note: If any $param(s) are supplied, then an equal number of corresponding $value(s) must also be specified.

Output: The instantiated Filesystem $object, fully initialized.

$object->check(no_prepare => $no_prepare)

Checks the filesystem for various changes, based upon the filesystem baseline, when the new() method was invoked.

Inputs:$no_prepare is an optional parameter, specifying the output format of the changes found.

Output:$changes, which is an array of hashtable references, where each hashtable has the following format:

  If $no_prepare == 1, then the format will be:

  $changes = [ {
      # Indicates if the filesystem entry was deleted,
      # added, or changed.
      'status' => $STATUS_DELETED | $STATUS_ADDED | $STATUS_MODIFIED,

      # If the entry has been added/changed, then this 
      # hashtable contains the file/directory's new information.
      'new' => {
          'name'  => 'C:\WINDOWS\SYSTEM32...',
          'size'  => 1263, # in bytes
          'mtime' => 1178135092, # modification time, seconds since epoch
      },

      # If the entry has been deleted/changed, then this
      # hashtable contains the file/directory's old information.
      'old' => {
          'name'  => 'C:\WINDOWS\SYSTEM32...',
          'size'  => 802, # in bytes
          'mtime' => 1178135028, # modification time, seconds since epoch
      },
  }, ]

  Otherwise, the format will be:

  $changes = [ {
      # Indicates if the filesystem entry was deleted,
      # added, or changed.
      'status' => $STATUS_DELETED | $STATUS_ADDED | $STATUS_MODIFIED,
      'name'  => 'C:\WINDOWS\SYSTEM32...',
      'mtime' => 'YYYY-MM-DD HH:MM:SS', # new mtime for added/modified files;
                                        # old mtime for deleted files

      # content will only exist for added/modified files
      'content' => {
          'size' => 1263,                                       # size of new content 
          'type' => 'application/octet-stream',                 # type of new content
          'md5'  => 'b1946ac92492d2347c6235b4d2611184',         # md5  of new content
          'sha1' => 'f572d396fae9206628714fb2ce00f72e94f2258f', # sha1 of new content
      },
  }, ]

Notes: If $no_prepare != 1 or $no_prepare == undef, then the outputted changes will NEVER refer to any directories. All the changes will correspond to individual files.

BUGS & ASSUMPTIONS

Top

This library performs STATIC checks of the Windows filesystem. If malware modifies the filesystem between the time the $object->new() and $object->check() methods are called, then this library may FAIL to detect those changes if:

This library also only monitors FILE changes. Thus, if malware manipulates EMPTY DIRECTORIES or SYMLINKS on the system, then this library will NOT report those changes.

SEE ALSO

Top

http://www.honeyclient.org/trac

REPORTING BUGS

Top

http://www.honeyclient.org/trac/newticket

ACKNOWLEDGEMENTS

Top

Mark-Jason Dominus <mjd-perl-diff@plover.com>, Ned Konz <perl@bike-nomad.com>, and Tye McQueen, for using their Algorithm::Diff code.

AUTHORS

Top

Xeno Kovah, <xkovah@mitre.org>

Darien Kindlund, <kindlund@mitre.org>

Brad Stephenson, <stephenson@mitre.org>

COPYRIGHT & LICENSE

Top


HoneyClient-Agent documentation Contained in the HoneyClient-Agent distribution.
################################################################################
# Created on:  April 12, 2007
# Package:     HoneyClient::Agent::Integrity::Filesystem
# File:        Filesystem.pm
# Description: Performs static checks of the Windows OS filesystem.
#
# CVS: $Id: Filesystem.pm 773 2007-07-26 19:04:55Z kindlund $
#
# @author xkovah, kindlund, stephenson
#
# Copyright (C) 2007 The MITRE Corporation.  All rights reserved.
#
# 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, using version 2
# of the License.
#
# 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.
#
################################################################################

package HoneyClient::Agent::Integrity::Filesystem;

use strict;
use warnings;
use Carp ();

# Include Global Configuration Processing Library
use HoneyClient::Util::Config qw(getVar);

# Include the File/Directory Search Library
use File::Find qw(find);

# Include the Diff algorithm for comparing files
use Algorithm::Diff;

# Include Cygwin Path Conversion Library.
use Filesys::CygwinPaths qw(:all);

# Use Storable Library
use Storable qw(nfreeze thaw dclone);
$Storable::Deparse = 1;
$Storable::Eval = 1;

# Use Dumper Library
use Data::Dumper;

# Use Basename Library
use File::Basename qw(dirname);

# Include Logging Library
use Log::Log4perl qw(:easy);

# Use DateTime Library
use DateTime;

# Use MD5 Library
use Digest::MD5;

# Use SHA Library
use Digest::SHA;

# Use File::Type Library
use File::Type;

# Use IO::File Library
use IO::File;

#######################################################################
# Module Initialization                                               #
#######################################################################

BEGIN {
    # Defines which functions can be called externally.
    require Exporter;
    our (@ISA, @EXPORT, @EXPORT_OK, %EXPORT_TAGS, $VERSION);

    # Set our package version.
    $VERSION = 0.98;

    @ISA = qw(Exporter);

    # Symbols to export automatically
    @EXPORT = qw( );

    # Items to export into callers namespace by default. Note: do not export
    # names by default without a very good reason. Use EXPORT_OK instead.
    # Do not simply export all your public functions/methods/constants.

    # This allows declaration use HoneyClient::Agent::Integrity::Filesystem ':all';
    # If you do not need this, moving things directly into @EXPORT or @EXPORT_OK
    # will save memory.

    %EXPORT_TAGS = (
        'all' => [ qw( ) ],
    );

    # Symbols to autoexport (when qw(:all) tag is used)
    @EXPORT_OK = ( @{ $EXPORT_TAGS{'all'} } );

    $SIG{PIPE} = 'IGNORE'; # Do not exit on broken pipes.
}
our (@EXPORT_OK, $VERSION);

#######################################################################
# Global Configuration Variables                                      #
#######################################################################

# TODO: Need to link these constants with DB code.
# Filesystem Status Identifiers
our $STATUS_DELETED  = 0;
our $STATUS_ADDED    = 1;
our $STATUS_MODIFIED = 2;

# TODO: Need to link these constants with DB code.
# Set hash value to this constant, if unable to compute. 
our $HASH_UNKNOWN    = 'UNKNOWN';
# Set type value to this constant, if unable to compute. 
our $TYPE_UNKNOWN    = 'UNKNOWN';

# The global logging object.
our $LOG = get_logger();

# Temporary global array reference, used to hold the file analysis information,
# for the baseline and check operations.
my $file_analysis = [ ];

# The global delimeter used for storing file analysis information inside
# a single string.
our $DELIMETER = ":";

# Make Dumper format more terse.
$Data::Dumper::Terse = 1;
$Data::Dumper::Indent = 0;

my %PARAMS = (
    # When set to 1, the object will forgo any type of initial baselining
    # process, upon initialization.  Otherwise, baselining will occur
    # as normal, upon initialization.
    bypass_baseline => 0,

    # An array of hashtables used to hold the file analysis information,
    # for the baseline filesystem operation.
    baseline_analysis => [ ],

    # The base list of drives/directories/files to monitor.
    monitored_directories => getVar(name => 'directories_to_check')->{name},

    # The list of drives/directories/files to ignore.
    ignored_entries => getVar(name => 'exclude_list')->{regex},
);

#######################################################################
# Private Methods Implemented                                         #
#######################################################################

# A helper function, designed to baseline the filesystem.
#
# Input: self
# Output: none
sub _baseline {
    # Extract arguments.
    my ($self) = @_;

    # Convert monitored directories to a Cygwin-style format.
    my @search_dirs;
    foreach (@{$self->{monitored_directories}}) {
        push (@search_dirs, posixpath($_));
    }

    # Save converted results back into our object.
    $self->{monitored_directories} = \@search_dirs;
    
    # Convert ignored entires to a Cygwin-style format.
    my @ignored_entries;
    foreach (@{$self->{ignored_entries}}) {
        push (@ignored_entries, posixpath($_));
    }

    # Save converted results back into our object.
    $self->{ignored_entries} = \@ignored_entries;

    # Analyze filesystem.
    $self->_analyze();

    # Save analyzed results to object's baseline array.
    $self->{baseline_analysis} = $file_analysis;
}

# A helper function, designed to analyze the filesystem
# and create entries of filesystem objects.
#
# Input: self
# Output: none
sub _analyze {
    # Extract arguments.
    my ($self) = @_;

    # Clear previous analysis array.
    $file_analysis = [ ];

    # Search the filesystem.
    # Trap and ignore all warnings from the find operation.
    {
        no warnings;
	    find(\&_processFile, @{$self->{monitored_directories}});
    };
}

# A helper callback function, designed to populate the $file_analysis
# global array reference with hashtable entries about filesystem objects.
#
# Input: none
# Output: none
sub _processFile {
    # Get file stats.
	my @attr = stat($File::Find::name);

    # Create a new entry.
    my $entry = {
        name  => defined($File::Find::name) ? $File::Find::name : 'UNKNOWN',
        size  => defined($attr[7]) ? $attr[7] : 0,
        mtime => defined($attr[9]) ? $attr[9] : 0,
    };

    # Push entry onto analysis array.
	push (@{$file_analysis}, $entry);
}

# A helper callback function, designed to stringify each filesystem
# entry object.  Used by Algorithm::Diff operations.
#
# Input: filesystem entry
# Output: unique string
sub _toString {
    # Extract arguments.
    my ($entry) = @_;

    # Check to make sure that each entry is defined.
    my $name  = defined($entry->{name})  ? $entry->{name}  : "";
    my $size  = defined($entry->{size})  ? $entry->{size}  : "";
    my $mtime = defined($entry->{mtime}) ? $entry->{mtime} : "";

    my $string = $name  . $DELIMETER .
                 $size  . $DELIMETER .
                 $mtime . $DELIMETER;

    return $string;
}

# A helper function, designed to take the output of an Algorithm::Diff object
# and return a list of changes found in the filesystem.
#
# Input: Algorithm::Diff object
# Output: Array reference of hashtables
# Notes: This function returns hashtables in the following
# format:
#
#  $changes = [ {
#      # Indicates if the filesystem entry was deleted,
#      # added, or changed.
#      'status' => $STATUS_DELETED | $STATUS_ADDED | $STATUS_MODIFIED,
#
#      # If the entry has been added/changed, then this 
#      # hashtable contains the file/directory's new information.
#      'new' => {
#          'name'  => 'C:\WINDOWS\SYSTEM32...',
#          'size'  => 1263, # in bytes
#          'mtime' => 1178135092, # modification time, seconds since epoch
#      },
#
#      # If the entry has been deleted/changed, then this
#      # hashtable contains the file/directory's old information.
#      'old' => {
#          'name'  => 'C:\WINDOWS\SYSTEM32...',
#          'size'  => 802, # in bytes
#          'mtime' => 1178135028, # modification time, seconds since epoch
#      },
#  }, ]
sub _diff {

    # Extract arguments.
   	my ($self, $diff) = @_;

    # List of changes found.
	my $ret = [ ];

    # Temporary variables.
    my $index;
    my $old_entry;
    my $new_entry;

	while ($diff->Next()) {
        # Ignore all matches.
	    next if $diff->Same();

        # Check if entries were deleted.
	    if(!$diff->Items(2)) {
	        for ($diff->Items(1)) {
                # Make Dumper format more terse.
                $Data::Dumper::Terse = 1;
                $Data::Dumper::Indent = 0;
                $LOG->debug("File Deleted - " . Dumper($_));

                push (@{$ret}, {
                    'status' => $STATUS_DELETED,
                    'old' => $_,
                });
	        }
        # Check if entries were added.
	    } elsif(!$diff->Items(1)) {
	        for ($diff->Items(2)) {
                # Make Dumper format more terse.
                $Data::Dumper::Terse = 1;
                $Data::Dumper::Indent = 0;
                $LOG->debug("File Added - " . Dumper($_));

                push (@{$ret}, {
                    'status' => $STATUS_ADDED,
                    'new' => $_,
                });
	        }
        # Check if entries are different.
	    } else {
	        # This is the complicated case where there may be a single change or
	        # multiple changes which are contiguous
	        my $size_of_1 = scalar($diff->Items(1));
	        my $size_of_2 = scalar($diff->Items(2));

	        # There are multiples, but the same number from each scan
	        if ($size_of_1 == $size_of_2) {

	            $index = 0;
	            for ($diff->Items(1)) {
                    $old_entry = $_;
                    $new_entry = ($diff->Items(2))[$index];

                    # If the entry names are the same, then we know the contents
                    # of the entry have changed.
                    if ($old_entry->{name} eq $new_entry->{name}) {

                        # Make Dumper format more terse.
                        $Data::Dumper::Terse = 1;
                        $Data::Dumper::Indent = 0;
                        $LOG->debug("File Changed - Old - " . Dumper($old_entry) .
                                                " - New - " . Dumper($new_entry));

                        push (@{$ret}, {
                            'status' => $STATUS_MODIFIED,
                            'old' => $old_entry,
                            'new' => $new_entry,
                        });
                    # Otherwise, the old entry got deleted and the new entry got
                    # added.
                    } else {
                        # Make Dumper format more terse.
                        $Data::Dumper::Terse = 1;
                        $Data::Dumper::Indent = 0;
                        $LOG->debug("File Deleted - " . Dumper($old_entry));
                        $LOG->debug("File Added - "   . Dumper($new_entry));

                        push (@{$ret}, {
                            'status' => $STATUS_DELETED,
                            'old' => $old_entry,
                        });
                        push (@{$ret}, {
                            'status' => $STATUS_ADDED,
                            'new' => $new_entry,
                        });
                    }

	                $index++;
	            }
	        # There are more contiguous entries in the baseline.
	        } elsif ($size_of_1 > $size_of_2) {
	            $index = 0;
	            for ($diff->Items(1)) {
                    $old_entry = $_;
                    $new_entry = ($diff->Items(2))[$index];

                    # If the entry names are the same, then we know the contents
                    # of the entry have changed.
                    if (defined($new_entry) && ($old_entry->{name} eq $new_entry->{name})) {
                        # Make Dumper format more terse.
                        $Data::Dumper::Terse = 1;
                        $Data::Dumper::Indent = 0;
                        $LOG->debug("File Changed - Old - " . Dumper($old_entry) .
                                                " - New - " . Dumper($new_entry));

                        push (@{$ret}, {
                            'status' => $STATUS_MODIFIED,
                            'old' => $old_entry,
                            'new' => $new_entry,
                        });
                        $index++;
                    # Otherwise, the old entry got deleted and the new entry got
                    # added (possibly).
                    } else {
                        # Make Dumper format more terse.
                        $Data::Dumper::Terse = 1;
                        $Data::Dumper::Indent = 0;
                        $LOG->debug("File Deleted - " . Dumper($old_entry));

                        push (@{$ret}, {
                            'status' => $STATUS_DELETED,
                            'old' => $old_entry,
                        });
                        # Mark the new entry as added, if and only if it's defined
                        # and NOT the final new entry in this chunk.
                        if (defined($new_entry) && ($index < ($size_of_2 - 1))) {
                            # Make Dumper format more terse.
                            $Data::Dumper::Terse = 1;
                            $Data::Dumper::Indent = 0;
                            $LOG->debug("File Added - "   . Dumper($new_entry));
                            push (@{$ret}, {
                                'status' => $STATUS_ADDED,
                                'new' => $new_entry,
                            });
	                        $index++;
                        }
                    }
	            }
                # If we still have a final new entry to process, and we're finished
                # with all old entries, then we know the new entry was a filesystem
                # addition.
                if ($index < $size_of_2) {
                    # Make Dumper format more terse.
                    $Data::Dumper::Terse = 1;
                    $Data::Dumper::Indent = 0;
                    $LOG->debug("File Added - "   . Dumper($new_entry));
                    push (@{$ret}, {
                        'status' => $STATUS_ADDED,
                        'new' => $new_entry,
                    });
                }
	        # There are more contiguous entries in the second scan.
	        } else {
	            $index = 0;
	            for ($diff->Items(2)) {
                    $old_entry = ($diff->Items(1))[$index];
                    $new_entry = $_;

                    # If the entry names are the same, then we know the contents
                    # of the entry have changed.
                    if (defined($old_entry) && ($old_entry->{name} eq $new_entry->{name})) {
                        # Make Dumper format more terse.
                        $Data::Dumper::Terse = 1;
                        $Data::Dumper::Indent = 0;
                        $LOG->debug("File Changed - Old - " . Dumper($old_entry) .
                                                " - New - " . Dumper($new_entry));
                        push (@{$ret}, {
                            'status' => $STATUS_MODIFIED,
                            'old' => $old_entry,
                            'new' => $new_entry,
                        });
                        $index++;
                    # Otherwise, the old entry got (possibly) deleted and the new entry got
                    # added.
                    } else {
                        # Make Dumper format more terse.
                        $Data::Dumper::Terse = 1;
                        $Data::Dumper::Indent = 0;
                        $LOG->debug("File Added - "   . Dumper($new_entry));
                        push (@{$ret}, {
                            'status' => $STATUS_ADDED,
                            'new' => $new_entry,
                        });
                        # Mark the old entry as deleted, if and only if it's defined
                        # and NOT the final old entry in this chunk.
                        if (defined($old_entry) && ($index < ($size_of_1 - 1))) {
                            # Make Dumper format more terse.
                            $Data::Dumper::Terse = 1;
                            $Data::Dumper::Indent = 0;
                            $LOG->debug("File Deleted - "   . Dumper($old_entry));
                            push (@{$ret}, {
                                'status' => $STATUS_DELETED,
                                'old' => $old_entry,
                            });
	                        $index++;
                        }
                    }
	            }
                # If we still have a final old entry to process, and we're finished
                # with all the new entries, then we know the old entry was a filesystem
                # deletion.
                if ($index < $size_of_1) {
                    # Make Dumper format more terse.
                    $Data::Dumper::Terse = 1;
                    $Data::Dumper::Indent = 0;
                    $LOG->debug("File Deleted - "   . Dumper($old_entry));
                    push (@{$ret}, {
                        'status' => $STATUS_DELETED,
                        'old' => $old_entry,
                    });
                }
	        }
	    }
	}
	return $ret;
}

# A helper function, designed to filter out changes that should be
# ignored and correlate matching add/delete entries as changes,
# instead of separate add/delete entries.
#
# Input: Array reference of hashtables 
# Output: Array reference of hashtables (filtered)
# Notes: This function expects and returns hashtables in the following
# format:
#
#  $changes = [ {
#      # Indicates if the filesystem entry was deleted,
#      # added, or changed.
#      'status' => $STATUS_DELETED | $STATUS_ADDED | $STATUS_MODIFIED,
#
#      # If the entry has been added/changed, then this 
#      # hashtable contains the file/directory's new information.
#      'new' => {
#          'name'  => 'C:\WINDOWS\SYSTEM32...',
#          'size'  => 1263, # in bytes
#          'mtime' => 1178135092, # modification time, seconds since epoch
#      },
#
#      # If the entry has been deleted/changed, then this
#      # hashtable contains the file/directory's old information.
#      'old' => {
#          'name'  => 'C:\WINDOWS\SYSTEM32...',
#          'size'  => 802, # in bytes
#          'mtime' => 1178135028, # modification time, seconds since epoch
#      },
#  }, ]
sub _filter {
	my ($self, $changes) = @_;
	my $ret = [];

    my $entries_by_name = { };
	foreach (@{$changes}) {
        # Extract the file name from each entry.
        my $name = undef;
        if (($_->{status} == $STATUS_ADDED) or ($_->{status} == $STATUS_MODIFIED)) {
            $name = $_->{'new'}->{name};
        } else {
            $name = $_->{'old'}->{name};
        }
        
        # Check to make sure a name exists, skip if not found.
        if (!defined($name)) {
            next;
        }

        # Set an exclude flag.
		my $exclude_flag = 0;
        foreach (@{$self->{ignored_entries}}) {
   			if ($name =~ /^$_$/i) {
   				$exclude_flag = 1;
                last; # We only need to set the flag once.
			}
        }
        
        # Skip if excluded.
        if ($exclude_flag) {
            $LOG->debug("Excluding '" . win32path($name) . "' from integrity checks.");
            next;
        }

        # Check to see if an entry with the same name has already
        # been pushed onto our return array.
        if (exists($entries_by_name->{$name}) &&
            defined($entries_by_name->{$name})) {

            $LOG->debug("Correlating multiple filesystem changes for '" . $name . "'.");

            my $prev_entry = $entries_by_name->{$name};
            my $curr_entry = $_;

            # Sanity check.
            if ((($prev_entry->{status} == $STATUS_MODIFIED) ||
                 ($curr_entry->{status} == $STATUS_MODIFIED)) ||
                (($prev_entry->{status} == $STATUS_ADDED) &&
                 ($curr_entry->{status} == $STATUS_ADDED)) ||
                (($prev_entry->{status} == $STATUS_DELETED) &&
                 ($curr_entry->{status} == $STATUS_DELETED))) {
                $LOG->error("Duplicate filesystem change entries were found. " .
                            "Previous Entry - " . Dumper($prev_entry) . " - ".
                            "Current Entry - " . Dumper($curr_entry));
                push (@{$ret}, $_);
                next;
            }

            # If the previous entry was added and the current
            # was deleted.
            if (($prev_entry->{status} == $STATUS_ADDED) &&
                ($curr_entry->{status} == $STATUS_DELETED)) {
                $prev_entry->{status} = $STATUS_MODIFIED;
                $prev_entry->{old} = $curr_entry->{old};

            # Otherwise, if the previous entry was deleted and the
            # current was added.
            } else {
                $prev_entry->{status} = $STATUS_MODIFIED;
                $prev_entry->{'new'} = $curr_entry->{'new'};
            }

        } else {
            # The entry is completely new, so record it.
            $entries_by_name->{$name} = $_;

            # And push it onto our return array.
            push (@{$ret}, $_);
        }
    }
	return $ret;
}

# A helper function, designed to manipulate the array of changes into 
# a format that is expected by the check() function -- collecting
# more forensic data about each change along the way.
#
# Input: Array reference of hashtables 
# Output: Array reference of hashtables (manipulated)
# Notes: This function expects hashtables in the following
# format:
#
#  $inputChanges = [ {
#      # Indicates if the filesystem entry was deleted,
#      # added, or changed.
#      'status' => $STATUS_DELETED | $STATUS_ADDED | $STATUS_MODIFIED,
#
#      # If the entry has been added/changed, then this 
#      # hashtable contains the file/directory's new information.
#      'new' => {
#          'name'  => 'C:\WINDOWS\SYSTEM32...',
#          'size'  => 1263, # in bytes
#          'mtime' => 1178135092, # modification time, seconds since epoch
#      },
#
#      # If the entry has been deleted/changed, then this
#      # hashtable contains the file/directory's old information.
#      'old' => {
#          'name'  => 'C:\WINDOWS\SYSTEM32...',
#          'size'  => 802, # in bytes
#          'mtime' => 1178135028, # modification time, seconds since epoch
#      },
#  }, ]
#
# And outputs hashtables in the following format:
# 
#  $outputChanges = [ {
#      # Indicates if the filesystem entry was deleted,
#      # added, or changed.
#      'status' => $STATUS_DELETED | $STATUS_ADDED | $STATUS_MODIFIED,
#      'name'  => 'C:\WINDOWS\SYSTEM32...',
#      'mtime' => 'YYYY-MM-DD HH:MM:SS', # new mtime for added/modified files;
#                                        # old mtime for deleted files
#
#      # content will only exist for added/modified files
#      'content' => {
#          'size' => 1263,                                       # size of new content 
#          'type' => 'application/octet-stream',                 # type of new content
#          'md5'  => 'b1946ac92492d2347c6235b4d2611184',         # md5  of new content
#          'sha1' => 'f572d396fae9206628714fb2ce00f72e94f2258f', # sha1 of new content
#      },
#  }, ]
#
sub _prepare {
	my ($self, $changes) = @_;
	my $ret = [];

    $LOG->debug("Preparing changes.");

    my $md5_ctx  = Digest::MD5->new();
    my $sha1_ctx = Digest::SHA->new("1");
    my $type_ctx = File::Type->new();

    foreach my $entry (@{$changes}) {
        # Construct a new entry in the new format.
        my $newEntry = {
            'status' => $entry->{'status'},
        };

        # Figure out which type of entry it is.
        if ($entry->{'status'} == $STATUS_DELETED) {
			# Convert Filename
            $newEntry->{'name'}  = _convertFilename($entry->{'old'}->{'name'});
            $newEntry->{'mtime'} = _convertTime($entry->{'old'}->{'mtime'});
    
            $LOG->debug("Filename: " . $newEntry->{'name'});
        } else {
            $newEntry->{'name'}  = $entry->{'new'}->{'name'};
            $newEntry->{'mtime'} = _convertTime($entry->{'new'}->{'mtime'});

            $LOG->debug("Filename: " . $newEntry->{'name'});

            # Create a new file handle.
            my $fh = IO::File->new($newEntry->{'name'}, "r");
            my $md5  = $HASH_UNKNOWN;
            my $sha1 = $HASH_UNKNOWN;
            my $type = $TYPE_UNKNOWN;

            # Check to make sure the new/changed file exists.
            if (defined($fh)) {
                # If the entry is a directory.
                if (-d $fh) {
                    $type = "directory";
                    undef $fh;

                    # XXX: We currently skip all entries that
                    # only correspond to directories.
                    # This is a known limitation.
                    next;

                # If the entry is a symlink.
                } elsif (-l $newEntry->{'name'}) {
                    $type = "symlink";
                    undef $fh;

                    # XXX: We currently skip all entries that
                    # only correspond to symlinks.
                    # This is a known limitation.
                    next;

                # If the entry is a file.
                } else {
                    # Compute MD5 Checksum.
                    $md5_ctx->addfile($fh);
                    $md5 = $md5_ctx->hexdigest();

                    # Rewind file handle.
                    seek($fh, 0, 0);

                    # Compute SHA1 Checksum.
                    $sha1_ctx->addfile($fh);
                    $sha1 = $sha1_ctx->hexdigest();

                    # Close the file handle.
                    undef $fh;

                    # Compute File Type.
                    $type = $type_ctx->mime_type($newEntry->{'name'});
               }
            }
            
            # Populate the content, accordingly.
            $newEntry->{'content'} = {
                'size' => $entry->{'new'}->{'size'},
                'type' => $type,
                'md5'  => $md5,
                'sha1' => $sha1,
            };

			# Convert Filename
            $newEntry->{'name'}  = _convertFilename($newEntry->{'name'});
        }

        # Finally, push it onto our return array.
        push (@{$ret}, $newEntry);
    }
	return $ret;
}

# Helper function, designed to convert seconds since epoch to
# an ISO 8601 date time format.
#
# Input: epoch
# Output: iso8601 date/time
sub _convertTime {
    my $dt = DateTime->from_epoch(epoch => shift);
    return $dt->ymd('-') . " " . $dt->hms(':');
}

# Helper function, designed to convert Cygwin filename paths to
# a Windows format, where the output is always lowercase.
#
# Input: cygwin filename path
# Output: absolute windows filename path
sub _convertFilename {
    my $path = shift;

    # Unfortunately Filesys::CygwinPaths seems to like
    # to follow symbolic links, when resolving win32 paths.
    # This is bad.  To counter this, we make sure the filename
    # we give it isn't a valid symlink so that it can properly
    # perform the conversion.
    if (-l $path) {
        $path .= "*";
        $path = lc(fullwin32path($path));
        chop($path);
        return $path;
    } else {
	    return lc(fullwin32path($path));
    }
}

#######################################################################
# Public Methods Implemented                                          #
#######################################################################

sub new {
    # - This function takes in an optional hashtable,
    #   that contains various key => 'value' configuration
    #   parameters.
    #
    # - For each parameter given, it overwrites any corresponding
    #   parameters specified within the default hashtable, %PARAMS,
    #   with custom entries that were given as parameters.
    #
    # - Finally, it returns a blessed instance of the
    #   merged hashtable, as an 'object'.

    # Get the class name.
    my $self = shift;

    # Get the rest of the arguments, as a hashtable.
    # Hash-based arguments are used, since HoneyClient::Util::SOAP is unable to handle
    # hash references directly.  Thus, flat hashtables are used throughout the code
    # for consistency.
    my %args = @_;

    # Check to see if the class name is inherited or defined.
    my $class = ref($self) || $self;

    # Initialize default parameters.
    $self = { };
    my %params = %{dclone(\%PARAMS)};
    @{$self}{keys %params} = values %params;

    # Now, overwrite any default parameters that were redefined
    # in the supplied arguments.
    @{$self}{keys %args} = values %args;

    # Now, assign our object the appropriate namespace.
    bless $self, $class;

    # Perform baselining, if not bypassed.
    if (!$self->{'bypass_baseline'}) {
        $LOG->info("Baselining filesystem.");
        $self->_baseline();
    }

    # Finally, return the blessed object.
    return $self;
}

sub check {

    # Extract arguments.
    my ($self, %args) = @_;

    # Sanity check: Make sure we've been fed an object.
    unless (ref($self)) {
        $LOG->error("Error: Function must be called in reference to a " .
                    __PACKAGE__ . "->new() object!");
        Carp::croak "Error: Function must be called in reference to a " .
                    __PACKAGE__ . "->new() object!\n";
    }

    # Log resolved arguments.
    $LOG->debug(sub {
        # Make Dumper format more terse.
        $Data::Dumper::Terse = 1;
        $Data::Dumper::Indent = 0;
        Dumper(\%args);
    });

    # Sanity checks; check if any args were specified.
    my $argsExist = scalar(%args);

    # Analyze the filesystem.
    $LOG->info("Analyzing filesystem.");
    $self->_analyze();

    # Compare analysis with baseline.
    $LOG->info("Checking for filesystem changes.");
    my $changes = $self->_diff(Algorithm::Diff->new($self->{baseline_analysis},
                                                    $file_analysis,
                                                    { keyGen => \&_toString }));
    # Filter results.
    $changes = $self->_filter($changes);
    if (scalar(@{$changes})) {
        $LOG->warn("Filesystem changes found.");
    } else {
        $LOG->info("No filesystem changes found.");
    }

    # Prepare results, if not directed otherwise.
    if (!$argsExist || 
        !exists($args{'no_prepare'}) || 
        !defined($args{'no_prepare'}) ||
        !$args{'no_prepare'}) {
        $changes = $self->_prepare($changes);
    }

    # Return formatted results.
    return $changes;
}

1;