#!/bin/sh
####
##      advchk v2.00 :: reads advisories so you don't have to
#       Copyright (c) 2006-2007 Stephan Schmieder, http://advchk.unixgu.ru
#
# bootstrap
#
export PATH="~/bin:$PATH:.";
perl -e '' || (
  echo "$0 ERROR: Can't find perl interpreter in $PATH!" 1>&2
  echo "Add your perl path to the list at the top of this $0 file." 1>&2
  exit 1
)
perl -x "$0" $@
exit $?
#
# initialize
#
#!perl
use strict;
use Storable;
use LWP::UserAgent;
&loadProgramMeta();
#
# parse command line
#
use Getopt::Std;
our( %Conf, %opts );
getopts( 'cCdDhquva:r:A:R:f:F:l:p:', \%opts );
$Conf{'checkFeeds' } = $opts{'c'};
&showChangeLog()    if $opts{'C'};
$Conf{'diagnostics'} = $opts{'d'};
$Conf{'dumpDb'     } = $opts{'D'};
$Conf{'quiet'      } = $opts{'q'};
$Conf{'update'     } = $opts{'u'};
$Conf{'addFeed'    } = $opts{'a'};
$Conf{'removeFeed' } = $opts{'r'};
$Conf{'addHost'    } = $opts{'A'};
$Conf{'removeHost' } = $opts{'R'};
$Conf{'dbFile'     } = $opts{'f'};
$Conf{'hostFile'   } = $opts{'F'};
$Conf{'logFile'    } = $opts{'l'};
$Conf{'paralell'   } = $opts{'p'};
$Conf{'paralell'   } = 42 unless $Conf{'paralell'};
&error( "-p has to be a number!" )
  if ( $Conf{'paralell'} and $Conf{'paralell'} =~ /^[^\d]+$/ );
&showHelp() if (
  (
    $opts{'h'} or
    $opts{'v'}
  ) or ! (
    $Conf{'checkFeeds'} or
    $Conf{'update'    } or
    $Conf{'addFeed'   } or
    $Conf{'removeFeed'} or
    $Conf{'addHost'   } or
    $Conf{'removeHost'} or
    $Conf{'hostFile'  } or
    $Conf{'dumpDb'    }
  )
);
undef %opts;
#
# debug mode?
#
if( $Conf{'diagnostics'} ) {
  use utf8;
  use warnings;
  use diagnostics;
}
&debug( 'initialized' );
#
#
#
print &getCopyright() if(
  (
    $Conf{'checkFeeds' } or
    $Conf{'help'       } or
    $Conf{'diagnostics'}
  ) and ! $Conf{'quiet'}
);
&main();
#
# returns copyright string
#
sub getCopyright { return <<COPYRIGHT;
####
##      $Conf{progName} v$Conf{version} :: $Conf{description}
#       Copyright (c) $Conf{copyrightYear} $Conf{copyrightHolder}, $Conf{www}

COPYRIGHT
}
#
# wrapper for changelog message
#
sub showChangeLog {
  print &getCopyright() . &getChangeLog();
  exit 0;
}
#
# wrapper for help message
#
sub showHelp {
  my @changes = split /\nv/, "\n" . &getChangeLog();
  print STDERR &getCopyright() . &getUsage();
  unless( $#changes == -1 ) {
    print STDERR "\nCHANGELOG:";
    for( my $i = 1; $i > -1; $i-- ) {
      next if $#changes <= $i;
      print STDERR "\nv" . $changes[ $#changes - $i ];
    }
  }
  exit 0;
}
#
# logs log messages with lvl error
#
sub error {
  return if $#_ == -1;
  warn &_log( 'ERROR', \@_, ( caller( 1 ) )[3] );
  exit 1;
}
#
# logs log messages with lvl alert
#
sub alert {
  return if $#_ == -1;
  warn &_log( 'ALERT', \@_, ( caller( 1 ) )[3] );
}
#
# logs log messages with lvl info
#
sub info {
  return if $#_ == -1;
  my $text = &_log( 'INFO', \@_, ( caller( 1 ) )[3] );
  print $text unless $Conf{'quiet'};
}
#
# logs log messages with lvl debug
#
sub debug {
  return unless $Conf{'diagnostics'};
  return if $#_ == -1;
  my $text = &_log( 'DEBUG', \@_, ( caller( 1 ) )[3] );
  print $text unless $Conf{'quiet'};
}
#
# backend function for logging messages
#
sub _log {
  #
  # build log message
  #
  my( $level, $caller ) = ( $_[0], $_[2] );
  my @messages = @{ $_[1] };
  $caller = 'main::unknown' unless $caller;
  $caller =~ s/main\:\://o;
  my $logMesg;
  my $time = localtime();
  $time =~ s/^\w+\ //o;
  $time =~ s/\ \d+$//o;
  #
  # verbose output
  # May 19 16:00:07 advchk[25784]/sshUpdate   INFO : updating package lists via ssh .. 
  #
  if( $Conf{'diagnostics'} ) {
    foreach my $mesg ( @messages ) {
      $logMesg .= sprintf(
        "$time $Conf{progName}\[$$\]/%-11s %-5s: %s\n",
        $caller,
        $level,
        $mesg
      );
    }
  #
  # normal output
  # May 19 16:00:07 INFO : updating package lists via ssh ...
  #
  } else {
    foreach my $mesg ( @messages ) {
      $logMesg .= sprintf( "$time %-5s: $mesg\n", $level );
    }
  }
  #
  # write log file
  #
  if( $Conf{'logFile'} ) {
    open LOG, '>>', $Conf{logFile}
      or die "Can't open log for writing: $!", @messages;
    print LOG $logMesg;
    close LOG or die "Can't write to log: $!", @messages;
  }
  return $logMesg;
}
#
# meta information about this program
#
sub loadProgramMeta {
  $Conf{'version'        } = '2.00';
  $Conf{'description'    } = "reads advisories so you don't have to";
  $Conf{'progName'       } = $1 if $0 =~ /\/?([^\/]+)$/o;
  $Conf{'www'            } = "http://$Conf{progName}.unixgu.ru";
  $Conf{'copyrightHolder'} = 'Stephan Schmieder';
  $Conf{'copyrightYear'  } = '2006-2007';
}
#
# keep track of your changes...
#
sub getChangeLog { return <<'CHANGELOG';
v0.30 :: 2006-06-17 :: ssc@unixgu.ru
- initial freshmeat announcement
v0.31 :: 2006-07-11 :: ssc@unixgu.ru
- This release adds three more RSS feeds to addFeeds.sh
v0.90 :: 2006-09-16 :: ssc@unixgu.ru
-  complete rewrite: bigger, better, faster and no more nmap =)
v0.91 :: 2006-09-16 :: Bernd Stolle, ssc@unixgu.ru
- added support for portage package manager (Gentoo)
v0.92 :: 2006-10-03 :: ssc@unixgu.ru
- fixed a small bug in enableSshUpdate.sh, which errored on missing .ssh/.id_?sa
- added ruby and django lists via google groups
v0.93 :: 2006-10-05 :: ssc@unixgu.ru
- sshUpdate.sh is now quite when running inside cron
v0.98 :: 2006-10-07 :: Dag Wiers, ssc@unixgu.ru,
- added support for AIX lslpp and Solaris pkginfo
v1.00 :: 2006-10-12 :: ssc@unixgu.ru
- integrated (enable)SshUpdate.sh functionalities into main program
- added -D option for Dumping the database
- turned README into a manual page
v1.01 :: 2006-10-26 :: ssc@unixgu.ru
- SSH-Update can now use multiple remote package managers on same host.
- Upgrade to v1.01 requires "advchk -R host -A host" for each host.
v1.02 :: 2006-11-15 :: Ludwig Ortmann, ssc@unixgu.ru
- Ludwig Ortmann pointed out that touch(1) isn't always in /bin.
v1.03 :: 2007-01-26 :: ssc@unixgu.ru
- A LICENSE file was added in order to comply with DFSG.
v1.10 :: 2007-02-11 :: ssc@unixgu.ru
- The "package list via nmap"-feature and buzzword filter from v0.31
  have been adopted and re-introduced to satisfy consumer needs.
- Changelog is shown upon "-C". Also: s/addfeeds.sh/advchk-addfeeds.sh/
v1.20 :: 2007-02-18 :: Bernd Stolle, ssc@unixgu.ru
- Support for IRIX package manager 'inst' was added.
- Some regex improvements occured and !debug output is less verbose now.
- Upgrade to v1.20 requires "advchk -R host -A host" for each SSH-host.
v2.00 :: 2007-05-26 :: ssc@unixgu.ru
- This release delivers massive speedups in the networking area (ssh,nmap,http),
  the -p switch, 4 new feeds for advchk-addfeeds.sh and some code polishing.
- Greetings to everyone at the ph-neutral.org party =)
CHANGELOG
}
#
# documents synopsis and usage information
#
sub getUsage{ return <<USAGE;
SYNOPSIS: [-cCdDhquv ] [-ar url] [-AR remotehost] [-fFl file] [-p num]
-c  Check advisories   -C Changelog   -d perl Diagnostics    -D  Dump database
-hv show this Help     -q be Quiet    -u Update pkg lists    -ar Add/Remove feed
-AR Add/Remove host    -f db File     -F load package File   -l  Log to file
-p  paralell procs

remotehost syntax for  SSH: [user@]host[:port]            =~ bar\@foo.com:22
remotehost syntax for NMAP: host:tcp|udp:lowport:highport =~ foo.com:tcp:0:1023
See "man 1 advchk" for details.
USAGE
}
#
# main part of this program
#
sub main {
  $Conf{'db' } = &loadDb( $Conf{'dbFile'} );
  $Conf{'www'} = LWP::UserAgent->new(
    'agent'         => "$0/$Conf{'version'}",
    'max_redirect'  => 5,
    'timeout'       => 23
  );
  $Conf{'www'}->env_proxy;
  #
  # add/remove/update hosts?
  #
  if( $Conf{'hostFile'} ) {
    $Conf{'db'}{'hosts'} = &loadHost( $Conf{'hostFile'}, $Conf{'db'}{'hosts'} );
  }
  if( $Conf{'removeHost'} ) {
    if(
      delete $Conf{'db'}{'sshUpdate' }{ $Conf{'removeHost'} } or
      delete $Conf{'db'}{'nmapUpdate'}{ $Conf{'removeHost'} }
    ) {
      &info(  "removed host '$Conf{'removeHost'}' from update-pool" );
    } else {
      &alert( "Can't remove '$Conf{'removeHost'}': Host not in update-pool." );
    }
  }
  if( $Conf{'addHost'} ) {
    if( $Conf{'db'}{'sshUpdate' }{ $Conf{'addHost'} } or
        $Conf{'db'}{'nmapUpdate'}{ $Conf{'addHost'} }
    ) {
      &alert( "Can't add '$Conf{'addHost'}: Host already in update-pool." );
    } else {
      if( $Conf{'addHost'} =~ /^.+\:(tcp|udp)(\:\d+){2}$/o ) {
        #
        # host for nmap update
        #
        $Conf{'db'}{'nmapUpdate'} = &addNmapHost(
          $Conf{'addHost'},
          $Conf{'db'}{'nmapUpdate'}
        );
      } elsif( $Conf{'addHost'} =~ /^(.*\@)?.+(\:\d+)?$/o ) {
        #
        # host for ssh update
        #
        $Conf{'db'}{'sshUpdate' } = &addSshHost(
          $Conf{'addHost'},
          $Conf{'db'}{'sshUpdate' }
        );
      } else {
        &alert( "Cannot parse remotehost syntax!" );
      }
    }
  }
  if( $Conf{'update'} ) {
    $Conf{'db'} = &sshUpdate(  $Conf{'db'} );
    $Conf{'db'} = &nmapUpdate( $Conf{'db'} );
  }
  #
  # add/remove/check feeds?
  #
  if( $Conf{'addFeed'} ) {
    $Conf{'db'}{'feeds'}{ $Conf{'addFeed'} } = 1;
    &info( "added feed '$Conf{'addFeed'}' to database" );
  }
  if( $Conf{'removeFeed'} ) {
    if( delete $Conf{'db'}{'feeds'}{ $Conf{'removeFeed'} } ) {
      &info(  "removed feed '$Conf{'removeFeed'}' from db" );
    } else {
      &alert( "Can't remove '$Conf{'removeFeed'}': 'Feed not in database." );
    }
  }
  if( $Conf{'checkFeeds'} ) {
    $Conf{'db'} = &checkFeeds( $Conf{'db'} );
  }
  #
  # dump db?
  #
  if( $Conf{'dumpDb'} ) {
    use Dumpvalue;
    my $d = new Dumpvalue;
    print $d->dumpValue( $Conf{'db'} );
  }

  &storeDb( $Conf{'dbFile'}, $Conf{'db'} );
}
#
# updates package lists via ssh
#
sub sshUpdate {
  my %db = %{ $_[0] };
  &error( "\$HOME environment variable is not set!" )
    unless ( $ENV{'HOME'} and -d $ENV{'HOME'} );
  mkdir "$ENV{'HOME'}/.advchk"
    unless -d "ENV{'HOME'}/.advchk";
  &info( 'updating package lists via ssh ...' );
  my $preCmd = "/bin/sh -c '";
  my $postCmd = " wait'";
  my $cmd;
  my @splitCmds;
  my $splitCmdCount = 0;

  foreach my $host ( keys %{ $db{'sshUpdate'} } ) {
    my $hostFile =  "$ENV{'HOME'}/.advchk/$host.list";
    my $sshHost  =  $host;
    $sshHost     =~ s/\:/\ -p\ /o;
    $cmd        .=  "ssh -T $sshHost \"" .
                    'dpkg -l; pkg_info; rpm -qa;equery list -i; lslpp -Lc; ' .
                    'pkginfo -x; showprods -1En; true' .
                    "\" > $hostFile 2>&1 &";

    #
    # make sure we only run $Conf{'paralell'} processes in paralell
    #
    $splitCmdCount++;
    if( $splitCmdCount >= $Conf{'paralell'} ) {
      push( @splitCmds, $cmd );
      $splitCmdCount = 0;
      $cmd = '';
    }
  }
  push( @splitCmds, $cmd );

  #
  # exec batches of ssh updates
  #
  foreach $cmd ( @splitCmds ) {
    $cmd = $preCmd . $cmd . $postCmd;
    &error( "Oops, this happened:", $!, "while executing:", $cmd  )
      if (
        $cmd ne "/bin/sh -c ' wait'" and system $cmd and $! ne 'File exists'
      );
  }

  foreach my $host ( keys %{ $db{'sshUpdate'} } ) {
    my $hostFile = "$ENV{'HOME'}/.advchk/$host.list";
    $db{'sshUpdate'}{$host} = 1;
    $db{'hosts'} = &loadHost( "$ENV{'HOME'}/.advchk/$host.list", $db{'hosts'} )
      if -f "$ENV{'HOME'}/.advchk/$host.list";
  }

  return \%db;
}
#
# updates package lists via nmap
#
sub nmapUpdate {
  my %db = %{ $_[0] };
  &error( "\$HOME environment variable is not set!" )
    unless ( $ENV{'HOME'} and -d $ENV{'HOME'} );
  mkdir "$ENV{'HOME'}/.advchk"
    unless -d "ENV{'HOME'}/.advchk";
  &info( 'updating package lists via nmap ...' );
  my $preCmd = "/bin/sh -c '";
  my $postCmd = " wait'";
  my $cmd;
  my @splitCmds;
  my $splitCmdCount = 0;

  foreach my $host ( keys %{ $db{'nmapUpdate'} } ) {
    &debug( "updating remotehost '$host' ..." );
    my $hostFile = "$ENV{'HOME'}/.advchk/$host.list";
    my( $nHost, $nProto, $nLow, $nHigh );
    if( $host =~ /^(.+)\:(tcp|udp)\:(\d+)\:(\d+)$/o ) {
      ( $nHost, $nProto, $nLow, $nHigh ) = ( $1, $2, $3, $4 );
    } else {
      &error( "Cannot parse remotehost syntax for $host!" );
    }

    $nProto = 'T' if $nProto =~ /^tcp$/o;
    $nProto = 'U' if $nProto =~ /^udp$/o;

    $cmd .=  "( nmap -A -T4 -s$nProto -p $nLow-$nHigh \"$nHost\" " .
             "2>&1 ) > \"$hostFile\" &";

    #
    # make sure we only run $Conf{'paralell'} processes in paralell
    #
    $splitCmdCount++;
    if( $splitCmdCount >= $Conf{'paralell'} ) {
      push( @splitCmds, $cmd );
      $splitCmdCount = 0;
      $cmd = '';
    }
  }
  push( @splitCmds, $cmd );

  #
  # exec batches of nmap updates
  #
  foreach $cmd ( @splitCmds ) {
    $cmd = $preCmd . $cmd . $postCmd;
    if( $cmd ne "/bin/sh -c ' wait'" ) {
      if( system $cmd and $! ne 'Bad file descriptor'  ) {
        &error( "Oops, this happened:", $!, "while executing:", $cmd );
      }
    }
  }

  foreach my $host ( keys %{ $db{'nmapUpdate'} } ) {
    $db{'nmapUpdate'}{$host} = 1;
    $db{'hosts'} = &loadHost( "$ENV{'HOME'}/.advchk/$host.list", $db{'hosts'} )
      if -f "$ENV{'HOME'}/.advchk/$host.list";
  }

  return \%db;
}
#
# adds an host to the SSH update-pool
#
sub addSshHost {
  my $host      = $_[0];
  my %sshUpdate = %{ $_[1] };
  my $cmd;

  # generate ssh private key?
  unless( -f "$ENV{HOME}/.ssh/id_rsa" or -f "$ENV{HOME}/.ssh/id_dsa" ) {
    &info(
      'Looks like you don\'t have a private key file yet.',
      'Let\'s generate one!' );
    $cmd = 'ssh-keygen -t dsa';
    &error( "Oops, this happened:", $!, "while executing:", $cmd  )
      if system $cmd;
  }

  &info( "enabling ssh update for '$host' ..." );
  my $sshHost =  $host;
  $sshHost    =~ s/\:/\ -p\ /o;
  $cmd        =  "/bin/sh -c 'echo command=\\\"" .
                 "dpkg -l\\; pkg_info\\; rpm -qa\\; equery list -i\\; " .
                 "lslpp -Lc\\; pkginfo -x\\; showprods -1En\\; true\\\" " .
                  "`/bin/cat $ENV{'HOME'}/.ssh/id_?sa.pub` | " .
                  "ssh -T $sshHost \"/bin/cat >> .ssh/authorized_keys2\"'";
  &error( "Oops, this happened:", $!, "while executing:", $cmd  )
    if system $cmd;

  $sshUpdate{$host} = 1;
  return \%sshUpdate;
}
#
# adds an host to the NMAP update-pool
#
sub addNmapHost {
  my $host       = $_[0];
  my %nmapUpdate = %{ $_[1] };
        
  &info( "enabling nmap update for '$host' ..." );
  &error( "Cannot parse remotehost syntax for $host!" )
    unless $host =~ /^(.+)\:(tcp|udp)\:(\d+)\:(\d+)$/o;

  $nmapUpdate{$host} = 1;
  return \%nmapUpdate;
}
#
# updates feeds and checks for advisories
#
sub checkFeeds {
  my %db = %{ $_[0] };
  my( %newItems, %matches );
  &info( 'checking advisories ...' );

  foreach my $feed ( keys %{ $db{'feeds'} } ) {
    &debug( "fetching feed '$feed' ..." );
    my $resp = $Conf{'www'}->get($feed);
    next unless $resp->is_success();
    foreach my $item ( split /\<item[^\>]*\>/i, $resp->content() ) {
      #
      # clean from XML
      #
      $item =~ s/\r//go;
      $item =~ s/\<(?:link|title)\>([^\<]+)\<\/(?:link|title)\>/$1\n/go;
      $item =~ s/(?:\<[^\>]*\>|\&lt\;[^\&]+\&gt\;|\&\w{2,4}\;|[\=\-\_]{3,})//go;
      $item =~ s/(?:\n\s+|\n\s*\n)/\n/go;
      $item =~ s/\+/\ /go;
      $item = "\n$item" unless $item =~ /^\n/o;

      next if $db{'items'}{$item};
      $newItems{$item} = 1;
      $db{'items'}{$item} = time;
    }
  }

  &debug( 'removing old items ...' );
  foreach my $item ( keys %{ $db{'items'} } ) {
    delete $db{'items'}{$item}
      if $db{'items'}{$item} < ( time - 7776000 );
  }

  &debug( 'matching feed items to host packages ...' );
  foreach my $item ( keys %newItems ) {
    foreach my $host ( keys %{ $db{'hosts'} } ) {
      my %packages  = %{ $db{'hosts'}{$host} };
      foreach my $package ( keys %packages ) {
        my $version = $packages{$package};
        $matches{$package}{$item}{$host} = 1
          if &match( $package, $version, $item );
      }
    }
  }

  if( scalar keys %matches ) {
    &debug( 'generating report ...' );
    my( $summary, $text );

    foreach my $pkg ( keys %matches ) {
      my %items = %{ $matches{$pkg} };
      my( %hosts, $fItem );
      foreach my $item ( keys %items ) {
        $fItem  = $item if ( ! $fItem and length $item > 42 );
        foreach my $host ( keys %{ $items{$item} } ) {
          $hosts{$host} = 1;
        }
      }
      $summary  .= "- $pkg (" . ( scalar keys %hosts ) .")\n";
      $text     .= "\n\n####\n##\t$pkg$fItem\nAffected hosts:\n";
      foreach my $host ( keys %hosts ) {
        $text   .= sprintf(
          "%-36s%-36s\n", $host, "$pkg-$db{'hosts'}{$host}{$pkg}"
        );
      }
    }

    print <<MESG;
####
##      Abstract
Advchk compared recent security advisories against a list of known software.
The following packages installed on your systems are very likely to be
vulnerable against some digital attack and require further investigations.

$summary
$text
MESG
  }

  return \%db; 
}
#
# matches software against feed items
#
sub match {
  my( $package, $version, $item ) = @_;
  return 0 unless ( $package and $version and $item );
  return 0 unless $item =~ /[\s\-\_\.\!\:]\Q$package\E[\s\-\_\.\!\:]/i;
  #
  # kill buzzwords to minimize false positives
  #
  my $filter = '\s(?:access|ready|protocol|version|release|root|account
                  |connect|server|client|microsoft|windows|win32|gpl
                  |(?:open|free|net|mir|dragonfly|desktop)\-?bsd|free
                  |suse|debian|gentoo|ubuntu|redhat|slackware|admin
                  |unix|fedora|hp-ux|solaris|irix|linux|administrator
                  |xss|ajax|cross.?site.?scripting)\s';
  $item      =~ s/$filter//xigo unless " $package $version " =~ /$filter/xigo;

  my $count  = 3;
  $count++ if $version =~ /.{1,4}\./o;
  $version   = substr $version, 0, $count;
  return 0 unless $item =~ /\Q$version\E/i;

  return 1;
}
#
# reads a host file
#
sub loadHost {
  my $hostFile  = $_[0];
  my %hosts;
  %hosts        = %{ $_[1] } if $_[1] and eval { my %foo = %{ $_[1] } };
  my $host;
  if( $hostFile =~ /^(?:.*\/)?(.+(\:(udp|tcp)\:\d+\:\d+)?)\.list$/io ) {
    $host       = $1;
  }
        
  &error( "Could not extract hostname out of host file '$hostFile'!" )
    unless $host;

  unless ( -f $hostFile ) {
    if( $hosts{$host} ) {
      delete $hosts{$host};
      &info(  "deleted host '$host' from database" );
    } else {
      &alert( "Can't remove '$host': Host not in database." );
    }
    return %hosts;
  }

  &info( "loading host file '$hostFile' ..." );
  open( hostFile, $hostFile )
    or &error( "Can't load host file '$hostFile':", $! );
  delete $hosts{$host};
  while( <hostFile> ) {
    chomp;
    next unless $_;
    my $line  = $_;

    if(
      $line   =~ /^\w\w\s+([^\s]{2,})\s+([\d\-\.]+\w*)[^\s]*\s+[^\s]+\s+.+$/o
    ) {
      $hosts{$host}{$1} = $2;
      &debug( "added DPKG     package '$1' version '$2'" );
    } elsif(
      $line   =~ /^([\w\d\+\-\.]{2,})\-([\d\-\.]+\w*)[^\s]*\s+[^\s]+\s+.+$/o
    ) {
      $hosts{$host}{$1} = $2;
      &debug( "added PKG_INFO package '$1' version '$2'" );
    } elsif( $line =~ /^[^\/]+\/([\w\d\+\-\.]{2,})\-([\d\-\.]+\w*).*$/o ) {
      $hosts{$host}{$1} = $2;
      &debug( "added PORTAGE  package '$1' version '$2'" );
    } elsif(
      $line   =~ /^([\w\d\+\-\.]{2,})\:[\w\d\+\-\.]{2,}\:([\d\-\.]+\w*).*\:.*$/o
    ) {
      $hosts{$host}{$1} = $2;
      &debug( "added LPP      package '$1' version '$2'" );
    } elsif( $line =~ /^([\w\d\+\-\.]{2,})\-([\d\-\.]+\w*).*$/o ) {
      $hosts{$host}{$1} = $2;
      &debug( "added RPM      package '$1' version '$2'" );
    } elsif(
      $line   =~ /^I\s+([\w\d\+\-\.\_]+)\s+\d+\s+.*[\-\/\sv\\]
                 ([\d\-\.]+\w*)\)?\,?(?:\s.*)?$/xo
    ) {
      $hosts{$host}{$1} = $2;
      &debug( "added INST     package '$1' version '$2'" );
    } elsif( $line =~ /^\d+\/(?:tcp|udp)\s+open\s+[^\s]/o ) {
      #
      # this is an nmap list and requires special fuzzyness
      #
      $line   =~ s/[\(\)\[\]\{\}]//go;
      if(
        $line =~ /^\d+\/(?:tcp|udp)\s+open\s+[^\s]+\s+
                  ([\w\d\+\-\.]{2,})[\-\ ](\d[\w\d\-\.]+)(?:\s+.*)?$/xo
      ) {
        $hosts{$host}{$1} = $2;
        &debug( "added NMAP0    package '$1' version '$2'" );
      } elsif(
        $line =~ /^\d+\/(?:tcp|udp)\s+open\s+[^\s]+\s+
                  ([\w\d\+\-\.]{2,})[\-\ ](\d[\w\d\-\.]+)(?:\s+.*)?$/xo ) {
        $hosts{$host}{$1} = $2;
        &debug( "added NMAP1    package '$1' version '$2'" );
      } elsif(
        $line =~ /^\d+\/(?:tcp|udp)\s+open\s+[^\s]+\s+
                  (?:.*[^\w\d])?([\w\d\+\-\.]{2,}[\-\ ][\w\d\+\-\.]{2,})
                  [\-\ ](\d[\w\d\-\.]+)(?:\s+.*)?$/xo
      ) {
        $hosts{$host}{$1} = $2;
        &debug( "added NMAP2    package '$1' version '$2'" );
      } elsif(
        $line =~ /^\d+\/(?:tcp|udp)\s+open\s+[^\s]+\s+
                  (?:.*[^\w\d])?([\w\d\+\-\.]{2,}[\-\ ][\w\d\+\-\.]{2,})
                  [\-\ ](\d[\w\d\-\.]+)(?:\s+.*)?$/xo
      ) {
        $hosts{$host}{$1} = $2;
        &debug( "added NMAP3    package '$1' version '$2'" );
      } elsif(
        $line =~ /^\d+\/(?:tcp|udp)\s+open\s+[^\s]+\s+
                  ([\w\d\+\-\.]{2,})[\-\ ]([\w\d\-\.]+)(?:\s+.*)?$/xo
      ) {
        $hosts{$host}{$1} = $2;
        &debug( "added NMAP4    package '$1' version '$2'" );
      } elsif(
        $line =~ /^\d+\/(?:tcp|udp)\s+open\s+[^\s]+\s+
                  (?:.*[^\w\d])?([\w\d\+\-\.]{2,}[\-\ ][\w\d\+\-\.]{2,})
                  [\-\ ]([\w\d\-\.]+)(?:\s+.*)?$/xo
      ) {
        $hosts{$host}{$1} = $2;
        &debug( "added NMAP5    package '$1' version '$2'" );
      } elsif(
        $line =~ /^\d+\/(?:tcp|udp)\s+open\s+[^\s]+\s+
                  ([\w\d\+\-\.]{2,})[\-\ ]([\w\d\-\.]+)(?:\s+.*)?$/xo
      ) {
        $hosts{$host}{$1} = $2;
        &debug( "added NMAP6    package '$1' version '$2'" );
      } elsif(
        $line =~ /^\d+\/(?:tcp|udp)\s+open\s+[^\s]+\s+
                  (?:.*[^\w\d])?.*([\w\d\+\-\.]{2,}[\-\ ][\w\d\+\-\.]{2,})
                  [\-\ ]([\w\d\-\.]+)(?:\s+.*)?$/xo
      ) {
        $hosts{$host}{$1} = $2;
        &debug( "added NMAP7    package '$1' version '$2'" );
      } elsif(
        $line =~ /^\d+\/(?:tcp|udp)\s+open\s+[^\s]+\s+(.{5,})(?:\s+.*)?$/o
      ) {
        $hosts{$host}{$1} = ' ';
        &debug( "added NMAP8    package '$1'" );
      #} elsif( $line =~ /^\d+\/(?:tcp|udp)\s+open\s+([^\s]+)(?:\s+.*)?$/x ) {
      #  $hosts{$host}{$1} = ' ';
      #  &debug( "added NMAP9    package '$1'" );
      } else {
        &debug( 'Could not make sense of this line:', $line );
      }
    } elsif( ! $line =~ /^PORT/ and $line =~ /^([\w\d\+\-\.]{2,})\s+\w+.*$/o ) {
      my $name       = $1;
      my $line2      = <hostFile>;
      if( $line2 ) {
        if( $line2   =~ /\s+\s+\([\w\d\+\-\.]{2,}\)\s+([\d\-\.]+\w*).*$/o ) {
          $hosts{$host}{$name} = $1;
          &debug( "added PKGINFO  package '$name' version '$1'" );
        } else {
          &debug( 'Could not make sense of these lines:', $line, $line2 );
        }
      } else {
        &debug( 'Could not make sense of this line:', $line )
      }
    } else {
      &debug( 'Could not make sense of this line:', $line );
    }
  }
  close hostFile;

  #&error( "Could not detect packages in '$hostFile' => $host not in db!" )
  #  unless $hosts{$host};

  return \%hosts;
}
#
# writes the database file
#
sub storeDb {
  my $dbFile = $_[0];
  my %db     = %{ $_[1] };

  &error( "Database file '$dbFile' does not exist." )
    unless ( $dbFile and -f $dbFile );

  &debug( "saving database to file '$dbFile' ..." );
  eval{
    store(
      \[
        $db{'hosts'     },
        $db{'feeds'     },
        $db{'items'     },
        $db{'sshUpdate' },
        $db{'nmapUpdate'}
      ],
      $dbFile
    ) if scalar keys %db > 0;
  } or &error( "Can't store database: $!\n" );
}
#
# reads the database file
#
sub loadDb {
  my $dbFile  = $_[0];
  my %db;

  unless( $dbFile ) {
    foreach my $path(
      '/etc', '/usr/local/etc', '/var', '.', "$ENV{'HOME'}/.advchk"
    ) {
      $dbFile = $path.'/advchk.db'
        if -f $path.'/advchk.db';
    }
    #
    # create db file if it doesn't exist
    #
    unless( $dbFile ) {
      $dbFile = "$ENV{'HOME'}/.advchk/advchk.db";
      mkdir "$ENV{'HOME'}/.advchk"
        unless -d "ENV{'HOME'}/.advchk";
      my $cmd = "sh -c \"touch '$dbFile'\"";
      &error( "Oops, this happened:", $!, "while executing: $cmd"  )
        if system $cmd;
      }
    }
    &error( "Database file '$dbFile' does not exist." )
      unless ( $dbFile and -f $dbFile );

    &debug( "loading database from file '$dbFile' ..." );
    eval{
      (
        $db{'hosts'     },
        $db{'feeds'     },
        $db{'items'     },
        $db{'sshUpdate' },
        $db{'nmapUpdate'}
      ) = @{ ${ retrieve( $dbFile ) } };
    } or eval{
      #
      # support for old format (pre 1.10)
      #
      (
        $db{'hosts'    },
        $db{'feeds'    },
        $db{'items'    },
        $db{'sshUpdate'}
      )  = @{ ${ retrieve( $dbFile ) } };
    };

    $Conf{'dbFile'} = $dbFile;
    return \%db;
}
