# Copyright (C) 1999-2007 Jay Beale # Copyright (C) 2001-2008 Hewlett-Packard Development Company, L.P. # Licensed under the GNU General Public License, version 2 package Bastille::API; ## TO DO: # # # 1) Look for more places to insert error handling... # # 2) Document this module more # # ########################################################################## # # This module forms the basis for the v1.1 API. # ########################################################################## # # This module forms the initial basis for the Bastille Engine, implemented # presently via a Perl API for Perl modules. # # This is still under construction -- it is very usable, but not very well # documented, yet. # ########################################################################## # # API Function Listing # ########################################################################## # The routines which should be called by Bastille modules are listed here, # though they are better documented throughout this file. # # Distro Specific Stuff: # # &GetDistro - figures out what distro we're running, if it knows it... # &ConfigureForDistro - sets global variables based on the distro # &GetGlobal - returns hash values defined in ConfigureForDistro # # &getGlobalConfig - returns value of hash set up by ReadConfig # # Logging Specific Stuff has moved to LogAPI.pm: # # &B_log(type,msg) -- takes care of all logging # # # Input functions for the old input method... # # File open/close/backup functions # # &B_open * -- opens a file handle and logs the action/error (OLD WAY!) # &B_open_plus -- opens a pair of file handles for the old and new version # of a file; respects logonly flag. (NEW WAY) # &B_close * -- closes a file handle and logs the action/error (OLD WAY!) # &B_close_plus -- closes a pair of file handles opened by B_open_plus, # backing up one file and renaming the new file to the # old one's name, logging actions/errors. Respects the # logonly flag -- needs B_backup file. Finally, sets # new file's mode,uid,gid to old file's... (NEW WAY) # &B_backup_file - backs up a file that is being changed/deleted into the # $GLOBAL_BDIR{"backup"} directory. # # Non-content file modification functions # # &B_delete_file - deletes the named file, backing up a copy # &B_create_file - creates the named file, if it doesn't exist # # &B_symlink - create a symlink to a file, recording the revert rm # # More stuff # # &B_createdir - make a directory, if it doesn't exist, record revert rmdir # &B_cp - copy a file, respecting LOGONLY and revert func. # &B_mknod - wrap mknod with revert and logonly and prefix functionality # # &B_read_sums - reads sum.csv file and parses input into the GLOBAL_SUM hash # &B_write_sums - writes sum.csv file from GLOBAL_SUM hash # &B_check_sum($) - take a file name and compares the stored cksum with the current # cksum of said file # &B_set_sum($) - takes a file name and gets that files current cksum then sets # that sum in the GLOBAL_SUM hash # &B_revert_log - create entry in shell script, executed later by bastille -r # &showDisclaimer - Print the disclaimer and wait for 5 minutes for acceptance ########################################################################### # Note: GLOBAL_VERBOSE # # All logging functions now check GLOBAL_VERBOSE and, if set, will print # all the information sent to log files to STDOUT/STDERR as well. # # # Note: GLOBAL_LOGONLY # # All Bastille API functions now check for the existence of a GLOBAL_LOGONLY # variable. When said variable is set, no function actually modifies the # system. # # Note: GLOBAL_DEBUG # # The B_log("DEBUG",...) function now checks GLOBAL_DEBUG and, if set, it will # print all the information to a new debug-log file. If GLOBAL_VERBOSE is # set it might log to STDOUT/STDERR as well (not yet implemented, pending # discussion). Developers should populate appropriate places with &B_log(DEBUG) # in order to be able to tell users to use this options and send the logs # for inspection and debugging. # # # Libraries for the Backup_file routine: Cwd and File::Path use Cwd; use Bastille::OSX_API; use Bastille::LogAPI; use File::Path; use File::Basename; # Export the API functions listed below for use by the modules. use Exporter; @ISA = qw ( Exporter ); @EXPORT = qw( setOptions GetDistro ConfigureForDistro B_log B_revert_log SanitizeEnv B_open B_close B_symlink StopLogging B_open_plus B_close_plus B_isFileinSumDB B_create_file B_read_sums B_check_sum B_set_sum isSumDifferent listModifiedFiles B_create_dir B_create_log_file B_delete_file B_cp B_place B_mknod showDisclaimer getSupportedOSHash B_Backtick B_System isProcessRunning checkProcsForService $GLOBAL_OS $GLOBAL_ACTUAL_OS $CLI $GLOBAL_LOGONLY $GLOBAL_VERBOSE $GLOBAL_DEBUG $GLOBAL_AUDITONLY $GLOBAL_AUDIT_NO_BROWSER $errorFlag %GLOBAL_BIN %GLOBAL_DIR %GLOBAL_FILE %GLOBAL_BDIR %GLOBAL_BFILE %GLOBAL_CONFIG %GLOBAL_SUM %GLOBAL_SERVICE %GLOBAL_SERVTYPE %GLOBAL_PROCESS %GLOBAL_RC_CONFIG %GLOBAL_TEST getGlobal setGlobal getGlobalConfig B_parse_fstab B_parse_mtab B_is_rpm_up_to_date NOTSECURE_CAN_CHANGE SECURE_CANT_CHANGE NOT_INSTALLED INCONSISTENT MANUAL NOTEST SECURE_CAN_CHANGE STRING_NOT_DEFINED NOT_INSTALLED_NOTSECURE DONT_KNOW RELEVANT_HEADERQ NOTRELEVANT_HEADERQ ); ###################################################### ###Testing Functions ################################################################## #Define "Constants" for test functions. Note these constants sometimes get #interpreted as literal strings when used as hash references, so you may # have to use CONSTANT() to disambiguate, like below. Sorry, it was either # that or create even *more* global variables. # See TestDriver.pm for definitions, and test design doc for full explaination use constant { NOTSECURE_CAN_CHANGE => 0, SECURE_CANT_CHANGE => 1, NOT_INSTALLED => 2, # (where the lack makes the system secure, eg telnet) INCONSISTENT => 3, MANUAL => 4, NOTEST => 5, SECURE_CAN_CHANGE => 6, STRING_NOT_DEFINED => 7, NOT_INSTALLED_NOTSECURE => 8, #(Where the missing s/w makes the system less secure eg IPFilter) #Intentional duplicates follow DONT_KNOW => 5, RELEVANT_HEADERQ => 6, NOTRELEVANT_HEADERQ => 0 }; &SanitizeEnv; # Set up some common error messages. These are independent of # operating system # These will allow us to line up the warnings and error messages my $err ="ERROR: "; my $spc =" "; my $GLOBAL_OS="None"; my $GLOBAL_ACTUAL_OS="None"; my %GLOBAL_SUMS=(); my $CLI=''; #OS independent Error Messages Follow, normally "bastille" script filters #options before interactive or Bastille runs, so this check is often redundant $GLOBAL_ERROR{"usage"}="\n". "$spc Usage: bastille [ -b | -c | -x ] [ --os ] [ -f ]\n". "$spc bastille [ -r | --assess | --assessnobowser ]\n\n". "$spc --assess : check status of system and report in browser\n". "$spc --assessnobrowser : check status of system and list report locations\n". "$spc -b : use a saved config file to apply changes\n". "$spc directly to system\n". "$spc -c : use the Curses (non-X11) TUI\n". "$spc -f : populate answers with a different config file\n". "$spc -r : revert all Bastille changes to-date\n". "$spc -x : use the Perl/Tk (X11) GUI\n" . "$spc --os : ask all questions for the given operating system\n" . "$spc version. e.g. --os RH6.0\n"; # These options don't work universally, so it's best not to # document them here (yet). Hopefully, we'll get them # straightened out soon. #"$spc --log : log-only option\n". #"$spc -v : verbose mode\n". #"$spc --debug : debug mode\n"; ############################################################################## # # Directory structure for Bastille Linux v1.2 and up # ############################################################################## # # /usr/sbin/ -- location of Bastille binaries # /usr/lib/Bastille -- location of Bastille modules # /usr/share/Bastille -- location of Bastille data files # /etc/Bastille -- location of Bastille config files # # /var/log/Bastille -- location of Bastille log files # /var/log/Bastille/revert -- directory holding all Bastille-created revert scripts # /var/log/Bastille/revert/backup -- directory holding the original files that # Bastille modifies, with permissions intact # ############################################################################## ############################################################################## # # Directory structure for HP-UX Bastille v2.0 and up # ############################################################################## # # /opt/sec_mgmt/bastille/bin/ -- location of Bastille binaries # /opt/sec_mgmt/bastille/lib/ -- location of Bastille modules # /etc/opt/sec_mgmt/bastille/ -- location of Bastille data and config files # # /var/opt/sec_mgmt/bastille/log/ -- location of Bastille log files # /var/opt/sec_mgmt/bastille/revert -- directory holding all Bastille-created # revert scripts and save files # ############################################################################## ############################################################################## ############################################################################## ################## Actual functions start here... ########################### ############################################################################## ############################################################################## ########################################################################### # setOptions takes six arguments, $GLOBAL_DEBUG, $GLOBAL_LOGONLY, # $GLOBAL_VERBOSE, $GLOBAL_AUDITONLY, $GLOBAL_AUDIT_NO_BROWSER, and GLOBAL_OS; ########################################################################### sub setOptions($$$$$$) { ($GLOBAL_DEBUG,$GLOBAL_LOGONLY,$GLOBAL_VERBOSE,$GLOBAL_AUDITONLY, $GLOBAL_AUDIT_NO_BROWSER,$GLOBAL_OS) = @_; if ($GLOBAL_AUDIT_NO_BROWSER) { $GLOBAL_AUDITONLY = 1; } if (not(defined($GLOBAL_OS))){ $GLOBAL_OS="None"; } } ########################################################################### # # SanitizeEnv load a proper environment so Bastille cannot be tricked # and Perl modules work correctly. # ########################################################################### sub SanitizeEnv { delete @ENV{'IFS','CDPATH','ENV','BASH_ENV'}; $ENV{CDPATH}="."; $ENV{BASH_ENV}= ""; # Bin is needed here or else /usr/lib/perl5/5.005/Cwd.pm # will not find `pwd` # Detected while testing with -w, jfs $ENV{PATH} = "/bin:/usr/bin"; # Giorgi, is /usr/local/bin needed? (jfs) } ########################################################################### # # GetDistro checks to see if the target is a known distribution and reports # said distribution. # # This is used throughout the script, but also by ConfigureForDistro. # # ########################################################################### sub GetDistro() { my ($release,$distro); # Only read files for the distro once. # if the --os option was used then if ($GLOBAL_OS eq "None") { if ( -e "/etc/mandrake-release" ) { open(MANDRAKE_RELEASE,"/etc/mandrake-release"); $release=; if ( ($release =~ /^Mandrake Linux release (\d+\.\d+\w*)/) or ($release =~ /^Linux Mandrake release (\d+\.\d+\w*)/) ) { $distro="MN$1"; } elsif ( $release =~ /^Mandrakelinux release (\d+\.\d+)\b/ ) { $distro="MN$1"; } else { print STDERR "$err Couldn't determine Mandrake/Mandriva version! Setting to 10.1!\n"; $distro="MN10.1"; } close(MANDRAKE_RELEASE); } elsif ( -e "/etc/immunix-release" ) { open(IMMUNIX_RELEASE,"/etc/immunix-release"); $release=; unless ($release =~ /^Immunix Linux release (\d+\.\d+\w*)/) { print STDERR "$err Couldn't determine Immunix version! Setting to 6.2!\n"; $distro="RH6.2"; } else { $distro="RH$1"; } close(*IMMUNIX_RELEASE); } elsif ( -e '/etc/fedora-release' ) { open(FEDORA_RELEASE,'/etc/fedora-release'); $release=; close FEDORA_RELEASE; if ($release =~ /^Fedora Core release (\d+\.?\d*)/) { $distro = "RHFC$1"; } elsif ($release =~ /^Fedora release (\d+\.?\d*)/) { $distro = "RHFC$1"; } else { print STDERR "$err Could not determine Fedora version! Setting to Fedora Core 8\n"; $distro='RHFC8'; } } elsif ( -e "/etc/redhat-release" ) { open(*REDHAT_RELEASE,"/etc/redhat-release"); $release=; if ($release =~ /^Red Hat Linux release (\d+\.?\d*\w*)/) { $distro="RH$1"; } elsif ($release =~ /^Red Hat Linux .+ release (\d+)\.?\d*([AEW]S)/) { $distro="RHEL$1$2"; } elsif ($release =~ /^Red Hat Enterprise Linux ([AEW]S) release (\d+)/) { $distro="RHEL$2$1"; } elsif ($release =~ /^CentOS release (\d+\.\d+)/) { my $version = $1; if ($version =~ /^4\./) { $distro='RHEL4AS'; } elsif ($version =~ /^3\./) { $distro='RHEL3AS'; } else { print STDERR "$err Could not determine CentOS version! Setting to Red Hat Enterprise 4 AS.\n"; $distro='RHEL4AS'; } } else { # JJB/HP - Should this be B_log? print STDERR "$err Couldn't determine Red Hat version! Setting to 9!\n"; $distro="RH9"; } close(REDHAT_RELEASE); } elsif ( -e "/etc/debian_version" ) { $stable="3.1"; #Change this when Debian stable changes open(*DEBIAN_RELEASE,"/etc/debian_version"); $release=; unless ($release =~ /^(\d+\.\d+\w*)/) { print STDERR "$err System is not running a stable Debian GNU/Linux version. Setting to $stable.\n"; $distro="DB$stable"; } else { $distro="DB$1"; } close(DEBIAN_RELEASE); } elsif ( -e "/etc/SuSE-release" ) { open(*SUSE_RELEASE,"/etc/SuSE-release"); $release=; if ($release =~ /^SuSE Linux (\d+\.\d+\w*)/i) { $distro="SE$1"; } elsif ($release =~ /^SUSE LINUX Enterprise Server (\d+\.?\d?\w*)/i) { $distro="SESLES$1"; } elsif ($release =~ /^SUSE Linux Enterprise Server (\d+\.?\d?\w*)/i) { $distro="SESLES$1"; } elsif ($release =~ /^openSuSE (\d+\.\d+\w*)/i) { $distro="SE$1"; } else { print STDERR "$err Couldn't determine SuSE version! Setting to 10.3!\n"; $distro="SE10.3"; } close(SUSE_RELEASE); } elsif ( -e "/etc/turbolinux-release") { open(*TURBOLINUX_RELEASE,"/etc/turbolinux-release"); $release=; unless ($release =~ /^Turbolinux Workstation (\d+\.\d+\w*)/) { print STDERR "$err Couldn't determine TurboLinux version! Setting to 7.0!\n"; $distro="TB7.0"; } else { $distro="TB$1"; } close(TURBOLINUX_RELEASE); } else { # We're either on Mac OS X, HP-UX or an unsupported O/S. if ( -x '/usr/bin/uname') { # uname is in /usr/bin on Mac OS X and HP-UX $release=`/usr/bin/uname -sr`; } else { print STDERR "$err Could not determine operating system version!\n"; $distro="unknown"; } # Figure out what kind of system we're on. if ($release ne "") { if ($release =~ /^Darwin\s+(\d+)\.(\d+)/) { if ($1 == 6 ) { $distro = "OSX10.2"; } elsif ($1 == 7) { $distro = "OSX10.3"; } elsif ($1 == 8) { $distro = "OSX10.3"; } else { $distro = "unknown"; } } elsif ( $release =~ /(^HP-UX)\s*B\.(\d+\.\d+)/ ) { $distro="$1$2"; } else { print STDERR "$err Could not determine operating system version!\n"; $distro="unknown"; } } } $GLOBAL_OS=$distro; } elsif (not (defined $GLOBAL_OS)) { print "ERROR: GLOBAL OS Scoping Issue\n"; } else { $distro = $GLOBAL_OS; } return $distro; } ################################################################################### # &getActualDistro; # # # # This subroutine returns the actual os version in which is running on. This # # os version is independent of the --os switch feed to bastille. # # # ################################################################################### sub getActualDistro { # set local variable to $GLOBAL_OS if ($GLOBAL_ACTUAL_OS eq "None") { my $os = $GLOBAL_OS; # undef GLOBAL_OS so that the GetDistro routine will return # the actualDistro, it might otherwise return the distro set # by the --os switch. $GLOBAL_OS = "None"; $GLOBAL_ACTUAL_OS = &GetDistro; # reset the GLOBAL_OS variable $GLOBAL_OS = $os; } return $GLOBAL_ACTUAL_OS; } # These are helper routines which used to be included inside GetDistro sub is_OS_supported($) { my $os=$_[0]; my $supported=0; my %supportedOSHash = &getSupportedOSHash; foreach my $oSType (keys %supportedOSHash) { foreach my $supported_os ( @{$supportedOSHash{$oSType}} ) { if ( $supported_os eq $os ) { $supported=1; } } } return $supported; } ############################################################################### # getSupportedOSHash # # This subrountine returns a hash of supported OSTypes, which point to a # a list of supported distros. When porting to a new distro, add the # distro id to the hash in its appropriate list. ############################################################################### sub getSupportedOSHash () { my %osHash = ("LINUX" => [ "DB2.2", "DB3.0", "RH6.0","RH6.1","RH6.2","RH7.0", "RH7.1","RH7.2","RH7.3","RH8.0", "RH9", "RHEL5", "RHEL4AS","RHEL4ES","RHEL4WS", "RHEL3AS","RHEL3ES","RHEL3WS", "RHEL2AS","RHEL2ES","RHEL2WS", "RHFC1","RHFC2","RHFC3","RHFC4", "RHFC5","RHFC6","RHFC7","RHFC8", "MN6.0","MN6.1 ","MN7.0","MN7.1", "MN7.2","MN8.0","MN8.1","MN8.2", "MN10.1", "SE7.2","SE7.3", "SE8.0","SE8.1","SE9.0","SE9.1", "SE9.2","SE9.3","SE10.0","SE10.1","SE10.2","SE10.3", "SESLES8","SESLES9","SESLES10", "TB7.0" ], "HP-UX" => [ "HP-UX11.00","HP-UX11.11", "HP-UX11.22", "HP-UX11.23", "HP-UX11.31" ], "OSX" => [ 'OSX10.2','OSX10.3','OSX10.4' ] ); return %osHash; } ############################################################################### # setFileLocations(OSMapFile, currentDistro); # # Given a file map location this subroutine will create the GLOBAL_* # hash entries specified within this file. ############################################################################### sub setFileLocations($$) { my ($fileInfoFile,$currentDistro) = @_; # define a mapping from the first argument to the proper hash my %map = ("BIN" => \%GLOBAL_BIN, "FILE" => \%GLOBAL_FILE, "BFILE" => \%GLOBAL_BFILE, "DIR" => \%GLOBAL_DIR, "BDIR" => \%GLOBAL_BDIR ); my @fileInfo = (); # File containing file location information if(open(FILEINFO, "<$fileInfoFile" )) { @fileInfo = ; close(FILEINFO); } else { print STDERR "$err Unable to find file location information for '$distro'.\n" . "$spc Contact the Bastille support list for details.\n"; exit(1); } # Each line of the file map follows the pattern below: # bdir,init.d,'/etc/rc.d/init.d',RH7.2,RH7.3 # if the distro information is not available, e.g. # bdir,init.d,'/etc/rc.d/init.d' # then the line applies to all distros under the OSType foreach my $file (@fileInfo) { # Perl comments are allowed within the file but only entire line comments if($file !~ /^\s+\#|^\s+$/) { chomp $file; # type relates to the map above, type bin will map to GLOBAL_BIN # id is the identifier used as the hash key by the GLOBAL hash # fileLocation is the full path to the file # distroList is an optional list of distros that this particular # file location, if no distro list is presented the file location # is considered to apply to all distros my ($type,$id,$fileLocation,@distroList) = split /\s*,\s*/, $file; $fileLocation =~ s/^\'(.*)\'$/$1/; if($#distroList == -1) { $map{uc($type)}->{$id}=$fileLocation; } else { foreach my $distro (@distroList) { # if the current distro matches the distro listed then # this file location applies if($currentDistro =~ /$distro/) { $map{uc($type)}->{$id}=$fileLocation; } } } } } unless(defined($map{uc("BFILE")}->{"current_config"})) { &setGlobal("BFILE","current_config",&getGlobal("BFILE","config")); } } ############################################################################### # setServiceInfo($OSServiceMapFile, $currentDistro # # Given the location of an OS Service map file, which describes # a service in terms of configurables, processes and a service type. # The subroutine fills out the GLOBAL_SERVICE, $GLOBAL_RC_CONFIG, GLOBAL_SERVTYPE, and # GLOBAL_PROCESS hashes for a given service ID. ############################################################################### sub setServiceInfo($$) { my ($serviceInfoFile,$currentDistro) = @_; my @serviceInfo = (); if(open(SERVICEINFO, "<$serviceInfoFile" )) { @serviceInfo = ; close(SERVICEINFO); } else { print STDERR "$err Unable to find service, service type, and process information\n" . "$spc for '$distro'.\n" . "$spc Contact the Bastille support list for details.\n"; exit(1); } # The following loop, parses the entire (YOUR OS).service file # to provide service information for YOUR OS. # The files format is as follows: # serviceID,servType,('service' 'configuration' 'list'),('process' 'list')[,DISTROS]* # if distros are not present then the service is assumed to be # relevant the the current distro # # More specifically, this file's format for rc-based daemons is: # # script_name,rc,(rc-config-file rc-config-file ...),(rc-variable1 rc-variable2 ...),('program_name1 program_name2 ...') # # ...where script_name is a file in /etc/init.d/ and # ...program_nameN is a program launced by the script. # # This file's format for inet-based daemons is: # # identifier, inet, line name/file name, program name # # label,inet,(port1 port2 ...),(daemon1 daemon2 ...) # # ...where label is arbitrary, portN is one of the ports # ...this one listens on, and daemonN is a program launched # ...in response to a connection on a port. foreach my $service (@serviceInfo) { # This file accepts simple whole line comments perl style if($service !~ /^\s+\#|^\s+$/) { chomp $service; my ($serviceID,$servType,$strConfigList,$strServiceList, $strProcessList,@distroList) = split /\s*,\s*/, $service; sub MakeArrayFromString($){ my $entryString = $_[0]; my @destArray = (); if ($entryString =~ /\'\S+\'/) { #Make sure we have something to extract before we try @destArray = split /\'\s+\'/, $entryString; $destArray[0] =~ s/^\(\'(.+)$/$1/; # Remove leading quotation mark $destArray[$#destArray] =~ s/^(.*)\'\)$/$1/; #Remove trailing quotation mark } return @destArray; } # produce a list of configuration files from the files # format ('configuration' 'files') my @configList = MakeArrayFromString($strConfigList); # produce a list of service configurables from the files # format ('service' 'configurable') my @serviceList = MakeArrayFromString($strServiceList); # produce a list of process names from the files format # ('my' 'process' 'list') my @processList = MakeArrayFromString($strProcessList); # if distros were not specified then accept the service information if($#distroList == -1) { @{$GLOBAL_SERVICE{$serviceID}} = @serviceList; $GLOBAL_SERVTYPE{$serviceID} = $servType; @{$GLOBAL_PROCESS{$serviceID}} = @processList; @{$GLOBAL_RC_CONFIG{$serviceID}} = @configList; } else { # only if the current distro matches one of the listed distros # include the service information. foreach my $distro (@distroList) { if($currentDistro =~ /$distro/) { @{$GLOBAL_SERVICE{$serviceID}} = @serviceList; $GLOBAL_SERVTYPE{$serviceID} = $servType; @{$GLOBAL_PROCESS{$serviceID}} = @processList; @{$GLOBAL_RC_CONFIG{$serviceID}} = @configList; } } } } } } ############################################################################### # getFileAndServiceInfo($distro,$actualDistro) # # This subrountine, given distribution information, will import system file # and service information into the GLOBA_* hashes. # # NOTE: $distro and $actualDistro will only differ when the --os switch is # used to generate a configuration file for an arbitrary operating # system. # ############################################################################### sub getFileAndServiceInfo($$) { my ($distro,$actualDistro) = @_; # defines the path to the OS map information for any supported OS type. # OS map information is used to determine file locations for a given # distribution. my %oSInfoPath = ( "LINUX" => "/usr/share/Bastille/OSMap/", "HP-UX" => "/etc/opt/sec_mgmt/bastille/OSMap/", "OSX" => "/usr/share/Bastille/OSMap/" ); # returns the OS, LINUX, HP-UX, or OSX, associated with this # distribution my $actualOS = &getOSType($actualDistro); my $oS = &getOSType($distro); if(defined $actualOS && defined $oS) { my $bastilleInfoFile = $oSInfoPath{$actualOS} . "${actualOS}.bastille"; my $systemInfoFile = $oSInfoPath{$actualOS} . "${oS}.system"; my $serviceInfoFile = $oSInfoPath{$actualOS} . "${oS}.service"; if(-f $bastilleInfoFile) { &setFileLocations($bastilleInfoFile,$actualDistro); } else { print STDERR "$err Unable to find bastille file information.\n" . "$spc $bastilleInfoFile does not exist on the system"; exit(1); } if(-f $systemInfoFile) { &setFileLocations($systemInfoFile,$distro); } else { print STDERR "$err Unable to find system file information.\n" . "$spc $systemInfoFile does not exist on the system"; exit(1); } # Service info File is optional if(-f $serviceInfoFile) { &setServiceInfo($serviceInfoFile,$distro); } } else { print STDERR "$err Unable to determine operating system type\n" . "$spc for $actualDistro or $distro\n"; exit(1); } } # returns the Operating System type associated with the specified # distribution. sub getOSType($) { my $distro = $_[0]; my %supportedOSHash = &getSupportedOSHash; foreach my $oSType (keys %supportedOSHash) { foreach my $oSDistro (@{$supportedOSHash{$oSType}}) { if($distro eq $oSDistro) { return $oSType; } } } return undef; } # Test subroutine used to debug file location info for new Distributions as # they are ported. sub dumpFileInfo { print "Dumping File Information\n"; foreach my $hashref (\%GLOBAL_BIN,\%GLOBAL_DIR,\%GLOBAL_FILE,\%GLOBAL_BFILE,\%GLOBAL_BDIR) { foreach my $id (keys %{$hashref}) { print "$id: ${$hashref}{$id}\n"; } print "-----------------------\n\n"; } } # Test subroutine used to debug service info for new Distributions as # they are ported. sub dumpServiceInfo { print "Dumping Service Information\n"; foreach my $serviceId (keys %GLOBAL_SERVICE) { print "$serviceId:\n"; print "Type - $GLOBAL_SERVTYPE{$serviceId}\n"; print "Service List:\n"; foreach my $service (@{$GLOBAL_SERVICE{$serviceId}}) { print "$service "; } print "\nProcess List:\n"; foreach my $process (@{$GLOBAL_PROCESS{$serviceId}}) { print "$process "; } print "\n----------------------\n"; } } ########################################################################### # # &ConfigureForDistro configures the API for a given distribution. This # includes setting global variables that tell the Bastille API about # given binaries and directories. # # WARNING: If a distro is not covered here, Bastille may not be 100% # compatible with it, though 1.1 is written to be much smarter # about unknown distros... # ########################################################################### sub ConfigureForDistro { my $retval=1; # checking to see if the os version given is in fact supported my $distro = &GetDistro; # checking to see if the actual os version is in fact supported my $actualDistro = &getActualDistro; $ENV{'LOCALE'}=''; # So that test cases checking for english results work ok. if ((! &is_OS_supported($distro)) or (! &is_OS_supported($actualDistro)) ) { # if either is not supported then print out a list of supported versions if (! &is_OS_supported($distro)) { print STDERR "$err '$distro' is not a supported operating system.\n"; } else { print STDERR "$err Bastille is unable to operate correctly on this\n"; print STDERR "$spc $distro operating system.\n"; } my %supportedOSHash = &getSupportedOSHash; print STDERR "$spc Valid operating system versions are as follows:\n"; foreach my $oSType (keys %supportedOSHash) { print STDERR "$spc $oSType:\n$spc "; my $os_number = 1; foreach my $os (@{$supportedOSHash{$oSType}}) { print STDERR "'$os' "; if ($os_number == 5){ print STDERR "\n$spc "; $os_number = 1; } else { $os_number++; } } print STDERR "\n"; } print "\n" . $GLOBAL_ERROR{"usage"}; exit(1); } # First, let's make sure that we do not create any files or # directories with more permissive permissions than we # intend via setting the Perl umask umask(077); &getFileAndServiceInfo($distro,$actualDistro); # &dumpFileInfo; # great for debuging file location issues # &dumpServiceInfo; # great for debuging service information issues # OS dependent error messages (after configuring file locations) my $nodisclaim_file = &getGlobal('BFILE', "nodisclaimer"); $GLOBAL_ERROR{"disclaimer"}="$err Unable to touch $nodisclaim_file:" . "$spc You must use Bastille\'s -n flag (for example:\n" . "$spc bastille -f -n) or \'touch $nodisclaim_file \'\n"; return $retval; } ########################################################################### ########################################################################### # # # The B_ file utilities are replacements for their Perl # # counterparts. These replacements log their actions and their errors, # # but are very similar to said counterparts. # # # ########################################################################### ########################################################################### ########################################################################### # B_open is used for opening a file for reading. B_open_plus is the preferred # function for writing, since it saves a backup copy of the file for # later restoration. # # B_open opens the given file handle, associated with the given filename # and logs appropriately. # ########################################################################### sub B_open { my $retval=1; my ($handle,$filename)=@_; unless ($GLOBAL_LOGONLY) { $retval = open $handle,$filename; } ($handle) = "$_[0]" =~ /[^:]+::[^:]+::([^:]+)/; &B_log("ACTION","open $handle,\"$filename\";\n"); unless ($retval) { &B_log("ERROR","open $handle, $filename failed...\n"); } return $retval; } ########################################################################### # B_open_plus is the v1.1 open command. # # &B_open_plus($handle_file,$handle_original,$file) opens the file $file # for reading and opens the file ${file}.bastille for writing. It is the # counterpart to B_close_plus, which will move the original file to # $GLOBAL_BDIR{"backup"} and will place the new file ${file}.bastille in its # place. # # &B_open_plus makes the appropriate log entries in the action and error # logs. ########################################################################### sub B_open_plus { my ($handle_file,$handle_original,$file)=@_; my $retval=1; my $return_file=1; my $return_old=1; my $original_file = $file; # Open the original file and open a copy for writing. unless ($GLOBAL_LOGONLY) { # if the temporary filename already exists then the open operation will fail. if ( $file eq "" ){ &B_log("ERROR","Internal Error - Attempt Made to Open Blank Filename"); $return_old=0; $return_file=0; return 0; #False } elsif (-e "${file}.bastille") { &B_log("ERROR","Unable to open $file as the swap file ". "${file}.bastille\" already exists. Rename the swap ". "file to allow Bastille to make desired file modifications."); $return_old=0; $return_file=0; } else { $return_old = open $handle_original,"$file"; $return_file = open $handle_file,("> $file.bastille"); } } # Error handling/logging here... #&B_log("ACTION","# Modifying file $original_file via temporary file $original_file.bastille\n"); unless ($return_file) { $retval=0; &B_log("ERROR","open file: \"$original_file.bastille\" failed...\n"); } unless ($return_old) { $retval=0; &B_log("ERROR","open file: \"$original_file\" failed.\n"); } return $retval; } ########################################################################### # B_close was the v1.0 close command. It is still used in places in the # code. # However the use of B _close_plus, which implements a new, smarter, # backup scheme is preferred. # # B_close closes the given file handle, associated with the given filename # and logs appropriately. ########################################################################### sub B_close { my $retval=1; unless ($GLOBAL_LOGONLY) { $retval = close $_[0]; } &B_log("ACTION", "close $_[0];\n"); unless ($retval) { &B_log("ERROR", "close $_[0] failed...\n"); } return $retval; } ########################################################################### # B_close_plus is the v1.1 close command. # # &B_close_plus($handle_file,$handle_original,$file) closes the files # $file and ${file}.bastille, backs up $file to $GLOBAL_BDIR{"backup"} and # renames ${file}.bastille to $file. This backup is made using the # internal API function &B_backup_file. Further, it sets the new file's # permissions and uid/gid to the same as the old file. # # B_close_plus is the counterpart to B_open_plus, which opened $file and # $file.bastille with the file handles $handle_original and $handle_file, # respectively. # # &B_close_plus makes the appropriate log entries in the action and error # logs. ########################################################################### sub B_close_plus { my ($handle_file,$handle_original,$file)=@_; my ($mode,$uid,$gid); my @junk; my $original_file; my $retval=1; my $return_file=1; my $return_old=1; # Append the global prefix, but save the original for B_backup_file b/c # it appends the prefix on its own... $original_file=$file; # # Close the files and prepare for the rename # if (($file eq "") or (not(-e $file ))) { &B_log("ERROR","Internal Error, attempted to close a blank filename ". "or nonexistent file."); return 0; #False } unless ($GLOBAL_LOGONLY) { $return_file = close $handle_file; $return_old = close $handle_original; } # Error handling/logging here... #&B_log("ACTION","#Closing $original_file and backing up to " . &getGlobal('BDIR', "backup")); #&B_log("ACTION","/$original_file\n"); unless ($return_file) { $retval=0; &B_log("ERROR","close $original_file failed...\n"); } unless ($return_old) { $retval=0; &B_log("ERROR","close $original_file.bastille failed.\n"); } # # If we've had no errors, backup the old file and put the new one # in its place, with the Right permissions. # unless ( ($retval == 0) or $GLOBAL_LOGONLY) { # Read the permissions/owners on the old file @junk=stat ($file); $mode=$junk[2]; $uid=$junk[4]; $gid=$junk[5]; # Set the permissions/owners on the new file chmod $mode, "$file.bastille" or &B_log("ERROR","Not able to retain permissions on $original_file!!!\n"); chown $uid, $gid, "$file.bastille" or &B_log("ERROR","Not able to retain owners on $original_file!!!\n"); # Backup the old file and put a new one in place. &B_backup_file($original_file); rename "$file.bastille", $file or &B_log("ERROR","B_close_plus: not able to move $original_file.bastille to $original_file\n"); # We add the file to the GLOBAL_SUMS hash if it is not already present &B_set_sum($file); } return $retval; } ########################################################################### # &B_backup_file ($file) makes a backup copy of the file $file in # &getGlobal('BDIR', "backup"). Note that this routine is intended for internal # use only -- only Bastille API functions should call B_backup_file. # ########################################################################### sub B_backup_file { my $file=$_[0]; my $complain = 1; my $original_file = $file; my $backup_dir = &getGlobal('BDIR', "backup"); my $backup_file = $backup_dir . $original_file; my $retval=1; # First, separate the file into the directory and the relative filename my $directory =""; if ($file =~ /^(.*)\/([^\/]+)$/) { #$relative_file=$2; $directory = $1; } else { $directory=cwd; } # Now, if the directory does not exist, create it. # Later: # Try to set the same permissions on the patch directory that the # original had...? unless ( -d ($backup_dir . $directory) ) { mkpath(( $backup_dir . $directory),0,0700); } # Now we backup the file. If there is already a backup file there, # we will leave it alone, since it exists from a previous run and # should be the _original_ (possibly user-modified) distro's version # of the file. if ( -e $file ) { unless ( -e $backup_file ) { my $command=&getGlobal("BIN","cp"); &B_Backtick("$command -p $file $backup_file"); &B_revert_log (&getGlobal("BIN","mv"). " $backup_file $file"); } } else { # The file we were trying to backup doesn't exist. $retval=0; # This is a non-fatal error, not worth complaining about $complain = 0; #&ErrorLog ("# Failed trying to backup file $file -- it doesn't exist!\n"); } # Check to make sure that the file does exist in the backup location. unless ( -e $backup_file ) { $retval=0; if ( $complain == 1 ) { &B_log("ERROR","Failed trying to backup $file -- the copy was not created.\n"); } } return $retval; } ########################################################################### # &B_read_sums reads in the sum.csv file which contains information # about Bastille modified files. The file structure is as follows: # # filename,filesize,cksum # # It reads the information into the GLOBAL_SUM hash i.e. # $GLOBAL_SUM{$file}{sum} = $cksum # $GLOBAL_SUM{$file}{filesize} = $size # For the first run of Bastille on a given system this subroutine # is a no-op, and returns "undefined." ########################################################################### sub B_read_sums { my $sumFile = &getGlobal('BFILE',"sum.csv"); if ( -e $sumFile ) { open( SUM, "< $sumFile") or &B_log("ERROR","Unable to open $sumFile for read.\n$!\n"); while( my $line = ) { chomp $line; my ($file,$filesize,$sum,$flag) = split /,/, $line; if(-e $file) { $GLOBAL_SUM{"$file"}{filesize} = $filesize; $GLOBAL_SUM{"$file"}{sum} = $sum; } } close(SUM); } else { return undef; } } ########################################################################### # &B_write_sums writes out the sum.csv file which contains information # about Bastille modified files. The file structure is as follows: # # filename,filesize,cksum # # It writes the information from the GLOBAL_SUM hash i.e. # # $file,$GLOBAL_SUM{$file}{sum},$GLOBAL_SUM{$file}{filesize} # # This subroutine requires access to the GLOBAL_SUM hash. ########################################################################### sub B_write_sums { my $sumFile = &getGlobal('BFILE',"sum.csv"); if ( %GLOBAL_SUM ) { open( SUM, "> $sumFile") or &B_log("ERROR","Unable to open $sumFile for write.\n$!\n"); for my $file (sort keys %GLOBAL_SUM) { if( -e $file) { print SUM "$file,$GLOBAL_SUM{\"$file\"}{filesize},$GLOBAL_SUM{\"$file\"}{sum}\n"; } } close(SUM); } } ########################################################################### # &B_check_sum($file) compares the stored cksum and filesize of the given # file compared to the current cksum and filesize respectively. # This subroutine also keeps the state of the sum check by setting the # checked flag which tells the subroutine that on this run this file # has already been checked. # # $GLOBAL_SUM{$file}{checked} = 1; # # This subroutine requires access to the GLOBAL_SUM hash. # # Returns 1 if sum checks out and 0 if not ########################################################################### sub B_check_sum($) { my $file = $_[0]; my $cksum = &getGlobal('BIN',"cksum"); if (not(%GLOBAL_SUM)) { &B_read_sums; } if(-e $file) { my ($sum,$size,$ckfile) = split(/\s+/, `$cksum $file`); my $commandRetVal = ($? >> 8); # find the command's return value if($commandRetVal != 0) { &B_log("ERROR","$cksum reported the following error:\n$!\n"); return 0; } else { if ( exists $GLOBAL_SUM{$file} ) { # if the file size or file sum differ from those recorded. if (( $GLOBAL_SUM{$file}{filesize} == $size) and ($GLOBAL_SUM{$file}{sum} == $sum )) { return 1; #True, since saved state matches up, all is well. } else { return 0; #False, since saved state doesn't match } } else { &B_log("ERROR","File: $file does not exist in sums database."); return 0; } } } else { &B_log("ERROR","The file: $file does not exist for comparison in B_check_sum."); return 0; } } # Don't think we need this anymore as function now check_sums returns # results directly #sub isSumDifferent($) { # my $file = $_[0]; # if(exists $GLOBAL_SUM{$file}) { # return $GLOBAL_SUM{$file}{flag} # } #} sub listModifiedFiles { my @listModifiedFiles=sort keys %GLOBAL_SUM; return @listModifiedFiles; } ########################################################################### # &B_isFileinSumDB($file) checks to see if a given file's sum was saved. # # $GLOBAL_SUM{$file}{filesize} = $size; # $GLOBAL_SUM{$file}{sum} = $cksum; # # This subroutine requires access to the GLOBAL_SUM hash. ########################################################################### sub B_isFileinSumDB($) { my $file = $_[0]; if (not(%GLOBAL_SUM)) { &B_log("DEBUG","Reading in DB from B_isFileinSumDB"); &B_read_sums; } if (exists($GLOBAL_SUM{"$file"})){ &B_log("DEBUG","$file is in sum database"); return 1; #true } else { &B_log("DEBUG","$file is not in sum database"); return 0; #false } } ########################################################################### # &B_set_sum($file) sets the current cksum and filesize of the given # file into the GLOBAL_SUM hash. # # $GLOBAL_SUM{$file}{filesize} = $size; # $GLOBAL_SUM{$file}{sum} = $cksum; # # This subroutine requires access to the GLOBAL_SUM hash. ########################################################################### sub B_set_sum($) { my $file = $_[0]; my $cksum = &getGlobal('BIN',"cksum"); if( -e $file) { my ($sum,$size,$ckfile) = split(/\s+/, `$cksum $file`); my $commandRetVal = ($? >> 8); # find the command's return value if($commandRetVal != 0) { &B_log("ERROR","$cksum reported the following error:\n$!\n"); } else { # new file size and sum are added to the hash $GLOBAL_SUM{$file}{filesize} = $size; $GLOBAL_SUM{$file}{sum} = $sum; &B_write_sums; } } else { &B_log("ERROR","Can not save chksum for file: $file since it does not exist"); } } ########################################################################### # # &B_delete_file ($file) deletes the file $file and makes a backup to # the backup directory. # ########################################################################## sub B_delete_file($) { #Currently Linux only (TMPDIR) #consideration: should create clear_sum routine if this is ever used to remove # A Bastille-generated file. # # This API routine deletes the named file, backing it up first to the # backup directory. # my $filename=shift @_; my $retval=1; # We have to append the prefix ourselves since we don't use B_open_plus my $original_filename=$filename; &B_log("ACTION","Deleting (and backing-up) file $original_filename\n"); &B_log("ACTION","rm $original_filename\n"); unless ($filename) { &B_log("ERROR","B_delete_file called with no arguments!\n"); } unless ($GLOBAL_LOGONLY) { if ( B_backup_file($original_filename) ) { unless ( unlink $filename ) { &B_log("ERROR","Couldn't unlink file $original_filename"); $retval=0; } } else { $retval=0; &B_log("ERROR","B_delete_file did not delete $original_filename since it could not back it up\n"); } } $retval; } ########################################################################### # &B_create_file ($file) creates the file $file, if it doesn't already # exist. # It will set a default mode of 0700 and a default uid/gid or 0/0. # # &B_create_file, to support Bastille's revert functionality, writes an # rm $file command to the end of the file &getGlobal('BFILE', "created-files"). # ########################################################################## sub B_create_file($) { my $file = $_[0]; my $retval=1; # We have to create the file ourselves since we don't use B_open_plus my $original_file = $file; if ($file eq ""){ &B_log("ERROR","Internal Error, attempt made to create blank filename"); return 0; #False } unless ( -e $file ) { unless ($GLOBAL_LOGONLY) { # find the directory in which the file is to reside. my $dirName = dirname($file); # if the directory does not exist then if(! -d $dirName) { # create it. mkpath ($dirName,0,0700); } $retval=open CREATE_FILE,">$file"; if ($retval) { close CREATE_FILE; chmod 0700,$file; # Make the revert functionality &B_revert_log( &getGlobal('BIN','rm') . " $original_file \n"); } else { &B_log("ERROR","Couldn't create file $original_file even though " . "it didn't already exist!\n"); } } &B_log("ACTION","Created file $original_file\n"); } else { &B_log("DEBUG","Didn't create file $original_file since it already existed.\n"); $retval=0; } $retval; } ########################################################################### # &B_create_dir ($dir) creates the directory $dir, if it doesn't already # exist. # It will set a default mode of 0700 and a default uid/gid or 0/0. # ########################################################################## sub B_create_dir($) { my $dir = $_[0]; my $retval=1; # We have to append the prefix ourselves since we don't use B_open_plus my $original_dir=$dir; unless ( -d $dir ) { unless ($GLOBAL_LOGONLY) { $retval=mkdir $dir,0700; if ($retval) { # Make the revert functionality &B_revert_log (&getGlobal('BIN','rmdir') . " $original_dir\n"); } else { &B_log("ERROR","Couldn't create dir $original_dir even though it didn't already exist!"); } } &B_log("ACTION","Created directory $original_dir\n"); } else { &B_log("ACTION","Didn't create directory $original_dir since it already existed.\n"); $retval=0; } $retval; } ########################################################################### # &B_symlink ($original_file,$new_symlink) creates a symbolic link from # $original_file to $new_symlink. # # &B_symlink respects $GLOBAL_LOGONLY. It supports # the revert functionality that you've come to know and love by adding every # symbolic link it creates to &getGlobal('BFILE', "created-symlinks"), currently set to: # # /root/Bastille/revert/revert-created-symlinks # # The revert script, if it works like I think it should, will run this file, # which should be a script or rm's... # ########################################################################## sub B_symlink($$) { my ($source_file,$new_symlink)=@_; my $retval=1; my $original_source = $source_file; my $original_symlink = $new_symlink; unless ($GLOBAL_LOGONLY) { $retval=symlink $source_file,$new_symlink; if ($retval) { &B_revert_log (&getGlobal('BIN',"rm") . " $original_symlink\n"); } } &B_log("ACTION", "Created a symbolic link called $original_symlink from $original_source\n"); &B_log("ACTION", "symlink \"$original_source\",\"$original_symlink\";\n"); unless ($retval) { &B_log("ERROR","Couldn't symlink $original_symlink -> $original_source\n"); } $retval; } sub B_cp($$) { my ($source,$target)=@_; my $retval=0; my $had_to_backup_target=0; use File::Copy; my $original_source=$source; my $original_target=$target; if( -e $target and -f $target ) { &B_backup_file($original_target); &B_log("ACTION","About to copy $original_source to $original_target -- had to backup target\n"); $had_to_backup_target=1; } $retval=copy($source,$target); if ($retval) { &B_log("ACTION","cp $original_source $original_target\n"); # # We want to add a line to the &getGlobal('BFILE', "created-files") so that the # file we just put at $original_target gets deleted. # &B_revert_log(&getGlobal('BIN',"rm") . " $original_target\n"); } else { &B_log("ERROR","Failed to copy $original_source to $original_target\n"); } # We add the file to the GLOBAL_SUMS hash if it is not already present &B_set_sum($target); $retval; } ############################################################################ # &B_place puts a file in place, using Perl's File::cp. This file is taken # from &getGlobal('BDIR', "share") and is used to place a file that came with # Bastille. # # This should be DEPRECATED in favor of &B_cp, since the only reason it exists # is because of GLOBAL_PREFIX, which has been broken for quite some time. # Otherwise, the two routines are identical. # # It respects $GLOBAL_LOGONLY. # If $target is an already-existing file, it is backed up. # # revert either appends another "rm $target" to &getGlobal('BFILE', "revert-actions") or # backs up the file that _was_ there into the &getGlobal('BDIR', "backup"), # appending a "mv" to revert-actions to put it back. # ############################################################################ sub B_place { # Only Linux references left (Firewall / TMPDIR) my ($source,$target)=@_; my $retval=0; my $had_to_backup_target=0; use File::Copy; my $original_source=$source; $source = &getGlobal('BDIR', "share") . $source; my $original_target=$target; if ( -e $target and -f $target ) { &B_backup_file($original_target); &B_log("ACTION","About to copy $original_source to $original_target -- had to backup target\n"); $had_to_backup_target=1; } $retval=copy($source,$target); if ($retval) { &B_log("ACTION","placed file $original_source as $original_target\n"); # # We want to add a line to the &getGlobal('BFILE', "created-files") so that the # file we just put at $original_target gets deleted. &B_revert_log(&getGlobal('BIN',"rm") . " $original_target\n"); } else { &B_log("ERROR","Failed to place $original_source as $original_target\n"); } # We add the file to the GLOBAL_SUMS hash if it is not already present &B_set_sum($target); $retval; } ############################################################################# ############################################################################# ############################################################################# ########################################################################### # &B_mknod ($file) creates the node $file, if it doesn't already # exist. It uses the prefix and suffix, like this: # # mknod $prefix $file $suffix # # This is just a wrapper to the mknod program, which tries to introduce # revert functionality, by writing rm $file to the end of the # file &getGlobal('BFILE', "created-files"). # ########################################################################## sub B_mknod($$$) { my ($prefix,$file,$suffix) = @_; my $retval=1; # We have to create the filename ourselves since we don't use B_open_plus my $original_file = $file; unless ( -e $file ) { my $command = &getGlobal("BIN","mknod") . " $prefix $file $suffix"; if ( system($command) == 0) { # Since system will return 0 on success, invert the error code $retval=1; } else { $retval=0; } if ($retval) { # Make the revert functionality &B_revert_log(&getGlobal('BIN',"rm") . " $original_file\n"); } else { &B_log("ERROR","Couldn't mknod $prefix $original_file $suffix even though it didn't already exist!\n"); } &B_log("ACTION","mknod $prefix $original_file $suffix\n"); } else { &B_log("ACTION","Didn't mknod $prefix $original_file $suffix since $original_file already existed.\n"); $retval=0; } $retval; } ########################################################################### # &B_revert_log("reverse_command") prepends a command to a shell script. This # shell script is intended to be run by bastille -r to reverse the changes that # Bastille made, returning the files which Bastille changed to their original # state. ########################################################################### sub B_revert_log($) { my $revert_command = $_[0]; my $revert_actions = &getGlobal('BFILE', "revert-actions"); my $revertdir= &getGlobal('BDIR', "revert"); my @lines; if (! (-e $revert_actions)) { mkpath($revertdir); #if this doesn't work next line catches if (open REVERT_ACTIONS,">" . $revert_actions){ # create revert file close REVERT_ACTIONS; # chown to root, rwx------ chmod 0700,$revert_actions; chown 0,0,$revert_actions; } else { &B_log("FATAL","Can not create revert-actions file: $revert_actions.\n" . " Unable to add the following command to the revert\n" . " actions script: $revert_command\n"); } } &B_open_plus (*REVERT_NEW, *REVERT_OLD, $revert_actions); while (my $line=) { #copy file into @lines push (@lines,$line); } print REVERT_NEW $revert_command . "\n"; #make the revert command first in the new file while (my $line = shift @lines) { #write the rest of the lines of the file print REVERT_NEW $line; } close REVERT_OLD; close REVERT_NEW; if (rename "${revert_actions}.bastille", $revert_actions) { #replace the old file with the new file we chmod 0700,$revert_actions; # just made / mirrors B_close_plus logic chown 0,0,$revert_actions; } else { &B_log("ERROR","B_revert_log: not able to move ${revert_actions}.bastille to ${revert_actions}!!! $!) !!!\n"); } } ########################################################################### # &getGlobalConfig($$) # # returns the requested GLOBAL_CONFIG hash value, ignoring the error # if the value does not exist (because every module uses this to find # out if the question was answered "Y") ########################################################################### sub getGlobalConfig ($$) { my $module = $_[0]; my $key = $_[1]; if (exists $GLOBAL_CONFIG{$module}{$key}) { my $answer=$GLOBAL_CONFIG{$module}{$key}; &B_log("ACTION","Answer to question $module.$key is \"$answer\".\n"); return $answer; } else { &B_log("ACTION","Answer to question $module.$key is undefined."); return undef; } } ########################################################################### # &getGlobal($$) # # returns the requested GLOBAL_* hash value, and logs an error # if the variable does not exist. ########################################################################### sub getGlobal ($$) { my $type = uc($_[0]); my $key = $_[1]; # define a mapping from the first argument to the proper hash my %map = ("BIN" => \%GLOBAL_BIN, "FILE" => \%GLOBAL_FILE, "BFILE" => \%GLOBAL_BFILE, "DIR" => \%GLOBAL_DIR, "BDIR" => \%GLOBAL_BDIR, "ERROR" => \%GLOBAL_ERROR, "SERVICE" => \%GLOBAL_SERVICE, "SERVTYPE" => \%GLOBAL_SERVTYPE, "PROCESS" => \%GLOBAL_PROCESS, "RCCONFIG" => \%GLOBAL_RC_CONFIG ); # check to see if the desired key is in the desired hash if (exists $map{$type}->{$key}) { # get the value from the right hash with the key return $map{$type}->{$key}; } else { # i.e. Bastille tried to use $GLOBAL_BIN{'cp'} but it does not exist. # Note that we can't use B_log, since it uses getGlobal ... recursive before # configureForDistro is run. print STDERR "ERROR: Bastille tried to use \$GLOBAL_${type}\{\'$key\'} but it does not exist.\n"; return undef; } } ########################################################################### # &getGlobal($$) # # sets the requested GLOBAL_* hash value ########################################################################### sub setGlobal ($$$) { my $type = uc($_[0]); my $key = $_[1]; my $input_value = $_[2]; # define a mapping from the first argument to the proper hash my %map = ("BIN" => \%GLOBAL_BIN, "FILE" => \%GLOBAL_FILE, "BFILE" => \%GLOBAL_BFILE, "DIR" => \%GLOBAL_DIR, "BDIR" => \%GLOBAL_BDIR, "ERROR" => \%GLOBAL_ERROR, "SERVICE" => \%GLOBAL_SERVICE, "SERVTYPE" => \%GLOBAL_SERVTYPE, "PROCESS" => \%GLOBAL_PROCESS, ); if ($map{$type}->{$key} = $input_value) { return 1; } else { &B_log('ERROR','Internal Error, Unable to set global config value:' . $type . ", " .$key); return 0; } } ########################################################################### # &showDisclaimer: # Print the disclaimer and wait for 2 minutes for acceptance # Do NOT do so if any of the following conditions hold # 1. the -n option was used # 2. the file ~/.bastille_disclaimer exists ########################################################################### sub showDisclaimer($) { my $nodisclaim = $_[0]; my $nodisclaim_file = &getGlobal('BFILE', "nodisclaimer"); my $response; my $WAIT_TIME = 300; # we'll wait for 5 minutes my $developersAnd; my $developersOr; if ($GLOBAL_OS =~ "^HP-UX") { $developersAnd ="HP AND ITS"; $developersOr ="HP OR ITS"; }else{ $developersAnd ="JAY BEALE, THE BASTILLE DEVELOPERS, AND THEIR"; $developersOr ="JAY BEALE, THE BASTILLE DEVELOPERS, OR THEIR"; } my $DISCLAIMER = "\n" . "Copyright (C) 1999-2006 Jay Beale\n" . "Copyright (C) 1999-2001 Peter Watkins\n" . "Copyright (C) 2000 Paul L. Allen\n" . "Copyright (C) 2001-2007 Hewlett-Packard Development Company, L.P.\n" . "Bastille is free software; you are welcome to redistribute it under\n" . "certain conditions. See the \'COPYING\' file in your distribution for terms.\n\n" . "DISCLAIMER. Use of Bastille can help optimize system security, but does not\n" . "guarantee system security. Information about security obtained through use of\n" . "Bastille is provided on an AS-IS basis only and is subject to change without\n" . "notice. Customer acknowledges they are responsible for their system\'s security.\n" . "TO THE EXTENT ALLOWED BY LOCAL LAW, Bastille (\"SOFTWARE\") IS PROVIDED TO YOU \n" . "\"AS IS\" WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, WHETHER ORAL OR WRITTEN,\n" . "EXPRESS OR IMPLIED. $developersAnd SUPPLIERS\n" . "DISCLAIM ALL WARRANTIES, EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION THE \n" . "IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.\n" . "Some countries, states and provinces do not allow exclusions of implied\n" . "warranties or conditions, so the above exclusion may not apply to you. You may\n" . "have other rights that vary from country to country, state to state, or province\n" . "to province. EXCEPT TO THE EXTENT PROHIBITED BY LOCAL LAW, IN NO EVENT WILL\n" . "$developersOr SUBSIDIARIES, AFFILIATES OR\n" . "SUPPLIERS BE LIABLE FOR DIRECT, SPECIAL, INCIDENTAL, CONSEQUENTIAL OR OTHER\n" . "DAMAGES (INCLUDING LOST PROFIT, LOST DATA, OR DOWNTIME COSTS), ARISING OUT OF\n" . "THE USE, INABILITY TO USE, OR THE RESULTS OF USE OF THE SOFTWARE, WHETHER BASED\n" . "IN WARRANTY, CONTRACT, TORT OR OTHER LEGAL THEORY, AND WHETHER OR NOT ADVISED\n" . "OF THE POSSIBILITY OF SUCH DAMAGES. Your use of the Software is entirely at your\n" . "own risk. Should the Software prove defective, you assume the entire cost of all\n" . "service, repair or correction. Some countries, states and provinces do not allow\n" . "the exclusion or limitation of liability for incidental or consequential \n" . "damages, so the above limitation may not apply to you. This notice will only \n". "display on the first run on a given system.\n". "To suppress the disclaimer on other machines, use Bastille\'s -n flag (example: bastille -n).\n"; # If the user has specified not to show the disclaimer, or # the .bastille_disclaimer file already exists, then return if( ( $nodisclaim ) || -e $nodisclaim_file ) { return 1; } # otherwise, show the disclaimer print ($DISCLAIMER); # there is a response my $touch = &getGlobal('BIN', "touch"); my $retVal = system("$touch $nodisclaim_file"); if( $retVal != 0 ) { &ErrorLog ( &getGlobal('ERROR','disclaimer')); } } # showDisclaimer ################################################################ # &systemCall #Function used by exported methods B_Backtick and B_system #to handle the mechanics of system calls. # This function also manages error handling. # Input: a system call # Output: a list containing the status, sstdout and stderr # of the the system call # ################################################################ sub systemCall ($){ no strict; local $command=$_[0]; # changed scoping so eval below can read it local $SIG{'ALRM'} = sub { die "timeout" }; # This subroutine exits the "eval" below. The program # can then move on to the next operation. Used "local" # to avoid name space collision with disclaim alarm. local $WAIT_TIME=120; # Wait X seconds for system commands local $commandOutput = ''; my $errOutput = ''; eval{ $errorFile = &getGlobal('BFILE','stderrfile'); unlink($errorFile); #To make sure we don't mix output alarm($WAIT_TIME); # start a time-out for command to complete. Some commands hang, and we want to # fail gracefully. When we call "die" it exits this eval statement # with a value we use below $commandOutput = `$command 2> $errorFile`; # run the command and gather its output my $commandRetVal = ($? >> 8); # find the commands return value if ($commandRetVal == 0) { &B_log("ACTION","Executed Command: " . $command . "\n"); &B_log("ACTION","Command Output: " . $commandOutput . "\n"); die "success"; } else { die "failure"; }; }; my $exitcode=$@; alarm(0); # End of the timed operation my $cat = &getGlobal("BIN","cat"); if ( -e $errorFile ) { $errOutput = `$cat $errorFile`; } if ($exitcode) { # The eval command above will exit with one of the 3 values below if ($exitcode =~ /timeout/) { &B_log("WARNING","No response received from $command after $WAIT_TIME seconds.\n" . "Command Output: " . $commandOutput . "\n"); return (0,'',''); } elsif ($exitcode =~ /success/) { return (1,$commandOutput,$errOutput); } elsif ($exitcode =~ /failure/) { return (0,$commandOutput,$errOutput); } else { &B_log("FATAL","Unexpected return state from command execution: $command\n" . "Command Output: " . $commandOutput . "\n"); } } } ############################################# # Use this **only** for commands used that are # intended to test system state and # not make any system change. Use this in place of the # prior use of "backticks throughout Bastille # Handles basic output redirection, but not for stdin # Input: Command # Output: Results ############################################# sub B_Backtick($) { my $command=$_[0]; my $combineOutput=0; my $stdoutRedir = ""; my $stderrRedir = ""; my $echo = &getGlobal('BIN','echo'); if (($command =~ s/2>&1//) or (s/>&2//)){ $combineOutput=1; } if ($command =~ s/>\s*([^>\s])+// ) { $stdoutRedir = $1; } if ($command =~ s/2>\s*([^>\s])+// ) { $stderrRedir = $1; } my ($ranFine, $stdout, $stderr) = &systemCall($command); if ($ranFine) { &B_log("DEBUG","Command: $command succeeded for test with output: $stdout , ". "and stderr: $stderr"); } else { &B_log("DEBUG","Command: $command failed for test with output: $stdout , ". "and stderr: $stderr"); } if ($combineOutput) { $stdout .= $stderr; $stderr = $stdout; #these should be the same } if ($stdoutRedir ne "") { system("$echo \'$stdout\' > $stdoutRedir"); } if ($stderrRedir ne "") { system("$echo \'$stderr\' > $stderrRedir"); } return $stdout; } #################################################################### # &B_System($command,$revertcommand); # This function executes a command, then places the associated # revert command in revert file. It takes two parameters, the # command and the command that reverts that command. # # uses ActionLog and ErrorLog for logging purposes. ################################################################### sub B_System ($$) { my ($command,$revertcmd)=@_; my ($ranFine, $stdout, $stderr) = &systemCall($command); if ($ranFine) { &B_revert_log ("$revertcmd \n"); if ($stderr ne '' ) { &B_log("ACTION",$command . "suceeded with STDERR: " . $stderr . "\n"); } return 1; } else { my $warningString = "Command Failed: " . $command . "\n" . "Command Output: " . $stdout . "\n"; if ($stderr ne '') { $warningString .= "Error message: " . $stderr; } &B_log("WARNING", $warningString); return 0; } } ########################################################################### # &isProcessRunning($procPattern); # # If called in scalar context this subroutine will return a 1 if the # pattern specified can be matched against the process table. It will # return a 0 otherwise. # If called in the list context this subroutine will return the list # of processes which matched the pattern supplied # # scalar return values: # 0: pattern not in process table # 1: pattern is in process table # # list return values: # proc lines from the process table if they are found ########################################################################### sub isProcessRunning($) { my $procPattern= $_[0]; my $ps = &getGlobal('BIN',"ps"); my $isRunning=0; # process table. my @psTable = `$ps -elf`; # list of processes that match the $procPattern my @procList; foreach my $process (@psTable) { if($process =~ $procPattern) { $isRunning = 1; push @procList, $process . "\n"; } } &B_log("DEBUG","$procPattern search yielded $isRunning\n\n"); # if this subroutine was called in scalar context if( ! wantarray ) { return $isRunning; } return @procList; } ########################################################################### # &checkProcsForService($service); # # Checks if the given service is running by analyzing the process table. # This is a helper function to checkServiceOnLinux and checkServiceOnHP # # Return values: # SECURE_CANT_CHANGE() if the service is off # INCONSISTENT() if the state of the service cannot be determined # # Mostly used in "check service" direct-return context, but added option use. # to ignore warning if a check for a service ... where a found service doesn't # have direct security problems. # ########################################################################### sub checkProcsForService ($;$) { my $service=$_[0]; my $ignore_warning=$_[1]; my @psnames=@{ &getGlobal('PROCESS',$service)}; my @processes; # inetd services don't have a separate process foreach my $psname (@psnames) { my @procList = &isProcessRunning($psname); if(@procList >= 0){ splice @processes,$#processes+1,0,@procList; } } if($#processes >= 0){ if ((defined($ignore_warning)) and ($ignore_warning eq "ignore_warning")) { &B_log("WARNING","The following processes were still running even though " . "the corresponding service appears to be turned off. Bastille " . "question and action will be skipped.\n\n" . "@processes\n\n"); # processes were still running, service is not off, but we don't know how # to configure it so we skip the question return INCONSISTENT(); } else { return NOTSECURE_CAN_CHANGE(); # In the case we're ignoring the warning, # ie: checking to make *sure* a process # is running, the answer isn't inconsistent } } else { &B_log("DEBUG","$service is off. Found no processes running on the system."); # no processes, so service is off return SECURE_CANT_CHANGE(); } # Can't determine the state of the service by looking at the processes, # so return INCONSISTENT(). return INCONSISTENT(); } ########################################################################### # B_parse_fstab() # # Search the filesystem table for a specific mount point. # # scalar return value: # The line form the table that matched the mount point, or the null string # if no match was found. # # list return value: # A list of parsed values from the line of the table that matched, with # element [3] containing a reference to a hash of the mount options. The # keys are: acl, dev, exec, rw, suid, sync, or user. The value of each key # can be either 0 or 1. To access the hash, use code similar to this: # %HashResult = %{(&B_parse_fstab($MountPoint))[3]}; # ########################################################################### sub B_parse_fstab($) { my $name = shift; my $file = &getGlobal('FILE','fstab'); my ($enable, $disable, $infile); my @lineopt; my $retline = ""; my @retlist = (); unless (open FH, $file) { &B_log('ERROR',"B_parse_fstab couldn't open fstab file at path $file.\n"); return 0; } while () { s/\#.*//; next unless /\S/; @retlist = split; next unless $retlist[1] eq $name; $retline .= $_; if (wantarray) { my $option = { # initialize to defaults acl => 0, # for ext2, etx3, reiserfs dev => 1, exec => 1, rw => 1, suid => 1, sync => 0, user => 0, }; my @lineopt = split(',',$retlist[3]); foreach my $entry (@lineopt) { if ($entry eq 'acl') { $option->{'acl'} = 1; } elsif ($entry eq 'nodev') { $option->{'dev'} = 0; } elsif ($entry eq 'noexec') { $option->{'exec'} = 0; } elsif ($entry eq 'ro') { $option->{'rw'} = 0; } elsif ($entry eq 'nosuid') { $option->{'suid'} = 0; } elsif ($entry eq 'sync') { $option->{'sync'} = 1; } elsif ($entry eq 'user') { $option->{'user'} = 1; } } $retlist[3]= $option; } last; } if (wantarray) { return @retlist; } else { return $retline; } } ########################################################################### # B_parse_mtab() # # This routine returns a hash of devices and their mount points from mtab, # simply so you can get a list of mounted filesystems. # ########################################################################### sub B_parse_mtab { my $mountpoints; open(MTAB,&getGlobal('FILE','mtab')); while(my $mtab_line = ) { #test if it's a device if ($mtab_line =~ /^\//) { #parse out device and mount point $mtab_line =~ /^(\S+)\s+(\S+)/; $mountpoints->{$1} = $2; } } return $mountpoints; } ########################################################################### # B_is_rpm_up_to_date() # # ########################################################################### sub B_is_rpm_up_to_date(@) { my($nameB,$verB,$relB,$epochB) = @_; my $installedpkg = $nameB; if ($epochB =~ /(none)/) { $epochB = 0; } my $rpmA = `rpm -q --qf '%{VERSION}-%{RELEASE}-%{EPOCH}\n' $installedpkg`; my $nameA = $nameB; my ($verA,$relA,$epochA); my $retval; # First, if the RPM isn't installed, let's handle that. if ($rpmA =~ /is not installed/) { $retval = -1; return $retval; } else { # Next, let's try to parse the EVR information without as few # calls as possible to rpm. if ($rpmA =~ /([^-]+)-([^-]+)-([^-]+)$/) { $verA = $1; $relA = $2; $epochA = $3; } else { $nameA = `rpm -q --qf '%{NAME}' $installedpkg`; $verA = `rpm -q --qf '%{VERSION}' $installedpkg`; $relA = `rpm -q --qf '%{RELEASE}' $installedpkg`; $epochA = `rpm -q --qf '%{EPOCH}' $installedpkg`; } } # Parse "none" as 0. if ($epochA =~ /(none)/) { $epochA = 0; } # Handle the case where only one of them is zero. if ($epochA == 0 xor $epochB == 0) { if ($epochA != 0) { $retval = 1; } else { $retval = 0; } } else { # ...otherwise they are either both 0 or both non-zero and # so the situation isn't trivial. # Check epoch first - highest epoch wins. my $rpmcmp = &cmp_vers_part($epochA, $epochB); #print "epoch rpmcmp is $rpmcmp\n"; if ($rpmcmp > 0) { $retval = 1; } elsif ($rpmcmp < 0) { $retval = 0; } else { # Epochs were the same. Check Version now. $rpmcmp = &cmp_vers_part($verA, $verB); #print "epoch rpmcmp is $rpmcmp\n"; if ($rpmcmp > 0) { $retval = 1; } elsif ($rpmcmp < 0) { $retval = 0; } else { # Versions were the same. Check Release now. my $rpmcmp = &cmp_vers_part($relA, $relB); #print "epoch rpmcmp is $rpmcmp\n"; if ($rpmcmp >= 0) { $retval = 1; } elsif ($rpmcmp < 0) { $retval = 0; } } } } return $retval; } ################################################# # Helper function for B_is_rpm_up_to_date() ################################################# #This cmp_vers_part function taken from Kirk Bauer's Autorpm. # This version comparison code was sent in by Robert Mitchell and, although # not yet perfect, is better than the original one I had. He took the code # from freshrpms and did some mods to it. Further mods by Simon Liddington # . # # Splits string into minors on . and change from numeric to non-numeric # characters. Minors are compared from the beginning of the string. If the # minors are both numeric then they are numerically compared. If both minors # are non-numeric and a single character they are alphabetically compared, if # they are not a single character they are checked to be the same if the are not # the result is unknown (currently we say the first is newer so that we have # a choice to upgrade). If one minor is numeric and one non-numeric then the # numeric one is newer as it has a longer version string. # We also assume that (for example) .15 is equivalent to 0.15 sub cmp_vers_part($$) { my($va, $vb) = @_; my(@va_dots, @vb_dots); my($a, $b); my($i); if ($vb !~ /^pre/ and $va =~ s/^pre(\d+.*)$/$1/) { if ($va eq $vb) { return -1; } } elsif ($va !~ /^pre/ and $vb =~ s/^pre(\d+.*)$/$1/) { if ($va eq $vb) { return 1; } } @va_dots = split(/\./, $va); @vb_dots = split(/\./, $vb); $a = shift(@va_dots); $b = shift(@vb_dots); # We also assume that (for example) .15 is equivalent to 0.15 if ($a eq '' && $va ne '') { $a = "0"; } if ($b eq '' && $vb ne '') { $b = "0"; } while ((defined($a) && $a ne '') || (defined($b) && $b ne '')) { # compare each minor from left to right if ((not defined($a)) || ($a eq '')) { return -1; } # the longer version is newer if ((not defined($b)) || ($b eq '')) { return 1; } if ($a =~ /^\d+$/ && $b =~ /^\d+$/) { # I have changed this so that when the two strings are numeric, but one or both # of them start with a 0, then do a string compare - Kirk Bauer - 5/28/99 if ($a =~ /^0/ or $b =~ /^0/) { # We better string-compare so that netscape-4.6 is newer than netscape-4.08 if ($a ne $b) {return ($a cmp $b);} } # numeric compare if ($a != $b) { return $a <=> $b; } } elsif ($a =~ /^\D+$/ && $b =~ /^\D+$/) { # string compare if (length($a) == 1 && length($b) == 1) { # only minors with one letter seem to be useful for versioning if ($a ne $b) { return $a cmp $b; } } elsif (($a cmp $b) != 0) { # otherwise we should at least check they are the same and if not say unknown # say newer for now so at least we get choice whether to upgrade or not return -1; } } elsif ( ($a =~ /^\D+$/ && $b =~ /^\d+$/) || ($a =~ /^\d+$/ && $b =~ /^\D+$/) ) { # if we get a number in one and a word in another the one with a number # has a longer version string if ($a =~ /^\d+$/) { return 1; } if ($b =~ /^\d+$/) { return -1; } } else { # minor needs splitting $a =~ /\d+/ || $a =~ /\D+/; # split the $a minor into numbers and non-numbers my @va_bits = ($`, $&, $'); $b =~ /\d+/ || $b =~ /\D+/; # split the $b minor into numbers and non-numbers my @vb_bits = ($`, $&, $'); for ( my $j=2; $j >= 0; $j--) { if ($va_bits[$j] ne '') { unshift(@va_dots,$va_bits[$j]); } if ($vb_bits[$j] ne '') { unshift(@vb_dots,$vb_bits[$j]); } } } $a = shift(@va_dots); $b = shift(@vb_dots); } return 0; } 1;