#!/usr/bin/perl
#
# $Id: nwatch,v 1.7 2001/10/09 03:18:13 levine Exp $
#
# Copyright (C) 2001  James D. Levine (jdl@vinecorp.com)
#
#
#   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; if not, write to the Free Software
#   Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 
#   02111-1307, USA.
#
####################################################################


use strict;


use Net::Pcap;
use Getopt::Long;

use NWatch::Packet;
use NWatch::EthernetPacket;
use NWatch::IPv4Packet;
use NWatch::TCPPacket;
use NWatch::UDPPacket;

use NWatch::RBPH;
use NWatch::IPStatefulPH;

use PortScan::ScannedHost;
use PortScan::ScanSet;
use PortScan::DataStore;
use PortScan::IPAddress;


package main;
 


sub usage
{
    print <<DONE;

 nwatch [-d|-device <device-name> ]
        [-o|-output <filename-or-:tag>] [-p|-ports <port list>] 
        [-fi|-flush-interval <seconds>] [-si|-sample-interval <seconds>]
        [-h|-host <specification> ... -h|-host <specification N>]


      <specification> = [!]<host spec>[:<port spec>]

DONE
    ;
    exit 1;
}

my $pspecs = "";
my @hspecs = ();
my $output_tag =   "%F-observed-%D%m%Y";
my $help = 0;
my $flush_interval = 300;	# flush every 5 minutes
my $sample_interval = 24 * 3600; # new sample each day
my $dev = "";

$help = 1 if $#ARGV == -1;

GetOptions(
	   "p|ports=s"    => \$pspecs,
	   "h|host=s@"    => \@hspecs,
	   "o|output=s"   => \$output_tag,
	   "fi|flush-interval=i" => \$flush_interval,
	   "si|sample-interval=i" => \$sample_interval,
	   "d|device=s"   => \$dev,
	   "help|?"         => \$help,
	   );


usage() if $help;

my $accept_any_host = 1;

# accept any host only if now hosts specified on the command line
$accept_any_host = 0 if( $#hspecs > -1 );

# initialize the scanset first time, new_scanset controls $processed_output_tag 

my( $actual_scanset, $processed_output_tag ) = new_scanset( $output_tag, \@hspecs, $pspecs );


# get pcap device handle for the interface

my $err;
$dev = Net::Pcap::lookupdev( \$err ) if !length( $dev );

if( defined $err )
{
    die "can't find a device";
}

my $dev_desc = Net::Pcap::open_live(
				    $dev,
				    2000, # snaplen
				    1, # promisc
				    2, # timeout
				    \$err
				    );

if( !( defined $dev_desc ) )
{
    die "can't open dev $dev";
}

my $sigint_raised = 0;
my $sigalrm_raised = 0;
my $sighup_raised = 0;
#my $somesignal_raised = 0;

# now set up sigint handler

$SIG{ INT } = sub { $sigint_raised = 1; };
$SIG{ ALRM } = sub{ $sigalrm_raised = 1; };
$SIG{ HUP } = sub{ $sighup_raised = 1; };


my $timer_grain = 4;
alarm $timer_grain;

my $results_last_flushed = time;
my $sample_interval_start = time;


my $template_scanset = &PortScan::IPAddress::make_scanset( \@hspecs, $pspecs, "unknown" );
#my $handler = new NWatch::RBPH( $template_scanset, $accept_any_host );
my $handler = new NWatch::IPStatefulPH( $template_scanset, $accept_any_host );

# associate the scanset with the handler
$handler->output_scanset( $actual_scanset ); 


while( 1 )
{
    my %hdr;
    my $pak = Net::Pcap::next( $dev_desc, \%hdr );

    &die_gracefully( $processed_output_tag, $actual_scanset, $handler ) 
	if ( $sigint_raised == 1 );

    do
	{
#	    print "****************************************** handling sigalrm...\n";

	    $sigalrm_raised = 0;
	    if( ( time - $sample_interval_start ) >= $sample_interval )
	    {
		&flush_results( $processed_output_tag, $actual_scanset, $handler );
		$results_last_flushed = time;
		( $actual_scanset, $processed_output_tag )
		    = &new_scanset( $output_tag, \@hspecs, $pspecs );

		$handler->output_scanset( $actual_scanset ); 
		$sample_interval_start = time;
	    }

            # don't need to flush if reset the sample scanset
	    elsif( ( time - $results_last_flushed ) >= $flush_interval )
	    {
		&flush_results( $processed_output_tag, $actual_scanset, $handler );
		$results_last_flushed = time;
	    }


	    alarm $timer_grain;
	}
    if  ( $sigalrm_raised == 1 );



    ( &flush_results( $processed_output_tag, $actual_scanset, $handler ), $sighup_raised = 0 ) if $sighup_raised == 1;

    if( defined $pak ) 
    {
	my $epak = new NWatch::ethernet_packet( $pak );

#	print "Main loop: epak is $epak \n";
#	print "Main loop: epak full is " . $epak->full_protocol_name() . "\n";


	my @log_specs = (
			 [
			  "ethernet:ipv4", "IP: %s -> %s \n", [ "ipv4:source_address_text",
								"ipv4:destination_address_text" ]
			  ],

			 );
	#&eval_log( $epak, \@log_specs );
 
	$handler->handle_packet( $epak, $actual_scanset, $accept_any_host );
    }

    # print "looping...\n";
}

exit 0;


sub die_gracefully
{
    my( $output_tag, $scanset, $packet_handler ) = @_;

    my $date = ` date "+%D %H:%M:%S" `; chomp $date;

    print "$date trying to exit gracefully...\n";

    &flush_results( $output_tag, $scanset, $packet_handler );

    exit 0;
}

sub flush_results
{
    my( $output_tag, $scanset, $packet_handler ) = @_;

    my( $processed_output_tag, $output )
	= PortScan::DataStore::data_store_for( $output_tag );

    $packet_handler->finish_interval;
    my $date = ` date "+%D %H:%M:%S" `; chomp $date;

    print "$date flushing data...\n";

    $output->put_scanset( $scanset );
}


sub new_scanset
{
    my( $output_tag, $hspecs, $pspecs ) = @_;

    my $date = ` date "+%D %H:%M:%S" `; chomp $date;
    print "$date initializing sample\n";

    my( $processed_output_tag, $output )
	= PortScan::DataStore::data_store_for( $output_tag );

    my $scanset = new PortScan::ScanSet;
    $scanset->tag( $processed_output_tag );

#    print "done resetting scanset...\n";
    ( $scanset, $processed_output_tag );
}



sub eval_log
{
    my( $packet, $specs ) = @_;

    # specs is a listref of tuples [ packet type, sprintf string, [ field keys ] ]
    foreach my $spec ( @$specs )
    {
	my( $type, $pattern, $keys ) = @$spec;

	if( $packet->proto_isa( $type ) )
	{
	    my $string = "";
	    my $sprintf_expr = "\$string = sprintf \"$pattern\"";
	    foreach my $key ( @$keys )
	    {
		$sprintf_expr .= ", \$packet->field_path( \"" . $key . "\" )";
	    }
	    $sprintf_expr .= ";";

	    eval $sprintf_expr;

	    print $string;

#	    print "sprintf_expr is $sprintf_expr \n";
#	    print "evaled: $string .\n";
	}

    }
}


=head1 NAME

nwatch - watch an interface for TCP/IP traffic, storing the results in an
         nmap- and NDiff-compatible format.


=head1 SYNOPSIS

 nwatch [-o|-output <filename-or-:tag>] [-p|-ports <port list>] 
        [-fi|-flush-interval <seconds>] [-si|-sample-interval <seconds>]
        [-h|-host <specification> ... -h|-host <specification N>]
        [-d|-device <device-name> ]

      <specification> = 
            [!]<host spec>[:<port spec>]



=head1 DESCRIPTION

NWatch is a sniffer but can be conceptualized as a "passive port scanner",
in that it is only interested in IP traffic and it organizes results as
a port scanner would.  This adds the benefit that any tool which operates
on such output (NDiff) can use the data.  NWatch differs from an actual
port scanner in many ways.  For example, it will catch ports that are
opened only transiently, something which a port scanner would likely miss.
For network security NWatch is an excellent complement to regular 
port-scanning of your networks.

By default NWatch stays active indefinitely until it receives a SIGINT (CTRL-c).
During that time it watches the default interface (eth0), tracking
each IP host/port combination it discovers.  The set of "interesting" hosts may
be limited by supplying "host specs" described below; otherwise all traffic
is tracked.  The latter case would typically be useful for spying or perhaps
sampling and analysis of net usage patterns rather than security monitoring.

The flush and sample intervals may be specified on the command line.  The
flush interval is the interval at which the tracked information will be written
to disk.  Flushing can also be triggered by sending the nwatch process a 
SIGHUP, or cancelling execution with SIGINT.  SIGHUP will not interrupt
execution of NWatch.

The sample interval is the length of time information is accumulated from the
interface.   Upon expiration of the interval, the data is flushed to disk,
cleared, and sampling begins anew with a clean slate.  This is useful if you
want to store hourly, daily, etc. samples separately on disk.

Meaningful use of the sample interval requires a naming convention for the
samples, such that the name will change each time a new sample is created.
For example, with a daily sample, one would desire the date be embedded in
the sample name.  In NWatch, this is achieved by supplying an output
string (B<-o> option) containing %-style substitutions, as described in SUBSTITUTIONS
below.

NWatch must have access to the watched interface; typically this means root.

NWatch requires NDiff, libpcap and the perl Net::Pcap module.  See the documentation
in the NWatch distribution for details.


=head1 OPTIONS

=over 4

=item B<-d> <device-name>

=item -device <device-name>

Specifies the device to try to open.  If not specified, nwatch (libpcap) will choose an
interface.


=item B<-o> <filename-or-:tag>

=item -output <filename-or-:tag>

Specifies the output filename, or optionally a data store
tag, if begins with a colon (:).  See L<"DATA STORES"> below
for more information.  The default is "%F-observed-%D%m%Y" which
evaluates to a string containing the hostname and date.


=item -h [!]<host ranges>[:<port ranges>]

=item -host  [!]<host ranges>[:<port ranges>]


Adds a host or range of interesting hosts.  NWatch will store information
only for these hosts, ignoring all other traffic.  For example-

    -h 192.168.2.2                   # one host
    -h 10.0.2.0-64                   # 65 hosts
    -host 192.168.1.0/26             # 64 hosts
    -host 192.168.*.*                # 65536 hosts

Port ranges are currently unsupported but the following discussion is
included since the functionality will be added in an upcoming release.
  
The above examples add hosts with all ports in a closed state.  To
restrict to a specific set of ports for the host, append a colon and a
port spec.  For example to add localhost with tcp ports 80 and 53.

    -host 127.0.0.1:80,53

Flags may be appended to change the protocol or state for a given port, for example:

    -host 127.0.0.1:7u

which adds echo service, udp port 7.


The full list of port flags are as follows:


    t - tcp port  (default)
    u - udp port

NWatch defaults all ports to the "unkown" psuedo-state.  

A host spec is treated as a negation if it starts with "!".  If ports are specified
as part of the host spec, those ports are deleted from any hosts previously added
which fall in the host range.  

Host specs are applied in order as they appear on the command line, and their
effects are cumulative.


=item [-fi|-flush-interval <seconds>] 

Sets the flush interval to <seconds>.  Default is 300 (5 minutes).  


=item [-si|-sample-interval <seconds>]

Sets the sampling interval to <seconds>  Default is 3600 * 24 (1 day).


=head1 DATA STORES

NWatch uses NDiff's data storage facilities, which can manipulate
results in regular nmap-format files, or can instead can handle
storing and organizing the data on behalf of the
user through a user-configurable "data store".

Whenever you precede a results tag with a colon (:), the tag will be
treated as a unique key into a data store, identifying the results
set.

Currently the only supported data store is nmap format files placed in
a preconfigured directory.  Other types may be added at a later date.

A legal tag may contain any alphanumeric string, plus dash, underscore, and dot.
%-style substitutions in the ilk of the "date" command are also supported,
allowing a tag to contain date, time, or the local hostname.  See L<"SUBSTITUTIONS">
below for more information.

=head1 SUBSTITUTIONS

When you specify an output filename or tag with NWatch's B<-o> switch, you
may embed %-style substitutions, which will be interpreted and replaced
in the string.  


=over 4

=item %H = hour

=item %M = minute

=item %S = second 

=item %D = day of month

=item %m = month of year (01-12)

=item %Y = year, four digits

=item %j = day of year, three digits

=item %w = day of week (0-6) one digit

=back

Except where noted, the above items are two digits, and local time.  All are zero-padded
as appropriate.


In addtion-

=over 4

=item %F = output of "hostname" on the local machine

For example, the default output string is "%F-observed-%D%m%Y" - if the hostname
is "pow" and the date is 12 April, 2000, the result would be "pow-observed-12042001".


=back


=head1 BUGS

Presently port specifications from the command line are ignored.  Only
host specifications are used to limit what traffic is tracked.

The Pcap timeout facility is apparently broken, at least on linux.
The result is that execution blocks until a packet is received.
Therefore all signals and timed events trigger only after a packet
has arrived.  In particular, breaking with CTRL-C, the flush- and
sample-interval, SIGHUP are affected.  

No support for human-readable hostnames and portnames.

The state machines design is still evolving - nwatch can
be fooled by deliberate spoofing as well as by certain
specific everyday occurances.

State machines are not garbage-collected so the nwatch process can
grow rather large over time on a busy, varied network.

The model for detecting filtered TCP and closed UDP ports is still
rather simplistic.  In addition, such ports will not be detected and
flushed until NWatch exits.



=head1 AUTHOR

James Levine <jdl@vinecorp.com>

=cut





