#!/usr/bin/perl
#
# -------------
# arpwatch2html 
#   version 0.9
#   dated 20041102
# -------------
#
# A program to sort and filter the output of arpwatch
# and produce the result as an html file.
#
# Copyright (C) 2004 - Wim Mees
#
# This program is copyrighted software. 
# It must be purchased if it is to be used in commercial endeavors, 
# but is free for non-commercial use.

use POSIX 'strftime';

# ----------------------------------------
# - initialization
# ----------------------------------------

# default values
$nameDatFile = '/var/lib/arpwatch/arp.dat';
$nameMsgFile = '/var/log/messages';
$sortBy = 'ip';
$columns = 'tmin';

# parse the options
use Getopt::Long;
GetOptions("datfile=s"=>\$nameDatFile,
            "msgfile=s"=>\$nameMsgFile,
            "outfile=s"=>\$nameOutFile,
            "sort=s"=>\$sortBy,
            "columns=s"=>\$columns,
            "pruneold=i"=>\$pruneOld,
            "header=s"=>\$nameHeaderFile,
            "footer=s"=>\$nameFooterFile,
            "css=s"=>\$cssReference,
            "help"=>\$showUsage,
            "usage"=>\$showUsage
          );

print "WARNING: unprocessed options:\n" if $ARGV[0];
foreach (@ARGV) {
  print "$_\n";
}

if ($showUsage)
{
  print "usage: arpwatch2html.pl [options]
options:
  -datfile=filename   location of arpwatch data         (default: /var/lib/arpwatch/arp.dat)
  -msgfile=filename   location of arpwatch messages     (default: /var/log/messages)
  -outfile=filename   the output html file            (default: stdout)
  -sort=[mac|ip|time|name] sort entries by given key  (default: ip)
  -columns=[mitn]     columns to show and their order (default: tmin)
  -pruneold=days      remove arpwatch older than \"days\"
  -header=filename    file with an HTML header   
  -footer=filename    file with an HTML footer
  -css=url            reference to a user define css file
  -help or -usage     shows this message
";  
  exit;
}

# the default html headers and footer
$htmlHeader = "<html>\

<head>
  <title>arpwatch2html</title>
  <style type=text/css>
  <!--
    body  { color: #00F; background-color: #EEF; font-family: arial; font-size: 10pt; }
    table { font-family: sans-serif; font-size: 10pt; border: solid; border-width: 1px; }
    td    { border: solid; border-width: 1px; padding-left: 5px; padding-right: 5px; }
  -->
  </style> 

</head>

<body>\n\n";
$htmlHeaderWithExternalCss = "<html>\

<head>

  <title>arpwatch2html</title>

  <link rel=stylesheet type=\"text/css\" HREF=\"$cssReference\">

</head>

<body>\n\n";
$htmlFooter = "
  <hr>

  <center>
    <a href=\"http://sisms.no-ip.com/software/arpwatch2html/\">arpwatch2html</a>
  </center>

</body>
</html>\n";

# read any html header and/or footer files
if ($nameHeaderFile)
{
  open(idHeaderFile, "< $nameHeaderFile")
    or die "ERROR: could not open $nameHeaderFile for reading: $!\n";
  @userHtmlHeader = <idHeaderFile>;
  close(idHeaderFile);
}

if ($nameFooterFile)
{
  open(idFooterFile, "< $nameFooterFile")
    or die "ERROR: could not open $nameFooterFile for reading: $!\n";
  @userHtmlFooter = <idFooterFile>;
  close(idFooterFile);
}

# open the output file
if ($nameOutFile)
{
  open (STDOUT, "> $nameOutFile")
    or die "ERROR: could not open $nameOutFile for writing: $!\n";
}

# produce an html header
if ($nameHeaderFile)
{ print @userHtmlHeader; }
elsif ($cssReference)
{ print $htmlHeaderWithExternalCss; }
else
{ print $htmlHeader; }

# ----------------------------------------
# - generate a table from the arpwatch data file
# ----------------------------------------

# read the arp.dat file 
open(idDatFile, "< $nameDatFile")
  or die "ERROR: could not open $nameDatFile for reading: $!\n";
@lines = <idDatFile>;
close(idDatFile);

# prune entries that are too old
if ($pruneOld)
{
  $timeOldestToKeep = time() - $pruneOld * 86400;

  for ( $i=0 ; $i<=$#lines ; $i++ )
  {
    $line = $lines[$i];
    @words = split(/\t/, $line);
    $timeLastSeen = $words[2];

    if ($timeLastSeen < $timeOldestToKeep)
    {
      splice(@lines, $i, 1);      
      $i--;
    }
  }
}

# sort the entries
if ($sortBy)
{
  caseSort:
  {
    ($sortBy eq 'mac') 
    && do
      {
        @sortedLines = sort { getMacAsInt($a) <=> getMacAsInt($b) } @lines;
        @lines = @sortedLines;
        last caseSort;
      };

    ($sortBy eq 'ip') 
    && do
      {
        @sortedLines = sort { getIpAsInt($a) <=> getIpAsInt($b) } @lines;
        @lines = @sortedLines;
        last caseSort;
      };

    ($sortBy eq 'time') 
    && do
      {
        @sortedLines = sort { getTimeAsInt($a) <=> getTimeAsInt($b) } @lines;
        @lines = @sortedLines;
        last caseSort;
      };

    ($sortBy eq 'name') 
    && do
      {
        @sortedLines = sort { getNameAsString($a) cmp getNameAsString($b) } @lines;
        @lines = @sortedLines;
        last caseSort;
      };

    die "ERROR: unknown sort key $sortBy\n";
  }
}

# print a title with the date
$now = localtime time;
print  "<h2>Situation at $now</h2>\n\n";

# define a table for the arpwatch data
print  "<center>
  <table>
    <tr>\n";

# print a header for the table
@columnsList = split //, $columns;
for ( $i=0 ; $i<=$#columnsList ; $i++ )
{
  caseColumnsHeader:
  {
    ($columnsList[$i] eq 'm') 
    && do
      {
        $label = "MAC address";
        last caseColumnsHeader;
      };

    ($columnsList[$i] eq 'i') 
    && do
      {
        $label = "IP address";
        last caseColumnsHeader;
      };

    ($columnsList[$i] eq 't') 
    && do
      {
        $label = "last seen";
        last caseColumnsHeader;
      };

    ($columnsList[$i] eq 'n') 
    && do
      {
        $label = "hostname";
        last caseColumnsHeader;
      };

    die "ERROR: unknown column $columnsList[$i]\n";
  }

  print "      <th>$label</th>\n";

}
print "    </tr>\n";

# print the data for the table
foreach $line (@lines)
{
  chop($line);
  @words = split(/\t/, $line);
  $macAddress   = $words[0];
  $ipAddress    = $words[1];
  $timeLastSeen = strftime("%Y%m%d %H:%M:%S", localtime($words[2]));
  $hostName     = $words[3];
  print "    <tr>\n";

for ( $i=0 ; $i<=$#columnsList ; $i++ )
{
  caseColumnsData:
  {
    ($columnsList[$i] eq 'm') 
    && do
      {
        $value = $macAddress;
        last caseColumnsData;
      };

    ($columnsList[$i] eq 'i') 
    && do
      {
        $value = $ipAddress;
        last caseColumnsData;
      };

    ($columnsList[$i] eq 't') 
    && do
      {
        $value = $timeLastSeen;
        last caseColumnsData;
      };

    ($columnsList[$i] eq 'n') 
    && do
      {
        $value = $hostName;
        last caseColumnsData;
      };

    die "ERROR: unknown column $columnsList[$i]\n";
  }

  print "      <td>$value</td>\n";
}

  print "    </tr>\n";
}

# end the table
print  "  </table>
</center>\n\n";

# ----------------------------------------
# - show recent arpwatch entries from the messages files
# ----------------------------------------

# show recent arpwatch entries from the messages file
print  "<h2>Recent history</h2>\n";
open (id, "/usr/bin/tail -n 1000 $nameMsgFile | /bin/grep arpwatch |");
@lines = <id>;
close(id);
foreach $line (@lines)
{
  print  "$line<br>\n";
}

# ----------------------------------------
# - the end
# ----------------------------------------

# produce an html footer
if ($nameFooterFile)
{ print @userHtmlFooter; }
else
{ print $htmlFooter; }

# close the output file
close(STDOUT);

# the end
exit;

# ----------------------------------------
# - subroutines
# ----------------------------------------

# subroutines to extract individual fields from an arpwatch entry
# (and convert them to a specific type)
sub getMacAsInt
{
  my @field = split(/\t/, $_[0]);
  my @value = split(/:/, $field[0]);
  return (((((hex($value[0]) * 256) + hex($value[1])) * 256 + hex($value[2])) * 256 + hex($value[3])) * 256 + hex($value[4])) * 256 + hex($value[5]);
}
sub getIpAsInt
{
  my @field = split(/\t/, $_[0]);
  my @value = split(/\./, $field[1]);
  return ((($value[0] * 256) + $value[1]) * 256 + $value[2]) * 256 + $value[3];
}
sub getTimeAsInt
{
  my @field = split(/\t/, $_[0]);
  return $field[2];
}
sub getNameAsString
{
  my @field = split(/\t/, $_[0]);
  return $field[3];
}
