#!/usr/bin/perl 
#
# acct_check.pl
#
# This program is a user account analyzer.  It checks the following
# things related to a user's account:
#
#     - SHELL related files
#     - user defined files
#     - permissions on these files
#     - checksums of these files
#     - file dates of these files
#     - PATH environment variable
#     - EDITOR environment variable
#     - umask environment
#
# If any differences in permissions, checksums, or dates are reported,
# a message is sent to the user and the user is asked if he/she wants
# to update the master file list of these attributes.
#
# The PATH enviroment is checked for any changes since the last 
# invocation of the program, and is also checked for any instances of
# '.'.  The user is warned if anything has changed.
#
# The EDITOR enviroment is checked for any changes since the last
# invocation of the program, and warns the user.
#
# The umask environment is checked for any changes or if the umask is
# too weak, and the user is warned.
#
# Notes:
# 
#---------------------------------------------------------------------
#

# enviroment include, get only the ENV's we're interested in
use Env qw(HOME SHELL PATH EDITOR);

# md5 shell command for checksumming
use Shell qw(md5);

# defines for shell specific files
#
# these files are scanned on top of the ones listed in the dat file
# they are subject to the same criteria as other files
#
# bash and tcsh look for sh and csh files, respectively.
# zsh has a ton of files...  nightmare...  almost as bad as bash.
#
@sh_files = (".profile",".shrc",".history");
@csh_files = (".login",".logout",".chsrc",".history");
@ksh_files = (".kshrc",".profile",".history");
@bash_files = (".bash_profile",".profile",".bashrc",".bash_history",
   ".history",".shrc", ".inputrc");
@tcsh_files = (".tcshrc",".cshrc:",".login",".logout",".history");
@zsh_files = (".zshenv",".zlogin",".zlogin",".zshrc",".zprofile",
   ".history");


#---------------------------------------------------------------------

# # # # # # # # # #
# Main Program
# Gets info from file, feeds it to the rest of the program
#

# check for args
die "Usage:  $0 [data file] | -h | -?\n" unless @ARGV <= 1;

# grab arg and clear ARGV
$filename = shift(@ARGV);
@ARGV = ();

$filename = "file.dat" unless defined($filename);

# print some online help
if ($filename eq "-h" || $filename eq "-?") {
   print <<'EOM';

Static Account Anaylzer - v0.6
----------------------------------------------------------------------
This program is a static account analyzer.  It will scan your
home directory for files and check their attributes.  You can
pre-define files by making a data file of the form:

   filename,checksum,filemode,filecreation,filemodification

If you do not know any values, leave them as 0 and the program
will fill them in for you.  Call this file 'file.dat'.  Or call
the file anything you want and invoke the program as:

   analyzer [filename]

Usage:  analyzer [data file]

EOM
  
   die;
}

# open up the file
open(FILE, $filename) or 
   die "Can't open $filename: $!\nUsage:  $0 [data file] | -h | -?\n";

# print the banner and get things going
print <<'BANNER';

Static Account Anaylzer - v0.6
----------------------------------------------------------------------

BANNER

@shell_grab = split(/\//,$SHELL);
$shell_env = $shell_grab[$#shell_grab];


# let the user know what's going on
print <<'EOM';
+-------------------------------------------------
| File Check
+-------------------------------------------------

EOM

# This is gonna be ugly...
# perl doesn't have a case structure, which would really be handy
# for this part...  unfortunately, nothing else is working right for
# me, so it's going to have to be an ugly if/else ladder...  ewww....

if    ($shell_env eq "sh")   { @shell_files = @sh_files }
elsif ($shell_env eq "csh")  { @shell_files = @csh_files }
elsif ($shell_env eq "ksh")  { @shell_files = @ksh_files }
elsif ($shell_env eq "bash") { @shell_files = @bash_files }
elsif ($shell_env eq "tcsh") { @shell_files = @tcsh_files }
elsif ($shell_env eq "zsh")  { @shell_files = @zsh_files }
else {
   @shell_files = @sh_files;
   print "Unknown shell.  Defaulting to 'sh' files\n";
}

print "You're running $shell_env.\n";
print "Checking permissions on these files automatically:\n";

foreach $file (@shell_files) {
   print "$file ";
}
print "\n\n";

# start checking the dot files

foreach $file (@shell_files) {
   if (-e "$HOME/$file") { 
      &Check_dotFile("$HOME/$file"); 
   }
}

# move on to the files from the file
# open a new file for writing the changes back to
open(NEWFILE, ">file.new") or 
   die "can't open new file: $!\n";

print "\nChecking files from $filename\n\n";

while (<FILE>) {              # grab a line

   @line = split(/,/);        # split it into parts on commas

   if (-e $line[0]) {
      $new_value = &Check_File(@line);
      chomp($new_value);
      print NEWFILE "$new_value\n";
   }
   else {
      print "Can't find $line[0]...  Removing from list\n";
   }
}

# cleanup
close(NEWFILE);
close(FILE);

# make changes.
rename "file.new", $filename;


print <<'EOM';

+-------------------------------------------------
| Environment Check
+-------------------------------------------------

EOM

# check for environment file, or open a new one.
open(ENVFILE, "envir.dat");
open(NEWENVFILE, ">envir.new");


if (defined(ENVFILE)) {

   $temp = <ENVFILE>;               # grab path
   chomp($temp);
   @curr_path = split(/:/, $temp);

   $temp = <ENVFILE>;               # grab editor
   chomp($temp);
   $curr_edit = $temp;

   $temp = <ENVFILE>;               # grab umask
   chomp($temp);
   $curr_umask = $temp;
}
else {                              # defaults if none found
   @curr_path = ();
   $curr_edit = "";
   $curr_umask = "";
}

print "Checking PATH...\n";

$newpath = &Check_Path(@curr_path);
print NEWENVFILE "$newpath\n";

print "\nChecking EDITOR...\n";
$newedit = &Check_Editor($curr_edit);
print NEWENVFILE "$newedit\n";

print "\nChecking umask...\n";
$newumask = &Check_umask($curr_umask);
print NEWENVFILE "$newumask";

close(NEWENVFILE);
close(ENVFILE);
rename "envir.new", "envir.dat";



# # # # # # # # # #
# End Main
#

#---------------------------------------------------------------------

# # # # # # # # # #
# Begin Functions
#

# # # # # # # # # #
# Check_Path
#
# Checks path for any abnormailities.
# Input:
#    List of path items from file
# Output:
#    Message if there is a difference found
# Returns updated path, if user wants
sub Check_Path {

   my @curr_path = @_;                 # grab args
   my @path_env = split(/:/, $PATH);   # grab $PATH env

   foreach $item (@path_env) {
      $curr_item = shift(@curr_path);

      if ($curr_item ne $item) {       # compare path
         print "PATH WARNING:  expected $curr_item, got $item\n";
         $path_changed = 1;
      }

      push(@new_path, $curr_item);      

      print "PATH WARNING:  '.' in path\n" unless $item ne ".";
   }

   # make the path managable again
   foreach $item (@new_path) {
      if (defined($path_temp)) {
         $path_temp = $path_temp.":".$item;
      }
      else {
         $path_temp = $item;
      }
   }

   if (defined($path_changed)) {       # ask user to change
      print "Do you wish to change your PATH? [y/N]";
      my $test = <STDIN>;
      chomp($test);
      if ($test eq "Y" || $test eq "y") {
         return $PATH;
      }
      else {
         return $path_temp;
      }
   }
   print "Path checks out\n";
   return $path_temp;
}

# # # # # # # # # #
# Check_Editor
# Checks if $EDITOR has changed
# Input:
#    EDITOR from file
# Output:
#    Warning if changed
# Returns current editor, if user wants
sub Check_Editor {

   my $curr_edit = shift(@_);    
   my $edit_env = $EDITOR;
      
   if ($curr_edit ne $edit_env) {
      print "EDITOR WARNING:  editor has changed from ";
      print "$curr_edit to $edit_env\n";

      print "Do you wish to change your EDITOR? [y/N]";
      $test = <STDIN>;
      chomp($test);
      if ($test eq "Y" || $test eq "y") {
         return $edit_env;
      }
      else {
         return $curr_edit;
      }
   }
   else {
      print "Editor checks out\n";
      return $curr_edit;
   }
}


# # # # # # # # # #
# Check_umask
# Checks if umask has changed or is weak
# Input:
#    umask from file
# Output:
#    Warning if changed
#    Warning if umask is too weak
# Returns current umask, if user wants
sub Check_umask {

   my $curr_umask = shift(@_);
   my $umask_env = umask;

   $umask_oct = sprintf("%lo", $umask_env);
   $cumask_oct = sprintf("%lo", $curr_umask);

   if ($umask_env lt "022") {
      print "UMASK WARNING:  umask of $umask_oct is too weak.\n";
      print "Suggest changing it to better than 022.  See 'man $shell_env'\n";
   }

   if ($curr_umask ne $umask_env) {
      print "UMASK WARNING:  umask has changed from ";
      print "$cumask_oct to $umask_oct\n";

      print "Do you wish to change your umask? [y/N]";
      my $test = <STDIN>;
      chomp($test);
      if ($test eq "Y" || $test eq "y") {
         return $umask_env;
      }
      else {
         return $curr_umask;
      }
   }
   else {
      print "umask checks out\n";
      return $curr_umask;
   }
}


# # # # # # # # # #
# Check_file 
# Checks file attributes
# Input:
#    filename          - character string
#    checksum          - md5 hash string
#    permission        - numeric string
#    creation date     - numeric string
#    modification date - numeric string
# Output:
#    if file doesn't exist, print warning
#    if checksum is invalid, print warning
#    if permission doesn't check, print warning
#    if permissions are weak, print warning
#    if dates have changed, print warning
# Returns changes to file, if user wants.
sub Check_File {

   my ($fname, $checksum, $fmode, $fctime, $fmtime) = @_;

   # get file stat
   # stat returns 13 elements, so we'll grab them all
   ($dev, $ino, $mode, $nlink, $uid, $gid, $rdev, $size, $atime, $mtime,
    $ctime, $blksize, $blocks) = stat $fname;

   # check for file owner against environment uid
   # $< returns the uid of the current user
   if ($< ne $uid) {
      print "FILE WARNING:  you don't own $fname\n";
   }

   # convert mode to octal
   $mode_oct = sprintf("%lo", $mode);
   $fmode_oct = sprintf("%lo", $fmode);

   # compare modes 
   if ($fmode_oct ne $mode_oct) {
      print "FILE WARNING:  $fname mode change from $fmode_oct to $mode_oct\n";
      $mode_change = 1;
   }

   # convert to decimal
   $weak_mode = oct(100664);

   # check for a weak mode
   if ($mode > $weak_mode) {
      print "FILE WARNING:  $fname of mode $mode_oct is world ";
      print "readable/writeable\n";
   }

   # check creation date
   if ($fctime != $ctime) {
      print "FILE WARNING:  $fname creation date changed from ";
      print "$fctime to $ctime\n";
      $ctime_change = 1;
   }

   # check modification date
   if ($fmtime != $mtime) {
      print "FILE WARNING:  $fname has been modified on $mtime\n";
      $mtime_change = 1;
   }

   # do checksum on file
   if ($checksum ne &CheckSum_File($fname)) {
      print "FILE WARNING:  $fname checksum changed\n";
      $checksum_change = 1;
   }

   #
   # Give the user the chance to change settings
   #

   if (defined($mode_change)) {
      print "Do you wish to update your mode for $fname? [y/N]";
      my $test = <STDIN>;
      chomp($test);
      if ($test eq "Y" || $test eq "y") {
         $new_mode = $mode;
      }
      else {
         $new_mode = $fmode;
      }
   }
   else {
      $new_mode = $fmode;
   }

   if (defined($ctime_change)) {
      print "Do you wish to update your creation time for $fname? [y/N]";
      $test = <STDIN>;
      chomp($test);
      if ($test eq "Y" || $test eq "y") {
         $new_ctime = $ctime;
      }
      else {
         $new_ctime = $fctime;
      }
   }
   else {
      $new_ctime = $fctime;
   }

   if (defined($mtime_change)) {
      print "Do you wish to update your modification time for $fname? [y/N]";
      $test = <STDIN>;
      chomp($test);
      if ($test eq "Y" || $test eq "y") {
         $new_mtime = $mtime;
      }
      else {
         $new_mtime = $fmtime;
      }
   }
   else {
      $new_mtime = $fmtime;
   }

   if (defined($checksum_change)) {
      print "Do you wish to update your checksum for $fname? [y/N]";
      $test = <STDIN>;
      chomp($test);
      if ($test eq "Y" || $test eq "y") {
         $new_checksum = &CheckSum_File($fname);
      }
      else {
         $new_checksum = $checksum;
      }
   }
   else {
      $new_checksum = $checksum;
   }

   print "$fname checks out\n";

   $ret_str = $fname.",".$new_checksum.",".$new_mode.",";
   $ret_str = $ret_str.$new_ctime.",".$new_mtime;

   # return the new string for the file
   # "fname,checksum,mode,ctime,mtime"
   return $ret_str;

}

# # # # # # # # # #
# Check_dotFile
# Stripped down version of Check_File
# Only checks for unsafe permissions
# Input:
#    File name
# Output:
#    Warning if permissions are weak
#
sub Check_dotFile {

   my ($fname) = shift(@_);

   # get file stat
   # stat returns 13 elements, so we'll grab them all
   ($dev, $ino, $mode, $nlink, $uid, $gid, $rdev, $size, $atime, $mtime,
    $ctime, $blksize, $blocks) = stat $fname;

   # check for file owner against environment uid
   # $< returns the uid of the current user
   if ($< ne $uid) {
      print "FILE WARNING:  you don't own $fname\n";
   }

   # convert mode to octal
   $mode_oct = sprintf("%lo", $mode);

   # convert to decimal
   $weak_mode = oct(100664);

   # check for a weak mode
   if ($mode > $weak_mode) {
      print "FILE WARNING:  $fname of mode $mode_oct is world ";
      print "readable/writeable\n";
   }
   else {
      print "$fname checks out\n";
   }
}


# # # # # # # # # #
# CheckSum_File
# Performs checksum of file
# Input:
#    File name
# Output:
#    None
# Returns checksum string for file
sub CheckSum_File {
   my $fname = shift(@_);              # grab args
   my $md5_out = md5($fname);          # do checksum

   chomp($md5_out);                    # strip \n

   @output = split(/ = /, $md5_out);   # massage output
                                       # 'MD5 (filename) = checksum'
                                       # so we'll split on ' = ' and get
                                       # 2 elements.
   return $output[1];                  # return md5 hash, the last element
}   


# # # # # # # # # #
#
# End Of File
#
# # # # # # # # # #

