#!/usr/bin/perl

#
# Reptor - Symantec/Axent/Raptor firewall logfile analysis and reporting utility.
#
# Copyright (C) 1999,2000,2001 Alex Howansky
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; see the file COPYING. If not, write to
# the Free Software Foundation, Inc., 59 Temple Place - Suite 330,
# Boston, MA 02111-1307, USA.
#

require 5.004;
use Getopt::Long;
use Net::SMTP;
use Net::FTP;

$reptor_version = "1.00";

%monthnum = (
  "Jan" => "01",
  "Feb" => "02",
  "Mar" => "03",
  "Apr" => "04",
  "May" => "05",
  "Jun" => "06",
  "Jul" => "07",
  "Aug" => "08",
  "Sep" => "09",
  "Oct" => "10",
  "Nov" => "11",
  "Dec" => "12"
);

@monthname = (
  "January",
  "February",
  "March",
  "April",
  "May",
  "June",
  "July",
  "August",
  "September",
  "October",
  "November",
  "December"
);

%linkalias = (
  "ftp" => "ftp",
  "ftp-data" => "ftp",
  "http" => "http",
  "http-https" => "https",
  "smtp" => "mailto",
  "109/tcp" => "mailto",
  "110/tcp" => "mailto",
  "119/tcp" => "news",
  "143/tcp" => "mailto"
);

@message_severity = (
  "Information",
  "Notice",
  "Warning",
  "Error",
  "Alert",
  "Critical",
  "Emergency",
);

%message_description = (
  "101" => "Raptor Network Security Management System starting up",
  "102" => "Shutdown command received",
  "103" => "Closing connection",
  "104" => "Re-read of new config file successful",
  "105" => "Connection for incoming to outgoing",
  "106" => "Path to destination",
  "107" => "Closing log file",
  "108" => "Starting new log file",
  "109" => "Re-reading configuration file",
  "110" => "User authenticated",
  "112" => "Rule expire, re-scanning rules",
  "115" => "Remote management connection",
  "116" => "Remote management completed",
  "117" => "Daemon starting",
  "118" => "Daemon exiting",
  "119" => "Request did not complete successfully",
  "120" => "Application information",
  "121" => "Statistics",
  "122" => "Daemon listening on port",
  "125" => "ISAKMP SA established with peer",
  "126" => "Protocol SA established with peer",
  "127" => "Connection request",
  "128" => "Finjan information",
  "129" => "Trace",
  "201" => "Access denied for IP address",
  "202" => "Access denied for user",
  "203" => "Password changed",
  "204" => "Password added",
  "209" => "VPN packet not valid for tunnel",
  "211" => "VPN authentication failed",
  "212" => "IP address not allowed on tunnel",
  "213" => "IP packet not allowed on implicit tunnel",
  "214" => "IP packet dropped",
  "215" => "VPN packet does not match any defined tunnel",
  "216" => "Access denied for IP address",
  "217" => "Cannot lookup hostname",
  "218" => "Invalid protocol",
  "219" => "Can't parse URL",
  "220" => "Local web server can't handle request",
  "221" => "Possible spoofed IP packet dropped",
  "222" => "VPN packet error",
  "223" => "VPN packet error",
  "224" => "User count is over limit",
  "225" => "Possible spoofed IP packet dropped",
  "226" => "IP packet dropped [restricted port]",
  "227" => "VPN packet dropped",
  "228" => "Can't connect to destination",
  "229" => "IP packet dropped [filter]",
  "230" => "Packet not allowed on tunnel",
  "231" => "Started on connection",
  "232" => "Sending original packet",
  "233" => "IP packet dropped [input packet filter]",
  "234" => "Network error",
  "235" => "NAT address allocation failed",
  "236" => "Cannot lookup active Protocol SA record",
  "237" => "Failed to connect to destination",
  "238" => "Application notice",
  "239" => "Sending TCP Reset, port not allowed",
  "240" => "Bad TCP flags combination",
  "301" => "Internal warning",
  "303" => "Service is already running",
  "306" => "Overlapping time range",
  "308" => "Can't lookup hostname",
  "309" => "Warning in config file",
  "310" => "Can't verify reverse address",
  "311" => "Can't verify ethernet address",
  "312" => "Bogus response to hostname lookup",
  "313" => "Access denied to user [bad password]",
  "314" => "Warning in user file [blank user name]",
  "315" => "Warning in user file [non-numeric user id]",
  "316" => "Warning in user file [non-numeric group id]",
  "317" => "Warning in user file [blank group name]",
  "318" => "Warning in user file [non-numeric group id]",
  "323" => "Readeagle already running",
  "333" => "No rules for firewall in config file",
  "334" => "Denied access to protocol command",
  "335" => "VPN packet dropped [VPN is not enabled]",
  "336" => "VPN packet dropped [invalid format or length]",
  "338" => "VPN packet dropped [UDP disabled]",
  "341" => "Child process stopped unexpectedly",
  "342" => "Child process exited with an unexpected error",
  "343" => "Software problem",
  "344" => "Non-transparent call",
  "345" => "Bad DNS information",
  "347" => "Possible port scan detected",
  "401" => "Internal error",
  "402" => "Can't get config file",
  "403" => "Can't read config file",
  "404" => "Error in config file",
  "405" => "Re-read of config file failed",
  "406" => "Cannot open audio file",
  "407" => "Can't open traceroute lockfile",
  "408" => "Not a valid audio file",
  "409" => "Audio file sample rate not available",
  "410" => "Filename encoding not available",
  "411" => "Unable to open notify schedule",
  "412" => "Unrecognized transport",
  "413" => "Have config file errors but continuing",
  "415" => "Syntax error in config file date/time string",
  "416" => "Syntax error in config file expression string",
  "418" => "Can't lookup host",
  "420" => "Can't open config file",
  "423" => "Bad protocol",
  "424" => "Can't use TCP port",
  "425" => "Can't lookup service",
  "430" => "EagleLAN can't lookup firewall hostname",
  "431" => "EagleLAN can't read firewall location",
  "432" => "Bad hostname",
  "433" => "Can't connect to host",
  "434" => "Error reading config file",
  "435" => "Can't execute service to read config file",
  "440" => "EagleLAN contact failed",
  "441" => "Parameter error",
  "442" => "VPN: Could not attach to data link driver",
  "444" => "Error in password file",
  "445" => "Can't read password file",
  "446" => "No entry for host in password file",
  "450" => "Management of service failed",
  "451" => "Bad port in config file",
  "452" => "Can't lookup proxy name",
  "454" => "Can't open file",
  "455" => "Child process exited under abnormal conditions",
  "456" => "HTTPS service not supported",
  "457" => "Application error",
  "501" => "Suspicious activity monitor threshold triggered",
  "502" => "Ethernet address mismatch for host",
  "503" => "Reverse address doesn't match for host",
  "504" => "Unknown entity connected to readeagle",
  "505" => "Unauthorized process killed",
  "506" => "Unauthorized user logged off",
  "510" => "Incorrect data checksum",
  "511" => "Incorrect challenge",
  "512" => "Unauthorized remote connect attempt",
  "513" => "Saved SMTP trace file",
  "514" => "Unauthorized protocol",
  "515" => "User attempted to connect to control port",
  "516" => "DNSd error",
  "523" => "Hostile Java applet blocked by SurfinGate",
  "601" => "Child process died unexpectedly",
  "602" => "Child process returned an unexpected error",
  "603" => "Fork failed",
  "605" => "Can't execute service",
  "606" => "Failed to notify",
  "607" => "Daemon exited on signal",
  "609" => "Syslog daemon not running",
  "610" => "Internal error",
  "611" => "User count limit reached",
  "612" => "Service error",
  "701" => "Can't allocate memory",
  "702" => "Quitting because of config errors",
  "704" => "Expriation date reached",
  "705" => "Invalid license key",
  "706" => "Module not licensed",
  "707" => "Service not installed",
  "708" => "Service is corrupt"
);

%valid_options = (
  "alert_summary" => 1,
  "alerts" => 1,
  "alias" => 1,
  "all" => 1,
  "basedir" => 1,
  "compress" => 1,
  "directory" => 1,
  "dns" => 1,
  "domain_summary" => 1,
  "duration" => 1,
  "fetcher_message" => 1,
  "filter" => 1,
  "graphs" => 1,
  "history_file" => 1,
  "history_summary" => 1,
  "host_protocol_detail" => 1,
  "host_summary" => 1,
  "hour_summary" => 1,
  "host_user_detail" => 1,
  "interface" => 1,
  "interface_summary" => 1,
  "links" => 1,
  "mail_from" => 1,
  "mail_server" => 1,
  "message_summary" => 1,
  "mobile_summary" => 1,
  "nice_format" => 1,
  "ooba_summary" => 1,
  "output" => 1,
  "protocol_host_detail" => 1,
  "protocol_summary" => 1,
  "protocol_user_detail" => 1,
  "ratings" => 1,
  "remotelog" => 1,
  "save_logfile" => 1,
  "search_arg" => 1,
  "search_destination" => 1,
  "search_source" => 1,
  "show_arg" => 1,
  "show_auth" => 1,
  "show_id" => 1,
  "show_interface" => 1,
  "show_op" => 1,
  "show_reason" => 1,
  "show_result" => 1,
  "show_rule" => 1,
  "show_user" => 1,
  "smtp_bugfix" => 1,
  "split" => 1,
  "theme" => 1,
  "time" => 1,
  "title" => 1,
  "totals" => 1,
  "uncompress" => 1,
  "user" => 1,
  "user_host_detail" => 1,
  "user_protocol_detail" => 1,
  "user_summary" => 1,
  "user_www_summary" => 1,
  "volume" => 1,
  "word" => 1
);

# defaults for command line options
$opt{"config"} = "reptor.cfg";

GetOptions(
  "basedir=s" => \$opt{"basedir"},
  "config=s" => \$opt{"config"},
  "date=s" => \$opt{"date"},
  "dir=s" => \$opt{"dir"},
  "history" => \$opt{"history"},
  "help" => \$opt{"help"},
  "ignore" => \$opt{"ignore"},
  "log=s" => \$opt{"log"},
  "verify" => \$opt{"verify"},
  "version" => \$opt{"version"}
);

if ($opt{"version"}) {
  version();
  exit;
}

# guess directory if not specified
$directory ||= $ENV{"EAGLEDIR"} ?
  $ENV{"EAGLEDIR"} . "/sg/oldlogs/" :
  $^O eq "MSWin32" ?
    "c:/raptor/eagle/sg/oldlogs/" :
    "/var/adm/sg/oldlogs/";

# figure out what yesterday was
{
  my ($year,$month,$day);
  ($year,$month,$day) = (localtime(time - 86400))[5,4,3];
  $yesterday = sprintf "%d%02d%02d", 1900 + $year, $month + 1, $day;
}

usage() if $opt{"help"};

# add a trailing slash if missing
$opt{"basedir"} = fmt($opt{"basedir"},"dir");

# parse configuration file
config(base_dir($opt{"config"}));

# command line overrides configuration file
$opt{"dir"} ||= $directory;
$opt{"basedir"} ||= $basedir;

# add a trailing slash if missing
$opt{"dir"} = fmt($opt{"dir"},"dir");
$opt{"basedir"} = fmt($opt{"basedir"},"dir");

# figure out what logfile to process
if ($opt{"log"}) {

  if ($uncompress) {

    # add compression extension if missing
    if ($opt{"log"} !~ /\.$extension$/) { $opt{"log"} .= "." . $extension; }

    $opt{"log"} = $uncompress . " " . $opt{"log"} . " |";

  }

} else {

  # default to yesterday unless overridden on the command line
  $file = "logfile." . ($opt{"date"} || $yesterday);

  # remotelog has precedence over local files
  if ($remotelog) {
    $opt{"log"} = $remotelog_path . "remotelogfile $remotelog $file |"
  } else {
    if ($uncompress) {
      $opt{"log"} = $uncompress . " " . $opt{"dir"} . $file . "." . $extension . " |";
    } else {
      $opt{"log"} = $opt{"dir"} . $file;
    }
  }

}

# load the theme
(-f $opt{"basedir"} . "themes/" . $theme . "/theme.pl") or die "ERROR: Unable to load theme.\n";
require($opt{"basedir"} . "themes/" . $theme . "/theme.pl");

# open the logfile
unless (open LOG, $opt{"log"}) {

  # not necessarily a fatal error yet
  print STDERR
    "WARNING: Unable to open logfile \"", $opt{"log"} ,"\"\n",
    "         Attempting v4 naming style...\n";

  # try the v4 naming style
  $opt{"log"} =~ s/logfile\.(?:[0-9]{2})([0-9]{6})/logfile\.$1/;
  open LOG, $opt{"log"} or die "ERROR: Unable to open logfile \"" . $opt{"log"} . "\"\n";

}

if ($opt{"verify"}) {
  if ($remotelog) {
    print "Remotelog program called successfully.\n";
  } else {
    if ($uncompress) {
      print "Uncompression program called successfully.\n";
    } else {
      print "Logfile opened successfully.\n";
    }
  }
}

# attempt to extract date information from first line
if ($opt{"ignore"}) {
  $filename = "reptor.html";
} else {
  $_ = <LOG>;
  if (/^(...) (..).+, Year is ([0-9]{4}), /) {
    $datetitle = "$monthname[$monthnum{$1} - 1] $2, $3";
    $filename = $3 . $monthnum{$1} . sprintf("%02d",$2) . ".html";
  } else {
    die "ERROR: Logfile does not exist or has been cut. Specify --ignore if cut.\n";
  }
}

if ($opt{"verify"}) {
  close LOG;
  print "Everything seems to be ok.\n";
  exit;
}

# create a local copy of the logfile
if ($save_logfile) {
  $save_name = $save_logfile . ($file ? $file : ($opt{"log"} =~ /([^\/\\]+)$/)[0]);
  if (-f $save_name) { die "ERROR: Filename for copy of logfile is already in use.\n"; }
  open SAVE, ">$save_name" or die "ERROR: Unable to create copy of logfile in $save_name.\n";
  print SAVE;
}

# open the output file
if ($output_how =~ /^(ftp|file|mail)$/) {
  if ($output_how eq "file") {
    if ($output_to =~ /[\/\\]$/) {
      open OUTPUT, ">$output_to/$filename" or die "ERROR: Unable to open output file $output_to.\n";
    } else {
      open OUTPUT, ">$output_to" or die "ERROR: Unable to open output file $output_to.\n";
    }
  } else {
    open OUTPUT, ">" . $opt{"basedir"} . "reptor.$$" or die "ERROR: Unable to create temporary file.\n";
  }
  select OUTPUT;
}

# print HTML header
html_header($datetitle);

# print interface filter list
if (%filter) {
  begin_table();
  begin_row("title");
  cell("Active filters");
  end_row();
  for ($index = 0; $index <= $#{$filter{"source"}}; $index++) {
    begin_row("data");
    cell("From " . $filter{"source"}[$index] . " to " . $filter{"destination"}[$index]);
    end_row();
  }
  end_table();
}

# print headers for alerts section
if ($alerts) {
  makelinks("alerts");
  begin_table();
  begin_row("title");
  cell("Alerts",'colspan="' . $columns . '"');
  end_row();
  begin_row("header");
  if ($show_reason) { cell("Reason"); }
  cell("Time");
  cell("Duration");
  cell("Protocol");
  cell("Bytes<br>Sent");
  cell("Bytes<br>Received");
  cell("Bytes<br>Total");
  if ($show_interface) { cell("Source<br>Interface"); }
  cell("Source<br>Host");
  if ($show_interface) { cell("Destination<br>Interface"); }
  cell("Destination<br>Host");
  if ($show_user) { cell("User"); }
  if ($show_auth) { cell("Authentication<br>Method"); }
  if ($show_rule) { cell("Rule"); }
  if ($show_id) { cell("ID"); }
  if ($show_op) { cell("Op"); }
  if ($show_result) { cell("Result"); }
  if ($show_arg) { cell("Argument"); }
  end_row();
}

SPLIT: while (1) {

while (<LOG>) {

  # localize variables
  my(
    $user,$auth,$sent,$rcvd,$srcif,$src,$dstif,$dst,$total,$www,
    $result,$id,$type,$time,$arg,$proto,$ok,$linkproto,$line,
    $newtime,$alerted,$search,$rule,$op,$domain,$duration,$hour
  );

  # make local copy of logfile
  if ($save_logfile) { print SAVE; }

  # parse out time and message type
  ($time,$type,$line) =
    /^
    [a-zA-Z]{3}[ ]     # month
    [ 0-9]{2}[ ]       # day
    ([0-9:]{8})        # hour:minute:second
    (?:\.[0-9]{3})?[ ] # microsecond (v6 only)
    [^ ]+[ ]           # firewall name
    [^ ]+[ ]           # subsystem & pid?
    ([0-9]+)[ ]        # message number
    (.+)               # rest of line
    $/x;

  # accumulate message summaries
  if (%message_summary && $type >= $message_summary{"limit"}) { $total_message{$type}++; }

  $_ = $line;

  # check for Raptor Mobile connections
  if (%mobile_summary && $type == 120) {

    # Raptor Mobile pre v6.5
    if (/^nsetupd Info: user ([^ ]+) authenticated /){
      $total_mobile{$1}++;
      $total_mobile++;
    } else {

      # Raptor Mobile 6.5
      if (/^isakmpd Info: Authenticated .* id=\(ID_KEY_ID,([^\)]+)\)/) {
        $total_mobile{$1}++;
        $total_mobile++;
      }
    }
  }

  # check for OOBA messages
  if (%ooba_summary && $type == 120 && /^oobauthui Info: user ([^ ]+) from host ([^ ]+) ([^ ]+) protocol oobauth$/) {
    $ooba{"time"}{$ooba} = $time;
    $ooba{"user"}{$ooba} = $1;
    $ooba{"host"}{$ooba} = $2;
    $ooba{"action"}{$ooba} = $3;
    $ooba++;
  }

  # check for WebNOT fetcher messages
  if ($fetcher_message && $type == 120 && !$fetcher && /^fetcher Info: (.*)/) { $fetcher = $1; }

  # only calculate stats for logfile entries that have type = 121
  next if ($type != 121);

  # only calculate stats for logfile entries that have duration
  if (/^Statistics: duration=([^ ]+) /) {

    # round off duration
    $duration = int($1 + 0.5);

    # source interface
    $srcif = (/ srcif=([^ ]+) /) ? $interface{$1} : "Unidentified";

    # destination interface
    $dstif = (/ dstif=([^ ]+) /) ? $interface{$1} : "Unidentified";

    # check for filtered interfaces before continuing
    if (%filter) {
      for ($index = 0; $index <= $#{$filter{"source"}}; $index++) {
        if (($filter{"source"}[$index] eq "any" || $srcif eq $filter{"source"}[$index]) &&
            ($filter{"destination"}[$index] eq "any" || $dstif eq $filter{"destination"}[$index])) {
          $ok = 1;
          last;
        }
      }
      next if (!$ok);
    }

    # bytes sent
    ($sent) = / sent=([0-9]+) /;

    # bytes received
    ($rcvd) = / rcvd=([0-9]+) /;

    # total bytes
    $total = $sent + $rcvd;

    # source host
    unless($src = lc((/ srcname=([^ ]+) /)[0])) {
      $src = (split(/\//,(/ src=([^ ]+) /)[0]))[0];
      $src = $dns eq "all" ? fmt($src,"dns") : $src;
    }

    # destination host
    unless($dst = lc((/ dstname=([^ ]+) /)[0])) {
      $dst = (split(/\//,(/ dst=([^ ]+) /)[0]))[0];
      $dst = $dns eq "all" ? fmt($dst,"dns") : $dst;
    }

    # protocol
    if (/ proto=([^ ]+) /) {
      next if ($smtp_bugfix && $1 eq "smtp" && $rcvd > 1000000000000);
      $proto = $alias{$1} || $1;
      if ($links && $linkalias{$1}) { $linkproto = $linkalias{$1}; }
      if (%user_www_summary && ($1 eq "http" || $1 eq "http-https")) { $www = 1; }
    }

    # user
    if (/ user=([^ ]+) /) {
      $user = $1;

      # remove less than and greater than signs
      $user =~ tr/^<//d;
      $user =~ tr/>$//d;
    }

    # argument
    if (($alerts && $show_arg) || %user_www_summary) {
      ($arg) = / arg=([^ ]+) /;
    }

    # process the entry for alert triggering
    if ($alerts) {

      # authentication method
      if ($show_auth) { ($auth) = / auth=([^ ]+) /; }

      # rule
      if ($show_rule) { ($rule) = / rule=([^ ]+) /; }

      # id
      if ($show_id) { ($id) = / id=([^ ]+) /; }

      # op
      if ($show_op) { ($op) = / op=([^\" ]+|\"[^\"]+\") /; }

      # result
      if ($show_result) { ($result) = / result=([^\" ]+|\"[^\"]+\") /; }

      # build string for word search
      if ($word) {
        if ($search_source) { $search .= " $src"; }
        if ($search_destination) { $search .= " $dst"; }
        if ($search_arg) { $search .= " $arg"; }
      }

      # determine if this entry triggers an alert condition
      # manually control short-circuiting to optimize speed
      # perform most computationally expensive checks last

      # check for all option
      if ($all) { $alerted = "All"; }

      # check for duration
      if (!$alerted && $duration{$proto} && $duration >= $duration{$proto}) { $alerted = "Duration"; }

      # check for volume
      if (!$alerted && $volume{$proto} && $total >= $volume{$proto}) { $alerted = "Volume"; }

      # check for time of day
      if (!$alerted && $time{$proto}) {
        $newtime = substr($time,0,2) . substr($time,3,2);
        if (substr($time,6,2) > 30) { $newtime++; }
        for ($index = 0; $index <= $#{$time{$proto}{"begin"}}; $index++) {
          if ($newtime >= $time{$proto}{"begin"}[$index] && $newtime <= $time{$proto}{"end"}[$index]) {
            $alerted = "Time";
            last;
          }
        }
      }

      # check for user
      if (!$alerted && $user{$proto}{$user}) { $alerted = "User"; }

      # rating
      if ($ratings && / \(Denied due to ratings check\)$/) { $alerted = "Ratings"; }

      # check for word
      if (!$alerted && $word && $search =~ $word) { $alerted = "Word"; }

      # print the entry if an alert triggered
      if ($alerted) {

        # accumulate number of alerts for this host
        if (%alert_summary) { $total_host_alerts{$src}++; }

        # make less than and greater than signs HTML friendly
        if ($show_arg && $arg) {
          $arg =~ s/</&lt;/g;
          $arg =~ s/>/&gt;/g;
        }

        begin_row("data");

        if ($show_reason) { cell($alerted,'align="left"'); }

        cell($time,'align="right"');
        cell(fmt($duration,"duration"),'align="right"');
        cell($proto,'align="left"');
        cell(fmt($sent,"byte"),'align="right"');
        cell(fmt($rcvd,"byte"),'align="right"');
        cell(fmt($total,"byte"),'align="right"');

        if ($show_interface) { cell($srcif,'align="left"'); }
        cell(fmt($src,"dns"),'align="left"');

        if ($show_interface) { cell($dstif,'align="left"'); }
        if ($links && $linkproto && $linkproto ne "mailto") {
          cell('<a href="' . $linkproto . '://' . $dst . '" target="_blank">' . fmt($dst,"dns") . '</a>','align="left"');
        } else {
          cell(fmt($dst,"dns"),'align="left"');
        }

        if ($show_user) {
          if ($user && $links && $linkproto eq "mailto") {
            cell('<a href="mailto:' . $user . '">' . $user . '</a>','align="left"');
          } else {
            cell($user,'align="left"');
          }
        }

        if ($show_auth) { cell($auth,'align="left"'); }

        if ($show_rule) { cell($rule,'align="right"'); }
        if ($show_id) { cell($id,'align="right"'); }
        if ($show_op) { cell($op,'align="right"'); }
        if ($show_result) { cell($result,'align="left"'); }

        if ($show_arg) {
          if ($arg && $links && $linkproto && $linkproto ne "mailto") {
            if ($arg =~ /^http:\/\//) {
              cell('<a href="' . $arg . '" target="_blank">' . $arg . '</a>','align="left"');
            } else {
              cell('<a href="' . $linkproto . '://' . $dst . $arg . '" target="_blank">' . $arg . '</a>','align="left"');
            }
          } else {
            cell($arg,'align="left"');
          }
        }

        end_row();

      } # if ($alerted)

    } # if ($alerts)

    # accumulate total stats
    $total_hits++;
    $total_total += $total;
    $total_duration += $duration;

    # accumulate top level domain stats
    if (%domain_summary) {
      if ($src =~ /\.[a-z]/) {
        $domain = substr($src,rindex($src,".") + 1);
        $total_domain_hits{$domain}++;
        $total_domain_sent{$domain} += $sent;
        $total_domain_received{$domain} += $rcvd;
        $total_domain_total{$domain} += $total;
        $total_domain_duration{$domain} += $duration;
      }
      if ($dst =~ /\.[a-z]/) {
        $domain = substr($dst,rindex($dst,".") + 1);
        $total_domain_hits{$domain}++;
        $total_domain_sent{$domain} += $rcvd;
        $total_domain_received{$domain} += $sent;
        $total_domain_total{$domain} += $total;
        $total_domain_duration{$domain} += $duration;
      }
    }

    # accumulate time of day stats
    if (%hour_summary) {
      ($hour) = split(/:/,$time);
      $total_hour_hits{$hour}++;
      $total_hour_sent{$hour} += $sent;
      $total_hour_received{$hour} += $rcvd;
      $total_hour_total{$hour} += $total;
      $total_hour_duration{$hour} += $duration;
    }

    # accumulate interface stats
    if (%interface_summary) {
      $total_interface_hits{$srcif}++;
      $total_interface_hits{$dstif}++;
      $total_interface_sent{$srcif} += $sent;
      $total_interface_sent{$dstif} += $rcvd;
      $total_interface_received{$srcif} += $rcvd;
      $total_interface_received{$dstif} += $sent;
      $total_interface_duration{$srcif} += $duration;
      $total_interface_duration{$dstif} += $duration;
      $total_interface_total{$srcif} += $total;
      $total_interface_total{$dstif} += $total;
    }

    # accumulate host stats
    if (%host_summary) {
      $total_host_hits{$srcif}{$src}++;
      $total_host_hits{$dstif}{$dst}++;
      $total_host_sent{$srcif}{$src} += $sent;
      $total_host_sent{$dstif}{$dst} += $rcvd;
      $total_host_received{$srcif}{$src} += $rcvd;
      $total_host_received{$dstif}{$dst} += $sent;
      $total_host_total{$srcif}{$src} += $total;
      $total_host_total{$dstif}{$dst} += $total;
      $total_host_duration{$srcif}{$src} += $duration;
      $total_host_duration{$dstif}{$dst} += $duration;
    }
    if (%host_protocol_detail) {
      $details{"host"}{"protocol"}{"hits"}{$src}{$proto}++;
      $details{"host"}{"protocol"}{"hits"}{$dst}{$proto}++;
      $details{"host"}{"protocol"}{"sent"}{$src}{$proto} += $sent;
      $details{"host"}{"protocol"}{"sent"}{$dst}{$proto} += $sent;
      $details{"host"}{"protocol"}{"received"}{$src}{$proto} += $rcvd;
      $details{"host"}{"protocol"}{"received"}{$dst}{$proto} += $rcvd;
      $details{"host"}{"protocol"}{"total"}{$src}{$proto} += $total;
      $details{"host"}{"protocol"}{"total"}{$dst}{$proto} += $total;
      $details{"host"}{"protocol"}{"duration"}{$src}{$proto} += $duration;
      $details{"host"}{"protocol"}{"duration"}{$dst}{$proto} += $duration;
    }
    if (%host_user_detail) {
      $details{"host"}{"user"}{"hits"}{$src}{$user}++;
      $details{"host"}{"user"}{"hits"}{$dst}{$user}++;
      $details{"host"}{"user"}{"sent"}{$src}{$user} += $sent;
      $details{"host"}{"user"}{"sent"}{$dst}{$user} += $sent;
      $details{"host"}{"user"}{"received"}{$src}{$user} += $rcvd;
      $details{"host"}{"user"}{"received"}{$dst}{$user} += $rcvd;
      $details{"host"}{"user"}{"total"}{$src}{$user} += $total;
      $details{"host"}{"user"}{"total"}{$dst}{$user} += $total;
      $details{"host"}{"user"}{"duration"}{$src}{$user} += $duration;
      $details{"host"}{"user"}{"duration"}{$dst}{$user} += $duration;
    }

    # accumulate user stats
    if ($user) {
      if (%user_summary) {
        $total_user_hits{$user}++;
        $total_user_sent{$user} += $sent;
        $total_user_received{$user} += $rcvd;
        $total_user_total{$user} += $total;
        $total_user_duration{$user} += $duration;
      }
      if (%user_protocol_detail) {
        $details{"user"}{"protocol"}{"hits"}{$user}{$proto}++;
        $details{"user"}{"protocol"}{"sent"}{$user}{$proto} += $sent;
        $details{"user"}{"protocol"}{"received"}{$user}{$proto} += $rcvd;
        $details{"user"}{"protocol"}{"total"}{$user}{$proto} += $total;
        $details{"user"}{"protocol"}{"duration"}{$user}{$proto} += $duration;
      }
      if (%user_host_detail) {
        $details{"user"}{"host"}{"hits"}{$user}{$src}++;
        $details{"user"}{"host"}{"hits"}{$user}{$dst}++;
        $details{"user"}{"host"}{"sent"}{$user}{$src} += $sent;
        $details{"user"}{"host"}{"sent"}{$user}{$dst} += $sent;
        $details{"user"}{"host"}{"received"}{$user}{$src} += $rcvd;
        $details{"user"}{"host"}{"received"}{$user}{$dst} += $rcvd;
        $details{"user"}{"host"}{"total"}{$user}{$src} += $total;
        $details{"user"}{"host"}{"total"}{$user}{$dst} += $total;
        $details{"user"}{"host"}{"duration"}{$user}{$src} += $duration;
        $details{"user"}{"host"}{"duration"}{$user}{$dst} += $duration;
      }
      if (%user_www_summary && $www) {
        ($domain) = $arg =~ /^(?:http:\/\/)?(?:.+@.+:)?([a-zA-Z0-9.\-]+)/;
        $total_user_www{$user}{$domain}{"hits"}++;
        $total_user_www{$user}{$domain}{"sent"} += $sent;
        $total_user_www{$user}{$domain}{"received"} += $rcvd;
        $total_user_www{$user}{$domain}{"total"} += $total;
        $total_user_www{$user}{$domain}{"duration"} += $duration;
        $total_user_www{$user}{"total"}{"hits"}++;
        $total_user_www{$user}{"total"}{"sent"} += $sent;
        $total_user_www{$user}{"total"}{"received"} += $rcvd;
        $total_user_www{$user}{"total"}{"total"} += $total;
        $total_user_www{$user}{"total"}{"duration"} += $duration;
      }
    }

    # accumulate protocol stats
    if (%protocol_summary) {
      $total_protocol_hits{$proto}++;
      $total_protocol_sent{$proto} += $sent;
      $total_protocol_received{$proto} += $rcvd;
      $total_protocol_total{$proto} += $total;
      $total_protocol_duration{$proto} += $duration;
    }
    if (%protocol_host_detail) {
      $details{"protocol"}{"host"}{"hits"}{$proto}{$src}++;
      $details{"protocol"}{"host"}{"hits"}{$proto}{$dst}++;
      $details{"protocol"}{"host"}{"sent"}{$proto}{$src} += $sent;
      $details{"protocol"}{"host"}{"sent"}{$proto}{$dst} += $sent;
      $details{"protocol"}{"host"}{"received"}{$proto}{$src} += $rcvd;
      $details{"protocol"}{"host"}{"received"}{$proto}{$dst} += $rcvd;
      $details{"protocol"}{"host"}{"total"}{$proto}{$src} += $total;
      $details{"protocol"}{"host"}{"total"}{$proto}{$dst} += $total;
      $details{"protocol"}{"host"}{"duration"}{$proto}{$src} += $duration;
      $details{"protocol"}{"host"}{"duration"}{$proto}{$dst} += $duration;
    }
    if (%protocol_user_detail) {
      $details{"protocol"}{"user"}{"hits"}{$proto}{$user}++;
      $details{"protocol"}{"user"}{"sent"}{$proto}{$user} += $sent;
      $details{"protocol"}{"user"}{"received"}{$proto}{$user} += $rcvd;
      $details{"protocol"}{"user"}{"total"}{$proto}{$user} += $total;
      $details{"protocol"}{"user"}{"duration"}{$proto}{$user} += $duration;
    }

  } # if entry selected

} # while (<LOG>)

close LOG;

# handle split logfiles
if ($split) {
  if ($remotelog) {
    my $file;
    ($file) = $opt{"log"} =~ / ([^ ]+)(?:-\d+)? \|$/;
    $file .= "-" . $split++;
    open LOG, $remotelog_path . "remotelogfile $remotelog $file |" or last SPLIT;
  } else {
    if ($uncompress) {
      my $file;
      ($file) = $opt{"log"} =~ /^$uncompress (.+)(?:-\d+)?\.$extension \|$/;
      $file .= "-" . $split++ . "." . $extension;
      if (-f $file) {
        open LOG, "$uncompress $file |" or last SPLIT;
      } else {
        last SPLIT;
      }
    } else {
      open LOG, $opt{"log"} . "-" . $split++ or last SPLIT;
    }
  }
  last SPLIT if (eof(LOG));
} else {
  last SPLIT;
}

} # SPLIT

if ($alerts) { end_table(); }

if ($save_logfile) {
  close SAVE;
  if ($compress) { system(split(/ /,$compress),$save_name); }
}

# update history only if we're processing yesterday's logfile, unless overridden on the command line
if ($history_file && ($opt{"history"} || $file eq "logfile.$yesterday")) {
  open HISTORY, ">>" . base_dir($history_file) or die "ERROR: Unable to open history file.\n";
  print HISTORY "$yesterday,$total_hits,$total_total,$total_duration\n";
  close HISTORY;
}

if (%alert_summary) { alert_summary(); }

if ($totals) { total_summary(); }

if (%protocol_summary) { protocol_summary(); }

if (%protocol_host_detail) {
  makelinks("protocol_host_detail");
  foreach $index (sort keys %protocol_host_detail) {
    detail("protocol","host",$index);
  }
}

if (%protocol_user_detail) {
  makelinks("protocol_user_detail");
  foreach $index (sort keys %protocol_user_detail) {
    detail("protocol","user",$index);
  }
}

if (%interface_summary) { interface_summary(); }

if (%hour_summary) { hour_summary(); }

if (%host_summary) {
  makelinks("host_summary");
  foreach $index (sort keys %host_summary) { host_summary($index); }
}

if (%host_protocol_detail) {
  makelinks("host_protocol_detail");
  foreach $index (sort keys %host_protocol_detail) {
    detail("host","protocol",$index);
  }
}

if (%host_user_detail) {
  makelinks("host_user_detail");
  foreach $index (sort keys %host_user_detail) {
    detail("host","user",$index);
  }
}

if (%user_summary) { user_summary(); }

if (%user_www_summary) { user_www_summary(); }

if (%user_protocol_detail) {
  makelinks("user_protocol_detail");
  foreach $index (sort keys %user_protocol_detail) {
    detail("user","protocol",$index);
  }
}

if (%user_host_detail) {
  makelinks("user_host_detail");
  foreach $index (sort keys %user_host_detail) {
    detail("user","host",$index);
  }
}

if (%domain_summary) { domain_summary(); }

if (%mobile_summary) { mobile_summary(); }

if (%ooba_summary) { ooba_summary(); }

if (%message_summary) { message_summary(); }

if ($fetcher_message) { fetcher_message(); }

if (%history_summary) {
  makelinks("history_summary");
  foreach $index (sort keys %history_summary) { history_summary($index); }
}

# print HTML footer
html_footer();

# send off the output file
if ($output_how =~ /^(ftp|file|mail)$/) {
  close OUTPUT;

  if ($output_how eq "ftp") {
    my ($ftp_user,$ftp_pwd,$ftp_host,$ftp_file,$ftp);
    ($ftp_user,$ftp_pwd,$ftp_host,$ftp_file) = split(/,/,$output_to);
    if ($ftp_file =~ /[\/\\]$/) { $ftp_file .= $filename; }
    $ftp = Net::FTP->new($ftp_host);
    $ftp->pasv();
    $ftp->login($ftp_user,$ftp_pwd);
    $ftp->put($opt{"basedir"} . "reptor.$$",$ftp_file);
    $ftp->quit;
    unlink $opt{"basedir"} . "reptor.$$";
  }

  if ($output_how eq "mail") {
    my ($mail_date,$smtp);
    $mail_date = localtime(time);
    $smtp = Net::SMTP->new($mail_server);
    $smtp->mail($mail_from);
    $smtp->to($output_to);
    $smtp->data();
    $smtp->datasend("From: $mail_from\n");
    $smtp->datasend("To: $output_to\n");
    $smtp->datasend("Date: $mail_date\n");
    $smtp->datasend("Subject: Reptor report" . ($datetitle ? " for $datetitle" : "") . "\n");
    $smtp->datasend("MIME-Version: 1.0\n");
    $smtp->datasend("Content-type: text/html\n");
    $smtp->datasend("\n");
    open TEMP, $opt{"basedir"} . "reptor.$$" or die "ERROR: Unable to open temporary file.\n";
    while (<TEMP>) { $smtp->datasend($_); }
    close TEMP;
    $smtp->dataend();
    $smtp->quit;
    unlink $opt{"basedir"} . "reptor.$$";
  }
}

# done

sub alert_summary {
  my ($num,$direction,$sort,$class,$index);
  $num = 0;
  makelinks("alert_summary");
  begin_table();

  begin_row("title");
  cell("Alert Summary",'colspan="2"');
  end_row();

  begin_row("header");
  cell("Host");
  cell("Alerts");
  end_row();

  $direction = substr($alert_summary{"sort"},0,1);
  substr($alert_summary{"sort"},0,1) = "";
  if ($alert_summary{"sort"} eq "host") {
    $sort = 'sort keys %total_host_alerts';
  } else {
    $sort = 'sort { $total_host_alerts{$a} <=> $total_host_alerts{$b} } keys %total_host_alerts';
  }
  if ($direction eq "-") { $sort = 'reverse ' . $sort; }

  foreach $index (eval($sort)) {
    begin_row("data");
    cell(fmt($index,"dns"),'align="right"');
    cell($total_host_alerts{$index},'align="right"');
    end_row();
    last if ($alert_summary{"limit"} && ++$num >= $alert_summary{"limit"});
  }

  end_table();
}

sub total_summary {
  my ($index,$class);
  makelinks("totals");
  begin_table();

  begin_row("title");
  cell("Totals",'colspan="2"');
  end_row();

  begin_row("data");
  cell("Hits",'align="right"');
  cell(fmt($total_hits,"comma"),'align="left"');
  end_row();

  begin_row("data");
  cell("Duration",'align="right"');
  cell(fmt($total_duration,"duration"),'align="left"');
  end_row();

  begin_row("data");
  cell("Bytes",'align="right"');
  cell(fmt($total_total,"byte"),'align="left"');
  end_row();

  if (%host_summary) {
    foreach $index (sort keys %host_summary) {
      begin_row("data");
      cell("Hosts [" . $index . "]",'align="right"');
      cell(scalar keys %{$total_host_hits{$index}},'align="left"');
      end_row();
    }
  }

  end_table();
}

sub protocol_summary {
  my ($num,$direction,$sort,$class,$index);
  $num = 0;
  makelinks("protocol_summary");
  begin_table();

  begin_row("title");
  cell("Protocol Summary",'colspan="' . ($graphs ? "8" : "7") . '"');
  end_row();

  begin_row("header");
  cell("Protocol");
  cell("Hits");
  cell("Duration");
  cell("Bytes<br>Sent");
  cell("Bytes<br>Received");
  cell("Bytes<br>Total");
  if ($graphs) {
    cell("Percent of Total Bytes",'colspan="2"');
  } else {
    cell("Percent of<br>Total Bytes");
  }
  end_row();

  $direction = substr($protocol_summary{"sort"},0,1);
  substr($protocol_summary{"sort"},0,1) = "";
  if ($protocol_summary{"sort"} eq "protocol") {
    $sort = 'sort keys %total_protocol_hits';
  } else {
    $sort =
     'sort { $total_protocol_' . $protocol_summary{"sort"} .
     '{$a} <=> $total_protocol_' . $protocol_summary{"sort"} .
     '{$b} } keys %total_protocol_hits';
  }
  if ($direction eq "-") { $sort = 'reverse ' . $sort; }

  foreach $index (eval($sort)) {
    begin_row("data");
    cell($index,'align="right"');
    cell(fmt($total_protocol_hits{$index},"comma"),'align="right"');
    cell(fmt($total_protocol_duration{$index},"duration"),'align="left"');
    cell(fmt($total_protocol_sent{$index},"byte"),'align="right"');
    cell(fmt($total_protocol_received{$index},"byte"),'align="right"');
    cell(fmt($total_protocol_total{$index},"byte"),'align="right"');
    cell(fmt($total_protocol_total{$index} / $total_total,"percent"),'align="right"');
    if ($graphs) {
      cell(hbar($total_protocol_total{$index},$total_total),'align="left"');
    }
    end_row();
    last if ($protocol_summary{"limit"} && ++$num >= $protocol_summary{"limit"});
  }

  end_table();
}

sub detail {
  my ($detail,$group,$object,$num,$config,$direction,$sort,$class,$index);

  $detail = shift;
  $group = shift;
  $object = shift;

  $num = 0;
  $config = $detail . "_" . $group . "_detail";

  begin_table();

  begin_row("title");
  cell(
    fmt($detail,"ucfirst") . " Detail by " . fmt($group,"ucfirst") . " [" . $object . "]",
    'colspan="' . ($graphs ? "8" : "7") . '"'
  );
  end_row();

  begin_row("header");
  cell(fmt($group,"ucfirst"));
  cell("Hits");
  cell("Duration");
  cell("Bytes<br>Sent");
  cell("Bytes<br>Received");
  cell("Bytes<br>Total");
  if ($graphs) {
    cell("Percent of Total Bytes",'colspan="2"');
  } else {
    cell("Percent of<br>Total Bytes");
  }
  end_row();

  $direction = substr(${$config}{$object}{"sort"},0,1);
  substr(${$config}{$object}{"sort"},0,1) = "";

  if (${$config}{$object}{"sort"} eq $group) {
    $sort = 'sort keys %{$details{$detail}{$group}{"hits"}{$object}}';
  } else {
    $sort =
      'sort { $details{$detail}{$group}{${$config}{$object}{"sort"}}{$object}' .
      '{$a} <=> $details{$detail}{$group}{${$config}{$object}{"sort"}}{$object}' .
      '{$b} } keys %{$details{$detail}{$group}{"hits"}{$object}}';
  }
  if ($direction eq "-") { $sort = 'reverse ' . $sort; }

  foreach $index (eval($sort)) {
    begin_row("data");
    cell($index,'align="right"');
    cell(fmt($details{$detail}{$group}{"hits"}{$object}{$index},"comma"),'align="right"');
    cell(fmt($details{$detail}{$group}{"duration"}{$object}{$index},"duration"),'align="left"');
    cell(fmt($details{$detail}{$group}{"sent"}{$object}{$index},"byte"),'align="right"');
    cell(fmt($details{$detail}{$group}{"received"}{$object}{$index},"byte"),'align="right"');
    cell(fmt($details{$detail}{$group}{"total"}{$object}{$index},"byte"),'align="right"');
    cell(fmt($details{$detail}{$group}{"total"}{$object}{$index} / $total_total,"percent"),'align="right"');
    if ($graphs) {
      cell(hbar($details{$detail}{$group}{"total"}{$object}{$index},$total_total),'align="left"');
    }
    end_row();
    last if (${$config}{$object}{"limit"} && ++$num >= ${$config}{$object}{"limit"});
  }

  end_table();
}

sub interface_summary {
  my ($num,$direction,$sort,$class,$index);

  $num = 0;
  makelinks("interface_summary");

  begin_table();

  begin_row("title");
  cell("Interface Summary",'colspan="' . ($graphs ? "8" : "7") . '"');
  end_row();

  begin_row("header");
  cell("Interface");
  cell("Hits");
  cell("Duration");
  cell("Bytes<br>Sent");
  cell("Bytes<br>Received");
  cell("Bytes<br>Total");
  if ($graphs) {
    cell("Percent of Total Bytes",'colspan="2"');
  } else {
    cell("Percent of<br>Total Bytes");
  }
  end_row();

  $direction = substr($interface_summary{"sort"},0,1);
  substr($interface_summary{"sort"},0,1) = "";
  if ($interface_summary{"sort"} eq "interface") {
    $sort = 'sort keys %total_interface_hits';
  } else {
    $sort =
      'sort { $total_interface_' . $interface_summary{"sort"} .
      '{$a} <=> $total_interface_' . $interface_summary{"sort"} .
      '{$b} } keys %total_interface_hits';
  }
  if ($direction eq "-") { $sort = 'reverse ' . $sort; }

  foreach $index (eval($sort)) {
    begin_row("data");
    cell($index,'align="right"');
    cell(fmt($total_interface_hits{$index},"comma"),'align="right"');
    cell(fmt($total_interface_duration{$index},"duration"),'align="left"');
    cell(fmt($total_interface_sent{$index},"byte"),'align="right"');
    cell(fmt($total_interface_received{$index},"byte"),'align="right"');
    cell(fmt($total_interface_total{$index},"byte"),'align="right"');
    cell(fmt($total_interface_total{$index} / $total_total,"percent"),'align="right"');
    if ($graphs) {
      cell(hbar($total_interface_total{$index},$total_total),'align="left"');
    }
    end_row();
    last if ($interface_summary{"limit"} && ++$num >= $interface_summary{"limit"});
  }

  end_table();
}

sub hour_summary {
  my ($num,$direction,$sort,$class,$index);

  $num = 0;
  makelinks("hour_summary");

  begin_table();

  begin_row("title");
  cell("Time of Day Summary",'colspan="' . ($graphs ? "8" : "7") . '"');
  end_row();

  begin_row("header");
  cell("Hour",'align="right"');
  cell("Hits",'align="right"');
  cell("Duration",'align="left"');
  cell("Bytes<br>Sent",'align="right"');
  cell("Bytes<br>Received",'align="right"');
  cell("Bytes<br>Total",'align="right"');
  if ($graphs) {
    cell("Percent of Total Bytes",'colspan="2"');
  } else {
    cell("Percent of<br>Total Bytes");
  }
  end_row();

  $direction = substr($hour_summary{"sort"},0,1);
  substr($hour_summary{"sort"},0,1) = "";
  if ($hour_summary{"sort"} eq "hour") {
    $sort = 'sort keys %total_hour_hits';
  } else {
    $sort =
      'sort { $total_hour_' . $hour_summary{"sort"} . '{$a} <=> $total_hour_' .
      $hour_summary{"sort"} . '{$b} } keys %total_hour_hits';
  }
  if ($direction eq "-") { $sort = 'reverse ' . $sort; }

  foreach $index (eval($sort)) {
    begin_row("data");
    cell($index . ":00 - " . $index . ":59",'align="right"');
    cell(fmt($total_hour_hits{$index},"comma"),'align="right"');
    cell(fmt($total_hour_duration{$index},"duration"),'align="left"');
    cell(fmt($total_hour_sent{$index},"byte"),'align="right"');
    cell(fmt($total_hour_received{$index},"byte"),'align="right"');
    cell(fmt($total_hour_total{$index},"byte"),'align="right"');
    cell(fmt($total_hour_total{$index} / $total_total,"percent"),'align="right"');
    if ($graphs) {
      cell(hbar($total_hour_total{$index},$total_total),'align="left"');
    }
    end_row();
    last if ($hour_summary{"limit"} && ++$num >= $hour_summary{"limit"});
  }

  end_table();
}

sub host_summary {
  my ($host,$num,$direction,$sort,$class,$index);

  $host = shift;
  $num = 0;

  begin_table();

  begin_row("title");
  cell("Host Summary [" . $host . "]",'colspan="' . ($graphs ? "8" : "7") . '"');
  end_row();

  begin_row("header");
  cell("Host");
  cell("Hits");
  cell("Duration");
  cell("Bytes<br>Sent");
  cell("Bytes<br>Received");
  cell("Bytes<br>Total");
  if ($graphs) {
    cell("Percent of Total Bytes",'colspan="2"');
  } else {
    cell("Percent of<br>Total Bytes");
  }
  end_row();
  
  $direction = substr($host_summary{$host}{"sort"},0,1);
  substr($host_summary{$host}{"sort"},0,1) = "";
  if ($host_summary{$host}{"sort"} eq "host") {
    $sort = 'sort keys %{$total_host_hits{$host}}';
  } else {
    $sort =
      'sort { $total_host_' . $host_summary{$host}{"sort"} .
      '{$host}{$a} <=> $total_host_' . $host_summary{$host}{"sort"} .
      '{$host}{$b} } keys %{$total_host_hits{$host}}';
  }
  if ($direction eq "-") { $sort = 'reverse ' . $sort; }

  foreach $index (eval($sort)) {
    begin_row("data");
    cell(fmt($index,"dns"),'align="right"');
    cell(fmt($total_host_hits{$host}{$index},"comma"),'align="right"');
    cell(fmt($total_host_duration{$host}{$index},"duration"),'align="left"');
    cell(fmt($total_host_sent{$host}{$index},"byte"),'align="right"');
    cell(fmt($total_host_received{$host}{$index},"byte"),'align="right"');
    cell(fmt($total_host_total{$host}{$index},"byte"),'align="right"');
    cell(fmt($total_host_total{$host}{$index} / $total_total,"percent"),'align="right"');
    if ($graphs) {
      cell(hbar($total_host_total{$host}{$index},$total_total),'align="left"');
    }
    end_row();
    last if ($host_summary{$host}{"limit"} && ++$num >= $host_summary{$host}{"limit"});
  }

  end_table();
}

sub user_summary {
  my ($num,$direction,$sort,$class,$index);

  $num = 0;
  makelinks("user_summary");

  begin_table();

  begin_row("title");
  cell("User Summary",'colspan="' . ($graphs ? "8" : "7") . '"');
  end_row();

  begin_row("header");
  cell("User");
  cell("Hits");
  cell("Duration");
  cell("Bytes<br>Sent");
  cell("Bytes<br>Received");
  cell("Bytes<br>Total");
  if ($graphs) {
    cell("Percent of Total Bytes",'colspan="2"');
  } else {
    cell("Percent of<br>Total Bytes");
  }
  end_row();

  $direction = substr($user_summary{"sort"},0,1);
  substr($user_summary{"sort"},0,1) = "";
  if ($user_summary{"sort"} eq "user") {
    $sort = 'sort keys %total_user_hits';
  } else {
    $sort =
      'sort { $total_user_' . $user_summary{"sort"} . '{$a} <=> $total_user_' .
      $user_summary{"sort"} . '{$b} } keys %total_user_hits';
  }
  if ($direction eq "-") { $sort = 'reverse ' . $sort; }

  foreach $index (eval($sort)) {
    begin_row("data");
    cell($index,'align="left"');
    cell(fmt($total_user_hits{$index},"comma"),'align="right"');
    cell(fmt($total_user_duration{$index},"duration"),'align="left"');
    cell(fmt($total_user_sent{$index},"byte"),'align="right"');
    cell(fmt($total_user_received{$index},"byte"),'align="right"');
    cell(fmt($total_user_total{$index},"byte"),'align="right"');
    cell(fmt($total_user_total{$index} / $total_total,"percent"),'align="right"');
    if ($graphs) {
      cell(hbar($total_user_total{$index},$total_total),'align="left"');
    }
    end_row();
    last if ($user_summary{"limit"} && ++$num >= $user_summary{"limit"});
  }

  end_table();
}

sub user_www_summary {
  my ($num1,$num2,$direction,$sort1,$sort2,$user,$domain);

  $num1 = 0;
  makelinks("user_www_summary");

  begin_table();

  begin_row("title");
  cell("User WWW Summary",'colspan="' . ($graphs ? "8" : "7") . '"');
  end_row();

  begin_row("header");
  cell("User /<br>Domain");
  cell("Hits");
  cell("Duration");
  cell("Bytes<br>Sent");
  cell("Bytes<br>Received");
  cell("Bytes<br>Total");
  if ($graphs) {
    cell("Percent of Total Bytes /<br>Percent of User Bytes",'colspan="2"');
  } else {
    cell("Percent of Total Bytes /<br>Percent of User Bytes");
  }
  end_row();

  $direction = substr($user_www_summary{"sort"},0,1);
  substr($user_www_summary{"sort"},0,1) = "";
  if ($user_www_summary{"sort"} eq "user") {
    $sort1 = 'sort keys %total_user_www';
  } else {
    $sort1 = 'sort {
      $total_user_www{$a}{"total"}{' . $user_www_summary{"sort"} . '} <=>
      $total_user_www{$b}{"total"}{' . $user_www_summary{"sort"} . '}
      } keys %total_user_www
    ';
  }
  if ($direction eq "-") { $sort1 = 'reverse ' . $sort1; }

  foreach $user (eval($sort1)) {
    begin_row("data");
    $toggle = 0;
    cell($user,'align="left"');
    cell(fmt($total_user_www{$user}{"total"}{"hits"},"comma"),'align="right"');
    cell(fmt($total_user_www{$user}{"total"}{"duration"},"duration"),'align="left"');
    cell(fmt($total_user_www{$user}{"total"}{"sent"},"byte"),'align="right"');
    cell(fmt($total_user_www{$user}{"total"}{"received"},"byte"),'align="right"');
    cell(fmt($total_user_www{$user}{"total"}{"total"},"byte"),'align="right"');
    cell(fmt($total_user_www{$user}{"total"}{"total"} / $total_total,"percent"),'align="right"');
    if ($graphs) {
      cell(hbar($total_user_www{$user}{"total"}{"total"},$total_total),'align="left"');
    }
    end_row();

    $num2 = 0;
    if ($user_www_summary{"sort"} eq "user") {
      $sort2 = 'sort keys %{$total_user_www{$user}}';
    } else {
      $sort2 = 'sort {
        $total_user_www{$user}{$a}{"' . $user_www_summary{"sort"} . '"} <=>
        $total_user_www{$user}{$b}{"' . $user_www_summary{"sort"} . '"}
        } keys %{$total_user_www{$user}}
      ';
    }
    if ($direction eq "-") { $sort2 = 'reverse ' . $sort2; }

    for $domain (eval($sort2)) {
      next if ($domain eq "total");
      begin_row("data");
      $toggle = 1;
      if ($links) {
        cell('&nbsp;&nbsp;<a href="http://' . $domain . '" target="_blank">' . $domain . '</a>','align="left"');
      } else {
        cell("&nbsp;&nbsp;" . $domain,'align="left"');
      }
      cell(fmt($total_user_www{$user}{$domain}{"hits"},"comma"),'align="right"');
      cell(fmt($total_user_www{$user}{$domain}{"duration"},"duration"),'align="left"');
      cell(fmt($total_user_www{$user}{$domain}{"sent"},"byte"),'align="right"');
      cell(fmt($total_user_www{$user}{$domain}{"received"},"byte"),'align="right"');
      cell(fmt($total_user_www{$user}{$domain}{"total"},"byte"),'align="right"');
      cell(fmt($total_user_www{$user}{$domain}{"total"} / $total_total,"percent"),'align="right"');
      if ($graphs) {
        cell(hbar($total_user_www{$user}{$domain}{"total"},$total_total),'align="left"');
      }
      end_row();

      last if ($user_www_summary{"limit"} && ++$num2 >= $user_www_summary{"limit"});
    }

    last if ($user_www_summary{"limit"} && ++$num1 >= $user_www_summary{"limit"});
  }

  end_table();
}

sub domain_summary {
  my ($num,$direction,$sort,$class,$index);

  $num = 0;
  makelinks("domain_summary");

  begin_table();

  begin_row("title");
  cell("Top Level Domain Summary",'colspan="' . ($graphs ? "8" : "7") . '"');
  end_row();

  begin_row("header");
  cell("Domain");
  cell("Hits");
  cell("Duration");
  cell("Bytes<br>Sent");
  cell("Bytes<br>Received");
  cell("Bytes<br>Total");
  if ($graphs) {
    cell("Percent of Total Bytes",'colspan="2"');
  } else {
    cell("Percent of<br>Total Bytes");
  }
  end_row();

  $direction = substr($domain_summary{"sort"},0,1);
  substr($domain_summary{"sort"},0,1) = "";
  if ($hour_summary{"sort"} eq "domain") {
    $sort = 'sort keys %total_domain_hits';
  } else {
    $sort =
      'sort { $total_domain_' . $domain_summary{"sort"} .
      '{$a} <=> $total_domain_' . $domain_summary{"sort"} .
      '{$b} } keys %total_domain_hits';
  }
  if ($direction eq "-") { $sort = 'reverse ' . $sort; }

  foreach $index (eval($sort)) {
    begin_row("data");
    cell($index,'align="right"');
    cell(fmt($total_domain_hits{$index},"comma"),'align="right"');
    cell(fmt($total_domain_duration{$index},"duration"),'align="left"');
    cell(fmt($total_domain_sent{$index},"byte"),'align="right"');
    cell(fmt($total_domain_received{$index},"byte"),'align="right"');
    cell(fmt($total_domain_total{$index},"byte"),'align="right"');
    cell(fmt($total_domain_total{$index} / $total_total,"percent"),'align="right"');
    if ($graphs) {
      cell(hbar($total_domain_total{$index},$total_total),'align="left"');
    }
    end_row();
    last if ($domain_summary{"limit"} && ++$num >= $domain_summary{"limit"});
  }

  end_table();
}

sub mobile_summary {
  my ($num,$direction,$sort,$class,$index);

  $num = 0;
  makelinks("mobile_summary");

  begin_table();

  begin_row("title");
  cell("Raptor Mobile Summary",'colspan="' . ($graphs ? "3" : "2") . '"');
  end_row();

  begin_row("header");
  cell("Net Entity");
  cell("Connections",$graphs ? 'colspan="2"' : "");
  end_row();

  $direction = substr($mobile_summary{"sort"},0,1);
  substr($mobile_summary{"sort"},0,1) = "";
  if ($mobile_summary{"sort"} eq "host") {
    $sort = 'sort keys %total_mobile';
  } else {
    $sort = 'sort { $total_mobile{$a} <=> $total_mobile{$b} } keys %total_mobile';
  }
  if ($direction eq "-") { $sort = 'reverse ' . $sort; }

  foreach $index (eval($sort)) {
    begin_row("data");
    cell(fmt($index,"dns"),'align="right"');
    cell($total_mobile{$index},'align="right"');
    if ($graphs) {
      cell(hbar($total_mobile{$index},$total_mobile),'align="left"');
    }
    end_row();
    last if ($mobile_summary{"limit"} && ++$num >= $mobile_summary{"limit"});
  }

  end_table();
}

sub ooba_summary {
  my ($num,$direction,$sort,$index);

  $num = 0;
  makelinks("ooba_summary");

  begin_table();

  begin_row("title");
  cell("OOBA Summary",'colspan="4"');
  end_row();

  begin_row("header");
  cell("Time");
  cell("User");
  cell("Host");
  cell("Action");
  end_row();

  $direction = substr($ooba_summary{"sort"},0,1);
  substr($ooba_summary{"sort"},0,1) = "";
  $sort =
    'sort { $ooba{"' . $ooba_summary{"sort"} . '"}{$a} cmp $ooba{"' . $ooba_summary{"sort"} . '"}{$b} } keys %{$ooba{"time"}}';
  if ($direction eq "-") { $sort = 'reverse ' . $sort; }

  foreach $index (eval($sort)) {
    begin_row("data");
    cell($ooba{"time"}{$index});
    cell($ooba{"user"}{$index});
    cell($ooba{"host"}{$index});
    cell($ooba{"action"}{$index});
    end_row();
    last if ($ooba_summary{"limit"} && ++$num >= $ooba_summary{"limit"});
  }

  end_table();
}

sub message_summary {
  my ($num,$direction,$sort,$class,$index);

  makelinks("message_summary");

  begin_table();

  begin_row("title");
  cell("Message Summary",'colspan="4"');
  end_row();

  begin_row("header");
  cell("Message");
  cell("Severity");
  cell("Occurances");
  cell("Description");
  end_row();

  $direction = substr($message_summary{"sort"},0,1);
  substr($message_summary{"sort"},0,1) = "";
  if ($message_summary{"sort"} eq "message") {
    $sort = 'sort keys %total_message';
  } else {
    $sort = 'sort { $total_message{$a} <=> $total_message{$b} } keys %total_message';
  }
  if ($direction eq "-") { $sort = 'reverse ' . $sort; }

  foreach $index (eval($sort)) {
    begin_row("data");
    cell($index,'align="right"');
    cell($message_severity[int($index / 100) - 1],'align="left"');
    cell(fmt($total_message{$index},"comma"),'align="right"');
    cell($message_description{$index},'align="left"');
    end_row();
  }

  end_table();
}

sub fetcher_message {
  makelinks("fetcher_message");
  begin_table();
  begin_row("title");
  cell("Fetcher Message");
  end_row();
  begin_row("data");
  cell($fetcher,'align="left"');
  end_row();
  end_table();
}

sub history_summary {
  my ($value,$num,$base,$index,$max,@history_stats);
  $value = shift;
  $num = 0;
  $max = 0;
  unless (open HISTORY, base_dir($history_file)) {
    print STDERR
      "WARNING: Unable to open history file. This is not a fatal error,\n",
      "processing will continue. If this is not the first time you are\n",
      "running Reptor with the history_file option specified, then you will\n",
      "probably need to include the full path along with the name of the\n",
      "history file.\n";
    return;
  }
  while (<HISTORY>) {
    my ($history_day,$history_hits,$history_total,$history_duration);
    ($history_day,$history_hits,$history_total,$history_duration) = split(/,/);
    $history_stats[$num]{"day"} = $history_day;
    $history_stats[$num]{"hits"} = $history_hits;
    $history_stats[$num]{"total"} = $history_total;
    $history_stats[$num]{"duration"} = $history_duration;
    $num++;
  }
  close HISTORY;
  $base = $num > $history_summary{$value} ? $num - $history_summary{$value} : 0;
  for ($index = 0; $index < $history_summary{$value}; $index++) {
    if ($history_stats[$base + $index]{$value} > $max) {
      $max = $history_stats[$base + $index]{$value};
    }
  }

  begin_table();

  begin_row("title");
  cell("History Summary [" . $value . "]",'colspan="' . ($history_summary{$value} + 1) . '"');
  end_row();

  begin_row("data");
  cell($value eq "total" ? fmt($max,"byte") : $max,'rowspan="3" valign="top"');

  for ($index = 0; $index < $history_summary{$value}; $index++) {
    cell(vbar($history_stats[$base + $index]{$value},$max),'align="center" valign="bottom"');
  }
  end_row();

  begin_row("data");
  for ($index = $history_summary{$value} - 1; $index >= 0; $index--) {
    cell(sprintf("%02d",$index),'align="center"');
  }
  end_row();

  begin_row("data");
  cell("Days Ago",'align="center" colspan="' . ($history_summary{$value} + 1) . '"');
  end_row();

  end_table();
}

sub fmt {
  my $value = shift;
  my $method = shift;

  # format a large number with commas
  if ($nice_format && $method eq "comma") {
    $value = reverse($value);
    $value =~ s/(...)/$1,/g;
    $value = reverse($value);
    $value =~ s/^,//;
    return $value || 0;
  }

  # format a byte value into Mb or Kb
  if ($nice_format && $method eq "byte") {
    if ($value >= 1048576000) {
      return sprintf "%.2f Gb", $value / 1073741824;
    }
    if ($value >= 1024000) {
      return sprintf "%.2f Mb", $value / 1048576;
    }
    if ($value >= 1000) {
      return sprintf "%.1f Kb", $value / 1024;
    }
    return $value || 0;
  }

  # format a percentage
  if ($method eq "percent") {
    return sprintf "%0.2f", 100 * $value;
  }

  # format a number of seconds as hhh:mm:ss
  if ($method eq "duration") {
    my $hour = int($value / 3600);
    $value -= ($hour * 3600);
    my $minute = int($value / 60);
    $value -= ($minute * 60);
    my $second = $value;
    return sprintf "%03d:%02d:%02d", $hour, $minute, $second;
  }

  # uppercase the first letter of a word
  if ($method eq "ucfirst") { return uc(substr($value,0,1)) . substr($value,1); }

  # resolve an IP address into a hostname
  if ($method eq "dns") {
    if ($dns && $value =~ /^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$/) {
      unless ($hostcache{$value}) { $hostcache{$value} = lc(gethostbyaddr(pack("c4", split(/\./, $value)), 2)) || "[$value]"; }
      return $hostcache{$value};
    } else {
      return $value;
    }
  }

  # add a trailing slash to a directory name if one's not already there
  if ($method eq "dir") {
    if ($value) { $value =~ s/([^\/\\])$/$1\//; }
    return $value;
  }

  # couldn't figure out what to do, so return what we got
  return $value;
}

sub base_dir {
  my $dir = shift;
  return $dir =~ /^(?:[a-zA-Z]:)?[\/\\]/ ? $dir : ($opt{"basedir"} . $dir);
}

sub makelinks {
  return unless ($links);
  html_link_pre(shift);
  if ($alerts) { html_link("Alerts"); }
  if (%alert_summary) { html_link("Alert Summary"); }
  if ($totals) { html_link("Total Summary"); }
  if (%protocol_summary) { html_link("Protocol Summary"); }
  if (%protocol_host_detail) { html_link("Protocol Host Detail"); }
  if (%protocol_user_detail) { html_link("Protocol User Detail"); }
  if (%hour_summary) { html_link("Time Summary"); }
  if (%interface_summary) { html_link("Interface Summary"); }
  if (%host_summary) { html_link("Host Summary"); }
  if (%host_protocol_detail) { html_link("Host Protocol Detail"); }
  if (%host_user_detail) { html_link("Host User Detail"); }
  if (%user_summary) { html_link("User Summary"); }
  if (%user_www_summary) { html_link("User WWW Summary"); }
  if (%user_protocol_detail) { html_link("User Protocol Detail"); }
  if (%user_host_detail) { html_link("User Host Detail"); }
  if (%domain_summary) { html_link("Domain Summary"); }
  if (%mobile_summary) { html_link("Mobile Summary"); }
  if (%ooba_summary) { html_link("OOBA Summary"); }
  if (%message_summary) { html_link("Message Summary"); }
  if ($fetcher_message) { html_link("Fetcher Message"); }
  if (%history_summary) { html_link("History Summary"); }
  html_link_post();
}

# parse the configuration file
sub config {
  my $file = shift;
  my $line = 0;

  open CONFIG, $file or die "ERROR: Unable to open configuration file $file.\n";

  while (<CONFIG>) {
    my $option;
    my $arg1;
    my $arg2;
    my $arg3;

    $line++;
    next if /^#/ || /^$/;

    ($option,$arg1,$arg2,$arg3) = /^
      \s*				# whitespace at start of line
      ([a-zA-Z_]+)			# option name
      (?:\s+([^\s\"]+|(?:\"[^\"]+\")))?	# argument 1
      (?:\s+([^\s\"]+|(?:\"[^\"]+\")))?	# argument 2
      (?:\s+([^\s\"]+|(?:\"[^\"]+\")))?	# argument 3
      \s+				# whitespace at end of line
    $/x;

    unless ($option) { die "ERROR: Bad format in configuration file line $line:\n$_"; }

    $option = lc($option);

    unless ($valid_options{$option}) { die "ERROR: Unknown configuration file option on line $line:\n$_"; }

    $arg1 =~ tr/\"//d if $arg1;
    $arg2 =~ tr/\"//d if $arg2;
    $arg3 =~ tr/\"//d if $arg3;

    SWITCH: {

      if ($option =~ /^(basedir|directory|compress|history_file|mail_server|mail_from|dns|title|graphs|theme)$/) {
        $$option = $arg1;
        last SWITCH;
      }

      if ($option eq "remotelog") {
        $remotelog = $arg1;
        if ($arg2) { $remotelog_path = $arg2; }
        last SWITCH;
      }

      if ($option eq "save_logfile") {
        $save_logfile = $arg1 || ".";
        last SWITCH;
      }

      if ($option eq "uncompress") {
        $uncompress = $arg1;
        $extension = $arg2;
        last SWITCH;
      }

      if ($option eq "interface") {
        $interface{$arg1} = $arg2 || $arg1;
        last SWITCH;
      }

      if ($option eq "filter") {
        unless ($arg1 eq "any" && $arg2 eq "any") {
          push @{$filter{"source"}}, $arg1;
          push @{$filter{"destination"}}, $arg2;
        }
        last SWITCH;
      }

      if ($option eq "alias") {
        $alias{$arg1} = $arg2;
        last SWITCH;
      }

      if ($option eq "time") {
        push @{$time{$arg1}{"begin"}}, $arg2;
        push @{$time{$arg1}{"end"}}, $arg3;
        last SWITCH;
      }

      if ($option eq "duration") {
        $arg3 = lc($arg3);
        if ($arg3 eq "h") { $arg3 = 3600; }
        elsif ($arg3 eq "m") { $arg3 = 60; }
        else { $arg3 = 1; }
        $duration{$arg1} = $arg2 * $arg3;
        last SWITCH;
      }

      if ($option eq "volume") {
        $arg3 = lc($arg3);
        if ($arg3 eq "m") { $arg3 = 1048576; }
        elsif ($arg3 eq "k") { $arg3 = 1024; }
        else { $arg3 = 1; }
        $volume{$arg1} = $arg2 * $arg3;
        last SWITCH;
      }

      if ($option eq "user") {
        $user{$arg1}{$arg2} = 1;
        last SWITCH;
      }

      if ($option eq "word") {
        $word .= $arg1 . "|";
        last SWITCH;
      }

      if ($option eq "host_summary" || $option =~ /_detail$/) {
        ${$option}{$arg1}{"sort"} = $arg2 =~ /^[a-zA-Z]/ ? "+" . $arg2 : $arg2;
        ${$option}{$arg1}{"limit"} = $arg3;
        last SWITCH;
      }

      if ($option eq "history_summary") {
        $history_summary{$arg1} = $arg2;
        last SWITCH;
      }

      if ($option =~ /_summary$/) {
        ${$option}{"sort"} = $arg1 =~ /^[a-zA-Z]/ ? "+" . $arg1 : $arg1;
        ${$option}{"limit"} = $arg2;
        last SWITCH;
      }

      if ($option eq "output") {
        $output_how = $arg1;
        $output_to = $arg2;
        last SWITCH;
      }

      $$option = 1;

    }
  }

  close CONFIG;

  # set defaults
  $output_how ||= "stdout";
  $title ||= "Firewall activity report";
  $mail_server ||= "127.0.0.1";
  $mail_from ||= "Reptor";

  # fix directory paths
  $remotelog_path = fmt($remotelog_path,"dir");
  if ($save_logfile) { $save_logfile = base_dir(fmt($save_logfile,"dir")); }

  # check validity of specified options
  if ($alerts && !(%time || %duration || %volume || %user || $ratings || $word)) {
    die "ERROR: Option \"alerts\" requires at least one of:\n  \"time\", \"duration\", \"volume\", \"user\", \"ratings\", or \"word\".\n";
  }

  if ($opt{"verify"} && $show_reason && !$alerts) {
    die "WARNING: Option \"show_reason\" requires \"alerts\".\n";
  }

  if ($opt{"verify"} && $show_user && !$alerts) {
    die "WARNING: Option \"show_user\" requires \"alerts\".\n";
  }

  if ($opt{"verify"} && $show_auth && !$alerts) {
    die "WARNING: Option \"show_auth\" requires \"alerts\".\n";
  }

  if ($opt{"verify"} && $show_rule && !$alerts) {
    die "WARNING: Option \"show_rule\" requires \"alerts\".\n";
  }

  if ($opt{"verify"} && $show_id && !$alerts) {
    die "WARNING: Option \"show_id\" requires \"alerts\".\n";
  }

  if ($opt{"verify"} && $show_op && !$alerts) {
    die "WARNING: Option \"show_op\" requires \"alerts\".\n";
  }

  if ($opt{"verify"} && $show_result && !$alerts) {
    die "WARNING: Option \"show_result\" requires \"alerts\".\n";
  }

  if ($opt{"verify"} && $show_arg && !$alerts) {
    die "WARNING: Option \"show_arg\" requires \"alerts\".\n";
  }

  if ($opt{"verify"} && $all && !$alerts) {
    die "WARNING: Option \"all\" requires \"alerts\".\n";
  }

  if ($opt{"verify"} && %time && !$alerts) {
    die "WARNING: Option \"time\" requires \"alerts\".\n";
  }

  if ($opt{"verify"} && %duration && !$alerts) {
    die "WARNING: Option \"duration\" requires \"alerts\".\n";
  }

  if ($opt{"verify"} && %volume && !$alerts) {
    die "WARNING: Option \"volume\" requires \"alerts\".\n";
  }

  if ($opt{"verify"} && %user && !$alerts) {
    die "WARNING: Option \"user\" requires \"alerts\".\n";
  }

  if ($opt{"verify"} && $word && !$alerts) {
    die "WARNING: Option \"word\" requires \"alerts\".\n";
  }

  if ($word && !($search_source || $search_destination || $search_arg)) {
    die "ERROR: Option \"word\" requires at least one of:\n \"search_source\", \"search_destination\", or \"search_arg\".\n";
  }

  if ($opt{"verify"} && $search_source && !$word) {
    die "WARNING: Option \"search_source\" requires \"word\".\n";
  }

  if ($opt{"verify"} && $search_destination && !$word) {
    die "WARNING: Option \"search_destination\" requires \"word\".\n";
  }

  if ($opt{"verify"} && $search_arg && !$word) {
    die "WARNING: Option \"search_arg\" requires \"word\".\n";
  }

  if (%alert_summary && !$alerts) {
    die "ERROR: Option \"alert_summary\" requires \"alerts\".\n";
  }

  if ($output_how !~ /^(file|ftp|mail|stdout)$/) {
    die "ERROR: Option \"output\" must be one of \"stdout\", \"file\", \"ftp\", or \"mail\".\n";
  }

  if (!$totals && !$alerts && !%alert_summary && !%protocol_summary && !%host_summary && !%user_summary && !%user_www_summary && !%hour_summary && !%domain_summary && !%mobile_summary && !%ooba_summary && !%message_summary && !%history_summary && !%host_protocol_detail && !%host_user_detail && !%protocol_host_detail && !%protocol_user_detail && !%user_protocol_detail && !%user_host_detail && !$fetcher_message) {
    die "ERROR: Selected options will produce no output.\n";
  }

  if ($opt{"verify"} && $remotelog && (! -x $remotelog_path . ($^O eq "MSWin32" ? "remotelogfile.exe" : "remotelogfile"))) { 
    die "ERROR: remotelogfile program not found.\n";
  }

  if ($opt{"verify"} && $save_logfile && !$remotelog) {
    die "WARNING: It doesn't make much sense to use save_logfile without using remotelog.\n";
  }

  if ($save_logfile && ! -d $save_logfile) {
    die "Directory specified by save_logfile does not exist.\n";
  }

  if ($opt{"verify"} && $compress && !$save_logfile) {
    die "WARNING: It doesn't make much sense to use compress without using save_logfile.\n";
  }

  if ($uncompress && ! -x (split(/ /,$uncompress))[0]) {
    die "ERROR: Unable to run uncompression program.\n";
  }

  if ($opt{"verify"} && %history_summary && !$history_file) {
    die "WARNING: Must specify history_file with history_summary.\n";
  }

  if ($opt{"verify"} && %history_summary && !$graphs) {
    die "WARNING: Must specify graphs with history_summary.\n";
  }

  if ($opt{"verify"}) { print "Configuration file parsed with no errors.\n"; }

  # make a proper regular expression
  if ($word) {
    chop($word);
    $word = "($word)";
  }

  # set number of columns in alert section
  if ($alerts) {
    $columns = 8 + (2 * $show_interface) + $show_reason +
      $show_user + $show_auth + $show_rule + $show_id +
      $show_op + $show_result + $show_arg;
  }

}

sub version {
  print << "EOF";

Reptor version $reptor_version, Copyright (C) 1999,2000,2001 Alex Howansky
Visit the Reptor home page at http://www.wankwood.com/reptor/

EOF
}

sub usage {
  version();
  print << "EOF";

Reptor is distributed under the GNU GPL and provided without
any warranty. For additional information, refer to the file
"COPYING" that has been supplied with this distribution.

Usage: reptor.pl [options]

Options:

  --basedir <directory>
    Reptor installation directory.
    Default: <current directory>
    Note: Overrides "basedir" configuration file option.

  --config <file>
    Name of configuration file.
    Default: <base directory>/reptor.cfg

  --date <date>
    Date of logfile to process.
    Default: $yesterday
    Note: v4.0 format is YYMMDD, v5.0 and later format is YYYYMMDD.

  --dir <directory>
    Directory where logfiles are located.
    Default: $directory
    Note: Overrides "directory" configuration file option.

  --help

  --history
    Always update the history file, regardless of the date of the logfile
    being processed.

  --ignore
    The log to process has been cut from a larger file or merged from multiple
    files, so don't look for the datestamp on the first line, or ignore it if
    it's there.

  --log <file>
    Name of logfile to process.
    Default: ${directory}logfile.$yesterday
    Note: Overrides --date and --dir command line options, and "directory"
          configuration file option.
    Note: Use "--log -" to indicate STDIN input.

  --verify
    Verify validity of configuration file, readability of logfile, and exit
    without processing.

  --version
    Print version number and exit.

EOF
  exit;
}
