#!/usr/bin/perl -w

# Programm zur Rechner-Sicherheitsüberprüfung
# Diplomarbeit von Stephan Löscher
# Bei Rückfragen:
# loescher@gmx.de oder 08142/7257

######################################################################
###
###   IMPORTANT!
###   Please read the warranty and legal notice 
###   at the end of this file!
###
######################################################################

require 5.000;
use lib '/usr/local/bin',"$ENV{HOME}/bin",'/usr/stud/loescher/bin';
use lib 'd:/bin','c:/mydos','c:/bin';
use slutil; # Verfügbar unter: http://www.leo.org/~loescher/progdata/slutil.pm
use English;
use FileHandle;
use Carp;

use SHA; # Unbedingt SHA-1 statt SHA verwenden!

######################################################################
### Unterprogramm-Funktionen für interne Zwecke
######################################################################

sub debug_rsh;  # Ausgabe von Informationen über remote-shell
sub debug;      # Ausgabe von Informationen
sub warning;    # Ausgabe von Warnungen
sub error;      # Ausgabe von Fehlern
sub myexit;     # Exit und Aufräumen
sub loginit;    # Schreibt einen kleinen Header ins LOG-file
sub logdie;     # Schreibt einen Text ins LOG-file und stirbt dann
sub logprint;   # Schreibt einen Text ins LOG-file

######################################################################
### Voreinstellungen
######################################################################

$version = '1.0';
$appname = 'SysCheck';

# Wohin soll das logging erfolgen?
$logfile = ">>/tmp/\L$appname\E.log"; # Log in eine Datei
# $logfile = ">&STDERR"; # Log auf STDERR

# Exitcodes beim Beenden
$NormalExitCode = 0;
$ErrorExitCode  = 1;

$0 = $0; # Oh, very strange... :-) ... Kommandozeile verbergen.

# Konstante Meldungen
$NichtLesbar                  = 'Nicht lesbar';
$NichtLaengeNull              = 'Nicht Länge Null';
$ExistiertNicht               = 'Existiert nicht';
$LinkExistiertNicht           = 'Link zeigt auf nicht vorhandenes File';
$ExistiertNichtSollteNullSein = 'Existiert nicht, sollte Länge Null haben';
$DarfNichtExistieren          = 'Darf nicht existieren';
$Weltschreibbar               = 'Datei ist welt-schreibbar';
$Entferne                     = 'Entferne';
$Suid                         = 'SUID-Bit gesetzt';
$Sgid                         = 'SGID-Bit gesetzt';
$Sticky                       = 'Sticky-Bit gesetzt';
$UngueltigeUID                = 'Ungültige UID';
$UngueltigeGID                = 'Ungültige GID';
$Type                         = 'Dateityp hat sich geändert';
$Uid                          = 'UserID hat sich geändert';
$Gid                          = 'GruppenID hat sich geändert';
$MTime                        = 'Zeitstempel hat sich geändert';
$Size                         = 'Dateigröße hat sich geändert';
$Mode                         = 'Mode hat sich geändert';
$Check                        = 'Prüfsumme hat sich geändert';
$Link                         = 'Link-Ziel hat sich geändert';
$RDev                         = 'Geräte-Attribute haben sich geändert';

######################################################################
### Log-File und Signal-Handler
######################################################################

# LOG bereits hier starten, denn Konfigurationsfehler sind entscheidend!
open(LOG, $logfile) || die "Kann Logfile '$logfile' nicht schreiben!\n";
select(LOG); $|=1; select(STDOUT); # Buffer ausschalten
loginit;

# Signal-Handler installieren
$SIG{HUP}  = \&catch_signal;
$SIG{INT}  = \&catch_signal;
$SIG{QUIT} = \&catch_signal;
$SIG{ABRT} = \&catch_signal;
$SIG{TERM} = \&catch_signal;
$SIG{'__WARN__'} = \&catch_warning;

logprint "Parameter sind: @ARGV\n";

$logLevel  = 0;
$inputfile = '-';
$BatchMode = $FALSE;
$RemoveOld = $FALSE;
$Quiet     = $FALSE;
while ( defined $ARGV[0] && ($ARGV[0] =~ /^-/ ) )
{
  SWITCH:
  {
    if ($ARGV[0] =~ /^-w(\d)/)
    {
      $logLevel = $1;
      shift;
      last SWITCH;
    }
    if ($ARGV[0] eq '-f')
    {
      shift;
      $inputfile = shift;
      unless (-r $inputfile)
      {
        logdie "Kann Datei '$inputfile', die bei '-f' angegeben ist nicht ".
        "lesen!\n";
      }
      last SWITCH;
    }
    if ($ARGV[0] eq '-b')
    {
      $BatchMode = $TRUE;
      shift;
      last SWITCH;
    }
    if ($ARGV[0] eq '-r')
    {
      $RemoveOld = $TRUE;
      shift;
      last SWITCH;
    }
    if ($ARGV[0] eq '-q')
    {
      $Quiet = $TRUE;
      shift;
      last SWITCH;
    }
    # Sonst:
    die "Option '$ARGV[0]' gibt es nicht!\n";
  }
}
logprint "Log-Level: $logLevel\n";
logprint "Eingabedatei: $inputfile\n";


######################################################################
### Hauptprogramm
######################################################################

if ($#ARGV<0)
{
  logprint "Ohne Parameter aufgerufen.\n";
  print "\nSie sollten $appname nicht ohne Parameter aufrufen.
Sie haben zur Auswahl:
  1. man-page erzeugen und ins aktuelle Verzeichnis schreiben
  2. HTML-Dokumentation ins aktuelle Verzeichnis schreiben
  3. LaTeX-Dokumentation ins aktuelle Verzeichnis schreiben
  4. Alle Dokumentationen (1 bis 3) erzeugen
  5. Kurzhilfe anzeigen
Auswahl: ";
  $input = readkey(); print "\n";
  POD_Ausgabe('man')   if $input =~ /1/;
  POD_Ausgabe('html')  if $input =~ /2/;
  POD_Ausgabe('latex') if $input =~ /3/;
  if ($input =~ /4/)
  {
    POD_Ausgabe('man');
    POD_Ausgabe('html');
    POD_Ausgabe('latex');
  }
  &Hilfe               if $input =~ /5/;
  myexit;
}

printumlaute Kopf() unless $Quiet;

$root = '';
ReadConfigFile();
$datenbankfile  = $root.$slash.'syscheck.db';
$immerliste = $root.$slash.'syscheckfiles.sc';
debug "SYSCHECK_ROOT = $root\n";
debug "Sicherheitsdatenbank = $datenbankfile\n";
debug "Standardeinstellungen = $immerliste\n";

$std = Standardeinstellungen->new($immerliste);


$aktion = shift || logdie "Keine Aktion angegeben!\n";
SWITCH: {
  # Init
  if ($aktion eq 'init')  
  {
    #    unlink "$datenbankfile.dir", "$datenbankfile.pag";
    unlink $datenbankfile;
    UpdateDatenbank($datenbankfile);
    last SWITCH;
  }
  
  # Update
  if ($aktion eq 'update')
  {
    UpdateDatenbank($datenbankfile);
    last SWITCH;
  }
  
  # Check
  if ($aktion eq 'check')
  {
    CheckDatenbank($datenbankfile);
    last SWITCH;
  }
  
  # Remove
  if ($aktion eq 'remove')
  {
    Remove($datenbankfile);
    last SWITCH;
  }
  
  logdie "Die Aktion '$aktion' gibt es nicht!\n";
}


myexit($NormalExitCode);


######################################################################
### Unterprogramme
######################################################################


sub UpdateDatenbank
{
  # Es wird die Sicherheitsdatenbank upgedatet
  # Parameter: Name der Datenbank, Name der "Immer-Liste"
  # Return: -
  #

  my ($datenbankfile, $immerliste) = @_;
  $db = Datenbank::new($datenbankfile);

  # Standardeinstellungen einlesen
  my @liste  = $std->ImmerListe;
  my $ignore = $std->IgnorePattern;
  debug "Ignore-Pattern: '$ignore'\n";

  # Öffnen der Dateiliste
  my $fh = FileHandle->new();
  open($fh, $inputfile) || logdie "Kann Dateiliste '$inputfile' nicht ".
  "öffnen!\n";

  my $pass = PasswordTest($fh);

  CheckDarfNichtExistieren($std->DarfNichtExistierenListe);
  CheckNichtLaengeNull    ($std->NichtLaengeNullListe    );

  my ($mode,$uid,$gid,$rdev,$size,$mtime);
  my $check = new SHA;
  while(defined ($filename = shift @liste || <$fh>) )
  {
    chomp $filename;

    next if $filename =~ /$ignore/o;

    print STDERR $filename,"\n" unless $Quiet;
    unless (-e $filename)
    {
      if (-l $filename)
      {
        Ergebnis($LinkExistiertNicht, $filename);
        next;
      }
      Ergebnis($ExistiertNicht, $filename);
      next;
    }
    unless (-r $filename)
    {
      Ergebnis($NichtLesbar, $filename);
      next;
    }

    (undef,undef,$mode,undef,$uid,$gid,$rdev,$size,undef,$mtime)
    = lstat($filename);

    # Dateityp
    my $type;
    if    (-l $filename) { $type = 'L' } # Link
    elsif (-f _        ) { $type = 'F' } # File
    elsif (-d _        ) { $type = 'D' } # Directory
    elsif (-p _        ) { $type = 'P' } # Pipe
    elsif (-S _        ) { $type = 'S' } # Socket
    elsif (-b _        ) { $type = 'B' } # Block special
    elsif (-c _        ) { $type = 'C' } # Char special
    else # Sonstwas
    {
      logdie "Kann Dateityp von '$filename' nicht feststellen!\n";
    }

    ModeOwnerGroupTest($filename,$mode,$uid,$gid,$type);

    $db->SetType($filename, $type);
    $db->SetUid  ($filename, $uid);
    $db->SetGid  ($filename, $gid);
    $db->SetMtime($filename, $mtime);

    # Normale Dateien
    if ($type eq 'F')
    {
      $db->SetSize ($filename, $size);
      $db->SetMode ($filename, $mode);
      my $fh = FileHandle->new(); open($fh, $filename);
      $check->reset; $check->add($pass); $check->addfile($fh);
      $db->SetCheck($filename, $check->hexdigest);
    }

    # Links
    if ($type eq 'L')
    {
      $db->SetLinkTo($filename, readlink($filename));
    }

    # Verzeichnisse
    if ($type eq 'D')
    {
      $db->SetMode ($filename, $mode);
    }

    # Spezielle Dateien
    if ($type =~ /[PSBC]/)
    {
      $db->SetMode ($filename, $mode);
      $db->SetRdev($filename, $rdev);
    }
  }
  close $fh;
}


sub CheckDatenbank
{
  # Es werden die Dateien anhand der Sicherheitsdatenbank überprüft
  # Parameter: Name der Datenbank, Name der "Immer-Liste"
  # Return: -
  #

  my ($datenbankfile, $immerliste) = @_;
  $db    = Datenbank::new($datenbankfile);

  my $pass = PasswordTest();

  CheckDarfNichtExistieren($std->DarfNichtExistierenListe);
  CheckNichtLaengeNull    ($std->NichtLaengeNullListe    );

  my ($mode,$uid,$gid,$rdev,$size,$mtime);
  my $check = new SHA;

  my $fh = $db->Liste;
  while( defined ($filename = <$fh>) )
  {
    chomp $filename;
    print STDERR $filename,"\n" unless $Quiet;
    unless (-e $filename)
    {
      if (-l $filename)
      {
        Ergebnis($LinkExistiertNicht, $filename);
        next;
      }
      Ergebnis($ExistiertNicht, $filename);
      if ($RemoveOld)
      {
        Ergebnis($Entferne, $filename);
        $db->Entferne($filename);
      }
      next;
    }
    unless (-r $filename)
    {
      Ergebnis($NichtLesbar, $filename);
      next;
    }

    (undef,undef,$mode,undef,$uid,$gid,$rdev,$size,undef,$mtime)
    = lstat($filename);

    # Dateityp
    my $type;
    if    (-l $filename) { $type = 'L' } # Link
    elsif (-f _        ) { $type = 'F' } # File
    elsif (-d _        ) { $type = 'D' } # Directory
    elsif (-p _        ) { $type = 'P' } # Pipe
    elsif (-S _        ) { $type = 'S' } # Socket
    elsif (-b _        ) { $type = 'B' } # Block special
    elsif (-c _        ) { $type = 'C' } # Char special
    else # Sonstwas
    {
      logdie "Kann Dateityp von '$filename' nicht feststellen!\n";
    }

    ModeOwnerGroupTest($filename,$mode,$uid,$gid,$type);

    Ergebnis($Type, $filename) if $type  ne $db->Type ($filename);
    Ergebnis($Uid,  $filename) if $uid   != $db->Uid  ($filename);
    Ergebnis($Gid,  $filename) if $gid   != $db->Gid  ($filename);
    Ergebnis($MTime,$filename) if $mtime != $db->Mtime($filename);

    # Normale Dateien
    if ($type eq 'F')
    {
      Ergebnis($Size, $filename) if $size != $db->Size($filename);
      Ergebnis($Mode, $filename) if $mode != $db->Mode($filename);
      my $fh = FileHandle->new(); open($fh, $filename);
      $check->reset; $check->add($pass); $check->addfile($fh);
      Ergebnis($Check,$filename) if $check->hexdigest ne $db->Check($filename);
    }

    # Links
    if ($type eq 'L')
    {
      Ergebnis($Link,$filename) if readlink($filename)
      ne $db->LinkTo($filename);
    }

    # Verzeichnisse
    if ($type eq 'D')
    {
      Ergebnis($Mode, $filename) if $mode != $db->Mode($filename);
    }

    # Spezielle Dateien
    if ($type =~ /[PSBC]/)
    {
      Ergebnis($Mode, $filename) if $mode != $db->Mode($filename);
      Ergebnis($RDev, $filename) if $rdev != $db->Rdev($filename);
    }
  }
}


sub Remove
{
  # Es werden die angegebenen Dateien aus der Sicherheitsdatenbank entfernt
  # Parameter: Name der Datenbank
  # Return: -
  #

  my $datenbankfile = shift;
  $db = Datenbank::new($datenbankfile);

  # Öffnen der Dateiliste
  my $fh = FileHandle->new();
  open($fh, $inputfile) || logdie "Kann Dateiliste '$inputfile' nicht ".
  "öffnen!\n";

  PasswordTest($fh);

  while(defined ($filename = <$fh>) )
  {
    chomp $filename;
    Ergebnis($Entferne, $filename);
    $db->Entferne($filename);
  }
  close $fh;
}


sub PasswordTest
{
  # Parameter: optional FileHandle
  # Return:    Paßwort im Klartext
  # Abbruch, wenn Paßwort nicht stimmt.
  #
  my $fh    = shift;
  my $pass  = '';
  my $crypt = '';
  my $passwdfile = $root.$slash.'syscheck.passwd';
  my $pfh = FileHandle->new();

  # Neues Paßwort vergeben
  if (!-e $passwdfile)
  {
    printumlaute "\nDas ist Ihr erster syscheck-Aufruf, da die Paßwortdatei
'$passwdfile'
noch nicht existiert. Sie müssen nun ein neues Paßwort eingeben.
Sollte das nicht der erste Aufruf sein und Sie schon ein Paßwort vergeben
haben, so ist entweder nur die Paßwortdatei nicht auffindbar oder es wurde
in Ihr System eingebrochen und die Sicherheitsdatenbank verändert.\n\n";
    # Ist STDIN mit einem Terminal verbunden?
    if (-t STDIN && !$BatchMode)
    {
      $pass = ReadWithoutEcho('Syscheck-Paßwort: ','STDIN');
      print "\n";
      my $pass2 = ReadWithoutEcho('Wiederholung: ','STDIN');
      print "\n";
      unless ($pass eq $pass2)
      { logdie "Paßwörter bei Neueingabe unterschiedlich!\n" }
    }
    else
    {
      logdie "STDIN ist nicht mit einem Terminal verbunden und es kann weder ".
      "von STDIN, noch von der Datei, die mit -f angegeben wurde ein ".
      "Paßwort gelesen werden!\n";
    }
    $crypt = crypt($pass,0);
    open($pfh, ">$passwdfile") || logdie "Kann '$passwdfile' nicht zum ".
    "Schreiben öffnen!\n";
    print $pfh $crypt;
    close $pfh;
    return $pass;
  }

  # Paßwort abprüfen
  # Ist STDIN mit einem Terminal verbunden?
  if (-t STDIN && !$BatchMode)
  {
    $pass = ReadWithoutEcho('Syscheck-Paßwort: ','STDIN');
    print "\n";
  }
  # Lesen aus der Datei
  else
  {
    if (defined $fh)
    {
      $pass = <$fh>;
      chomp $pass;
    }
    else
    {
      logdie "STDIN ist nicht mit einem Terminal verbunden und es kann weder ".
      "von STDIN, noch von der Datei, die mit -f angegeben wurde ein ".
      "Paßwort gelesen werden!\n";
    }
  }
  $crypt = crypt($pass,0);
  open($pfh, $passwdfile) || logdie "Kann '$passwdfile' nicht zum ".
  "Lesen öffnen!\n";
  my $savedpass = <$pfh>;
  close $pfh;
  logdie "Paßwort stimmt nicht!\n" unless $crypt eq $savedpass;
  return $pass;
}


sub CheckDarfNichtExistieren
{
  # Überprüft, daß die angegebenen Files _nicht_ existieren
  # Parameter: Dateiliste
  # Return;    -
  #
  foreach(@_)
  {
    Ergebnis($DarfNichtExistieren, $_) if -e;
  }
}


sub CheckNichtLaengeNull
{
  # Überprüft, daß die angegebenen Files die Länge Null haben
  # Parameter: Dateiliste
  # Return;    -
  #
  foreach(@_)
  {
    unless (-e)
    {
      Ergebnis($ExistiertNichtSollteNullSein, $_);
      next;
    }
    Ergebnis($NichtLaengeNull, $_) unless -z;
  }
}


sub ModeOwnerGroupTest
{
  # Tests bzgl. Mode, UID, GID und Gruppe
  # Paremeter: Dateiname, Mode, Uid, Gid, Dateityp
  # Return: - 
  #
  my ($file,$mode,$uid,$gid,$type) = @_;

  # Mode-Aufbau: rwx rwx rwx

  # Ist die Datei welt-schreibbar?
  Ergebnis($Weltschreibbar, $file) if ($mode & 2) && ($type ne 'L');

  # Hat die Datei das SUID-Bit gesetzt?
  Ergebnis($Suid, $file) if -u $file;

  # Hat die Datei das SGID-Bit gesetzt?
  Ergebnis($Sgid, $file) if -g $file;

  # Hat die Datei das Sticky-Bit gesetzt?
  Ergebnis($Sticky, $file) if -k $file;

  # Prüfen, ob UID gültig ist
  Ergebnis($UngueltigeUID, $file) unless defined getpwuid($uid);

  # Prüfen, ob GID gültig ist
  Ergebnis($UngueltigeGID, $file) unless defined getgrgid($gid);
}


sub Ergebnis
{
  # Funktion zur Fehlerausgabe
  # Parameter: Fehlermeldung, Dateiname
  # Return: -
  #
  my ($meldung, $datei) = @_;
  unless ($Quiet)
  {
    print STDERR "# $meldung: $datei\n";
  }
  else
  {
    logprint "# $meldung: $datei\n";
  }
}


sub TesteDateiBesitzer
{
  # Parameter: Voller Pfad einer Datei
  # Kein Returnwert. (Bei Gefahr sofort Abbruch.)
  #
  # Es wird überprüft, ob eine Konfigurationsdatei nur für den Menschen
  # schreibbar ist, der auch syscheck ausführt.
  # Sonst könnte irgendjemand die Konfigurationsfiles verändern und Root
  # läßt das dann aufs System los.
  #
  my $file = shift;
  if ( (lstat($file))[2] != 33188 ) # "-rw-r--r--"
  {
    logdie "Sicherheitslücke: Das File '$file' ist nicht Mode 644!\n" 
  }
  unless (-o $file)
  {
    logdie "Sicherheitslücke: Sie sind nicht Besitzer von '$file'\n"
  }
}


sub ReadConfigFile
{
  # Setzen von globalen Parametern aus dem Konfigurationsfile
  # Parameter: -
  # Return:    -
  #
  my $file = './syscheckrc';
  $file = "$ENV{HOME}/.syscheckrc" unless -r $file;
  $file = '/etc/syscheckrc' unless -r $file;
  logdie "Kann weder './syscheckrc' noch '$ENV{HOME}/.syscheckrc' noch ".
  "'/etc/syscheckrc' lesen!\n" unless -r $file;
  debug "Verwende Konfigurationsfile '$file'\n";
 
  TesteDateiBesitzer($file);

  my $fh = FileHandle->new();
  open($fh, $file);
  while(<$fh>)
  {
    next if /^\#/; # Kommentare überspringen
    $root  = $1 if /^SYSCHECK_ROOT\s*=\s*(.+)/i;
    next;
  }
  close $fh;
  logdie "Kein 'SYSCHECK_ROOT' in '$file' definiert!\n" if $root eq '';
  $root = KillSlashAtEnd($root);
}


######################################################################
### Debug, Logging, Exit, ...
######################################################################


sub myexit
{
  # Diese Funktion macht einen normalen exit() mit dem übergebenen
  # Exitcode
  # und erledigt vorher noch Aufräum-Arbeiten, wie LOG-file schließen, ...
  my $error = defined $_[0] ? shift : $NormalExitCode;
  logprint("$appname PID $$ Ende um ",date,"\n");
  close(LOG);
  exit $error;
}


sub logprint
{
  # Schreibt einen Text ins LOG-file
  print LOG @_;
}


sub debug
{
  # Es werden Debug-Informationen erzeugt
  logprint("DEBUG: ",@_) if ($logLevel >= 3);
}


sub warning
{
  # Falls Warnings eingeschaltet sind, dann werden Informationen erzeugt
  logprint("WARN:  ",@_) if ($logLevel >= 2);
}


sub error
{
  # Falls Warnings eingeschaltet sind, dann werden Informationen erzeugt
  logprint("ERROR: ",@_) if ($logLevel >= 1);
}


sub loginit
{
  # Schreibt einen kleinen Header ins LOG-file
  my $login = (getpwuid($<))[0] || 'unknown';
  logprint("\n---\n\n$appname $version PID $$ mit Perl $]\nStart um ",date,
           " durch ",$login,"\n");
}


sub logdie
{
  # Schreibt einen Text ins LOG-file und stirbt dann
  print "FATAL: ",@_;
  logprint("FATAL: ",@_);
  myexit($ErrorExitCode);
}


sub catch_signal 
{
  my $signame = shift;
  logdie "Ende von $appname wegen Signal SIG$signame.\n";
}


sub catch_warning
{
  # Abfangen von Laufzeit-Warnungen
  my $warnung = shift;
  warning "INTERNAL: $warnung     (Interne Warnungen deuten auf einen ",
          "möglichen internen Programm-Fehler\n",
          "     hin oder auf eine fehlerhafte Eingabe, die nicht abgefangen ",
          "wurde!)\n";
  print $warnung if ($logLevel < 2);
}


######################################################################
### Kopf und Hilfe
######################################################################


sub Kopf
{
  my $head = "$appname $version   -   von Stephan Löscher";
  return "\n$head\n" . '~' x length($head) . "\n";
}


sub Hilfe
{
  printumlautepaged
  Kopf().
"Syntax: syscheck optionen action

Syscheck überwacht Dateien anhand einer Sicherheitsdatenbank mit Prüfsummen
auf Veränderungen.

Es werden die Namen der zu überprüfenden Dateien zeilenweise von STDIN gelesen.
Außerdem kann man in der Datei syscheckfiles.sc Standardeinstellungen vorgeben.
Alle Unstimmigkeiten werden auf STDERR ausgegeben.
Zur Sicherheit wird ein Paßwort benötigt. Wenn STDIN von syscheck nicht mit
einem Terminal verbunden ist, dann erwartet syscheck das Paßwort in der ersten
Zeile der Eingabe von STDIN. Wenn -b angegeben ist, dann wird das Paßwort nie
vom Terminal gelesen, sondern immer von STDIN oder der Datei, die mit -f
angegeben ist. Das Paßwort wird mit zur Prüfsummenberechnung verwendet.

action    := init | update | check | remove
Erklärungen:
init:   Sicherheitsdatenbank erstellen
update: Sicherheitsdatenbank aktualisieren
check:  Überprüfung anhand der Sicherheitsdatenbank
remove: Die angegebenen Dateien werden aus der Sicherheitsdatenbank entfernt

Optionen:
-f FILE : Namen, der überprüfenden Dateien werden zeilenweise aus FILE gelesen.
-b      : Batch-Mode: Paßwort von der Eingabedatei lesen und nicht vom Terminal
-r      : Remove: Bei 'check' werden nicht mehr vorhandene Dateien aus der
          Datenbank entfernt.
-q      : Quiet: Keine Meldungen nach STDOUT oder STDERR
          Die STDERR-Meldungen landen stattdessen im Logfile.
-wX     : mit X=0-4 gibt den LOG-Level an.
          0: Nur fatale Fehler
          1: zusätzlich alle Fehler (Genaue Fehlerbeschreibungen)
          2: zusätzlich alle Warnungen (ganz informativ)
          3: zusätzlich alle Debug-Informationen (ausführlicher Status, etc.)

Beispiele:
find / -print -xdev > liste.txt
syscheck -f liste.txt init 2> /tmp/out
syscheck -f liste.txt update
syscheck -w3 check 2> /tmp/out

Man kann auch mit einem Pipe arbeiten:
( echo 'MeinGeheimesPaßwort' ; find / -xdev ) | syscheck init 2> out

Die kritischen Files kann man so extrahieren:
grep ^# /tmp/out | perl -pe 's/^#[^:]+: //'


";
  logprint "Es wird nur Hilfe ausgegeben.\n";
  myexit;
}


sub POD_Ausgabe
{
  # Erstellt Dokumentation aus POD im aktuellen Verzeichnis
  # Parameter: "man" oder "html" oder "latex"
  #
  $art = shift;
  if ($art eq 'man')
  {
    which('pod2man') || die "Leider kein 'pod2man' verfügbar!\n";
    which('nroff') || die "Leider kein 'nroff' verfügbar!\n";
    system("pod2man $0 | nroff -man > \L$appname\E.man");
  }
  if ($art eq 'html')
  {
    which('pod2html') || die "Leider kein 'pod2html' verfügbar!\n";
    system("pod2html $0 > \L$appname\E.html");
    # Nachbesserung: (FIXME)
    system('perl -i -pe \'s/&lt;(.?)EM&gt;/<${1}EM>/g\' '."\L$appname\E.html");
  }
  if ($art eq 'latex')
  {
    which('pod2latex') || die "Leider kein 'pod2latex' verfügbar!\n";
    system("pod2latex \L$appname\E");
    open(FH,"\L$appname\E.tex");
    @tex = <FH>;
    close FH;
    unshift @tex, '\documentclass[9pt]{article}\usepackage{german,a4,t1enc}'.
    '\usepackage[latin1]{inputenc}\begin{document}\def\C++{{\rm C'.
    '\kern-.05em\raise.3ex\hbox{\footnotesize ++}}}\def\underscore'.
    '{\leavevmode\kern.04em\vbox{\hrule width 0.4em height 0.3pt}}'.
    '\setlength{\parindent}{0pt}';
    push @tex, '\end{document}';
    grep(s/\"/\'\'/g, @tex); # Anführungszeichen ersetzen
    open(FH,">\L$appname\E.tex");
    print FH @tex;
    close FH;
  }
}

######################################################################
### Datenbank-Objekt
######################################################################

package Datenbank;

use Carp;
use FileHandle;
use Fcntl;
use Storable 0.603;
use MLDBM 2.00 qw (DB_File Storable);
# SDBM_File ist fehlerhaft implementiert in Perl 5.004_04

sub new
{
  my $datenbank = shift;
  my $func = (caller(0))[3];
  unless (defined $datenbank)
  {
    carp("$func() ohne Parameter aufgerufen!\n");
    main::myexit($main::ErrorExitCode);
  }
  main::debug "Datenbank: Verwende Datenbank '$datenbank'\n";
#  my $daten = {
# Das sieht so aus:
#              Dateiname => {
#                             TYPE   => Typ (File, Link, ...),
#                             SIZE   => Dateigröße,
#                             MODE   => Rechte,
#                             UID    => Besitzer,
#                             GID    => Gruppe,
#                             MTIME  => Zeitstempel,
#                             LINKTO => Worauf der Link zeigt,
#                             RDEV   => Geräteidentifier,
#                             CHECK  => Prüfsumme,
#                            }
#              };
  tie (%daten, 'MLDBM', $datenbank, O_RDWR|O_CREAT, 0644) || die $!;
  bless \%daten;
}

sub DESTROY {}

sub AUTOLOAD
{
  # Alle Methoden-Aufrufen landen in dieser Funktion und werden behandelt
  my $objekt = shift;
  my $name   = shift;
  my $func   = $AUTOLOAD; # Wie heißt die Funktion?
  my $set;
  unless (defined $name)
  {
    carp("$func() ohne Parameter aufgerufen!\n");
    main::myexit($main::ErrorExitCode);
  }
  # Soll ein Wert gesetzt oder gelesen werden?
  my ($FUNC) = ($func =~ /.*::(.+)/); $FUNC = uc $FUNC;
  if ($FUNC =~ /^SET(.+)/)
  {
    $set = 1;
    $FUNC = $1;
  }
  # Test auf zulässiges Feld
  unless ($FUNC =~ /^TYPE$|^SIZE$|^MODE$|^UID$|^GID$|^MTIME$|^LINKTO$|
          ^RDEV$|^CHECK$/x)
  {
    carp("$func() gibt es nicht!\n");
    main::myexit($main::ErrorExitCode);
  }

  if ($set) # Wert setzen
  {
    # So wäre es normalerweise, aber das geht nicht mit MLDBM:
    #    $objekt->{$name}->{$FUNC} = shift;
    my $temp = $objekt->{$name};
    my $wert = shift;
    unless (defined $wert)
    {
      carp("$func() ohne zweiten Parameter aufgerufen!\n");
      main::myexit($main::ErrorExitCode);
    };
    $temp->{$FUNC} = $wert;
    $objekt->{$name} = $temp;
  }
  else # Wert zurückgeben
  {
    unless (defined $objekt->{$name})
    {
      carp("$func(): kann Wert '$name' nicht aus der Datenbank lesen!\n".
           "Wenn dieser Fehler bei 'check' auftritt, dann ist die ".
           "Datenbankimplementierung fehlerhaft!\n");
    }
    return $objekt->{$name}->{$FUNC};
  }
}


# # So wäre es ohne Autoload für jede Methode:
# sub Type
# {
#   my $objekt = shift;
#   my $name   = shift;
#   my $func = (caller(0))[3];
#   my ($FUNC) = ($func =~ /.*::(.+)/); $FUNC = uc $FUNC;
#   unless (defined $name)
#   {
#     carp("$func() ohne Parameter aufgerufen!\n");
#     main::myexit($main::ErrorExitCode);
#   }
#   return $objekt->{$name}->{$FUNC};
# }
#
# sub SetType
# {
#   my $objekt = shift;
#   my $name   = shift;
#   my $func = (caller(0))[3];
#   my ($FUNC) = ($func =~ /.*::Set(.+)/); $FUNC = uc $FUNC;
#   unless (defined $name)
#   {
#     carp("$func() ohne Parameter aufgerufen!\n");
#     main::myexit($main::ErrorExitCode);
#   }
#   $objekt->{$name}->{$FUNC} = shift;
# }

sub Enthalten
{
  # Return: TRUE, wenn Datei in der Datenbank enthalten ist
  my $objekt = shift;
  my $name   = shift;
  my $func = (caller(0))[3];
  unless (defined $name)
  {
    carp("$func() ohne Parameter aufgerufen!\n");
    main::myexit($main::ErrorExitCode);
  }
  return ( defined $objekt->{$name} ? 1 : 0 );
}

sub Entferne
{
  # Entfernt den angegebenen Datensatz
  my $objekt = shift;
  my $name   = shift;
  my $func = (caller(0))[3];
  unless (defined $name)
  {
    carp("$func() ohne Parameter aufgerufen!\n");
    main::myexit($main::ErrorExitCode);
  }
  delete $objekt->{$name};
  return 1;
}

sub Liste
{
  # Liefert ein FileHandle auf die Liste der Dateinamen
  #
  my $objekt = shift;
  my $temp = "/tmp/syscheck.$$";

  my $fh = FileHandle->new();
  open($fh, ">$temp") || main::logdie "Kann '$temp' nicht anlegen!\n";
  # Alle Filenamen in eine Datei schreiben
  my $key;
  while (($key) = each %$objekt)
  { print $fh $key,"\n"; }
  close $fh;
  # Datei öffnen
  open($fh, $temp) || main::logdie "Kann '$temp' nicht öffnen!\n";
  # Datei löschen (Das geht nur unter _richtigen_ Betriebssystemen!)
  unlink $temp;
  # Und den Filehandle der "gelöschten" Datei zurückgeben
  return $fh;
}


######################################################################
### Standardeinstellungen (Listen) Objekt
######################################################################

package Standardeinstellungen;

use Carp;

sub new
{
  my $objekt    = shift;
  my $dateiname = shift;
  my $func = (caller(0))[3];
  unless (defined $dateiname)
  {
    carp("$func() ohne Parameter aufgerufen!\n");
    main::myexit($main::ErrorExitCode);
  }
  main::debug "Standardeinstellungen: Verwende Datei '$dateiname'\n";
  my $daten = {
# Das sieht so aus:
#              IMMER  => Liste von Dateinamen,
#              NO     => Liste von Dateinamen,
#              ZERO   => Liste von Dateinamen,
#              IGNORE => Regulärer Ausdruck (String)
              };
  bless $daten, 'Standardeinstellungen';

  # Einlesen der Standardeinstellungenliste
  my @immer  = ();
  my @no     = ();
  my @zero   = ();
  my $ignore = '';
  my @temp;
  my $fh = FileHandle->new();
  open($fh, $dateiname) || main::logdie "Kann Standardeinstellungsliste ".
  "'$dateiname' nicht öffnen!\n";
  my $zeile;
  while(defined ($zeile = <$fh>) )
  {
    next if $zeile =~ /^\s*$/; # Leerzeilen überspringen
    chomp $zeile;
    # Files, die nicht existieren dürfen
    if ($zeile =~ /^NO\s+(.+)/)
    {
      $zeile = $1;

      # Jokerzeichen expandieren
      if ($zeile =~ /[\?\*]/)
      {
        @temp = glob($zeile);
        push @no, @temp;
      }
      else
      {
        push @no, $zeile;
      }
      next;
    }

    # Files, die Länge Null haben sollen
    if ($zeile =~ /^ZERO\s+(.+)/)
    {
      $zeile = $1;
      # Jokerzeichen expandieren
      if ($zeile =~ /[\?\*]/)
      {
        @temp = glob($zeile);
        push @zero, @temp;
      }
      else
      {
        push @zero, $zeile;
      }
      next;
    }

    # Files, die ignoriert werden sollen
    if ($zeile =~ /^IGNORE\s+(.+)/)
    {
      $ignore .= '|'.$1;
      next;
    }

    # Sonstige Files
    if ($zeile =~ /[\?\*]/)
    {
      @temp = glob($zeile);
      push @immer, @temp;
    }
    else
    {
      push @immer, $zeile;
    }
  }

  $daten->{IMMER}  = [ @immer ];
  $daten->{NO}     = [ @no    ];
  $daten->{ZERO}   = [ @zero  ];
  substr($ignore,0,1) = ''; # Erstes Zeichen löschen
  $daten->{IGNORE} = $ignore;

  return $daten;
}

sub ImmerListe
{
  my $objekt  = shift;
  return @{$objekt->{IMMER}};
}


sub DarfNichtExistierenListe
{
  my $objekt  = shift;
  return @{$objekt->{NO}};
}


sub NichtLaengeNullListe
{
  my $objekt  = shift;
  return @{$objekt->{ZERO}};
}


sub IgnorePattern
{
  my $objekt  = shift;
  return $objekt->{IGNORE};
}


######################################################################
### POD-Dokumentation
######################################################################

__END__

=head1 NAME

syscheck - Veränderungen von Dateien anhand einer Sicherheitsdatenbank mit
Prüfsummen überwachen.

=head1 SYNOPSIS

 syscheck [options] action

=head1 DESCRIPTION

Es werden die Namen der zu überprüfenden Dateien zeilenweise von STDIN
gelesen. Außerdem kann man in der Datei syscheckfiles.sc Standardeinstellungen
vorgeben. Alle Unstimmigkeiten werden auf STDERR ausgegeben.
Zur Sicherheit wird ein Paßwort benötigt. Wenn STDIN von syscheck nicht mit
einem Terminal verbunden ist, dann erwartet syscheck das Paßwort in der ersten
Zeile der Eingabe von STDIN. Wenn -b angegeben ist, dann wird das
Paßwort nie vom Terminal gelesen, sondern immer von STDIN oder der Datei, die
mit -f angegeben ist. Das Paßwort wird mit zur Prüfsummenberechnung
verwendet.

 action    := init | update | check | remove
 Erklärungen:
 init:   Sicherheitsdatenbank erstellen
 update: Sicherheitsdatenbank aktualisieren
 check:  Überprüfung anhand der Sicherheitsdatenbank
 remove: Die angegebenen Dateien werden aus der Sicherheitsdatenbank
         entfernt

 Optionen:
 -f FILE : Namen, der überprüfenden Dateien werden zeilenweise aus FILE
           gelesen.
 -b      : Batch-Mode: Paßwort von der Eingabedatei lesen und nicht vom
           Terminal
 -r      : Remove: Bei 'check' werden nicht mehr vorhandene Dateien aus der
           Datenbank entfernt.
 -q      : Quiet: Keine Meldungen nach STDOUT oder STDERR
           Die STDERR-Meldungen landen stattdessen im Logfile.
 -wX     : mit X=0-4 gibt den LOG-Level an.
           0: Nur fatale Fehler
           1: zusätzlich alle Fehler (Genaue Fehlerbeschreibungen)
           2: zusätzlich alle Warnungen (ganz informativ)
           3: zusätzlich alle Debug-Informationen (ausführlicher Status,
              etc.)

 Beispiele:
 find / -print -xdev > liste.txt
 syscheck -f liste.txt init 2> /tmp/out
 syscheck -f liste.txt update
 syscheck -w3 check 2> /tmp/out
 
 Man kann auch mit einem Pipe arbeiten:
 ( echo 'MeinGeheimesPaßwort' ; find / -xdev ) | syscheck init 2> out

 Die kritischen Files kann man so extrahieren:
 grep ^# /tmp/out | perl -pe 's/^#[^:]+: //'
 

=head2 Hauptkonfigurationsdatei


In der Datei "/etc/syscheckrc" bzw. "./syscheckrc" bzw. "~/.syscheckrc" ist
gespeichert, wo alle anderen Dateien und Verzeichnisse sind:

 SYSCHECK_ROOT=/var/adm/syscheck/

In diesem Verzeichnis wird die Sicherheitsdatenbank angelegt.
Außerdem sucht hier syscheck nach der weiteren Konfigurationsdatei
F<syscheckfiles.sc>.


=head2 Sicherheitsdatenbank


In der Sicherheitsdatenbank werden alle Informationen über die zu überwachenden
Dateien gespeichert. Diese Datenbank kann und darf nicht direkt durch einen
Benutzer verändert werden.


=head2 Datei F<syscheckfiles.sc>


In dieser Datei kann der Benutzer angeben, welche Dateien standardmäßig
geprüft werden sollen. Dazu trägt man zeilenweise den kompletten Pfadnamen
der Datei ein. Die Dateinamen dürfen die Jokerzeichen "*" und "?" beinhalten,
welche nach den üblichen Shell-Regeln expandiert werden.
Man kann dem Dateinamen auch spezielle Schlüsselwörter voranstellen, nämlich:

 NO     : Diese Datei darf nicht existieren
 ZERO   : Diese Datei muß die Länge Null haben
 IGNORE : Dateien, die diesem Perl-Pattern entsprechen, werden ignoriert

Beispiel:

 /etc/passwd
 /etc/hosts
 /etc/shadow
 NO /home/*/.rhosts
 NO /home/*/.forward
 NO /etc/nologin
 ZERO /var/spool/lpd/lockfile
 /root/*
 IGNORE ~$|.bak$
 IGNORE ^/var/texfonts/
 IGNORE /tmp/
 IGNORE ^/home/[^/].+/temp/


=head1 RETURN


Keine Rückgabewerte.


=head1 AUTHOR


=for text
Syscheck wurde geschrieben von Stephan Löscher, http://www.leo.org/~loescher/,
loescher@gmx.de in 1998.

=for man
Syscheck wurde geschrieben von Stephan Löscher, http://www.leo.org/~loescher/,
loescher@gmx.de in 1998.

=for latex
Syscheck wurde geschrieben von Stephan Löscher, http://www.leo.org/~loescher/,
loescher@gmx.de in 1998.

=for html
Syscheck wurde geschrieben von
<A HREF="http://www.leo.org/~loescher/">Stephan L&ouml;scher</A>,
<A HREF="mailto:loescher@gmx.de">loescher@gmx.de</A>
in 1998.

=cut

######################################################################
#
# Warranty and legal notice
# ~~~~~~~~~~~~~~~~~~~~~~~~~
#
# Copyright (c) 1998 by Stephan Löscher  -  all rights reserved
# My Address: Stephan Löscher, Dr.Troll-str. 3, 82194 Gröbenzell, Germany
# Email: loescher@gmx.de
# WWW: http://www.leo.org/~loescher/
#
# This program is freeware.
# It is NOT Public-Domain-Software!
# The author (Stephan Löscher) does NOT give up his copyright, but he 
# reserves his copyright. Usage and copying is free of charge for private
# use, but NOT for commercial use!
# 
# You may and should copy this program free of charge, use it,
# give it to your friends, upload it to a BBS or something similar, under
# the following conditions:
# * Don't charge any money for it. If you upload it to a BBS, make sure that
#    it can be downloaded free (without paying for downloading it, except
#    for usage fees that have to be paid anyway). Small copying fees (up to
#    5 DM or 3 $US) may be charged.
#  * Only distribute the whole original package, with all the files included.
#  * This program may not be part of any commercial product or service without
#    the written permission by the author.
#  * If you want to include this program on a CD-ROM and/or book, please send
#    me a free copy of the CD/book (this is not a must, but I would appreciate
#    it very much).
# 
# Distribution of the program is explicitly desired, provided that the above
# conditions are accepted.
# 
# YOU ARE USING THIS PROGRAM AT YOUR OWN RISK! THE AUTHOR (STEPHAN LÖSCHER)
# IS NOT LIABLE FOR ANY DAMAGE OR DATA-LOSS CAUSED BY THE USE OF THIS PROGRAM
# OR BY THE INABILITY TO USE THIS PROGRAM. IF YOU ARE NOT SURE ABOUT THIS, OR
# IF YOU DON'T ACCEPT THIS, THEN DO NOT USE THIS PROGRAM!
# BECAUSE OF THE VARIOUS HARDWARE AND SOFTWARE ENVIRONMENTS INTO WHICH THIS
# PROGRAM MAY BE PUT, NO WARRANTY OF FITNESS FOR A PARTICULAR PURPOSE IS
# OFFERED.
# GOOD DATA PROCESSING PROCEDURE DICTATES THAT ANY PROGRAM BE THOROUGHLY
# TESTED WITH NON-CRITICAL DATA BEFORE RELYING ON IT.
# 
# No part of the documentation may be reproduced, transmitted, transcribed,
# stored in any retrieval system, or translated into any other language in
# whole or in part, in any form or by any means, whether it be electronic,
# mechanical, magnetic, optical, manual or otherwise, without prior written
# consent of the author, Stephan Löscher.
# 
# You may not make any changes or modifications to this software or this
# manual. You may not decompile, disassemble, or otherwise reverse-engineer
# the software in any way.
# If you got the source, then you are permitted to modify it if you
# contact me and tell me your enhancements.
# You also may include the source as a whole or parts of it into other
# programs, as long as you don't make profit directly out of selling
# the result. If you re-use code of this program then do not remove my name!
# If you include this source-code in your projects, mark it clearly as such
# "... derived from code XXX by Stephan Löscher".
# But don't distribute modified code!
# 
# If you believe your copy of this software has been tampered or altered in
# anyway, shape or form, please contact me immediately! Do not hesitate a
# moment to inform me. Remember, this software should be available to all, in
# the original form, so please do not accept modified or damaged versions of
# my software.
# 
# The author reserves his right for taking legal steps if the copyright or the
# license agreement is violated.
# 
# All product names mentioned in this software are trademarks or registered
# trademarks of their respective owners.
# 
# If you have any questions, ideas, suggestions for improvements or if you find
# bugs (I don't hope so.) then feel free to contact me. (Email is appreciated.)
# 
# I'm not a native english speaker. If you are one and discover some strange
# sounding parts in this documentation or in the program, please, feel free
# to point it out to me and give me suggestions for alteration!
# 
# If the program works for you, and you want to honour my efforts, you are
# invited to donate as much as you want... :)
#
# In any case, if you don't like the restrictions in this license, contact
# me, and we can work something out.
#
######################################################################