#! /usr/local/bin/perl -w
#
# $Id: tripwire.pl,v 1.125 2002/02/16 15:20:18 coelho Exp $
#
# (c) Fabien Coelho <fabien@coelho.net> 2001-2002
#
# See `tripwire.pl --man' for extensive help about this script.
# See `http://www.tripwire.com/' for the real stuff.
#
# License: GNU - GPL, see `http://www.gnu.org/' for details.
#
################################################# POD - PLAIN OLD DOCUMENTATION

=head1 NAME

B<tripwire.pl> - intrusion detection tool.

=head1 SYNOPSIS

B<tripwire.pl> [options as B<--help> and B<--man>] [arguments]

=head1 DESCRIPTION

A simple tripwire-like perl script.
This program can help check the integrity of a file system, so as
to detect if an intruder broke into your system and installed
a root kit, a troyan sniffer or back door...
This can help improve the security of your system.

Metadata about files, directories, links are stored in a database
and can be checked later for differences. They can be updated.
The integrity of the database itself can be checked with the help of a
pass phrase.

=head1 OPTIONS

Options deal with information about the script, its verbosity while
running, the database to use, the operations to be performed, restrictions
about what is put in the database, comparisons ajustements and finally 
security. Most options have one letter shortcuts. 

=head2 INFORMATION OPTIONS

If you need help, consider these.

=over 4

=item B<--help|h|?>

Show help and quit.

=item B<--manual|man|m>

Show extensive help and quit.

=item B<--version>

Show version number and quit.

=back

=head2 VERBOSITY OPTIONS

How verbose the script should be.

=over 4

=item B<--verbose|v>

Be verbose, especially by displaying details about files.

=item B<--debug|g>

Be even more verbose. You do not want that.

=back


=head2 BASE OPTION

There is a database to specify.

=over 4

=item B<--base=db_file_name>

Mandatory database file name for all operations.

=back

=head2 OPERATION OPTIONS

One of these must be chosen.

=over 4

=item B<--create|new|n>

Create a new database.

=item B<--append|a>

Append to an existing database. This option may also be used to update a
database, wrt prune and excluded entries.

=item B<--check|c>

Check reference versus saved database.

=item B<--auto>

Check saved database versus reference (might be faster but cannot
detect added files).

=item B<--update|u>

Ask whether to update differences, with options B<--check> and B<--auto>.
If none of these operations is provided, option B<--check> is assumed.

=over 4

=item B<--force>

Force default answer to interactive questions with update.

=item B<--silent>

Do not bip when asking questions.

=item B<--default=y|n>

Use this as the default answer to questions. Default is 'y'.

=back

=item B<--list|ls|l>

List files stored in database.
Consider B<--verbose> option for more informations.

=item B<--status|s>

State of options in database.

=item B<--report|r>

Create a report base of the differences, which can be later used
to update the database.

=over 4

=item B<--report-base|rb=base>

Base which should store the report. A default is provided.

=item B<--report-file|rf=file>

Text file to be append a summary report. No default.

=item B<--report-prefix|rp=sg>

Key-prefix to store data in the report base. Use hostname + base?!

=back

=back

=head2 RESTRICTION OPTIONS

The options can narrow to particular files the database content.

=over 4

=item B<--device=major.minor>

Stay on this device number.
May be specified more than once.
Only available with create.

=item B<--local>

Stay on same devices as initial directories and files.
Thus mount points are not followed unless on same devices.

=item B<--exclude=file>

Colon-separated excluded files.
Note that excluded directories are not pruned.
Set B<--prune> option to prune.

=item B<--prune=dir>

Colon-separated pruned directories. The directory itself is checked
for integrity, but its content is not scanned.

=item B<--owner=uid>

Restrict to files owned by this uid. Only with create.

=item B<--system-owner>

Restrict to files owned by system-related uids (0 .. 100).

=item B<--not-owner=uid>

Restrict to files not owned by this uid. Only with create.

=item B<--mode=mask>

Restrict to this mode mask in octal. Only with create.

=item B<--setuid>

Shortcut for B<--mode=7000>. 
Whether to consider only setuid/gid/special files or directories.

=item B<--type=T>

Restrict to this type of file. Only with create.
T may be: directory, file, link...

=over 4

=item B<--directory>

Shortcut for B<--type=directory>

=item B<--file>

Shortcut for B<--type=file>

=item B<--link>

Shortcut for B<--type=link>

=back

=back

=head2 COMPARISON OPTIONS

These options help avoid false alarms by ignoring some fields when
comparing files or directories.

=over 4

=item B<--no-date>

Do not use time data when comparing anything.

=item B<--no-content>

Do not use size and digest data when comparing anything.

=item B<--no-dir-date>

Do not use time data when comparing directories.

=item B<--no-dir-content>

Do not use content size and digest when comparing directories.

=back

=head2 SECURITY OPTIONS

Chose digest algorithm, plus additionnal pass phrase for secured
integrity checking.

=over 4

=item B<--digest=md2|md5|sha1>

Use this digest algorithm, default is sha1. Only available with create.
The algorithm is used both for checking the integrity of files and of the
database itself.

=over 4

=item B<--md2>

Shortcut for B<--digest=md2>

=item B<--md5>

Shortcut for B<--digest=md5>

=item B<--sha1>

Shortcut for B<--digest=sha1>

=back

=item B<--security|x[=pass]>

Sign/verify DB file integrity with a pass phrase.
If not specified, the integrity may be neither checked nor updated.
The pass phrase is by priority:

=over 4

=item 1

Taken from the security option value.

=item 2

Taken from environment variable C<TRIPWIRE_PASSPHRASE>.

=item 3

Prompted for interactively.

=back

=item B<--secured-digest|sd>

Digests of files are also secured with the pass phrase.
This option implies option B<--security> to provide the pass phrase.
Only available with B<--create>.

=back

=head1 ARGUMENTS

Files or directories to consider for create, append, check and auto.
When directories are provided, the files are checked recursively from
these, unless pruned.

=head1 EXAMPLES

Typical uses of this script are as follows

To create a integrity-checked base for configuration files and directories:

C<tripwire.pl --base=/tmp/etc.db --security --create /etc>

To create a non integrity-checked base for all setuid root files:

C<tripwire.pl --base=/tmp/setuid.db --setuid --owner=0 --file --create />

To list the content of a base:

C<tripwire.pl --base=/tmp/setuid.db --list>

To check for the status of files:

C<tripwire.pl --base=/tmp/etc.db --security --check>

To update the content of the base (consider B<--force> if many and ok):

C<tripwire.pl --base=/tmp/etc.db --security --check --update --verbose>

=head1 EXAMPLE SCRIPT

You should wish to check for the integrity of your file system,
especially system, configurations, executables and setuid files.
A typical use of this program would be to devise a shell script
to check for the integrity of the files, that could look like:

 #! /bin/sh
 # example tripwire script
 
 dbdir=/var/tripwire
 config=$dbdir/config.db
 setuid=$dbdir/setuid.db
 trip=/usr/local/bin/tripwire.pl
 
 echo -n 'enter passphrase: '
 read TRIPWIRE_PASSPHRASE
 export TRIPWIRE_PASSPHRASE
 
 case $1 in
  create)
   $trip --base=$setuid --security --secured-digest --setuid --file \
     --local --"$@" / /usr
   $trip --base=$config --security --secured-digest \
     --local --"$@" /etc /boot /bin /sbin /usr
   ;;
  check|auto|update)
   $trip --base=$setuid --security --"$@"
   $trip --base=$config --security --no-date --no-dir-digest --"$@"
   ;;
  list|status)
   $trip --base=$setuid --security --"$@"
   $trip --base=$config --security --"$@"
   ;;
  *)
   echo "use: $0 (create|check|auto|update|list|report|status) [tripwire-options]";;
 esac

=head1 IN CASE OF INTRUSION

Well, it may happen that this script detects some unexpected changes
that lead you to think you have been hacked. It may also happen that
although you have been hacked, the script did not detect so, for instance
if the intruder did not modify the checked files, or was able to update
an unsecured base to make it look okay.

So, you have been hacked. Some general advices to follow:

=over 4

=item 1

Disconnect your machine from the network.

=item 2

Backup the machine for later investigation.
Ok, this may require to connect the machine to the network.

=item 3

Restore or re-install your machine, in an un-hacked state the better.

=item 4

Investigate how the intruder came in.
The list of modified files may help.
Having a look at log files as well.
Consider security-checking scripts such as B<tiger> or the like.

=item 5

Fix the configuration so that the intruder cannot come back the same way.
Also apply new security patches if any, that should not hurt.

=item 6

Think of all the machines the intruder may have also hacked on your local
network: stations, servers, dumb PCs, laptops, switches, routers,
printers, palms, smart-cards, fridges...

Think of remote machines too: VPN, user .rhosts, any sniffed password...

Close your eyes. Say "it can't be". Open you eyes. Sorry, it may be;-)

=item 7

Finally ignore the previous item so as to keep yourself sane: it definitely
cannot be that bad. Reconnect your machine to the network.

=item 8

Resume normal work.
Smile, as everything must be okay;-)

=back

=head1 KEYWORDS

tripwire-like free open source system administration integrity security
check intrusion detection perl script un*x tool software hacker pirate.

=head1 SEE ALSO

C<http://www.tripwire.com/>, C<http://www.tripwire.org/> or
C<http://sourceforge.net/projects/tripwire/>
for a (better) full feature stuff!
The real package is 1.5MB tar-gziped source, versus expanded 34KB
for this very simple script.
If you are really paranoid, you can check this script;-)
You can also have this script on a floopy (well, it needs perl
and some modules, which might not fit on a floopy).
Obviously you do not have the same configurability and features here.

=head1 LICENSE

THIS PROGRAM IS DISTRIBUTED AS-IS, UNDER THE TERMS OF THE GNU GPL.
(GNU - GENERAL PUBLIC LICENSE)

A BRIEF SUMMARY OF THE GPL (see C<http://www.gnu.org/> for details):

THERE IS NO WARRANTY WHATSOEVER ABOUT THIS PROGRAM.
IF IT IS USEFUL TO YOU, FINE.
IF IT DESTROYS ALL YOUR DATA, IT IS YOUR PROBLEM, NOT MINE.
IF YOU WANT TO SEND ME MONEY, NO PROBLEM;-)

=head1 AUTHOR

Fabien Coelho C<fabien@coelho.net> 2001.

=head1 INTERNALS

Data are saved in a Berkeley database file.
The DB key is the full filename.
The DB value is a text of stat informations, a possibly-secured
digest for the file content, and whether the file is pruned.
The special empty key '' is used to store reference files or directories.
The special zero key 0 is used to store some options.
The special '-' and '+' values are used to tag excluded or pruned entries.
An auxiliary DB file is used to detect removed files if necessary.
An auxiliary DB file is used when files need be deleted.

=head1 VERSION

$Id: tripwire.pl,v 1.125 2002/02/16 15:20:18 coelho Exp $

=head1 TODO

=over 4

=item *

options: exclude-type? owner interval?

=item *

crypt database?

=back

=head1 FEATURES (BUGS;-)

=over 4

=item *

Does not handle ACL. there are ACLs under solaris.

=item *

On partial check, missing files in the checked area are not detected.
They are detected only with a full check.

=item *

Not yet ok if run non-interactively.

=back

=cut

####################################################################### IMPORTS

use Getopt::Long;
use Pod::Usage;
use File::Find;
use Fcntl qw(O_CREAT O_RDONLY O_RDWR
	     S_ISDIR S_ISREG S_ISLNK S_ISCHR S_ISBLK S_ISSOCK S_ISFIFO);
use DB_File; # Any_DBM ? exists not always implemented. several files used.

############################################################### INITIALISATIONS

$version_id = '$Id: tripwire.pl,v 1.125 2002/02/16 15:20:18 coelho Exp $ ';
$version = (split / /, $version_id)[2];
$temporary_see = "/tmp/tripwire-seen-$$.db";
$temporary_del = "/tmp/tripwire-delete-$$.db";

%TYPE_NAME = ('file'      => \&S_ISREG,
	      'directory' => \&S_ISDIR,
	      'link'      => \&S_ISLNK,
	      'char'      => \&S_ISCHR,
	      'block'     => \&S_ISBLK,
	      'socket'    => \&S_ISSOCK,
	      'fifo'      => \&S_ISFIFO,
	      'unknown'   => 0);

# global options and their defaults
$opt_verbose = 0;
$opt_debug = 0;

$opt_base = '';
$opt_reportbase = '';
$opt_reportprefix = '';
$opt_reportfile = '';

@opt_exclude = ();
@opt_prune = ();
@opt_owner = ();
@opt_not_owner = ();
@opt_type = ();
@opt_device = ();
$opt_local = 0;
$opt_mode = '';

$opt_digest = '';
$opt_security = undef;
$opt_secured_digest = 0;

$opt_force = 0;
$opt_silent = 0;
$opt_default = 'y';

$opt_no_date = 0;
$opt_no_content = 0;
$opt_no_dir_date = 0;
$opt_no_dir_content = 0;

# actions
$opt_version = 0;
$opt_help = 0;

$opt_check = 0;
$opt_create = 0;
$opt_append = 0;
$opt_list = 0;
$opt_auto = 0;
$opt_status = 0;
$opt_update = 0;
$opt_report = 0;

# statistics
$count_new = 0;
$count_removed = 0;
$count_different = 0;
$count_equals = 0;
$count_total = 0;
$count_updates = 0;

# interruptions
$SIG{INT}  = \&close_handler;
$SIG{QUIT} = \&close_handler;
$SIG{TERM} = \&close_handler;
#$SIG{STOP} = \&close_handler; # ???
# should I put others?

############################################################# OPTION MANAGEMENT

# yes, there is indeed a lot of options.
GetOptions("help|h|?"              => sub { $opt_help=1; },
	   "manual|man|m"          => sub { $opt_help=2; },
	   "version"               => \$opt_version,
	   "verbose|v"             => \$opt_verbose,
	   "base|b=s"              => \$opt_base,
           "report-base|rb=s"      => \$opt_reportbase,
           "report-prefix|rp=s"    => \$opt_reportprefix,
           "report-file|rf=s"      => \$opt_reportfile,
	   "debug|g"               => \$opt_debug,
	   "local"                 => \$opt_local,
	   "silent|quiet|no-bip|q" => \$opt_silent,
	   "force|f"               => \$opt_force,
	   "default|def=s"         => \$opt_default,
	   "security|sec|sign|x:s" => \$opt_security,
	   "owner|o=i"             => \@opt_owner,
           "system-owner|so"       => sub { push @opt_owner, (0 .. 100); },
           "not-owner|no=i"        => \@opt_not_owner,
	   "type|t=s"              => \@opt_type,
	   "directory|d"           => sub { push @opt_type, 'directory'; },
	   "file|f"                => sub { push @opt_type, 'file'; },
	   "link|k"                => sub { push @opt_type, 'link'; },
	   "update|u"              => \$opt_update,
	   "digest|dg=s"           => \$opt_digest,
	   "md2"                   => sub { $opt_digest='md2'; },
	   "md5"                   => sub { $opt_digest='md5'; },
	   "sha1"                  => sub { $opt_digest='sha1'; },
	   "secured-digest|sd"     => \$opt_secured_digest,
	   "exclude|e=s"           => \@opt_exclude,
	   "prune|p=s"             => \@opt_prune,
	   "device|dev=s"          => \@opt_device,
           "mode=s"                => \$opt_mode,
           "setuid"                => sub { $opt_mode='7000'; },
	   "no-date|nd"            => \$opt_no_date,
           "no-content|nc"         => \$opt_no_content,
	   "no-dir-date|ndd"       => \$opt_no_dir_date,
	   "no-dir-content|ndc"    => \$opt_no_dir_content,
	   "create|new|nouveau|n"  => \$opt_create,
	   "append|add|ajoute|a"   => \$opt_append,
	   "check|verifie|c"       => \$opt_check,
	   "auto|self"             => \$opt_auto,
	   "list|ls|liste|l"       => \$opt_list,
           "report|r"              => \$opt_report,
	   "status|stat|etat|s"    => \$opt_status)
    or &usage(1, "invalid option ($!)");

#
# check option coherency
#

$opt_verbose=1 if $opt_debug;

# let us allow update as a shortcut for check and update
$opt_check=1 if $opt_update and not ($opt_check or $opt_auto);

&usage(0)
    if $opt_help or $opt_version;

&usage(2, "must specify --{create|check|append|list|report|auto|status}")
    if ($opt_list+$opt_create+$opt_append+$opt_check+
        $opt_report+$opt_auto+$opt_status)!=1;

$opt_check=1 if $opt_report;

&usage(3, "must specify a db file with base")
    unless $opt_base;

&usage(4, 'must specify arguments with create')
    if !@ARGV and $opt_create;

&usage(8, 'exclude option only available with create/append')
    if @opt_exclude and not ($opt_create or $opt_append);

&usage(10, 'owner option only available with create')
    if @opt_owner and not $opt_create;

&usage(11, 'type option only available with create')
    if @opt_type and not $opt_create;

&usage(12, 'local option only available with create')
    if @opt_local and not $opt_create;

&usage(13, 'device option only available with create')
    if @opt_device and not $opt_create;

foreach $d (@opt_device)
{
    &usage(14, "invalid device '$d'")
	unless $d =~ /^\d+\.\d+$/;
}

foreach $t (@opt_type)
{
    &usage(15, "unexpected type value '$t'")
	unless exists $TYPE_NAME{$t};
}

&usage(16, 'digest option only available with create')
    if $opt_digest and not $opt_create;

&usage(18, 'update option only available with check/auto')
    if $opt_update and not ($opt_check or $opt_auto);

&usage(19, 'no-date option only with check/auto')
    if $opt_no_date and not ($opt_check or $opt_auto);

&usage(26, 'no-content option only with check/auto')
    if $opt_no_content and not ($opt_check or $opt_auto);

&usage(20, 'no-dir-content option only with check/auto')
    if $opt_no_dir_content and not ($opt_check or $opt_auto);

&usage(21, 'no-dir-date option only with check/auto')
    if $opt_no_dir_date and not ($opt_check or $opt_auto);

&usage(22, "default value must be 'y' or 'n'")
    if $opt_default !~ /^[yn]$/;

&usage(23, 'no arguments expected with list/status')
    if @ARGV and ($opt_list or $opt_status);

&usage(24, 'secured-digest option only with create')
    if $opt_secured_digest and not $opt_create;

&usage(25, 'prune option only available with create/append')
    if @opt_prune and not ($opt_create or $opt_append);

&usage(27, 'mode option only available with create')
    if $opt_mode and not $opt_create;

&usage(28, 'mode option syntax mask: [0-7]+')
    if $opt_mode and $opt_mode !~ /^[0-7]+$/;

&usage(29, 'not-owner option only available with create')
    if @opt_not_owner and not $opt_create;

&usage(30, 'it does not make sense to use both owner and not-owner')
    if @opt_owner and @opt_not_owner;

&usage(31, 'report-file option requires report action')
    if !$opt_report and $opt_reportfile;

# set default digest
$opt_digest='sha1' unless $opt_digest;

####################################################################### PREPARE

# not portable across db file structures?
# %file_info = () after tie?
unlink $opt_base or &usage(5, "cannot remove $opt_base")
    if $opt_create and -f $opt_base;

# set tie mode depending on action
$tiemode = O_RDONLY if $opt_check or $opt_list or $opt_auto or $opt_status;
$tiemode = O_RDWR if $opt_update or $opt_append;
$tiemode = O_CREAT|O_RDWR if $opt_create;
die 'unexpected action' unless defined $tiemode;

if ($full_check)
{
    tie %seen, DB_File, $temporary_see, O_CREAT|O_RDWR, 0600
	or &usage(6, "cannot tie to tmp see '$temporary_see' ($!)");
}

if ($opt_update)
{
    # cannot delete while enumerating with each()...
    # that was a hard bug to find;-)
    # so files to be deleted are stored here as keys if necessary.

    tie %delete, DB_File, $temporary_del, O_CREAT|O_RDWR, 0600
	or &usage(19, "cannot tie to tmp del '$temporary_del' ($!)");
}

tie %file_info, DB_File, $opt_base, $tiemode, 0600
    or &usage(7, "cannot tie to base '$opt_base' ($!)");

# report base ?
$opt_reportbase = $opt_base . ".report"
    if $opt_report and not $opt_reportbase;

open REPORT, ">>$opt_reportfile"
    or &usage(32, "cannot open '$opt_reportfile' ($!)")
    if $opt_reportfile;

tie %report, DB_File, $opt_reportbase, O_CREAT|O_RDWR, 0600
    or &usage(32, "cannot tie to report base '$opt_reportbase' ($!)")
    if $opt_reportbase;

# save arguments under '' key
# separator is :, which should not be in a file name.
if (@ARGV and ($opt_create or $opt_append))
{
    $toadd = join ':', @ARGV;
    if ($opt_create)
    {
	$file_info{''} = $toadd;
    }
    else # must be appended
    {
	$file_info{''} .= ":" . $toadd;
    }
}

print "initial files: ", $file_info{''}, "\n"
    if $opt_status or $opt_list or ($opt_debug and exists $file_info{''});

# EXCLUDE option
if (@opt_exclude and ($opt_create or $opt_append))
{
    foreach $exclude (@opt_exclude)
    {
	foreach $ex (split /:/, $exclude)
	{
	    print STDERR "excluding $ex\n" if $opt_debug;
	    $file_info{$ex} = '-';
	}
    }
}

# PRUNE option
if (@opt_prune and ($opt_create or $opt_append))
{
    foreach $prune (@opt_prune)
    {
	foreach $pr (split /:/, $prune)
	{
	    print STDERR "pruning $pr\n" if $opt_debug;
	    if (exists $file_info{$pr})
	    {
		$file_info{$pr} =~ s/:0$/:1/;
	    }
	    else
	    {
		$file_info{$pr} = '+'; # just tag.
	    }
	}
    }
}

# use reference to check if necessary
$full_check = ($opt_check and not @ARGV);

if ($full_check)
{
    @ARGV = split /:/, $file_info{''};
}

# LOCAL option
if ($opt_local)
{
    %devseen = ();
    foreach $f (@ARGV)
    {
	($dev) = lstat($f);
	if (defined($dev) and not exists $devseen{$dev})
	{
	    print STDERR "adding device $dev for file $f\n" if $opt_debug;
	    push @opt_device, ((($dev>>8)&0xff) . "." . ($dev&0xff));
	    $devseen{$dev} = 1;
	}
    }
}

# store/retrieve MODE/OWNER/NOT-OWNER/TYPE/DEVICE/DIGEST option status
if ($opt_create)
{
    $file_info{0} =
	$opt_mode . ":" .
	join(',', @opt_owner) . ":" .
	join(',', @opt_type) . ":" .
	join(',', @opt_device) . ":" .
	$version . ":" .
	$opt_digest . ":" .
	$opt_secured_digest . ":" .
	join(',', @opt_not_owner);
}
else
{
    ($opt_mode,$owners,$types,$devices,$the_version,
     $opt_digest,$opt_secured_digest,$notowners) = split /:/, $file_info{0};
    @opt_device = split /,/, $devices;
    @opt_owner = split /,/, $owners;
    @opt_type = split /,/, $types;
    @opt_not_owner = split /,/, $notowners;
    if ($opt_status or $opt_debug or $opt_list)
    {
	print "base created by version $the_version of $0\n" .
	      " - digest option: $opt_digest\n" .
	      " - secured digest option: $opt_secured_digest\n" .
	      " - mode option: $opt_mode\n" .
	      " - owner option: $owners\n" .
	      " - not owner option: $notowners\n" .
	      " - type option: $types\n" .
	      " - device option: $devices\n";
    }
}

# DIGEST option
if ($opt_digest eq 'md5')
{
    use Digest::MD5;
    $DIGEST = new Digest::MD5;
}
elsif ($opt_digest eq 'sha1')
{
    use Digest::SHA1;
    $DIGEST = new Digest::SHA1;
}
elsif ($opt_digest eq 'md2')
{
    use Digest::MD2;
    $DIGEST = new Digest::MD2;
}
else
{
    &usage(17, "unexpected digest value '$opt_digest'");
}

# OWNER option
map { $owners{$_} = 1; } @opt_owner;

# NOT-OWNER option
map { $notowners{$_} = 1; } @opt_not_owner;

# TYPE option
map { $types{$_} = 1; } @opt_type;

# DEVICE option
foreach $d (@opt_device)
{
    die "invalid device" unless $d =~ /^(\d+)\.(\d+)$/;
    $dev = ($1<<8) + $2;
    $devices{$dev} = 1;
}

# MODE option
$mode_mask = oct $opt_mode if $opt_mode;

# SECURITY option - verification
$opt_security='' if $opt_secured_digest and not defined $opt_security;
$security_todo = 0;

if (defined($opt_security))
{
    # get passphrase if necessary.
    if (not $opt_security)
    {
	if (exists $ENV{TRIPWIRE_PASSPHRASE})
	{
	    print "passphrase taken from environment\n";
	    $opt_security = $ENV{TRIPWIRE_PASSPHRASE};
	}
	else
	{
	    print "please enter passphrase: ";
	    $opt_security = <STDIN>;
	    chomp($opt_security);
	}
    }

    # verify signature if appropriate.
    if ($opt_append or $opt_check or $opt_auto or $opt_list or $opt_status)
    {
	$digest1 = &digestofbase($opt_base, $opt_security);

	if (open FILE, "$opt_base.sig")
	{
	    $digest2 = <FILE>;
	    close FILE;
	    chomp($digest2);

	    if ($digest1 ne $digest2)
	    {
		die "invalid signature"
		    unless &ask("signature does not match! continue", '');
	    }
	}
	else
	{
	    print STDERR "cannot open '$opt_base.sig' ($!)";
	    die "cannot open signature file '$opt_base.sig'"
		unless &ask("no signature file! continue", '');
	    $security_todo = 1;
	}
    }
}

#################################################################### DO THE JOB

if ($opt_update and $opt_reportbase)
{
    &updatefromreport;
}
elsif ($opt_check or $opt_create or $opt_append)
{
    foreach $arg (reverse sort @ARGV)
    {
	# recursive search of files!
	print STDERR "recurring from '$arg'\n" if $opt_debug;

	# although find can handle multiple arguments,
	# it is not clever enough not to visit some directories
	# twice when given several starting points.
	# thus the reverse sort and the visited hashtable.
	find({ wanted => \&filter, no_chdir => 1 }, $arg);
	$visited{$arg} = 1;
    }

    $message = 'removed file';
}
elsif ($opt_auto)
{
    if (@ARGV)
    {
	map { &analyse($_); } @ARGV;
    }
    else
    {
	while (($f,$i) = each %file_info)
	{
	    &analyse($f) if $f;
	}
    }
}
elsif ($opt_list or $opt_status)
{
    $message = 'entry';
}
else
{
    die 'no action required'; # should not get there
}

# detect removed files or list if appropriate.
if ($opt_list or $full_check)
{
    while (($f,$i) = each %file_info)
    {
	if ($f and ($i ne '-') and ($opt_list or not exists $seen{$f}))
	{
	    $count_total++;

	    &report($f, 2, $message, '-', $i);
	    #&printinfo($message, $f, $i);

	    if ($opt_update and &ask('remove from base'))
	    {
		$count_updates++;
		#cannot touch dbfile while enumerating
		$delete{$f} = 1;
	    }
	}
	elsif ($opt_verbose and $f and ($i eq '-'))
	{
	    print "file excluded '$f'\n";
	}
    }
}

# actually remove files if appropriate
if ($opt_update and !$opt_reportbase)
{
    print STDERR 'deleting files' if $opt_debug;
    while (($f) = each %delete)
    {
	delete $file_info{$f};
    }
}

print "number of files considered: $count_total\n";
if ($opt_create or $opt_append or $opt_check or $opt_auto)
{
    if ($opt_check or $opt_auto)
    {
	print "\tdifferent : $count_different\n";
	print "\tequals    : $count_equals\n";
    }
    print "\tnew       : $count_new\n"
	if $count_new or $opt_create+$opt_append+$opt_update;
    if ($opt_update)
    {
	print "\tremoved   : $count_removed\n";
	print "\tupdated   : $count_updates\n";
    }
}

######################################################################### CLEAN
# these are not checked.

# untie open databases
&close_handler;

# create/update signature file for later integrity check
if ($opt_security and
    ($security_todo or $opt_create or $opt_append or
     ($opt_update and $count_updates)))
{
    print "generating signature file '$opt_base.sig'\n";
    $digest = &digestofbase($opt_base, $opt_security);
    open FILE, ">$opt_base.sig"
	or die "cannot open '$opt_base.sig' ($!)";
    print FILE $digest, "\n";
    close FILE;
}

################################################################### SUBROUTINES

# help function. never returns.
# &usage($status, $message);
sub usage()
{
    my ($status, $msg) = @_;

    if ($opt_version)
    {
	print "$0 version $version\ntry option --help for help\n";
	exit $status;
    }

    pod2usage(-msg => $msg,
	      -exitval => $status,
	      -verbose => $opt_help);
}

# untie and maybe remove open databases
# may be called in the end of by an interruption
sub close_handler
{
    my ($sig) = @_;
    print "signal $sig - " if defined($sig);
    print "closing databases and files\n";

    # UNTIE main DB
    untie %file_info;

    # UNTIE and remove seen DB
    if ($full_check and $temporary_see)
    {
	untie %seen;
	unlink $temporary_see;
	$temporary_see = 0;
    }

    # UNTIE and remove delete DB
    if ($opt_update and $temporary_del)
    {
	untie %delete;
	unlink $temporary_del;
	$temporary_del = 0;
    }

    if ($opt_reportbase)
    {
	untie %report;
	$opt_reportbase = '';
    }

    if ($opt_reportfile)
    {
	close REPORT;
	$opt_reportfile = '';
    }

    exit 1 if defined $sig;
}

# compute a secured digest for the base content.
# $digest = &digestofbase($basename,$password)
sub digestofbase
{
    my ($basename, $password) = @_;

    # compute base digest (must be a file)
    $DIGEST->reset();
    open BASE, $basename
	or die "cannot open '$basename' ($!)";
    $DIGEST->addfile(*BASE);
    my $basedig = $DIGEST->hexdigest();

    # password signature
    $DIGEST->reset();
    $DIGEST->add("$basedig:je calcule de resume de la base:" .
		 "$password:$opt_digest:$opt_mode:That's all folks!");

    return $DIGEST->hexdigest();
}

# compute the digest of a file the mode of which is mode
# $digest = &digestoffile($file_name, $mode);
sub digestoffile
{
    my ($filename,$mode) = @_;
    my ($dig, $link, $entry);
    $DIGEST->reset();
    if (S_ISLNK($mode) and ($link = readlink($filename)))
    {
	# if it is a link, we want the content of the link
	$DIGEST->add($link);
	$dig = $DIGEST->hexdigest();
    }
    elsif (S_ISDIR($mode) and opendir(DIR, $filename))
    {
	while ($entry = readdir DIR)
	{
	    $DIGEST->add($entry);
	    $DIGEST->add(':');
	}
	closedir DIR;
	$dig = $DIGEST->hexdigest();
    }
    elsif (S_ISREG($mode) and open(FILE, $filename))
    {
	binmode(FILE);
	$DIGEST->addfile(*FILE);
	$dig = $DIGEST->hexdigest();
    }
    else # error, FIFO CHR BLK SOCKET...
    {
	return "no digest for entry '$filename' mode=$mode ($!)";
    }

    if ($opt_secured_digest)
    {
	print STDERR
	    "secured digest of $filename ($mode) with '$opt_security'\n"
		if $opt_debug;

	$DIGEST->reset();
	$DIGEST->add($dig);
	$DIGEST->add(":option resume securise de '$filename':");
	$DIGEST->add("'$opt_security' est la phrase de passe utilisee...");
	$dig = $DIGEST->hexdigest();
    }

    return $dig
}

# forward to analyse subroutine for find()
sub filter
{
    $File::Find::prune = &analyse($File::Find::name);
}

# make a more comprehensive string out of kept informations
# $str = &stringinfo($info);
sub stringinfo
{
    my ($info) = @_;
    return '-' if $info eq '-';
    my ($mode, $uid, $gid, $size, $mtime, $ctime, $dig, $prune)
	= split /:/, $info;
    return "mode=" . sprintf("%04o", $mode & 07777) .
	" type='" . typename($mode) . "'" .
	    " uid=$uid gid=$gid size=$size prune=$prune\n" .
		"\tmtime=" . scalar localtime($mtime) .
		    "\n\tctime=" . scalar localtime($ctime) .
			"\n\tdigest=$dig";
}

# return type name depending on mode. not quite fast.
# $str = &typename($mode);
sub typename
{
    my ($mode) = @_;
    my ($name,$check);
    foreach $name (sort keys %TYPE_NAME)
    {
	return $name
	    if $TYPE_NAME{$name} and &{$TYPE_NAME{$name}}($mode);
    }
    return 'unknown';
}

# print information on a file
# &printinfo($message, $file_name, $informations);
sub printinfo
{
    my ($msg, $filename, $info) = @_;
    print "$msg '$filename':\n";
    print "\t", &stringinfo($info), "\n"
	if $opt_verbose and $info;
}

# &dropfields($string, @listoffields);
sub dropfields
{
    map { $_[0] =~ s/^(([^:]*:){$_})[^:]*/$1/e; } @_[1..@_-1];
}

# compare two strings while handling some options.
# string format is: mode:uid:gid:size:mtime:ctime:digest:prune
# if (&equals($info1,$info2)) { ... }
sub equals
{
    my ($info1, $info2) = @_;
    if ($opt_no_date)
    {
	# drop time data
	&dropfields($info1, 4, 5);
	&dropfields($info2, 4, 5);
    }
    if ($opt_no_content)
    {
	# drop size and digest
	&dropfields($info1, 3, 6);
	&dropfields($info2, 3, 6);
    }
    if ($opt_no_dir_content or $opt_no_dir_date)
    {
	if ($info1 =~ /^([^:]*):/ and S_ISDIR($1) and
	    $info2 =~ /^([^:]*):/ and S_ISDIR($1))
	{
	    # it is a directory

	    if ($opt_no_dir_date)
	    {
		# drop time data
		&dropfields($info1, 4, 5);
		&dropfields($info2, 4, 5);
	    }
	    if ($opt_no_dir_content)
	    {
		# drop size and digest data
		&dropfields($info1, 3, 6);
		&dropfields($info2, 3, 6);
	    }
	}
    }
    return $info1 eq $info2;
}

# asks a questions and returns the answer as a truth value.
# if (&ask($question[,$default])) { ... }
sub ask
{
    my ($question, $default) = @_;
    $default = $opt_default unless defined $default;
    if ($opt_force and $default)
    {
	print "\t$question: '$default' done\n";
	return $default eq 'y';
    }
    else
    {
	print "\a" unless $opt_silent;
	print "\t$question? ", $default eq 'y'? "([y]/n): ": "(y/[n]): ";
	my $answer = <STDIN>;
	chomp $answer;
	return $answer ? $answer =~ /^y/i : $default eq 'y';
    }
}

# one-char summary of file operation
sub operationchar
{
    my ($op) = @_;
    return "=" if $op==0;
    return "+" if $op==1;
    return "-" if $op==2;
    return "/" if $op==3;
    return "*" if $op==4;
    die "unexpected operation=$op in $report";
}

# update database based on generated report.
sub updatefromreport
{
    my ($k, $v, $op, $info, $filename);
    while (($k,$v) = each %report)
    {
	if ($k =~ /^$opt_reportprefix:(.*)/)
	{
	    $count_updates++;
	    $filename = $1;
	    ($op,$info) = split /;/, $v;
	    
	    print "handling $filename (op=$op)\n" 
		if $opt_verbose;

	    if ($op==1 or $op==4)
	    {
		# add or update
		$file_info{$filename} = $info;
	    }
	    elsif ($op==2 or $op==3)
	    {
		# remove
		delete $file_info{$filename};
	    }
	    # delete $report{$k}; # can I do that while each'ing ?
	}
    }
}

# report a difference:
# may ask for whether to update the base
# or append it to a report base
# &report($filename, $operation, $msg, $new_info, $old_info)
# operation: (0=equal, 1=new, 2=delete, 3=change, 4=differ)
sub report
{
    my ($filename, $operation, $msg, $new_info, $old_info) = @_;

    if ($opt_reportbase)
    {
	$report{"$opt_reportprefix:$filename"} = 
	    "$operation;$new_info;$old_info";
    }

    if ($opt_reportfile)
    {
	print REPORT operationchar($operation) . 
	    " $opt_reportprefix $filename\n";
    }

    &printinfo($msg, $filename, $new_info);
    &printinfo('  was', $filename, $old_info) if $old_info;
}

# that's were everything is done for a given file.
# $whether_to_prune = &analyse($file_name)
sub analyse
{
    my ($filename) = @_;
    my ($info, $there_is);

    print STDERR "considering '$filename'\n" if $opt_debug;

    die "no such file '$filename'" unless $filename;
    return 1 if exists $visited{$filename};

    if ($full_check)
    {
	return 1 if exists $seen{$filename}; # already visited?!
	$seen{$filename} = 1;
    }

    my $prune = 0;

    if (exists $file_info{$filename})
    {
	# EXCLUDE option
	if ($file_info{$filename} =~ /^\-/)
	{
	    print STDERR "$filename excluded\n" if $opt_debug;
	    return $file_info{$filename} eq '-:1';
	}

	# PRUNE option
	if ($file_info{$filename} eq '+' or $file_info{$filename} =~ /:1$/)
	{
	    $prune = 1;
	}
	else
	{
	    $prune = 0;
	}
    }

    my $return = 0;
    my $nope = 0; # nothing to do
    my @stat = lstat $filename;
    my $there_was = (exists $file_info{$filename} and
		     $file_info{$filename} ne '+');

    # MODE option
    $return=0, $nope=1
	if $opt_mode and (not @stat or not $stat[2]&$mode_mask);

    # OWNER option
    $return=0, $nope=1
	if @opt_owner and (not @stat or not exists $owners{$stat[4]});

    # NOT-OWNER option
    $return=0, $nope=1
	if @opt_not_owner and (not @stat or exists $notowners{$stat[4]});

    # TYPE option
    $return=0, $nope=1
	if @opt_type and (not @stat or not exists $types{&typename($stat[2])});

    # DEVICE option (prune on device changes!)
    $return=1, $nope=1
	if @opt_device and (not @stat or not exists $devices{$stat[0]});

    my $current = exists $file_info{$filename}? $file_info{$filename}: '';

    print STDERR
	"nope=$nope prune=$prune return=$return there_was=$there_was\n" .
	    "\t$current\n"
		if $opt_debug;

    if ($nope and not $there_was)
    {
	return $prune? 1: $return;
    }

    if (@stat)
    {
	# maybe I could store binary data instead of a string?
	# well, a string is fine for debugging.
	# extract: mode, uid, gid, size, mtime, ctime
	$info = join(':', @stat[2,4,5,7,9,10]) .
	    ':' . digestoffile($filename,$stat[2]) . ":$prune";
	$there_is = 1;
    }
    else
    {
	# there is no file
	$info = "0::::0:0:no such file ($filename):$prune";
	$there_is = 0;
    }


    if ($opt_debug)
    {
	printinfo('analyse-lstat', $filename, $info)
	    if $there_is;
	printinfo('analyse-base', $filename, $file_info{$filename})
	    if $there_was;
    }

    $count_total++;

    if ($there_is and ($opt_create or $opt_append))
    {
	$count_new++;

	&printinfo('adding', $filename, $info) if $opt_verbose;

	$file_info{$filename} = $info;
    }
    elsif ($opt_check or $opt_auto)
    {
	if ($nope and $there_was)
	{
	    $count_different++;

	    &report($filename, 3, 'changed file', 
		    $info, $file_info{$filename});

	    if ($opt_update and &ask('remove from base'))
	    {
		$count_updates++;
		if ($opt_auto)
		{
		    $delete{$filename} = 1;
		}
		else
		{
		    delete $file_info{$filename};
		}
	    }
	}
	elsif ($there_is and not $there_was)
	{
	    $count_new++;

	    &report($filename, 1, 'new file', $info, '-');
	    #&printinfo('new file', $filename, $info);

	    if ($opt_update and &ask('add to base'))
	    {
		$count_updates++;
		$file_info{$filename} = $info;
	    }
	}
	elsif ($there_is and $there_was and
	       not &equals($info, $file_info{$filename}))
	{
	    $count_different++;

	    &report($filename, 4, 'differing entry ', 
		    $info, $file_info{$filename});

	    if ($opt_update and &ask("replace in base"))
	    {
		$count_updates++;
		$file_info{$filename} = $info;
	    }
	}
	elsif ($there_was and not $there_is)
	{
	    $count_removed++;

	    &report($filename, 2, 'removed file', 
		    '-', $file_info{$filename});

	    if ($opt_update && &ask("remove from base"))
	    {
		$count_updates++;
		if ($opt_auto)
		{
		    $delete{$filename} = 1;
		}
		else
		{
		    delete $file_info{$filename};
		}
	    }
	}
	else
	{
	    # else it is not different
	    $count_equals++;
	}
    }
    else
    {
	die "should not get there";
    }

    return $prune? 1: $return;
}
